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, - std::unique_ptr> _secondaryWindows; std::vector> _windowStack; Window::Controller *_lastActiveWindow = nullptr; Window::Controller *_lastActivePrimaryWindow = nullptr; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 1c24dc510c10c..eadb16181e122 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -20,11 +20,14 @@ For license and copyright information please follow this link: #include "history/history.h" #include "history/view/history_view_element.h" #include "history/history_item.h" +#include "inline_bots/bot_attach_web_view.h" +#include "data/data_game.h" #include "data/data_user.h" #include "data/data_session.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" +#include "styles/style_calls.h" // groupCallBoxLabel #include "styles/style_layers.h" namespace { @@ -121,7 +124,10 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { } else { const auto parsedUrl = QUrl::fromUserInput(url); if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) { - Core::App().hideMediaView(); + const auto my = context.value(); + if (!my.show) { + Core::App().hideMediaView(); + } const auto displayed = parsedUrl.isValid() ? parsedUrl.toDisplayString() : url; @@ -130,7 +136,6 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { : parsedUrl.isValid() ? QString::fromUtf8(parsedUrl.toEncoded()) : ShowEncoded(displayed); - const auto my = context.value(); const auto controller = my.sessionWindow.get(); const auto use = controller ? &controller->window() @@ -140,8 +145,11 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { .text = (tr::lng_open_this_link(tr::now)), .confirmed = [=](Fn hide) { hide(); open(); }, .confirmText = tr::lng_open_link(), + .labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr, }); - const auto &st = st::boxLabel; + const auto &st = my.dark + ? st::groupCallBoxLabel + : st::boxLabel; box->addSkip(st.style.lineHeight - st::boxPadding.bottom()); const auto url = box->addRow( object_ptr(box, displayUrl, st)); @@ -165,23 +173,40 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const { if (Core::InternalPassportLink(url)) { return; } - - const auto open = [=] { + const auto openLink = [=] { UrlClickHandler::Open(url, context.other); }; - if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { - open(); - } else if (!_bot - || _bot->isVerified() + const auto my = context.other.value(); + const auto weakController = my.sessionWindow; + const auto controller = weakController.get(); + const auto item = controller + ? controller->session().data().message(my.itemId) + : nullptr; + const auto media = item ? item->media() : nullptr; + const auto game = media ? media->game() : nullptr; + if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || !_bot || !game) { + openLink(); + } + const auto bot = _bot; + const auto title = game->title; + const auto itemId = my.itemId; + const auto openGame = [=] { + bot->session().attachWebView().showGame({ + .bot = bot, + .context = itemId, + .url = url, + .title = title, + }); + }; + if (_bot->isVerified() || _bot->session().local().isBotTrustedOpenGame(_bot->id)) { - open(); + openGame(); } else { - const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { const auto callback = [=, bot = _bot](Fn close) { close(); bot->session().local().markBotTrustedOpenGame(bot->id); - open(); + openGame(); }; controller->show(Ui::MakeConfirmBox({ .text = tr::lng_allow_bot_pass( @@ -190,6 +215,7 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const { _bot->name()), .confirmed = callback, .confirmText = tr::lng_allow_bot(), + .labelStyle = my.dark ? &st::groupCallBoxLabel : nullptr, })); } } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 20879a0abe6a9..a6359419551a3 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -47,6 +47,7 @@ struct ClickHandlerContext { bool skipBotAutoLogin = false; bool botStartAutoSubmit = false; bool ignoreIv = false; + bool dark = false; // Is filled from peer info. PeerData *peer = nullptr; }; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index daaff5acf7c07..efd0ba4d5a36f 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -220,7 +220,7 @@ QByteArray Settings::serialize() const { + Serialize::bytearraySize(ivPosition) + Serialize::stringSize(noWarningExtensions) + Serialize::stringSize(_customFontFamily) - + sizeof(qint32); + + sizeof(qint32) * 2; auto result = QByteArray(); result.reserve(size); @@ -371,7 +371,8 @@ QByteArray Settings::serialize() const { << qint32(std::clamp( qRound(_dialogsNoChatWidthRatio.current() * 1000000), 0, - 1000000)); + 1000000)) + << qint32(_systemUnlockEnabled ? 1 : 0); } Ensures(result.size() == size); @@ -491,6 +492,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0; QByteArray ivPosition; QString customFontFamily = _customFontFamily; + qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -788,6 +790,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { 0., 1.); } + if (!stream.atEnd()) { + stream >> systemUnlockEnabled; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -995,6 +1000,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _ivPosition = Deserialize(ivPosition); } _customFontFamily = customFontFamily; + _systemUnlockEnabled = (systemUnlockEnabled == 1); } QString Settings::getSoundPath(const QString &key) const { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 2588923c69d9b..3412c4588a9bc 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -884,6 +884,13 @@ class Settings final { _customFontFamily = value; } + [[nodiscard]] bool systemUnlockEnabled() const { + return _systemUnlockEnabled; + } + void setSystemUnlockEnabled(bool enabled) { + _systemUnlockEnabled = enabled; + } + [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); @@ -1014,6 +1021,7 @@ class Settings final { rpl::variable _ttlVoiceClickTooltipHidden = false; WindowPosition _ivPosition; QString _customFontFamily; + bool _systemUnlockEnabled = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 03c3254ab076d..9f4860da364d6 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -327,21 +327,6 @@ bool ConfirmPhone( return true; } -bool ShareGameScore( - Window::SessionController *controller, - const Match &match, - const QVariant &context) { - if (!controller) { - return false; - } - const auto params = url_parse_params( - match->captured(1), - qthelp::UrlParamNameTransform::ToLower); - ShareGameScoreByHash(controller, params.value(u"hash"_q)); - controller->window().activate(); - return true; -} - bool ApplySocksProxy( Window::SessionController *controller, const Match &match, @@ -1230,10 +1215,6 @@ const std::vector &LocalUrlHandlers() { u"^confirmphone/?\\?(.+)(#|$)"_q, ConfirmPhone }, - { - u"^share_game_score/?\\?(.+)(#|$)"_q, - ShareGameScore - }, { u"^socks/?\\?(.+)(#|$)"_q, ApplySocksProxy diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp index cc1c14c845d3c..5e15fd4e3df0e 100644 --- a/Telegram/SourceFiles/core/mime_type.cpp +++ b/Telegram/SourceFiles/core/mime_type.cpp @@ -285,7 +285,7 @@ pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 psd1 psm1 pssc pst py py3 pyc \ pyd pyi pyo pyw pyzw pyz rb reg rgs scf scr sct search-ms settingcontent-ms \ sh shb shs slk sys swf t tmp u3p url vb vbe vbp vbs vbscript vdx vsmacros \ vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx vtx website wlua ws wsc \ -wsf wsh xbap xll xlsm xnk xs"_q +wsf wsh xbap xll xlsb xlsm xnk xs"_q #elif defined Q_OS_MAC // Q_OS_MAC u"\ applescript action app bin command csh osx workflow terminal url caction \ diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index 4110a73a764f8..41888829f71da 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -620,7 +620,7 @@ void Sandbox::processPostponedCalls(int level) { bool Sandbox::nativeEventFilter( const QByteArray &eventType, void *message, - base::NativeEventResult *result) { + native_event_filter_result *result) { registerEnterFromEventLoop(); return false; } diff --git a/Telegram/SourceFiles/core/sandbox.h b/Telegram/SourceFiles/core/sandbox.h index dfb1fe4a637d8..5dd5cea2bc2cb 100644 --- a/Telegram/SourceFiles/core/sandbox.h +++ b/Telegram/SourceFiles/core/sandbox.h @@ -8,7 +8,6 @@ For license and copyright information please follow this link: #pragma once #include "mtproto/mtproto_proxy_data.h" -#include "base/qt/qt_common_adapters.h" #include #include @@ -87,7 +86,7 @@ class Sandbox final bool nativeEventFilter( const QByteArray &eventType, void *message, - base::NativeEventResult *result) override; + native_event_filter_result *result) override; void processPostponedCalls(int level); void singleInstanceChecked(); void launchApplication(); diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index f3de162ce0270..1bc5a1f852305 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -271,7 +271,7 @@ bool UiIntegration::copyPreOnClick(const QVariant &context) { } std::unique_ptr UiIntegration::createCustomEmoji( - const QString &data, + QStringView data, const std::any &context) { const auto my = std::any_cast(&context); if (!my || !my->session) { diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index 36ee45777c267..a745f0fce85b6 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -58,7 +58,7 @@ class UiIntegration final : public Ui::Integration { const Ui::Emoji::One *defaultEmojiVariant( const Ui::Emoji::One *emoji) override; std::unique_ptr createCustomEmoji( - const QString &data, + QStringView data, const std::any &context) override; Fn createSpoilerRepaint(const std::any &context) override; bool allowClickHandlerActivation( diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 3ef90ef9d5fc2..5d6b3323e6d82 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5001005; -constexpr auto AppVersionStr = "5.1.5 mod"; +constexpr auto AppVersion = 5002003; +constexpr auto AppVersionStr = "5.2.3 mod"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 2ba8346d2adc0..6162024fd9793 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -583,6 +583,10 @@ bool ChannelData::canDeleteStories() const { || (adminRights() & AdminRight::DeleteStories); } +bool ChannelData::canPostPaidMedia() const { + return canPostMessages() && (flags() & Flag::PaidMediaAllowed); +} + bool ChannelData::anyoneCanAddMembers() const { return !(defaultRestrictions() & Restriction::AddParticipants); } @@ -1084,7 +1088,9 @@ void ApplyChannelUpdate( | Flag::ParticipantsHidden | Flag::CanGetStatistics | Flag::ViewAsMessages - | Flag::CanViewRevenue; + | Flag::CanViewRevenue + | Flag::PaidMediaAllowed + | Flag::CanViewCreditsRevenue; channel->setFlags((channel->flags() & ~mask) | (update.is_can_set_username() ? Flag::CanSetUsername : Flag()) | (update.is_can_view_participants() @@ -1101,7 +1107,11 @@ void ApplyChannelUpdate( | (update.is_view_forum_as_messages() ? Flag::ViewAsMessages : Flag()) - | (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag())); + | (update.is_paid_media_allowed() ? Flag::PaidMediaAllowed : Flag()) + | (update.is_can_view_revenue() ? Flag::CanViewRevenue : Flag()) + | (update.is_can_view_stars_revenue() + ? Flag::CanViewCreditsRevenue + : Flag())); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { channel->addFlags(Flag::Megagroup); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 6dbe84329b3dc..81ad7778dd18b 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -66,6 +66,8 @@ enum class ChannelDataFlag : uint64 { ViewAsMessages = (1ULL << 30), SimilarExpanded = (1ULL << 31), CanViewRevenue = (1ULL << 32), + PaidMediaAllowed = (1ULL << 33), + CanViewCreditsRevenue = (1ULL << 34), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -357,6 +359,7 @@ class ChannelData final : public PeerData { [[nodiscard]] bool canPostStories() const; [[nodiscard]] bool canEditStories() const; [[nodiscard]] bool canDeleteStories() const; + [[nodiscard]] bool canPostPaidMedia() const; [[nodiscard]] bool hiddenPreHistory() const; [[nodiscard]] bool canViewMembers() const; [[nodiscard]] bool canViewAdmins() const; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index efac543af99dd..7a6f63a743c29 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -43,6 +43,7 @@ ChatFilter::ChatFilter( FilterId id, const QString &title, const QString &iconEmoji, + std::optional colorIndex, Flags flags, base::flat_set> always, std::vector> pinned, @@ -50,6 +51,7 @@ ChatFilter::ChatFilter( : _id(id) , _title(title) , _iconEmoji(iconEmoji) +, _colorIndex(colorIndex) , _always(std::move(always)) , _pinned(std::move(pinned)) , _never(std::move(never)) @@ -95,6 +97,9 @@ ChatFilter ChatFilter::FromTL( data.vid().v, qs(data.vtitle()), qs(data.vemoticon().value_or_empty()), + data.vcolor() + ? std::make_optional(data.vcolor()->v) + : std::nullopt, flags, std::move(list), std::move(pinned), @@ -140,6 +145,9 @@ ChatFilter ChatFilter::FromTL( data.vid().v, qs(data.vtitle()), qs(data.vemoticon().value_or_empty()), + data.vcolor() + ? std::make_optional(data.vcolor()->v) + : std::nullopt, (Flag::Chatlist | (data.is_has_my_invites() ? Flag::HasMyLinks : Flag())), std::move(list), @@ -189,18 +197,20 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { } if (_flags & Flag::Chatlist) { using TLFlag = MTPDdialogFilterChatlist::Flag; - const auto flags = TLFlag::f_emoticon; + const auto flags = TLFlag::f_emoticon + | (_colorIndex ? TLFlag::f_color : TLFlag(0)); return MTP_dialogFilterChatlist( MTP_flags(flags), MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), - MTPint(), // color + MTP_int(_colorIndex.value_or(0)), MTP_vector(pinned), MTP_vector(include)); } using TLFlag = MTPDdialogFilter::Flag; const auto flags = TLFlag::f_emoticon + | (_colorIndex ? TLFlag::f_color : TLFlag(0)) | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) @@ -221,7 +231,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), - MTPint(), // color + MTP_int(_colorIndex.value_or(0)), MTP_vector(pinned), MTP_vector(include), MTP_vector(never)); @@ -239,6 +249,10 @@ QString ChatFilter::iconEmoji() const { return _iconEmoji; } +std::optional ChatFilter::colorIndex() const { + return _colorIndex; +} + ChatFilter::Flags ChatFilter::flags() const { return _flags; } @@ -555,7 +569,7 @@ void ChatFilters::applyInsert(ChatFilter filter, int position) { _list.insert( begin(_list) + position, - ChatFilter(filter.id(), {}, {}, {}, {}, {}, {})); + ChatFilter(filter.id(), {}, {}, {}, {}, {}, {}, {})); applyChange(*(begin(_list) + position), std::move(filter)); } @@ -582,7 +596,7 @@ void ChatFilters::applyRemove(int position) { Expects(position >= 0 && position < _list.size()); const auto i = begin(_list) + position; - applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {})); + applyChange(*i, ChatFilter(i->id(), {}, {}, {}, {}, {}, {}, {})); _list.erase(i); } @@ -711,6 +725,7 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( id, i->title(), i->iconEmoji(), + i->colorIndex(), i->flags(), std::move(always), std::move(pinned), diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 7b5a96476853c..e123ab2c15684 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -52,6 +52,7 @@ class ChatFilter final { FilterId id, const QString &title, const QString &iconEmoji, + std::optional colorIndex, Flags flags, base::flat_set> always, std::vector> pinned, @@ -71,6 +72,7 @@ class ChatFilter final { [[nodiscard]] FilterId id() const; [[nodiscard]] QString title() const; [[nodiscard]] QString iconEmoji() const; + [[nodiscard]] std::optional colorIndex() const; [[nodiscard]] Flags flags() const; [[nodiscard]] bool chatlist() const; [[nodiscard]] bool hasMyLinks() const; @@ -84,6 +86,7 @@ class ChatFilter final { FilterId _id = 0; QString _title; QString _iconEmoji; + std::optional _colorIndex; base::flat_set> _always; std::vector> _pinned; base::flat_set> _never; @@ -94,6 +97,7 @@ class ChatFilter final { inline bool operator==(const ChatFilter &a, const ChatFilter &b) { return (a.title() == b.title()) && (a.iconEmoji() == b.iconEmoji()) + && (a.colorIndex() == b.colorIndex()) && (a.flags() == b.flags()) && (a.always() == b.always()) && (a.never() == b.never()); diff --git a/Telegram/SourceFiles/data/data_cloud_file.cpp b/Telegram/SourceFiles/data/data_cloud_file.cpp index 526dd8904293f..7b3e7382890b2 100644 --- a/Telegram/SourceFiles/data/data_cloud_file.cpp +++ b/Telegram/SourceFiles/data/data_cloud_file.cpp @@ -170,6 +170,12 @@ void UpdateCloudFile( if (data.progressivePartSize && !file.location.valid()) { file.progressivePartSize = data.progressivePartSize; } + if (data.location.width() + && data.location.height() + && !file.location.valid() + && !file.location.width()) { + file.location = data.location; + } return; } diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index ddfd22a83ea61..75da4db5b13f5 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -19,6 +19,16 @@ struct CreditTopupOption final { using CreditTopupOptions = std::vector; +enum class CreditsHistoryMediaType { + Photo, + Video, +}; + +struct CreditsHistoryMedia { + CreditsHistoryMediaType type = CreditsHistoryMediaType::Photo; + uint64 id = 0; +}; + struct CreditsHistoryEntry final { using PhotoId = uint64; enum class PeerType { @@ -28,16 +38,26 @@ struct CreditsHistoryEntry final { Fragment, Unsupported, PremiumBot, + Ads, }; + QString id; QString title; QString description; QDateTime date; PhotoId photoId = 0; + std::vector extended; uint64 credits = 0; - uint64 bareId = 0; + uint64 bareMsgId = 0; + uint64 barePeerId = 0; PeerType peerType; bool refunded = false; + bool pending = false; + bool failed = false; + QDateTime successDate; + QString successLink; + bool in = false; + }; struct CreditsStatusSlice final { diff --git a/Telegram/SourceFiles/data/data_credits_earn.h b/Telegram/SourceFiles/data/data_credits_earn.h new file mode 100644 index 0000000000000..c82d58a744464 --- /dev/null +++ b/Telegram/SourceFiles/data/data_credits_earn.h @@ -0,0 +1,32 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_statistics_chart.h" + +#include + +namespace Data { + +using CreditsEarnInt = uint64; + +struct CreditsEarnStatistics final { + explicit operator bool() const { + return !!usdRate; + } + Data::StatisticalGraph revenueGraph; + CreditsEarnInt currentBalance = 0; + CreditsEarnInt availableBalance = 0; + CreditsEarnInt overallRevenue = 0; + float64 usdRate = 0.; + bool isWithdrawalEnabled = false; + QDateTime nextWithdrawalAt; + QString buyAdsUrl; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 2fa5f05d454f1..29a1899e0b20f 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -531,7 +531,7 @@ void DownloadManager::loadingStopWithConfirmation( return; } const auto window = Core::App().windowFor( - &item->history()->session().account()); + not_null(&item->history()->session().account())); if (!window) { return; } diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index c97d98a1ad730..76838edc37a2d 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -76,6 +76,12 @@ struct FileReferenceAccumulator { }, [](const auto &data) { }); } + void push(const MTPMessageExtendedMedia &data) { + data.match([&](const MTPDmessageExtendedMediaPreview &data) { + }, [&](const MTPDmessageExtendedMedia &data) { + push(data.vmedia()); + }); + } void push(const MTPMessageMedia &data) { data.match([&](const MTPDmessageMediaPhoto &data) { push(data.vphoto()); @@ -85,6 +91,10 @@ struct FileReferenceAccumulator { push(data.vwebpage()); }, [&](const MTPDmessageMediaGame &data) { push(data.vgame()); + }, [&](const MTPDmessageMediaInvoice &data) { + push(data.vextended_media()); + }, [&](const MTPDmessageMediaPaidMedia &data) { + push(data.vextended_media()); }, [](const auto &data) { }); } diff --git a/Telegram/SourceFiles/data/data_groups.cpp b/Telegram/SourceFiles/data/data_groups.cpp index cf75482dbf7ea..15bb0d820e894 100644 --- a/Telegram/SourceFiles/data/data_groups.cpp +++ b/Telegram/SourceFiles/data/data_groups.cpp @@ -81,6 +81,7 @@ void Groups::refreshMessage( bool justRefreshViews) { if (!isGrouped(item)) { unregisterMessage(item); + _data->requestItemViewRefresh(item); return; } if (!item->isRegular() && !item->isScheduled()) { diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index feb8ad16dc037..da16410b0366f 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -7,12 +7,13 @@ For license and copyright information please follow this link: */ #include "data/data_media_types.h" +#include "base/random.h" +#include "boxes/send_credits_box.h" // CreditsEmoji. #include "history/history.h" #include "history/history_item.h" // CreateMedia. #include "history/history_location_manager.h" #include "history/view/history_view_element.h" #include "history/view/history_view_item_preview.h" -#include "history/view/media/history_view_extended_preview.h" #include "history/view/media/history_view_photo.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_gif.h" @@ -23,6 +24,7 @@ For license and copyright information please follow this link: #include "history/view/media/history_view_giveaway.h" #include "history/view/media/history_view_invoice.h" #include "history/view/media/history_view_media_generic.h" +#include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_call.h" #include "history/view/media/history_view_web_page.h" #include "history/view/media/history_view_poll.h" @@ -84,6 +86,13 @@ constexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL); using ItemPreview = HistoryView::ItemPreview; using ItemPreviewImage = HistoryView::ItemPreviewImage; +struct AlbumCounts { + int photos = 0; + int videos = 0; + int audios = 0; + int files = 0; +}; + [[nodiscard]] TextWithEntities WithCaptionNotificationText( const QString &attachType, const TextWithEntities &caption, @@ -112,8 +121,8 @@ using ItemPreviewImage = HistoryView::ItemPreviewImage; ImageRoundRadius radius, bool spoiler) { const auto original = image->original(); - if (original.width() * 10 < original.height() - || original.height() * 10 < original.width()) { + if (original.width() * 20 < original.height() + || original.height() * 20 < original.width()) { return QImage(); } const auto factor = style::DevicePixelRatio(); @@ -165,7 +174,7 @@ template return (reinterpret_cast(data.get()) & ~1) | (spoiler ? 1 : 0); } -[[nodiscard]] ItemPreviewImage PreparePhotoPreview( +[[nodiscard]] ItemPreviewImage PreparePhotoPreviewImage( not_null item, const std::shared_ptr &media, ImageRoundRadius radius, @@ -181,14 +190,15 @@ template } const auto allowedToDownload = media->autoLoadThumbnailAllowed( item->history()->peer); - const auto cacheKey = allowedToDownload ? 0 : counted; + const auto spoilered = uint64(spoiler ? 1 : 0); + const auto cacheKey = allowedToDownload ? spoilered : counted; if (allowedToDownload) { media->owner()->load(PhotoSize::Small, item->fullId()); } if (const auto blurred = media->thumbnailInline()) { return { PreparePreviewImage(blurred, radius, spoiler), cacheKey }; } - return { QImage(), allowedToDownload ? 0 : cacheKey }; + return { QImage(), allowedToDownload ? spoilered : cacheKey }; } [[nodiscard]] ItemPreviewImage PrepareFilePreviewImage( @@ -207,10 +217,11 @@ template }; } document->loadThumbnail(item->fullId()); + const auto spoilered = uint64(spoiler ? 1 : 0); if (const auto blurred = media->thumbnailInline()) { - return { PreparePreviewImage(blurred, radius, spoiler), 0 }; + return { PreparePreviewImage(blurred, radius, spoiler), spoilered }; } - return { QImage(), 0 }; + return { QImage(), spoilered }; } [[nodiscard]] QImage PutPlayIcon(QImage preview) { @@ -225,6 +236,18 @@ template return preview; } +[[nodiscard]] ItemPreviewImage PreparePhotoPreview( + not_null item, + const std::shared_ptr &media, + ImageRoundRadius radius, + bool spoiler) { + auto result = PreparePhotoPreviewImage(item, media, radius, spoiler); + if (media->owner()->extendedMediaVideoDuration().has_value()) { + result.data = PutPlayIcon(std::move(result.data)); + } + return result; +} + [[nodiscard]] ItemPreviewImage PrepareFilePreview( not_null item, const std::shared_ptr &media, @@ -261,48 +284,82 @@ template } bool UpdateExtendedMedia( - Invoice &invoice, + std::unique_ptr &media, not_null item, - const MTPMessageExtendedMedia &media) { - return media.match([&](const MTPDmessageExtendedMediaPreview &data) { - if (invoice.extendedMedia) { - return false; + const MTPMessageExtendedMedia &extended) { + return extended.match([&](const MTPDmessageExtendedMediaPreview &data) { + auto photo = (PhotoData*)nullptr; + if (!media) { + const auto id = base::RandomValue(); + photo = item->history()->owner().photo(id); + } else { + photo = media->photo(); + if (!photo || !photo->extendedMediaPreview()) { + return false; + } } + auto changed = false; - auto &preview = invoice.extendedPreview; + auto size = QSize(); + auto thumbnail = QByteArray(); + auto videoDuration = std::optional(); if (const auto &w = data.vw()) { const auto &h = data.vh(); Assert(h.has_value()); - const auto dimensions = QSize(w->v, h->v); - if (preview.dimensions != dimensions) { - preview.dimensions = dimensions; + size = QSize(w->v, h->v); + if (!changed && photo->size(PhotoSize::Large) != size) { changed = true; } } if (const auto &thumb = data.vthumb()) { if (thumb->type() == mtpc_photoStrippedSize) { - const auto bytes = thumb->c_photoStrippedSize().vbytes().v; - if (preview.inlineThumbnailBytes != bytes) { - preview.inlineThumbnailBytes = bytes; + thumbnail = thumb->c_photoStrippedSize().vbytes().v; + if (!changed && photo->inlineThumbnailBytes() != thumbnail) { changed = true; } } } if (const auto &duration = data.vvideo_duration()) { - if (preview.videoDuration != duration->v) { - preview.videoDuration = duration->v; + videoDuration = duration->v; + if (photo->extendedMediaVideoDuration() != videoDuration) { changed = true; } + } else if (photo->extendedMediaVideoDuration().has_value()) { + changed = true; + } + if (changed) { + photo->setExtendedMediaPreview(size, thumbnail, videoDuration); + } + if (!media) { + media = std::make_unique(item, photo, true); } return changed; }, [&](const MTPDmessageExtendedMedia &data) { - invoice.extendedMedia = HistoryItem::CreateMedia( - item, - data.vmedia()); + media = HistoryItem::CreateMedia(item, data.vmedia()); return true; }); } +bool UpdateExtendedMedia( + Invoice &invoice, + not_null item, + const QVector &media) { + auto changed = false; + const auto count = int(media.size()); + for (auto i = 0; i != count; ++i) { + if (i <= invoice.extendedMedia.size()) { + invoice.extendedMedia.emplace_back(); + changed = true; + } + UpdateExtendedMedia(invoice.extendedMedia[i], item, media[i]); + } + if (count < invoice.extendedMedia.size()) { + invoice.extendedMedia.resize(count); + changed = true; + } + return changed; +} + TextForMimeData WithCaptionClipboardText( const QString &attachType, TextForMimeData &&caption) { @@ -322,6 +379,29 @@ TextForMimeData WithCaptionClipboardText( return result; } +[[nodiscard]] QString ComputeAlbumCountsString(AlbumCounts counts) { + const auto medias = counts.photos + counts.videos; + return (counts.photos && counts.videos) + ? tr::lng_in_dlg_media_count(tr::now, lt_count, medias) + : (counts.photos > 1) + ? tr::lng_in_dlg_photo_count(tr::now, lt_count, counts.photos) + : counts.photos + ? tr::lng_in_dlg_photo(tr::now) + : (counts.videos > 1) + ? tr::lng_in_dlg_video_count(tr::now, lt_count, counts.videos) + : counts.videos + ? tr::lng_in_dlg_video(tr::now) + : (counts.audios > 1) + ? tr::lng_in_dlg_audio_count(tr::now, lt_count, counts.audios) + : counts.audios + ? tr::lng_in_dlg_audio(tr::now) + : (counts.files > 1) + ? tr::lng_in_dlg_file_count(tr::now, lt_count, counts.files) + : counts.files + ? tr::lng_in_dlg_file(tr::now) + : tr::lng_in_dlg_album(tr::now); +} + } // namespace Invoice ComputeInvoiceData( @@ -344,11 +424,23 @@ Invoice ComputeInvoiceData( .isTest = data.is_test(), }; if (const auto &media = data.vextended_media()) { - UpdateExtendedMedia(result, item, *media); + UpdateExtendedMedia(result, item, { *media }); } return result; } +Invoice ComputeInvoiceData( + not_null item, + const MTPDmessageMediaPaidMedia &data) { + auto result = Invoice{ + .amount = data.vstars_amount().v, + .currency = Ui::kCreditsCurrency, + .isPaidMedia = true, + }; + UpdateExtendedMedia(result, item, data.vextended_media().v); + return result; +} + Call ComputeCallData(const MTPDmessageActionPhoneCall &call) { auto result = Call(); result.finishReason = [&] { @@ -424,6 +516,27 @@ GiveawayResults ComputeGiveawayResultsData( return result; } +bool HasExtendedMedia(const Invoice &invoice) { + return !invoice.extendedMedia.empty(); +} + +bool HasUnpaidMedia(const Invoice &invoice) { + for (const auto &media : invoice.extendedMedia) { + const auto photo = media->photo(); + return photo && photo->extendedMediaPreview(); + } + return false; +} + +bool IsFirstVideo(const Invoice &invoice) { + if (invoice.extendedMedia.empty()) { + return false; + } else if (const auto photo = invoice.extendedMedia.front()->photo()) { + return photo->extendedMediaVideoDuration().has_value(); + } + return true; +} + Media::Media(not_null parent) : _parent(parent) { } @@ -586,21 +699,18 @@ ItemPreview Media::toGroupPreview( ToPreviewOptions options) const { auto result = ItemPreview(); auto loadingContext = std::vector(); - auto photoCount = 0; - auto videoCount = 0; - auto audioCount = 0; - auto fileCount = 0; + auto counts = AlbumCounts(); auto manyCaptions = false; for (const auto &item : items) { if (const auto media = item->media()) { if (media->photo()) { - photoCount++; + counts.photos++; } else if (const auto document = media->document()) { (document->isVideoFile() - ? videoCount + ? counts.videos : document->isAudioFile() - ? audioCount - : fileCount)++; + ? counts.audios + : counts.files)++; } auto copy = options; copy.ignoreGroup = true; @@ -630,19 +740,7 @@ ItemPreview Media::toGroupPreview( } } if (manyCaptions || result.text.text.isEmpty()) { - const auto mediaCount = photoCount + videoCount; - auto genericText = (photoCount && videoCount) - ? tr::lng_in_dlg_media_count(tr::now, lt_count, mediaCount) - : photoCount - ? tr::lng_in_dlg_photo_count(tr::now, lt_count, photoCount) - : videoCount - ? tr::lng_in_dlg_video_count(tr::now, lt_count, videoCount) - : audioCount - ? tr::lng_in_dlg_audio_count(tr::now, lt_count, audioCount) - : fileCount - ? tr::lng_in_dlg_file_count(tr::now, lt_count, fileCount) - : tr::lng_in_dlg_album(tr::now); - result.text = Ui::Text::Colorized(genericText); + result.text = Ui::Text::Colorized(ComputeAlbumCountsString(counts)); } if (!loadingContext.empty()) { result.loadingContext = std::move(loadingContext); @@ -1219,6 +1317,92 @@ std::unique_ptr MediaFile::createView( _document); } +SharedContact::VcardItems SharedContact::ParseVcard(const QString &data) { + const auto decode = [&](const QByteArray &input) -> QString { + auto output = QByteArray(); + for (auto i = 0; i < input.size(); ++i) { + if ((input.at(i) == '=') && ((i + 2) < input.size())) { + const auto value = input.mid((++i)++, 2); + auto converted = false; + const auto character = char(value.toUInt(&converted, 16)); + if (converted) { + output.append(character); + } else { + output.append('='); + output.append(value); + } + } else { + output.append(input.at(i)); + } + } + + return QString::fromUtf8(output); + }; + + using Type = SharedContact::VcardItemType; + auto items = SharedContact::VcardItems(); + for (const auto &item : data.split('\n')) { + const auto parts = item.split(':'); + if (parts.size() == 2) { + const auto &type = parts.front(); + const auto attributes = type.split(';', Qt::SkipEmptyParts); + + const auto c = Qt::CaseInsensitive; + auto isQuotedPrintable = false; + for (const auto &attribute : attributes) { + const auto parts = attribute.split('=', Qt::SkipEmptyParts); + if (parts.size() == 2) { + if (parts.front().startsWith("ENCODING", c)) { + isQuotedPrintable = parts[1].startsWith( + "QUOTED-PRINTABLE", + c); + break; + } + } + } + + const auto &value = isQuotedPrintable + ? decode(parts[1].toUtf8()) + : parts[1]; + + if (type.startsWith("TEL")) { + const auto telType = type.contains("PREF") + ? Type::PhoneMain + : type.contains("HOME") + ? Type::PhoneHome + : type.contains("WORK") + ? Type::PhoneWork + : (type.contains("CELL") + || type.contains("MOBILE")) + ? Type::PhoneMobile + : type.contains("OTHER") + ? Type::PhoneOther + : Type::Phone; + items[telType] = value; + } else if (type.startsWith("EMAIL")) { + items[Type::Email] = value; + } else if (type.startsWith("URL")) { + items[Type::Url] = value; + } else if (type.startsWith("NOTE")) { + items[Type::Note] = value; + } else if (type.startsWith("ORG")) { + items[Type::Organization] = base::duplicate(value) + .replace(';', ' ') + .trimmed(); + } else if (type.startsWith("ADR")) { + items[Type::Address] = value; + } else if (type.startsWith("BDAY")) { + items[Type::Birthday] = value; + } else if (type.startsWith("N")) { + items[Type::Name] = base::duplicate(value) + .replace(';', ' ') + .trimmed(); + } + } + } + return items; +} + MediaContact::MediaContact( not_null parent, UserId userId, @@ -1765,14 +1949,15 @@ MediaInvoice::MediaInvoice( .currency = data.currency, .title = data.title, .description = data.description, - .extendedPreview = data.extendedPreview, - .extendedMedia = (data.extendedMedia - ? data.extendedMedia->clone(parent) - : nullptr), .photo = data.photo, + .isPaidMedia = data.isPaidMedia, .isTest = data.isTest, } { - if (_invoice.extendedPreview && !_invoice.extendedMedia) { + _invoice.extendedMedia.reserve(data.extendedMedia.size()); + for (auto &item : data.extendedMedia) { + _invoice.extendedMedia.push_back(item->clone(parent)); + } + if (HasUnpaidMedia(_invoice)) { Ui::PreloadImageSpoiler(); } } @@ -1808,9 +1993,89 @@ bool MediaInvoice::replyPreviewLoaded() const { } TextWithEntities MediaInvoice::notificationText() const { + if (_invoice.isPaidMedia && !_invoice.extendedMedia.empty()) { + return WithCaptionNotificationText( + (IsFirstVideo(_invoice) + ? tr::lng_in_dlg_video + : tr::lng_in_dlg_photo)(tr::now), + parent()->originalText()); + } return { .text = _invoice.title }; } +ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const { + if (!_invoice.isPaidMedia || _invoice.extendedMedia.empty()) { + return Media::toPreview(options); + } + auto counts = AlbumCounts(); + auto images = std::vector(); + auto context = std::vector(); + const auto existing = options.existing; + const auto spoiler = HasUnpaidMedia(_invoice); + for (const auto &media : _invoice.extendedMedia) { + const auto raw = media.get(); + const auto photo = raw->photo(); + const auto document = raw->document(); + if (!photo && !document) { + continue; + } else if (images.size() < kMaxPreviewImages) { + auto found = photo + ? FindCachedPreview(existing, not_null(photo), spoiler) + : FindCachedPreview(existing, not_null(document), spoiler); + const auto radius = ImageRoundRadius::Small; + if (found) { + images.push_back(std::move(found)); + } else if (photo) { + const auto media = photo->createMediaView(); + if (auto prepared = PreparePhotoPreview( + parent(), + media, + radius, + spoiler) + ; prepared || !prepared.cacheKey) { + images.push_back(std::move(prepared)); + if (!prepared.cacheKey) { + context.push_back(media); + } + } + } else if (TryFilePreview(document)) { + const auto media = document->createMediaView(); + if (auto prepared = PrepareFilePreview( + parent(), + media, + radius, + spoiler) + ; prepared || !prepared.cacheKey) { + images.push_back(std::move(prepared)); + if (!prepared.cacheKey) { + context.push_back(media); + } + } + } + } + if (photo && !photo->extendedMediaVideoDuration().has_value()) { + ++counts.photos; + } else { + ++counts.videos; + } + } + const auto type = ComputeAlbumCountsString(counts); + const auto caption = (options.hideCaption || options.ignoreMessageText) + ? TextWithEntities() + : options.translated + ? parent()->translatedText() + : parent()->originalText(); + const auto hasMiniImages = !images.empty(); + auto nice = Ui::Text::Colorized( + Ui::CreditsEmojiSmall(&parent()->history()->session())); + nice.append(WithCaptionNotificationText(type, caption, hasMiniImages)); + return { + .text = std::move(nice), + .images = std::move(images), + .loadingContext = std::move(context), + }; +} + QString MediaInvoice::pinnedTextSubstring() const { return QString::fromUtf8("\xC2\xAB") + _invoice.title @@ -1831,7 +2096,7 @@ bool MediaInvoice::updateSentMedia(const MTPMessageMedia &media) { bool MediaInvoice::updateExtendedMedia( not_null item, - const MTPMessageExtendedMedia &media) { + const QVector &media) { Expects(item == parent()); return UpdateExtendedMedia(_invoice, item, media); @@ -1841,15 +2106,15 @@ std::unique_ptr MediaInvoice::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - if (_invoice.extendedMedia) { - return _invoice.extendedMedia->createView( + if (_invoice.extendedMedia.size() == 1) { + return _invoice.extendedMedia.front()->createView( message, realParent, replacing); - } else if (_invoice.extendedPreview) { - return std::make_unique( + } else if (!_invoice.extendedMedia.empty()) { + return std::make_unique( message, - &_invoice); + _invoice.extendedMedia); } return std::make_unique(message, &_invoice); } @@ -2366,7 +2631,11 @@ const GiveawayResults *MediaGiveawayResults::giveawayResults() const { } TextWithEntities MediaGiveawayResults::notificationText() const { - return Ui::Text::Colorized({ tr::lng_prizes_results_title(tr::now) }); + return Ui::Text::Colorized({ + ((_data.winnersCount == 1) + ? tr::lng_prizes_results_title_one + : tr::lng_prizes_results_title)(tr::now) + }); } QString MediaGiveawayResults::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index d9f0773c02730..424e2c4484bc3 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -70,6 +70,8 @@ struct SharedContact final { }; using VcardItems = base::flat_map; + static VcardItems ParseVcard(const QString &); + VcardItems vcardItems; }; @@ -82,19 +84,6 @@ struct Call { }; -struct ExtendedPreview { - QByteArray inlineThumbnailBytes; - QSize dimensions; - TimeId videoDuration = -1; - - [[nodiscard]] bool empty() const { - return dimensions.isEmpty(); - } - explicit operator bool() const { - return !empty(); - } -}; - class Media; struct Invoice { @@ -103,11 +92,14 @@ struct Invoice { QString currency; QString title; TextWithEntities description; - ExtendedPreview extendedPreview; - std::unique_ptr extendedMedia; + std::vector> extendedMedia; PhotoData *photo = nullptr; + bool isPaidMedia = false; bool isTest = false; }; +[[nodiscard]] bool HasExtendedMedia(const Invoice &invoice); +[[nodiscard]] bool HasUnpaidMedia(const Invoice &invoice); +[[nodiscard]] bool IsFirstVideo(const Invoice &invoice); struct GiveawayStart { std::vector> channels; @@ -205,7 +197,7 @@ class Media { virtual bool updateSentMedia(const MTPMessageMedia &media) = 0; virtual bool updateExtendedMedia( not_null item, - const MTPMessageExtendedMedia &media) { + const QVector &media) { return false; } virtual std::unique_ptr createView( @@ -515,6 +507,7 @@ class MediaInvoice final : public Media { Image *replyPreview() const override; bool replyPreviewLoaded() const override; TextWithEntities notificationText() const override; + ItemPreview toPreview(ToPreviewOptions way) const override; QString pinnedTextSubstring() const override; TextForMimeData clipboardText() const override; @@ -522,7 +515,7 @@ class MediaInvoice final : public Media { bool updateSentMedia(const MTPMessageMedia &media) override; bool updateExtendedMedia( not_null item, - const MTPMessageExtendedMedia &media) override; + const QVector &media) override; std::unique_ptr createView( not_null message, not_null realParent, @@ -748,6 +741,9 @@ class MediaGiveawayResults final : public Media { [[nodiscard]] Invoice ComputeInvoiceData( not_null item, const MTPDmessageMediaInvoice &data); +[[nodiscard]] Invoice ComputeInvoiceData( + not_null item, + const MTPDmessageMediaPaidMedia &data); [[nodiscard]] Call ComputeCallData(const MTPDmessageActionPhoneCall &call); diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 60a1c5f43d15b..e4694f88d63e7 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -65,12 +65,14 @@ std::optional OnlineTextCommon(LastseenStatus status, TimeId now) { return tr::lng_status_online(tr::now); } else if (status.isLongAgo()) { return tr::lng_status_offline(tr::now); - } else if (status.isRecently() || status.isHidden()) { + } else if (status.isRecently()) { return tr::lng_status_recently(tr::now); } else if (status.isWithinWeek()) { return tr::lng_status_last_week(tr::now); } else if (status.isWithinMonth()) { return tr::lng_status_last_month(tr::now); + } else if (status.isHidden()) { + return tr::lng_status_recently(tr::now); } return std::nullopt; } diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index 2530ccc75918d..869bbf7338b58 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -50,6 +50,38 @@ PhotoData::~PhotoData() { base::take(_videoSizes); } +void PhotoData::setFields(TimeId date, bool hasAttachedStickers) { + _dateOrExtendedVideoDuration = date; + _hasStickers = hasAttachedStickers; + _extendedMediaPreview = false; +} + +void PhotoData::setExtendedMediaPreview( + QSize dimensions, + const QByteArray &inlineThumbnailBytes, + std::optional videoDuration) { + _extendedMediaPreview = true; + updateImages( + inlineThumbnailBytes, + {}, + {}, + { .location = { {}, dimensions.width(), dimensions.height() } }, + {}, + {}, + {}); + _dateOrExtendedVideoDuration = videoDuration ? (*videoDuration + 1) : 0; +} + +bool PhotoData::extendedMediaPreview() const { + return _extendedMediaPreview; +} + +std::optional PhotoData::extendedMediaVideoDuration() const { + return (_extendedMediaPreview && _dateOrExtendedVideoDuration) + ? TimeId(_dateOrExtendedVideoDuration - 1) + : std::optional(); +} + Data::Session &PhotoData::owner() const { return *_owner; } @@ -74,6 +106,10 @@ void PhotoData::load( load(PhotoSize::Large, origin, fromCloud, autoLoading); } +TimeId PhotoData::date() const { + return _extendedMediaPreview ? 0 : _dateOrExtendedVideoDuration; +} + bool PhotoData::loading() const { return loading(PhotoSize::Large); } diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index 33d990723e002..3cd749a71512b 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -53,6 +53,7 @@ class PhotoData final { void automaticLoadSettingsChanged(); + [[nodiscard]] TimeId date() const; [[nodiscard]] bool loading() const; [[nodiscard]] bool displayLoading() const; void cancel(); @@ -89,6 +90,14 @@ class PhotoData final { [[nodiscard]] auto activeMediaView() const -> std::shared_ptr; + void setFields(TimeId date, bool hasAttachedStickers); + void setExtendedMediaPreview( + QSize dimensions, + const QByteArray &inlineThumbnailBytes, + std::optional videoDuration); + [[nodiscard]] bool extendedMediaPreview() const; + [[nodiscard]] std::optional extendedMediaVideoDuration() const; + void updateImages( const QByteArray &inlineThumbnailBytes, const ImageWithLocation &small, @@ -148,11 +157,10 @@ class PhotoData final { void setHasAttachedStickers(bool value); // For now they return size of the 'large' image. - int width() const; - int height() const; + [[nodiscard]] int width() const; + [[nodiscard]] int height() const; PhotoId id = 0; - TimeId date = 0; PeerData *peer = nullptr; // for chat and channel photos connection // geo, caption @@ -164,6 +172,8 @@ class PhotoData final { [[nodiscard]] const Data::CloudFile &videoFile( Data::PhotoSize size) const; + TimeId _dateOrExtendedVideoDuration = 0; + struct VideoSizes { Data::CloudFile small; Data::CloudFile large; @@ -177,6 +187,8 @@ class PhotoData final { int32 _dc = 0; uint64 _access = 0; bool _hasStickers = false; + bool _extendedMediaPreview = false; + QByteArray _fileReference; std::unique_ptr _replyPreview; std::weak_ptr _media; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index c11c2ef8aafcc..869476243c560 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3077,8 +3077,7 @@ void Session::photoApplyFields( return; } photo->setRemoteLocation(dc, access, fileReference); - photo->date = date; - photo->setHasAttachedStickers(hasStickers); + photo->setFields(date, hasStickers); photo->updateImages( inlineThumbnailBytes, small, @@ -4683,16 +4682,16 @@ uint64 Session::wallpapersHash() const { return _wallpapersHash; } -MTP::DcId Session::statsDcId(not_null channel) { - const auto it = _channelStatsDcIds.find(channel); - return (it == end(_channelStatsDcIds)) ? MTP::DcId(0) : it->second; +MTP::DcId Session::statsDcId(not_null peer) { + const auto it = _peerStatsDcIds.find(peer); + return (it == end(_peerStatsDcIds)) ? MTP::DcId(0) : it->second; } void Session::applyStatsDcId( - not_null channel, + not_null peer, MTP::DcId dcId) { - if (dcId != channel->session().mainDcId()) { - _channelStatsDcIds[channel] = dcId; + if (dcId != peer->session().mainDcId()) { + _peerStatsDcIds[peer] = dcId; } } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 1178238024241..12c4cba3a658b 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -753,8 +753,8 @@ class Session final { [[nodiscard]] auto peerDecorationsUpdated() const -> rpl::producer>; - void applyStatsDcId(not_null, MTP::DcId); - [[nodiscard]] MTP::DcId statsDcId(not_null); + void applyStatsDcId(not_null, MTP::DcId); + [[nodiscard]] MTP::DcId statsDcId(not_null); void viewTagsChanged( not_null view, @@ -1053,7 +1053,7 @@ class Session final { base::flat_map, TimeId> _watchingForOffline; base::Timer _watchForOfflineTimer; - base::flat_map, MTP::DcId> _channelStatsDcIds; + base::flat_map, MTP::DcId> _peerStatsDcIds; rpl::event_stream _webViewResultSent; diff --git a/Telegram/SourceFiles/data/data_statistics_chart.h b/Telegram/SourceFiles/data/data_statistics_chart.h index fba664d34d24d..11782651e5181 100644 --- a/Telegram/SourceFiles/data/data_statistics_chart.h +++ b/Telegram/SourceFiles/data/data_statistics_chart.h @@ -11,6 +11,12 @@ For license and copyright information please follow this link: namespace Data { +enum class StatisticalCurrency { + None, + Ton, + Credits, +}; + struct StatisticalChart { StatisticalChart() = default; @@ -67,6 +73,8 @@ struct StatisticalChart { bool isFooterHidden = false; bool hasPercentages = false; bool weekFormat = false; + + StatisticalCurrency currency = StatisticalCurrency::None; float64 currencyRate = 0.; // View data. diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index 9db62b958b4ba..38ea28aae6681 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -82,6 +82,7 @@ using UpdateFlag = StoryUpdate::Flag; }); }, [&](const MTPDmediaAreaSuggestedReaction &data) { }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDmediaAreaUrl &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -103,6 +104,7 @@ using UpdateFlag = StoryUpdate::Flag; .dark = data.is_dark(), }); }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDmediaAreaUrl &data) { }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -124,6 +126,27 @@ using UpdateFlag = StoryUpdate::Flag; peerFromChannel(data.vchannel_id()), data.vmsg_id().v), }); + }, [&](const MTPDmediaAreaUrl &data) { + }, [&](const MTPDinputMediaAreaChannelPost &data) { + LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); + }, [&](const MTPDinputMediaAreaVenue &data) { + LOG(("API Error: Unexpected inputMediaAreaVenue from API.")); + }); + return result; +} + +[[nodiscard]] auto ParseUrlArea(const MTPMediaArea &area) +-> std::optional { + auto result = std::optional(); + area.match([&](const MTPDmediaAreaVenue &data) { + }, [&](const MTPDmediaAreaGeoPoint &data) { + }, [&](const MTPDmediaAreaSuggestedReaction &data) { + }, [&](const MTPDmediaAreaChannelPost &data) { + }, [&](const MTPDmediaAreaUrl &data) { + result.emplace(UrlArea{ + .area = ParseArea(data.vcoordinates()), + .url = qs(data.vurl()), + }); }, [&](const MTPDinputMediaAreaChannelPost &data) { LOG(("API Error: Unexpected inputMediaAreaChannelPost from API.")); }, [&](const MTPDinputMediaAreaVenue &data) { @@ -662,6 +685,10 @@ const std::vector &Story::channelPosts() const { return _channelPosts; } +const std::vector &Story::urlAreas() const { + return _urlAreas; +} + void Story::applyChanges( StoryMedia media, const MTPDstoryItem &data, @@ -765,6 +792,7 @@ void Story::applyFields( auto locations = std::vector(); auto suggestedReactions = std::vector(); auto channelPosts = std::vector(); + auto urlAreas = std::vector(); if (const auto areas = data.vmedia_areas()) { for (const auto &area : areas->v) { if (const auto location = ParseLocation(area)) { @@ -778,6 +806,8 @@ void Story::applyFields( suggestedReactions.push_back(*reaction); } else if (auto post = ParseChannelPost(area)) { channelPosts.push_back(*post); + } else if (auto url = ParseUrlArea(area)) { + urlAreas.push_back(*url); } } } @@ -790,6 +820,7 @@ void Story::applyFields( const auto suggestedReactionsChanged = (_suggestedReactions != suggestedReactions); const auto channelPostsChanged = (_channelPosts != channelPosts); + const auto urlAreasChanged = (_urlAreas != urlAreas); const auto reactionChanged = (_sentReactionId != reaction); _out = out; @@ -815,6 +846,9 @@ void Story::applyFields( if (channelPostsChanged) { _channelPosts = std::move(channelPosts); } + if (urlAreasChanged) { + _urlAreas = std::move(urlAreas); + } if (reactionChanged) { _sentReactionId = reaction; } @@ -824,7 +858,8 @@ void Story::applyFields( || captionChanged || mediaChanged || locationsChanged - || channelPostsChanged; + || channelPostsChanged + || urlAreasChanged; const auto reactionsChanged = reactionChanged || suggestedReactionsChanged; if (!initial && (changed || reactionsChanged)) { diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index 512f0ed38e1e7..b318ce9fe885a 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -122,6 +122,15 @@ struct ChannelPost { const ChannelPost &) = default; }; +struct UrlArea { + StoryArea area; + QString url; + + friend inline bool operator==( + const UrlArea &, + const UrlArea &) = default; +}; + class Story final { public: Story( @@ -197,6 +206,8 @@ class Story final { -> const std::vector &; [[nodiscard]] auto channelPosts() const -> const std::vector &; + [[nodiscard]] auto urlAreas() const + -> const std::vector &; void applyChanges( StoryMedia media, @@ -247,6 +258,7 @@ class Story final { std::vector _locations; std::vector _suggestedReactions; std::vector _channelPosts; + std::vector _urlAreas; StoryViews _views; StoryViews _channelReactions; const TimeId _date = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index f4f8faccd653c..4f45efda24694 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -433,7 +433,7 @@ bool UserData::someRequirePremiumToWrite() const { } bool UserData::meRequiresPremiumToWrite() const { - return (flags() & UserDataFlag::MeRequiresPremiumToWrite); + return !isSelf() && (flags() & UserDataFlag::MeRequiresPremiumToWrite); } bool UserData::requirePremiumToWriteKnown() const { diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index dee018efde230..279defe7d4479 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -285,7 +285,7 @@ dialogsFilter: InputField(defaultInputField) { borderRadius: 18px; borderDenominator: 2; - font: normalFont; + style: defaultTextStyle; heightMin: 35px; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 2614a334117e6..6e60f072dfc73 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -512,8 +512,12 @@ int InnerWidget::pinnedOffset() const { return dialogsOffset() + shownHeight(fixedOnTopCount()); } +int InnerWidget::hashtagsOffset() const { + return searchInChatOffset() + searchInChatSkip(); +} + int InnerWidget::filteredOffset() const { - return _hashtagResults.size() * st::mentionHeight; + return hashtagsOffset() + (_hashtagResults.size() * st::mentionHeight); } int InnerWidget::filteredIndex(int y) const { @@ -539,29 +543,23 @@ int InnerWidget::peerSearchOffset() const { + st::searchedBarHeight; } -int InnerWidget::searchTagsOffset() const { - auto result = peerSearchOffset() - st::searchedBarHeight; - if (!_peerSearchResults.empty()) { - result += (_peerSearchResults.size() * st::dialogsRowHeight) - + st::searchedBarHeight; - } - return result; -} - int InnerWidget::searchInChatOffset() const { - return searchTagsOffset() + (_searchTags ? _searchTags->height() : 0); -} - -int InnerWidget::searchedOffset() const { - return searchInChatOffset() - + searchInChatSkip() - + st::searchedBarHeight; + return (_searchTags ? _searchTags->height() : 0); } int InnerWidget::searchInChatSkip() const { return _searchIn ? _searchIn->height() : 0; } +int InnerWidget::searchedOffset() const { + auto result = peerSearchOffset(); + if (!_peerSearchResults.empty()) { + result += (_peerSearchResults.size() * st::dialogsRowHeight) + + st::searchedBarHeight; + } + return result; +} + void InnerWidget::changeOpenedFolder(Data::Folder *folder) { Expects(!folder || !_savedSublists); @@ -796,10 +794,26 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.fillRect(dialogsClip, currentBg()); } } else if (_state == WidgetState::Filtered) { - [[maybe_unused]] auto top = 0; + if (_searchTags) { + paintSearchTags(p, { + .st = &st::forumTopicRow, + .currentBg = currentBg(), + .now = ms, + .width = fullWidth, + .paused = videoPaused, + }); + p.translate(0, _searchTags->height()); + } + if (_searchIn) { + p.translate(0, searchInChatSkip()); + if (_searchResults.empty()) { + p.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg); + } + } if (!_hashtagResults.empty()) { - auto from = floorclamp(r.y(), st::mentionHeight, 0, _hashtagResults.size()); - auto to = ceilclamp(r.y() + r.height(), st::mentionHeight, 0, _hashtagResults.size()); + const auto skip = hashtagsOffset(); + auto from = floorclamp(r.y() - skip, st::mentionHeight, 0, _hashtagResults.size()); + auto to = ceilclamp(r.y() + r.height() - skip, st::mentionHeight, 0, _hashtagResults.size()); p.translate(0, from * st::mentionHeight); if (from < _hashtagResults.size()) { const auto htagleft = st::defaultDialogRow.padding.left(); @@ -841,7 +855,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.drawText(htagleft + firstwidth, st::mentionTop + st::mentionFont->ascent, second); } p.translate(0, st::mentionHeight); - top += st::mentionHeight; + } + if (to < _hashtagResults.size()) { + p.translate(0, (_hashtagResults.size() - to) * st::mentionHeight); } } } @@ -853,7 +869,6 @@ void InnerWidget::paintEvent(QPaintEvent *e) { int(_filterResults.size())); const auto height = filteredHeight(from); p.translate(0, height); - top += height; for (; from < to; ++from) { const auto selected = isPressed() ? (from == _filteredPressed) @@ -861,7 +876,6 @@ void InnerWidget::paintEvent(QPaintEvent *e) { const auto row = _filterResults[from].row; paintRow(row, selected, !activeEntry.fullId); p.translate(0, row->height()); - top += row->height(); } } @@ -871,13 +885,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_search_global_results(tr::now)); p.translate(0, st::searchedBarHeight); - top += st::searchedBarHeight; auto skip = peerSearchOffset(); auto from = floorclamp(r.y() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size()); auto to = ceilclamp(r.y() + r.height() - skip, st::dialogsRowHeight, 0, _peerSearchResults.size()); p.translate(0, from * st::dialogsRowHeight); - top += from * st::dialogsRowHeight; if (from < _peerSearchResults.size()) { const auto activePeer = activeEntry.key.peer(); for (; from < to; ++from) { @@ -900,34 +912,10 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .paused = videoPaused, }); p.translate(0, st::dialogsRowHeight); - top += st::dialogsRowHeight; } - } - } - - if (_searchTags) { - paintSearchTags(p, { - .st = &st::forumTopicRow, - .currentBg = currentBg(), - .now = ms, - .width = fullWidth, - .paused = videoPaused, - }); - p.translate(0, _searchTags->height()); - top += _searchTags->height(); - } - if (_searchIn) { - //paintSearchInChat(p, { - // .st = &st::forumTopicRow, - // .currentBg = currentBg(), - // .now = ms, - // .width = fullWidth, - // .paused = videoPaused, - //}); - p.translate(0, searchInChatSkip()); - top += searchInChatSkip(); - if (_searchResults.empty()) { - p.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg); + if (to < _peerSearchResults.size()) { + p.translate(0, (_peerSearchResults.size() - to) * st::dialogsRowHeight); + } } } @@ -940,7 +928,6 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); p.translate(0, st::searchedBarHeight); - top += st::searchedBarHeight; } } else { const auto text = showUnreadInSearchResults @@ -954,13 +941,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setPen(st::searchedBarFg); p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); p.translate(0, st::searchedBarHeight); - top += st::searchedBarHeight; auto skip = searchedOffset(); auto from = floorclamp(r.y() - skip, _st->height, 0, _searchResults.size()); auto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _searchResults.size()); p.translate(0, from * _st->height); - top += from * _st->height; if (from < _searchResults.size()) { for (; from < to; ++from) { const auto &result = _searchResults[from]; @@ -988,7 +973,6 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .displayUnreadInfo = showUnreadInSearchResults, }); p.translate(0, _st->height); - top += _st->height; } } } @@ -1363,7 +1347,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { const auto tagBase = QPoint( _searchTagsLeft, - searchTagsOffset() + st::dialogsSearchTagBottom / 2); + st::dialogsSearchTagBottom / 2); const auto tagPoint = local - tagBase; const auto inTags = _searchTags && QRect( @@ -1414,7 +1398,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { _hashtagSelected = -1; _hashtagDeleteSelected = false; } else { - auto skip = 0; + auto skip = hashtagsOffset(); auto hashtagSelected = (mouseY >= skip) ? ((mouseY - skip) / st::mentionHeight) : -1; if (hashtagSelected < 0 || hashtagSelected >= _hashtagResults.size()) { hashtagSelected = -1; @@ -1547,8 +1531,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { _dragStart = e->pos(); } else if (base::in_range(_hashtagPressed, 0, _hashtagResults.size()) && !_hashtagDeletePressed) { auto row = &_hashtagResults[_hashtagPressed]->row; - row->addRipple(e->pos(), QSize(width(), st::mentionHeight), [this, index = _hashtagPressed] { - update(0, index * st::mentionHeight, width(), st::mentionHeight); + const auto origin = e->pos() - QPoint(0, hashtagsOffset() + _hashtagPressed * st::mentionHeight); + row->addRipple(origin, QSize(width(), st::mentionHeight), [this, index = _hashtagPressed] { + update(0, hashtagsOffset() + index * st::mentionHeight, width(), st::mentionHeight); }); } else if (base::in_range(_filteredPressed, 0, _filterResults.size())) { const auto &result = _filterResults[_filteredPressed]; @@ -1989,9 +1974,7 @@ void InnerWidget::moveSearchIn() { width(), st::columnMinimalWidthLeft - _narrowWidth); _searchIn->resizeToWidth(searchInWidth); - - const auto top = (_searchTags ? _searchTags->height() : 0); - _searchIn->moveToLeft(0, top); + _searchIn->moveToLeft(0, searchInChatOffset()); } void InnerWidget::dialogRowReplaced( @@ -2318,7 +2301,7 @@ void InnerWidget::updateSelectedRow(Key key) { } } } else if (_hashtagSelected >= 0) { - update(0, _hashtagSelected * st::mentionHeight, width(), st::mentionHeight); + update(0, hashtagsOffset() + _hashtagSelected * st::mentionHeight, width(), st::mentionHeight); } else if (_filteredSelected >= 0) { if (_filteredSelected < _filterResults.size()) { const auto &result = _filterResults[_filteredSelected]; @@ -2605,6 +2588,15 @@ void InnerWidget::dragPinnedFromTouch() { updateReorderPinned(now); } +void InnerWidget::searchRequested(bool loading) { + _searchWaiting = false; + _searchLoading = loading; + if (loading) { + clearSearchResults(true); + } + refresh(true); +} + void InnerWidget::applySearchState(SearchState state) { if (_searchState == state) { return; @@ -2631,7 +2623,7 @@ void InnerWidget::applySearchState(SearchState state) { _searchTags->repaintRequests() | rpl::start_with_next([=] { const auto height = _searchTags->height(); - update(0, searchTagsOffset(), width(), height); + update(0, 0, width(), height); }, _searchTags->lifetime()); _searchTags->menuRequests( @@ -2663,7 +2655,7 @@ void InnerWidget::applySearchState(SearchState state) { onHashtagFilterUpdate(QStringView()); } _searchState = std::move(state); - _searchingHashtag = IsHashtagSearchQuery(_searchState.query); + _searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query); updateSearchIn(); moveSearchIn(); @@ -2717,9 +2709,13 @@ void InnerWidget::applySearchState(SearchState state) { clearMouseSelection(true); } if (_state != WidgetState::Default) { - _searchLoading = true; - _searchMessages.fire({}); - refresh(true); + _searchWaiting = true; + _searchRequests.fire(otherChanged + ? SearchRequestDelay::Instant + : SearchRequestDelay::Delayed); + if (_searchWaiting) { + refresh(true); + } } } @@ -2935,8 +2931,8 @@ rpl::producer InnerWidget::dialogMoved() const { return _dialogMoved.events(); } -rpl::producer<> InnerWidget::searchMessages() const { - return _searchMessages.events(); +rpl::producer InnerWidget::searchRequests() const { + return _searchRequests.events(); } rpl::producer InnerWidget::completeHashtagRequests() const { @@ -3024,6 +3020,7 @@ void InnerWidget::searchReceived( HistoryItem *inject, SearchRequestType type, int fullCount) { + _searchWaiting = false; _searchLoading = false; const auto uniquePeers = uniqueSearchResults(); @@ -3188,7 +3185,7 @@ void InnerWidget::refreshEmpty() { && _searchResults.empty() && _peerSearchResults.empty() && _hashtagResults.empty(); - if (_searchLoading || !empty) { + if (_searchLoading || _searchWaiting || !empty) { if (_searchEmpty) { _searchEmpty->hide(); } @@ -3202,7 +3199,7 @@ void InnerWidget::refreshEmpty() { _searchEmpty->show(); } - if (!_searchLoading || !empty) { + if ((!_searchLoading && !_searchWaiting) || !empty) { _loadingAnimation.destroy(); } else if (!_loadingAnimation) { _loadingAnimation = Ui::CreateLoadingDialogRowWidget( @@ -3335,7 +3332,8 @@ auto InnerWidget::searchTagsChanges() const } void InnerWidget::updateSearchIn() { - if (!_searchState.inChat && !_searchingHashtag) { + if (!_searchState.inChat + && _searchHashOrCashtag == HashOrCashtag::None) { _searchIn = nullptr; return; } else if (!_searchIn) { @@ -3384,7 +3382,7 @@ void InnerWidget::updateSearchIn() { ? Ui::MakeUserpicThumbnail(sublist->peer()) : nullptr; const auto myIcon = Ui::MakeIconThumbnail(st::menuIconChats); - const auto publicIcon = _searchingHashtag + const auto publicIcon = (_searchHashOrCashtag != HashOrCashtag::None) ? Ui::MakeIconThumbnail(st::menuIconChannel) : nullptr; const auto peerTabType = (peer && peer->isBroadcast()) @@ -3678,7 +3676,7 @@ void InnerWidget::preloadRowsData() { } } -bool InnerWidget::chooseCollapsedRow() { +bool InnerWidget::chooseCollapsedRow(Qt::KeyboardModifiers modifiers) { if (_state != WidgetState::Default) { return false; } else if ((_collapsedSelected < 0) @@ -3692,6 +3690,9 @@ bool InnerWidget::chooseCollapsedRow() { } void InnerWidget::switchToFilter(FilterId filterId) { + if (_controller->windowId().type != Window::SeparateType::Primary) { + return; + } const auto &list = session().data().chatsFilters().list(); const auto filterIt = filterId ? ranges::find(list, filterId, &Data::ChatFilter::id) @@ -3771,7 +3772,15 @@ bool InnerWidget::chooseHashtag() { ChosenRow InnerWidget::computeChosenRow() const { if (_state == WidgetState::Default) { - if (_selected) { + if ((_collapsedSelected >= 0) + && (_collapsedSelected < _collapsedRows.size())) { + const auto &row = _collapsedRows[_collapsedSelected]; + Assert(row->folder != nullptr); + return { + .key = row->folder, + .message = Data::UnreadMessagePosition, + }; + } else if (_selected) { return { .key = _selected->key(), .message = Data::UnreadMessagePosition, @@ -3805,7 +3814,9 @@ ChosenRow InnerWidget::computeChosenRow() const { bool InnerWidget::isUserpicPress() const { return (_lastRowLocalMouseX >= 0) - && (_lastRowLocalMouseX < _st->nameLeft); + && (_lastRowLocalMouseX < _st->nameLeft) + && (_collapsedSelected < 0 + || _collapsedSelected >= _collapsedRows.size()); } bool InnerWidget::isUserpicPressOnWide() const { @@ -3815,9 +3826,7 @@ bool InnerWidget::isUserpicPressOnWide() const { bool InnerWidget::chooseRow( Qt::KeyboardModifiers modifiers, MsgId pressedTopicRootId) { - if (chooseCollapsedRow()) { - return true; - } else if (chooseHashtag()) { + if (chooseHashtag()) { return true; } const auto modifyChosenRow = [&]( diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 9d74bf8cb0758..a0b80ef67173e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -62,6 +62,7 @@ class IndexedList; class SearchTags; class SearchEmpty; class ChatSearchIn; +enum class HashOrCashtag : uchar; struct ChosenRow { Key key; @@ -71,7 +72,7 @@ struct ChosenRow { bool newWindow : 1 = false; }; -enum class SearchRequestType { +enum class SearchRequestType : uchar { FromStart, FromOffset, PeerFromStart, @@ -80,6 +81,12 @@ enum class SearchRequestType { MigratedFromOffset, }; +enum class SearchRequestDelay : uchar { + InCache, + Instant, + Delayed, +}; + enum class WidgetState { Default, Filtered, @@ -145,6 +152,7 @@ class InnerWidget final : public Ui::RpWidget { } [[nodiscard]] bool hasFilteredResults() const; + void searchRequested(bool loading); void applySearchState(SearchState state); [[nodiscard]] auto searchTagsChanges() const -> rpl::producer>; @@ -168,7 +176,7 @@ class InnerWidget final : public Ui::RpWidget { [[nodiscard]] rpl::producer scrollByDeltaRequests() const; [[nodiscard]] rpl::producer mustScrollTo() const; [[nodiscard]] rpl::producer dialogMoved() const; - [[nodiscard]] rpl::producer<> searchMessages() const; + [[nodiscard]] rpl::producer searchRequests() const; [[nodiscard]] rpl::producer completeHashtagRequests() const; [[nodiscard]] rpl::producer<> refreshHashtagsRequests() const; @@ -244,7 +252,7 @@ class InnerWidget final : public Ui::RpWidget { void repaintCollapsedFolderRow(not_null folder); void refreshWithCollapsedRows(bool toTop = false); bool needCollapsedRowsRefresh() const; - bool chooseCollapsedRow(); + bool chooseCollapsedRow(Qt::KeyboardModifiers modifiers); void switchToFilter(FilterId filterId); bool chooseHashtag(); ChosenRow computeChosenRow() const; @@ -343,10 +351,10 @@ class InnerWidget final : public Ui::RpWidget { [[nodiscard]] int filteredIndex(int y) const; [[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int peerSearchOffset() const; - [[nodiscard]] int searchTagsOffset() const; [[nodiscard]] int searchInChatOffset() const; [[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchInChatSkip() const; + [[nodiscard]] int hashtagsOffset() const; void paintCollapsedRows( Painter &p, @@ -507,7 +515,7 @@ class InnerWidget final : public Ui::RpWidget { Ui::DraggingScrollManager _draggingScroll; SearchState _searchState; - bool _searchingHashtag = false; + HashOrCashtag _searchHashOrCashtag = {}; History *_searchInMigrated = nullptr; PeerData *_searchFromShown = nullptr; Ui::Text::String _searchFromUserText; @@ -529,7 +537,7 @@ class InnerWidget final : public Ui::RpWidget { rpl::event_stream _mustScrollTo; rpl::event_stream _dialogMoved; - rpl::event_stream<> _searchMessages; + rpl::event_stream _searchRequests; rpl::event_stream _completeHashtagRequests; rpl::event_stream<> _refreshHashtagsRequests; @@ -547,6 +555,7 @@ class InnerWidget final : public Ui::RpWidget { bool _savedSublists = false; bool _searchLoading = false; + bool _searchWaiting = false; base::unique_qptr _menu; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.cpp b/Telegram/SourceFiles/dialogs/dialogs_key.cpp index 544c48cca8e3e..db5df96270d59 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_key.cpp @@ -100,7 +100,9 @@ ChatSearchTab SearchState::defaultTabForMe() const { } bool SearchState::filterChatsList() const { - return !inChat && (tab == ChatSearchTab::MyMessages); + using Tab = ChatSearchTab; + return !inChat // ThisPeer can be in opened forum. + && (tab == Tab::MyMessages || tab == Tab::ThisPeer); } } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index d421b0508baf6..922b673a06756 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -95,6 +95,7 @@ namespace { constexpr auto kSearchPerPage = 50; constexpr auto kStoriesExpandDuration = crl::time(200); +constexpr auto kSearchRequestDelay = crl::time(900); base::options::toggle OptionForumHideChatsList({ .id = kOptionForumHideChatsList, @@ -324,9 +325,9 @@ Widget::Widget( _scroll->scrollToY(st + _inner->st()->height); } }, lifetime()); - _inner->searchMessages( - ) | rpl::start_with_next([=] { - searchRequested(); + _inner->searchRequests( + ) | rpl::start_with_next([=](SearchRequestDelay delay) { + searchRequested(delay); }, lifetime()); _inner->completeHashtagRequests( ) | rpl::start_with_next([=](const QString &tag) { @@ -346,8 +347,11 @@ Widget::Widget( }, lifetime()); _inner->cancelSearchRequests( ) | rpl::start_with_next([=] { - setInnerFocus(true); - applySearchState({}); + cancelSearch({ + .forceFullCancel = true, + .jumpBackToSearchedChat = true, + }); + controller->widget()->setInnerFocus(); }, lifetime()); _inner->cancelSearchFromRequests( ) | rpl::start_with_next([=] { @@ -418,7 +422,9 @@ Widget::Widget( }, lifetime()); } - _cancelSearch->setClickedCallback([this] { cancelSearch(); }); + _cancelSearch->setClickedCallback([this] { + cancelSearch({ .jumpBackToSearchedChat = true }); + }); _jumpToDate->entity()->setClickedCallback([this] { showCalendar(); }); _chooseFromUser->entity()->setClickedCallback([this] { showSearchFrom(); }); rpl::single(rpl::empty) | rpl::then( @@ -556,6 +562,8 @@ void Widget::chosenRow(const ChosenRow &row) { if (topicJump) { if (controller()->shownForum().current() == topicJump->forum()) { controller()->closeForum(); + } else if (row.newWindow) { + controller()->showInNewWindow(Window::SeparateId(topicJump)); } else { if (!controller()->adaptive().isOneColumn()) { controller()->showForum( @@ -569,11 +577,17 @@ void Widget::chosenRow(const ChosenRow &row) { } return; } else if (const auto topic = row.key.topic()) { - session().data().saveViewAsMessages(topic->forum(), false); - controller()->showThread( - topic, - row.message.fullId.msg, - Window::SectionShow::Way::ClearStack); + if (row.newWindow) { + controller()->showInNewWindow( + Window::SeparateId(topic), + row.message.fullId.msg); + } else { + session().data().saveViewAsMessages(topic->forum(), false); + controller()->showThread( + topic, + row.message.fullId.msg, + Window::SectionShow::Way::ClearStack); + } } else if (history && row.userpicClick && (row.message.fullId.msg == ShowAtUnreadMsgId) @@ -589,16 +603,19 @@ void Widget::chosenRow(const ChosenRow &row) { const auto forum = history->peer->forum(); if (controller()->shownForum().current() == forum) { controller()->closeForum(); - return; - } - controller()->showForum( - forum, - Window::SectionShow().withChildColumn()); - if (forum->channel()->viewForumAsMessages()) { - controller()->showThread( - history, - ShowAtUnreadMsgId, - Window::SectionShow::Way::ClearStack); + } else if (row.newWindow) { + controller()->showInNewWindow( + Window::SeparateId(Window::SeparateType::Forum, history)); + } else { + controller()->showForum( + forum, + Window::SectionShow().withChildColumn()); + if (forum->channel()->viewForumAsMessages()) { + controller()->showThread( + history, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } } return; } else if (history) { @@ -624,6 +641,12 @@ void Widget::chosenRow(const ChosenRow &row) { return; } } + if (row.newWindow) { + controller()->showInNewWindow(Window::SeparateId( + Window::SeparateType::Archive, + &session())); + return; + } controller()->openFolder(folder); hideChildList(); } @@ -687,7 +710,7 @@ void Widget::setupMoreChatsBar() { controller()->activeChatsFilter( ) | rpl::start_with_next([=](FilterId id) { storiesToggleExplicitExpand(false); - const auto cancelled = cancelSearch(true); + const auto cancelled = cancelSearch({ .forceFullCancel = true }); const auto guard = gsl::finally([&] { if (cancelled) { controller()->content()->dialogsCancelled(); @@ -890,13 +913,15 @@ void Widget::setupStories() { Core::App().settings().setStoriesClickTooltipHidden(true); Core::App().saveSettingsDelayed(); }; - _stories->setShowTooltip( - parentWidget(), - rpl::combine( - Core::App().settings().storiesClickTooltipHiddenValue(), - shownValue(), - !rpl::mappers::_1 && rpl::mappers::_2), - hideTooltip); + InvokeQueued(_stories.get(), [=] { + _stories->setShowTooltip( + controller()->content(), + rpl::combine( + Core::App().settings().storiesClickTooltipHiddenValue(), + shownValue(), + !rpl::mappers::_1 && rpl::mappers::_2), + hideTooltip); + }); } _storiesContents.fire(Stories::ContentForSession( @@ -1162,7 +1187,7 @@ bool Widget::cancelSearchByMouseBack() { return _searchHasFocus && !_searchSuggestionsLocked && !_searchState.inChat - && cancelSearch(); + && cancelSearch({ .jumpBackToSearchedChat = true }); } void Widget::processSearchFocusChange() { @@ -1301,7 +1326,7 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { return; } changeOpenedSubsection([&] { - cancelSearch(true); + cancelSearch({ .forceFullCancel = true }); closeChildList(anim::type::instant); controller()->closeForum(); _openedFolder = folder; @@ -1355,7 +1380,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { return; } changeOpenedSubsection([&] { - cancelSearch(true); + cancelSearch({ .forceFullCancel = true }); closeChildList(anim::type::instant); _openedForum = forum; _searchState.tab = forum @@ -1838,14 +1863,22 @@ void Widget::slideFinished() { } void Widget::escape() { - if (!cancelSearch()) { - if (controller()->shownForum().current()) { - controller()->closeForum(); + if (!cancelSearch({ .jumpBackToSearchedChat = true })) { + if (const auto forum = controller()->shownForum().current()) { + const auto id = controller()->windowId(); + const auto initial = id.forum(); + if (!initial) { + controller()->closeForum(); + } else if (initial != forum) { + controller()->showForum(initial); + } } else if (controller()->openedFolder().current()) { - controller()->closeFolder(); + if (!controller()->windowId().folder()) { + controller()->closeFolder(); + } } else if (controller()->activeChatEntryCurrent().key) { controller()->content()->dialogsCancelled(); - } else { + } else if (controller()->isPrimary()) { const auto filters = &session().data().chatsFilters(); const auto &list = filters->list(); const auto first = list.empty() ? FilterId() : list.front().id(); @@ -1916,7 +1949,7 @@ void Widget::loadMoreBlockedByDate() { session().api().requestMoreBlockedByDateDialogs(); } -bool Widget::search(bool inCache) { +bool Widget::search(bool inCache, SearchRequestDelay delay) { _processingSearch = true; const auto guard = gsl::finally([&] { _processingSearch = false; @@ -1945,7 +1978,7 @@ bool Widget::search(bool inCache) { return true; } else if (inCache) { const auto success = _singleMessageSearch.lookup(query, [=] { - searchRequested(); + searchRequested(delay); }); if (!success) { return false; @@ -2063,6 +2096,9 @@ bool Widget::search(bool inCache) { }).send(); _searchQueries.emplace(_searchRequest, _searchQuery); } + _inner->searchRequested(true); + } else { + _inner->searchRequested(false); } const auto peerQuery = Api::ConvertPeerSearchQuery(query); if (searchForPeersRequired(peerQuery)) { @@ -2114,20 +2150,25 @@ bool Widget::searchForPeersRequired(const QString &query) const { return _searchState.filterChatsList() && !_openedForum && !query.isEmpty() - && !IsHashtagSearchQuery(query); + && (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None); } bool Widget::searchForTopicsRequired(const QString &query) const { return _searchState.filterChatsList() && _openedForum && !query.isEmpty() - && !IsHashtagSearchQuery(query) + && (IsHashOrCashtagSearchQuery(query) == HashOrCashtag::None) && !_openedForum->topicsList()->loaded(); } -void Widget::searchRequested() { - if (!search(true)) { - _searchTimer.callOnce(AutoSearchTimeout); +void Widget::searchRequested(SearchRequestDelay delay) { + if (search(true, delay)) { + return; + } else if (delay == SearchRequestDelay::Instant) { + _searchTimer.cancel(); + search(); + } else { + _searchTimer.callOnce(kSearchRequestDelay); } } @@ -2182,10 +2223,11 @@ void Widget::searchTopics() { } void Widget::searchMore() { - if (_searchRequest || _searchInHistoryRequest) { + if (_searchRequest + || _searchInHistoryRequest + || _searchTimer.isActive()) { return; - } - if (!_searchFull) { + } else if (!_searchFull) { if (const auto peer = searchInPeer()) { auto &histories = session().data().histories(); const auto topic = searchInTopic(); @@ -2637,16 +2679,19 @@ void Widget::updateCancelSearch() { QString Widget::validateSearchQuery() { const auto query = currentSearchQuery(); if (_searchState.tab == ChatSearchTab::PublicPosts) { - _searchingHashtag = true; + if (_searchHashOrCashtag == HashOrCashtag::None) { + _searchHashOrCashtag = HashOrCashtag::Hashtag; + } const auto fixed = FixHashtagSearchQuery( query, - currentSearchQueryCursorPosition()); + currentSearchQueryCursorPosition(), + _searchHashOrCashtag); if (fixed.text != query) { setSearchQuery(fixed.text, fixed.cursorPosition); } return fixed.text; } else { - _searchingHashtag = IsHashtagSearchQuery(query); + _searchHashOrCashtag = IsHashOrCashtagSearchQuery(query); } return query; } @@ -2684,6 +2729,9 @@ void Widget::updateForceDisplayWide() { void Widget::showForum( not_null forum, const Window::SectionShow ¶ms) { + if (_openedForum == forum) { + return; + } const auto nochat = !controller()->mainSectionShown(); if (!params.childColumn || (Core::App().settings().dialogsWidthRatio(nochat) == 0.) @@ -2692,7 +2740,7 @@ void Widget::showForum( changeOpenedForum(forum, params.animated); return; } - cancelSearch(true); + cancelSearch({ .forceFullCancel = true }); openChildList(forum, params); } @@ -2823,6 +2871,9 @@ bool Widget::applySearchState(SearchState state) { } hideChildList(); } + if (state.inChat && _layout == Layout::Main) { + controller()->closeFolder(); + } // Adjust state to be consistent. if (const auto peer = state.inChat.peer()) { @@ -2840,11 +2891,12 @@ bool Widget::applySearchState(SearchState state) { state.fromPeer = nullptr; } if (state.tab == ChatSearchTab::PublicPosts - && !IsHashtagSearchQuery(state.query)) { + && IsHashOrCashtagSearchQuery(state.query) == HashOrCashtag::None) { state.tab = (_openedForum && !state.inChat) ? ChatSearchTab::ThisPeer : ChatSearchTab::MyMessages; - } else if (!state.inChat && !_searchingHashtag) { + } else if (!state.inChat + && _searchHashOrCashtag == HashOrCashtag::None) { state.tab = (forum || _openedForum) ? ChatSearchTab::ThisPeer : ChatSearchTab::MyMessages; @@ -2884,7 +2936,7 @@ bool Widget::applySearchState(SearchState state) { && !state.inChat && !_openedForum) || (state.tab == ChatSearchTab::PublicPosts - && !_searchingHashtag)) { + && _searchHashOrCashtag == HashOrCashtag::None)) { state.tab = state.inChat.topic() ? ChatSearchTab::ThisTopic : (state.inChat.owningHistory() || state.inChat.sublist()) @@ -2929,10 +2981,6 @@ bool Widget::applySearchState(SearchState state) { _peerSearchQuery = QString(); } - if (_searchState.inChat && _layout == Layout::Main) { - controller()->closeFolder(); - } - if (_searchState.query != currentSearchQuery()) { setSearchQuery(_searchState.query); } @@ -3573,10 +3621,11 @@ void Widget::setSearchQuery(const QString &query, int cursorPosition) { } } -bool Widget::cancelSearch(bool forceFullCancel) { +bool Widget::cancelSearch(CancelSearchOptions options) { cancelSearchRequest(); auto updatedState = _searchState; const auto clearingQuery = !updatedState.query.isEmpty(); + const auto forceFullCancel = options.forceFullCancel; auto clearingInChat = (forceFullCancel || !clearingQuery) && (updatedState.inChat || updatedState.fromPeer @@ -3585,7 +3634,9 @@ bool Widget::cancelSearch(bool forceFullCancel) { updatedState.query = QString(); } if (clearingInChat) { - if (updatedState.inChat && controller()->adaptive().isOneColumn()) { + if (options.jumpBackToSearchedChat + && updatedState.inChat + && controller()->adaptive().isOneColumn()) { if (const auto thread = updatedState.inChat.thread()) { controller()->showThread(thread); } else { @@ -3617,7 +3668,11 @@ bool Widget::cancelSearch(bool forceFullCancel) { _inner->clearFilter(); applySearchState(std::move(updatedState)); if (_suggestions && clearSearchFocus) { + const auto clearLockedFocus = !_searchHasFocus; setInnerFocus(true); + if (clearLockedFocus) { + processSearchFocusChange(); + } } updateForceDisplayWide(); return clearingQuery || clearingInChat || clearSearchFocus; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 29e68a3ccaeb9..347683d0c5129 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -57,6 +57,7 @@ namespace Window { class SessionController; class ConnectionState; struct SectionShow; +struct SeparateId; } // namespace Window namespace Dialogs::Stories { @@ -74,10 +75,12 @@ class FakeRow; class Key; struct ChosenRow; class InnerWidget; -enum class SearchRequestType; +enum class SearchRequestType : uchar; +enum class SearchRequestDelay : uchar; class Suggestions; class ChatSearchIn; enum class ChatSearchTab : uchar; +enum class HashOrCashtag : uchar; class Widget final : public Window::AbstractSectionWidget { public: @@ -131,7 +134,6 @@ class Widget final : public Window::AbstractSectionWidget { bool floatPlayerHandleWheelEvent(QEvent *e) override; QRect floatPlayerAvailableRect() override; - bool cancelSearch(bool forceFullCancel = false); bool cancelSearchByMouseBack(); QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; @@ -157,8 +159,8 @@ class Widget final : public Window::AbstractSectionWidget { [[nodiscard]] QString currentSearchQuery() const; [[nodiscard]] int currentSearchQueryCursorPosition() const; void clearSearchField(); - void searchRequested(); - bool search(bool inCache = false); + void searchRequested(SearchRequestDelay delay); + bool search(bool inCache = false, SearchRequestDelay after = {}); void searchTopics(); void searchMore(); @@ -261,6 +263,12 @@ class Widget final : public Window::AbstractSectionWidget { [[nodiscard]] bool redirectKeyToSearch(QKeyEvent *e) const; [[nodiscard]] bool redirectImeToSearch() const; + struct CancelSearchOptions { + bool forceFullCancel = false; + bool jumpBackToSearchedChat = false; + }; + bool cancelSearch(CancelSearchOptions options); + MTP::Sender _api; bool _dragInScroll = false; @@ -308,7 +316,7 @@ class Widget final : public Window::AbstractSectionWidget { object_ptr _scrollToTop; bool _scrollToTopIsShown = false; bool _forumSearchRequested = false; - bool _searchingHashtag = false; + HashOrCashtag _searchHashOrCashtag = {}; Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp b/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp index 0524b3a635d48..42e8c33d78635 100644 --- a/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp @@ -29,6 +29,7 @@ class Action final : public Ui::Menu::ItemBase { std::shared_ptr icon, const QString &label, bool chosen); + ~Action(); bool isEnabled() const override; not_null action() const override; @@ -111,6 +112,10 @@ Action::Action( enableMouseSelecting(); } +Action::~Action() { + _icon->subscribeToUpdates(nullptr); +} + void Action::resolveMinWidth() { const auto maxWidth = st::dialogsSearchInPhotoPadding + st::dialogsSearchInPhotoSize @@ -199,12 +204,14 @@ void Action::handleKeyPress(not_null e) { FixedHashtagSearchQuery FixHashtagSearchQuery( const QString &query, - int cursorPosition) { + int cursorPosition, + HashOrCashtag tag) { const auto trimmed = query.trimmed(); const auto hash = int(trimmed.isEmpty() ? query.size() : query.indexOf(trimmed)); const auto start = std::min(cursorPosition, hash); + const auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#'); auto result = query.mid(0, start); for (const auto &ch : query.mid(start)) { if (ch.isSpace()) { @@ -213,33 +220,41 @@ FixedHashtagSearchQuery FixHashtagSearchQuery( } continue; } else if (result.size() == start) { - result += '#'; - if (ch != '#') { + result += first; + if (ch != first) { ++cursorPosition; } } - if (ch != '#') { + if (ch != first) { result += ch; } } if (result.size() == start) { - result += '#'; + result += first; ++cursorPosition; } return { result, cursorPosition }; } -bool IsHashtagSearchQuery(const QString &query) { +HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) { const auto trimmed = query.trimmed(); - if (trimmed.isEmpty() || trimmed[0] != '#') { - return false; - } - for (const auto &ch : trimmed) { - if (ch.isSpace()) { - return false; + const auto first = trimmed.isEmpty() ? QChar() : trimmed[0]; + if (first == '#') { + for (const auto &ch : trimmed) { + if (ch.isSpace()) { + return HashOrCashtag::None; + } + } + return HashOrCashtag::Hashtag; + } else if (first == '$') { + for (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) { + if ((*it) < 'A' || (*it) > 'Z') { + return HashOrCashtag::None; + } } + return HashOrCashtag::Cashtag; } - return true; + return HashOrCashtag::None; } void ChatSearchIn::Section::update() { diff --git a/Telegram/SourceFiles/dialogs/ui/chat_search_in.h b/Telegram/SourceFiles/dialogs/ui/chat_search_in.h index 5e99f2d056f65..b5b7426162d6e 100644 --- a/Telegram/SourceFiles/dialogs/ui/chat_search_in.h +++ b/Telegram/SourceFiles/dialogs/ui/chat_search_in.h @@ -87,14 +87,21 @@ class ChatSearchIn final : public Ui::RpWidget { }; +enum class HashOrCashtag : uchar { + None, + Hashtag, + Cashtag, +}; + struct FixedHashtagSearchQuery { QString text; int cursorPosition = 0; }; [[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery( const QString &query, - int cursorPosition); + int cursorPosition, + HashOrCashtag tag); -[[nodiscard]] bool IsHashtagSearchQuery(const QString &query); +[[nodiscard]] HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query); } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 1e4a4cd5d72b6..7bca7a120a2b4 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -903,7 +903,7 @@ TextWithEntities List::computeTooltipText() const { } void List::setShowTooltip( - not_null tooltipParent, + not_null tooltipParent, rpl::producer shown, Fn hide) { _tooltip = nullptr; @@ -925,16 +925,6 @@ void List::setShowTooltip( tooltip->toggleFast(false); updateTooltipGeometry(); - const auto handle = tooltipParent->window()->windowHandle(); - auto windowActive = rpl::single( - handle->isActive() - ) | rpl::then(base::qt_signal_producer( - handle, - &QWindow::activeChanged - ) | rpl::map([=] { - return handle->isActive(); - })) | rpl::distinct_until_changed(); - { const auto recompute = [=] { updateTooltipGeometry(); @@ -955,7 +945,7 @@ void List::setShowTooltip( _tooltipText.value() | rpl::map( notEmpty ) | rpl::distinct_until_changed(), - std::move(windowActive) + tooltipParent->windowActiveValue() ) | rpl::start_with_next([=](bool, bool, bool active) { _tooltipWindowActive = active; if (!isHidden()) { @@ -981,7 +971,7 @@ void List::toggleTooltip(bool fast) { && !isHidden() && _tooltipNotHidden.current() && !_tooltipText.current().empty() - && window()->windowHandle()->isActive(); + && isActiveWindow(); if (_tooltip) { if (fast) { _tooltip->toggleFast(shown); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 3039389574e2f..c3140a9ca466e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -72,7 +72,7 @@ class List final : public Ui::RpWidget { style::align alignSmall, QRect geometryFull = QRect()); void setShowTooltip( - not_null tooltipParent, + not_null tooltipParent, rpl::producer shown, Fn hide); void raiseTooltip(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index ad6fe1f698f5f..a19200d8ab27d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -41,6 +41,7 @@ For license and copyright information please follow this link: #include "ui/dynamic_thumbnails.h" #include "ui/painter.h" #include "ui/unread_badge_paint.h" +#include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "styles/style_chat.h" diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 71750a5756e16..9e65eac39c604 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -666,6 +666,28 @@ Invoice ParseInvoice(const MTPDmessageMediaInvoice &data) { return result; } +PaidMedia ParsePaidMedia( + ParseMediaContext &context, + const MTPDmessageMediaPaidMedia &data, + const QString &folder, + TimeId date) { + auto result = PaidMedia(); + result.stars = data.vstars_amount().v; + result.extended.reserve(data.vextended_media().v.size()); + for (const auto &extended : data.vextended_media().v) { + result.extended.push_back(extended.match([]( + const MTPDmessageExtendedMediaPreview &) + -> std::unique_ptr { + return std::unique_ptr(); + }, [&](const MTPDmessageExtendedMedia &data) + -> std::unique_ptr { + return std::make_unique( + ParseMedia(context, data.vmedia(), folder, date)); + })); + } + return result; +} + Poll ParsePoll(const MTPDmessageMediaPoll &data) { auto result = Poll(); data.vpoll().match([&](const MTPDpoll &poll) { @@ -1225,6 +1247,8 @@ Media ParseMedia( result.content = ParseGiveaway(data); }, [&](const MTPDmessageMediaGiveawayResults &data) { // #TODO export giveaway + }, [&](const MTPDmessageMediaPaidMedia &data) { + result.content = ParsePaidMedia(context, data, folder, date); }, [](const MTPDmessageMediaEmpty &data) {}); return result; } @@ -1491,6 +1515,13 @@ ServiceAction ParseServiceAction( result.content = content; }, [&](const MTPDmessageActionRequestedPeerSentMe &data) { // Should not be in user inbox. + }, [&](const MTPDmessageActionPaymentRefunded &data) { + auto content = ActionPaymentRefunded(); + content.currency = ParseString(data.vcurrency()); + content.amount = data.vtotal_amount().v; + content.peerId = ParsePeerId(data.vpeer()); + content.transactionId = data.vcharge().data().vid().v; + result.content = content; }, [](const MTPDmessageActionEmpty &data) {}); return result; } diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 76585991ca8ad..72824a2eef781 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -182,6 +182,18 @@ struct Invoice { int32 receiptMsgId = 0; }; +struct Media; +struct PaidMedia { + PaidMedia() = default; + PaidMedia(PaidMedia &&) = default; + PaidMedia &operator=(PaidMedia &&) = default; + PaidMedia(const PaidMedia &) = delete; + PaidMedia &operator=(const PaidMedia &) = delete; + + uint64 stars = 0; + std::vector> extended; +}; + struct Poll { struct Answer { Utf8String text; @@ -337,6 +349,7 @@ struct Media { Invoice, Poll, GiveawayStart, + PaidMedia, UnsupportedMedia> content; TimeId ttl = 0; @@ -563,6 +576,13 @@ struct ActionBoostApply { int boosts = 0; }; +struct ActionPaymentRefunded { + PeerId peerId = 0; + Utf8String currency; + uint64 amount = 0; + Utf8String transactionId; +}; + struct ServiceAction { std::variant< v::null_t, @@ -604,7 +624,8 @@ struct ServiceAction { ActionGiftCode, ActionGiveawayLaunch, ActionGiveawayResults, - ActionBoostApply> content; + ActionBoostApply, + ActionPaymentRefunded> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 9df6bcbb3f46f..d988fc00c33b1 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1315,6 +1315,12 @@ auto HtmlWriter::Wrap::pushMessage( + " boosted the group " + QByteArray::number(data.boosts) + (data.boosts > 1 ? " times" : " time"); + }, [&](const ActionPaymentRefunded &data) { + const auto amount = FormatMoneyAmount(data.amount, data.currency); + auto result = peers.wrapPeerName(data.peerId) + + " refunded back " + + amount; + return result; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -2092,6 +2098,9 @@ MediaData HtmlWriter::Wrap::prepareMediaData( result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const Poll &data) { }, [](const GiveawayStart &data) { + }, [&](const PaidMedia &data) { + result.classes = "media_invoice"; + result.status = Data::FormatMoneyAmount(data.stars, "XTR"); }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](v::null_t) {}); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index b485e615b2021..5d4c7e42dbb89 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -625,6 +625,13 @@ QByteArray SerializeMessage( pushActor(); pushAction("boost_apply"); push("boosts", data.boosts); + }, [&](const ActionPaymentRefunded &data) { + pushAction("refunded_payment"); + push("amount", data.amount); + push("currency", data.currency); + pushBare("peer_name", wrapPeerName(data.peerId)); + push("peer_id", data.peerId); + push("charge_id", data.transactionId); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { @@ -779,6 +786,8 @@ QByteArray SerializeMessage( { "until_date", SerializeDate(data.untilDate) }, { "channels", serialized }, })); + }, [&](const PaidMedia &data) { + push("paid_stars_amount", data.stars); }, [](const UnsupportedMedia &data) { Unexpected("Unsupported message."); }, [](v::null_t) {}); diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index 8f546260a7bad..6cd12298fd445 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -283,6 +283,7 @@ void SettingsWidget::setupPathAndFormat( addLocationLabel(container); addFormatOption(tr::lng_export_option_html(tr::now), Format::Html); addFormatOption(tr::lng_export_option_json(tr::now), Format::Json); + addFormatOption(tr::lng_export_option_html_and_json(tr::now), Format::HtmlAndJson); } void SettingsWidget::addLocationLabel( diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index cb099afe4ec1c..973a44a21a868 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -683,6 +683,10 @@ QString InnerWidget::elementAuthorRank(not_null view) { return {}; } +bool InnerWidget::elementHideTopicButton(not_null view) { + return false; +} + void InnerWidget::saveState(not_null memento) { memento->setFilter(std::move(_filter)); memento->setAdmins(std::move(_admins)); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index bbe3c03813e93..5f55e5d40751f 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -141,6 +141,8 @@ class InnerWidget final HistoryView::Element *replacing) override; QString elementAuthorRank( not_null view) override; + bool elementHideTopicButton( + not_null view) override; ~InnerWidget(); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 73961de734e63..ff8a7266d9c09 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -324,6 +324,10 @@ class HistoryMainElementDelegate final return {}; } + bool elementHideTopicButton(not_null view) override { + return false; + } + not_null delegate() override { return this; } @@ -1027,7 +1031,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { } } session().data().reactions().poll(item, context.now); - if (item->hasExtendedMediaPreview()) { + if (item->hasUnpaidContent()) { session().api().views().pollExtendedMedia(item); } _reactionsManager->recordCurrentReactionEffect( @@ -1727,7 +1731,9 @@ std::unique_ptr HistoryInner::prepareDrag() { updateDragSelection(nullptr, nullptr, false); _selectScroll.cancel(); - if (!urls.isEmpty()) mimeData->setUrls(urls); + if (!urls.isEmpty()) { + mimeData->setUrls(urls); + } if (uponSelected && !_controller->adaptive().isOneColumn()) { auto selectedState = getSelectionState(); if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { @@ -1748,21 +1754,21 @@ std::unique_ptr HistoryInner::prepareDrag() { == forwardSelectionState.canForwardCount)) { forwardIds = getSelectedItems(); } else if (_mouseCursorState == CursorState::Date) { - forwardIds = session().data().itemOrItsGroup(_mouseActionItem); + const auto item = _mouseActionItem; + if (item && item->allowsForward()) { + forwardIds = session().data().itemOrItsGroup(item); + } } else if ((pressedView->isHiddenByGroup() && pressedHandler) || (pressedView->media() && pressedView->media()->dragItemByHandler(pressedHandler))) { const auto item = _dragStateItem ? _dragStateItem : _mouseActionItem; - forwardIds = MessageIdsList(1, item->fullId()); - } - if (forwardIds.empty()) { - return nullptr; + if (item && item->allowsForward()) { + forwardIds = MessageIdsList(1, item->fullId()); + } } - session().data().setMimeForwardIds(std::move(forwardIds)); - auto result = std::make_unique(); - result->setData(u"application/x-td-forward"_q, "1"); + if (pressedHandler) { const auto lnkDocument = reinterpret_cast( pressedHandler->property( @@ -1770,12 +1776,23 @@ std::unique_ptr HistoryInner::prepareDrag() { if (lnkDocument) { const auto filepath = lnkDocument->filepath(true); if (!filepath.isEmpty()) { - QList urls; urls.push_back(QUrl::fromLocalFile(filepath)); - result->setUrls(urls); } } } + + if (forwardIds.empty() && urls.isEmpty()) { + return nullptr; + } + + auto result = std::make_unique(); + if (!forwardIds.empty()) { + session().data().setMimeForwardIds(std::move(forwardIds)); + result->setData(u"application/x-td-forward"_q, "1"); + } + if (!urls.isEmpty()) { + result->setUrls(urls); + } return result; } return nullptr; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 73b92e21d51a0..d2386e7e62515 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -23,6 +23,7 @@ For license and copyright information please follow this link: #include "ui/text/format_values.h" #include "ui/text/text_isolated_emoji.h" #include "ui/text/text_utilities.h" +#include "settings/settings_credits_graphics.h" // ShowRefundInfoBox. #include "storage/file_upload.h" #include "storage/storage_shared_media.h" #include "main/main_account.h" @@ -211,59 +212,13 @@ std::unique_ptr HistoryItem::CreateMedia( const MTPMessageMedia &media) { using Result = std::unique_ptr; return media.match([&](const MTPDmessageMediaContact &media) -> Result { - - const auto vcardItems = [&] { - using Type = Data::SharedContact::VcardItemType; - auto items = Data::SharedContact::VcardItems(); - for (const auto &item : qs(media.vvcard()).split('\n')) { - const auto parts = item.split(':'); - if (parts.size() == 2) { - const auto &type = parts.front(); - const auto &value = parts[1]; - - if (type.startsWith("TEL")) { - const auto telType = type.contains("PREF") - ? Type::PhoneMain - : type.contains("HOME") - ? Type::PhoneHome - : type.contains("WORK") - ? Type::PhoneWork - : (type.contains("CELL") - || type.contains("MOBILE")) - ? Type::PhoneMobile - : type.contains("OTHER") - ? Type::PhoneOther - : Type::Phone; - items[telType] = value; - } else if (type.startsWith("EMAIL")) { - items[Type::Email] = value; - } else if (type.startsWith("URL")) { - items[Type::Url] = value; - } else if (type.startsWith("NOTE")) { - items[Type::Note] = value; - } else if (type.startsWith("ORG")) { - items[Type::Organization] = value; - items[Type::Organization].replace(';', ' '); - } else if (type.startsWith("ADR")) { - items[Type::Address] = value; - } else if (type.startsWith("BDAY")) { - items[Type::Birthday] = value; - } else if (type.startsWith("N")) { - items[Type::Name] = value; - items[Type::Name].replace(';', ' '); - } - } - } - return items; - }(); - return std::make_unique( item, media.vuser_id().v, qs(media.vfirst_name()), qs(media.vlast_name()), qs(media.vphone_number()), - vcardItems); + Data::SharedContact::ParseVcard(qs(media.vvcard()))); }, [&](const MTPDmessageMediaGeo &media) -> Result { return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( @@ -395,6 +350,10 @@ std::unique_ptr HistoryItem::CreateMedia( return std::make_unique( item, Data::ComputeGiveawayResultsData(item, media)); + }, [&](const MTPDmessageMediaPaidMedia &media) -> Result { + return std::make_unique( + item, + Data::ComputeInvoiceData(item, media)); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -1859,7 +1818,8 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { } } -void HistoryItem::applyEdition(const MTPMessageExtendedMedia &media) { +void HistoryItem::applyEdition( + const QVector &media) { if (const auto existing = this->media()) { if (existing->updateExtendedMedia(this, media)) { checkBuyButton(); @@ -2287,7 +2247,7 @@ bool HistoryItem::forbidsSaving() const { if (forbidsForward()) { return true; } else if (const auto invoice = _media ? _media->invoice() : nullptr) { - return (invoice->extendedMedia != nullptr); + return HasExtendedMedia(*invoice); } return false; } @@ -2430,6 +2390,13 @@ std::optional HistoryItem::errorTextForForward( && _media->poll()->publicVotes() && peer->isBroadcast()) { return tr::lng_restricted_send_public_polls(tr::now); + } else if (_media + && _media->invoice() + && _media->invoice()->isPaidMedia + && peer->isBroadcast() + && peer->isFullLoaded() + && !peer->asBroadcast()->canPostPaidMedia()) { + return tr::lng_restricted_send_paid_media(tr::now); } else if (!Data::CanSend(to, requiredRight, false)) { return tr::lng_forward_cant(tr::now); } @@ -3037,10 +3004,10 @@ bool HistoryItem::externalReply() const { return false; } -bool HistoryItem::hasExtendedMediaPreview() const { +bool HistoryItem::hasUnpaidContent() const { if (const auto media = _media.get()) { if (const auto invoice = media->invoice()) { - return (invoice->extendedPreview && !invoice->extendedMedia); + return HasUnpaidMedia(*invoice); } } return false; @@ -3824,7 +3791,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { void HistoryItem::refreshMedia(const MTPMessageMedia *media) { const auto was = (_media != nullptr); if (const auto invoice = was ? _media->invoice() : nullptr) { - if (invoice->extendedMedia) { + if (HasExtendedMedia(*invoice)) { return; } } @@ -4062,6 +4029,22 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { } } else if (type == mtpc_messageActionGiveawayResults) { UpdateComponents(HistoryServiceGiveawayResults::Bit()); + } else if (type == mtpc_messageActionPaymentRefunded) { + const auto &data = action.c_messageActionPaymentRefunded(); + UpdateComponents(HistoryServicePaymentRefund::Bit()); + const auto refund = Get(); + refund->peer = _history->owner().peer(peerFromMTP(data.vpeer())); + refund->amount = data.vtotal_amount().v; + refund->currency = qs(data.vcurrency()); + refund->transactionId = qs(data.vcharge().data().vid()); + const auto id = fullId(); + refund->link = std::make_shared([=]( + ClickContext context) { + const auto my = context.other.value(); + if (const auto window = my.sessionWindow.get()) { + Settings::ShowRefundInfoBox(window, id); + } + }); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -4975,6 +4958,25 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto preparePaymentRefunded = [&](const MTPDmessageActionPaymentRefunded &action) { + auto result = PreparedServiceText(); + const auto refund = Get(); + Assert(refund != nullptr); + Assert(refund->peer != nullptr); + + const auto amount = refund->amount; + const auto currency = refund->currency; + result.links.push_back(refund->peer->createOpenLink()); + result.text = tr::lng_action_payment_refunded( + tr::now, + lt_peer, + Ui::Text::Link(refund->peer->name(), 1), // Link 1. + lt_amount, + { Ui::FillAmountAndCurrency(amount, currency) }, + Ui::Text::WithEntities); + return result; + }; + setServiceText(action.match( prepareChatAddUserText, prepareChatJoinedByLink, @@ -5017,6 +5019,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareGiveawayLaunch, prepareGiveawayResults, prepareBoostApply, + preparePaymentRefunded, PrepareEmptyText, PrepareErrorText)); @@ -5595,3 +5598,9 @@ void HistoryItem::clearDependencyMessage() { } } } + +void HistoryItem::overrideMedia(std::unique_ptr media) { + Expects(!media || media->parent() == this); + + _media = std::move(media); +} diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 3118bf3bcc859..e4ed45cf97bf8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -326,7 +326,7 @@ class HistoryItem final : public RuntimeComposer { [[nodiscard]] int repliesCount() const; [[nodiscard]] bool repliesAreComments() const; [[nodiscard]] bool externalReply() const; - [[nodiscard]] bool hasExtendedMediaPreview() const; + [[nodiscard]] bool hasUnpaidContent() const; [[nodiscard]] bool inHighlightProcess() const; void highlightProcessDone(); @@ -345,7 +345,7 @@ class HistoryItem final : public RuntimeComposer { void applyChanges(not_null story); void applyEdition(const MTPDmessageService &message); - void applyEdition(const MTPMessageExtendedMedia &media); + void applyEdition(const QVector &media); void updateForwardedInfo(const MTPMessageFwdHeader *fwd); void updateSentContent( const TextWithEntities &textWithEntities, @@ -356,6 +356,7 @@ class HistoryItem final : public RuntimeComposer { const MTPDupdateShortSentMessage &data, bool wasAlready); void updateReactions(const MTPMessageReactions *reactions); + void overrideMedia(std::unique_ptr media); void applyEditionToHistoryCleared(); void updateReplyMarkup(HistoryMessageMarkupData &&markup); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 60c29e7ee465c..55e9e09a54f49 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "ui/effects/spoiler_mess.h" #include "ui/image/image.h" #include "ui/toast/toast.h" +#include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/chat/chat_style.h" @@ -48,9 +49,11 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" -#include "styles/style_widgets.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" // dialogsMiniReplyStory. +#include "styles/style_settings.h" +#include "styles/style_widgets.h" #include @@ -679,6 +682,11 @@ ReplyKeyboard::ReplyKeyboard( const auto context = _item->fullId(); const auto rowCount = int(markup->data.rows.size()); _rows.reserve(rowCount); + const auto buttonEmoji = Ui::Text::SingleCustomEmoji( + owner->customEmojiManager().registerInternalEmoji( + st::settingsPremiumIconStar, + QMargins(0, -st::moderateBoxExpandInnerSkip, 0, 0), + true)); for (auto i = 0; i != rowCount; ++i) { const auto &row = markup->data.rows[i]; const auto rowSize = int(row.size()); @@ -686,17 +694,54 @@ ReplyKeyboard::ReplyKeyboard( newRow.reserve(rowSize); for (auto j = 0; j != rowSize; ++j) { auto button = Button(); - const auto text = row[j].text; + using Type = HistoryMessageMarkupButton::Type; + const auto isBuy = (row[j].type == Type::Buy); + static const auto RegExp = QRegularExpression("\\b" + + Ui::kCreditsCurrency + + "\\b"); + const auto text = isBuy + ? base::duplicate(row[j].text).replace( + RegExp, + QChar(0x2B50)) + : row[j].text; + const auto textWithEntities = [&] { + if (!isBuy) { + return TextWithEntities(); + } + auto result = TextWithEntities(); + auto firstPart = true; + for (const auto &part : text.split(QChar(0x2B50))) { + if (!firstPart) { + result.append(buttonEmoji); + } + result.append(part); + firstPart = false; + } + return result.entities.empty() + ? TextWithEntities() + : result; + }(); button.type = row.at(j).type; button.link = std::make_shared( owner, i, j, context); - button.text.setText( - _st->textStyle(), - TextUtilities::SingleLine(text), - kPlainTextOptions); + if (!textWithEntities.text.isEmpty()) { + button.text.setMarkedText( + _st->textStyle(), + TextUtilities::SingleLine(textWithEntities), + kMarkupTextOptions, + Core::MarkedTextContext{ + .session = &item->history()->owner().session(), + .customEmojiRepaint = [=] { _st->repaint(item); }, + }); + } else { + button.text.setText( + _st->textStyle(), + TextUtilities::SingleLine(text), + kPlainTextOptions); + } button.characters = text.isEmpty() ? 1 : text.size(); newRow.push_back(std::move(button)); } @@ -1046,8 +1091,8 @@ void HistoryMessageReplyMarkup::updateData( bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const { if (media && (data.flags & ReplyMarkupFlag::OnlyBuyButton)) { if (const auto invoice = media->invoice()) { - if (invoice->extendedPreview - && (!invoice->extendedMedia || !invoice->receiptMsgId)) { + if (HasUnpaidMedia(*invoice) + || (HasExtendedMedia(*invoice) && !invoice->receiptMsgId)) { return true; } } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 8d262a52e3098..7e9fb6396bac2 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -671,6 +671,15 @@ struct HistoryServiceCustomLink ClickHandlerPtr link; }; +struct HistoryServicePaymentRefund +: public RuntimeComponent { + ClickHandlerPtr link; + PeerData *peer = nullptr; + QString transactionId; + QString currency; + uint64 amount = 0; +}; + enum class HistorySelfDestructType { Photo, Video, diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index d3d9569e6dcde..30303e5f898e2 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -317,7 +317,7 @@ ClickHandlerPtr JumpToMessageClickHandler( TextWithEntities highlightPart, int highlightPartOffsetHint) { return std::make_shared([=] { - const auto separate = Core::App().separateWindowForPeer(peer); + const auto separate = Core::App().separateWindowFor(peer); const auto controller = separate ? separate->sessionController() : peer->session().tryResolveWindow(); @@ -347,7 +347,7 @@ ClickHandlerPtr JumpToStoryClickHandler( not_null peer, StoryId storyId) { return std::make_shared([=] { - const auto separate = Core::App().separateWindowForPeer(peer); + const auto separate = Core::App().separateWindowFor(peer); const auto controller = separate ? separate->sessionController() : peer->session().tryResolveWindow(); @@ -557,6 +557,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaGiveawayResults &) { return Result::Good; + }, [](const MTPDmessageMediaPaidMedia &) { + return Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a805c547962e1..0a3c5a0647d34 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -378,11 +378,10 @@ HistoryWidget::HistoryWidget( _field->setEnabled(shown); }, _field->lifetime()); #endif // Q_OS_MAC - connect( - controller->widget()->windowHandle(), - &QWindow::visibleChanged, - this, - [=] { windowIsVisibleChanged(); }); + controller->widget()->shownValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + windowIsVisibleChanged(); + }, lifetime()); initTabbedSelector(); @@ -824,7 +823,7 @@ HistoryWidget::HistoryWidget( if (flags & PeerUpdateFlag::UnavailableReason) { const auto unavailable = _peer->computeUnavailableReason(); if (!unavailable.isEmpty()) { - const auto account = &_peer->account(); + const auto account = not_null(&_peer->account()); closeCurrent(); if (const auto primary = Core::App().windowFor(account)) { primary->showToast(unavailable); @@ -1856,12 +1855,13 @@ void HistoryWidget::setInnerFocus() { if (_list) { if (isSearching()) { _composeSearch->setInnerFocus(); - } else if (_chooseTheme && _chooseTheme->shouldBeShown()) { + } else if (isChoosingTheme()) { _chooseTheme->setFocus(); } else if (_showAnimation || _nonEmptySelection || (_list && _list->wasSelectedText()) || isRecording() + || isJoinChannel() || isBotStart() || isBlocked() || (!_canSendTexts && !_editMsgId)) { @@ -3343,7 +3343,8 @@ void HistoryWidget::messagesFailed(const MTP::Error &error, int requestId) { || error.type() == u"USER_BANNED_IN_CHANNEL"_q) { auto was = _peer; closeCurrent(); - if (const auto primary = Core::App().windowFor(&was->account())) { + const auto wasAccount = not_null(&was->account()); + if (const auto primary = Core::App().windowFor(wasAccount)) { primary->showToast((was && was->isMegagroup()) ? tr::lng_group_not_accessible(tr::now) : tr::lng_channel_not_accessible(tr::now)); @@ -4975,8 +4976,8 @@ bool HistoryWidget::updateCmdStartShown() { } bool HistoryWidget::searchInChatEmbedded(Dialogs::Key chat, QString query) { - const auto peer = chat.peer(); - if (!peer || peer != controller()->singlePeer()) { + const auto peer = chat.peer(); // windows todo + if (!peer || Window::SeparateId(peer) != controller()->windowId()) { return false; } else if (_peer != peer) { const auto weak = Ui::MakeWeak(this); @@ -5667,7 +5668,7 @@ bool HistoryWidget::confirmSendingFiles( cursor.setPosition(position, QTextCursor::KeepAnchor); } _field->setTextCursor(cursor); - if (!insertTextOnCancel.isEmpty()) { + if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) { _field->textCursor().insertText(insertTextOnCancel); } })); @@ -7513,7 +7514,7 @@ void HistoryWidget::showPremiumToast(not_null document) { } void HistoryWidget::checkCharsCount() { - _fieldCharsCountManager.setCount(Ui::FieldCharacterCount(_field)); + _fieldCharsCountManager.setCount(Ui::ComputeFieldCharacterCount(_field)); checkCharsLimitation(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 977415fbafd96..b90ef037daf4a 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -504,7 +504,7 @@ class HistoryWidget final int countMembersDropdownHeightMax() const; void updateReplyToName(); - bool editingMessage() const { + [[nodiscard]] bool editingMessage() const { return _editMsgId != 0; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index cc4a91a1f5b1d..09aa2178eeb64 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1658,6 +1658,7 @@ void ComposeControls::initField() { InitMessageFieldFade(_field, _st.field.textBg); _field->setEditLinkCallback( DefaultEditLinkCallback(_show, _field, &_st.boxField)); + _field->setEditLanguageCallback(DefaultEditLanguageCallback(_show)); initAutocomplete(); const auto allow = [=](not_null emoji) { return _history @@ -3267,7 +3268,7 @@ not_null ComposeControls::likeAnimationTarget() const { } int ComposeControls::fieldCharacterCount() const { - return Ui::FieldCharacterCount(_field); + return Ui::ComputeFieldCharacterCount(_field); } bool ComposeControls::preventsClose(Fn &&continueCallback) const { @@ -3414,7 +3415,7 @@ Fn ComposeControls::restoreTextCallback( cursor.setPosition(position, QTextCursor::KeepAnchor); } _field->setTextCursor(cursor); - if (!insertTextOnCancel.isEmpty()) { + if (Ui::InsertTextOnImageCancel(insertTextOnCancel)) { _field->textCursor().insertText(insertTextOnCancel); } }); @@ -3450,7 +3451,8 @@ void ComposeControls::checkCharsLimitation() { const auto maxCaptionSize = !hasMediaWithCaption ? MaxMessageSize : Data::PremiumLimits(&session()).captionLengthCurrent(); - const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; + const auto remove = Ui::ComputeFieldCharacterCount(_field) + - maxCaptionSize; if (remove > 0) { if (!_charsLimitation) { using namespace Controls; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 91540a7291ba4..9bf9739f3c799 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -126,6 +126,7 @@ class Item final Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; @@ -279,6 +280,8 @@ void Item::setupTop() { const auto topic = _thread->asTopic(); auto nameValue = (topic ? Info::Profile::TitleValue(topic) + : _thread->peer()->isSelf() + ? tr::lng_saved_messages() : Info::Profile::NameValue(_thread->peer()) ) | rpl::start_spawning(_top->lifetime()); const auto name = Ui::CreateChild( @@ -294,18 +297,24 @@ void Item::setupTop() { ) | rpl::map([](StatusFields &&fields) { return fields.text; }); - const auto status = Ui::CreateChild( - _top.get(), - (topic - ? Info::Profile::NameValue(topic->channel()) - : std::move(statusText)), - st::previewStatus); - std::move(statusFields) | rpl::start_with_next([=](const StatusFields &fields) { - status->setTextColorOverride(fields.active - ? st::windowActiveTextFg->c - : std::optional()); - }, status->lifetime()); - status->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto status = _thread->peer()->isSelf() + ? nullptr + : Ui::CreateChild( + _top.get(), + (topic + ? Info::Profile::NameValue(topic->channel()) + : std::move(statusText)), + st::previewStatus); + if (status) { + std::move( + statusFields + ) | rpl::start_with_next([=](const StatusFields &fields) { + status->setTextColorOverride(fields.active + ? st::windowActiveTextFg->c + : std::optional()); + }, status->lifetime()); + status->setAttribute(Qt::WA_TransparentForMouseEvents); + } const auto userpic = topic ? nullptr : Ui::CreateChild( @@ -313,6 +322,7 @@ void Item::setupTop() { _thread->peer(), st::previewUserpic); if (userpic) { + userpic->showSavedMessagesOnSelf(true); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); } const auto icon = topic @@ -334,15 +344,23 @@ void Item::setupTop() { name->resizeToNaturalWidth(width - st.namePosition.x() - st.photoPosition.x()); - name->move(st::previewTop.namePosition); + if (status) { + name->move(st::previewTop.namePosition); + } else { + name->move( + st::previewTop.namePosition.x(), + (st::previewTop.height - name->height()) / 2); + } }, name->lifetime()); _top->geometryValue() | rpl::start_with_next([=](QRect geometry) { const auto &st = st::previewTop; - status->resizeToWidth(geometry.width() - - st.statusPosition.x() - - st.photoPosition.x()); - status->move(st.statusPosition); + if (status) { + status->resizeToWidth(geometry.width() + - st.statusPosition.x() + - st.photoPosition.x()); + status->move(st.statusPosition); + } shadow->setGeometry( geometry.x(), geometry.y() + geometry.height(), @@ -559,8 +577,12 @@ MessagesBarData Item::listMessagesBar( ? _replies->computeInboxReadTillFull() : MsgId(); const auto migrated = _replies ? nullptr : _history->migrateFrom(); - const auto migratedTill = migrated ? migrated->inboxReadTillId() : 0; - const auto historyTill = _replies ? 0 : _history->inboxReadTillId(); + const auto migratedTill = (migrated && migrated->unreadCount() > 0) + ? migrated->inboxReadTillId() + : 0; + const auto historyTill = (_replies || !_history->unreadCount()) + ? 0 + : _history->inboxReadTillId(); if (!_replies && !migratedTill && !historyTill) { return {}; } @@ -673,6 +695,10 @@ QString Item::listElementAuthorRank(not_null view) { return {}; } +bool Item::listElementHideTopicButton(not_null view) { + return _thread->asTopic() != nullptr; +} + History *Item::listTranslateHistory() { return nullptr; } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index a53ab20284877..49013f1c85cb2 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1284,7 +1284,14 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context) { - const auto item = controller->session().data().message(itemId); + CopyPostLink(controller->uiShow(), itemId, context); +} + +void CopyPostLink( + std::shared_ptr show, + FullMsgId itemId, + Context context) { + const auto item = show->session().data().message(itemId); if (!item || !item->hasDirectLink()) { return; } @@ -1311,7 +1318,7 @@ void CopyPostLink( return channel->hasUsername(); }(); - controller->showToast(isPublicLink + show->showToast(isPublicLink ? tr::lng_channel_public_link_copied(tr::now) : tr::lng_context_about_private_link(tr::now)); } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 8f00f4da80345..efb2a5fdd3ca9 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -61,6 +61,10 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context); +void CopyPostLink( + std::shared_ptr show, + FullMsgId itemId, + Context context); void CopyStoryLink( std::shared_ptr show, FullStoryId storyId); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 84bd945e060b0..843010177dcfb 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -198,10 +198,16 @@ void DefaultElementDelegate::elementStartEffect( } QString DefaultElementDelegate::elementAuthorRank( - not_null view) { + not_null view) { return {}; } +bool DefaultElementDelegate::elementHideTopicButton( + not_null view) { + return true; +} + + SimpleElementDelegate::SimpleElementDelegate( not_null controller, Fn update) @@ -704,6 +710,14 @@ void Element::overrideMedia(std::unique_ptr media) { _flags |= Flag::MediaOverriden; } +not_null Element::enforcePurchasedTag() { + if (const auto purchased = Get()) { + return purchased; + } + AddComponents(PurchasedTag::Bit()); + return Get(); +} + void Element::refreshMedia(Element *replacing) { if (_flags & Flag::MediaOverriden) { return; diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 0c01e97672260..1d52d79252ed1 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -117,6 +117,7 @@ class ElementDelegate { not_null view, Element *replacing) = 0; virtual QString elementAuthorRank(not_null view) = 0; + virtual bool elementHideTopicButton(not_null view) = 0; virtual ~ElementDelegate() { } @@ -170,6 +171,7 @@ class DefaultElementDelegate : public ElementDelegate { not_null view, Element *replacing) override; QString elementAuthorRank(not_null view) override; + bool elementHideTopicButton(not_null view) override; }; @@ -276,6 +278,10 @@ struct FakeBotAboutTop : public RuntimeComponent { int height = 0; }; +struct PurchasedTag : public RuntimeComponent { + Ui::Text::String text; +}; + struct TopicButton { std::unique_ptr ripple; ClickHandlerPtr link; @@ -562,6 +568,8 @@ class Element void overrideMedia(std::unique_ptr media); + [[nodiscard]] not_null enforcePurchasedTag(); + virtual bool consumeHorizontalScroll(QPoint position, int delta) { return false; } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 5480f1162db1b..14bdf8f446893 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1867,6 +1867,11 @@ QString ListWidget::elementAuthorRank(not_null view) { return _delegate->listElementAuthorRank(view); } +bool ListWidget::elementHideTopicButton(not_null view) { + return _delegate->listElementHideTopicButton(view); +} + + void ListWidget::saveState(not_null memento) { memento->setAroundPosition(_aroundPosition); const auto state = countScrollState(); @@ -2258,7 +2263,7 @@ void ListWidget::paintEvent(QPaintEvent *e) { } } session->data().reactions().poll(item, context.now); - if (item->hasExtendedMediaPreview()) { + if (item->hasUnpaidContent()) { session->api().views().pollExtendedMedia(item); } if (_reactionsManager) { @@ -3770,12 +3775,7 @@ std::unique_ptr ListWidget::prepareDrag() { forwardIds = MessageIdsList(1, exactItem->fullId()); } } - if (forwardIds.empty()) { - return nullptr; - } - session().data().setMimeForwardIds(std::move(forwardIds)); - auto result = std::make_unique(); - result->setData(u"application/x-td-forward"_q, "1"); + if (pressedHandler) { const auto lnkDocument = reinterpret_cast( pressedHandler->property( @@ -3783,12 +3783,23 @@ std::unique_ptr ListWidget::prepareDrag() { if (lnkDocument) { const auto filepath = lnkDocument->filepath(true); if (!filepath.isEmpty()) { - QList urls; urls.push_back(QUrl::fromLocalFile(filepath)); - result->setUrls(urls); } } } + + if (forwardIds.empty() && urls.isEmpty()) { + return nullptr; + } + + auto result = std::make_unique(); + if (!forwardIds.empty()) { + session().data().setMimeForwardIds(std::move(forwardIds)); + result->setData(u"application/x-td-forward"_q, "1"); + } + if (!urls.isEmpty()) { + result->setUrls(urls); + } return result; } return nullptr; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 5cfc57c0824ad..d0507474f40fb 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -155,6 +155,7 @@ class ListDelegate { Painter &p, const Ui::ChatPaintContext &context) = 0; virtual QString listElementAuthorRank(not_null view) = 0; + virtual bool listElementHideTopicButton(not_null view) = 0; virtual History *listTranslateHistory() = 0; virtual void listAddTranslatedItems( not_null tracker) = 0; @@ -368,7 +369,7 @@ class ListWidget final [[nodiscard]] auto replyToMessageRequested() const -> rpl::producer; void replyToMessageRequestNotify( - FullReplyTo to, + FullReplyTo to, bool forceAnotherChat = false); [[nodiscard]] rpl::producer readMessageRequested() const; [[nodiscard]] rpl::producer showMessageRequested() const; @@ -425,6 +426,7 @@ class ListWidget final not_null view, Element *replacing) override; QString elementAuthorRank(not_null view) override; + bool elementHideTopicButton(not_null view) override; void setEmptyInfoWidget(base::unique_qptr &&w); void overrideIsChatWide(bool isWide); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 46185003caa5b..7f3c390575605 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1037,8 +1037,7 @@ QSize Message::performCountOptimalSize() { void Message::refreshTopicButton() { const auto item = data(); if (isAttachedToPrevious() - || (context() != Context::History - && context() != Context::ChatPreview)) { + || delegate()->elementHideTopicButton(this)) { _topicButton = nullptr; } else if (const auto topic = item->topic()) { if (!_topicButton) { diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index d530e4779b894..eebbb18480015 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -76,9 +76,9 @@ namespace { return rpl::single(ContentWithoutPreview(item, repaint)); } constexpr auto kFullLoaded = 2; - constexpr auto kSomeLoaded = 1; - constexpr auto kNotLoaded = 0; const auto loadedLevel = [=] { + constexpr auto kSomeLoaded = 1; + constexpr auto kNotLoaded = 0; const auto preview = media->replyPreview(); return media->replyPreviewLoaded() ? kFullLoaded diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 51a3101ebf0bb..7a9ecd8dcccf2 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -679,6 +679,11 @@ QString PinnedWidget::listElementAuthorRank(not_null view) { return {}; } +bool PinnedWidget::listElementHideTopicButton( + not_null view) { + return true; +} + History *PinnedWidget::listTranslateHistory() { return _history; } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 2c54e0684d244..75956e403d038 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -132,6 +132,7 @@ class PinnedWidget final Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 492992642c7c8..738377b58cca2 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -2614,6 +2614,11 @@ QString RepliesWidget::listElementAuthorRank(not_null view) { : QString(); } +bool RepliesWidget::listElementHideTopicButton( + not_null view) { + return true; +} + History *RepliesWidget::listTranslateHistory() { return _history; } diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 37c8540fdefba..c8bacc2c7d74e 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -175,6 +175,7 @@ class RepliesWidget final Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index bd4a1123b65dc..07ce6a82da197 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -1399,7 +1399,7 @@ CopyRestrictionType ScheduledWidget::listCopyMediaRestrictionType( not_null item) { if (const auto media = item->media()) { if (const auto invoice = media->invoice()) { - if (invoice->extendedMedia) { + if (HasExtendedMedia(*invoice)) { return CopyMediaRestrictionTypeFor(_history->peer, item); } } @@ -1450,6 +1450,11 @@ QString ScheduledWidget::listElementAuthorRank( return {}; } +bool ScheduledWidget::listElementHideTopicButton( + not_null view) { + return true; +} + History *ScheduledWidget::listTranslateHistory() { return nullptr; } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index b2b0efa41d4fc..ba48137136468 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -159,6 +159,7 @@ class ScheduledWidget final Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 3462ae90342e6..de18f45703813 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -667,6 +667,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { result.link = results->lnk; } else if (const auto custom = item->Get()) { result.link = custom->link; + } else if (const auto payment = item->Get()) { + result.link = payment->link; } else if (media && data()->showSimilarChannels()) { result = media->textState(mediaPoint, request); } diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index df4f4ea048c4e..424aaa5f02e5e 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -736,6 +736,11 @@ QString SublistWidget::listElementAuthorRank(not_null view) { return {}; } +bool SublistWidget::listElementHideTopicButton( + not_null view) { + return true; +} + History *SublistWidget::listTranslateHistory() { return _history; } diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h index eff71d50cf1de..6379c98453233 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.h @@ -136,6 +136,7 @@ class SublistWidget final Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 59a94d5850541..92bd2180050df 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -44,6 +44,7 @@ For license and copyright information please follow this link: #include "data/data_peer_values.h" #include "data/data_group_call.h" // GroupCall::input. #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" @@ -55,7 +56,6 @@ For license and copyright information please follow this link: #include "data/data_send_action.h" #include "chat_helpers/emoji_interactions.h" #include "base/unixtime.h" -#include "base/event_filter.h" #include "support/support_helper.h" #include "apiwrap.h" #include "api/api_chat_participants.h" @@ -233,16 +233,6 @@ TopBarWidget::TopBarWidget( updateConnectingState(); }, lifetime()); - base::install_event_filter( - this, - window()->windowHandle(), - [=](not_null e) { - if (e->type() == QEvent::Expose) { - updateConnectingState(); - } - return base::EventFilterResult::Continue; - }); - setCursor(style::cur_pointer); } @@ -254,7 +244,8 @@ Main::Session &TopBarWidget::session() const { void TopBarWidget::updateConnectingState() { const auto state = _controller->session().mtp().dcstate(); - const auto exposed = window()->windowHandle()->isExposed(); + const auto exposed = window()->windowHandle() + && window()->windowHandle()->isExposed(); if (state == MTP::ConnectedState || !exposed) { if (_connecting) { _connecting = nullptr; @@ -271,6 +262,7 @@ void TopBarWidget::updateConnectingState() { void TopBarWidget::connectingAnimationCallback() { if (!anim::Disabled()) { + updateConnectingState(); update(); } } @@ -436,6 +428,7 @@ void TopBarWidget::paintEvent(QPaintEvent *e) { if (_animatingMode) { return; } + updateConnectingState(); Painter p(this); const auto selectedButtonsTop = countSelectedButtonsTop( @@ -722,10 +715,13 @@ void TopBarWidget::mousePressEvent(QMouseEvent *e) { && !showSelectedState() && !_chooseForReportReason; if (handleClick) { + const auto archiveTop = (_activeChat.section == Section::ChatsList) + && _activeChat.key.folder(); if ((_animatingMode && _back->rect().contains(e->pos())) - || (_activeChat.section == Section::ChatsList - && _activeChat.key.folder())) { - backClicked(); + || archiveTop) { + if (!rootChatsListBar()) { + backClicked(); + } } else { infoClicked(); } @@ -898,9 +894,22 @@ void TopBarWidget::setCustomTitle(const QString &title) { } } +bool TopBarWidget::rootChatsListBar() const { + if (_activeChat.section != Section::ChatsList) { + return false; + } + const auto id = _controller->windowId(); + const auto separateFolder = id.folder(); + const auto separateForum = id.forum(); + const auto active = _activeChat.key; + return (separateForum && separateForum->history() == active.history()) + || (separateFolder && separateFolder == active.folder()); +} + void TopBarWidget::refreshInfoButton() { if (_activeChat.key.topic() - || _activeChat.section == Section::ChatsList) { + || (_activeChat.section == Section::ChatsList + && !rootChatsListBar())) { _info.destroy(); } else if (const auto peer = _activeChat.key.peer()) { auto info = object_ptr( @@ -997,6 +1006,14 @@ void TopBarWidget::updateControlsGeometry() { _leftTaken += _back->width(); } if (_info && !_info->isHidden()) { + if (_back->isHidden() && _narrowRatio > 0.) { + const auto &infoSt = st::topBarInfoButton; + const auto middle = (_narrowWidth - infoSt.photoSize) / 2; + _leftTaken = anim::interpolate( + _leftTaken, + middle - infoSt.photoPosition.x(), + _narrowRatio); + } _info->moveToLeft(_leftTaken, otherButtonsTop); _leftTaken += _info->width(); } else if (_activeChat.key.topic() @@ -1005,7 +1022,9 @@ void TopBarWidget::updateControlsGeometry() { } if (_searchField) { - const auto fieldLeft = _leftTaken; + const auto fieldLeft = _back->isHidden() + ? st::topBarArrowPadding.right() + : _leftTaken; const auto fieldTop = searchFieldTop + (height() - _searchField->height()) / 2; const auto fieldRight = st::dialogsFilterSkip @@ -1083,9 +1102,10 @@ void TopBarWidget::updateControlsVisibility() { _sendNow->setVisible(_canSendNow); const auto isOneColumn = _controller->adaptive().isOneColumn(); - auto backVisible = isOneColumn - || !_controller->content()->stackIsEmpty() - || (_activeChat.section == Section::ChatsList); + const auto backVisible = !rootChatsListBar() + && (isOneColumn + || (_activeChat.section == Section::ChatsList) + || !_controller->content()->stackIsEmpty()); _back->setVisible(backVisible && !_chooseForReportReason); _cancelChoose->setVisible(_chooseForReportReason.has_value()); if (_info) { @@ -1093,7 +1113,8 @@ void TopBarWidget::updateControlsVisibility() { && (isOneColumn || !_primaryWindow)); } if (_unreadBadge) { - _unreadBadge->setVisible(!_chooseForReportReason); + _unreadBadge->setVisible(!_chooseForReportReason + && !rootChatsListBar()); } const auto topic = _activeChat.key.topic(); const auto section = _activeChat.section; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 5c94220b1493c..6b81c26a09026 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -131,6 +131,7 @@ class TopBarWidget final : public Ui::RpWidget { private: struct EmojiInteractionSeenAnimation; + [[nodiscard]] bool rootChatsListBar() const; void refreshInfoButton(); void refreshLang(); void updateSearchVisibility(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp deleted file mode 100644 index 9c1a73a64a366..0000000000000 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp +++ /dev/null @@ -1,366 +0,0 @@ -/* -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 "history/view/media/history_view_extended_preview.h" - -#include "history/history_item.h" -#include "history/history.h" -#include "history/history_item_components.h" -#include "history/view/history_view_element.h" -#include "history/view/history_view_cursor_state.h" -#include "history/view/media/history_view_media_common.h" -#include "media/streaming/media_streaming_utility.h" -#include "ui/effects/spoiler_mess.h" -#include "ui/image/image.h" -#include "ui/image/image_prepare.h" -#include "ui/chat/chat_style.h" -#include "ui/painter.h" -#include "ui/power_saving.h" -#include "data/data_session.h" -#include "payments/payments_checkout_process.h" -#include "payments/payments_non_panel_process.h" -#include "window/window_session_controller.h" -#include "mainwindow.h" -#include "core/click_handler_types.h" -#include "styles/style_chat.h" - -namespace HistoryView { -namespace { - -[[nodiscard]] ClickHandlerPtr MakeInvoiceLink(not_null item) { - return std::make_shared([=](ClickContext context) { - const auto my = context.other.value(); - const auto controller = my.sessionWindow.get(); - Payments::CheckoutProcess::Start( - item, - Payments::Mode::Payment, - (controller - ? crl::guard( - controller, - [=](auto) { controller->widget()->activate(); }) - : Fn()), - (controller - ? Payments::ProcessNonPanelPaymentFormFactory( - controller, - item) - : nullptr)); - }); -} - -} // namespace - -ExtendedPreview::ExtendedPreview( - not_null parent, - not_null invoice) -: Media(parent) -, _invoice(invoice) { - const auto item = parent->data(); - _spoiler.link = MakeInvoiceLink(item); - resolveButtonText(); -} - -void ExtendedPreview::resolveButtonText() { - if (const auto markup = _parent->data()->inlineReplyMarkup()) { - for (const auto &row : markup->data.rows) { - for (const auto &button : row) { - if (button.type == HistoryMessageMarkupButton::Type::Buy) { - _buttonText.setText( - st::semiboldTextStyle, - TextUtilities::SingleLine(button.text)); - return; - } - } - } - } -} - -ExtendedPreview::~ExtendedPreview() { - if (hasHeavyPart()) { - unloadHeavyPart(); - _parent->checkHeavyPart(); - } -} - -void ExtendedPreview::ensureThumbnailRead() const { - if (!_inlineThumbnail.isNull() || _imageCacheInvalid) { - return; - } - const auto &bytes = _invoice->extendedPreview.inlineThumbnailBytes; - if (bytes.isEmpty()) { - return; - } - _inlineThumbnail = Images::FromInlineBytes(bytes); - if (_inlineThumbnail.isNull()) { - _imageCacheInvalid = true; - } else { - history()->owner().registerHeavyViewPart(_parent); - } -} - -bool ExtendedPreview::hasHeavyPart() const { - return _spoiler.animation || !_inlineThumbnail.isNull(); -} - -void ExtendedPreview::unloadHeavyPart() { - _inlineThumbnail - = _spoiler.background - = _spoiler.cornerCache - = _buttonBackground = QImage(); - _spoiler.animation = nullptr; -} - -bool ExtendedPreview::enforceBubbleWidth() const { - return true; -} - -QSize ExtendedPreview::countOptimalSize() { - const auto &preview = _invoice->extendedPreview; - const auto dimensions = preview.dimensions; - const auto minWidth = std::min( - std::max({ - _parent->minWidthForMedia(), - (_parent->hasBubble() - ? st::historyPhotoBubbleMinWidth - : st::minPhotoSize), - minWidthForButton(), - }), - st::maxMediaSize); - const auto scaled = CountDesiredMediaSize(dimensions); - auto maxWidth = qMax(scaled.width(), minWidth); - auto minHeight = qMax(scaled.height(), st::minPhotoSize); - if (preview.videoDuration < 0) { - accumulate_max(maxWidth, scaled.height()); - } - return { maxWidth, minHeight }; -} - -QSize ExtendedPreview::countCurrentSize(int newWidth) { - const auto &preview = _invoice->extendedPreview; - const auto dimensions = preview.dimensions; - const auto thumbMaxWidth = std::min(newWidth, st::maxMediaSize); - const auto minWidth = std::min( - std::max({ - _parent->minWidthForMedia(), - (_parent->hasBubble() - ? st::historyPhotoBubbleMinWidth - : st::minPhotoSize), - minWidthForButton(), - }), - thumbMaxWidth); - const auto scaled = (preview.videoDuration >= 0) - ? CountMediaSize( - CountDesiredMediaSize(dimensions), - newWidth) - : CountPhotoMediaSize( - CountDesiredMediaSize(dimensions), - newWidth, - maxWidth()); - newWidth = qMax(scaled.width(), minWidth); - auto newHeight = qMax(scaled.height(), st::minPhotoSize); - if (_parent->hasBubble()) { - const auto maxWithCaption = qMin( - st::msgMaxWidth, - _parent->textualMaxWidth()); - newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); - } - if (newWidth >= maxWidth()) { - newHeight = qMin(newHeight, minHeight()); - } - return { newWidth, newHeight }; -} - -int ExtendedPreview::minWidthForButton() const { - return (st::msgBotKbButton.margin + st::msgBotKbButton.padding) * 2 - + _buttonText.maxWidth(); -} - -void ExtendedPreview::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - - auto paintx = 0, painty = 0, paintw = width(), painth = height(); - auto bubble = _parent->hasBubble(); - auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); - const auto inWebPage = (_parent->media() != this); - const auto rounding = inWebPage - ? std::optional() - : adjustedBubbleRounding(); - if (!bubble) { - Assert(rounding.has_value()); - fillImageShadow(p, rthumb, *rounding, context); - } - validateImageCache(rthumb.size(), rounding); - p.drawImage(rthumb.topLeft(), _spoiler.background); - fillImageSpoiler(p, &_spoiler, rthumb, context); - paintButton(p, rthumb, context); - if (context.selected()) { - fillImageOverlay(p, rthumb, rounding, context); - } - - // date - if (!inWebPage) { - auto fullRight = paintx + paintw; - auto fullBottom = painty + painth; - if (needInfoDisplay()) { - _parent->drawInfo( - p, - context, - fullRight, - fullBottom, - 2 * paintx + paintw, - InfoDisplayType::Image); - } - if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { - auto fastShareLeft = _parent->hasRightLayout() - ? (paintx - size->width() - st::historyFastShareLeft) - : (fullRight + st::historyFastShareLeft); - auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); - _parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw); - } - } -} - -void ExtendedPreview::validateImageCache( - QSize outer, - std::optional rounding) const { - const auto ratio = style::DevicePixelRatio(); - if (_spoiler.background.size() == (outer * ratio) - && _spoiler.backgroundRounding == rounding) { - return; - } - _spoiler.background = Images::Round( - prepareImageCache(outer), - MediaRoundingMask(rounding)); - _spoiler.backgroundRounding = rounding; -} - -QImage ExtendedPreview::prepareImageCache(QSize outer) const { - ensureThumbnailRead(); - return PrepareWithBlurredBackground(outer, {}, {}, _inlineThumbnail); -} - -void ExtendedPreview::paintButton( - Painter &p, - QRect outer, - const PaintContext &context) const { - const auto st = context.st; - const auto &padding = st::extendedPreviewButtonPadding; - const auto margin = st::extendedPreviewButtonMargin; - const auto width = std::min( - _buttonText.maxWidth() + padding.left() + padding.right(), - outer.width() - 2 * margin); - const auto height = padding.top() - + st::semiboldFont->height - + padding.bottom(); - const auto overlay = st->msgDateImgBg()->c; - const auto ratio = style::DevicePixelRatio(); - const auto size = QSize(width, height); - if (_buttonBackground.size() != size * ratio - || _buttonBackgroundOverlay != overlay) { - auto &background = _spoiler.background; - if (background.width() < width * ratio - || background.height() < height * ratio) { - return; - } - _buttonBackground = background.copy(QRect( - (background.width() - width * ratio) / 2, - (background.height() - height * ratio) / 2, - width * ratio, - height * ratio)); - _buttonBackground.setDevicePixelRatio(ratio); - auto p = QPainter(&_buttonBackground); - p.fillRect(0, 0, width, height, overlay); - p.end(); - _buttonBackground = Images::Round( - std::move(_buttonBackground), - Images::CornersMask(height / 2)); - } - const auto left = outer.x() + (outer.width() - width) / 2; - const auto top = outer.y() + (outer.height() - height) / 2; - p.drawImage(left, top, _buttonBackground); - p.setPen(st->msgDateImgFg()->c); - _buttonText.drawLeftElided( - p, - left + padding.left(), - top + padding.top(), - width - padding.left() - padding.right(), - outer.width()); -} - -TextState ExtendedPreview::textState(QPoint point, StateRequest request) const { - auto result = TextState(_parent); - - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { - return result; - } - auto paintx = 0, painty = 0, paintw = width(), painth = height(); - auto bubble = _parent->hasBubble(); - if (QRect(paintx, painty, paintw, painth).contains(point)) { - result.link = _spoiler.link; - } - if (!bubble && _parent->media() == this) { - auto fullRight = paintx + paintw; - auto fullBottom = painty + painth; - const auto bottomInfoResult = _parent->bottomInfoTextState( - fullRight, - fullBottom, - point, - InfoDisplayType::Image); - if (bottomInfoResult.link - || bottomInfoResult.cursor != CursorState::None - || bottomInfoResult.customTooltip) { - return bottomInfoResult; - } - if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { - auto fastShareLeft = _parent->hasRightLayout() - ? (paintx - size->width() - st::historyFastShareLeft) - : (fullRight + st::historyFastShareLeft); - auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); - if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) { - result.link = _parent->rightActionLink(point - - QPoint(fastShareLeft, fastShareTop)); - } - } - } - return result; -} - -bool ExtendedPreview::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const { - return p == _spoiler.link; -} - -bool ExtendedPreview::dragItemByHandler(const ClickHandlerPtr &p) const { - return p == _spoiler.link; -} - -bool ExtendedPreview::needInfoDisplay() const { - return _parent->data()->isSending() - || _parent->data()->hasFailed() - || _parent->isUnderCursor() - || (_parent->delegate()->elementContext() == Context::ChatPreview) - || _parent->isLastAndSelfMessage(); -} - -bool ExtendedPreview::needsBubble() const { - const auto item = _parent->data(); - return !item->isService() - && (item->repliesAreComments() - || item->externalReply() - || item->viaBot() - || !item->emptyText() - || _parent->displayReply() - || _parent->displayForwardedFrom() - || _parent->displayFromName() - || _parent->displayedTopicButton()); -} - -QPoint ExtendedPreview::resolveCustomInfoRightBottom() const { - const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x()); - const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y()); - return QPoint(width() - skipx, height() - skipy); -} - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h deleted file mode 100644 index da2cd5d14ea8f..0000000000000 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.h +++ /dev/null @@ -1,87 +0,0 @@ -/* -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 "history/view/media/history_view_media.h" -#include "history/view/media/history_view_media_spoiler.h" - -enum class ImageRoundRadius; - -namespace Ui { -class SpoilerAnimation; -} // namespace Ui - -namespace Data { -struct Invoice; -} // namespace Data - -namespace HistoryView { - -class Element; - -class ExtendedPreview final : public Media { -public: - ExtendedPreview( - not_null parent, - not_null invoice); - ~ExtendedPreview(); - - bool hideMessageText() const override { - return false; - } - - void draw(Painter &p, const PaintContext &context) const override; - TextState textState(QPoint point, StateRequest request) const override; - - [[nodiscard]] bool toggleSelectionByHandlerClick( - const ClickHandlerPtr &p) const override; - [[nodiscard]] bool dragItemByHandler( - const ClickHandlerPtr &p) const override; - - bool needsBubble() const override; - bool customInfoLayout() const override { - return true; - } - QPoint resolveCustomInfoRightBottom() const override; - bool skipBubbleTail() const override { - return isRoundedInBubbleBottom(); - } - - bool hasHeavyPart() const override; - void unloadHeavyPart() override; - bool enforceBubbleWidth() const override; - -private: - int minWidthForButton() const; - void resolveButtonText(); - void ensureThumbnailRead() const; - - QSize countOptimalSize() override; - QSize countCurrentSize(int newWidth) override; - - bool needInfoDisplay() const; - void validateImageCache( - QSize outer, - std::optional rounding) const; - [[nodiscard]] QImage prepareImageCache(QSize outer) const; - void paintButton( - Painter &p, - QRect outer, - const PaintContext &context) const; - - const not_null _invoice; - mutable MediaSpoiler _spoiler; - mutable QImage _inlineThumbnail; - mutable QImage _buttonBackground; - mutable QColor _buttonBackgroundOverlay; - mutable Ui::Text::String _buttonText; - mutable bool _imageCacheInvalid = false; - -}; - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 20748d9fa09c7..ffe0d7757b5fc 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -191,6 +191,8 @@ Gif::Gif( } } ensureTranscribeButton(); + + _purchasedPriceTag = hasPurchasedTag(); } Gif::~Gif() { @@ -663,6 +665,10 @@ void Gif::draw(Painter &p, const PaintContext &context) const { const auto skipDrawingSurrounding = context.skipDrawingParts == PaintContext::SkipDrawingParts::Surrounding; + if (!skipDrawingSurrounding && _purchasedPriceTag) { + drawPurchasedTag(p, rthumb, context); + } + if (!unwrapped && !skipDrawingSurrounding) { if (!isRound || !inWebPage) { drawCornerStatus(p, context, QPoint()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index f0460bf0d5814..f86421f9268f3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -213,6 +213,7 @@ class Gif final : public File { mutable bool _thumbCacheBlurred : 1 = false; mutable bool _thumbIsEllipse : 1 = false; mutable bool _pollingStory : 1 = false; + mutable bool _purchasedPriceTag : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 310af8fa6a0bb..f2f98d05e37c8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -200,9 +200,11 @@ auto GenerateGiveawayResults( margins, links)); }; + const auto isSingleWinner = (data->winnersCount == 1); pushText( - Ui::Text::Bold( - tr::lng_prizes_results_title(tr::now)), + (isSingleWinner + ? tr::lng_prizes_results_title_one + : tr::lng_prizes_results_title)(tr::now, Ui::Text::Bold), st::chatGiveawayPrizesTitleMargin); const auto showGiveawayHandler = JumpToMessageClickHandler( data->channel, @@ -219,7 +221,9 @@ auto GenerateGiveawayResults( st::chatGiveawayPrizesMargin, { { 1, showGiveawayHandler } }); pushText( - Ui::Text::Bold(tr::lng_prizes_results_winners(tr::now)), + (isSingleWinner + ? tr::lng_prizes_results_winner + : tr::lng_prizes_results_winners)(tr::now, Ui::Text::Bold), st::chatGiveawayPrizesTitleMargin); push(std::make_unique( @@ -235,6 +239,8 @@ auto GenerateGiveawayResults( } pushText({ data->unclaimedCount ? tr::lng_prizes_results_some(tr::now) + : isSingleWinner + ? tr::lng_prizes_results_one(tr::now) : tr::lng_prizes_results_all(tr::now) }, st::chatGiveawayEndDateMargin); }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 4f8c8096b9703..ddd76f9455e8d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "history/view/media/history_view_media.h" +#include "boxes/send_credits_box.h" // CreditsEmoji. #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_element.h" @@ -18,12 +19,16 @@ For license and copyright information please follow this link: #include "data/data_document.h" #include "data/data_session.h" #include "data/data_web_page.h" +#include "lang/lang_tag.h" // FormatCountDecimal. #include "ui/item_text_options.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" #include "ui/effects/spoiler_mess.h" #include "ui/image/image_prepare.h" +#include "ui/cached_round_corners.h" +#include "ui/painter.h" #include "ui/power_saving.h" +#include "ui/text/text_utilities.h" #include "core/ui_integration.h" #include "styles/style_chat.h" @@ -202,6 +207,73 @@ QSize Media::countCurrentSize(int newWidth) { return QSize(qMin(newWidth, maxWidth()), minHeight()); } +bool Media::hasPurchasedTag() const { + if (const auto media = parent()->data()->media()) { + if (const auto invoice = media->invoice()) { + if (invoice->isPaidMedia && !invoice->extendedMedia.empty()) { + const auto photo = invoice->extendedMedia.front()->photo(); + return !photo || !photo->extendedMediaPreview(); + } + } + } + return false; +} + +void Media::drawPurchasedTag( + Painter &p, + QRect outer, + const PaintContext &context) const { + const auto purchased = parent()->enforcePurchasedTag(); + if (purchased->text.isEmpty()) { + const auto item = parent()->data(); + const auto media = item->media(); + const auto invoice = media ? media->invoice() : nullptr; + const auto amount = invoice ? invoice->amount : 0; + if (!amount) { + return; + } + const auto session = &item->history()->session(); + auto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall(session)); + text.append(Lang::FormatCountDecimal(amount)); + purchased->text.setMarkedText(st::defaultTextStyle, text, kMarkupTextOptions, Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }); + } + + const auto st = context.st; + const auto sti = context.imageStyle(); + const auto &padding = st::purchasedTagPadding; + auto right = outer.x() + outer.width(); + auto top = outer.y(); + right -= st::msgDateImgDelta + padding.right(); + top += st::msgDateImgDelta + padding.top(); + + const auto size = QSize( + purchased->text.maxWidth(), + st::normalFont->height); + const auto tagX = right - size.width(); + const auto tagY = top; + const auto tagW = padding.left() + size.width() + padding.right(); + const auto tagH = padding.top() + size.height() + padding.bottom(); + Ui::FillRoundRect( + p, + tagX - padding.left(), + tagY - padding.top(), + tagW, + tagH, + sti->msgDateImgBg, + sti->msgDateImgBgCorners); + + p.setPen(st->msgDateImgFg()); + purchased->text.draw(p, { + .position = { tagX, tagY }, + .outerWidth = width(), + .availableWidth = size.width(), + .palette = &st->priceTagTextPalette(), + }); +} + void Media::fillImageShadow( QPainter &p, QRect rect, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 4926ca7c8bc85..94ad6520b9312 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -223,6 +223,20 @@ class Media : public Object, public base::has_weak_ptr { QPoint point, StateRequest request) const; + virtual void drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual ClickHandlerPtr priceTagLink() const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual QImage priceTagBackground() const { + Unexpected("Price tag method call."); + } + [[nodiscard]] virtual bool animating() const { return false; } @@ -343,6 +357,12 @@ class Media : public Object, public base::has_weak_ptr { return false; } + [[nodiscard]] bool hasPurchasedTag() const; + void drawPurchasedTag( + Painter &p, + QRect outer, + const PaintContext &context) const; + virtual ~Media() = default; protected: diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index 273262e8dc6a2..dd32fa00f96aa 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -7,9 +7,14 @@ For license and copyright information please follow this link: */ #include "history/view/media/history_view_media_common.h" +#include "api/api_views.h" +#include "apiwrap.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" +#include "core/click_handler_types.h" #include "data/data_document.h" +#include "data/data_session.h" #include "data/data_wall_paper.h" #include "data/data_media_types.h" #include "history/view/history_view_element.h" @@ -19,10 +24,23 @@ For license and copyright information please follow this link: #include "history/view/media/history_view_document.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_theme_document.h" +#include "history/history_item.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwindow.h" #include "media/streaming/media_streaming_utility.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_non_panel_process.h" +#include "window/window_session_controller.h" #include "styles/style_chat.h" namespace HistoryView { +namespace { + +constexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000); + +} // namespace void PaintInterpolatedIcon( QPainter &p, @@ -180,4 +198,63 @@ QSize CountPhotoMediaSize( media.scaled(media.width(), newWidth, Qt::KeepAspectRatio)); } +void ShowPaidMediaUnlockedToast( + not_null controller, + not_null item) { + const auto media = item->media(); + const auto invoice = media ? media->invoice() : nullptr; + if (!invoice || !invoice->isPaidMedia) { + return; + } + const auto sender = item->originalSender(); + const auto broadcast = (sender && sender->isBroadcast()) + ? sender + : item->history()->peer.get(); + auto text = tr::lng_credits_media_done_title( + tr::now, + Ui::Text::Bold + ).append('\n').append(tr::lng_credits_media_done_text( + tr::now, + lt_count, + invoice->amount, + lt_chat, + Ui::Text::Bold(broadcast->name()), + Ui::Text::RichLangValue)); + controller->showToast(std::move(text), kMediaUnlockedTooltipDuration); +} + +ClickHandlerPtr MakePaidMediaLink(not_null item) { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + const auto weak = my.sessionWindow; + const auto itemId = item->fullId(); + const auto session = &item->history()->session(); + using Result = Payments::CheckoutResult; + const auto done = crl::guard(session, [=](Result result) { + if (result != Result::Paid) { + return; + } else if (const auto item = session->data().message(itemId)) { + session->api().views().pollExtendedMedia(item, true); + if (const auto strong = weak.get()) { + ShowPaidMediaUnlockedToast(strong, item); + } + } + }); + Payments::CheckoutProcess::Start( + item, + Payments::Mode::Payment, + (controller + ? crl::guard( + controller, + [=](auto) { controller->widget()->activate(); }) + : Fn()), + ((controller && Payments::IsCreditsInvoice(item)) + ? Payments::ProcessNonPanelPaymentFormFactory( + controller, + done) + : nullptr)); + }); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.h b/Telegram/SourceFiles/history/view/media/history_view_media_common.h index 2a46f5b3eed51..a37d0ab62decb 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.h @@ -75,4 +75,6 @@ void PaintInterpolatedIcon( int newWidth, int maxWidth); +[[nodiscard]] ClickHandlerPtr MakePaidMediaLink(not_null item); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index 6a67bf426dfca..4b26222ee3fff 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -17,6 +17,7 @@ For license and copyright information please follow this link: #include "data/data_session.h" #include "storage/storage_shared_media.h" #include "lang/lang_keys.h" +#include "media/streaming/media_streaming_utility.h" #include "ui/grouped_layout.h" #include "ui/chat/chat_style.h" #include "ui/chat/message_bubble.h" @@ -103,7 +104,11 @@ HistoryItem *GroupedMedia::itemForText() const { auto result = (HistoryItem*)nullptr; for (const auto &part : _parts) { if (!part.item->emptyText()) { - if (result) { + if (result == part.item) { + // All parts are from the same message, that means + // this is an album with a single item, single text. + return result; + } else if (result) { return nullptr; } else { result = part.item; @@ -128,6 +133,8 @@ GroupedMedia::Mode GroupedMedia::DetectMode(not_null media) { } QSize GroupedMedia::countOptimalSize() { + _purchasedPriceTag = hasPurchasedTag(); + std::vector sizes; const auto partsCount = _parts.size(); sizes.reserve(partsCount); @@ -290,6 +297,42 @@ QMargins GroupedMedia::groupedPadding() const { (normal.bottom() - grouped.bottom()) + addToBottom); } +Media *GroupedMedia::lookupUnpaidMedia() const { + if (_parts.empty()) { + return nullptr; + } + const auto media = _parts.front().content.get(); + const auto photo = media ? media->getPhoto() : nullptr; + return (photo && photo->extendedMediaPreview()) ? media : nullptr; +} + +QImage GroupedMedia::generatePriceTagBackground(QRect full) const { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage( + full.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + auto p = QPainter(&result); + const auto shift = -full.topLeft(); + const auto skip1 = st::historyGroupSkip / 2; + const auto skip2 = st::historyGroupSkip - skip1; + for (const auto &part : _parts) { + auto background = part.content->priceTagBackground(); + const auto extended = part.geometry.translated(shift).marginsAdded( + { skip1, skip1, skip2, skip2 }); + if (background.isNull()) { + p.fillRect(extended, Qt::black); + } else { + p.drawImage(extended, background); + } + } + p.end(); + + return ::Media::Streaming::PrepareBlurredBackground( + full.size(), + std::move(result)); +} + void GroupedMedia::drawHighlight( Painter &p, const PaintContext &context, @@ -351,6 +394,8 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { ? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall } : adjustedBubbleRounding(); auto highlight = context.highlight.range; + const auto unpaid = lookupUnpaidMedia(); + auto fullRect = QRect(); const auto subpartHighlight = IsSubGroupSelection(highlight); for (auto i = 0, count = int(_parts.size()); i != count; ++i) { const auto &part = _parts[i]; @@ -390,11 +435,22 @@ void GroupedMedia::draw(Painter &p, const PaintContext &context) const { if (!part.cache.isNull()) { nowCache = true; } + if (unpaid || _purchasedPriceTag) { + fullRect = fullRect.united(part.geometry); + } } if (nowCache && !wasCache) { history()->owner().registerHeavyViewPart(_parent); } + if (unpaid) { + unpaid->drawPriceTag(p, fullRect, context, [&] { + return generatePriceTagBackground(fullRect); + }); + } else if (_purchasedPriceTag) { + drawPurchasedTag(p, fullRect, context); + } + // date if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); @@ -455,6 +511,11 @@ PointState GroupedMedia::pointState(QPoint point) const { TextState GroupedMedia::textState(QPoint point, StateRequest request) const { const auto groupPadding = groupedPadding(); auto result = getPartState(point - QPoint(0, groupPadding.top()), request); + if (const auto unpaid = lookupUnpaidMedia()) { + if (QRect(0, 0, width(), height()).contains(point)) { + result.link = unpaid->priceTagLink(); + } + } if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) { auto fullRight = width(); auto fullBottom = height(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h index 84dca4884a428..5397b681786f5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h @@ -149,10 +149,14 @@ class GroupedMedia : public Media { RectParts sides) const; [[nodiscard]] QMargins groupedPadding() const; + [[nodiscard]] Media *lookupUnpaidMedia() const; + [[nodiscard]] QImage generatePriceTagBackground(QRect full) const; + mutable std::optional _captionItem; std::vector _parts; Mode _mode = Mode::Grid; - bool _needBubble = false; + bool _needBubble : 1 = false; + bool _purchasedPriceTag : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 2205625d88cca..0438830350353 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "history/view/media/history_view_photo.h" +#include "boxes/send_credits_box.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" @@ -14,6 +15,7 @@ For license and copyright information please follow this link: #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" +#include "lang/lang_keys.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_document.h" @@ -23,6 +25,7 @@ For license and copyright information please follow this link: #include "ui/image/image.h" #include "ui/effects/spoiler_mess.h" #include "ui/chat/chat_style.h" +#include "ui/text/text_utilities.h" #include "ui/grouped_layout.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" @@ -37,7 +40,9 @@ For license and copyright information please follow this link: #include "data/data_auto_download.h" #include "data/data_web_page.h" #include "core/application.h" +#include "core/ui_integration.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -58,6 +63,15 @@ struct Photo::Streamed { QImage roundingMask; }; +struct Photo::PriceTag { + uint64 price = 0; + QImage cache; + QColor darken; + QColor fg; + QColor star; + ClickHandlerPtr link; +}; + Photo::Streamed::Streamed( std::shared_ptr<::Media::Streaming::Document> shared) : instance(std::move(shared), nullptr) { @@ -125,6 +139,7 @@ void Photo::create(FullMsgId contextId, PeerData *chat) { if (_spoiler) { createSpoilerLink(_spoiler.get()); } + _purchasedPriceTag = hasPurchasedTag() ? 1 : 0; } void Photo::ensureDataMediaCreated() const { @@ -140,7 +155,8 @@ void Photo::dataMediaCreated() const { if (_data->inlineThumbnailBytes().isEmpty() && !_dataMedia->image(PhotoSize::Large) - && !_dataMedia->image(PhotoSize::Thumbnail)) { + && !_dataMedia->image(PhotoSize::Thumbnail) + && !_data->extendedMediaPreview()) { _dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); @@ -219,10 +235,14 @@ QSize Photo::countCurrentSize(int newWidth) { : st::minPhotoSize), thumbMaxWidth); const auto dimensions = photoSize(); - auto pix = CountPhotoMediaSize( - CountDesiredMediaSize(dimensions), - newWidth, - maxWidth()); + auto pix = _data->extendedMediaVideoDuration() + ? CountMediaSize( + CountDesiredMediaSize(dimensions), + newWidth) + : CountPhotoMediaSize( + CountDesiredMediaSize(dimensions), + newWidth, + maxWidth()); newWidth = qMax(pix.width(), minWidth); auto newHeight = qMax(pix.height(), st::minPhotoSize); if (_parent->hasBubble()) { @@ -273,8 +293,9 @@ void Photo::draw(Painter &p, const PaintContext &context) const { _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); const auto st = context.st; const auto sti = context.imageStyle(); - auto loaded = _dataMedia->loaded(); - auto displayLoading = _data->displayLoading(); + const auto preview = _data->extendedMediaPreview(); + const auto loaded = preview || _dataMedia->loaded(); + const auto displayLoading = !preview && _data->displayLoading(); auto inWebPage = (_parent->media() != this); auto paintx = 0, painty = 0, paintw = width(), painth = height(); @@ -361,6 +382,10 @@ void Photo::draw(Painter &p, const PaintContext &context) const { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } + } else if (preview) { + drawPriceTag(p, rthumb, context, [&] { + return priceTagBackground(); + }); } if (showEnlarge) { auto hq = PainterHighQualityEnabler(p); @@ -369,6 +394,14 @@ void Photo::draw(Painter &p, const PaintContext &context) const { p.drawRoundedRect(rect, radius, radius); sti->historyPageEnlarge.paintInCenter(p, rect); } + if (_purchasedPriceTag) { + auto geometry = rthumb; + if (showEnlarge) { + const auto rect = enlargeRect(); + geometry.setY(rect.y() + rect.height()); + } + drawPurchasedTag(p, geometry, context); + } // date if (!inWebPage && (!bubble || isBubbleBottom())) { @@ -393,6 +426,107 @@ void Photo::draw(Painter &p, const PaintContext &context) const { } } +void Photo::setupPriceTag() const { + const auto media = parent()->data()->media(); + const auto invoice = media ? media->invoice() : nullptr; + const auto price = invoice->isPaidMedia ? invoice->amount : 0; + if (!price) { + return; + } + _priceTag = std::make_unique(); + _priceTag->price = price; +} + +void Photo::drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const { + if (!_priceTag) { + setupPriceTag(); + if (!_priceTag) { + return; + } + } + const auto st = context.st; + const auto darken = st->msgDateImgBg()->c; + const auto fg = st->msgDateImgFg()->c; + const auto star = st->creditsBg1()->c; + if (_priceTag->cache.isNull() + || _priceTag->darken != darken + || _priceTag->fg != fg + || _priceTag->star != star) { + const auto ratio = style::DevicePixelRatio(); + auto bg = generateBackground(); + if (bg.isNull()) { + bg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied); + bg.fill(Qt::black); + } + + auto text = Ui::Text::String(); + const auto session = &history()->session(); + auto price = Ui::Text::Colorized(Ui::CreditsEmoji(session)); + price.append(Lang::FormatCountDecimal(_priceTag->price)); + text.setMarkedText( + st::semiboldTextStyle, + tr::lng_paid_price( + tr::now, + lt_price, + price, + Ui::Text::WithEntities), + kMarkupTextOptions, + Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }); + const auto width = text.maxWidth(); + const auto inner = QRect(0, 0, width, text.minHeight()); + const auto outer = inner.marginsAdded(st::paidTagPadding); + const auto size = outer.size(); + const auto radius = std::min(size.width(), size.height()) / 2; + auto cache = QImage( + size * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(ratio); + cache.fill(Qt::black); + auto p = Painter(&cache); + auto hq = PainterHighQualityEnabler(p); + p.drawImage( + QRect( + (size.width() - rthumb.width()) / 2, + (size.height() - rthumb.height()) / 2, + rthumb.width(), + rthumb.height()), + bg); + p.fillRect(QRect(QPoint(), size), darken); + p.setPen(fg); + p.setTextPalette(st->priceTagTextPalette()); + text.draw(p, -outer.x(), -outer.y(), width); + p.end(); + + _priceTag->darken = darken; + _priceTag->fg = fg; + _priceTag->cache = Images::Round( + std::move(cache), + Images::CornersMask(radius)); + } + const auto &cache = _priceTag->cache; + const auto size = cache.size() / cache.devicePixelRatio(); + const auto left = rthumb.x() + (rthumb.width() - size.width()) / 2; + const auto top = rthumb.y() + (rthumb.height() - size.height()) / 2; + p.drawImage(left, top, cache); + if (context.selected()) { + auto hq = PainterHighQualityEnabler(p); + const auto radius = std::min(size.width(), size.height()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(st->msgSelectOverlay()); + p.drawRoundedRect( + QRect(left, top, size.width(), size.height()), + radius, + radius); + } +} + void Photo::validateUserpicImageCache(QSize size, bool forum) const { const auto forumValue = forum ? 1 : 0; const auto large = _dataMedia->image(PhotoSize::Large); @@ -600,6 +734,26 @@ QRect Photo::enlargeRect() const { }; } +ClickHandlerPtr Photo::priceTagLink() const { + const auto item = parent()->data(); + if (!item->isRegular()) { + return nullptr; + } else if (!_priceTag) { + setupPriceTag(); + if (!_priceTag) { + return nullptr; + } + } + if (!_priceTag->link) { + _priceTag->link = MakePaidMediaLink(item); + } + return _priceTag->link; +} + +QImage Photo::priceTagBackground() const { + return _spoiler ? _spoiler->background : QImage(); +} + TextState Photo::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); @@ -613,7 +767,9 @@ TextState Photo::textState(QPoint point, StateRequest request) const { if (QRect(paintx, painty, paintw, painth).contains(point)) { ensureDataMediaCreated(); - result.link = (_spoiler && !_spoiler->revealed) + result.link = _data->extendedMediaPreview() + ? priceTagLink() + : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell @@ -678,8 +834,9 @@ void Photo::drawGrouped( const auto st = context.st; const auto sti = context.imageStyle(); - const auto loaded = _dataMedia->loaded(); - const auto displayLoading = _data->displayLoading(); + const auto preview = _data->extendedMediaPreview(); + const auto loaded = preview || _dataMedia->loaded(); + const auto displayLoading = !preview && _data->displayLoading(); if (displayLoading) { ensureAnimation(); @@ -784,7 +941,9 @@ TextState Photo::getStateGrouped( return {}; } ensureDataMediaCreated(); - return TextState(_parent, (_spoiler && !_spoiler->revealed) + return TextState(_parent, _data->extendedMediaPreview() + ? priceTagLink() + : (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell @@ -830,7 +989,8 @@ void Photo::validateGroupedCache( ensureDataMediaCreated(); - const auto loaded = _dataMedia->loaded(); + const auto preview = _data->extendedMediaPreview(); + const auto loaded = preview || _dataMedia->loaded(); const auto loadLevel = loaded ? 2 : (_dataMedia->thumbnailInline() diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 44fbed00c7a48..3c1aec3e4811d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -75,6 +75,14 @@ class Photo final : public File { QPoint point, StateRequest request) const override; + void drawPriceTag( + Painter &p, + QRect rthumb, + const PaintContext &context, + Fn generateBackground) const override; + ClickHandlerPtr priceTagLink() const override; + QImage priceTagBackground() const override; + void hideSpoilers() override; bool needsBubble() const override; bool customInfoLayout() const override { @@ -97,6 +105,7 @@ class Photo final : public File { private: struct Streamed; + struct PriceTag; void create(FullMsgId contextId, PeerData *chat = nullptr); @@ -106,6 +115,7 @@ class Photo final : public File { void ensureDataMediaCreated() const; void dataMediaCreated() const; + void setupPriceTag() const; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; @@ -151,12 +161,14 @@ class Photo final : public File { const not_null _data; const FullStoryId _storyId; + mutable std::unique_ptr _priceTag; mutable std::shared_ptr _dataMedia; mutable std::unique_ptr _streamed; const std::unique_ptr _spoiler; mutable QImage _imageCache; mutable std::optional _imageCacheRounding; - uint32 _serviceWidth : 28 = 0; + uint32 _serviceWidth : 27 = 0; + uint32 _purchasedPriceTag : 1 = 0; mutable uint32 _imageCacheForum : 1 = 0; mutable uint32 _imageCacheBlurred : 1 = 0; mutable uint32 _pollingStory : 1 = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp index 05a03df4dc03a..aa14b12c1b5f9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_similar_channels.cpp @@ -167,7 +167,7 @@ void SimilarChannels::draw(Painter &p, const PaintContext &context) const { _hasHeavyPart = 1; validateLastPremiumLock(); const auto drawOne = [&](const Channel &channel) { - const auto geometry = channel.geometry.translated(-_scrollLeft, 0); + const auto geometry = channel.geometry.translated(-int(_scrollLeft), 0); const auto right = geometry.x() + geometry.width(); if (right <= 0) { return; @@ -501,7 +501,7 @@ TextState SimilarChannels::textState( return result; } for (const auto &channel : _channels) { - if (channel.geometry.translated(-_scrollLeft, 0).contains(point)) { + if (channel.geometry.translated(-int(_scrollLeft), 0).contains(point)) { result.link = channel.link; _lastPoint = point + QPoint(_scrollLeft, 0) diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 11ba8209435f2..2fc888c6f0c63 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -16,6 +16,7 @@ For license and copyright information please follow this link: #include "ui/text/text_utilities.h" #include "ui/platform/ui_platform_utility.h" #include "ui/painter.h" +#include "history/view/media/history_view_sticker.h" #include "history/history.h" #include "history/history_item.h" #include "data/data_document.h" @@ -351,6 +352,16 @@ int Selector::countWidth(int desiredWidth, int maxWidth) { return std::max(2 * _skipx + _columns * _size, desiredWidth); } +int Selector::effectPreviewHeight() const { + if (_listMode != ChatHelpers::EmojiListMode::MessageEffects) { + return 0; + } + return st::previewMenu.shadow.extend.top() + + HistoryView::Sticker::MessageEffectSize().height() + + st::effectPreviewSend.height + + st::previewMenu.shadow.extend.bottom(); +} + QMargins Selector::marginsForShadow() const { const auto line = st::lineWidth; return useTransparency() @@ -409,9 +420,33 @@ void Selector::setOpaqueHeightExpand(int expand, Fn apply) { _opaqueApplyHeightExpand = std::move(apply); } -int Selector::minimalHeight() const { +int Selector::minimalHeight(int fullWidth) const { + auto inner = _recentRows * _size; + if (const auto stickers = int(_reactions.stickers.size())) { + // See StickersListWidget. + const auto listWidth = fullWidth + - marginsForShadow().left() + - marginsForShadow().right() + - _st.margin.left() + - _st.margin.right(); + const auto availableWidth = listWidth + - (st::stickerPanPadding - _st.margin.left()); + const auto min = st::stickerEffectWidthMin; + if (const auto columns = availableWidth / min) { + const auto rows = (stickers + columns - 1) / columns; + const auto singleWidth = availableWidth / columns; + const auto singleHeight = singleWidth; + const auto stickersHeight = rows * singleHeight; + inner += _st.header + stickersHeight; + } + } + if (_listMode == ChatHelpers::EmojiListMode::MessageEffects) { + inner += _st.searchMargin.top() + + _st.search.height + + _st.searchMargin.bottom(); + } return _skipy - + std::min(_recentRows * _size, st::emojiPanMinHeight) + + std::min(inner, st::emojiPanMinHeight) + st::emojiPanRadius + _st.padding.bottom(); } @@ -919,7 +954,7 @@ void Selector::expand() { const auto margins = marginsForShadow(); const auto heightLimit = _reactions.customAllowed ? st::emojiPanMaxHeight - : minimalHeight(); + : minimalHeight(width()); const auto opaqueAdded = _useTransparency ? 0 : _opaqueHeightExpand; const auto willBeHeight = std::min( parent.height() - y() + opaqueAdded, @@ -1116,7 +1151,13 @@ void Selector::createList() { _list->searchQueries( ) | rpl::start_with_next([=](std::vector &&query) { _stickers->applySearchQuery(std::move(query)); - updateVisibleTopBottom(); + }, _stickers->lifetime()); + + rpl::combine( + _list->heightValue(), + _stickers->heightValue() + ) | rpl::start_with_next([=] { + InvokeQueued(lists, updateVisibleTopBottom); }, _stickers->lifetime()); rpl::combine( @@ -1164,9 +1205,9 @@ bool AdjustMenuGeometryForSelector( menu->setForceWidth(width - added); const auto height = menu->height(); const auto fullTop = margins.top() + categoriesAboutTop + extend.top(); - const auto minimalHeight = margins.top() - + selector->minimalHeight() - + margins.bottom(); + const auto minimalHeight = std::max( + margins.top() + selector->minimalHeight(width) + margins.bottom(), + selector->effectPreviewHeight()); const auto willBeHeightWithoutBottomPadding = fullTop + height - menu->st().shadow.extend.top(); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index 56f5210b78dea..107dbfdc77641 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -102,11 +102,12 @@ class Selector final : public Ui::RpWidget { [[nodiscard]] bool useTransparency() const; int countWidth(int desiredWidth, int maxWidth); + [[nodiscard]] int effectPreviewHeight() const; [[nodiscard]] QMargins marginsForShadow() const; [[nodiscard]] int extendTopForCategories() const; [[nodiscard]] int extendTopForCategoriesAndAbout(int width) const; [[nodiscard]] int opaqueExtendTopAbout(int width) const; - [[nodiscard]] int minimalHeight() const; + [[nodiscard]] int minimalHeight(int fullWidth) const; [[nodiscard]] int countAppearedWidth(float64 progress) const; void setSpecialExpandTopSkip(int skip); void initGeometry(int innerTop); diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp new file mode 100644 index 0000000000000..169fe8883b5a9 --- /dev/null +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -0,0 +1,460 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "info/bot/earn/info_bot_earn_list.h" + +#include "api/api_credits.h" +#include "api/api_filter_updates.h" +#include "base/unixtime.h" +#include "core/ui_integration.h" +#include "data/data_channel_earn.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "info/bot/earn/info_bot_earn_widget.h" +#include "info/channel_statistics/earn/earn_format.h" +#include "info/info_controller.h" +#include "info/statistics/info_statistics_inner_widget.h" // FillLoading. +#include "info/statistics/info_statistics_list_controllers.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "settings/settings_credits_graphics.h" +#include "statistics/chart_widget.h" +#include "statistics/widgets/chart_header_widget.h" +#include "ui/effects/credits_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/toast/toast.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/label_with_custom_emoji.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/slider_natural_width.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_channel_earn.h" +#include "styles/style_credits.h" +#include "styles/style_settings.h" +#include "styles/style_statistics.h" + +namespace Info::BotEarn { +namespace { + +void AddHeader( + not_null content, + tr::phrase<> text) { + Ui::AddSkip(content); + const auto header = content->add( + object_ptr( + content, + text(), + st::channelEarnHeaderLabel), + st::boxRowPadding); + header->resizeToWidth(header->width()); +} + +} // namespace + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + not_null peer) +: VerticalLayout(parent) +, _controller(controller) +, _peer(peer) +, _show(controller->uiShow()) { +} + +void InnerWidget::load() { + const auto apiLifetime = lifetime().make_state(); + + const auto request = [=](Fn done) { + const auto api = apiLifetime->make_state( + _peer->asUser()); + api->request( + ) | rpl::start_with_error_done([show = _show](const QString &error) { + show->showToast(error); + }, [=] { + done(api->data()); + apiLifetime->destroy(); + }, *apiLifetime); + }; + + Info::Statistics::FillLoading( + this, + _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), + _showFinished.events()); + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + request([=](Data::CreditsEarnStatistics state) { + _state = state; + _loaded.fire(true); + fill(); + + _peer->session().account().mtpUpdates( + ) | rpl::start_with_next([=](const MTPUpdates &updates) { + using TL = MTPDupdateStarsRevenueStatus; + Api::PerformForUpdate(updates, [&](const TL &d) { + const auto peerId = peerFromMTP(d.vpeer()); + if (peerId == _peer->id) { + request([=](Data::CreditsEarnStatistics state) { + _state = state; + _stateUpdated.fire({}); + }); + } + }); + }, lifetime()); + }); + }, lifetime()); +} + +void InnerWidget::fill() { + using namespace Info::ChannelEarn; + const auto container = this; + const auto &data = _state; + const auto multiplier = data.usdRate * Data::kEarnMultiplier; + + auto availableBalanceValue = rpl::single( + data.availableBalance + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.availableBalance; + }) + ); + auto valueToString = [](uint64 v) { return QString::number(v); }; + + if (data.revenueGraph.chart) { + Ui::AddSkip(container); + Ui::AddSkip(container); + using Type = Statistic::ChartViewType; + const auto widget = container->add( + object_ptr(container), + st::statisticsLayerMargins); + + auto chart = data.revenueGraph.chart; + chart.currencyRate = data.usdRate; + + widget->setChartData(chart, Type::StackBar); + widget->setTitle(tr::lng_bot_earn_chart_revenue()); + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + Ui::AddSkip(container); + } + { + AddHeader(container, tr::lng_bot_earn_overview_title); + Ui::AddSkip(container, st::channelEarnOverviewTitleSkip); + + const auto addOverview = [&]( + rpl::producer value, + const tr::phrase<> &text) { + const auto line = container->add( + Ui::CreateSkipWidget(container, 0), + st::boxRowPadding); + const auto majorLabel = Ui::CreateChild( + line, + rpl::duplicate(value) | rpl::map(valueToString), + st::channelEarnOverviewMajorLabel); + const auto icon = Ui::CreateSingleStarWidget( + line, + majorLabel->height()); + const auto secondMinorLabel = Ui::CreateChild( + line, + std::move( + value + ) | rpl::map([=](uint64 v) { + return v ? ToUsd(v, multiplier) : QString(); + }), + st::channelEarnOverviewSubMinorLabel); + rpl::combine( + line->widthValue(), + majorLabel->sizeValue() + ) | rpl::start_with_next([=](int available, const QSize &size) { + line->resize(line->width(), size.height()); + majorLabel->moveToLeft( + icon->width() + st::channelEarnOverviewMinorLabelSkip, + majorLabel->y()); + secondMinorLabel->resizeToWidth(available + - size.width() + - icon->width()); + secondMinorLabel->moveToLeft( + rect::right(majorLabel) + + st::channelEarnOverviewSubMinorLabelPos.x(), + st::channelEarnOverviewSubMinorLabelPos.y()); + }, majorLabel->lifetime()); + Ui::ToggleChildrenVisibility(line, true); + + Ui::AddSkip(container); + container->add( + object_ptr( + container, + text(), + st::channelEarnOverviewSubMinorLabel), + st::boxRowPadding); + }; + addOverview( + rpl::duplicate(availableBalanceValue), + tr::lng_bot_earn_available); + Ui::AddSkip(container); + Ui::AddSkip(container); + // addOverview(data.currentBalance, tr::lng_bot_earn_reward); + // Ui::AddSkip(container); + // Ui::AddSkip(container); + addOverview( + rpl::single( + data.overallRevenue + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.overallRevenue; + }) + ), + tr::lng_bot_earn_total); + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddDividerText(container, tr::lng_bot_earn_balance_about()); + Ui::AddSkip(container); + } + { + AddHeader(container, tr::lng_bot_earn_balance_title); + auto dateValue = rpl::single( + data.nextWithdrawalAt + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.nextWithdrawalAt; + }) + ); + ::Settings::AddWithdrawalWidget( + container, + _controller->parentController(), + _peer, + rpl::single( + data.buyAdsUrl + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.buyAdsUrl; + }) + ), + rpl::duplicate(availableBalanceValue), + rpl::duplicate(dateValue), + std::move(dateValue) | rpl::map([=](const QDateTime &dt) { + return !dt.isNull() || (!_state.isWithdrawalEnabled); + }), + rpl::duplicate(availableBalanceValue) | rpl::map([=](uint64 v) { + return v ? ToUsd(v, multiplier) : QString(); + })); + } + + fillHistory(); +} + +void InnerWidget::fillHistory() { + const auto container = this; + Ui::AddSkip(container, st::settingsPremiumOptionsPadding.top()); + const auto history = container->add( + object_ptr(container)); + + const auto sectionIndex = history->lifetime().make_state(0); + + const auto fill = [=]( + not_null premiumBot, + const Data::CreditsStatusSlice &fullSlice, + const Data::CreditsStatusSlice &inSlice, + const Data::CreditsStatusSlice &outSlice) { + if (fullSlice.list.empty()) { + return; + } + const auto inner = history->add( + object_ptr(history)); + const auto hasOneTab = inSlice.list.empty() && outSlice.list.empty(); + const auto hasIn = !inSlice.list.empty(); + const auto hasOut = !outSlice.list.empty(); + const auto fullTabText = tr::lng_credits_summary_history_tab_full( + tr::now); + const auto inTabText = tr::lng_credits_summary_history_tab_in( + tr::now); + const auto outTabText = tr::lng_credits_summary_history_tab_out( + tr::now); + if (hasOneTab) { + Ui::AddSkip(inner); + const auto header = inner->add( + object_ptr(inner), + st::statisticsLayerMargins + + st::boostsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(fullTabText); + header->setSubTitle({}); + } + + const auto slider = inner->add( + object_ptr>( + inner, + object_ptr( + inner, + st::defaultTabsSlider)), + st::boxRowPadding); + slider->toggle(!hasOneTab, anim::type::instant); + + slider->entity()->addSection(fullTabText); + if (hasIn) { + slider->entity()->addSection(inTabText); + } + if (hasOut) { + slider->entity()->addSection(outTabText); + } + + slider->entity()->setActiveSectionFast(*sectionIndex); + + { + const auto &st = st::defaultTabsSlider; + slider->entity()->setNaturalWidth(0 + + st.labelStyle.font->width(fullTabText) + + (hasIn ? st.labelStyle.font->width(inTabText) : 0) + + (hasOut ? st.labelStyle.font->width(outTabText) : 0) + + rect::m::sum::h(st::boxRowPadding)); + } + + const auto fullWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto inWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto outWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + + rpl::single(slider->entity()->activeSection()) | rpl::then( + slider->entity()->sectionActivated() + ) | rpl::start_with_next([=](int index) { + if (index == 0) { + fullWrap->toggle(true, anim::type::instant); + inWrap->toggle(false, anim::type::instant); + outWrap->toggle(false, anim::type::instant); + } else if (index == 1) { + inWrap->toggle(true, anim::type::instant); + fullWrap->toggle(false, anim::type::instant); + outWrap->toggle(false, anim::type::instant); + } else { + outWrap->toggle(true, anim::type::instant); + fullWrap->toggle(false, anim::type::instant); + inWrap->toggle(false, anim::type::instant); + } + *sectionIndex = index; + }, inner->lifetime()); + + const auto controller = _controller->parentController(); + const auto entryClicked = [=](const Data::CreditsHistoryEntry &e) { + controller->uiShow()->show(Box( + ::Settings::ReceiptCreditsBox, + controller, + premiumBot.get(), + e)); + }; + + const auto star = lifetime().make_state( + Ui::GenerateStars(st::creditsTopupButton.height, 1)); + + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + fullSlice, + fullWrap->entity(), + entryClicked, + premiumBot, + star, + true, + true); + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + inSlice, + inWrap->entity(), + entryClicked, + premiumBot, + star, + true, + false); + Info::Statistics::AddCreditsHistoryList( + controller->uiShow(), + outSlice, + outWrap->entity(), + std::move(entryClicked), + premiumBot, + star, + false, + true); + + Ui::AddSkip(inner); + Ui::AddSkip(inner); + }; + + const auto apiLifetime = history->lifetime().make_state(); + rpl::single(rpl::empty) | rpl::then( + _stateUpdated.events() + ) | rpl::start_with_next([=] { + using Api = Api::CreditsHistory; + const auto apiFull = apiLifetime->make_state(_peer, true, true); + const auto apiIn = apiLifetime->make_state(_peer, true, false); + const auto apiOut = apiLifetime->make_state(_peer, false, true); + apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) { + apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) { + apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) { + ::Api::PremiumPeerBot( + &_controller->session() + ) | rpl::start_with_next([=](not_null bot) { + fill(bot, fullSlice, inSlice, outSlice); + container->resizeToWidth(container->width()); + while (history->count() > 1) { + delete history->widgetAt(0); + } + apiLifetime->destroy(); + }, *apiLifetime); + }); + }); + }); + }, history->lifetime()); +} + +void InnerWidget::saveState(not_null memento) { + memento->setState(base::take(_state)); +} + +void InnerWidget::restoreState(not_null memento) { + _state = memento->state(); + if (_state) { + fill(); + } else { + load(); + } + Ui::RpWidget::resizeToWidth(width()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +auto InnerWidget::showRequests() const -> rpl::producer { + return _showRequests.events(); +} + +void InnerWidget::showFinished() { + _showFinished.fire({}); +} + +void InnerWidget::setInnerFocus() { + _focusRequested.fire({}); +} + +not_null InnerWidget::peer() const { + return _peer; +} + +} // namespace Info::BotEarn + diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h new file mode 100644 index 0000000000000..7157c4f0277d5 --- /dev/null +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h @@ -0,0 +1,71 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_credits_earn.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/vertical_layout.h" + +namespace Ui { +class Show; +} // namespace Ui + +namespace Info { +class Controller; +} // namespace Info + +namespace Info::BotEarn { + +class Memento; + +[[nodiscard]] QImage IconCurrency( + const style::FlatLabel &label, + const QColor &c); + +class InnerWidget final : public Ui::VerticalLayout { +public: + struct ShowRequest final { + }; + + InnerWidget( + QWidget *parent, + not_null controller, + not_null peer); + + [[nodiscard]] not_null peer() const; + + [[nodiscard]] rpl::producer scrollToRequests() const; + [[nodiscard]] rpl::producer showRequests() const; + + void showFinished(); + void setInnerFocus(); + + void saveState(not_null memento); + void restoreState(not_null memento); + +private: + void load(); + void fill(); + void fillHistory(); + + not_null _controller; + not_null _peer; + std::shared_ptr _show; + + Data::CreditsEarnStatistics _state; + + rpl::event_stream _scrollToRequests; + rpl::event_stream _showRequests; + rpl::event_stream<> _showFinished; + rpl::event_stream<> _focusRequested; + rpl::event_stream _loaded; + rpl::event_stream<> _stateUpdated; + +}; + +} // namespace Info::BotEarn diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp new file mode 100644 index 0000000000000..03ad190ce5c80 --- /dev/null +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp @@ -0,0 +1,125 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "info/bot/earn/info_bot_earn_widget.h" + +#include "info/bot/earn/info_bot_earn_list.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "lang/lang_keys.h" + +namespace Info::BotEarn { + +Memento::Memento(not_null controller) +: ContentMemento(Info::Statistics::Tag{ + controller->statisticsPeer(), + {}, + {}, +}) { +} + +Memento::Memento(not_null peer) +: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) { +} + +Memento::~Memento() = default; + +Section Memento::section() const { + return Section(Section::Type::BotEarn); +} + +void Memento::setState(SavedState state) { + _state = std::move(state); +} + +Memento::SavedState Memento::state() { + return base::take(_state); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +Widget::Widget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) +, _inner(setInnerWidget( + object_ptr( + this, + controller, + controller->statisticsPeer()))) { + _inner->showRequests( + ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { + }, _inner->lifetime()); + _inner->scrollToRequests( + ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { + scrollTo(request); + }, _inner->lifetime()); +} + +not_null Widget::peer() const { + return _inner->peer(); +} + +bool Widget::showInternal(not_null memento) { + return (memento->statisticsPeer() == peer()); +} + +rpl::producer Widget::title() { + return tr::lng_bot_earn_title(); +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +rpl::producer Widget::desiredShadowVisibility() const { + return rpl::single(true); +} + +void Widget::showFinished() { + _inner->showFinished(); +} + +void Widget::setInnerFocus() { + _inner->setInnerFocus(); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void Widget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void Widget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +std::shared_ptr Make(not_null peer) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer))); +} + +} // namespace Info::BotEarn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.h b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h similarity index 91% rename from Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.h rename to Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h index 73b3737696df0..37ab50e941d4f 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.h +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h @@ -7,10 +7,10 @@ For license and copyright information please follow this link: */ #pragma once -#include "data/data_channel_earn.h" +#include "data/data_credits_earn.h" #include "info/info_content_widget.h" -namespace Info::ChannelEarn { +namespace Info::BotEarn { class InnerWidget; @@ -27,7 +27,7 @@ class Memento final : public ContentMemento { Section section() const override; - using SavedState = Data::EarnStatistics; + using SavedState = Data::CreditsEarnStatistics; void setState(SavedState states); [[nodiscard]] SavedState state(); @@ -65,4 +65,4 @@ class Widget final : public ContentWidget { [[nodiscard]] std::shared_ptr Make(not_null peer); -} // namespace Info::ChannelEarn +} // namespace Info::BotEarn diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.cpp index 15c72c5f22786..6234ab0100d94 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_inner_widget.cpp @@ -32,8 +32,8 @@ For license and copyright information please follow this link: #include "ui/rect.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" -#include "ui/widgets/discrete_sliders.h" #include "ui/widgets/labels.h" +#include "ui/widgets/slider_natural_width.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" @@ -423,25 +423,12 @@ void InnerWidget::fill() { header->setSubTitle({}); } - class Slider final : public Ui::SettingsSlider { - public: - using Ui::SettingsSlider::SettingsSlider; - void setNaturalWidth(int w) { - _naturalWidth = w; - } - int naturalWidth() const override { - return _naturalWidth; - } - - private: - int _naturalWidth = 0; - - }; - const auto slider = inner->add( - object_ptr>( + object_ptr>( inner, - object_ptr(inner, st::defaultTabsSlider)), + object_ptr( + inner, + st::defaultTabsSlider)), st::boxRowPadding); slider->toggle(!hasOneTab, anim::type::instant); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style index 6c23041416d94..ab88fa2bac89b 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style +++ b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style @@ -136,3 +136,16 @@ sponsoredReportLabel: FlatLabel(defaultFlatLabel) { style: boxTextStyle; minWidth: 150px; } + +botEarnInputField: InputField(defaultInputField) { + textMargins: margins(23px, 28px, 0px, 4px); + placeholderMargins: margins(-23px, 0px, 0px, 0px); + width: 100px; + heightMax: 55px; +} +botEarnLockedButtonLabel: FlatLabel(channelEarnOverviewMajorLabel) { + style: TextStyle(defaultTextStyle) { + font: font(10px semibold); + } +} +botEarnButtonLockMargins: margins(-2px, 4px, 0px, 0px); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp similarity index 50% rename from Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp rename to Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index a151443e62dde..c16acc204c45b 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -5,10 +5,14 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "info/channel_statistics/earn/info_earn_inner_widget.h" +#include "info/channel_statistics/earn/info_channel_earn_list.h" +#include "api/api_credits.h" #include "api/api_earn.h" +#include "api/api_filter_updates.h" #include "api/api_statistics.h" +#include "api/api_text_entities.h" +#include "api/api_updates.h" #include "base/unixtime.h" #include "boxes/peers/edit_peer_color_box.h" // AddLevelBadge. #include "chat_helpers/stickers_emoji_pack.h" @@ -20,20 +24,24 @@ For license and copyright information please follow this link: #include "data/stickers/data_custom_emoji.h" #include "history/view/controls/history_view_webpage_processor.h" #include "info/channel_statistics/earn/earn_format.h" -#include "info/channel_statistics/earn/info_earn_widget.h" +#include "info/channel_statistics/earn/info_channel_earn_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" // Info::Profile::NameValue. #include "info/statistics/info_statistics_inner_widget.h" // FillLoading. +#include "info/statistics/info_statistics_list_controllers.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" +#include "main/main_account.h" #include "main/main_app_config.h" #include "main/main_session.h" +#include "settings/settings_credits_graphics.h" #include "statistics/chart_widget.h" #include "ui/basic_click_handlers.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/boxes/boost_box.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/animation_value_f.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/toggle_arrow.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" @@ -41,7 +49,9 @@ For license and copyright information please follow this link: #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/slider_natural_width.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_boxes.h" #include "styles/style_channel_earn.h" @@ -50,6 +60,7 @@ For license and copyright information please follow this link: #include "styles/style_layers.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" +#include "styles/style_credits.h" #include "styles/style_window.h" // mainMenuToggleFourStrokes. #include @@ -256,34 +267,151 @@ InnerWidget::InnerWidget( } void InnerWidget::load() { - const auto api = lifetime().make_state( - _peer->asChannel()); + struct State final { + State(not_null peer) + : api(peer->asChannel()) + , apiCredits(peer) + , apiCreditsHistory(peer, true, true) { + } + Api::ChannelEarnStatistics api; + Api::CreditsEarnStatistics apiCredits; + Api::CreditsHistory apiCreditsHistory; + rpl::lifetime apiLifetime; + rpl::lifetime apiCreditsLifetime; + rpl::lifetime apiPremiumBotLifetime; + }; + const auto state = lifetime().make_state(_peer); + using ChannelFlag = ChannelDataFlag; + const auto canViewCredits = !_peer->isChannel() + || (_peer->asChannel()->flags() & ChannelFlag::CanViewCreditsRevenue); Info::Statistics::FillLoading( this, _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), _showFinished.events()); + const auto show = _controller->uiShow(); + const auto fail = [=](const QString &error) { show->showToast(error); }; + + const auto finish = [=] { + _loaded.fire(true); + fill(); + state->apiLifetime.destroy(); + state->apiCreditsLifetime.destroy(); + + _peer->session().account().mtpUpdates( + ) | rpl::start_with_next([=, peerId = _peer->id]( + const MTPUpdates &updates) { + using TLCreditsUpdate = MTPDupdateStarsRevenueStatus; + using TLCurrencyUpdate = MTPDupdateBroadcastRevenueTransactions; + using TLNotificationUpdate = MTPDupdateServiceNotification; + Api::PerformForUpdate(updates, [&]( + const TLCreditsUpdate &d) { + if (peerId != peerFromMTP(d.vpeer())) { + return; + } + const auto &data = d.vstatus().data(); + auto &e = _state.creditsEarn; + e.currentBalance = data.vcurrent_balance().v; + e.availableBalance = data.vavailable_balance().v; + e.overallRevenue = data.voverall_revenue().v; + e.overallRevenue = data.voverall_revenue().v; + e.isWithdrawalEnabled = data.is_withdrawal_enabled(); + e.nextWithdrawalAt = data.vnext_withdrawal_at() + ? base::unixtime::parse( + data.vnext_withdrawal_at()->v) + : QDateTime(); + state->apiCreditsHistory.request({}, [=]( + const Data::CreditsStatusSlice &data) { + _state.creditsStatusSlice = data; + _stateUpdated.fire({}); + }); + }); + Api::PerformForUpdate(updates, [&]( + const TLCurrencyUpdate &d) { + if (peerId == peerFromMTP(d.vpeer())) { + const auto &data = d.vbalances().data(); + auto &e = _state.currencyEarn; + e.currentBalance = data.vcurrent_balance().v; + e.availableBalance = data.vavailable_balance().v; + e.overallRevenue = data.voverall_revenue().v; + _stateUpdated.fire({}); + } + }); + Api::PerformForUpdate(updates, [&]( + const TLNotificationUpdate &d) { + if (Api::IsWithdrawalNotification(d) && d.is_popup()) { + show->show(Ui::MakeInformBox(TextWithEntities{ + qs(d.vmessage()), + Api::EntitiesFromMTP(& + _peer->session(), + d.ventities().v), + })); + } + }); + }, lifetime()); + }; + _showFinished.events( ) | rpl::take(1) | rpl::start_with_next([=] { - api->request( - ) | rpl::start_with_error_done([](const QString &error) { - }, [=] { - _state = api->data(); - _loaded.fire(true); - fill(); - }, lifetime()); + state->api.request( + ) | rpl::start_with_error_done(fail, [=] { + _state.currencyEarn = state->api.data(); + state->apiCreditsHistory.request({}, [=]( + const Data::CreditsStatusSlice &data) { + _state.creditsStatusSlice = data; + ::Api::PremiumPeerBot( + &_peer->session() + ) | rpl::start_with_next([=](not_null bot) { + _state.premiumBotId = bot->id; + state->apiCredits.request( + ) | rpl::start_with_error_done([=](const QString &error) { + if (canViewCredits) { + fail(error); + } else { + _state.creditsEarn = {}; + } + finish(); + }, [=] { + _state.creditsEarn = state->apiCredits.data(); + finish(); + }, state->apiCreditsLifetime); + state->apiPremiumBotLifetime.destroy(); + }, state->apiPremiumBotLifetime); + }); + }, state->apiLifetime); }, lifetime()); } void InnerWidget::fill() { const auto container = this; - const auto &data = _state; + const auto &data = _state.currencyEarn; + const auto &creditsData = _state.creditsEarn; + + auto currencyStateValue = rpl::single( + data + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.currencyEarn; + }) + ); + + auto creditsStateValue = rpl::single( + creditsData + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { return _state.creditsEarn; }) + ); constexpr auto kMinus = QChar(0x2212); //constexpr auto kApproximately = QChar(0x2248); const auto multiplier = data.usdRate; + const auto creditsToUsdMap = [=](EarnInt c) { + const auto creditsMultiplier = _state.creditsEarn.usdRate + * Data::kEarnMultiplier; + return c ? ToUsd(c, creditsMultiplier) : QString(); + }; + constexpr auto kNonInteractivePeriod = 1717200000; const auto nonInteractive = base::unixtime::now() < kNonInteractivePeriod; @@ -299,7 +427,7 @@ void InnerWidget::fill() { }; const auto addEmojiToMajor = [=]( not_null label, - EarnInt value, + rpl::producer value, std::optional isIn, std::optional margins) { const auto &st = label->st(); @@ -314,12 +442,16 @@ void InnerWidget::fill() { : st::menuIconAttentionColor->c), margins ? *margins : st::channelEarnCurrencyCommonMargins, false)); - auto prepended = !isIn + const auto prepended = !isIn ? TextWithEntities() : TextWithEntities::Simple((*isIn) ? QChar('+') : kMinus); - label->setMarkedText( - prepended.append(icon).append(MajorPart(value)), - makeContext(label)); + std::move( + value + ) | rpl::start_with_next([=](EarnInt v) { + label->setMarkedText( + base::duplicate(prepended).append(icon).append(MajorPart(v)), + makeContext(label)); + }, label->lifetime()); }; const auto bigCurrencyIcon = Ui::Text::SingleCustomEmoji( @@ -345,8 +477,7 @@ void InnerWidget::fill() { ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link(std::move(text), 1); }), - Ui::Text::RichLangValue - ), + Ui::Text::RichLangValue), { .session = session }, st::boxDividerLabel); label->setLink(1, std::make_shared([=] { @@ -530,6 +661,7 @@ void InnerWidget::fill() { using Type = Statistic::ChartViewType; Ui::AddSkip(container); Ui::AddSkip(container); + auto hasPreviousChart = false; if (data.topHoursGraph.chart) { const auto widget = container->add( object_ptr(container), @@ -537,24 +669,49 @@ void InnerWidget::fill() { widget->setChartData(data.topHoursGraph.chart, Type::Bar); widget->setTitle(tr::lng_channel_earn_chart_top_hours()); + hasPreviousChart = true; } if (data.revenueGraph.chart) { - Ui::AddSkip(container); - Ui::AddDivider(container); - Ui::AddSkip(container); - Ui::AddSkip(container); + if (hasPreviousChart) { + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + Ui::AddSkip(container); + } const auto widget = container->add( object_ptr(container), st::statisticsLayerMargins); - auto chart = data.revenueGraph.chart; - chart.currencyRate = multiplier; - - widget->setChartData(chart, Type::StackBar); + widget->setChartData([&] { + auto chart = data.revenueGraph.chart; + chart.currencyRate = multiplier; + return chart; + }(), Type::StackBar); widget->setTitle(tr::lng_channel_earn_chart_revenue()); + hasPreviousChart = true; + } + if (creditsData.revenueGraph.chart) { + if (hasPreviousChart) { + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + Ui::AddSkip(container); + } + const auto widget = container->add( + object_ptr(container), + st::statisticsLayerMargins); + + widget->setChartData([&] { + auto chart = creditsData.revenueGraph.chart; + chart.currencyRate = creditsData.usdRate; + return chart; + }(), Type::StackBar); + widget->setTitle(tr::lng_bot_earn_chart_revenue()); } } - if (data.topHoursGraph.chart || data.revenueGraph.chart) { + if (data.topHoursGraph.chart + || data.revenueGraph.chart + || creditsData.revenueGraph.chart) { Ui::AddSkip(container); Ui::AddSkip(container); Ui::AddDivider(container); @@ -565,38 +722,85 @@ void InnerWidget::fill() { Ui::AddSkip(container, st::channelEarnOverviewTitleSkip); const auto addOverview = [&]( - EarnInt value, - const tr::phrase<> &text) { + rpl::producer currencyValue, + rpl::producer creditsValue, + const tr::phrase<> &text, + bool showCredits) { const auto line = container->add( Ui::CreateSkipWidget(container, 0), st::boxRowPadding); const auto majorLabel = Ui::CreateChild( line, st::channelEarnOverviewMajorLabel); - addEmojiToMajor(majorLabel, value, {}, {}); + addEmojiToMajor( + majorLabel, + rpl::duplicate(currencyValue), + {}, + {}); const auto minorLabel = Ui::CreateChild( line, - MinorPart(value), + rpl::duplicate(currencyValue) | rpl::map(MinorPart), st::channelEarnOverviewMinorLabel); const auto secondMinorLabel = Ui::CreateChild( line, - value ? ToUsd(value, multiplier) : QString(), + std::move( + currencyValue + ) | rpl::map([=](EarnInt value) { + return value ? ToUsd(value, multiplier) : QString(); + }), + st::channelEarnOverviewSubMinorLabel); + + const auto creditsLabel = Ui::CreateChild( + line, + rpl::duplicate(creditsValue) | rpl::map([](EarnInt value) { + return QString::number(value); + }), + st::channelEarnOverviewMajorLabel); + const auto icon = Ui::CreateSingleStarWidget( + line, + creditsLabel->height()); + const auto creditsSecondLabel = Ui::CreateChild( + line, + rpl::duplicate(creditsValue) | rpl::map(creditsToUsdMap), st::channelEarnOverviewSubMinorLabel); rpl::combine( line->widthValue(), - majorLabel->sizeValue() - ) | rpl::start_with_next([=](int available, const QSize &size) { + majorLabel->sizeValue(), + creditsLabel->sizeValue(), + std::move(creditsValue) + ) | rpl::start_with_next([=]( + int available, + const QSize &size, + const QSize &creditsSize, + EarnInt credits) { + const auto skip = st::channelEarnOverviewSubMinorLabelPos.x(); line->resize(line->width(), size.height()); minorLabel->moveToLeft( size.width(), st::channelEarnOverviewMinorLabelSkip); - secondMinorLabel->resizeToWidth(available - - size.width() - - minorLabel->width()); + secondMinorLabel->resizeToWidth( + (showCredits ? (available / 2) : available) + - size.width() + - minorLabel->width()); secondMinorLabel->moveToLeft( - rect::right(minorLabel) - + st::channelEarnOverviewSubMinorLabelPos.x(), + rect::right(minorLabel) + skip, + st::channelEarnOverviewSubMinorLabelPos.y()); + + icon->moveToLeft( + available / 2 + st::boxRowPadding.left() / 2, + 0); + creditsLabel->moveToLeft(rect::right(icon) + skip, 0); + creditsSecondLabel->moveToLeft( + rect::right(creditsLabel) + skip, st::channelEarnOverviewSubMinorLabelPos.y()); + creditsSecondLabel->resizeToWidth( + available - creditsSecondLabel->pos().x()); + if (!showCredits) { + const auto x = std::numeric_limits::max(); + icon->moveToLeft(x, 0); + creditsLabel->moveToLeft(x, 0); + creditsSecondLabel->moveToLeft(x, 0); + } }, minorLabel->lifetime()); Ui::ToggleChildrenVisibility(line, true); @@ -609,13 +813,31 @@ void InnerWidget::fill() { st::boxRowPadding); sub->setTextColorOverride(st::windowSubTextFg->c); }; - addOverview(data.availableBalance, tr::lng_channel_earn_available); + auto availValueMap = [](const auto &v) { return v.availableBalance; }; + auto currentValueMap = [](const auto &v) { return v.currentBalance; }; + auto overallValueMap = [](const auto &v) { return v.overallRevenue; }; + const auto hasAnyCredits = creditsData.availableBalance + || creditsData.currentBalance + || creditsData.overallRevenue; + addOverview( + rpl::duplicate(currencyStateValue) | rpl::map(availValueMap), + rpl::duplicate(creditsStateValue) | rpl::map(availValueMap), + tr::lng_channel_earn_available, + hasAnyCredits); Ui::AddSkip(container); Ui::AddSkip(container); - addOverview(data.currentBalance, tr::lng_channel_earn_reward); + addOverview( + rpl::duplicate(currencyStateValue) | rpl::map(currentValueMap), + rpl::duplicate(creditsStateValue) | rpl::map(currentValueMap), + tr::lng_channel_earn_reward, + hasAnyCredits); Ui::AddSkip(container); Ui::AddSkip(container); - addOverview(data.overallRevenue, tr::lng_channel_earn_total); + addOverview( + rpl::duplicate(currencyStateValue) | rpl::map(overallValueMap), + rpl::duplicate(creditsStateValue) | rpl::map(overallValueMap), + tr::lng_channel_earn_total, + hasAnyCredits); Ui::AddSkip(container); } #ifndef _DEBUG @@ -644,7 +866,7 @@ void InnerWidget::fill() { { const auto &m = st::channelEarnCurrencyCommonMargins; const auto p = QMargins(m.left(), 0, m.right(), m.bottom()); - addEmojiToMajor(majorLabel, value, {}, p); + addEmojiToMajor(majorLabel, rpl::single(value), {}, p); } majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); const auto minorLabel = Ui::CreateChild( @@ -715,7 +937,10 @@ void InnerWidget::fill() { !withdrawalEnabled); #endif - Api::HandleWithdrawalButton(channel, button, _controller->uiShow()); + Api::HandleWithdrawalButton( + { .currencyReceiver = channel }, + button, + _controller->uiShow()); Ui::ToggleChildrenVisibility(button, true); Ui::AddSkip(container); @@ -725,320 +950,502 @@ void InnerWidget::fill() { : tr::lng_channel_earn_balance_about_temp); Ui::AddSkip(container); } - if (!data.firstHistorySlice.list.empty()) { - AddHeader(container, tr::lng_channel_earn_history_title); - Ui::AddSkip(container); + if (creditsData.availableBalance > 0) { + AddHeader(container, tr::lng_bot_earn_balance_title); + auto availableBalanceValue = rpl::single( + creditsData.availableBalance + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.creditsEarn.availableBalance; + }) + ); + auto dateValue = rpl::single( + creditsData.nextWithdrawalAt + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.creditsEarn.nextWithdrawalAt; + }) + ); + ::Settings::AddWithdrawalWidget( + container, + _controller->parentController(), + _peer, + rpl::single( + creditsData.buyAdsUrl + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.creditsEarn.buyAdsUrl; + }) + ), + rpl::duplicate(availableBalanceValue), + rpl::duplicate(dateValue), + std::move(dateValue) | rpl::map([=](const QDateTime &dt) { + return !dt.isNull() + || (!_state.creditsEarn.isWithdrawalEnabled); + }), + rpl::duplicate( + availableBalanceValue + ) | rpl::map(creditsToUsdMap)); + } - const auto historyList = container->add( - object_ptr(container)); - const auto addHistoryEntry = [=]( - const Data::EarnHistoryEntry &entry, - const tr::phrase<> &text) { - const auto wrap = historyList->add( - object_ptr>( - historyList, - object_ptr(historyList), - QMargins())); - const auto inner = wrap->entity(); - inner->setAttribute(Qt::WA_TransparentForMouseEvents); - inner->add(object_ptr( - inner, - text(), - st::channelEarnSemiboldLabel)); - - const auto isIn = entry.type == Data::EarnHistoryEntry::Type::In; - const auto recipient = Ui::Text::Wrapped( - { entry.provider }, - EntityType::Code); - if (!recipient.text.isEmpty()) { - Ui::AddSkip(inner, st::channelEarnHistoryThreeSkip); - const auto label = inner->add(object_ptr( - inner, - rpl::single(recipient), - st::channelEarnHistoryRecipientLabel)); - label->setBreakEverywhere(true); - label->setTryMakeSimilarLines(true); - Ui::AddSkip(inner, st::channelEarnHistoryThreeSkip); - } else { - Ui::AddSkip(inner, st::channelEarnHistoryTwoSkip); + const auto sectionIndex = container->lifetime().make_state(0); + const auto rebuildLists = [=]( + const Memento::SavedState &data, + not_null listsContainer) { + const auto hasCurrencyTab + = !data.currencyEarn.firstHistorySlice.list.empty(); + const auto hasCreditsTab = !data.creditsStatusSlice.list.empty() + && data.premiumBotId; + const auto hasOneTab = (hasCurrencyTab || hasCreditsTab) + && (hasCurrencyTab != hasCreditsTab); + + const auto currencyTabText = tr::lng_channel_earn_currency_history( + tr::now); + const auto creditsTabText = tr::lng_channel_earn_credits_history( + tr::now); + + const auto slider = listsContainer->add( + object_ptr>( + listsContainer, + object_ptr( + listsContainer, + st::defaultTabsSlider)), + st::boxRowPadding); + slider->toggle(!hasOneTab, anim::type::instant); + + if (hasCurrencyTab) { + slider->entity()->addSection(currencyTabText); + } + if (hasCreditsTab) { + slider->entity()->addSection(creditsTabText); + } + + { + const auto &st = st::defaultTabsSlider; + slider->entity()->setNaturalWidth(0 + + (hasCurrencyTab + ? st.labelStyle.font->width(currencyTabText) + : 0) + + (hasCreditsTab + ? st.labelStyle.font->width(creditsTabText) + : 0) + + rect::m::sum::h(st::boxRowPadding)); + } + + if (hasOneTab) { + if (hasCurrencyTab) { + AddHeader(listsContainer, tr::lng_channel_earn_history_title); + } else if (hasCreditsTab) { + AddHeader( + listsContainer, + tr::lng_channel_earn_credits_history); + slider->entity()->setActiveSectionFast(1); } + } else { + slider->entity()->setActiveSectionFast(*sectionIndex); + } - const auto isFailed = entry.status - == Data::EarnHistoryEntry::Status::Failed; - const auto isPending = entry.status - == Data::EarnHistoryEntry::Status::Pending; - const auto dateText = (!entry.dateTo.isNull() || isFailed) - ? (FormatDate(entry.date) - + ' ' - + QChar(8212) - + ' ' - + (isFailed - ? tr::lng_channel_earn_history_out_failed(tr::now) - : FormatDate(entry.dateTo))) - : isPending - ? tr::lng_channel_earn_history_pending(tr::now) - : FormatDate(entry.date); - inner->add(object_ptr( - inner, - dateText, - st::channelEarnHistorySubLabel) - )->setTextColorOverride(isFailed - ? std::make_optional(st::menuIconAttentionColor->c) - : std::nullopt); - - const auto color = (isIn - ? st::boxTextFgGood - : st::menuIconAttentionColor)->c; - const auto majorLabel = Ui::CreateChild( - wrap, - st::channelEarnHistoryMajorLabel); - addEmojiToMajor(majorLabel, entry.amount, isIn, {}); - majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - majorLabel->setTextColorOverride(color); - const auto minorText = MinorPart(entry.amount); - const auto minorLabel = Ui::CreateChild( - wrap, - rpl::single(minorText), - st::channelEarnHistoryMinorLabel); - minorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); - minorLabel->setTextColorOverride(color); - const auto button = Ui::CreateChild( - wrap, - rpl::single(QString())); - Ui::ToggleChildrenVisibility(wrap, true); - - const auto detailsBox = [=, amount = entry.amount, peer = _peer]( - not_null box) { - box->addTopButton( - st::boxTitleClose, - [=] { box->closeBox(); }); - Ui::AddSkip(box->verticalLayout()); - Ui::AddSkip(box->verticalLayout()); - const auto labels = box->addRow( - object_ptr>( - box, - object_ptr(box)))->entity(); + const auto tabCurrencyList = listsContainer->add( + object_ptr>( + listsContainer, + object_ptr(listsContainer))); + const auto tabCreditsList = listsContainer->add( + object_ptr>( + listsContainer, + object_ptr(listsContainer))); + + rpl::single(slider->entity()->activeSection()) | rpl::then( + slider->entity()->sectionActivated() + ) | rpl::start_with_next([=](int index) { + if (index == 0) { + tabCurrencyList->toggle(true, anim::type::instant); + tabCreditsList->toggle(false, anim::type::instant); + } else if (index == 1) { + tabCurrencyList->toggle(false, anim::type::instant); + tabCreditsList->toggle(true, anim::type::instant); + } + *sectionIndex = index; + }, listsContainer->lifetime()); + + if (hasCurrencyTab) { + Ui::AddSkip(listsContainer); + + const auto historyList = tabCurrencyList->entity(); + const auto addHistoryEntry = [=]( + const Data::EarnHistoryEntry &entry, + const tr::phrase<> &text) { + const auto wrap = historyList->add( + object_ptr>( + historyList, + object_ptr(historyList), + QMargins())); + const auto inner = wrap->entity(); + inner->setAttribute(Qt::WA_TransparentForMouseEvents); + inner->add(object_ptr( + inner, + text(), + st::channelEarnSemiboldLabel)); + const auto isIn + = (entry.type == Data::EarnHistoryEntry::Type::In); + const auto recipient = Ui::Text::Wrapped( + { entry.provider }, + EntityType::Code); + if (!recipient.text.isEmpty()) { + Ui::AddSkip(inner, st::channelEarnHistoryThreeSkip); + const auto label = inner->add(object_ptr( + inner, + rpl::single(recipient), + st::channelEarnHistoryRecipientLabel)); + label->setBreakEverywhere(true); + label->setTryMakeSimilarLines(true); + Ui::AddSkip(inner, st::channelEarnHistoryThreeSkip); + } else { + Ui::AddSkip(inner, st::channelEarnHistoryTwoSkip); + } + + const auto isFailed = entry.status + == Data::EarnHistoryEntry::Status::Failed; + const auto isPending = entry.status + == Data::EarnHistoryEntry::Status::Pending; + const auto dateText = (!entry.dateTo.isNull() || isFailed) + ? (FormatDate(entry.date) + + ' ' + + QChar(8212) + + ' ' + + (isFailed + ? tr::lng_channel_earn_history_out_failed(tr::now) + : FormatDate(entry.dateTo))) + : isPending + ? tr::lng_channel_earn_history_pending(tr::now) + : FormatDate(entry.date); + inner->add(object_ptr( + inner, + dateText, + st::channelEarnHistorySubLabel) + )->setTextColorOverride(isFailed + ? std::make_optional( + st::menuIconAttentionColor->c) + : std::nullopt); + + const auto color = (isIn + ? st::boxTextFgGood + : st::menuIconAttentionColor)->c; const auto majorLabel = Ui::CreateChild( - labels, - st::channelEarnOverviewMajorLabel); - addEmojiToMajor(majorLabel, amount, isIn, {}); + wrap, + st::channelEarnHistoryMajorLabel); + addEmojiToMajor( + majorLabel, + rpl::single(entry.amount), + isIn, + {}); majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); majorLabel->setTextColorOverride(color); + const auto minorText = MinorPart(entry.amount); const auto minorLabel = Ui::CreateChild( - labels, - minorText, - st::channelEarnOverviewMinorLabel); + wrap, + rpl::single(minorText), + st::channelEarnHistoryMinorLabel); minorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); minorLabel->setTextColorOverride(color); - rpl::combine( - majorLabel->sizeValue(), - minorLabel->sizeValue() - ) | rpl::start_with_next([=]( - const QSize &majorSize, - const QSize &minorSize) { - labels->resize( - majorSize.width() + minorSize.width(), - majorSize.height()); - majorLabel->moveToLeft(0, 0); - minorLabel->moveToRight( - 0, - st::channelEarnOverviewMinorLabelSkip); - }, box->lifetime()); - - Ui::AddSkip(box->verticalLayout()); - box->addRow(object_ptr>( - box, - object_ptr( - box, - dateText, - st::channelEarnHistorySubLabel))); - Ui::AddSkip(box->verticalLayout()); - Ui::AddSkip(box->verticalLayout()); - Ui::AddSkip(box->verticalLayout()); - box->addRow(object_ptr>( - box, - object_ptr( - box, - isIn - ? tr::lng_channel_earn_history_in_about() - : tr::lng_channel_earn_history_out(), - st::channelEarnHistoryDescriptionLabel))); - Ui::AddSkip(box->verticalLayout()); - if (isIn) { + const auto button = Ui::CreateChild( + wrap, + rpl::single(QString())); + Ui::ToggleChildrenVisibility(wrap, true); + + const auto detailsBox = [=, peer = _peer]( + not_null box) { + box->addTopButton( + st::boxTitleClose, + [=] { box->closeBox(); }); Ui::AddSkip(box->verticalLayout()); - } - - if (!recipient.text.isEmpty()) { - AddRecipient(box, recipient); - } - if (isIn) { - const auto peerBubble = box->addRow( - object_ptr>( + Ui::AddSkip(box->verticalLayout()); + const auto labels = box->addRow( + object_ptr>( box, object_ptr(box)))->entity(); - peerBubble->setAttribute( + + const auto majorLabel = Ui::CreateChild( + labels, + st::channelEarnOverviewMajorLabel); + addEmojiToMajor( + majorLabel, + rpl::single(entry.amount), + isIn, + {}); + majorLabel->setAttribute( Qt::WA_TransparentForMouseEvents); - const auto left = Ui::CreateChild( - peerBubble, - peer, - st::uploadUserpicButton); - const auto right = Ui::CreateChild( - peerBubble, - Info::Profile::NameValue(peer), - st::channelEarnSemiboldLabel); + majorLabel->setTextColorOverride(color); + const auto minorLabel = Ui::CreateChild( + labels, + minorText, + st::channelEarnOverviewMinorLabel); + minorLabel->setAttribute( + Qt::WA_TransparentForMouseEvents); + minorLabel->setTextColorOverride(color); rpl::combine( - left->sizeValue(), - right->sizeValue() + majorLabel->sizeValue(), + minorLabel->sizeValue() ) | rpl::start_with_next([=]( - const QSize &leftSize, - const QSize &rightSize) { - const auto padding = QMargins( - st::chatGiveawayPeerPadding.left() * 2, - st::chatGiveawayPeerPadding.top(), - st::chatGiveawayPeerPadding.right(), - st::chatGiveawayPeerPadding.bottom()); - peerBubble->resize( - leftSize.width() - + rightSize.width() - + rect::m::sum::h(padding), - leftSize.height()); - left->moveToLeft(0, 0); - right->moveToRight(padding.right(), padding.top()); - const auto maxRightSize = box->width() - - rect::m::sum::h(st::boxRowPadding) - - rect::m::sum::h(padding) - - leftSize.width(); - if (rightSize.width() > maxRightSize) { - right->resizeToWidth(maxRightSize); - } - }, peerBubble->lifetime()); - peerBubble->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(peerBubble); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::windowBgOver); - const auto rect = peerBubble->rect(); - const auto radius = rect.height() / 2; - p.drawRoundedRect(rect, radius, radius); - }, peerBubble->lifetime()); - } - { - const auto &st = st::premiumPreviewDoubledLimitsBox; - box->setStyle(st); - auto button = object_ptr( - container, - (!entry.successLink.isEmpty()) - ? tr::lng_channel_earn_history_out_button() - : tr::lng_box_ok(), - st::defaultActiveButton); - button->resizeToWidth(box->width() - - st.buttonPadding.left() - - st.buttonPadding.left()); - if (!entry.successLink.isEmpty()) { - button->setAcceptBoth(); - button->addClickHandler([=](Qt::MouseButton button) { - if (button == Qt::LeftButton) { - UrlClickHandler::Open(entry.successLink); - } else if (button == Qt::RightButton) { - ShowMenu(box, entry.successLink); + const QSize &majorSize, + const QSize &minorSize) { + labels->resize( + majorSize.width() + minorSize.width(), + majorSize.height()); + majorLabel->moveToLeft(0, 0); + minorLabel->moveToRight( + 0, + st::channelEarnOverviewMinorLabelSkip); + }, box->lifetime()); + + Ui::AddSkip(box->verticalLayout()); + box->addRow(object_ptr>( + box, + object_ptr( + box, + dateText, + st::channelEarnHistorySubLabel))); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + box->addRow(object_ptr>( + box, + object_ptr( + box, + isIn + ? tr::lng_channel_earn_history_in_about() + : tr::lng_channel_earn_history_out(), + st::channelEarnHistoryDescriptionLabel))); + Ui::AddSkip(box->verticalLayout()); + if (isIn) { + Ui::AddSkip(box->verticalLayout()); + } + + if (!recipient.text.isEmpty()) { + AddRecipient(box, recipient); + } + if (isIn) { + const auto peerBubble = box->addRow( + object_ptr>( + box, + object_ptr(box)))->entity(); + peerBubble->setAttribute( + Qt::WA_TransparentForMouseEvents); + const auto left = Ui::CreateChild( + peerBubble, + peer, + st::uploadUserpicButton); + const auto right = Ui::CreateChild( + peerBubble, + Info::Profile::NameValue(peer), + st::channelEarnSemiboldLabel); + rpl::combine( + left->sizeValue(), + right->sizeValue() + ) | rpl::start_with_next([=]( + const QSize &leftSize, + const QSize &rightSize) { + const auto padding = QMargins( + st::chatGiveawayPeerPadding.left() * 2, + st::chatGiveawayPeerPadding.top(), + st::chatGiveawayPeerPadding.right(), + st::chatGiveawayPeerPadding.bottom()); + peerBubble->resize( + leftSize.width() + + rightSize.width() + + rect::m::sum::h(padding), + leftSize.height()); + left->moveToLeft(0, 0); + right->moveToRight( + padding.right(), + padding.top()); + const auto maxRightSize = box->width() + - rect::m::sum::h(st::boxRowPadding) + - rect::m::sum::h(padding) + - leftSize.width(); + if (rightSize.width() > maxRightSize) { + right->resizeToWidth(maxRightSize); } - }); - } else { - button->setClickedCallback([=] { box->closeBox(); }); + }, peerBubble->lifetime()); + peerBubble->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(peerBubble); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgOver); + const auto rect = peerBubble->rect(); + const auto radius = rect.height() / 2; + p.drawRoundedRect(rect, radius, radius); + }, peerBubble->lifetime()); } - box->addButton(std::move(button)); - } - Ui::AddSkip(box->verticalLayout()); - Ui::AddSkip(box->verticalLayout()); - box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); - }; + const auto closeBox = [=] { box->closeBox(); }; + { + const auto &st = st::premiumPreviewDoubledLimitsBox; + box->setStyle(st); + auto button = object_ptr( + box, + (!entry.successLink.isEmpty()) + ? tr::lng_channel_earn_history_out_button() + : tr::lng_box_ok(), + st::defaultActiveButton); + button->resizeToWidth(box->width() + - st.buttonPadding.left() + - st.buttonPadding.left()); + if (!entry.successLink.isEmpty()) { + button->setAcceptBoth(); + button->addClickHandler([=]( + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + UrlClickHandler::Open(entry.successLink); + } else if (button == Qt::RightButton) { + ShowMenu(box, entry.successLink); + } + }); + } else { + button->setClickedCallback(closeBox); + } + box->addButton(std::move(button)); + } + Ui::AddSkip(box->verticalLayout()); + Ui::AddSkip(box->verticalLayout()); + box->addButton(tr::lng_box_ok(), closeBox); + }; - button->setClickedCallback([=] { - _show->showBox(Box(detailsBox)); - }); - wrap->geometryValue( - ) | rpl::start_with_next([=](const QRect &g) { - const auto &padding = st::boxRowPadding; - const auto majorTop = (g.height() - majorLabel->height()) / 2; - minorLabel->moveToRight( - padding.right(), - majorTop + st::channelEarnHistoryMinorLabelSkip); - majorLabel->moveToRight( - padding.right() + minorLabel->width(), - majorTop); - const auto rightWrapPadding = rect::m::sum::h(padding) - + minorLabel->width() - + majorLabel->width(); - wrap->setPadding( - st::channelEarnHistoryOuter + button->setClickedCallback([=] { + _show->showBox(Box(detailsBox)); + }); + wrap->geometryValue( + ) | rpl::start_with_next([=](const QRect &g) { + const auto &padding = st::boxRowPadding; + const auto majorTop = (g.height() - majorLabel->height()) + / 2; + minorLabel->moveToRight( + padding.right(), + majorTop + st::channelEarnHistoryMinorLabelSkip); + majorLabel->moveToRight( + padding.right() + minorLabel->width(), + majorTop); + const auto rightWrapPadding = rect::m::sum::h(padding) + + minorLabel->width() + + majorLabel->width(); + wrap->setPadding(st::channelEarnHistoryOuter + QMargins(padding.left(), 0, rightWrapPadding, 0)); - button->resize(g.size()); - button->lower(); - }, wrap->lifetime()); - }; - const auto handleSlice = [=](const Data::EarnHistorySlice &slice) { - for (const auto &entry : slice.list) { - addHistoryEntry( - entry, - (entry.type == Data::EarnHistoryEntry::Type::In) - ? tr::lng_channel_earn_history_in - : (entry.type == Data::EarnHistoryEntry::Type::Return) - ? tr::lng_channel_earn_history_return - : tr::lng_channel_earn_history_out); - } - historyList->resizeToWidth(container->width()); - }; - handleSlice(data.firstHistorySlice); - if (!data.firstHistorySlice.allLoaded) { - struct ShowMoreState final { - ShowMoreState(not_null channel) - : api(channel) { - } - Api::EarnStatistics api; - bool loading = false; - Data::EarnHistorySlice::OffsetToken token; - rpl::variable showed = 0; + button->resize(g.size()); + button->lower(); + }, wrap->lifetime()); }; - const auto state = lifetime().make_state(channel); - state->token = data.firstHistorySlice.token; - state->showed = data.firstHistorySlice.list.size(); - const auto max = data.firstHistorySlice.total; - const auto wrap = container->add( - object_ptr>( - container, - object_ptr( - container, - tr::lng_channel_earn_history_show_more( - lt_count, - state->showed.value( - ) | rpl::map( - max - rpl::mappers::_1 - ) | tr::to_count()), - st::statisticsShowMoreButton))); - const auto button = wrap->entity(); - AddArrow(button); - - wrap->toggle(true, anim::type::instant); - const auto handleReceived = [=](Data::EarnHistorySlice slice) { - state->loading = false; - handleSlice(slice); - wrap->toggle(!slice.allLoaded, anim::type::instant); - state->token = slice.token; - state->showed = state->showed.current() + slice.list.size(); + const auto handleSlice = [=](const Data::EarnHistorySlice &s) { + using Type = Data::EarnHistoryEntry::Type; + for (const auto &entry : s.list) { + addHistoryEntry( + entry, + (entry.type == Type::In) + ? tr::lng_channel_earn_history_in + : (entry.type == Type::Return) + ? tr::lng_channel_earn_history_return + : tr::lng_channel_earn_history_out); + } + historyList->resizeToWidth(listsContainer->width()); }; - button->setClickedCallback([=] { - if (!state->loading) { + const auto &firstSlice = data.currencyEarn.firstHistorySlice; + handleSlice(firstSlice); + if (!firstSlice.allLoaded) { + struct ShowMoreState final { + ShowMoreState(not_null channel) + : api(channel) { + } + Api::ChannelEarnStatistics api; + bool loading = false; + Data::EarnHistorySlice::OffsetToken token; + rpl::variable showed = 0; + }; + const auto state + = lifetime().make_state(channel); + state->token = firstSlice.token; + state->showed = firstSlice.list.size(); + const auto max = firstSlice.total; + const auto wrap = listsContainer->add( + object_ptr>( + listsContainer, + object_ptr( + listsContainer, + tr::lng_channel_earn_history_show_more( + lt_count, + state->showed.value( + ) | rpl::map( + max - rpl::mappers::_1 + ) | tr::to_count()), + st::statisticsShowMoreButton))); + const auto button = wrap->entity(); + AddArrow(button); + + wrap->toggle(true, anim::type::instant); + const auto handleReceived = [=]( + Data::EarnHistorySlice slice) { + state->loading = false; + handleSlice(slice); + wrap->toggle(!slice.allLoaded, anim::type::instant); + state->token = slice.token; + state->showed = state->showed.current() + + slice.list.size(); + }; + button->setClickedCallback([=] { + if (state->loading) { + return; + } state->loading = true; state->api.requestHistory(state->token, handleReceived); - } - }); + }); + } } - Ui::AddSkip(container); - Ui::AddDivider(container); - Ui::AddSkip(container); - } + if (hasCreditsTab) { + const auto controller = _controller->parentController(); + const auto show = controller->uiShow(); + const auto premiumBot = _peer->owner().peer(data.premiumBotId); + const auto entryClicked = [=]( + const Data::CreditsHistoryEntry &e) { + show->show(Box( + ::Settings::ReceiptCreditsBox, + controller, + premiumBot.get(), + e)); + }; + + const auto star = tabCreditsList->lifetime().make_state( + Ui::GenerateStars(st::creditsTopupButton.height, 1)); + + Info::Statistics::AddCreditsHistoryList( + show, + data.creditsStatusSlice, + tabCreditsList->entity(), + entryClicked, + premiumBot, + star, + true, + true); + } + if (hasCurrencyTab || hasCreditsTab) { + Ui::AddSkip(listsContainer); + Ui::AddDivider(listsContainer); + Ui::AddSkip(listsContainer); + } + + listsContainer->resizeToWidth(width()); + }; + + const auto historyContainer = container->add( + object_ptr(container)); + rpl::single(rpl::empty) | rpl::then( + _stateUpdated.events() + ) | rpl::start_with_next([=] { + const auto listsContainer = historyContainer->add( + object_ptr(container)); + rebuildLists(_state, listsContainer); + while (historyContainer->count() > 1) { + delete historyContainer->widgetAt(0); + } + }, historyContainer->lifetime()); + if (channel) { //constexpr auto kMaxCPM = 50; // Debug. const auto requiredLevel = Data::LevelLimits(session) @@ -1104,7 +1511,7 @@ void InnerWidget::saveState(not_null memento) { void InnerWidget::restoreState(not_null memento) { _state = memento->state(); - if (_state) { + if (_state.currencyEarn || _state.creditsEarn) { fill(); } else { load(); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h similarity index 91% rename from Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h rename to Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h index 99e9511be987d..061d69b7ed184 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h @@ -7,7 +7,7 @@ For license and copyright information please follow this link: */ #pragma once -#include "data/data_channel_earn.h" +#include "info/channel_statistics/earn/info_channel_earn_widget.h" #include "ui/widgets/scroll_area.h" #include "ui/wrap/vertical_layout.h" @@ -56,13 +56,14 @@ class InnerWidget final : public Ui::VerticalLayout { not_null _peer; std::shared_ptr _show; - Data::EarnStatistics _state; + Memento::SavedState _state; rpl::event_stream _scrollToRequests; rpl::event_stream _showRequests; rpl::event_stream<> _showFinished; rpl::event_stream<> _focusRequested; rpl::event_stream _loaded; + rpl::event_stream<> _stateUpdated; }; diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp similarity index 95% rename from Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.cpp rename to Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp index b0c38e1ade3e6..dabd5e6eaab61 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp @@ -5,9 +5,9 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "info/channel_statistics/earn/info_earn_widget.h" +#include "info/channel_statistics/earn/info_channel_earn_widget.h" -#include "info/channel_statistics/earn/info_earn_inner_widget.h" +#include "info/channel_statistics/earn/info_channel_earn_list.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "lang/lang_keys.h" diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h new file mode 100644 index 0000000000000..becd834c6a3f8 --- /dev/null +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.h @@ -0,0 +1,75 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_channel_earn.h" +#include "data/data_credits.h" +#include "data/data_credits_earn.h" +#include "info/info_content_widget.h" + +namespace Info::ChannelEarn { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento(not_null controller); + Memento(not_null peer); + ~Memento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + struct SavedState final { + Data::EarnStatistics currencyEarn; + Data::CreditsEarnStatistics creditsEarn; + Data::CreditsStatusSlice creditsStatusSlice; + PeerId premiumBotId = PeerId(0); + }; + + void setState(SavedState states); + [[nodiscard]] SavedState state(); + +private: + SavedState _state; + +}; + +class Widget final : public ContentWidget { +public: + Widget(QWidget *parent, not_null controller); + + bool showInternal(not_null memento) override; + rpl::producer title() override; + rpl::producer desiredShadowVisibility() const override; + void showFinished() override; + void setInnerFocus() override; + + [[nodiscard]] not_null peer() const; + + void setInternalState( + const QRect &geometry, + not_null memento); + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + const not_null _inner; + +}; + +[[nodiscard]] std::shared_ptr Make(not_null peer); + +} // namespace Info::ChannelEarn diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index ab16a1bd1d606..8e44a7a79cd64 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -701,7 +701,7 @@ manageGroupReactionsField: InputField(defaultInputField) { placeholderScale: 0.; placeholderFont: normalFont; placeholderShift: -50px; - font: normalFont; + style: defaultTextStyle; heightMin: 36px; heightMax: 158px; } @@ -764,8 +764,6 @@ editPeerDescription: InputField(defaultInputField) { borderActive: 0px; heightMin: 32px; - - font: boxTextFont; } editPeerDescriptionMargins: margins(22px, 3px, 22px, 2px); editPeerPrivaciesMargins: margins(15px, 7px, 22px, 0px); diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 628d50bca8bf6..de0337d269a41 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -135,6 +135,7 @@ class Section final { Statistics, Boosts, ChannelEarn, + BotEarn, }; using SettingsType = ::Settings::Type; using MediaType = Storage::SharedMediaType; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index f7e37ced57545..6e17ed54914ec 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1240,9 +1240,6 @@ object_ptr DetailsFiller::setupPersonalChannel( ) | rpl::map(rpl::mappers::_1 != nullptr)); result->finishAnimating(); - auto channelToggleValue = PersonalChannelValue( - user - ) | rpl::map([=] { return !!user->personalChannelId(); }); auto channel = PersonalChannelValue( user ) | rpl::start_spawning(result->lifetime()); @@ -1270,8 +1267,10 @@ object_ptr DetailsFiller::setupPersonalChannel( object_ptr>( container, object_ptr(container))); - onlyChannelWrap->toggleOn(rpl::duplicate(channelToggleValue) - | rpl::map(!rpl::mappers::_1)); + onlyChannelWrap->toggleOn(PersonalChannelValue(user) | rpl::map([=] { + return user->personalChannelId() + && !user->personalChannelMessageId(); + })); onlyChannelWrap->finishAnimating(); Ui::AddDivider(onlyChannelWrap->entity()); @@ -1312,7 +1311,12 @@ object_ptr DetailsFiller::setupPersonalChannel( object_ptr>( container, object_ptr(container))); - messageChannelWrap->toggleOn(rpl::duplicate(channelToggleValue)); + messageChannelWrap->toggleOn(PersonalChannelValue( + user + ) | rpl::map([=] { + return user->personalChannelId() + && user->personalChannelMessageId(); + })); messageChannelWrap->finishAnimating(); const auto clear = [=] { diff --git a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp index cfc911aaf9c11..ae77ec2e295b2 100644 --- a/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp +++ b/Telegram/SourceFiles/info/similar_channels/info_similar_channels_widget.cpp @@ -85,7 +85,10 @@ std::unique_ptr ListController::createRow( if (const auto channel = peer->asChannel()) { if (const auto count = channel->membersCount(); count > 1) { result->setCustomStatus( - tr::lng_chat_status_subscribers(tr::now, lt_count, count)); + tr::lng_chat_status_subscribers( + tr::now, + lt_count_decimal, + count)); } } return result; diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index f319f873b9339..a176be565d46e 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -45,6 +45,15 @@ using BoostCallback = Fn; constexpr auto kColorIndexUnclaimed = int(3); constexpr auto kColorIndexPending = int(4); +[[nodiscard]] PeerListRowId UniqueRowIdFromEntry( + const Data::CreditsHistoryEntry &entry) { + return UniqueRowIdFromString(entry.id + + (entry.refunded ? '1' : '0') + + (entry.pending ? '1' : '0') + + (entry.failed ? '1' : '0') + + (entry.in ? '1' : '0')); +} + void AddArrow(not_null parent) { const auto arrow = Ui::CreateChild(parent.get()); arrow->paintRequest( @@ -747,26 +756,30 @@ class CreditsRow final : public PeerListRow { QString _name; Ui::Text::String _rightText; + + base::has_weak_ptr _guard; }; -CreditsRow::CreditsRow(not_null peer, const Descriptor &descriptor) -: PeerListRow(peer, UniqueRowIdFromString(descriptor.entry.id)) +CreditsRow::CreditsRow( + not_null peer, + const Descriptor &descriptor) +: PeerListRow(peer, UniqueRowIdFromEntry(descriptor.entry)) , _entry(descriptor.entry) , _creditIcon(descriptor.creditIcon) , _rowHeight(descriptor.rowHeight) { - const auto photo = _entry.photoId - ? peer->session().data().photo(_entry.photoId).get() - : nullptr; - if (photo) { - _paintUserpicCallback = Ui::GenerateCreditsPaintEntryCallback( - photo, - [this, update = descriptor.updateCallback] { update(this); }); + const auto callback = Ui::PaintPreviewCallback( + &peer->session(), + _entry); + if (callback) { + _paintUserpicCallback = callback(crl::guard(&_guard, [this, update = descriptor.updateCallback] { + update(this); + })); } init(); } CreditsRow::CreditsRow(const Descriptor &descriptor) -: PeerListRow(UniqueRowIdFromString(descriptor.entry.id)) +: PeerListRow(UniqueRowIdFromEntry(descriptor.entry)) , _entry(descriptor.entry) , _creditIcon(descriptor.creditIcon) , _rowHeight(descriptor.rowHeight) { @@ -782,13 +795,17 @@ void CreditsRow::init() { langDateTimeFull(_entry.date) + (_entry.refunded ? (joiner + tr::lng_channel_earn_history_return(tr::now)) + : _entry.pending + ? (joiner + tr::lng_channel_earn_history_pending(tr::now)) + : _entry.failed + ? (joiner + tr::lng_channel_earn_history_failed(tr::now)) : QString()) + (_entry.title.isEmpty() ? QString() : (joiner + _name))); { constexpr auto kMinus = QChar(0x2212); _rightText.setText( st::semiboldTextStyle, - ((!_entry.bareId || _entry.refunded) ? QChar('+') : kMinus) + (_entry.in ? QChar('+') : kMinus) + Lang::FormatCountDecimal(std::abs(int64(_entry.credits)))); } if (!_paintUserpicCallback) { @@ -836,7 +853,9 @@ void CreditsRow::rightActionPaint( bool actionSelected) { const auto &font = _rightText.style()->font; y += _rowHeight / 2; - p.setPen((!_entry.bareId || _entry.refunded) + p.setPen(_entry.pending + ? st::creditsStroke + : _entry.in ? st::boxTextFgGood : st::menuIconAttentionColor); x += st::creditsHistoryRightSkip; @@ -932,11 +951,9 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { }, }; using Type = Data::CreditsHistoryEntry::PeerType; - if (item.bareId) { - const auto peer = session().data().peer(PeerId(item.bareId)); + if (const auto peerId = PeerId(item.barePeerId)) { + const auto peer = session().data().peer(peerId); return std::make_unique(peer, descriptor); - } else if (item.peerType == Type::PremiumBot) { - return std::make_unique(_premiumBot, descriptor); } else { return std::make_unique(descriptor); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 68da29a3a7376..ff263701120dc 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -9,6 +9,8 @@ For license and copyright information please follow this link: #include "api/api_blocked_peers.h" #include "api/api_common.h" +#include "base/qthelp_url.h" +#include "boxes/share_box.h" #include "core/click_handler_types.h" #include "data/data_bot_app.h" #include "data/data_changes.h" @@ -18,6 +20,7 @@ For license and copyright information please follow this link: #include "data/data_document_media.h" #include "data/data_session.h" #include "data/data_web_page.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "main/main_domain.h" #include "storage/storage_domain.h" @@ -664,6 +667,19 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { } } +bool AttachWebView::botValidateExternalLink(QString uri) { + const auto lower = uri.toLower(); + const auto allowed = _session->appConfig().get>( + "web_app_allowed_protocols", + std::vector{ u"http"_q, u"https"_q }); + for (const auto &protocol : allowed) { + if (lower.startsWith(protocol + u"://"_q)) { + return true; + } + } + return false; +} + void AttachWebView::botOpenIvLink(QString uri) { const auto window = _context ? _context->controller.get() : nullptr; if (window) { @@ -787,6 +803,16 @@ void AttachWebView::botInvokeCustomMethod( }).send(); } +void AttachWebView::botShareGameScore() { + if (!_panel || !_gameContext) { + return; + } else if (const auto item = _session->data().message(_gameContext)) { + FastShareMessage(uiShow(), item); + } else { + _panel->showToast({ tr::lng_message_not_found(tr::now) }); + } +} + void AttachWebView::botClose() { crl::on_main(this, [=] { cancel(); }); } @@ -895,7 +921,7 @@ void AttachWebView::request(const WebViewButton &button) { _requestId = 0; const auto &data = result.data(); show( - data.vquery_id().v, + data.vquery_id().value_or_empty(), qs(data.vurl()), button.text, button.fromAttachMenu || button.url.isEmpty()); @@ -1208,25 +1234,61 @@ void AttachWebView::requestSimple(const WebViewButton &button) { MTP_string(button.startCommand), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop") - )).done([=](const MTPSimpleWebViewResult &result) { + )).done([=](const MTPWebViewResult &result) { _requestId = 0; - result.match([&](const MTPDsimpleWebViewResultUrl &data) { - show( - uint64(), - qs(data.vurl()), - button.text, - false, - nullptr, - button.fromMainMenu); - }); + const auto &data = result.data(); + const auto queryId = uint64(); + show( + queryId, + qs(data.vurl()), + button.text, + false, + nullptr, + button.fromMainMenu); }).fail([=](const MTP::Error &error) { _requestId = 0; }).send(); } -void AttachWebView::requestMenu( +bool AttachWebView::openAppFromMenuLink( not_null controller, not_null bot) { + Expects(bot->botInfo != nullptr); + + const auto &url = bot->botInfo->botMenuButtonUrl; + const auto local = Core::TryConvertUrlToLocal(url); + const auto prefix = u"tg://resolve?"_q; + if (!local.startsWith(prefix)) { + return false; + } + const auto params = qthelp::url_parse_params( + local.mid(prefix.size()), + qthelp::UrlParamNameTransform::ToLower); + const auto domainParam = params.value(u"domain"_q); + const auto appnameParam = params.value(u"appname"_q); + const auto webChannelPreviewLink = (domainParam == u"s"_q) + && !appnameParam.isEmpty(); + const auto appname = webChannelPreviewLink ? QString() : appnameParam; + if (appname.isEmpty()) { + return false; + } + requestApp( + controller, + Api::SendAction(bot->owner().history(bot)), + bot, + appname, + params.value(u"startapp"_q), + true); + return true; +} + +void AttachWebView::requestMenu( + not_null controller, + not_null bot) { + if (openAppFromMenuLink(controller, bot)) { + return; + } + cancel(); _bot = bot; _context = std::make_unique(LookupContext( @@ -1257,7 +1319,7 @@ void AttachWebView::requestMenu( )).done([=](const MTPWebViewResult &result) { _requestId = 0; const auto &data = result.data(); - show(data.vquery_id().v, qs(data.vurl()), text); + show(data.vquery_id().value_or_empty(), qs(data.vurl()), text); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"BOT_INVALID"_q) { @@ -1380,7 +1442,7 @@ void AttachWebView::requestAppView(bool allowWrite) { MTP_string(_startCommand), MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)), MTP_string("tdesktop") - )).done([=](const MTPAppWebViewResult &result) { + )).done([=](const MTPWebViewResult &result) { _requestId = 0; const auto &data = result.data(); const auto queryId = uint64(); @@ -1496,6 +1558,7 @@ void AttachWebView::show( _lastShownUrl = url; _lastShownQueryId = queryId; _lastShownButtonText = buttonText; + _gameContext = {}; base::take(_panel); _catchingCancelInShowCall = true; _panel = Ui::BotWebView::Show({ @@ -1511,6 +1574,24 @@ void AttachWebView::show( started(queryId); } +void AttachWebView::showGame(ShowGameParams &¶ms) { + ActiveWebViews().emplace(this); + + base::take(_panel); + _gameContext = params.context; + + _catchingCancelInShowCall = true; + _panel = Ui::BotWebView::Show({ + .url = params.url, + .storageId = _session->local().resolveStorageIdBots(), + .title = rpl::single(params.title), + .bottom = rpl::single('@' + params.bot->username()), + .delegate = static_cast(this), + .menuButtons = Ui::BotWebView::MenuButton::ShareGame, + }); + _catchingCancelInShowCall = false; +} + void AttachWebView::started(uint64 queryId) { Expects(_bot != nullptr); Expects(_context != nullptr); @@ -1550,6 +1631,57 @@ void AttachWebView::started(uint64 queryId) { }, _panel->lifetime()); } +std::shared_ptr AttachWebView::uiShow() { + class Show final : public Main::SessionShow { + public: + explicit Show(not_null that) : _that(that) { + } + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override { + using UniqueLayer = std::unique_ptr; + using ObjectBox = object_ptr; + const auto panel = _that ? _that->_panel.get() : nullptr; + if (auto layerWidget = std::get_if(&layer)) { + Unexpected("Layers in AttachWebView are not implemented."); + } else if (auto box = std::get_if(&layer)) { + if (panel) { + panel->showBox(std::move(*box), options, animated); + } + } else if (panel) { + panel->hideLayer(animated); + } + } + [[nodiscard]] not_null toastParent() const override { + const auto panel = _that ? _that->_panel.get() : nullptr; + + Ensures(panel != nullptr); + return panel->toastParent(); + } + [[nodiscard]] bool valid() const override { + return _that && (_that->_panel != nullptr); + } + operator bool() const override { + return valid(); + } + + [[nodiscard]] Main::Session &session() const override { + Expects(_that.get() != nullptr); + return *_that->_session; + } + + private: + const base::weak_ptr _that; + + }; + return std::make_shared(this); +} + void AttachWebView::showToast( const QString &text, Window::SessionController *controller) { diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index aebf9933258e7..87648b07e88b7 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -29,6 +29,7 @@ class Panel; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Window { @@ -155,16 +156,26 @@ class AttachWebView final [[nodiscard]] std::optional lookupLastAction( const QString &url) const; + struct ShowGameParams { + not_null bot; + FullMsgId context; + QString url; + QString title; + }; + void showGame(ShowGameParams &¶ms); + + [[nodiscard]] std::shared_ptr uiShow(); + static void ClearAll(); private: struct Context; - Webview::ThemeParams botThemeParams() override; bool botHandleLocalUri(QString uri, bool keepOpen) override; void botHandleInvoice(QString slug) override; void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; + bool botValidateExternalLink(QString uri) override; void botOpenIvLink(QString uri) override; void botSendData(QByteArray data) override; void botSwitchInlineQuery( @@ -175,6 +186,7 @@ class AttachWebView final void botSharePhone(Fn callback) override; void botInvokeCustomMethod( Ui::BotWebView::CustomMethodRequest request) override; + void botShareGameScore() override; void botClose() override; [[nodiscard]] static Context LookupContext( @@ -184,6 +196,9 @@ class AttachWebView final const std::unique_ptr &a, const Context &b); + bool openAppFromMenuLink( + not_null controller, + not_null bot); void requestWithOptionalConfirm( not_null bot, const WebViewButton &button, @@ -267,6 +282,8 @@ class AttachWebView final rpl::event_stream<> _attachBotsUpdates; base::flat_set> _disclaimerAccepted; + FullMsgId _gameContext; + std::unique_ptr _panel; bool _catchingCancelInShowCall = false; diff --git a/Telegram/SourceFiles/intro/intro.style b/Telegram/SourceFiles/intro/intro.style index bedbad7489a5c..ae504be09be5e 100644 --- a/Telegram/SourceFiles/intro/intro.style +++ b/Telegram/SourceFiles/intro/intro.style @@ -96,7 +96,9 @@ introPhoneTop: 6px; introLinkTop: 24px; introCountry: InputField(defaultInputField) { textMargins: margins(3px, 27px, 3px, 6px); - font: font(16px); + style: TextStyle(defaultTextStyle) { + font: font(16px); + } width: 300px; heightMin: 61px; } diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index cc9d727fed4fc..3b30dc6dbbf65 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -221,7 +221,7 @@ Controller::~Controller() { void Controller::updateTitleGeometry(int newWidth) const { _subtitleWrap->setGeometry( 0, - st::windowTitleHeight, + 0, newWidth, st::ivSubtitleHeight); _subtitleWrap->paintRequest() | rpl::start_with_next([=](QRect clip) { @@ -241,7 +241,7 @@ void Controller::updateTitleGeometry(int newWidth) const { } void Controller::initControls() { - _subtitleWrap = std::make_unique(_window.get()); + _subtitleWrap = std::make_unique(_window->body().get()); _subtitleText = _index.value() | rpl::filter( rpl::mappers::_1 >= 0 ) | rpl::map([=](int index) { @@ -275,7 +275,7 @@ void Controller::initControls() { _back->toggledValue( ) | rpl::start_with_next([=](bool toggled) { _subtitleLeft.start( - [=] { updateTitleGeometry(_window->width()); }, + [=] { updateTitleGeometry(_window->body()->width()); }, toggled ? 0. : 1., toggled ? 1. : 0., st::fadeWrapDuration); @@ -353,7 +353,7 @@ void Controller::createWindow() { initControls(); - window->widthValue() | rpl::start_with_next([=](int width) { + window->body()->widthValue() | rpl::start_with_next([=](int width) { updateTitleGeometry(width); }, _subtitle->lifetime()); @@ -366,13 +366,13 @@ void Controller::createWindow() { _delegate->ivSaveGeometry(window); }, window->lifetime()); - _container = Ui::CreateChild(window->window()); + _container = Ui::CreateChild(window->body().get()); rpl::combine( - window->sizeValue(), + window->body()->sizeValue(), _subtitleWrap->heightValue() ) | rpl::start_with_next([=](QSize size, int title) { _container->setGeometry(QRect(QPoint(), size).marginsRemoved( - { 0, title + st::windowTitleHeight, 0, 0 })); + { 0, title, 0, 0 })); }, _container->lifetime()); _container->paintRequest() | rpl::start_with_next([=](QRect clip) { @@ -807,11 +807,10 @@ void Controller::showShareMenu() { } _shareWrap = std::make_unique(_shareHidesContent - ? _window->window() + ? _window->body().get() : nullptr); - const auto margins = QMargins(0, st::windowTitleHeight, 0, 0); if (!_shareHidesContent) { - _shareWrap->setGeometry(_window->geometry().marginsRemoved(margins)); + _shareWrap->setGeometry(_window->body()->rect()); _shareWrap->setWindowFlag(Qt::FramelessWindowHint); _shareWrap->setAttribute(Qt::WA_TranslucentBackground); _shareWrap->setAttribute(Qt::WA_NoSystemBackground); @@ -819,14 +818,14 @@ void Controller::showShareMenu() { _shareContainer.reset(QWidget::createWindowContainer( _shareWrap->windowHandle(), - _window.get(), + _window->body().get(), Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint)); } - _window->sizeValue() | rpl::start_with_next([=](QSize size) { + _window->body()->sizeValue() | rpl::start_with_next([=](QSize size) { const auto widget = _shareHidesContent ? _shareWrap.get() : _shareContainer.get(); - widget->setGeometry(QRect(QPoint(), size).marginsRemoved(margins)); + widget->setGeometry(QRect(QPoint(), size)); }, _shareWrap->lifetime()); auto result = _showShareBox({ diff --git a/Telegram/SourceFiles/iv/iv_data.cpp b/Telegram/SourceFiles/iv/iv_data.cpp index 5258e6c9f1f14..16f9c0d519d60 100644 --- a/Telegram/SourceFiles/iv/iv_data.cpp +++ b/Telegram/SourceFiles/iv/iv_data.cpp @@ -71,6 +71,12 @@ bool Data::partial() const { Data::~Data() = default; +void Data::updateCachedViews(int cachedViews) { + _source->updatedCachedViews = std::max( + _source->updatedCachedViews, + cachedViews); +} + void Data::prepare(const Options &options, Fn done) const { crl::async([source = *_source, options, done = std::move(done)] { done(Prepare(source, options)); diff --git a/Telegram/SourceFiles/iv/iv_data.h b/Telegram/SourceFiles/iv/iv_data.h index 87900bce37b3d..cdb2d87089d53 100644 --- a/Telegram/SourceFiles/iv/iv_data.h +++ b/Telegram/SourceFiles/iv/iv_data.h @@ -45,6 +45,8 @@ class Data final { [[nodiscard]] QString id() const; [[nodiscard]] bool partial() const; + void updateCachedViews(int cachedViews); + void prepare(const Options &options, Fn done) const; private: diff --git a/Telegram/SourceFiles/iv/iv_delegate.h b/Telegram/SourceFiles/iv/iv_delegate.h index fd94cac0a34f7..09374fa1749e9 100644 --- a/Telegram/SourceFiles/iv/iv_delegate.h +++ b/Telegram/SourceFiles/iv/iv_delegate.h @@ -7,13 +7,17 @@ For license and copyright information please follow this link: */ #pragma once +namespace Ui { +class RpWindow; +} // namespace Ui + namespace Iv { class Delegate { public: virtual void ivSetLastSourceWindow(not_null window) = 0; [[nodiscard]] virtual QRect ivGeometry() const = 0; - virtual void ivSaveGeometry(not_null window) = 0; + virtual void ivSaveGeometry(not_null window) = 0; }; } // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp index 5ac753d17f0b1..4de0fa03c8157 100644 --- a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp +++ b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp @@ -67,7 +67,7 @@ QRect DelegateImpl::ivGeometry() const { return result; } -void DelegateImpl::ivSaveGeometry(not_null window) { +void DelegateImpl::ivSaveGeometry(not_null window) { if (!window->windowHandle()) { return; } @@ -82,7 +82,7 @@ void DelegateImpl::ivSaveGeometry(not_null window) { realPosition.moncrc = 0; DEBUG_LOG(("IV Pos: Saving maximized position.")); } else { - auto r = window->geometry(); + auto r = window->body()->mapToGlobal(window->body()->rect()); realPosition.x = r.x(); realPosition.y = r.y(); realPosition.w = r.width(); diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.h b/Telegram/SourceFiles/iv/iv_delegate_impl.h index 32b15ae8fe090..9c7c0fb9d2475 100644 --- a/Telegram/SourceFiles/iv/iv_delegate_impl.h +++ b/Telegram/SourceFiles/iv/iv_delegate_impl.h @@ -17,7 +17,7 @@ class DelegateImpl final : public Delegate { void ivSetLastSourceWindow(not_null window) override; [[nodiscard]] QRect ivGeometry() const override; - void ivSaveGeometry(not_null window) override; + void ivSaveGeometry(not_null window) override; private: QPointer _lastSourceWindow; diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 69df5f772a1d7..d070c93d2a3c7 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -57,6 +57,7 @@ constexpr auto kGeoPointScale = 1; constexpr auto kGeoPointZoomMin = 13; constexpr auto kMaxLoadParts = 5; constexpr auto kKeepLoadingParts = 8; +constexpr auto kAllowPageReloadAfter = 3 * crl::time(1000); } // namespace @@ -815,19 +816,19 @@ void Instance::show( } break; case Type::OpenPage: - case Type::OpenLink: + case Type::OpenLink: { if (!urlChecked) { break; } - _fullRequested[_shownSession].emplace(event.url); - _shownSession->api().request(MTPmessages_GetWebPage( - MTP_string(event.url), - MTP_int(0) + const auto session = _shownSession; + const auto url = event.url; + auto &requested = _fullRequested[session][url]; + requested.lastRequestedAt = crl::now(); + session->api().request(MTPmessages_GetWebPage( + MTP_string(url), + MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { - _shownSession->data().processUsers(result.data().vusers()); - _shownSession->data().processChats(result.data().vchats()); - const auto page = _shownSession->data().processWebpage( - result.data().vwebpage()); + const auto page = processReceivedPage(session, url, result); if (page && page->iv) { const auto parts = event.url.split('#'); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; @@ -838,7 +839,7 @@ void Instance::show( }).fail([=] { UrlClickHandler::Open(event.url); }).send(); - break; + } break; case Type::Report: if (const auto controller = _shownSession->tryResolveWindow()) { controller->window().activate(); @@ -954,15 +955,13 @@ void Instance::openWithIvPreferred( }; _ivRequestSession = session; _ivRequestUri = uri; - _fullRequested[session].emplace(url); + auto &requested = _fullRequested[session][url]; + requested.lastRequestedAt = crl::now(); _ivRequestId = session->api().request(MTPmessages_GetWebPage( MTP_string(url), - MTP_int(0) + MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { - const auto &data = result.data(); - session->data().processUsers(data.vusers()); - session->data().processChats(data.vchats()); - finish(session->data().processWebpage(data.vwebpage())); + finish(processReceivedPage(session, url, result)); }).fail([=] { finish(nullptr); }).send(); @@ -971,24 +970,53 @@ void Instance::openWithIvPreferred( void Instance::requestFull( not_null session, const QString &id) { - if (!_tracking.contains(session) - || !_fullRequested[session].emplace(id).second) { + if (!_tracking.contains(session)) { + return; + } + auto &requested = _fullRequested[session][id]; + const auto last = requested.lastRequestedAt; + const auto now = crl::now(); + if (last && (now - last) < kAllowPageReloadAfter) { return; } + requested.lastRequestedAt = now; session->api().request(MTPmessages_GetWebPage( MTP_string(id), - MTP_int(0) + MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { - session->data().processUsers(result.data().vusers()); - session->data().processChats(result.data().vchats()); - const auto page = session->data().processWebpage( - result.data().vwebpage()); + const auto page = processReceivedPage(session, id, result); if (page && page->iv && _shown && _shownSession == session) { _shown->update(page->iv.get()); } }).send(); } +WebPageData *Instance::processReceivedPage( + not_null session, + const QString &url, + const MTPmessages_WebPage &result) { + const auto &data = result.data(); + const auto owner = &session->data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + auto &requested = _fullRequested[session][url]; + const auto &mtp = data.vwebpage(); + mtp.match([&](const MTPDwebPageNotModified &data) { + const auto page = requested.page; + if (const auto views = data.vcached_page_views()) { + if (page && page->iv) { + page->iv->updateCachedViews(views->v); + } + } + }, [&](const MTPDwebPage &data) { + requested.hash = data.vhash().v; + requested.page = owner->processWebpage(data).get(); + }, [&](const auto &) { + requested.page = owner->processWebpage(mtp).get(); + }); + return requested.page; +} + void Instance::processOpenChannel(const QString &context) { if (!_shownSession) { return; diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index 1299bbdddbe7d..9083734adf2fb 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -61,12 +61,23 @@ class Instance final { [[nodiscard]] rpl::lifetime &lifetime(); private: + struct FullResult { + crl::time lastRequestedAt = 0; + WebPageData *page = nullptr; + int32 hash = 0; + }; + void processOpenChannel(const QString &context); void processJoinChannel(const QString &context); void requestFull(not_null session, const QString &id); void trackSession(not_null session); + WebPageData *processReceivedPage( + not_null session, + const QString &url, + const MTPmessages_WebPage &result); + const not_null _delegate; std::unique_ptr _shown; @@ -77,7 +88,7 @@ class Instance final { base::flat_set>> _joining; base::flat_map< not_null, - base::flat_set> _fullRequested; + base::flat_map> _fullRequested; base::flat_map< not_null, diff --git a/Telegram/SourceFiles/iv/iv_prepare.cpp b/Telegram/SourceFiles/iv/iv_prepare.cpp index d5f5375247535..15d5edaa73325 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.cpp +++ b/Telegram/SourceFiles/iv/iv_prepare.cpp @@ -229,7 +229,9 @@ Parser::Parser(const Source &source, const Options &options) _result.name = source.name; _result.rtl = source.page.data().is_rtl(); - const auto views = source.page.data().vviews().value_or_empty(); + const auto views = std::max( + source.page.data().vviews().value_or_empty(), + source.updatedCachedViews); const auto content = list(source.page.data().vblocks()); _result.content = wrap(content, views); } diff --git a/Telegram/SourceFiles/iv/iv_prepare.h b/Telegram/SourceFiles/iv/iv_prepare.h index a896c79e030b1..ef919db2f7590 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.h +++ b/Telegram/SourceFiles/iv/iv_prepare.h @@ -18,6 +18,7 @@ struct Source { std::optional webpagePhoto; std::optional webpageDocument; QString name; + int updatedCachedViews = 0; }; [[nodiscard]] Prepared Prepare(const Source &source, const Options &options); diff --git a/Telegram/SourceFiles/main/main_domain.cpp b/Telegram/SourceFiles/main/main_domain.cpp index aafbcd2ac978b..714f7e29196ea 100644 --- a/Telegram/SourceFiles/main/main_domain.cpp +++ b/Telegram/SourceFiles/main/main_domain.cpp @@ -318,8 +318,8 @@ not_null Domain::add(MTP::Environment environment) { void Domain::addActivated(MTP::Environment environment, bool newWindow) { const auto added = [&](not_null account) { if (newWindow) { - Core::App().ensureSeparateWindowForAccount(account); - } else if (const auto window = Core::App().separateWindowForAccount( + Core::App().ensureSeparateWindowFor(account); + } else if (const auto window = Core::App().separateWindowFor( account)) { window->activate(); } else { @@ -371,11 +371,11 @@ void Domain::watchSession(not_null account) { void Domain::closeAccountWindows(not_null account) { auto another = (Main::Account*)nullptr; for (auto i = _accounts.begin(); i != _accounts.end(); ++i) { - const auto other = i->account.get(); + const auto other = not_null(i->account.get()); if (other == account) { continue; - } else if (Core::App().separateWindowForAccount(other)) { - const auto that = Core::App().separateWindowForAccount(account); + } else if (Core::App().separateWindowFor(other)) { + const auto that = Core::App().separateWindowFor(account); if (that) { that->close(); } @@ -403,6 +403,8 @@ bool Domain::removePasscodeIfEmpty() { return false; } _local->setPasscode(QByteArray()); + Core::App().settings().setSystemUnlockEnabled(false); + Core::App().saveSettingsDelayed(); return true; } @@ -411,7 +413,7 @@ void Domain::removeRedundantAccounts() { const auto was = _accounts.size(); for (auto i = _accounts.begin(); i != _accounts.end();) { - if (Core::App().separateWindowForAccount(i->account.get()) + if (Core::App().separateWindowFor(not_null(i->account.get())) || i->account->sessionExists()) { ++i; continue; @@ -442,7 +444,7 @@ void Domain::checkForLastProductionConfig( } void Domain::maybeActivate(not_null account) { - if (Core::App().separateWindowForAccount(account)) { + if (Core::App().separateWindowFor(account)) { activate(account); } else { Core::App().preventOrInvoke(crl::guard(account, [=] { @@ -452,7 +454,7 @@ void Domain::maybeActivate(not_null account) { } void Domain::activate(not_null account) { - if (const auto window = Core::App().separateWindowForAccount(account)) { + if (const auto window = Core::App().separateWindowFor(account)) { window->activate(); } if (_active.current() == account.get()) { diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 951ec735acc30..ea61115740775 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -497,7 +497,8 @@ Window::SessionController *Session::tryResolveWindow( if (forPeer) { auto primary = (Window::SessionController*)nullptr; for (const auto &window : _windows) { - if (window->singlePeer() == forPeer) { + const auto thread = window->windowId().thread; + if (thread && thread->peer() == forPeer) { return window; } else if (window->isPrimary()) { primary = window; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 025b66cf8b34c..854441b8c2184 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -37,6 +37,7 @@ For license and copyright information please follow this link: #include "window/window_connecting_widget.h" #include "window/window_top_bar_wrap.h" #include "window/notifications_manager.h" +#include "window/window_separate_id.h" #include "window/window_slide_animation.h" #include "window/window_history_hider.h" #include "window/window_controller.h" @@ -232,19 +233,19 @@ MainWidget::MainWidget( , _controller(controller) , _dialogsWidth(st::columnMinimalWidthLeft) , _thirdColumnWidth(st::columnMinimalWidthThird) -, _sideShadow(isPrimary() - ? base::make_unique_q(this) - : nullptr) -, _dialogs(isPrimary() +, _dialogs(windowId().hasChatsList() ? base::make_unique_q( this, _controller, Dialogs::Widget::Layout::Main) : nullptr) , _history(std::in_place, this, _controller) +, _sideShadow(_dialogs + ? base::make_unique_q(this) + : nullptr) , _playerPlaylist(this, _controller) , _changelogs(Core::Changelogs::Create(&controller->session())) { - if (isPrimary()) { + if (_dialogs) { setupConnectingWidget(); } @@ -732,7 +733,7 @@ void MainWidget::hideSingleUseKeyboard(FullMsgId replyToId) { void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { auto tags = Data::SearchTagsFromQuery(query); - if (controller()->isPrimary()) { + if (_dialogs) { auto state = Dialogs::SearchState{ .inChat = ((tags.empty() || inChat.sublist()) ? inChat @@ -758,7 +759,7 @@ void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { if ((!_mainSection || !_mainSection->searchInChatEmbedded(inChat, query)) && !_history->searchInChatEmbedded(inChat, query)) { - const auto account = &session().account(); + const auto account = not_null(&session().account()); if (const auto window = Core::App().windowFor(account)) { if (const auto controller = window->sessionController()) { controller->content()->searchMessages(query, inChat); @@ -1191,9 +1192,11 @@ void MainWidget::setInnerFocus() { _mainSection->setInnerFocus(); } else if (!_hider && _thirdSection) { _thirdSection->setInnerFocus(); - } else { - Assert(_dialogs != nullptr); + } else if (_dialogs) { _dialogs->setInnerFocus(); + } else { + // Maybe we're just closing a child window, content is destroyed. + _history->setFocus(); } } else if (_mainSection) { _mainSection->setInnerFocus(); @@ -1238,36 +1241,35 @@ bool MainWidget::showHistoryInDifferentWindow( PeerId peerId, const SectionShow ¶ms, MsgId showAtMsgId) { + if (!peerId) { + return false; + } const auto peer = session().data().peer(peerId); - const auto account = &session().account(); - auto primary = Core::App().separateWindowForAccount(account); - if (const auto separate = Core::App().separateWindowForPeer(peer)) { - if (separate == &_controller->window()) { - return false; + if (const auto separateChat = _controller->windowId().chat()) { + if (const auto history = separateChat->asHistory()) { + if (history->peer == peer) { + return false; + } } - separate->sessionController()->showPeerHistory( + } + const auto window = Core::App().windowForShowingHistory(peer); + if (window == &_controller->window()) { + return false; + } else if (window) { + window->sessionController()->showPeerHistory( peerId, params, showAtMsgId); - separate->activate(); + window->activate(); return true; - } else if (isPrimary()) { - if (primary && primary != &_controller->window()) { - primary->sessionController()->showPeerHistory( - peerId, - params, - showAtMsgId); - primary->activate(); - return true; - } + } else if (windowId().hasChatsList()) { return false; - } else if (!peerId) { - return true; - } else if (singlePeer()->id == peerId) { - return false; - } else if (!primary) { + } + const auto account = not_null(&session().account()); + auto primary = Core::App().separateWindowFor(account); + if (!primary) { Core::App().domain().activate(account); - primary = Core::App().separateWindowForAccount(account); + primary = Core::App().separateWindowFor(account); } if (primary && &primary->account() == account) { primary->sessionController()->showPeerHistory( @@ -1293,8 +1295,9 @@ void MainWidget::showHistory( } const auto unavailable = peer->computeUnavailableReason(); if (!unavailable.isEmpty()) { - Assert(isPrimary()); - if (params.activation != anim::activation::background) { + if (!isPrimary()) { + _controller->window().close(); + } else if (params.activation != anim::activation::background) { _controller->show(Ui::MakeInformBox(unavailable)); _controller->window().activate(); } @@ -1510,7 +1513,7 @@ void MainWidget::showMessage( void MainWidget::showForum( not_null forum, const SectionShow ¶ms) { - Expects(isPrimary() || (singlePeer() && singlePeer()->forum() == forum)); + Expects(_dialogs != nullptr); _dialogs->showForum(forum, params); @@ -1846,8 +1849,8 @@ void MainWidget::checkMainSectionToLayer() { updateMainSectionShown(); } -PeerData *MainWidget::singlePeer() const { - return _controller->singlePeer(); +Window::SeparateId MainWidget::windowId() const { + return _controller->windowId(); } bool MainWidget::isPrimary() const { @@ -1950,20 +1953,19 @@ void MainWidget::showNonPremiumLimitToast(bool download) { }); } -void MainWidget::showBackFromStack( - const SectionShow ¶ms) { +bool MainWidget::showBackFromStack(const SectionShow ¶ms) { if (preventsCloseSection([=] { showBackFromStack(params); }, params)) { - return; + return false; } if (_stack.empty()) { - if (isPrimary()) { + if (_dialogs) { _controller->clearSectionStack(params); } crl::on_main(this, [=] { _controller->widget()->setInnerFocus(); }); - return; + return (_dialogs != nullptr); } auto item = std::move(_stack.back()); _stack.pop_back(); @@ -1995,6 +1997,7 @@ void MainWidget::showBackFromStack( anim::activation::background)); } + return true; } void MainWidget::orderWidgets() { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 3653070acb7e9..7acbbead4e174 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -90,6 +90,7 @@ struct SectionSlideParams; struct SectionShow; enum class Column; class HistoryHider; +struct SeparateId; } // namespace Window namespace Calls { @@ -121,7 +122,7 @@ class MainWidget final [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null controller() const; - [[nodiscard]] PeerData *singlePeer() const; + [[nodiscard]] Window::SeparateId windowId() const; [[nodiscard]] bool isPrimary() const; [[nodiscard]] bool isMainSectionShown() const; [[nodiscard]] bool isThirdSectionShown() const; @@ -151,8 +152,7 @@ class MainWidget final const SectionShow ¶ms); void updateColumnLayout(); bool stackIsEmpty() const; - void showBackFromStack( - const SectionShow ¶ms); + bool showBackFromStack(const SectionShow ¶ms); void orderWidgets(); QPixmap grabForShowAnimation(const Window::SectionSlideParams ¶ms); void checkMainSectionToLayer(); @@ -350,10 +350,6 @@ class MainWidget final int _thirdColumnWidth = 0; Ui::Animations::Simple _a_dialogsWidth; - const base::unique_qptr _sideShadow; - object_ptr _thirdShadow = { nullptr }; - object_ptr _firstColumnResizeArea = { nullptr }; - object_ptr _thirdColumnResizeArea = { nullptr }; const base::unique_qptr _dialogs; const base::unique_qptr _history; object_ptr _mainSection = { nullptr }; @@ -361,6 +357,11 @@ class MainWidget final std::shared_ptr _thirdSectionFromStack; std::unique_ptr _connecting; + const base::unique_qptr _sideShadow; + object_ptr _thirdShadow = { nullptr }; + object_ptr _firstColumnResizeArea = { nullptr }; + object_ptr _thirdColumnResizeArea = { nullptr }; + base::weak_ptr _currentCall; base::weak_ptr _currentGroupCall; rpl::lifetime _currentCallLifetime; diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index 93d6d3060c600..df1f2ef2587a1 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -139,7 +139,9 @@ void MainWindow::finishFirstShow() { applyInitialWorkMode(); createGlobalMenu(); - windowDeactivateEvents( + windowActiveValue( + ) | rpl::skip(1) | rpl::filter( + !rpl::mappers::_1 ) | rpl::start_with_next([=] { Ui::Tooltip::Hide(); }, lifetime()); @@ -267,13 +269,11 @@ void MainWindow::setupMain( auto created = object_ptr(bodyWidget(), sessionController()); clearWidgets(); _main = std::move(created); - if (const auto peer = singlePeer()) { - updateControlsGeometry(); - _main->controller()->showPeerHistory( - peer, - Window::SectionShow::Way::ClearStack, - singlePeerShowAtMsgId); - } + updateControlsGeometry(); + Ui::SendPendingMoveResizeEvents(_main); + _main->controller()->showByInitialId( + Window::SectionShow::Way::ClearStack, + singlePeerShowAtMsgId); if (_passcodeLock) { _main->hide(); } else { @@ -594,7 +594,7 @@ bool MainWindow::eventFilter(QObject *object, QEvent *e) { case QEvent::ApplicationActivate: { if (object == QCoreApplication::instance()) { InvokeQueued(this, [=] { - handleActiveChanged(); + handleActiveChanged(isActiveWindow()); }); } } break; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 04c06e1970ed5..c5fd13992d55e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "boxes/peers/prepare_short_info_box.h" #include "chat_helpers/compose/compose_show.h" #include "core/application.h" +#include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/update_checker.h" #include "data/data_changes.h" @@ -328,16 +329,11 @@ Controller::Controller(not_null delegate) } }, _lifetime); - const auto window = _wrap->window()->windowHandle(); - Assert(window != nullptr); - base::qt_signal_producer( - window, - &QWindow::activeChanged - ) | rpl::start_with_next([=] { - _windowActive = window->isActive(); + _wrap->windowActiveValue( + ) | rpl::start_with_next([=](bool active) { + _windowActive = active; updatePlayingAllowed(); }, _lifetime); - _windowActive = window->isActive(); _contentFadeAnimation.stop(); } @@ -1051,6 +1047,9 @@ void Controller::updateAreas(Data::Story *story) { const auto &channelPosts = story ? story->channelPosts() : std::vector(); + const auto &urlAreas = story + ? story->urlAreas() + : std::vector(); if (_locations != locations) { _locations = locations; _areas.clear(); @@ -1059,6 +1058,10 @@ void Controller::updateAreas(Data::Story *story) { _channelPosts = channelPosts; _areas.clear(); } + if (_urlAreas != urlAreas) { + _urlAreas = urlAreas; + _areas.clear(); + } const auto reactionsCount = int(suggestedReactions.size()); if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) { for (auto i = 0; i != reactionsCount; ++i) { @@ -1202,13 +1205,15 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { if (!layout || (_locations.empty() && _suggestedReactions.empty() - && _channelPosts.empty())) { + && _channelPosts.empty() + && _urlAreas.empty())) { return nullptr; } else if (_areas.empty()) { const auto now = story(); _areas.reserve(_locations.size() + _suggestedReactions.size() - + _channelPosts.size()); + + _channelPosts.size() + + _urlAreas.size()); for (const auto &location : _locations) { _areas.push_back({ .original = location.area.geometry, @@ -1249,6 +1254,13 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const { }); } } + for (const auto &url : _urlAreas) { + _areas.push_back({ + .original = url.area.geometry, + .rotation = url.area.rotation, + .handler = std::make_shared(url.url), + }); + } rebuildActiveAreas(*layout); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 776667f736097..3d486fa873e57 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -302,6 +302,7 @@ class Controller final : public base::has_weak_ptr { std::vector _locations; std::vector _suggestedReactions; std::vector _channelPosts; + std::vector _urlAreas; mutable std::vector _areas; std::vector _cachedSourcesList; diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index c8f5ffe94660e..73bd57e9328eb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -170,6 +170,10 @@ void View::showFullCaption() { _controller->showFullCaption(); } +std::shared_ptr View::uiShow() const { + return _controller->uiShow(); +} + rpl::lifetime &View::lifetime() { return _controller->lifetime(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index da71d80d1ab7b..9436841d36309 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -9,6 +9,10 @@ For license and copyright information please follow this link: class ClickHandlerHost; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { class Story; struct StoriesContext; @@ -125,6 +129,8 @@ class View final { not_null menu, QPoint desiredPosition); + [[nodiscard]] std::shared_ptr uiShow() const; + [[nodiscard]] rpl::lifetime &lifetime(); private: diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 99d9b4288ea2c..fe1a7b5b6fd61 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -638,6 +638,7 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { menuSpoilerOff: icon {{ "menu/spoiler_off", storiesComposeWhiteText }}; menuBelow: icon {{ "menu/link_below", storiesComposeWhiteText }}; menuAbove: icon {{ "menu/link_above", storiesComposeWhiteText }}; + menuPrice: icon {{ "menu/earn", storiesComposeWhiteText }}; stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, diff --git a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp index 2aff460676616..c03c20c8e7e9d 100644 --- a/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp +++ b/Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp @@ -669,6 +669,11 @@ auto GroupThumbs::createThumb(Key key) key, page->collage, collageKey->index); + } else if (const auto invoice = media->invoice()) { + return createThumb( + key, + *invoice, + collageKey->index); } } } @@ -695,6 +700,23 @@ auto GroupThumbs::createThumb( return createThumb(key, nullptr); } +auto GroupThumbs::createThumb( + Key key, + const Data::Invoice &invoice, + int index) +-> std::unique_ptr { + if (index < 0 || index >= invoice.extendedMedia.size()) { + return createThumb(key, nullptr); + } + const auto &media = invoice.extendedMedia[index]; + if (const auto photo = media->photo()) { + return createThumb(key, photo); + } else if (const auto document = media->document()) { + return createThumb(key, document); + } + return createThumb(key, nullptr); +} + auto GroupThumbs::createThumb(Key key, std::nullptr_t) -> std::unique_ptr { const auto weak = base::make_weak(this); diff --git a/Telegram/SourceFiles/media/view/media_view_group_thumbs.h b/Telegram/SourceFiles/media/view/media_view_group_thumbs.h index b624c985d8d5b..ebcdc373713ec 100644 --- a/Telegram/SourceFiles/media/view/media_view_group_thumbs.h +++ b/Telegram/SourceFiles/media/view/media_view_group_thumbs.h @@ -15,6 +15,10 @@ class SharedMediaWithLastSlice; class UserPhotosSlice; struct WebPageCollage; +namespace Data { +struct Invoice; +} // namespace Data + namespace Main { class Session; } // namespace Main @@ -109,6 +113,10 @@ class GroupThumbs : public base::has_weak_ptr { Key key, const WebPageCollage &collage, int index); + std::unique_ptr createThumb( + Key key, + const Data::Invoice &invoice, + int index); std::unique_ptr createThumb(Key key, not_null photo); std::unique_ptr createThumb( Key key, diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 6d7e8d73b9cae..c6a7e9c6e66a6 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "apiwrap.h" #include "api/api_attached_stickers.h" #include "api/api_peer_photo.h" +#include "base/qt/qt_common_adapters.h" #include "lang/lang_keys.h" #include "boxes/premium_preview_box.h" #include "core/application.h" @@ -504,6 +505,22 @@ OverlayWidget::OverlayWidget() if (handleContextMenu(position)) { return base::EventFilterResult::Cancel; } + } else if (e->type() == QEvent::WindowStateChange) { + const auto state = window()->windowState(); + if (state == Qt::WindowMinimized || Platform::IsMac()) { + } else if (state == Qt::WindowMaximized) { + if (_fullscreen || _windowed) { + _fullscreen = _windowed = false; + savePosition(); + } + } else if (_fullscreen || _windowed) { + } else if (state == Qt::WindowFullScreen) { + _fullscreen = true; + savePosition(); + } else { + _windowed = true; + savePosition(); + } } return base::EventFilterResult::Continue; }); @@ -615,10 +632,10 @@ OverlayWidget::OverlayWidget() } _widget->setMouseTracking(true); - QObject::connect( - window(), - &QWindow::screenChanged, - [=](QScreen *screen) { handleScreenChanged(screen); }); + _window->screenValue( + ) | rpl::skip(1) | rpl::start_with_next([=](not_null screen) { + handleScreenChanged(screen); + }, lifetime()); subscribeToScreenGeometry(); updateGeometry(); updateControlsGeometry(); @@ -733,29 +750,6 @@ void OverlayWidget::setupWindow() { return Flag::Move | Flag(0); }); - const auto callback = [=](Qt::WindowState state) { - if (state == Qt::WindowMinimized || Platform::IsMac()) { - return; - } else if (state == Qt::WindowMaximized) { - if (_fullscreen || _windowed) { - _fullscreen = _windowed = false; - savePosition(); - } - } else if (_fullscreen || _windowed) { - return; - } else if (state == Qt::WindowFullScreen) { - _fullscreen = true; - savePosition(); - } else { - _windowed = true; - savePosition(); - } - }; - QObject::connect( - _window->windowHandle(), - &QWindow::windowStateChanged, - callback); - _window->setAttribute(Qt::WA_NoSystemBackground, true); _window->setAttribute(Qt::WA_TranslucentBackground, true); @@ -809,7 +803,11 @@ void OverlayWidget::moveToScreen(bool inMove) { DEBUG_LOG(("Viewer Pos: Currently on screen %1, moving to screen %2") .arg(screenList.indexOf(myScreen)) .arg(screenList.indexOf(activeWindowScreen))); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + _window->setScreen(activeWindowScreen); +#else // Qt >= 6.0.0 window()->setScreen(activeWindowScreen); +#endif // Qt < 6.0.0 DEBUG_LOG(("Viewer Pos: New actual screen: %1") .arg(screenList.indexOf(_window->screen()))); } @@ -867,7 +865,8 @@ void OverlayWidget::savePosition() { } else if (!_wasWindowedMode && !Platform::IsMac()) { return; } else { - auto r = _normalGeometry = _window->geometry(); + auto r = _normalGeometry = _window->body()->mapToGlobal( + _window->body()->rect()); realPosition.x = r.x(); realPosition.y = r.y(); realPosition.w = r.width(); @@ -912,7 +911,7 @@ void OverlayWidget::updateGeometry(bool inMove) { .arg(_normalGeometry.y()) .arg(_normalGeometry.width()) .arg(_normalGeometry.height())); - _window->RpWidget::setGeometry(_normalGeometry); + _window->setGeometry(_normalGeometry); } if constexpr (!Platform::IsMac()) { if (_fullscreen) { @@ -1363,7 +1362,7 @@ void OverlayWidget::updateControls() { if (_message) { return ItemDateTime(_message); } else if (_photo) { - return base::unixtime::parse(_photo->date); + return base::unixtime::parse(_photo->date()); } else if (_document) { return base::unixtime::parse(_document->date); } @@ -2216,9 +2215,6 @@ void OverlayWidget::close() { return; } hide(); - if (const auto window = Core::App().activeWindow()) { - window->reActivate(); - } _helper->clearState(); } @@ -2318,7 +2314,7 @@ void OverlayWidget::dropdownHidden() { } } -void OverlayWidget::handleScreenChanged(QScreen *screen) { +void OverlayWidget::handleScreenChanged(not_null screen) { subscribeToScreenGeometry(); if (isHidden()) { return; @@ -2440,7 +2436,7 @@ void OverlayWidget::saveAs() { u".mp4"_q, QString(), false, - _photo->date), + _photo->date()), crl::guard(_window, [=](const QString &result) { QFile f(result); if (!result.isEmpty() @@ -2471,7 +2467,7 @@ void OverlayWidget::saveAs() { u".jpg"_q, QString(), false, - _photo->date), + _photo->date()), crl::guard(_window, [=](const QString &result) { if (!result.isEmpty() && _photo == photo) { media->saveToFile(result); @@ -2775,7 +2771,7 @@ auto OverlayWidget::sharedMediaType() const using Type = SharedMediaType; if (_message) { if (const auto media = _message->media()) { - if (media->webpage()) { + if (media->webpage() || media->invoice()) { return std::nullopt; } } @@ -3003,6 +2999,14 @@ std::optional OverlayWidget::collageKey() const { return item; } } + } else if (const auto invoice = media->invoice()) { + for (const auto &item : invoice->extendedMedia) { + if (_photo && item->photo() == _photo) { + return _photo; + } else if (_document && item->document() == _document) { + return _document; + } + } } } } @@ -3036,6 +3040,16 @@ void OverlayWidget::validateCollage() { if (const auto media = _message->media()) { if (const auto page = media->webpage()) { _collageData = page->collage; + } else if (const auto invoice = media->invoice()) { + auto &data = *_collageData; + data.items.reserve(invoice->extendedMedia.size()); + for (const auto &item : invoice->extendedMedia) { + if (const auto photo = item->photo()) { + data.items.push_back(photo); + } else if (const auto document = item->document()) { + data.items.push_back(document); + } + } } } } @@ -3248,7 +3262,7 @@ bool OverlayWidget::isHidden() const { } bool OverlayWidget::isMinimized() const { - return _window->windowHandle()->windowState() == Qt::WindowMinimized; + return _window->isMinimized(); } bool OverlayWidget::isFullScreen() const { @@ -3826,12 +3840,13 @@ void OverlayWidget::updatePowerSaveBlocker( && _document->isVideoFile() && !IsPausedOrPausing(state.state) && !IsStoppedOrStopping(state.state); + base::UpdatePowerSaveBlocker( _streamed->powerSaveBlocker, block, base::PowerSaveBlockType::PreventDisplaySleep, [] { return u"Video playback is active"_q; }, - [=] { return window(); }); + [=] { return _window->windowHandle(); }); } QImage OverlayWidget::transformedShownContent() const { @@ -5839,6 +5854,8 @@ void OverlayWidget::handleMouseRelease( QVariant::fromValue(ClickHandlerContext{ .itemId = _message ? _message->fullId() : FullMsgId(), .sessionWindow = base::make_weak(findWindow()), + .show = _stories ? _stories->uiShow() : nullptr, + .dark = true, }) }); return; @@ -6137,7 +6154,7 @@ Window::SessionController *OverlayWidget::findWindow(bool switchTo) const { if (switchTo) { auto controllerPtr = (Window::SessionController*)nullptr; - const auto account = &_session->account(); + const auto account = not_null(&_session->account()); const auto sessionWindow = Core::App().windowFor(account); const auto anyWindow = (sessionWindow && &sessionWindow->account() == account) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 02bdca5fb007b..1bac50ff3a14f 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -303,7 +303,7 @@ class OverlayWidget final bool moveToNext(int delta); void preloadData(int delta); - void handleScreenChanged(QScreen *screen); + void handleScreenChanged(not_null screen); [[nodiscard]] bool computeSaveButtonVisible() const; void checkForSaveLoaded(); diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index f2bc745500714..da359769fa7bf 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -24,7 +24,6 @@ For license and copyright information please follow this link: #include "core/application.h" #include "base/platform/base_platform_info.h" #include "base/power_save_blocker.h" -#include "base/event_filter.h" #include "ui/platform/ui_platform_utility.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/buttons.h" @@ -351,49 +350,29 @@ void PipPanel::init() { widget()->setMouseTracking(true); widget()->resize(0, 0); widget()->hide(); - widget()->createWinId(); - rp()->shownValue( - ) | rpl::filter([=](bool shown) { - return shown; - }) | rpl::start_with_next([=] { + rpl::merge( + rp()->shownValue() | rpl::to_empty, + rp()->paintRequest() | rpl::to_empty + ) | rpl::map([=] { + return widget()->windowHandle() + && widget()->windowHandle()->isExposed(); + }) | rpl::distinct_until_changed( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { // Workaround Qt's forced transient parent. Ui::Platform::ClearTransientParent(widget()); }, rp()->lifetime()); - QObject::connect( - widget()->windowHandle(), - &QWindow::screenChanged, - [=](QScreen *screen) { - handleScreenChanged(screen); - }); + rp()->screenValue( + ) | rpl::skip(1) | rpl::start_with_next([=](not_null screen) { + handleScreenChanged(screen); + }, rp()->lifetime()); if (Platform::IsWayland()) { rp()->sizeValue( - ) | rpl::start_with_next([=](QSize size) { + ) | rpl::skip(1) | rpl::start_with_next([=](QSize size) { handleWaylandResize(size); }, rp()->lifetime()); - - base::install_event_filter(widget(), [=](not_null event) { - if (event->type() == QEvent::Resize && _inHandleWaylandResize) { - return base::EventFilterResult::Cancel; - } - return base::EventFilterResult::Continue; - }); - - base::install_event_filter(widget()->windowHandle(), [=](not_null event) { - if (event->type() == QEvent::Resize) { - if (_inHandleWaylandResize) { - return base::EventFilterResult::Cancel; - } - const auto newSize = static_cast(event.get())->size(); - if (_suggestedWaylandSize == newSize) { - handleWaylandResize(newSize); - return base::EventFilterResult::Cancel; - } - } - return base::EventFilterResult::Continue; - }); } } @@ -514,7 +493,11 @@ void PipPanel::setPositionDefault() { const auto parentScreen = widgetScreen(_parent); const auto myScreen = widget()->screen(); if (parentScreen && myScreen != parentScreen) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + widget()->setScreen(parentScreen); +#else // Qt >= 6.0.0 widget()->windowHandle()->setScreen(parentScreen); +#endif // Qt < 6.0.0 } auto position = Position(); position.snapped = RectPart::Top | RectPart::Left; @@ -608,8 +591,10 @@ void PipPanel::setGeometry(QRect geometry) { } void PipPanel::handleWaylandResize(QSize size) { + if (_inHandleWaylandResize) { + return; + } _inHandleWaylandResize = true; - _suggestedWaylandSize = size; // Apply aspect ratio. const auto max = std::max(size.width(), size.height()); @@ -630,10 +615,12 @@ void PipPanel::handleWaylandResize(QSize size) { : scaled; widget()->resize(normalized); + QResizeEvent e(normalized, size); + QCoreApplication::sendEvent(widget()->windowHandle(), &e); _inHandleWaylandResize = false; } -void PipPanel::handleScreenChanged(QScreen *screen) { +void PipPanel::handleScreenChanged(not_null screen) { const auto screenGeometry = screen->availableGeometry(); const auto minimalSize = _ratio.scaled( st::pipMinimalSize, diff --git a/Telegram/SourceFiles/media/view/media_view_pip.h b/Telegram/SourceFiles/media/view/media_view_pip.h index dcc251ec3d104..d0156e1bd224a 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.h +++ b/Telegram/SourceFiles/media/view/media_view_pip.h @@ -77,7 +77,7 @@ class PipPanel final { [[nodiscard]] bool dragging() const; void handleWaylandResize(QSize size); - void handleScreenChanged(QScreen *screen); + void handleScreenChanged(not_null screen); void handleMousePress(QPoint position, Qt::MouseButton button); void handleMouseRelease(QPoint position, Qt::MouseButton button); void handleMouseMove(QPoint position); @@ -106,7 +106,6 @@ class PipPanel final { bool _useTransparency = true; bool _dragDisabled = false; bool _inHandleWaylandResize = false; - QSize _suggestedWaylandSize; style::margins _padding; RectPart _overState = RectPart(); diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 58328da12326d..231e7ff23ebe5 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "base/event_filter.h" #include "base/unixtime.h" #include "boxes/abstract_box.h" +#include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_emoji_pack.h" #include "core/shortcuts.h" @@ -526,7 +527,7 @@ void EffectPreview::setupSend(Details details) { if (const auto onstack = _close) { onstack(); } - Settings::ShowPremium(window, "message_effect"); + ShowPremiumPreviewBox(window, PremiumFeature::Effects); } return false; }); @@ -618,7 +619,8 @@ FillMenuResult FillSendMenu( const auto sending = (type != Type::Disabled); const auto empty = !sending && (details.spoiler == SpoilerState::None) - && (details.caption == CaptionState::None); + && (details.caption == CaptionState::None) + && !details.price.has_value(); if (empty || !action) { return FillMenuResult::Skipped; } @@ -651,7 +653,8 @@ FillMenuResult FillSendMenu( if ((type != Type::Disabled) && ((details.spoiler != SpoilerState::None) - || (details.caption != CaptionState::None))) { + || (details.caption != CaptionState::None) + || details.price.has_value())) { menu->addSeparator(&st::expandedMenuSeparator); } if (details.spoiler != SpoilerState::None) { @@ -678,6 +681,14 @@ FillMenuResult FillSendMenu( }, details); }, above ? &icons.menuBelow : &icons.menuAbove); } + if (details.price) { + menu->addAction( + ((*details.price > 0) + ? tr::lng_context_change_price(tr::now) + : tr::lng_context_make_paid(tr::now)), + [=] { action({ .type = ActionType::ChangePrice }, details); }, + &icons.menuPrice); + } using namespace HistoryView::Reactions; const auto effect = std::make_shared>(); diff --git a/Telegram/SourceFiles/menu/menu_send.h b/Telegram/SourceFiles/menu/menu_send.h index 2c6dbb9b721ba..5c3a99bae9cf9 100644 --- a/Telegram/SourceFiles/menu/menu_send.h +++ b/Telegram/SourceFiles/menu/menu_send.h @@ -53,6 +53,7 @@ struct Details { Type type = Type::Disabled; SpoilerState spoiler = SpoilerState::None; CaptionState caption = CaptionState::None; + std::optional price; bool effectAllowed = false; }; @@ -69,6 +70,7 @@ enum class ActionType : uchar { SpoilerOff, CaptionUp, CaptionDown, + ChangePrice, }; struct Action { using Type = ActionType; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 93865a58746f2..4ab0ce83991cc 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/124.0.0.0 Safari/537.36"); + "Chrome/126.0.0.0 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index f62e07bf66fdf..394a50efaf381 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -44,6 +44,7 @@ inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector s inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; +inputMediaPaidMedia#aa661fc3 stars_amount:long extended_media:Vector = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -101,7 +102,7 @@ channel#aadfc8f flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5 channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull; +channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -133,6 +134,7 @@ messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; messageMediaGiveaway#daad85b0 flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector countries_iso2:flags.1?Vector prize_description:flags.3?string quantity:int months:int until_date:int = MessageMedia; messageMediaGiveawayResults#c6991068 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector months:int prize_description:flags.1?string until_date:int = MessageMedia; +messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; @@ -177,6 +179,7 @@ messageActionGiveawayLaunch#332ba9ed = MessageAction; messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = MessageAction; messageActionBoostApply#cc02aa6d boosts:int = MessageAction; messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector = MessageAction; +messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -384,7 +387,7 @@ updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update; updateRecentEmojiStatuses#30f443db = Update; updateRecentReactions#6f7863f4 = Update; updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update; -updateMessageExtendedMedia#5a73a98c peer:Peer msg_id:int extended_media:MessageExtendedMedia = Update; +updateMessageExtendedMedia#d5a41724 peer:Peer msg_id:int extended_media:Vector = Update; updateChannelPinnedTopic#192efbe3 flags:# pinned:flags.0?true channel_id:long topic_id:int = Update; updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector = Update; updateUser#20529438 user_id:long = Update; @@ -415,6 +418,8 @@ updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages: updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; updateStarsBalance#fb85198 balance:long = Update; +updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; +updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -759,7 +764,7 @@ auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeTyp auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#13c90f17 flags:# nonce:flags.0?bytes play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; +auth.sentCodeTypeFirebaseSms#9fd736 flags:# nonce:flags.0?bytes play_integrity_project_id:flags.2?long play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; @@ -792,7 +797,7 @@ contacts.topPeers#70b772a8 categories:Vector chats:Vector< contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#3fccf7ef flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int = DraftMessage; +draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; @@ -1420,9 +1425,7 @@ attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector attachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector = AttachMenuBotsBot; -webViewResultUrl#c14557c query_id:long url:string = WebViewResult; - -simpleWebViewResultUrl#882f76bb url:string = SimpleWebViewResult; +webViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true query_id:flags.0?long url:string = WebViewResult; webViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent; @@ -1552,8 +1555,6 @@ botApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string messages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true has_settings:flags.2?true app:BotApp = messages.BotApp; -appWebViewResultUrl#3c1b4f0d url:string = AppWebViewResult; - inlineBotWebView#b57295d5 text:string url:string = InlineBotWebView; readParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate; @@ -1603,14 +1604,15 @@ exportedStoryLink#3fc9053b link:string = ExportedStoryLink; storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; -mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double = MediaAreaCoordinates; +mediaAreaCoordinates#cfc9e002 flags:# x:double y:double w:double h:double rotation:double radius:flags.0?double = MediaAreaCoordinates; mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; -mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; +mediaAreaGeoPoint#cad5452d flags:# coordinates:MediaAreaCoordinates geo:GeoPoint address:flags.0?GeoPointAddress = MediaArea; mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea; inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea; +mediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea; peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; @@ -1800,13 +1802,30 @@ starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; +starsTransactionPeerAds#60682812 = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#cc7079b2 flags:# refund:flags.3?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument = StarsTransaction; +starsTransaction#2db5418f flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector = StarsTransaction; payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; +foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; + +stories.foundStories#e2de7737 flags:# count:int stories:Vector next_offset:flags.0?string chats:Vector users:Vector = stories.FoundStories; + +geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress; + +starsRevenueStatus#79342946 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long next_withdrawal_at:flags.1?int = StarsRevenueStatus; + +payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; + +payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl; + +payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl; + +inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2050,7 +2069,7 @@ messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_me messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; -messages.saveDraft#7ff3b806 flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia = Bool; +messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia effect:flags.7?long = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector = Bool; @@ -2150,9 +2169,9 @@ messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = mes messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; messages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool; -messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; +messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true compact:flags.7?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; messages.prolongWebView#b0d81a83 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = Bool; -messages.requestSimpleWebView#1a46500a flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = SimpleWebViewResult; +messages.requestSimpleWebView#413a3e73 flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true compact:flags.7?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = WebViewResult; messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio; @@ -2174,7 +2193,7 @@ messages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups; messages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList; messages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool; messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; -messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult; +messages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; @@ -2349,9 +2368,13 @@ payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayI payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; payments.getStarsTopupOptions#c00ec7d3 = Vector; payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; -payments.getStarsTransactions#673ac2f9 flags:# inbound:flags.0?true outbound:flags.1?true peer:InputPeer offset:string = payments.StarsStatus; +payments.getStarsTransactions#97938d5a flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true peer:InputPeer offset:string limit:int = payments.StarsStatus; payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; +payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer = payments.StarsRevenueStats; +payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; +payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl; +payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector = payments.StarsStatus; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2453,6 +2476,7 @@ stories.getChatsToSend#a56a8b60 = messages.Chats; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; stories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList; stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector = Bool; +stories.searchPosts#6cea116a flags:# hashtag:flags.0?string area:flags.1?MediaArea offset:string limit:int = stories.FoundStories; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; @@ -2470,4 +2494,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 181 +// LAYER 184 diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index c7e2a9466d3fd..c5bf744439535 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -159,7 +159,7 @@ passportDetailsField: InputField(defaultInputField) { placeholderScale: 0.; placeholderFont: normalFont; heightMin: 32px; - font: normalFont; + style: defaultTextStyle; } passportDetailsDateField: InputField(passportDetailsField) { textMargins: margins(2px, 8px, 2px, 0px); @@ -178,7 +178,7 @@ passportDetailsSeparator: FlatLabel(passportPasswordLabelBold) { } passportDetailsSeparatorPadding: margins(5px, 8px, 5px, 0px); passportContactField: InputField(defaultInputField) { - font: normalFont; + style: defaultTextStyle; } passportDetailsFieldLeft: 116px; passportDetailsFieldTop: 2px; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 8dac6d0c55542..c09b41c36351d 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -399,6 +399,7 @@ void Form::requestForm() { .amount = amount, }; const auto formData = CreditsFormData{ + .id = _id, .formId = data.vform_id().v, .botId = data.vbot_id().v, .title = qs(data.vtitle()), diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index cf4cef890372b..c85d946ed298b 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -178,6 +178,7 @@ struct InvoiceId { }; struct CreditsFormData { + InvoiceId id; uint64 formId = 0; uint64 botId = 0; QString title; diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index 112cdd5762cf2..69cf7abdea222 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -27,7 +27,6 @@ For license and copyright information please follow this link: #include "window/window_session_controller.h" namespace Payments { -namespace { bool IsCreditsInvoice(not_null item) { if (const auto payment = item->Get()) { @@ -38,8 +37,6 @@ bool IsCreditsInvoice(not_null item) { return invoice && (invoice->currency == Ui::kCreditsCurrency); } -} // namespace - Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot) { @@ -108,7 +105,8 @@ Fn ProcessNonPanelPaymentFormFactory( .date = base::unixtime::parse(receipt->date), .photoId = receipt->photo ? receipt->photo->id : 0, .credits = receipt->credits, - .bareId = receipt->peerId.value, + .bareMsgId = uint64(), + .barePeerId = receipt->peerId.value, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, }; controller->uiShow()->show(Box( diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.h b/Telegram/SourceFiles/payments/payments_non_panel_process.h index fb647a72a35b9..e8ab9375c84bb 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.h +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.h @@ -18,6 +18,8 @@ namespace Payments { enum class CheckoutResult; struct NonPanelPaymentForm; +[[nodiscard]] bool IsCreditsInvoice(not_null item); + Fn ProcessNonPanelPaymentFormFactory( not_null controller, Fn maybeReturnToBot = nullptr); diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 1314674ce239e..e5fbcb34e68fd 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -305,7 +305,7 @@ struct SimpleFieldState { .st = st::paymentsMoneyField, }); const auto &rule = state->rule; - state->currencySkip = rule.space ? state->st.font->spacew : 0; + state->currencySkip = rule.space ? state->st.style.font->spacew : 0; state->currencyText = ((!rule.left && rule.space) ? QString(QChar(' ')) : QString()) + (*rule.international @@ -343,7 +343,7 @@ struct SimpleFieldState { } const auto updateRight = [=] { const auto text = result->getLastText(); - const auto width = state->st.font->width(text); + const auto width = state->st.style.font->width(text); const auto &rule = state->rule; const auto symbol = QChar(rule.decimal); const auto decimal = text.indexOf(symbol); diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index ff9fddc747bdb..7b17fcc77c5b0 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -51,13 +51,13 @@ using WorkMode = Core::Settings::WorkMode; #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION void XCBSkipTaskbar(QWindow *window, bool skip) { - const auto connection = base::Platform::XCB::GetConnectionFromQt(); - if (!connection) { + const base::Platform::XCB::Connection connection; + if (!connection || xcb_connection_has_error(connection)) { return; } const auto root = base::Platform::XCB::GetRootWindow(connection); - if (!root.has_value()) { + if (!root) { return; } @@ -65,7 +65,7 @@ void XCBSkipTaskbar(QWindow *window, bool skip) { connection, "_NET_WM_STATE"); - if (!stateAtom.has_value()) { + if (!stateAtom) { return; } @@ -73,29 +73,32 @@ void XCBSkipTaskbar(QWindow *window, bool skip) { connection, "_NET_WM_STATE_SKIP_TASKBAR"); - if (!skipTaskbarAtom.has_value()) { + if (!skipTaskbarAtom) { return; } xcb_client_message_event_t xev; xev.response_type = XCB_CLIENT_MESSAGE; - xev.type = *stateAtom; + xev.type = stateAtom; xev.sequence = 0; xev.window = window->winId(); xev.format = 32; xev.data.data32[0] = skip ? 1 : 0; - xev.data.data32[1] = *skipTaskbarAtom; + xev.data.data32[1] = skipTaskbarAtom; xev.data.data32[2] = 0; xev.data.data32[3] = 0; xev.data.data32[4] = 0; - xcb_send_event( - connection, - false, - *root, - XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT - | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, - reinterpret_cast(&xev)); + free( + xcb_request_check( + connection, + xcb_send_event_checked( + connection, + false, + root, + XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT + | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, + reinterpret_cast(&xev)))); } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION @@ -109,16 +112,18 @@ void SkipTaskbar(QWindow *window, bool skip) { } void SendKeySequence( - Qt::Key key, - Qt::KeyboardModifiers modifiers = Qt::NoModifier) { - const auto focused = static_cast(QApplication::focusWidget()); + Qt::Key key, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) { + const auto focused = QApplication::focusWidget(); if (qobject_cast(focused) || qobject_cast(focused) || dynamic_cast(focused)) { - QKeyEvent pressEvent(QEvent::KeyPress, key, modifiers); - focused->event(&pressEvent); - QKeyEvent releaseEvent(QEvent::KeyRelease, key, modifiers); - focused->event(&releaseEvent); + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyPress, key, modifiers)); + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyRelease, key, modifiers)); } } @@ -136,35 +141,6 @@ MainWindow::MainWindow(not_null controller) : Window::MainWindow(controller) { } -void MainWindow::initHook() { - events() | rpl::start_with_next([=](not_null e) { - if (e->type() == QEvent::ThemeChange) { - updateWindowIcon(); - } - }, lifetime()); - - base::install_event_filter(windowHandle(), [=](not_null e) { - if (e->type() == QEvent::Expose) { - auto ee = static_cast(e.get()); - if (ee->region().isNull()) { - return base::EventFilterResult::Continue; - } - if (!windowHandle() - || windowHandle()->parent() - || !windowHandle()->isVisible()) { - return base::EventFilterResult::Continue; - } - handleNativeSurfaceChanged(true); - } else if (e->type() == QEvent::Hide) { - if (!windowHandle() || windowHandle()->parent()) { - return base::EventFilterResult::Continue; - } - handleNativeSurfaceChanged(false); - } - return base::EventFilterResult::Continue; - }); -} - void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) { if (!TrayIconSupported()) { return; @@ -258,6 +234,7 @@ void MainWindow::createGlobalMenu() { QKeySequence::Quit); quit->setMenuRole(QAction::QuitRole); + quit->setShortcutContext(Qt::WidgetShortcut); auto edit = psMainMenu->addMenu(tr::lng_mac_menu_edit(tr::now)); @@ -266,6 +243,8 @@ void MainWindow::createGlobalMenu() { [] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); }, QKeySequence::Undo); + psUndo->setShortcutContext(Qt::WidgetShortcut); + psRedo = edit->addAction( tr::lng_linux_menu_redo(tr::now), [] { @@ -275,6 +254,8 @@ void MainWindow::createGlobalMenu() { }, QKeySequence::Redo); + psRedo->setShortcutContext(Qt::WidgetShortcut); + edit->addSeparator(); psCut = edit->addAction( @@ -282,21 +263,29 @@ void MainWindow::createGlobalMenu() { [] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); }, QKeySequence::Cut); + psCut->setShortcutContext(Qt::WidgetShortcut); + psCopy = edit->addAction( tr::lng_mac_menu_copy(tr::now), [] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); }, QKeySequence::Copy); + psCopy->setShortcutContext(Qt::WidgetShortcut); + psPaste = edit->addAction( tr::lng_mac_menu_paste(tr::now), [] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); }, QKeySequence::Paste); + psPaste->setShortcutContext(Qt::WidgetShortcut); + psDelete = edit->addAction( tr::lng_mac_menu_delete(tr::now), [] { SendKeySequence(Qt::Key_Delete); }, QKeySequence(Qt::ControlModifier | Qt::Key_Backspace)); + psDelete->setShortcutContext(Qt::WidgetShortcut); + edit->addSeparator(); psBold = edit->addAction( @@ -304,16 +293,22 @@ void MainWindow::createGlobalMenu() { [] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); }, QKeySequence::Bold); + psBold->setShortcutContext(Qt::WidgetShortcut); + psItalic = edit->addAction( tr::lng_menu_formatting_italic(tr::now), [] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); }, QKeySequence::Italic); + psItalic->setShortcutContext(Qt::WidgetShortcut); + psUnderline = edit->addAction( tr::lng_menu_formatting_underline(tr::now), [] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); }, QKeySequence::Underline); + psUnderline->setShortcutContext(Qt::WidgetShortcut); + psStrikeOut = edit->addAction( tr::lng_menu_formatting_strike_out(tr::now), [] { @@ -323,6 +318,8 @@ void MainWindow::createGlobalMenu() { }, Ui::kStrikeOutSequence); + psStrikeOut->setShortcutContext(Qt::WidgetShortcut); + psBlockquote = edit->addAction( tr::lng_menu_formatting_blockquote(tr::now), [] { @@ -332,6 +329,8 @@ void MainWindow::createGlobalMenu() { }, Ui::kBlockquoteSequence); + psBlockquote->setShortcutContext(Qt::WidgetShortcut); + psMonospace = edit->addAction( tr::lng_menu_formatting_monospace(tr::now), [] { @@ -341,6 +340,8 @@ void MainWindow::createGlobalMenu() { }, Ui::kMonospaceSequence); + psMonospace->setShortcutContext(Qt::WidgetShortcut); + psClearFormat = edit->addAction( tr::lng_menu_formatting_clear(tr::now), [] { @@ -350,6 +351,8 @@ void MainWindow::createGlobalMenu() { }, Ui::kClearFormatSequence); + psClearFormat->setShortcutContext(Qt::WidgetShortcut); + edit->addSeparator(); psSelectAll = edit->addAction( @@ -357,6 +360,8 @@ void MainWindow::createGlobalMenu() { [] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); }, QKeySequence::SelectAll); + psSelectAll->setShortcutContext(Qt::WidgetShortcut); + edit->addSeparator(); auto prefs = edit->addAction( @@ -369,6 +374,7 @@ void MainWindow::createGlobalMenu() { QKeySequence(Qt::ControlModifier | Qt::Key_Comma)); prefs->setMenuRole(QAction::PreferencesRole); + prefs->setShortcutContext(Qt::WidgetShortcut); auto tools = psMainMenu->addMenu(tr::lng_linux_menu_tools(tr::now)); @@ -514,19 +520,22 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *evt) { updateGlobalMenu(); } } + } else if (obj == this && t == QEvent::Paint) { + if (!_exposed) { + _exposed = true; + SkipTaskbar( + windowHandle(), + (Core::App().settings().workMode() == WorkMode::TrayOnly) + && TrayIconSupported()); + } + } else if (obj == this && t == QEvent::Hide) { + _exposed = false; + } else if (obj == this && t == QEvent::ThemeChange) { + updateWindowIcon(); } return Window::MainWindow::eventFilter(obj, evt); } -void MainWindow::handleNativeSurfaceChanged(bool exist) { - if (exist) { - SkipTaskbar( - windowHandle(), - (Core::App().settings().workMode() == WorkMode::TrayOnly) - && TrayIconSupported()); - } -} - MainWindow::~MainWindow() { } diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.h b/Telegram/SourceFiles/platform/linux/main_window_linux.h index 311ed3a7422f1..4ef98ba7735a1 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.h +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.h @@ -28,7 +28,6 @@ class MainWindow : public Window::MainWindow { protected: bool eventFilter(QObject *obj, QEvent *evt) override; - void initHook() override; void unreadCounterChangedHook() override; void updateGlobalMenuHook() override; @@ -37,7 +36,6 @@ class MainWindow : public Window::MainWindow { private: void updateUnityCounter(); - void handleNativeSurfaceChanged(bool exist); QMenuBar *psMainMenu = nullptr; QAction *psLogout = nullptr; @@ -61,6 +59,8 @@ class MainWindow : public Window::MainWindow { QAction *psMonospace = nullptr; QAction *psClearFormat = nullptr; + bool _exposed = false; + }; [[nodiscard]] inline int32 ScreenNameChecksum(const QString &name) { diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index c874ae71d1cdd..6d8a8ec54d731 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -7,6 +7,7 @@ For license and copyright information please follow this link: */ #include "platform/linux/specific_linux.h" +#include "base/openssl_help.h" #include "base/random.h" #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_dbus_utilities.h" @@ -480,6 +481,16 @@ void InstallLauncher() { }); } +[[nodiscard]] QByteArray HashForSocketPath(const QByteArray &data) { + constexpr auto kHashForSocketPathLength = 24; + + const auto binary = openssl::Sha256(bytes::make_span(data)); + const auto base64 = QByteArray( + reinterpret_cast(binary.data()), + binary.size()).toBase64(QByteArray::Base64UrlEncoding); + return base64.mid(0, kHashForSocketPathLength); +} + } // namespace namespace Platform { @@ -564,7 +575,7 @@ bool SkipTaskbarSupported() { #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION if (IsX11()) { return base::Platform::XCB::IsSupportedByWM( - base::Platform::XCB::GetConnectionFromQt(), + base::Platform::XCB::Connection(), "_NET_WM_STATE_SKIP_TASKBAR"); } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION @@ -686,8 +697,8 @@ void start() { Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-%4"_q.arg( QDir::tempPath(), - h, - QCoreApplication::applicationName(), + HashForSocketPath(d), + u"TD"_q,//QCoreApplication::applicationName(), - make path smaller. u"%1"_q).toStdString()); InstallLauncher(); diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.cpp b/Telegram/SourceFiles/platform/linux/tray_linux.cpp index ab1e34df8c020..2d0e23aecfb4b 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/tray_linux.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "base/invoke_queued.h" #include "base/qt_signal_producer.h" +#include "base/platform/linux/base_linux_dbus_utilities.h" #include "core/application.h" #include "core/sandbox.h" #include "platform/platform_specific.h" @@ -21,9 +22,13 @@ For license and copyright information please follow this link: #include #include +#include + namespace Platform { namespace { +using namespace gi::repository; + [[nodiscard]] QString PanelIconName(int counter, bool muted) { return (counter > 0) ? (muted @@ -282,11 +287,29 @@ rpl::producer<> TrayEventFilter::contextMenuFilters() const { } Tray::Tray() { - LOG(("System tray available: %1").arg(Logs::b(TrayIconSupported()))); + auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr); + if (connection) { + _sniWatcher = std::make_unique( + connection.gobj_(), + "org.kde.StatusNotifierWatcher", + [=]( + const std::string &service, + const std::string &oldOwner, + const std::string &newOwner) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + if (hasIcon()) { + destroyIcon(); + createIcon(); + } + }); + }); + } } void Tray::createIcon() { if (!_icon) { + LOG(("System tray available: %1").arg(Logs::b(TrayIconSupported()))); + if (!_iconGraphic) { _iconGraphic = std::make_unique(); } diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.h b/Telegram/SourceFiles/platform/linux/tray_linux.h index dd0175a2c5a68..712592777076a 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.h +++ b/Telegram/SourceFiles/platform/linux/tray_linux.h @@ -11,6 +11,10 @@ For license and copyright information please follow this link: #include "base/unique_qptr.h" +namespace base::Platform::DBus { +class ServiceWatcher; +} // namespace base::Platform::DBus + namespace Ui { class PopupMenu; } // namespace Ui @@ -51,6 +55,7 @@ class Tray final { [[nodiscard]] rpl::lifetime &lifetime(); private: + std::unique_ptr _sniWatcher; std::unique_ptr _iconGraphic; base::unique_qptr _icon; diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.mm b/Telegram/SourceFiles/platform/mac/main_window_mac.mm index 43ba894b43abb..f4e49f798b019 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.mm +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.mm @@ -158,15 +158,19 @@ - (void) screenIsUnlocked:(NSNotification *)aNotification { namespace Platform { namespace { -void SendKeySequence(Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { - const auto focused = static_cast(QApplication::focusWidget()); +void SendKeySequence( + Qt::Key key, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) { + const auto focused = QApplication::focusWidget(); if (qobject_cast(focused) || qobject_cast(focused) || dynamic_cast(focused)) { - QKeyEvent pressEvent(QEvent::KeyPress, key, modifiers); - focused->event(&pressEvent); - QKeyEvent releaseEvent(QEvent::KeyRelease, key, modifiers); - focused->event(&releaseEvent); + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyPress, key, modifiers)); + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyRelease, key, modifiers)); } } @@ -376,12 +380,13 @@ QString strNotificationAboutScreenUnlocked() { ensureWindowShown(); controller().showSettings(); }; - main->addAction( + auto prefs = main->addAction( tr::lng_mac_menu_preferences(tr::now), this, std::move(callback), - QKeySequence(Qt::ControlModifier | Qt::Key_Comma)) - ->setMenuRole(QAction::PreferencesRole); + QKeySequence(Qt::ControlModifier | Qt::Key_Comma)); + prefs->setMenuRole(QAction::PreferencesRole); + prefs->setShortcutContext(Qt::WidgetShortcut); } QMenu *file = psMainMenu.addMenu(tr::lng_mac_menu_file(tr::now)); @@ -402,6 +407,7 @@ QString strNotificationAboutScreenUnlocked() { this, [] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); }, QKeySequence::Undo); + psUndo->setShortcutContext(Qt::WidgetShortcut); psRedo = edit->addAction( tr::lng_mac_menu_redo(tr::now), this, @@ -411,27 +417,32 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, QKeySequence::Redo); + psRedo->setShortcutContext(Qt::WidgetShortcut); edit->addSeparator(); psCut = edit->addAction( tr::lng_mac_menu_cut(tr::now), this, [] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); }, QKeySequence::Cut); + psCut->setShortcutContext(Qt::WidgetShortcut); psCopy = edit->addAction( tr::lng_mac_menu_copy(tr::now), this, [] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); }, QKeySequence::Copy); + psCopy->setShortcutContext(Qt::WidgetShortcut); psPaste = edit->addAction( tr::lng_mac_menu_paste(tr::now), this, [] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); }, QKeySequence::Paste); + psPaste->setShortcutContext(Qt::WidgetShortcut); psDelete = edit->addAction( tr::lng_mac_menu_delete(tr::now), this, [] { SendKeySequence(Qt::Key_Delete); }, QKeySequence(Qt::ControlModifier | Qt::Key_Backspace)); + psDelete->setShortcutContext(Qt::WidgetShortcut); edit->addSeparator(); psBold = edit->addAction( @@ -439,16 +450,19 @@ QString strNotificationAboutScreenUnlocked() { this, [] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); }, QKeySequence::Bold); + psBold->setShortcutContext(Qt::WidgetShortcut); psItalic = edit->addAction( tr::lng_menu_formatting_italic(tr::now), this, [] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); }, QKeySequence::Italic); + psItalic->setShortcutContext(Qt::WidgetShortcut); psUnderline = edit->addAction( tr::lng_menu_formatting_underline(tr::now), this, [] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); }, QKeySequence::Underline); + psUnderline->setShortcutContext(Qt::WidgetShortcut); psStrikeOut = edit->addAction( tr::lng_menu_formatting_strike_out(tr::now), this, @@ -458,6 +472,7 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, Ui::kStrikeOutSequence); + psStrikeOut->setShortcutContext(Qt::WidgetShortcut); psBlockquote = edit->addAction( tr::lng_menu_formatting_blockquote(tr::now), this, @@ -467,6 +482,7 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, Ui::kBlockquoteSequence); + psBlockquote->setShortcutContext(Qt::WidgetShortcut); psMonospace = edit->addAction( tr::lng_menu_formatting_monospace(tr::now), this, @@ -476,6 +492,7 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, Ui::kMonospaceSequence); + psMonospace->setShortcutContext(Qt::WidgetShortcut); psClearFormat = edit->addAction( tr::lng_menu_formatting_clear(tr::now), this, @@ -485,6 +502,7 @@ QString strNotificationAboutScreenUnlocked() { Qt::ControlModifier | Qt::ShiftModifier); }, Ui::kClearFormatSequence); + psClearFormat->setShortcutContext(Qt::WidgetShortcut); edit->addSeparator(); psSelectAll = edit->addAction( @@ -492,13 +510,15 @@ QString strNotificationAboutScreenUnlocked() { this, [] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); }, QKeySequence::SelectAll); + psSelectAll->setShortcutContext(Qt::WidgetShortcut); edit->addSeparator(); edit->addAction( tr::lng_mac_menu_emoji_and_symbols(tr::now).replace('&', "&&"), this, [] { [NSApp orderFrontCharacterPalette:nil]; }, - QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_Space)); + QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_Space) + )->setShortcutContext(Qt::WidgetShortcut); QMenu *window = psMainMenu.addMenu(tr::lng_mac_menu_window(tr::now)); psContacts = window->addAction(tr::lng_mac_menu_contacts(tr::now)); diff --git a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp index a63249ea5bdba..5bf88365897ff 100644 --- a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp +++ b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp @@ -19,6 +19,7 @@ For license and copyright information please follow this link: #include #include #include +#include #include #include diff --git a/Telegram/SourceFiles/platform/win/integration_win.cpp b/Telegram/SourceFiles/platform/win/integration_win.cpp index 3d8b592e7c2ae..0a44470da6f65 100644 --- a/Telegram/SourceFiles/platform/win/integration_win.cpp +++ b/Telegram/SourceFiles/platform/win/integration_win.cpp @@ -43,7 +43,7 @@ WindowsIntegration &WindowsIntegration::Instance() { bool WindowsIntegration::nativeEventFilter( const QByteArray &eventType, void *message, - long *result) { + native_event_filter_result *result) { return Core::Sandbox::Instance().customEnterFromEventLoop([&] { const auto msg = static_cast(message); return processEvent( diff --git a/Telegram/SourceFiles/platform/win/integration_win.h b/Telegram/SourceFiles/platform/win/integration_win.h index 0b29007d32856..c58a2846ceaf9 100644 --- a/Telegram/SourceFiles/platform/win/integration_win.h +++ b/Telegram/SourceFiles/platform/win/integration_win.h @@ -29,7 +29,7 @@ class WindowsIntegration final bool nativeEventFilter( const QByteArray &eventType, void *message, - long *result) override; + native_event_filter_result *result) override; bool processEvent( HWND hWnd, UINT msg, diff --git a/Telegram/SourceFiles/platform/win/main_window_win.cpp b/Telegram/SourceFiles/platform/win/main_window_win.cpp index dd2f3dafec104..ede58134f488c 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.cpp +++ b/Telegram/SourceFiles/platform/win/main_window_win.cpp @@ -30,7 +30,6 @@ For license and copyright information please follow this link: #include "window/window_controller.h" #include "history/history.h" -#include #include #include #include @@ -81,7 +80,7 @@ class EventFilter final : public QAbstractNativeEventFilter { bool nativeEventFilter( const QByteArray &eventType, void *message, - long *result) override; + native_event_filter_result *result) override; bool mainWindowEvent( HWND hWnd, @@ -172,7 +171,7 @@ EventFilter::EventFilter(not_null window) : _window(window) { bool EventFilter::nativeEventFilter( const QByteArray &eventType, void *message, - long *result) { + native_event_filter_result *result) { return Core::Sandbox::Instance().customEnterFromEventLoop([&] { const auto msg = static_cast(message); if (msg->hwnd == _window->psHwnd() @@ -483,7 +482,7 @@ bool MainWindow::initGeometryFromSystem() { bool MainWindow::nativeEvent( const QByteArray &eventType, void *message, - long *result) { + native_event_filter_result *result) { if (message) { const auto msg = static_cast(message); if (msg->message == WM_IME_STARTCOMPOSITION) { diff --git a/Telegram/SourceFiles/platform/win/main_window_win.h b/Telegram/SourceFiles/platform/win/main_window_win.h index 63dc6ba17b237..9f4884467ecf2 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.h +++ b/Telegram/SourceFiles/platform/win/main_window_win.h @@ -51,7 +51,7 @@ class MainWindow : public Window::MainWindow { bool nativeEvent( const QByteArray &eventType, void *message, - long *result) override; + native_event_filter_result *result) override; private: struct Private; diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index ef5c88e3518f8..a35cfd3c44e22 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -29,7 +29,6 @@ For license and copyright information please follow this link: #include #include -#include #include #include @@ -99,7 +98,6 @@ BOOL CALLBACK FindToActivate(HWND hwnd, LPARAM lParam) { return TRUE; } // Found a Top-Level window. - auto level = 0; if (WindowIdFromHWND(hwnd) == request->windowId) { request->result = hwnd; request->resultLevel = 3; @@ -311,8 +309,8 @@ void psDoFixPrevious() { if (oldKeyRes2 == ERROR_SUCCESS) RegCloseKey(oldKey2); if (existNew1 || existNew2) { - const auto deleteKeyRes1 = existOld1 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str()) : ERROR_SUCCESS; - const auto deleteKeyRes2 = existOld2 ? RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str()) : ERROR_SUCCESS; + if (existOld1) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str()); + if (existOld2) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str()); } QString userDesktopLnk, commonDesktopLnk; @@ -327,7 +325,7 @@ void psDoFixPrevious() { } QFile userDesktopFile(userDesktopLnk), commonDesktopFile(commonDesktopLnk); if (QFile::exists(userDesktopLnk) && QFile::exists(commonDesktopLnk) && userDesktopLnk != commonDesktopLnk) { - bool removed = QFile::remove(commonDesktopLnk); + QFile::remove(commonDesktopLnk); } } catch (...) { } diff --git a/Telegram/SourceFiles/platform/win/tray_win.h b/Telegram/SourceFiles/platform/win/tray_win.h index cb79c3beb5aef..aed4cc6e3442d 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.h +++ b/Telegram/SourceFiles/platform/win/tray_win.h @@ -12,7 +12,7 @@ For license and copyright information please follow this link: #include "base/unique_qptr.h" namespace Window { -class CounterLayerArgs; +struct CounterLayerArgs; } // namespace Window namespace Ui { diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.cpp b/Telegram/SourceFiles/platform/win/windows_dlls.cpp index 7184b2f4b05ec..0e03904f70c37 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.cpp +++ b/Telegram/SourceFiles/platform/win/windows_dlls.cpp @@ -8,13 +8,16 @@ For license and copyright information please follow this link: #include "platform/win/windows_dlls.h" #include "base/platform/win/base_windows_safe_library.h" +#include "ui/gl/gl_detection.h" #include #include #define LOAD_SYMBOL(lib, name) ::base::Platform::LoadMethod(lib, #name, name) +#ifdef DESKTOP_APP_USE_ANGLE bool DirectXResolveCompiler(); +#endif // DESKTOP_APP_USE_ANGLE namespace Platform { namespace Dlls { @@ -67,6 +70,7 @@ SafeIniter kSafeIniter; } // namespace void CheckLoadedModules() { +#ifdef DESKTOP_APP_USE_ANGLE if (DirectXResolveCompiler()) { auto LibD3DCompiler = HMODULE(); if (GetModuleHandleEx(0, L"d3dcompiler_47.dll", &LibD3DCompiler)) { @@ -88,6 +92,7 @@ void CheckLoadedModules() { } else { LOG(("Error: Could not resolve DirectX compiler library.")); } +#endif // DESKTOP_APP_USE_ANGLE } } // namespace Dlls diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 96c6cb8418d2f..5594e6c4c4a29 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -161,6 +161,7 @@ class ShortcutMessages Painter &p, const Ui::ChatPaintContext &context) override; QString listElementAuthorRank(not_null view) override; + bool listElementHideTopicButton(not_null view) override; History *listTranslateHistory() override; void listAddTranslatedItems( not_null tracker) override; @@ -967,7 +968,7 @@ CopyRestrictionType ShortcutMessages::listCopyMediaRestrictionType( not_null item) { if (const auto media = item->media()) { if (const auto invoice = media->invoice()) { - if (invoice->extendedMedia) { + if (!invoice->extendedMedia.empty()) { return CopyMediaRestrictionTypeFor(_history->peer, item); } } @@ -1046,6 +1047,11 @@ QString ShortcutMessages::listElementAuthorRank( return {}; } +bool ShortcutMessages::listElementHideTopicButton( + not_null view) { + return true; +} + History *ShortcutMessages::listTranslateHistory() { return nullptr; } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index d931f1df710b8..6ffd1842a373d 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -95,6 +95,7 @@ settingsPremiumIconTags: icon {{ "settings/premium/tags", settingsIconFg }}; settingsPremiumIconLastSeen: icon {{ "settings/premium/lastseen", settingsIconFg }}; settingsPremiumIconPrivacy: icon {{ "settings/premium/privacy", settingsIconFg }}; settingsPremiumIconBusiness: icon {{ "settings/premium/market", settingsIconFg }}; +settingsPremiumIconEffects: icon {{ "settings/premium/effects", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; @@ -214,8 +215,6 @@ settingsBio: InputField(defaultInputField) { borderActive: 0px; heightMin: 32px; - - font: boxTextFont; } settingsBioMargins: margins(22px, 6px, 22px, 4px); @@ -309,8 +308,6 @@ settingsDeviceName: InputField(defaultInputField) { placeholderFont: normalFont; heightMin: 29px; - - font: boxTextFont; } dictionariesSectionButton: SettingsButton(settingsUpdateToggle) { @@ -677,5 +674,5 @@ settingsChatLinkField: InputField(defaultInputField) { heightMin: 32px; - font: normalFont; + style: defaultTextStyle; } diff --git a/Telegram/SourceFiles/settings/settings_advanced.cpp b/Telegram/SourceFiles/settings/settings_advanced.cpp index b833383f53e43..a2ae6487ad728 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced.cpp @@ -822,7 +822,7 @@ void SetupHardwareAcceleration(not_null container) { }, container->lifetime()); } -#ifdef Q_OS_WIN +#ifdef DESKTOP_APP_USE_ANGLE void SetupANGLE( not_null controller, not_null container) { @@ -895,7 +895,7 @@ void SetupANGLE( })); }); } -#endif // Q_OS_WIN +#endif // DESKTOP_APP_USE_ANGLE void SetupOpenGL( not_null controller, @@ -938,13 +938,13 @@ void SetupPerformance( not_null container) { SetupAnimations(&controller->window(), container); SetupHardwareAcceleration(container); -#ifdef Q_OS_WIN +#ifdef DESKTOP_APP_USE_ANGLE SetupANGLE(controller, container); -#else // Q_OS_WIN +#else // DESKTOP_APP_USE_ANGLE if constexpr (!Platform::IsMac()) { SetupOpenGL(controller, container); } -#endif // Q_OS_WIN +#endif // DESKTOP_APP_USE_ANGLE } void SetupWindowTitle( diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 4cfeff84380e3..0890f2703d0c2 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -7,7 +7,6 @@ For license and copyright information please follow this link: */ #include "settings/settings_credits.h" -#include "settings/settings_credits_graphics.h" #include "api/api_credits.h" #include "boxes/gift_premium_box.h" #include "core/click_handler_types.h" @@ -20,8 +19,10 @@ For license and copyright information please follow this link: #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common_session.h" +#include "settings/settings_credits_graphics.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/boxes/boost_box.h" // Ui::StartFireworks. +#include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.h" @@ -30,7 +31,7 @@ For license and copyright information please follow this link: #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" -#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/slider_natural_width.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -93,8 +94,8 @@ Credits::Credits( not_null controller) : Section(parent) , _controller(controller) -, _star(GenerateStars(st::creditsTopupButton.height, 1)) -, _balanceStar(GenerateStars(st::creditsBalanceStarHeight, 1)) { +, _star(Ui::GenerateStars(st::creditsTopupButton.height, 1)) +, _balanceStar(Ui::GenerateStars(st::creditsBalanceStarHeight, 1)) { setupContent(); } @@ -159,25 +160,12 @@ void Credits::setupHistory(not_null container) { header->setSubTitle({}); } - class Slider final : public Ui::SettingsSlider { - public: - using Ui::SettingsSlider::SettingsSlider; - void setNaturalWidth(int w) { - _naturalWidth = w; - } - int naturalWidth() const override { - return _naturalWidth; - } - - private: - int _naturalWidth = 0; - - }; - const auto slider = inner->add( - object_ptr>( + object_ptr>( inner, - object_ptr(inner, st::defaultTabsSlider)), + object_ptr( + inner, + st::defaultTabsSlider)), st::boxRowPadding); slider->toggle(!hasOneTab, anim::type::instant); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index c42b611e97d5c..282945d9efc73 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -8,20 +8,30 @@ For license and copyright information please follow this link: #include "settings/settings_credits_graphics.h" #include "api/api_credits.h" +#include "api/api_earn.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" #include "boxes/gift_premium_box.h" #include "core/click_handler_types.h" +#include "core/ui_integration.h" +#include "data/data_document.h" #include "data/data_file_origin.h" +#include "core/click_handler_types.h" // UrlClickHandler #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" // HistoryServicePaymentRefund. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" #include "settings/settings_common_session.h" -#include "settings/settings_credits_graphics.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/controls/userpic_button.h" #include "ui/effects/credits_graphics.h" @@ -33,9 +43,12 @@ For license and copyright information please follow this link: #include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" @@ -43,6 +56,8 @@ For license and copyright information please follow this link: #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" +#include "styles/style_channel_earn.h" +#include "styles/style_chat.h" #include "styles/style_credits.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" @@ -68,6 +83,11 @@ namespace { return XXH64(string.data(), string.size() * sizeof(ushort), 0); } +[[nodiscard]] int WithdrawalMin(not_null session) { + const auto key = u"stars_revenue_withdrawal_min"_q; + return session->appConfig().get(key, 1000); +} + class Balance final : public Ui::RpWidget , public Ui::AbstractTooltipShower { @@ -107,65 +127,107 @@ class Balance final }; -} // namespace +void AddViewMediaHandler( + not_null thumb, + not_null controller, + const Data::CreditsHistoryEntry &e) { + if (e.extended.empty()) { + return; + } + thumb->setCursor(style::cur_pointer); -QImage GenerateStars(int height, int count) { - constexpr auto kOutlineWidth = .6; - constexpr auto kStrokeWidth = 3; - constexpr auto kShift = 3; - - auto colorized = qs(Ui::Premium::ColorizedSvg( - Ui::Premium::CreditsIconGradientStops())); - colorized.replace( - u"stroke=\"none\""_q, - u"stroke=\"%1\""_q.arg(st::creditsStroke->c.name())); - colorized.replace( - u"stroke-width=\"1\""_q, - u"stroke-width=\"%1\""_q.arg(kStrokeWidth)); - auto svg = QSvgRenderer(colorized.toUtf8()); - svg.setViewBox(svg.viewBox() + Margins(kStrokeWidth)); - - const auto starSize = Size(height - kOutlineWidth * 2); - - auto frame = QImage( - QSize( - (height + kShift * (count - 1)) * style::DevicePixelRatio(), - height * style::DevicePixelRatio()), - QImage::Format_ARGB32_Premultiplied); - frame.setDevicePixelRatio(style::DevicePixelRatio()); - frame.fill(Qt::transparent); - const auto drawSingle = [&](QPainter &q) { - const auto s = kOutlineWidth; - q.save(); - q.translate(s, s); - q.setCompositionMode(QPainter::CompositionMode_Clear); - svg.render(&q, QRectF(QPointF(s, 0), starSize)); - svg.render(&q, QRectF(QPointF(s, s), starSize)); - svg.render(&q, QRectF(QPointF(0, s), starSize)); - svg.render(&q, QRectF(QPointF(-s, s), starSize)); - svg.render(&q, QRectF(QPointF(-s, 0), starSize)); - svg.render(&q, QRectF(QPointF(-s, -s), starSize)); - svg.render(&q, QRectF(QPointF(0, -s), starSize)); - svg.render(&q, QRectF(QPointF(s, -s), starSize)); - q.setCompositionMode(QPainter::CompositionMode_SourceOver); - svg.render(&q, Rect(starSize)); - q.restore(); + struct State { + ~State() { + if (item) { + item->destroy(); + } + } + + HistoryItem *item = nullptr; + bool pressed = false; + bool over = false; }; - { - auto q = QPainter(&frame); - q.translate(frame.width() / style::DevicePixelRatio() - height, 0); - for (auto i = count; i > 0; --i) { - drawSingle(q); - q.translate(-kShift, 0); + const auto state = thumb->lifetime().make_state(); + const auto session = &controller->session(); + const auto owner = &session->data(); + const auto peerId = e.barePeerId + ? PeerId(e.barePeerId) + : session->userPeerId(); + const auto history = owner->history(session->user()); + state->item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, + .from = peerId, + .date = base::unixtime::serialize(e.date), + }, TextWithEntities(), MTP_messageMediaEmpty()); + auto fake = std::vector>(); + fake.reserve(e.extended.size()); + for (const auto &item : e.extended) { + if (item.type == Data::CreditsHistoryMediaType::Photo) { + fake.push_back(std::make_unique( + state->item, + owner->photo(item.id), + false)); // spoiler + } else { + fake.push_back(std::make_unique( + state->item, + owner->document(item.id), + true, // skipPremiumEffect + false, // spoiler + 0)); // ttlSeconds } } - return frame; + state->item->overrideMedia(std::make_unique( + state->item, + Data::Invoice{ + .amount = uint64(std::abs(int64(e.credits))), + .currency = Ui::kCreditsCurrency, + .extendedMedia = std::move(fake), + .isPaidMedia = true, + })); + const auto showMedia = crl::guard(controller, [=] { + if (const auto media = state->item->media()) { + if (const auto invoice = media->invoice()) { + if (!invoice->extendedMedia.empty()) { + const auto first = invoice->extendedMedia[0].get(); + if (const auto photo = first->photo()) { + controller->openPhoto(photo, { + .id = state->item->fullId(), + }); + } else if (const auto document = first->document()) { + controller->openDocument(document, true, { + .id = state->item->fullId(), + }); + } + } + } + } + }); + thumb->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + const auto mouse = static_cast(e.get()); + if (mouse->button() == Qt::LeftButton) { + state->over = true; + state->pressed = true; + } + } else if (e->type() == QEvent::MouseButtonRelease + && state->over + && state->pressed) { + showMedia(); + } else if (e->type() == QEvent::Enter) { + state->over = true; + } else if (e->type() == QEvent::Leave) { + state->over = false; + } + }, thumb->lifetime()); } +} // namespace + void FillCreditOptions( not_null controller, not_null container, - int minCredits, + int minimumCredits, Fn paid) { const auto options = container->add( object_ptr>( @@ -175,7 +237,7 @@ void FillCreditOptions( Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); - const auto singleStarWidth = GenerateStars( + const auto singleStarWidth = Ui::GenerateStars( st::creditsTopupButton.height, 1).width() / style::DevicePixelRatio(); @@ -191,6 +253,10 @@ void FillCreditOptions( - st.iconLeft - singleStarWidth; const auto buttonHeight = st.height + rect::m::sum::v(st.padding); + const auto minCredits = (!options.empty() + && (minimumCredits > options.back().credits)) + ? 0 + : minimumCredits; for (auto i = 0; i < options.size(); i++) { const auto &option = options[i]; if (option.credits < minCredits) { @@ -211,14 +277,11 @@ void FillCreditOptions( Ui::FillAmountAndCurrency(option.amount, option.currency), st::creditsTopupPrice); const auto inner = Ui::CreateChild(button); - const auto stars = GenerateStars(st.height, (i + 1)); + const auto stars = Ui::GenerateStars(st.height, (i + 1)); inner->paintRequest( ) | rpl::start_with_next([=](const QRect &rect) { auto p = QPainter(inner); - p.drawImage( - 0, - (buttonHeight - stars.height()) / 2, - stars); + p.drawImage(0, 0, stars); const auto textLeft = diffBetweenTextAndStar + stars.width() / style::DevicePixelRatio(); p.setPen(st.textFg); @@ -310,7 +373,7 @@ not_null AddBalanceWidget( bool rightAlign) { const auto balance = Ui::CreateChild(parent); const auto balanceStar = balance->lifetime().make_state( - GenerateStars(st::creditsBalanceStarHeight, 1)); + Ui::GenerateStars(st::creditsBalanceStarHeight, 1)); const auto starSize = balanceStar->size() / style::DevicePixelRatio(); const auto label = balance->lifetime().make_state( st::defaultTextStyle, @@ -373,7 +436,7 @@ void ReceiptCreditsBox( box->setStyle(st::giveawayGiftCodeBox); box->setNoContentMargin(true); - const auto star = GenerateStars(st::creditsTopupButton.height, 1); + const auto star = Ui::GenerateStars(st::creditsTopupButton.height, 1); const auto content = box->verticalLayout(); Ui::AddSkip(content); @@ -383,18 +446,17 @@ void ReceiptCreditsBox( using Type = Data::CreditsHistoryEntry::PeerType; const auto &stUser = st::boostReplaceUserpic; + const auto session = &controller->session(); const auto peer = (e.peerType == Type::PremiumBot) - ? premiumBot - : e.bareId - ? controller->session().data().peer(PeerId(e.bareId)).get() + ? nullptr + : e.barePeerId + ? session->data().peer(PeerId(e.barePeerId)).get() : nullptr; - const auto photo = e.photoId - ? controller->session().data().photo(e.photoId).get() - : nullptr; - if (photo) { - content->add(object_ptr>( + if (const auto callback = Ui::PaintPreviewCallback(session, e)) { + const auto thumb = content->add(object_ptr>( content, - HistoryEntryPhoto(content, photo, stUser.photoSize))); + GenericEntryPhoto(content, callback, stUser.photoSize))); + AddViewMediaHandler(thumb->entity(), controller, e); } else if (peer) { content->add(object_ptr>( content, @@ -438,14 +500,19 @@ void ReceiptCreditsBox( auto &lifetime = content->lifetime(); const auto text = lifetime.make_state( st::semiboldTextStyle, - ((!e.bareId || e.refunded) ? QChar('+') : kMinus) + (e.in ? QChar('+') : kMinus) + Lang::FormatCountDecimal(std::abs(int64(e.credits)))); - const auto refundedText = tr::lng_channel_earn_history_return( - tr::now); - const auto refunded = e.refunded + const auto roundedText = e.refunded + ? tr::lng_channel_earn_history_return(tr::now) + : e.pending + ? tr::lng_channel_earn_history_pending(tr::now) + : e.failed + ? tr::lng_channel_earn_history_failed(tr::now) + : QString(); + const auto rounded = !roundedText.isEmpty() ? lifetime.make_state( st::defaultTextStyle, - refundedText) + roundedText) : (Ui::Text::String*)(nullptr); const auto amount = content->add( @@ -453,23 +520,25 @@ void ReceiptCreditsBox( content, star.height() / style::DevicePixelRatio())); const auto font = text->style()->font; - const auto refundedFont = st::defaultTextStyle.font; + const auto roundedFont = st::defaultTextStyle.font; const auto starWidth = star.width() / style::DevicePixelRatio(); - const auto refundedSkip = refundedFont->spacew * 2; - const auto refundedWidth = refunded - ? refundedFont->width(refundedText) - + refundedSkip - + refundedFont->height + const auto roundedSkip = roundedFont->spacew * 2; + const auto roundedWidth = rounded + ? roundedFont->width(roundedText) + + roundedSkip + + roundedFont->height : 0; const auto fullWidth = text->maxWidth() + font->spacew * 1 + starWidth - + refundedWidth; + + roundedWidth; amount->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(amount); - p.setPen((!e.bareId || e.refunded) + p.setPen(e.pending + ? st::creditsStroke + : e.in ? st::boxTextFgGood : st::menuIconAttentionColor); const auto x = (amount->width() - fullWidth) / 2; @@ -481,15 +550,15 @@ void ReceiptCreditsBox( .availableWidth = amount->width(), }); p.drawImage( - x + fullWidth - starWidth - refundedWidth, + x + fullWidth - starWidth - roundedWidth, 0, star); - if (refunded) { - const auto refundedLeft = fullWidth + if (rounded) { + const auto roundedLeft = fullWidth + x - - refundedWidth - + refundedSkip; + - roundedWidth + + roundedSkip; const auto pen = p.pen(); auto color = pen.color(); color.setAlphaF(color.alphaF() * 0.15); @@ -498,20 +567,20 @@ void ReceiptCreditsBox( { auto hq = PainterHighQualityEnabler(p); p.drawRoundedRect( - refundedLeft, - (amount->height() - refundedFont->height) / 2, - refundedWidth - refundedSkip, - refundedFont->height, - refundedFont->height / 2, - refundedFont->height / 2); + roundedLeft, + (amount->height() - roundedFont->height) / 2, + roundedWidth - roundedSkip, + roundedFont->height, + roundedFont->height / 2, + roundedFont->height / 2); } p.setPen(pen); - refunded->draw(p, Ui::Text::PaintContext{ + rounded->draw(p, Ui::Text::PaintContext{ .position = QPoint( - refundedLeft + refundedFont->height / 2, - (amount->height() - refundedFont->height) / 2), - .outerWidth = refundedWidth, - .availableWidth = refundedWidth, + roundedLeft + roundedFont->height / 2, + (amount->height() - roundedFont->height) / 2), + .outerWidth = roundedWidth, + .availableWidth = roundedWidth, }); } }, amount->lifetime()); @@ -553,6 +622,33 @@ void ReceiptCreditsBox( Ui::AddSkip(content); + if (e.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) { + const auto widget = Ui::CreateChild(content); + using ColoredMiniStars = Ui::Premium::ColoredMiniStars; + const auto stars = widget->lifetime().make_state( + widget, + false, + Ui::Premium::MiniStars::Type::BiStars); + stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); + widget->resize( + st::boxWidth - stUser.photoSize, + stUser.photoSize * 2); + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + widget->moveToLeft(stUser.photoSize / 2, 0); + const auto starsRect = Rect(widget->size()); + stars->setPosition(starsRect.topLeft()); + stars->setSize(starsRect.size()); + widget->lower(); + }, widget->lifetime()); + widget->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(widget); + p.fillRect(r, Qt::transparent); + stars->paint(p); + }, widget->lifetime()); + } + const auto button = box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); @@ -565,18 +661,43 @@ void ReceiptCreditsBox( }, button->lifetime()); } -object_ptr HistoryEntryPhoto( +void ShowRefundInfoBox( + not_null controller, + FullMsgId refundItemId) { + const auto owner = &controller->session().data(); + const auto item = owner->message(refundItemId); + const auto refund = item + ? item->Get() + : nullptr; + if (!refund) { + return; + } + Assert(refund->peer != nullptr); + auto info = Data::CreditsHistoryEntry(); + info.id = refund->transactionId; + info.date = base::unixtime::parse(item->date()); + info.credits = refund->amount; + info.barePeerId = refund->peer->id.value; + info.peerType = Data::CreditsHistoryEntry::PeerType::Peer; + info.refunded = true; + info.in = true; + controller->show(Box( + ::Settings::ReceiptCreditsBox, + controller, + nullptr, // premiumBot + info)); +} + +object_ptr GenericEntryPhoto( not_null parent, - not_null photo, + Fn(Fn)> callback, int photoSize) { auto owned = object_ptr(parent); const auto widget = owned.data(); widget->resize(Size(photoSize)); - const auto draw = Ui::GenerateCreditsPaintEntryCallback( - photo, - [=] { widget->update(); }); - + const auto draw = callback( + crl::guard(widget, [=] { widget->update(); })); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); @@ -586,6 +707,36 @@ object_ptr HistoryEntryPhoto( return owned; } +object_ptr HistoryEntryPhoto( + not_null parent, + not_null photo, + int photoSize) { + return GenericEntryPhoto( + parent, + [=](Fn update) { + return Ui::GenerateCreditsPaintEntryCallback(photo, update); + }, + photoSize); +} + +object_ptr PaidMediaThumbnail( + not_null parent, + not_null photo, + PhotoData *second, + int totalCount, + int photoSize) { + return GenericEntryPhoto( + parent, + [=](Fn update) { + return Ui::GeneratePaidMediaPaintCallback( + photo, + second, + totalCount, + update); + }, + photoSize); +} + void SmallBalanceBox( not_null box, not_null controller, @@ -664,4 +815,302 @@ void SmallBalanceBox( } } +void AddWithdrawalWidget( + not_null container, + not_null controller, + not_null peer, + rpl::producer secondButtonUrl, + rpl::producer availableBalanceValue, + rpl::producer dateValue, + rpl::producer lockedValue, + rpl::producer usdValue) { + Ui::AddSkip(container); + + const auto labels = container->add( + object_ptr>( + container, + object_ptr(container)))->entity(); + + const auto majorLabel = Ui::CreateChild( + labels, + rpl::duplicate(availableBalanceValue) | rpl::map([](uint64 v) { + return Lang::FormatCountDecimal(v); + }), + st::channelEarnBalanceMajorLabel); + const auto icon = Ui::CreateSingleStarWidget( + labels, + majorLabel->height()); + majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + majorLabel->sizeValue( + ) | rpl::start_with_next([=](const QSize &majorSize) { + const auto skip = st::channelEarnBalanceMinorLabelSkip; + labels->resize( + majorSize.width() + icon->width() + skip, + majorSize.height()); + majorLabel->moveToLeft(icon->width() + skip, 0); + }, labels->lifetime()); + Ui::ToggleChildrenVisibility(labels, true); + + Ui::AddSkip(container); + container->add( + object_ptr>( + container, + object_ptr( + container, + std::move(usdValue), + st::channelEarnOverviewSubMinorLabel))); + + Ui::AddSkip(container); + + const auto input = Ui::AddInputFieldForCredits( + container, + rpl::duplicate(availableBalanceValue)); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + const auto &stButton = st::defaultActiveButton; + const auto buttonsContainer = container->add( + Ui::CreateSkipWidget(container, stButton.height), + st::boxRowPadding); + + const auto button = Ui::CreateChild( + buttonsContainer, + rpl::never(), + stButton); + + const auto buttonCredits = Ui::CreateChild( + buttonsContainer, + tr::lng_bot_earn_balance_button_buy_ads(), + stButton); + buttonCredits->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + + Ui::ToggleChildrenVisibility(buttonsContainer, true); + + rpl::combine( + std::move(secondButtonUrl), + buttonsContainer->sizeValue() + ) | rpl::start_with_next([=](const QString &url, const QSize &size) { + if (url.isEmpty()) { + button->resize(size.width(), size.height()); + buttonCredits->resize(0, 0); + } else { + const auto w = size.width() - st::boxRowPadding.left() / 2; + button->resize(w / 2, size.height()); + buttonCredits->resize(w / 2, size.height()); + buttonCredits->moveToRight(0, 0); + buttonCredits->setClickedCallback([=] { + UrlClickHandler::Open(url); + }); + } + }, buttonsContainer->lifetime()); + + rpl::duplicate( + lockedValue + ) | rpl::start_with_next([=](bool v) { + button->setAttribute(Qt::WA_TransparentForMouseEvents, v); + }, button->lifetime()); + + const auto session = &controller->session(); + + const auto label = Ui::CreateChild( + button, + tr::lng_channel_earn_balance_button(tr::now), + st::channelEarnSemiboldLabel); + const auto processInputChange = [&] { + const auto buttonEmoji = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::settingsPremiumIconStar, + { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, + true)); + const auto context = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [=] { label->update(); }, + }; + using Balance = rpl::variable; + const auto currentBalance = input->lifetime().make_state( + rpl::duplicate(availableBalanceValue)); + const auto process = [=] { + const auto amount = input->getLastText().toDouble(); + if (amount >= currentBalance->current()) { + label->setText( + tr::lng_bot_earn_balance_button_all(tr::now)); + } else { + label->setMarkedText( + tr::lng_bot_earn_balance_button( + tr::now, + lt_count, + amount, + lt_emoji, + buttonEmoji, + Ui::Text::RichLangValue), + context); + } + }; + QObject::connect(input, &Ui::MaskedInputField::changed, process); + process(); + return process; + }(); + label->setTextColorOverride(stButton.textFg->c); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + rpl::duplicate(lockedValue), + button->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](bool v, const QSize &b, const QSize &l) { + label->moveToLeft( + (b.width() - l.width()) / 2, + (v ? -10 : 1) * (b.height() - l.height()) / 2); + }, label->lifetime()); + + const auto lockedColor = anim::with_alpha(stButton.textFg->c, .5); + const auto lockedLabelTop = Ui::CreateChild( + button, + tr::lng_bot_earn_balance_button_locked(), + st::botEarnLockedButtonLabel); + lockedLabelTop->setTextColorOverride(lockedColor); + lockedLabelTop->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto lockedLabelBottom = Ui::CreateChild( + button, + QString(), + st::botEarnLockedButtonLabel); + lockedLabelBottom->setTextColorOverride(lockedColor); + lockedLabelBottom->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + rpl::duplicate(lockedValue), + button->sizeValue(), + lockedLabelTop->sizeValue(), + lockedLabelBottom->sizeValue() + ) | rpl::start_with_next([=]( + bool locked, + const QSize &b, + const QSize &top, + const QSize &bottom) { + const auto factor = locked ? 1 : -10; + const auto sumHeight = top.height() + bottom.height(); + lockedLabelTop->moveToLeft( + (b.width() - top.width()) / 2, + factor * (b.height() - sumHeight) / 2); + lockedLabelBottom->moveToLeft( + (b.width() - bottom.width()) / 2, + factor * ((b.height() - sumHeight) / 2 + top.height())); + }, lockedLabelTop->lifetime()); + + const auto dateUpdateLifetime + = lockedLabelBottom->lifetime().make_state(); + std::move( + dateValue + ) | rpl::start_with_next([=](const QDateTime &dt) { + dateUpdateLifetime->destroy(); + if (dt.isNull()) { + return; + } + constexpr auto kDateUpdateInterval = crl::time(250); + const auto was = base::unixtime::serialize(dt); + + const auto context = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [=] { lockedLabelBottom->update(); }, + }; + const auto emoji = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::chatSimilarLockedIcon, + st::botEarnButtonLockMargins, + true)); + + rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(kDateUpdateInterval) + ) | rpl::start_with_next([=] { + const auto secondsDifference = std::max( + was - base::unixtime::now() - 1, + 0); + const auto hours = secondsDifference / 3600; + const auto minutes = (secondsDifference % 3600) / 60; + const auto seconds = secondsDifference % 60; + constexpr auto kZero = QChar('0'); + const auto formatted = (hours > 0) + ? (u"%1:%2:%3"_q) + .arg(hours, 2, 10, kZero) + .arg(minutes, 2, 10, kZero) + .arg(seconds, 2, 10, kZero) + : (u"%1:%2"_q) + .arg(minutes, 2, 10, kZero) + .arg(seconds, 2, 10, kZero); + lockedLabelBottom->setMarkedText( + base::duplicate(emoji).append(formatted), + context); + }, *dateUpdateLifetime); + }, lockedLabelBottom->lifetime()); + + Api::HandleWithdrawalButton( + Api::RewardReceiver{ + .creditsReceiver = peer, + .creditsAmount = [=, show = controller->uiShow()] { + const auto amount = input->getLastText().toULongLong(); + const auto min = float64(WithdrawalMin(session)); + if (amount < min) { + auto text = tr::lng_bot_earn_credits_out_minimal( + tr::now, + lt_link, + Ui::Text::Link( + tr::lng_bot_earn_credits_out_minimal_link( + tr::now, + lt_count, + min), + u"internal:"_q), + Ui::Text::RichLangValue); + show->showToast(Ui::Toast::Config{ + .text = std::move(text), + .filter = [=](const auto ...) { + input->setText(QString::number(min)); + processInputChange(); + return true; + }, + }); + return 0ULL; + } + return amount; + }, + }, + button, + controller->uiShow()); + Ui::ToggleChildrenVisibility(button, true); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + const auto arrow = Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::topicButtonArrow, + st::channelEarnLearnArrowMargins, + false)); + auto about = Ui::CreateLabelWithCustomEmoji( + container, + tr::lng_bot_earn_learn_credits_out_about( + lt_link, + tr::lng_channel_earn_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_bot_earn_balance_about_url(tr::now)); + }), + Ui::Text::RichLangValue), + { .session = session }, + st::boxDividerLabel); + Ui::AddSkip(container); + container->add(object_ptr( + container, + std::move(about), + st::defaultBoxDividerLabelPadding, + RectPart::Top | RectPart::Bottom)); + + Ui::AddSkip(container); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index cb6c27e7eadf7..406bfabcaf756 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -28,8 +28,6 @@ class VerticalLayout; namespace Settings { -[[nodiscard]] QImage GenerateStars(int height, int count); - void FillCreditOptions( not_null controller, not_null container, @@ -41,17 +39,42 @@ void FillCreditOptions( rpl::producer balanceValue, bool rightAlign); +void AddWithdrawalWidget( + not_null container, + not_null controller, + not_null peer, + rpl::producer secondButtonUrl, + rpl::producer availableBalanceValue, + rpl::producer dateValue, + rpl::producer lockedValue, + rpl::producer usdValue); + void ReceiptCreditsBox( not_null box, not_null controller, PeerData *premiumBot, const Data::CreditsHistoryEntry &e); +void ShowRefundInfoBox( + not_null controller, + FullMsgId refundItemId); + +[[nodiscard]] object_ptr GenericEntryPhoto( + not_null parent, + Fn(Fn update)> callback, + int photoSize); [[nodiscard]] object_ptr HistoryEntryPhoto( not_null parent, not_null photo, int photoSize); +[[nodiscard]] object_ptr PaidMediaThumbnail( + not_null parent, + not_null photo, + PhotoData *second, + int totalCount, + int photoSize); + void SmallBalanceBox( not_null box, not_null controller, diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index 0149872459e6f..f578bce829fa3 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -145,7 +145,6 @@ void SetupExperimental( addToggle(Core::kOptionFractionalScalingEnabled); addToggle(Window::kOptionViewProfileInChatsListContextMenu); addToggle(Info::Profile::kOptionShowPeerIdBelowAbout); - addToggle(Ui::GL::kOptionAllowLinuxNvidiaOpenGL); addToggle(Ui::kOptionUseSmallMsgBubbleRadius); addToggle(Media::Player::kOptionDisableAutoplayNext); addToggle(kOptionSendLargePhotos); @@ -153,6 +152,7 @@ void SetupExperimental( addToggle(kOptionAutoScrollInactiveChat); addToggle(Window::Notifications::kOptionGNotification); addToggle(Core::kOptionFreeType); + addToggle(Core::kOptionSkipUrlSchemeRegister); addToggle(Data::kOptionExternalVideoPlayer); addToggle(Window::kOptionNewWindowsSizeAsFirst); } diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 9db8d59a92138..911d4494264a3 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -573,8 +573,7 @@ void SetupBio( } changed->fire(*current != text); const auto limit = self->isPremium() ? premiumLimit : defaultLimit; - const auto countLeft = limit - - bio->lastTextSizeWithoutSurrogatePairsCount(); + const auto countLeft = limit - Ui::ComputeFieldCharacterCount(bio); countdown->setText(QString::number(countLeft)); countdown->setTextColorOverride( countLeft < 0 ? st::boxTextFgError->c : std::optional()); @@ -984,13 +983,13 @@ void AccountsList::rebuild() { _reorder->finishReordering(); if (newWindow) { _closeRequests.fire({}); - Core::App().ensureSeparateWindowForAccount( - account); + Core::App().ensureSeparateWindowFor(account); } Core::App().domain().maybeActivate(account); } }; - if (const auto window = Core::App().separateWindowForAccount(account)) { + if (const auto window = Core::App().separateWindowFor( + account)) { _closeRequests.fire({}); window->activate(); } else { diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp index 9a766f16d0398..193445caf3dff 100644 --- a/Telegram/SourceFiles/settings/settings_local_passcode.cpp +++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp @@ -8,6 +8,8 @@ For license and copyright information please follow this link: #include "settings/settings_local_passcode.h" #include "base/platform/base_platform_last_input.h" +#include "base/platform/base_platform_info.h" +#include "base/system_unlock.h" #include "boxes/auto_lock_box.h" #include "core/application.h" #include "core/core_settings.h" @@ -23,6 +25,7 @@ For license and copyright information please follow this link: #include "ui/widgets/fields/password_input.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -77,6 +80,7 @@ class LocalPasscodeEnter : public AbstractSection { rpl::event_stream<> _setInnerFocus; rpl::event_stream _showOther; rpl::event_stream<> _showBack; + bool _systemUnlockWithBiometric = false; }; @@ -94,6 +98,13 @@ rpl::producer LocalPasscodeEnter::title() { void LocalPasscodeEnter::setupContent() { const auto content = Ui::CreateChild(this); + base::SystemUnlockStatus( + true + ) | rpl::start_with_next([=](base::SystemUnlockAvailability status) { + _systemUnlockWithBiometric = status.available + && status.withBiometrics; + }, lifetime()); + const auto isCreate = (enterType() == EnterType::Create); const auto isCheck = (enterType() == EnterType::Check); [[maybe_unused]] const auto isChange = (enterType() == EnterType::Change); @@ -199,11 +210,11 @@ void LocalPasscodeEnter::setupContent() { content, object_ptr( content, - isCreate + (isCreate ? tr::lng_passcode_create_button() : isCheck ? tr::lng_passcode_check_button() - : tr::lng_passcode_change_button(), + : tr::lng_passcode_change_button()), st::changePhoneButton)), st::settingLocalPasscodeButtonPadding)->entity(); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); @@ -239,6 +250,10 @@ void LocalPasscodeEnter::setupContent() { } SetPasscode(_controller, newText); if (isCreate) { + if (Platform::IsWindows() || _systemUnlockWithBiometric) { + Core::App().settings().setSystemUnlockEnabled(true); + Core::App().saveSettingsDelayed(); + } _showOther.fire(LocalPasscodeManageId()); } else if (isChange) { _showBack.fire({}); @@ -506,6 +521,85 @@ void LocalPasscodeManage::setupContent() { divider->skipEdge(Qt::BottomEdge, shown); }, divider->lifetime()); + const auto systemUnlockWrap = content->add( + object_ptr>( + content, + object_ptr(content)) + )->setDuration(0); + const auto systemUnlockContent = systemUnlockWrap->entity(); + + enum class UnlockType { + None, + Default, + Biometrics, + Companion, + }; + const auto unlockType = systemUnlockContent->lifetime().make_state< + rpl::variable + >(base::SystemUnlockStatus( + true + ) | rpl::map([](base::SystemUnlockAvailability status) { + return status.withBiometrics + ? UnlockType::Biometrics + : status.withCompanion + ? UnlockType::Companion + : status.available + ? UnlockType::Default + : UnlockType::None; + })); + + unlockType->value( + ) | rpl::start_with_next([=](UnlockType type) { + while (systemUnlockContent->count()) { + delete systemUnlockContent->widgetAt(0); + } + + Ui::AddSkip(systemUnlockContent); + + AddButtonWithIcon( + systemUnlockContent, + (Platform::IsWindows() + ? tr::lng_settings_use_winhello() + : (type == UnlockType::Biometrics) + ? tr::lng_settings_use_touchid() + : (type == UnlockType::Companion) + ? tr::lng_settings_use_applewatch() + : tr::lng_settings_use_systempwd()), + st::settingsButton, + { Platform::IsWindows() + ? &st::menuIconWinHello + : (type == UnlockType::Biometrics) + ? &st::menuIconTouchID + : (type == UnlockType::Companion) + ? &st::menuIconAppleWatch + : &st::menuIconSystemPwd } + )->toggleOn( + rpl::single(Core::App().settings().systemUnlockEnabled()) + )->toggledChanges( + ) | rpl::filter([=](bool value) { + return value != Core::App().settings().systemUnlockEnabled(); + }) | rpl::start_with_next([=](bool value) { + Core::App().settings().setSystemUnlockEnabled(value); + Core::App().saveSettingsDelayed(); + }, systemUnlockContent->lifetime()); + + Ui::AddSkip(systemUnlockContent); + + Ui::AddDividerText( + systemUnlockContent, + (Platform::IsWindows() + ? tr::lng_settings_use_winhello_about() + : (type == UnlockType::Biometrics) + ? tr::lng_settings_use_touchid_about() + : (type == UnlockType::Companion) + ? tr::lng_settings_use_applewatch_about() + : tr::lng_settings_use_systempwd_about())); + + }, systemUnlockContent->lifetime()); + + systemUnlockWrap->toggleOn(unlockType->value( + ) | rpl::map(rpl::mappers::_1 != UnlockType::None)); + Ui::ResizeFitChild(this, content); } @@ -517,6 +611,8 @@ QPointer LocalPasscodeManage::createPinnedToBottom( .text = tr::lng_settings_passcode_disable_sure(), .confirmed = [=](Fn &&close) { SetPasscode(_controller, QString()); + Core::App().settings().setSystemUnlockEnabled(false); + Core::App().saveSettingsDelayed(); close(); _showBack.fire({}); diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index d08ce9a11c09c..363ad342f3f91 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -503,7 +503,7 @@ void SetupPremium( AddPremiumStar( AddButtonWithLabel( wrap->entity(), - tr::lng_credits_summary_title(), + tr::lng_settings_credits(), controller->session().creditsValue( ) | rpl::map([=](uint64 c) { return c ? Lang::FormatCountToShort(c).string : QString{}; diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index fdfe37148e1d3..1a04419be3247 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -28,7 +28,6 @@ For license and copyright information please follow this link: #include "platform/platform_specific.h" #include "platform/platform_notifications_manager.h" #include "base/platform/base_platform_info.h" -#include "base/call_delayed.h" #include "mainwindow.h" #include "core/application.h" #include "main/main_session.h" @@ -133,13 +132,10 @@ class NotificationsCount::SampleWidget : public QWidget { void startAnimation(); void animationCallback(); - void destroyDelayed(); - NotificationsCount *_owner; QPixmap _cache; Ui::Animations::Simple _opacity; bool _hiding = false; - bool _deleted = false; }; @@ -665,18 +661,6 @@ void NotificationsCount::SampleWidget::animationCallback() { _owner->removeSample(this); } hide(); - destroyDelayed(); - } -} - -void NotificationsCount::SampleWidget::destroyDelayed() { - if (_deleted) return; - _deleted = true; - - // Ubuntu has a lag if deleteLater() called immediately. - if constexpr (Platform::IsLinux()) { - base::call_delayed(1000, this, [this] { delete this; }); - } else { deleteLater(); } } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 9a003d3d23a0c..45ef5f63d366d 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -197,6 +197,7 @@ using Order = std::vector; u"animated_userpics"_q, u"premium_stickers"_q, u"business"_q, + u"effects"_q, }; } @@ -377,6 +378,16 @@ using Order = std::vector; true, }, }, + { + u"effects"_q, + Entry{ + &st::settingsPremiumIconEffects, + tr::lng_premium_summary_subtitle_effects(), + tr::lng_premium_summary_about_effects(), + PremiumFeature::Effects, + true, + }, + }, }; } @@ -1601,6 +1612,8 @@ std::vector PremiumFeaturesOrder( return PremiumFeature::RealTimeTranslation; } else if (s == u"wallpapers"_q) { return PremiumFeature::Wallpapers; + } else if (s == u"effects"_q) { + return PremiumFeature::Effects; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature type) { diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index ab8f322174d82..daea2e9ee83d6 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -22,6 +22,7 @@ For license and copyright information please follow this link: #include "settings/settings_premium.h" // Settings::ShowPremium. #include "settings/settings_privacy_controllers.h" #include "settings/settings_websites.h" +#include "base/system_unlock.h" #include "base/timer_rpl.h" #include "boxes/passcode_box.h" #include "boxes/sessions_box.h" @@ -1079,6 +1080,8 @@ PrivacySecurity::PrivacySecurity( not_null controller) : Section(parent) { setupContent(controller); + + [[maybe_unused]] auto preload = base::SystemUnlockStatus(); } rpl::producer PrivacySecurity::title() { diff --git a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp index 7f47a396ae2e6..4e37805295bd9 100644 --- a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp +++ b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp @@ -8,8 +8,10 @@ For license and copyright information please follow this link: #include "statistics/statistics_data_deserialize.h" #include "base/debug_log.h" +#include "data/data_channel_earn.h" // kEarnMultiplier. #include "data/data_statistics_chart.h" #include "statistics/statistics_types.h" +#include "ui/text/format_values.h" // kCreditsCurrency. #include #include @@ -40,6 +42,18 @@ Data::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) { }) | ranges::to_vector; auto result = Data::StatisticalChart(); + + { + const auto tickFormatIt = root.constFind(u"yTickFormatter"_q); + if (tickFormatIt != root.constEnd()) { + const auto tickFormat = tickFormatIt->toString(); + if (tickFormat.contains(u"TON"_q)) { + result.currency = Data::StatisticalCurrency::Ton; + } else if (tickFormat.contains(Ui::kCreditsCurrency)) { + result.currency = Data::StatisticalCurrency::Credits; + } + } + } auto columnIdCount = 0; for (const auto &column : columns) { const auto array = column.toArray(); @@ -62,8 +76,13 @@ Data::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) { line.isHiddenOnStart = ranges::contains(hiddenLines, columnId); line.y.resize(length); for (auto i = 0; i < length; i++) { - const auto value = ChartValue(base::SafeRound( - array.at(i + 1).toDouble())); + using Currency = Data::StatisticalCurrency; + const auto multiplier = (result.currency == Currency::Credits) + ? Data::kEarnMultiplier + : 1; + const auto value = ChartValue( + base::SafeRound(array.at(i + 1).toDouble())) + * multiplier; line.y[i] = value; if (value > line.maxValue) { line.maxValue = value; diff --git a/Telegram/SourceFiles/statistics/statistics_graphics.cpp b/Telegram/SourceFiles/statistics/statistics_graphics.cpp new file mode 100644 index 0000000000000..167082a5a4464 --- /dev/null +++ b/Telegram/SourceFiles/statistics/statistics_graphics.cpp @@ -0,0 +1,43 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "statistics/statistics_graphics.h" + +#include "data/data_statistics_chart.h" +#include "ui/effects/credits_graphics.h" // GenerateStars. +#include "ui/painter.h" +#include "styles/style_basic.h" +#include "styles/style_statistics.h" + +namespace Statistic { + +QImage ChartCurrencyIcon( + const Data::StatisticalChart &chartData, + std::optional color) { + auto result = QImage(); + const auto iconSize = st::statisticsCurrencyIcon.size(); + if (chartData.currency == Data::StatisticalCurrency::Ton) { + result = QImage( + iconSize * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(Qt::transparent); + { + auto p = Painter(&result); + if (const auto w = iconSize.width(); w && color) { + st::statisticsCurrencyIcon.paint(p, 0, 0, w, *color); + } else { + st::statisticsCurrencyIcon.paint(p, 0, 0, iconSize.width()); + } + } + } else if (chartData.currency == Data::StatisticalCurrency::Credits) { + return Ui::GenerateStars(iconSize.height(), 1); + } + return result; +} + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/statistics_graphics.h b/Telegram/SourceFiles/statistics/statistics_graphics.h new file mode 100644 index 0000000000000..e6c3a801149f3 --- /dev/null +++ b/Telegram/SourceFiles/statistics/statistics_graphics.h @@ -0,0 +1,20 @@ +/* +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 StatisticalChart; +} // namespace Data + +namespace Statistic { + +[[nodiscard]] QImage ChartCurrencyIcon( + const Data::StatisticalChart &chartData, + std::optional color); + +} // namespace Statistic diff --git a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp index 6925f30e5a730..ac9dc4582ab5f 100644 --- a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp +++ b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp @@ -12,6 +12,7 @@ For license and copyright information please follow this link: #include "lang/lang_keys.h" #include "statistics/chart_lines_filter_controller.h" #include "statistics/statistics_common.h" +#include "statistics/statistics_graphics.h" #include "styles/style_basic.h" #include "styles/style_statistics.h" @@ -37,7 +38,7 @@ void ChartRulersView::setChartData( _isDouble = (type == ChartViewType::DoubleLinear) || chartData.currencyRate; if (chartData.currencyRate) { - _currencyIcon = &st::statisticsCurrencyIcon; + _currencyIcon = ChartCurrencyIcon(chartData, {}); _leftCustomCaption = [=](float64 value) { return FormatF(value / float64(Data::kEarnMultiplier)); }; @@ -92,7 +93,9 @@ void ChartRulersView::paintCaptionsToRulers( for (auto &ruler : _rulers) { const auto rulerAlpha = alpha * ruler.alpha; p.setOpacity(rulerAlpha); - const auto left = _currencyIcon ? _currencyIcon->width() : 0; + const auto left = _currencyIcon.isNull() + ? 0 + : _currencyIcon.width() / style::DevicePixelRatio(); for (const auto &line : ruler.lines) { const auto y = offset + r.height() * line.relativeValue; const auto hasLinesFilter = _isDouble && _linesFilter; @@ -102,11 +105,11 @@ void ChartRulersView::paintCaptionsToRulers( } else { p.setPen(st::windowSubTextFg); } - if (_currencyIcon) { + if (!_currencyIcon.isNull()) { const auto iconTop = y - - _currencyIcon->height() + - _currencyIcon.height() / style::DevicePixelRatio() + st::statisticsChartRulerCaptionSkip; - _currencyIcon->paint(p, 0, iconTop, r.width()); + p.drawImage(0, iconTop, _currencyIcon); } p.drawText( left, diff --git a/Telegram/SourceFiles/statistics/view/chart_rulers_view.h b/Telegram/SourceFiles/statistics/view/chart_rulers_view.h index 44326d15eef6a..8ce8e989f4f6a 100644 --- a/Telegram/SourceFiles/statistics/view/chart_rulers_view.h +++ b/Telegram/SourceFiles/statistics/view/chart_rulers_view.h @@ -42,7 +42,7 @@ struct ChartRulersView final { QPen _rightPen; int _leftLineId = 0; int _rightLineId = 0; - const style::icon *_currencyIcon = nullptr; + QImage _currencyIcon; Fn _leftCustomCaption = nullptr; Fn _rightCustomCaption = nullptr; diff --git a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp index 305010866461e..f96c4ac3a5af4 100644 --- a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp +++ b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp @@ -12,6 +12,7 @@ For license and copyright information please follow this link: #include "lang/lang_keys.h" #include "statistics/statistics_common.h" #include "statistics/statistics_format_values.h" +#include "statistics/statistics_graphics.h" #include "statistics/view/stack_linear_chart_common.h" #include "ui/cached_round_corners.h" #include "ui/effects/ripple_animation.h" @@ -135,9 +136,7 @@ PointDetailsWidget::PointDetailsWidget( , _zoomEnabled(zoomEnabled) , _chartData(chartData) , _textStyle(st::statisticsDetailsPopupStyle) -, _headerStyle(st::statisticsDetailsPopupHeaderStyle) -, _valueIcon(chartData.currencyRate ? &st::statisticsCurrencyIcon : nullptr) { - +, _headerStyle(st::statisticsDetailsPopupHeaderStyle) { if (zoomEnabled) { rpl::single(rpl::empty_value()) | rpl::then( style::PaletteChanged() @@ -205,7 +204,9 @@ PointDetailsWidget::PointDetailsWidget( + rect::m::sum::h(st::statisticsDetailsPopupPadding) + st::statisticsDetailsPopupPadding.left() // Between strings. + maxNameTextWidth - + (_valueIcon ? _valueIcon->width() : 0) + + (_valueIcon.isNull() + ? 0 + : _valueIcon.width() / style::DevicePixelRatio()) + _maxPercentageWidth; }(); sizeValue( @@ -310,6 +311,9 @@ void PointDetailsWidget::setXIndex(int xIndex) { } _lines.push_back(std::move(textLine)); } + if (_chartData.currencyRate && _valueIcon.isNull()) { + _valueIcon = ChartCurrencyIcon(_chartData, _lines.front().valueColor); + } const auto clickable = _zoomEnabled && hasPositiveValues; _hasPositiveValues = hasPositiveValues; QWidget::setAttribute( @@ -408,13 +412,12 @@ void PointDetailsWidget::paintEvent(QPaintEvent *e) { .outerWidth = _textRect.width(), .availableWidth = valueWidth, }; - if (!i && _valueIcon) { - _valueIcon->paint( - p, - valueContext.position.x() - _valueIcon->width(), + if (!i && !_valueIcon.isNull()) { + p.drawImage( + valueContext.position.x() + - _valueIcon.width() / style::DevicePixelRatio(), lineY, - valueContext.outerWidth, - line.valueColor); + _valueIcon); } const auto nameContext = Ui::Text::PaintContext{ .position = QPoint( diff --git a/Telegram/SourceFiles/statistics/widgets/point_details_widget.h b/Telegram/SourceFiles/statistics/widgets/point_details_widget.h index a1e3dcccc0ee8..cdf6b3bd3acc2 100644 --- a/Telegram/SourceFiles/statistics/widgets/point_details_widget.h +++ b/Telegram/SourceFiles/statistics/widgets/point_details_widget.h @@ -47,7 +47,7 @@ class PointDetailsWidget : public Ui::AbstractButton { const style::TextStyle &_textStyle; const style::TextStyle &_headerStyle; Ui::Text::String _header; - const style::icon *_valueIcon = nullptr; + QImage _valueIcon; void invalidateCache(); diff --git a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp index 9baca321c93fc..695f8f58aa2cf 100644 --- a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp +++ b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp @@ -553,7 +553,7 @@ bool ReadSetting( const auto proxy = readProxy(); if (proxy) { list.push_back(proxy); - } else if (index < -list.size()) { + } else if (index < -int64(list.size())) { ++index; } else if (index > list.size()) { --index; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 691d84f16c78c..bb99e43a8690e 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -175,7 +175,7 @@ void AddFeaturesList( st::boostFeatureIconPosition); }; const auto proj = &Ui::Text::RichLangValue; - const auto max = std::max({ + const auto lowMax = std::max({ features.linkLogoLevel, features.transcribeLevel, features.emojiPackLevel, @@ -189,9 +189,13 @@ void AddFeaturesList( ? 0 : features.linkStylesByLevel.back().first), }); + const auto highMax = std::max(lowMax, features.sponsoredLevel); auto nameColors = 0; auto linkStyles = 0; - for (auto i = std::max(startFromLevel, 1); i <= max; ++i) { + for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) { + if ((i > lowMax) && (i < highMax)) { + continue; + } const auto unlocks = (i == startFromLevel); container->add( MakeFeaturesBadge( @@ -202,6 +206,9 @@ void AddFeaturesList( lt_count, rpl::single(float64(i)))), st::boostLevelBadgePadding); + if (i >= features.sponsoredLevel) { + add(tr::lng_channel_earn_off(proj), st::boostFeatureOffSponsored); + } if (i >= features.customWallpaperLevel) { add( (group diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index ecac121b9c3c3..6c129512deb71 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -40,6 +40,7 @@ struct BoostFeatures { int wallpaperLevel = 0; int wallpapersCount = 0; int customWallpaperLevel = 0; + int sponsoredLevel = 0; }; struct BoostBoxData { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp index 01b94a9d130bd..b1a211e4994d2 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.cpp @@ -32,9 +32,11 @@ constexpr auto kMinPreviewWidth = 20; AbstractSingleMediaPreview::AbstractSingleMediaPreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type) + AttachControls::Type type, + Fn canToggleSpoiler) : AbstractSinglePreview(parent) , _st(st) +, _canToggleSpoiler(std::move(canToggleSpoiler)) , _minThumbH(st::sendBoxAlbumGroupSize.height() + st::sendBoxAlbumGroupSkipTop * 2) , _controls(base::make_unique_q(this, type)) { @@ -82,6 +84,10 @@ rpl::producer AbstractSingleMediaPreview::spoileredChanges() const { return _spoileredChanges.events(); } +QImage AbstractSingleMediaPreview::generatePriceTagBackground() const { + return (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage(); +} + void AbstractSingleMediaPreview::preparePreview(QImage preview) { auto maxW = 0; auto maxH = 0; @@ -266,7 +272,9 @@ void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) { } void AbstractSingleMediaPreview::showContextMenu(QPoint position) { - if (!_sendWay.sendImagesAsPhotos() || !supportsSpoilers()) { + if (!_canToggleSpoiler() + || !_sendWay.sendImagesAsPhotos() + || !supportsSpoilers()) { return; } _menu = base::make_unique_q( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h index 0f5f8e7841461..ba9595f289e69 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_media_preview.h @@ -26,7 +26,8 @@ class AbstractSingleMediaPreview : public AbstractSinglePreview { AbstractSingleMediaPreview( QWidget *parent, const style::ComposeControls &st, - AttachControls::Type type); + AttachControls::Type type, + Fn canToggleSpoiler); ~AbstractSingleMediaPreview(); void setSendWay(SendFilesWay way); @@ -43,6 +44,8 @@ class AbstractSingleMediaPreview : public AbstractSinglePreview { [[nodiscard]] bool canHaveSpoiler() const; [[nodiscard]] rpl::producer spoileredChanges() const; + [[nodiscard]] QImage generatePriceTagBackground() const; + protected: virtual bool supportsSpoilers() const = 0; virtual bool drawBackground() const = 0; @@ -71,6 +74,7 @@ class AbstractSingleMediaPreview : public AbstractSinglePreview { const style::ComposeControls &_st; SendFilesWay _sendWay; + Fn _canToggleSpoiler; bool _animated = false; QPixmap _preview; QPixmap _previewBlurred; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp index 9b842aae53376..b02409084427f 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp @@ -20,6 +20,12 @@ For license and copyright information please follow this link: #include +namespace Media::Streaming { + +[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame); + +} // namespace Media::Streaming + namespace Ui { namespace { @@ -31,10 +37,12 @@ AlbumPreview::AlbumPreview( QWidget *parent, const style::ComposeControls &st, gsl::span items, - SendFilesWay way) + SendFilesWay way, + Fn canToggleSpoiler) : RpWidget(parent) , _st(st) , _sendWay(way) +, _canToggleSpoiler(std::move(canToggleSpoiler)) , _dragTimer([=] { switchToDrag(); }) { setMouseTracking(true); prepareThumbs(items); @@ -271,6 +279,7 @@ void AlbumPreview::finishDrag() { _finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration); updateSizeAnimated(layout); + _orderUpdated.fire({}); } else { for (const auto &thumb : _thumbs) { thumb->resetLayoutAnimation(); @@ -289,7 +298,7 @@ int AlbumPreview::countLayoutHeight( } void AlbumPreview::updateSizeAnimated( - const std::vector &layout) { + const std::vector &layout) { const auto newHeight = countLayoutHeight(layout); if (newHeight != _thumbsHeight) { _thumbsHeightAnimation.start( @@ -573,7 +582,7 @@ void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) { void AlbumPreview::showContextMenu( not_null thumb, QPoint position) { - if (!_sendWay.sendImagesAsPhotos()) { + if (!_canToggleSpoiler() || !_sendWay.sendImagesAsPhotos()) { return; } _menu = base::make_unique_q( @@ -608,8 +617,47 @@ void AlbumPreview::switchToDrag() { update(); } -rpl::producer AlbumPreview::thumbModified() const { - return _thumbModified.events(); +QImage AlbumPreview::generatePriceTagBackground() const { + auto wmax = 0; + auto hmax = 0; + for (auto &thumb : _thumbs) { + const auto geometry = thumb->geometry(); + accumulate_max(wmax, geometry.x() + geometry.width()); + accumulate_max(hmax, geometry.y() + geometry.height()); + } + const auto size = QSize(wmax, hmax); + if (size.isEmpty()) { + return {}; + } + const auto ratio = style::DevicePixelRatio(); + const auto full = size * ratio; + const auto skip = st::historyGroupSkip; + auto result = QImage(full, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::black); + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + for (auto &thumb : _thumbs) { + const auto geometry = thumb->geometry(); + if (geometry.isEmpty()) { + continue; + } + const auto w = geometry.width(); + const auto h = geometry.height(); + const auto wscale = (w + skip) / float64(w); + const auto hscale = (h + skip) / float64(h); + p.save(); + p.translate(geometry.center()); + p.scale(wscale, hscale); + p.translate(-geometry.center()); + thumb->paintInAlbum(p, 0, 0, 1., 1.); + p.restore(); + } + p.end(); + + return ::Media::Streaming::PrepareBlurredBackground( + full, + std::move(result)); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h index 11eda122f5776..9f1c9969d82a0 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h @@ -28,7 +28,8 @@ class AlbumPreview final : public RpWidget { QWidget *parent, const style::ComposeControls &st, gsl::span items, - SendFilesWay way); + SendFilesWay way, + Fn canToggleSpoiler); ~AlbumPreview(); void setSendWay(SendFilesWay way); @@ -46,7 +47,15 @@ class AlbumPreview final : public RpWidget { return _thumbChanged.events(); } - rpl::producer thumbModified() const; + [[nodiscard]] rpl::producer thumbModified() const { + return _thumbModified.events(); + } + + [[nodiscard]] rpl::producer<> orderUpdated() const { + return _orderUpdated.events(); + } + + [[nodiscard]] QImage generatePriceTagBackground() const; protected: void paintEvent(QPaintEvent *e) override; @@ -92,6 +101,7 @@ class AlbumPreview final : public RpWidget { const style::ComposeControls &_st; SendFilesWay _sendWay; + Fn _canToggleSpoiler; style::cursor _cursor = style::cur_default; std::vector _order; std::vector _itemsShownDimensions; @@ -114,6 +124,7 @@ class AlbumPreview final : public RpWidget { rpl::event_stream _thumbDeleted; rpl::event_stream _thumbChanged; rpl::event_stream _thumbModified; + rpl::event_stream<> _orderUpdated; base::unique_qptr _menu; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp index adf38109b92cb..92025e3cc683e 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp @@ -503,6 +503,10 @@ void AlbumThumbnail::paintFile( _fileThumb.size() / style::DevicePixelRatio()); } +QRect AlbumThumbnail::geometry() const { + return _layout.geometry; +} + bool AlbumThumbnail::containsPoint(QPoint position) const { return _layout.geometry.contains(position); } diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h index 1bfd8fc62bef2..209f864cde193 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h @@ -42,8 +42,8 @@ class AlbumThumbnail final { void setSpoiler(bool spoiler); [[nodiscard]] bool hasSpoiler() const; - int photoHeight() const; - int fileHeight() const; + [[nodiscard]] int photoHeight() const; + [[nodiscard]] int fileHeight() const; void paintInAlbum( QPainter &p, @@ -54,20 +54,22 @@ class AlbumThumbnail final { void paintPhoto(Painter &p, int left, int top, int outerWidth); void paintFile(Painter &p, int left, int top, int outerWidth); - bool containsPoint(QPoint position) const; - bool buttonsContainPoint(QPoint position) const; - AttachButtonType buttonTypeFromPoint(QPoint position) const; - int distanceTo(QPoint position) const; - bool isPointAfter(QPoint position) const; + [[nodiscard]] QRect geometry() const; + [[nodiscard]] bool containsPoint(QPoint position) const; + [[nodiscard]] bool buttonsContainPoint(QPoint position) const; + [[nodiscard]] AttachButtonType buttonTypeFromPoint( + QPoint position) const; + [[nodiscard]] int distanceTo(QPoint position) const; + [[nodiscard]] bool isPointAfter(QPoint position) const; void moveInAlbum(QPoint to); - QPoint center() const; + [[nodiscard]] QPoint center() const; void suggestMove(float64 delta, Fn callback); void finishAnimations(); void setButtonVisible(bool value); void moveButtons(int thumbTop); - bool isCompressedSticker() const; + [[nodiscard]] bool isCompressedSticker() const; static constexpr auto kShrinkDuration = crl::time(150); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 3fbe6e484e349..d367e3382e2ef 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -25,6 +25,7 @@ For license and copyright information please follow this link: #include "webview/webview_interface.h" #include "base/debug_log.h" #include "base/invoke_queued.h" +#include "base/qt_signal_producer.h" #include "styles/style_payments.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -34,6 +35,7 @@ For license and copyright information please follow this link: #include #include #include +#include namespace Ui::BotWebView { namespace { @@ -178,6 +180,8 @@ void Panel::Button::updateFg(QColor fg) { void Panel::Button::updateArgs(MainButtonArgs &&args) { _textFull = std::move(args.text); setDisabled(!args.isActive); + setPointerCursor(false); + setCursor(args.isActive ? style::cur_pointer : Qt::ForbiddenCursor); setVisible(args.isVisible); toggleProgress(args.isProgressVisible); update(); @@ -371,6 +375,13 @@ Panel::~Panel() { void Panel::requestActivate() { _widget->showAndActivate(); + if (const auto widget = _webview ? _webview->window.widget() : nullptr) { + InvokeQueued(widget, [=] { + if (widget->isVisible()) { + _webview->window.focus(); + } + }); + } } void Panel::toggleProgress(bool shown) { @@ -525,9 +536,15 @@ bool Panel::showWebview( _webview->window.navigate(url); } }, &st::menuIconRestore); - callback(tr::lng_bot_terms(tr::now), [=] { - File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); - }, &st::menuIconGroupLog); + if (_menuButtons & MenuButton::ShareGame) { + callback(tr::lng_iv_share(tr::now), [=] { + _delegate->botShareGameScore(); + }, &st::menuIconShare); + } else { + callback(tr::lng_bot_terms(tr::now), [=] { + File::OpenUrl(tr::lng_mini_apps_tos_url(tr::now)); + }, &st::menuIconGroupLog); + } const auto main = (_menuButtons & MenuButton::RemoveFromMainMenu); if (main || (_menuButtons & MenuButton::RemoveFromMenu)) { const auto handler = [=] { @@ -689,6 +706,8 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { requestClipboardText(arguments); } else if (command == "web_app_set_header_color") { processHeaderColor(arguments); + } else if (command == "share_score") { + _delegate->botShareGameScore(); } }); @@ -720,6 +739,17 @@ postEvent: function(eventType, eventData) { setupProgressGeometry(); + base::qt_signal_producer( + _widget->window()->windowHandle(), + &QWindow::activeChanged + ) | rpl::filter([=] { + return _webview && _widget->window()->windowHandle()->isActive(); + }) | rpl::start_with_next([=] { + if (_webview && !_webview->window.widget()->isHidden()) { + _webview->window.focus(); + } + }, _webview->lifetime); + return true; } @@ -803,8 +833,7 @@ void Panel::openExternalLink(const QJsonObject &args) { } const auto iv = args["try_instant_view"].toBool(); const auto url = args["url"].toString(); - const auto lower = url.toLower(); - if (!lower.startsWith("http://") && !lower.startsWith("https://")) { + if (!_delegate->botValidateExternalLink(url)) { LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url)); _delegate->botClose(); return; @@ -1206,6 +1235,13 @@ void Panel::updateFooterHeight() { } void Panel::showBox(object_ptr box) { + showBox(std::move(box), LayerOption::KeepOther, anim::type::normal); +} + +void Panel::showBox( + object_ptr box, + LayerOptions options, + anim::type animated) { if (const auto widget = _webview ? _webview->window.widget() : nullptr) { const auto hideNow = !widget->isHidden(); if (hideNow || _webview->lastHidingBox) { @@ -1219,10 +1255,12 @@ void Panel::showBox(object_ptr box) { && widget->isHidden() && _webview->lastHidingBox == raw) { widget->show(); + _webviewBottom->show(); } }, _webview->lifetime); if (hideNow) { widget->hide(); + _webviewBottom->hide(); } } } @@ -1236,6 +1274,14 @@ void Panel::showToast(TextWithEntities &&text) { _widget->showToast(std::move(text)); } +not_null Panel::toastParent() const { + return _widget->uiShow()->toastParent(); +} + +void Panel::hideLayer(anim::type animated) { + _widget->hideLayer(animated); +} + void Panel::showCriticalError(const TextWithEntities &text) { _progress = nullptr; _webviewProgress = false; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index fb0231a012d28..d9071c846e50e 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -20,6 +20,8 @@ namespace Ui { class BoxContent; class RpWidget; class SeparatePanel; +enum class LayerOption; +using LayerOptions = base::flags; } // namespace Ui namespace Webview { @@ -40,6 +42,7 @@ enum class MenuButton { OpenBot = 0x01, RemoveFromMenu = 0x02, RemoveFromMainMenu = 0x04, + ShareGame = 0x08, }; inline constexpr bool is_flag_type(MenuButton) { return true; } using MenuButtons = base::flags; @@ -57,6 +60,7 @@ class Delegate { virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0; virtual void botHandleInvoice(QString slug) = 0; virtual void botHandleMenuButton(MenuButton button) = 0; + virtual bool botValidateExternalLink(QString uri) = 0; virtual void botOpenIvLink(QString uri) = 0; virtual void botSendData(QByteArray data) = 0; virtual void botSwitchInlineQuery( @@ -66,6 +70,7 @@ class Delegate { virtual void botAllowWriteAccess(Fn callback) = 0; virtual void botSharePhone(Fn callback) = 0; virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0; + virtual void botShareGameScore() = 0; virtual void botClose() = 0; }; @@ -88,7 +93,13 @@ class Panel final : public base::has_weak_ptr { rpl::producer bottomText); void showBox(object_ptr box); + void showBox( + object_ptr box, + LayerOptions options, + anim::type animated); + void hideLayer(anim::type animated); void showToast(TextWithEntities &&text); + not_null toastParent() const; void showCriticalError(const TextWithEntities &text); void showWebviewError( const QString &text, diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp index b686ee6fe1270..d180059ddd6d3 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_item_single_media_preview.cpp @@ -36,7 +36,7 @@ ItemSingleMediaPreview::ItemSingleMediaPreview( Fn gifPaused, not_null item, AttachControls::Type type) -: AbstractSingleMediaPreview(parent, st, type) +: AbstractSingleMediaPreview(parent, st, type, [] { return true; }) , _gifPaused(std::move(gifPaused)) , _fullId(item->fullId()) { const auto media = item->media(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp index 1a70c69b6de8a..505fa455bd20c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp @@ -82,6 +82,10 @@ bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) { Unexpected("AlbumType in CanBeInAlbumType."); } +bool InsertTextOnImageCancel(const QString &text) { + return !text.isEmpty() && !text.startsWith(u"data:image"_q); +} + PreparedList PreparedList::Reordered( PreparedList &&list, std::vector order) { @@ -195,6 +199,10 @@ bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const { || (file.type == PreparedFile::Type::Photo && compress); } +bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const { + return canMoveCaption(sendingAlbum, compress); +} + bool PreparedList::hasGroupOption(bool slowmode) const { if (slowmode || files.size() < 2) { return false; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index 30210e3d62042..c1fb3a379312d 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -88,6 +88,7 @@ struct PreparedFile { }; [[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album); +[[nodiscard]] bool InsertTextOnImageCancel(const QString &text); struct PreparedList { enum class Error { @@ -115,6 +116,9 @@ struct PreparedList { [[nodiscard]] bool canMoveCaption( bool sendingAlbum, bool compress) const; + [[nodiscard]] bool canChangePrice( + bool sendingAlbum, + bool compress) const; [[nodiscard]] bool canBeSentInSlowmode() const; [[nodiscard]] bool canBeSentInSlowmodeWith( const PreparedList &other) const; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp index ed250540c06a8..cb80676cd50f9 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.cpp @@ -19,6 +19,7 @@ SingleMediaPreview *SingleMediaPreview::Create( const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, + Fn canToggleSpoiler, AttachControls::Type type) { auto preview = QImage(); auto animated = false; @@ -51,7 +52,8 @@ SingleMediaPreview *SingleMediaPreview::Create( Core::IsMimeSticker(file.information->filemime), file.spoiler, animationPreview ? file.path : QString(), - type); + type, + std::move(canToggleSpoiler)); } SingleMediaPreview::SingleMediaPreview( @@ -63,8 +65,9 @@ SingleMediaPreview::SingleMediaPreview( bool sticker, bool spoiler, const QString &animatedPreviewPath, - AttachControls::Type type) -: AbstractSingleMediaPreview(parent, st, type) + AttachControls::Type type, + Fn canToggleSpoiler) +: AbstractSingleMediaPreview(parent, st, type, std::move(canToggleSpoiler)) , _gifPaused(std::move(gifPaused)) , _sticker(sticker) { Expects(!preview.isNull()); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h index ac5d4f84f447e..d57ccd3663df9 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_media_preview.h @@ -25,6 +25,7 @@ class SingleMediaPreview final : public AbstractSingleMediaPreview { const style::ComposeControls &st, Fn gifPaused, const PreparedFile &file, + Fn canToggleSpoiler, AttachControls::Type type = AttachControls::Type::Full); SingleMediaPreview( @@ -36,7 +37,8 @@ class SingleMediaPreview final : public AbstractSingleMediaPreview { bool sticker, bool spoiler, const QString &animatedPreviewPath, - AttachControls::Type type); + AttachControls::Type type, + Fn canToggleSpoiler); protected: bool supportsSpoilers() const override; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 3ea2342e299ce..2f766c37271bb 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -54,31 +54,8 @@ msgDateDelta: point(2px, 5px); msgDateImgDelta: 4px; msgDateImgPadding: point(8px, 2px); -messageQuoteStyle: QuoteStyle(defaultQuoteStyle) { - padding: margins(10px, 2px, 4px, 2px); - verticalSkip: 4px; - outline: 3px; - outlineShift: 2px; - radius: 5px; -} -messageTextStyle: TextStyle(defaultTextStyle) { - blockquote: QuoteStyle(messageQuoteStyle) { - 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(messageQuoteStyle) { - header: 20px; - headerPosition: point(10px, 2px); - scrollable: true; - icon: icon{{ "chat/mini_copy", windowFg }}; - iconPosition: point(4px, 2px); - } -} +messageQuoteStyle: historyQuoteStyle; +messageTextStyle: historyTextStyle; historyPagePreview: QuoteStyle(messageQuoteStyle) { padding: margins(10px, 5px, 7px, 7px); } @@ -129,6 +106,9 @@ outTextPaletteSelected: TextPalette(outTextPalette) { monoFg: msgOutMonoFgSelected; spoilerFg: msgOutDateFgSelected; } +priceTagTextPalette: TextPalette(defaultTextPalette) { + linkFg: creditsBg1; +} fwdTextUserpicPadding: margins(0px, 1px, 3px, 0px); fwdTextStyle: TextStyle(semiboldTextStyle) { linkUnderline: kLinkUnderlineNever; @@ -1162,5 +1142,6 @@ factcheckField: InputField(defaultInputField) { heightMin: 24px; - font: normalFont; + style: defaultTextStyle; } +purchasedTagPadding: margins(3px, 2px, 6px, 2px); diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index 56f236481f11e..0c50f77b88ea2 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -171,6 +171,7 @@ ChatStyle::ChatStyle(rpl::producer colorIndices) { make(_historyPsaForwardPalette, st::historyPsaForwardPalette); make(_imgReplyTextPalette, st::imgReplyTextPalette); make(_serviceTextPalette, st::serviceTextPalette); + make(_priceTagTextPalette, st::priceTagTextPalette); make(_historyRepliesInvertedIcon, st::historyRepliesInvertedIcon); make(_historyViewsInvertedIcon, st::historyViewsInvertedIcon); make(_historyViewsSendingIcon, st::historyViewsSendingIcon); diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index f7499a2d05c42..a00fe941ab13e 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -349,6 +349,9 @@ class ChatStyle final : public style::palette { [[nodiscard]] const style::TextPalette &serviceTextPalette() const { return _serviceTextPalette; } + [[nodiscard]] const style::TextPalette &priceTagTextPalette() const { + return _priceTagTextPalette; + } [[nodiscard]] const style::icon &historyRepliesInvertedIcon() const { return _historyRepliesInvertedIcon; } @@ -516,6 +519,7 @@ class ChatStyle final : public style::palette { style::TextPalette _historyPsaForwardPalette; style::TextPalette _imgReplyTextPalette; style::TextPalette _serviceTextPalette; + style::TextPalette _priceTagTextPalette; style::icon _historyRepliesInvertedIcon = { Qt::Uninitialized }; style::icon _historyViewsInvertedIcon = { Qt::Uninitialized }; style::icon _historyViewsSendingIcon = { Qt::Uninitialized }; diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 1b86567b3f931..0b26e1810bc64 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -416,7 +416,7 @@ void UserpicButton::openPeerPhoto() { return; } const auto photo = _peer->owner().photo(id); - if (photo->date && _controller) { + if (photo->date() && _controller) { _controller->openPhoto(photo, _peer); } } @@ -744,7 +744,7 @@ void UserpicButton::updateVideo() { return; } const auto photo = _peer->owner().photo(id); - if (!photo->date || !photo->videoCanBePlayed()) { + if (!photo->date() || !photo->videoCanBePlayed()) { clearStreaming(); return; } else if (_streamed && _streamedPhoto == photo) { diff --git a/Telegram/SourceFiles/ui/countryinput.cpp b/Telegram/SourceFiles/ui/countryinput.cpp index a9c9e1add1e67..bdf77d1c18698 100644 --- a/Telegram/SourceFiles/ui/countryinput.cpp +++ b/Telegram/SourceFiles/ui/countryinput.cpp @@ -55,7 +55,7 @@ void CountryInput::paintEvent(QPaintEvent *e) { st::introCountryIconPosition.y(), width()); - p.setFont(_st.font); + p.setFont(_st.style.font); p.setPen(_st.textFg); p.drawText(rect().marginsRemoved(_st.textMargins), _text, _st.textAlign); } @@ -159,7 +159,7 @@ rpl::producer CountryInput::codeChanged() const { } void CountryInput::setText(const QString &newText) { - _text = _st.font->elided( + _text = _st.style.font->elided( newText, width() - _st.textMargins.left() - _st.textMargins.right()); } diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 2da64404d5353..baab38fb77279 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -35,11 +35,17 @@ creditsBoxAbout: FlatLabel(defaultFlatLabel) { minWidth: 256px; align: align(top); } - creditsBoxAboutTitle: FlatLabel(settingsPremiumUserTitle) { minWidth: 256px; } - creditsBoxAboutDivider: FlatLabel(boxDividerLabel) { align: align(top); } +creditsBoxButtonLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; +} + +starIconSmall: icon{{ "payments/small_star", windowFg }}; +starIconSmallPadding: margins(0px, -2px, 0px, 0px); + +creditsHistoryEntryTypeAds: icon {{ "folders/folders_channels", premiumButtonFg }}; diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index 3c7a714f7120c..e99a4d4eb37af 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -11,33 +11,207 @@ For license and copyright information please follow this link: #include "data/data_credits.h" #include "data/data_file_origin.h" +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/spoiler_mess.h" #include "ui/empty_userpic.h" #include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_channel_earn.h" #include "styles/style_credits.h" +#include "styles/style_dialogs.h" #include "styles/style_intro.h" // introFragmentIcon. +#include "styles/style_layers.h" #include "styles/style_settings.h" -#include "styles/style_dialogs.h" +#include "styles/style_widgets.h" + +#include namespace Ui { +namespace { + +PaintRoundImageCallback MultiThumbnail( + PaintRoundImageCallback first, + PaintRoundImageCallback second, + int totalCount) { + const auto cache = std::make_shared(); + return [=](Painter &p, int x, int y, int outerWidth, int size) { + const auto stroke = st::lineWidth * 2; + const auto shift = stroke * 3; + if (size <= 2 * shift) { + first(p, x, y, outerWidth, size); + return; + } + const auto smaller = size - shift; + const auto ratio = style::DevicePixelRatio(); + const auto full = QSize(size, size) * ratio; + if (cache->size() != full) { + *cache = QImage(full, QImage::Format_ARGB32_Premultiplied); + cache->setDevicePixelRatio(ratio); + } + cache->fill(Qt::transparent); + auto q = Painter(cache.get()); + second(q, shift, 0, outerWidth, smaller); + q.setCompositionMode(QPainter::CompositionMode_Source); + q.setPen(QPen(Qt::transparent, 2 * stroke)); + q.setBrush(Qt::NoBrush); + const auto radius = st::roundRadiusLarge; + auto hq = PainterHighQualityEnabler(q); + q.drawRoundedRect(QRect(0, shift, smaller, smaller), radius, radius); + q.setCompositionMode(QPainter::CompositionMode_SourceOver); + first(q, 0, shift, outerWidth, smaller); + q.setPen(Qt::NoPen); + q.setBrush(st::shadowFg); + q.drawRoundedRect(QRect(0, shift, smaller, smaller), radius, radius); + q.setPen(st::toastFg); + q.setFont(style::font(smaller / 2, style::FontFlag::Semibold, 0)); + q.drawText( + QRect(0, shift, smaller, smaller), + QString::number(totalCount), + style::al_center); + q.end(); + + p.drawImage(x, y, *cache); + }; +} + +} // namespace -using PaintRoundImageCallback = Fn; +QImage GenerateStars(int height, int count) { + constexpr auto kOutlineWidth = .6; + constexpr auto kStrokeWidth = 3; + constexpr auto kShift = 3; + + auto colorized = qs(Premium::ColorizedSvg( + Premium::CreditsIconGradientStops())); + colorized.replace( + u"stroke=\"none\""_q, + u"stroke=\"%1\""_q.arg(st::creditsStroke->c.name())); + colorized.replace( + u"stroke-width=\"1\""_q, + u"stroke-width=\"%1\""_q.arg(kStrokeWidth)); + auto svg = QSvgRenderer(colorized.toUtf8()); + svg.setViewBox(svg.viewBox() + Margins(kStrokeWidth)); + + const auto starSize = Size(height - kOutlineWidth * 2); + + auto frame = QImage( + QSize( + (height + kShift * (count - 1)) * style::DevicePixelRatio(), + height * style::DevicePixelRatio()), + QImage::Format_ARGB32_Premultiplied); + frame.setDevicePixelRatio(style::DevicePixelRatio()); + frame.fill(Qt::transparent); + const auto drawSingle = [&](QPainter &q) { + const auto s = kOutlineWidth; + q.save(); + q.translate(s, s); + q.setCompositionMode(QPainter::CompositionMode_Clear); + svg.render(&q, QRectF(QPointF(s, 0), starSize)); + svg.render(&q, QRectF(QPointF(s, s), starSize)); + svg.render(&q, QRectF(QPointF(0, s), starSize)); + svg.render(&q, QRectF(QPointF(-s, s), starSize)); + svg.render(&q, QRectF(QPointF(-s, 0), starSize)); + svg.render(&q, QRectF(QPointF(-s, -s), starSize)); + svg.render(&q, QRectF(QPointF(0, -s), starSize)); + svg.render(&q, QRectF(QPointF(s, -s), starSize)); + q.setCompositionMode(QPainter::CompositionMode_SourceOver); + svg.render(&q, Rect(starSize)); + q.restore(); + }; + { + auto q = QPainter(&frame); + q.translate(frame.width() / style::DevicePixelRatio() - height, 0); + for (auto i = count; i > 0; --i) { + drawSingle(q); + q.translate(-kShift, 0); + } + } + return frame; +} + +not_null CreateSingleStarWidget( + not_null parent, + int height) { + const auto widget = CreateChild(parent); + const auto image = GenerateStars(height, 1); + widget->resize(image.size() / style::DevicePixelRatio()); + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(widget); + p.drawImage(0, 0, image); + }, widget->lifetime()); + widget->setAttribute(Qt::WA_TransparentForMouseEvents); + return widget; +} + +not_null AddInputFieldForCredits( + not_null container, + rpl::producer value) { + const auto &st = st::botEarnInputField; + const auto inputContainer = container->add( + CreateSkipWidget(container, st.heightMin)); + const auto currentValue = rpl::variable( + rpl::duplicate(value)); + const auto input = CreateChild( + inputContainer, + st, + tr::lng_bot_earn_out_ph(), + QString::number(currentValue.current()), + currentValue.current()); + rpl::duplicate( + value + ) | rpl::start_with_next([=](uint64 v) { + input->changeLimit(v); + input->setText(QString::number(v)); + }, input->lifetime()); + const auto icon = CreateSingleStarWidget( + inputContainer, + st.style.font->height); + inputContainer->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + input->resize( + size.width() - rect::m::sum::h(st::boxRowPadding), + st.heightMin); + input->moveToLeft(st::boxRowPadding.left(), 0); + icon->moveToLeft( + st::boxRowPadding.left(), + st.textMargins.top()); + }, input->lifetime()); + ToggleChildrenVisibility(inputContainer, true); + return input; +} PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const Data::CreditsHistoryEntry &entry) { - const auto bg = [&]() -> Ui::EmptyUserpic::BgColors { + using PeerType = Data::CreditsHistoryEntry::PeerType; + if (entry.peerType == PeerType::PremiumBot) { + const auto svg = std::make_shared(Ui::Premium::Svg()); + return [=](Painter &p, int x, int y, int, int size) mutable { + const auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + { + auto gradient = QLinearGradient(x + size, y + size, x, y); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setBrush(gradient); + } + p.drawEllipse(x, y, size, size); + svg->render(&p, QRectF(x, y, size, size) - Margins(size / 5.)); + }; + } + const auto bg = [&]() -> EmptyUserpic::BgColors { switch (entry.peerType) { case Data::CreditsHistoryEntry::PeerType::Peer: - return Ui::EmptyUserpic::UserpicColor(0); + return EmptyUserpic::UserpicColor(0); case Data::CreditsHistoryEntry::PeerType::AppStore: return { st::historyPeer7UserpicBg, st::historyPeer7UserpicBg2 }; case Data::CreditsHistoryEntry::PeerType::PlayMarket: @@ -46,6 +220,8 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 }; case Data::CreditsHistoryEntry::PeerType::PremiumBot: return { st::historyPeer8UserpicBg, st::historyPeer8UserpicBg2 }; + case Data::CreditsHistoryEntry::PeerType::Ads: + return { st::historyPeer6UserpicBg, st::historyPeer6UserpicBg2 }; case Data::CreditsHistoryEntry::PeerType::Unsupported: return { st::historyPeerArchiveUserpicBg, @@ -54,13 +230,9 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( } Unexpected("Unknown peer type."); }(); - const auto userpic = std::make_shared(bg, QString()); + const auto userpic = std::make_shared(bg, QString()); return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { userpic->paintCircle(p, x, y, outerWidth, size); - using PeerType = Data::CreditsHistoryEntry::PeerType; - if (entry.peerType == PeerType::PremiumBot) { - return; - } const auto rect = QRect(x, y, size, size); ((entry.peerType == PeerType::AppStore) ? st::sessionIconiPhone @@ -68,11 +240,13 @@ PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( ? st::sessionIconAndroid : (entry.peerType == PeerType::Fragment) ? st::introFragmentIcon + : (entry.peerType == PeerType::Ads) + ? st::creditsHistoryEntryTypeAds : st::dialogsInaccessibleUserpic).paintInCenter(p, rect); }; } -Fn GenerateCreditsPaintEntryCallback( +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( not_null photo, Fn update) { struct State { @@ -84,7 +258,7 @@ Fn GenerateCreditsPaintEntryCallback( }; const auto state = std::make_shared(); state->view = photo->createMediaView(); - photo->load(Data::PhotoSize::Thumbnail, {}); + photo->load(Data::PhotoSize::Large, {}); rpl::single(rpl::empty_value()) | rpl::then( photo->owner().session().downloaderTaskFinished() @@ -116,15 +290,177 @@ Fn GenerateCreditsPaintEntryCallback( minSize, minSize), size * style::DevicePixelRatio(), - { .options = Images::Option::RoundCircle }); + { .options = Images::Option::RoundLarge }); + } + p.drawImage(x, y, state->image); + }; +} + +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( + not_null video, + Fn update) { + struct State { + std::shared_ptr view; + Image *imagePtr = nullptr; + QImage image; + rpl::lifetime downloadLifetime; + bool entryImageLoaded = false; + }; + const auto state = std::make_shared(); + state->view = video->createMediaView(); + video->loadThumbnail({}); + + rpl::single(rpl::empty_value()) | rpl::then( + video->owner().session().downloaderTaskFinished() + ) | rpl::start_with_next([=] { + if (const auto thumbnail = state->view->thumbnail()) { + state->imagePtr = thumbnail; + } + update(); + if (state->imagePtr) { + state->entryImageLoaded = true; + state->downloadLifetime.destroy(); + } + }, state->downloadLifetime); + + return [=](Painter &p, int x, int y, int outerWidth, int size) { + if (state->imagePtr + && (!state->entryImageLoaded || state->image.isNull())) { + const auto image = state->imagePtr->original(); + const auto minSize = std::min(image.width(), image.height()); + state->image = Images::Prepare( + image.copy( + (image.width() - minSize) / 2, + (image.height() - minSize) / 2, + minSize, + minSize), + size * style::DevicePixelRatio(), + { .options = Images::Option::RoundLarge }); } p.drawImage(x, y, state->image); }; } +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( + not_null session, + Data::CreditsHistoryMedia media, + Fn update) { + return (media.type == Data::CreditsHistoryMediaType::Photo) + ? GenerateCreditsPaintEntryCallback( + session->data().photo(media.id), + std::move(update)) + : GenerateCreditsPaintEntryCallback( + session->data().document(media.id), + std::move(update)); +} + +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( + not_null session, + const std::vector &media, + Fn update) { + if (media.size() == 1) { + return GenerateCreditsPaintEntryCallback( + session, + media.front(), + std::move(update)); + } + return MultiThumbnail( + GenerateCreditsPaintEntryCallback(session, media[0], update), + GenerateCreditsPaintEntryCallback(session, media[1], update), + media.size()); +} + +PaintRoundImageCallback GeneratePaidPhotoPaintCallback( + not_null photo, + Fn update) { + struct State { + explicit State(Fn update) : spoiler(std::move(update)) { + } + + QImage image; + QImage spoilerCornerCache; + SpoilerAnimation spoiler; + }; + const auto state = std::make_shared(update); + + return [=](Painter &p, int x, int y, int outerWidth, int size) { + if (state->image.isNull()) { + const auto media = photo->createMediaView(); + const auto thumbnail = media->thumbnailInline(); + const auto ratio = style::DevicePixelRatio(); + const auto scaled = QSize(size, size) * ratio; + auto image = thumbnail + ? Images::Blur(thumbnail->original(), true) + : QImage(scaled, QImage::Format_ARGB32_Premultiplied); + if (!thumbnail) { + image.fill(Qt::black); + image.setDevicePixelRatio(ratio); + } + const auto minSize = std::min(image.width(), image.height()); + state->image = Images::Prepare( + image.copy( + (image.width() - minSize) / 2, + (image.height() - minSize) / 2, + minSize, + minSize), + size * ratio, + { .options = Images::Option::RoundLarge }); + } + p.drawImage(x, y, state->image); + FillSpoilerRect( + p, + QRect(x, y, size, size), + Images::CornersMaskRef( + Images::CornersMask(ImageRoundRadius::Large)), + DefaultImageSpoiler().frame( + state->spoiler.index(crl::now(), false)), + state->spoilerCornerCache); + }; +} + +PaintRoundImageCallback GeneratePaidMediaPaintCallback( + not_null photo, + PhotoData *second, + int totalCount, + Fn update) { + if (!second) { + return GeneratePaidPhotoPaintCallback(photo, update); + } + return MultiThumbnail( + GeneratePaidPhotoPaintCallback(photo, update), + GeneratePaidPhotoPaintCallback(second, update), + totalCount); +} + +Fn)> PaintPreviewCallback( + not_null session, + const Data::CreditsHistoryEntry &entry) { + const auto &extended = entry.extended; + if (!extended.empty()) { + return [=](Fn update) { + return GenerateCreditsPaintEntryCallback( + session, + extended, + std::move(update)); + }; + } else if (entry.photoId) { + const auto photo = session->data().photo(entry.photoId); + return [=](Fn update) { + return GenerateCreditsPaintEntryCallback( + photo, + std::move(update)); + }; + } + return nullptr; +} + TextWithEntities GenerateEntryName(const Data::CreditsHistoryEntry &entry) { return ((entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment) ? tr::lng_bot_username_description1_link + : (entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) + ? tr::lng_credits_box_history_entry_premium_bot + : (entry.peerType == Data::CreditsHistoryEntry::PeerType::Ads) + ? tr::lng_credits_box_history_entry_ads : tr::lng_credits_summary_history_entry_inner_in)( tr::now, TextWithEntities::Simple); diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.h b/Telegram/SourceFiles/ui/effects/credits_graphics.h index ddfbea07cfec2..70d9830566cac 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.h +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.h @@ -8,20 +8,68 @@ For license and copyright information please follow this link: #pragma once class PhotoData; +class DocumentData; namespace Data { struct CreditsHistoryEntry; +struct CreditsHistoryMedia; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace Ui { +class MaskedInputField; +class RpWidget; +class VerticalLayout; +} // namespace Ui + +namespace Ui { + +using PaintRoundImageCallback = Fn; -Fn GenerateCreditsPaintUserpicCallback( +[[nodiscard]] QImage GenerateStars(int height, int count); + +[[nodiscard]] not_null CreateSingleStarWidget( + not_null parent, + int height); + +[[nodiscard]] not_null AddInputFieldForCredits( + not_null container, + rpl::producer value); + +PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const Data::CreditsHistoryEntry &entry); -Fn GenerateCreditsPaintEntryCallback( +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( not_null photo, Fn update); +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( + not_null video, + Fn update); + +PaintRoundImageCallback GenerateCreditsPaintEntryCallback( + not_null session, + Data::CreditsHistoryMedia media, + Fn update); + +PaintRoundImageCallback GeneratePaidMediaPaintCallback( + not_null photo, + PhotoData *second, + int totalCount, + Fn update); + +Fn)> PaintPreviewCallback( + not_null session, + const Data::CreditsHistoryEntry &entry); + [[nodiscard]] TextWithEntities GenerateEntryName( const Data::CreditsHistoryEntry &entry); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index afdac71466323..33fae62ebd5c2 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -364,3 +364,4 @@ boostFeatureLink: icon{{ "settings/premium/features/feature_links", windowBgActi boostFeatureName: icon{{ "settings/premium/features/feature_color_names", windowBgActive }}; boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowBgActive }}; boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }}; +boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }}; diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index d04dae62b642a..54361d5455a72 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -27,7 +27,9 @@ For license and copyright information please follow this link: #include "styles/style_settings.h" #include "styles/style_window.h" +#include #include +#include namespace Ui { namespace Premium { @@ -903,6 +905,70 @@ void Line::recache(const QSize &s) { } // namespace +QString Svg() { + return u":/gui/icons/settings/star.svg"_q; +} + +QByteArray ColorizedSvg(const QGradientStops &gradientStops) { + auto f = QFile(Svg()); + if (!f.open(QIODevice::ReadOnly)) { + return QByteArray(); + } + auto content = QString::fromUtf8(f.readAll()); + auto stops = [&] { + auto s = QString(); + for (const auto &stop : gradientStops) { + s += QString("") + .arg(QString::number(stop.first), stop.second.name()); + } + return s; + }(); + const auto color = QString("%5") + .arg(0) + .arg(1) + .arg(1) + .arg(0) + .arg(std::move(stops)); + content.replace(u"gradientPlaceholder"_q, color); + content.replace(u"#fff"_q, u"url(#Gradient2)"_q); + f.close(); + return content.toUtf8(); +} + +QImage GenerateStarForLightTopBar(QRectF rect) { + auto svg = QSvgRenderer(Ui::Premium::Svg()); + + const auto size = rect.size().toSize(); + auto frame = QImage( + size * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + frame.setDevicePixelRatio(style::DevicePixelRatio()); + + auto mask = frame; + mask.fill(Qt::transparent); + { + auto p = QPainter(&mask); + auto gradient = QLinearGradient( + 0, + size.height(), + size.width(), + 0); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setPen(Qt::NoPen); + p.setBrush(gradient); + p.drawRect(0, 0, size.width(), size.height()); + } + frame.fill(Qt::transparent); + { + auto q = QPainter(&frame); + svg.render(&q, QRect(QPoint(), size)); + q.setCompositionMode(QPainter::CompositionMode_SourceIn); + q.drawImage(0, 0, mask); + } + return frame; +} + void AddBubbleRow( not_null parent, const style::PremiumBubble &st, diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index 339d49a3d30c8..d1ba4f9d7ae6b 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -41,6 +41,10 @@ namespace Premium { inline constexpr auto kLimitRowRatio = 0.5; +[[nodiscard]] QString Svg(); +[[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops); +[[nodiscard]] QImage GenerateStarForLightTopBar(QRectF rect); + void AddBubbleRow( not_null parent, const style::PremiumBubble &st, diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 640a96b3e9fd0..e2b61927301f7 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -16,8 +16,6 @@ For license and copyright information please follow this link: #include "styles/style_settings.h" #include "styles/style_premium.h" -#include - namespace Ui::Premium { namespace { @@ -42,70 +40,6 @@ constexpr auto kMinAcceptableContrast = 4.5; // 1.14; } // namespace -QString Svg() { - return u":/gui/icons/settings/star.svg"_q; -} - -QByteArray ColorizedSvg(const QGradientStops &gradientStops) { - auto f = QFile(Svg()); - if (!f.open(QIODevice::ReadOnly)) { - return QByteArray(); - } - auto content = QString::fromUtf8(f.readAll()); - auto stops = [&] { - auto s = QString(); - for (const auto &stop : gradientStops) { - s += QString("") - .arg(QString::number(stop.first), stop.second.name()); - } - return s; - }(); - const auto color = QString("%5") - .arg(0) - .arg(1) - .arg(1) - .arg(0) - .arg(std::move(stops)); - content.replace(u"gradientPlaceholder"_q, color); - content.replace(u"#fff"_q, u"url(#Gradient2)"_q); - f.close(); - return content.toUtf8(); -} - -QImage GenerateStarForLightTopBar(QRectF rect) { - auto svg = QSvgRenderer(Ui::Premium::Svg()); - - const auto size = rect.size().toSize(); - auto frame = QImage( - size * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - frame.setDevicePixelRatio(style::DevicePixelRatio()); - - auto mask = frame; - mask.fill(Qt::transparent); - { - auto p = QPainter(&mask); - auto gradient = QLinearGradient( - 0, - size.height(), - size.width(), - 0); - gradient.setStops(Ui::Premium::ButtonGradientStops()); - p.setPen(Qt::NoPen); - p.setBrush(gradient); - p.drawRect(0, 0, size.width(), size.height()); - } - frame.fill(Qt::transparent); - { - auto q = QPainter(&frame); - svg.render(&q, QRect(QPoint(), size)); - q.setCompositionMode(QPainter::CompositionMode_SourceIn); - q.drawImage(0, 0, mask); - } - return frame; -} - TopBarAbstract::TopBarAbstract( QWidget *parent, const style::PremiumCover &st) diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 86c8158fffad9..1945270252492 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -25,10 +25,6 @@ class FlatLabel; namespace Ui::Premium { -[[nodiscard]] QString Svg(); -[[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops); -[[nodiscard]] QImage GenerateStarForLightTopBar(QRectF rect); - class TopBarAbstract : public RpWidget { public: TopBarAbstract( diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 8d08ff7de5591..96838e8a5bfe0 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -155,6 +155,10 @@ menuIconTagRename: icon{{ "menu/tag_rename", menuIconColor }}; menuIconGroupsHide: icon {{ "menu/hide_members", menuIconColor }}; menuIconFont: icon {{ "menu/fonts", menuIconColor }}; menuIconFactcheck: icon {{ "menu/factcheck", menuIconColor }}; +menuIconWinHello: icon {{ "menu/passcode_winhello", menuIconColor }}; +menuIconTouchID: icon {{ "menu/passcode_finger", menuIconColor }}; +menuIconAppleWatch: icon {{ "menu/passcode_watch", menuIconColor }}; +menuIconSystemPwd: menuIconPermissions; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); diff --git a/Telegram/SourceFiles/ui/widgets/color_editor.cpp b/Telegram/SourceFiles/ui/widgets/color_editor.cpp index 3141faef39184..bf2ba26fcfc1c 100644 --- a/Telegram/SourceFiles/ui/widgets/color_editor.cpp +++ b/Telegram/SourceFiles/ui/widgets/color_editor.cpp @@ -704,7 +704,7 @@ void ColorEditor::Field::correctValue( } void ColorEditor::Field::paintAdditionalPlaceholder(QPainter &p) { - p.setFont(_st.font); + p.setFont(_st.style.font); p.setPen(_st.placeholderFg); const auto inner = QRect( _st.textMargins.right(), @@ -829,7 +829,7 @@ void ColorEditor::ResultField::correctValue( } void ColorEditor::ResultField::paintAdditionalPlaceholder(QPainter &p) { - p.setFont(_st.font); + p.setFont(_st.style.font); p.setPen(_st.placeholderFg); p.drawText( QRect( diff --git a/Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp b/Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp index 81b1b1a0d5058..0b7487caf12f6 100644 --- a/Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp @@ -267,14 +267,14 @@ UsernameInput::UsernameInput( void UsernameInput::setLinkPlaceholder(const QString &placeholder) { _linkPlaceholder = placeholder; if (!_linkPlaceholder.isEmpty()) { - setTextMargins(style::margins(_st.textMargins.left() + _st.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom())); + setTextMargins(style::margins(_st.textMargins.left() + _st.style.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom())); setPlaceholderHidden(true); } } void UsernameInput::paintAdditionalPlaceholder(QPainter &p) { if (!_linkPlaceholder.isEmpty()) { - p.setFont(_st.font); + p.setFont(_st.style.font); p.setPen(_st.placeholderFg); p.drawText(QRect(_st.textMargins.left(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), _linkPlaceholder, style::al_topleft); } diff --git a/Telegram/SourceFiles/ui/widgets/slider_natural_width.h b/Telegram/SourceFiles/ui/widgets/slider_natural_width.h new file mode 100644 index 0000000000000..f2c52aa0a9ba7 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/slider_natural_width.h @@ -0,0 +1,29 @@ +/* +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 "ui/widgets/discrete_sliders.h" + +namespace Ui { + +class CustomWidthSlider final : public SettingsSlider { +public: + using Ui::SettingsSlider::SettingsSlider; + void setNaturalWidth(int w) { + _naturalWidth = w; + } + int naturalWidth() const override { + return _naturalWidth; + } + +private: + int _naturalWidth = 0; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 1c5de596762e2..3f7da1aa3725d 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "ui/platform/ui_platform_window.h" #include "platform/platform_window_title.h" #include "history/history.h" +#include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "window/window_lock_widgets.h" #include "window/window_controller.h" @@ -28,7 +29,6 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "main/main_session_settings.h" #include "base/options.h" -#include "base/call_delayed.h" #include "base/crc32hash.h" #include "ui/toast/toast.h" #include "ui/widgets/shadow.h" @@ -352,6 +352,20 @@ MainWindow::MainWindow(not_null controller) Ui::Toast::SetDefaultParent(_body.data()); } + windowActiveValue( + ) | rpl::skip(1) | rpl::start_with_next([=](bool active) { + InvokeQueued(this, [=] { + handleActiveChanged(active); + }); + }, lifetime()); + + shownValue( + ) | rpl::skip(1) | rpl::start_with_next([=](bool visible) { + InvokeQueued(this, [=] { + handleVisibleChanged(visible); + }); + }, lifetime()); + body()->sizeValue( ) | rpl::start_with_next([=](QSize size) { updateControlsGeometry(); @@ -374,8 +388,8 @@ Main::Account &MainWindow::account() const { return _controller->account(); } -PeerData *MainWindow::singlePeer() const { - return _controller->singlePeer(); +Window::SeparateId MainWindow::id() const { + return _controller->id(); } bool MainWindow::isPrimary() const { @@ -442,28 +456,8 @@ QRect MainWindow::desktopRect() const { } void MainWindow::init() { - createWinId(); - initHook(); - // Non-queued activeChanged handlers must use QtSignalProducer. - connect( - windowHandle(), - &QWindow::activeChanged, - this, - [=] { handleActiveChanged(); }, - Qt::QueuedConnection); - connect( - windowHandle(), - &QWindow::windowStateChanged, - this, - [=](Qt::WindowState state) { handleStateChanged(state); }); - connect( - windowHandle(), - &QWindow::visibleChanged, - this, - [=](bool visible) { handleVisibleChanged(visible); }); - updatePalette(); if (Ui::Platform::NativeWindowFrameSupported()) { @@ -496,9 +490,9 @@ void MainWindow::handleStateChanged(Qt::WindowState state) { savePosition(state); } -void MainWindow::handleActiveChanged() { +void MainWindow::handleActiveChanged(bool active) { checkActivation(); - if (isActiveWindow()) { + if (active) { Core::App().windowActivated(&controller()); } if (const auto controller = sessionController()) { @@ -609,20 +603,26 @@ WindowPosition MainWindow::initialPosition() const { ? Core::AdjustToScale( Core::App().settings().windowPosition(), u"Window"_q) - : active->widget()->nextInitialChildPosition(isPrimary()); + : active->widget()->nextInitialChildPosition(id()); } -WindowPosition MainWindow::nextInitialChildPosition(bool primary) { +WindowPosition MainWindow::nextInitialChildPosition(SeparateId childId) { const auto rect = geometry().marginsRemoved(frameMargins()); const auto position = rect.topLeft(); const auto adjust = [&](int value) { - return primary ? value : (value * 3 / 4); + return (value * 3 / 4); }; const auto width = OptionNewWindowsSizeAsFirst.value() ? Core::App().settings().windowPosition().w + : childId.primary() + ? st::windowDefaultWidth + : childId.hasChatsList() + ? (st::columnMinimalWidthLeft + adjust(st::windowDefaultWidth)) : adjust(st::windowDefaultWidth); const auto height = OptionNewWindowsSizeAsFirst.value() ? Core::App().settings().windowPosition().h + : childId.primary() + ? st::windowDefaultHeight : adjust(st::windowDefaultHeight); const auto skip = ChildSkip(); const auto delta = _lastChildIndex @@ -951,28 +951,6 @@ bool MainWindow::minimizeToTray() { return true; } -void MainWindow::reActivateWindow() { - // X11 is the only platform with unreliable activate requests - if (!Platform::IsX11()) { - return; - } - const auto weak = Ui::MakeWeak(this); - const auto reActivate = [=] { - if (const auto w = weak.data()) { - if (auto f = QApplication::focusWidget()) { - f->clearFocus(); - } - w->activate(); - if (auto f = QApplication::focusWidget()) { - f->clearFocus(); - } - w->setInnerFocus(); - } - }; - crl::on_main(this, reActivate); - base::call_delayed(200, this, reActivate); -} - void MainWindow::showRightColumn(object_ptr widget) { const auto wasWidth = width(); const auto wasRightWidth = _rightColumn ? _rightColumn->width() : 0; diff --git a/Telegram/SourceFiles/window/main_window.h b/Telegram/SourceFiles/window/main_window.h index 67f38567ca50b..85d495e7e3bfa 100644 --- a/Telegram/SourceFiles/window/main_window.h +++ b/Telegram/SourceFiles/window/main_window.h @@ -33,6 +33,7 @@ class Controller; class SessionController; class TitleWidget; struct TermsLock; +struct SeparateId; [[nodiscard]] const QImage &Logo(); [[nodiscard]] const QImage &LogoNoMargin(); @@ -66,7 +67,7 @@ class MainWindow : public Ui::RpWindow { [[nodiscard]] Window::Controller &controller() const { return *_controller; } - [[nodiscard]] PeerData *singlePeer() const; + [[nodiscard]] Window::SeparateId id() const; [[nodiscard]] bool isPrimary() const; [[nodiscard]] Main::Account &account() const; [[nodiscard]] Window::SessionController *sessionController() const; @@ -98,8 +99,6 @@ class MainWindow : public Ui::RpWindow { } void positionUpdated(); - void reActivateWindow(); - void showRightColumn(object_ptr widget); int maximalExtendBy() const; bool canExtendNoMove(int extendBy) const; @@ -153,7 +152,7 @@ class MainWindow : public Ui::RpWindow { void savePosition(Qt::WindowState state = Qt::WindowActive); void handleStateChanged(Qt::WindowState state); - void handleActiveChanged(); + void handleActiveChanged(bool active); void handleVisibleChanged(bool visible); virtual void checkActivation() { @@ -202,7 +201,7 @@ class MainWindow : public Ui::RpWindow { [[nodiscard]] Core::WindowPosition initialPosition() const; [[nodiscard]] Core::WindowPosition nextInitialChildPosition( - bool primary); + SeparateId childId); [[nodiscard]] QRect countInitialGeometry(Core::WindowPosition position); bool computeIsActive() const; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 052abca8fa232..40f8664f21094 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1076,7 +1076,6 @@ void Manager::notificationActivated( history->setLocalDraft(std::move(draft)); } window->widget()->showFromTray(); - window->widget()->reActivateWindow(); if (Core::App().passcodeLocked()) { window->widget()->setInnerFocus(); system()->clearAll(); @@ -1097,7 +1096,7 @@ void Manager::openNotificationMessage( && item->isRegular() && (item->out() || (item->mentionsMe() && !history->peer->isUser())); const auto topic = item ? item->topic() : nullptr; - const auto separate = Core::App().separateWindowForPeer(history->peer); + const auto separate = Core::App().separateWindowFor(history->peer); const auto window = separate ? separate->sessionController() : history->session().tryResolveWindow(); diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 2ce56b1133d6e..232d972ab512e 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -496,22 +496,11 @@ Widget::Widget( _a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::notifyFastAnim); } -void Widget::destroyDelayed() { - hide(); - if (_deleted) return; - _deleted = true; - - // Ubuntu has a lag if a fully transparent widget is destroyed immediately. - base::call_delayed(1000, this, [this] { - manager()->removeWidget(this); - }); -} - void Widget::opacityAnimationCallback() { updateOpacity(); update(); if (!_a_opacity.animating() && _hiding) { - destroyDelayed(); + manager()->removeWidget(this); } } @@ -1242,8 +1231,6 @@ HideAllButton::HideAllButton( auto position = computePosition(st::notifyHideAllHeight); updateGeometry(position.x(), position.y(), st::notifyWidth, st::notifyHideAllHeight); - hide(); - createWinId(); style::PaletteChanged( ) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index b8e5822468802..5bd4344d8db4a 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -171,7 +171,6 @@ class Widget : public Ui::RpWidget { private: void opacityAnimationCallback(); - void destroyDelayed(); void moveByShift(); void hideAnimated(float64 duration, const anim::transition &func); bool shiftAnimationCallback(crl::time now); @@ -179,7 +178,6 @@ class Widget : public Ui::RpWidget { const not_null _manager; bool _hiding = false; - bool _deleted = false; base::binary_guard _hidingDelayed; Ui::Animations::Simple _a_opacity; diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index ae24ad4d3e3a2..4ec94b0ab71d5 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "window/section_widget.h" #include "mainwidget.h" +#include "mainwindow.h" #include "ui/ui_utility.h" #include "ui/chat/chat_theme.h" #include "ui/painter.h" @@ -456,7 +457,7 @@ void SectionWidget::showFinished() { showChildren(); showFinishedHook(); - setInnerFocus(); + controller()->widget()->setInnerFocus(); } rpl::producer SectionWidget::desiredHeight() const { diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 450cfe541cc97..88bbe9cf2f490 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -1505,18 +1505,33 @@ bool ReadPaletteValues(const QByteArray &content, Fnc.getRgb(&r, &g, &b); + color.getRgb(&r, &g, &b); const auto hex = [](int component) { const auto digit = [](int c) { return QChar((c < 10) ? ('0' + c) : ('a' + c - 10)); }; return QString() + digit(component / 16) + digit(component % 16); }; - object.insert(name, '#' + hex(r) + hex(g) + hex(b)); + return '#' + hex(r) + hex(g) + hex(b); + }; + for (const auto &[name, color] : colors) { + object.insert(name, wrap(color->c)); + } + { + const auto bg = st::windowBg->c; + const auto shadow = st::shadowFg->c; + const auto shadowAlpha = shadow.alphaF(); + const auto mix = [&](int a, int b) { + return anim::interpolate(a, b, shadowAlpha); + }; + object.insert("section_separator_color", wrap(QColor( + mix(bg.red(), shadow.red()), + mix(bg.green(), shadow.green()), + mix(bg.blue(), shadow.blue())))); } return { .opaqueBg = st::windowBg->c, diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index e8dfecfda3203..376ac68cbc64a 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -601,7 +601,7 @@ void Generator::paintComposeArea() { _p->setClipRect(field); _p->save(); - _p->setFont(st::historyComposeField.font); + _p->setFont(st::historyComposeField.style.font); _p->setPen(st::historyComposeField.placeholderFg[_palette]); auto placeholderRect = QRect( @@ -655,7 +655,7 @@ void Generator::paintDialogs() { _p->save(); _p->setClipRect(filter); auto phRect = QRect(filter.x() + st::dialogsFilter.textMargins.left() + st::dialogsFilter.placeholderMargins.left(), filter.y() + st::dialogsFilter.textMargins.top() + st::dialogsFilter.placeholderMargins.top(), filter.width() - st::dialogsFilter.textMargins.left() - st::dialogsFilter.textMargins.right(), filter.height() - st::dialogsFilter.textMargins.top() - st::dialogsFilter.textMargins.bottom()); - _p->setFont(st::dialogsFilter.font); + _p->setFont(st::dialogsFilter.style.font); _p->setPen(st::dialogsFilter.placeholderFg[_palette]); _p->drawText(phRect, tr::lng_dlg_filter(tr::now), QTextOption(st::dialogsFilter.placeholderAlign)); _p->restore(); diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 8b13c0a3691f5..37654c990738f 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -61,7 +61,7 @@ notifyActionsDuration: 200; notifyHideAllHeight: 36px; notifyReplyArea: InputField(defaultInputField) { - font: normalFont; + style: defaultTextStyle; textMargins: margins(8px, 8px, 8px, 6px); heightMin: 36px; heightMax: 72px; @@ -235,8 +235,6 @@ createThemeLink: InputField(defaultInputField) { placeholderFont: boxTextFont; heightMin: 34px; - - font: boxTextFont; } windowFiltersWidth: 72px; diff --git a/Telegram/SourceFiles/window/window_connecting_widget.cpp b/Telegram/SourceFiles/window/window_connecting_widget.cpp index a7b1f3a1f6d61..9940287fac0ee 100644 --- a/Telegram/SourceFiles/window/window_connecting_widget.cpp +++ b/Telegram/SourceFiles/window/window_connecting_widget.cpp @@ -7,7 +7,6 @@ For license and copyright information please follow this link: */ #include "window/window_connecting_widget.h" -#include "base/event_filter.h" #include "ui/widgets/buttons.h" #include "ui/effects/radial_animation.h" #include "ui/painter.h" @@ -37,6 +36,8 @@ class Progress : public Ui::RpWidget { public: Progress(QWidget *parent); + rpl::producer<> animationStepRequests() const; + protected: void paintEvent(QPaintEvent *e) override; @@ -44,6 +45,7 @@ class Progress : public Ui::RpWidget { void animationStep(); Ui::InfiniteRadialAnimation _animation; + rpl::event_stream<> _animationStepRequests; }; @@ -71,10 +73,15 @@ void Progress::paintEvent(QPaintEvent *e) { void Progress::animationStep() { if (!anim::Disabled()) { + _animationStepRequests.fire({}); update(); } } +rpl::producer<> Progress::animationStepRequests() const { + return _animationStepRequests.events(); +} + } // namespace class ConnectionState::Widget : public Ui::AbstractButton { @@ -109,7 +116,7 @@ class ConnectionState::Widget : public Ui::AbstractButton { const not_null _account; Layout _currentLayout; base::unique_qptr _retry; - QPointer _progress; + QPointer _progress; QPointer _proxyIcon; rpl::event_stream<> _refreshStateRequests; @@ -210,14 +217,6 @@ ConnectionState::ConnectionState( rpl::producer shown) : _account(account) , _parent(parent) -, _exposeFilter(base::install_event_filter( - parent->window()->windowHandle(), - [=](not_null e) { - if (e->type() == QEvent::Expose) { - refreshState(); - } - return base::EventFilterResult::Continue; - })) , _refreshTimer([=] { refreshState(); }) , _currentLayout(computeLayout(_state)) { rpl::combine( @@ -241,7 +240,9 @@ ConnectionState::ConnectionState( }, _lifetime); } - Core::App().settings().proxy().connectionTypeValue( + rpl::combine( + Core::App().settings().proxy().connectionTypeValue(), + rpl::single(QRect()) | rpl::then(_parent->paintRequest()) ) | rpl::start_with_next([=] { refreshState(); }, _lifetime); @@ -301,7 +302,8 @@ void ConnectionState::setBottomSkip(int skip) { void ConnectionState::refreshState() { using Checker = Core::UpdateChecker; const auto state = [&]() -> State { - const auto exposed = _parent->window()->windowHandle()->isExposed(); + const auto exposed = _parent->window()->windowHandle() + && _parent->window()->windowHandle()->isExposed(); const auto under = _widget && _widget->isOver(); const auto ready = (Checker().state() == Checker::State::Ready); const auto state = _account->mtp().dcstate(); @@ -501,6 +503,11 @@ ConnectionState::Widget::Widget( addClickHandler([=] { Ui::show(ProxiesBoxController::CreateOwningBox(account)); }); + + _progress->animationStepRequests( + ) | rpl::start_with_next([=] { + _refreshStateRequests.fire({}); + }, _progress->lifetime()); } void ConnectionState::Widget::onStateChanged( diff --git a/Telegram/SourceFiles/window/window_connecting_widget.h b/Telegram/SourceFiles/window/window_connecting_widget.h index 7813ce5ad18d9..891a91072e816 100644 --- a/Telegram/SourceFiles/window/window_connecting_widget.h +++ b/Telegram/SourceFiles/window/window_connecting_widget.h @@ -80,7 +80,6 @@ class ConnectionState { const not_null _account; not_null _parent; - base::unique_qptr _exposeFilter; rpl::variable _bottomSkip; base::unique_qptr _widget; bool _forceHidden = false; diff --git a/Telegram/SourceFiles/window/window_controller.cpp b/Telegram/SourceFiles/window/window_controller.cpp index 19c12c30e9255..8ef614d218a4f 100644 --- a/Telegram/SourceFiles/window/window_controller.cpp +++ b/Telegram/SourceFiles/window/window_controller.cpp @@ -28,7 +28,7 @@ For license and copyright information please follow this link: #include "window/window_session_controller.h" #include "window/themes/window_theme_editor.h" #include "ui/boxes/confirm_box.h" -#include "data/data_peer.h" +#include "data/data_thread.h" #include "apiwrap.h" // ApiWrap::acceptTerms. #include "styles/style_layers.h" @@ -93,23 +93,18 @@ Show::operator bool() const { } // namespace -Controller::Controller() : Controller(CreateArgs{}) { +Controller::Controller() : Controller(CreateArgs{ nullptr }) { } -Controller::Controller(not_null account) -: Controller(CreateArgs{}) { - showAccount(account); -} - -Controller::Controller( - not_null singlePeer, - MsgId showAtMsgId) -: Controller(CreateArgs{ singlePeer.get() }) { - showAccount(&singlePeer->account(), showAtMsgId); +Controller::Controller(SeparateId id, MsgId showAtMsgId) +: Controller(CreateArgs{ id }) { + if (id) { + showAccount(id.account, showAtMsgId); + } } Controller::Controller(CreateArgs &&args) -: _singlePeer(args.singlePeer) +: _id(args.id) , _isActiveTimer([=] { updateIsActive(); }) , _widget(this) , _adaptive(std::make_unique()) { @@ -125,6 +120,20 @@ Controller::~Controller() { _sessionController = nullptr; } +SeparateId Controller::id() const { + return _id; +} + +bool Controller::isPrimary() const { + return _id.primary(); +} + +Main::Account &Controller::account() const { + Expects(_id.account != nullptr); + + return *_id.account; +} + void Controller::showAccount(not_null account) { showAccount(account, ShowAtUnreadMsgId); } @@ -132,20 +141,21 @@ void Controller::showAccount(not_null account) { void Controller::showAccount( not_null account, MsgId singlePeerShowAtMsgId) { - Expects(isPrimary() || &_singlePeer->account() == account); + Expects(isPrimary() || _id.account == account); - const auto prevSessionUniqueId = (_account && _account->sessionExists()) - ? _account->session().uniqueId() + const auto prevSession = maybeSession(); + const auto prevSessionUniqueId = prevSession + ? prevSession->uniqueId() : 0; _accountLifetime.destroy(); - _account = account; - Core::App().checkWindowAccount(this); + _id.account = account; + Core::App().checkWindowId(this); - const auto updateOnlineOfPrevSesssion = crl::guard(_account, [=] { + const auto updateOnlineOfPrevSesssion = crl::guard(account, [=] { if (!prevSessionUniqueId) { return; } - for (auto &[index, account] : _account->domain().accounts()) { + for (auto &[index, account] : _id.account->domain().accounts()) { if (const auto anotherSession = account->maybeSession()) { if (anotherSession->uniqueId() == prevSessionUniqueId) { anotherSession->updates().updateOnline(crl::now()); @@ -155,12 +165,15 @@ void Controller::showAccount( } }); - _account->sessionValue( - ) | rpl::start_with_next([=](Main::Session *session) { - if (!isPrimary() && (&_singlePeer->session() != session)) { + if (!isPrimary()) { + _id.account->sessionChanges( + ) | rpl::start_with_next([=](Main::Session *session) { Core::App().closeWindow(this); - return; - } + }, _accountLifetime); + } + + _id.account->sessionValue( + ) | rpl::start_with_next([=](Main::Session *session) { const auto was = base::take(_sessionController); _sessionController = session ? std::make_unique(session, this) @@ -205,10 +218,6 @@ void Controller::showAccount( }, _accountLifetime); } -PeerData *Controller::singlePeer() const { - return _singlePeer; -} - void Controller::setupSideBar() { Expects(_sessionController != nullptr); @@ -321,7 +330,7 @@ void Controller::finishFirstShow() { } Main::Session *Controller::maybeSession() const { - return _account ? _account->maybeSession() : nullptr; + return _id.account ? _id.account->maybeSession() : nullptr; } auto Controller::sessionControllerValue() const @@ -356,7 +365,7 @@ void Controller::setupPasscodeLock() { } void Controller::clearPasscodeLock() { - if (!_account) { + if (!_id) { showAccount(&Core::App().activeAccount()); } else { _widget.clearPasscodeLock(); @@ -444,10 +453,6 @@ void Controller::activate() { _widget.activate(); } -void Controller::reActivate() { - _widget.reActivateWindow(); -} - void Controller::updateIsActiveFocus() { _isActiveTimer.callOnce(sessionController() ? sessionController()->session().serverConfig().onlineFocusTimeout @@ -486,7 +491,7 @@ void Controller::invokeForSessionController( PeerData *singlePeer, Fn)> &&callback) { const auto separateWindow = singlePeer - ? Core::App().separateWindowForPeer(singlePeer) + ? Core::App().separateWindowFor(not_null(singlePeer)) : nullptr; const auto separateSession = separateWindow ? separateWindow->sessionController() @@ -494,7 +499,7 @@ void Controller::invokeForSessionController( if (separateSession) { return callback(separateSession); } - _account->domain().activate(std::move(account)); + _id.account->domain().activate(std::move(account)); if (_sessionController) { callback(_sessionController.get()); } diff --git a/Telegram/SourceFiles/window/window_controller.h b/Telegram/SourceFiles/window/window_controller.h index cd59790065ba5..3eb6184e9d733 100644 --- a/Telegram/SourceFiles/window/window_controller.h +++ b/Telegram/SourceFiles/window/window_controller.h @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "mainwindow.h" #include "window/window_adaptive.h" +#include "window/window_separate_id.h" namespace Main { class Account; @@ -36,32 +37,20 @@ namespace Window { class Controller final : public base::has_weak_ptr { public: Controller(); - explicit Controller(not_null account); - Controller( - not_null singlePeer, - MsgId showAtMsgId); + Controller(SeparateId id, MsgId showAtMsgId); ~Controller(); Controller(const Controller &other) = delete; Controller &operator=(const Controller &other) = delete; void showAccount(not_null account); - [[nodiscard]] PeerData *singlePeer() const; - [[nodiscard]] bool isPrimary() const { - return (singlePeer() == nullptr); - } + [[nodiscard]] SeparateId id() const; + [[nodiscard]] bool isPrimary() const; [[nodiscard]] not_null<::MainWindow*> widget() { return &_widget; } - [[nodiscard]] Main::Account &account() const { - Expects(_account != nullptr); - - return *_account; - } - [[nodiscard]] Main::Account *maybeAccount() const { - return _account; - } + [[nodiscard]] Main::Account &account() const; [[nodiscard]] Main::Session *maybeSession() const; [[nodiscard]] SessionController *sessionController() const { return _sessionController.get(); @@ -119,7 +108,6 @@ class Controller final : public base::has_weak_ptr { } void activate(); - void reActivate(); void updateIsActiveFocus(); void updateIsActiveBlur(); void updateIsActive(); @@ -156,7 +144,7 @@ class Controller final : public base::has_weak_ptr { private: struct CreateArgs { - PeerData *singlePeer = nullptr; + SeparateId id; }; explicit Controller(CreateArgs &&args); @@ -174,8 +162,7 @@ class Controller final : public base::has_weak_ptr { void showTermsDecline(); void showTermsDelete(); - PeerData *_singlePeer = nullptr; - Main::Account *_account = nullptr; + SeparateId _id; base::Timer _isActiveTimer; ::MainWindow _widget; const std::unique_ptr _adaptive; diff --git a/Telegram/SourceFiles/window/window_lock_widgets.cpp b/Telegram/SourceFiles/window/window_lock_widgets.cpp index 58f4881fae3db..02350886e1dff 100644 --- a/Telegram/SourceFiles/window/window_lock_widgets.cpp +++ b/Telegram/SourceFiles/window/window_lock_widgets.cpp @@ -7,6 +7,9 @@ For license and copyright information please follow this link: */ #include "window/window_lock_widgets.h" +#include "base/platform/base_platform_info.h" +#include "base/call_delayed.h" +#include "base/system_unlock.h" #include "lang/lang_keys.h" #include "storage/storage_domain.h" #include "mainwindow.h" @@ -27,6 +30,11 @@ For license and copyright information please follow this link: #include "styles/style_boxes.h" namespace Window { +namespace { + +constexpr auto kSystemUnlockDelay = crl::time(1000); + +} // namespace LockWidget::LockWidget(QWidget *parent, not_null window) : RpWidget(parent) @@ -99,6 +107,140 @@ PasscodeLockWidget::PasscodeLockWidget( _logout->setClickedCallback([=] { window->showLogoutConfirmation(); }); + + using namespace rpl::mappers; + if (Core::App().settings().systemUnlockEnabled()) { + _systemUnlockAvailable = base::SystemUnlockStatus( + true + ) | rpl::map([](base::SystemUnlockAvailability status) { + return status.withBiometrics + ? SystemUnlockType::Biometrics + : status.withCompanion + ? SystemUnlockType::Companion + : status.available + ? SystemUnlockType::Default + : SystemUnlockType::None; + }); + if (Core::App().domain().started()) { + _systemUnlockAllowed = _systemUnlockAvailable.value(); + setupSystemUnlock(); + } else { + setupSystemUnlockInfo(); + } + } +} + +void PasscodeLockWidget::setupSystemUnlockInfo() { + const auto macos = [&] { + return _systemUnlockAvailable.value( + ) | rpl::map([](SystemUnlockType type) { + return (type == SystemUnlockType::Biometrics) + ? tr::lng_passcode_touchid() + : (type == SystemUnlockType::Companion) + ? tr::lng_passcode_applewatch() + : tr::lng_passcode_systempwd(); + }) | rpl::flatten_latest(); + }; + auto text = Platform::IsWindows() + ? tr::lng_passcode_winhello() + : macos(); + const auto info = Ui::CreateChild( + this, + std::move(text), + st::passcodeSystemUnlockLater); + _logout->geometryValue( + ) | rpl::start_with_next([=](QRect logout) { + info->resizeToWidth(width() + - st::boxRowPadding.left() + - st::boxRowPadding.right()); + info->moveToLeft( + st::boxRowPadding.left(), + logout.y() + logout.height() + st::passcodeSystemUnlockSkip); + }, info->lifetime()); + info->showOn(_systemUnlockAvailable.value( + ) | rpl::map(rpl::mappers::_1 != SystemUnlockType::None)); +} + +void PasscodeLockWidget::setupSystemUnlock() { + windowActiveValue() | rpl::skip(1) | rpl::filter([=](bool active) { + return active + && !_systemUnlockSuggested + && !_systemUnlockCooldown.isActive(); + }) | rpl::start_with_next([=](bool) { + [[maybe_unused]] auto refresh = base::SystemUnlockStatus(); + suggestSystemUnlock(); + }, lifetime()); + + const auto button = Ui::CreateChild( + _passcode.data(), + st::passcodeSystemUnlock); + if (!Platform::IsWindows()) { + using namespace base; + _systemUnlockAllowed.value( + ) | rpl::start_with_next([=](SystemUnlockType type) { + const auto icon = (type == SystemUnlockType::Biometrics) + ? &st::passcodeSystemTouchID + : (type == SystemUnlockType::Companion) + ? &st::passcodeSystemAppleWatch + : &st::passcodeSystemSystemPwd; + button->setIconOverride(icon, icon); + }, button->lifetime()); + } + button->showOn(_systemUnlockAllowed.value( + ) | rpl::map(rpl::mappers::_1 != SystemUnlockType::None)); + _passcode->sizeValue() | rpl::start_with_next([=](QSize size) { + button->moveToRight(0, size.height() - button->height()); + }, button->lifetime()); + button->setClickedCallback([=] { + const auto delay = st::passcodeSystemUnlock.ripple.hideDuration; + base::call_delayed(delay, this, [=] { + suggestSystemUnlock(); + }); + }); +} + +void PasscodeLockWidget::suggestSystemUnlock() { + InvokeQueued(this, [=] { + if (_systemUnlockSuggested) { + return; + } + _systemUnlockCooldown.cancel(); + + using namespace base; + _systemUnlockAllowed.value( + ) | rpl::filter( + rpl::mappers::_1 != SystemUnlockType::None + ) | rpl::take(1) | rpl::start_with_next([=] { + const auto weak = Ui::MakeWeak(this); + const auto done = [weak](SystemUnlockResult result) { + crl::on_main([=] { + if (const auto strong = weak.data()) { + strong->systemUnlockDone(result); + } + }); + }; + SuggestSystemUnlock( + this, + (::Platform::IsWindows() + ? tr::lng_passcode_winhello_unlock(tr::now) + : tr::lng_passcode_touchid_unlock(tr::now)), + done); + }, _systemUnlockSuggested); + }); +} + +void PasscodeLockWidget::systemUnlockDone(base::SystemUnlockResult result) { + if (result == base::SystemUnlockResult::Success) { + Core::App().unlockPasscode(); + return; + } + _systemUnlockCooldown.callOnce(kSystemUnlockDelay); + _systemUnlockSuggested.destroy(); + if (result == base::SystemUnlockResult::FloodError) { + _error = tr::lng_flood_error(tr::now); + _passcode->setFocusFast(); + update(); + } } void PasscodeLockWidget::paintContent(QPainter &p) { diff --git a/Telegram/SourceFiles/window/window_lock_widgets.h b/Telegram/SourceFiles/window/window_lock_widgets.h index d2967d47fff0f..03c6b59b3bbfe 100644 --- a/Telegram/SourceFiles/window/window_lock_widgets.h +++ b/Telegram/SourceFiles/window/window_lock_widgets.h @@ -11,6 +11,11 @@ For license and copyright information please follow this link: #include "ui/effects/animations.h" #include "ui/layers/box_content.h" #include "base/bytes.h" +#include "base/timer.h" + +namespace base { +enum class SystemUnlockResult; +} // namespace base namespace Ui { class PasswordInput; @@ -60,16 +65,33 @@ class PasscodeLockWidget : public LockWidget { void resizeEvent(QResizeEvent *e) override; private: + enum class SystemUnlockType : uchar { + None, + Default, + Biometrics, + Companion, + }; + void paintContent(QPainter &p) override; + + void setupSystemUnlockInfo(); + void setupSystemUnlock(); + void suggestSystemUnlock(); + void systemUnlockDone(base::SystemUnlockResult result); void changed(); void submit(); void error(); + rpl::variable _systemUnlockAvailable; + rpl::variable _systemUnlockAllowed; object_ptr _passcode; object_ptr _submit; object_ptr _logout; QString _error; + rpl::lifetime _systemUnlockSuggested; + base::Timer _systemUnlockCooldown; + }; struct TermsLock { diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 9fc13700c1474..1d7077a426ba9 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -47,6 +47,7 @@ For license and copyright information please follow this link: #include "ui/text/text_utilities.h" #include "ui/unread_badge_paint.h" #include "ui/vertical_list.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" @@ -551,9 +552,15 @@ void MainMenu::setupArchive() { const auto folder = [=] { return controller->session().data().folderLoaded(Data::Folder::kId); }; - const auto showArchive = [=] { + const auto showArchive = [=](Qt::KeyboardModifiers modifiers) { if (const auto f = folder()) { - controller->openFolder(f); + if (modifiers & Qt::ControlModifier) { + controller->showInNewWindow(Window::SeparateId( + Window::SeparateType::Archive, + &controller->session())); + } else { + controller->openFolder(f); + } controller->window().hideSettingsAndLayer(); } }; @@ -583,7 +590,7 @@ void MainMenu::setupArchive() { button->clicks( ) | rpl::start_with_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { - showArchive(); + showArchive(button->clickModifiers()); return; } else if (which != Qt::RightButton) { return; @@ -591,35 +598,13 @@ void MainMenu::setupArchive() { _contextMenu = base::make_unique_q( this, st::popupMenuExpandedSeparator); - const auto addAction = PeerMenuCallback([&]( - PeerMenuCallback::Args a) { - return _contextMenu->addAction( - a.text, - std::move(a.handler), - a.icon); - }); - - const auto hide = [=] { - controller->session().settings().setArchiveInMainMenu(false); - controller->session().saveSettingsDelayed(); - controller->window().hideSettingsAndLayer(); - }; - addAction( - tr::lng_context_archive_to_list(tr::now), - std::move(hide), - &st::menuIconFromMainMenu); - - MenuAddMarkAsReadChatListAction( - controller, - [f = folder()] { return f->chatsList(); }, - addAction); - - _contextMenu->addSeparator(); - Settings::PreloadArchiveSettings(&controller->session()); - addAction(tr::lng_context_archive_settings(tr::now), [=] { - controller->show(Box(Settings::ArchiveSettingsBox, controller)); - }, &st::menuIconManage); - + Window::FillDialogsEntryMenu( + _controller, + Dialogs::EntryState{ + .key = folder(), + .section = Dialogs::EntryState::Section::ContextMenu, + }, + Ui::Menu::CreateAddActionCallback(_contextMenu)); _contextMenu->popup(QCursor::pos()); }, button->lifetime()); @@ -957,21 +942,15 @@ void MainMenu::drawName(Painter &p) { } void MainMenu::initResetScaleButton() { - if (!window() || !window()->windowHandle()) { - return; - } - const auto handle = window()->windowHandle(); - rpl::single( - handle->screen() - ) | rpl::then( - base::qt_signal_producer(handle, &QWindow::screenChanged) - ) | rpl::filter([](QScreen *screen) { - return screen != nullptr; - }) | rpl::map([](QScreen * screen) { + _controller->widget()->screenValue( + ) | rpl::map([](not_null screen) { return rpl::single( screen->availableGeometry() ) | rpl::then( - base::qt_signal_producer(screen, &QScreen::availableGeometryChanged) + base::qt_signal_producer( + screen.get(), + &QScreen::availableGeometryChanged + ) ); }) | rpl::flatten_latest( ) | rpl::map([](QRect available) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index d3aecb1c5e74b..74f6f7ed864ac 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -59,6 +59,7 @@ For license and copyright information please follow this link: #include "history/history.h" #include "history/history_item_helpers.h" // GetErrorTextForSending. #include "history/view/history_view_context_menu.h" +#include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "settings/settings_advanced.h" @@ -66,7 +67,7 @@ For license and copyright information please follow this link: #include "info/info_controller.h" #include "info/info_memento.h" #include "info/channel_statistics/boosts/info_boosts_widget.h" -#include "info/channel_statistics/earn/info_earn_widget.h" +#include "info/channel_statistics/earn/info_channel_earn_widget.h" #include "info/profile/info_profile_values.h" #include "info/statistics/info_statistics_widget.h" #include "info/stories/info_stories_widget.h" @@ -650,20 +651,49 @@ void Filler::addToggleUnreadMark() { } void Filler::addNewWindow() { + const auto controller = _controller; + if (_folder) { + _addAction(tr::lng_context_new_window(tr::now), [=] { + Ui::PreventDelayedActivation(); + controller->showInNewWindow(SeparateId( + SeparateType::Archive, + &controller->session())); + }, &st::menuIconNewWindow); + AddSeparatorAndShiftUp(_addAction); + return; + } else if (const auto weak = base::make_weak(_sublist)) { + _addAction(tr::lng_context_new_window(tr::now), [=] { + Ui::PreventDelayedActivation(); + if (const auto sublist = weak.get()) { + const auto peer = sublist->peer(); + controller->showInNewWindow(SeparateId( + SeparateType::SavedSublist, + peer->owner().history(peer))); + } + }, &st::menuIconNewWindow); + AddSeparatorAndShiftUp(_addAction); + return; + } const auto history = _request.key.history(); if (!_peer - || _topic - || _peer->isForum() || (history && history->useTopPromotion() && !history->topPromotionType().isEmpty())) { return; } const auto peer = _peer; - const auto controller = _controller; + const auto thread = _topic + ? not_null(_topic) + : _peer->owner().history(_peer); + const auto weak = base::make_weak(thread); _addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); - controller->showInNewWindow(peer); + if (const auto strong = weak.get()) { + const auto forum = !strong->asTopic() && peer->isForum(); + controller->showInNewWindow(SeparateId( + forum ? SeparateType::Forum : SeparateType::Chat, + strong)); + } }, &st::menuIconNewWindow); AddSeparatorAndShiftUp(_addAction); } @@ -1060,6 +1090,8 @@ void Filler::addViewStatistics() { using Flag = ChannelDataFlag; const auto canGetStats = (channel->flags() & Flag::CanGetStatistics); const auto canViewEarn = (channel->flags() & Flag::CanViewRevenue); + const auto canViewCreditsEarn + = (channel->flags() & Flag::CanViewCreditsRevenue); if (canGetStats) { _addAction(tr::lng_stats_title(tr::now), [=] { if (const auto strong = weak.get()) { @@ -1077,7 +1109,7 @@ void Filler::addViewStatistics() { } }, &st::menuIconBoosts); } - if (canViewEarn) { + if (canViewEarn || canViewCreditsEarn) { _addAction(tr::lng_channel_earn_title(tr::now), [=] { if (const auto strong = weak.get()) { controller->showSection(Info::ChannelEarn::Make(peer)); @@ -1253,6 +1285,12 @@ void Filler::addViewAsMessages() { FullMsgId(), }, callback, QApplication::activePopupWidget()); return true; + } else if (base::IsCtrlPressed()) { + Ui::PreventDelayedActivation(); + controller->showInNewWindow(SeparateId( + SeparateType::Chat, + peer->owner().history(peer))); + return true; } return false; }; @@ -1432,27 +1470,39 @@ void Filler::fillArchiveActions() { if (_folder->id() != Data::Folder::kId) { return; } + addNewWindow(); + const auto controller = _controller; const auto hidden = controller->session().settings().archiveCollapsed(); - const auto text = hidden - ? tr::lng_context_archive_expand(tr::now) - : tr::lng_context_archive_collapse(tr::now); - _addAction(text, [=] { - controller->session().settings().setArchiveCollapsed(!hidden); - controller->session().saveSettingsDelayed(); - }, hidden ? &st::menuIconExpand : &st::menuIconCollapse); - - _addAction(tr::lng_context_archive_to_menu(tr::now), [=] { - controller->showToast({ - .text = { tr::lng_context_archive_to_menu_info(tr::now) }, - .st = &st::windowArchiveToast, - .duration = kArchivedToastDuration, - }); - - controller->session().settings().setArchiveInMainMenu( - !controller->session().settings().archiveInMainMenu()); - controller->session().saveSettingsDelayed(); - }, &st::menuIconToMainMenu); + const auto inmenu = controller->session().settings().archiveInMainMenu(); + if (!inmenu) { + const auto text = hidden + ? tr::lng_context_archive_expand(tr::now) + : tr::lng_context_archive_collapse(tr::now); + _addAction(text, [=] { + controller->session().settings().setArchiveCollapsed(!hidden); + controller->session().saveSettingsDelayed(); + }, hidden ? &st::menuIconExpand : &st::menuIconCollapse); + } + { + const auto text = inmenu + ? tr::lng_context_archive_to_list(tr::now) + : tr::lng_context_archive_to_menu(tr::now); + _addAction(text, [=] { + if (!inmenu) { + controller->showToast({ + .text = { + tr::lng_context_archive_to_menu_info(tr::now) + }, + .st = &st::windowArchiveToast, + .duration = kArchivedToastDuration, + }); + } + controller->session().settings().setArchiveInMainMenu(!inmenu); + controller->session().saveSettingsDelayed(); + controller->window().hideSettingsAndLayer(); + }, inmenu ? &st::menuIconFromMainMenu : &st::menuIconToMainMenu); + } MenuAddMarkAsReadChatListAction( controller, @@ -1460,6 +1510,7 @@ void Filler::fillArchiveActions() { _addAction); _addAction({ .isSeparator = true }); + Settings::PreloadArchiveSettings(&controller->session()); _addAction(tr::lng_context_archive_settings(tr::now), [=] { controller->show(Box(Settings::ArchiveSettingsBox, controller)); @@ -1467,6 +1518,7 @@ void Filler::fillArchiveActions() { } void Filler::fillSavedSublistActions() { + addNewWindow(); addTogglePin(); } @@ -1983,17 +2035,17 @@ QPointer ShowForwardMessagesBox( ForwardToSelf(show, draft); return true; } - auto controller = Core::App().windowFor(peer); + const auto id = SeparateId( + (peer->isForum() + ? SeparateType::Forum + : SeparateType::Chat), + thread); + auto controller = Core::App().windowFor(id); if (!controller) { return false; } if (controller->maybeSession() != &peer->session()) { - controller = peer->isForum() - ? Core::App().ensureSeparateWindowForAccount( - &peer->account()) - : Core::App().ensureSeparateWindowForPeer( - peer, - ShowAtUnreadMsgId); + controller = Core::App().ensureSeparateWindowFor(id); if (controller->maybeSession() != &peer->session()) { return false; } diff --git a/Telegram/SourceFiles/window/window_separate_id.cpp b/Telegram/SourceFiles/window/window_separate_id.cpp new file mode 100644 index 0000000000000..f2ed89183915b --- /dev/null +++ b/Telegram/SourceFiles/window/window_separate_id.cpp @@ -0,0 +1,77 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "window/window_separate_id.h" + +#include "data/data_folder.h" +#include "data/data_peer.h" +#include "data/data_saved_messages.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "history/history.h" +#include "main/main_account.h" +#include "main/main_session.h" + +namespace Window { + +SeparateId::SeparateId(std::nullptr_t) { +} + +SeparateId::SeparateId(not_null account) +: account(account) { +} + +SeparateId::SeparateId(SeparateType type, not_null session) +: type(type) +, account(&session->account()) { +} + +SeparateId::SeparateId(SeparateType type, not_null thread) +: type(type) +, account(&thread->session().account()) +, thread(thread) { +} + +SeparateId::SeparateId(not_null thread) +: SeparateId(SeparateType::Chat, thread) { +} + +SeparateId::SeparateId(not_null peer) +: SeparateId(SeparateType::Chat, peer->owner().history(peer)) { +} + +bool SeparateId::primary() const { + return (type == SeparateType::Primary); +} + +Data::Thread *SeparateId::chat() const { + return (type == SeparateType::Chat) ? thread : nullptr; +} + +Data::Forum *SeparateId::forum() const { + return (type == SeparateType::Forum) ? thread->asForum() : nullptr; +} + +Data::Folder *SeparateId::folder() const { + return (type == SeparateType::Archive) + ? account->session().data().folder(Data::Folder::kId).get() + : nullptr; +} + +Data::SavedSublist *SeparateId::sublist() const { + return (type == SeparateType::SavedSublist) + ? thread->owner().savedMessages().sublist(thread->peer()).get() + : nullptr; +} + +bool SeparateId::hasChatsList() const { + return (type == SeparateType::Primary) + || (type == SeparateType::Archive) + || (type == SeparateType::Forum); +} + +} // namespace Window diff --git a/Telegram/SourceFiles/window/window_separate_id.h b/Telegram/SourceFiles/window/window_separate_id.h new file mode 100644 index 0000000000000..bcbe728604339 --- /dev/null +++ b/Telegram/SourceFiles/window/window_separate_id.h @@ -0,0 +1,69 @@ +/* +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 + +class PeerData; + +namespace Data { +class Thread; +class Folder; +class Forum; +class SavedSublist; +} // namespace Data + +namespace Main { +class Account; +class Session; +} // namespace Main + +namespace Window { + +enum class SeparateType { + Primary, + Archive, + Chat, + Forum, + SavedSublist, +}; + +struct SeparateId { + SeparateId(std::nullptr_t); + SeparateId(not_null account); + SeparateId(SeparateType type, not_null session); + SeparateId(SeparateType type, not_null thread); + SeparateId(not_null thread); + SeparateId(not_null peer); + + SeparateType type = SeparateType::Primary; + Main::Account *account = nullptr; + Data::Thread *thread = nullptr; // For types except Main and Archive. + + [[nodiscard]] bool valid() const { + return account != nullptr; + } + explicit operator bool() const { + return valid(); + } + + [[nodiscard]] bool primary() const; + [[nodiscard]] Data::Thread *chat() const; + [[nodiscard]] Data::Forum *forum() const; + [[nodiscard]] Data::Folder *folder() const; + [[nodiscard]] Data::SavedSublist *sublist() const; + + [[nodiscard]] bool hasChatsList() const; + + friend inline auto operator<=>( + const SeparateId &, + const SeparateId &) = default; + friend inline bool operator==( + const SeparateId &, + const SeparateId &) = default; +}; + +} // namespace Window diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 59d12ab358f82..8380e458a1039 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -16,7 +16,8 @@ For license and copyright information please follow this link: #include "window/window_chat_preview.h" #include "window/window_controller.h" #include "window/window_filters_menu.h" -#include "info/channel_statistics/earn/info_earn_inner_widget.h" +#include "window/window_separate_id.h" +#include "info/channel_statistics/earn/info_channel_earn_list.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "inline_bots/bot_attach_web_view.h" @@ -26,11 +27,13 @@ For license and copyright information please follow this link: //#include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_replies_section.h" #include "history/view/history_view_scheduled_section.h" +#include "history/view/history_view_sublist_section.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document_resolver.h" #include "data/data_download_manager.h" +#include "data/data_saved_messages.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "data/data_folder.h" @@ -1100,6 +1103,36 @@ void SessionNavigation::showPeerHistory( showPeerHistory(history->peer->id, params, msgId); } +void SessionNavigation::showByInitialId( + const SectionShow ¶ms, + MsgId msgId) { + const auto parent = parentController(); + const auto id = parent->window().id(); + auto instant = params; + instant.animated = anim::type::instant; + switch (id.type) { + case SeparateType::Archive: + clearSectionStack(instant); + parent->openFolder(id.folder()); + break; + case SeparateType::Forum: + clearSectionStack(instant); + parent->showForum(id.forum(), instant); + break; + case SeparateType::Primary: + clearSectionStack(instant); + break; + case SeparateType::Chat: + showThread(id.thread, msgId, instant); + break; + case SeparateType::SavedSublist: + showSection( + std::make_shared(id.sublist()), + instant); + break; + } +} + void SessionNavigation::showSettings( Settings::Type type, const SectionShow ¶ms) { @@ -1182,6 +1215,7 @@ SessionController::SessionController( std::make_unique(session)) , _chatPreviewManager(std::make_unique(this)) , _isPrimary(window->isPrimary()) +, _hasDialogs(window->id().hasChatsList()) , _sendingAnimation( std::make_unique(this)) , _tabbedSelector( @@ -1191,6 +1225,7 @@ SessionController::SessionController( GifPauseReason::TabbedPanel)) , _invitePeekTimer([=] { checkInvitePeek(); }) , _activeChatsFilter(session->data().chatsFilters().defaultId()) +, _openedFolder(window->id().folder()) , _defaultChatTheme(std::make_shared()) , _chatStyle(std::make_unique(session->colorIndicesValue())) , _giftPremiumValidator(this) { @@ -1373,8 +1408,8 @@ void SessionController::suggestArchiveAndMute() { })); } -PeerData *SessionController::singlePeer() const { - return _window->singlePeer(); +SeparateId SessionController::windowId() const { + return _window->id(); } bool SessionController::isPrimary() const { @@ -1466,7 +1501,7 @@ void SessionController::setupShortcuts() { if (account == &session().account()) { return false; } - const auto window = app->separateWindowForAccount(account); + const auto window = app->separateWindowFor(account); if (window) { window->activate(); } else { @@ -1523,7 +1558,9 @@ void SessionController::checkOpenedFilter() { } void SessionController::activateFirstChatsFilter() { - if (_filtersActivated || !session().data().chatsFilters().loaded()) { + if (_filtersActivated + || !isPrimary() + || !session().data().chatsFilters().loaded()) { return; } _filtersActivated = true; @@ -1536,8 +1573,24 @@ bool SessionController::uniqueChatsInSearchResults() const { && !_searchInChat.current(); } +bool SessionController::openFolderInDifferentWindow( + not_null folder) { + const auto id = SeparateId(SeparateType::Archive, &session()); + if (const auto separate = Core::App().separateWindowFor(id)) { + if (separate == _window) { + return false; + } + separate->sessionController()->showByInitialId(); + separate->activate(); + return true; + } + return false; +} + void SessionController::openFolder(not_null folder) { - if (_openedFolder.current() != folder) { + if (openFolderInDifferentWindow(folder)) { + return; + } else if (_openedFolder.current() != folder) { resetFakeUnreadWhileOpened(); } if (activeChatsFilterCurrent() != 0) { @@ -1550,22 +1603,44 @@ void SessionController::openFolder(not_null folder) { } void SessionController::closeFolder() { + if (_openedFolder.current() + && windowId().type == SeparateType::Archive) { + Core::App().closeWindow(_window); + return; + } _openedFolder = nullptr; } -void SessionController::showForum( +bool SessionController::showForumInDifferentWindow( not_null forum, const SectionShow ¶ms) { - if (!isPrimary()) { - auto primary = Core::App().windowFor(&session().account()); - if (&primary->account() != &session().account()) { - Core::App().domain().activate(&session().account()); - primary = Core::App().windowFor(&session().account()); - } - if (&primary->account() == &session().account()) { - primary->sessionController()->showForum(forum, params); - } + const auto window = Core::App().windowForShowingForum(forum); + if (window == _window) { + return false; + } else if (window) { + window->sessionController()->showForum(forum, params); + window->activate(); + return true; + } else if (windowId().hasChatsList()) { + return false; + } + const auto account = not_null(&session().account()); + auto primary = Core::App().separateWindowFor(account); + if (!primary) { + Core::App().domain().activate(account); + primary = Core::App().separateWindowFor(account); + } + if (primary && &primary->account() == account) { + primary->sessionController()->showForum(forum, params); primary->activate(); + } + return true; +} + +void SessionController::showForum( + not_null forum, + const SectionShow ¶ms) { + if (showForumInDifferentWindow(forum, params)) { return; } _shownForumLifetime.destroy(); @@ -1585,8 +1660,9 @@ void SessionController::showForum( ) | rpl::start_with_next([=, history = forum->history()] { const auto now = activeChatCurrent().owningHistory(); const auto showHistory = !now || (now == history); + const auto weak = base::make_weak(this); closeForum(); - if (showHistory) { + if (weak && showHistory) { showPeerHistory(history, { SectionShow::Way::Backward, anim::type::normal, @@ -1598,6 +1674,18 @@ void SessionController::showForum( } void SessionController::closeForum() { + if (const auto forum = _shownForum.current()) { + const auto id = windowId(); + if (id.type == SeparateType::Forum) { + const auto initial = id.forum(); + if (!initial || initial == forum) { + Core::App().closeWindow(_window); + } else { + showForum(initial); + } + return; + } + } _shownForumLifetime.destroy(); _shownForum = nullptr; } @@ -1889,7 +1977,7 @@ int SessionController::dialogsSmallColumnWidth() const { } int SessionController::minimalThreeColumnWidth() const { - return (_isPrimary ? st::columnMinimalWidthLeft : 0) + return (_hasDialogs ? st::columnMinimalWidthLeft : 0) + st::columnMinimalWidthMain + st::columnMinimalWidthThird; } @@ -1903,7 +1991,7 @@ auto SessionController::computeColumnLayout() const -> ColumnLayout { auto useOneColumnLayout = [&] { auto minimalNormal = st::columnMinimalWidthLeft + st::columnMinimalWidthMain; - if (_isPrimary && bodyWidth < minimalNormal) { + if (_hasDialogs && bodyWidth < minimalNormal) { return true; } return false; @@ -1945,7 +2033,7 @@ auto SessionController::computeColumnLayout() const -> ColumnLayout { } int SessionController::countDialogsWidthFromRatio(int bodyWidth) const { - if (!_isPrimary) { + if (!_hasDialogs) { return 0; } const auto nochat = !mainSectionShown(); @@ -1979,8 +2067,8 @@ SessionController::ShrinkResult SessionController::shrinkDialogsAndThirdColumns( if (thirdWidthNew < st::columnMinimalWidthThird) { thirdWidthNew = st::columnMinimalWidthThird; dialogsWidthNew = bodyWidth - thirdWidthNew - chatWidth; - Assert(!_isPrimary || dialogsWidthNew >= st::columnMinimalWidthLeft); - } else if (_isPrimary && dialogsWidthNew < st::columnMinimalWidthLeft) { + Assert(!_hasDialogs || dialogsWidthNew >= st::columnMinimalWidthLeft); + } else if (_hasDialogs && dialogsWidthNew < st::columnMinimalWidthLeft) { dialogsWidthNew = st::columnMinimalWidthLeft; thirdWidthNew = bodyWidth - dialogsWidthNew - chatWidth; Assert(thirdWidthNew >= st::columnMinimalWidthThird); @@ -2089,9 +2177,11 @@ void SessionController::closeThirdSection() { } } -bool SessionController::canShowSeparateWindow( - not_null peer) const { - return !peer->isForum() && peer->computeUnavailableReason().isEmpty(); +bool SessionController::canShowSeparateWindow(SeparateId id) const { + if (const auto thread = id.thread) { + return thread->peer()->computeUnavailableReason().isEmpty(); + } + return true; } void SessionController::showPeer(not_null peer, MsgId msgId) { @@ -2314,21 +2404,20 @@ void SessionController::clearChooseReportMessages() const { } void SessionController::showInNewWindow( - not_null peer, + SeparateId id, MsgId msgId) { - if (!canShowSeparateWindow(peer)) { - showThread( - peer->owner().history(peer), - msgId, - Window::SectionShow::Way::ClearStack); + if (!canShowSeparateWindow(id)) { + Assert(id.thread != nullptr); + showThread(id.thread, msgId, SectionShow::Way::ClearStack); return; } const auto active = activeChatCurrent(); - const auto fromActive = active.history() - ? (active.history()->peer == peer) + // windows check active forum / active archive + const auto fromActive = active.thread() + ? (active.thread() == id.thread) : false; const auto toSeparate = [=] { - Core::App().ensureSeparateWindowForPeer(peer, msgId); + Core::App().ensureSeparateWindowFor(id, msgId); }; if (fromActive) { window().preventOrInvoke([=] { @@ -2441,7 +2530,13 @@ void SessionController::showBackFromStack(const SectionShow ¶ms) { return topic && topic->forum()->topicDeleted(topic->rootId()); }; do { - content()->showBackFromStack(params); + const auto empty = content()->stackIsEmpty(); + const auto shown = content()->showBackFromStack(params); + if (empty && !shown && content()->stackIsEmpty() && bad()) { + clearSectionStack(anim::type::instant); + window().close(); + break; + } } while (bad()); } @@ -2485,6 +2580,9 @@ FilterId SessionController::activeChatsFilterCurrent() const { void SessionController::setActiveChatsFilter( FilterId id, const SectionShow ¶ms) { + if (!isPrimary()) { + return; + } const auto changed = (activeChatsFilterCurrent() != id); if (changed) { resetFakeUnreadWhileOpened(); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 35e6909039185..d738a048e7755 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -91,6 +91,7 @@ class FiltersMenu; class ChatPreviewManager; struct PeerByLinkInfo; +struct SeparateId; struct PeerThemeOverride { PeerData *peer = nullptr; @@ -232,6 +233,10 @@ class SessionNavigation : public base::has_weak_ptr { ShowAtUnreadMsgId); } + void showByInitialId( + const SectionShow ¶ms = SectionShow::Way::ClearStack, + MsgId msgId = ShowAtUnreadMsgId); + void showSettings( Settings::Type type, const SectionShow ¶ms = SectionShow()); @@ -326,7 +331,7 @@ class SessionController : public SessionNavigation { [[nodiscard]] Controller &window() const { return *_window; } - [[nodiscard]] PeerData *singlePeer() const; + [[nodiscard]] SeparateId windowId() const; [[nodiscard]] bool isPrimary() const; [[nodiscard]] not_null<::MainWindow*> widget() const; [[nodiscard]] not_null content() const; @@ -432,7 +437,7 @@ class SessionController : public SessionNavigation { void resizeForThirdSection(); void closeThirdSection(); - [[nodiscard]] bool canShowSeparateWindow(not_null peer) const; + [[nodiscard]] bool canShowSeparateWindow(SeparateId id) const; void showPeer(not_null peer, MsgId msgId = ShowAtUnreadMsgId); void startOrJoinGroupCall(not_null peer); @@ -509,7 +514,7 @@ class SessionController : public SessionNavigation { void clearChooseReportMessages() const; void showInNewWindow( - not_null peer, + SeparateId id, MsgId msgId = ShowAtUnreadMsgId); void toggleChooseChatTheme( @@ -667,10 +672,16 @@ class SessionController : public SessionNavigation { void checkNonPremiumLimitToastDownload(DocumentId id); void checkNonPremiumLimitToastUpload(FullMsgId id); + bool openFolderInDifferentWindow(not_null folder); + bool showForumInDifferentWindow( + not_null forum, + const SectionShow ¶ms); + const not_null _window; const std::unique_ptr _emojiInteractions; const std::unique_ptr _chatPreviewManager; const bool _isPrimary = false; + const bool _hasDialogs = false; mutable std::shared_ptr _cachedShow; diff --git a/Telegram/ThirdParty/dispatch b/Telegram/ThirdParty/dispatch index ee39300b12a77..542b7f3231168 160000 --- a/Telegram/ThirdParty/dispatch +++ b/Telegram/ThirdParty/dispatch @@ -1 +1 @@ -Subproject commit ee39300b12a77efd3f2f020e009e42d557adbb29 +Subproject commit 542b7f32311680b11b6fc8fcb2576955460ba7da diff --git a/Telegram/ThirdParty/fcitx5-qt b/Telegram/ThirdParty/fcitx5-qt index cc77e32c0ab67..c743b12e6780e 160000 --- a/Telegram/ThirdParty/fcitx5-qt +++ b/Telegram/ThirdParty/fcitx5-qt @@ -1 +1 @@ -Subproject commit cc77e32c0ab675a663a7c019b3bb8cfcc60c5ec3 +Subproject commit c743b12e6780edf1dcfe9071531c80f050cacb95 diff --git a/Telegram/ThirdParty/kcoreaddons b/Telegram/ThirdParty/kcoreaddons index 79b99f162b200..fd84da51b554e 160000 --- a/Telegram/ThirdParty/kcoreaddons +++ b/Telegram/ThirdParty/kcoreaddons @@ -1 +1 @@ -Subproject commit 79b99f162b200413671dbabe21c73356d9956e35 +Subproject commit fd84da51b554eac25e35b1e3f373edaab3029b15 diff --git a/Telegram/ThirdParty/kimageformats b/Telegram/ThirdParty/kimageformats index 63a9de758f413..106279d32ec4b 160000 --- a/Telegram/ThirdParty/kimageformats +++ b/Telegram/ThirdParty/kimageformats @@ -1 +1 @@ -Subproject commit 63a9de758f4132b73ea4535fd9dd7fde3138dc33 +Subproject commit 106279d32ec4b93ccf5e29a92616e0f0cc8d2382 diff --git a/Telegram/ThirdParty/libtgvoip b/Telegram/ThirdParty/libtgvoip index 25facad342c32..2d2592860478e 160000 --- a/Telegram/ThirdParty/libtgvoip +++ b/Telegram/ThirdParty/libtgvoip @@ -1 +1 @@ -Subproject commit 25facad342c3280315f9ef553906f46c3eeba1e4 +Subproject commit 2d2592860478e60d972b96e67ee034b8a71bb57a diff --git a/Telegram/ThirdParty/nimf b/Telegram/ThirdParty/nimf index 955ee48b4f402..498ec7ffab3ac 160000 --- a/Telegram/ThirdParty/nimf +++ b/Telegram/ThirdParty/nimf @@ -1 +1 @@ -Subproject commit 955ee48b4f4020b317ece1e3b1e58c0b7f4bd0f3 +Subproject commit 498ec7ffab3ac140c2469638a14451788f03e798 diff --git a/Telegram/ThirdParty/xdg-desktop-portal b/Telegram/ThirdParty/xdg-desktop-portal index fa8d41a2f9a5d..11c8a96b147ae 160000 --- a/Telegram/ThirdParty/xdg-desktop-portal +++ b/Telegram/ThirdParty/xdg-desktop-portal @@ -1 +1 @@ -Subproject commit fa8d41a2f9a5d30a1e41568b6fb53b046dce14dc +Subproject commit 11c8a96b147aeae70e3f770313f93b367d53fedd diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 02c3f9f3e935b..e4f7d1fb11ce2 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,9 +1,7 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} -{%- set QT = "6.7.1" -%} +{%- set QT = "6.7.2" -%} {%- set QT_TAG = "v" ~ QT -%} -{%- set CMAKE_VER = "3.27.6" -%} -{%- set CMAKE_FILE = "cmake-" ~ CMAKE_VER ~ "-Linux-x86_64.sh" -%} {%- set CFLAGS_DEBUG = "-g -pipe -fPIC -fstack-protector-all -fstack-clash-protection -fcf-protection -D_GLIBCXX_ASSERTIONS" -%} {%- set CFLAGS_LTO = "-flto=auto -ffat-lto-objects" -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} @@ -18,7 +16,7 @@ ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/loc RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ - && dnf -y install autoconf automake libtool pkgconfig make patch git \ + && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang lld nasm yasm \ file which perl-open perl-XML-Parser perl-IPC-Cmd xorg-x11-util-macros \ gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils \ @@ -34,12 +32,6 @@ WORKDIR {{ LibrariesPath }} RUN python3 -m pip install meson ninja -RUN mkdir /opt/cmake \ - && curl -sSLo {{ CMAKE_FILE }} {{ GIT }}/Kitware/CMake/releases/download/v{{ CMAKE_VER }}/{{ CMAKE_FILE }} \ - && sh {{ CMAKE_FILE }} --prefix=/opt/cmake --skip-license \ - && ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake \ - && rm {{ CMAKE_FILE }} - FROM builder-base AS builder ENV AR gcc-ar ENV RANLIB gcc-ranlib @@ -51,7 +43,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 803f1c2630f5eb0d3b00ba3f095b3079c0533156 \ + && git fetch --depth=1 origin 20a7c5ffd8265fc6e45203ea2536f7b1965be19a \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -250,7 +242,7 @@ COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -RUN git clone -b v0.10.2 --depth=1 {{ GIT }}/libjxl/libjxl.git \ +RUN git clone -b v0.10.3 --depth=1 {{ GIT }}/libjxl/libjxl.git \ && cd libjxl \ && git apply ../patches/libjxl.patch \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ @@ -761,7 +753,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / RUN git init tg_owt \ && cd tg_owt \ && git remote add origin {{ GIT }}/desktop-app/tg_owt.git \ - && git fetch --depth=1 origin afd9d5d31798d3eacf9ed6c30601e91d0f1e4d60 \ + && git fetch --depth=1 origin c9cc4390ab951f2cbc103ff783a11f398b27660b \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ && rm -rf .git \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index c4a1e64bb04e9..6704bdbd28d60 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1,7 +1,10 @@ import os, sys, pprint, re, json, pathlib, hashlib, subprocess, glob executePath = os.getcwd() +sys.dont_write_bytecode = True scriptPath = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(scriptPath + '/..') +import qt_version def finish(code): global executePath @@ -23,11 +26,24 @@ def nativeToolsError(): win32 = win and (os.environ['Platform'] == 'x86') win64 = win and (os.environ['Platform'] == 'x64') +winarm = win and (os.environ['Platform'] == 'arm') + +arch = '' +if win32: + arch = 'x86' +elif win64: + arch = 'x64' +elif winarm: + arch = 'arm' +if not qt_version.resolve(arch): + error('Usupported platform.') + +qt = os.environ.get('QT') if win and not 'COMSPEC' in os.environ: error('COMSPEC environment variable is not set.') -if win and not win32 and not win64: +if win and not win32 and not win64 and not winarm: nativeToolsError() os.chdir(scriptPath + '/../../../..') @@ -42,11 +58,8 @@ def nativeToolsError(): usedPrefix = os.path.realpath(os.path.join(libsDir, 'local')) optionsList = [ + 'qt6', 'skip-release', - 'build-qt5', - 'skip-qt5', - 'build-qt6', - 'skip-qt6', 'build-stackwalk', ] options = [] @@ -63,9 +76,6 @@ def nativeToolsError(): customRunCommand = True runCommand.append('shell') -buildQt5 = not 'skip-qt5' in options if win else 'build-qt5' in options -buildQt6 = 'build-qt6' in options if win else not 'skip-qt6' in options - if not os.path.isdir(os.path.join(libsDir, keysLoc)): pathlib.Path(os.path.join(libsDir, keysLoc)).mkdir(parents=True, exist_ok=True) if not os.path.isdir(os.path.join(thirdPartyDir, keysLoc)): @@ -107,7 +117,7 @@ def nativeToolsError(): elif (mac): environment.update({ 'SPECIAL_TARGET': 'mac', - 'MAKE_THREADS_CNT': '-j8', + 'MAKE_THREADS_CNT': '-j' + str(os.cpu_count()), 'MACOSX_DEPLOYMENT_TARGET': '10.13', 'UNGUARDED': '-Werror=unguarded-availability-new', 'MIN_VER': '-mmacosx-version-min=10.13', @@ -435,7 +445,7 @@ def runStages(): stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 25f76cf4d5 + git checkout e0cdca2e79 """) stage('msys64', """ @@ -511,9 +521,9 @@ def runStages(): win: git clone https://github.com/desktop-app/lzma.git cd lzma\\C\\Util\\LzmaLib - msbuild LzmaLib.sln /property:Configuration=Debug /property:Platform="$X8664" + msbuild -m LzmaLib.sln /property:Configuration=Debug /property:Platform="$X8664" release: - msbuild LzmaLib.sln /property:Configuration=Release /property:Platform="$X8664" + msbuild -m LzmaLib.sln /property:Configuration=Release /property:Platform="$X8664" """) stage('xz', """ @@ -540,9 +550,9 @@ def runStages(): -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DCMAKE_C_FLAGS="/DZLIB_WINAPI" - cmake --build . --config Debug + cmake --build . --config Debug --parallel release: - cmake --build . --config Release + cmake --build . --config Release --parallel mac: CFLAGS="$MIN_VER $UNGUARDED" LDFLAGS="$MIN_VER" ./configure \\ --static \\ @@ -560,9 +570,9 @@ def runStages(): -A %WIN32X64% ^ -DWITH_JPEG8=ON ^ -DPNG_SUPPORTED=OFF - cmake --build . --config Debug + cmake --build . --config Debug --parallel release: - cmake --build . --config Release + cmake --build . --config Release --parallel mac: CFLAGS="-arch arm64" cmake -B build.arm64 . \\ -D CMAKE_SYSTEM_NAME=Darwin \\ @@ -606,11 +616,11 @@ def runStages(): move out.dbg\\ossl_static.pdb out.dbg\\ossl_static jom clean move out.dbg\\ossl_static out.dbg\\ossl_static.pdb -win32: +win32_release: perl Configure no-shared no-tests VC-WIN32 /FS -win64: +win64_release: perl Configure no-shared no-tests VC-WIN64A /FS -win: +win_release: jom -j%NUMBER_OF_PROCESSORS% mkdir out move libcrypto.lib out @@ -643,8 +653,8 @@ def runStages(): -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" - cmake --build out --config Debug - cmake --build out --config Release + cmake --build out --config Debug --parallel + cmake --build out --config Release --parallel cmake --install out --config Release mac: CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\ @@ -663,9 +673,9 @@ def runStages(): cd out win: cmake -A %WIN32X64% .. - cmake --build . --config Debug + cmake --build . --config Debug --parallel release: - cmake --build . --config Release + cmake --build . --config Release --parallel !win: mkdir Debug cd Debug @@ -780,10 +790,10 @@ def runStages(): -DBUILD_SHARED_LIBS=OFF ^ -DAVIF_ENABLE_WERROR=OFF ^ -DAVIF_CODEC_DAV1D=ON - cmake --build . --config Debug + cmake --build . --config Debug --parallel cmake --install . --config Debug release: - cmake --build . --config Release + cmake --build . --config Release --parallel cmake --install . --config Release mac: cmake . \\ @@ -816,10 +826,10 @@ def runStages(): -DBUILD_SHARED_LIBS=OFF ^ -DENABLE_DECODER=OFF ^ -DENABLE_ENCODER=OFF - cmake --build . --config Debug + cmake --build . --config Debug --parallel cmake --install . --config Debug release: - cmake --build . --config Release + cmake --build . --config Release --parallel cmake --install . --config Release mac: cmake . \\ @@ -898,10 +908,10 @@ def runStages(): -DWITH_RAV1E=OFF ^ -DWITH_RAV1E_PLUGIN=OFF ^ -DWITH_EXAMPLES=OFF - cmake --build . --config Debug + cmake --build . --config Debug --parallel cmake --install . --config Debug release: - cmake --build . --config Release + cmake --build . --config Release --parallel cmake --install . --config Release mac: cmake . \\ @@ -928,7 +938,7 @@ def runStages(): """) stage('libjxl', """ - git clone -b v0.8.2 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git + git clone -b v0.10.3 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl """ + setVar("cmake_defines", """ -DBUILD_SHARED_LIBS=OFF @@ -944,12 +954,10 @@ def runStages(): -DJPEGXL_ENABLE_SJPEG=OFF -DJPEGXL_ENABLE_OPENEXR=OFF -DJPEGXL_ENABLE_SKCMS=ON - -DJPEGXL_BUNDLE_SKCMS=ON -DJPEGXL_ENABLE_VIEWERS=OFF -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_PLUGINS=OFF -DJPEGXL_ENABLE_COVERAGE=OFF - -DJPEGXL_ENABLE_PROFILER=OFF -DJPEGXL_WARNINGS_AS_ERRORS=OFF """) + """ win: @@ -957,17 +965,17 @@ def runStages(): -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ - -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ - -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ + -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^ + -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE /DJXL_CMS_STATIC_DEFINE" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ %cmake_defines% - cmake --build . --config Debug + cmake --build . --config Debug --parallel cmake --install . --config Debug release: - cmake --build . --config Release + cmake --build . --config Release --parallel cmake --install . --config Release mac: cmake . \\ @@ -1322,7 +1330,7 @@ def runStages(): ninja -C out/Release%FolderPostfix% common crash_generation_client exception_handler cd tools\\windows\\dump_syms gyp dump_syms.gyp --format=msvs - msbuild dump_syms.vcxproj /property:Configuration=Release /property:Platform="x64" + msbuild -m dump_syms.vcxproj /property:Configuration=Release /property:Platform="x64" win: deactivate mac: @@ -1398,7 +1406,9 @@ def runStages(): lipo -create Release.arm64/libcrashpad_client.a Release.x86_64/libcrashpad_client.a -output Release/libcrashpad_client.a """) -stage('tg_angle', """ +if qt < '6': + if win: + stage('tg_angle', """ win: git clone https://github.com/desktop-app/tg_angle.git cd tg_angle @@ -1424,22 +1434,21 @@ def runStages(): cd ..\\..\\.. """) -if buildQt5: - stage('qt_5_15_13', """ - git clone -b v5.15.13-lts-lgpl https://github.com/qt/qt5.git qt_5_15_13 - cd qt_5_15_13 + stage('qt_' + qt, """ + git clone -b v$QT-lts-lgpl https://github.com/qt/qt5.git qt_$QT + cd qt_$QT git submodule update --init --recursive qtbase qtimageformats qtsvg -depends:patches/qtbase_5.15.13/*.patch +depends:patches/qtbase_""" + qt + """/*.patch cd qtbase win: - for /r %%i in (..\\..\\patches\\qtbase_5.15.13\\*) do git apply %%i -v + for /r %%i in (..\\..\\patches\\qtbase_%QT%\\*) do git apply %%i -v cd .. SET CONFIGURATIONS=-debug release: SET CONFIGURATIONS=-debug-and-release win: - """ + removeDir("\"%LIBS_DIR%\\Qt-5.15.13\"") + """ + """ + removeDir('"%LIBS_DIR%\\Qt-' + qt + '"') + """ SET ANGLE_DIR=%LIBS_DIR%\\tg_angle SET ANGLE_LIBS_DIR=%ANGLE_DIR%\\out SET MOZJPEG_DIR=%LIBS_DIR%\\mozjpeg @@ -1447,7 +1456,7 @@ def runStages(): SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\out SET ZLIB_LIBS_DIR=%LIBS_DIR%\\zlib SET WEBP_DIR=%LIBS_DIR%\\libwebp - configure -prefix "%LIBS_DIR%\\Qt-5.15.13" ^ + configure -prefix "%LIBS_DIR%\\Qt-%QT%" ^ %CONFIGURATIONS% ^ -force-debug-info ^ -opensource ^ @@ -1482,14 +1491,14 @@ def runStages(): jom -j%NUMBER_OF_PROCESSORS% jom -j%NUMBER_OF_PROCESSORS% install mac: - find ../../patches/qtbase_5.15.13 -type f -print0 | sort -z | xargs -0 git apply + find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -0 git apply cd .. CONFIGURATIONS=-debug release: CONFIGURATIONS=-debug-and-release mac: - ./configure -prefix "$USED_PREFIX/Qt-5.15.13" \ + ./configure -prefix "$USED_PREFIX/Qt-$QT" \ $CONFIGURATIONS \ -force-debug-info \ -opensource \ @@ -1508,16 +1517,16 @@ def runStages(): make $MAKE_THREADS_CNT make install """) - -if buildQt6: - stage('qt_6_2_8', """ -mac: - git clone -b v6.2.8-lts-lgpl https://github.com/qt/qt5.git qt_6_2_8 - cd qt_6_2_8 +else: # qt > '6' + branch = 'v$QT' + ('-lts-lgpl' if qt < '6.3' else '') + stage('qt_' + qt, """ + git clone -b """ + branch + """ https://github.com/qt/qt5.git qt_$QT + cd qt_$QT git submodule update --init --recursive qtbase qtimageformats qtsvg -depends:patches/qtbase_6.2.8/*.patch +depends:patches/qtbase_""" + qt + """/*.patch cd qtbase - find ../../patches/qtbase_6.2.8 -type f -print0 | sort -z | xargs -0 git apply -v +mac: + find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -0 git apply -v cd .. sed -i.bak 's/tqtc-//' {qtimageformats,qtsvg}/dependencies.yaml @@ -1525,7 +1534,7 @@ def runStages(): release: CONFIGURATIONS=-debug-and-release mac: - ./configure -prefix "$USED_PREFIX/Qt-6.2.8" \ + ./configure -prefix "$USED_PREFIX/Qt-$QT" \ $CONFIGURATIONS \ -force-debug-info \ -opensource \ @@ -1546,6 +1555,62 @@ def runStages(): ninja ninja install +win: + for /r %%i in (..\\..\\patches\\qtbase_%QT%\\*) do git apply %%i -v + cd .. + + SET CONFIGURATIONS=-debug +release: + SET CONFIGURATIONS=-debug-and-release +win: + """ + removeDir('"%LIBS_DIR%\\Qt' + qt + '"') + """ + SET MOZJPEG_DIR=%LIBS_DIR%\\mozjpeg + SET OPENSSL_DIR=%LIBS_DIR%\\openssl3 + SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\out + SET ZLIB_LIBS_DIR=%LIBS_DIR%\\zlib + SET WEBP_DIR=%LIBS_DIR%\\libwebp + configure -prefix "%LIBS_DIR%\\Qt-%QT%" ^ + %CONFIGURATIONS% ^ + -force-debug-info ^ + -opensource ^ + -confirm-license ^ + -static ^ + -static-runtime ^ + -feature-c++20 ^ + -openssl linked ^ + -system-webp ^ + -system-zlib ^ + -system-libjpeg ^ + -nomake examples ^ + -nomake tests ^ + -platform win32-msvc ^ + -D ZLIB_WINAPI ^ + -- ^ + -D OPENSSL_FOUND=1 ^ + -D OPENSSL_INCLUDE_DIR="%OPENSSL_DIR%\\include" ^ + -D LIB_EAY_DEBUG="%OPENSSL_LIBS_DIR%.dbg\\libcrypto.lib" ^ + -D SSL_EAY_DEBUG="%OPENSSL_LIBS_DIR%.dbg\\libssl.lib" ^ + -D LIB_EAY_RELEASE="%OPENSSL_LIBS_DIR%\\libcrypto.lib" ^ + -D SSL_EAY_RELEASE="%OPENSSL_LIBS_DIR%\\libssl.lib" ^ + -D JPEG_FOUND=1 ^ + -D JPEG_INCLUDE_DIR="%MOZJPEG_DIR%" ^ + -D JPEG_LIBRARY_DEBUG="%MOZJPEG_DIR%\\Debug\\jpeg-static.lib" ^ + -D JPEG_LIBRARY_RELEASE="%MOZJPEG_DIR%\\Release\\jpeg-static.lib" ^ + -D ZLIB_FOUND=1 ^ + -D ZLIB_INCLUDE_DIR="%ZLIB_LIBS_DIR%" ^ + -D ZLIB_LIBRARY_DEBUG="%ZLIB_LIBS_DIR%\\Debug\\zlibstaticd.lib" ^ + -D ZLIB_LIBRARY_RELEASE="%ZLIB_LIBS_DIR%\\Release\\zlibstatic.lib" ^ + -D WebP_INCLUDE_DIR="%WEBP_DIR%\\src" ^ + -D WebP_demux_INCLUDE_DIR="%WEBP_DIR%\\src" ^ + -D WebP_mux_INCLUDE_DIR="%WEBP_DIR%\\src" ^ + -D WebP_LIBRARY="%WEBP_DIR%\\out\\release-static\\$X8664\\lib\\webp.lib" ^ + -D WebP_demux_LIBRARY="%WEBP_DIR%\\out\\release-static\\$X8664\\lib\\webpdemux.lib" ^ + -D WebP_mux_LIBRARY="%WEBP_DIR%\\out\\release-static\\$X8664\\lib\\webpmux.lib" + + cmake --build . --config Debug --parallel + cmake --install . --config Debug + cmake --build . --parallel + cmake --install . """) stage('tg_owt', """ diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py new file mode 100644 index 0000000000000..2bc778741d1e8 --- /dev/null +++ b/Telegram/build/qt_version.py @@ -0,0 +1,16 @@ +import sys, os + +def resolve(arch): + if sys.platform == 'darwin': + os.environ['QT'] = '6.2.9' + elif sys.platform == 'win32': + if arch == 'arm' or 'qt6' in sys.argv: + print('Choosing Qt 6.') + os.environ['QT'] = '6.7.2' + elif os.environ.get('QT') is None: + print('Choosing Qt 5.') + os.environ['QT'] = '5.15.13' + elif os.environ.get('QT') is None: + return False + print('Choosing Qt ' + os.environ.get('QT')) + return True diff --git a/Telegram/build/version b/Telegram/build/version index e382b01adc064..4d2b8605937ff 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5001005 -AppVersionStrMajor 5.1 -AppVersionStrSmall 5.1.5 -AppVersionStr 5.1.5 +AppVersion 5002003 +AppVersionStrMajor 5.2 +AppVersionStrSmall 5.2.3 +AppVersionStr 5.2.3 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.1.5 +AppVersionOriginal 5.2.3 diff --git a/Telegram/cmake/generate_midl.cmake b/Telegram/cmake/generate_midl.cmake index 51d960ba842ec..d3ec53cc59ec9 100644 --- a/Telegram/cmake/generate_midl.cmake +++ b/Telegram/cmake/generate_midl.cmake @@ -8,7 +8,9 @@ function(generate_midl target_name src_loc) set(gen_dst ${CMAKE_CURRENT_BINARY_DIR}/gen) file(MAKE_DIRECTORY ${gen_dst}) - if (build_win64) + if (build_winarm) + set(env arm64) + elseif (build_win64) set(env x64) else() set(env win32) diff --git a/Telegram/cmake/lib_tgvoip.cmake b/Telegram/cmake/lib_tgvoip.cmake index 886cd9935beb6..fbae709660848 100644 --- a/Telegram/cmake/lib_tgvoip.cmake +++ b/Telegram/cmake/lib_tgvoip.cmake @@ -134,20 +134,13 @@ PRIVATE ) if (WIN32) - if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - target_compile_options(lib_tgvoip_bundled - PRIVATE - /wd4005 - /wd4244 # conversion from 'int' to 'float', possible loss of data (several in webrtc) - /wd5055 # operator '>' deprecated between enumerations and floating-point types - ) - else() - target_compile_definitions(lib_tgvoip_bundled - PUBLIC - # Doesn't build with mingw for now - TGVOIP_NO_DSP - ) - endif() + target_compile_options_if_exists(lib_tgvoip_bundled + PRIVATE + /wd4005 # 'identifier' : macro redefinition + /wd4068 # unknown pragma + /wd4996 # deprecated + /wd5055 # operator '>' deprecated between enumerations and floating-point types + ) elseif (APPLE) target_compile_definitions(lib_tgvoip_bundled PUBLIC diff --git a/Telegram/cmake/td_scheme.cmake b/Telegram/cmake/td_scheme.cmake index cf241941b080c..10f2c57c40428 100644 --- a/Telegram/cmake/td_scheme.cmake +++ b/Telegram/cmake/td_scheme.cmake @@ -34,6 +34,13 @@ PUBLIC desktop-app::lib_tl ) +if (WIN32 AND NOT build_win64 AND NOT build_winarm) + target_compile_options(td_scheme + PRIVATE + /bigobj # scheme.cpp has too many sections. + ) +endif() + if (CMAKE_SYSTEM_PROCESSOR STREQUAL "mips64") # Sometimes final linking may fail with error "relocation truncated to fit" # due to large scheme size. diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 2b69b2df7906b..3555c92189cb4 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -82,6 +82,7 @@ PRIVATE data/data_birthday.h data/data_channel_earn.h data/data_credits.h + data/data_credits_earn.h data/data_statistics_chart.cpp data/data_statistics_chart.h data/data_subscription_option.h @@ -213,6 +214,8 @@ PRIVATE statistics/statistics_data_deserialize.h statistics/statistics_format_values.cpp statistics/statistics_format_values.h + statistics/statistics_graphics.cpp + statistics/statistics_graphics.h statistics/statistics_types.h statistics/view/abstract_chart_view.cpp statistics/view/abstract_chart_view.h @@ -401,6 +404,7 @@ PRIVATE ui/widgets/multi_select.h ui/widgets/sent_code_field.cpp ui/widgets/sent_code_field.h + ui/widgets/slider_natural_width.h ui/widgets/vertical_drum_picker.cpp ui/widgets/vertical_drum_picker.h diff --git a/Telegram/configure.py b/Telegram/configure.py index ceb27bfb90cb1..f31dd70a7238a 100644 --- a/Telegram/configure.py +++ b/Telegram/configure.py @@ -11,6 +11,8 @@ scriptPath = os.path.dirname(os.path.realpath(__file__)) sys.path.append(scriptPath + '/../cmake') import run_cmake +sys.path.append(scriptPath + '/build') +import qt_version executePath = os.getcwd() def finish(code): @@ -41,12 +43,18 @@ def error(message): arch = 'x86' elif officialTarget in ['win64', 'uwp64']: arch = 'x64' +elif officialTarget in ['winarm', 'uwparm']: + arch = 'arm' +if not qt_version.resolve(arch): + error('Unsupported platform.') + +if 'qt6' in arguments: + arguments.remove('qt6') if officialTarget != '': officialApiIdFile = scriptPath + '/../../DesktopPrivate/custom_api_id.h' if not os.path.isfile(officialApiIdFile): - print("[ERROR] DesktopPrivate/custom_api_id.h not found.") - finish(1) + error('DesktopPrivate/custom_api_id.h not found.') with open(officialApiIdFile, 'r') as f: for line in f: apiIdMatch = re.search(r'ApiId\s+=\s+(\d+)', line) diff --git a/Telegram/create.bat b/Telegram/create.bat index 85ec9439b1bf2..2307b0d2a1e83 100644 --- a/Telegram/create.bat +++ b/Telegram/create.bat @@ -67,26 +67,30 @@ exit /b %errorlevel% set "CommandPath=%1" set "CommandPathUnix=!CommandPath:\=/!" set "CommandPathWin=!CommandPath:/=\!" - + if "!CommandPathUnix:~-4!" == "_mac" ( + set "CommandExt=mm" + ) else ( + set "CommandExt=cpp" + ) if "!CommandPathUnix!" == "" ( echo Provide source path. exit /b 1 - ) else if exist "SourceFiles\!CommandPathWin!.cpp" ( + ) else if exist "SourceFiles\!CommandPathWin!.!CommandExt!" ( echo This source already exists. exit /b 1 ) - echo Generating source !CommandPathUnix!.cpp.. - mkdir "SourceFiles\!CommandPathWin!.cpp" - rmdir "SourceFiles\!CommandPathWin!.cpp" + echo Generating source !CommandPathUnix!.!CommandExt!.. + mkdir "SourceFiles\!CommandPathWin!.!CommandExt!" + rmdir "SourceFiles\!CommandPathWin!.!CommandExt!" - call :write_comment !CommandPathWin!.cpp + call :write_comment !CommandPathWin!.!CommandExt! set "quote=""" set "quote=!quote:~0,1!" set "source1=#include !quote!!CommandPathUnix!.h!quote!" ( echo !source1! echo. - )>> "SourceFiles\!CommandPathWin!.cpp" + )>> "SourceFiles\!CommandPathWin!.!CommandExt!" exit /b ) diff --git a/Telegram/lib_base b/Telegram/lib_base index 4d56f8b4bba52..1a50fd2300da3 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 4d56f8b4bba52b46844f46fafa5f9b6b2704429e +Subproject commit 1a50fd2300da3198e751a22bf728d33822180e15 diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index 9b52030bfcd7e..0b7622ff38778 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit 9b52030bfcd7e90e3e550231a3783ad1982fda78 +Subproject commit 0b7622ff38778e9cd03d3997de59351973480a1f diff --git a/Telegram/lib_storage b/Telegram/lib_storage index 0971b69ca90f1..ccdc72548a506 160000 --- a/Telegram/lib_storage +++ b/Telegram/lib_storage @@ -1 +1 @@ -Subproject commit 0971b69ca90f1697ef81276d9820dcd6d26de4ac +Subproject commit ccdc72548a5065b5991b4e06e610d76bc4f6023e diff --git a/Telegram/lib_ui b/Telegram/lib_ui index fe734ee3a980d..a5096035953ee 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit fe734ee3a980dcf8bbec4fe18ee739041d409368 +Subproject commit a5096035953ee1d1a684892a2fd988812be110f1 diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index f701713cd798b..eb94965403569 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit f701713cd798bd7d5f69d318fdefb125d101aa76 +Subproject commit eb9496540356945e2c9fb700bcfa51444fd36f41 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 66ed73106e018..363db4e49a0b7 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 66ed73106e0180d8ae92c9c9a272e4a8d76ff336 +Subproject commit 363db4e49a0b78e5dd08bd922e09cf8810318c09 diff --git a/changelog.txt b/changelog.txt index c91e229ac02c3..c0298f855afe6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,46 @@ +5.2.3 (07.07.24) + +- Fix crash in bot star stats page. +- Bug fixes and other minor improvements. + +5.2.2 (02.07.24) + +- Fix topics search in topic groups. +- Fix Instant View pages content updating. + +5.2.1 (01.07.24) + +- Fix crash when opening topic in a new window. +- Fix crash in topic search scope dropdown. +- Fix crash in video player. +- Fix feeze and crash in Instant View (Windows). +- Allow unlock by Apple Watch or System Password (macOS). + +5.2 (30.06.24) + +- Pay for content with Telegram Stars. +- Enable local passcode unlock by Windows Hello and Touch ID. +- Allow opening forums, topics and archive in a separate window. + +5.1.8 beta (15.06.24) + +- Support nice blockquotes and code blocks edition when composing messages. +- Support collapsing blockquotes and specifying syntax highlight language. +- Support nice spoiler animation in the message composing input field. + +5.1.7 (14.06.24) + +- Fix recently searched hashtags in chats search. +- Fix formatting shortcuts on macOS. +- Fix non-Telegram-Stars-invoice bot buttons with star emoji. + +5.1.6 (13.06.24) + +- Fix search in archived chats in single-column layout. +- Improve chat previews for topics, Saved Messages and groups. +- Fix formatting shortcuts on Linux. +- Fix options for Telegram Stars buying in case of large amounts. + 5.1.5 (07.06.24) - Return WebView on Windows. diff --git a/cmake b/cmake index a7527c0e6eba1..4a4bc4cd34b3a 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit a7527c0e6eba1c71cd0dfd7bd8de9c1e68cb529f +Subproject commit 4a4bc4cd34b3ade038541a2b8b2c79f05393d67b diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a571c7197ec31..033424e1d6fb7 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -165,7 +165,7 @@ parts: patches: source: https://github.com/desktop-app/patches.git source-depth: 1 - source-commit: 803f1c2630f5eb0d3b00ba3f095b3079c0533156 + source-commit: 20a7c5ffd8265fc6e45203ea2536f7b1965be19a plugin: dump override-pull: | craftctl default @@ -237,7 +237,7 @@ parts: libjxl: source: https://github.com/libjxl/libjxl.git source-depth: 1 - source-tag: v0.10.2 + source-tag: v0.10.3 plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s @@ -360,7 +360,7 @@ parts: - mesa-vulkan-drivers - xkb-data override-pull: | - QT=6.7.1 + QT=6.7.2 git clone -b v${QT} --depth=1 https://github.com/qt/qt5.git . git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools @@ -369,7 +369,6 @@ parts: find $CRAFT_STAGE/patches/qtbase_${QT} -type f -print0 | sort -z | xargs -r0 git apply cd ../qtwayland find $CRAFT_STAGE/patches/qtwayland_${QT} -type f -print0 | sort -z | xargs -r0 git apply - sed -i 's/qMin(version, 8)/qMin(version, 7)/' src/client/qwaylandinputdevice.cpp cd .. override-build: | ./configure \ @@ -434,7 +433,7 @@ parts: webrtc: source: https://github.com/desktop-app/tg_owt.git source-depth: 1 - source-commit: 3bb3d757681e6cc5135aec6529a753dc3dcdcfb9 + source-commit: c9cc4390ab951f2cbc103ff783a11f398b27660b plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s