diff --git a/.gitmodules b/.gitmodules index a2a91a3f1..23ed731c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/telegramdesktop/libtgvoip [submodule "Telegram/ThirdParty/GSL"] path = Telegram/ThirdParty/GSL - url = https://github.com/desktop-app/GSL.git + url = https://github.com/Microsoft/GSL.git [submodule "Telegram/ThirdParty/xxHash"] path = Telegram/ThirdParty/xxHash url = https://github.com/Cyan4973/xxHash.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 680ac3d5a..82f1089f8 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -192,6 +192,8 @@ PRIVATE api/api_bot.h api/api_chat_filters.cpp api/api_chat_filters.h + api/api_chat_filters_remove_manager.cpp + api/api_chat_filters_remove_manager.h api/api_chat_invite.cpp api/api_chat_invite.h api/api_chat_links.cpp @@ -716,6 +718,8 @@ PRIVATE data/data_thread.h data/data_types.cpp data/data_types.h + data/data_unread_value.cpp + data/data_unread_value.h data/data_user.cpp data/data_user.h data/data_user_photos.cpp @@ -1062,6 +1066,10 @@ PRIVATE info/profile/info_profile_values.h info/profile/info_profile_widget.cpp info/profile/info_profile_widget.h + info/reactions_list/info_reactions_list_widget.cpp + info/reactions_list/info_reactions_list_widget.h + info/requests_list/info_requests_list_widget.cpp + info/requests_list/info_requests_list_widget.h info/saved/info_saved_sublists_widget.cpp info/saved/info_saved_sublists_widget.h info/settings/info_settings_widget.cpp @@ -1075,6 +1083,7 @@ PRIVATE info/statistics/info_statistics_list_controllers.h info/statistics/info_statistics_recent_message.cpp info/statistics/info_statistics_recent_message.h + info/statistics/info_statistics_tag.h info/statistics/info_statistics_widget.cpp info/statistics/info_statistics_widget.h info/stories/info_stories_inner_widget.cpp @@ -1111,6 +1120,10 @@ PRIVATE info/info_wrap_widget.h inline_bots/bot_attach_web_view.cpp inline_bots/bot_attach_web_view.h + inline_bots/inline_bot_confirm_prepared.cpp + inline_bots/inline_bot_confirm_prepared.h + inline_bots/inline_bot_downloads.cpp + inline_bots/inline_bot_downloads.h inline_bots/inline_bot_layout_internal.cpp inline_bots/inline_bot_layout_internal.h inline_bots/inline_bot_layout_item.cpp @@ -1242,6 +1255,8 @@ PRIVATE media/streaming/media_streaming_player.h media/streaming/media_streaming_reader.cpp media/streaming/media_streaming_reader.h + media/streaming/media_streaming_round_preview.cpp + media/streaming/media_streaming_round_preview.h media/streaming/media_streaming_utility.cpp media/streaming/media_streaming_utility.h media/streaming/media_streaming_video_track.cpp @@ -1581,6 +1596,8 @@ PRIVATE ui/chat/choose_send_as.h ui/chat/choose_theme_controller.cpp ui/chat/choose_theme_controller.h + ui/chat/sponsored_message_bar.cpp + ui/chat/sponsored_message_bar.h ui/controls/emoji_button_factory.cpp ui/controls/emoji_button_factory.h ui/controls/location_picker.cpp @@ -1612,6 +1629,8 @@ PRIVATE ui/widgets/expandable_peer_list.h ui/widgets/label_with_custom_emoji.cpp ui/widgets/label_with_custom_emoji.h + ui/widgets/chat_filters_tabs_strip.cpp + ui/widgets/chat_filters_tabs_strip.h ui/countryinput.cpp ui/countryinput.h ui/dynamic_thumbnails.cpp @@ -1931,7 +1950,7 @@ endif() set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder}) -if (WIN32 AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") +if (MSVC) target_link_libraries(Telegram PRIVATE delayimp @@ -2028,7 +2047,7 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins base/platform/win/base_windows_safe_library.h ) target_include_directories(Updater PRIVATE ${lib_base_loc}) - if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + if (MSVC) target_link_libraries(Updater PRIVATE delayimp diff --git a/Telegram/Resources/animations/hello_status.tgs b/Telegram/Resources/animations/hello_status.tgs new file mode 100644 index 000000000..b48182c2c Binary files /dev/null and b/Telegram/Resources/animations/hello_status.tgs differ diff --git a/Telegram/Resources/art/round_placeholder.jpg b/Telegram/Resources/art/round_placeholder.jpg new file mode 100644 index 000000000..cbdc9aee3 Binary files /dev/null and b/Telegram/Resources/art/round_placeholder.jpg differ diff --git a/Telegram/Resources/icons/chat/input_video.png b/Telegram/Resources/icons/chat/input_video.png new file mode 100644 index 000000000..34a71853d Binary files /dev/null and b/Telegram/Resources/icons/chat/input_video.png differ diff --git a/Telegram/Resources/icons/chat/input_video@2x.png b/Telegram/Resources/icons/chat/input_video@2x.png new file mode 100644 index 000000000..0657613a8 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_video@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_video@3x.png b/Telegram/Resources/icons/chat/input_video@3x.png new file mode 100644 index 000000000..9fb24d542 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_video@3x.png differ diff --git a/Telegram/Resources/icons/menu/edited_status.png b/Telegram/Resources/icons/menu/edited_status.png new file mode 100644 index 000000000..70fb24466 Binary files /dev/null and b/Telegram/Resources/icons/menu/edited_status.png differ diff --git a/Telegram/Resources/icons/menu/edited_status@2x.png b/Telegram/Resources/icons/menu/edited_status@2x.png new file mode 100644 index 000000000..004167a14 Binary files /dev/null and b/Telegram/Resources/icons/menu/edited_status@2x.png differ diff --git a/Telegram/Resources/icons/menu/edited_status@3x.png b/Telegram/Resources/icons/menu/edited_status@3x.png new file mode 100644 index 000000000..1acdf8ea9 Binary files /dev/null and b/Telegram/Resources/icons/menu/edited_status@3x.png differ diff --git a/Telegram/Resources/icons/player/player_settings.png b/Telegram/Resources/icons/player/player_settings.png new file mode 100644 index 000000000..65a420de0 Binary files /dev/null and b/Telegram/Resources/icons/player/player_settings.png differ diff --git a/Telegram/Resources/icons/player/player_settings@2x.png b/Telegram/Resources/icons/player/player_settings@2x.png new file mode 100644 index 000000000..af78b2666 Binary files /dev/null and b/Telegram/Resources/icons/player/player_settings@2x.png differ diff --git a/Telegram/Resources/icons/player/player_settings@3x.png b/Telegram/Resources/icons/player/player_settings@3x.png new file mode 100644 index 000000000..da1ec8c72 Binary files /dev/null and b/Telegram/Resources/icons/player/player_settings@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/input_round_s.png b/Telegram/Resources/icons/voice_lock/input_round_s.png new file mode 100644 index 000000000..549dd1ab1 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s.png differ diff --git a/Telegram/Resources/icons/voice_lock/input_round_s@2x.png b/Telegram/Resources/icons/voice_lock/input_round_s@2x.png new file mode 100644 index 000000000..4ab801a46 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/input_round_s@3x.png b/Telegram/Resources/icons/voice_lock/input_round_s@3x.png new file mode 100644 index 000000000..878aa7e6a Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/input_round_s@3x.png differ diff --git a/Telegram/Resources/iv_html/page.js b/Telegram/Resources/iv_html/page.js index fda34772f..9bd67163a 100644 --- a/Telegram/Resources/iv_html/page.js +++ b/Telegram/Resources/iv_html/page.js @@ -72,6 +72,9 @@ var IV = { } }, frameKeyDown: function (e) { + const key0 = (e.key === '0') + || (e.code === 'Key0') + || (e.keyCode === 48); const keyW = (e.key === 'w') || (e.code === 'KeyW') || (e.keyCode === 87); @@ -81,12 +84,12 @@ var IV = { const keyM = (e.key === 'm') || (e.code === 'KeyM') || (e.keyCode === 77); - if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) { + if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM || key0)) { e.preventDefault(); IV.notify({ event: 'keydown', modifier: e.ctrlKey ? 'ctrl' : 'cmd', - key: keyW ? 'w' : keyQ ? 'q' : 'm', + key: key0 ? '0' : keyW ? 'w' : keyQ ? 'q' : 'm', }); } else if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e13e83477..69205e1f4 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -499,8 +499,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_notify_global" = "Global settings"; "lng_settings_notify_title" = "Notifications for chats"; "lng_settings_desktop_notify" = "Desktop notifications"; -"lng_settings_native_title" = "Native notifications"; +"lng_settings_native_title" = "System integration"; "lng_settings_use_windows" = "Use Windows notifications"; +"lng_settings_skip_in_focus" = "Respect system Focus mode"; "lng_settings_use_native_notifications" = "Use native notifications"; "lng_settings_notifications_position" = "Location on the screen"; "lng_settings_notifications_count" = "Notifications count"; @@ -681,6 +682,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_messages_privacy" = "Messages"; "lng_settings_voices_privacy" = "Voice messages"; "lng_settings_bio_privacy" = "Bio"; +"lng_settings_gifts_privacy" = "Gifts"; "lng_settings_birthday_privacy" = "Date of Birth"; "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages."; "lng_settings_privacy_premium_link" = "Telegram Premium"; @@ -1161,19 +1163,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_blocked_list_subtitle#other" = "{count} blocked users"; "lng_edit_privacy_everyone" = "Everybody"; +"lng_edit_privacy_no_miniapps" = "Not Mini Apps"; "lng_edit_privacy_contacts" = "My contacts"; "lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_contacts_and_premium" = "Contacts & Premium"; +"lng_edit_privacy_contacts_and_miniapps" = "Contacts & Mini Apps"; "lng_edit_privacy_nobody" = "Nobody"; "lng_edit_privacy_premium" = "Premium users"; +"lng_edit_privacy_miniapps" = "Mini Apps"; "lng_edit_privacy_exceptions" = "Add exceptions"; "lng_edit_privacy_user_types" = "User types"; "lng_edit_privacy_users_and_groups" = "Users and groups"; "lng_edit_privacy_premium_status" = "all Telegram Premium subscribers"; +"lng_edit_privacy_miniapps_status" = "web mini apps that you use"; "lng_edit_privacy_exceptions_count#one" = "{count} user"; "lng_edit_privacy_exceptions_count#other" = "{count} users"; "lng_edit_privacy_exceptions_premium_and" = "Premium & {users}"; +"lng_edit_privacy_exceptions_miniapps_and" = "Mini Apps & {users}"; "lng_edit_privacy_exceptions_add" = "Add users"; "lng_edit_privacy_phone_number_title" = "Phone number privacy"; @@ -1227,6 +1234,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_birthday_yet" = "You haven't entered your date of birth yet.\n{link}"; "lng_edit_privacy_birthday_yet_link" = "Add my birthday >"; +"lng_edit_privacy_gifts_title" = "Gifts"; +"lng_edit_privacy_gifts_header" = "Who can display gifts on my profile"; +"lng_edit_privacy_gifts_always_empty" = "Always allow"; +"lng_edit_privacy_gifts_never_empty" = "Never allow"; +"lng_edit_privacy_gifts_exceptions" = "Choose whether gifts from specific senders need your approval before they're visible to others on your profile."; +"lng_edit_privacy_gifts_always_title" = "Always allow"; +"lng_edit_privacy_gifts_never_title" = "Never allow"; + "lng_edit_privacy_calls_title" = "Calls"; "lng_edit_privacy_calls_header" = "Who can call me"; "lng_edit_privacy_calls_always_empty" = "Always allow"; @@ -1460,11 +1475,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_open_app" = "Open App"; "lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; "lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; +"lng_profile_bot_permissions_title" = "Allow access to"; +"lng_profile_bot_emoji_status_access" = "Emoji Status"; "lng_info_add_as_contact" = "Add to contacts"; "lng_profile_shared_media" = "Shared media"; "lng_profile_suggest_photo" = "Suggest Profile Photo"; +"lng_profile_suggest_photo_from_clipboard" = "Suggest From Clipboard"; "lng_profile_set_photo_for" = "Set Profile Photo"; +"lng_profile_set_photo_for_from_clipboard" = "Set From Clipboard"; "lng_profile_photo_reset" = "Reset to Original"; +"lng_profile_photo_from_clipboard" = "From clipboard"; "lng_profile_suggest_sure" = "You can suggest {user} to set this photo for their Telegram profile."; "lng_profile_suggest_button" = "Suggest"; "lng_profile_set_personal_sure" = "Only you will see this photo and it will replace any photo {user} sets for themselves."; @@ -1593,6 +1613,8 @@ 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_balance_currency" = "Toncoin"; +"lng_manage_peer_bot_balance_credits" = "Stars"; "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"; @@ -1870,6 +1892,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_got_subtitle" = "Gift from {user}"; "lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star."; "lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars."; +"lng_action_gift_got_gift_text" = "You can keep this gift on your page."; +"lng_action_gift_can_remove_text" = "You can remove this gift from your page."; "lng_action_gift_sent_subtitle" = "Gift for {user}"; "lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star."; "lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars."; @@ -2097,6 +2121,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_public_link_copied" = "Link copied to clipboard."; "lng_context_about_private_link" = "This link will only work for members of this chat."; +"lng_public_post_private_hint_ctrl" = "Use Ctrl+Click to copy a non-public link."; +"lng_public_post_private_hint_cmd" = "Use Cmd+Click to copy a non-public link."; "lng_forwarded" = "Forwarded from {user}"; "lng_forwarded_story" = "Story from {user}"; @@ -2115,8 +2141,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recommended_message_title" = "Recommended"; "lng_edited" = "edited"; "lng_commented" = "commented"; +"lng_approximate" = "appx."; "lng_edited_date" = "Edited: {date}"; "lng_sent_date" = "Sent: {date}"; +"lng_approximate_about" = "Estimated date of video publishing."; "lng_views_tooltip#one" = "Views: {count}"; "lng_views_tooltip#other" = "Views: {count}"; "lng_forwards_tooltip#one" = "Shares: {count}"; @@ -2151,6 +2179,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_media_cancel" = "Cancel"; "lng_media_video" = "Video"; "lng_media_audio" = "Voice message"; +"lng_media_round" = "Video message"; "lng_media_auto_settings" = "Automatic media download"; "lng_media_auto_in_private" = "In private chats"; @@ -2404,6 +2433,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_history_tab_out" = "Outgoing"; "lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; "lng_credits_summary_balance" = "Balance"; +"lng_credits_more_options" = "More Options"; +"lng_credits_balance_me" = "your balance"; +"lng_credits_buy_button" = "Buy More Stars"; "lng_credits_gift_button" = "Gift Stars to Friends"; "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**?"; @@ -2442,18 +2474,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_giveaway_name" = "Received Prize"; "lng_credits_box_history_entry_gift_sent" = "Sent Gift"; "lng_credits_box_history_entry_gift_converted" = "Converted Gift"; +"lng_credits_box_history_entry_gift_unavailable" = "Unavailable"; +"lng_credits_box_history_entry_gift_sold_out" = "This gift has sold out"; "lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}"; "lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}"; "lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}"; "lng_credits_box_history_entry_gift_examples" = "Examples"; "lng_credits_box_history_entry_ads" = "Ads Platform"; "lng_credits_box_history_entry_premium_bot" = "Stars Top-Up"; +"lng_credits_box_history_entry_api" = "Paid Broadcast"; +"lng_credits_box_history_entry_floodskip_about#one" = "{count} Message"; +"lng_credits_box_history_entry_floodskip_about#other" = "{count} Messages"; +"lng_credits_box_history_entry_floodskip_row" = "Messages"; "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_message" = "Message"; "lng_credits_box_history_entry_about" = "You can dispute this transaction {link}."; "lng_credits_box_history_entry_about_link" = "here"; "lng_credits_box_history_entry_reaction_name" = "Star Reaction"; @@ -2988,6 +3027,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_link_reason_unclaimed" = "Incomplete Giveaway"; "lng_gift_link_reason_chosen" = "You were selected by the channel"; "lng_gift_link_label_date" = "Date"; +"lng_gift_link_label_first_sale" = "First Sale"; +"lng_gift_link_label_last_sale" = "Last Sale"; +"lng_gift_link_label_value" = "Value"; "lng_gift_link_also_send" = "You can also {link} to a friend as a gift."; "lng_gift_link_also_send_link" = "send this link"; "lng_gift_link_use" = "Use Link"; @@ -3044,8 +3086,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_convert_to_stars#one" = "Convert to {count} Star"; "lng_gift_convert_to_stars#other" = "Convert to {count} Stars"; "lng_gift_convert_sure_title" = "Convert Gift to Stars"; -"lng_gift_convert_sure_text#one" = "Do you want to convert this gift from {user} to **{count} Star**?\n\nThis action cannot be undone."; -"lng_gift_convert_sure_text#other" = "Do you want to convert this gift from {user} to **{count} Stars**?\n\nThis action cannot be undone."; +"lng_gift_convert_sure_confirm#one" = "Do you want to convert this gift from {user} to **{count} Star**?"; +"lng_gift_convert_sure_confirm#other" = "Do you want to convert this gift from {user} to **{count} Stars**?"; +"lng_gift_convert_sure_limit#one" = "Conversion is available for the next **{count} day**."; +"lng_gift_convert_sure_limit#other" = "Conversion is available for the next **{count} days**."; +"lng_gift_convert_sure_caution" = "This action cannot be undone. This will permanently destroy the gift."; "lng_gift_convert_sure" = "Convert"; "lng_gift_display_done" = "The gift is now shown on your profile page."; "lng_gift_display_done_hide" = "The gift is now hidden from your profile page."; @@ -3054,6 +3099,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sold_out_title" = "Sold Out!"; "lng_gift_sold_out_text#one" = "All {count} gift was already sold."; "lng_gift_sold_out_text#other" = "All {count} gifts were already sold."; +"lng_gift_send_small" = "send a gift"; +"lng_gift_sell_small#one" = "sell for {count} Star"; +"lng_gift_sell_small#other" = "sell for {count} Stars"; "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; @@ -3243,11 +3291,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_record_cancel" = "Release outside this field to cancel"; "lng_record_cancel_stories" = "Release outside to cancel"; "lng_record_lock_cancel_sure" = "Do you want to stop recording and discard your voice message?"; +"lng_record_lock_cancel_sure_round" = "Do you want to stop recording and discard your video message?"; "lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?"; +"lng_record_listen_cancel_sure_round" = "Do you want to discard your recorded video message?"; "lng_record_lock_discard" = "Discard"; "lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message."; +"lng_record_voice_tip" = "Hold to record audio. Click to switch to video."; +"lng_record_video_tip" = "Hold to record video. Click to switch to audio."; +"lng_record_audio_problem" = "Could not start audio recording. Please check your microphone."; +"lng_record_video_problem" = "Could not start video recording. Please check your camera."; "lng_record_once_first_tooltip" = "Click to set this message to **Play Once**."; "lng_record_once_active_tooltip" = "The recipient will be able to listen only once."; +"lng_record_once_active_video" = "The recipient will be able to watch only once."; "lng_will_be_notified" = "Subscribers will be notified when you post."; "lng_wont_be_notified" = "Subscribers will receive a silent notification."; "lng_willbe_history" = "Select a chat to start messaging"; @@ -3276,6 +3331,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_scheduled_send_now" = "Send message now?"; "lng_scheduled_send_now_many#one" = "Send {count} message now?"; "lng_scheduled_send_now_many#other" = "Send {count} messages now?"; +"lng_scheduled_video_tip_title" = "Improving video..."; +"lng_scheduled_video_tip_text" = "The video will be published after it's optimized for the best viewing experience."; +"lng_scheduled_video_tip" = "Processing video may take a few minutes."; +"lng_scheduled_video_published" = "Video Published."; +"lng_scheduled_video_view" = "View"; "lng_replies_view#one" = "View {count} Reply"; "lng_replies_view#other" = "View {count} Replies"; @@ -3359,6 +3419,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_settings" = "Settings"; "lng_bot_open" = "Open Bot"; "lng_bot_terms" = "Terms of Use"; +"lng_bot_privacy" = "Privacy Policy"; "lng_bot_reload_page" = "Reload Page"; "lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachment menu so you can access it from any chat."; "lng_bot_add_to_menu_done" = "Bot added to the menu."; @@ -3374,6 +3435,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_add_to_side_menu_done" = "Bot added to the main menu."; "lng_bot_no_scan_qr" = "QR Codes for bots are not supported on Desktop. Please use one of Telegram's mobile apps."; "lng_bot_no_share_story" = "Sharing to Stories is not supported on Desktop. Please use one of Telegram's mobile apps."; +"lng_bot_emoji_status_confirm" = "Confirm"; +"lng_bot_emoji_status_title" = "Set Emoji Status"; +"lng_bot_emoji_status_text" = "Do you want to set this emoji status suggested by {bot}?"; +"lng_bot_emoji_status_access_text" = "{bot} requests access to set your **emoji status**. You will be able to revoke this access in the profile page of {name}."; +"lng_bot_emoji_status_access_allow" = "Allow"; +"lng_bot_share_prepared_title" = "Share Message"; +"lng_bot_share_prepared_about" = "{bot} mini app suggests you to send this message to a chat you select."; +"lng_bot_share_prepared_button" = "Share With..."; +"lng_bot_download_file" = "Download File"; +"lng_bot_download_file_sure" = "{bot} suggests you download the following file:"; +"lng_bot_download_file_button" = "Download"; +"lng_bot_download_starting" = "Starting..."; +"lng_bot_download_failed" = "Failed. {retry}"; +"lng_bot_download_retry" = "Retry"; "lng_bot_status_users#one" = "{count} monthly user"; "lng_bot_status_users#other" = "{count} monthly users"; @@ -3585,6 +3660,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_make_paid" = "Make This Content Paid"; "lng_context_change_price" = "Change Price"; +"lng_context_mention" = "Mention"; +"lng_context_search_from" = "Search messages"; + "lng_factcheck_title" = "Fact Check"; "lng_factcheck_placeholder" = "Add Facts or Context"; "lng_factcheck_whats_this" = "what's this?"; @@ -3682,6 +3760,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; +"lng_reply_in_author" = "Message author"; +"lng_reply_in_chats_list" = "Your chats"; "lng_reply_show_in_chat" = "Show in Chat"; "lng_reply_remove" = "Do Not Reply"; "lng_reply_about_quote" = "You can select a specific part to quote."; @@ -3869,6 +3949,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_downloads" = "Downloads"; "lng_mediaview_playback_speed" = "Playback speed: {speed}"; "lng_mediaview_rotate_video" = "Rotate video"; +"lng_mediaview_quality_auto" = "Auto"; "lng_theme_preview_title" = "Theme Preview"; "lng_theme_preview_generating" = "Generating color theme preview..."; @@ -3950,7 +4031,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_webview_no_use" = "Unfortunately, you can't use payments with current system configuration."; "lng_payments_webview_install_edge" = "Please install {link}."; -"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkitgtk-6.0/webkit2gtk-4.1/webkit2gtk-4.0) using your package manager."; +"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkit2gtk-4.1/webkit2gtk-4.0) using your package manager."; +"lng_payments_webview_enable_opengl" = "Please enable OpenGL in application settings."; +"lng_payments_webview_switch_x11" = "Unsupported display server. Please switch to X11."; "lng_payments_webview_update_windows" = "Please update your system to Windows 8.1 or later."; "lng_payments_sure_close" = "Are you sure you want to close this payment form? The changes you made will be lost."; "lng_payments_receipt_label" = "Receipt"; @@ -4310,7 +4393,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'."; "lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights."; "lng_rights_about_add_admins_no" = "This admin will not be able to add new admins."; -"lng_rights_about_by" = "This admin promoted by {user} at {date}."; +"lng_rights_about_by" = "This admin promoted by {user} on {date}."; "lng_rights_about_admin_cant_edit" = "You can't edit the rights of this admin."; "lng_rights_about_restriction_cant_edit" = "You cannot change the restrictions for this user."; @@ -4389,8 +4472,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_chat_files" = "Files"; "lng_rights_chat_voice_messages" = "Voice messages"; "lng_rights_chat_video_messages" = "Video messages"; -"lng_rights_chat_restricted_by" = "Restricted by {user} at {date}."; -"lng_rights_chat_banned_by" = "Banned by {user} at {date}."; +"lng_rights_chat_restricted_by" = "Restricted by {user} on {date}."; +"lng_rights_chat_banned_by" = "Banned by {user} on {date}."; "lng_rights_chat_banned_until_header" = "Restricted until"; "lng_rights_chat_banned_forever" = "Forever"; "lng_rights_chat_banned_day#one" = "For {count} day"; @@ -4999,6 +5082,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_outdated_now" = "So Telegram Desktop can update to newer versions."; "lng_filters_all" = "All chats"; +"lng_filters_all_short" = "All"; "lng_filters_setup" = "Edit"; "lng_filters_title" = "Folders"; "lng_filters_subtitle" = "My folders"; @@ -5053,6 +5137,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_toast_add" = "{chat} added to {folder} folder"; "lng_filters_toast_remove" = "{chat} removed from {folder} folder"; "lng_filters_shareable_status" = "shareable folder"; +"lng_filters_view_subtitle" = "Tabs view"; +"lng_filters_vertical" = "Tabs on the left"; +"lng_filters_horizontal" = "Tabs at the top"; "lng_filters_delete_sure" = "Are you sure you want to delete this folder? This will also deactivate all the invite links created to share this folder."; "lng_filters_link" = "Share Folder"; @@ -5172,13 +5259,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sponsored_revenued_subtitle" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:"; "lng_sponsored_revenued_info1_title" = "Respect Your Privacy"; "lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them."; +"lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them."; "lng_sponsored_revenued_info2_title" = "Help the Channel Creator"; +"lng_sponsored_revenued_info2_bot_title" = "Help the Bot Developer"; "lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed."; +"lng_sponsored_revenued_info2_bot_description" = "50% of the revenue from Telegram Ads goes to the developer of the mini app where they are displayed."; "lng_sponsored_revenued_info3_title" = "Can Be Removed"; "lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; "lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers."; +"lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}."; "lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?"; "lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; +"lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}"; +"lng_sponsored_top_bar_hide" = "remove"; "lng_telegram_features_url" = "https://t.me/TelegramTips"; @@ -5482,6 +5575,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_earn_title" = "Monetization"; "lng_channel_earn_about" = "Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}"; +"lng_channel_earn_about_bot" = "Telegram shares 50% of the revenue from ads displayed in your bot. {link}"; "lng_channel_earn_about_link" = "Learn more {emoji}"; "lng_channel_earn_overview_title" = "Rewards overview"; "lng_channel_earn_available" = "Rewards available for collection"; @@ -5514,8 +5608,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_earn_cpm#one" = "{emoji} {count} CPM"; "lng_channel_earn_cpm#other" = "{emoji} {count} CPM"; "lng_channel_earn_learn_title" = "Earn From Your Channel"; +"lng_channel_earn_bot_learn_title" = "Earn From Your Bot"; "lng_channel_earn_learn_in_subtitle" = "Telegram Ads"; "lng_channel_earn_learn_in_about" = "Telegram can display ads in your channel."; +"lng_channel_earn_learn_bot_in_about" = "Telegram can display ads in your bot."; "lng_channel_earn_learn_split_subtitle" = "50:50 revenue split"; "lng_channel_earn_learn_split_about" = "You can receive 50% of the ad revenue as rewards in TON."; "lng_channel_earn_learn_out_subtitle" = "Flexible withdrawals"; @@ -5552,6 +5648,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_earn_credits_out_minimal" = "You cannot withdraw less than {link}."; "lng_bot_earn_credits_out_minimal_link#one" = "{count} star"; "lng_bot_earn_credits_out_minimal_link#other" = "{count} stars"; +"lng_bot_copy_text_tooltip" = "Copy to Clipboard: {text}"; "lng_contact_add" = "Add"; "lng_contact_send_message" = "Message"; @@ -5562,6 +5659,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_iv_window_title" = "Instant View"; "lng_iv_wrong_layout" = "Wrong layout?"; "lng_iv_not_supported" = "This link appears to be invalid."; +"lng_iv_zoom_tooltip_ctrl" = "Hold Ctrl to zoom by 5%.\nHold Alt to zoom by 1%."; +"lng_iv_zoom_tooltip_cmd" = "Hold Cmd to zoom by 5%.\nHold Alt to zoom by 1%."; "lng_limit_download_title" = "Download speed limited"; "lng_limit_download_subscribe" = "Subscribe to {link} to increase download speed {increase}."; @@ -5599,6 +5698,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channels_recommended" = "Similar channels"; "lng_bot_apps_your" = "Apps you use"; "lng_bot_apps_popular" = "Grossing apps"; +"lng_bot_apps_which" = "Which apps are included here? {link}"; +"lng_bot_apps_which_link" = "Learn >"; + +"lng_popular_apps_info_title" = "Top Mini Apps"; +"lng_popular_apps_info_text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average."; +"lng_popular_apps_info_bot" = "@botfather"; +"lng_popular_apps_info_here" = "here"; +"lng_popular_apps_info_url" = "https://core.telegram.org/bots/webapps#launching-the-main-mini-app"; +"lng_popular_apps_info_confirm" = "Understood"; "lng_font_box_title" = "Choose font family"; "lng_font_default" = "Default"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index ac6ff1dbf..f60061f99 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -28,6 +28,7 @@ ../../animations/collectible_phone.tgs ../../animations/search.tgs ../../animations/noresults.tgs + ../../animations/hello_status.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index db6371e82..7d0463c4e 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -7,6 +7,7 @@ ../../art/logo_256.png ../../art/logo_256_no_margin.png ../../art/themeimage.jpg + ../../art/round_placeholder.jpg ../../day-blue.tdesktop-theme ../../night.tdesktop-theme ../../night-green.tdesktop-theme diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 798a9e220..8d93b797d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.8.2.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 4404a4c05..4f0000cc7 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,6,3,0 - PRODUCTVERSION 5,6,3,0 + FILEVERSION 5,8,2,0 + PRODUCTVERSION 5,8,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.6.3.0" + VALUE "FileVersion", "5.8.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.6.3.0" + VALUE "ProductVersion", "5.8.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index fbe2ad09e..3cb7b8d82 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,6,3,0 - PRODUCTVERSION 5,6,3,0 + FILEVERSION 5,8,2,0 + PRODUCTVERSION 5,8,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "5.6.3.0" + VALUE "FileVersion", "5.8.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.6.3.0" + VALUE "ProductVersion", "5.8.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index ee6b97b58..2c8410b80 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_limits_box.h" #include "boxes/filters/edit_filter_links.h" // FilterChatStatusText #include "core/application.h" +#include "core/core_settings.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_filters.h" @@ -152,6 +153,7 @@ void InitFilterLinkHeader( .badge = (type == Ui::FilterLinkHeaderType::AddingChats ? std::move(count) : rpl::single(0)), + .horizontalFilters = Core::App().settings().chatFiltersHorizontal(), }); const auto widget = header.widget; widget->resizeToWidth(st::boxWideWidth); diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp new file mode 100644 index 000000000..fe96accd6 --- /dev/null +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp @@ -0,0 +1,128 @@ +/* +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_chat_filters_remove_manager.h" + +#include "api/api_chat_filters.h" +#include "apiwrap.h" +#include "data/data_chat_filters.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/ui_utility.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" + +namespace Api { +namespace { + +void RemoveChatFilter( + not_null session, + FilterId filterId, + std::vector> leave) { + const auto api = &session->api(); + session->data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag(0)), + MTP_int(filterId), + MTPDialogFilter())); + if (leave.empty()) { + api->request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)), + MTP_int(filterId), + MTPDialogFilter() + )).send(); + } else { + api->request(MTPchatlists_LeaveChatlist( + MTP_inputChatlistDialogFilter(MTP_int(filterId)), + MTP_vector(ranges::views::all( + leave + ) | ranges::views::transform([](not_null peer) { + return MTPInputPeer(peer->input); + }) | ranges::to>()) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + } +} + +} // namespace + +RemoveComplexChatFilter::RemoveComplexChatFilter() = default; + +void RemoveComplexChatFilter::request( + QPointer widget, + base::weak_ptr weak, + FilterId id) { + const auto session = &weak->session(); + const auto &list = session->data().chatsFilters().list(); + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + const auto filter = (i != end(list)) ? *i : Data::ChatFilter(); + const auto has = filter.hasMyLinks(); + const auto confirm = [=](Fn action, bool onlyWhenHas = false) { + if (!has && onlyWhenHas) { + action(); + return; + } + weak->window().show(Ui::MakeConfirmBox({ + .text = (has + ? tr::lng_filters_delete_sure() + : tr::lng_filters_remove_sure()), + .confirmed = [=](Fn &&close) { close(); action(); }, + .confirmText = (has + ? tr::lng_box_delete() + : tr::lng_filters_remove_yes()), + .confirmStyle = &st::attentionBoxButton, + })); + }; + const auto simple = [=] { + confirm([=] { RemoveChatFilter(session, id, {}); }); + }; + const auto suggestRemoving = Api::ExtractSuggestRemoving(filter); + if (suggestRemoving.empty()) { + simple(); + return; + } else if (_removingRequestId) { + if (_removingId == id) { + return; + } + session->api().request(_removingRequestId).cancel(); + } + _removingId = id; + _removingRequestId = session->api().request( + MTPchatlists_GetLeaveChatlistSuggestions( + MTP_inputChatlistDialogFilter( + MTP_int(id))) + ).done(crl::guard(widget, [=, this](const MTPVector &result) { + _removingRequestId = 0; + const auto suggestRemovePeers = ranges::views::all( + result.v + ) | ranges::views::transform([=](const MTPPeer &peer) { + return session->data().peer(peerFromMTP(peer)); + }) | ranges::to_vector; + const auto chosen = crl::guard(widget, [=]( + std::vector> peers) { + RemoveChatFilter(session, id, std::move(peers)); + }); + confirm(crl::guard(widget, [=] { + Api::ProcessFilterRemove( + weak, + filter.title(), + filter.iconEmoji(), + suggestRemoving, + suggestRemovePeers, + chosen); + }), true); + })).fail(crl::guard(widget, [=, this] { + _removingRequestId = 0; + simple(); + })).send(); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h new file mode 100644 index 000000000..ce92b2df3 --- /dev/null +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h @@ -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 +*/ +#pragma once + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Api { + +class RemoveComplexChatFilter final { +public: + RemoveComplexChatFilter(); + + void request( + QPointer widget, + base::weak_ptr weak, + FilterId id); + +private: + FilterId _removingId = 0; + mtpRequestId _removingRequestId = 0; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index be07e72ac..0a53c61dd 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits_graphics.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/empty_userpic.h" @@ -129,6 +130,7 @@ void ConfirmSubscriptionBox( struct State final { std::shared_ptr photoMedia; std::unique_ptr photoEmpty; + QImage frame; std::optional api; Ui::RpWidget* saveButton = nullptr; @@ -146,25 +148,45 @@ void ConfirmSubscriptionBox( const auto userpic = userpicWrap->entity(); const auto photoSize = st::confirmInvitePhotoSize; userpic->resize(Size(photoSize)); + const auto creditsIconSize = photoSize / 3; + const auto creditsIconCallback = + Ui::PaintOutlinedColoredCreditsIconCallback( + creditsIconSize, + 1.5); + state->frame = QImage( + Size(photoSize * style::DevicePixelRatio()), + QImage::Format_ARGB32_Premultiplied); + state->frame.setDevicePixelRatio(style::DevicePixelRatio()); const auto options = Images::Option::RoundCircle; userpic->paintRequest( ) | rpl::start_with_next([=, small = Data::PhotoSize::Small] { - auto p = QPainter(userpic); - if (state->photoMedia) { - if (const auto image = state->photoMedia->image(small)) { - p.drawPixmap( + state->frame.fill(Qt::transparent); + { + auto p = QPainter(&state->frame); + if (state->photoMedia) { + if (const auto image = state->photoMedia->image(small)) { + p.drawPixmap( + 0, + 0, + image->pix(Size(photoSize), { .options = options })); + } + } else if (state->photoEmpty) { + state->photoEmpty->paintCircle( + p, 0, 0, - image->pix(Size(photoSize), { .options = options })); + userpic->width(), + photoSize); + } + if (creditsIconCallback) { + p.translate( + photoSize - creditsIconSize, + photoSize - creditsIconSize); + creditsIconCallback(p); } - } else if (state->photoEmpty) { - state->photoEmpty->paintCircle( - p, - 0, - 0, - userpic->width(), - photoSize); } + auto p = QPainter(userpic); + p.drawImage(0, 0, state->frame); }, userpicWrap->lifetime()); userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents); if (photo) { diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 4ef4b09c8..80e995219 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -39,8 +39,8 @@ constexpr auto kTransactionsLimit = 100; 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()) { + media.match([&](const MTPDmessageMediaPhoto &data) { + if (const auto inner = data.vphoto()) { const auto photo = owner->processPhoto(*inner); if (!photo->isNull()) { extended.push_back(CreditsHistoryMedia{ @@ -49,9 +49,11 @@ constexpr auto kTransactionsLimit = 100; }); } } - }, [&](const MTPDmessageMediaDocument &document) { - if (const auto inner = document.vdocument()) { - const auto document = owner->processDocument(*inner); + }, [&](const MTPDmessageMediaDocument &data) { + if (const auto inner = data.vdocument()) { + const auto document = owner->processDocument( + *inner, + data.valt_documents()); if (document->isAnimation() || document->isVideoFile() || document->isGifv()) { @@ -71,7 +73,9 @@ constexpr auto kTransactionsLimit = 100; return PeerId(0); }).value; const auto stargift = tl.data().vstargift(); + const auto reaction = tl.data().is_reaction(); const auto incoming = (int64(tl.data().vstars().v) >= 0); + const auto saveActorId = (reaction || !extended.empty()) && incoming; return Data::CreditsHistoryEntry{ .id = qs(tl.data().vid()), .title = qs(tl.data().vtitle().value_or_empty()), @@ -81,12 +85,13 @@ constexpr auto kTransactionsLimit = 100; .extended = std::move(extended), .credits = tl.data().vstars().v, .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()), - .barePeerId = barePeerId, + .barePeerId = saveActorId ? peer->id.value : barePeerId, .bareGiveawayMsgId = uint64( tl.data().vgiveaway_post_id().value_or_empty()), .bareGiftStickerId = (stargift ? owner->processDocument(stargift->data().vsticker())->id : 0), + .bareActorId = saveActorId ? barePeerId : uint64(0), .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { return Data::CreditsHistoryEntry::PeerType::Peer; }, [](const MTPDstarsTransactionPeerPlayMarket &) { @@ -101,6 +106,8 @@ constexpr auto kTransactionsLimit = 100; return Data::CreditsHistoryEntry::PeerType::PremiumBot; }, [](const MTPDstarsTransactionPeerAds &) { return Data::CreditsHistoryEntry::PeerType::Ads; + }, [](const MTPDstarsTransactionPeerAPI &) { + return Data::CreditsHistoryEntry::PeerType::API; }), .subscriptionUntil = tl.data().vsubscription_period() ? base::unixtime::parse(base::unixtime::now() @@ -110,10 +117,12 @@ constexpr auto kTransactionsLimit = 100; ? base::unixtime::parse(tl.data().vtransaction_date()->v) : QDateTime(), .successLink = qs(tl.data().vtransaction_url().value_or_empty()), - .convertStars = int(stargift + .starsConverted = int(stargift ? stargift->data().vconvert_stars().v : 0), + .floodSkip = int(tl.data().vfloodskip_number().value_or(0)), .converted = stargift && incoming, + .stargift = stargift.has_value(), .reaction = tl.data().is_reaction(), .refunded = tl.data().is_refund(), .pending = tl.data().is_pending(), @@ -166,7 +175,8 @@ constexpr auto kTransactionsLimit = 100; .balance = status.data().vbalance().v, .subscriptionsMissingBalance = status.data().vsubscriptions_missing_balance().value_or_empty(), - .allLoaded = !status.data().vnext_offset().has_value(), + .allLoaded = !status.data().vnext_offset().has_value() + && !status.data().vsubscriptions_next_offset().has_value(), .token = qs(status.data().vnext_offset().value_or_empty()), .tokenSubscriptions = qs( status.data().vsubscriptions_next_offset().value_or_empty()), diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h index d2e819e08..e178cb866 100644 --- a/Telegram/SourceFiles/api/api_credits.h +++ b/Telegram/SourceFiles/api/api_credits.h @@ -99,8 +99,8 @@ public: [[nodiscard]] Data::CreditsEarnStatistics data() const; private: + const bool _isUser = false; Data::CreditsEarnStatistics _data; - bool _isUser = false; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/api/api_earn.cpp b/Telegram/SourceFiles/api/api_earn.cpp index f05e5f618..c44690151 100644 --- a/Telegram/SourceFiles/api/api_earn.cpp +++ b/Telegram/SourceFiles/api/api_earn.cpp @@ -40,6 +40,7 @@ void HandleWithdrawalButton( std::shared_ptr show) { Expects(receiver.currencyReceiver || (receiver.creditsReceiver && receiver.creditsAmount)); + struct State { rpl::lifetime lifetime; bool loading = false; @@ -58,8 +59,7 @@ void HandleWithdrawalButton( const auto processOut = [=] { if (state->loading) { return; - } - if (peer && !receiver.creditsAmount()) { + } else if (peer && !receiver.creditsAmount()) { return; } state->loading = true; @@ -89,12 +89,15 @@ void HandleWithdrawalButton( } }; const auto fail = [=](const MTP::Error &error) { - show->showToast(error.type()); + const auto message = error.type(); + if (box && !box->handleCustomCheckError(message)) { + show->showToast(message); + } }; if (channel) { session->api().request( MTPstats_GetBroadcastRevenueWithdrawalUrl( - channel->inputChannel, + channel->input, result.result )).done([=](const ChannelOutUrl &r) { done(qs(r.data().vurl())); @@ -134,7 +137,7 @@ void HandleWithdrawalButton( if (channel) { session->api().request( MTPstats_GetBroadcastRevenueWithdrawalUrl( - channel->inputChannel, + channel->input, MTP_inputCheckPasswordEmpty() )).fail(fail).send(); } else if (peer) { diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 84ce45783..d88a5ddce 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -772,10 +772,13 @@ std::optional FromTL( return StarGift{ .id = uint64(data.vid().v), .stars = int64(data.vstars().v), - .convertStars = int64(data.vconvert_stars().v), + .starsConverted = int64(data.vconvert_stars().v), .document = document, .limitedLeft = remaining.value_or_empty(), .limitedCount = total.value_or_empty(), + .firstSaleDate = data.vfirst_sale_date().value_or_empty(), + .lastSaleDate = data.vlast_sale_date().value_or_empty(), + .birthday = data.is_birthday(), }; } @@ -789,7 +792,7 @@ std::optional FromTL( return {}; } return UserStarGift{ - .gift = std::move(*parsed), + .info = std::move(*parsed), .message = (data.vmessage() ? TextWithEntities{ .text = qs(data.vmessage()->data().vtext()), @@ -798,7 +801,7 @@ std::optional FromTL( data.vmessage()->data().ventities().v), } : TextWithEntities()), - .convertStars = int64(data.vconvert_stars().value_or_empty()), + .starsConverted = int64(data.vconvert_stars().value_or_empty()), .fromId = (data.vfrom_id() ? peerFromUser(data.vfrom_id()->v) : PeerId()), diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 805b68e03..23455ba18 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -76,16 +76,23 @@ struct GiftOptionData { struct StarGift { uint64 id = 0; int64 stars = 0; - int64 convertStars = 0; + int64 starsConverted = 0; not_null document; int limitedLeft = 0; int limitedCount = 0; + TimeId firstSaleDate = 0; + TimeId lastSaleDate = 0; + bool birthday = false; + + friend inline bool operator==( + const StarGift &, + const StarGift &) = default; }; struct UserStarGift { - StarGift gift; + StarGift info; TextWithEntities message; - int64 convertStars = 0; + int64 starsConverted = 0; PeerId fromId = 0; MsgId messageId = 0; TimeId date = 0; diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp index 309e2d240..f3b0658bd 100644 --- a/Telegram/SourceFiles/api/api_report.cpp +++ b/Telegram/SourceFiles/api/api_report.cpp @@ -40,29 +40,20 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) { } // namespace -void SendReport( +void SendPhotoReport( std::shared_ptr show, not_null peer, Ui::ReportReason reason, const QString &comment, - std::variant> data) { - auto done = [=] { + not_null photo) { + peer->session().api().request(MTPaccount_ReportProfilePhoto( + peer->input, + photo->mtpInput(), + ReasonToTL(reason), + MTP_string(comment) + )).done([=] { show->showToast(tr::lng_report_thanks(tr::now)); - }; - v::match(data, [&](v::null_t) { - peer->session().api().request(MTPaccount_ReportPeer( - peer->input, - ReasonToTL(reason), - MTP_string(comment) - )).done(std::move(done)).send(); - }, [&](not_null photo) { - peer->session().api().request(MTPaccount_ReportProfilePhoto( - peer->input, - photo->mtpInput(), - ReasonToTL(reason), - MTP_string(comment) - )).done(std::move(done)).send(); - }); + }).send(); } auto CreateReportMessagesOrStoriesCallback( diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h index f0c0320f3..02d1c9bef 100644 --- a/Telegram/SourceFiles/api/api_report.h +++ b/Telegram/SourceFiles/api/api_report.h @@ -41,12 +41,12 @@ struct ReportResult final { bool successful = false; }; -void SendReport( +void SendPhotoReport( std::shared_ptr show, not_null peer, Ui::ReportReason reason, const QString &comment, - std::variant> data); + not_null photo); [[nodiscard]] auto CreateReportMessagesOrStoriesCallback( std::shared_ptr show, diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index a3003bab4..d0b897a07 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -456,6 +456,7 @@ void SendConfirmedFile( not_null session, const std::shared_ptr &file) { const auto isEditing = (file->type != SendMediaType::Audio) + && (file->type != SendMediaType::Round) && (file->to.replaceMediaOf != 0); const auto newId = FullMsgId( file->to.peer, @@ -525,7 +526,8 @@ void SendConfirmedFile( // Shortcut messages have no 'edited' badge. flags |= MessageFlag::HideEdited; } - if (file->type == SendMediaType::Audio) { + if (file->type == SendMediaType::Audio + || file->type == SendMediaType::Round) { if (!peer->isChannel() || peer->isMegagroup()) { flags |= MessageFlag::MediaIsUnread; } @@ -551,29 +553,25 @@ void SendConfirmedFile( MTPint()); } else if (file->type == SendMediaType::Audio) { const auto ttlSeconds = file->to.options.ttlSeconds; - const auto isVoice = [&] { - return file->document.match([](const MTPDdocumentEmpty &d) { - return false; - }, [](const MTPDdocument &d) { - return ranges::any_of(d.vattributes().v, [&]( - const MTPDocumentAttribute &attribute) { - using Att = MTPDdocumentAttributeAudio; - return attribute.match([](const Att &data) -> bool { - return data.vflags().v & Att::Flag::f_voice; - }, [](const auto &) { - return false; - }); - }); - }); - }(); using Flag = MTPDmessageMediaDocument::Flag; return MTP_messageMediaDocument( MTP_flags(Flag::f_document - | (isVoice ? Flag::f_voice : Flag()) + | Flag::f_voice | (ttlSeconds ? Flag::f_ttl_seconds : Flag())), file->document, MTPVector(), // alt_documents MTP_int(ttlSeconds)); + } else if (file->type == SendMediaType::Round) { + using Flag = MTPDmessageMediaDocument::Flag; + const auto ttlSeconds = file->to.options.ttlSeconds; + return MTP_messageMediaDocument( + MTP_flags(Flag::f_document + | Flag::f_round + | (ttlSeconds ? Flag::f_ttl_seconds : Flag()) + | (file->spoiler ? Flag::f_spoiler : Flag())), + file->document, + MTPVector(), // alt_documents + MTP_int(ttlSeconds)); } else { Unexpected("Type in sendFilesConfirmed."); } diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp index c960aca33..daed8f1ee 100644 --- a/Telegram/SourceFiles/api/api_statistics.cpp +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_story.h" +#include "data/data_user.h" #include "history/history.h" #include "main/main_session.h" @@ -341,6 +342,10 @@ void PublicForwards::request( .token = nextToken, }); }; + const auto processFail = [=] { + _requestId = 0; + done({}); + }; constexpr auto kLimit = tl::make_int(100); if (_fullId.messageId) { @@ -349,14 +354,14 @@ void PublicForwards::request( MTP_int(_fullId.messageId.msg), MTP_string(token), kLimit - )).done(processResult).fail([=] { _requestId = 0; }).send(); + )).done(processResult).fail(processFail).send(); } else if (_fullId.storyId) { _requestId = makeRequest(MTPstats_GetStoryPublicForwards( channel->input, MTP_int(_fullId.storyId.story), MTP_string(token), kLimit - )).done(processResult).fail([=] { _requestId = 0; }).send(); + )).done(processResult).fail(processFail).send(); } } @@ -381,7 +386,7 @@ Data::PublicForwardsSlice MessageStatistics::firstSlice() const { } void MessageStatistics::request(Fn done) { - if (channel()->isMegagroup()) { + if (channel()->isMegagroup() && !_storyId) { return; } const auto requestFirstPublicForwards = [=]( @@ -681,17 +686,18 @@ Data::BoostStatus Boosts::boostStatus() const { return _boostStatus; } -ChannelEarnStatistics::ChannelEarnStatistics(not_null channel) -: StatisticsRequestSender(channel) { +EarnStatistics::EarnStatistics(not_null peer) +: StatisticsRequestSender(peer) +, _isUser(peer->isUser()) { } -rpl::producer ChannelEarnStatistics::request() { +rpl::producer EarnStatistics::request() { return [=](auto consumer) { auto lifetime = rpl::lifetime(); makeRequest(MTPstats_GetBroadcastRevenueStats( MTP_flags(0), - channel()->inputChannel + (_isUser ? user()->input : channel()->input) )).done([=](const MTPstats_BroadcastRevenueStats &result) { const auto &data = result.data(); const auto &balances = data.vbalances().data(); @@ -708,18 +714,22 @@ rpl::producer ChannelEarnStatistics::request() { requestHistory({}, [=](Data::EarnHistorySlice &&slice) { _data.firstHistorySlice = std::move(slice); - api().request( - MTPchannels_GetFullChannel(channel()->inputChannel) - ).done([=](const MTPmessages_ChatFull &result) { - result.data().vfull_chat().match([&]( - const MTPDchannelFull &d) { - _data.switchedOff = d.is_restricted_sponsored(); - }, [](const auto &) { - }); + if (!_isUser) { + api().request( + MTPchannels_GetFullChannel(channel()->inputChannel) + ).done([=](const MTPmessages_ChatFull &result) { + result.data().vfull_chat().match([&]( + const MTPDchannelFull &d) { + _data.switchedOff = d.is_restricted_sponsored(); + }, [](const auto &) { + }); + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + } else { consumer.put_done(); - }).fail([=](const MTP::Error &error) { - consumer.put_error_copy(error.type()); - }).send(); + } }); }).fail([=](const MTP::Error &error) { consumer.put_error_copy(error.type()); @@ -729,7 +739,7 @@ rpl::producer ChannelEarnStatistics::request() { }; } -void ChannelEarnStatistics::requestHistory( +void EarnStatistics::requestHistory( const Data::EarnHistorySlice::OffsetToken &token, Fn done) { if (_requestId) { @@ -738,7 +748,7 @@ void ChannelEarnStatistics::requestHistory( constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); constexpr auto kTlLimit = tl::make_int(kLimit); _requestId = api().request(MTPstats_GetBroadcastRevenueTransactions( - channel()->inputChannel, + (_isUser ? user()->input : channel()->input), MTP_int(token), (!token) ? kTlFirstSlice : kTlLimit )).done([=](const MTPstats_BroadcastRevenueTransactions &result) { @@ -799,7 +809,7 @@ void ChannelEarnStatistics::requestHistory( }).send(); } -Data::EarnStatistics ChannelEarnStatistics::data() const { +Data::EarnStatistics EarnStatistics::data() const { return _data; } diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h index 213ab9293..b419b8841 100644 --- a/Telegram/SourceFiles/api/api_statistics.h +++ b/Telegram/SourceFiles/api/api_statistics.h @@ -79,9 +79,9 @@ private: }; -class ChannelEarnStatistics final : public StatisticsRequestSender { +class EarnStatistics final : public StatisticsRequestSender { public: - explicit ChannelEarnStatistics(not_null channel); + explicit EarnStatistics(not_null peer); [[nodiscard]] rpl::producer request(); void requestHistory( @@ -94,6 +94,7 @@ public: static constexpr auto kLimit = int(10); private: + const bool _isUser = false; Data::EarnStatistics _data; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index eea4f6f35..b4775f07b 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -320,6 +320,9 @@ void Updates::feedUpdateVector( } else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) { return; } + if (policy == SkipUpdatePolicy::SkipNone) { + applyConvertToScheduledOnSend(updates); + } for (const auto &entry : std::as_const(list)) { const auto type = entry.type(); if ((policy == SkipUpdatePolicy::SkipMessageIds @@ -333,6 +336,15 @@ void Updates::feedUpdateVector( session().data().sendHistoryChangeNotifications(); } +void Updates::checkForSentToScheduled(const MTPUpdates &updates) { + updates.match([&](const MTPDupdates &data) { + applyConvertToScheduledOnSend(data.vupdates(), true); + }, [&](const MTPDupdatesCombined &data) { + applyConvertToScheduledOnSend(data.vupdates(), true); + }, [](const auto &) { + }); +} + void Updates::feedMessageIds(const MTPVector &updates) { for (const auto &update : updates.v) { if (update.type() == mtpc_updateMessageID) { @@ -436,6 +448,7 @@ void Updates::feedChannelDifference( session().data().processChats(data.vchats()); _handlingChannelDifference = true; + applyConvertToScheduledOnSend(data.vother_updates()); feedMessageIds(data.vother_updates()); session().data().processMessages( data.vnew_messages(), @@ -600,6 +613,7 @@ void Updates::feedDifference( Core::App().checkAutoLock(); session().data().processUsers(users); session().data().processChats(chats); + applyConvertToScheduledOnSend(other); feedMessageIds(other); session().data().processMessages(msgs, NewMessageType::Unread); feedUpdateVector(other, SkipUpdatePolicy::SkipMessageIds); @@ -885,6 +899,51 @@ void Updates::mtpUpdateReceived(const MTPUpdates &updates) { } } +void Updates::applyConvertToScheduledOnSend( + const MTPVector &other, + bool skipScheduledCheck) { + for (const auto &update : other.v) { + update.match([&](const MTPDupdateNewScheduledMessage &data) { + const auto &message = data.vmessage(); + const auto id = IdFromMessage(message); + const auto scheduledMessages = &_session->scheduledMessages(); + const auto scheduledId = scheduledMessages->localMessageId(id); + for (const auto &updateId : other.v) { + updateId.match([&](const MTPDupdateMessageID &dataId) { + if (dataId.vid().v == id) { + auto &owner = session().data(); + if (skipScheduledCheck) { + const auto peerId = PeerFromMessage(message); + const auto history = owner.historyLoaded(peerId); + if (history) { + _session->data().sentToScheduled({ + .history = history, + .scheduledId = scheduledId, + }); + } + return; + } + const auto rand = dataId.vrandom_id().v; + const auto localId = owner.messageIdByRandomId(rand); + if (const auto local = owner.message(localId)) { + if (!local->isScheduled()) { + _session->data().sentToScheduled({ + .history = local->history(), + .scheduledId = scheduledId, + }); + + // We've sent a non-scheduled message, + // but it was converted to a scheduled. + local->destroy(); + } + } + } + }, [](const auto &) {}); + } + }, [](const auto &) {}); + } +} + void Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) { updates.match([&](const MTPDupdates &data) { session().data().processUsers(data.vusers()); diff --git a/Telegram/SourceFiles/api/api_updates.h b/Telegram/SourceFiles/api/api_updates.h index 654e36b51..b1ecb870f 100644 --- a/Telegram/SourceFiles/api/api_updates.h +++ b/Telegram/SourceFiles/api/api_updates.h @@ -40,6 +40,8 @@ public: void applyUpdatesNoPtsCheck(const MTPUpdates &updates); void applyUpdateNoPtsCheck(const MTPUpdate &update); + void checkForSentToScheduled(const MTPUpdates &updates); + [[nodiscard]] int32 pts() const; void updateOnline(crl::time lastNonIdleTime = 0); @@ -131,6 +133,9 @@ private: // Doesn't call sendHistoryChangeNotifications itself. void feedUpdate(const MTPUpdate &update); + void applyConvertToScheduledOnSend( + const MTPVector &other, + bool skipScheduledCheck = false); void applyGroupCallParticipantUpdates(const MTPUpdates &updates); bool whenGetDiffChanged( diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index d0c17fa6b..78f863bed 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -69,6 +69,9 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) { if (rule.always.premiums && (rule.option != Option::Everyone)) { result.push_back(MTP_inputPrivacyValueAllowPremium()); } + if (rule.always.miniapps && (rule.option != Option::Everyone)) { + result.push_back(MTP_inputPrivacyValueAllowBots()); + } } if (!rule.ignoreNever) { const auto users = collectInputUsers(rule.never); @@ -83,6 +86,9 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) { MTP_inputPrivacyValueDisallowChatParticipants( MTP_vector(chats))); } + if (rule.never.miniapps && (rule.option != Option::Nobody)) { + result.push_back(MTP_inputPrivacyValueDisallowBots()); + } } result.push_back([&] { switch (rule.option) { @@ -124,6 +130,10 @@ UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) { setOption(Option::CloseFriends); }, [&](const MTPDprivacyValueAllowPremium &) { result.always.premiums = true; + }, [&](const MTPDprivacyValueAllowBots &) { + result.always.miniapps = true; + }, [&](const MTPDprivacyValueDisallowBots &) { + result.never.miniapps = true; }, [&](const MTPDprivacyValueAllowUsers &data) { const auto &users = data.vusers().v; always.reserve(always.size() + users.size()); @@ -199,6 +209,7 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) { case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages(); case Key::About: return MTP_inputPrivacyKeyAbout(); case Key::Birthday: return MTP_inputPrivacyKeyBirthday(); + case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave(); } Unexpected("Key in Api::UserPrivacy::KetToTL."); } @@ -228,6 +239,8 @@ std::optional TLToKey(mtpTypeId type) { case mtpc_inputPrivacyKeyAbout: return Key::About; case mtpc_privacyKeyBirthday: case mtpc_inputPrivacyKeyBirthday: return Key::Birthday; + case mtpc_privacyKeyStarGiftsAutoSave: + case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave; } return std::nullopt; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 471f41f48..a1f66189f 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -31,6 +31,7 @@ public: Voices, About, Birthday, + GiftsAutoSave, }; enum class Option { Everyone, @@ -41,6 +42,7 @@ public: struct Exceptions { std::vector> peers; bool premiums = false; + bool miniapps = false; }; struct Rule { Option option = Option::Everyone; diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 607e4453b..86bc8d49b 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -756,5 +756,19 @@ rpl::producer WhoReacted( const style::WhoRead &st) { return WhoReacted(item, reaction, context, st, nullptr); } +rpl::producer WhenEdited( + not_null author, + TimeId date) { + return rpl::single(Ui::WhoReadContent{ + .participants = { Ui::WhoReadParticipant{ + .name = author->name(), + .date = FormatReadDate(date, QDateTime::currentDateTime()), + .id = author->id.value, + } }, + .type = Ui::WhoReadType::Edited, + .fullReadCount = 1, + }); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index 9a9100535..0d1cf7234 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -61,5 +61,8 @@ struct WhoReadList { const Data::ReactionId &reaction, not_null context, // Cache results for this lifetime. const style::WhoRead &st); +[[nodiscard]] rpl::producer WhenEdited( + not_null author, + TimeId date); } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a91296d2d..6ffccae96 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -731,7 +731,8 @@ void ApiWrap::finalizeMessageDataRequest( QString ApiWrap::exportDirectMessageLink( not_null item, - bool inRepliesContext) { + bool inRepliesContext, + bool forceNonPublicLink) { Expects(item->history()->peer->isChannel()); const auto itemId = item->fullId(); @@ -754,7 +755,7 @@ QString ApiWrap::exportDirectMessageLink( const auto sender = root ? root->discussionPostOriginalSender() : nullptr; - if (sender && sender->hasUsername()) { + if (sender && sender->hasUsername() && !forceNonPublicLink) { // Comment to a public channel. const auto forwarded = root->Get(); linkItemId = forwarded->savedFromMsgId; @@ -770,7 +771,7 @@ QString ApiWrap::exportDirectMessageLink( } } } - const auto base = linkChannel->hasUsername() + const auto base = (linkChannel->hasUsername() && !forceNonPublicLink) ? linkChannel->username() : "c/" + QString::number(peerToChannel(linkChannel->id).bare); const auto post = QString::number(linkItemId.bare); @@ -784,6 +785,7 @@ QString ApiWrap::exportDirectMessageLink( ? (QString::number(linkThreadId.bare) + '/' + post) : post); if (linkChannel->hasUsername() + && !forceNonPublicLink && !linkChannel->isMegagroup() && !linkCommentId && !linkThreadId) { @@ -797,6 +799,9 @@ QString ApiWrap::exportDirectMessageLink( } return session().createInternalLinkFull(query); }; + if (forceNonPublicLink) { + return fallback(); + } const auto i = _unlikelyMessageLinks.find(itemId); const auto current = (i != end(_unlikelyMessageLinks)) ? i->second @@ -3373,6 +3378,7 @@ void ApiWrap::forwardMessages( } const auto requestType = Data::Histories::RequestType::Send; const auto idsCopy = localIds; + const auto scheduled = action.options.scheduled; histories.sendRequest(history, requestType, [=](Fn finish) { history->sendRequestId = request(MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -3385,6 +3391,9 @@ void ApiWrap::forwardMessages( (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId) )).done([=](const MTPUpdates &result) { + if (!scheduled) { + this->updates().checkForSentToScheduled(result); + } applyUpdates(result); if (shared && !--shared->requestsLeft) { shared->callback(); @@ -3553,6 +3562,7 @@ void ApiWrap::sendVoiceMessage( QByteArray result, VoiceWaveform waveform, crl::time duration, + bool video, const SendAction &action) { const auto caption = TextWithTags(); const auto to = FileLoadTaskOptions(action); @@ -3561,6 +3571,7 @@ void ApiWrap::sendVoiceMessage( result, duration, waveform, + video, to, caption)); } @@ -4008,7 +4019,8 @@ void ApiWrap::sendInlineResult( not_null bot, not_null data, const SendAction &action, - std::optional localMessageId) { + std::optional localMessageId, + Fn done) { sendAction(action); const auto history = action.history; @@ -4088,11 +4100,17 @@ void ApiWrap::sendInlineResult( history->finishSavingCloudDraft( topicRootId, UnixtimeFromMsgId(response.outerMsgId)); + if (done) { + done(true); + } }, [=](const MTP::Error &error, const MTP::Response &response) { sendMessageFail(error, peer, randomId, newId); history->finishSavingCloudDraft( topicRootId, UnixtimeFromMsgId(response.outerMsgId)); + if (done) { + done(false); + } }); finishForwarding(action); } @@ -4304,6 +4322,7 @@ void ApiWrap::sendMultiPaidMedia( auto &histories = history->owner().histories(); const auto peer = history->peer; const auto itemId = item->fullId(); + album->sent = true; histories.sendPreparedMessage( history, replyTo, @@ -4375,6 +4394,9 @@ void ApiWrap::sendAlbumWithCancelled( } void ApiWrap::sendAlbumIfReady(not_null album) { + if (album->sent) { + return; + } const auto groupId = album->groupId; if (album->items.empty()) { _sendingAlbums.remove(groupId); @@ -4399,6 +4421,7 @@ void ApiWrap::sendAlbumIfReady(not_null album) { return; } else if (medias.size() < 2) { const auto &single = medias.front().data(); + album->sent = true; sendMediaWithRandomId( sample, single.vmedia(), @@ -4431,6 +4454,7 @@ void ApiWrap::sendAlbumIfReady(not_null album) { | (album->options.invertCaption ? Flag::f_invert_media : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; + album->sent = true; histories.sendPreparedMessage( history, replyTo, diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 7259c410d..f25368293 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -164,7 +164,8 @@ public: void requestMessageData(PeerData *peer, MsgId msgId, Fn done); QString exportDirectMessageLink( not_null item, - bool inRepliesContext); + bool inRepliesContext, + bool forceNonPublicLink = false); QString exportDirectStoryLink(not_null item); void requestContacts(); @@ -317,6 +318,7 @@ public: QByteArray result, VoiceWaveform waveform, crl::time duration, + bool video, const SendAction &action); void sendFiles( Ui::PreparedList &&list, @@ -359,7 +361,8 @@ public: not_null bot, not_null data, const SendAction &action, - std::optional localMessageId); + std::optional localMessageId, + Fn done = nullptr); void sendMessageFail( const MTP::Error &error, not_null peer, diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 54798434a..be1a42ae2 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -154,9 +154,7 @@ contactsSortButton: IconButton(defaultIconButton) { iconPosition: point(10px, -1px); rippleAreaPosition: point(1px, 6px); rippleAreaSize: 42px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } contactsSortOnlineIcon: icon{{ "contacts_online", boxTitleCloseFg }}; contactsSortOnlineIconOver: icon{{ "contacts_online", boxTitleCloseFgOver }}; @@ -416,9 +414,7 @@ calendarPrevious: IconButton { rippleAreaPosition: point(2px, 2px); rippleAreaSize: 44px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } calendarPreviousDisabled: icon {{ "calendar_down-flip_vertical", menuIconFg }}; calendarNext: IconButton(calendarPrevious) { @@ -616,9 +612,7 @@ proxyTryIPv6Padding: margins(22px, 8px, 22px, 5px); proxyRowPadding: margins(22px, 8px, 8px, 8px); proxyRowIconSkip: 32px; proxyRowSkip: 2px; -proxyRowRipple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; -} +proxyRowRipple: defaultRippleAnimationBgOver; proxyRowTitleFg: windowFg; proxyRowTitlePalette: TextPalette(defaultTextPalette) { linkFg: windowSubTextFg; @@ -683,9 +677,7 @@ themesMenuToggle: IconButton(defaultIconButton) { rippleAreaPosition: point(4px, 4px); rippleAreaSize: 36px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } themesMenuPosition: point(-2px, 25px); @@ -738,9 +730,7 @@ createPollOptionRemove: CrossButton { duration: 135; loadingPeriod: 1000; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } createPollOptionRemovePosition: point(11px, 9px); createPollOptionEmojiPositionSkip: 4px; @@ -888,6 +878,13 @@ peerListWithInviteViaLink: PeerList(peerListBox) { peerListSingleRow: PeerList(peerListBox) { padding: margins(0px, 0px, 0px, 0px); } +peerListSmallSkips: PeerList(peerListBox) { + padding: margins( + 0px, + defaultVerticalListSkip, + 0px, + defaultVerticalListSkip); +} scheduleHeight: 95px; scheduleDateTop: 38px; @@ -951,9 +948,7 @@ sponsoredUrlButton: RoundButton(defaultActiveButton) { textTop: 7px; style: defaultTextStyle; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } requestPeerRestriction: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 4e99a01ba..252403af8 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -96,6 +96,9 @@ void ChangeFilterById( Ui::Text::WithEntities)); } }).fail([=](const MTP::Error &error) { + LOG(("API Error: failed to %1 a dialog to a folder. %2") + .arg(add ? u"add"_q : u"remove"_q) + .arg(error.type())); // Revert filter on fail. history->owner().chatsFilters().set(was); }).send(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 96d8fa782..f6e48b221 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -238,7 +238,7 @@ EditCaptionBox::EditCaptionBox( Fn saved) : _controller(controller) , _historyItem(item) -, _isAllowedEditMedia(item->media() && item->media()->allowsEditMedia()) +, _isAllowedEditMedia(item->allowsEditMedia()) , _albumType(ComputeAlbumType(item)) , _controls(base::make_unique_q(this)) , _scroll(base::make_unique_q(this, st::boxScroll)) @@ -253,8 +253,8 @@ EditCaptionBox::EditCaptionBox( , _initialText(std::move(text)) , _initialList(std::move(list)) , _saved(std::move(saved)) { - Expects(item->media() != nullptr); - Expects(item->media()->allowsEditCaption()); + Expects(!_initialList.files.empty()); + Expects(!item->media() || item->media()->allowsEditCaption()); _mediaEditManager.start(item, spoilered, invertCaption); @@ -422,7 +422,8 @@ void EditCaptionBox::prepare() { setInitialText(); if (!setPreparedList(std::move(_initialList))) { - rebuildPreview(); + crl::on_main(this, [=] { closeBox(); }); + return; } setupEditEventHandler(); SetupShadowsToScrollContent(this, _scroll, _contentHeight.events()); diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index c9a9055e1..94c4af054 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -36,13 +36,63 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_window.h" namespace { constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; +constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; using Exceptions = Api::UserPrivacy::Exceptions; +enum class SpecialRowType { + Premiums, + MiniApps, +}; + +[[nodiscard]] PaintRoundImageCallback GeneratePremiumsUserpicCallback( + bool forceRound) { + return [=](QPainter &p, int x, int y, int outerWidth, int size) { + auto gradient = QLinearGradient( + QPointF(x, y), + QPointF(x + size, y + size)); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(gradient); + if (forceRound) { + p.drawEllipse(x, y, size, size); + } else { + const auto radius = size * Ui::ForumUserpicRadiusMultiplier(); + p.drawRoundedRect(x, y, size, size, radius, radius); + } + st::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size)); + }; +} + +[[nodiscard]] PaintRoundImageCallback GenerateMiniAppsUserpicCallback( + bool forceRound) { + return [=](QPainter &p, int x, int y, int outerWidth, int size) { + const auto &color1 = st::historyPeer6UserpicBg; + const auto &color2 = st::historyPeer6UserpicBg2; + + auto hq = PainterHighQualityEnabler(p); + auto gradient = QLinearGradient(x, y, x, y + size); + gradient.setStops({ { 0., color1->c }, { 1., color2->c } }); + + p.setPen(Qt::NoPen); + p.setBrush(gradient); + if (forceRound) { + p.drawEllipse(x, y, size, size); + } else { + const auto radius = size * Ui::ForumUserpicRadiusMultiplier(); + p.drawRoundedRect(x, y, size, size, radius, radius); + } + st::windowFilterTypeBots.paintInCenter(p, QRect(x, y, size, size)); + }; +} + void CreateRadiobuttonLock( not_null widget, const style::Checkbox &st) { @@ -102,7 +152,7 @@ public: not_null session, rpl::producer title, const Exceptions &selected, - bool allowChoosePremiums); + std::optional allowChooseSpecial); Main::Session &session() const override; void rowClicked(not_null row) override; @@ -110,18 +160,20 @@ public: bool handleDeselectForeignRow(PeerListRowId itemId) override; [[nodiscard]] bool premiumsSelected() const; + [[nodiscard]] bool miniAppsSelected() const; protected: void prepareViewHook() override; std::unique_ptr createRow(not_null history) override; private: - [[nodiscard]] object_ptr preparePremiumsRowList(); + [[nodiscard]] object_ptr prepareSpecialRowList( + SpecialRowType type); const not_null _session; rpl::producer _title; Exceptions _selected; - bool _allowChoosePremiums = false; + std::optional _allowChooseSpecial; PeerListContentDelegate *_typesDelegate = nullptr; Fn _deselectOption; @@ -133,9 +185,9 @@ struct RowSelectionChange { bool checked = false; }; -class PremiumsRow final : public PeerListRow { +class SpecialRow final : public PeerListRow { public: - PremiumsRow(); + explicit SpecialRow(SpecialRowType type); QString generateName() override; QString generateShortName() override; @@ -147,66 +199,61 @@ public: class TypesController final : public PeerListController { public: - TypesController(not_null session, bool premiums); + TypesController(not_null session, SpecialRowType type); Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; - [[nodiscard]] bool premiumsSelected() const; - [[nodiscard]] rpl::producer premiumsChanges() const; + [[nodiscard]] bool specialSelected() const; + [[nodiscard]] rpl::producer specialChanges() const; [[nodiscard]] auto rowSelectionChanges() const -> rpl::producer; private: const not_null _session; + const SpecialRowType _type; rpl::event_stream<> _selectionChanged; rpl::event_stream _rowSelectionChanges; }; -PremiumsRow::PremiumsRow() : PeerListRow(kPremiumsRowId) { - setCustomStatus(tr::lng_edit_privacy_premium_status(tr::now)); +SpecialRow::SpecialRow(SpecialRowType type) +: PeerListRow((type == SpecialRowType::Premiums) + ? kPremiumsRowId + : kMiniAppsRowId) { + setCustomStatus((id() == kPremiumsRowId) + ? tr::lng_edit_privacy_premium_status(tr::now) + : tr::lng_edit_privacy_miniapps_status(tr::now)); } -QString PremiumsRow::generateName() { - return tr::lng_edit_privacy_premium(tr::now); +QString SpecialRow::generateName() { + return (id() == kPremiumsRowId) + ? tr::lng_edit_privacy_premium(tr::now) + : tr::lng_edit_privacy_miniapps(tr::now); } -QString PremiumsRow::generateShortName() { +QString SpecialRow::generateShortName() { return generateName(); } -PaintRoundImageCallback PremiumsRow::generatePaintUserpicCallback( +PaintRoundImageCallback SpecialRow::generatePaintUserpicCallback( bool forceRound) { - return [=](QPainter &p, int x, int y, int outerWidth, int size) { - auto gradient = QLinearGradient( - QPointF(x, y), - QPointF(x + size, y + size)); - gradient.setStops(Ui::Premium::ButtonGradientStops()); - - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(gradient); - if (forceRound) { - p.drawEllipse(x, y, size, size); - } else { - const auto radius = size * Ui::ForumUserpicRadiusMultiplier(); - p.drawRoundedRect(x, y, size, size, radius, radius); - } - st::settingsPrivacyPremium.paintInCenter(p, QRect(x, y, size, size)); - }; + return (id() == kPremiumsRowId) + ? GeneratePremiumsUserpicCallback(forceRound) + : GenerateMiniAppsUserpicCallback(forceRound); } -bool PremiumsRow::useForumLikeUserpic() const { +bool SpecialRow::useForumLikeUserpic() const { return true; } TypesController::TypesController( not_null session, - bool premiums) -: _session(session) { + SpecialRowType type) +: _session(session) +, _type(type) { } Main::Session &TypesController::session() const { @@ -214,12 +261,15 @@ Main::Session &TypesController::session() const { } void TypesController::prepare() { - delegate()->peerListAppendRow(std::make_unique()); + delegate()->peerListAppendRow(std::make_unique(_type)); delegate()->peerListRefreshRows(); } -bool TypesController::premiumsSelected() const { - const auto row = delegate()->peerListFindRow(kPremiumsRowId); +bool TypesController::specialSelected() const { + const auto premiums = (_type == SpecialRowType::Premiums); + const auto row = delegate()->peerListFindRow(premiums + ? kPremiumsRowId + : kMiniAppsRowId); Assert(row != nullptr); return row->checked(); @@ -231,10 +281,10 @@ void TypesController::rowClicked(not_null row) { _rowSelectionChanges.fire({ row, checked }); } -rpl::producer TypesController::premiumsChanges() const { +rpl::producer TypesController::specialChanges() const { return _rowSelectionChanges.events( ) | rpl::map([=] { - return premiumsSelected(); + return specialSelected(); }); } @@ -247,12 +297,12 @@ PrivacyExceptionsBoxController::PrivacyExceptionsBoxController( not_null session, rpl::producer title, const Exceptions &selected, - bool allowChoosePremiums) + std::optional allowChooseSpecial) : ChatsListBoxController(session) , _session(session) , _title(std::move(title)) , _selected(selected) -, _allowChoosePremiums(allowChoosePremiums) { +, _allowChooseSpecial(allowChooseSpecial) { } Main::Session &PrivacyExceptionsBoxController::session() const { @@ -261,14 +311,18 @@ Main::Session &PrivacyExceptionsBoxController::session() const { void PrivacyExceptionsBoxController::prepareViewHook() { delegate()->peerListSetTitle(std::move(_title)); - if (_allowChoosePremiums || _selected.premiums) { - delegate()->peerListSetAboveWidget(preparePremiumsRowList()); + if (_allowChooseSpecial || _selected.premiums || _selected.miniapps) { + delegate()->peerListSetAboveWidget(prepareSpecialRowList( + _allowChooseSpecial.value_or(_selected.premiums + ? SpecialRowType::Premiums + : SpecialRowType::MiniApps))); } delegate()->peerListAddSelectedPeers(_selected.peers); } bool PrivacyExceptionsBoxController::isForeignRow(PeerListRowId itemId) { - return (itemId == kPremiumsRowId); + return (itemId == kPremiumsRowId) + || (itemId == kMiniAppsRowId); } bool PrivacyExceptionsBoxController::handleDeselectForeignRow( @@ -280,7 +334,8 @@ bool PrivacyExceptionsBoxController::handleDeselectForeignRow( return false; } -auto PrivacyExceptionsBoxController::preparePremiumsRowList() +auto PrivacyExceptionsBoxController::prepareSpecialRowList( + SpecialRowType type) -> object_ptr { auto result = object_ptr((QWidget*)nullptr); const auto container = result.data(); @@ -291,30 +346,39 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList() _typesDelegate = lifetime.make_state(); const auto controller = lifetime.make_state( &session(), - _selected.premiums); + type); const auto content = result->add(object_ptr( container, controller)); _typesDelegate->setContent(content); controller->setDelegate(_typesDelegate); + const auto selectType = [&](PeerListRowId id) { + const auto row = _typesDelegate->peerListFindRow(id); + if (row) { + content->changeCheckState(row, true, anim::type::instant); + this->delegate()->peerListSetForeignRowChecked( + row, + true, + anim::type::instant); + } + }; if (_selected.premiums) { - const auto row = _typesDelegate->peerListFindRow(kPremiumsRowId); - Assert(row != nullptr); - - content->changeCheckState(row, true, anim::type::instant); - this->delegate()->peerListSetForeignRowChecked( - row, - true, - anim::type::instant); + selectType(kPremiumsRowId); + } else if (_selected.miniapps) { + selectType(kMiniAppsRowId); } container->add(CreatePeerListSectionSubtitle( container, tr::lng_edit_privacy_users_and_groups())); - controller->premiumsChanges( - ) | rpl::start_with_next([=](bool premiums) { - _selected.premiums = premiums; + controller->specialChanges( + ) | rpl::start_with_next([=](bool chosen) { + if (type == SpecialRowType::Premiums) { + _selected.premiums = chosen; + } else { + _selected.miniapps = chosen; + } }, lifetime); controller->rowSelectionChanges( @@ -329,6 +393,8 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList() if (const auto row = _typesDelegate->peerListFindRow(itemId)) { if (itemId == kPremiumsRowId) { _selected.premiums = false; + } else if (itemId == kMiniAppsRowId) { + _selected.miniapps = false; } _typesDelegate->peerListSetRowChecked(row, false); } @@ -337,10 +403,14 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList() return result; } -[[nodiscard]] bool PrivacyExceptionsBoxController::premiumsSelected() const { +bool PrivacyExceptionsBoxController::premiumsSelected() const { return _selected.premiums; } +bool PrivacyExceptionsBoxController::miniAppsSelected() const { + return _selected.miniapps; +} + void PrivacyExceptionsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); @@ -412,6 +482,11 @@ EditPrivacyBox::EditPrivacyBox( // If we switch from Everyone to Contacts or Nobody suggest Premiums. _value.always.premiums = true; } + if (_controller->allowMiniAppsToggle(Exception::Always) + && _value.option == Option::Everyone) { + // If we switch from Everyone to Contacts or Nobody suggest MiniApps. + _value.always.miniapps = true; + } } void EditPrivacyBox::prepare() { @@ -427,12 +502,18 @@ void EditPrivacyBox::editExceptions( &_window->session(), _controller->exceptionBoxTitle(exception), exceptions(exception), - _controller->allowPremiumsToggle(exception)); + (_controller->allowPremiumsToggle(exception) + ? SpecialRowType::Premiums + : _controller->allowMiniAppsToggle(exception) + ? SpecialRowType::MiniApps + : std::optional())); auto initBox = [=, controller = controller.get()]( not_null box) { box->addButton(tr::lng_settings_save(), crl::guard(this, [=] { - exceptions(exception).peers = box->collectSelectedRows(); - exceptions(exception).premiums = controller->premiumsSelected(); + auto &setTo = exceptions(exception); + setTo.peers = box->collectSelectedRows(); + setTo.premiums = controller->premiumsSelected(); + setTo.miniapps = controller->miniAppsSelected(); const auto type = [&] { switch (exception) { case Exception::Always: return Exception::Never; @@ -440,11 +521,17 @@ void EditPrivacyBox::editExceptions( } Unexpected("Invalid exception value."); }(); - auto &removeFrom = exceptions(type).peers; + auto &removeFrom = exceptions(type); for (const auto peer : exceptions(exception).peers) { - removeFrom.erase( - ranges::remove(removeFrom, peer), - end(removeFrom)); + removeFrom.peers.erase( + ranges::remove(removeFrom.peers, peer), + end(removeFrom.peers)); + } + if (setTo.premiums) { + removeFrom.premiums = false; + } + if (setTo.miniapps) { + removeFrom.miniapps = false; } done(); box->closeBox(); @@ -566,14 +653,21 @@ void EditPrivacyBox::setupContent() { lt_count, count) : tr::lng_edit_privacy_exceptions_add(tr::now); - return !value.premiums - ? users - : !count - ? tr::lng_edit_privacy_premium(tr::now) - : tr::lng_edit_privacy_exceptions_premium_and( - tr::now, - lt_users, - users); + return value.premiums + ? (!count + ? tr::lng_edit_privacy_premium(tr::now) + : tr::lng_edit_privacy_exceptions_premium_and( + tr::now, + lt_users, + users)) + : value.miniapps + ? (!count + ? tr::lng_edit_privacy_miniapps(tr::now) + : tr::lng_edit_privacy_exceptions_miniapps_and( + tr::now, + lt_users, + users)) + : users; }); _controller->handleExceptionsChange( exception, diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 3aae28403..bd87e90f2 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -61,6 +61,10 @@ public: Exception exception) const { return false; } + [[nodiscard]] virtual bool allowMiniAppsToggle( + Exception exception) const { + return false; + } virtual void handleExceptionsChange( Exception exception, rpl::producer value) { diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index f0ecd05f6..896ecea51 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -123,7 +123,9 @@ void GiftCreditsBox( box->verticalLayout(), peer, 0, - [=] { gifted(); box->uiShow()->hideLayer(); }); + [=] { gifted(); box->uiShow()->hideLayer(); }, + tr::lng_credits_summary_options_subtitle(), + {}); box->setPinnedToBottomContent( object_ptr(box)); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index fd640e496..a3d385c72 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/prepare_short_info_box.h" #include "boxes/peers/replace_boost_box.h" // BoostsForGift. #include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox. +#include "boxes/star_gift_box.h" // ShowStarGiftBox. #include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -123,7 +124,8 @@ namespace { [[nodiscard]] object_ptr MakePeerTableValue( not_null parent, not_null controller, - PeerId id) { + PeerId id, + bool withSendGiftButton = false) { auto result = object_ptr(parent); const auto raw = result.data(); @@ -134,15 +136,40 @@ namespace { const auto userpic = Ui::CreateChild(raw, peer, st); const auto label = Ui::CreateChild( raw, - peer->name(), + withSendGiftButton ? peer->shortName() : peer->name(), st::giveawayGiftCodeValue); - raw->widthValue( - ) | rpl::start_with_next([=](int width) { + const auto send = withSendGiftButton + ? Ui::CreateChild( + raw, + tr::lng_gift_send_small(), + st::starGiftSmallButton) + : nullptr; + if (send) { + send->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + send->setClickedCallback([=] { + Ui::ShowStarGiftBox(controller->parentController(), peer); + }); + } + rpl::combine( + raw->widthValue(), + send ? send->widthValue() : rpl::single(0) + ) | rpl::start_with_next([=](int width, int sendWidth) { const auto position = st::giveawayGiftCodeNamePosition; - label->resizeToNaturalWidth(width - position.x()); + const auto sendSkip = sendWidth + ? (st::normalFont->spacew + sendWidth) + : 0; + label->resizeToNaturalWidth(width - position.x() - sendSkip); label->moveToLeft(position.x(), position.y(), width); const auto top = (raw->height() - userpic->height()) / 2; userpic->moveToLeft(0, top, width); + if (send) { + send->moveToLeft( + position.x() + label->width() + st::normalFont->spacew, + (position.y() + + st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + } }, label->lifetime()); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -210,14 +237,82 @@ void AddTableRow( valueMargins); } +object_ptr MakeStarGiftStarsValue( + not_null parent, + not_null controller, + const Data::CreditsHistoryEntry &entry, + Fn convertToStars) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto session = &controller->session(); + const auto makeContext = [session](Fn update) { + return Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = std::move(update), + }; + }; + auto star = session->data().customEmojiManager().creditsEmoji(); + const auto label = Ui::CreateChild( + raw, + rpl::single( + star.append(' ' + Lang::FormatCountDecimal(entry.credits))), + st::giveawayGiftCodeValue, + st::defaultPopupMenu, + std::move(makeContext)); + + const auto convert = convertToStars + ? Ui::CreateChild( + raw, + tr::lng_gift_sell_small( + lt_count_decimal, + rpl::single(entry.starsConverted * 1.)), + st::starGiftSmallButton) + : nullptr; + if (convert) { + convert->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + convert->setClickedCallback(std::move(convertToStars)); + } + rpl::combine( + raw->widthValue(), + convert ? convert->widthValue() : rpl::single(0) + ) | rpl::start_with_next([=](int width, int convertWidth) { + const auto convertSkip = convertWidth + ? (st::normalFont->spacew + convertWidth) + : 0; + label->resizeToNaturalWidth(width - convertSkip); + label->moveToLeft(0, 0, width); + if (convert) { + convert->moveToLeft( + label->width() + st::normalFont->spacew, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + } + }, label->lifetime()); + + label->heightValue() | rpl::start_with_next([=](int height) { + raw->resize( + raw->width(), + height + st::giveawayGiftCodeValueMargin.bottom()); + }, raw->lifetime()); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + return result; +} + not_null AddTableRow( not_null table, rpl::producer label, - rpl::producer value) { + rpl::producer value, + const Fn)> &makeContext = nullptr) { auto widget = object_ptr( table, std::move(value), - st::giveawayGiftCodeValue); + st::giveawayGiftCodeValue, + st::defaultPopupMenu, + std::move(makeContext)); const auto result = widget.data(); AddTableRow( table, @@ -939,26 +1034,57 @@ void ResolveGiveawayInfo( void AddStarGiftTable( not_null controller, not_null container, - const Data::CreditsHistoryEntry &entry) { + const Data::CreditsHistoryEntry &entry, + Fn convertToStars) { auto table = container->add( object_ptr( container, st::giveawayGiftCodeTable), st::giveawayGiftCodeTableMargin); const auto peerId = PeerId(entry.barePeerId); + const auto session = &controller->session(); if (peerId) { + const auto user = session->data().peer(peerId)->asUser(); + const auto withSendButton = entry.in && user && !user->isBot(); AddTableRow( table, tr::lng_credits_box_history_entry_peer_in(), - controller, - peerId); - } else { + MakePeerTableValue(table, controller, peerId, withSendButton), + st::giveawayGiftCodePeerMargin); + } else if (!entry.soldOutInfo) { AddTableRow( table, tr::lng_credits_box_history_entry_peer_in(), MakeHiddenPeerTableValue(table, controller), st::giveawayGiftCodePeerMargin); } + if (!entry.firstSaleDate.isNull()) { + AddTableRow( + table, + tr::lng_gift_link_label_first_sale(), + rpl::single(Ui::Text::WithEntities( + langDateTime(entry.firstSaleDate)))); + } + if (!entry.lastSaleDate.isNull()) { + AddTableRow( + table, + tr::lng_gift_link_label_last_sale(), + rpl::single(Ui::Text::WithEntities( + langDateTime(entry.lastSaleDate)))); + } + { + const auto margin = st::giveawayGiftCodeValueMargin + - QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom()); + AddTableRow( + table, + tr::lng_gift_link_label_value(), + MakeStarGiftStarsValue( + table, + controller, + entry, + std::move(convertToStars)), + margin); + } if (!entry.date.isNull()) { AddTableRow( table, @@ -967,14 +1093,14 @@ void AddStarGiftTable( } if (entry.limitedCount > 0) { auto amount = rpl::single(TextWithEntities{ - QString::number(entry.limitedCount) + Lang::FormatCountDecimal(entry.limitedCount) }); AddTableRow( table, tr::lng_gift_availability(), ((entry.limitedLeft > 0) ? tr::lng_gift_availability_left( - lt_count, + lt_count_decimal, rpl::single(entry.limitedLeft * 1.), lt_amount, std::move(amount), @@ -985,7 +1111,6 @@ void AddStarGiftTable( Ui::Text::WithEntities))); } if (!entry.description.empty()) { - const auto session = &controller->session(); const auto makeContext = [=](Fn update) { return Core::MarkedTextContext{ .session = session, @@ -1020,12 +1145,17 @@ void AddCreditsHistoryEntryTable( st::giveawayGiftCodeTable), st::giveawayGiftCodeTableMargin); const auto peerId = PeerId(entry.barePeerId); + const auto actorId = PeerId(entry.bareActorId); const auto session = &controller->session(); - if (peerId) { + if (actorId || 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); + AddTableRow( + table, + std::move(text), + controller, + actorId ? actorId : peerId); } if (const auto msgId = MsgId(peerId ? entry.bareMsgId : 0)) { const auto peer = session->data().peer(peerId); @@ -1044,7 +1174,9 @@ void AddCreditsHistoryEntryTable( }); AddTableRow( table, - tr::lng_credits_box_history_entry_media(), + (entry.reaction + ? tr::lng_credits_box_history_entry_message + : tr::lng_credits_box_history_entry_media)(), std::move(label), st::giveawayGiftCodeValueMargin); } @@ -1116,12 +1248,24 @@ void AddCreditsHistoryEntryTable( } } if (!entry.id.isEmpty()) { - constexpr auto kOneLineCount = 18; - const auto oneLine = entry.id.length() <= kOneLineCount; + constexpr auto kOneLineCount = 22; + const auto oneLine = entry.id.size() <= kOneLineCount; + auto multiLine = QString(); + if (!oneLine) { + for (auto i = 0; i < entry.id.size(); ++i) { + multiLine.append(entry.id[i]); + if ((i + 1) % kOneLineCount == 0) { + multiLine.append('\n'); + } + } + } auto label = object_ptr( table, rpl::single( - Ui::Text::Wrapped({ entry.id }, EntityType::Code, {})), + Ui::Text::Wrapped( + { oneLine ? entry.id : std::move(multiLine) }, + EntityType::Code, + {})), oneLine ? st::giveawayGiftCodeValue : st::giveawayGiftCodeValueMultiline); @@ -1138,6 +1282,14 @@ void AddCreditsHistoryEntryTable( std::move(label), st::giveawayGiftCodeValueMargin); } + if (entry.floodSkip) { + AddTableRow( + table, + tr::lng_credits_box_history_entry_floodskip_row(), + rpl::single( + Ui::Text::WithEntities( + Lang::FormatCountDecimal(entry.floodSkip)))); + } if (!entry.date.isNull()) { AddTableRow( table, @@ -1178,6 +1330,16 @@ void AddSubscriptionEntryTable( controller, peerId); if (!s.until.isNull()) { + if (s.subscription.period > 0) { + const auto subscribed = s.until.addSecs(-s.subscription.period); + if (subscribed.isValid()) { + AddTableRow( + table, + tr::lng_group_invite_joined_row_date(), + rpl::single( + Ui::Text::WithEntities(langDateTime(subscribed)))); + } + } AddTableRow( table, s.expired diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 31ca5fcf2..3a2c20498 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -57,7 +57,8 @@ void ResolveGiveawayInfo( void AddStarGiftTable( not_null controller, not_null container, - const Data::CreditsHistoryEntry &entry); + const Data::CreditsHistoryEntry &entry, + Fn convertToStars); void AddCreditsHistoryEntryTable( not_null controller, not_null container, diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index fcdbd8d6d..647d6e0e9 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -240,7 +240,8 @@ int LocalStorageBox::Row::resizeGetHeight(int newWidth) { } void LocalStorageBox::Row::paintEvent(QPaintEvent *e) { - if (!_progress || true) { +#if 0 // not used + if (!_progress) { return; } auto p = QPainter(this); @@ -254,6 +255,7 @@ void LocalStorageBox::Row::paintEvent(QPaintEvent *e) { st::proxyCheckingPosition.y() + bottom }, width()); +#endif } QString LocalStorageBox::Row::titleText(const Database::TaggedSummary &data) const { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index b0f3c0a50..5a440ced5 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -206,7 +206,9 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) { content()->selectSkipPage(height(), 1); } else if (e->key() == Qt::Key_PageUp) { content()->selectSkipPage(height(), -1); - } else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) { + } else if (e->key() == Qt::Key_Escape + && _select + && !_select->entity()->getQuery().isEmpty()) { _select->entity()->clearQuery(); } else { BoxContent::keyPressEvent(e); @@ -215,7 +217,19 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) { void PeerListBox::searchQueryChanged(const QString &query) { scrollToY(0); - content()->searchQueryChanged(query); + const auto isEmpty = content()->searchQueryChanged(query); + if (_specialTabsMode.enabled) { + const auto was = _specialTabsMode.searchIsActive; + _specialTabsMode.searchIsActive = !isEmpty; + if (was != _specialTabsMode.searchIsActive) { + if (_specialTabsMode.searchIsActive) { + _specialTabsMode.topSkip = _addedTopScrollSkip; + setAddedTopScrollSkip(0); + } else { + setAddedTopScrollSkip(_specialTabsMode.topSkip); + } + } + } } void PeerListBox::resizeEvent(QResizeEvent *e) { @@ -543,6 +557,19 @@ auto PeerListBox::collectSelectedRows() return result; } +rpl::producer PeerListBox::multiSelectHeightValue() const { + return _select ? _select->heightValue() : rpl::single(0); +} + +void PeerListBox::setSpecialTabMode(bool value) { + content()->setIgnoreHiddenRowsOnSearch(value); + if (value) { + _specialTabsMode.enabled = true; + } else { + _specialTabsMode = {}; + } +} + PeerListRow::PeerListRow(not_null peer) : PeerListRow(peer, peer->id.value) { } @@ -1385,10 +1412,12 @@ int PeerListContent::labelHeight() const { void PeerListContent::refreshRows() { if (!_hiddenRows.empty()) { - _filterResults.clear(); - for (const auto &row : _rows) { - if (!row->hidden()) { - _filterResults.push_back(row.get()); + if (!_ignoreHiddenRowsOnSearch || _normalizedSearchQuery.isEmpty()) { + _filterResults.clear(); + for (const auto &row : _rows) { + if (!row->hidden()) { + _filterResults.push_back(row.get()); + } } } } @@ -1944,6 +1973,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) { } } + if (_controller->overrideKeyboardNavigation( + direction, + _selected.index.value, + newSelectedIndex)) { + return { _selected.index.value, _selected.index.value }; + } + _selected.index.value = newSelectedIndex; _selected.element = 0; if (newSelectedIndex >= 0) { @@ -2043,13 +2079,16 @@ void PeerListContent::checkScrollForPreload() { } } -void PeerListContent::searchQueryChanged(QString query) { +PeerListContent::IsEmpty PeerListContent::searchQueryChanged(QString query) { const auto searchWordsList = TextUtilities::PrepareSearchWords(query); const auto normalizedQuery = searchWordsList.join(' '); + if (_ignoreHiddenRowsOnSearch && !normalizedQuery.isEmpty()) { + _filterResults.clear(); + } if (_normalizedSearchQuery != normalizedQuery) { setSearchQuery(query, normalizedQuery); if (_controller->searchInLocal() && !searchWordsList.isEmpty()) { - Assert(_hiddenRows.empty()); + Assert(_hiddenRows.empty() || _ignoreHiddenRowsOnSearch); auto minimalList = (const std::vector>*)nullptr; for (const auto &searchWord : searchWordsList) { @@ -2097,6 +2136,7 @@ void PeerListContent::searchQueryChanged(QString query) { } refreshRows(); } + return _normalizedSearchQuery.isEmpty(); } std::unique_ptr PeerListContent::saveState() const { @@ -2185,6 +2225,10 @@ void PeerListContent::dragLeft() { clearSelection(); } +void PeerListContent::setIgnoreHiddenRowsOnSearch(bool value) { + _ignoreHiddenRowsOnSearch = value; +} + void PeerListContent::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 4c4374b84..bd71470a7 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -357,6 +357,8 @@ public: virtual int peerListPartitionRows(Fn border) = 0; virtual std::shared_ptr peerListUiShow() = 0; + virtual void peerListSelectSkip(int direction) = 0; + virtual void peerListPressLeftToContextMenu(bool shown) = 0; virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0; @@ -573,6 +575,13 @@ public: Unexpected("PeerListController::customRowRippleMaskGenerator."); } + virtual bool overrideKeyboardNavigation( + int direction, + int fromIndex, + int toIndex) { + return false; + } + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -643,12 +652,15 @@ public: [[nodiscard]] bool hasPressed() const; void clearSelection(); - void searchQueryChanged(QString query); + using IsEmpty = bool; + IsEmpty searchQueryChanged(QString query); bool submitted(); PeerListRowId updateFromParentDrag(QPoint globalPosition); void dragLeft(); + void setIgnoreHiddenRowsOnSearch(bool value); + // Interface for the controller. void appendRow(std::unique_ptr row); void appendSearchRow(std::unique_ptr row); @@ -870,6 +882,7 @@ private: int _aboveHeight = 0; int _belowHeight = 0; bool _hideEmpty = false; + bool _ignoreHiddenRowsOnSearch = false; object_ptr _aboveWidget = { nullptr }; object_ptr _aboveSearchWidget = { nullptr }; object_ptr _belowWidget = { nullptr }; @@ -1016,6 +1029,10 @@ public: bool highlightRow, Fn)> destroyed = nullptr) override; + void peerListSelectSkip(int direction) override { + _content->selectSkip(direction); + } + void peerListPressLeftToContextMenu(bool shown) override { _content->pressLeftToContextMenu(shown); } @@ -1089,6 +1106,9 @@ public: [[nodiscard]] std::vector collectSelectedIds(); [[nodiscard]] std::vector> collectSelectedRows(); + [[nodiscard]] rpl::producer multiSelectHeightValue() const; + + void setSpecialTabMode(bool value); void peerListSetTitle(rpl::producer title) override { setTitle(std::move(title)); @@ -1155,4 +1175,11 @@ private: bool _scrollBottomFixed = false; int _addedTopScrollSkip = 0; + struct SpecialTabsMode final { + bool enabled = false; + bool searchIsActive = false; + int topSkip = 0; + }; + SpecialTabsMode _specialTabsMode; + }; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 53d56080a..05b8d6d20 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -1065,6 +1065,11 @@ std::unique_ptr ChooseTopicBoxController::createSearchRow( return nullptr; } +std::unique_ptr ChooseTopicBoxController::MakeRow( + not_null topic) { + return std::make_unique(topic); +} + auto ChooseTopicBoxController::createRow(not_null topic) -> std::unique_ptr { const auto skip = _filter && !_filter(topic); diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 20479c2b4..07f71534a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -335,6 +335,9 @@ public: void loadMoreRows() override; std::unique_ptr createSearchRow(PeerListRowId id) override; + [[nodiscard]] static std::unique_ptr MakeRow( + not_null topic); + private: class Row final : public PeerListRow { public: diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 345d9880e..bdd3bdc24 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -30,10 +30,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "base/unixtime.h" #include "ui/effects/outline_segments.h" +#include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" +#include "ui/text/text_utilities.h" #include "info/profile/info_profile_values.h" #include "window/window_session_controller.h" #include "history/history.h" +#include "styles/style_chat.h" #include "styles/style_menu_icons.h" namespace { @@ -1645,6 +1648,51 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( auto result = base::make_unique_q( parent, st::popupMenuWithIcons); + const auto addToEnd = gsl::finally([&] { + const auto addInfoAction = [&]( + not_null by, + tr::phrase phrase, + TimeId since) { + auto text = phrase( + tr::now, + lt_user, + Ui::Text::Bold(by->name()), + lt_date, + Ui::Text::Bold( + langDateTimeFull(base::unixtime::parse(since))), + Ui::Text::WithEntities); + auto button = base::make_unique_q( + result->menu(), + result->st().menu, + st::historyHasCustomEmoji, + st::historyHasCustomEmojiPosition, + std::move(text)); + if (const auto n = _navigation) { + button->setClickedCallback([=] { + n->parentController()->show(PrepareShortInfoBox(by, n)); + }); + } + result->addSeparator(); + result->addAction(std::move(button)); + }; + + if (const auto by = _additional.restrictedBy(participant)) { + if (const auto since = _additional.restrictedSince(participant)) { + addInfoAction( + by, + _additional.isKicked(participant) + ? tr::lng_rights_chat_banned_by + : tr::lng_rights_chat_restricted_by, + since); + } + } else if (user) { + if (const auto by = _additional.adminPromotedBy(user)) { + if (const auto since = _additional.adminPromotedSince(user)) { + addInfoAction(by, tr::lng_rights_about_by, since); + } + } + } + }); if (_navigation) { result->addAction( (participant->isUser() @@ -1652,39 +1700,14 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( : participant->isBroadcast() ? tr::lng_context_view_channel : tr::lng_context_view_group)(tr::now), - crl::guard(this, [=] { - _navigation->showPeerInfo(participant); }), + crl::guard(this, [=, this] { + _navigation->parentController()->show( + PrepareShortInfoBox(participant, _navigation)); + }), (participant->isUser() ? &st::menuIconProfile : &st::menuIconInfo)); } - if (const auto by = _additional.restrictedBy(participant)) { - result->addAction( - (_role == Role::Kicked - ? tr::lng_channel_banned_status_removed_by - : tr::lng_channel_banned_status_restricted_by)( - tr::now, - lt_user, - by->name()), - crl::guard(this, [=] { - _navigation->parentController()->show( - PrepareShortInfoBox(by, _navigation)); - }), - &st::menuIconAdmin); - } else if (user) { - if (const auto by = _additional.adminPromotedBy(user)) { - result->addAction( - tr::lng_channel_admin_status_promoted_by( - tr::now, - lt_user, - by->name()), - crl::guard(this, [=] { - _navigation->parentController()->show( - PrepareShortInfoBox(by, _navigation)); - }), - &st::menuIconAdmin); - } - } if (_role == Role::Kicked) { if (_peer->isMegagroup() && _additional.canRestrictParticipant(participant)) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 4b11e3745..76fd9856b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_credits.h" #include "api/api_peer_photo.h" +#include "api/api_statistics.h" #include "api/api_user_names.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" @@ -46,6 +47,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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/channel_statistics/earn/earn_format.h" +#include "info/channel_statistics/earn/earn_icons.h" +#include "info/channel_statistics/earn/info_channel_earn_widget.h" #include "info/profile/info_profile_values.h" #include "info/info_memento.h" #include "lang/lang_keys.h" @@ -352,7 +356,8 @@ private: void fillPendingRequestsButton(); void fillBotUsernamesButton(); - void fillBotBalanceButton(); + void fillBotCurrencyButton(); + void fillBotCreditsButton(); void fillBotEditIntroButton(); void fillBotEditCommandsButton(); void fillBotEditSettingsButton(); @@ -1174,7 +1179,8 @@ void Controller::fillManageSection() { ::AddSkip(container, 0); fillBotUsernamesButton(); - fillBotBalanceButton(); + fillBotCurrencyButton(); + fillBotCreditsButton(); fillBotEditIntroButton(); fillBotEditCommandsButton(); fillBotEditSettingsButton(); @@ -1583,7 +1589,72 @@ void Controller::fillBotUsernamesButton() { { &st::menuIconLinks }); } -void Controller::fillBotBalanceButton() { +void Controller::fillBotCurrencyButton() { + Expects(_isBot); + + struct State final { + rpl::variable balance; + }; + + auto &lifetime = _controls.buttonsLayout->lifetime(); + const auto state = lifetime.make_state(); + const auto format = [=](uint64 balance) { + return Info::ChannelEarn::MajorPart(balance) + + Info::ChannelEarn::MinorPart(balance); + }; + const auto was = _peer->session().credits().balanceCurrency( + _peer->id); + if (was) { + state->balance = format(was); + } + + const auto wrap = _controls.buttonsLayout->add( + object_ptr>( + _controls.buttonsLayout, + EditPeerInfoBox::CreateButton( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_balance_currency(), + state->balance.value(), + [controller = _navigation->parentController(), peer = _peer] { + controller->showSection(Info::ChannelEarn::Make(peer)); + }, + st::manageGroupButton, + {}))); + wrap->toggle(!state->balance.current().isEmpty(), anim::type::instant); + + const auto button = wrap->entity(); + { + const auto currencyLoad + = button->lifetime().make_state(_peer); + currencyLoad->request( + ) | rpl::start_with_error_done([=](const QString &error) { + }, [=] { + const auto balance = currencyLoad->data().currentBalance; + if (balance) { + wrap->toggle(true, anim::type::normal); + } + state->balance = format(balance); + }, button->lifetime()); + } + { + const auto icon = Ui::CreateChild(button); + icon->resize(st::menuIconLinks.size()); + const auto image = Ui::Earn::MenuIconCurrency(icon->size()); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + p.drawImage(0, 0, image); + }, icon->lifetime()); + + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + icon->moveToLeft( + button->st().iconLeft, + (size.height() - icon->height()) / 2); + }, icon->lifetime()); + } +} + +void Controller::fillBotCreditsButton() { Expects(_isBot); struct State final { @@ -1593,7 +1664,7 @@ void Controller::fillBotBalanceButton() { auto &lifetime = _controls.buttonsLayout->lifetime(); const auto state = lifetime.make_state(); if (const auto balance = _peer->session().credits().balance(_peer->id)) { - state->balance = QString::number(balance); + state->balance = Lang::FormatCountDecimal(balance); } const auto wrap = _controls.buttonsLayout->add( @@ -1601,7 +1672,7 @@ void Controller::fillBotBalanceButton() { _controls.buttonsLayout, EditPeerInfoBox::CreateButton( _controls.buttonsLayout, - tr::lng_manage_peer_bot_balance(), + tr::lng_manage_peer_bot_balance_credits(), state->balance.value(), [controller = _navigation->parentController(), peer = _peer] { controller->showSection(Info::BotEarn::Make(peer)); @@ -1618,46 +1689,22 @@ void Controller::fillBotBalanceButton() { if (data.balance) { wrap->toggle(true, anim::type::normal); } - state->balance = QString::number(data.balance); + state->balance = Lang::FormatCountDecimal(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([=] { + const auto image = Ui::Earn::MenuIconCredits(); + icon->resize(image.size() / style::DevicePixelRatio()); + icon->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(icon); - svg->render(&p, Rect(starSize)); + p.drawImage(0, 0, image); }, icon->lifetime()); button->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { icon->moveToLeft( - button->st().iconLeft + kSizeShift / 2., + button->st().iconLeft, (size.height() - icon->height()) / 2); }, icon->lifetime()); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 8e1a11030..c8b4f2d48 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -12,12 +12,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/gift_premium_box.h" #include "boxes/peer_list_box.h" +#include "boxes/peer_list_controllers.h" #include "boxes/share_box.h" #include "core/application.h" #include "core/ui_integration.h" // Core::MarkedTextContext. #include "data/components/credits.h" #include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_peer.h" #include "data/data_session.h" @@ -51,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_credits.h" +#include "styles/style_dialogs.h" #include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_layers.h" // st::boxDividerLabel. @@ -264,8 +267,9 @@ private: class SingleRowController final : public PeerListController { public: SingleRowController( - not_null peer, - rpl::producer status); + not_null thread, + rpl::producer status, + Fn clicked); void prepare() override; void loadMoreRows() override; @@ -273,8 +277,10 @@ public: Main::Session &session() const override; private: - const not_null _peer; + const not_null _session; + const base::weak_ptr _thread; rpl::producer _status; + Fn _clicked; rpl::lifetime _lifetime; }; @@ -956,12 +962,11 @@ void Controller::rowClicked(not_null row) { Ui::AddSkip(content); Ui::AddSkip(content); - const auto &stUser = st::boostReplaceUserpic; + const auto photoSize = st::boostReplaceUserpic.photoSize; const auto session = &row->peer()->session(); content->add(object_ptr>( content, - object_ptr(content, channel, stUser)) - )->setAttribute(Qt::WA_TransparentForMouseEvents); + Settings::SubscriptionUserpic(content, channel, photoSize))); Ui::AddSkip(content); Ui::AddSkip(content); @@ -1145,36 +1150,59 @@ int Controller::descriptionTopSkipMin() const { } SingleRowController::SingleRowController( - not_null peer, - rpl::producer status) -: _peer(peer) -, _status(std::move(status)) { + not_null thread, + rpl::producer status, + Fn clicked) +: _session(&thread->session()) +, _thread(thread) +, _status(std::move(status)) +, _clicked(std::move(clicked)) { } void SingleRowController::prepare() { - auto row = std::make_unique(_peer); - + const auto strong = _thread.get(); + if (!strong) { + return; + } + const auto topic = strong->asTopic(); + auto row = topic + ? ChooseTopicBoxController::MakeRow(topic) + : std::make_unique(strong->peer()); const auto raw = row.get(); - std::move( - _status - ) | rpl::start_with_next([=](const QString &status) { - raw->setCustomStatus(status); - delegate()->peerListUpdateRow(raw); - }, _lifetime); - + if (_status) { + std::move( + _status + ) | rpl::start_with_next([=](const QString &status) { + raw->setCustomStatus(status); + delegate()->peerListUpdateRow(raw); + }, _lifetime); + } delegate()->peerListAppendRow(std::move(row)); delegate()->peerListRefreshRows(); + + if (topic) { + topic->destroyed() | rpl::start_with_next([=] { + while (delegate()->peerListFullRowsCount()) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(0)); + } + delegate()->peerListRefreshRows(); + }, _lifetime); + } } void SingleRowController::loadMoreRows() { } void SingleRowController::rowClicked(not_null row) { - ShowPeerInfoSync(row->peer()); + if (const auto onstack = _clicked) { + onstack(); + } else { + ShowPeerInfoSync(row->peer()); + } } Main::Session &SingleRowController::session() const { - return _peer->session(); + return *_session; } } // namespace @@ -1187,14 +1215,29 @@ bool IsExpiredLink(const Api::InviteLink &data, TimeId now) { void AddSinglePeerRow( not_null container, not_null peer, - rpl::producer status) { + rpl::producer status, + Fn clicked) { + AddSinglePeerRow( + container, + peer->owner().history(peer), + std::move(status), + std::move(clicked)); +} + +void AddSinglePeerRow( + not_null container, + not_null thread, + rpl::producer status, + Fn clicked) { const auto delegate = container->lifetime().make_state< PeerListContentDelegateSimple >(); const auto controller = container->lifetime().make_state< SingleRowController - >(peer, std::move(status)); - controller->setStyleOverrides(&st::peerListSingleRow); + >(thread, std::move(status), std::move(clicked)); + controller->setStyleOverrides(thread->asTopic() + ? &st::chooseTopicList + : &st::peerListSingleRow); const auto content = container->add(object_ptr( container, controller)); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h index e59ffdd5e..45abe3a46 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h @@ -16,6 +16,10 @@ namespace Api { struct InviteLink; } // namespace Api +namespace Data { +class Thread; +} // namespace Data + namespace Main { class Session; } // namespace Main @@ -31,7 +35,14 @@ class BoxContent; void AddSinglePeerRow( not_null container, not_null peer, - rpl::producer status); + rpl::producer status, + Fn clicked = nullptr); + +void AddSinglePeerRow( + not_null container, + not_null thread, + rpl::producer status, + Fn clicked = nullptr); void AddPermanentLinkBlock( std::shared_ptr show, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp index db23233aa..4663d4332 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp @@ -7,27 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peers/edit_peer_requests_box.h" -#include "ui/effects/ripple_animation.h" +#include "api/api_invite_links.h" +#include "apiwrap.h" +#include "base/unixtime.h" #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration #include "boxes/peers/edit_peer_invite_link.h" // PrepareRequestedRowStatus -#include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfoBox -#include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit -#include "data/data_peer.h" -#include "data/data_user.h" -#include "data/data_chat.h" +#include "boxes/peers/edit_peer_requests_box.h" #include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_peer.h" #include "data/data_session.h" -#include "base/unixtime.h" +#include "data/data_user.h" +#include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "info/requests_list/info_requests_list_widget.h" +#include "lang/lang_keys.h" #include "main/main_session.h" #include "mtproto/sender.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" #include "ui/round_rect.h" #include "ui/text/text_utilities.h" -#include "ui/painter.h" -#include "lang/lang_keys.h" #include "window/window_session_controller.h" -#include "apiwrap.h" -#include "api/api_invite_links.h" #include "styles/style_boxes.h" namespace { @@ -262,14 +265,10 @@ RequestsBoxController::~RequestsBoxController() = default; void RequestsBoxController::Start( not_null navigation, not_null peer) { - auto controller = std::make_unique( - navigation, - peer->migrateToOrMe()); - const auto initBox = [=](not_null box) { - box->addButton(tr::lng_close(), [=] { box->closeBox(); }); - }; - navigation->parentController()->show( - Box(std::move(controller), initBox)); + navigation->showSection( + std::make_shared( + peer->migrateToOrMe(), + Info::Section::Type::RequestsList)); } Main::Session &RequestsBoxController::session() const { @@ -289,6 +288,58 @@ std::unique_ptr RequestsBoxController::createSearchRow( return nullptr; } +std::unique_ptr RequestsBoxController::createRestoredRow( + not_null peer) { + if (const auto user = peer->asUser()) { + return createRow(user, _dates[user]); + } + return nullptr; +} + +auto RequestsBoxController::saveState() const +-> std::unique_ptr { + auto result = PeerListController::saveState(); + + auto my = std::make_unique(); + my->dates = _dates; + my->offsetDate = _offsetDate; + my->offsetUser = _offsetUser; + my->allLoaded = _allLoaded; + my->wasLoading = (_loadRequestId != 0); + if (const auto search = searchController()) { + my->searchState = search->saveState(); + } + result->controllerState = std::move(my); + return result; +} + +void RequestsBoxController::restoreState( + std::unique_ptr state) { + auto typeErasedState = state + ? state->controllerState.get() + : nullptr; + if (const auto my = dynamic_cast(typeErasedState)) { + if (const auto requestId = base::take(_loadRequestId)) { + _api.request(requestId).cancel(); + } + _dates = std::move(my->dates); + _offsetDate = my->offsetDate; + _offsetUser = my->offsetUser; + _allLoaded = my->allLoaded; + if (const auto search = searchController()) { + search->restoreState(std::move(my->searchState)); + } + if (my->wasLoading) { + loadMoreRows(); + } + PeerListController::restoreState(std::move(state)); + if (delegate()->peerListFullRowsCount() || _allLoaded) { + refreshDescription(); + delegate()->peerListRefreshRows(); + } + } +} + void RequestsBoxController::prepare() { delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); delegate()->peerListSetTitle(_peer->isBroadcast() @@ -356,9 +407,7 @@ void RequestsBoxController::refreshDescription() { } void RequestsBoxController::rowClicked(not_null row) { - _navigation->parentController()->show(PrepareShortInfoBox( - row->peer(), - _navigation)); + _navigation->showPeerInfo(row->peer()); } void RequestsBoxController::rowElementClicked( @@ -405,6 +454,7 @@ void RequestsBoxController::appendRow( not_null user, TimeId date) { if (!delegate()->peerListFindRow(user->id.value)) { + _dates.emplace(user, date); if (auto row = createRow(user, date)) { delegate()->peerListAppendRow(std::move(row)); setDescriptionText(QString()); @@ -503,6 +553,7 @@ std::unique_ptr RequestsBoxController::createRow( const auto search = static_cast( searchController()); date = search->dateForUser(user); + _dates.emplace(user, date); } return std::make_unique(_helper.get(), user, date); } @@ -574,6 +625,36 @@ TimeId RequestsBoxSearchController::dateForUser(not_null user) { return {}; } +auto RequestsBoxSearchController::saveState() const +-> std::unique_ptr { + auto result = std::make_unique(); + result->query = _query; + result->offsetDate = _offsetDate; + result->offsetUser = _offsetUser; + result->allLoaded = _allLoaded; + result->wasLoading = (_requestId != 0); + return result; +} + +void RequestsBoxSearchController::restoreState( + std::unique_ptr state) { + if (auto my = dynamic_cast(state.get())) { + if (auto requestId = base::take(_requestId)) { + _api.request(requestId).cancel(); + } + _cache.clear(); + _queries.clear(); + + _allLoaded = my->allLoaded; + _offsetDate = my->offsetDate; + _offsetUser = my->offsetUser; + _query = my->query; + if (my->wasLoading) { + searchOnServer(); + } + } +} + bool RequestsBoxSearchController::searchInCache() { const auto i = _cache.find(_query); if (i != _cache.cend()) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h index 005b83c84..4db714bc3 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.h @@ -35,15 +35,32 @@ public: Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; - void rowElementClicked(not_null row, int element) override; + void rowElementClicked( + not_null row, + int element) override; void loadMoreRows() override; std::unique_ptr createSearchRow( not_null peer) override; + std::unique_ptr createRestoredRow( + not_null peer) override; + + std::unique_ptr saveState() const override; + void restoreState(std::unique_ptr state) override; private: class RowHelper; + struct SavedState : SavedStateBase { + using SearchStateBase = PeerListSearchController::SavedStateBase; + std::unique_ptr searchState; + base::flat_map, TimeId> dates; + TimeId offsetDate = 0; + UserData *offsetUser = nullptr; + bool allLoaded = false; + bool wasLoading = false; + }; + static std::unique_ptr CreateSearchController( not_null peer); @@ -63,6 +80,8 @@ private: not_null _peer; MTP::Sender _api; + base::flat_map, TimeId> _dates; + TimeId _offsetDate = 0; UserData *_offsetUser = nullptr; mtpRequestId _loadRequestId = 0; @@ -82,7 +101,17 @@ public: void removeFromCache(not_null user); [[nodiscard]] TimeId dateForUser(not_null user); + std::unique_ptr saveState() const override; + void restoreState(std::unique_ptr state) override; + private: + struct SavedState : SavedStateBase { + QString query; + TimeId offsetDate = 0; + UserData *offsetUser = nullptr; + bool allLoaded = false; + bool wasLoading = false; + }; struct Item { not_null user; TimeId date = 0; diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index 830dc13bd..dc7524e76 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -534,15 +534,16 @@ void PeerShortInfoCover::handleStreamingUpdate( v::match(update.data, [&](Information &update) { streamingReady(std::move(update)); - }, [&](const PreloadedVideo &update) { - }, [&](const UpdateVideo &update) { + }, [](PreloadedVideo) { + }, [&](UpdateVideo update) { _videoPosition = update.position; _widget->update(); - }, [&](const PreloadedAudio &update) { - }, [&](const UpdateAudio &update) { - }, [&](const WaitingForData &update) { - }, [&](MutedByOther) { - }, [&](Finished) { + }, [](PreloadedAudio) { + }, [](UpdateAudio) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { + }, [](Finished) { }); } @@ -774,6 +775,10 @@ void PeerShortInfoBox::prepareRows() { result->setContextCopyText(contextCopyText); return result; }; + addInfoOneLine( + tr::lng_settings_channel_label(), + channelValue(), + tr::lng_context_copy_link(tr::now)); addInfoOneLine( tr::lng_info_link_label(), linkValue(), @@ -835,6 +840,13 @@ rpl::producer PeerShortInfoBox::nameValue() const { }) | rpl::distinct_until_changed(); } +rpl::producer PeerShortInfoBox::channelValue() const { + return _fields.value( + ) | rpl::map([](const PeerShortInfoFields &fields) { + return Ui::Text::Link(fields.channelName, fields.channelLink); + }) | rpl::distinct_until_changed(); +} + rpl::producer PeerShortInfoBox::linkValue() const { return _fields.value( ) | rpl::map([](const PeerShortInfoFields &fields) { diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index cda68a99b..b51ac3e2d 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -37,6 +37,8 @@ enum class PeerShortInfoType { struct PeerShortInfoFields { QString name; + QString channelName; + QString channelLink; QString phone; QString link; TextWithEntities about; @@ -169,6 +171,7 @@ private: int fillRoundedTopHeight(); [[nodiscard]] rpl::producer nameValue() const; + [[nodiscard]] rpl::producer channelValue() const; [[nodiscard]] rpl::producer linkValue() const; [[nodiscard]] rpl::producer phoneValue() const; [[nodiscard]] rpl::producer usernameValue() const; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index 363a2e9dc..72482cffd 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -202,6 +202,7 @@ void ProcessFullPhoto( return peer->session().changes().peerFlagsValue( peer, (UpdateFlag::Name + | UpdateFlag::PersonalChannel | UpdateFlag::PhoneNumber | UpdateFlag::Username | UpdateFlag::About @@ -209,8 +210,20 @@ void ProcessFullPhoto( ) | rpl::map([=] { const auto user = peer->asUser(); const auto username = peer->username(); + const auto channelId = user ? user->personalChannelId() : 0; + const auto channel = channelId + ? user->owner().channel(channelId).get() + : nullptr; + const auto channelUsername = channel + ? channel->username() + : QString(); + const auto hasChannel = !channelUsername.isEmpty(); return PeerShortInfoFields{ .name = peer->name(), + .channelName = hasChannel ? channel->name() : QString(), + .channelLink = (hasChannel + ? channel->session().createInternalLinkFull(channelUsername) + : QString()), .phone = user ? Ui::FormatPhone(user->phone()) : QString(), .link = ((user || username.isEmpty()) ? QString() diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index f163be612..89e0edfed 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -77,7 +77,7 @@ bool operator==(const Descriptor &a, const Descriptor &b) { struct Preload { Descriptor descriptor; std::shared_ptr media; - std::weak_ptr show; + std::weak_ptr show; }; [[nodiscard]] std::vector &Preloads() { diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp index bac19fbf4..25e548f1b 100644 --- a/Telegram/SourceFiles/boxes/report_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp @@ -27,13 +27,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { -[[nodiscard]] object_ptr Report( +[[nodiscard]] object_ptr ReportPhoto( not_null peer, - std::variant> data, + not_null photo, const style::ReportBox *stOverride) { - const auto source = v::match(data, [](const MessageIdsList &ids) { - return Ui::ReportSource::Message; - }, [&](not_null photo) { + const auto source = [&] { return peer->isUser() ? (photo->hasVideo() ? Ui::ReportSource::ProfileVideo @@ -45,19 +43,14 @@ namespace { : (photo->hasVideo() ? Ui::ReportSource::ChannelVideo : Ui::ReportSource::ChannelPhoto); - }, [&](StoryId id) { - return Ui::ReportSource::Story; - }, [](v::null_t) { - Unexpected("Bad source report."); - return Ui::ReportSource::Bot; - }); + }(); const auto st = stOverride ? stOverride : &st::defaultReportBox; return Box([=](not_null box) { const auto show = box->uiShow(); Ui::ReportReasonBox(box, *st, source, [=](Ui::ReportReason reason) { show->showBox(Box([=](not_null box) { Ui::ReportDetailsBox(box, *st, [=](const QString &text) { - Api::SendReport(show, peer, reason, text, data); + Api::SendPhotoReport(show, peer, reason, text, photo); show->hideLayer(); }); })); @@ -70,7 +63,7 @@ namespace { object_ptr ReportProfilePhotoBox( not_null peer, not_null photo) { - return Report(peer, photo, nullptr); + return ReportPhoto(peer, photo, nullptr); } void ShowReportMessageBox( @@ -86,7 +79,6 @@ void ShowReportMessageBox( auto performRequest = [=]( const auto &repeatRequest, Data::ReportInput reportInput) -> void { - constexpr auto kToastDuration = crl::time(4000); report(reportInput, [=](const Api::ReportResult &result) { if (!result.error.isEmpty()) { if (result.error == u"MESSAGE_ID_REQUIRED"_q) { @@ -206,6 +198,7 @@ void ShowReportMessageBox( } })); } else if (result.successful) { + constexpr auto kToastDuration = crl::time(4000); show->showToast( tr::lng_report_thanks(tr::now), kToastDuration); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 9fb3f1219..6c4d7d76f 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "ui/boxes/confirm_box.h" #include "apiwrap.h" +#include "ui/widgets/chat_filters_tabs_strip.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/scroll_area.h" @@ -39,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/share_message_phrase_factory.h" #include "data/business/data_shortcut_messages.h" #include "data/data_channel.h" +#include "data/data_chat_filters.h" #include "data/data_game.h" #include "data/data_histories.h" #include "data/data_user.h" @@ -86,11 +88,14 @@ public: void activateSkipColumn(int direction); void activateSkipPage(int pageHeight, int direction); void updateFilter(QString filter = QString()); + [[nodiscard]] bool isFilterEmpty() const; void selectActive(); rpl::producer scrollToRequests() const; rpl::producer<> searchRequests() const; + void applyChatFilter(FilterId id); + protected: void visibleTopBottomUpdated( int visibleTop, @@ -171,7 +176,9 @@ private: int _upon = -1; int _visibleTop = 0; - std::unique_ptr _chatsIndexed; + std::unique_ptr _defaultChatsIndexed; + std::unique_ptr _customChatsIndexed; + not_null _chatsIndexed; QString _filter; std::vector> _filtered; @@ -287,6 +294,10 @@ void ShareBox::prepare() { _select->setQueryChangedCallback([=](const QString &query) { applyFilterUpdate(query); + if (_chatsFilters) { + updateScrollSkips(); + scrollToY(0); + } }); _select->setItemRemovedCallback([=](uint64 itemId) { if (const auto peer = _descriptor.session->data().peerLoaded(PeerId(itemId))) { @@ -342,10 +353,32 @@ void ShareBox::prepare() { { .suggestCustomEmoji = true }); _select->raise(); + + { + const auto chatsFilters = AddChatFiltersTabsStrip( + this, + _descriptor.session, + [this](FilterId id) { + _inner->applyChatFilter(id); + scrollToY(0); + }); + chatsFilters->lower(); + chatsFilters->heightValue() | rpl::start_with_next([this](int h) { + updateScrollSkips(); + scrollToY(0); + }, lifetime()); + _select->heightValue() | rpl::start_with_next([=](int h) { + chatsFilters->moveToLeft(0, h); + }, chatsFilters->lifetime()); + _chatsFilters = chatsFilters; + } } int ShareBox::getTopScrollSkip() const { - return _select->isHidden() ? 0 : _select->height(); + return (_select->isHidden() ? 0 : _select->height()) + + ((_chatsFilters && _inner && _inner->isFilterEmpty()) + ? _chatsFilters->height() + : 0); } int ShareBox::getBottomScrollSkip() const { @@ -676,9 +709,10 @@ ShareBox::Inner::Inner( , _descriptor(descriptor) , _show(std::move(show)) , _st(_descriptor.st ? *_descriptor.st : st::shareBoxList) -, _chatsIndexed( +, _defaultChatsIndexed( std::make_unique( - Dialogs::SortMode::Add)) { + Dialogs::SortMode::Add)) +, _chatsIndexed(_defaultChatsIndexed.get()) { _rowsTop = st::shareRowsTop; _rowHeight = st::shareRowHeight; setAttribute(Qt::WA_OpaquePaintEvent); @@ -696,7 +730,7 @@ ShareBox::Inner::Inner( const auto self = _descriptor.session->user(); const auto selfHistory = self->owner().history(self); if (_descriptor.filterCallback(selfHistory)) { - _chatsIndexed->addToEnd(selfHistory); + _defaultChatsIndexed->addToEnd(selfHistory); } const auto addList = [&](not_null list) { for (const auto &row : list->all()) { @@ -704,7 +738,7 @@ ShareBox::Inner::Inner( if (!history->peer->isSelf() && (history->asForum() || _descriptor.filterCallback(history))) { - _chatsIndexed->addToEnd(history); + _defaultChatsIndexed->addToEnd(history); } } } @@ -727,7 +761,7 @@ ShareBox::Inner::Inner( _descriptor.session->changes().realtimeNameUpdates( ) | rpl::start_with_next([=](const Data::NameUpdate &update) { - _chatsIndexed->peerNameChanged( + _defaultChatsIndexed->peerNameChanged( update.peer, update.oldFirstLetters); }, lifetime()); @@ -1336,6 +1370,10 @@ void ShareBox::Inner::updateFilter(QString filter) { } } +bool ShareBox::Inner::isFilterEmpty() const { + return _filter.isEmpty(); +} + rpl::producer ShareBox::Inner::scrollToRequests() const { return _scrollToRequests.events(); } @@ -1344,6 +1382,30 @@ rpl::producer<> ShareBox::Inner::searchRequests() const { return _searchRequests.events(); } +void ShareBox::Inner::applyChatFilter(FilterId id) { + if (!id) { + _chatsIndexed = _defaultChatsIndexed.get(); + } else { + _customChatsIndexed = std::make_unique( + Dialogs::SortMode::Add); + _chatsIndexed = _customChatsIndexed.get(); + + const auto addList = [&](not_null list) { + for (const auto &row : list->all()) { + if (const auto history = row->history()) { + if (history->asForum() + || _descriptor.filterCallback(history)) { + _customChatsIndexed->addToEnd(history); + } + } + } + }; + const auto &data = _descriptor.session->data(); + addList(data.chatsFilters().chatsList(id)->indexed()); + } + update(); +} + void ShareBox::Inner::peopleReceived( const QString &query, const QVector &my, diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index d0bf28ce9..ef6960af0 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -174,6 +174,8 @@ private: bool _peopleFull = false; mtpRequestId _peopleRequest = 0; + RpWidget *_chatsFilters = nullptr; + using PeopleCache = QMap; PeopleCache _peopleCache; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 5a863f605..781e008d2 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/random.h" +#include "base/unixtime.h" #include "api/api_premium.h" #include "boxes/peer_list_controllers.h" #include "boxes/send_credits_box.h" @@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "core/ui_integration.h" +#include "data/data_credits.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" @@ -40,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" #include "settings/settings_credits.h" +#include "settings/settings_credits_graphics.h" #include "settings/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -136,6 +139,23 @@ private: }; +[[nodiscard]] bool SortForBirthday(not_null peer) { + const auto user = peer->asUser(); + if (!user) { + return false; + } + const auto birthday = user->birthday(); + if (!birthday) { + return false; + } + const auto is = [&](const QDate &date) { + return (date.day() == birthday.day()) + && (date.month() == birthday.month()); + }; + const auto now = QDate::currentDate(); + return is(now) || is(now.addDays(1)) || is(now.addDays(-1)); +} + PreviewDelegate::PreviewDelegate( not_null parent, not_null st, @@ -213,7 +233,7 @@ auto GenerateGiftMedia( return tr::lng_action_gift_got_stars_text( tr::now, lt_count, - gift.convertStars, + gift.info.starsConverted, Ui::Text::RichLangValue); }); auto description = data.text.empty() @@ -280,7 +300,7 @@ void ShowSentToast( return tr::lng_gift_sent_about( tr::now, lt_count, - gift.stars, + gift.info.stars, Ui::Text::RichLangValue); }); const auto strong = window->showToast({ @@ -338,7 +358,10 @@ void PreviewWrap::prepare(rpl::producer details) { const auto cost = v::match(descriptor, [&](GiftTypePremium data) { return FillAmountAndCurrency(data.cost, data.currency, true); }, [&](GiftTypeStars data) { - return tr::lng_gift_stars_title(tr::now, lt_count, data.stars); + return tr::lng_gift_stars_title( + tr::now, + lt_count, + data.info.stars); }); const auto text = tr::lng_action_gift_received( tr::now, @@ -508,14 +531,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { const auto &gifts = api->starGifts(); list.reserve(gifts.size()); for (auto &gift : gifts) { - list.push_back({ - .id = gift.id, - .stars = gift.stars, - .convertStars = gift.convertStars, - .document = gift.document, - .limitedCount = gift.limitedCount, - .limitedLeft = gift.limitedLeft, - }); + list.push_back({ .info = gift }); } auto &map = Map[session]; if (map.last != list) { @@ -587,7 +603,8 @@ struct GiftPriceTabs { auto sameKey = 0; for (const auto &gift : gifts) { if (same) { - const auto key = gift.stars * (gift.limitedCount ? -1 : 1); + const auto key = gift.info.stars + * (gift.info.limitedCount ? -1 : 1); if (!sameKey) { sameKey = key; } else if (sameKey != key) { @@ -595,12 +612,12 @@ struct GiftPriceTabs { } } - if (gift.limitedCount + if (gift.info.limitedCount && (result.size() < 2 || result[1] != kPriceTabLimited)) { result.insert(begin(result) + 1, kPriceTabLimited); } - if (!ranges::contains(result, gift.stars)) { - result.push_back(gift.stars); + if (!ranges::contains(result, gift.info.stars)) { + result.push_back(gift.info.stars); } } if (same) { @@ -838,16 +855,38 @@ void SendGift( const auto processNonPanelPaymentFormFactory = Payments::ProcessNonPanelPaymentFormFactory(window, done); Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{ - .giftId = gift.id, + .giftId = gift.info.id, .randomId = details.randomId, .message = details.text, .user = peer->asUser(), - .limitedCount = gift.limitedCount, + .limitedCount = gift.info.limitedCount, .anonymous = details.anonymous, }, done, processNonPanelPaymentFormFactory); }); } +void SoldOutBox( + not_null box, + not_null window, + const GiftTypeStars &gift) { + Settings::ReceiptCreditsBox( + box, + window, + Data::CreditsHistoryEntry{ + .firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate), + .lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate), + .credits = uint64(gift.info.stars), + .bareGiftStickerId = gift.info.document->id, + .peerType = Data::CreditsHistoryEntry::PeerType::Peer, + .limitedCount = gift.info.limitedCount, + .limitedLeft = gift.info.limitedLeft, + .soldOutInfo = true, + .gift = true, + }, + Data::SubscriptionEntry()); + +} + void SendGiftBox( not_null box, not_null window, @@ -873,7 +912,7 @@ void SendGiftBox( }; }, [&](const GiftTypeStars &data) { return Ui::CreditsEmojiSmall(session).append( - Lang::FormatCountDecimal(std::abs(data.stars))); + Lang::FormatCountDecimal(std::abs(data.info.stars))); }); }()); @@ -1046,10 +1085,23 @@ void SendGiftBox( const auto padding = st::giftBoxPadding; const auto available = width - padding.left() - padding.right(); const auto perRow = available / single.width(); + const auto count = int(gifts.list.size()); + + auto order = ranges::views::ints + | ranges::views::take(count) + | ranges::to_vector; + + if (SortForBirthday(peer)) { + ranges::stable_partition(order, [&](int i) { + const auto &gift = gifts.list[i]; + const auto stars = std::get_if(&gift); + return stars && stars->info.birthday; + }); + } auto x = padding.left(); auto y = padding.top(); - state->buttons.resize(gifts.list.size()); + state->buttons.resize(count); for (auto &button : state->buttons) { if (!button) { button = std::make_unique(raw, &state->delegate); @@ -1057,9 +1109,9 @@ void SendGiftBox( } } const auto api = gifts.api; - for (auto i = 0, count = int(gifts.list.size()); i != count; ++i) { + for (auto i = 0; i != count; ++i) { const auto button = state->buttons[i].get(); - const auto &descriptor = gifts.list[i]; + const auto &descriptor = gifts.list[order[i]]; button->setDescriptor(descriptor); const auto last = !((i + 1) % perRow); @@ -1076,27 +1128,22 @@ void SendGiftBox( button->setClickedCallback([=] { const auto star = std::get_if(&descriptor); - if (star && star->limitedCount && !star->limitedLeft) { - window->showToast({ - .title = tr::lng_gift_sold_out_title(tr::now), - .text = tr::lng_gift_sold_out_text( - tr::now, - lt_count_decimal, - star->limitedCount, - Ui::Text::RichLangValue), - }); + if (star + && star->info.limitedCount + && !star->info.limitedLeft) { + window->show(Box(SoldOutBox, window, *star)); } else { window->show( Box(SendGiftBox, window, peer, api, descriptor)); } }); } - if (gifts.list.size() % perRow) { + if (count % perRow) { y += padding.bottom() + single.height(); } else { y += padding.bottom() - st::giftBoxGiftSkip.y(); } - raw->resize(raw->width(), gifts.list.empty() ? 0 : y); + raw->resize(raw->width(), count ? y : 0); }, raw->lifetime()); return result; @@ -1187,8 +1234,8 @@ void AddBlock( ) | rpl::map([=](std::vector &&gifts, int price) { gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) { return (price == kPriceTabLimited) - ? (!gift.limitedCount) - : (price && gift.stars != price); + ? (!gift.info.limitedCount) + : (price && gift.info.stars != price); }), end(gifts)); return GiftsDescriptor{ gifts | ranges::to>(), diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 0405c0174..234bdb8c0 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -409,9 +409,7 @@ callRatingStar: IconButton { icon: icon {{ "calls/call_rating", windowSubTextFg }}; iconPosition: point(-1px, -1px); - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; rippleAreaPosition: point(0px, 0px); rippleAreaSize: 36px; } @@ -1410,9 +1408,7 @@ groupCallRtmpShowButton: IconButton(defaultIconButton) { rippleAreaPosition: point(0px, 0px); rippleAreaSize: 32px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } groupCallSettingsRtmpShowButton: IconButton(groupCallRtmpShowButton) { ripple: groupCallRipple; diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp index fded07614..db94f72cb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp @@ -9,7 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "calls/group/calls_group_common.h" -#include "data/data_peer.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_session.h" @@ -169,7 +171,12 @@ void StartRtmpProcess::finish(JoinInfo info) { void StartRtmpProcess::createBox() { auto done = [=] { const auto peer = _request->peer; - finish({ .peer = peer, .joinAs = peer, .rtmp = true }); + const auto joinAs = (peer->isChat() && peer->asChat()->amCreator()) + ? peer + : (peer->isChannel() && peer->asChannel()->amCreator()) + ? peer + : peer->session().user(); + finish({ .peer = peer, .joinAs = joinAs, .rtmp = true }); }; auto revoke = [=] { const auto guard = base::make_weak(&_request->guard); 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 4e77023f2..a122f09a2 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -73,8 +73,8 @@ private: SourceButton _widget; FlatLabel _label; - RoundRect _selectedRect; - RoundRect _activeRect; + Ui::RoundRect _selectedRect; + Ui::RoundRect _activeRect; tgcalls::DesktopCaptureSource _source; std::unique_ptr _preview; rpl::event_stream<> _activations; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index a944d96e4..926117bcf 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -150,6 +150,8 @@ SendButton { inner: IconButton; record: icon; recordOver: icon; + round: icon; + roundOver: icon; sendDisabledFg: color; } @@ -331,9 +333,7 @@ stickersRemove: IconButton(defaultIconButton) { rippleAreaSize: 40px; rippleAreaPosition: point(0px, 0px); - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } stickersUndoRemove: RoundButton(defaultLightButton) { width: -16px; @@ -494,9 +494,7 @@ hashtagClose: IconButton { rippleAreaPosition: point(5px, 5px); rippleAreaSize: 20px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } stickerPanWidthMin: 64px; @@ -898,9 +896,7 @@ historyBusinessBotSettings: IconButton(defaultIconButton) { iconPosition: point(-1px, -1px); rippleAreaSize: 40px; rippleAreaPosition: point(4px, 9px); - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; height: 58px; width: 48px; } @@ -927,9 +923,7 @@ historyReplyCancel: IconButton { rippleAreaPosition: point(4px, 4px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } historyPinnedShowAll: IconButton(historyReplyCancel) { icon: icon {{ "pinned_show_all", historyReplyCancelFg }}; @@ -1058,9 +1052,7 @@ historyAttach: IconButton(defaultIconButton) { rippleAreaPosition: point(2px, 3px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } historyMessagesTTL: IconButtonWithText { @@ -1082,6 +1074,13 @@ historyReplaceMedia: IconButton(historyAttach) { color: lightButtonBgOver; } } +historyAddMedia: IconButton(historyAttach) { + icon: icon {{ "chat/input_attach", windowBgActive }}; + iconOver: icon {{ "chat/input_attach", windowBgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgOver; + } +} historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; historyEmojiCircle: size(20px, 20px); @@ -1169,6 +1168,10 @@ historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }}; historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; +historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }}; +historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }}; +historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }}; +historyRecordRoundIconPosition: point(0px, 0px); historyRecordSendIconPosition: point(2px, 0px); historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordSignalRadius: 5px; @@ -1214,6 +1217,7 @@ historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }} historyRecordLockMargin: margins(4px, 4px, 4px, 4px); historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; historyRecordLockInput: icon {{ "voice_lock/input_mic_s", historyToDownFg }}; +historyRecordLockRound: icon {{ "voice_lock/input_round_s", historyToDownFg }}; historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); historyRecordDelete: IconButton(historyAttach) { @@ -1274,6 +1278,8 @@ historySend: SendButton { } record: historyRecordVoice; recordOver: historyRecordVoiceOver; + round: historyRecordRound; + roundOver: historyRecordRoundOver; sendDisabledFg: historyComposeIconFg; } @@ -1287,9 +1293,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) { rippleAreaPosition: point(1px, 6px); rippleAreaSize: 42px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } defaultComposeFilesField: InputField(defaultInputField) { textMargins: margins(1px, 26px, 31px, 4px); @@ -1349,9 +1353,7 @@ moreChatsBarClose: IconButton(defaultIconButton) { rippleAreaPosition: point(0px, 4px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } reportReasonTopSkip: 8px; @@ -1517,3 +1519,22 @@ pickLocationChooseOnMap: RoundButton(defaultActiveButton) { sendGifBox: Box(defaultBox) { shadowIgnoreBottomSkip: true; } + +processingVideoTipMaxWidth: 364px; +processingVideoTipShift: 8px; +processingVideoToast: Toast(defaultToast) { + minWidth: 32px; + maxWidth: 380px; + padding: margins(19px, 17px, 19px, 17px); +} +processingVideoPreviewSkip: 8px; +processingVideoView: RoundButton(defaultActiveButton) { + width: -24px; + height: 52px; + textTop: 17px; + textFg: mediaviewTextLinkFg; + textFgOver: mediaviewTextLinkFg; + textBg: transparent; + textBgOver: transparent; + ripple: emptyRippleAnimation; +} diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index 8d666249f..237865424 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -822,7 +822,7 @@ void StickersListFooter::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; } - _iconsMousePos = e ? e->globalPos() : QCursor::pos(); + _iconsMousePos = e->globalPos(); updateSelected(); if (_selected == SpecialOver::Settings) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 095abbb57..9c830dc57 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -192,9 +192,7 @@ std::unique_ptr LottieThumbnail( }; const auto session = thumb ? &thumb->owner()->session() - : media - ? &media->owner()->session() - : nullptr; + : &media->owner()->session(); return LottieCachedFromContent( method, baseKey, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index d1368a8af..794803ee9 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -35,6 +35,10 @@ base::options::toggle TabbedPanelShowOnClick({ const char kOptionTabbedPanelShowOnClick[] = "tabbed-panel-show-on-click"; +bool ShowPanelOnClick() { + return TabbedPanelShowOnClick.value(); +} + TabbedPanel::TabbedPanel( QWidget *parent, not_null controller, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h index fcedb5efc..97d66e2c7 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h @@ -25,6 +25,7 @@ namespace ChatHelpers { class TabbedSelector; extern const char kOptionTabbedPanelShowOnClick[]; +[[nodiscard]] bool ShowPanelOnClick(); struct TabbedPanelDescriptor { Window::SessionController *regularWindow = nullptr; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 231d01972..ded39a2f8 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -189,6 +189,7 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const { const auto game = media ? media->game() : nullptr; if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive) || !_bot || !game) { openLink(); + return; } const auto bot = _bot; const auto title = game->title; diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index b3aa0bae0..43295e196 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -16,6 +16,7 @@ constexpr auto kDocumentLinkMediaProperty = 0x03; constexpr auto kSendReactionEmojiProperty = 0x04; constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kDocumentFilenameTooltipProperty = 0x06; +constexpr auto kPhoneNumberLinkProperty = 0x07; namespace Ui { class Show; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 91ccad4c9..deb71c0db 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -27,6 +27,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Core { namespace { +constexpr auto kInitialVideoQuality = 480; // Start with SD. + [[nodiscard]] WindowPosition Deserialize(const QByteArray &data) { QDataStream stream(data); stream.setVersion(QDataStream::Qt_5_1); @@ -92,6 +94,21 @@ void LogPosition(const WindowPosition &position, const QString &name) { return RecentEmojiDocument{ id, (test == '1') }; } +[[nodiscard]] quint32 SerializeVideoQuality(Media::VideoQuality quality) { + static_assert(sizeof(Media::VideoQuality) == sizeof(uint32)); + auto result = uint32(); + const auto data = static_cast(&quality); + memcpy(&result, data, sizeof(quality)); + return result; +} + +[[nodiscard]] Media::VideoQuality DeserializeVideoQuality(quint32 value) { + auto result = Media::VideoQuality(); + const auto data = static_cast(&result); + memcpy(data, &value, sizeof(result)); + return (result.height <= 4320) ? result : Media::VideoQuality(); +} + } // namespace [[nodiscard]] WindowPosition AdjustToScale( @@ -128,7 +145,8 @@ Settings::Settings() , _floatPlayerColumn(Window::Column::Second) , _floatPlayerCorner(RectPart::TopRight) , _dialogsWithChatWidthRatio(DefaultDialogsWidthRatio()) -, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio()) { +, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio()) +, _videoQuality({ .height = kInitialVideoQuality }) { } Settings::~Settings() = default; @@ -226,7 +244,7 @@ QByteArray Settings::serialize() const { + Serialize::stringSize(_customFontFamily) + sizeof(qint32) * 3 + Serialize::bytearraySize(_tonsiteStorageToken) - + sizeof(qint32) * 2; + + sizeof(qint32) * 7; auto result = QByteArray(); result.reserve(size); @@ -295,7 +313,7 @@ QByteArray Settings::serialize() const { << qint32(_thirdSectionExtendedBy) << qint32(_notifyFromAll ? 1 : 0) << qint32(_nativeWindowFrame.current() ? 1 : 0) - << qint32(_systemDarkModeEnabled.current() ? 1 : 0) + << qint32(0) // Legacy system dark mode << _cameraDeviceId.current() << qint32(_ipRevealWarning ? 1 : 0) << qint32(_groupCallPushToTalk ? 1 : 0) @@ -382,7 +400,12 @@ QByteArray Settings::serialize() const { << qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2) << _tonsiteStorageToken << qint32(_includeMutedCounterFolders ? 1 : 0) - << qint32(_ivZoom.current()); + << qint32(_chatFiltersHorizontal.current() ? 1 : 0) + << qint32(_skipToastsInFocus ? 1 : 0) + << qint32(_recordVideoMessages ? 1 : 0) + << SerializeVideoQuality(_videoQuality) + << qint32(_ivZoom.current()) + << qint32(_systemDarkModeEnabled.current() ? 1 : 0); } Ensures(result.size() == size); @@ -509,6 +532,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2; QByteArray tonsiteStorageToken = _tonsiteStorageToken; qint32 ivZoom = _ivZoom.current(); + qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0; + qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0; + quint32 videoQuality = SerializeVideoQuality(_videoQuality); + quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -590,6 +617,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { stream >> nativeWindowFrame; } if (!stream.atEnd()) { + // Read over this one below, if was in the file. stream >> systemDarkModeEnabled; } if (!stream.atEnd()) { @@ -818,9 +846,24 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> includeMutedCounterFolders; } + if (!stream.atEnd()) { + stream >> chatFiltersHorizontal; + } + if (!stream.atEnd()) { + stream >> skipToastsInFocus; + } + if (!stream.atEnd()) { + stream >> recordVideoMessages; + } + if (!stream.atEnd()) { + stream >> videoQuality; + } if (!stream.atEnd()) { stream >> ivZoom; } + if (!stream.atEnd()) { + stream >> systemDarkModeEnabled; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1034,6 +1077,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) { : (weatherInCelsius == 1); _tonsiteStorageToken = tonsiteStorageToken; _ivZoom = ivZoom; + _skipToastsInFocus = (skipToastsInFocus == 1); + _recordVideoMessages = (recordVideoMessages == 1); + _videoQuality = DeserializeVideoQuality(videoQuality); + _chatFiltersHorizontal = (chatFiltersHorizontal == 1); } QString Settings::getSoundPath(const QString &key) const { @@ -1360,6 +1407,7 @@ void Settings::resetOnLastLogout() { _flashBounceNotify = true; _notifyView = NotifyView::ShowPreview; //_nativeNotifications = std::nullopt; + //_skipToastsInFocus = false; //_notificationsCount = 3; //_notificationsCorner = ScreenCorner::BottomRight; _includeMutedCounter = true; @@ -1417,11 +1465,13 @@ void Settings::resetOnLastLogout() { _thirdColumnWidth = kDefaultThirdColumnWidth; // p-w _notifyFromAll = true; _tabbedReplacedWithInfo = false; // per-window - _systemDarkModeEnabled = false; _hiddenGroupCallTooltips = 0; _storiesClickTooltipHidden = false; _ttlVoiceClickTooltipHidden = false; _ivZoom = 100; + _recordVideoMessages = false; + _videoQuality = {}; + _chatFiltersHorizontal = false; _recentEmojiPreload.clear(); _recentEmoji.clear(); @@ -1479,6 +1529,14 @@ void Settings::setNativeNotifications(bool value) { : std::make_optional(value); } +bool Settings::skipToastsInFocus() const { + return _skipToastsInFocus; +} + +void Settings::setSkipToastsInFocus(bool value) { + _skipToastsInFocus = value; +} + void Settings::setTranslateButtonEnabled(bool value) { _translateButtonEnabled = value; } @@ -1557,6 +1615,7 @@ auto Settings::skipTranslationLanguagesValue() const void Settings::setRememberedDeleteMessageOnlyForYou(bool value) { _rememberedDeleteMessageOnlyForYou = value; } + bool Settings::rememberedDeleteMessageOnlyForYou() const { return _rememberedDeleteMessageOnlyForYou; } @@ -1564,13 +1623,40 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const { int Settings::ivZoom() const { return _ivZoom.current(); } + rpl::producer Settings::ivZoomValue() const { return _ivZoom.value(); } + void Settings::setIvZoom(int value) { +#ifdef Q_OS_WIN + constexpr auto kMin = 25; + constexpr auto kMax = 500; +#else constexpr auto kMin = 30; constexpr auto kMax = 200; +#endif _ivZoom = std::clamp(value, kMin, kMax); } +Media::VideoQuality Settings::videoQuality() const { + return _videoQuality; +} + +void Settings::setVideoQuality(Media::VideoQuality value) { + _videoQuality = value; +} + +bool Settings::chatFiltersHorizontal() const { + return _chatFiltersHorizontal.current(); +} + +rpl::producer Settings::chatFiltersHorizontalChanges() const { + return _chatFiltersHorizontal.changes(); +} + +void Settings::setChatFiltersHorizontal(bool value) { + _chatFiltersHorizontal = value; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 1d5037dcf..e92c679fd 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -226,6 +226,9 @@ public: [[nodiscard]] bool nativeNotifications() const; void setNativeNotifications(bool value); + [[nodiscard]] bool skipToastsInFocus() const; + void setSkipToastsInFocus(bool value); + [[nodiscard]] int notificationsCount() const { return _notificationsCount; } @@ -631,6 +634,13 @@ public: return _floatPlayerCorner; } + [[nodiscard]] bool recordVideoMessages() const { + return _recordVideoMessages; + } + void setRecordVideoMessages(bool value) { + _recordVideoMessages = value; + } + void updateDialogsWidthRatio(float64 ratio, bool nochat); [[nodiscard]] float64 dialogsWidthRatio(bool nochat) const; @@ -925,6 +935,13 @@ public: [[nodiscard]] rpl::producer ivZoomValue() const; void setIvZoom(int value); + [[nodiscard]] bool chatFiltersHorizontal() const; + [[nodiscard]] rpl::producer chatFiltersHorizontalChanges() const; + void setChatFiltersHorizontal(bool value); + + [[nodiscard]] Media::VideoQuality videoQuality() const; + void setVideoQuality(Media::VideoQuality quality); + [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); @@ -964,6 +981,7 @@ private: bool _flashBounceNotify = true; NotifyView _notifyView = NotifyView::ShowPreview; std::optional _nativeNotifications; + bool _skipToastsInFocus = false; int _notificationsCount = 3; ScreenCorner _notificationsCorner = ScreenCorner::BottomRight; bool _includeMutedCounter = true; @@ -1024,7 +1042,7 @@ private: bool _notifyFromAll = true; rpl::variable _nativeWindowFrame = false; rpl::variable> _systemDarkMode = std::nullopt; - rpl::variable _systemDarkModeEnabled = false; + rpl::variable _systemDarkModeEnabled = true; rpl::variable _windowTitleContent; WindowPosition _windowPosition; // per-window bool _disableOpenGL = false; @@ -1061,6 +1079,8 @@ private: std::optional _weatherInCelsius; QByteArray _tonsiteStorageToken; rpl::variable _ivZoom = 100; + Media::VideoQuality _videoQuality; + rpl::variable _chatFiltersHorizontal = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window @@ -1071,6 +1091,8 @@ private: bool _rememberedFlashBounceNotifyFromTray = false; bool _dialogsWidthSetToZeroWithoutChat = false; + bool _recordVideoMessages = false; + QByteArray _photoEditorBrush; }; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 339e5256e..190a7c0f1 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -179,6 +179,34 @@ auto PersonalChannelController::chosen() const return _chosen.events(); } +Window::SessionController *ApplyAccountIndex( + not_null controller, + int accountIndex) { + if (accountIndex <= 0) { + return nullptr; + } + const auto list = Core::App().domain().orderedAccounts(); + if (accountIndex > int(list.size())) { + return nullptr; + } + const auto account = list[accountIndex - 1]; + if (account == &controller->session().account()) { + return controller; + } else if (const auto window = Core::App().windowFor({ account })) { + if (&window->account() != account) { + Core::App().domain().maybeActivate(account); + if (&window->account() != account) { + return nullptr; + } + } + const auto session = window->sessionController(); + if (session) { + return session; + } + } + return nullptr; +} + void SavePersonalChannel( not_null window, ChannelData *channel) { @@ -475,6 +503,20 @@ bool ResolveUsernameOrPhone( const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); + + if (params.contains(u"acc"_q)) { + const auto switched = ApplyAccountIndex( + controller, + params.value(u"acc"_q).toInt()); + if (switched) { + controller = switched; + } else { + controller->showToast(u"Could not activate account %1."_q.arg( + params.value(u"acc"_q))); + return false; + } + } + const auto domainParam = params.value(u"domain"_q); const auto appnameParam = params.value(u"appname"_q); const auto myContext = context.value(); @@ -580,6 +622,7 @@ bool ResolveUsernameOrPhone( .startAutoSubmit = myContext.botStartAutoSubmit, .botAppName = (appname.isEmpty() ? postParam : appname), .botAppForceConfirmation = myContext.mayShowConfirmation, + .botAppFullScreen = (params.value(u"mode"_q) == u"fullscreen"_q), .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) ? params.value(u"startattach"_q) @@ -969,6 +1012,17 @@ bool ShowStarsExamples( return true; } +bool ShowPopularAppsAbout( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + controller->show(Dialogs::PopularAppsAboutBox(controller)); + return true; +} + void ExportTestChatTheme( not_null controller, not_null theme) { @@ -1443,6 +1497,10 @@ const std::vector &InternalUrlHandlers() { u"^stars_examples$"_q, ShowStarsExamples, }, + { + u"^about_popular_apps$"_q, + ShowPopularAppsAbout, + }, }; return Result; } diff --git a/Telegram/SourceFiles/core/phone_click_handler.cpp b/Telegram/SourceFiles/core/phone_click_handler.cpp index 19d15b78a..ec34b8f8b 100644 --- a/Telegram/SourceFiles/core/phone_click_handler.cpp +++ b/Telegram/SourceFiles/core/phone_click_handler.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/phone_click_handler.h" +#include "boxes/add_contact_box.h" #include "core/click_handler_types.h" #include "data/data_session.h" #include "data/data_user.h" @@ -48,6 +49,9 @@ public: void handleKeyPress(not_null e) override; + [[nodiscard]] QString firstName() const; + [[nodiscard]] QString lastName() const; + protected: QPoint prepareRippleStartPosition() const override; QImage prepareRippleMask() const override; @@ -130,6 +134,18 @@ ResolvePhoneAction::ResolvePhoneAction( prepare(); } +QString ResolvePhoneAction::firstName() const { + const auto peer = _peer.current(); + const auto user = peer ? peer->asUser() : nullptr; + return user ? user->firstName : QString(); +} + +QString ResolvePhoneAction::lastName() const { + const auto peer = _peer.current(); + const auto user = peer ? peer->asUser() : nullptr; + return user ? user->lastName : QString(); +} + void ResolvePhoneAction::paint(Painter &p) { const auto selected = isSelected() && _peer.current(); const auto height = contentHeight(); @@ -275,6 +291,7 @@ PhoneClickHandler::PhoneClickHandler( QString text) : _session(session) , _text(text) { + setProperty(kPhoneNumberLinkProperty, _text); } void PhoneClickHandler::onClick(ClickContext context) const { @@ -314,14 +331,29 @@ void PhoneClickHandler::onClick(ClickContext context) const { TextForMimeData::Simple(phone.trimmed())); }, &st::menuIconCopy); + auto resolvePhoneAction = base::make_unique_q( + menu, + menu->st().menu, + phone, + controller); + + if (Trim(phone) != Trim(controller->session().user()->phone())) { + menu->addAction( + tr::lng_info_add_as_contact(tr::now), + [=, raw = Ui::MakeWeak(resolvePhoneAction.get())] { + controller->show( + Box( + &controller->session(), + raw ? raw->firstName() : QString(), + raw ? raw->lastName() : QString(), + Trim(phone))); + }, + &st::menuIconInvite); + } + menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator); - menu->addAction( - base::make_unique_q( - menu, - menu->st().menu, - phone, - controller)); + menu->addAction(std::move(resolvePhoneAction)); menu->popup(pos); } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 71f8e0a58..eb693524e 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5006003; -constexpr auto AppVersionStr = "5.6.3"; +constexpr auto AppVersion = 5008002; +constexpr auto AppVersionStr = "5.8.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 4c2379a95..e213fcf9a 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -74,6 +74,11 @@ uint64 Credits::balance(PeerId peerId) const { return (it != _cachedPeerBalances.end()) ? it->second : 0; } +uint64 Credits::balanceCurrency(PeerId peerId) const { + const auto it = _cachedPeerCurrencyBalances.find(peerId); + return (it != _cachedPeerCurrencyBalances.end()) ? it->second : 0; +} + rpl::producer Credits::balanceValue() const { return _nonLockedBalance.value(); } @@ -128,4 +133,8 @@ void Credits::apply(PeerId peerId, uint64 balance) { _cachedPeerBalances[peerId] = balance; } +void Credits::applyCurrency(PeerId peerId, uint64 balance) { + _cachedPeerCurrencyBalances[peerId] = balance; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index 62fefcc3c..da953b29b 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -35,6 +35,9 @@ public: [[nodiscard]] rpl::producer rateValue( not_null ownedBotOrChannel); + void applyCurrency(PeerId peerId, uint64 balance); + [[nodiscard]] uint64 balanceCurrency(PeerId peerId) const; + void lock(int count); void unlock(int count); void withdrawLocked(int count); @@ -50,6 +53,7 @@ private: std::unique_ptr _loader; base::flat_map _cachedPeerBalances; + base::flat_map _cachedPeerCurrencyBalances; uint64 _balance = 0; uint64 _locked = 0; diff --git a/Telegram/SourceFiles/data/components/recent_peers.cpp b/Telegram/SourceFiles/data/components/recent_peers.cpp index 58fb7dfcc..6b49ea759 100644 --- a/Telegram/SourceFiles/data/components/recent_peers.cpp +++ b/Telegram/SourceFiles/data/components/recent_peers.cpp @@ -112,6 +112,7 @@ void RecentPeers::applyLocal(QByteArray serialized) { ).arg(streamAppVersion)); _list.reserve(count); for (auto i = 0; i != int(count); ++i) { + const auto streamPosition = stream.underlying().device()->pos(); const auto peer = Serialize::readPeer( _session, streamAppVersion, @@ -123,7 +124,8 @@ void RecentPeers::applyLocal(QByteArray serialized) { DEBUG_LOG(("Suggestions: Failed RecentPeers reading %1 / %2." ).arg(i + 1 ).arg(count)); - _list.clear(); + DEBUG_LOG(("Failed bytes: %1.").arg( + QString::fromUtf8(serialized.mid(streamPosition).toHex()))); return; } } diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index 9e69dd35b..460a4df58 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -343,10 +343,20 @@ void ScheduledMessages::apply( if (i == end(_data)) { return; } - for (const auto &id : update.vmessages().v) { + const auto sent = update.vsent_messages(); + const auto &ids = update.vmessages().v; + for (auto k = 0, count = int(ids.size()); k != count; ++k) { + const auto id = ids[k].v; const auto &list = i->second; - const auto j = list.itemById.find(id.v); + const auto j = list.itemById.find(id); if (j != end(list.itemById)) { + if (sent && k < sent->v.size()) { + const auto &sentId = sent->v[k]; + _session->data().sentFromScheduled({ + .item = j->second, + .sentId = sentId.v, + }); + } j->second->destroy(); i = _data.find(history); if (i == end(_data)) { diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index bd1d4ef1c..d6007918a 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/chat/sponsored_message_bar.h" #include "ui/text/text_utilities.h" // Ui::Text::RichLangValue. // AyuGram includes @@ -77,6 +78,9 @@ void SponsoredMessages::clearOldRequests() { SponsoredMessages::AppendResult SponsoredMessages::append( not_null history) { + if (isTopBarFor(history)) { + return SponsoredMessages::AppendResult::None; + } const auto it = _data.find(history); if (it == end(_data)) { return SponsoredMessages::AppendResult::None; @@ -198,9 +202,31 @@ void SponsoredMessages::inject( } bool SponsoredMessages::canHaveFor(not_null history) const { - // AyuGram disableAds auto settings = &AyuSettings::getInstance(); - return !settings->disableAds && history->peer->isChannel(); + if (settings->disableAds) { + return false; + } + + if (history->peer->isChannel()) { + return true; + } else if (const auto user = history->peer->asUser()) { + return user->isBot(); + } + return false; +} + +bool SponsoredMessages::isTopBarFor(not_null history) const { + auto settings = &AyuSettings::getInstance(); + if (settings->disableAds) { + return false; + } + + if (peerIsUser(history->peer->id)) { + if (const auto user = history->peer->asUser()) { + return user->isBot(); + } + } + return false; } void SponsoredMessages::request(not_null history, Fn done) { @@ -224,10 +250,8 @@ void SponsoredMessages::request(not_null history, Fn done) { } } } - const auto channel = history->peer->asChannel(); - Assert(channel != nullptr); request.requestId = _session->api().request( - MTPchannels_GetSponsoredMessages(channel->inputChannel) + MTPmessages_GetSponsoredMessages(history->peer->input) ).done([=](const MTPmessages_sponsoredMessages &result) { parse(history, result); if (done) { @@ -263,12 +287,62 @@ void SponsoredMessages::parse( list.postsBetween = postsBetween->v; list.state = State::InjectToMiddle; } else { - list.state = State::AppendToEnd; + list.state = history->peer->isChannel() + ? State::AppendToEnd + : State::AppendToTopBar; } }, [](const MTPDmessages_sponsoredMessagesEmpty &) { }); } +FullMsgId SponsoredMessages::fillTopBar( + not_null history, + not_null widget) { + const auto it = _data.find(history); + if (it != end(_data)) { + auto &list = it->second; + if (!list.entries.empty()) { + const auto &entry = list.entries.front(); + const auto fullId = entry.itemFullId; + Ui::FillSponsoredMessageBar( + widget, + _session, + fullId, + entry.sponsored.from, + entry.sponsored.textWithEntities); + return fullId; + } + } + return {}; +} + +rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) { + if (IsServerMsgId(fullId.msg) || !fullId) { + return rpl::never<>(); + } + const auto history = _session->data().history(fullId.peer); + const auto it = _data.find(history); + if (it == end(_data)) { + return rpl::never<>(); + } + auto &list = it->second; + const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) { + return e.itemFullId == fullId; + }); + if (entryIt == end(list.entries)) { + return rpl::never<>(); + } + if (!entryIt->optionalDestructionNotifier) { + entryIt->optionalDestructionNotifier + = std::make_unique(); + entryIt->optionalDestructionNotifier->add([this, fullId] { + _itemRemoved.fire_copy(fullId); + }); + } + return _itemRemoved.events( + ) | rpl::filter(rpl::mappers::_1 == fullId) | rpl::to_empty; +} + void SponsoredMessages::append( not_null history, List &list, @@ -289,7 +363,9 @@ void SponsoredMessages::append( }, [&](const MTPDmessageMediaDocument &media) { if (const auto tlDocument = media.vdocument()) { tlDocument->match([&](const MTPDdocument &data) { - const auto d = history->owner().processDocument(data); + const auto d = history->owner().processDocument( + data, + media.valt_documents()); if (d->isVideoFile() || d->isSilentVideo() || d->isAnimation() @@ -412,7 +488,7 @@ void SponsoredMessages::clearItems(not_null history) { const SponsoredMessages::Entry *SponsoredMessages::find( const FullMsgId &fullId) const { - if (!peerIsChannel(fullId.peer)) { + if (!peerIsChannel(fullId.peer) && !peerIsUser(fullId.peer)) { return nullptr; } const auto history = _session->data().history(fullId.peer); @@ -440,11 +516,11 @@ void SponsoredMessages::view(const FullMsgId &fullId) { if (request.requestId || TooEarlyForRequest(request.lastReceived)) { return; } - const auto channel = entryPtr->item->history()->peer->asChannel(); - Assert(channel != nullptr); request.requestId = _session->api().request( - MTPchannels_ViewSponsoredMessage( - channel->inputChannel, + MTPmessages_ViewSponsoredMessage( + entryPtr->item + ? entryPtr->item->history()->peer->input + : _session->data().peer(fullId.peer)->input, MTP_bytes(randomId)) ).done([=] { auto &request = _viewRequests[randomId]; @@ -495,14 +571,14 @@ void SponsoredMessages::clicked( return; } const auto randomId = entryPtr->sponsored.randomId; - const auto channel = entryPtr->item->history()->peer->asChannel(); - Assert(channel != nullptr); - using Flag = MTPchannels_ClickSponsoredMessage::Flag; - _session->api().request(MTPchannels_ClickSponsoredMessage( + using Flag = MTPmessages_ClickSponsoredMessage::Flag; + _session->api().request(MTPmessages_ClickSponsoredMessage( MTP_flags(Flag(0) | (isMedia ? Flag::f_media : Flag(0)) | (isFullscreen ? Flag::f_fullscreen : Flag(0))), - channel->inputChannel, + entryPtr->item + ? entryPtr->item->history()->peer->input + : _session->data().peer(fullId.peer)->input, MTP_bytes(randomId) )).send(); } @@ -531,11 +607,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) return; } - const auto history = entry->item->history(); - const auto channel = history->peer->asChannel(); - if (!channel) { - return; - } + const auto history = _session->data().history(fullId.peer); const auto erase = [=] { const auto it = _data.find(history); @@ -554,8 +626,8 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId) } state->requestId = _session->api().request( - MTPchannels_ReportSponsoredMessage( - channel->inputChannel, + MTPmessages_ReportSponsoredMessage( + history->peer->input, MTP_bytes(entry->sponsored.randomId), MTP_bytes(optionId)) ).done([=]( diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index 028861af9..ebcefb9e2 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -18,6 +18,10 @@ namespace Main { class Session; } // namespace Main +namespace Ui { +class RpWidget; +} // namespace Ui + namespace Data { class MediaPreload; @@ -76,6 +80,7 @@ public: None, AppendToEnd, InjectToMiddle, + AppendToTopBar, }; struct Details { std::vector info; @@ -94,10 +99,15 @@ public: ~SponsoredMessages(); [[nodiscard]] bool canHaveFor(not_null history) const; + [[nodiscard]] bool isTopBarFor(not_null history) const; void request(not_null history, Fn done); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); + [[nodiscard]] FullMsgId fillTopBar( + not_null history, + not_null widget); + [[nodiscard]] rpl::producer<> itemRemoved(const FullMsgId &); [[nodiscard]] AppendResult append(not_null history); void inject( @@ -122,6 +132,7 @@ private: FullMsgId itemFullId; SponsoredMessage sponsored; std::unique_ptr preload; + std::unique_ptr optionalDestructionNotifier; }; struct List { std::vector entries; @@ -156,6 +167,8 @@ private: base::flat_map, Request> _requests; base::flat_map _viewRequests; + rpl::event_stream _itemRemoved; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp index 1d6307220..e068be199 100644 --- a/Telegram/SourceFiles/data/components/top_peers.cpp +++ b/Telegram/SourceFiles/data/components/top_peers.cpp @@ -300,6 +300,7 @@ void TopPeers::applyLocal(QByteArray serialized) { _list.reserve(count); for (auto i = 0; i != int(count); ++i) { auto rating = quint64(); + const auto streamPosition = stream.underlying().device()->pos(); const auto peer = Serialize::readPeer( _session, streamAppVersion, @@ -313,6 +314,8 @@ void TopPeers::applyLocal(QByteArray serialized) { } else { DEBUG_LOG(("Suggestions: " "Failed TopPeers reading %1 / %2.").arg(i + 1).arg(count)); + DEBUG_LOG(("Failed bytes: %1.").arg( + QString::fromUtf8(serialized.mid(streamPosition).toHex()))); _list.clear(); return; } diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 3397cf7ea..fe77aedc5 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -46,12 +46,15 @@ struct CreditsHistoryEntry final { Unsupported, PremiumBot, Ads, + API, }; QString id; QString title; TextWithEntities description; QDateTime date; + QDateTime firstSaleDate; + QDateTime lastSaleDate; PhotoId photoId = 0; std::vector extended; uint64 credits = 0; @@ -59,23 +62,27 @@ struct CreditsHistoryEntry final { uint64 barePeerId = 0; uint64 bareGiveawayMsgId = 0; uint64 bareGiftStickerId = 0; + uint64 bareActorId = 0; PeerType peerType; QDateTime subscriptionUntil; QDateTime successDate; QString successLink; int limitedCount = 0; int limitedLeft = 0; - int convertStars = 0; - bool converted = false; - bool anonymous = false; - bool savedToProfile = false; - bool fromGiftsList = false; - bool reaction = false; - bool refunded = false; - bool pending = false; - bool failed = false; - bool in = false; - bool gift = false; + int starsConverted = 0; + int floodSkip = 0; + bool converted : 1 = false; + bool anonymous : 1 = false; + bool stargift : 1 = false; + bool savedToProfile : 1 = false; + bool fromGiftsList : 1 = false; + bool soldOutInfo : 1 = false; + bool reaction : 1 = false; + bool refunded : 1 = false; + bool pending : 1 = false; + bool failed : 1 = false; + bool in : 1 = false; + bool gift : 1 = false; }; struct CreditsStatusSlice final { diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 5456ef280..c14d29bfc 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -332,6 +332,8 @@ void DocumentData::setattributes( validateLottieSticker(); + auto wasVideoData = isVideoFile() ? std::move(_additional) : nullptr; + _videoPreloadPrefix = 0; for (const auto &attribute : attributes) { attribute.match([&](const MTPDdocumentAttributeImageSize &data) { @@ -388,11 +390,21 @@ void DocumentData::setattributes( : VideoDocument; if (data.is_round_message()) { _additional = std::make_unique(); - } else if (const auto size = data.vpreload_prefix_size()) { - if (size->v > 0 && size->v < kMaxAllowedPreloadPrefix) { - _videoPreloadPrefix = size->v; + } else { + if (const auto size = data.vpreload_prefix_size()) { + if (size->v > 0 + && size->v < kMaxAllowedPreloadPrefix) { + _videoPreloadPrefix = size->v; + } } + _additional = wasVideoData + ? std::move(wasVideoData) + : std::make_unique(); + video()->codec = qs( + data.vvideo_codec().value_or_empty()); } + } else if (type == VideoDocument && wasVideoData) { + _additional = std::move(wasVideoData); } else if (const auto info = sticker()) { info->type = StickerType::Webm; } @@ -511,6 +523,108 @@ void DocumentData::setattributes( } } +void DocumentData::setVideoQualities(const QVector &list) { + auto qualities = std::vector>(); + qualities.reserve(list.size()); + for (const auto &document : list) { + qualities.push_back(owner().processDocument(document)); + } + setVideoQualities(std::move(qualities)); +} + +void DocumentData::setVideoQualities( + std::vector> qualities) { + const auto data = video(); + if (!data) { + return; + } + auto count = int(qualities.size()); + if (qualities.empty()) { + return; + } + const auto good = [&](not_null document) { + return document->isVideoFile() + && !document->dimensions.isEmpty() + && !document->inappPlaybackFailed() + && document->useStreamingLoader() + && document->canBeStreamed(nullptr); + }; + ranges::sort( + qualities, + ranges::greater(), + &DocumentData::resolveVideoQuality); + for (auto i = 0; i != count - 1;) { + const auto my = qualities[i]; + const auto next = qualities[i + 1]; + const auto myQuality = my->resolveVideoQuality(); + const auto nextQuality = next->resolveVideoQuality(); + const auto myGood = good(my); + const auto nextGood = good(next); + if (!myGood || !nextGood || myQuality == nextQuality) { + const auto removeMe = !myGood + || (nextGood && (my->size > next->size)); + const auto from = i + (removeMe ? 1 : 2); + for (auto j = from; j != count; ++j) { + qualities[j - 1] = qualities[j]; + } + --count; + } else { + ++i; + } + } + if (!qualities[count - 1]->resolveVideoQuality()) { + --count; + } + qualities.erase(qualities.begin() + count, qualities.end()); + if (!qualities.empty()) { + if (const auto mine = resolveVideoQuality()) { + if (mine > qualities.front()->resolveVideoQuality()) { + qualities.insert(begin(qualities), this); + } + } + } + data->qualities = std::move(qualities); +} + +int DocumentData::resolveVideoQuality() const { + const auto size = isVideoFile() ? dimensions : QSize(); + return size.isEmpty() ? 0 : std::min(size.width(), size.height()); +} + +auto DocumentData::resolveQualities(HistoryItem *context) const +-> const std::vector> & { + static const auto empty = std::vector>(); + const auto info = video(); + const auto media = context ? context->media() : nullptr; + if (!info || !media || media->document() != this) { + return empty; + } + return media->hasQualitiesList() ? info->qualities : empty; +} + +not_null DocumentData::chooseQuality( + HistoryItem *context, + Media::VideoQuality request) { + const auto &list = resolveQualities(context); + if (list.empty() || !request.height) { + return this; + } + const auto height = int(request.height); + auto closest = this; + auto closestAbs = std::abs(height - resolveVideoQuality()); + auto closestSize = size; + for (const auto &quality : list) { + const auto abs = std::abs(height - quality->resolveVideoQuality()); + if (abs < closestAbs + || (abs == closestAbs && quality->size < closestSize)) { + closest = quality; + closestAbs = abs; + closestSize = quality->size; + } + } + return closest; +} + void DocumentData::validateLottieSticker() { if (type == FileDocument && hasMimeType(u"application/x-tgsticker"_q)) { @@ -631,6 +745,14 @@ bool DocumentData::emojiUsesTextColor() const { return (_flags & Flag::UseTextColor); } +void DocumentData::overrideEmojiUsesTextColor(bool value) { + if (value) { + _flags |= Flag::UseTextColor; + } else { + _flags &= ~Flag::UseTextColor; + } +} + bool DocumentData::hasThumbnail() const { return _thumbnail.location.valid() && !thumbnailFailed() @@ -1384,6 +1506,16 @@ const RoundData *DocumentData::round() const { return const_cast(this)->round(); } +VideoData *DocumentData::video() { + return isVideoFile() + ? static_cast(_additional.get()) + : nullptr; +} + +const VideoData *DocumentData::video() const { + return const_cast(this)->video(); +} + bool DocumentData::hasRemoteLocation() const { return (_dc != 0 && _access != 0); } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 93297550c..29f4bde56 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -31,11 +31,13 @@ struct Key; } // namespace Storage namespace Media { -namespace Streaming { -class Loader; -} // namespace Streaming +struct VideoQuality; } // namespace Media +namespace Media::Streaming { +class Loader; +} // namespace Media::Streaming + namespace Data { class Session; class DocumentMedia; @@ -92,6 +94,11 @@ struct VoiceData : public DocumentAdditionalData { char wavemax = 0; }; +struct VideoData : public DocumentAdditionalData { + QString codec; + std::vector> qualities; +}; + using RoundData = VoiceData; namespace Serialize { @@ -108,8 +115,16 @@ public: void setattributes( const QVector &attributes); + void setVideoQualities(const QVector &list); void automaticLoadSettingsChanged(); + void setVideoQualities(std::vector> qualities); + [[nodiscard]] int resolveVideoQuality() const; + [[nodiscard]] auto resolveQualities(HistoryItem *context) const + -> const std::vector> &; + [[nodiscard]] not_null chooseQuality( + HistoryItem *context, + Media::VideoQuality request); [[nodiscard]] bool loading() const; [[nodiscard]] QString loadingFilePath() const; @@ -161,6 +176,8 @@ public: [[nodiscard]] const VoiceData *voice() const; [[nodiscard]] RoundData *round(); [[nodiscard]] const RoundData *round() const; + [[nodiscard]] VideoData *video(); + [[nodiscard]] const VideoData *video() const; void forceIsStreamedAnimation(); [[nodiscard]] bool isVoiceMessage() const; @@ -189,6 +206,7 @@ public: [[nodiscard]] bool isPremiumSticker() const; [[nodiscard]] bool isPremiumEmoji() const; [[nodiscard]] bool emojiUsesTextColor() const; + void overrideEmojiUsesTextColor(bool value); [[nodiscard]] bool hasThumbnail() const; [[nodiscard]] bool thumbnailLoading() const; diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index 76838edc3..48f2be854 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -87,6 +87,7 @@ struct FileReferenceAccumulator { push(data.vphoto()); }, [&](const MTPDmessageMediaDocument &data) { push(data.vdocument()); + push(data.valt_documents()); }, [&](const MTPDmessageMediaWebPage &data) { push(data.vwebpage()); }, [&](const MTPDmessageMediaGame &data) { diff --git a/Telegram/SourceFiles/data/data_lastseen_status.h b/Telegram/SourceFiles/data/data_lastseen_status.h index a7d0171e2..f5da5fbd8 100644 --- a/Telegram/SourceFiles/data/data_lastseen_status.h +++ b/Telegram/SourceFiles/data/data_lastseen_status.h @@ -112,7 +112,9 @@ private: static constexpr auto kValidAfter = kLifeStartDate + kSpecialValueSkip; [[nodiscard]] bool valid() const { - return !_available || (_value >= kSpecialValueSkip); + constexpr auto kMaxSum = uint32(std::numeric_limits::max()); + return (kMaxSum - _value > uint32(kLifeStartDate)) + && (!_available || (_value >= kSpecialValueSkip)); } LastseenStatus(uint32 value, bool available, bool hiddenByMe) diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 81dc056e9..bdcf6f95b 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -551,6 +551,10 @@ DocumentData *Media::document() const { return nullptr; } +bool Media::hasQualitiesList() const { + return false; +} + PhotoData *Media::photo() const { return nullptr; } @@ -964,12 +968,14 @@ MediaFile::MediaFile( not_null parent, not_null document, bool skipPremiumEffect, + bool hasQualitiesList, bool spoiler, crl::time ttlSeconds) : Media(parent) , _document(document) , _emoji(document->sticker() ? document->sticker()->alt : QString()) , _skipPremiumEffect(skipPremiumEffect) +, _hasQualitiesList(hasQualitiesList) , _spoiler(spoiler) , _ttlSeconds(ttlSeconds) { parent->history()->owner().registerDocumentItem(_document, parent); @@ -999,6 +1005,7 @@ std::unique_ptr MediaFile::clone(not_null parent) { parent, _document, !_document->session().premium(), + _hasQualitiesList, _spoiler, _ttlSeconds); } @@ -1007,6 +1014,10 @@ DocumentData *MediaFile::document() const { return _document; } +bool MediaFile::hasQualitiesList() const { + return _hasQualitiesList; +} + bool MediaFile::uploading() const { return _document->uploading(); } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index e1213e39a..57e684ee9 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -139,7 +139,7 @@ struct GiftCode { TextWithEntities message; ChannelData *channel = nullptr; MsgId giveawayMsgId = 0; - int convertStars = 0; + int starsConverted = 0; int limitedCount = 0; int limitedLeft = 0; int count = 0; @@ -165,6 +165,7 @@ public: virtual std::unique_ptr clone(not_null parent) = 0; virtual DocumentData *document() const; + virtual bool hasQualitiesList() const; virtual PhotoData *photo() const; virtual WebPageData *webpage() const; virtual MediaWebPageFlags webpageFlags() const; @@ -287,6 +288,7 @@ public: not_null parent, not_null document, bool skipPremiumEffect, + bool hasQualitiesList, bool spoiler, crl::time ttlSeconds); ~MediaFile(); @@ -294,6 +296,7 @@ public: std::unique_ptr clone(not_null parent) override; DocumentData *document() const override; + bool hasQualitiesList() const override; bool uploading() const override; Storage::SharedMediaTypesMask sharedMediaTypes() const override; @@ -324,6 +327,7 @@ private: not_null _document; QString _emoji; bool _skipPremiumEffect = false; + bool _hasQualitiesList = false; bool _spoiler = false; // Video (unsupported) / Voice / Round. diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.cpp b/Telegram/SourceFiles/data/data_message_reaction_id.cpp index 1deac2bbb..b4a34a7d3 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.cpp +++ b/Telegram/SourceFiles/data/data_message_reaction_id.cpp @@ -40,6 +40,25 @@ std::vector SearchTagsFromQuery( return result; } +HashtagWithUsername HashtagWithUsernameFromQuery(QStringView query) { + const auto match = TextUtilities::RegExpHashtag(true).match(query); + if (match.hasMatch()) { + const auto username = match.capturedView(2).mid(1).toString(); + const auto offset = int(match.capturedLength(1)); + const auto full = int(query.size()); + const auto length = full + - int(username.size()) + - 1 + - offset + - int(match.capturedLength(3)); + if (!username.isEmpty() && length > 0 && offset + length <= full) { + const auto hashtag = query.mid(offset, length).toString(); + return { hashtag, username }; + } + } + return {}; +} + QString ReactionEntityData(const ReactionId &id) { if (id.empty()) { return {}; diff --git a/Telegram/SourceFiles/data/data_message_reaction_id.h b/Telegram/SourceFiles/data/data_message_reaction_id.h index c624ef865..5462ba65e 100644 --- a/Telegram/SourceFiles/data/data_message_reaction_id.h +++ b/Telegram/SourceFiles/data/data_message_reaction_id.h @@ -65,6 +65,13 @@ struct MessageReaction { [[nodiscard]] std::vector SearchTagsFromQuery( const QString &query); +struct HashtagWithUsername { + QString hashtag; + QString username; +}; +[[nodiscard]] HashtagWithUsername HashtagWithUsernameFromQuery( + QStringView query); + [[nodiscard]] QString ReactionEntityData(const ReactionId &id); [[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 83a4e20b3..e707f4cf7 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -1230,6 +1230,9 @@ not_null Reactions::resolveListener() { } void Reactions::customEmojiResolveDone(not_null document) { + if (!document->sticker()) { + return; + } const auto id = ReactionId{ { document->id } }; const auto favorite = (_unresolvedFavoriteId == id); const auto i = _unresolvedTop.find(id); diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index b3ecb27b0..b2faef165 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -156,6 +156,18 @@ struct FullMsgId { MsgId msg = 0; }; +#ifdef _DEBUG +inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) { + debug.nospace() + << "FullMsgId(peer: " + << fullMsgId.peer.value + << ", msg: " + << fullMsgId.msg.bare + << ")"; + return debug; +} +#endif // _DEBUG + Q_DECLARE_METATYPE(FullMsgId); struct FullReplyTo { diff --git a/Telegram/SourceFiles/data/data_peer_bot_commands.cpp b/Telegram/SourceFiles/data/data_peer_bot_commands.cpp index 8adc31a9e..2487dc768 100644 --- a/Telegram/SourceFiles/data/data_peer_bot_commands.cpp +++ b/Telegram/SourceFiles/data/data_peer_bot_commands.cpp @@ -17,11 +17,16 @@ ChatBotCommands::Changed ChatBotCommands::update( clear(); } else { for (const auto &commands : list) { - auto &value = operator[](commands.userId); - changed |= commands.commands.empty() - ? remove(commands.userId) - : !ranges::equal(value, commands.commands); - value = commands.commands; + if (commands.commands.empty()) { + changed |= remove(commands.userId); + } else { + auto &value = operator[](commands.userId); + const auto isEqual = ranges::equal(value, commands.commands); + changed |= !isEqual; + if (!isEqual) { + value = commands.commands; + } + } } } return changed; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index ec6ecadf4..95ced4eb8 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -3214,17 +3214,24 @@ not_null Session::document(DocumentId id) { return i->second.get(); } -not_null Session::processDocument(const MTPDocument &data) { +not_null Session::processDocument( + const MTPDocument &data, + const MTPVector *qualities) { return data.match([&](const MTPDdocument &data) { - return processDocument(data); + return processDocument(data, qualities); }, [&](const MTPDdocumentEmpty &data) { return document(data.vid().v); }); } -not_null Session::processDocument(const MTPDdocument &data) { +not_null Session::processDocument( + const MTPDdocument &data, + const MTPVector *qualities) { const auto result = document(data.vid().v); documentApplyFields(result, data); + if (qualities) { + result->setVideoQualities(qualities->v); + } return result; } @@ -4884,6 +4891,22 @@ void Session::viewTagsChanged( } } +void Session::sentToScheduled(SentToScheduled value) { + _sentToScheduled.fire(std::move(value)); +} + +rpl::producer Session::sentToScheduled() const { + return _sentToScheduled.events(); +} + +void Session::sentFromScheduled(SentFromScheduled value) { + _sentFromScheduled.fire(std::move(value)); +} + +rpl::producer Session::sentFromScheduled() const { + return _sentFromScheduled.events(); +} + void Session::clearLocalStorage() { _cache->close(); _cache->clear(); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index a87e7e604..b9d048062 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -89,6 +89,15 @@ struct GiftUpdate { Action action = {}; }; +struct SentToScheduled { + not_null history; + MsgId scheduledId = 0; +}; +struct SentFromScheduled { + not_null item; + MsgId sentId = 0; +}; + class Session final { public: using ViewElement = HistoryView::Element; @@ -558,8 +567,12 @@ public: const ImageLocation &thumbnailLocation); [[nodiscard]] not_null document(DocumentId id); - not_null processDocument(const MTPDocument &data); - not_null processDocument(const MTPDdocument &data); + not_null processDocument( + const MTPDocument &data, + const MTPVector *qualities = nullptr); + not_null processDocument( + const MTPDdocument &data, + const MTPVector *qualities = nullptr); not_null processDocument( const MTPdocument &data, const ImageWithLocation &thumbnail); @@ -787,6 +800,11 @@ public: std::vector &&was, std::vector &&now); + void sentToScheduled(SentToScheduled value); + [[nodiscard]] rpl::producer sentToScheduled() const; + void sentFromScheduled(SentFromScheduled value); + [[nodiscard]] rpl::producer sentFromScheduled() const; + void clearLocalStorage(); private: @@ -959,6 +977,8 @@ private: rpl::event_stream _chatListEntryRefreshes; rpl::event_stream<> _unreadBadgeChanges; rpl::event_stream _repliesReadTillUpdates; + rpl::event_stream _sentToScheduled; + rpl::event_stream _sentFromScheduled; Dialogs::MainList _chatsList; Dialogs::IndexedList _contactsList; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 8e65e95b2..7dff948f5 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -66,7 +66,9 @@ using UpdateFlag = StoryUpdate::Flag; }, [&](const MTPDmessageMediaDocument &data) -> std::optional { if (const auto document = data.vdocument()) { - const auto result = owner->processDocument(*document); + const auto result = owner->processDocument( + *document, + data.valt_documents()); if (!result->isNull() && (result->isGifv() || result->isVideoFile())) { result->setStoryMedia(true); diff --git a/Telegram/SourceFiles/data/data_streaming.cpp b/Telegram/SourceFiles/data/data_streaming.cpp index dd3b13b38..658c83807 100644 --- a/Telegram/SourceFiles/data/data_streaming.cpp +++ b/Telegram/SourceFiles/data/data_streaming.cpp @@ -41,6 +41,43 @@ bool PruneDestroyedAndSet( return result; } +[[nodiscard]] auto LookupOtherQualities( + DocumentData *original, + not_null quality, + HistoryItem *context) +-> std::vector { + if (!original || !context) { + return {}; + } + auto qualities = original->resolveQualities(context); + if (qualities.empty()) { + return {}; + } + auto result = std::vector(); + result.reserve(qualities.size()); + for (const auto &video : qualities) { + if (video != quality) { + if (const auto height = video->resolveVideoQuality()) { + result.push_back({ + .sizeInBytes = uint32(video->size), + .height = uint32(height), + }); + } + } + } + return result; +} + +[[nodiscard]] auto LookupOtherQualities( + DocumentData *original, + not_null quality, + HistoryItem *context) +-> std::vector { + Expects(!original); + + return {}; +} + } // namespace Streaming::Streaming(not_null owner) @@ -50,7 +87,6 @@ Streaming::Streaming(not_null owner) Streaming::~Streaming() = default; - template [[nodiscard]] std::shared_ptr Streaming::sharedReader( base::flat_map, std::weak_ptr> &readers, @@ -84,10 +120,16 @@ template base::flat_map, std::weak_ptr> &documents, base::flat_map, std::weak_ptr> &readers, not_null data, + DocumentData *original, + HistoryItem *context, FileOrigin origin) { + auto otherQualities = LookupOtherQualities(original, data, context); const auto i = documents.find(data); if (i != end(documents)) { if (auto result = i->second.lock()) { + if (!otherQualities.empty()) { + result->setOtherQualities(std::move(otherQualities)); + } return result; } } @@ -95,7 +137,10 @@ template if (!reader) { return nullptr; } - auto result = std::make_shared(data, std::move(reader)); + auto result = std::make_shared( + data, + std::move(reader), + std::move(otherQualities)); if (!PruneDestroyedAndSet(documents, data, result)) { documents.emplace_or_assign(data, result); } @@ -136,7 +181,27 @@ std::shared_ptr Streaming::sharedReader( std::shared_ptr Streaming::sharedDocument( not_null document, FileOrigin origin) { - return sharedDocument(_fileDocuments, _fileReaders, document, origin); + return sharedDocument( + _fileDocuments, + _fileReaders, + document, + nullptr, + nullptr, + origin); +} + +std::shared_ptr Streaming::sharedDocument( + not_null quality, + not_null original, + HistoryItem *context, + FileOrigin origin) { + return sharedDocument( + _fileDocuments, + _fileReaders, + quality, + original, + context, + origin); } std::shared_ptr Streaming::sharedReader( @@ -149,7 +214,13 @@ std::shared_ptr Streaming::sharedReader( std::shared_ptr Streaming::sharedDocument( not_null photo, FileOrigin origin) { - return sharedDocument(_photoDocuments, _photoReaders, photo, origin); + return sharedDocument( + _photoDocuments, + _photoReaders, + photo, + nullptr, + nullptr, + origin); } void Streaming::keepAlive(not_null document) { diff --git a/Telegram/SourceFiles/data/data_streaming.h b/Telegram/SourceFiles/data/data_streaming.h index 14e863f4a..51a6f1854 100644 --- a/Telegram/SourceFiles/data/data_streaming.h +++ b/Telegram/SourceFiles/data/data_streaming.h @@ -12,12 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PhotoData; class DocumentData; -namespace Media { -namespace Streaming { +namespace Media::Streaming { class Reader; class Document; -} // namespace Streaming -} // namespace Media +} // namespace Media::Streaming namespace Data { @@ -41,6 +39,11 @@ public: [[nodiscard]] std::shared_ptr sharedDocument( not_null document, FileOrigin origin); + [[nodiscard]] std::shared_ptr sharedDocument( + not_null quality, + not_null original, + HistoryItem *context, + FileOrigin origin); [[nodiscard]] std::shared_ptr sharedReader( not_null photo, @@ -68,6 +71,8 @@ private: base::flat_map, std::weak_ptr> &documents, base::flat_map, std::weak_ptr> &readers, not_null data, + DocumentData *original, + HistoryItem *context, FileOrigin origin); template diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index fbcdf3e3f..df62562ab 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -327,6 +327,8 @@ enum class MessageFlag : uint64 { SensitiveContent = (1ULL << 47), HasRestrictions = (1ULL << 48), + + EstimatedDate = (1ULL << 49), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_unread_value.cpp b/Telegram/SourceFiles/data/data_unread_value.cpp new file mode 100644 index 000000000..c374dc0f6 --- /dev/null +++ b/Telegram/SourceFiles/data/data_unread_value.cpp @@ -0,0 +1,68 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_unread_value.h" + +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_chat_filters.h" +#include "data/data_folder.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "window/notifications_manager.h" + +namespace Data { +namespace { + +rpl::producer MainListUnreadState( + not_null list) { + return rpl::single(rpl::empty) | rpl::then( + list->unreadStateChanges() | rpl::to_empty + ) | rpl::map([=] { + return list->unreadState(); + }); +} + +} // namespace + +[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState( + not_null session, + const Dialogs::UnreadState &state) { + const auto folderId = Data::Folder::kId; + if (const auto folder = session->data().folderLoaded(folderId)) { + return state - folder->chatsList()->unreadState(); + } + return state; +} + +rpl::producer UnreadStateValue( + not_null session, + FilterId filterId) { + if (filterId > 0) { + const auto filters = &session->data().chatsFilters(); + return MainListUnreadState(filters->chatsList(filterId)); + } + return MainListUnreadState( + session->data().chatsList() + ) | rpl::map([=](const Dialogs::UnreadState &state) { + return MainListMapUnreadState(session, state); + }); +} + +rpl::producer IncludeMutedCounterFoldersValue() { + using namespace Window::Notifications; + return rpl::single(rpl::empty_value()) | rpl::then( + Core::App().notifications().settingsChanged( + ) | rpl::filter( + rpl::mappers::_1 == ChangeType::IncludeMuted + ) | rpl::to_empty + ) | rpl::map([] { + return Core::App().settings().includeMutedCounterFolders(); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_unread_value.h b/Telegram/SourceFiles/data/data_unread_value.h new file mode 100644 index 000000000..1acdbcc98 --- /dev/null +++ b/Telegram/SourceFiles/data/data_unread_value.h @@ -0,0 +1,30 @@ +/* +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 Dialogs { +struct UnreadState; +} // namespace Dialogs + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +[[nodiscard]] Dialogs::UnreadState MainListMapUnreadState( + not_null session, + const Dialogs::UnreadState &state); + +[[nodiscard]] rpl::producer UnreadStateValue( + not_null session, + FilterId filterId); + +[[nodiscard]] rpl::producer IncludeMutedCounterFoldersValue(); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index f304f4f65..57068b8df 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_user.h" +#include "api/api_credits.h" #include "api/api_sensitive_content.h" +#include "api/api_statistics.h" #include "storage/localstorage.h" #include "storage/storage_user_photos.h" #include "main/main_session.h" #include "data/business/data_business_common.h" #include "data/business/data_business_info.h" +#include "data/components/credits.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -347,6 +350,24 @@ void UserData::setBotInfo(const MTPBotInfo &info) { const auto privacyChanged = (botInfo->privacyPolicyUrl != privacy); botInfo->privacyPolicyUrl = privacy; + if (const auto settings = d.vapp_settings()) { + const auto &data = settings->data(); + botInfo->botAppColorTitleDay = Ui::MaybeColorFromSerialized( + data.vheader_color()).value_or(QColor(0, 0, 0, 0)); + botInfo->botAppColorTitleNight = Ui::MaybeColorFromSerialized( + data.vheader_dark_color()).value_or(QColor(0, 0, 0, 0)); + botInfo->botAppColorBodyDay = Ui::MaybeColorFromSerialized( + data.vbackground_color()).value_or(QColor(0, 0, 0, 0)); + botInfo->botAppColorBodyNight = Ui::MaybeColorFromSerialized( + data.vbackground_dark_color()).value_or(QColor(0, 0, 0, 0)); + } else { + botInfo->botAppColorTitleDay + = botInfo->botAppColorTitleNight + = botInfo->botAppColorBodyDay + = botInfo->botAppColorBodyNight + = QColor(0, 0, 0, 0); + } + if (changedCommands || changedButton || privacyChanged) { owner().botCommandsChanged(this); } @@ -591,6 +612,9 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { } else { user->setBotInfoVersion(-1); } + if (const auto info = user->botInfo.get()) { + info->canManageEmojiStatus = update.is_bot_can_manage_emoji_status(); + } if (const auto pinned = update.vpinned_msg_id()) { SetTopPinnedMessageId(user, pinned->v); } @@ -649,6 +673,35 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user, Data::PeerUpdate::Flag::Rights); } + if (info->canEditInformation) { + const auto id = user->id; + const auto weak = base::make_weak(&user->session()); + const auto creditsLoadLifetime + = std::make_shared(); + const auto creditsLoad + = creditsLoadLifetime->make_state(user); + creditsLoad->request({}, [=](Data::CreditsStatusSlice slice) { + if (const auto strong = weak.get()) { + strong->credits().apply(id, slice.balance); + creditsLoadLifetime->destroy(); + } + }); + const auto currencyLoadLifetime + = std::make_shared(); + const auto currencyLoad + = currencyLoadLifetime->make_state(user); + currencyLoad->request( + ) | rpl::start_with_error_done([=](const QString &error) { + currencyLoadLifetime->destroy(); + }, [=] { + if (const auto strong = weak.get()) { + strong->credits().applyCurrency( + id, + currencyLoad->data().currentBalance); + currencyLoadLifetime->destroy(); + } + }, *currencyLoadLifetime); + } } if (const auto paper = update.vwallpaper()) { diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index f016a9d4d..63f03549a 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -33,6 +33,11 @@ struct BotInfo { QString botMenuButtonUrl; QString privacyPolicyUrl; + QColor botAppColorTitleDay = QColor(0, 0, 0, 0); + QColor botAppColorTitleNight = QColor(0, 0, 0, 0); + QColor botAppColorBodyDay = QColor(0, 0, 0, 0); + QColor botAppColorBodyNight = QColor(0, 0, 0, 0); + QString startToken; Dialogs::EntryState inlineReturnTo; @@ -47,6 +52,7 @@ struct BotInfo { bool cantJoinGroups : 1 = false; bool supportsAttachMenu : 1 = false; bool canEditInformation : 1 = false; + bool canManageEmojiStatus : 1 = false; bool supportsBusiness : 1 = false; bool hasMainApp : 1 = false; }; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index bb90a581d..2ed3baef8 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -652,22 +652,27 @@ void CustomEmojiManager::unregisterListener(not_null listener) { } } -rpl::producer> CustomEmojiManager::resolve( - DocumentId documentId) { +auto CustomEmojiManager::resolve(DocumentId documentId) +-> rpl::producer, rpl::empty_error> { return [=](auto consumer) { auto result = rpl::lifetime(); - const auto put = [=](not_null document) { + const auto put = [=]( + not_null document, + bool resolved = true) { if (!document->sticker()) { + if (resolved) { + consumer.put_error({}); + } return false; } consumer.put_next_copy(document); return true; }; - if (!put(owner().document(documentId))) { - const auto listener = new CallbackListener(put); + if (!put(owner().document(documentId), false)) { + const auto listener = result.make_state(put); + resolve(documentId, listener); result.add([=] { unregisterListener(listener); - delete listener; }); } return result; @@ -763,6 +768,9 @@ void CustomEmojiManager::request() { requestFinished(); }).fail([=] { LOG(("API Error: Failed to get documents for emoji.")); + for (const auto &id : ids) { + processListeners(_owner->document(id.v)); + } requestFinished(); }).send(); } @@ -792,7 +800,8 @@ void CustomEmojiManager::processLoaders(not_null document) { } } -void CustomEmojiManager::processListeners(not_null document) { +void CustomEmojiManager::processListeners( + not_null document) { const auto id = document->id; if (const auto listeners = _resolvers.take(id)) { for (const auto &listener : *listeners) { diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index d51a52190..1d2f7940b 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -66,8 +66,8 @@ public: void resolve(DocumentId documentId, not_null listener); void unregisterListener(not_null listener); - [[nodiscard]] rpl::producer> resolve( - DocumentId documentId); + [[nodiscard]] auto resolve(DocumentId documentId) + -> rpl::producer, rpl::empty_error>; [[nodiscard]] std::unique_ptr createLoader( not_null document, diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 645c691d2..aa62c3238 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -206,22 +206,20 @@ void Stickers::incrementSticker(not_null document) { auto &sets = setsRef(); auto it = sets.find(Data::Stickers::CloudRecentSetId); if (it == sets.cend()) { - if (it == sets.cend()) { - it = sets.emplace( + it = sets.emplace( + Data::Stickers::CloudRecentSetId, + std::make_unique( + &session().data(), Data::Stickers::CloudRecentSetId, - std::make_unique( - &session().data(), - Data::Stickers::CloudRecentSetId, - uint64(0), // accessHash - uint64(0), // hash - tr::lng_recent_stickers(tr::now), - QString(), - 0, // count - SetFlag::Special, - TimeId(0))).first; - } else { - it->second->title = tr::lng_recent_stickers(tr::now); - } + uint64(0), // accessHash + uint64(0), // hash + tr::lng_recent_stickers(tr::now), + QString(), + 0, // count + SetFlag::Special, + TimeId(0))).first; + } else { + it->second->title = tr::lng_recent_stickers(tr::now); } const auto set = it->second.get(); auto removedFromEmoji = std::vector>(); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 0c72d7090..818b7ef10 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -222,9 +222,7 @@ dialogsMenuToggle: IconButton { rippleAreaPosition: point(0px, 0px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } dialogsMenuToggleUnread: icon { { "dialogs/dialogs_menu_unread", dialogsMenuIconFg }, @@ -694,6 +692,10 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) { } dialogsSearchTabsPadding: 8px; +chatsFiltersTabs: SettingsSlider(dialogsSearchTabs) { + rippleBottomSkip: 0px; +} + dialogsStoriesList: DialogsStoriesList { small: dialogsStories; full: dialogsStoriesFull; @@ -745,3 +747,7 @@ dialogsSearchTagPromoLeft: 6px; dialogsSearchTagPromoRight: 1px; dialogsSearchTagPromoSkip: 6px; +dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px); +dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) { + minWidth: 128px; +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index ab6577613..f587dab2b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -11,13 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/chat_search_empty.h" #include "dialogs/ui/chat_search_in.h" #include "dialogs/ui/dialogs_layout.h" -#include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_search_tags.h" -#include "history/view/history_view_chat_preview.h" #include "history/view/history_view_context_menu.h" #include "history/history.h" #include "history/history_item.h" @@ -44,7 +42,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" -#include "data/data_cloud_file.h" #include "data/data_changes.h" #include "data/data_message_reactions.h" #include "data/data_saved_messages.h" @@ -68,7 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/loading_element.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" -#include "ui/empty_userpic.h" #include "ui/unread_badge.h" #include "boxes/filters/edit_filter_box.h" #include "boxes/peers/edit_forum_topic_box.h" @@ -88,6 +84,7 @@ namespace { constexpr auto kHashtagResultsLimit = 5; constexpr auto kStartReorderThreshold = 30; constexpr auto kQueryPreviewLimit = 32; +constexpr auto kPreviewPostsLimit = 3; [[nodiscard]] int FixedOnTopDialogsCount(not_null list) { auto result = 0; @@ -555,7 +552,7 @@ int InnerWidget::searchInChatSkip() const { return _searchIn ? _searchIn->height() : 0; } -int InnerWidget::searchedOffset() const { +int InnerWidget::previewOffset() const { auto result = peerSearchOffset(); if (!_peerSearchResults.empty()) { result += (_peerSearchResults.size() * st::dialogsRowHeight) @@ -564,6 +561,15 @@ int InnerWidget::searchedOffset() const { return result; } +int InnerWidget::searchedOffset() const { + auto result = previewOffset(); + if (!_previewResults.empty()) { + result += (_previewResults.size() * st::dialogsRowHeight) + + st::searchedBarHeight; + } + return result; +} + void InnerWidget::changeOpenedFolder(Data::Folder *folder) { Expects(!folder || !_savedSublists); @@ -810,7 +816,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } if (_searchIn) { p.translate(0, searchInChatSkip()); - if (_searchResults.empty()) { + if (_previewResults.empty() && _searchResults.empty()) { p.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg); } } @@ -924,7 +930,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } const auto showUnreadInSearchResults = uniqueSearchResults(); - if (_searchResults.empty()) { + if (_previewResults.empty() && _searchResults.empty()) { if (_loadingAnimation) { const auto text = tr::lng_contacts_loading(tr::now); p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg); @@ -933,7 +939,68 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); p.translate(0, st::searchedBarHeight); } - } else { + return; + } + if (!_previewResults.empty()) { + const auto text = tr::lng_search_tab_public_posts(tr::now); + p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg); + p.setFont(st::searchedBarFont); + p.setPen(st::searchedBarFg); + p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text); + const auto moreFont = (_selectedMorePosts || _pressedMorePosts) + ? st::searchedBarFont->underline() + : st::searchedBarFont; + { + const auto text = tr::lng_channels_your_more(tr::now); + if (!_morePostsWidth) { + _morePostsWidth = moreFont->width(text); + } + p.setFont(moreFont); + p.drawTextLeft( + width() - st::searchedBarPosition.x() - _morePostsWidth, + st::searchedBarPosition.y(), + width(), + text); + p.translate(0, st::searchedBarHeight); + } + auto skip = previewOffset(); + auto from = floorclamp(r.y() - skip, _st->height, 0, _previewResults.size()); + auto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _previewResults.size()); + p.translate(0, from * _st->height); + if (from < _previewResults.size()) { + for (; from < to; ++from) { + const auto &result = _previewResults[from]; + const auto active = isSearchResultActive(result.get(), activeEntry); + const auto selected = _menuRow.key + ? isSearchResultActive(result.get(), _menuRow) + : _chatPreviewRow.key + ? isSearchResultActive(result.get(), _chatPreviewRow) + : (from == (isPressed() + ? _previewPressed + : _previewSelected)); + Ui::RowPainter::Paint(p, result.get(), { + .st = _st, + .folder = _openedFolder, + .forum = _openedForum, + .currentBg = currentBg(), + .filter = _filterId, + .now = ms, + .width = fullWidth, + .active = active, + .selected = selected, + .paused = videoPaused, + .search = true, + .narrow = (fullWidth < st::columnMinimalWidthLeft / 2), + .displayUnreadInfo = showUnreadInSearchResults, + }); + p.translate(0, _st->height); + } + } + if (to < _previewResults.size()) { + p.translate(0, (_previewResults.size() - to) * _st->height); + } + } + if (!_searchResults.empty()) { const auto text = showUnreadInSearchResults ? u"Search results"_q : tr::lng_search_found_results( @@ -1330,6 +1397,8 @@ void InnerWidget::clearIrrelevantState() { setFilteredPressed(-1, false); _peerSearchSelected = -1; setPeerSearchPressed(-1); + _previewSelected = -1; + setPreviewPressed(-1); _searchedSelected = -1; setSearchedPressed(-1); } else if (_state == WidgetState::Filtered) { @@ -1446,6 +1515,32 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { updateSelectedRow(); } } + if (!_previewResults.empty()) { + auto skip = previewOffset(); + auto previewSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1; + if (previewSelected < 0 || previewSelected >= _previewResults.size()) { + previewSelected = -1; + } + if (_previewSelected != previewSelected) { + updateSelectedRow(); + _previewSelected = previewSelected; + updateSelectedRow(); + } + auto selectedMorePosts = false; + const auto from = skip - st::searchedBarHeight; + if (mouseY <= skip && mouseY >= from) { + const auto left = width() + - _morePostsWidth + - 2 * st::searchedBarPosition.x(); + if (_morePostsWidth > 0 && local.x() >= left) { + selectedMorePosts = true; + } + } + if (_selectedMorePosts != selectedMorePosts) { + update(0, from, width(), st::searchedBarHeight); + _selectedMorePosts = selectedMorePosts; + } + } if (!_searchResults.empty()) { auto skip = searchedOffset(); auto searchedSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1; @@ -1495,7 +1590,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { _hashtagDeletePressed = _hashtagDeleteSelected; setFilteredPressed(_filteredSelected, _selectedTopicJump); setPeerSearchPressed(_peerSearchSelected); + setPreviewPressed(_previewSelected); setSearchedPressed(_searchedSelected); + _pressedMorePosts = _selectedMorePosts; const auto alt = (e->modifiers() & Qt::AltModifier); if (alt && showChatPreview()) { @@ -1862,8 +1959,12 @@ void InnerWidget::mousePressReleased( setFilteredPressed(-1, false); auto peerSearchPressed = _peerSearchPressed; setPeerSearchPressed(-1); + auto previewPressed = _previewPressed; + setPreviewPressed(-1); auto searchedPressed = _searchedPressed; setSearchedPressed(-1); + const auto pressedMorePosts = _pressedMorePosts; + _pressedMorePosts = false; if (wasDragging) { selectByMouse(globalPosition); } @@ -1879,8 +1980,12 @@ void InnerWidget::mousePressReleased( || (filteredPressed >= 0 && filteredPressed == _filteredSelected) || (peerSearchPressed >= 0 && peerSearchPressed == _peerSearchSelected) + || (previewPressed >= 0 + && previewPressed == _previewSelected) || (searchedPressed >= 0 - && searchedPressed == _searchedSelected)) { + && searchedPressed == _searchedSelected) + || (pressedMorePosts + && pressedMorePosts == _selectedMorePosts)) { chooseRow(modifiers, pressedTopicRootId); } } @@ -1955,6 +2060,13 @@ void InnerWidget::setPeerSearchPressed(int pressed) { _peerSearchPressed = pressed; } +void InnerWidget::setPreviewPressed(int pressed) { + if (base::in_range(_previewPressed, 0, _previewResults.size())) { + _previewResults[_previewPressed]->stopLastRipple(); + } + _previewPressed = pressed; +} + void InnerWidget::setSearchedPressed(int pressed) { if (base::in_range(_searchedPressed, 0, _searchResults.size())) { _searchResults[_searchedPressed]->stopLastRipple(); @@ -2313,6 +2425,8 @@ void InnerWidget::updateSelectedRow(Key key) { } } else if (_peerSearchSelected >= 0) { update(0, peerSearchOffset() + _peerSearchSelected * st::dialogsRowHeight, width(), st::dialogsRowHeight); + } else if (_previewSelected >= 0) { + update(0, previewOffset() + _previewSelected * _st->height, width(), _st->height); } else if (_searchedSelected >= 0) { update(0, searchedOffset() + _searchedSelected * _st->height, width(), _st->height); } @@ -2354,9 +2468,11 @@ void InnerWidget::clearSelection() { if (isSelected()) { updateSelectedRow(); _collapsedSelected = -1; + _selectedMorePosts = false; _selected = nullptr; _filteredSelected = _searchedSelected + = _previewSelected = _peerSearchSelected = _hashtagSelected = -1; @@ -2449,6 +2565,11 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) { } else if (_state == WidgetState::Filtered) { if (base::in_range(_filteredSelected, 0, _filterResults.size())) { return { _filterResults[_filteredSelected].key(), FullMsgId() }; + } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { + return { + _previewResults[_previewSelected]->item()->history(), + _previewResults[_previewSelected]->item()->fullId() + }; } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { return { _searchResults[_searchedSelected]->item()->history(), @@ -2597,6 +2718,7 @@ void InnerWidget::searchRequested(bool loading) { _searchLoading = loading; if (loading) { clearSearchResults(true); + clearPreviewResults(); } refresh(true); } @@ -2660,6 +2782,7 @@ void InnerWidget::applySearchState(SearchState state) { } _searchState = std::move(state); _searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query); + _searchWithPostsPreview = computeSearchWithPostsPreview(); updateSearchIn(); moveSearchIn(); @@ -2767,7 +2890,7 @@ void InnerWidget::appendToFiltered(Key key) { const auto height = filteredHeight(); _filterResults.emplace_back(i->second.get()); _filterResults.back().top = height; - trackSearchResultsHistory(key.owningHistory()); + trackResultsHistory(key.owningHistory()); } InnerWidget::~InnerWidget() { @@ -2780,13 +2903,16 @@ void InnerWidget::clearSearchResults(bool clearPeerSearchResults) { _peerSearchResults.clear(); } _searchResults.clear(); - _searchResultsLifetime.destroy(); - _searchResultsHistories.clear(); _searchedCount = _searchedMigratedCount = 0; } -void InnerWidget::trackSearchResultsHistory(not_null history) { - if (!_searchResultsHistories.emplace(history).second) { +void InnerWidget::clearPreviewResults() { + _previewResults.clear(); + _previewCount = 0; +} + +void InnerWidget::trackResultsHistory(not_null history) { + if (!_trackedHistories.emplace(history).second) { return; } const auto channel = history->peer->asChannel(); @@ -2827,7 +2953,7 @@ void InnerWidget::trackSearchResultsHistory(not_null history) { clearMouseSelection(true); } update(); - }, _searchResultsLifetime); + }, _trackedLifetime); if (const auto forum = channel->forum()) { forum->topicDestroyed( @@ -2857,7 +2983,7 @@ void InnerWidget::trackSearchResultsHistory(not_null history) { if (_chatPreviewRow.key.topic() == topic) { _chatPreviewRow = {}; } - }, _searchResultsLifetime); + }, _trackedLifetime); } } @@ -2875,6 +3001,13 @@ Data::Thread *InnerWidget::updateFromParentDrag(QPoint globalPosition) { } else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) { return session().data().history( _peerSearchResults[_peerSearchSelected]->peer); + } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { + if (const auto item = _previewResults[_previewSelected]->item()) { + if (const auto topic = item->topic()) { + return topic; + } + return item->history(); + } } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { if (const auto item = _searchResults[_searchedSelected]->item()) { if (const auto topic = item->topic()) { @@ -3028,12 +3161,14 @@ void InnerWidget::searchReceived( _searchLoading = false; const auto uniquePeers = uniqueSearchResults(); - if (type == SearchRequestType::FromStart - || type == SearchRequestType::PeerFromStart) { + const auto withPreview = _searchWithPostsPreview; + const auto toPreview = withPreview && type.posts; + if (type.start && !type.migrated && (!withPreview || !type.posts)) { clearSearchResults(false); } - const auto isMigratedSearch = (type == SearchRequestType::MigratedFromStart) - || (type == SearchRequestType::MigratedFromOffset); + if (!withPreview || toPreview) { + clearPreviewResults(); + } const auto key = (!_openedForum || _searchState.inChat.topic()) ? _searchState.inChat @@ -3042,34 +3177,40 @@ void InnerWidget::searchReceived( && (!_searchState.inChat || inject->history() == _searchState.inChat.history())) { Assert(_searchResults.empty()); + Assert(!toPreview); const auto index = int(_searchResults.size()); _searchResults.push_back( std::make_unique( key, inject, [=] { repaintSearchResult(index); })); - trackSearchResultsHistory(inject->history()); + trackResultsHistory(inject->history()); ++fullCount; } + auto &results = toPreview ? _previewResults : _searchResults; for (const auto &item : messages) { const auto history = item->history(); - if (!uniquePeers || !hasHistoryInResults(history)) { - const auto index = int(_searchResults.size()); - _searchResults.push_back( - std::make_unique( - key, - item, - [=] { repaintSearchResult(index); })); - trackSearchResultsHistory(history); - if (uniquePeers && !history->unreadCountKnown()) { + if (toPreview || !uniquePeers || !hasHistoryInResults(history)) { + const auto index = int(results.size()); + const auto repaint = toPreview + ? Fn([=] { repaintSearchResult(index); }) + : [=] { repaintPreviewResult(index); }; + results.push_back( + std::make_unique(key, item, repaint)); + trackResultsHistory(history); + if (!toPreview && uniquePeers && !history->unreadCountKnown()) { history->owner().histories().requestDialogEntry(history); + } else if (toPreview && results.size() >= kPreviewPostsLimit) { + break; } } } - if (isMigratedSearch) { + if (type.migrated) { _searchedMigratedCount = fullCount; - } else { + } else if (!withPreview || !toPreview) { _searchedCount = fullCount; + } else { + _previewCount = fullCount; } refresh(); @@ -3313,6 +3454,7 @@ void InnerWidget::clearMouseSelection(bool clearSelection) { } else if (_state == WidgetState::Filtered) { _filteredSelected = _peerSearchSelected + = _previewSelected = _searchedSelected = _hashtagSelected = -1; } @@ -3416,6 +3558,19 @@ void InnerWidget::repaintSearchResult(int index) { _st->height); } +void InnerWidget::repaintPreviewResult(int index) { + rtlupdate( + 0, + previewOffset() + index * _st->height, + width(), + _st->height); +} + +bool InnerWidget::computeSearchWithPostsPreview() const { + return (_searchHashOrCashtag != HashOrCashtag::None) + && (_searchState.tab == ChatSearchTab::MyMessages); +} + void InnerWidget::clearFilter() { if (_state == WidgetState::Filtered || _searchState.inChat) { if (_searchState.inChat) { @@ -3428,6 +3583,9 @@ void InnerWidget::clearFilter() { _filterResultsGlobal.clear(); _peerSearchResults.clear(); _searchResults.clear(); + _previewResults.clear(); + _trackedHistories.clear(); + _trackedLifetime.destroy(); _filter = QString(); refresh(true); } @@ -3474,15 +3632,22 @@ void InnerWidget::selectSkip(int32 direction) { } scrollToDefaultSelected(); } else if (_state == WidgetState::Filtered) { - if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty() && _searchResults.empty()) { + if (_hashtagResults.empty() + && _filterResults.empty() + && _peerSearchResults.empty() + && _previewResults.empty() + && _searchResults.empty()) { return; } - if ((_hashtagSelected < 0 || _hashtagSelected >= _hashtagResults.size()) && - (_filteredSelected < 0 || _filteredSelected >= _filterResults.size()) && - (_peerSearchSelected < 0 || _peerSearchSelected >= _peerSearchResults.size()) && - (_searchedSelected < 0 || _searchedSelected >= _searchResults.size())) { - if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty()) { + if ((_hashtagSelected < 0 || _hashtagSelected >= _hashtagResults.size()) + && (_filteredSelected < 0 || _filteredSelected >= _filterResults.size()) + && (_peerSearchSelected < 0 || _peerSearchSelected >= _peerSearchResults.size()) + && (_previewSelected < 0 || _previewSelected >= _previewResults.size()) + && (_searchedSelected < 0 || _searchedSelected >= _searchResults.size())) { + if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty() && _previewResults.empty()) { _searchedSelected = 0; + } else if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty()) { + _previewSelected = 0; } else if (_hashtagResults.empty() && _filterResults.empty()) { _peerSearchSelected = 0; } else if (_hashtagResults.empty()) { @@ -3493,30 +3658,36 @@ void InnerWidget::selectSkip(int32 direction) { } else { int32 cur = base::in_range(_hashtagSelected, 0, _hashtagResults.size()) ? _hashtagSelected - : (base::in_range(_filteredSelected, 0, _filterResults.size()) - ? (_hashtagResults.size() + _filteredSelected) - : (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size()) - ? (_peerSearchSelected + _filterResults.size() + _hashtagResults.size()) - : (_searchedSelected + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size()))); + : base::in_range(_filteredSelected, 0, _filterResults.size()) + ? (_hashtagResults.size() + _filteredSelected) + : base::in_range(_peerSearchSelected, 0, _peerSearchResults.size()) + ? (_peerSearchSelected + _filterResults.size() + _hashtagResults.size()) + : base::in_range(_previewSelected, 0, _previewResults.size()) + ? (_previewSelected + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size()) + : (_searchedSelected + _previewResults.size() + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size()); cur = std::clamp( cur + direction, 0, static_cast(_hashtagResults.size() + _filterResults.size() + _peerSearchResults.size() + + _previewResults.size() + _searchResults.size()) - 1); if (cur < _hashtagResults.size()) { _hashtagSelected = cur; - _filteredSelected = _peerSearchSelected = _searchedSelected = -1; + _filteredSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1; } else if (cur < _hashtagResults.size() + _filterResults.size()) { _filteredSelected = cur - _hashtagResults.size(); - _hashtagSelected = _peerSearchSelected = _searchedSelected = -1; + _hashtagSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1; } else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size()) { _peerSearchSelected = cur - _hashtagResults.size() - _filterResults.size(); - _hashtagSelected = _filteredSelected = _searchedSelected = -1; + _hashtagSelected = _filteredSelected = _previewSelected = _searchedSelected = -1; + } else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size() + _previewResults.size()) { + _previewSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size(); + _hashtagSelected = _filteredSelected = _peerSearchSelected = _searchedSelected = -1; } else { - _hashtagSelected = _filteredSelected = _peerSearchSelected = -1; - _searchedSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size(); + _searchedSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size() - _previewResults.size(); + _hashtagSelected = _filteredSelected = _peerSearchSelected = _previewSelected = -1; } } if (base::in_range(_hashtagSelected, 0, _hashtagResults.size())) { @@ -3533,6 +3704,13 @@ void InnerWidget::selectSkip(int32 direction) { const auto height = st::dialogsRowHeight + (_peerSearchSelected ? 0 : st::searchedBarHeight); scrollToItem(from, height); + } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { + const auto from = previewOffset() + + _previewSelected * _st->height + + (_previewSelected ? 0 : -st::searchedBarHeight); + const auto height = _st->height + + (_previewSelected ? 0 : st::searchedBarHeight); + scrollToItem(from, height); } else { const auto from = searchedOffset() + _searchedSelected * _st->height @@ -3551,7 +3729,14 @@ void InnerWidget::scrollToEntry(const RowDescriptor &entry) { scrollToItem(dialogsOffset() + row->top(), row->height()); } } else if (_state == WidgetState::Filtered) { - for (int32 i = 0, c = _searchResults.size(); i < c; ++i) { + for (auto i = 0, c = int(_previewResults.size()); i != c; ++i) { + if (isSearchResultActive(_previewResults[i].get(), entry)) { + const auto from = previewOffset() + i * _st->height; + scrollToItem(from, _st->height); + return; + } + } + for (auto i = 0, c = int(_searchResults.size()); i != c; ++i) { if (isSearchResultActive(_searchResults[i].get(), entry)) { const auto from = searchedOffset() + i * _st->height; scrollToItem(from, _st->height); @@ -3646,33 +3831,45 @@ void InnerWidget::preloadRowsData() { } yTo -= otherStart; } else if (_state == WidgetState::Filtered) { - int32 from = (yFrom - filteredOffset()) / _st->height; + auto from = (yFrom - filteredOffset()) / _st->height; if (from < 0) from = 0; if (from < _filterResults.size()) { - int32 to = (yTo / _st->height) + 1; - if (to > _filterResults.size()) to = _filterResults.size(); - + const auto to = std::min( + ((yTo - filteredOffset()) / _st->height) + 1, + int(_filterResults.size())); for (; from < to; ++from) { _filterResults[from].key().entry()->chatListPreloadData(); } } - from = (yFrom > filteredOffset() + st::searchedBarHeight ? ((yFrom - filteredOffset() - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size(); + from = (yFrom - peerSearchOffset()) / st::dialogsRowHeight; if (from < 0) from = 0; if (from < _peerSearchResults.size()) { - int32 to = (yTo > filteredOffset() + st::searchedBarHeight ? ((yTo - filteredOffset() - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() + 1; - if (to > _peerSearchResults.size()) to = _peerSearchResults.size(); - + const auto to = std::min( + ((yTo - peerSearchOffset()) / st::dialogsRowHeight) + 1, + int(_peerSearchResults.size())); for (; from < to; ++from) { _peerSearchResults[from]->peer->loadUserpic(); } } - from = (yFrom > filteredOffset() + ((_peerSearchResults.empty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight) ? ((yFrom - filteredOffset() - (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() - _peerSearchResults.size(); + + from = (yFrom - previewOffset()) / _st->height; + if (from < 0) from = 0; + if (from < _previewResults.size()) { + const auto to = std::min( + ((yTo - previewOffset()) / _st->height) + 1, + int(_previewResults.size())); + for (; from < to; ++from) { + _previewResults[from]->item()->history()->peer->loadUserpic(); + } + } + + from = (yFrom - searchedOffset()) / _st->height; if (from < 0) from = 0; if (from < _searchResults.size()) { - int32 to = (yTo > filteredOffset() + (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight ? ((yTo - filteredOffset() - (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() - _peerSearchResults.size() + 1; - if (to > _searchResults.size()) to = _searchResults.size(); - + const auto to = std::min( + ((yTo - searchedOffset()) / _st->height) + 1, + int(_searchResults.size())); for (; from < to; ++from) { _searchResults[from]->item()->history()->peer->loadUserpic(); } @@ -3803,6 +4000,14 @@ ChosenRow InnerWidget::computeChosenRow() const { .key = session().data().history(peer), .message = Data::UnreadMessagePosition }; + } else if (base::in_range(_previewSelected, 0, _previewResults.size())) { + const auto result = _previewResults[_previewSelected].get(); + const auto topic = result->topic(); + const auto item = result->item(); + return { + .key = (topic ? (Entry*)topic : (Entry*)item->history()), + .message = item->position() + }; } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { const auto result = _searchResults[_searchedSelected].get(); const auto topic = result->topic(); @@ -3832,6 +4037,11 @@ bool InnerWidget::chooseRow( MsgId pressedTopicRootId) { if (chooseHashtag()) { return true; + } else if (_selectedMorePosts) { + if (_searchHashOrCashtag != HashOrCashtag::None) { + _changeSearchTabRequests.fire(ChatSearchTab::PublicPosts); + } + return true; } const auto modifyChosenRow = [&]( ChosenRow row, diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index a0b80ef67..a284cca11 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -72,13 +72,18 @@ struct ChosenRow { bool newWindow : 1 = false; }; -enum class SearchRequestType : uchar { - FromStart, - FromOffset, - PeerFromStart, - PeerFromOffset, - MigratedFromStart, - MigratedFromOffset, +struct SearchRequestType { + bool migrated : 1 = false; + bool posts : 1 = false; + bool start : 1 = false; + bool peer : 1 = false; + + friend inline constexpr auto operator<=>( + SearchRequestType a, + SearchRequestType b) = default; + friend inline constexpr bool operator==( + SearchRequestType a, + SearchRequestType b) = default; }; enum class SearchRequestDelay : uchar { @@ -283,6 +288,7 @@ private: void setHashtagPressed(int pressed); void setFilteredPressed(int pressed, bool pressedTopicJump); void setPeerSearchPressed(int pressed); + void setPreviewPressed(int pressed); void setSearchedPressed(int pressed); bool isPressed() const { return (_collapsedPressed >= 0) @@ -290,7 +296,9 @@ private: || (_hashtagPressed >= 0) || (_filteredPressed >= 0) || (_peerSearchPressed >= 0) - || (_searchedPressed >= 0); + || (_previewPressed >= 0) + || (_searchedPressed >= 0) + || _pressedMorePosts; } bool isSelected() const { return (_collapsedSelected >= 0) @@ -298,7 +306,9 @@ private: || (_hashtagSelected >= 0) || (_filteredSelected >= 0) || (_peerSearchSelected >= 0) - || (_searchedSelected >= 0); + || (_previewSelected >= 0) + || (_searchedSelected >= 0) + || _selectedMorePosts; } bool uniqueSearchResults() const; bool hasHistoryInResults(not_null history) const; @@ -352,6 +362,7 @@ private: [[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int peerSearchOffset() const; [[nodiscard]] int searchInChatOffset() const; + [[nodiscard]] int previewOffset() const; [[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchInChatSkip() const; [[nodiscard]] int hashtagsOffset() const; @@ -403,14 +414,18 @@ private: // const Ui::Text::String &text) const; void updateSearchIn(); void repaintSearchResult(int index); + void repaintPreviewResult(int index); + + [[nodiscard]] bool computeSearchWithPostsPreview() const; Ui::VideoUserpic *validateVideoUserpic(not_null row); Ui::VideoUserpic *validateVideoUserpic(not_null history); Row *shownRowByKey(Key key); void clearSearchResults(bool clearPeerSearchResults = true); + void clearPreviewResults(); void updateSelectedRow(Key key = Key()); - void trackSearchResultsHistory(not_null history); + void trackResultsHistory(not_null history); [[nodiscard]] QBrush currentBg() const; [[nodiscard]] RowDescriptor computeChatPreviewRow() const; @@ -449,6 +464,8 @@ private: std::vector> _collapsedRows; not_null _st; mutable std::unique_ptr _topicJumpCache; + bool _selectedMorePosts = false; + bool _pressedMorePosts = false; int _collapsedSelected = -1; int _collapsedPressed = -1; bool _skipTopDialog = false; @@ -487,14 +504,21 @@ private: EmptyState _emptyState = EmptyState::None; + base::flat_set> _trackedHistories; + rpl::lifetime _trackedLifetime; + QString _peerSearchQuery; std::vector> _peerSearchResults; int _peerSearchSelected = -1; int _peerSearchPressed = -1; + std::vector> _previewResults; + int _previewCount = 0; + int _previewSelected = -1; + int _previewPressed = -1; + int _morePostsWidth = 0; + std::vector> _searchResults; - base::flat_set> _searchResultsHistories; - rpl::lifetime _searchResultsLifetime; int _searchedCount = 0; int _searchedMigratedCount = 0; int _searchedSelected = -1; @@ -516,6 +540,7 @@ private: SearchState _searchState; HashOrCashtag _searchHashOrCashtag = {}; + bool _searchWithPostsPreview = false; History *_searchInMigrated = nullptr; PeerData *_searchFromShown = nullptr; Ui::Text::String _searchFromUserText; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 62155fd42..3459d04cc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -16,7 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_key.h" -#include "dialogs/dialogs_entry.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_top_bar_widget.h" @@ -26,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_requests_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/chat_filters_tabs_strip.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/fields/input_field.h" #include "ui/wrap/fade_wrap.h" @@ -46,14 +46,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "api/api_chat_filters.h" #include "apiwrap.h" -#include "base/event_filter.h" #include "core/application.h" #include "core/ui_integration.h" #include "core/update_checker.h" #include "core/shortcuts.h" -#include "boxes/peer_list_box.h" -#include "boxes/peers/edit_participants_box.h" -#include "window/window_adaptive.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" @@ -239,7 +235,9 @@ void Widget::BottomButton::radialAnimationCallback() { } } -void Widget::BottomButton::onStateChanged(State was, StateChangeSource source) { +void Widget::BottomButton::onStateChanged( + State was, + StateChangeSource source) { RippleButton::onStateChanged(was, source); if ((was & StateFlag::Disabled) != (state() & StateFlag::Disabled)) { _loading = isDisabled() @@ -537,11 +535,11 @@ Widget::Widget( }, lifetime()); } - _cancelSearch->setClickedCallback([this] { + _cancelSearch->setClickedCallback([=] { cancelSearch({ .jumpBackToSearchedChat = true }); }); - _jumpToDate->entity()->setClickedCallback([this] { showCalendar(); }); - _chooseFromUser->entity()->setClickedCallback([this] { showSearchFrom(); }); + _jumpToDate->entity()->setClickedCallback([=] { showCalendar(); }); + _chooseFromUser->entity()->setClickedCallback([=] { showSearchFrom(); }); rpl::single(rpl::empty) | rpl::then( session().domain().local().localPasscodeChanged() ) | rpl::start_with_next([=] { @@ -581,11 +579,10 @@ Widget::Widget( }); _inner->setLoadMoreCallback([=] { const auto state = _inner->state(); + const auto process = currentSearchProcess(); if (state == WidgetState::Filtered - && (!_searchFull - || (_searchInMigrated - && _searchFull - && !_searchFullMigrated))) { + && (!process->full + || (_searchInMigrated && !_migratedProcess.full))) { searchMore(); } else if (_openedForum && state == WidgetState::Default) { _openedForum->requestTopics(); @@ -659,6 +656,12 @@ Widget::Widget( setupMoreChatsBar(); setupDownloadBar(); } + + if (session().settings().dialogsFiltersEnabled() + && (Core::App().settings().chatFiltersHorizontal() + || !controller->enoughSpaceForFilters())) { + toggleFiltersMenu(true); + } } void Widget::chosenRow(const ChosenRow &row) { @@ -937,7 +940,7 @@ void Widget::updateScrollUpVisibility() { } startScrollUpButtonAnimation( - (_scroll->scrollTop() > st::historyToDownShownAfter) + (_scroll->scrollTop() > (st::historyToDownShownAfter / 2)) && (_scroll->scrollTop() < _scroll->scrollTopMax())); } @@ -1218,11 +1221,12 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) { return !_searchQuery.isEmpty(); }) | rpl::start_with_next([=] { _searchTimer.cancel(); - _searchCache.clear(); - _singleMessageSearch.clear(); - for (const auto &[requestId, query] : base::take(_searchQueries)) { + _searchProcess.cache.clear(); + const auto queries = base::take(_searchProcess.queries); + for (const auto &[requestId, query] : queries) { session().api().request(requestId).cancel(); } + _singleMessageSearch.clear(); _searchQuery = QString(); _scroll->scrollToY(0); cancelSearchRequest(); @@ -1244,6 +1248,9 @@ void Widget::updateControlsVisibility(bool fast) { if (_moreChatsBar) { _moreChatsBar->show(); } + if (_chatFilters) { + _chatFilters->show(); + } if (_openedFolder || _openedForum) { _subsectionTopBar->show(); if (_forumTopShadow) { @@ -1313,6 +1320,60 @@ void Widget::updateHasFocus(not_null focused) { } } +void Widget::toggleFiltersMenu(bool enabled) { + if (_layout == Layout::Child) { + enabled = false; + } + if (!enabled == !_chatFilters) { + return; + } else if (enabled) { + class NoScrollPropagationWidget final : public Ui::RpWidget { + public: + using Ui::RpWidget::RpWidget; + + protected: + void touchEvent(QTouchEvent *e) { + e->accept(); + } + void wheelEvent(QWheelEvent *e) override final { + e->accept(); + } + + }; + + _chatFilters = base::make_unique_q(this); + const auto raw = _chatFilters.get(); + const auto inner = Ui::AddChatFiltersTabsStrip( + _chatFilters.get(), + &session(), + [this](FilterId id) { + _scroll->scrollToY(0); + if (controller()->activeChatsFilterCurrent() != id) { + controller()->setActiveChatsFilter(id); + } + }, + controller(), + true); + raw->show(); + raw->stackUnder(_scroll); + raw->resizeToWidth(width()); + const auto shadow = Ui::CreateChild(raw); + shadow->show(); + inner->sizeValue() | rpl::start_with_next([=, this](const QSize &s) { + raw->resize(s); + shadow->setGeometry( + 0, + s.height() - shadow->height(), + s.width(), + shadow->height()); + updateControlsGeometry(); + }, _chatFilters->lifetime()); + updateControlsGeometry(); + } else { + _chatFilters = nullptr; + } +} + bool Widget::cancelSearchByMouseBack() { return _searchHasFocus && !_searchSuggestionsLocked @@ -1548,6 +1609,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { _searchState.tab = forum ? ChatSearchTab::ThisPeer : ChatSearchTab::MyMessages; + _searchWithPostsPreview = computeSearchWithPostsPreview(); _api.request(base::take(_topicSearchRequest)).cancel(); _inner->changeOpenedForum(forum); storiesToggleExplicitExpand(false); @@ -1692,11 +1754,7 @@ QPixmap Widget::grabForFolderSlideAnimation() { _scrollToTop->hide(); } - const auto rect = QRect( - 0, - 0, - width(), - _scroll->y() + _scroll->height()); + const auto rect = QRect(0, 0, width(), rect::bottom(_scroll)); auto result = Ui::GrabWidget(this, rect); if (!hidden) { @@ -1865,6 +1923,12 @@ void Widget::scrollToDefault(bool verytop) { this, QPoint(), QRect(0, top, wideGeometry.width(), skip)); + if (_chatFilters) { + Ui::RenderWidget( + p, + _chatFilters, + QPoint(0, skip - _chatFilters->height())); + } Ui::RenderWidget(p, _scroll, QPoint(0, skip)); } if (scrollGeometry != wideGeometry) { @@ -1880,6 +1944,9 @@ void Widget::startWidthAnimation() { } _widthAnimationCache = grabNonNarrowScrollFrame(); _scroll->hide(); + if (_chatFilters) { + _chatFilters->hide(); + } updateStoriesVisibility(); } @@ -1887,6 +1954,9 @@ void Widget::stopWidthAnimation() { _widthAnimationCache = QPixmap(); if (!_showAnimation) { _scroll->setVisible(!_suggestions); + if (_chatFilters) { + _chatFilters->setVisible(!_suggestions); + } } updateStoriesVisibility(); update(); @@ -1988,6 +2058,9 @@ void Widget::startSlideAnimation( if (_moreChatsBar) { _moreChatsBar->hide(); } + if (_chatFilters) { + _chatFilters->hide(); + } if (_forumTopShadow) { _forumTopShadow->hide(); } @@ -2136,12 +2209,18 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { const auto fromPeer = searchFromPeer(); const auto &inTags = searchInTags(); const auto tab = _searchState.tab; - const auto fromStartType = inPeer - ? SearchRequestType::PeerFromStart - : SearchRequestType::FromStart; + const auto fromStartType = SearchRequestType{ + .start = true, + .peer = (inPeer != nullptr), + }; if (trimmed.isEmpty() && !fromPeer && inTags.empty()) { cancelSearchRequest(); - searchApplyEmpty(fromStartType, 0); + searchApplyEmpty(fromStartType, currentSearchProcess()); + if (_searchWithPostsPreview) { + searchApplyEmpty( + { .posts = true, .start = true }, + &_postsProcess); + } _api.request(base::take(_peerSearchRequest)).cancel(); _peerSearchQuery = QString(); peerSearchApplyEmpty(0); @@ -2154,28 +2233,32 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { if (!success) { return false; } - const auto i = _searchCache.find(query); - if (i != _searchCache.end()) { + const auto process = currentSearchProcess(); + const auto i = process->cache.find(query); + if (i != process->cache.end()) { _searchQuery = query; _searchQueryFrom = fromPeer; _searchQueryTags = inTags; _searchQueryTab = tab; - _searchNextRate = 0; - _searchFull = _searchFullMigrated = false; + process->nextRate = 0; + process->full = false; + _migratedProcess.full = false; cancelSearchRequest(); - searchReceived(fromStartType, i->second, 0); + searchReceived(fromStartType, i->second, process, true); result = true; } } else if (_searchQuery != query || _searchQueryFrom != fromPeer || _searchQueryTags != inTags || _searchQueryTab != tab) { + const auto process = currentSearchProcess(); _searchQuery = query; _searchQueryFrom = fromPeer; _searchQueryTags = inTags; _searchQueryTab = tab; - _searchNextRate = 0; - _searchFull = _searchFullMigrated = false; + process->nextRate = 0; + process->full = false; + _migratedProcess.full = false; cancelSearchRequest(); if (inPeer) { const auto topic = searchInTopic(); @@ -2189,83 +2272,55 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { const auto savedPeer = sublist ? sublist->peer().get() : nullptr; - _searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn finish) { - const auto type = SearchRequestType::PeerFromStart; + _historiesRequest = histories.sendRequest(history, type, [=]( + Fn finish) { + const auto type = SearchRequestType{ + .start = true, + .peer = true, + }; using Flag = MTPmessages_Search::Flag; - _searchRequest = session().api().request(MTPmessages_Search( - MTP_flags((topic ? Flag::f_top_msg_id : Flag()) - | (fromPeer ? Flag::f_from_id : Flag()) - | (savedPeer ? Flag::f_saved_peer_id : Flag()) - | (_searchQueryTags.empty() - ? Flag() - : Flag::f_saved_reaction)), - inPeer->input, - MTP_string(_searchQuery), - (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()), - (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()), - MTP_vector_from_range( - _searchQueryTags | ranges::views::transform( - Data::ReactionToMTP - )), - MTP_int(topic ? topic->rootId() : 0), - MTP_inputMessagesFilterEmpty(), - MTP_int(0), // min_date - MTP_int(0), // max_date - MTP_int(0), // offset_id - MTP_int(0), // add_offset - MTP_int(kSearchPerPage), - MTP_int(0), // max_id - MTP_int(0), // min_id - MTP_long(0) // hash - )).done([=](const MTPmessages_Messages &result) { - _searchInHistoryRequest = 0; - searchReceived(type, result, _searchRequest); + process->requestId = session().api().request( + MTPmessages_Search( + MTP_flags((topic ? Flag::f_top_msg_id : Flag()) + | (fromPeer ? Flag::f_from_id : Flag()) + | (savedPeer ? Flag::f_saved_peer_id : Flag()) + | (_searchQueryTags.empty() + ? Flag() + : Flag::f_saved_reaction)), + inPeer->input, + MTP_string(_searchQuery), + (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()), + (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()), + MTP_vector_from_range( + _searchQueryTags | ranges::views::transform( + Data::ReactionToMTP + )), + MTP_int(topic ? topic->rootId() : 0), + MTP_inputMessagesFilterEmpty(), + MTP_int(0), // min_date + MTP_int(0), // max_date + MTP_int(0), // offset_id + MTP_int(0), // add_offset + MTP_int(kSearchPerPage), + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + _historiesRequest = 0; + searchReceived(type, result, process); finish(); }).fail([=](const MTP::Error &error) { - _searchInHistoryRequest = 0; - searchFailed(type, error, _searchRequest); + _historiesRequest = 0; + searchFailed(type, error, process); finish(); }).send(); - _searchQueries.emplace(_searchRequest, _searchQuery); - return _searchRequest; + process->queries.emplace(process->requestId, _searchQuery); + return process->requestId; }); } else if (_searchState.tab == ChatSearchTab::PublicPosts) { - const auto type = SearchRequestType::FromStart; - _searchRequest = session().api().request(MTPchannels_SearchPosts( - MTP_string(_searchState.query.trimmed().mid(1)), - MTP_int(0), // offset_rate - MTP_inputPeerEmpty(), // offset_peer - MTP_int(0), // offset_id - MTP_int(kSearchPerPage) - )).done([=](const MTPmessages_Messages &result) { - searchReceived(type, result, _searchRequest); - }).fail([=](const MTP::Error &error) { - searchFailed(type, error, _searchRequest); - }).send(); - _searchQueries.emplace(_searchRequest, _searchQuery); + requestPublicPosts(true); } else { - const auto type = SearchRequestType::FromStart; - const auto flags = session().settings().skipArchiveInSearch() - ? MTPmessages_SearchGlobal::Flag::f_folder_id - : MTPmessages_SearchGlobal::Flag(0); - const auto folderId = 0; - _searchRequest = session().api().request(MTPmessages_SearchGlobal( - MTP_flags(flags), - MTP_int(folderId), - MTP_string(_searchQuery), - MTP_inputMessagesFilterEmpty(), - MTP_int(0), // min_date - MTP_int(0), // max_date - MTP_int(0), // offset_rate - MTP_inputPeerEmpty(), // offset_peer - MTP_int(0), // offset_id - MTP_int(kSearchPerPage) - )).done([=](const MTPmessages_Messages &result) { - searchReceived(type, result, _searchRequest); - }).fail([=](const MTP::Error &error) { - searchFailed(type, error, _searchRequest); - }).send(); - _searchQueries.emplace(_searchRequest, _searchQuery); + requestMessages(true); } _inner->searchRequested(true); } else { @@ -2287,7 +2342,9 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { _peerSearchRequest = _api.request(MTPcontacts_Search( MTP_string(_peerSearchQuery), MTP_int(SearchPeopleLimit) - )).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) { + )).done([=]( + const MTPcontacts_Found &result, + mtpRequestId requestId) { peerSearchReceived(result, requestId); }).fail([=](const MTP::Error &error, mtpRequestId requestId) { peerSearchFailed(error, requestId); @@ -2394,11 +2451,12 @@ void Widget::searchTopics() { } void Widget::searchMore() { - if (_searchRequest - || _searchInHistoryRequest + const auto process = currentSearchProcess(); + if (process->requestId + || _historiesRequest || _searchTimer.isActive()) { return; - } else if (!_searchFull) { + } else if (!process->full) { if (const auto peer = searchInPeer()) { auto &histories = session().data().histories(); const auto topic = searchInTopic(); @@ -2411,154 +2469,213 @@ void Widget::searchMore() { const auto savedPeer = sublist ? sublist->peer().get() : nullptr; - _searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn finish) { - const auto type = _lastSearchId - ? SearchRequestType::PeerFromOffset - : SearchRequestType::PeerFromStart; + _historiesRequest = histories.sendRequest(history, type, [=]( + Fn finish) { + const auto type = SearchRequestType{ + .start = !process->lastId, + .peer = true, + }; using Flag = MTPmessages_Search::Flag; - _searchRequest = session().api().request(MTPmessages_Search( - MTP_flags((topic ? Flag::f_top_msg_id : Flag()) - | (fromPeer ? Flag::f_from_id : Flag()) - | (savedPeer ? Flag::f_saved_peer_id : Flag()) - | (_searchQueryTags.empty() - ? Flag() - : Flag::f_saved_reaction)), - peer->input, + process->requestId = session().api().request( + MTPmessages_Search( + MTP_flags((topic ? Flag::f_top_msg_id : Flag()) + | (fromPeer ? Flag::f_from_id : Flag()) + | (savedPeer ? Flag::f_saved_peer_id : Flag()) + | (_searchQueryTags.empty() + ? Flag() + : Flag::f_saved_reaction)), + peer->input, + MTP_string(_searchQuery), + (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()), + (savedPeer + ? savedPeer->input + : MTP_inputPeerEmpty()), + MTP_vector_from_range( + _searchQueryTags | ranges::views::transform( + Data::ReactionToMTP + )), + MTP_int(topic ? topic->rootId() : 0), + MTP_inputMessagesFilterEmpty(), + MTP_int(0), // min_date + MTP_int(0), // max_date + MTP_int(process->lastId), + MTP_int(0), // add_offset + MTP_int(kSearchPerPage), + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + searchReceived(type, result, process); + _historiesRequest = 0; + finish(); + }).fail([=](const MTP::Error &error) { + searchFailed(type, error, process); + _historiesRequest = 0; + finish(); + }).send(); + if (!process->lastId) { + process->queries.emplace( + process->requestId, + _searchQuery); + } + return process->requestId; + }); + } else if (_searchState.tab == ChatSearchTab::PublicPosts) { + requestPublicPosts(false); + } else { + requestMessages(false); + } + } else if (_searchInMigrated && !_migratedProcess.full) { + auto &histories = session().data().histories(); + const auto type = Data::Histories::RequestType::History; + const auto history = _searchInMigrated; + _historiesRequest = histories.sendRequest(history, type, [=]( + Fn finish) { + const auto type = SearchRequestType{ + .migrated = true, + .start = !_migratedProcess.lastId, + }; + const auto flags = _searchQueryFrom + ? MTP_flags(MTPmessages_Search::Flag::f_from_id) + : MTP_flags(0); + _migratedProcess.requestId = session().api().request( + MTPmessages_Search( + flags, + _searchInMigrated->peer->input, MTP_string(_searchQuery), - (fromPeer ? fromPeer->input : MTP_inputPeerEmpty()), - (savedPeer ? savedPeer->input : MTP_inputPeerEmpty()), - MTP_vector_from_range( - _searchQueryTags | ranges::views::transform( - Data::ReactionToMTP - )), - MTP_int(topic ? topic->rootId() : 0), + (_searchQueryFrom + ? _searchQueryFrom->input + : MTP_inputPeerEmpty()), + MTPInputPeer(), // saved_peer_id + MTPVector(), // saved_reaction + MTPint(), // top_msg_id MTP_inputMessagesFilterEmpty(), MTP_int(0), // min_date MTP_int(0), // max_date - MTP_int(_lastSearchId), + MTP_int(_migratedProcess.lastId), MTP_int(0), // add_offset MTP_int(kSearchPerPage), MTP_int(0), // max_id MTP_int(0), // min_id - MTP_long(0) // hash - )).done([=](const MTPmessages_Messages &result) { - searchReceived(type, result, _searchRequest); - _searchInHistoryRequest = 0; - finish(); - }).fail([=](const MTP::Error &error) { - searchFailed(type, error, _searchRequest); - _searchInHistoryRequest = 0; - finish(); - }).send(); - if (!_lastSearchId) { - _searchQueries.emplace(_searchRequest, _searchQuery); - } - return _searchRequest; - }); - } else { - const auto type = _lastSearchId - ? SearchRequestType::FromOffset - : SearchRequestType::FromStart; - const auto flags = session().settings().skipArchiveInSearch() - ? MTPmessages_SearchGlobal::Flag::f_folder_id - : MTPmessages_SearchGlobal::Flag(0); - const auto folderId = 0; - _searchRequest = session().api().request(MTPmessages_SearchGlobal( - MTP_flags(flags), - MTP_int(folderId), - MTP_string(_searchQuery), - MTP_inputMessagesFilterEmpty(), - MTP_int(0), // min_date - MTP_int(0), // max_date - MTP_int(_searchNextRate), - (_lastSearchPeer - ? _lastSearchPeer->input - : MTP_inputPeerEmpty()), - MTP_int(_lastSearchId), - MTP_int(kSearchPerPage) - )).done([=](const MTPmessages_Messages &result) { - searchReceived(type, result, _searchRequest); - }).fail([=](const MTP::Error &error) { - searchFailed(type, error, _searchRequest); - }).send(); - if (!_lastSearchId) { - _searchQueries.emplace(_searchRequest, _searchQuery); - } - } - } else if (_searchInMigrated && !_searchFullMigrated) { - auto &histories = session().data().histories(); - const auto type = Data::Histories::RequestType::History; - const auto history = _searchInMigrated; - _searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn finish) { - const auto type = _lastSearchMigratedId - ? SearchRequestType::MigratedFromOffset - : SearchRequestType::MigratedFromStart; - const auto flags = _searchQueryFrom - ? MTP_flags(MTPmessages_Search::Flag::f_from_id) - : MTP_flags(0); - _searchRequest = session().api().request(MTPmessages_Search( - flags, - _searchInMigrated->peer->input, - MTP_string(_searchQuery), - (_searchQueryFrom - ? _searchQueryFrom->input - : MTP_inputPeerEmpty()), - MTPInputPeer(), // saved_peer_id - MTPVector(), // saved_reaction - MTPint(), // top_msg_id - MTP_inputMessagesFilterEmpty(), - MTP_int(0), // min_date - MTP_int(0), // max_date - MTP_int(_lastSearchMigratedId), - MTP_int(0), // add_offset - MTP_int(kSearchPerPage), - MTP_int(0), // max_id - MTP_int(0), // min_id - MTP_long(0) // hash - )).done([=](const MTPmessages_Messages &result) { - searchReceived(type, result, _searchRequest); - _searchInHistoryRequest = 0; + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + searchReceived(type, result, &_migratedProcess); + _historiesRequest = 0; finish(); }).fail([=](const MTP::Error &error) { - searchFailed(type, error, _searchRequest); - _searchInHistoryRequest = 0; + searchFailed(type, error, &_migratedProcess); + _historiesRequest = 0; finish(); }).send(); - return _searchRequest; + return _migratedProcess.requestId; }); } } +void Widget::requestPublicPosts(bool fromStart) { + if (!_postsProcess.lastId || !_postsProcess.lastPeer) { + fromStart = true; + } + const auto type = SearchRequestType{ + .posts = true, + .start = fromStart, + }; + _postsProcess.requestId = session().api().request( + MTPchannels_SearchPosts( + MTP_string(_searchState.query.trimmed().mid(1)), + MTP_int(fromStart ? 0 : _postsProcess.nextRate), + (fromStart + ? MTP_inputPeerEmpty() + : _postsProcess.lastPeer->input), + MTP_int(fromStart ? 0 : _postsProcess.lastId), + MTP_int(kSearchPerPage)) + ).done([=](const MTPmessages_Messages &result) { + searchReceived(type, result, &_postsProcess); + }).fail([=](const MTP::Error &error) { + searchFailed(type, error, &_postsProcess); + }).send(); + if (fromStart) { + _postsProcess.queries.emplace(_postsProcess.requestId, _searchQuery); + } +} + +void Widget::requestMessages(bool fromStart) { + if (!_searchProcess.lastId || !_searchProcess.lastPeer) { + fromStart = true; + } + const auto type = SearchRequestType{ + .start = fromStart, + }; + const auto flags = session().settings().skipArchiveInSearch() + ? MTPmessages_SearchGlobal::Flag::f_folder_id + : MTPmessages_SearchGlobal::Flag(0); + const auto folderId = 0; + _searchProcess.requestId = session().api().request( + MTPmessages_SearchGlobal( + MTP_flags(flags), + MTP_int(folderId), + MTP_string(_searchQuery), + MTP_inputMessagesFilterEmpty(), + MTP_int(0), // min_date + MTP_int(0), // max_date + MTP_int(fromStart ? 0 : _searchProcess.nextRate), + (fromStart + ? MTP_inputPeerEmpty() + : _searchProcess.lastPeer->input), + MTP_int(fromStart ? 0 : _searchProcess.lastId), + MTP_int(kSearchPerPage)) + ).done([=](const MTPmessages_Messages &result) { + searchReceived(type, result, &_searchProcess); + }).fail([=](const MTP::Error &error) { + searchFailed(type, error, &_searchProcess); + }).send(); + if (!_searchProcess.lastId) { + _searchProcess.queries.emplace( + _searchProcess.requestId, + _searchQuery); + } + if (fromStart && _searchWithPostsPreview) { + requestPublicPosts(true); + } +} + +auto Widget::currentSearchProcess() -> not_null { + return (_searchState.tab == ChatSearchTab::PublicPosts) + ? &_postsProcess + : &_searchProcess; +} + +bool Widget::computeSearchWithPostsPreview() const { + return (_searchHashOrCashtag != HashOrCashtag::None) + && (_searchState.tab == ChatSearchTab::MyMessages); +} + void Widget::searchReceived( SearchRequestType type, const MTPmessages_Messages &result, - mtpRequestId requestId) { + not_null process, + bool cacheResults) { const auto state = _inner->state(); - if (state == WidgetState::Filtered) { - if (type == SearchRequestType::FromStart || type == SearchRequestType::PeerFromStart) { - auto i = _searchQueries.find(requestId); - if (i != _searchQueries.end()) { - _searchCache[i->second] = result; - _searchQueries.erase(i); - } + if (!cacheResults + && (state == WidgetState::Filtered) + && type.start) { + const auto i = process->queries.find(process->requestId); + if (i != process->queries.end()) { + process->cache[i->second] = result; + process->queries.erase(i); } } - const auto inject = (type == SearchRequestType::FromStart - || type == SearchRequestType::PeerFromStart) + const auto inject = (type.start && !type.posts) ? *_singleMessageSearch.lookup(_searchQuery) : nullptr; - - if (_searchRequest != requestId) { + if (cacheResults && process->requestId) { return; } - if (type == SearchRequestType::FromStart - || type == SearchRequestType::PeerFromStart) { - _lastSearchPeer = nullptr; - _lastSearchId = _lastSearchMigratedId = 0; + if (type.start) { + process->lastPeer = nullptr; + process->lastId = 0; } - const auto isMigratedSearch = (type == SearchRequestType::MigratedFromStart) - || (type == SearchRequestType::MigratedFromOffset); - const auto process = [&](const MTPVector &messages) { + const auto processList = [&](const MTPVector &messages) { auto result = std::vector>(); for (const auto &message : messages.v) { const auto msgId = IdFromMessage(message); @@ -2572,55 +2689,44 @@ void Widget::searchReceived( NewMessageType::Existing); result.push_back(item); } - _lastSearchPeer = peer; + process->lastPeer = peer; } else { LOG(("API Error: a search results with not loaded peer %1" ).arg(peerId.value)); } - if (isMigratedSearch) { - _lastSearchMigratedId = msgId; - } else { - _lastSearchId = msgId; - } + process->lastId = msgId; } return result; }; auto fullCount = 0; auto messages = result.match([&](const MTPDmessages_messages &data) { - if (_searchRequest != 0) { + if (!cacheResults) { // Don't apply cached data! session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); } - if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { - _searchFullMigrated = true; - } else { - _searchFull = true; - } - auto list = process(data.vmessages()); + process->full = true; + auto list = processList(data.vmessages()); fullCount = list.size(); return list; }, [&](const MTPDmessages_messagesSlice &data) { - if (_searchRequest != 0) { + if (!cacheResults) { // Don't apply cached data! session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); } - auto list = process(data.vmessages()); + auto list = processList(data.vmessages()); const auto nextRate = data.vnext_rate(); - const auto rateUpdated = nextRate && (nextRate->v != _searchNextRate); - const auto finished = (type == SearchRequestType::FromStart || type == SearchRequestType::FromOffset) - ? !rateUpdated - : list.empty(); + const auto rateUpdated = nextRate + && (nextRate->v != process->nextRate); + const auto finished = (type.peer || type.migrated || type.posts) + ? list.empty() + : !rateUpdated; if (rateUpdated) { - _searchNextRate = nextRate->v; + process->nextRate = nextRate->v; } if (finished) { - if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { - _searchFullMigrated = true; - } else { - _searchFull = true; - } + process->full = true; } fullCount = data.vcount().v; return list; @@ -2639,33 +2745,26 @@ void Widget::searchReceived( "received messages.channelMessages when no channel " "was passed! (Widget::searchReceived)")); } - if (_searchRequest != 0) { + if (!cacheResults) { // Don't apply cached data! session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); } - auto list = process(data.vmessages()); + auto list = processList(data.vmessages()); if (list.empty()) { - if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { - _searchFullMigrated = true; - } else { - _searchFull = true; - } + process->full = true; } fullCount = data.vcount().v; return list; }, [&](const MTPDmessages_messagesNotModified &) { - LOG(("API Error: received messages.messagesNotModified! (Widget::searchReceived)")); - if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { - _searchFullMigrated = true; - } else { - _searchFull = true; - } + LOG(("API Error: received messages.messagesNotModified! " + "(Widget::searchReceived)")); + process->full = true; return std::vector>(); }); _inner->searchReceived(messages, inject, type, fullCount); - _searchRequest = 0; + process->requestId = 0; listScrollUpdated(); update(); } @@ -2697,15 +2796,17 @@ void Widget::peerSearchReceived( } } -void Widget::searchApplyEmpty(SearchRequestType type, mtpRequestId id) { - _searchFull = _searchFullMigrated = true; +void Widget::searchApplyEmpty( + SearchRequestType type, + not_null process) { + process->full = true; searchReceived( type, MTP_messages_messages( MTP_vector(), MTP_vector(), MTP_vector()), - id); + process); } void Widget::peerSearchApplyEmpty(mtpRequestId id) { @@ -2722,16 +2823,12 @@ void Widget::peerSearchApplyEmpty(mtpRequestId id) { void Widget::searchFailed( SearchRequestType type, const MTP::Error &error, - mtpRequestId requestId) { + not_null process) { if (error.type() == u"SEARCH_QUERY_EMPTY"_q) { - searchApplyEmpty(type, requestId); - } else if (_searchRequest == requestId) { - _searchRequest = 0; - if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) { - _searchFullMigrated = true; - } else { - _searchFull = true; - } + searchApplyEmpty(type, process); + } else { + process->requestId = 0; + process->full = true; } } @@ -2864,6 +2961,7 @@ QString Widget::validateSearchQuery() { } else { _searchHashOrCashtag = IsHashOrCashtagSearchQuery(query); } + _searchWithPostsPreview = computeSearchWithPostsPreview(); return query; } @@ -3087,6 +3185,9 @@ bool Widget::applySearchState(SearchState state) { const auto tagsChanged = (_searchState.tags != state.tags); const auto queryChanged = (_searchState.query != state.query); const auto tabChanged = (_searchState.tab != state.tab); + const auto queryEmptyChanged = queryChanged + ? (_searchState.query.isEmpty() != state.query.isEmpty()) + : false; if (forum) { if (_openedForum == forum) { @@ -3122,6 +3223,11 @@ bool Widget::applySearchState(SearchState state) { ? peer->owner().history(migrateFrom).get() : nullptr; _searchState = state; + if (_chatFilters && queryEmptyChanged) { + _chatFilters->setVisible(_searchState.query.isEmpty()); + updateControlsGeometry(); + } + _searchWithPostsPreview = computeSearchWithPostsPreview(); if (queryChanged) { updateLockUnlockVisibility(anim::type::normal); updateLoadMoreChatsVisibility(); @@ -3137,16 +3243,20 @@ bool Widget::applySearchState(SearchState state) { updateSearchFromVisibility(); updateLockUnlockPosition(); - if ((state.query.isEmpty() && !state.fromPeer && state.tags.empty()) + const auto searchCleared = state.query.isEmpty() + && !state.fromPeer + && state.tags.empty(); + if (searchCleared || inChatChanged || fromPeerChanged || tagsChanged || tabChanged) { - clearSearchCache(); + clearSearchCache(searchCleared); } if (state.query.isEmpty()) { _peerSearchCache.clear(); - for (const auto &[requestId, query] : base::take(_peerSearchQueries)) { + const auto queries = base::take(_peerSearchQueries); + for (const auto &[requestId, query] : queries) { _api.request(requestId).cancel(); } _peerSearchQuery = QString(); @@ -3184,15 +3294,23 @@ bool Widget::applySearchState(SearchState state) { return true; } -void Widget::clearSearchCache() { - _searchCache.clear(); +void Widget::clearSearchCache(bool clearPosts) { + _searchProcess.cache.clear(); _singleMessageSearch.clear(); - for (const auto &[requestId, query] : base::take(_searchQueries)) { + const auto queries = base::take(_searchProcess.queries); + for (const auto &[requestId, query] : queries) { session().api().request(requestId).cancel(); } _searchQuery = QString(); _searchQueryFrom = nullptr; _searchQueryTags.clear(); + if (clearPosts) { + _postsProcess.cache.clear(); + const auto queries = base::take(_postsProcess.queries); + for (const auto &[requestId, query] : queries) { + session().api().request(requestId).cancel(); + } + } _topicSearchQuery = QString(); _topicSearchOffsetDate = 0; _topicSearchOffsetId = _topicSearchOffsetTopicId = 0; @@ -3267,10 +3385,17 @@ void Widget::completeHashtag(QString tag) { if (cur == start + 1 || base::StringViewMid(t, start + 1, cur - start - 1) == base::StringViewMid(tag, 0, cur - start - 1)) { - for (; cur < t.size() && cur - start - 1 < tag.size(); ++cur) { - if (t.at(cur) != tag.at(cur - start - 1)) break; + while (cur < t.size() && cur - start - 1 < tag.size()) { + if (t.at(cur) != tag.at(cur - start - 1)) { + break; + } + ++cur; + } + if (cur - start - 1 == tag.size() + && cur < t.size() + && t.at(cur) == ' ') { + ++cur; } - if (cur - start - 1 == tag.size() && cur < t.size() && t.at(cur) == ' ') ++cur; hashtag = t.mid(0, start + 1) + tag + ' ' + t.mid(cur); setSearchQuery(hashtag, start + 1 + tag.size() + 1); applySearchUpdate(); @@ -3382,7 +3507,8 @@ void Widget::updateControlsGeometry() { ? st::dialogsFilterSkip : (st::dialogsFilterPadding.x() + _mainMenu.toggle->width())) + st::dialogsFilterPadding.x(); - const auto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x(); + const auto filterRight = st::dialogsFilterSkip + + st::dialogsFilterPadding.x(); const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight; const auto filterAreaHeight = st::topBarHeight; _searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight); @@ -3395,7 +3521,11 @@ void Widget::updateControlsGeometry() { auto filterTop = (filterAreaHeight - _search->height()) / 2; filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio); - _search->setGeometryToLeft(filterLeft, filterTop, filterWidth, _search->height()); + _search->setGeometryToLeft( + filterLeft, + filterTop, + filterWidth, + _search->height()); auto mainMenuLeft = anim::interpolate( st::dialogsFilterPadding.x(), @@ -3413,12 +3543,16 @@ void Widget::updateControlsGeometry() { -_searchForNarrowLayout->width(), (_narrowWidth - _searchForNarrowLayout->width()) / 2, narrowRatio); - _searchForNarrowLayout->moveToLeft(searchLeft, st::dialogsFilterPadding.y()); + _searchForNarrowLayout->moveToLeft( + searchLeft, + st::dialogsFilterPadding.y()); auto right = filterLeft + filterWidth; _cancelSearch->moveToLeft(right - _cancelSearch->width(), _search->y()); - right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _search->y()); - right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _search->y()); + right -= _jumpToDate->width(); + _jumpToDate->moveToLeft(right, _search->y()); + right -= _chooseFromUser->width(); + _chooseFromUser->moveToLeft(right, _search->y()); const auto barw = width(); const auto expandedStoriesTop = filterAreaTop + filterAreaHeight; @@ -3480,6 +3614,9 @@ void Widget::updateControlsGeometry() { if (_forumRequestsBar) { _forumRequestsBar->resizeToWidth(barw); } + if (_chatFilters) { + _chatFilters->resizeToWidth(barw); + } _updateScrollGeometryCached = [=] { const auto moreChatsBarTop = expandedStoriesTop + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); @@ -3501,8 +3638,15 @@ void Widget::updateControlsGeometry() { if (_forumReportBar) { _forumReportBar->bar().move(0, forumReportTop); } - const auto scrollTop = forumReportTop + const auto chatFiltersTop = forumReportTop + (_forumReportBar ? _forumReportBar->bar().height() : 0); + if (_chatFilters) { + _chatFilters->move(0, chatFiltersTop); + } + const auto scrollTop = chatFiltersTop + + ((_chatFilters && _searchState.query.isEmpty()) + ? (_chatFilters->height() * (1. - narrowRatio)) + : 0); const auto scrollHeight = height() - scrollTop - bottomSkip; const auto wasScrollHeight = _scroll->height(); _scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight); @@ -3717,9 +3861,11 @@ void Widget::scrollToEntry(const RowDescriptor &entry) { } void Widget::cancelSearchRequest() { - session().api().request(base::take(_searchRequest)).cancel(); + session().api().request(base::take(_searchProcess.requestId)).cancel(); + session().api().request(base::take(_migratedProcess.requestId)).cancel(); + session().api().request(base::take(_postsProcess.requestId)).cancel(); session().data().histories().cancelRequest( - base::take(_searchInHistoryRequest)); + base::take(_historiesRequest)); } PeerData *Widget::searchInPeer() const { @@ -3836,8 +3982,12 @@ bool Widget::cancelSearch(CancelSearchOptions options) { // Don't create suggestions in unfocus case. setInnerFocus(true); } - _lastSearchPeer = nullptr; - _lastSearchId = _lastSearchMigratedId = 0; + _searchProcess.lastPeer = nullptr; + _searchProcess.lastId = 0; + _migratedProcess.lastPeer = nullptr; + _migratedProcess.lastId = 0; + _postsProcess.lastPeer = nullptr; + _postsProcess.lastId = 0; _inner->clearFilter(); applySearchState(std::move(updatedState)); if (_suggestions && clearSearchFocus) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index a1ee3950d..6f2bab88e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -75,7 +75,7 @@ class FakeRow; class Key; struct ChosenRow; class InnerWidget; -enum class SearchRequestType : uchar; +struct SearchRequestType; enum class SearchRequestDelay : uchar; class Suggestions; class ChatSearchIn; @@ -129,6 +129,7 @@ public: [[nodiscard]] RowDescriptor resolveChatNext(RowDescriptor from = {}) const; [[nodiscard]] RowDescriptor resolveChatPrevious(RowDescriptor from = {}) const; void updateHasFocus(not_null focused); + void toggleFiltersMenu(bool value); // Float player interface. bool floatPlayerHandleWheelEvent(QEvent *e) override; @@ -151,10 +152,26 @@ protected: void paintEvent(QPaintEvent *e) override; private: + struct SearchProcessState { + base::flat_map cache; + base::flat_map queries; + + PeerData *lastPeer = nullptr; + MsgId lastId = 0; + int32 nextRate = 0; + mtpRequestId requestId = 0; + bool full = false; + }; + void chosenRow(const ChosenRow &row); void listScrollUpdated(); void searchCursorMoved(); void completeHashtag(QString tag); + void requestPublicPosts(bool fromStart); + void requestMessages(bool fromStart); + [[nodiscard]] not_null currentSearchProcess(); + + [[nodiscard]] bool computeSearchWithPostsPreview() const; [[nodiscard]] QString currentSearchQuery() const; [[nodiscard]] int currentSearchQueryCursorPosition() const; @@ -168,7 +185,8 @@ private: void searchReceived( SearchRequestType type, const MTPmessages_Messages &result, - mtpRequestId requestId); + not_null process, + bool cacheResults = false); void peerSearchReceived( const MTPcontacts_Found &result, mtpRequestId requestId); @@ -201,7 +219,7 @@ private: void showCalendar(); void showSearchFrom(); void showMainMenu(); - void clearSearchCache(); + void clearSearchCache(bool clearPosts); void setSearchQuery(const QString &query, int cursorPosition = -1); void updateControlsVisibility(bool fast = false); void updateLockUnlockVisibility( @@ -244,9 +262,11 @@ private: void searchFailed( SearchRequestType type, const MTP::Error &error, - mtpRequestId requestId); + not_null process); void peerSearchFailed(const MTP::Error &error, mtpRequestId requestId); - void searchApplyEmpty(SearchRequestType type, mtpRequestId id); + void searchApplyEmpty( + SearchRequestType type, + not_null process); void peerSearchApplyEmpty(mtpRequestId id); void updateForceDisplayWide(); @@ -276,7 +296,7 @@ private: bool _dragForward = false; base::Timer _chooseByDragTimer; - Layout _layout = Layout::Main; + const Layout _layout = Layout::Main; int _narrowWidth = 0; object_ptr _searchControls; object_ptr _subsectionTopBar = { nullptr }; @@ -298,6 +318,8 @@ private: std::unique_ptr _forumRequestsBar; std::unique_ptr _forumReportBar; + base::unique_qptr _chatFilters; + object_ptr _scroll; QPointer _inner; std::unique_ptr _suggestions; @@ -318,6 +340,7 @@ private: bool _scrollToTopIsShown = false; bool _forumSearchRequested = false; HashOrCashtag _searchHashOrCashtag = {}; + bool _searchWithPostsPreview = false; Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; @@ -358,19 +381,13 @@ private: PeerData *_searchQueryFrom = nullptr; std::vector _searchQueryTags; ChatSearchTab _searchQueryTab = {}; - int32 _searchNextRate = 0; - bool _searchFull = false; - bool _searchFullMigrated = false; - int _searchInHistoryRequest = 0; // Not real mtpRequestId. - mtpRequestId _searchRequest = 0; - PeerData *_lastSearchPeer = nullptr; - MsgId _lastSearchId = 0; - MsgId _lastSearchMigratedId = 0; + SearchProcessState _searchProcess; + SearchProcessState _migratedProcess; + SearchProcessState _postsProcess; + int _historiesRequest = 0; // Not real mtpRequestId. - base::flat_map _searchCache; Api::SingleMessageSearch _singleMessageSearch; - base::flat_map _searchQueries; base::flat_map _peerSearchCache; base::flat_map _peerSearchQueries; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index 13f52f944..43b5c5882 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -57,7 +57,6 @@ namespace { constexpr auto kCollapsedChannelsCount = 5; constexpr auto kProbablyMaxChannels = 1000; -constexpr auto kProbablyMaxRecommendations = 100; constexpr auto kCollapsedAppsCount = 5; constexpr auto kProbablyMaxApps = 100; @@ -1163,8 +1162,22 @@ void PopularAppsController::fill() { appendRow(bot); } } + const auto count = delegate()->peerListFullRowsCount(); + setCount(count); + if (count > 0) { + delegate()->peerListSetBelowWidget(object_ptr( + (QWidget*)nullptr, + object_ptr( + (QWidget*)nullptr, + tr::lng_bot_apps_which( + lt_link, + tr::lng_bot_apps_which_link( + ) | Ui::Text::ToLink(u"internal:about_popular_apps"_q), + Ui::Text::WithEntities), + st::dialogsPopularAppsAbout), + st::dialogsPopularAppsPadding)); + } delegate()->peerListRefreshRows(); - setCount(delegate()->peerListFullRowsCount()); } void PopularAppsController::appendRow(not_null bot) { @@ -2326,4 +2339,21 @@ object_ptr StarsExamplesBox( return Box(std::move(controller), std::move(initBox)); } +object_ptr PopularAppsAboutBox( + not_null window) { + return Ui::MakeInformBox({ + .text = tr::lng_popular_apps_info_text( + lt_bot, + rpl::single(Ui::Text::Link( + u"@botfather"_q, + u"https://t.me/botfather"_q)), + lt_link, + tr::lng_popular_apps_info_here( + ) | Ui::Text::ToLink(tr::lng_popular_apps_info_url(tr::now)), + Ui::Text::RichLangValue), + .confirmText = tr::lng_popular_apps_info_confirm(), + .title = tr::lng_popular_apps_info_title(), + }); +} + } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index be868b77a..32998519f 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -219,4 +219,7 @@ private: [[nodiscard]] object_ptr StarsExamplesBox( not_null window); +[[nodiscard]] object_ptr PopularAppsAboutBox( + not_null window); + } // namespace Dialogs diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index b0c6d7fcc..4fc78bad5 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -438,8 +438,6 @@ void ApiWrap::startExport( } if (_settings->types & Settings::Type::AnyChatsMask) { _startProcess->steps.push_back(Step::SplitRanges); - } - if (_settings->types & Settings::Type::AnyChatsMask) { _startProcess->steps.push_back(Step::DialogsCount); } if (_settings->types & Settings::Type::GroupsChannelsMask) { diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 0f7083df3..8fd0f3e97 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer( return {}; } result->pb = io.get(); + result->flags |= AVFMT_FLAG_CUSTOM_IO; auto options = (AVDictionary*)nullptr; const auto guard = gsl::finally([&] { av_dict_free(&options); }); av_dict_set(&options, "usetoc", "1", 0); + const auto error = AvErrorWrap(avformat_open_input( &result, nullptr, @@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer( return FormatPointer(result); } +FormatPointer MakeWriteFormatPointer( + void *opaque, + int(*read)(void *opaque, uint8_t *buffer, int bufferSize), +#if DA_FFMPEG_CONST_WRITE_CALLBACK + int(*write)(void *opaque, const uint8_t *buffer, int bufferSize), +#else + int(*write)(void *opaque, uint8_t *buffer, int bufferSize), +#endif + int64_t(*seek)(void *opaque, int64_t offset, int whence), + const QByteArray &format) { + const AVOutputFormat *found = nullptr; + void *i = nullptr; + while ((found = av_muxer_iterate(&i))) { + if (found->name == format) { + break; + } + } + if (!found) { + LogError( + "av_muxer_iterate", + u"Format %1 not found"_q.arg(QString::fromUtf8(format))); + return {}; + } + + auto io = MakeIOPointer(opaque, read, write, seek); + if (!io) { + return {}; + } + io->seekable = (seek != nullptr); + + auto result = (AVFormatContext*)nullptr; + auto error = AvErrorWrap(avformat_alloc_output_context2( + &result, + (AVOutputFormat*)found, + nullptr, + nullptr)); + if (!result || error) { + LogError("avformat_alloc_output_context2", error); + return {}; + } + result->pb = io.get(); + result->flags |= AVFMT_FLAG_CUSTOM_IO; + + // Now FormatPointer will own and free the IO context. + io.release(); + return FormatPointer(result); +} + void FormatDeleter::operator()(AVFormatContext *value) { if (value) { const auto deleter = IOPointer(value->pb); @@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer( existing); } +void SwresampleDeleter::operator()(SwrContext *value) { + if (value) { + swr_free(&value); + } +} + +SwresamplePointer MakeSwresamplePointer( +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *srcLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t srcLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat srcFormat, + int srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *dstLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t dstLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat dstFormat, + int dstRate, + SwresamplePointer *existing) { + // We have to use custom caching for SwsContext, because + // sws_getCachedContext checks passed flags with existing context flags, + // and re-creates context if they're different, but in the process of + // context creation the passed flags are modified before being written + // to the resulting context, so the caching doesn't work. + if (existing && (*existing) != nullptr) { + const auto &deleter = existing->get_deleter(); + if (true +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + && srcLayout->nb_channels == deleter.srcChannels + && dstLayout->nb_channels == deleter.dstChannels +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + && (av_get_channel_layout_nb_channels(srcLayout) + == deleter.srcChannels) + && (av_get_channel_layout_nb_channels(dstLayout) + == deleter.dstChannels) +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + && srcFormat == deleter.srcFormat + && dstFormat == deleter.dstFormat + && srcRate == deleter.srcRate + && dstRate == deleter.dstRate) { + return std::move(*existing); + } + } + + // Initialize audio resampler +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + auto result = (SwrContext*)nullptr; + auto error = AvErrorWrap(swr_alloc_set_opts2( + &result, + dstLayout, + dstFormat, + dstRate, + srcLayout, + srcFormat, + srcRate, + 0, + nullptr)); + if (error || !result) { + LogError(u"swr_alloc_set_opts2"_q, error); + return SwresamplePointer(); + } +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + auto result = swr_alloc_set_opts( + existing ? existing.get() : nullptr, + dstLayout, + dstFormat, + dstRate, + srcLayout, + srcFormat, + srcRate, + 0, + nullptr); + if (!result) { + LogError(u"swr_alloc_set_opts"_q); + } +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + + error = AvErrorWrap(swr_init(result)); + if (error) { + LogError(u"swr_init"_q, error); + swr_free(&result); + return SwresamplePointer(); + } + + return SwresamplePointer( + result, + { + srcFormat, + srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + srcLayout->nb_channels, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + av_get_channel_layout_nb_channels(srcLayout), +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + dstFormat, + dstRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + dstLayout->nb_channels, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + av_get_channel_layout_nb_channels(dstLayout), +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + }); +} + void SwscaleDeleter::operator()(SwsContext *value) { if (value) { sws_freeContext(value); } } -void LogError(const QString &method) { - LOG(("Streaming Error: Error in %1.").arg(method)); +void LogError(const QString &method, const QString &details) { + LOG(("Streaming Error: Error in %1%2." + ).arg(method + ).arg(details.isEmpty() ? QString() : " - " + details)); } -void LogError(const QString &method, AvErrorWrap error) { - LOG(("Streaming Error: Error in %1 (code: %2, text: %3)." +void LogError( + const QString &method, + AvErrorWrap error, + const QString &details) { + LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4." ).arg(method ).arg(error.code() - ).arg(error.text())); + ).arg(error.text() + ).arg(details.isEmpty() ? QString() : " - " + details)); } crl::time PtsToTime(int64_t pts, AVRational timeBase) { diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h index d96daa9c7..6397cbb92 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h @@ -19,6 +19,8 @@ extern "C" { #include #include #include +#include +#include #include } // extern "C" @@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr; int(*write)(void *opaque, uint8_t *buffer, int bufferSize), #endif int64_t(*seek)(void *opaque, int64_t offset, int whence)); +[[nodiscard]] FormatPointer MakeWriteFormatPointer( + void *opaque, + int(*read)(void *opaque, uint8_t *buffer, int bufferSize), +#if DA_FFMPEG_CONST_WRITE_CALLBACK + int(*write)(void *opaque, const uint8_t *buffer, int bufferSize), +#else + int(*write)(void *opaque, uint8_t *buffer, int bufferSize), +#endif + int64_t(*seek)(void *opaque, int64_t offset, int whence), + const QByteArray &format); struct CodecDeleter { void operator()(AVCodecContext *value); @@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr; QSize resize, SwscalePointer *existing = nullptr); -void LogError(const QString &method); -void LogError(const QString &method, FFmpeg::AvErrorWrap error); +struct SwresampleDeleter { + AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE; + int srcRate = 0; + int srcChannels = 0; + AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE; + int dstRate = 0; + int dstChannels = 0; + + void operator()(SwrContext *value); +}; +using SwresamplePointer = std::unique_ptr; +[[nodiscard]] SwresamplePointer MakeSwresamplePointer( +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *srcLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t srcLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat srcFormat, + int srcRate, +#if DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVChannelLayout *dstLayout, +#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT + uint64_t dstLayout, +#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT + AVSampleFormat dstFormat, + int dstRate, + SwresamplePointer *existing = nullptr); + +void LogError(const QString &method, const QString &details = {}); +void LogError( + const QString &method, + FFmpeg::AvErrorWrap error, + const QString &details = {}); [[nodiscard]] const AVCodec *FindDecoder(not_null context); [[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase); 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 90203afe2..e6bb0716d 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -273,13 +273,13 @@ InnerWidget::InnerWidget( _scrollDateHideTimer.setCallback([=] { scrollDateHideByTimer(); }); session().data().viewRepaintRequest( ) | rpl::start_with_next([=](auto view) { - if (view->delegate() == this) { + if (myView(view)) { repaintItem(view); } }, lifetime()); session().data().viewResizeRequest( ) | rpl::start_with_next([=](auto view) { - if (view->delegate() == this) { + if (myView(view)) { resizeItem(view); } }, lifetime()); @@ -291,7 +291,7 @@ InnerWidget::InnerWidget( }, lifetime()); session().data().viewLayoutChanged( ) | rpl::start_with_next([=](auto view) { - if (view->delegate() == this) { + if (myView(view)) { if (view->isUnderCursor()) { updateSelected(); } @@ -333,6 +333,10 @@ InnerWidget::InnerWidget( [=] { requestAdmins(); })); } +bool InnerWidget::myView(not_null view) const { + return !_items.empty() && (view->delegate().get() == this); +} + Main::Session &InnerWidget::session() const { return _controller->session(); } @@ -662,7 +666,8 @@ bool InnerWidget::elementUnderCursor( return (Element::Hovered() == view); } -HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode() { +HistoryView::SelectionModeResult InnerWidget::elementInSelectionMode( + const HistoryView::Element *) { return {}; } @@ -790,11 +795,16 @@ void InnerWidget::saveState(not_null memento) { } void InnerWidget::restoreState(not_null memento) { - _items = memento->takeItems(); - for (auto &item : _items) { + // OwnedItem::refreshView may call requestItemResize. + // So we postpone resizing until all views are created. + _items.clear(); + auto items = memento->takeItems(); + for (auto &item : items) { item.refreshView(this); _itemsByData.emplace(item->data(), item.get()); } + _items = std::move(items); + _eventIds = memento->takeEventIds(); _admins = memento->takeAdmins(); _adminsCanEdit = memento->takeAdminsCanEdit(); 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 db52cac1c..209cc0384 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -94,7 +94,8 @@ public: HistoryView::Context elementContext() override; bool elementUnderCursor( not_null view) override; - HistoryView::SelectionModeResult elementInSelectionMode() override; + HistoryView::SelectionModeResult elementInSelectionMode( + const HistoryView::Element *view) override; bool elementIntersectsRange( not_null view, int from, @@ -229,8 +230,12 @@ private: void paintEmpty(Painter &p, not_null st); void clearAfterFilterChange(); void clearAndRequestLog(); - void addEvents(Direction direction, const QVector &events); - Element *viewForItem(const HistoryItem *item); + void addEvents( + Direction direction, + const QVector &events); + [[nodiscard]] Element *viewForItem(const HistoryItem *item); + [[nodiscard]] bool myView( + not_null view) const; void toggleScrollDateShown(); void repaintScrollDateCallback(); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 619ef8e40..a61bc6aab 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -720,6 +720,14 @@ not_null History::addNewLocalMessage( true); } +not_null History::addNewLocalMessage( + not_null item) { + Expects(item->history() == this); + Expects(item->isLocal()); + + return addNewItem(item, true); +} + not_null History::addSponsoredMessage( MsgId id, Data::SponsoredFrom from, diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 73695785f..7685e0ba4 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -180,6 +180,7 @@ public: not_null addNewLocalMessage( HistoryItemCommonFields &&fields, not_null game); + not_null addNewLocalMessage(not_null item); not_null addSponsoredMessage( MsgId id, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index abf19c62e..5a1d7e731 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -10,13 +10,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/stickers_emoji_pack.h" #include "core/file_utilities.h" #include "core/click_handler_types.h" +#include "core/phone_click_handler.h" #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" -#include "api/api_report.h" #include "history/view/controls/history_view_draft_options.h" #include "boxes/moderate_messages_box.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" +#include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/history_view_about_view.h" @@ -36,12 +37,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/reaction_fly_animation.h" -#include "ui/text/text_options.h" #include "ui/text/text_isolated_emoji.h" -#include "ui/boxes/confirm_box.h" #include "ui/boxes/edit_factcheck_box.h" #include "ui/boxes/report_box_graphics.h" -#include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" #include "ui/inactive_press.h" #include "ui/painter.h" @@ -57,7 +55,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "boxes/report_messages_box.h" #include "boxes/sticker_set_box.h" -#include "boxes/premium_preview_box.h" #include "boxes/translate_box.h" #include "chat_helpers/message_field.h" #include "chat_helpers/emoji_interactions.h" @@ -72,6 +69,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "mainwidget.h" #include "menu/menu_item_download_files.h" +#include "menu/menu_sponsored.h" #include "core/application.h" #include "apiwrap.h" #include "api/api_attached_stickers.h" @@ -145,60 +143,6 @@ int BinarySearchBlocksOrItems(const T &list, int edge) { && (!peer->isChannel() || peer->asChannel()->amIn())); } -void FillSponsoredMessagesMenu( - not_null controller, - FullMsgId itemId, - not_null menu) { - const auto &data = controller->session().sponsoredMessages(); - const auto info = data.lookupDetails(itemId).info; - const auto show = controller->uiShow(); - if (!info.empty()) { - auto fillSubmenu = [&](not_null menu) { - const auto allText = ranges::accumulate( - info, - TextWithEntities(), - [](TextWithEntities a, TextWithEntities b) { - return a.text.isEmpty() ? b : a.append('\n').append(b); - }).text; - const auto callback = [=] { - QGuiApplication::clipboard()->setText(allText); - show->showToast(tr::lng_text_copied(tr::now)); - }; - for (const auto &i : info) { - auto item = base::make_unique_q( - menu, - st::defaultMenu, - st::historySponsorInfoItem, - st::historyHasCustomEmojiPosition, - base::duplicate(i)); - item->clicks( - ) | rpl::start_with_next(callback, menu->lifetime()); - menu->addAction(std::move(item)); - if (i != info.back()) { - menu->addSeparator(); - } - } - }; - using namespace Ui::Menu; - CreateAddActionCallback(menu)(MenuCallback::Args{ - .text = tr::lng_sponsored_info_menu(tr::now), - .handler = nullptr, - .icon = &st::menuIconChannel, - .fillSubmenu = std::move(fillSubmenu), - }); - menu->addSeparator(&st::expandedMenuSeparator); - } - menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { - if (controller->session().premium()) { - using Result = Data::SponsoredReportResult; - controller->session().sponsoredMessages().createReportCallback( - itemId)(Result::Id("-1"), [](const auto &) {}); - } else { - ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); - } - }, &st::menuIconCancel); -} - } // namespace // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html @@ -221,7 +165,11 @@ public: not_null view) override { return (Element::Moused() == view); } - HistoryView::SelectionModeResult elementInSelectionMode() override { + HistoryView::SelectionModeResult elementInSelectionMode( + const Element *view) override { + if (view && view->data()->isSponsored()) { + return HistoryView::SelectionModeResult(); + } return _widget ? _widget->inSelectionMode() : HistoryView::SelectionModeResult(); @@ -2314,30 +2262,48 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } return item; }; - const auto whoReactedItem = groupLeaderOrSelf(_dragStateItem); - const auto hasWhoReactedItem = whoReactedItem - && Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::All); - const auto clickedReaction = link - ? link->property( - kReactionsCountEmojiProperty).value() - : Data::ReactionId(); + const auto leaderOrSelf = groupLeaderOrSelf(_dragStateItem); + const auto hasWhoReactedItem = leaderOrSelf + && Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::All); + using namespace HistoryView::Reactions; + const auto clickedReaction = ReactionIdOfLink(link); + const auto linkPhoneNumber = link + ? link->property(kPhoneNumberLinkProperty).toString() + : QString(); + const auto linkUserpicPeerId = (link && _dragStateUserpic) + ? link->property(kPeerLinkPeerIdProperty).toULongLong() + : 0; + const auto session = &this->session(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() - && whoReactedItem - && Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::One)) { + && leaderOrSelf + && Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::One)) { HistoryView::ShowWhoReactedMenu( &_menu, e->globalPos(), this, - whoReactedItem, + leaderOrSelf, clickedReaction, _controller, _whoReactedMenuLifetime); e->accept(); return; + } else if (!linkPhoneNumber.isEmpty()) { + PhoneClickHandler(session, linkPhoneNumber).onClick( + prepareClickContext( + Qt::LeftButton, + _dragStateItem ? _dragStateItem->fullId() : FullMsgId())); + return; } _menu = base::make_unique_q(this, st::popupMenuWithIcons); - const auto session = &this->session(); + if (linkUserpicPeerId) { + _widget->fillSenderUserpicMenu( + _menu.get(), + session->data().peer(PeerId(linkUserpicPeerId))); + _menu->popup(e->globalPos()); + e->accept(); + return; + } const auto controller = _controller; const auto addItemActions = [&]( HistoryItem *item, @@ -2536,6 +2502,14 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (const auto sticker = emojiStickers->stickerForEmoji( isolated)) { addDocumentActions(sticker.document, item); + } else if (v::is(isolated.items.front()) + && v::is_null(isolated.items[1])) { + const auto id = v::get(isolated.items.front()); + const auto docId = id.toULongLong(); + const auto document = session->data().document(docId); + if (document->sticker()) { + addDocumentActions(document, item); + } } } } @@ -2820,8 +2794,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { ? link->copyToClipboardContextItemText() : QString(); - if (item && item->isSponsored()) { - FillSponsoredMessagesMenu(controller, item->fullId(), _menu); + const auto sponsored = (item && item->isSponsored()) + ? item + : (Element::Moused() && Element::Moused()->data()->isSponsored()) + ? Element::Moused()->data().get() + : nullptr; + if (sponsored) { + Menu::FillSponsored( + this, + Ui::Menu::CreateAddActionCallback(_menu), + controller->uiShow(), + sponsored->fullId(), + false); } if (isUponSelected > 0) { addReplyAction(item); @@ -2929,22 +2913,30 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { HistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History); }, &st::menuIconLink); } - if (item && item->isSponsored()) { - if (!_menu->empty()) { - _menu->addSeparator(&st::expandedMenuSeparator); + if (sponsored) { + const auto hasAbout = ranges::any_of( + _menu->actions(), + [about = tr::lng_sponsored_menu_revenued_about(tr::now)]( + const QAction *action) { + return action->text() == about; + }); + if (!hasAbout) { + if (!_menu->empty()) { + _menu->addSeparator(&st::expandedMenuSeparator); + } + auto item = base::make_unique_q( + _menu, + st::menuWithIcons, + st::historyHasCustomEmoji, + st::historySponsoredAboutMenuLabelPosition, + TextWithEntities{ tr::lng_sponsored_title(tr::now) }, + &st::menuIconInfo); + item->clicks( + ) | rpl::start_with_next([=] { + controller->show(Box(Ui::AboutSponsoredBox)); + }, item->lifetime()); + _menu->addAction(std::move(item)); } - auto item = base::make_unique_q( - _menu, - st::menuWithIcons, - st::historyHasCustomEmoji, - st::historySponsoredAboutMenuLabelPosition, - TextWithEntities{ tr::lng_sponsored_title(tr::now) }, - &st::menuIconInfo); - item->clicks( - ) | rpl::start_with_next([=] { - controller->show(Box(Ui::AboutSponsoredBox)); - }, item->lifetime()); - _menu->addAction(std::move(item)); } if (isUponSelected > 1) { if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { @@ -3011,9 +3003,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { HistoryView::EmojiPacksSource::Message, _controller); /*const auto added = (_menu->actions().size() > wasAmount); - if (!added) { - _menu->addSeparator(); - } HistoryView::AddSelectRestrictionAction( _menu, textItem ? textItem : _dragStateItem, @@ -3023,8 +3012,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { HistoryView::AddWhoReactedAction( _menu, this, - whoReactedItem, + leaderOrSelf, _controller); + } else if (leaderOrSelf) { + HistoryView::MaybeAddWhenEditedAction(_menu, leaderOrSelf); } if (_menu->empty()) { @@ -4039,6 +4030,7 @@ void HistoryInner::mouseActionUpdate() { TextState dragState; ClickHandlerHost *lnkhost = nullptr; + auto dragStateUserpic = false; auto selectingText = (item == _mouseActionItem) && (view == Element::Hovered()) && !_selected.empty() @@ -4134,6 +4126,7 @@ void HistoryInner::mouseActionUpdate() { // stop enumeration if we've found a userpic under the cursor if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) { dragState = TextState(nullptr, view->fromPhotoLink()); + dragStateUserpic = true; _dragStateItem = nullptr; lnkhost = view; return false; @@ -4145,6 +4138,7 @@ void HistoryInner::mouseActionUpdate() { } } auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); + _dragStateUserpic = dragStateUserpic; if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } @@ -4769,6 +4763,11 @@ QString HistoryInner::tooltipText() const { } } } else if (const auto lnk = ClickHandler::getActive()) { + using namespace HistoryView::Reactions; + const auto count = ReactionCountOfLink(_dragStateItem, lnk); + if (count.count && count.shortened) { + return Lang::FormatCountDecimal(count.count); + } return lnk->tooltip(); } else if (const auto view = Element::Moused()) { StateRequest request; diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 1b7803b24..818b5435b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -499,6 +499,7 @@ private: HistoryItem *_dragStateItem = nullptr; CursorState _mouseCursorState = CursorState(); uint16 _mouseTextSymbol = 0; + bool _dragStateUserpic = false; bool _pressWasInactive = false; bool _recountedAfterPendingResizedItems = false; bool _useCornerReaction = false; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ba784e263..833e66e8b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -304,10 +304,12 @@ std::unique_ptr HistoryItem::CreateMedia( return nullptr; } return document->match([&](const MTPDdocument &document) -> Result { + const auto list = media.valt_documents(); return std::make_unique( item, - item->history()->owner().processDocument(document), + item->history()->owner().processDocument(document, list), media.is_nopremium(), + list && !list->v.isEmpty(), media.is_spoiler(), media.vttl_seconds().value_or_empty()); }, [](const MTPDdocumentEmpty &) -> Result { @@ -666,11 +668,13 @@ HistoryItem::HistoryItem( createComponentsHelper(std::move(fields)); const auto skipPremiumEffect = !history->session().premium(); + const auto video = document->video(); const auto spoiler = false; _media = std::make_unique( this, document, skipPremiumEffect, + video && !video->qualities.empty(), spoiler, /*ttlSeconds = */0); setText(caption); @@ -799,6 +803,10 @@ TimeId HistoryItem::date() const { return _date; } +bool HistoryItem::awaitingVideoProcessing() const { + return (_flags & MessageFlag::EstimatedDate); +} + HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { if (const auto pinned = Get()) { return pinned; @@ -1525,12 +1533,10 @@ void HistoryItem::returnSavedMedia() { } void HistoryItem::savePreviousMedia() { - Expects(_media != nullptr); - AddComponents(HistoryMessageSavedMediaData::Bit()); const auto data = Get(); data->text = originalText(); - data->media = _media->clone(this); + data->media = _media ? _media->clone(this) : nullptr; } bool HistoryItem::isEditingMedia() const { @@ -1840,6 +1846,7 @@ void HistoryItem::setStoryFields(not_null story) { this, document, /*skipPremiumEffect=*/false, + /*hasQualitiesList=*/false, spoiler, /*ttlSeconds = */0); } @@ -2267,6 +2274,10 @@ bool HistoryItem::allowsSendNow() const { && !isEditingMedia(); } +bool HistoryItem::allowsReschedule() const { + return allowsSendNow() && !awaitingVideoProcessing(); +} + bool HistoryItem::allowsForward() const { return !isService() && isRegular() @@ -2294,6 +2305,11 @@ bool HistoryItem::allowsEdit(TimeId now) const { && !isEditingMedia(); } +bool HistoryItem::allowsEditMedia() const { + return !awaitingVideoProcessing() + && (!_media || _media->allowsEditMedia()); +} + bool HistoryItem::canBeEdited() const { if (_deleted) { return false; @@ -5657,7 +5673,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { data.vmessage()->data().ventities().v), } : TextWithEntities()), - .convertStars = int(data.vconvert_stars().v), + .starsConverted = int(data.vconvert_stars().value_or_empty()), .limitedCount = gift.vavailability_total().value_or_empty(), .limitedLeft = gift.vavailability_remains().value_or_empty(), .count = int(gift.vstars().v), diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 0667c44bf..01b1d203d 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -432,8 +432,10 @@ public: [[nodiscard]] bool forbidsForward() const; [[nodiscard]] bool forbidsSaving() const; [[nodiscard]] bool allowsSendNow() const; + [[nodiscard]] bool allowsReschedule() const; [[nodiscard]] bool allowsForward() const; [[nodiscard]] bool allowsEdit(TimeId now) const; + [[nodiscard]] bool allowsEditMedia() const; [[nodiscard]] bool canDelete() const; [[nodiscard]] bool canDeleteForEveryone(TimeId now) const; [[nodiscard]] bool suggestReport() const; @@ -484,6 +486,7 @@ public: [[nodiscard]] GlobalMsgId globalId() const; [[nodiscard]] Data::MessagePosition position() const; [[nodiscard]] TimeId date() const; + [[nodiscard]] bool awaitingVideoProcessing() const; [[nodiscard]] Data::Media *media() const { return _media.get(); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index ffb52c006..ded4b07e3 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -673,6 +673,14 @@ QString ReplyMarkupClickHandler::buttonText() const { } QString ReplyMarkupClickHandler::tooltip() const { + if (const auto button = getButton()) { + if (button->type == HistoryMessageMarkupButton::Type::CopyText) { + return tr::lng_bot_copy_text_tooltip( + tr::now, + lt_text, + st::wrap_rtl(QString::fromUtf8(button->data))); + } + } const auto button = getUrlButton(); const auto url = button ? QString::fromUtf8(button->data) : QString(); const auto text = _fullDisplayed ? QString() : buttonText(); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index fa79b218b..c6183ccdc 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -418,11 +418,21 @@ ClickHandlerPtr ReportSponsoredClickHandler(not_null item) { Menu::ShowSponsored( controller->widget(), controller->uiShow(), - item); + item->fullId()); } }); } +ClickHandlerPtr AboutSponsoredClickHandler() { + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + Menu::ShowSponsoredAbout(controller->uiShow(), my.itemId); + } + }); +} + + MessageFlags FlagsFromMTP( MsgId id, MTPDmessage::Flags flags, @@ -451,7 +461,10 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_views) ? Flag::HasViews : Flag()) // AyuGram: removed // | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag()) - | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()); + | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()) + | ((flags & MTP::f_video_processing_pending) + ? Flag::EstimatedDate + : Flag()); } MessageFlags FlagsFromMTP( diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index dd5121dbe..ba5bd2cda 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -148,6 +148,7 @@ ClickHandlerPtr JumpToStoryClickHandler( [[nodiscard]] ClickHandlerPtr HideSponsoredClickHandler(); [[nodiscard]] ClickHandlerPtr ReportSponsoredClickHandler( not_null item); +[[nodiscard]] ClickHandlerPtr AboutSponsoredClickHandler(); [[nodiscard]] not_null GenerateJoinedMessage( not_null history, diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 5cd532ad5..319a77238 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -14,28 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { -[[nodiscard]] InlineBots::PeerTypes PeerTypesFromMTP( - const MTPvector &types) { - using namespace InlineBots; - auto result = PeerTypes(0); - for (const auto &type : types.v) { - result |= type.match([&](const MTPDinlineQueryPeerTypePM &data) { - return PeerType::User; - }, [&](const MTPDinlineQueryPeerTypeChat &data) { - return PeerType::Group; - }, [&](const MTPDinlineQueryPeerTypeMegagroup &data) { - return PeerType::Group; - }, [&](const MTPDinlineQueryPeerTypeBroadcast &data) { - return PeerType::Broadcast; - }, [&](const MTPDinlineQueryPeerTypeBotPM &data) { - return PeerType::Bot; - }, [&](const MTPDinlineQueryPeerTypeSameBotPM &data) { - return PeerType(); - }); - } - return result; -} - [[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( const MTPDkeyboardButtonRequestPeer &query) { using Type = RequestPeerQuery::Type; @@ -76,6 +54,28 @@ namespace { } // namespace +InlineBots::PeerTypes PeerTypesFromMTP( + const MTPvector &types) { + using namespace InlineBots; + auto result = PeerTypes(0); + for (const auto &type : types.v) { + result |= type.match([&](const MTPDinlineQueryPeerTypePM &data) { + return PeerType::User; + }, [&](const MTPDinlineQueryPeerTypeChat &data) { + return PeerType::Group; + }, [&](const MTPDinlineQueryPeerTypeMegagroup &data) { + return PeerType::Group; + }, [&](const MTPDinlineQueryPeerTypeBroadcast &data) { + return PeerType::Broadcast; + }, [&](const MTPDinlineQueryPeerTypeBotPM &data) { + return PeerType::Bot; + }, [&](const MTPDinlineQueryPeerTypeSameBotPM &data) { + return PeerType(); + }); + } + return result; +} + HistoryMessageMarkupButton::HistoryMessageMarkupButton( Type type, const QString &text, diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 8d1d4e937..77895c3aa 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -19,6 +19,9 @@ enum class PeerType : uint8; using PeerTypes = base::flags; } // namespace InlineBots +[[nodiscard]] InlineBots::PeerTypes PeerTypesFromMTP( + const MTPvector &types); + enum class ReplyMarkupFlag : uint32 { None = (1U << 0), ForceReply = (1U << 1), diff --git a/Telegram/SourceFiles/history/history_view_swipe.cpp b/Telegram/SourceFiles/history/history_view_swipe.cpp index 214310fea..cbe9d65a1 100644 --- a/Telegram/SourceFiles/history/history_view_swipe.cpp +++ b/Telegram/SourceFiles/history/history_view_swipe.cpp @@ -198,7 +198,8 @@ void SetupSwipeHandler( const auto &touches = t->touchPoints(); const auto released = [&](int index) { return (touches.size() > index) - && (touches.at(index).state() & Qt::TouchPointReleased); + && (int(touches.at(index).state()) + & int(Qt::TouchPointReleased)); }; const auto cancel = released(0) || released(1) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0bb2b3908..4bad170f3 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "ui/chat/attach/attach_prepare.h" #include "ui/chat/choose_theme_controller.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/inner_dropdown.h" #include "ui/widgets/dropdown_menu.h" @@ -150,6 +151,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/session/send_as_peers.h" +#include "webrtc/webrtc_environment.h" #include "window/notifications_manager.h" #include "window/window_adaptive.h" #include "window/window_controller.h" @@ -451,8 +453,16 @@ HistoryWidget::HistoryWidget( initTabbedSelector(); _attachToggle->setClickedCallback([=] { + const auto toggle = _attachBotsMenu && _attachBotsMenu->isHidden(); base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] { - chooseAttach(); + if (_attachBotsMenu && toggle) { + _attachBotsMenu->showAnimated(); + } else { + chooseAttach(); + if (_attachBotsMenu) { + _attachBotsMenu->hideAnimated(); + } + } }); }); @@ -592,6 +602,14 @@ HistoryWidget::HistoryWidget( Window::ActivateWindow(controller); }); + Core::App().mediaDevices().recordAvailabilityValue( + ) | rpl::start_with_next([=](Webrtc::RecordAvailability value) { + _recordAvailability = value; + if (_list) { + updateSendButtonType(); + } + }, lifetime()); + session().data().newItemAdded( ) | rpl::start_with_next([=](not_null item) { newItemAdded(item); @@ -781,6 +799,35 @@ HistoryWidget::HistoryWidget( maybeMarkReactionsRead(update.item); }, lifetime()); + session().data().sentToScheduled( + ) | rpl::start_with_next([=](const Data::SentToScheduled &value) { + const auto history = value.history; + if (history == _history) { + const auto id = value.scheduledId; + crl::on_main(this, [=] { + if (history == _history) { + controller->showSection( + std::make_shared( + history, + id)); + } + }); + return; + } + }, lifetime()); + + session().data().sentFromScheduled( + ) | rpl::start_with_next([=](const Data::SentFromScheduled &value) { + if (value.item->awaitingVideoProcessing() + && !_sentFromScheduledTip + && HistoryView::ShowScheduledVideoPublished( + controller, + value, + crl::guard(this, [=] { _sentFromScheduledTip = false; }))) { + _sentFromScheduledTip = true; + } + }, lifetime()); + using MediaSwitch = Media::Player::Instance::Switch; Media::Player::instance()->switchToNextEvents( ) | rpl::filter([=](const MediaSwitch &pair) { @@ -889,7 +936,7 @@ HistoryWidget::HistoryWidget( } if (flags & PeerUpdateFlag::FullInfo) { fullInfoUpdated(); - if (const auto channel = _peer ? _peer->asChannel() : nullptr) { + if (const auto channel = _peer->asChannel()) { if (channel->allowedReactions().paidEnabled) { session().credits().load(); } @@ -1116,6 +1163,7 @@ void HistoryWidget::initVoiceRecordBar() { data.bytes, data.waveform, data.duration, + data.video, action); _voiceRecordBar->clearListenState(); }, lifetime()); @@ -1129,6 +1177,24 @@ void HistoryWidget::initVoiceRecordBar() { _cornerButtons.updateUnreadThingsVisibility(); }, lifetime()); + _voiceRecordBar->errors( + ) | rpl::start_with_next([=](::Media::Capture::Error error) { + using Error = ::Media::Capture::Error; + switch (error) { + case Error::AudioInit: + case Error::AudioTimeout: + controller()->showToast(tr::lng_record_audio_problem(tr::now)); + break; + case Error::VideoInit: + case Error::VideoTimeout: + controller()->showToast(tr::lng_record_video_problem(tr::now)); + break; + default: + controller()->showToast(u"Unknown error."_q); + break; + } + }, lifetime()); + _voiceRecordBar->updateSendButtonTypeRequests( ) | rpl::start_with_next([=] { updateSendButtonType(); @@ -1141,7 +1207,20 @@ void HistoryWidget::initVoiceRecordBar() { _voiceRecordBar->recordingTipRequests( ) | rpl::start_with_next([=] { - controller()->showToast(tr::lng_record_hold_tip(tr::now)); + Core::App().settings().setRecordVideoMessages( + !Core::App().settings().recordVideoMessages()); + updateSendButtonType(); + switch (_send->type()) { + case Ui::SendButton::Type::Record: { + const auto can = Webrtc::RecordAvailability::VideoAndAudio; + controller()->showToast((_recordAvailability == can) + ? tr::lng_record_voice_tip(tr::now) + : tr::lng_record_hold_tip(tr::now)); + } break; + case Ui::SendButton::Type::Round: + controller()->showToast(tr::lng_record_video_tip(tr::now)); + break; + } }, lifetime()); _voiceRecordBar->recordingStateChanges( @@ -1655,6 +1734,9 @@ void HistoryWidget::orderWidgets() { if (_translateBar) { _translateBar->raise(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->raise(); + } if (_pinnedBar) { _pinnedBar->raise(); } @@ -2185,6 +2267,7 @@ void HistoryWidget::showHistory( MsgId showAtMsgId, const TextWithEntities &highlightPart, int highlightPartOffsetHint) { + _pinnedClickedId = FullMsgId(); _minPinnedId = std::nullopt; _showAtMsgHighlightPart = {}; @@ -2334,6 +2417,7 @@ void HistoryWidget::showHistory( _history->showAtMsgId = _showAtMsgId; destroyUnreadBarOnClose(); + _sponsoredMessageBar = nullptr; _pinnedBar = nullptr; _translateBar = nullptr; _pinnedTracker = nullptr; @@ -2362,7 +2446,7 @@ void HistoryWidget::showHistory( _processingReplyItem = _replyEditMsg = nullptr; _processingReplyTo = _replyTo = FullReplyTo(); _editMsgId = MsgId(); - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; updateReplaceMediaButton(); _fieldBarCancel->hide(); @@ -2379,6 +2463,8 @@ void HistoryWidget::showHistory( _contactStatus = nullptr; _businessBotStatus = nullptr; + Core::App().mediaDevices().refreshRecordAvailability(); + if (peerId) { using namespace HistoryView; _peer = session().data().peer(peerId); @@ -2553,12 +2639,9 @@ void HistoryWidget::showHistory( unreadCountUpdated(); // set _historyDown badge. showAboutTopPromotion(); - { + if (!session().sponsoredMessages().isTopBarFor(_history)) { _scroll->setTrackingContent(false); - const auto checkState = crl::guard(this, [=, history = _history] { - if (history != _history) { - return; - } + const auto checkState = [=] { using State = Data::SponsoredMessages::State; const auto state = session().sponsoredMessages().state( _history); @@ -2568,10 +2651,20 @@ void HistoryWidget::showHistory( session().sponsoredMessages().canHaveFor(_history)); } else if (state == State::InjectToMiddle) { injectSponsoredMessages(); + } else if (state == State::AppendToTopBar) { } - }); - session().sponsoredMessages().request(_history, checkState); + }; + const auto history = _history; + session().sponsoredMessages().request( + _history, + crl::guard(this, [=, this] { + if (history == _history) { + checkState(); + } + })); checkState(); + } else { + requestSponsoredMessageBar(); } } else { _chooseForReport = nullptr; @@ -2611,7 +2704,7 @@ void HistoryWidget::setHistory(History *history) { if (was && !now) { _attachToggle->removeEventFilter(_attachBotsMenu.get()); _attachBotsMenu->hideFast(); - } else if (now && !was) { + } else if (now && !was && !ChatHelpers::ShowPanelOnClick()) { _attachToggle->installEventFilter(_attachBotsMenu.get()); } @@ -2755,7 +2848,7 @@ void HistoryWidget::setEditMsgId(MsgId msgId) { _editMsgId = msgId; if (!msgId) { _mediaEditManager.cancel(); - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; if (_preview) { _preview->setDisabled(false); } @@ -2811,14 +2904,16 @@ void HistoryWidget::clearAllLoadRequests() { } bool HistoryWidget::updateReplaceMediaButton() { - if (!_canReplaceMedia) { + if (!_canReplaceMedia && !_canAddMedia) { const auto result = (_replaceMedia != nullptr); _replaceMedia.destroy(); return result; } else if (_replaceMedia) { return false; } - _replaceMedia.create(this, st::historyReplaceMedia); + _replaceMedia.create( + this, + _canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia); const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration; _replaceMedia->setClickedCallback([=] { base::call_delayed(hideDuration, this, [=] { @@ -2986,6 +3081,9 @@ void HistoryWidget::updateControlsVisibility() { if (_pinnedBar) { _pinnedBar->show(); } + if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) { + _sponsoredMessageBar->toggle(true, anim::type::normal); + } if (_translateBar) { _translateBar->show(); } @@ -3452,7 +3550,7 @@ void HistoryWidget::messagesFailed(const MTP::Error &error, int requestId) { closeCurrent(); const auto wasAccount = not_null(&was->account()); if (const auto primary = Core::App().windowFor(wasAccount)) { - primary->showToast((was && was->isMegagroup()) + primary->showToast(was->isMegagroup() ? tr::lng_group_not_accessible(tr::now) : tr::lng_channel_not_accessible(tr::now)); } @@ -4208,6 +4306,9 @@ void HistoryWidget::hideChildWidgets() { if (_pinnedBar) { _pinnedBar->hide(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->toggle(false, anim::type::instant); + } if (_translateBar) { _translateBar->hide(); } @@ -4379,7 +4480,11 @@ auto HistoryWidget::computeSendButtonType() const { } else if (_isInlineBot) { return Type::Cancel; } else if (showRecordButton()) { - return Type::Record; + const auto both = Webrtc::RecordAvailability::VideoAndAudio; + const auto video = Core::App().settings().recordVideoMessages(); + return (video && _recordAvailability == both) + ? Type::Round + : Type::Record; } return Type::Send; } @@ -4593,6 +4698,10 @@ void HistoryWidget::doneShow() { if (_pinnedBar) { _pinnedBar->finishAnimating(); } + checkSponsoredMessageBar(); + if (_sponsoredMessageBar) { + _sponsoredMessageBar->finishAnimating(); + } if (_translateBar) { _translateBar->finishAnimating(); } @@ -4734,7 +4843,8 @@ void HistoryWidget::sendButtonClicked() { const auto type = _send->type(); if (type == Ui::SendButton::Type::Cancel) { cancelInlineBot(); - } else if (type != Ui::SendButton::Type::Record) { + } else if (type != Ui::SendButton::Type::Record + && type != Ui::SendButton::Type::Round) { send({}); } } @@ -5034,7 +5144,7 @@ bool HistoryWidget::showRecordButton() const { return false; } - return Media::Capture::instance()->available() + return (_recordAvailability != Webrtc::RecordAvailability::None) && !_voiceRecordBar->isListenState() && !_voiceRecordBar->isRecordingByAnotherBar() && !HasSendText(_field) @@ -5065,7 +5175,9 @@ void HistoryWidget::updateSendButtonType() { }(); _send->setSlowmodeDelay(delay); _send->setDisabled(disabledBySlowmode - && (type == Type::Send || type == Type::Record)); + && (type == Type::Send + || type == Type::Record + || type == Type::Round)); if (delay != 0) { base::call_delayed( @@ -5154,7 +5266,7 @@ bool HistoryWidget::updateCmdStartShown() { const auto textSmall = _fieldCharsCountManager.count() > kSmallMenuAfter; const auto textChanged = _botMenu.button && ((_botMenu.text != bot->botInfo->botMenuButtonText) - || (_botMenu.small != textSmall)); + || (_botMenu.small != textSmall)); if (textChanged) { _botMenu.text = bot->botInfo->botMenuButtonText; if ((_botMenu.small = textSmall)) { @@ -5170,7 +5282,10 @@ bool HistoryWidget::updateCmdStartShown() { return commandsChanged || buttonChanged || textChanged; } -bool HistoryWidget::searchInChatEmbedded(Dialogs::Key chat, QString query) { +bool HistoryWidget::searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom) { const auto peer = chat.peer(); // windows todo if (!peer || Window::SeparateId(peer) != controller()->windowId()) { return false; @@ -5826,7 +5941,7 @@ bool HistoryWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { if (_editMsgId) { - if (_canReplaceMedia) { + if (_canReplaceMedia || _canAddMedia) { EditCaptionBox::StartMediaReplace( controller(), { _history->peer->id, _editMsgId }, @@ -6112,8 +6227,14 @@ void HistoryWidget::updateControlsGeometry() { _pinnedBar->move(0, pinnedBarTop); _pinnedBar->resizeToWidth(width()); } - const auto translateTop = pinnedBarTop + const auto sponsoredMessageBarTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); + if (_sponsoredMessageBar) { + _sponsoredMessageBar->move(0, sponsoredMessageBarTop); + _sponsoredMessageBar->resizeToWidth(width()); + } + const auto translateTop = sponsoredMessageBarTop + + (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0); if (_translateBar) { _translateBar->move(0, translateTop); _translateBar->resizeToWidth(width()); @@ -6358,6 +6479,9 @@ void HistoryWidget::updateHistoryGeometry( if (_translateBar) { newScrollHeight -= _translateBar->height(); } + if (_sponsoredMessageBar) { + newScrollHeight -= _sponsoredMessageBar->height(); + } if (_pinnedBar) { newScrollHeight -= _pinnedBar->height(); } @@ -6778,6 +6902,7 @@ int HistoryWidget::computeMaxFieldHeight() const { - _topBar->height() - (_contactStatus ? _contactStatus->bar().height() : 0) - (_businessBotStatus ? _businessBotStatus->bar().height() : 0) + - (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0) - (_pinnedBar ? _pinnedBar->height() : 0) - (_groupCallBar ? _groupCallBar->height() : 0) - (_requestsBar ? _requestsBar->height() : 0) @@ -7666,6 +7791,116 @@ void HistoryWidget::requestMessageData(MsgId msgId) { session().api().requestMessageData(_peer, msgId, callback); } +bool HistoryWidget::checkSponsoredMessageBarVisibility() const { + const auto h = _list->height() + - (_kbScroll->isHidden() ? 0 : _kbScroll->height()); + return (h > _scroll->height()); +} + +void HistoryWidget::requestSponsoredMessageBar() { + if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) { + return; + } + const auto checkState = [=, this] { + using State = Data::SponsoredMessages::State; + const auto state = session().sponsoredMessages().state( + _history); + _sponsoredMessagesStateKnown = (state != State::None); + if (state == State::AppendToTopBar) { + createSponsoredMessageBar(); + if (checkSponsoredMessageBarVisibility()) { + _sponsoredMessageBar->toggle(true, anim::type::normal); + } else { + auto &lifetime = _sponsoredMessageBar->lifetime(); + const auto heightLifetime + = lifetime.make_state(); + _list->heightValue( + ) | rpl::start_with_next([=, this] { + if (_sponsoredMessageBar->toggled()) { + heightLifetime->destroy(); + } else if (checkSponsoredMessageBarVisibility()) { + _sponsoredMessageBar->toggle( + true, + anim::type::normal); + heightLifetime->destroy(); + } + }, *heightLifetime); + } + } + }; + const auto history = _history; + session().sponsoredMessages().request( + _history, + crl::guard(this, [=, this] { + if (history == _history) { + checkState(); + } + })); +} + +void HistoryWidget::checkSponsoredMessageBar() { + if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) { + return; + } + const auto state = session().sponsoredMessages().state(_history); + if (state == Data::SponsoredMessages::State::AppendToTopBar) { + if (checkSponsoredMessageBarVisibility()) { + if (!_sponsoredMessageBar) { + createSponsoredMessageBar(); + } + _sponsoredMessageBar->toggle(true, anim::type::instant); + } + } +} + +void HistoryWidget::createSponsoredMessageBar() { + _sponsoredMessageBar = base::make_unique_q>( + this, + object_ptr(this)); + + _sponsoredMessageBar->entity()->resizeToWidth(_scroll->width()); + const auto maybeFullId = session().sponsoredMessages().fillTopBar( + _history, + _sponsoredMessageBar->entity()); + session().sponsoredMessages().itemRemoved( + maybeFullId + ) | rpl::start_with_next([this] { + _sponsoredMessageBar->toggle(false, anim::type::normal); + _sponsoredMessageBar->shownValue() | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next([this] { + _sponsoredMessageBar = nullptr; + }, _sponsoredMessageBar->lifetime()); + }, _sponsoredMessageBar->lifetime()); + + if (maybeFullId) { + const auto viewLifetime + = _sponsoredMessageBar->lifetime().make_state(); + rpl::combine( + _sponsoredMessageBar->entity()->heightValue(), + _sponsoredMessageBar->heightValue() + ) | rpl::filter( + rpl::mappers::_1 == rpl::mappers::_2 + ) | rpl::start_with_next([=] { + session().sponsoredMessages().view(maybeFullId); + viewLifetime->destroy(); + }, *viewLifetime); + } + + _sponsoredMessageBarHeight = 0; + _sponsoredMessageBar->heightValue( + ) | rpl::start_with_next([=](int height) { + _topDelta = _preserveScrollTop + ? 0 + : (height - _sponsoredMessageBarHeight); + _sponsoredMessageBarHeight = height; + updateHistoryGeometry(); + updateControlsGeometry(); + _topDelta = 0; + }, _sponsoredMessageBar->lifetime()); + _sponsoredMessageBar->toggle(false, anim::type::instant); +} + bool HistoryWidget::sendExistingDocument( not_null document, Api::MessageToSend messageToSend, @@ -8015,6 +8250,18 @@ void HistoryWidget::editMessage( setInnerFocus(); } +void HistoryWidget::fillSenderUserpicMenu( + not_null menu, + not_null peer) { + const auto inGroup = _peer && (_peer->isChat() || _peer->isMegagroup()); + Window::FillSenderUserpicMenu( + controller(), + peer, + (inGroup && _canSendTexts) ? _field.data() : nullptr, + inGroup ? _peer->owner().history(_peer) : Dialogs::Key(), + Ui::Menu::CreateAddActionCallback(menu)); +} + void HistoryWidget::hidePinnedMessage() { Expects(_pinnedBar != nullptr); @@ -8120,7 +8367,7 @@ void HistoryWidget::cancelEdit() { return; } - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; updateReplaceMediaButton(); _replyEditMsg = nullptr; @@ -8503,7 +8750,16 @@ void HistoryWidget::updateReplyEditTexts(bool force) { if (_editMsgId && _replyEditMsg) { _mediaEditManager.start(_replyEditMsg); } - _canReplaceMedia = editMedia && editMedia->allowsEditMedia(); + _canReplaceMedia = _editMsgId && _replyEditMsg->allowsEditMedia(); + if (editMedia) { + _canAddMedia = false; + } else { + _canAddMedia = base::take(_canReplaceMedia); + } + if (_canReplaceMedia || _canAddMedia) { + // Invalidate the button, maybe icon has changed. + _replaceMedia.destroy(); + } _photoEditMedia = (_canReplaceMedia && editMedia->photo() && !editMedia->photo()->isNull()) diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 3e3619aad..aaaed201a 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -73,12 +73,18 @@ class SpoilerAnimation; class ChooseThemeController; class ContinuousScroll; struct ChatPaintHighlight; +template +class SlideWrap; } // namespace Ui namespace Ui::Emoji { class SuggestionsController; } // namespace Ui::Emoji +namespace Webrtc { +enum class RecordAvailability : uchar; +} // namespace Webrtc + namespace Window { class SessionController; } // namespace Window @@ -205,6 +211,10 @@ public: not_null item, const TextSelection &selection); + void fillSenderUserpicMenu( + not_null menu, + not_null peer); + [[nodiscard]] FullReplyTo replyTo() const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; @@ -257,7 +267,10 @@ public: [[nodiscard]] rpl::producer<> cancelRequests() const { return _cancelRequests.events(); } - bool searchInChatEmbedded(Dialogs::Key chat, QString query); + bool searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom = nullptr); void updateNotifyControls(); @@ -532,6 +545,11 @@ private: void setupGroupCallBar(); void setupRequestsBar(); + void checkSponsoredMessageBar(); + [[nodiscard]] bool checkSponsoredMessageBarVisibility() const; + void requestSponsoredMessageBar(); + void createSponsoredMessageBar(); + void sendInlineResult(InlineBots::ResultSelected result); void drawField(Painter &p, const QRect &rect); @@ -665,6 +683,7 @@ private: MsgId _editMsgId = 0; std::shared_ptr _photoEditMedia; bool _canReplaceMedia = false; + bool _canAddMedia = false; HistoryView::MediaEditManager _mediaEditManager; HistoryItem *_replyEditMsg = nullptr; @@ -689,8 +708,12 @@ private: std::unique_ptr _requestsBar; int _requestsBarHeight = 0; + base::unique_qptr> _sponsoredMessageBar; + int _sponsoredMessageBarHeight = 0; + bool _preserveScrollTop = false; bool _repaintFieldScheduled = false; + bool _sentFromScheduledTip = false; mtpRequestId _saveEditMsgRequestId = 0; @@ -750,6 +773,8 @@ private: mtpRequestId _inlineBotResolveRequestId = 0; bool _isInlineBot = false; + Webrtc::RecordAvailability _recordAvailability = {}; + std::unique_ptr _contactStatus; std::unique_ptr _businessBotStatus; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index e51bae134..bebcc9c57 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -28,6 +28,7 @@ struct VoiceToSend { VoiceWaveform waveform; crl::time duration = 0; Api::SendOptions options; + bool video = false; }; struct SendActionUpdate { Api::SendProgressType type = Api::SendProgressType(); 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 22769da1b..5f7f2ddbd 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -81,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/silent_toggle.h" #include "ui/chat/choose_send_as.h" #include "ui/effects/spoiler_mess.h" +#include "webrtc/webrtc_environment.h" #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" @@ -380,7 +381,7 @@ void FieldHeader::init() { return; } const auto e = static_cast(event.get()); - const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos()); + const auto pos = e->pos(); const auto inPreviewRect = _clickableRect.contains(pos); const auto inPhotoEdit = _shownMessageHasPreview && _photoEditAllowed @@ -1151,7 +1152,7 @@ void ComposeControls::setMimeDataHook(MimeDataHook hook) { bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { if (!isEditingMessage() || !_regularWindow) { return false; - } else if (_canReplaceMedia) { + } else if (_canReplaceMedia || _canAddMedia) { const auto queryToEdit = _header->queryToEdit(); EditCaptionBox::StartMediaReplace( _regularWindow, @@ -1190,9 +1191,7 @@ void ComposeControls::showStarted() { if (_attachBotsMenu) { _attachBotsMenu->hideFast(); } - if (_voiceRecordBar) { - _voiceRecordBar->hideFast(); - } + _voiceRecordBar->hideFast(); if (_autocomplete) { _autocomplete->hideFast(); } @@ -1212,9 +1211,7 @@ void ComposeControls::showFinished() { if (_attachBotsMenu) { _attachBotsMenu->hideFast(); } - if (_voiceRecordBar) { - _voiceRecordBar->hideFast(); - } + _voiceRecordBar->hideFast(); if (_autocomplete) { _autocomplete->hideFast(); } @@ -1517,7 +1514,7 @@ void ComposeControls::orderControls() { } bool ComposeControls::showRecordButton() const { - return ::Media::Capture::instance()->available() + return (_recordAvailability != Webrtc::RecordAvailability::None) && !_voiceRecordBar->isListenState() && !_voiceRecordBar->isRecordingByAnotherBar() && !HasSendText(_field) @@ -1942,7 +1939,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { _preview->apply({ .removed = true }); _preview->setDisabled(false); } - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; return; } @@ -1962,7 +1959,16 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { const auto resolve = [=] { if (const auto item = _history->owner().message(editingId)) { const auto media = item->media(); - _canReplaceMedia = media && media->allowsEditMedia(); + _canReplaceMedia = item->allowsEditMedia(); + if (media) { + _canAddMedia = false; + } else { + _canAddMedia = base::take(_canReplaceMedia); + } + if (_canReplaceMedia || _canAddMedia) { + // Invalidate the button, maybe icon has changed. + _replaceMedia = nullptr; + } _photoEditMedia = (_canReplaceMedia && _regularWindow && media->photo() @@ -1983,7 +1989,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } return true; } - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; _header->editMessage(editingId, false); return false; @@ -2004,7 +2010,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _header->replyToMessage({}); } else { - _canReplaceMedia = false; + _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; _header->replyToMessage(draft->reply); if (_header->replyingToMessage()) { @@ -2137,12 +2143,17 @@ void ComposeControls::initSendButton() { } }; - SendMenu::SetupMenuAndShortcuts( _send.get(), _show, [=] { return sendButtonMenuDetails(); }, sendAction); + + Core::App().mediaDevices().recordAvailabilityValue( + ) | rpl::start_with_next([=](Webrtc::RecordAvailability value) { + _recordAvailability = value; + updateSendButtonType(); + }, _send->lifetime()); } void ComposeControls::initSendAsButton(not_null peer) { @@ -2413,6 +2424,42 @@ void ComposeControls::initVoiceRecordBar() { return false; }); + _voiceRecordBar->recordingTipRequests( + ) | rpl::start_with_next([=] { + Core::App().settings().setRecordVideoMessages( + !Core::App().settings().recordVideoMessages()); + updateSendButtonType(); + switch (_send->type()) { + case Ui::SendButton::Type::Record: { + const auto both = Webrtc::RecordAvailability::VideoAndAudio; + _show->showToast((_recordAvailability == both) + ? tr::lng_record_voice_tip(tr::now) + : tr::lng_record_hold_tip(tr::now)); + } break; + case Ui::SendButton::Type::Round: + _show->showToast(tr::lng_record_video_tip(tr::now)); + break; + } + }, _wrap->lifetime()); + + _voiceRecordBar->errors( + ) | rpl::start_with_next([=](::Media::Capture::Error error) { + using Error = ::Media::Capture::Error; + switch (error) { + case Error::AudioInit: + case Error::AudioTimeout: + _show->showToast(tr::lng_record_audio_problem(tr::now)); + break; + case Error::VideoInit: + case Error::VideoTimeout: + _show->showToast(tr::lng_record_video_problem(tr::now)); + break; + default: + _show->showToast(u"Unknown error."_q); + break; + } + }, _wrap->lifetime()); + _voiceRecordBar->updateSendButtonTypeRequests( ) | rpl::start_with_next([=] { updateSendButtonType(); @@ -2454,7 +2501,11 @@ auto ComposeControls::computeSendButtonType() const { } else if (_isInlineBot) { return Type::Cancel; } else if (showRecordButton()) { - return Type::Record; + const auto both = Webrtc::RecordAvailability::VideoAndAudio; + const auto video = Core::App().settings().recordVideoMessages(); + return (video && _recordAvailability == both) + ? Type::Round + : Type::Record; } return (_mode == Mode::Normal) ? Type::Send : Type::Schedule; } @@ -2487,7 +2538,9 @@ void ComposeControls::updateSendButtonType() { }(); _send->setSlowmodeDelay(delay); _send->setDisabled(_sendDisabledBySlowmode.current() - && (type == Type::Send || type == Type::Record)); + && (type == Type::Send + || type == Type::Record + || type == Type::Round)); } void ComposeControls::finishAnimating() { @@ -2882,7 +2935,7 @@ void ComposeControls::editMessage(not_null item) { } bool ComposeControls::updateReplaceMediaButton() { - if (!_canReplaceMedia || !_regularWindow) { + if ((!_canReplaceMedia && !_canAddMedia) || !_regularWindow) { const auto result = (_replaceMedia != nullptr); _replaceMedia = nullptr; return result; @@ -2891,7 +2944,7 @@ bool ComposeControls::updateReplaceMediaButton() { } _replaceMedia = std::make_unique( _wrap.get(), - st::historyReplaceMedia); + _canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia); const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration; _replaceMedia->setClickedCallback([=] { base::call_delayed(hideDuration, _wrap.get(), [=] { @@ -3149,8 +3202,9 @@ bool ComposeControls::isRecording() const { bool ComposeControls::isRecordingPressed() const { return !_voiceRecordBar->isRecordingLocked() && (!_voiceRecordBar->isHidden() - || (_send->type() == Ui::SendButton::Type::Record - && _send->isDown())); + || (_send->isDown() + && (_send->type() == Ui::SendButton::Type::Record + || _send->type() == Ui::SendButton::Type::Round))); } rpl::producer ComposeControls::recordingActiveValue() const { @@ -3325,6 +3379,10 @@ Fn ComposeControls::restoreTextCallback( }); } +Ui::InputField *ComposeControls::fieldForMention() const { + return _writeRestriction.current() ? nullptr : _field.get(); +} + TextWithEntities ComposeControls::prepareTextForEditMsg() const { if (!_history) { return {}; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index fc26079e7..2b97c4f98 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -76,6 +76,10 @@ namespace Main { class Session; } // namespace Main +namespace Webrtc { +enum class RecordAvailability : uchar; +} // namespace Webrtc + namespace Window { struct SectionShow; class SessionController; @@ -245,6 +249,8 @@ public: Fn restoreTextCallback(const QString &insertTextOnCancel) const; + [[nodiscard]] Ui::InputField *fieldForMention() const; + private: enum class TextUpdateEvent { SaveDraft = (1 << 0), @@ -436,10 +442,12 @@ private: bool _isInlineBot = false; bool _botCommandShown = false; bool _likeShown = false; + Webrtc::RecordAvailability _recordAvailability = {}; FullMsgId _editingId; std::shared_ptr _photoEditMedia; bool _canReplaceMedia = false; + bool _canAddMedia = false; std::unique_ptr _preview; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp index 35fbc2134..45ababebc 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp @@ -29,6 +29,7 @@ void MediaEditManager::start( std::optional invertCaption) { const auto media = item->media(); if (!media) { + cancel(); return; } _item = item; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index d0c8ca2b8..8b0429c28 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer_rpl.h" #include "base/unixtime.h" +#include "boxes/filters/edit_filter_chats_list.h" #include "boxes/peer_list_box.h" #include "boxes/peer_list_controllers.h" #include "chat_helpers/compose/compose_show.h" @@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_session_controller.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -911,6 +913,117 @@ void DraftOptionsBox( }, box->lifetime()); } } + +struct AuthorSelector { + object_ptr content = { nullptr }; + Fn overrideKey; +}; +[[nodiscard]] AuthorSelector AuthorRowSelector( + not_null session, + FullReplyTo reply, + Fn)> chosen) { + const auto item = session->data().message(reply.messageId); + if (!item) { + return {}; + } + const auto displayFrom = item->displayFrom(); + const auto from = displayFrom ? displayFrom : item->from().get(); + if (!from->isUser() || from == item->history()->peer || from->isSelf()) { + return {}; + } + + class AuthorController final : public PeerListController { + public: + AuthorController(not_null peer, Fn click) + : _peer(peer) + , _click(std::move(click)) { + } + + void prepare() override { + delegate()->peerListAppendRow( + std::make_unique( + _peer->owner().history(_peer), + &computeListSt().item)); + delegate()->peerListRefreshRows(); + TrackPremiumRequiredChanges(this, _lifetime); + } + void loadMoreRows() override { + } + void rowClicked(not_null row) override { + if (RecipientRow::ShowLockedError(this, row, WritePremiumRequiredError)) { + return; + } else if (const auto onstack = _click) { + onstack(); + } + } + Main::Session &session() const override { + return _peer->session(); + } + + private: + const not_null _peer; + Fn _click; + rpl::lifetime _lifetime; + + }; + + auto result = object_ptr((QWidget*)nullptr); + const auto container = result.data(); + + container->add(CreatePeerListSectionSubtitle( + container, + tr::lng_reply_in_author())); + Ui::AddSkip(container); + + const auto delegate = container->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = container->lifetime().make_state< + AuthorController + >(from, [=] { chosen(from->owner().history(from)); }); + controller->setStyleOverrides(&st::peerListSingleRow); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); + + Ui::AddSkip(container); + container->add(CreatePeerListSectionSubtitle( + container, + tr::lng_reply_in_chats_list())); + + const auto overrideKey = [=](int direction, int from, int to) { + if (!content->isVisible()) { + return false; + } else if (direction > 0 && from < 0 && to >= 0) { + if (content->hasSelection()) { + const auto was = content->selectedIndex(); + const auto now = content->selectSkip(1).reallyMovedTo; + if (was != now) { + return true; + } + content->clearSelection(); + } else { + content->selectSkip(1); + return true; + } + } else if (direction < 0 && to < 0) { + if (!content->hasSelection()) { + content->selectLast(); + } else if (from >= 0 || content->hasSelection()) { + content->selectSkip(-1); + } + } + return false; + }; + + return { + .content = std::move(result), + .overrideKey = overrideKey, + }; +} + } // namespace void ShowReplyToChatBox( @@ -921,7 +1034,7 @@ void ShowReplyToChatBox( public: using Chosen = not_null; - Controller(not_null session) + Controller(not_null session, FullReplyTo reply) : ChooseRecipientBoxController({ .session = session, .callback = [=](Chosen thread) { @@ -929,6 +1042,13 @@ void ShowReplyToChatBox( }, .premiumRequiredError = WritePremiumRequiredError, }) { + _authorRow = AuthorRowSelector( + session, + reply, + [=](Chosen thread) { _singleChosen.fire_copy(thread); }); + if (_authorRow.content) { + setStyleOverrides(&st::peerListSmallSkips); + } } [[nodiscard]] rpl::producer singleChosen() const { @@ -939,13 +1059,26 @@ void ShowReplyToChatBox( return tr::lng_saved_quote_here(tr::now); } + bool overrideKeyboardNavigation( + int direction, + int fromIndex, + int toIndex) override { + return _authorRow.overrideKey + && _authorRow.overrideKey(direction, fromIndex, toIndex); + } + private: void prepareViewHook() override { + if (_authorRow.content) { + delegate()->peerListSetAboveWidget( + std::move(_authorRow.content)); + } ChooseRecipientBoxController::prepareViewHook(); delegate()->peerListSetTitle(tr::lng_reply_in_another_title()); } rpl::event_stream _singleChosen; + AuthorSelector _authorRow; }; @@ -956,7 +1089,7 @@ void ShowReplyToChatBox( }; const auto session = &show->session(); const auto state = [&] { - auto controller = std::make_unique(session); + auto controller = std::make_unique(session, reply); const auto controllerRaw = controller.get(); auto box = Box(std::move(controller), [=]( not_null box) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 9d2999960..2825fb0a7 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "base/unixtime.h" #include "ui/boxes/confirm_box.h" +#include "calls/calls_instance.h" #include "chat_helpers/compose/compose_show.h" #include "core/application.h" #include "data/data_document.h" @@ -27,21 +28,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" +#include "media/streaming/media_streaming_instance.h" +#include "media/streaming/media_streaming_round_preview.h" +#include "storage/storage_account.h" +#include "ui/controls/round_video_recorder.h" #include "ui/controls/send_button.h" #include "ui/effects/animation_value.h" #include "ui/effects/animation_value_f.h" #include "ui/effects/ripple_animation.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/dynamic_image.h" #include "ui/painter.h" #include "ui/widgets/tooltip.h" #include "ui/rect.h" #include "ui/ui_utility.h" +#include "webrtc/webrtc_video_track.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_media_player.h" +#include + // AyuGram includes #include "ayu/ayu_settings.h" #include "boxes/abstract_box.h" @@ -50,9 +59,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView::Controls { namespace { -using SendActionUpdate = VoiceRecordBar::SendActionUpdate; -using VoiceToSend = VoiceRecordBar::VoiceToSend; - constexpr auto kAudioVoiceUpdateView = crl::time(200); constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes constexpr auto kMaxSamples @@ -74,6 +80,61 @@ enum class FilterType { Cancel, }; +class SoundedPreview final : public Ui::DynamicImage { +public: + SoundedPreview( + not_null document, + rpl::producer<> repaints); + std::shared_ptr clone() override; + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + const not_null _document; + QImage _roundingMask; + Fn _repaint; + rpl::lifetime _lifetime; + +}; + +SoundedPreview::SoundedPreview( + not_null document, + rpl::producer<> repaints) +: _document(document) { + std::move(repaints) | rpl::start_with_next([=] { + if (const auto onstack = _repaint) { + onstack(); + } + }, _lifetime); +} + +std::shared_ptr SoundedPreview::clone() { + Unexpected("ListenWrap::videoPreview::clone."); +} + +QImage SoundedPreview::image(int size) { + const auto player = ::Media::Player::instance(); + const auto streamed = player->roundVideoPreview(_document); + if (!streamed) { + return {}; + } + + const auto full = QSize(size, size) * style::DevicePixelRatio(); + if (_roundingMask.size() != full) { + _roundingMask = Images::EllipseMask(full); + } + const auto frame = streamed->frameWithInfo({ + .resize = full, + .outer = full, + .mask = _roundingMask, + }); + return frame.image; +} + +void SoundedPreview::subscribeToUpdates(Fn callback) { + _repaint = std::move(callback); +} + [[nodiscard]] auto InactiveColor(const QColor &c) { return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha); } @@ -82,10 +143,6 @@ enum class FilterType { return std::clamp(float64(low) / high, 0., 1.); } -[[nodiscard]] crl::time Duration(int samples) { - return samples * crl::time(1000) / ::Media::Player::kDefaultFrequency; -} - [[nodiscard]] auto FormatVoiceDuration(int samples) { const int duration = kPrecision * (float64(samples) / ::Media::Player::kDefaultFrequency); @@ -206,6 +263,44 @@ void PaintWaveform( } } +void FillWithMinithumbs( + QPainter &p, + not_null data, + QRect rect, + float64 progress) { + if (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) { + return; + } + const auto size = rect.height(); + const auto single = data->minithumbSize; + const auto perrow = data->minithumbs.width() / single; + const auto thumbs = (rect.width() + size - 1) / size; + if (!thumbs || !perrow) { + return; + } + for (auto i = 0; i != thumbs - 1; ++i) { + const auto index = (i * data->minithumbsCount) / thumbs; + p.drawImage( + QRect(rect.x() + i * size, rect.y(), size, size), + data->minithumbs, + QRect( + (index % perrow) * single, + (index / perrow) * single, + single, + single)); + } + const auto last = rect.width() - (thumbs - 1) * size; + const auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs; + p.drawImage( + QRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size), + data->minithumbs, + QRect( + (index % perrow) * single, + (index / perrow) * single, + (last * single) / size, + single)); +} + [[nodiscard]] QRect DrawLockCircle( QPainter &p, const QRect &widgetRect, @@ -274,7 +369,8 @@ class TTLButton final : public Ui::RippleButton { public: TTLButton( not_null parent, - const style::RecordBar &st); + const style::RecordBar &st, + bool recordingVideo); void clearState() override; @@ -293,7 +389,8 @@ private: TTLButton::TTLButton( not_null parent, - const style::RecordBar &st) + const style::RecordBar &st, + bool recordingVideo) : RippleButton(parent, st.lock.ripple) , _st(st) , _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width())) @@ -320,8 +417,10 @@ TTLButton::TTLButton( } auto text = rpl::conditional( Core::App().settings().ttlVoiceClickTooltipHiddenValue(), - tr::lng_record_once_active_tooltip( - Ui::Text::RichLangValue), + (recordingVideo + ? tr::lng_record_once_active_video + : tr::lng_record_once_active_tooltip)( + Ui::Text::RichLangValue), tr::lng_record_once_first_tooltip( Ui::Text::RichLangValue)); _tooltip.reset(Ui::CreateChild( @@ -380,7 +479,7 @@ TTLButton::TTLButton( ) | rpl::start_with_next([=](bool toHide) { const auto isFirstTooltip = !Core::App().settings().ttlVoiceClickTooltipHidden(); - if (isFirstTooltip || (!isFirstTooltip && toHide)) { + if (isFirstTooltip || toHide) { _tooltip->toggleAnimated(!toHide); } }, _tooltip->lifetime()); @@ -431,35 +530,37 @@ public: not_null parent, const style::RecordBar &st, not_null session, - ::Media::Capture::Result *data, + not_null data, const style::font &font); void requestPaintProgress(float64 progress); - rpl::producer<> stopRequests() const; + [[nodiscard]] rpl::producer<> stopRequests() const; void playPause(); + [[nodiscard]] std::shared_ptr videoPreview(); - rpl::lifetime &lifetime(); + [[nodiscard]] rpl::lifetime &lifetime(); private: void init(); void initPlayButton(); void initPlayProgress(); - bool isInPlayer(const ::Media::Player::TrackState &state) const; - bool isInPlayer() const; + [[nodiscard]] bool isInPlayer( + const ::Media::Player::TrackState &state) const; + [[nodiscard]] bool isInPlayer() const; - int computeTopMargin(int height) const; - QRect computeWaveformRect(const QRect ¢erRect) const; + [[nodiscard]] int computeTopMargin(int height) const; + [[nodiscard]] QRect computeWaveformRect(const QRect ¢erRect) const; - not_null _parent; + const not_null _parent; const style::RecordBar &_st; const not_null _session; const not_null _document; const std::unique_ptr _voiceData; const std::shared_ptr _mediaView; - const not_null<::Media::Capture::Result*> _data; + const not_null _data; const base::unique_qptr _delete; const style::font &_durationFont; const QString _duration; @@ -480,6 +581,7 @@ private: anim::value _playProgress; rpl::variable _showProgress = 0.; + rpl::event_stream<> _videoRepaints; rpl::lifetime _lifetime; @@ -489,7 +591,7 @@ ListenWrap::ListenWrap( not_null parent, const style::RecordBar &st, not_null session, - ::Media::Capture::Result *data, + not_null data, const style::font &font) : _parent(parent) , _st(st) @@ -500,8 +602,7 @@ ListenWrap::ListenWrap( , _data(data) , _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) -, _duration(Ui::FormatDurationText( - float64(_data->samples) / ::Media::Player::kDefaultFrequency)) +, _duration(Ui::FormatDurationText(_data->duration / 1000)) , _durationWidth(_durationFont->width(_duration)) , _playPauseSt(st::mediaPlayerButton) , _playPauseButton(base::make_unique_q(parent)) @@ -608,20 +709,27 @@ void ListenWrap::init() { } // Waveform paint. - { - const auto rect = (progress == 1.) - ? _waveformFgRect - : computeWaveformRect(bgCenterRect); - if (rect.width() > 0) { - p.translate(rect.topLeft()); + const auto waveformRect = (progress == 1.) + ? _waveformFgRect + : computeWaveformRect(bgCenterRect); + if (!waveformRect.isEmpty()) { + const auto playProgress = _playProgress.current(); + if (_data->minithumbs.isNull()) { + p.translate(waveformRect.topLeft()); PaintWaveform( p, _voiceData.get(), - rect.width(), + waveformRect.width(), _activeWaveformBar, _inactiveWaveformBar, - _playProgress.current()); + playProgress); p.resetTransform(); + } else { + FillWithMinithumbs( + p, + _data, + waveformRect, + playProgress); } } } @@ -635,9 +743,11 @@ void ListenWrap::initPlayButton() { using namespace ::Media::Player; using State = TrackState; - _mediaView->setBytes(_data->bytes); - _document->size = _data->bytes.size(); - _document->type = VoiceDocument; + _mediaView->setBytes(_data->content); + _document->size = _data->content.size(); + _document->type = _data->minithumbs.isNull() + ? VoiceDocument + : RoundVideoDocument; const auto &play = _playPauseSt.playOuter; const auto &width = _waveformBgFinalCenterRect.height(); @@ -673,6 +783,9 @@ void ListenWrap::initPlayButton() { ) | rpl::start_with_next([=](const State &state) { if (isInPlayer(state)) { *showPause = ShowPauseIcon(state.state); + if (!_data->minithumbs.isNull()) { + _videoRepaints.fire({}); + } } else if (showPause->current()) { *showPause = false; } @@ -683,6 +796,13 @@ void ListenWrap::initPlayButton() { ) | rpl::start_with_next([=] { *showPause = false; }, _lifetime); + + _lifetime.add([=] { + const auto current = instance()->current(AudioMsgId::Type::Voice); + if (current.audio() == _document) { + instance()->stop(AudioMsgId::Type::Voice, true); + } + }); } void ListenWrap::initPlayProgress() { @@ -822,6 +942,12 @@ rpl::producer<> ListenWrap::stopRequests() const { return _delete->clicks() | rpl::to_empty; } +std::shared_ptr ListenWrap::videoPreview() { + return std::make_shared( + _document, + _videoRepaints.events()); +} + rpl::lifetime &ListenWrap::lifetime() { return _lifetime; } @@ -836,6 +962,7 @@ public: void requestPaintLockToStopProgress(float64 progress); void requestPaintPauseToInputProgress(float64 progress); void setVisibleTopPart(int part); + void setRecordingVideo(bool value); [[nodiscard]] rpl::producer<> locks() const; [[nodiscard]] bool isLocked() const; @@ -864,6 +991,7 @@ private: float64 _pauseToInputProgress = 0.; rpl::variable _progress = 0.; int _visibleTopPart = -1; + bool _recordingVideo = false; }; @@ -887,6 +1015,10 @@ void RecordLock::setVisibleTopPart(int part) { _visibleTopPart = part; } +void RecordLock::setRecordingVideo(bool value) { + _recordingVideo = value; +} + void RecordLock::init() { shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -977,9 +1109,10 @@ void RecordLock::drawProgress(QPainter &p) { p.setBrush(_st.fg); if (_pauseToInputProgress > 0.) { p.setOpacity(_pauseToInputProgress); - st::historyRecordLockInput.paintInCenter( - p, - blockRect.toRect()); + const auto &icon = _recordingVideo + ? st::historyRecordLockRound + : st::historyRecordLockInput; + icon.paintInCenter(p, blockRect.toRect()); p.setOpacity(1. - _pauseToInputProgress); } p.drawRoundedRect( @@ -1249,7 +1382,7 @@ VoiceRecordBar::VoiceRecordBar( } VoiceRecordBar::~VoiceRecordBar() { - if (isRecording()) { + if (isActive()) { stopRecording(StopType::Cancel); } } @@ -1303,7 +1436,12 @@ void VoiceRecordBar::updateTTLGeometry( const auto parent = parentWidget(); const auto me = Ui::MapFrom(_outerContainer, parent, geometry()); const auto anyTop = me.y() - st::historyRecordLockPosition.y(); - const auto ttlFrom = anyTop - _ttlButton->height() * 2; + const auto lockHiddenProgress = (_lockShowing.current() || !_fullRecord) + ? 0. + : (1. - _showLockAnimation.value(0.)); + const auto ttlFrom = anyTop + - _ttlButton->height() + - (_ttlButton->height() * (1. - lockHiddenProgress)); if (type == TTLAnimationType::RightLeft) { const auto finalRight = _outerContainer->width() - rect::right(me) @@ -1423,6 +1561,9 @@ void VoiceRecordBar::init() { } else if (value == 1. && show) { computeAndSetLockProgress(QCursor::pos()); } + if (_fullRecord && !show) { + updateTTLGeometry(TTLAnimationType::RightLeft, 1.); + } }; _showLockAnimation.start(std::move(callback), from, to, duration); }, lifetime()); @@ -1478,7 +1619,6 @@ void VoiceRecordBar::init() { if (!paused) { return; } - // _lockShowing = false; const auto to = 1.; auto callback = [=](float64 value) { @@ -1501,7 +1641,8 @@ void VoiceRecordBar::init() { if (!_ttlButton) { _ttlButton = std::make_unique( _outerContainer, - _st); + _st, + _recordingVideo); } _ttlButton->show(); } @@ -1534,12 +1675,14 @@ void VoiceRecordBar::init() { if (_startRecordingFilter && _startRecordingFilter()) { return; } - _recordingTipRequired = true; + _recordingTipRequire = crl::now(); + _recordingVideo = (_send->type() == Ui::SendButton::Type::Round); + _fullRecord = false; + _ttlButton = nullptr; + _lock->setRecordingVideo(_recordingVideo); _startTimer.callOnce(st::universalDuration); } else if (e->type() == QEvent::MouseButtonRelease) { - if (base::take(_recordingTipRequired)) { - _recordingTipRequests.fire({}); - } + checkTipRequired(); _startTimer.cancel(); } }, lifetime()); @@ -1584,6 +1727,11 @@ void VoiceRecordBar::activeAnimate(bool active) { } void VoiceRecordBar::visibilityAnimate(bool show, Fn &&callback) { + if (_send->type() == Ui::SendButton::Type::Round) { + _level->setType(VoiceRecordButton::Type::Round); + } else { + _level->setType(VoiceRecordButton::Type::Record); + } const auto to = show ? 1. : 0.; const auto from = show ? 0. : 1.; auto animationCallback = [=, callback = std::move(callback)](auto value) { @@ -1657,6 +1805,10 @@ void VoiceRecordBar::startRecording() { } using namespace ::Media::Capture; + if (_recordingVideo && !createVideoRecorder()) { + stop(false); + return; + } if (!instance()->available()) { stop(false); return; @@ -1669,16 +1821,36 @@ void VoiceRecordBar::startRecording() { if (_paused.current()) { _paused = false; instance()->pause(false, nullptr); + if (_videoRecorder) { + _videoRecorder->resume({ + .video = std::move(_data), + }); + } } else { - instance()->start(); + instance()->start(_videoRecorder + ? _videoRecorder->audioChunkProcessor() + : nullptr); } instance()->updated( ) | rpl::start_with_next_error([=](const Update &update) { - _recordingTipRequired = (update.samples < kMinSamples); recordUpdated(update.level, update.samples); }, [=] { stop(false); }, _recordingLifetime); + if (_videoRecorder) { + _videoRecorder->updated( + ) | rpl::start_with_next_error([=](const Update &update) { + recordUpdated(update.level, update.samples); + if (update.finished) { + _fullRecord = true; + stopRecording(StopType::Listen); + _lockShowing = false; + } + }, [=](Error error) { + stop(false); + _errors.fire_copy(error); + }, _recordingLifetime); + } _recordingLifetime.add([=] { _recording = false; }); @@ -1710,14 +1882,22 @@ void VoiceRecordBar::startRecording() { } computeAndSetLockProgress(mouse->globalPos()); } else if (type == QEvent::MouseButtonRelease) { - if (base::take(_recordingTipRequired)) { - _recordingTipRequests.fire({}); - } + checkTipRequired(); stop(_inField.current()); } }, _recordingLifetime); } +void VoiceRecordBar::checkTipRequired() { + const auto require = base::take(_recordingTipRequire); + const auto duration = st::universalDuration + + (kMinSamples * crl::time(1000) + / ::Media::Player::kDefaultFrequency); + if (require && (require + duration > crl::now())) { + _recordingTipRequests.fire({}); + } +} + void VoiceRecordBar::recordUpdated(quint16 level, int samples) { _level->requestPaintLevel(level); _recordingSamples = samples; @@ -1726,7 +1906,10 @@ void VoiceRecordBar::recordUpdated(quint16 level, int samples) { } Core::App().updateNonIdle(); update(_durationRect); - _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice }); + const auto type = _recordingVideo + ? Api::SendProgressType::RecordRound + : Api::SendProgressType::RecordVoice; + _sendActionUpdates.fire({ type }); } void VoiceRecordBar::stop(bool send) { @@ -1740,7 +1923,6 @@ void VoiceRecordBar::stop(bool send) { const auto type = send ? StopType::Send : StopType::Cancel; stopRecording(type, ttlBeforeHide); }; - // _lockShowing = false; visibilityAnimate(false, std::move(disappearanceCallback)); } @@ -1759,7 +1941,10 @@ void VoiceRecordBar::finish() { [[maybe_unused]] const auto s = takeTTLState(); - _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); + const auto type = _recordingVideo + ? Api::SendProgressType::RecordRound + : Api::SendProgressType::RecordVoice; + _sendActionUpdates.fire({ type, -1 }); _data = {}; } @@ -1774,39 +1959,99 @@ void VoiceRecordBar::hideFast() { void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { using namespace ::Media::Capture; if (type == StopType::Cancel) { + if (_videoRecorder) { + _videoRecorder->hide(); + } instance()->stop(crl::guard(this, [=](Result &&data) { _cancelRequests.fire({}); })); } else if (type == StopType::Listen) { - instance()->pause(true, crl::guard(this, [=](Result &&data) { - if (data.bytes.isEmpty()) { - // Close everything. - stop(false); - return; - } - _paused = true; - _data = std::move(data); + if (const auto recorder = _videoRecorder.get()) { + const auto weak = base::make_weak(recorder); + recorder->pause([=](Ui::RoundVideoResult data) { + crl::on_main(weak, [=, data = std::move(data)]() mutable { + window()->raise(); + window()->activateWindow(); - window()->raise(); - window()->activateWindow(); - _listen = std::make_unique( - this, - _st, - &_show->session(), - &_data, - _cancelFont); - _listenChanges.fire({}); + _paused = true; + _data = std::move(data); + _listen = std::make_unique( + this, + _st, + &_show->session(), + &_data, + _cancelFont); + _listenChanges.fire({}); - // _lockShowing = false; - })); + using SilentPreview = ::Media::Streaming::RoundPreview; + recorder->showPreview( + std::make_shared( + _data.content, + recorder->previewSize()), + _listen->videoPreview()); + }); + }); + instance()->pause(true); + } else { + instance()->pause(true, crl::guard(this, [=](Result &&data) { + if (data.bytes.isEmpty()) { + // Close everything. + stop(false); + return; + } + _paused = true; + _data = Ui::RoundVideoResult{ + .content = std::move(data.bytes), + .waveform = std::move(data.waveform), + .duration = data.duration, + }; + + window()->raise(); + window()->activateWindow(); + _listen = std::make_unique( + this, + _st, + &_show->session(), + &_data, + _cancelFont); + _listenChanges.fire({}); + })); + } } else if (type == StopType::Send) { + if (_videoRecorder) { + const auto weak = Ui::MakeWeak(this); + _videoRecorder->hide([=](Ui::RoundVideoResult data) { + crl::on_main([=, data = std::move(data)]() mutable { + if (weak) { + window()->raise(); + window()->activateWindow(); + const auto options = Api::SendOptions{ + .ttlSeconds = (ttlBeforeHide + ? std::numeric_limits::max() + : 0), + }; + _sendVoiceRequests.fire({ + .bytes = data.content, + //.waveform = {}, + .duration = data.duration, + .options = options, + .video = true, + }); + } + }); + }); + } instance()->stop(crl::guard(this, [=](Result &&data) { if (data.bytes.isEmpty()) { // Close everything. stop(false); return; } - _data = std::move(data); + _data = Ui::RoundVideoResult{ + .content = std::move(data.bytes), + .waveform = std::move(data.waveform), + .duration = data.duration, + }; window()->raise(); window()->activateWindow(); @@ -1826,10 +2071,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { [=, this](Fn &&close) { _sendVoiceRequests.fire({ - _data.bytes, - _data.waveform, - Duration(_data.samples), - options, + .bytes = _data.content, + .waveform =_data.waveform, + .duration = _data.duration, + .options =options, }); close(); }); @@ -1918,10 +2163,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) { [=, this](Fn &&close) { _sendVoiceRequests.fire({ - _data.bytes, - _data.waveform, - Duration(_data.samples), - options, + .bytes = _data.content, + .waveform =_data.waveform, + .duration = _data.duration, + .options = options, + .video = !_data.minithumbs.isNull(), }); close(); }); @@ -2009,6 +2255,10 @@ rpl::producer<> VoiceRecordBar::recordingTipRequests() const { return _recordingTipRequests.events(); } +auto VoiceRecordBar::errors() const -> rpl::producer { + return _errors.events(); +} + bool VoiceRecordBar::isLockPresent() const { return _lockShowing.current(); } @@ -2018,7 +2268,8 @@ bool VoiceRecordBar::isListenState() const { } bool VoiceRecordBar::isTypeRecord() const { - return (_send->type() == Ui::SendButton::Type::Record); + return (_send->type() == Ui::SendButton::Type::Record) + || (_send->type() == Ui::SendButton::Type::Round); } bool VoiceRecordBar::isRecordingByAnotherBar() const { @@ -2140,8 +2391,12 @@ void VoiceRecordBar::showDiscardBox( }; _show->showBox(Ui::MakeConfirmBox({ .text = (isListenState() - ? tr::lng_record_listen_cancel_sure - : tr::lng_record_lock_cancel_sure)(), + ? (_recordingVideo + ? tr::lng_record_listen_cancel_sure_round + : tr::lng_record_listen_cancel_sure) + : (_recordingVideo + ? tr::lng_record_lock_cancel_sure_round + : tr::lng_record_lock_cancel_sure))(), .confirmed = std::move(sure), .confirmText = tr::lng_record_lock_discard(), .confirmStyle = &st::attentionBoxButton, @@ -2149,4 +2404,52 @@ void VoiceRecordBar::showDiscardBox( _warningShown = true; } +bool VoiceRecordBar::createVideoRecorder() { + if (_videoRecorder) { + return true; + } + const auto hiding = [=](not_null which) { + if (_videoRecorder.get() == which) { + _videoHiding.push_back(base::take(_videoRecorder)); + } + }; + const auto hidden = [=](not_null which) { + if (_videoRecorder.get() == which) { + _videoRecorder = nullptr; + } + _videoHiding.erase( + ranges::remove( + _videoHiding, + which.get(), + &std::unique_ptr::get), + end(_videoHiding)); + }; + auto capturer = Core::App().calls().getVideoCapture(); + auto track = std::make_shared( + Webrtc::VideoState::Active); + capturer->setOutput(track->sink()); + capturer->setPreferredAspectRatio(1.); + _videoCapturerLifetime = track->stateValue( + ) | rpl::start_with_next([=](Webrtc::VideoState state) { + capturer->setState((state == Webrtc::VideoState::Active) + ? tgcalls::VideoState::Active + : tgcalls::VideoState::Inactive); + }); + _videoRecorder = std::make_unique( + Ui::RoundVideoRecorderDescriptor{ + .container = _outerContainer, + .hiding = hiding, + .hidden = hidden, + .capturer = std::move(capturer), + .track = std::move(track), + .placeholder = _show->session().local().readRoundPlaceholder(), + }); + _videoRecorder->placeholderUpdates( + ) | rpl::start_with_next([=](QImage &&placeholder) { + _show->session().local().writeRoundPlaceholder(placeholder); + }, _videoCapturerLifetime); + + return true; +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 332bf5b1f..027366a59 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "history/view/controls/compose_controls_common.h" #include "media/audio/media_audio_capture_common.h" +#include "ui/controls/round_video_recorder.h" #include "ui/effects/animations.h" #include "ui/round_rect.h" #include "ui/rp_widget.h" @@ -21,9 +22,14 @@ namespace style { struct RecordBar; } // namespace style +namespace Media::Capture { +enum class Error : uchar; +} // namespace Media::Capture + namespace Ui { class AbstractButton; class SendButton; +class RoundVideoRecorder; } // namespace Ui namespace Window { @@ -56,6 +62,7 @@ public: using SendActionUpdate = Controls::SendActionUpdate; using VoiceToSend = Controls::VoiceToSend; using FilterCallback = Fn; + using Error = ::Media::Capture::Error; VoiceRecordBar( not_null parent, @@ -87,6 +94,7 @@ public: [[nodiscard]] rpl::producer> lockViewportEvents() const; [[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const; [[nodiscard]] rpl::producer<> recordingTipRequests() const; + [[nodiscard]] rpl::producer errors() const; void requestToSendWithOptions(Api::SendOptions options); @@ -123,14 +131,12 @@ private: void updateTTLGeometry(TTLAnimationType type, float64 progress); void recordUpdated(quint16 level, int samples); - - [[nodiscard]] bool recordingAnimationCallback(crl::time now); + void checkTipRequired(); void stop(bool send); void stopRecording(StopType type, bool ttlBeforeHide = false); void visibilityAnimate(bool show, Fn &&callback); - [[nodiscard]] bool showRecordButton() const; void drawDuration(QPainter &p); void drawRedCircle(QPainter &p); void drawMessage(QPainter &p, float64 recordActive); @@ -153,6 +159,8 @@ private: [[nodiscard]] bool peekTTLState() const; [[nodiscard]] bool takeTTLState() const; + [[nodiscard]] bool createVideoRecorder(); + const style::RecordBar &_st; const not_null _outerContainer; const std::shared_ptr _show; @@ -163,7 +171,7 @@ private: std::unique_ptr _ttlButton; std::unique_ptr _listen; - ::Media::Capture::Result _data; + Ui::RoundVideoResult _data; rpl::variable _paused; base::Timer _startTimer; @@ -172,6 +180,7 @@ private: rpl::event_stream _sendVoiceRequests; rpl::event_stream<> _cancelRequests; rpl::event_stream<> _listenChanges; + rpl::event_stream _errors; int _centerY = 0; QRect _redCircleRect; @@ -192,9 +201,15 @@ private: float64 _redCircleProgress = 0.; rpl::event_stream<> _recordingTipRequests; - bool _recordingTipRequired = false; + crl::time _recordingTipRequire = 0; bool _lockFromBottom = false; + std::unique_ptr _videoRecorder; + std::vector> _videoHiding; + rpl::lifetime _videoCapturerLifetime; + bool _recordingVideo = false; + bool _fullRecord = false; + const style::font &_cancelFont; rpl::lifetime _recordingLifetime; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index 986ea9d6e..b32fa2d96 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -137,10 +137,14 @@ void VoiceRecordButton::init() { const auto state = *currentState; const auto icon = (state == Type::Send) ? st::historySendIcon - : st::historyRecordVoiceActive; + : (state == Type::Record) + ? st::historyRecordVoiceActive + : st::historyRecordRoundActive; const auto position = (state == Type::Send) ? st::historyRecordSendIconPosition - : QPoint(0, 0); + : (state == Type::Record) + ? QPoint(0, 0) + : st::historyRecordRoundIconPosition; icon.paint( p, -icon.width() / 2 + position.x(), diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index bd3f0150c..e8e2dd7b6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -31,6 +31,7 @@ public: enum class Type { Send, Record, + Round, }; void setType(Type state); diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 05721eff3..d98c5d16a 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -414,7 +414,9 @@ void BottomInfo::layoutDateText() { const auto edited = (_data.flags & Data::Flag::Edited) ? (settings->editedMark + ' ') - : QString(); + : (_data.flags & Data::Flag::EstimateDate) + ? (tr::lng_approximate(tr::now) + ' ') + : QString(); const auto author = _data.author; const auto prefix = !author.isEmpty() ? (author == settings->deletedMark ? u" "_q : u", "_q) : QString(); const auto date = edited + QLocale().toString( @@ -611,6 +613,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { if (forwarded && forwarded->imported) { result.flags |= Flag::Imported; } + if (item->awaitingVideoProcessing()) { + result.flags |= Flag::EstimateDate; + } // We don't want to pass and update it in Data for now. //if (item->unread()) { // result.flags |= Flag::Unread; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index 594a488f1..32e3e8fcd 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -32,15 +32,16 @@ struct TextState; class BottomInfo final : public Object { public: struct Data { - enum class Flag : uchar { - Edited = 0x01, - OutLayout = 0x02, - Sending = 0x04, - RepliesContext = 0x08, - Sponsored = 0x10, - Pinned = 0x20, - Imported = 0x40, - Shortcut = 0x80, + enum class Flag : uint16 { + Edited = 0x001, + OutLayout = 0x002, + Sending = 0x004, + RepliesContext = 0x008, + Sponsored = 0x010, + Pinned = 0x020, + Imported = 0x040, + Shortcut = 0x080, + EstimateDate = 0x100, //Unread, // We don't want to pass and update it in Date for now. }; friend inline constexpr bool is_flag_type(Flag) { return true; }; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 4747ce609..da6b492d3 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -161,6 +161,8 @@ private: bool listShowReactPremiumError( not_null item, const Data::ReactionId &id) override; + base::unique_qptr listFillSenderUserpicMenu( + PeerId userpicPeerId) override; void listWindowSetInnerFocus() override; bool listAllowsDragForward() override; void listLaunchDrag( @@ -828,6 +830,11 @@ bool Item::listShowReactPremiumError( return false; } +base::unique_qptr Item::listFillSenderUserpicMenu( + PeerId userpicPeerId) { + return nullptr; +} + void Item::listWindowSetInnerFocus() { } diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index a764199da..5d3025d45 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -98,7 +98,7 @@ namespace { text.size(), Data::SerializeCustomEmojiId(document)) }, }; - }); + }) | rpl::map_error_to_done(); } [[nodiscard]] rpl::producer PeerCustomStatus( diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 78cd3eda7..03345d97a 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_components.h" #include "history/history_item_text.h" #include "history/view/history_view_schedule_box.h" #include "history/view/media/history_view_media.h" @@ -102,6 +103,7 @@ namespace { constexpr auto kRescheduleLimit = 20; constexpr auto kTagNameLimit = 12; +constexpr auto kPublicPostLinkToastDuration = 4 * crl::time(1000); bool HasEditMessageAction( const ContextMenuRequest &request, @@ -530,7 +532,7 @@ bool AddRescheduleAction( const auto owner = &request.navigation->session().data(); const auto goodSingle = HasEditMessageAction(request, list) - && request.item->isScheduled(); + && request.item->allowsReschedule(); const auto goodMany = [&] { if (goodSingle) { return false; @@ -542,7 +544,7 @@ bool AddRescheduleAction( if (items.size() > kRescheduleLimit) { return false; } - return ranges::all_of(items, &SelectedItem::canSendNow); + return ranges::all_of(items, &SelectedItem::canReschedule); }(); if (!goodSingle && !goodMany) { return false; @@ -1297,15 +1299,14 @@ base::unique_qptr FillContextMenu( HistoryView::EmojiPacksSource::Message, list->controller()); } - /*{ + /*if (item) { const auto added = (result->actions().size() > wasAmount); - if (!added) { - result->addSeparator(); - } AddSelectRestrictionAction(result, item, !added); }*/ if (hasWhoReactedItem) { AddWhoReactedAction(result, list, item, list->controller()); + } else if (item) { + MaybeAddWhenEditedAction(result, item); } return result; @@ -1327,12 +1328,17 @@ void CopyPostLink( return; } const auto inRepliesContext = (context == Context::Replies); + const auto forceNonPublicLink = base::IsCtrlPressed(); QGuiApplication::clipboard()->setText( item->history()->session().api().exportDirectMessageLink( item, - inRepliesContext)); + inRepliesContext, + forceNonPublicLink)); const auto isPublicLink = [&] { + if (forceNonPublicLink) { + return false; + } const auto channel = item->history()->peer->asChannel(); Assert(channel != nullptr); if (const auto rootId = item->replyToTop()) { @@ -1348,10 +1354,20 @@ void CopyPostLink( } return channel->hasUsername(); }(); - - show->showToast(isPublicLink - ? tr::lng_channel_public_link_copied(tr::now) - : tr::lng_context_about_private_link(tr::now)); + if (isPublicLink) { + show->showToast({ + .text = tr::lng_channel_public_link_copied( + tr::now, Ui::Text::Bold + ).append('\n').append(Platform::IsMac() + ? tr::lng_public_post_private_hint_cmd(tr::now) + : tr::lng_public_post_private_hint_ctrl(tr::now)), + .duration = kPublicPostLinkToastDuration, + }); + } else { + show->showToast(isPublicLink + ? tr::lng_channel_public_link_copied(tr::now) + : tr::lng_context_about_private_link(tr::now)); + } } void CopyStoryLink( @@ -1461,6 +1477,24 @@ void AddSaveSoundForNotifications( }, &st::menuIconSoundAdd); } +void AddWhenEditedActionHelper( + not_null menu, + not_null item, + bool insertSeparator) { + if (item->history()->peer->isUser()) { + if (const auto edited = item->Get()) { + if (!item->hideEditedBadge()) { + if (insertSeparator && !menu->empty()) { + menu->addSeparator(&st::expandedMenuSeparator); + } + menu->addAction(Ui::WhenReadContextAction( + menu.get(), + Api::WhenEdited(item->from(), edited->date))); + } + } + } +} + void AddWhoReactedAction( not_null menu, not_null context, @@ -1501,16 +1535,19 @@ void AddWhoReactedAction( strong->hideMenu(); } if (const auto item = controller->session().data().message(itemId)) { - controller->window().show(Reactions::FullListBox( - controller, - item, - {}, - whoReadIds)); + controller->showSection( + std::make_shared( + whoReadIds, + itemId, + HistoryView::Reactions::DefaultSelectedTab( + item, + whoReadIds))); } }; if (!menu->empty()) { menu->addSeparator(&st::expandedMenuSeparator); } + AddWhenEditedActionHelper(menu, item, false); if (item->history()->peer->isUser()) { menu->addAction(Ui::WhenReadContextAction( menu.get(), @@ -1526,6 +1563,12 @@ void AddWhoReactedAction( } } +void MaybeAddWhenEditedAction( + not_null menu, + not_null item) { + AddWhenEditedActionHelper(menu, item, true); +} + void AddEditTagAction( not_null menu, const Data::ReactionId &id, @@ -1669,10 +1712,10 @@ void ShowWhoReactedMenu( }; const auto showAllChosen = [=, itemId = item->fullId()]{ if (const auto item = controller->session().data().message(itemId)) { - controller->window().show(Reactions::FullListBox( - controller, - item, - id)); + controller->showSection(std::make_shared( + nullptr, + itemId, + HistoryView::Reactions::DefaultSelectedTab(item, id))); } }; const auto owner = &controller->session().data(); @@ -1882,6 +1925,9 @@ void AddSelectRestrictionAction( || item->isSponsored()) { return; } + if (addIcon && !menu->empty()) { + menu->addSeparator(); + } auto button = base::make_unique_q( menu->menu(), menu->st().menu, diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 1b14804aa..1d02e64c2 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -84,6 +84,9 @@ void AddWhoReactedAction( not_null context, not_null item, not_null controller); +void MaybeAddWhenEditedAction( + not_null menu, + not_null item); void ShowWhoReactedMenu( not_null*> menu, QPoint position, diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 609d6521a..7163a1b07 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -117,7 +117,8 @@ bool DefaultElementDelegate::elementUnderCursor( return false; } -SelectionModeResult DefaultElementDelegate::elementInSelectionMode() { +SelectionModeResult DefaultElementDelegate::elementInSelectionMode( + const Element *view) { return {}; } @@ -270,6 +271,9 @@ QString DateTooltipText(not_null view) { const auto format = QLocale::LongFormat; const auto item = view->data(); auto dateText = locale.toString(view->dateTime(), format); + if (item->awaitingVideoProcessing()) { + dateText += '\n' + tr::lng_approximate_about(tr::now); + } if (const auto editedDate = view->displayedEditDate()) { dateText += '\n' + tr::lng_edited_date( tr::now, diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index be41e07dc..e8a5a1c53 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -80,7 +80,8 @@ class ElementDelegate { public: virtual Context elementContext() = 0; virtual bool elementUnderCursor(not_null view) = 0; - virtual SelectionModeResult elementInSelectionMode() = 0; + virtual SelectionModeResult elementInSelectionMode( + const Element *view) = 0; virtual bool elementIntersectsRange( not_null view, int from, @@ -136,7 +137,7 @@ public: class DefaultElementDelegate : public ElementDelegate { public: bool elementUnderCursor(not_null view) override; - SelectionModeResult elementInSelectionMode() override; + SelectionModeResult elementInSelectionMode(const Element *view) override; bool elementIntersectsRange( not_null view, int from, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 3bf556db6..95c48c0e2 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_text.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_sticker.h" +#include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "history/view/history_view_context_menu.h" @@ -29,9 +30,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" #include "mainwindow.h" #include "mainwidget.h" -#include "core/click_handler_types.h" #include "core/application.h" +#include "core/click_handler_types.h" #include "core/core_settings.h" +#include "core/phone_click_handler.h" #include "apiwrap.h" #include "api/api_who_reacted.h" #include "api/api_views.h" @@ -174,6 +176,11 @@ bool WindowListDelegate::listShowReactPremiumError( return Window::ShowReactPremiumError(_window, item, id); } +auto WindowListDelegate::listFillSenderUserpicMenu(PeerId userpicPeerId) +-> base::unique_qptr { + return nullptr; +} + void WindowListDelegate::listWindowSetInnerFocus() { _window->widget()->setInnerFocus(); } @@ -1187,6 +1194,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { result.canDelete = selection.canDelete; result.canForward = selection.canForward; result.canSendNow = selection.canSendNow; + result.canReschedule = selection.canReschedule; return result; }; auto items = SelectedItems(); @@ -1325,6 +1333,7 @@ bool ListWidget::addToSelection( iterator->second.canDelete = item->canDelete(); iterator->second.canForward = item->allowsForward() && !item->isDeleted(); iterator->second.canSendNow = item->allowsSendNow(); + iterator->second.canReschedule = item->allowsReschedule(); return true; } @@ -1787,7 +1796,11 @@ bool ListWidget::elementUnderCursor( return (_overElement == view); } -SelectionModeResult ListWidget::elementInSelectionMode() { +SelectionModeResult ListWidget::elementInSelectionMode( + const HistoryView::Element *view) { + if (view && !_delegate->listIsItemGoodForSelection(view->data())) { + return {}; + } return inSelectionMode(); } @@ -2796,10 +2809,13 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : _overElement ? _overElement->data().get() : nullptr; - const auto clickedReaction = link - ? link->property( - kReactionsCountEmojiProperty).value() - : Data::ReactionId(); + const auto clickedReaction = Reactions::ReactionIdOfLink(link); + const auto linkPhoneNumber = link + ? link->property(kPhoneNumberLinkProperty).toString() + : QString(); + const auto linkUserpicPeerId = (link && _overSenderUserpic) + ? PeerId(link->property(kPeerLinkPeerIdProperty).toULongLong()) + : PeerId(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() && overItem @@ -2814,6 +2830,19 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _whoReactedMenuLifetime); e->accept(); return; + } else if (!linkPhoneNumber.isEmpty()) { + PhoneClickHandler(&session(), linkPhoneNumber).onClick( + prepareClickContext( + Qt::LeftButton, + _overItemExact ? _overItemExact->fullId() : FullMsgId())); + return; + } else if (linkUserpicPeerId) { + _menu = _delegate->listFillSenderUserpicMenu(linkUserpicPeerId); + if (_menu) { + _menu->popup(e->globalPos()); + e->accept(); + return; + } } auto request = ContextMenuRequest(controller()); @@ -3574,6 +3603,15 @@ ClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) { }; } +ClickContext ListWidget::prepareClickContext( + Qt::MouseButton button, + FullMsgId itemId) { + return { + button, + QVariant::fromValue(prepareClickHandlerContext(itemId)), + }; +} + int ListWidget::SelectionViewOffset( not_null inner, not_null view) { @@ -3635,6 +3673,7 @@ void ListWidget::mouseActionUpdate() { auto inTextSelection = (_overState.pointState != PointState::Outside) && (_overState.itemId == _pressState.itemId) && hasSelectedText(); + auto dragStateUserpic = false; const auto overReaction = reactionView && reactionState.link; if (overReaction) { dragState = reactionState; @@ -3733,6 +3772,7 @@ void ListWidget::mouseActionUpdate() { // stop enumeration if we've found a userpic under the cursor if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) { dragState = TextState(nullptr, view->fromPhotoLink()); + dragStateUserpic = true; _overItemExact = nullptr; lnkhost = view; return false; @@ -3744,6 +3784,7 @@ void ListWidget::mouseActionUpdate() { } } const auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost); + _overSenderUserpic = dragStateUserpic; if (lnkChanged || dragState.cursor != _mouseCursorState) { Ui::Tooltip::Hide(); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 02ec9a370..a21cea06e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -80,6 +80,7 @@ struct SelectedItem { bool canDelete = false; bool canForward = false; bool canSendNow = false; + bool canReschedule = false; }; struct MessagesBar { @@ -184,6 +185,8 @@ public: virtual bool listShowReactPremiumError( not_null item, const Data::ReactionId &id) = 0; + virtual base::unique_qptr listFillSenderUserpicMenu( + PeerId userpicPeerId) = 0; virtual void listWindowSetInnerFocus() = 0; virtual bool listAllowsDragForward() = 0; virtual void listLaunchDrag( @@ -217,6 +220,8 @@ public: bool listShowReactPremiumError( not_null item, const Data::ReactionId &id) override; + base::unique_qptr listFillSenderUserpicMenu( + PeerId userpicPeerId) override; void listWindowSetInnerFocus() override; bool listAllowsDragForward() override; void listLaunchDrag( @@ -232,6 +237,7 @@ struct SelectionData { bool canDelete = false; bool canForward = false; bool canSendNow = false; + bool canReschedule = false; }; using SelectedMap = base::flat_map< @@ -354,6 +360,9 @@ public: int top) const; [[nodiscard]] ClickHandlerContext prepareClickHandlerContext( FullMsgId id); + [[nodiscard]] ClickContext prepareClickContext( + Qt::MouseButton button, + FullMsgId itemId); // AbstractTooltipShower interface QString tooltipText() const override; @@ -389,7 +398,7 @@ public: // ElementDelegate interface. Context elementContext() override; bool elementUnderCursor(not_null view) override; - SelectionModeResult elementInSelectionMode() override; + SelectionModeResult elementInSelectionMode(const Element *view) override; bool elementIntersectsRange( not_null view, int from, @@ -806,6 +815,7 @@ private: CursorState _mouseCursorState = CursorState(); uint16 _mouseTextSymbol = 0; bool _pressWasInactive = false; + bool _overSenderUserpic = false; bool _selectEnabled = false; HistoryItem *_selectedTextItem = nullptr; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 4d8f480ed..1e18a5cd8 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1132,7 +1132,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (hasGesture) { p.translate(context.gestureHorizontal.translation, 0); } - const auto selectionModeResult = delegate()->elementInSelectionMode(); + const auto selectionModeResult = delegate()->elementInSelectionMode(this); const auto selectionTranslation = (selectionModeResult.progress > 0) ? (selectionModeResult.progress * AdditionalSpaceForSelectionCheckbox(this, g)) @@ -1647,18 +1647,20 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto o = ScopedPainterOpacity(p, progress); const auto &st = st::msgSelectionCheck; const auto right = delegate()->elementIsChatWide() - ? (st::msgMaxWidth - + st::msgPhotoSkip - + st::msgSelectionOffset - + st::msgPadding.left() - + st.size) + ? std::min( + int(_bubbleWidthLimit + + st::msgPhotoSkip + + st::msgSelectionOffset + + st::msgPadding.left() + + st.size), + width()) : width(); const auto pos = QPoint( (right - (st::msgSelectionOffset * progress - st.size) / 2 - st::msgPadding.right() / 2 - st.size), - g.y() + (g.height() - st.size) / 2); + rect::bottom(g) - st.size - st::msgSelectionBottomSkip); { p.setPen(QPen(st.border, st.width)); p.setBrush(context.st->msgServiceBg()); @@ -2163,6 +2165,7 @@ PointState Message::pointState(QPoint point) const { // Entry page is always a bubble bottom. auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || check || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); if (item->repliesAreComments() || item->externalReply()) { g.setHeight(g.height() - st::historyCommentsButtonHeight); @@ -2203,11 +2206,18 @@ PointState Message::pointState(QPoint point) const { trect.setHeight(trect.height() - entryHeight); } - auto mediaHeight = media->height(); - auto mediaLeft = trect.x() - st::msgPadding.left(); - auto mediaTop = (trect.y() + trect.height() - mediaHeight); - - if (point.y() >= mediaTop && point.y() < mediaTop + mediaHeight) { + const auto mediaHeight = mediaDisplayed ? media->height() : 0; + const auto mediaLeft = trect.x() - st::msgPadding.left(); + const auto mediaTop = (!mediaDisplayed || _invertMedia) + ? (trect.y() + (mediaOnTop ? 0 : st::mediaInBubbleSkip)) + : (trect.y() + trect.height() - mediaHeight); + if (mediaDisplayed && _invertMedia) { + trect.setY(mediaTop + + mediaHeight + + (mediaOnBottom ? 0 : st::mediaInBubbleSkip)); + } + if (point.y() >= mediaTop + && point.y() < mediaTop + mediaHeight) { return media->pointState(point - QPoint(mediaLeft, mediaTop)); } } @@ -3909,7 +3919,7 @@ bool Message::displayFastReply() const { return hasFastReply() && data()->isRegular() && canSendAnything() - && !delegate()->elementInSelectionMode().inSelectionMode; + && !delegate()->elementInSelectionMode(this).inSelectionMode; } bool Message::displayRightActionComments() const { @@ -4077,7 +4087,7 @@ void Message::drawRightAction( ClickHandlerPtr Message::rightActionLink( std::optional pressPoint) const { - if (delegate()->elementInSelectionMode().progress > 0) { + if (delegate()->elementInSelectionMode(this).progress > 0) { return nullptr; } ensureRightAction(); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 4ddfab3bf..164d2b833 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_view_swipe.h" #include "ui/chat/pinned_bar.h" #include "ui/chat/chat_style.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" @@ -906,7 +907,7 @@ void RepliesWidget::setupSwipeReply() { } }, [=, show = controller()->uiShow()](int cursorTop) { auto result = HistoryView::SwipeHandlerFinishData(); - if (_inner->elementInSelectionMode().inSelectionMode) { + if (_inner->elementInSelectionMode(nullptr).inSelectionMode) { return result; } const auto view = _inner->lookupItemByY(cursorTop); @@ -923,8 +924,9 @@ void RepliesWidget::setupSwipeReply() { result.callback = [=, itemId = view->data()->fullId()] { const auto still = show->session().data().message(itemId); const auto view = _inner->viewByPosition(still->position()); - const auto selected = view->selectedQuote( - _inner->getSelectedTextRange(still)); + const auto selected = view + ? view->selectedQuote(_inner->getSelectedTextRange(still)) + : SelectedQuote(); const auto replyToItemId = (selected.item ? selected.item : still)->fullId(); @@ -1232,6 +1234,7 @@ void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _composeControls->cancelReplyMessage(); @@ -2712,6 +2715,23 @@ Ui::ChatPaintContext RepliesWidget::listPreparePaintContext( return context; } +base::unique_qptr RepliesWidget::listFillSenderUserpicMenu( + PeerId userpicPeerId) { + const auto searchInEntry = _topic + ? Dialogs::Key(_topic) + : Dialogs::Key(_history); + auto menu = base::make_unique_q( + this, + st::popupMenuWithIcons); + Window::FillSenderUserpicMenu( + controller(), + _history->owner().peer(userpicPeerId), + _composeControls->fieldForMention(), + searchInEntry, + Ui::Menu::CreateAddActionCallback(menu.get())); + return menu->empty() ? nullptr : std::move(menu); +} + void RepliesWidget::setupEmptyPainter() { Expects(_topic != nullptr); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 71321f14c..1312eabd7 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -183,6 +183,8 @@ public: not_null tracker) override; Ui::ChatPaintContext listPreparePaintContext( Ui::ChatPaintContextArgs &&args) override; + base::unique_qptr listFillSenderUserpicMenu( + PeerId userpicPeerId) override; // CornerButtonsDelegate delegate. void cornerButtonsShowAtPosition( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 113315ef5..279a1d795 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -16,10 +16,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_drag_area.h" #include "history/history_item_helpers.h" // GetErrorTextForSending. #include "menu/menu_send.h" // SendMenu::Type. +#include "ui/widgets/buttons.h" +#include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/chat/chat_style.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" #include "ui/ui_utility.h" #include "api/api_editing.h" #include "api/api_sending.h" @@ -34,7 +39,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/mime_type.h" #include "chat_helpers/tabbed_selector.h" #include "main/main_session.h" +#include "mainwindow.h" #include "data/components/scheduled_messages.h" +#include "data/data_document.h" +#include "data/data_file_origin.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_session.h" @@ -59,12 +67,47 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { +namespace { -ScheduledMemento::ScheduledMemento(not_null history) +constexpr auto kVideoProcessingInfoDuration = 4 * crl::time(1000); + +[[nodiscard]] DocumentData *FindVideoFile(not_null item) { + const auto fromItem = [](not_null item) { + if (const auto media = item->media()) { + if (const auto document = media->document()) { + if (document->isVideoFile()) { + return document; + } + } + } + return (DocumentData*)nullptr; + }; + if (const auto group = item->history()->owner().groups().find(item)) { + for (const auto &entry : group->items) { + if (const auto result = fromItem(entry)) { + return result; + } + } + } else if (const auto result = fromItem(item)) { + return result; + } + return nullptr; +} + +} // namespace + +ScheduledMemento::ScheduledMemento( + not_null history, + MsgId sentToScheduledId) : _history(history) -, _forumTopic(nullptr) { +, _forumTopic(nullptr) +, _sentToScheduledId(sentToScheduledId) { const auto list = _history->session().scheduledMessages().list(_history); - if (!list.ids.empty()) { + if (sentToScheduledId) { + _list.setScrollTopState({ + .item = { .fullId = { _history->peer->id, sentToScheduledId } }, + }); + } else if (!list.ids.empty()) { _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); } } @@ -75,15 +118,15 @@ ScheduledMemento::ScheduledMemento(not_null forumTopic) const auto list = _history->session().scheduledMessages().list( _forumTopic); if (!list.ids.empty()) { - _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); + _list.setScrollTopState({ .item = {.fullId = list.ids.front() } }); } } object_ptr ScheduledMemento::createWidget( - QWidget *parent, - not_null controller, - Window::Column column, - const QRect &geometry) { + QWidget *parent, + not_null controller, + Window::Column column, + const QRect &geometry) { if (column == Window::Column::Third) { return nullptr; } @@ -125,9 +168,9 @@ ScheduledWidget::ScheduledWidget( .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), })) , _cornerButtons( - _scroll.data(), - controller->chatStyle(), - static_cast(this)) { + _scroll.data(), + controller->chatStyle(), + static_cast(this)) { controller->chatStyle()->paletteChanged( ) | rpl::start_with_next([=] { _scroll->updateBars(); @@ -217,83 +260,83 @@ ScheduledWidget::~ScheduledWidget() = default; void ScheduledWidget::setupComposeControls() { auto writeRestriction = _forumTopic ? [&] { - auto topicWriteRestrictions = rpl::single( - ) | rpl::then(session().changes().topicUpdates( - Data::TopicUpdate::Flag::Closed - ) | rpl::filter([=](const Data::TopicUpdate &update) { - return (update.topic->history() == _history) - && (update.topic->rootId() == _forumTopic->rootId()); - }) | rpl::to_empty) | rpl::map([=] { - return (!_forumTopic - || _forumTopic->canToggleClosed() - || !_forumTopic->closed()) - ? std::optional() - : tr::lng_forum_topic_closed(tr::now); - }); - return rpl::combine( - session().changes().peerFlagsValue( - _history->peer, - Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer), - std::move(topicWriteRestrictions) - ) | rpl::map([=]( - auto, - auto, - std::optional topicRestriction) { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - const auto canSendAnything = Data::CanSendAnyOf( - _forumTopic, - allWithoutPolls); - const auto restriction = Data::RestrictionError( - _history->peer, - ChatRestriction::SendOther); - auto text = !canSendAnything - ? (restriction - ? restriction - : topicRestriction - ? std::move(topicRestriction) - : tr::lng_group_not_accessible(tr::now)) + auto topicWriteRestrictions = rpl::single( + ) | rpl::then(session().changes().topicUpdates( + Data::TopicUpdate::Flag::Closed + ) | rpl::filter([=](const Data::TopicUpdate &update) { + return (update.topic->history() == _history) + && (update.topic->rootId() == _forumTopic->rootId()); + }) | rpl::to_empty) | rpl::map([=] { + return (!_forumTopic + || _forumTopic->canToggleClosed() + || !_forumTopic->closed()) + ? std::optional() + : tr::lng_forum_topic_closed(tr::now); + }); + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer), + std::move(topicWriteRestrictions) + ) | rpl::map([=]( + auto, + auto, + std::optional topicRestriction) { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _forumTopic, + allWithoutPolls); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction : topicRestriction ? std::move(topicRestriction) - : std::optional(); - return text ? Controls::WriteRestriction{ - .text = std::move(*text), - .type = Controls::WriteRestrictionType::Rights, - } : Controls::WriteRestriction(); - }) | rpl::type_erased(); - }() + : tr::lng_group_not_accessible(tr::now)) + : topicRestriction + ? std::move(topicRestriction) + : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }) | rpl::type_erased(); + }() : [&] { - return rpl::combine( - session().changes().peerFlagsValue( - _history->peer, - Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer) - ) | rpl::map([=] { - const auto allWithoutPolls = Data::AllSendRestrictions() - & ~ChatRestriction::SendPolls; - const auto canSendAnything = Data::CanSendAnyOf( - _history->peer, - allWithoutPolls, - false); - const auto restriction = Data::RestrictionError( - _history->peer, - ChatRestriction::SendOther); - auto text = !canSendAnything - ? (restriction - ? restriction - : tr::lng_group_not_accessible(tr::now)) - : std::optional(); - return text ? Controls::WriteRestriction{ - .text = std::move(*text), - .type = Controls::WriteRestrictionType::Rights, - } : Controls::WriteRestriction(); - }) | rpl::type_erased(); - }(); + return rpl::combine( + session().changes().peerFlagsValue( + _history->peer, + Data::PeerUpdate::Flag::Rights), + Data::CanSendAnythingValue(_history->peer) + ) | rpl::map([=] { + const auto allWithoutPolls = Data::AllSendRestrictions() + & ~ChatRestriction::SendPolls; + const auto canSendAnything = Data::CanSendAnyOf( + _history->peer, + allWithoutPolls, + false); + const auto restriction = Data::RestrictionError( + _history->peer, + ChatRestriction::SendOther); + auto text = !canSendAnything + ? (restriction + ? restriction + : tr::lng_group_not_accessible(tr::now)) + : std::optional(); + return text ? Controls::WriteRestriction{ + .text = std::move(*text), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }) | rpl::type_erased(); + }(); _composeControls->setHistory({ .history = _history.get(), .writeRestriction = std::move(writeRestriction), - }); + }); _composeControls->height( ) | rpl::start_with_next([=] { @@ -316,7 +359,7 @@ void ScheduledWidget::setupComposeControls() { _composeControls->sendVoiceRequests( ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { - sendVoice(data.bytes, data.waveform, data.duration); + sendVoice(std::move(data)); }, lifetime()); _composeControls->sendCommandRequests( @@ -401,8 +444,8 @@ void ScheduledWidget::setupComposeControls() { }, lifetime()); _composeControls->setMimeDataHook([=]( - not_null data, - Ui::InputField::MimeAction action) { + not_null data, + Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { @@ -434,7 +477,7 @@ void ScheduledWidget::chooseAttach() { const auto filter = FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( - FileDialog::OpenResult &&result) { + FileDialog::OpenResult &&result) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { return; } @@ -442,7 +485,7 @@ void ScheduledWidget::chooseAttach() { if (!result.remoteContent.isEmpty()) { auto read = Images::Read({ .content = result.remoteContent, - }); + }); if (!read.image.isNull() && !read.animated) { confirmSendingFiles( std::move(read.image), @@ -462,9 +505,9 @@ void ScheduledWidget::chooseAttach() { } bool ScheduledWidget::confirmSendingFiles( - not_null data, - std::optional overrideSendImagesAsPhotos, - const QString &insertTextOnCancel) { + not_null data, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { const auto hasImage = data->hasImage(); const auto premium = controller()->session().user()->isPremium(); @@ -496,8 +539,8 @@ bool ScheduledWidget::confirmSendingFiles( } bool ScheduledWidget::confirmSendingFiles( - Ui::PreparedList &&list, - const QString &insertTextOnCancel) { + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { if (_composeControls->confirmMediaEdit(list)) { return true; } else if (showSendingFilesError(list)) { @@ -515,11 +558,11 @@ bool ScheduledWidget::confirmSendingFiles( SendMenu::Details()); box->setConfirmedCallback(crl::guard(this, [=]( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { sendingFilesConfirmed( std::move(list), way, @@ -537,11 +580,11 @@ bool ScheduledWidget::confirmSendingFiles( } void ScheduledWidget::sendingFilesConfirmed( - Ui::PreparedList &&list, - Ui::SendFilesWay way, - TextWithTags &&caption, - Api::SendOptions options, - bool ctrlShiftEnter) { + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { Expects(list.filesToProcess.empty()); if (showSendingFilesError(list, way.sendImagesAsPhotos())) { @@ -573,10 +616,10 @@ void ScheduledWidget::sendingFilesConfirmed( } bool ScheduledWidget::confirmSendingFiles( - QImage &&image, - QByteArray &&content, - std::optional overrideSendImagesAsPhotos, - const QString &insertTextOnCancel) { + QImage &&image, + QByteArray &&content, + std::optional overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { if (image.isNull()) { return false; } @@ -612,8 +655,8 @@ void ScheduledWidget::checkReplyReturns() { } void ScheduledWidget::uploadFile( - const QByteArray &fileContent, - SendMediaType type) { + const QByteArray &fileContent, + SendMediaType type) { const auto callback = [=](Api::SendOptions options) { session().api().sendFile( fileContent, @@ -625,13 +668,13 @@ void ScheduledWidget::uploadFile( } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list) const { + const Ui::PreparedList &list) const { return showSendingFilesError(list, std::nullopt); } bool ScheduledWidget::showSendingFilesError( - const Ui::PreparedList &list, - std::optional compress) const { + const Ui::PreparedList &list, + std::optional compress) const { const auto text = [&] { using Error = Ui::PreparedList::Error; const auto peer = _history->peer; @@ -664,7 +707,7 @@ bool ScheduledWidget::showSendingFilesError( } Api::SendAction ScheduledWidget::prepareSendAction( - Api::SendOptions options) const { + Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.options.sendAs = _composeControls->sendAsPeer(); if (_forumTopic) { @@ -724,26 +767,22 @@ void ScheduledWidget::send(Api::SendOptions options) { _composeControls->focus(); } -void ScheduledWidget::sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration) { +void ScheduledWidget::sendVoice(const Controls::VoiceToSend &data) { const auto callback = [=](Api::SendOptions options) { - sendVoice(bytes, waveform, duration, options); + sendVoice(base::duplicate(data), options); }; controller()->show( PrepareScheduleBox(this, _show, sendMenuDetails(), callback)); } void ScheduledWidget::sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration, + const Controls::VoiceToSend &data, Api::SendOptions options) { session().api().sendVoiceMessage( - bytes, - waveform, - duration, + data.bytes, + data.waveform, + data.duration, + data.video, prepareSendAction(options)); _composeControls->clearListenState(); } @@ -1075,6 +1114,24 @@ void ScheduledWidget::saveState(not_null memento) { void ScheduledWidget::restoreState(not_null memento) { _inner->restoreState(memento->list()); + if (const auto id = memento->sentToScheduledId()) { + const auto item = _history->owner().message(_history->peer, id); + if (item) { + controller()->showToast({ + .title = tr::lng_scheduled_video_tip_title(tr::now), + .text = { tr::lng_scheduled_video_tip_text(tr::now) }, + .attach = RectPart::Top, + .duration = kVideoProcessingInfoDuration, + }); + clearProcessingVideoTracking(false); + _processingVideoPosition = item->position(); + _processingVideoTipTimer.setCallback([=] { + _processingVideoCanShow = true; + updateInnerVisibleArea(); + }); + _processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration); + } + } } void ScheduledWidget::resizeEvent(QResizeEvent *e) { @@ -1146,9 +1203,153 @@ void ScheduledWidget::updateInnerVisibleArea() { checkReplyReturns(); } const auto scrollTop = _scroll->scrollTop(); - _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + const auto scrollBottom = scrollTop + _scroll->height(); + _inner->setVisibleTopBottom(scrollTop, scrollBottom); _cornerButtons.updateJumpDownVisibility(); _cornerButtons.updateUnreadThingsVisibility(); + if (!_processingVideoLifetime) { + if (const auto &position = _processingVideoPosition) { + if (const auto view = _inner->viewByPosition(position)) { + initProcessingVideoView(view); + } + } + } + checkProcessingVideoTooltip(scrollTop, scrollBottom); +} + +void ScheduledWidget::initProcessingVideoView(not_null view) { + _processingVideoView = view; + + controller()->session().data().sentFromScheduled( + ) | rpl::start_with_next([=](const Data::SentFromScheduled &value) { + if (value.item->position() == _processingVideoPosition) { + controller()->showPeerHistory( + value.item->history(), + Window::SectionShow::Way::Backward, + value.sentId); + } + }, _processingVideoLifetime); + + controller()->session().data().viewRemoved( + ) | rpl::start_with_next([=](not_null view) { + if (view == _processingVideoView.get()) { + const auto position = _processingVideoPosition; + if (const auto now = _inner->viewByPosition(position)) { + _processingVideoView = now; + updateProcessingVideoTooltipPosition(); + } else { + clearProcessingVideoTracking(true); + } + } + }, _processingVideoLifetime); + + controller()->session().data().viewResizeRequest( + ) | rpl::start_with_next([this](not_null view) { + if (view->delegate() == _inner.data()) { + if (!_processingVideoUpdateScheduled) { + if (const auto tooltip = _processingVideoTooltip.get()) { + _processingVideoUpdateScheduled = true; + crl::on_main(tooltip, [=] { + _processingVideoUpdateScheduled = false; + updateProcessingVideoTooltipPosition(); + }); + } + } + } + }, _processingVideoLifetime); +} + +void ScheduledWidget::clearProcessingVideoTracking(bool fast) { + if (const auto tooltip = _processingVideoTooltip.release()) { + tooltip->toggleAnimated(false); + } + _processingVideoPosition = {}; + if (const auto tooltip = _processingVideoTooltip.release()) { + if (fast) { + tooltip->toggleFast(false); + } else { + tooltip->toggleAnimated(false); + } + } + _processingVideoTooltipShown = false; + _processingVideoCanShow = false; + _processingVideoView = nullptr; + _processingVideoTipTimer.cancel(); + _processingVideoLifetime.destroy(); +} + +void ScheduledWidget::checkProcessingVideoTooltip( + int visibleTop, + int visibleBottom) { + if (_processingVideoTooltip + || _processingVideoTooltipShown + || !_processingVideoCanShow) { + return; + } + const auto view = _processingVideoView.get(); + if (!view) { + _processingVideoCanShow = false; + return; + } + const auto rect = view->effectIconGeometry(); + if (rect.top() > visibleTop + && rect.top() + rect.height() <= visibleBottom) { + showProcessingVideoTooltip(); + } +} + +void ScheduledWidget::updateProcessingVideoTooltipPosition() { + const auto tooltip = _processingVideoTooltip.get(); + if (!tooltip) { + return; + } + const auto view = _processingVideoView.get(); + if (!view) { + clearProcessingVideoTracking(true); + return; + } + const auto shift = view->skipBlockWidth() / 2; + const auto rect = view->effectIconGeometry().translated(shift, 0); + const auto countPosition = [=](QSize size) { + const auto origin = rect.bottomLeft(); + return origin - QPoint( + size.width() / 2, + size.height() + st::processingVideoTipShift); + }; + tooltip->pointAt(rect, RectPart::Top, countPosition); +} + +void ScheduledWidget::showProcessingVideoTooltip() { + _processingVideoTooltipShown = true; + _processingVideoTooltip = std::make_unique( + _inner.data(), + Ui::MakeNiceTooltipLabel( + _inner.data(), + tr::lng_scheduled_video_tip(Ui::Text::WithEntities), + st::processingVideoTipMaxWidth, + st::defaultImportantTooltipLabel), + st::defaultImportantTooltip); + const auto tooltip = _processingVideoTooltip.get(); + const auto weak = QPointer(tooltip); + const auto destroy = [=] { + delete weak.data(); + }; + tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); + tooltip->setHiddenCallback([=] { + const auto tip = _processingVideoTooltip.get(); + if (tooltip == tip) { + _processingVideoTooltip.release(); + } + crl::on_main(tip, [=] { + delete tip; + }); + }); + updateProcessingVideoTooltipPosition(); + tooltip->toggleAnimated(true); + _processingVideoTipTimer.setCallback(crl::guard(tooltip, [=] { + tooltip->toggleAnimated(false); + })); + _processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration); } void ScheduledWidget::showAnimatedHook( @@ -1500,4 +1701,113 @@ void ScheduledWidget::setupDragArea() { areas.photo->setDroppedCallback(droppedCallback(true)); } +bool ShowScheduledVideoPublished( + not_null controller, + const Data::SentFromScheduled &info, + Fn hidden) { + if (!controller->widget()->isActive()) { + return false; + } + const auto document = FindVideoFile(info.item); + if (!document) { + return false; + } + const auto history = info.item->history(); + const auto itemId = info.sentId; + + const auto text = tr::lng_scheduled_video_published( + tr::now, + Ui::Text::Bold); + const auto &st = st::processingVideoToast; + const auto skip = st::processingVideoPreviewSkip; + const auto size = st.style.font->height * 2; + const auto view = tr::lng_scheduled_video_view(tr::now); + const auto additional = QMargins( + skip + size, + 0, + (st::processingVideoView.style.font->width(view) + - (st::processingVideoView.width / 2)), + 0); + + const auto parent = controller->uiShow()->toastParent(); + const auto weak = Ui::Toast::Show(parent, Ui::Toast::Config{ + .text = text, + .padding = rpl::single(additional), + .st = &st, + .attach = RectPart::Top, + .acceptinput = true, + .duration = kVideoProcessingInfoDuration, + }); + const auto strong = weak.get(); + if (!strong) { + return false; + } + const auto widget = strong->widget(); + const auto hideToast = [weak] { + if (const auto strong = weak.get()) { + strong->hideAnimated(); + } + }; + + const auto clickableBackground = Ui::CreateChild( + widget.get()); + clickableBackground->setPointerCursor(false); + clickableBackground->setAcceptBoth(); + clickableBackground->show(); + clickableBackground->addClickHandler([=](Qt::MouseButton button) { + if (button == Qt::RightButton) { + hideToast(); + } + }); + + const auto button = Ui::CreateChild( + widget.get(), + rpl::single(view), + st::processingVideoView); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->show(); + rpl::combine( + widget->sizeValue(), + button->sizeValue() + ) | rpl::start_with_next([=](QSize outer, QSize inner) { + button->moveToRight( + 0, + (outer.height() - inner.height()) / 2, + outer.width()); + clickableBackground->resize(outer); + }, widget->lifetime()); + const auto preview = Ui::CreateChild(widget.get()); + preview->moveToLeft(skip, skip); + preview->resize(size, size); + preview->show(); + + const auto thumbnail = Ui::MakeDocumentThumbnail(document, FullMsgId( + history->peer->id, + itemId)); + thumbnail->subscribeToUpdates([=] { + preview->update(); + }); + preview->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(preview); + const auto image = Images::Round( + thumbnail->image(size), + ImageRoundRadius::Small); + p.drawImage(QRect(0, 0, size, size), image); + }, preview->lifetime()); + + button->setClickedCallback([=] { + controller->showPeerHistory( + history, + Window::SectionShow::Way::Forward, + itemId); + hideToast(); + }); + + if (hidden) { + widget->lifetime().add(std::move(hidden)); + } + return true; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 0a7ff39b0..a2e3a5438 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -21,6 +21,10 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Data { +struct SentFromScheduled; +} // namespace Data + namespace SendMenu { struct Details; } // namespace SendMenu @@ -37,6 +41,7 @@ class PlainShadow; class FlatButton; struct PreparedList; class SendFilesWay; +class ImportantTooltip; } // namespace Ui namespace Profile { @@ -47,6 +52,14 @@ namespace InlineBots { class Result; } // namespace InlineBots +namespace HistoryView::Controls { +struct VoiceToSend; +} // namespace HistoryView::Controls + +namespace Window { +class SessionController; +} // namespace Window + namespace HistoryView { class Element; @@ -195,6 +208,12 @@ private: Data::MessagePosition position, FullMsgId originId = {}); + void initProcessingVideoView(not_null view); + void checkProcessingVideoTooltip(int visibleTop, int visibleBottom); + void showProcessingVideoTooltip(); + void updateProcessingVideoTooltipPosition(); + void clearProcessingVideoTracking(bool fast); + void setupComposeControls(); void setupDragArea(); @@ -207,14 +226,9 @@ private: Api::SendOptions options) const; void send(); void send(Api::SendOptions options); + void sendVoice(const Controls::VoiceToSend &data); void sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration); - void sendVoice( - QByteArray bytes, - VoiceWaveform waveform, - crl::time duration, + const Controls::VoiceToSend &data, Api::SendOptions options); void edit( not_null item, @@ -278,7 +292,16 @@ private: std::unique_ptr _composeControls; bool _skipScrollEvent = false; + Data::MessagePosition _processingVideoPosition; + base::weak_ptr _processingVideoView; + rpl::lifetime _processingVideoLifetime; + std::unique_ptr _stickerToast; + std::unique_ptr _processingVideoTooltip; + base::Timer _processingVideoTipTimer; + bool _processingVideoUpdateScheduled = false; + bool _processingVideoTooltipShown = false; + bool _processingVideoCanShow = false; CornerButtons _cornerButtons; @@ -289,7 +312,9 @@ private: class ScheduledMemento final : public Window::SectionMemento { public: - ScheduledMemento(not_null history); + ScheduledMemento( + not_null history, + MsgId sentToScheduledId = 0); ScheduledMemento(not_null forumTopic); object_ptr createWidget( @@ -298,19 +323,29 @@ public: Window::Column column, const QRect &geometry) override; - not_null getHistory() const { + [[nodiscard]] not_null getHistory() const { return _history; } - not_null list() { + [[nodiscard]] not_null list() { return &_list; } + [[nodiscard]] MsgId sentToScheduledId() const { + return _sentToScheduledId; + } + private: const not_null _history; const Data::ForumTopic *_forumTopic; ListMemento _list; + MsgId _sentToScheduledId = 0; }; +bool ShowScheduledVideoPublished( + not_null controller, + const Data::SentFromScheduled &info, + Fn hidden = nullptr); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index 4ecd9d97a..10875977e 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -337,7 +337,10 @@ void SublistWidget::setInternalState( restoreState(memento); } -bool SublistWidget::searchInChatEmbedded(Dialogs::Key chat, QString query) { +bool SublistWidget::searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom) { const auto sublist = chat.sublist(); if (!sublist || sublist != _sublist) { return false; diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h index 6379c9845..33b655720 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.h @@ -76,7 +76,10 @@ public: return Window::SectionActionResult::Fallback; } - bool searchInChatEmbedded(Dialogs::Key chat, QString query) override; + bool searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom = nullptr) override; // Float player interface. bool floatPlayerHandleWheelEvent(QEvent *e) 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 38dbf90c6..d8558b949 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -80,10 +80,8 @@ namespace { constexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000); -inline bool HasGroupCallMenu(const not_null &peer) { - return !peer->groupCall() - && ((peer->isChannel() && peer->asChannel()->amCreator()) - || (peer->isChat() && peer->asChat()->amCreator())); +[[nodiscard]] inline bool HasGroupCallMenu(not_null peer) { + return !peer->groupCall() && peer->canManageGroupCall(); } QString TopBarNameText( diff --git a/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp b/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp index 51c69c89b..94de0a30c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_custom_emoji.cpp @@ -108,6 +108,9 @@ CustomEmoji::CustomEmoji( } void CustomEmoji::customEmojiResolveDone(not_null document) { + if (!document->sticker()) { + return; + } _resolving = false; const auto id = document->id; for (auto &line : _lines) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 43f08b916..3128b6f4c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_spoiler.h" #include "window/window_session_controller.h" #include "core/application.h" // Application::showDocument. +#include "core/core_settings.h" #include "ui/chat/attach/attach_prepare.h" #include "ui/chat/chat_style.h" #include "ui/image/image.h" @@ -113,8 +114,10 @@ constexpr auto kMaxInlineArea = 1920 * 1080; struct Gif::Streamed { Streamed( + not_null chosen, std::shared_ptr<::Media::Streaming::Document> shared, Fn waitingCallback); + const not_null chosen; ::Media::Streaming::Instance instance; ::Media::Streaming::FrameRequest frozenRequest; QImage frozenFrame; @@ -122,9 +125,11 @@ struct Gif::Streamed { }; Gif::Streamed::Streamed( + not_null chosen, std::shared_ptr<::Media::Streaming::Document> shared, Fn waitingCallback) -: instance(std::move(shared), std::move(waitingCallback)) { +: chosen(chosen) +, instance(std::move(shared), std::move(waitingCallback)) { } [[nodiscard]] bool IsHiddenRoundMessage(not_null parent) { @@ -805,7 +810,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { p, context, fullRight, - fullBottom, + fullBottom - st::msgDateImgDelta, 2 * paintx + paintw, (unwrapped ? InfoDisplayType::Background @@ -1852,13 +1857,21 @@ void Gif::playAnimation(bool autoplay) { } void Gif::createStreamedPlayer() { + const auto quality = Core::App().settings().videoQuality(); + const auto chosen = _data->chooseQuality(_realParent, quality); + if (_streamed && _streamed->chosen == chosen) { + return; + } auto shared = _data->owner().streaming().sharedDocument( + chosen, _data, + _realParent, _realParent->fullId()); if (!shared) { return; } setStreamed(std::make_unique( + chosen, std::move(shared), [=] { repaintStreamedContent(); })); @@ -1869,6 +1882,20 @@ void Gif::createStreamedPlayer() { handleStreamingError(std::move(error)); }, _streamed->instance.lifetime()); + _streamed->instance.switchQualityRequests( + ) | rpl::start_with_next([=](int quality) { + auto now = Core::App().settings().videoQuality(); + if (now.manual || now.height == quality) { + return; + } + Core::App().settings().setVideoQuality({ + .manual = 0, + .height = uint32(quality), + }); + Core::App().saveSettingsDelayed(); + createStreamedPlayer(); + }, _streamed->instance.lifetime()); + if (_streamed->instance.ready()) { streamingReady(base::duplicate(_streamed->instance.info())); } @@ -1916,14 +1943,15 @@ void Gif::handleStreamingUpdate(::Media::Streaming::Update &&update) { v::match(update.data, [&](Information &update) { streamingReady(std::move(update)); - }, [&](const PreloadedVideo &update) { - }, [&](const UpdateVideo &update) { + }, [](PreloadedVideo) { + }, [&](UpdateVideo) { repaintStreamedContent(); - }, [&](const PreloadedAudio &update) { - }, [&](const UpdateAudio &update) { - }, [&](const WaitingForData &update) { - }, [&](MutedByOther) { - }, [&](Finished) { + }, [](PreloadedAudio) { + }, [](UpdateAudio) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { + }, [](Finished) { }); } @@ -1986,10 +2014,12 @@ bool Gif::dataLoaded() const { } bool Gif::needInfoDisplay() const { - if (_parent->data()->isFakeAboutView()) { + const auto item = _parent->data(); + if (item->isFakeAboutView()) { return false; } - return _parent->data()->isSending() + return item->isSending() + || item->awaitingVideoProcessing() || _data->uploading() || _parent->isUnderCursor() || (_parent->delegate()->elementContext() == Context::ChatPreview) diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index 18625b684..077a8ebd9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -110,7 +110,13 @@ QSize MediaGeneric::countOptimalSize() { } QSize MediaGeneric::countCurrentSize(int newWidth) { - return { maxWidth(), minHeight() }; + if (newWidth > maxWidth()) { + newWidth = maxWidth(); + } + for (auto &entry : _entries) { + entry.object->resizeGetHeight(newWidth); + } + return { newWidth, minHeight() }; } void MediaGeneric::draw(Painter &p, const PaintContext &context) const { 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 3e268d17c..3bbec1bc8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -897,9 +897,11 @@ bool GroupedMedia::computeNeedBubble() const { } bool GroupedMedia::needInfoDisplay() const { + const auto item = _parent->data(); return (_mode != Mode::Column) - && (_parent->data()->isSending() - || _parent->data()->hasFailed() + && (item->isSending() + || item->awaitingVideoProcessing() + || item->hasFailed() || _parent->isUnderCursor() || (_parent->delegate()->elementContext() == Context::ChatPreview) || _parent->isLastAndSelfMessage()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index b48a247b2..b67f72b2e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -987,14 +987,15 @@ void Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) { v::match(update.data, [&](Information &update) { streamingReady(std::move(update)); - }, [&](const PreloadedVideo &update) { - }, [&](const UpdateVideo &update) { + }, [](PreloadedVideo) { + }, [&](UpdateVideo) { repaintStreamedContent(); - }, [&](const PreloadedAudio &update) { - }, [&](const UpdateAudio &update) { - }, [&](const WaitingForData &update) { - }, [&](MutedByOther) { - }, [&](Finished) { + }, [](PreloadedAudio) { + }, [](UpdateAudio) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { + }, [](Finished) { }); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index e12ab3d3f..0a9aea114 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -80,7 +80,7 @@ TextWithEntities PremiumGift::subtitle() { ? tr::lng_action_gift_sent_text( tr::now, lt_count, - _data.convertStars, + _data.starsConverted, lt_user, Ui::Text::Bold(_parent->history()->peer->shortName()), Ui::Text::RichLangValue) @@ -89,7 +89,7 @@ TextWithEntities PremiumGift::subtitle() { : tr::lng_action_gift_got_stars_text)( tr::now, lt_count, - _data.convertStars, + _data.starsConverted, Ui::Text::RichLangValue); } const auto isCreditsPrize = creditsPrize(); @@ -151,6 +151,10 @@ rpl::producer PremiumGift::button() { : tr::lng_prize_open(); } +bool PremiumGift::buttonMinistars() { + return true; +} + ClickHandlerPtr PremiumGift::createViewLink() { if (starGift() && outgoingGift()) { return nullptr; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index 00883240d..db976370e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -29,6 +29,7 @@ public: QString title() override; TextWithEntities subtitle() override; rpl::producer button() override; + bool buttonMinistars() override; QString cornerTagText() override; int buttonSkip() override; void draw( diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 0252e3281..cda0204c6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -16,9 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/effects/animation_value.h" +#include "ui/effects/premium_stars_colored.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" #include "styles/style_chat.h" #include "styles/style_premium.h" @@ -98,6 +100,12 @@ ServiceBox::ServiceBox( } }, _lifetime); } + if (_content->buttonMinistars()) { + _button.stars = std::make_unique( + [=](const QRect &) { repaint(); }, + Ui::Premium::MiniStars::Type::SlowStars); + _button.lastFg = std::make_unique(); + } } ServiceBox::~ServiceBox() = default; @@ -117,7 +125,21 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const { const auto radius = st::msgServiceGiftBoxRadius; p.setPen(Qt::NoPen); p.setBrush(context.st->msgServiceBg()); - p.drawRoundedRect(QRect(QPoint(), _innerSize), radius, radius); + p.drawRoundedRect(Rect(_innerSize), radius, radius); + + if (_button.stars) { + const auto &c = context.st->msgServiceFg()->c; + if ((*_button.lastFg) != c) { + _button.lastFg->setRgb(c.red(), c.green(), c.blue()); + const auto padding = _button.size.height() / 2; + _button.stars->setColorOverride(QGradientStops{ + { 0., anim::with_alpha(c, .3) }, + { 1., c }, + }); + _button.stars->setCenter( + Rect(_button.size) - QMargins(padding, 0, padding, 0)); + } + } const auto content = contentRect(); auto top = content.top() + content.height(); @@ -340,7 +362,15 @@ bool ServiceBox::Button::empty() const { void ServiceBox::Button::drawBg(QPainter &p) const { const auto radius = size.height() / 2.; - p.drawRoundedRect(0, 0, size.width(), size.height(), radius, radius); + const auto r = Rect(size); + p.drawRoundedRect(r, radius, radius); + if (stars) { + auto clipPath = QPainterPath(); + clipPath.addRoundedRect(r, radius, radius); + p.setClipPath(clipPath); + stars->paint(p); + p.setClipping(false); + } } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index 2a52e7336..733176c7d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -11,6 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class RippleAnimation; +namespace Premium { +class ColoredMiniStars; +} // namespace Premium } // namespace Ui namespace HistoryView { @@ -28,6 +31,9 @@ public: return top(); } [[nodiscard]] virtual rpl::producer button() = 0; + [[nodiscard]] virtual bool buttonMinistars() { + return false; + } [[nodiscard]] virtual QString cornerTagText() { return {}; } @@ -106,6 +112,8 @@ private: ClickHandlerPtr link; std::unique_ptr ripple; + std::unique_ptr stars; + std::unique_ptr lastFg; mutable QPoint lastPoint; } _button; diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index 66db74036..ba324f7e3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -91,7 +91,8 @@ ThemeDocument::ThemeDocument( : File(parent, parent->data()) , _data(document) , _serviceWidth(serviceWidth) { - Expects(params.has_value() || _data->hasThumbnail() || _data->isTheme()); + Expects(params.has_value() + || (_data && (_data->hasThumbnail() || _data->isTheme()))); if (params) { _background = params->backgroundColors(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 902b984f0..515ab33e1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_sponsored_click_handler.h" #include "history/history.h" #include "history/history_item_components.h" +#include "history/history_item_helpers.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_sponsored.h" @@ -77,11 +78,13 @@ constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000); const auto spoiler = false; for (const auto &item : data.items) { if (const auto document = std::get_if(&item)) { + const auto hasQualitiesList = false; const auto skipPremiumEffect = false; result.push_back(std::make_unique( parent, *document, skipPremiumEffect, + hasQualitiesList, spoiler, /*ttlSeconds = */0)); } else if (const auto photo = std::get_if(&item)) { @@ -148,15 +151,6 @@ constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000); }); } -[[nodiscard]] ClickHandlerPtr AboutSponsoredClickHandler() { - return std::make_shared([=](ClickContext context) { - const auto my = context.other.value(); - if (const auto controller = my.sessionWindow.get()) { - Menu::ShowSponsoredAbout(controller->uiShow()); - } - }); -} - [[nodiscard]] QString LookupFactcheckCountryIso2( not_null item) { const auto info = item->Get(); @@ -570,7 +564,9 @@ QSize WebPage::countOptimalSize() { } // init dimensions - const auto skipBlockWidth = _parent->skipBlockWidth(); + const auto skipBlockWidth = (sponsored && sponsored->hasMedia) + ? 0 + : _parent->skipBlockWidth(); auto maxWidth = skipBlockWidth; auto minHeight = 0; @@ -638,8 +634,10 @@ QSize WebPage::countOptimalSize() { _durationWidth = st::msgDateFont->width(_duration); } if (!_openButton.isEmpty()) { - maxWidth += rect::m::sum::h(st::historyPageButtonPadding) - + _openButton.maxWidth(); + accumulate_max( + maxWidth, + rect::m::sum::h(st::historyPageButtonPadding) + + _openButton.maxWidth()); } maxWidth += rect::m::sum::h(padding); minHeight += rect::m::sum::v(padding); @@ -673,8 +671,8 @@ QSize WebPage::countCurrentSize(int newWidth) { const auto stickerSet = stickerSetData(); const auto factcheck = factcheckData(); const auto sponsored = sponsoredData(); - const auto specialRightPix = ((sponsored && !sponsored->hasMedia) - || stickerSet); + const auto specialRightPix = (stickerSet + || (sponsored && !sponsored->hasMedia && _data->photo)); const auto lineHeight = UnitedLineHeight(); const auto factcheckMetrics = factcheck ? computeFactcheckMetrics(_description.countHeight(innerWidth)) @@ -688,7 +686,7 @@ QSize WebPage::countCurrentSize(int newWidth) { } const auto linesMax = factcheck ? (factcheckMetrics.lines + 1) - : (specialRightPix || isLogEntryOriginal()) + : (sponsored || isLogEntryOriginal()) ? kMaxOriginalEntryLines : 5; const auto siteNameHeight = _siteNameLines ? lineHeight : 0; @@ -721,7 +719,9 @@ QSize WebPage::countCurrentSize(int newWidth) { newHeight += _titleLines * lineHeight; } - const auto descriptionHeight = _description.countHeight(wleft); + const auto descriptionHeight = _description.countHeight(sponsored + ? innerWidth + : wleft); const auto restLines = (linesMax - _siteNameLines - _titleLines); if (descriptionHeight < restLines * descriptionLineHeight) { // We have height for all the lines. @@ -1238,8 +1238,9 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { .position = QPoint( inner.x() + (inner.width() - _openButton.maxWidth()) / 2, end + st::historyPageButtonPadding.top()), - .availableWidth = paintw, + .availableWidth = inner.width(), .now = context.now, + .elisionLines = 1, }); } } @@ -1522,7 +1523,7 @@ void WebPage::clickHandlerPressedChanged( } return; } - if (p == _openl) { + if ((p == _openl) || (sponsoredData() && sponsoredData()->link == p)) { if (pressed) { if (!_ripple) { const auto full = Rect(currentSize()); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 59d89e0e7..9c36be6ce 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -848,4 +848,30 @@ InlineListData InlineListDataFromMessage(not_null message) { return result; } -} // namespace HistoryView +ReactionId ReactionIdOfLink(const ClickHandlerPtr &link) { + return link + ? link->property(kReactionsCountEmojiProperty).value() + : ReactionId(); +} + +ReactionCount ReactionCountOfLink( + HistoryItem *item, + const ClickHandlerPtr &link) { + const auto id = ReactionIdOfLink(link); + if (!item || !id) { + return {}; + } + const auto groups = &item->history()->owner().groups(); + if (const auto group = groups->find(item)) { + item = group->items.front(); + } + const auto &list = item->reactions(); + const auto i = ranges::find(list, id, &Data::MessageReaction::id); + if (i == end(list) || !i->count) { + return {}; + } + const auto formatted = Lang::FormatCountToShort(i->count); + return { .count = i->count, .shortened = formatted.shortened }; +} + +} // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index be7aa4e64..31982fd1c 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -149,4 +149,14 @@ private: [[nodiscard]] InlineListData InlineListDataFromMessage( not_null message); -} // namespace HistoryView +[[nodiscard]] ReactionId ReactionIdOfLink(const ClickHandlerPtr &link); + +struct ReactionCount { + int count = 0; + bool shortened = false; +}; +[[nodiscard]] ReactionCount ReactionCountOfLink( + HistoryItem *item, + const ClickHandlerPtr &link); + +} // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp index f925dcb90..209e73e3c 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp @@ -62,8 +62,8 @@ private: class Controller final : public PeerListController { public: Controller( - not_null window, - not_null item, + not_null window, + FullMsgId itemId, const ReactionId &selected, rpl::producer switches, std::shared_ptr whoReadIds); @@ -73,9 +73,26 @@ public: void rowClicked(not_null row) override; void loadMoreRows() override; + std::unique_ptr createRestoredRow( + not_null peer) override; + + std::unique_ptr saveState() const override; + void restoreState(std::unique_ptr state) override; + private: using AllEntry = std::pair, Data::ReactionId>; + struct SavedState : SavedStateBase { + ReactionId shownReaction; + base::flat_map, uint64> idsMap; + uint64 idsCounter = 0; + std::vector all; + QString allOffset; + std::vector> filtered; + QString filteredOffset; + bool wasLoading = false; + }; + void fillWhoRead(); void loadMore(const ReactionId &reaction); bool appendRow(not_null peer, ReactionId reaction); @@ -88,14 +105,15 @@ private: not_null peer, const ReactionId &reaction) const; - const not_null _window; - const not_null _item; + const not_null _window; + const not_null _peer; + const FullMsgId _itemId; const Ui::Text::CustomEmojiFactory _factory; + const std::shared_ptr _whoReadIds; + const std::vector> _whoRead; MTP::Sender _api; ReactionId _shownReaction; - std::shared_ptr _whoReadIds; - std::vector> _whoRead; mutable base::flat_map, uint64> _idsMap; mutable uint64 _idsCounter = 0; @@ -110,6 +128,22 @@ private: }; +[[nodiscard]] std::vector> ResolveWhoRead( + not_null window, + const std::shared_ptr &whoReadIds) { + if (!whoReadIds || whoReadIds->list.empty()) { + return {}; + } + auto result = std::vector>(); + auto &owner = window->session().data(); + for (const auto &peerWithDate : whoReadIds->list) { + if (const auto peer = owner.peerLoaded(peerWithDate.peer)) { + result.push_back(peer); + } + } + return result; +} + Row::Row( uint64 id, not_null peer, @@ -166,17 +200,19 @@ void Row::rightActionPaint( } Controller::Controller( - not_null window, - not_null item, + not_null window, + FullMsgId itemId, const ReactionId &selected, rpl::producer switches, std::shared_ptr whoReadIds) : _window(window) -, _item(item) +, _peer(window->session().data().peer(itemId.peer)) +, _itemId(itemId) , _factory(Data::ReactedMenuFactory(&window->session())) +, _whoReadIds(whoReadIds) +, _whoRead(ResolveWhoRead(window, _whoReadIds)) , _api(&window->session().mtp()) -, _shownReaction(selected) -, _whoReadIds(whoReadIds) { +, _shownReaction(selected) { std::move( switches ) | rpl::filter([=](const ReactionId &reaction) { @@ -248,14 +284,6 @@ uint64 Controller::id( } void Controller::fillWhoRead() { - if (_whoReadIds && !_whoReadIds->list.empty() && _whoRead.empty()) { - auto &owner = _window->session().data(); - for (const auto &peerWithDate : _whoReadIds->list) { - if (const auto peer = owner.peerLoaded(peerWithDate.peer)) { - _whoRead.push_back(peer); - } - } - } for (const auto &peer : _whoRead) { appendRow(peer, ReactionId()); } @@ -271,6 +299,60 @@ void Controller::loadMoreRows() { loadMore(_shownReaction); } +std::unique_ptr Controller::createRestoredRow( + not_null peer) { + if (_shownReaction.emoji() == u"read"_q) { + return createRow(peer, Data::ReactionId()); + } else if (_shownReaction.empty()) { + const auto i = ranges::find(_all, peer, &AllEntry::first); + const auto reaction = (i != end(_all)) ? i->second : _shownReaction; + return createRow(peer, reaction); + } + return createRow(peer, _shownReaction); +} + +std::unique_ptr Controller::saveState() const { + auto result = PeerListController::saveState(); + + auto my = std::make_unique(); + my->shownReaction = _shownReaction; + my->idsMap = _idsMap; + my->idsCounter = _idsCounter; + my->all = _all; + my->allOffset = _allOffset; + my->filtered = _filtered; + my->filteredOffset = _filteredOffset; + my->wasLoading = (_loadRequestId != 0); + result->controllerState = std::move(my); + return result; +} + +void Controller::restoreState(std::unique_ptr state) { + auto typeErasedState = state + ? state->controllerState.get() + : nullptr; + if (const auto my = dynamic_cast(typeErasedState)) { + if (const auto requestId = base::take(_loadRequestId)) { + _api.request(requestId).cancel(); + } + _shownReaction = my->shownReaction; + _idsMap = std::move(my->idsMap); + _idsCounter = my->idsCounter; + _all = std::move(my->all); + _allOffset = std::move(my->allOffset); + _filtered = std::move(my->filtered); + _filteredOffset = std::move(my->filteredOffset); + if (my->wasLoading) { + loadMoreRows(); + } + PeerListController::restoreState(std::move(state)); + if (delegate()->peerListFullRowsCount()) { + setDescriptionText(QString()); + delegate()->peerListRefreshRows(); + } + } +} + void Controller::loadMore(const ReactionId &reaction) { if (reaction.emoji() == u"read"_q) { loadMore(ReactionId()); @@ -290,8 +372,8 @@ void Controller::loadMore(const ReactionId &reaction) { | (reaction.empty() ? Flag(0) : Flag::f_reaction); _loadRequestId = _api.request(MTPmessages_GetMessageReactionsList( MTP_flags(flags), - _item->history()->peer->input, - MTP_int(_item->id), + _peer->input, + MTP_int(_itemId.msg), Data::ReactionToMTP(reaction), MTP_string(offset), MTP_int(offset.isEmpty() ? kPerPageFirst : kPerPage) @@ -332,7 +414,7 @@ void Controller::rowClicked(not_null row) { const auto window = _window; const auto peer = row->peer(); crl::on_main(window, [=] { - window->show(PrepareShortInfoBox(peer, window)); + window->showPeerInfo(peer); }); } @@ -353,72 +435,75 @@ std::unique_ptr Controller::createRow( _factory, Data::ReactionEntityData(reaction), [=](Row *row) { delegate()->peerListUpdateRow(row); }, - [=] { return _window->isGifPausedAtLeastFor( + [=] { return _window->parentController()->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); }); } } // namespace -object_ptr FullListBox( - not_null window, +Data::ReactionId DefaultSelectedTab( + not_null item, + std::shared_ptr whoReadIds) { + return DefaultSelectedTab(item, {}, std::move(whoReadIds)); +} + +Data::ReactionId DefaultSelectedTab( not_null item, Data::ReactionId selected, std::shared_ptr whoReadIds) { - Expects(IsServerMsgId(item->id)); - - if (!ranges::contains( - item->reactions(), - selected, - &Data::MessageReaction::id)) { + const auto proj = &Data::MessageReaction::id; + if (!ranges::contains(item->reactions(), selected, proj)) { selected = {}; } - if (selected.empty() && whoReadIds && !whoReadIds->list.empty()) { - selected = Data::ReactionId{ u"read"_q }; - } - const auto tabRequests = std::make_shared< - rpl::event_stream>(); - const auto initBox = [=](not_null box) { - box->setNoContentMargin(true); + return (selected.empty() && whoReadIds && !whoReadIds->list.empty()) + ? Data::ReactionId{ u"read"_q } + : selected; +} - auto map = item->reactions(); - if (whoReadIds && !whoReadIds->list.empty()) { - map.push_back({ - .id = Data::ReactionId{ u"read"_q }, - .count = int(whoReadIds->list.size()), - }); - } - const auto tabs = CreateTabs( - box, - Data::ReactedMenuFactory(&item->history()->session()), - [=] { return window->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); }, - map, - selected, - whoReadIds ? whoReadIds->type : Ui::WhoReadType::Reacted); - tabs->changes( - ) | rpl::start_to_stream(*tabRequests, box->lifetime()); - - box->widthValue( - ) | rpl::start_with_next([=](int width) { - tabs->resizeToWidth(width); - tabs->move(0, 0); - }, box->lifetime()); - tabs->heightValue( - ) | rpl::start_with_next([=](int height) { - box->setAddedTopScrollSkip(height); - }, box->lifetime()); - box->addButton(tr::lng_close(), [=] { - box->closeBox(); +not_null CreateReactionsTabs( + not_null parent, + not_null window, + FullMsgId itemId, + Data::ReactionId selected, + std::shared_ptr whoReadIds) { + const auto item = window->session().data().message(itemId); + auto map = item + ? item->reactions() + : std::vector(); + if (whoReadIds && !whoReadIds->list.empty()) { + map.push_back({ + .id = Data::ReactionId{ u"read"_q }, + .count = int(whoReadIds->list.size()), }); - }; - return Box( - std::make_unique( + } + return CreateTabs( + parent, + Data::ReactedMenuFactory(&window->session()), + [=] { return window->parentController()->isGifPausedAtLeastFor( + Window::GifPauseReason::Layer); }, + map, + selected, + whoReadIds ? whoReadIds->type : Ui::WhoReadType::Reacted); +} + +PreparedFullList FullListController( + not_null window, + FullMsgId itemId, + Data::ReactionId selected, + std::shared_ptr whoReadIds) { + Expects(IsServerMsgId(itemId.msg)); + + const auto tab = std::make_shared< + rpl::event_stream>(); + return { + .controller = std::make_unique( window, - item, + itemId, selected, - tabRequests->events(), + tab->events(), whoReadIds), - initBox); + .switchTab = [=](Data::ReactionId id) { tab->fire_copy(id); }, + }; } } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h index b65c21065..be6dc59fe 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" class HistoryItem; +class PeerListController; namespace Data { struct ReactionId; @@ -21,6 +22,7 @@ struct WhoReadList; namespace Window { class SessionController; +class SessionNavigation; } // namespace Window namespace Ui { @@ -29,10 +31,31 @@ class BoxContent; namespace HistoryView::Reactions { -object_ptr FullListBox( - not_null window, +[[nodiscard]] Data::ReactionId DefaultSelectedTab( + not_null item, + std::shared_ptr whoReadIds); + +[[nodiscard]] Data::ReactionId DefaultSelectedTab( not_null item, Data::ReactionId selected, std::shared_ptr whoReadIds = nullptr); +struct Tabs; +[[nodiscard]] not_null CreateReactionsTabs( + not_null parent, + not_null window, + FullMsgId itemId, + Data::ReactionId selected, + std::shared_ptr whoReadIds); + +struct PreparedFullList { + std::unique_ptr controller; + Fn switchTab; +}; +[[nodiscard]] PreparedFullList FullListController( + not_null window, + FullMsgId itemId, + Data::ReactionId selected, + std::shared_ptr whoReadIds = nullptr); + } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h index 51d776004..8e3566552 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_tabs.h @@ -27,7 +27,7 @@ struct Tabs { Fn()> heightValue; }; -not_null CreateTabs( +[[nodiscard]] not_null CreateTabs( not_null parent, Ui::Text::CustomEmojiFactory factory, Fn paused, diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index 84910f9ab..7b2072cda 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -60,13 +60,9 @@ void AddHeader( } // namespace -InnerWidget::InnerWidget( - QWidget *parent, - not_null controller, - not_null peer) +InnerWidget::InnerWidget(QWidget *parent, not_null controller) : VerticalLayout(parent) , _controller(controller) -, _peer(peer) , _show(controller->uiShow()) { } @@ -75,7 +71,7 @@ void InnerWidget::load() { const auto request = [=](Fn done) { const auto api = apiLifetime->make_state( - _peer->asUser()); + peer()->asUser()); api->request( ) | rpl::start_with_error_done([show = _show](const QString &error) { show->showToast(error); @@ -92,18 +88,18 @@ void InnerWidget::load() { _showFinished.events()); _showFinished.events( - ) | rpl::take(1) | rpl::start_with_next([=] { + ) | rpl::take(1) | rpl::start_with_next([=, this, peer = peer()] { request([=](Data::CreditsEarnStatistics state) { _state = state; _loaded.fire(true); fill(); - _peer->session().account().mtpUpdates( + 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) { + if (peerId == peer->id) { request([=](Data::CreditsEarnStatistics state) { _state = state; _stateUpdated.fire({}); @@ -120,6 +116,7 @@ void InnerWidget::fill() { const auto container = this; const auto &data = _state; const auto multiplier = data.usdRate * Data::kEarnMultiplier; + constexpr auto kMinorLength = 3; auto availableBalanceValue = rpl::single( data.availableBalance @@ -128,7 +125,7 @@ void InnerWidget::fill() { return _state.availableBalance; }) ); - auto valueToString = [](uint64 v) { return QString::number(v); }; + auto valueToString = [](uint64 v) { return Lang::FormatCountDecimal(v); }; if (data.revenueGraph.chart) { Ui::AddSkip(container); @@ -170,7 +167,7 @@ void InnerWidget::fill() { std::move( value ) | rpl::map([=](uint64 v) { - return v ? ToUsd(v, multiplier) : QString(); + return v ? ToUsd(v, multiplier, kMinorLength) : QString(); }), st::channelEarnOverviewSubMinorLabel); rpl::combine( @@ -233,7 +230,7 @@ void InnerWidget::fill() { ::Settings::AddWithdrawalWidget( container, _controller->parentController(), - _peer, + peer(), rpl::single( data.buyAdsUrl ) | rpl::then( @@ -243,11 +240,11 @@ void InnerWidget::fill() { ), rpl::duplicate(availableBalanceValue), rpl::duplicate(dateValue), - std::move(dateValue) | rpl::map([=](const QDateTime &dt) { + rpl::duplicate(dateValue) | rpl::map([=](const QDateTime &dt) { return !dt.isNull() || (!_state.isWithdrawalEnabled); }), rpl::duplicate(availableBalanceValue) | rpl::map([=](uint64 v) { - return v ? ToUsd(v, multiplier) : QString(); + return v ? ToUsd(v, multiplier, kMinorLength) : QString(); })); } @@ -262,7 +259,7 @@ void InnerWidget::fillHistory() { const auto sectionIndex = history->lifetime().make_state(0); - const auto fill = [=, peer = _peer]( + const auto fill = [=, peer = peer()]( not_null premiumBot, const Data::CreditsStatusSlice &fullSlice, const Data::CreditsStatusSlice &inSlice, @@ -395,7 +392,7 @@ void InnerWidget::fillHistory() { const auto apiLifetime = history->lifetime().make_state(); rpl::single(rpl::empty) | rpl::then( _stateUpdated.events() - ) | rpl::start_with_next([=, peer = _peer] { + ) | rpl::start_with_next([=, peer = peer()] { using Api = Api::CreditsHistory; const auto apiFull = apiLifetime->make_state(peer, true, true); const auto apiIn = apiLifetime->make_state(peer, true, false); @@ -450,7 +447,7 @@ void InnerWidget::setInnerFocus() { } not_null InnerWidget::peer() const { - return _peer; + return _controller->statisticsTag().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 index 7157c4f02..1d89d4aa9 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h @@ -32,10 +32,7 @@ public: struct ShowRequest final { }; - InnerWidget( - QWidget *parent, - not_null controller, - not_null peer); + InnerWidget(QWidget *parent, not_null controller); [[nodiscard]] not_null peer() const; @@ -54,7 +51,6 @@ private: void fillHistory(); not_null _controller; - not_null _peer; std::shared_ptr _show; Data::CreditsEarnStatistics _state; diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp index 7eab7cf7b..123c8e307 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp @@ -16,11 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::BotEarn { Memento::Memento(not_null controller) -: ContentMemento(Info::Statistics::Tag{ - controller->statisticsPeer(), - {}, - {}, -}) { +: ContentMemento(controller->statisticsTag()) { } Memento::Memento(not_null peer) @@ -54,11 +50,7 @@ Widget::Widget( QWidget *parent, not_null controller) : ContentWidget(parent, controller) -, _inner(setInnerWidget( - object_ptr( - this, - controller, - controller->statisticsPeer()))) { +, _inner(setInnerWidget(object_ptr(this, controller))) { _inner->showRequests( ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { }, _inner->lifetime()); @@ -73,7 +65,7 @@ not_null Widget::peer() const { } bool Widget::showInternal(not_null memento) { - return (memento->statisticsPeer() == peer()); + return (memento->statisticsTag().peer == peer()); } rpl::producer Widget::title() { diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index d3fb04e8d..ca7d302e6 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -96,6 +96,10 @@ giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { giveawayGiftCodeValueMultiline: FlatLabel(giveawayGiftCodeValue) { minWidth: 128px; maxHeight: 100px; + style: TextStyle(defaultTextStyle) { + font: font(10px); + linkUnderline: kLinkUnderlineNever; + } } giveawayGiftMessage: FlatLabel(giveawayGiftCodeValue) { minWidth: 128px; @@ -208,10 +212,24 @@ startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimati thickness: 2px; } -starGiftBox: Box(giveawayGiftCodeBox) { - buttonPadding: margins(22px, 11px, 22px, 52px); -} starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) { color: windowActiveTextFg; thickness: 2px; } + +starGiftSmallButton: RoundButton(defaultActiveButton) { + textFg: windowActiveTextFg; + textFgOver: windowActiveTextFg; + textBg: lightButtonBgOver; + textBgOver: lightButtonBgOver; + width: -12px; + height: 18px; + radius: 9px; + textTop: 0px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + } + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgRipple; + } +} 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 09ca04209..e2436585c 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 @@ -35,9 +35,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" #include "ui/widgets/slider_natural_width.h" #include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" +#include "styles/style_dialogs.h" // dialogsSearchTabs #include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_premium.h" @@ -430,7 +432,7 @@ void InnerWidget::fill() { #else const auto hasOneTab = (hasBoosts != hasGifts); #endif - const auto boostsTabText = tr::lng_boosts_list_title( + const auto boostsTabText = tr::lng_giveaway_quantity( tr::now, lt_count, status.firstSliceBoosts.multipliedTotal); @@ -454,8 +456,19 @@ void InnerWidget::fill() { inner, object_ptr( inner, - st::defaultTabsSlider)), + st::dialogsSearchTabs)), st::boxRowPadding); + if (const auto shadow = Ui::CreateChild(inner)) { + shadow->show(); + slider->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + shadow->setGeometry( + inner->x(), + rect::bottom(r) - shadow->height(), + inner->width(), + shadow->height()); + }, shadow->lifetime()); + } slider->toggle(!hasOneTab, anim::type::instant); slider->entity()->addSection(boostsTabText); diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.cpp index 60b37cb86..447897dff 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/info_boosts_widget.cpp @@ -16,11 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Boosts { Memento::Memento(not_null controller) -: ContentMemento(Info::Statistics::Tag{ - controller->statisticsPeer(), - {}, - {}, -}) { +: ContentMemento(controller->statisticsTag()) { } Memento::Memento(not_null peer) @@ -58,7 +54,7 @@ Widget::Widget( object_ptr( this, controller, - controller->statisticsPeer()))) { + controller->statisticsTag().peer))) { _inner->showRequests( ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { }, _inner->lifetime()); @@ -73,7 +69,7 @@ not_null Widget::peer() const { } bool Widget::showInternal(not_null memento) { - return (memento->statisticsPeer() == peer()); + return (memento->statisticsTag().peer == peer()); } rpl::producer Widget::title() { diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp index 4f87fa505..11f17b8b3 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp @@ -46,7 +46,10 @@ QString MinorPart(EarnInt value) { return result.chopped(zeroCount); } -QString ToUsd(EarnInt value, float64 rate) { +QString ToUsd( + Data::EarnInt value, + float64 rate, + int afterFloat) { constexpr auto kApproximately = QChar(0x2248); const auto result = value @@ -56,7 +59,9 @@ QString ToUsd(EarnInt value, float64 rate) { return QString(kApproximately) + QChar('$') + MajorPart(result) - + MinorPart(result); + + ((afterFloat > 0) + ? MinorPart(result).left(afterFloat) + : MinorPart(result)); } } // namespace Info::ChannelEarn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h index 4a7b401ab..7fcd19b4d 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h @@ -13,6 +13,9 @@ namespace Info::ChannelEarn { [[nodiscard]] QString MajorPart(Data::EarnInt value); [[nodiscard]] QString MinorPart(Data::EarnInt value); -[[nodiscard]] QString ToUsd(Data::EarnInt value, float64 rate); +[[nodiscard]] QString ToUsd( + Data::EarnInt value, + float64 rate, + int afterFloat); } // namespace Info::ChannelEarn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp new file mode 100644 index 000000000..0a57f9288 --- /dev/null +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp @@ -0,0 +1,132 @@ +/* +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/channel_statistics/earn/earn_icons.h" + +#include "ui/effects/premium_graphics.h" +#include "ui/rect.h" +#include "styles/style_menu_icons.h" +#include "styles/style_widgets.h" +#include "styles/style_info.h" // infoIconReport. + +#include +#include + +namespace Ui::Earn { +namespace { + +[[nodiscard]] QByteArray CurrencySvg(const QColor &c) { + const auto color = u"rgb(%1,%2,%3)"_q + .arg(c.red()) + .arg(c.green()) + .arg(c.blue()) + .toUtf8(); + return R"( + + + + + + + +)"; +} + +} // namespace + +QImage IconCurrencyColored( + const style::font &font, + const QColor &c) { + const auto s = Size(font->ascent); + auto svg = QSvgRenderer(CurrencySvg(c)); + auto image = QImage( + s * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + svg.render(&p, Rect(s)); + } + return image; +} + +QImage MenuIconCurrency(const QSize &size) { + auto image = QImage( + size * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + auto p = QPainter(&image); + st::infoIconReport.paintInCenter( + p, + Rect(size), + st::infoIconFg->c); + p.setCompositionMode(QPainter::CompositionMode_Clear); + const auto w = st::lineWidth * 6; + p.fillRect( + QRect( + rect::center(Rect(size)).x() - w / 2, + rect::center(Rect(size)).y() - w, + w, + w * 2), + Qt::white); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + const auto i = IconCurrencyColored( + st::inviteLinkSubscribeBoxTerms.style.font, + st::infoIconFg->c); + p.drawImage( + (size.width() - i.width() / style::DevicePixelRatio()) / 2, + (size.height() - i.height() / style::DevicePixelRatio()) / 2, + i); + return image; +} + +QImage MenuIconCredits() { + constexpr auto kStrokeWidth = 5; + const auto sizeShift = st::lineWidth * 1.5; + + 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)); + auto svg = QSvgRenderer(colorized.toUtf8()); + svg.setViewBox(svg.viewBox() + + Margins(style::ConvertScale(kStrokeWidth))); + + auto image = QImage( + st::menuIconLinks.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + svg.render(&p, Rect(st::menuIconLinks.size()) - Margins(sizeShift)); + } + return image; +} + +} // namespace Ui::Earn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h new file mode 100644 index 000000000..ccc2ca7f8 --- /dev/null +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.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 Ui::Earn { + +[[nodiscard]] QImage IconCurrencyColored( + const style::font &font, + const QColor &c); + +[[nodiscard]] QImage MenuIconCurrency(const QSize &size); +[[nodiscard]] QImage MenuIconCredits(); + +} // namespace Ui::Earn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index 51cf9506a..3f98b5f23 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -21,9 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_web_page.h" +#include "data/data_user.h" #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/earn_icons.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. @@ -63,7 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_credits.h" #include "styles/style_window.h" // mainMenuToggleFourStrokes. -#include #include namespace Info::ChannelEarn { @@ -118,52 +119,6 @@ void ShowMenu(not_null box, const QString &text) { }); } -[[nodiscard]] QByteArray CurrencySvg(const QColor &c) { - const auto color = u"rgb(%1,%2,%3)"_q - .arg(c.red()) - .arg(c.green()) - .arg(c.blue()) - .toUtf8(); - return R"( - - - - - - - -)"; -} - -void AddArrow(not_null parent) { - const auto arrow = Ui::CreateChild(parent.get()); - arrow->paintRequest( - ) | rpl::start_with_next([=](const QRect &r) { - auto p = QPainter(arrow); - - const auto path = Ui::ToggleUpDownArrowPath( - st::statisticsShowMoreButtonArrowSize, - st::statisticsShowMoreButtonArrowSize, - st::statisticsShowMoreButtonArrowSize, - st::mainMenuToggleFourStrokes, - 0.); - - auto hq = PainterHighQualityEnabler(p); - p.fillPath(path, st::lightButtonFg); - }, arrow->lifetime()); - arrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2)); - arrow->move(st::statisticsShowMoreButtonArrowPosition); - arrow->show(); -} - void AddHeader( not_null content, tr::phrase<> text) { @@ -239,23 +194,6 @@ void AddRecipient(not_null box, const TextWithEntities &t) { } // namespace -QImage IconCurrency( - const style::FlatLabel &label, - const QColor &c) { - const auto s = Size(label.style.font->ascent); - auto svg = QSvgRenderer(CurrencySvg(c)); - auto image = QImage( - s * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - image.setDevicePixelRatio(style::DevicePixelRatio()); - image.fill(Qt::transparent); - { - auto p = QPainter(&image); - svg.render(&p, Rect(s)); - } - return image; -} - InnerWidget::InnerWidget( QWidget *parent, not_null controller, @@ -269,11 +207,11 @@ InnerWidget::InnerWidget( void InnerWidget::load() { struct State final { State(not_null peer) - : api(peer->asChannel()) + : api(peer) , apiCredits(peer) , apiCreditsHistory(peer, true, true) { } - Api::ChannelEarnStatistics api; + Api::EarnStatistics api; Api::CreditsEarnStatistics apiCredits; Api::CreditsHistory apiCreditsHistory; rpl::lifetime apiLifetime; @@ -386,6 +324,9 @@ void InnerWidget::load() { void InnerWidget::fill() { const auto container = this; + const auto bot = (peerIsUser(_peer->id) && _peer->asUser()->botInfo) + ? _peer->asUser() + : nullptr; const auto channel = _peer->asChannel(); const auto canViewCurrencyEarn = channel ? (channel->flags() & ChannelDataFlag::CanViewRevenue) @@ -393,7 +334,9 @@ void InnerWidget::fill() { const auto &data = canViewCurrencyEarn ? _state.currencyEarn : Data::EarnStatistics(); - const auto &creditsData = _state.creditsEarn; + const auto &creditsData = bot + ? Data::CreditsEarnStatistics() + : _state.creditsEarn; auto currencyStateValue = rpl::single( data @@ -403,12 +346,14 @@ void InnerWidget::fill() { }) ); - auto creditsStateValue = rpl::single( - creditsData - ) | rpl::then( - _stateUpdated.events() | rpl::map([=] { return _state.creditsEarn; }) - ); + auto creditsStateValue = bot + ? rpl::single(Data::CreditsEarnStatistics()) | rpl::type_erased() + : rpl::single(creditsData) | rpl::then( + _stateUpdated.events( + ) | rpl::map([this] { return _state.creditsEarn; }) + ); + constexpr auto kMinorLength = 3; constexpr auto kMinus = QChar(0x2212); //constexpr auto kApproximately = QChar(0x2248); const auto multiplier = data.usdRate; @@ -416,15 +361,11 @@ void InnerWidget::fill() { const auto creditsToUsdMap = [=](EarnInt c) { const auto creditsMultiplier = _state.creditsEarn.usdRate * Data::kEarnMultiplier; - return c ? ToUsd(c, creditsMultiplier) : QString(); + return c ? ToUsd(c, creditsMultiplier, 0) : QString(); }; - constexpr auto kNonInteractivePeriod = 1717200000; - const auto nonInteractive = base::unixtime::now() < kNonInteractivePeriod; - const auto session = &_peer->session(); - const auto withdrawalEnabled = WithdrawalEnabled(session) - && !nonInteractive; + const auto withdrawalEnabled = WithdrawalEnabled(session); const auto makeContext = [=](not_null l) { return Core::MarkedTextContext{ .session = session, @@ -439,8 +380,8 @@ void InnerWidget::fill() { const auto &st = label->st(); auto icon = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( - IconCurrency( - st, + Ui::Earn::IconCurrencyColored( + st.style.font, !isIn ? st::activeButtonBg->c : (*isIn) @@ -462,7 +403,9 @@ void InnerWidget::fill() { const auto bigCurrencyIcon = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( - IconCurrency(st::boxTitle, st::activeButtonBg->c), + Ui::Earn::IconCurrencyColored( + st::boxTitle.style.font, + st::activeButtonBg->c), st::channelEarnCurrencyLearnMargins, false)); @@ -520,7 +463,9 @@ void InnerWidget::fill() { content, object_ptr( content, - tr::lng_channel_earn_learn_title(), + bot + ? tr::lng_channel_earn_bot_learn_title() + : tr::lng_channel_earn_learn_title(), st::boxTitle))); Ui::AddSkip(content); Ui::AddSkip(content); @@ -566,7 +511,9 @@ void InnerWidget::fill() { }; addEntry( tr::lng_channel_earn_learn_in_subtitle(), - tr::lng_channel_earn_learn_in_about(), + bot + ? tr::lng_channel_earn_learn_bot_in_about() + : tr::lng_channel_earn_learn_in_about(), st::channelEarnLearnChannelIcon); Ui::AddSkip(content); Ui::AddSkip(content); @@ -662,7 +609,9 @@ void InnerWidget::fill() { st::defaultBoxDividerLabelPadding, RectPart::Top | RectPart::Bottom)); }; - addAboutWithLearn(tr::lng_channel_earn_about); + addAboutWithLearn(bot + ? tr::lng_channel_earn_about_bot + : tr::lng_channel_earn_about); { using Type = Statistic::ChartViewType; Ui::AddSkip(container); @@ -746,14 +695,18 @@ void InnerWidget::fill() { {}); const auto minorLabel = Ui::CreateChild( line, - rpl::duplicate(currencyValue) | rpl::map(MinorPart), + rpl::duplicate(currencyValue) | rpl::map([=](EarnInt v) { + return MinorPart(v).left(kMinorLength); + }), st::channelEarnOverviewMinorLabel); const auto secondMinorLabel = Ui::CreateChild( line, std::move( currencyValue ) | rpl::map([=](EarnInt value) { - return value ? ToUsd(value, multiplier) : QString(); + return value + ? ToUsd(value, multiplier, kMinorLength) + : QString(); }), st::channelEarnOverviewSubMinorLabel); @@ -859,7 +812,7 @@ void InnerWidget::fill() { Ui::AddSkip(container); } #ifndef _DEBUG - if (!channel->amCreator()) { + if (channel && !channel->amCreator()) { Ui::AddSkip(container); Ui::AddSkip(container); return; @@ -914,7 +867,7 @@ void InnerWidget::fill() { container, object_ptr( container, - ToUsd(value, multiplier), + ToUsd(value, multiplier, 0), st::channelEarnOverviewSubMinorLabel))); Ui::AddSkip(container); @@ -1372,16 +1325,16 @@ void InnerWidget::fill() { handleSlice(firstSlice); if (!firstSlice.allLoaded) { struct ShowMoreState final { - ShowMoreState(not_null channel) - : api(channel) { + ShowMoreState(not_null peer) + : api(peer) { } - Api::ChannelEarnStatistics api; + Api::EarnStatistics api; bool loading = false; Data::EarnHistorySlice::OffsetToken token; rpl::variable showed = 0; }; const auto state - = lifetime().make_state(channel); + = lifetime().make_state(_peer); state->token = firstSlice.token; state->showed = firstSlice.list.size(); const auto max = firstSlice.total; @@ -1398,7 +1351,7 @@ void InnerWidget::fill() { ) | tr::to_count()), st::statisticsShowMoreButton))); const auto button = wrap->entity(); - AddArrow(button); + Ui::AddToggleUpDownArrowToMoreButton(button); wrap->toggle(true, anim::type::instant); const auto handleReceived = [=]( diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h index 061d69b7e..d67eba323 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.h @@ -23,10 +23,6 @@ namespace Info::ChannelEarn { class Memento; -[[nodiscard]] QImage IconCurrency( - const style::FlatLabel &label, - const QColor &c); - class InnerWidget final : public Ui::VerticalLayout { public: struct ShowRequest final { diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp index 3074468b6..99424a19d 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_widget.cpp @@ -16,11 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::ChannelEarn { Memento::Memento(not_null controller) -: ContentMemento(Info::Statistics::Tag{ - controller->statisticsPeer(), - {}, - {}, -}) { +: ContentMemento(controller->statisticsTag()) { } Memento::Memento(not_null peer) @@ -58,7 +54,7 @@ Widget::Widget( object_ptr( this, controller, - controller->statisticsPeer()))) { + controller->statisticsTag().peer))) { _inner->showRequests( ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { }, _inner->lifetime()); @@ -73,7 +69,7 @@ not_null Widget::peer() const { } bool Widget::showInternal(not_null memento) { - return (memento->statisticsPeer() == peer()); + return (memento->statisticsTag().peer == peer()); } rpl::producer Widget::title() { diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp index 37defed0d..cd5fe0b2e 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp @@ -177,9 +177,15 @@ void ListController::restoreState( } void ListController::rowClicked(not_null row) { - _controller->parentController()->showPeerHistory( - row->peer(), - Window::SectionShow::Way::Forward); + const auto peer = row->peer(); + const auto controller = _controller->parentController(); + if (const auto forum = peer->forum()) { + controller->showForum(forum); + } else { + controller->showPeerHistory( + peer, + Window::SectionShow::Way::Forward); + } } } // namespace diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 84badf874..3ee9b6032 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -129,9 +129,7 @@ infoTopBarBack: IconButton(defaultIconButton) { rippleAreaPosition: point(6px, 6px); rippleAreaSize: 42px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } infoTopBarTitle: FlatLabel(defaultFlatLabel) { textFg: windowBoldFg; @@ -443,19 +441,16 @@ infoProfileSeparatorPadding: margins( infoProfileSkip, 0px, infoProfileSkip); -infoProfileLabeledButtonCopy: IconButton(defaultIconButton) { +infoProfileLabeledButtonQr: IconButton(defaultIconButton) { width: 34px; height: 34px; - icon: icon {{ "menu/copy", windowBgActive }}; - iconOver: icon {{ "menu/copy", windowBgActive }}; + icon: icon {{ "menu/qr_code", windowActiveTextFg }}; + iconOver: icon {{ "menu/qr_code", windowActiveTextFg }}; rippleAreaPosition: point(0px, 0px); rippleAreaSize: 34px; ripple: defaultRippleAnimation; } -infoProfileLabeledButtonQr: IconButton(infoProfileLabeledButtonCopy) { - icon: icon {{ "menu/qr_code", windowBgActive }}; - iconOver: icon {{ "menu/qr_code", windowBgActive }}; -} +infoProfileLabeledButtonQrRightSkip: 10px; infoIconInformation: icon {{ "info/info_information", infoIconFg }}; infoIconAddMember: icon {{ "info/info_add_member", infoIconFg }}; @@ -475,6 +470,7 @@ infoIconMediaSaved: icon {{ "info/info_media_saved", infoIconFg }}; infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; infoIconMediaGifts: icon {{ "menu/gift_premium", infoIconFg, point(4px, 4px) }}; +infoIconEmojiStatusAccess: icon {{ "menu/read_reactions", infoIconFg, point(4px, 4px) }}; infoIconShare: icon {{ "info/info_share", infoIconFg }}; infoIconEdit: icon {{ "info/info_edit", infoIconFg }}; @@ -490,6 +486,8 @@ infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 4px); infoChannelAdminsIconPosition: point(24px, 7px); +infoEarnCreditsIconPosition: point(24px, 7px); +infoEarnCurrencyIconPosition: point(20px, 3px); infoOpenApp: RoundButton(defaultActiveButton) { textTop: 11px; @@ -623,9 +621,7 @@ infoMembersCancelSearch: CrossButton { duration: 135; loadingPeriod: 1000; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } infoMembersSearchTop: 15px; @@ -841,9 +837,7 @@ topBarSearch: IconButton { rippleAreaPosition: point(0px, 7px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } topBarCloseChoose: IconButton(topBarSearch) { width: 56px; @@ -1146,9 +1140,7 @@ infoHoursDayLabel: infoHoursState; infoHoursOuter: RoundButton(defaultActiveButton) { textBg: transparent; textBgOver: transparent; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } infoHoursOuterMargin: margins(8px, 4px, 8px, 4px); infoHoursDaySkip: 6px; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 507ce0f15..15583240a 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -7,27 +7,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/info_content_widget.h" -#include "window/window_session_controller.h" -#include "ui/widgets/scroll_area.h" -#include "ui/widgets/fields/input_field.h" -#include "ui/wrap/padding_wrap.h" -#include "ui/search_field_controller.h" -#include "ui/ui_utility.h" -#include "lang/lang_keys.h" -#include "info/profile/info_profile_widget.h" -#include "info/media/info_media_widget.h" -#include "info/common_groups/info_common_groups_widget.h" -#include "info/info_layer_widget.h" -#include "info/info_section_widget.h" -#include "info/info_controller.h" +#include "api/api_who_reacted.h" #include "boxes/peer_list_box.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_session.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "info/profile/info_profile_widget.h" +#include "info/media/info_media_widget.h" +#include "info/common_groups/info_common_groups_widget.h" +#include "info/info_layer_widget.h" +#include "info/info_section_widget.h" +#include "info/info_controller.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/search_field_controller.h" +#include "ui/ui_utility.h" #include "window/window_peer_menu.h" +#include "window/window_session_controller.h" #include "styles/style_info.h" #include "styles/style_profile.h" #include "styles/style_layers.h" @@ -375,12 +376,10 @@ Key ContentMemento::key() const { return Settings::Tag{ self }; } else if (const auto peer = storiesPeer()) { return Stories::Tag{ peer, storiesTab() }; - } else if (const auto peer = statisticsPeer()) { - return Statistics::Tag{ - peer, - statisticsContextId(), - statisticsStoryId(), - }; + } else if (const auto peer = statisticsTag().peer) { + return statisticsTag(); + } else if (const auto who = reactionsWhoReadIds()) { + return Key(who, _reactionsSelected, _pollReactionsContextId); } else { return Downloads::Tag(); } @@ -418,9 +417,18 @@ ContentMemento::ContentMemento(Stories::Tag stories) } ContentMemento::ContentMemento(Statistics::Tag statistics) -: _statisticsPeer(statistics.peer) -, _statisticsContextId(statistics.contextId) -, _statisticsStoryId(statistics.storyId) { +: _statisticsTag(statistics) { +} + +ContentMemento::ContentMemento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) +: _reactionsWhoReadIds(whoReadIds + ? whoReadIds + : std::make_shared()) +, _reactionsSelected(selected) +, _pollReactionsContextId(contextId) { } } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 7f7cd0f2f..f0c46da5a 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -7,9 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include -#include "ui/rp_widget.h" #include "info/info_wrap_widget.h" +#include "info/statistics/info_statistics_tag.h" + +namespace Api { +struct WhoReadList; +} // namespace Api namespace Dialogs::Stories { struct Content; @@ -190,8 +193,12 @@ public: explicit ContentMemento(Statistics::Tag statistics); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) - , _pollContextId(contextId) { + , _pollReactionsContextId(contextId) { } + ContentMemento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); virtual object_ptr createWidget( QWidget *parent, @@ -216,20 +223,23 @@ public: Stories::Tab storiesTab() const { return _storiesTab; } - PeerData *statisticsPeer() const { - return _statisticsPeer; - } - FullMsgId statisticsContextId() const { - return _statisticsContextId; - } - FullStoryId statisticsStoryId() const { - return _statisticsStoryId; + Statistics::Tag statisticsTag() const { + return _statisticsTag; } PollData *poll() const { return _poll; } FullMsgId pollContextId() const { - return _pollContextId; + return _poll ? _pollReactionsContextId : FullMsgId(); + } + std::shared_ptr reactionsWhoReadIds() const { + return _reactionsWhoReadIds; + } + Data::ReactionId reactionsSelected() const { + return _reactionsSelected; + } + FullMsgId reactionsContextId() const { + return _reactionsWhoReadIds ? _pollReactionsContextId : FullMsgId(); } Key key() const; @@ -269,11 +279,11 @@ private: UserData * const _settingsSelf = nullptr; PeerData * const _storiesPeer = nullptr; Stories::Tab _storiesTab = {}; - PeerData * const _statisticsPeer = nullptr; - const FullMsgId _statisticsContextId; - const FullStoryId _statisticsStoryId; + Statistics::Tag _statisticsTag; PollData * const _poll = nullptr; - const FullMsgId _pollContextId; + std::shared_ptr _reactionsWhoReadIds; + Data::ReactionId _reactionsSelected; + const FullMsgId _pollReactionsContextId; int _scrollTop = 0; QString _searchFieldQuery; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 0cdd4bd61..388f870fe 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -50,6 +50,13 @@ Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } +Key::Key( + std::shared_ptr whoReadIds, + Data::ReactionId selected, + FullMsgId contextId) +: _value(ReactionsKey{ whoReadIds, selected, contextId }) { +} + PeerData *Key::peer() const { if (const auto peer = std::get_if>(&_value)) { return *peer; @@ -92,25 +99,11 @@ Stories::Tab Key::storiesTab() const { return Stories::Tab(); } -PeerData *Key::statisticsPeer() const { +Statistics::Tag Key::statisticsTag() const { if (const auto tag = std::get_if(&_value)) { - return tag->peer; + return *tag; } - return nullptr; -} - -FullMsgId Key::statisticsContextId() const { - if (const auto tag = std::get_if(&_value)) { - return tag->contextId; - } - return {}; -} - -FullStoryId Key::statisticsStoryId() const { - if (const auto tag = std::get_if(&_value)) { - return tag->storyId; - } - return {}; + return Statistics::Tag(); } PollData *Key::poll() const { @@ -127,6 +120,27 @@ FullMsgId Key::pollContextId() const { return FullMsgId(); } +std::shared_ptr Key::reactionsWhoReadIds() const { + if (const auto data = std::get_if(&_value)) { + return data->whoReadIds; + } + return nullptr; +} + +Data::ReactionId Key::reactionsSelected() const { + if (const auto data = std::get_if(&_value)) { + return data->selected; + } + return Data::ReactionId(); +} + +FullMsgId Key::reactionsContextId() const { + if (const auto data = std::get_if(&_value)) { + return data->contextId; + } + return FullMsgId(); +} + rpl::producer AbstractController::mediaSource( SparseIdsMergedSlice::UniversalMsgId aroundId, int limitBefore, @@ -197,6 +211,19 @@ PollData *AbstractController::poll() const { return nullptr; } +auto AbstractController::reactionsWhoReadIds() const +-> std::shared_ptr { + return key().reactionsWhoReadIds(); +} + +Data::ReactionId AbstractController::reactionsSelected() const { + return key().reactionsSelected(); +} + +FullMsgId AbstractController::reactionsContextId() const { + return key().reactionsContextId(); +} + void AbstractController::showSection( std::shared_ptr memento, const Window::SectionShow ¶ms) { @@ -292,7 +319,7 @@ bool Controller::validateMementoPeer( && memento->migratedPeerId() == migratedPeerId() && memento->settingsSelf() == settingsSelf() && memento->storiesPeer() == storiesPeer() - && memento->statisticsPeer() == statisticsPeer(); + && memento->statisticsTag().peer == statisticsTag().peer; } void Controller::setSection(not_null memento) { @@ -310,6 +337,7 @@ void Controller::updateSearchControllers( : Section::MediaType::kCount; const auto hasMediaSearch = isMedia && SharedMediaAllowSearch(mediaType); + const auto hasRequestsListSearch = (type == Type::RequestsList); const auto hasCommonGroupsSearch = (type == Type::CommonGroups); const auto hasDownloadsSearch = (type == Type::Downloads); const auto hasMembersSearch = (type == Type::Members) @@ -326,6 +354,7 @@ void Controller::updateSearchControllers( _searchController = nullptr; } if (hasMediaSearch + || hasRequestsListSearch || hasCommonGroupsSearch || hasDownloadsSearch || hasMembersSearch) { diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index ee4a7fc33..4fad8ca05 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -7,9 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_message_reaction_id.h" #include "data/data_search_controller.h" +#include "info/statistics/info_statistics_tag.h" #include "window/window_session_controller.h" +namespace Api { +struct WhoReadList; +} // namespace Api + namespace Data { class ForumTopic; } // namespace Data @@ -55,25 +61,6 @@ struct Tag { } // namespace Info::Stories -namespace Info::Statistics { - -struct Tag { - explicit Tag( - not_null peer, - FullMsgId contextId, - FullStoryId storyId) - : peer(peer) - , contextId(contextId) - , storyId(storyId) { - } - - not_null peer; - FullMsgId contextId; - FullStoryId storyId; -}; - -} // namespace Info::Statistics - namespace Info { class Key { @@ -85,6 +72,10 @@ public: Key(Stories::Tag stories); Key(Statistics::Tag statistics); Key(not_null poll, FullMsgId contextId); + Key( + std::shared_ptr whoReadIds, + Data::ReactionId selected, + FullMsgId contextId); PeerData *peer() const; Data::ForumTopic *topic() const; @@ -92,17 +83,23 @@ public: bool isDownloads() const; PeerData *storiesPeer() const; Stories::Tab storiesTab() const; - PeerData *statisticsPeer() const; - FullMsgId statisticsContextId() const; - FullStoryId statisticsStoryId() const; + Statistics::Tag statisticsTag() const; PollData *poll() const; FullMsgId pollContextId() const; + std::shared_ptr reactionsWhoReadIds() const; + Data::ReactionId reactionsSelected() const; + FullMsgId reactionsContextId() const; private: struct PollKey { not_null poll; FullMsgId contextId; }; + struct ReactionsKey { + std::shared_ptr whoReadIds; + Data::ReactionId selected; + FullMsgId contextId; + }; std::variant< not_null, not_null, @@ -110,7 +107,8 @@ private: Downloads::Tag, Stories::Tag, Statistics::Tag, - PollKey> _value; + PollKey, + ReactionsKey> _value; }; @@ -126,6 +124,8 @@ public: Media, CommonGroups, SimilarChannels, + RequestsList, + ReactionsList, SavedSublists, PeerGifts, Members, @@ -199,19 +199,17 @@ public: [[nodiscard]] Stories::Tab storiesTab() const { return key().storiesTab(); } - [[nodiscard]] PeerData *statisticsPeer() const { - return key().statisticsPeer(); - } - [[nodiscard]] FullMsgId statisticsContextId() const { - return key().statisticsContextId(); - } - [[nodiscard]] FullStoryId statisticsStoryId() const { - return key().statisticsStoryId(); + [[nodiscard]] Statistics::Tag statisticsTag() const { + return key().statisticsTag(); } [[nodiscard]] PollData *poll() const; [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); } + [[nodiscard]] auto reactionsWhoReadIds() const + -> std::shared_ptr; + [[nodiscard]] Data::ReactionId reactionsSelected() const; + [[nodiscard]] FullMsgId reactionsContextId() const; virtual void setSearchEnabledByContent(bool enabled) { } diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index a8018f21c..7e38fb5d1 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/saved/info_saved_sublists_widget.h" #include "info/settings/info_settings_widget.h" #include "info/similar_channels/info_similar_channels_widget.h" +#include "info/reactions_list/info_reactions_list_widget.h" +#include "info/requests_list/info_requests_list_widget.h" #include "info/peer_gifts/info_peer_gifts_widget.h" #include "info/polls/info_polls_results_widget.h" #include "info/info_section_widget.h" @@ -53,6 +55,13 @@ Memento::Memento(not_null poll, FullMsgId contextId) : Memento(DefaultStack(poll, contextId)) { } +Memento::Memento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) +: Memento(DefaultStack(std::move(whoReadIds), contextId, selected)) { +} + Memento::Memento(std::vector> stack) : _stack(std::move(stack)) { auto topics = base::flat_set>(); @@ -112,6 +121,18 @@ std::vector> Memento::DefaultStack( return result; } +std::vector> Memento::DefaultStack( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) { + auto result = std::vector>(); + result.push_back(std::make_shared( + std::move(whoReadIds), + contextId, + selected)); + return result; +} + Section Memento::DefaultSection(not_null peer) { if (peer->savedSublistsInfo()) { return Section(Section::Type::SavedSublists); @@ -149,6 +170,8 @@ std::shared_ptr Memento::DefaultContent( case Section::Type::SimilarChannels: return std::make_shared( peer->asChannel()); + case Section::Type::RequestsList: + return std::make_shared(peer->asChannel()); case Section::Type::PeerGifts: return std::make_shared(peer->asUser()); case Section::Type::SavedSublists: diff --git a/Telegram/SourceFiles/info/info_memento.h b/Telegram/SourceFiles/info/info_memento.h index 3d18296cb..dc50f2f89 100644 --- a/Telegram/SourceFiles/info/info_memento.h +++ b/Telegram/SourceFiles/info/info_memento.h @@ -13,12 +13,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/section_memento.h" #include "base/object_ptr.h" +namespace Api { +struct WhoReadList; +} // namespace Api + namespace Storage { enum class SharedMediaType : signed char; } // namespace Storage namespace Data { class ForumTopic; +struct ReactionId; } // namespace Data namespace Ui { @@ -46,6 +51,10 @@ public: Memento(not_null topic, Section section); Memento(Settings::Tag settings, Section section); Memento(not_null poll, FullMsgId contextId); + Memento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); explicit Memento(std::vector> stack); object_ptr createWidget( @@ -91,6 +100,10 @@ private: static std::vector> DefaultStack( not_null poll, FullMsgId contextId); + static std::vector> DefaultStack( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); static std::shared_ptr DefaultContent( not_null peer, diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 59c15978f..64bc95ea8 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -294,7 +294,7 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const { } else if (key().settingsSelf() || key().isDownloads() || key().poll() - || key().statisticsPeer()) { + || key().statisticsTag().peer) { return Dialogs::RowDescriptor(); } Unexpected("Owner in WrapWidget::activeChat()."); diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index dd12b6646..ebf27afcf 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" +#include "ui/effects/premium_graphics.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_credits.h" @@ -39,7 +40,8 @@ GiftButton::GiftButton( QWidget *parent, not_null delegate) : AbstractButton(parent) -, _delegate(delegate) { +, _delegate(delegate) +, _stars(this, true, Ui::Premium::MiniStars::Type::SlowStars) { } GiftButton::~GiftButton() { @@ -79,10 +81,17 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor) { data.currency, true)); _userpic = nullptr; + _stars.setColorOverride(QGradientStops{ + { 0., anim::with_alpha(st::windowActiveTextFg->c, .3) }, + { 1., st::windowActiveTextFg->c }, + }); }, [&](const GiftTypeStars &data) { + const auto soldOut = data.info.limitedCount + && !data.userpic + && !data.info.limitedLeft; _price.setMarkedText( st::semiboldTextStyle, - _delegate->star().append(' ' + QString::number(data.stars)), + _delegate->star().append(' ' + QString::number(data.info.stars)), kMarkupTextOptions, _delegate->textContext()); _userpic = !data.userpic @@ -90,6 +99,14 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor) { : data.from ? Ui::MakeUserpicThumbnail(data.from) : Ui::MakeHiddenAuthorThumbnail(); + if (soldOut) { + _stars.setColorOverride(QGradientStops{ + { 0., Qt::transparent }, + { 1., Qt::transparent }, + }); + } else { + _stars.setColorOverride(Ui::Premium::CreditsIconGradientStops()); + } }); if (const auto document = _delegate->lookupSticker(descriptor)) { setDocument(document); @@ -107,6 +124,10 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor) { const auto skipx = (width() - inner.width()) / 2; const auto outer = (width() - 2 * skipx); _button = QRect(skipx, skipy, outer, inner.height()); + { + const auto padding = _button.height() / 2; + _stars.setCenter(_button - QMargins(padding, 0, padding, 0)); + } } bool GiftButton::documentResolved() const { @@ -159,6 +180,8 @@ void GiftButton::setGeometry(QRect inner, QMargins extend) { void GiftButton::resizeEvent(QResizeEvent *e) { if (!_button.isEmpty()) { _button.moveLeft((width() - _button.width()) / 2); + const auto padding = _button.height() / 2; + _stars.setCenter(_button - QMargins(padding, 0, padding, 0)); } } @@ -259,8 +282,8 @@ void GiftButton::paintEvent(QPaintEvent *e) { } return QString(); }, [&](const GiftTypeStars &data) { - if (const auto count = data.limitedCount) { - const auto soldOut = !data.userpic && !data.limitedLeft; + if (const auto count = data.info.limitedCount) { + const auto soldOut = !data.userpic && !data.info.limitedLeft; p.setBrush(soldOut ? st::attentionButtonFg : st::windowActiveTextFg); @@ -307,6 +330,13 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (!premium) { p.setOpacity(1.); } + { + auto clipPath = QPainterPath(); + clipPath.addRoundedRect(geometry, radius, radius); + p.setClipPath(clipPath); + _stars.paint(p); + p.setClipping(false); + } if (!_text.isEmpty()) { p.setPen(st::windowFg); @@ -433,9 +463,7 @@ DocumentData *LookupGiftSticker( return v::match(descriptor, [&](GiftTypePremium data) { return packs.lookup(data.months); }, [&](GiftTypeStars data) { - return data.document - ? data.document - : packs.lookup(packs.monthsForStars(data.stars)); + return data.info.document.get(); }); } diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index 6fd2b7413..f8e094e1b 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_premium.h" #include "ui/abstract_button.h" +#include "ui/effects/premium_stars_colored.h" #include "ui/text/text.h" class StickerPremiumMark; @@ -42,13 +44,8 @@ struct GiftTypePremium { }; struct GiftTypeStars { - uint64 id = 0; - int64 stars = 0; - int64 convertStars = 0; - DocumentData *document = nullptr; + Api::StarGift info; PeerData *from = nullptr; - int limitedCount = 0; - int limitedLeft = 0; bool userpic = false; bool hidden = false; bool mine = false; @@ -101,6 +98,7 @@ private: Ui::Text::String _text; Ui::Text::String _price; std::shared_ptr _userpic; + Ui::Premium::ColoredMiniStars _stars; bool _subscribed = false; QRect _button; diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 46ec0f1d7..722752f2d 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -34,14 +34,10 @@ constexpr auto kPerPage = 50; not_null to, const Api::UserStarGift &gift) { return GiftTypeStars{ - .id = gift.gift.id, - .stars = gift.gift.stars, - .convertStars = gift.gift.convertStars, - .document = gift.gift.document, + .info = gift.info, .from = ((gift.anonymous || !gift.fromId) ? nullptr : to->owner().peer(gift.fromId).get()), - .limitedCount = gift.gift.limitedCount, .userpic = true, .hidden = gift.hidden, .mine = to->isSelf(), diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp index f54e09ea5..d2b8b31b9 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp +++ b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.cpp @@ -24,8 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_info.h" -namespace Info { -namespace Polls { +namespace Info::Polls { namespace { constexpr auto kFirstPage = 15; @@ -659,6 +658,4 @@ auto InnerWidget::showPeerInfoRequests() const return _showPeerInfoRequests.events(); } -} // namespace Polls -} // namespace Info - +} // namespace Info::Polls diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.h b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.h index f2fbf05af..c84e10ef9 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.h +++ b/Telegram/SourceFiles/info/polls/info_polls_results_inner_widget.h @@ -16,10 +16,10 @@ class VerticalLayout; } // namespace Ui namespace Info { - class Controller; +} // namespace Info -namespace Polls { +namespace Info::Polls { class Memento; class ListController; @@ -70,5 +70,4 @@ private: }; -} // namespace Polls -} // namespace Info +} // namespace Info::Polls diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp b/Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp index 02d3fe0a2..056eb35cf 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp +++ b/Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp @@ -13,8 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_poll.h" #include "ui/ui_utility.h" -namespace Info { -namespace Polls { +namespace Info::Polls { Memento::Memento(not_null poll, FullMsgId contextId) : ContentMemento(poll, contextId) { @@ -113,5 +112,4 @@ void Widget::restoreState(not_null memento) { scrollTopRestore(memento->scrollTop()); } -} // namespace Polls -} // namespace Info +} // namespace Info::Polls diff --git a/Telegram/SourceFiles/info/polls/info_polls_results_widget.h b/Telegram/SourceFiles/info/polls/info_polls_results_widget.h index 6766da4f5..a3c4ac752 100644 --- a/Telegram/SourceFiles/info/polls/info_polls_results_widget.h +++ b/Telegram/SourceFiles/info/polls/info_polls_results_widget.h @@ -12,8 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct PeerListState; -namespace Info { -namespace Polls { +namespace Info::Polls { class InnerWidget; @@ -68,5 +67,4 @@ private: }; -} // namespace Polls -} // namespace Info +} // namespace Info::Polls diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index c472b5377..e0558985e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_chat_participants.h" #include "api/api_credits.h" +#include "api/api_statistics.h" #include "apiwrap.h" #include "base/options.h" #include "base/timer_rpl.h" @@ -45,13 +46,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/history_view_item_preview.h" #include "info/bot/earn/info_bot_earn_widget.h" -#include "info/info_controller.h" -#include "info/info_memento.h" +#include "info/channel_statistics/earn/earn_format.h" +#include "info/channel_statistics/earn/earn_icons.h" +#include "info/channel_statistics/earn/info_channel_earn_list.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_phone_menu.h" #include "info/profile/info_profile_text.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" #include "inline_bots/bot_attach_web_view.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" @@ -79,6 +83,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" // Window::Controller::show. #include "window/window_peer_menu.h" #include "window/window_session_controller.h" +#include "styles/style_channel_earn.h" // st::channelEarnCurrencyCommonMargins #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -779,6 +784,169 @@ template st)); } +rpl::producer AddCurrencyAction( + not_null user, + not_null wrap, + not_null controller) { + struct State final { + rpl::variable balance; + }; + const auto state = wrap->lifetime().make_state(); + const auto parentController = controller->parentController(); + const auto wrapButton = AddActionButton( + wrap, + tr::lng_manage_peer_bot_balance_currency(), + state->balance.value() | rpl::map(rpl::mappers::_1 > 0), + [=] { parentController->showSection(Info::ChannelEarn::Make(user)); }, + nullptr); + { + const auto button = wrapButton->entity(); + const auto icon = Ui::CreateChild(button); + icon->resize(st::infoIconReport.size()); + const auto image = Ui::Earn::MenuIconCurrency(icon->size()); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + p.drawImage(0, 0, image); + }, icon->lifetime()); + + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + icon->move(st::infoEarnCurrencyIconPosition); + }, icon->lifetime()); + } + const auto balance = user->session().credits().balanceCurrency(user->id); + if (balance) { + state->balance = balance; + } + { + const auto weak = Ui::MakeWeak(wrap); + const auto currencyLoadLifetime + = std::make_shared(); + const auto currencyLoad + = currencyLoadLifetime->make_state(user); + currencyLoad->request( + ) | rpl::start_with_error_done([=](const QString &error) { + currencyLoadLifetime->destroy(); + }, [=] { + if (const auto strong = weak.data()) { + state->balance = currencyLoad->data().currentBalance; + currencyLoadLifetime->destroy(); + } + }, *currencyLoadLifetime); + } + const auto &st = st::infoSharedMediaButton; + const auto button = wrapButton->entity(); + const auto name = Ui::CreateChild(button, st.rightLabel); + const auto icon = Ui::Text::SingleCustomEmoji( + user->owner().customEmojiManager().registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st.rightLabel.style.font, + st.rightLabel.textFg->c), + st::channelEarnCurrencyCommonMargins, + false)); + name->show(); + rpl::combine( + button->widthValue(), + tr::lng_manage_peer_bot_balance_currency(), + state->balance.value() + ) | rpl::start_with_next([=, &st]( + int width, + const QString &button, + uint64 balance) { + const auto available = width + - rect::m::sum::h(st.padding) + - st.style.font->width(button) + - st::settingsButtonRightSkip; + name->setMarkedText( + base::duplicate(icon) + .append(QChar(' ')) + .append(Info::ChannelEarn::MajorPart(balance)) + .append(Info::ChannelEarn::MinorPart(balance)), + Core::MarkedTextContext{ + .session = &user->session(), + .customEmojiRepaint = [=] { name->update(); }, + }); + name->resizeToNaturalWidth(available); + name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); + }, name->lifetime()); + name->setAttribute(Qt::WA_TransparentForMouseEvents); + wrapButton->finishAnimating(); + return state->balance.value(); +} + +rpl::producer AddCreditsAction( + not_null user, + not_null wrap, + not_null controller) { + struct State final { + rpl::variable balance; + }; + const auto state = wrap->lifetime().make_state(); + const auto parentController = controller->parentController(); + const auto wrapButton = AddActionButton( + wrap, + tr::lng_manage_peer_bot_balance_credits(), + state->balance.value() | rpl::map(rpl::mappers::_1 > 0), + [=] { parentController->showSection(Info::BotEarn::Make(user)); }, + nullptr); + { + const auto button = wrapButton->entity(); + const auto icon = Ui::CreateChild(button); + const auto image = Ui::Earn::MenuIconCredits(); + icon->resize(image.size() / style::DevicePixelRatio()); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + p.drawImage(0, 0, image); + }, icon->lifetime()); + + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + icon->move(st::infoEarnCreditsIconPosition); + }, icon->lifetime()); + } + if (const auto balance = user->session().credits().balance(user->id)) { + state->balance = balance; + } + { + const auto api = wrap->lifetime().make_state( + user); + api->request({}, [=](Data::CreditsStatusSlice data) { + state->balance = data.balance; + }); + } + const auto &st = st::infoSharedMediaButton; + const auto button = wrapButton->entity(); + const auto name = Ui::CreateChild(button, st.rightLabel); + const auto icon = user->owner().customEmojiManager().creditsEmoji(); + name->show(); + rpl::combine( + button->widthValue(), + tr::lng_manage_peer_bot_balance_credits(), + state->balance.value() + ) | rpl::start_with_next([=, &st]( + int width, + const QString &button, + uint64 balance) { + const auto available = width + - rect::m::sum::h(st.padding) + - st.style.font->width(button) + - st::settingsButtonRightSkip; + name->setMarkedText( + base::duplicate(icon) + .append(QChar(' ')) + .append(Lang::FormatCountDecimal(balance)), + Core::MarkedTextContext{ + .session = &user->session(), + .customEmojiRepaint = [=] { name->update(); }, + }); + name->resizeToNaturalWidth(available); + name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); + }, name->lifetime()); + name->setAttribute(Qt::WA_TransparentForMouseEvents); + wrapButton->finishAnimating(); + return state->balance.value(); +} + class DetailsFiller { public: DetailsFiller( @@ -798,6 +966,7 @@ private: object_ptr setupInfo(); object_ptr setupMuteToggle(); void setupMainApp(); + void setupBotPermissions(); void setupMainButtons(); Ui::MultiSlideTracker fillTopicButtons(); Ui::MultiSlideTracker fillUserButtons( @@ -842,8 +1011,8 @@ public: object_ptr fill(); private: + void addBalanceActions(not_null user); void addInviteToGroupAction(not_null user); - void addCreditsAction(not_null user); void addShareContactAction(not_null user); void addEditContactAction(not_null user); void addDeleteContactAction(not_null user); @@ -1072,7 +1241,8 @@ object_ptr DetailsFiller::setupInfo() { }; const auto fitLabelToButton = [&]( not_null button, - not_null label) { + not_null label, + int rightSkip) { const auto parent = label->parentWidget(); rpl::combine( label->geometryValue(), @@ -1080,10 +1250,11 @@ object_ptr DetailsFiller::setupInfo() { ) | rpl::start_with_next([=](const QRect &, const QSize &buttonSize) { const auto s = parent->size(); button->moveToRight( - 0, + rightSkip, (s.height() - buttonSize.height()) / 2); label->resizeToWidth( s.width() + - rightSkip - label->geometry().left() - st::lineWidth * 2 - buttonSize.width()); @@ -1160,7 +1331,19 @@ object_ptr DetailsFiller::setupInfo() { PhoneOrHiddenValue(user), tr::lng_profile_copy_phone(tr::now)).text; const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) { - phoneLabel->fillContextMenu(request); + if (request.selection.empty()) { + const auto callback = [=] { + auto phone = rpl::variable( + PhoneOrHiddenValue(user)).current().text; + phone.replace(' ', QString()).replace('-', QString()); + TextUtilities::SetClipboardText({ phone }); + }; + request.menu->addAction( + tr::lng_profile_copy_phone(tr::now), + callback); + } else { + phoneLabel->fillContextMenu(request); + } AddPhoneMenu(request.menu, user); }; phoneLabel->setContextMenuHook(hook); @@ -1193,8 +1376,9 @@ object_ptr DetailsFiller::setupInfo() { const auto qrButton = Ui::CreateChild( usernameLine.text->parentWidget(), st::infoProfileLabeledButtonQr); - fitLabelToButton(qrButton, usernameLine.text); - fitLabelToButton(qrButton, usernameLine.subtext); + const auto rightSkip = st::infoProfileLabeledButtonQrRightSkip; + fitLabelToButton(qrButton, usernameLine.text, rightSkip); + fitLabelToButton(qrButton, usernameLine.subtext, rightSkip); qrButton->setClickedCallback([=] { controller->show( Box(Ui::FillPeerQrBox, user, std::nullopt, nullptr)); @@ -1298,8 +1482,9 @@ object_ptr DetailsFiller::setupInfo() { const auto qr = Ui::CreateChild( linkLine.text->parentWidget(), st::infoProfileLabeledButtonQr); - fitLabelToButton(qr, linkLine.text); - fitLabelToButton(qr, linkLine.subtext); + const auto rightSkip = st::infoProfileLabeledButtonQrRightSkip; + fitLabelToButton(qr, linkLine.text, rightSkip); + fitLabelToButton(qr, linkLine.subtext, rightSkip); qr->setClickedCallback([=, peer = _peer] { controller->show( Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr)); @@ -1803,6 +1988,37 @@ void DetailsFiller::setupMainApp() { Ui::AddSkip(_wrap); } +void DetailsFiller::setupBotPermissions() { + AddSkip(_wrap); + AddSubsectionTitle(_wrap, tr::lng_profile_bot_permissions_title()); + const auto emoji = _wrap->add( + object_ptr( + _wrap, + tr::lng_profile_bot_emoji_status_access(), + st::infoSharedMediaButton)); + object_ptr( + emoji, + st::infoIconEmojiStatusAccess, + st::infoSharedMediaButtonIconPosition); + + const auto user = _peer->asUser(); + emoji->toggleOn( + rpl::single(bool(user->botInfo->canManageEmojiStatus)) + )->toggledValue() | rpl::filter([=](bool allowed) { + return allowed != user->botInfo->canManageEmojiStatus; + }) | rpl::start_with_next([=](bool allowed) { + user->botInfo->canManageEmojiStatus = allowed; + const auto session = &user->session(); + session->api().request(MTPbots_ToggleUserEmojiStatusPermission( + user->inputUser, + MTP_bool(allowed) + )).send(); + }, emoji->lifetime()); + AddSkip(_wrap); + AddDivider(_wrap); + AddSkip(_wrap); +} + void DetailsFiller::setupMainButtons() { auto wrapButtons = [=](auto &&callback) { auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap)); @@ -1997,6 +2213,9 @@ object_ptr DetailsFiller::fill() { if (info->hasMainApp) { setupMainApp(); } + if (info->canManageEmojiStatus) { + setupBotPermissions(); + } } } if (!_peer->isSelf()) { @@ -2017,6 +2236,25 @@ ActionsFiller::ActionsFiller( , _peer(peer) { } +void ActionsFiller::addBalanceActions(not_null user) { + const auto wrap = _wrap->add( + object_ptr>( + _wrap.data(), + object_ptr(_wrap.data()))); + const auto inner = wrap->entity(); + Ui::AddSubsectionTitle(inner, tr::lng_manage_peer_bot_balance()); + auto currencyBalance = AddCurrencyAction(user, inner, _controller); + auto creditsBalance = AddCreditsAction(user, inner, _controller); + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + wrap->toggleOn( + rpl::combine( + std::move(currencyBalance), + std::move(creditsBalance) + ) | rpl::map((rpl::mappers::_1 + rpl::mappers::_2) > 0)); +} + void ActionsFiller::addInviteToGroupAction(not_null user) { const auto notEmpty = [](const QString &value) { return !value.isEmpty(); @@ -2041,59 +2279,6 @@ void ActionsFiller::addInviteToGroupAction(not_null user) { about->finishAnimating(); } -void ActionsFiller::addCreditsAction(not_null user) { - struct State final { - rpl::variable balance; - }; - const auto state = _wrap->lifetime().make_state(); - const auto controller = _controller->parentController(); - const auto wrap = AddActionButton( - _wrap, - tr::lng_manage_peer_bot_balance(), - state->balance.value() | rpl::map(rpl::mappers::_1 > 0), - [=] { controller->showSection(Info::BotEarn::Make(user)); }, - &st::infoIconBotBalance); - if (const auto balance = user->session().credits().balance(user->id)) { - state->balance = balance; - } - { - const auto api = _wrap->lifetime().make_state( - user); - api->request({}, [=](Data::CreditsStatusSlice data) { - state->balance = data.balance; - }); - } - const auto &st = st::infoSharedMediaButton; - const auto button = wrap->entity(); - const auto name = Ui::CreateChild(button, st.rightLabel); - name->show(); - rpl::combine( - button->widthValue(), - tr::lng_manage_peer_bot_balance(), - state->balance.value() - ) | rpl::start_with_next([=, &st]( - int width, - const QString &button, - uint64 balance) { - const auto available = width - - rect::m::sum::h(st.padding) - - st.style.font->width(button) - - st::settingsButtonRightSkip; - name->setMarkedText( - user->owner().customEmojiManager().creditsEmoji() - .append(QChar(' ')) - .append(QString::number(balance)), - Core::MarkedTextContext{ - .session = &user->session(), - .customEmojiRepaint = [=] { name->update(); }, - }); - name->resizeToNaturalWidth(available); - name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); - }, name->lifetime()); - name->setAttribute(Qt::WA_TransparentForMouseEvents); - wrap->finishAnimating(); -} - void ActionsFiller::addShareContactAction(not_null user) { const auto controller = _controller->parentController(); AddActionButton( @@ -2205,7 +2390,6 @@ void ActionsFiller::addBotCommandActions(not_null user) { rpl::single(true), openPrivacyPolicy, nullptr); - } void ActionsFiller::addReportAction() { @@ -2316,7 +2500,7 @@ void ActionsFiller::addJoinChannelAction( void ActionsFiller::fillUserActions(not_null user) { if (user->isBot()) { - addCreditsAction(user); + addBalanceActions(user); addInviteToGroupAction(user); } addShareContactAction(user); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 571951004..3c92eed95 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -166,7 +166,7 @@ void TopicIconView::setupPlayer(not_null topic) { id ) | rpl::map([=](not_null document) { return document.get(); - }); + }) | rpl::map_error_to_done(); }) | rpl::flatten_latest( ) | rpl::map([=](DocumentData *document) -> rpl::producer> { diff --git a/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.cpp b/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.cpp new file mode 100644 index 000000000..a94f2f2a7 --- /dev/null +++ b/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.cpp @@ -0,0 +1,352 @@ +/* +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/reactions_list/info_reactions_list_widget.h" + +#include "api/api_who_reacted.h" +#include "boxes/peer_list_box.h" +#include "data/data_channel.h" +#include "history/view/reactions/history_view_reactions_list.h" +#include "history/view/reactions/history_view_reactions_tabs.h" +#include "info/info_controller.h" +#include "ui/controls/who_reacted_context_action.h" +#include "ui/widgets/scroll_area.h" +#include "ui/search_field_controller.h" +#include "ui/ui_utility.h" +#include "lang/lang_keys.h" +#include "styles/style_info.h" + +namespace Info::ReactionsList { +namespace { + +} // namespace + +class InnerWidget final + : public Ui::RpWidget + , private PeerListContentDelegate { +public: + InnerWidget( + QWidget *parent, + not_null controller, + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); + + [[nodiscard]] std::shared_ptr whoReadIds() const; + [[nodiscard]] FullMsgId contextId() const; + [[nodiscard]] Data::ReactionId selected() const; + + rpl::producer scrollToRequests() const; + + int desiredHeight() const; + + void saveState(not_null memento); + void restoreState(not_null memento); + +protected: + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + using ListWidget = PeerListContent; + + // PeerListContentDelegate interface + void peerListSetTitle(rpl::producer title) override; + void peerListSetAdditionalTitle(rpl::producer title) override; + bool peerListIsRowChecked(not_null row) override; + int peerListSelectedRowsCount() override; + void peerListScrollToTop() override; + void peerListAddSelectedPeerInBunch(not_null peer) override; + void peerListAddSelectedRowInBunch(not_null row) override; + void peerListFinishSelectedRowsBunch() override; + void peerListSetDescription(object_ptr description) override; + std::shared_ptr peerListUiShow() override; + + object_ptr setupList( + RpWidget *parent, + not_null controller); + + const std::shared_ptr _show; + not_null _controller; + Data::ReactionId _selected; + not_null _tabs; + rpl::variable _tabsHeight; + HistoryView::Reactions::PreparedFullList _full; + object_ptr _list; + + rpl::event_stream _scrollToRequests; +}; + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) +: RpWidget(parent) +, _show(controller->uiShow()) +, _controller(controller) +, _selected(selected) +, _tabs(HistoryView::Reactions::CreateReactionsTabs( + this, + controller, + controller->reactionsContextId(), + _selected, + controller->reactionsWhoReadIds())) +, _tabsHeight(_tabs->heightValue()) +, _full(HistoryView::Reactions::FullListController( + controller, + controller->reactionsContextId(), + _selected, + controller->reactionsWhoReadIds())) +, _list(setupList(this, _full.controller.get())) { + setContent(_list.data()); + _full.controller->setDelegate(static_cast(this)); + _tabs->changes( + ) | rpl::start_with_next([=](Data::ReactionId reaction) { + _selected = reaction; + _full.switchTab(reaction); + }, _list->lifetime()); +} + +std::shared_ptr InnerWidget::whoReadIds() const { + return _controller->reactionsWhoReadIds(); +} + +FullMsgId InnerWidget::contextId() const { + return _controller->reactionsContextId(); +} + +Data::ReactionId InnerWidget::selected() const { + return _selected; +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +void InnerWidget::saveState(not_null memento) { + memento->setListState(_full.controller->saveState()); +} + +void InnerWidget::restoreState(not_null memento) { + _full.controller->restoreState(memento->listState()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +int InnerWidget::desiredHeight() const { + auto desired = 0; + desired += _list->fullRowsCount() * st::infoMembersList.item.height; + return qMax(height(), desired); +} + +object_ptr InnerWidget::setupList( + RpWidget *parent, + not_null controller) { + auto result = object_ptr(parent, controller); + const auto raw = result.data(); + + raw->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + const auto skip = _tabsHeight.current() + + st::infoCommonGroupsMargin.top(); + auto addmin = (request.ymin < 0) ? 0 : skip; + auto addmax = (request.ymax < 0) ? 0 : skip; + _scrollToRequests.fire({ + request.ymin + addmin, + request.ymax + addmax }); + }, raw->lifetime()); + + _tabs->move(0, 0); + _tabsHeight.value() | rpl::start_with_next([=](int tabs) { + raw->moveToLeft(0, tabs + st::infoCommonGroupsMargin.top()); + }, raw->lifetime()); + + parent->widthValue( + ) | rpl::start_with_next([=](int newWidth) { + _tabs->resizeToWidth(newWidth); + raw->resizeToWidth(newWidth); + }, raw->lifetime()); + + rpl::combine( + _tabsHeight.value(), + raw->heightValue() + ) | rpl::start_with_next([parent](int tabsHeight, int listHeight) { + const auto newHeight = tabsHeight + + st::infoCommonGroupsMargin.top() + + listHeight + + st::infoCommonGroupsMargin.bottom(); + parent->resize(parent->width(), newHeight); + }, result->lifetime()); + + return result; +} + +void InnerWidget::peerListSetTitle(rpl::producer title) { +} + +void InnerWidget::peerListSetAdditionalTitle(rpl::producer title) { +} + +bool InnerWidget::peerListIsRowChecked(not_null row) { + return false; +} + +int InnerWidget::peerListSelectedRowsCount() { + return 0; +} + +void InnerWidget::peerListScrollToTop() { + _scrollToRequests.fire({ -1, -1 }); +} + +void InnerWidget::peerListAddSelectedPeerInBunch(not_null peer) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void InnerWidget::peerListAddSelectedRowInBunch(not_null row) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void InnerWidget::peerListFinishSelectedRowsBunch() { +} + +void InnerWidget::peerListSetDescription( + object_ptr description) { + description.destroy(); +} + +std::shared_ptr InnerWidget::peerListUiShow() { + return _show; +} + +Memento::Memento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) +: ContentMemento(std::move(whoReadIds), contextId, selected) { +} + +Section Memento::section() const { + return Section(Section::Type::ReactionsList); +} + +std::shared_ptr Memento::whoReadIds() const { + return reactionsWhoReadIds(); +} + +FullMsgId Memento::contextId() const { + return reactionsContextId(); +} + +Data::ReactionId Memento::selected() const { + return reactionsSelected(); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr( + parent, + controller, + whoReadIds(), + contextId(), + selected()); + result->setInternalState(geometry, this); + return result; +} + +void Memento::setListState(std::unique_ptr state) { + _listState = std::move(state); +} + +std::unique_ptr Memento::listState() { + return std::move(_listState); +} + +Memento::~Memento() = default; + +Widget::Widget( + QWidget *parent, + not_null controller, + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected) +: ContentWidget(parent, controller) { + _inner = setInnerWidget(object_ptr( + this, + controller, + std::move(whoReadIds), + contextId, + selected)); +} + +rpl::producer Widget::title() { + const auto ids = whoReadIds(); + const auto count = ids ? int(ids->list.size()) : 0; + return !count + ? tr::lng_manage_peer_reactions() + : (ids->type == Ui::WhoReadType::Seen) + ? tr::lng_context_seen_text(lt_count, rpl::single(1. * count)) + : (ids->type == Ui::WhoReadType::Listened) + ? tr::lng_context_seen_listened(lt_count, rpl::single(1. * count)) + : (ids->type == Ui::WhoReadType::Watched) + ? tr::lng_context_seen_watched(lt_count, rpl::single(1. * count)) + : tr::lng_manage_peer_reactions(); +} + +std::shared_ptr Widget::whoReadIds() const { + return _inner->whoReadIds(); +} + +FullMsgId Widget::contextId() const { + return _inner->contextId(); +} + +Data::ReactionId Widget::selected() const { + return _inner->selected(); +} + +bool Widget::showInternal(not_null memento) { + return false; +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared( + whoReadIds(), + contextId(), + selected()); + 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()); +} + +} // namespace Info::ReactionsList diff --git a/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.h b/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.h new file mode 100644 index 000000000..a713c6e2b --- /dev/null +++ b/Telegram/SourceFiles/info/reactions_list/info_reactions_list_widget.h @@ -0,0 +1,82 @@ +/* +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 "info/info_content_widget.h" + +class ChannelData; +struct PeerListState; + +namespace Api { +struct WhoReadList; +} // namespace Api + +namespace Info::ReactionsList { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + Memento( + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + [[nodiscard]] std::shared_ptr whoReadIds() const; + [[nodiscard]] FullMsgId contextId() const; + [[nodiscard]] Data::ReactionId selected() const; + + void setListState(std::unique_ptr state); + std::unique_ptr listState(); + + ~Memento(); + +private: + std::unique_ptr _listState; + +}; + +class Widget final : public ContentWidget { +public: + Widget( + QWidget *parent, + not_null controller, + std::shared_ptr whoReadIds, + FullMsgId contextId, + Data::ReactionId selected); + + [[nodiscard]] std::shared_ptr whoReadIds() const; + [[nodiscard]] FullMsgId contextId() const; + [[nodiscard]] Data::ReactionId selected() const; + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer title() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + InnerWidget *_inner = nullptr; +}; + +} // namespace Info::ReactionsList diff --git a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp new file mode 100644 index 000000000..70f638e1c --- /dev/null +++ b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp @@ -0,0 +1,279 @@ +/* +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/requests_list/info_requests_list_widget.h" + +#include "boxes/peers/edit_peer_requests_box.h" +#include "data/data_channel.h" +#include "info/info_controller.h" +#include "ui/widgets/scroll_area.h" +#include "ui/search_field_controller.h" +#include "ui/ui_utility.h" +#include "lang/lang_keys.h" +#include "styles/style_info.h" + +namespace Info::RequestsList { +namespace { + +} // namespace + +class InnerWidget final + : public Ui::RpWidget + , private PeerListContentDelegate { +public: + InnerWidget( + QWidget *parent, + not_null controller, + not_null channel); + + [[nodiscard]] not_null channel() const { + return _channel; + } + + rpl::producer scrollToRequests() const; + + int desiredHeight() const; + + void saveState(not_null memento); + void restoreState(not_null memento); + +protected: + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + using ListWidget = PeerListContent; + + // PeerListContentDelegate interface + void peerListSetTitle(rpl::producer title) override; + void peerListSetAdditionalTitle(rpl::producer title) override; + bool peerListIsRowChecked(not_null row) override; + int peerListSelectedRowsCount() override; + void peerListScrollToTop() override; + void peerListAddSelectedPeerInBunch(not_null peer) override; + void peerListAddSelectedRowInBunch(not_null row) override; + void peerListFinishSelectedRowsBunch() override; + void peerListSetDescription(object_ptr description) override; + std::shared_ptr peerListUiShow() override; + + object_ptr setupList( + RpWidget *parent, + not_null controller); + + const std::shared_ptr _show; + not_null _controller; + const not_null _channel; + std::unique_ptr _listController; + object_ptr _list; + + rpl::event_stream _scrollToRequests; +}; + +InnerWidget::InnerWidget( + QWidget *parent, + not_null controller, + not_null channel) +: RpWidget(parent) +, _show(controller->uiShow()) +, _controller(controller) +, _channel(channel) +, _listController(std::make_unique( + controller, + _channel)) +, _list(setupList(this, _listController.get())) { + setContent(_list.data()); + _listController->setDelegate(static_cast(this)); + + controller->searchFieldController()->queryValue( + ) | rpl::start_with_next([this](QString &&query) { + peerListScrollToTop(); + content()->searchQueryChanged(std::move(query)); + }, lifetime()); +} + +void InnerWidget::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +void InnerWidget::saveState(not_null memento) { + memento->setListState(_listController->saveState()); +} + +void InnerWidget::restoreState(not_null memento) { + _listController->restoreState(memento->listState()); +} + +rpl::producer InnerWidget::scrollToRequests() const { + return _scrollToRequests.events(); +} + +int InnerWidget::desiredHeight() const { + auto desired = 0; + desired += _list->fullRowsCount() * st::infoMembersList.item.height; + return qMax(height(), desired); +} + +object_ptr InnerWidget::setupList( + RpWidget *parent, + not_null controller) { + auto result = object_ptr(parent, controller); + result->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + auto addmin = (request.ymin < 0) ? 0 : st::infoCommonGroupsMargin.top(); + auto addmax = (request.ymax < 0) ? 0 : st::infoCommonGroupsMargin.top(); + _scrollToRequests.fire({ + request.ymin + addmin, + request.ymax + addmax }); + }, result->lifetime()); + result->moveToLeft(0, st::infoCommonGroupsMargin.top()); + + parent->widthValue( + ) | rpl::start_with_next([list = result.data()](int newWidth) { + list->resizeToWidth(newWidth); + }, result->lifetime()); + + result->heightValue( + ) | rpl::start_with_next([parent](int listHeight) { + auto newHeight = st::infoCommonGroupsMargin.top() + + listHeight + + st::infoCommonGroupsMargin.bottom(); + parent->resize(parent->width(), newHeight); + }, result->lifetime()); + + return result; +} + +void InnerWidget::peerListSetTitle(rpl::producer title) { +} + +void InnerWidget::peerListSetAdditionalTitle(rpl::producer title) { +} + +bool InnerWidget::peerListIsRowChecked(not_null row) { + return false; +} + +int InnerWidget::peerListSelectedRowsCount() { + return 0; +} + +void InnerWidget::peerListScrollToTop() { + _scrollToRequests.fire({ -1, -1 }); +} + +void InnerWidget::peerListAddSelectedPeerInBunch(not_null peer) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void InnerWidget::peerListAddSelectedRowInBunch(not_null row) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void InnerWidget::peerListFinishSelectedRowsBunch() { +} + +void InnerWidget::peerListSetDescription( + object_ptr description) { + description.destroy(); +} + +std::shared_ptr InnerWidget::peerListUiShow() { + return _show; +} + +Memento::Memento(not_null channel) +: ContentMemento(channel, nullptr, PeerId()) { +} + +Section Memento::section() const { + return Section(Section::Type::RequestsList); +} + +not_null Memento::channel() const { + return peer()->asChannel(); +} + +object_ptr Memento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller, channel()); + result->setInternalState(geometry, this); + return result; +} + +void Memento::setListState(std::unique_ptr state) { + _listState = std::move(state); +} + +std::unique_ptr Memento::listState() { + return std::move(_listState); +} + +Memento::~Memento() = default; + +Widget::Widget( + QWidget *parent, + not_null controller, + not_null channel) +: ContentWidget(parent, controller) { + controller->setSearchEnabledByContent(true); + _inner = setInnerWidget(object_ptr( + this, + controller, + channel)); +} + +rpl::producer Widget::title() { + return tr::lng_manage_peer_requests(); +} + +not_null Widget::channel() const { + return _inner->channel(); +} + +bool Widget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + if (auto requestsMemento = dynamic_cast(memento.get())) { + if (requestsMemento->channel() == channel()) { + restoreState(requestsMemento); + return true; + } + } + return false; +} + +void Widget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr Widget::doCreateMemento() { + auto result = std::make_shared(channel()); + 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()); +} + +} // namespace Info::RequestsList diff --git a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.h b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.h new file mode 100644 index 000000000..32e04ac5d --- /dev/null +++ b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.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 + +#include "info/info_content_widget.h" + +class ChannelData; +struct PeerListState; + +namespace Info::RequestsList { + +class InnerWidget; + +class Memento final : public ContentMemento { +public: + explicit Memento(not_null channel); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + [[nodiscard]] not_null channel() const; + + void setListState(std::unique_ptr state); + std::unique_ptr listState(); + + ~Memento(); + +private: + std::unique_ptr _listState; + +}; + +class Widget final : public ContentWidget { +public: + Widget( + QWidget *parent, + not_null controller, + not_null channel); + + [[nodiscard]] not_null channel() const; + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer title() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + InnerWidget *_inner = nullptr; +}; + +} // namespace Info::RequestsList diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index 193622135..5412b5886 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -404,6 +404,9 @@ void FillOverview( - st::statisticsOverviewSubtext.style.font->height + g.y() + diffBetweenHeaders); + if (container->height() < rect::bottom(sub)) { + container->resize(container->width(), rect::bottom(sub)); + } }, primary->lifetime()); }; @@ -535,8 +538,7 @@ void FillOverview( } } container->showChildren(); - container->resize(container->width(), topLeftLabel->height() * 5); - container->sizeValue( + container->sizeValue() | rpl::distinct_until_changed( ) | rpl::start_with_next([=](const QSize &s) { const auto halfWidth = s.width() / 2; { diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 0dc51cb07..abbd690c7 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -60,27 +60,6 @@ constexpr auto kColorIndexPending = int(4); + (entry.in ? '1' : '0')); } -void AddArrow(not_null parent) { - const auto arrow = Ui::CreateChild(parent.get()); - arrow->paintRequest( - ) | rpl::start_with_next([=](const QRect &r) { - auto p = QPainter(arrow); - - const auto path = Ui::ToggleUpDownArrowPath( - st::statisticsShowMoreButtonArrowSize, - st::statisticsShowMoreButtonArrowSize, - st::statisticsShowMoreButtonArrowSize, - st::mainMenuToggleFourStrokes, - 0.); - - auto hq = PainterHighQualityEnabler(p); - p.fillPath(path, st::lightButtonFg); - }, arrow->lifetime()); - arrow->resize(Size(st::statisticsShowMoreButtonArrowSize * 2)); - arrow->move(st::statisticsShowMoreButtonArrowPosition); - arrow->show(); -} - void AddSubtitle( not_null container, rpl::producer title) { @@ -843,9 +822,7 @@ void CreditsRow::init() { const auto name = !isSpecial ? PeerListRow::generateName() : Ui::GenerateEntryName(_entry).text; - _name = (_entry.reaction - || _entry.bareGiveawayMsgId - || _entry.convertStars) + _name = (_entry.reaction || _entry.stargift || _entry.bareGiveawayMsgId) ? Ui::GenerateEntryName(_entry).text : _entry.title.isEmpty() ? name @@ -854,7 +831,12 @@ void CreditsRow::init() { setSkipPeerBadge(true); PeerListRow::setCustomStatus( langDateTime(_entry.date) - + (_entry.refunded + + (_entry.floodSkip + ? (joiner + tr::lng_credits_box_history_entry_floodskip_about( + tr::now, + lt_count_decimal, + _entry.floodSkip)) + : _entry.refunded ? (joiner + tr::lng_channel_earn_history_return(tr::now)) : _entry.pending ? (joiner + tr::lng_channel_earn_history_pending(tr::now)) @@ -892,7 +874,7 @@ void CreditsRow::init() { _context); } if (!_paintUserpicCallback) { - _paintUserpicCallback = _entry.convertStars + _paintUserpicCallback = _entry.stargift ? Ui::GenerateGiftStickerUserpicCallback( _context.session, _entry.bareGiftStickerId, @@ -931,7 +913,7 @@ QSize CreditsRow::rightActionSize() const { _rowHeight); } else if (_subscription || _entry) { return QSize( - _rightText.maxWidth() + st::boxRowPadding.right(), + _rightText.maxWidth() + st::boxRowPadding.right() / 2, _rowHeight); } else if (!_entry && !_subscription) { return QSize(); @@ -1045,7 +1027,7 @@ void CreditsController::requestNext() { _requesting = false; applySlice(s); }; - if (!_firstSlice.subscriptions.empty()) { + if (_subscription) { return _api.requestSubscriptions(_apiToken, done); } _api.request(_apiToken, done); @@ -1075,7 +1057,10 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { delegate()->peerListUpdateRow(row); }, }; - if (const auto peerId = PeerId(i.barePeerId + s.barePeerId)) { + if (i.bareActorId) { + const auto peer = session().data().peer(PeerId(i.bareActorId)); + return std::make_unique(peer, descriptor); + } else if (const auto peerId = PeerId(i.barePeerId + s.barePeerId)) { const auto peer = session().data().peer(peerId); return std::make_unique(peer, descriptor); } else { @@ -1300,7 +1285,7 @@ not_null*> AddShowMoreButton( std::move(title), st::statisticsShowMoreButton)), { 0, -st::settingsButton.padding.top(), 0, 0 }); - AddArrow(wrap->entity()); + Ui::AddToggleUpDownArrowToMoreButton(wrap->entity()); return wrap; } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp index c09038bee..2f8b1d069 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_recent_message.cpp @@ -224,6 +224,7 @@ void MessagePreview::processPreview() { auto image = QImage( rect.size() * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); image.fill(Qt::transparent); { auto p = QPainter(&image); diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_tag.h b/Telegram/SourceFiles/info/statistics/info_statistics_tag.h new file mode 100644 index 000000000..e2bc2760f --- /dev/null +++ b/Telegram/SourceFiles/info/statistics/info_statistics_tag.h @@ -0,0 +1,31 @@ +/* +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 Info::Statistics { + +struct Tag final { + explicit Tag() = default; + explicit Tag( + PeerData *peer, + FullMsgId contextId, + FullStoryId storyId) + : peer(peer) + , contextId(contextId) + , storyId(storyId) { + } + + PeerData *peer = nullptr; + FullMsgId contextId; + FullStoryId storyId; + +}; + +} // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp index fc40d9e09..087d3dd11 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_widget.cpp @@ -19,11 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Statistics { Memento::Memento(not_null controller) -: ContentMemento(Tag{ - controller->statisticsPeer(), - controller->statisticsContextId(), - controller->statisticsStoryId(), -}) { +: ContentMemento(controller->statisticsTag()) { } Memento::Memento(not_null peer, FullMsgId contextId) @@ -65,9 +61,9 @@ Widget::Widget( object_ptr( this, controller, - controller->statisticsPeer(), - controller->statisticsContextId(), - controller->statisticsStoryId()))) { + controller->statisticsTag().peer, + controller->statisticsTag().contextId, + controller->statisticsTag().storyId))) { _inner->showRequests( ) | rpl::start_with_next([=](InnerWidget::ShowRequest request) { if (request.history) { @@ -79,7 +75,7 @@ Widget::Widget( controller->showPeerInfo(request.info); } else if (request.messageStatistic || request.storyStatistic) { controller->showSection(Make( - controller->statisticsPeer(), + controller->statisticsTag().peer, request.messageStatistic, request.storyStatistic)); } else if (const auto &s = request.story) { @@ -92,7 +88,7 @@ Widget::Widget( } }, _inner->lifetime()); _inner->scrollToRequests( - ) | rpl::start_with_next([=](const Ui::ScrollToRequest &request) { + ) | rpl::start_with_next([this](const Ui::ScrollToRequest &request) { scrollTo(request); }, _inner->lifetime()); } @@ -102,9 +98,9 @@ bool Widget::showInternal(not_null memento) { } rpl::producer Widget::title() { - return controller()->statisticsContextId() + return controller()->statisticsTag().contextId ? tr::lng_stats_message_title() - : controller()->statisticsStoryId() + : controller()->statisticsTag().storyId ? tr::lng_stats_story_title() : tr::lng_stats_title(); } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 2062d4c02..dadb510b3 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -11,11 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "api/api_sending.h" #include "apiwrap.h" +#include "base/call_delayed.h" +#include "base/base_file_utilities.h" #include "base/qthelp_url.h" #include "base/random.h" #include "base/timer_rpl.h" +#include "base/unixtime.h" #include "boxes/peer_list_controllers.h" +#include "boxes/premium_preview_box.h" #include "boxes/share_box.h" +#include "chat_helpers/stickers_lottie.h" +#include "chat_helpers/tabbed_panel.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/local_url_handlers.h" @@ -25,15 +31,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_emoji_statuses.h" #include "data/data_file_origin.h" #include "data/data_peer_bot_command.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_web_page.h" #include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" #include "history/history.h" #include "history/history_item.h" #include "info/profile/info_profile_values.h" +#include "inline_bots/inline_bot_result.h" +#include "inline_bots/inline_bot_confirm_prepared.h" +#include "inline_bots/inline_bot_downloads.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -42,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" +#include "settings/settings_premium.h" #include "storage/storage_account.h" #include "storage/storage_domain.h" #include "ui/basic_click_handlers.h" @@ -52,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "ui/painter.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/dropdown_menu.h" @@ -431,6 +444,213 @@ void FillBotUsepic( Ui::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel); } +std::unique_ptr MakeEmojiSetStatusPreview( + not_null parent, + not_null peer, + not_null document) { + auto result = std::make_unique(parent); + + const auto size = st::chatGiveawayPeerSize; + const auto padding = st::chatGiveawayPeerPadding; + + const auto raw = result.get(); + + const auto width = raw->lifetime().make_state(); + const auto name = raw->lifetime().make_state( + raw, + rpl::single(peer->name()), + st::botEmojiStatusName); + const auto makeContext = [=](Fn update) { + return Core::MarkedTextContext{ + .session = &peer->session(), + .customEmojiRepaint = update, + }; + }; + const auto emoji = raw->lifetime().make_state( + raw, + rpl::single( + Ui::Text::SingleCustomEmoji( + Data::SerializeCustomEmojiId(document->id), + document->sticker() ? document->sticker()->alt : QString())), + st::botEmojiStatusEmoji, + st::defaultPopupMenu, + makeContext); + const auto userpic = raw->lifetime().make_state( + raw, + peer, + st::botEmojiStatusUserpic); + + raw->resize(size, size); + raw->sizeValue() | rpl::start_with_next([=](QSize outer) { + const auto full = outer.width(); + const auto decorations = size + + padding.left() + + padding.right() + + emoji->width() + + st::normalFont->spacew; + const auto inner = full - decorations; + const auto use = std::min(inner, name->textMaxWidth()); + *width = use + decorations; + const auto left = (full - *width) / 2; + if (inner > 0) { + userpic->moveToLeft(left, 0, outer.width()); + emoji->moveToLeft( + left + *width - padding.right() - emoji->width(), + padding.top(), + outer.width()); + name->resizeToWidth(use); + name->moveToLeft( + left + size + padding.left(), + padding.top(), + outer.width()); + } + }, raw->lifetime()); + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + const auto left = (raw->width() - *width) / 2; + const auto skip = size / 2; + p.setClipRect(left + skip, 0, *width - skip, size); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgOver); + p.drawRoundedRect(left, 0, *width, size, skip, skip); + }, raw->lifetime()); + + return result; +} + +bool CheckEmojiStatusPremium(not_null bot) { + if (bot->session().premium()) { + return true; + } + const auto window = ChatHelpers::ResolveWindowDefault()( + &bot->session(), + ChatHelpers::WindowUsage::PremiumPromo); + if (window) { + ShowPremiumPreviewBox(window, PremiumFeature::EmojiStatus); + window->window().activate(); + } + return false; +} + +void ConfirmEmojiStatusAccessBox( + not_null box, + not_null bot, + Fn done) { + box->setNoContentMargin(true); + + const auto set = box->lifetime().make_state(); + + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + AddSkip(box->verticalLayout(), 4 * st::defaultVerticalListSkip); + + const auto statusIcon = ChatHelpers::GenerateLocalTgsSticker( + &bot->session(), + u"hello_status"_q); + statusIcon->overrideEmojiUsesTextColor(true); + + auto ownedSet = MakeEmojiSetStatusPreview( + box, + bot->session().user(), + statusIcon); + box->addRow( + object_ptr::fromRaw(ownedSet.release())); + + AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip); + + auto name = Ui::Text::Bold(bot->name()); + box->addRow(object_ptr( + box, + tr::lng_bot_emoji_status_access_text( + lt_bot, + rpl::single(name), + lt_name, + rpl::single(name), + Ui::Text::RichLangValue), + st::botEmojiStatusText)); + + box->addButton(tr::lng_bot_emoji_status_access_allow(), [=] { + if (!CheckEmojiStatusPremium(bot)) { + return; + } + *set = true; + box->closeBox(); + done(true); + }); + box->addButton(tr::lng_cancel(), [=] { + const auto was = *set; + box->closeBox(); + if (!was) { + done(false); + } + }); +} + +void ConfirmEmojiStatusBox( + not_null box, + not_null bot, + not_null document, + TimeId duration, + Fn done) { + box->setNoContentMargin(true); + + auto owned = Settings::MakeEmojiStatusPreview(box, document); + const auto preview = box->addRow( + object_ptr::fromRaw(owned.release())); + preview->resize(preview->width(), st::botEmojiStatusPreviewHeight); + + const auto set = box->lifetime().make_state(); + + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + box->addRow(object_ptr( + box, + tr::lng_bot_emoji_status_title(), + st::botEmojiStatusTitle)); + AddSkip(box->verticalLayout()); + + box->addRow(object_ptr( + box, + tr::lng_bot_emoji_status_text( + lt_bot, + rpl::single(Ui::Text::Bold(bot->name())), + Ui::Text::RichLangValue), + st::botEmojiStatusText)); + + AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip); + + auto ownedSet = MakeEmojiSetStatusPreview( + box, + document->session().user(), + document); + box->addRow( + object_ptr::fromRaw(ownedSet.release())); + + box->addButton(tr::lng_bot_emoji_status_confirm(), [=] { + if (!CheckEmojiStatusPremium(bot)) { + return; + } + document->owner().emojiStatuses().set( + document->id, + duration ? (base::unixtime::now() + duration) : 0); + *set = true; + box->closeBox(); + done(true); + }); + box->addButton(tr::lng_cancel(), [=] { + const auto was = *set; + box->closeBox(); + if (!was) { + done(false); + } + }); +} + class BotAction final : public Ui::Menu::ItemBase { public: BotAction( @@ -922,6 +1142,7 @@ void WebViewInstance::requestButton() { using Flag = MTPmessages_RequestWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestWebView( MTP_flags(Flag::f_theme_params + | (_context.fullscreen ? Flag::f_fullscreen : Flag(0)) | (_button.url.isEmpty() ? Flag(0) : Flag::f_url) | (_button.startCommand.isEmpty() ? Flag(0) @@ -944,7 +1165,11 @@ void WebViewInstance::requestButton() { : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { const auto &data = result.data(); - show(qs(data.vurl()), data.vquery_id().value_or_empty()); + show({ + .url = qs(data.vurl()), + .queryId = data.vquery_id().value_or_empty(), + .fullscreen = data.is_fullscreen(), + }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); if (error.type() == u"BOT_INVALID"_q) { @@ -958,6 +1183,7 @@ void WebViewInstance::requestSimple() { using Flag = MTPmessages_RequestSimpleWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestSimpleWebView( MTP_flags(Flag::f_theme_params + | (_context.fullscreen ? Flag::f_fullscreen : Flag(0)) | (v::is(_source) ? (Flag::f_url | Flag::f_from_switch_webview) : v::is(_source) @@ -972,7 +1198,11 @@ void WebViewInstance::requestSimple() { MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_string(WebviewPlatform()) )).done([=](const MTPWebViewResult &result) { - show(qs(result.data().vurl())); + const auto &data = result.data(); + show({ + .url = qs(data.vurl()), + .fullscreen = data.is_fullscreen(), + }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); close(); @@ -983,6 +1213,7 @@ void WebViewInstance::requestMain() { using Flag = MTPmessages_RequestMainWebView::Flag; _requestId = _session->api().request(MTPmessages_RequestMainWebView( MTP_flags(Flag::f_theme_params + | (_context.fullscreen ? Flag::f_fullscreen : Flag(0)) | (_button.startCommand.isEmpty() ? Flag() : Flag::f_start_param) @@ -997,7 +1228,11 @@ void WebViewInstance::requestMain() { MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_string(WebviewPlatform()) )).done([=](const MTPWebViewResult &result) { - show(qs(result.data().vurl())); + const auto &data = result.data(); + show({ + .url = qs(data.vurl()), + .fullscreen = data.is_fullscreen(), + }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); close(); @@ -1011,6 +1246,7 @@ void WebViewInstance::requestApp(bool allowWrite) { using Flag = MTPmessages_RequestAppWebView::Flag; const auto app = _app; const auto flags = Flag::f_theme_params + | (_context.fullscreen ? Flag::f_fullscreen : Flag(0)) | (_appStartParam.isEmpty() ? Flag(0) : Flag::f_start_param) | (allowWrite ? Flag::f_write_allowed : Flag(0)); _requestId = _session->api().request(MTPmessages_RequestAppWebView( @@ -1022,7 +1258,11 @@ void WebViewInstance::requestApp(bool allowWrite) { MTP_string(WebviewPlatform()) )).done([=](const MTPWebViewResult &result) { _requestId = 0; - show(qs(result.data().vurl())); + const auto &data = result.data(); + show({ + .url = qs(data.vurl()), + .fullscreen = data.is_fullscreen(), + }); }).fail([=](const MTP::Error &error) { _requestId = 0; if (error.type() == u"BOT_INVALID"_q) { @@ -1101,7 +1341,7 @@ void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) { close(); } -void WebViewInstance::show(const QString &url, uint64 queryId) { +void WebViewInstance::show(ShowArgs &&args) { auto title = Info::Profile::NameValue(_bot); auto titleBadge = _bot->isVerified() ? object_ptr(_parentShow->toastParent()) @@ -1135,22 +1375,25 @@ void WebViewInstance::show(const QString &url, uint64 queryId) { : attached->inMainMenu ? Button::RemoveFromMainMenu : Button::RemoveFromMenu); - const auto allowClipboardRead = v::is(_source) + const auto allowClipboardRead = v::is(_source) || v::is(_source) || (attached != end(bots) && (attached->inAttachMenu || attached->inMainMenu)); - _panelUrl = url; + const auto downloads = &_session->attachWebView().downloads(); + _panelUrl = args.url; _panel = Ui::BotWebView::Show({ - .url = url, + .url = args.url, .storageId = _session->local().resolveStorageIdBots(), .title = std::move(title), .titleBadge = std::move(titleBadge), .bottom = rpl::single('@' + _bot->username()), .delegate = static_cast(this), .menuButtons = buttons, + .fullscreen = args.fullscreen, .allowClipboardRead = allowClipboardRead, + .downloadsProgress = downloads->progress(_bot), }); - started(queryId); + started(args.queryId); if (const auto strong = PendingActivation.get()) { if (strong == this) { @@ -1218,7 +1461,34 @@ void WebViewInstance::started(uint64 queryId) { } Webview::ThemeParams WebViewInstance::botThemeParams() { - return Window::Theme::WebViewParams(); + auto result = Window::Theme::WebViewParams(); + if (const auto info = _bot->botInfo.get()) { + const auto night = Window::Theme::IsNightMode(); + const auto &title = night + ? info->botAppColorTitleNight + : info->botAppColorTitleDay; + const auto &body = night + ? info->botAppColorBodyNight + : info->botAppColorBodyDay; + if (title.alpha() == 255) { + result.titleBg = title; + } + if (body.alpha() == 255) { + result.bodyBg = body; + } + } + return result; +} + +auto WebViewInstance::botDownloads(bool forceCheck) +-> const std::vector & { + return _session->attachWebView().downloads().list(_bot, forceCheck); +} + +void WebViewInstance::botDownloadsAction( + uint32 id, + Ui::BotWebView::DownloadsAction type) { + _session->attachWebView().downloads().action(_bot, id, type); } bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) { @@ -1466,6 +1736,33 @@ void WebViewInstance::botAllowWriteAccess(Fn callback) { }).send(); } +void WebViewInstance::botRequestEmojiStatusAccess( + Fn callback) { + if (_bot->botInfo->canManageEmojiStatus) { + callback(true); + } else if (const auto panel = _panel.get()) { + const auto bot = _bot; + panel->showBox(Box(ConfirmEmojiStatusAccessBox, bot, [=](bool ok) { + if (!ok) { + callback(false); + return; + } + const auto session = &bot->session(); + bot->botInfo->canManageEmojiStatus = true; + session->api().request(MTPbots_ToggleUserEmojiStatusPermission( + bot->inputUser, + MTP_bool(true) + )).done([=] { + callback(true); + }).fail([=] { + callback(false); + }).send(); + })); + } else { + callback(false); + } +} + void WebViewInstance::botSharePhone(Fn callback) { const auto history = _bot->owner().history(_bot); if (_bot->isBlocked()) { @@ -1501,6 +1798,195 @@ void WebViewInstance::botInvokeCustomMethod( }).send(); } +void WebViewInstance::botSendPreparedMessage( + Ui::BotWebView::SendPreparedMessageRequest request) { + const auto bot = _bot; + const auto id = request.id; + const auto panel = _panel.get(); + const auto weak = base::make_weak(panel); + const auto callback = request.callback; + if (!panel) { + callback(u"UNKNOWN_ERROR"_q); + return; + } + _session->api().request(MTPmessages_GetPreparedInlineMessage( + bot->inputUser, + MTP_string(request.id) + )).done([=](const MTPmessages_PreparedInlineMessage &result) { + const auto panel = weak.get(); + const auto &data = result.data(); + bot->owner().processUsers(data.vusers()); + const auto parsed = std::shared_ptr(Result::Create( + &bot->session(), + data.vquery_id().v, + data.vresult())); + if (!parsed || !panel) { + callback(u"UNKNOWN_ERROR"_q); + return; + } + const auto types = PeerTypesFromMTP(data.vpeer_types()); + const auto history = bot->owner().history(bot->session().user()); + const auto item = parsed->makeMessage(history, { + .id = bot->owner().nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem, + .from = bot->session().userPeerId(), + .date = base::unixtime::now(), + .viaBotId = peerToUser(bot->id), + }); + struct State { + QPointer preview; + QPointer choose; + rpl::event_stream> recipient; + bool sent = false; + }; + const auto state = std::make_shared(); + auto recipient = state->recipient.events(); + const auto send = [=](std::vector> list) { + if (state->sent) { + return; + } + state->sent = true; + const auto failed = std::make_shared(); + const auto count = int(list.size()); + const auto weak1 = state->preview; + const auto weak2 = state->choose; + const auto close = [=] { + if (const auto strong = weak1.data()) { + strong->closeBox(); + } + if (const auto strong = weak2.data()) { + strong->closeBox(); + } + }; + const auto done = [=](bool success) { + if (*failed < 0) { + return; + } + if (success) { + *failed = -1; + if (const auto strong2 = weak2.data()) { + strong2->showToast({ tr::lng_share_done(tr::now) }); + } else if (const auto strong1 = weak1.data()) { + strong1->showToast({ tr::lng_share_done(tr::now) }); + } + base::call_delayed(Ui::Toast::kDefaultDuration, close); + callback(QString()); + } else if (++*failed == count) { + close(); + callback(u"MESSAGE_SEND_FAILED"_q); + } + }; + for (const auto &thread : list) { + bot->session().api().sendInlineResult( + bot, + parsed.get(), + Api::SendAction(thread), + std::nullopt, + done); + } + }; + auto box = Box(PreparedPreviewBox, item, std::move(recipient), [=] { + if (state->sent) { + return; + } + const auto chosen = [=](not_null thread) { + if (!Data::CanSend(thread, ChatRestriction::SendInline)) { + panel->showToast({ + tr::lng_restricted_send_inline_all(tr::now), + }); + return false; + } + state->recipient.fire_copy(thread); + return true; + }; + auto box = Window::PrepareChooseRecipientBox( + &bot->session(), + chosen, + tr::lng_inline_switch_choose(), + nullptr, + types, + send); + state->choose = box.data(); + panel->showBox(std::move(box)); + }, [=](not_null thread) { + send({ thread }); + }); + box->boxClosing() | rpl::start_with_next([=] { + if (!state->sent) { + callback("USER_DECLINED"); + } + }, box->lifetime()); + state->preview = box.data(); + panel->showBox(std::move(box)); + }).fail([=] { + callback(u"MESSAGE_EXPIRED"_q); + }).send(); +} + +void WebViewInstance::botSetEmojiStatus( + Ui::BotWebView::SetEmojiStatusRequest request) { + const auto bot = _bot; + const auto panel = _panel.get(); + const auto callback = request.callback; + const auto duration = request.duration; + if (!panel) { + callback(u"UNKNOWN_ERROR"_q); + return; + } + _session->data().customEmojiManager().resolve( + request.customEmojiId + ) | rpl::start_with_next_error([=](not_null document) { + const auto sticker = document->sticker(); + if (!sticker || sticker->setType != Data::StickersType::Emoji) { + callback(u"SUGGESTED_EMOJI_INVALID"_q); + return; + } + const auto done = [=](bool success) { + callback(success ? QString() : u"USER_DECLINED"_q); + }; + panel->showBox( + Box(ConfirmEmojiStatusBox, bot, document, duration, done)); + }, [=] { callback(u"SUGGESTED_EMOJI_INVALID"_q); }, panel->lifetime()); +} + +void WebViewInstance::botDownloadFile( + Ui::BotWebView::DownloadFileRequest request) { + const auto callback = request.callback; + if (_confirmingDownload || !_panel) { + callback(false); + return; + } + _confirmingDownload = true; + const auto done = [=](QString path) { + _confirmingDownload = false; + if (path.isEmpty()) { + callback(false); + return; + } + _bot->session().attachWebView().downloads().start({ + .bot = _bot, + .url = request.url, + .path = path, + }); + callback(true); + }; + _bot->session().api().request(MTPbots_CheckDownloadFileParams( + _bot->inputUser, + MTP_string(request.name), + MTP_string(request.url) + )).done([=] { + _panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{ + .session = &_bot->session(), + .bot = _bot->name(), + .name = base::FileNameFromUserString(request.name), + .url = request.url, + .done = done, + })); + }).fail([=] { + done(QString()); + }).send(); +} + void WebViewInstance::botOpenPrivacyPolicy() { const auto bot = _bot; const auto weak = _context.controller; @@ -1613,6 +2099,7 @@ std::shared_ptr WebViewInstance::uiShow() { AttachWebView::AttachWebView(not_null session) : _session(session) +, _downloads(std::make_unique(session)) , _refreshTimer([=] { requestBots(); }) { _refreshTimer.callEach(kRefreshBotsTimeout); } @@ -1626,20 +2113,25 @@ void AttachWebView::openByUsername( not_null controller, const Api::SendAction &action, const QString &botUsername, - const QString &startCommand) { + const QString &startCommand, + bool fullscreen) { if (botUsername.isEmpty() - || (_botUsername == botUsername && _startCommand == startCommand)) { + || (_botUsername == botUsername + && _startCommand == startCommand + && _fullScreenRequested == fullscreen)) { return; } cancel(); _botUsername = botUsername; _startCommand = startCommand; + _fullScreenRequested = fullscreen; const auto weak = base::make_weak(controller); const auto show = controller->uiShow(); resolveUsername(show, crl::guard(weak, [=](not_null peer) { _botUsername = QString(); const auto token = base::take(_startCommand); + const auto fullscreen = base::take(_fullScreenRequested); const auto bot = peer->asUser(); if (!bot || !bot->isBot()) { @@ -1654,6 +2146,7 @@ void AttachWebView::openByUsername( .context = { .controller = controller, .action = action, + .fullscreen = fullscreen, }, .button = { .startCommand = token }, .source = InlineBots::WebViewSourceLinkAttachMenu{}, @@ -2048,9 +2541,6 @@ std::unique_ptr MakeAttachBotsMenu( not_null peer, Fn actionFactory, Fn attach) { - if (!Data::CanSend(peer, ChatRestriction::SendInline)) { - return nullptr; - } auto result = std::make_unique( parent, st::dropdownMenuWithIcons); @@ -2105,8 +2595,10 @@ std::unique_ptr MakeAttachBotsMenu( ChooseAndSendLocation(controller, config, actionFactory()); }, &st::menuIconAddress); } + const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline); for (const auto &bot : bots->attachBots()) { - if (!bot.inAttachMenu + if (!addBots + || !bot.inAttachMenu || !PeerMatchesTypes(peer, bot.user, bot.types)) { continue; } @@ -2137,7 +2629,11 @@ std::unique_ptr MakeAttachBotsMenu( }, action->lifetime()); raw->addAction(std::move(action)); } - if (raw->actions().size() <= minimal) { + const auto actions = raw->actions().size(); + const auto onclick = ChatHelpers::ShowPanelOnClick(); + if (!actions) { + return nullptr; + } else if (actions <= minimal && !onclick) { return nullptr; } return result; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 4958ba19d..febf74dfa 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -7,11 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_common.h" #include "base/flags.h" #include "base/timer.h" #include "base/weak_ptr.h" #include "dialogs/dialogs_key.h" -#include "api/api_common.h" #include "mtproto/sender.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/rp_widget.h" @@ -28,6 +28,7 @@ class DropdownMenu; namespace Ui::BotWebView { class Panel; +struct DownloadsEntry; } // namespace Ui::BotWebView namespace Main { @@ -51,6 +52,7 @@ enum class CheckoutResult; namespace InlineBots { class WebViewInstance; +class Downloads; enum class PeerType : uint8 { SameBot = 0x01, @@ -187,6 +189,7 @@ struct WebViewContext { base::weak_ptr controller; Dialogs::EntryState dialogsEntryState; std::optional action; + bool fullscreen = false; bool maySkipConfirmation = false; }; @@ -234,7 +237,12 @@ private: void confirmOpen(Fn done); void confirmAppOpen(bool writeAccess, Fn done); - void show(const QString &url, uint64 queryId = 0); + struct ShowArgs { + QString url; + uint64 queryId = 0; + bool fullscreen = false; + }; + void show(ShowArgs &&args); void showGame(); void started(uint64 queryId); @@ -243,6 +251,11 @@ private: -> Fn; Webview::ThemeParams botThemeParams() override; + auto botDownloads(bool forceCheck = false) + -> const std::vector & override; + void botDownloadsAction( + uint32 id, + Ui::BotWebView::DownloadsAction type) override; bool botHandleLocalUri(QString uri, bool keepOpen) override; void botHandleInvoice(QString slug) override; void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; @@ -254,9 +267,17 @@ private: QString query) override; void botCheckWriteAccess(Fn callback) override; void botAllowWriteAccess(Fn callback) override; + void botRequestEmojiStatusAccess( + Fn callback) override; void botSharePhone(Fn callback) override; void botInvokeCustomMethod( Ui::BotWebView::CustomMethodRequest request) override; + void botSendPreparedMessage( + Ui::BotWebView::SendPreparedMessageRequest request) override; + void botSetEmojiStatus( + Ui::BotWebView::SetEmojiStatusRequest request) override; + void botDownloadFile( + Ui::BotWebView::DownloadFileRequest request) override; void botOpenPrivacyPolicy() override; void botClose() override; @@ -270,6 +291,7 @@ private: BotAppData *_app = nullptr; QString _appStartParam; bool _dataSent = false; + bool _confirmingDownload = false; mtpRequestId _requestId = 0; mtpRequestId _prolongId = 0; @@ -277,6 +299,8 @@ private: QString _panelUrl; std::unique_ptr _panel; + rpl::lifetime _lifetime; + static base::weak_ptr PendingActivation; }; @@ -286,12 +310,17 @@ public: explicit AttachWebView(not_null session); ~AttachWebView(); + [[nodiscard]] Downloads &downloads() const { + return *_downloads; + } + void open(WebViewDescriptor &&descriptor); void openByUsername( not_null controller, const Api::SendAction &action, const QString &botUsername, - const QString &startCommand); + const QString &startCommand, + bool fullscreen); void cancel(); @@ -355,11 +384,13 @@ private: Fn callback = nullptr); const not_null _session; + const std::unique_ptr _downloads; base::Timer _refreshTimer; QString _botUsername; QString _startCommand; + bool _fullScreenRequested = false; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp new file mode 100644 index 000000000..4b4b434ae --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.cpp @@ -0,0 +1,241 @@ +/* +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 "inline_bots/inline_bot_confirm_prepared.h" + +#include "boxes/peers/edit_peer_invite_link.h" +#include "data/data_forum_topic.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/admin_log/history_admin_log_item.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/layers/generic_box.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" + +namespace InlineBots { +namespace { + +using namespace HistoryView; + +class PreviewDelegate final : public DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + Context elementContext() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +class PreviewWrap final : public Ui::RpWidget { +public: + PreviewWrap(not_null parent, not_null item); + ~PreviewWrap(); + +private: + void paintEvent(QPaintEvent *e) override; + + void resizeTo(int width); + void prepare(not_null item); + + const not_null _history; + const std::unique_ptr _theme; + const std::unique_ptr _style; + const std::unique_ptr _delegate; + AdminLog::OwnedItem _item; + QPoint _position; + +}; + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +auto PreviewDelegate::elementPathShiftGradient() +-> not_null { + return _pathGradient.get(); +} + +Context PreviewDelegate::elementContext() { + return Context::History; +} + +PreviewWrap::PreviewWrap( + not_null parent, + not_null item) +: RpWidget(parent) +, _history(item->history()) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _style(std::make_unique( + _history->session().colorIndicesValue())) +, _delegate(std::make_unique( + parent, + _style.get(), + [=] { update(); })) +, _position(0, st::msgMargin.bottom()) { + _style->apply(_theme.get()); + + using namespace HistoryView; + _history->owner().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == _item.get()) { + update(); + } + }, lifetime()); + + _history->session().downloaderTaskFinished() | rpl::start_with_next([=] { + update(); + }, lifetime()); + + prepare(item); +} + +PreviewWrap::~PreviewWrap() { + _item = {}; +} + +void PreviewWrap::prepare(not_null item) { + _item = AdminLog::OwnedItem(_delegate.get(), item); + if (width() >= st::msgMinWidth) { + resizeTo(width()); + } + + widthValue( + ) | rpl::filter([=](int width) { + return width >= st::msgMinWidth; + }) | rpl::start_with_next([=](int width) { + resizeTo(width); + }, lifetime()); +} + +void PreviewWrap::resizeTo(int width) { + const auto height = _position.y() + + _item->resizeGetHeight(width) + + _position.y() + + st::msgServiceMargin.top() + + st::msgServiceGiftBoxTopSkip + - st::msgServiceMargin.bottom(); + resize(width, height); +} + +void PreviewWrap::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + + const auto clip = e->rect(); + if (!clip.isEmpty()) { + p.setClipRect(clip); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(width(), window()->height()), + clip); + } + + auto context = _theme->preparePaintContext( + _style.get(), + rect(), + e->rect(), + !window()->isActiveWindow()); + p.translate(_position); + _item->draw(p, context); +} + +} // namespace + +void PreparedPreviewBox( + not_null box, + not_null item, + rpl::producer> recipient, + Fn choose, + Fn)> send) { + box->setTitle(tr::lng_bot_share_prepared_title()); + const auto container = box->verticalLayout(); + container->add(object_ptr(container, item)); + const auto bot = item->viaBot(); + const auto name = bot ? bot->name() : u"Bot"_q; + const auto info = container->add( + object_ptr>( + container, + object_ptr( + container, + object_ptr( + container, + tr::lng_bot_share_prepared_about(lt_bot, rpl::single(name)), + st::boxDividerLabel), + st::defaultBoxDividerLabelPadding, + RectPart::Top | RectPart::Bottom))); + const auto row = container->add(object_ptr( + container)); + + const auto reset = [=] { + info->show(anim::type::instant); + while (row->count()) { + delete row->widgetAt(0); + } + box->addButton(tr::lng_bot_share_prepared_button(), choose); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + reset(); + + const auto lifetime = box->lifetime().make_state(); + std::move( + recipient + ) | rpl::start_with_next([=](not_null thread) { + info->hide(anim::type::instant); + while (row->count()) { + delete row->widgetAt(0); + } + AddSkip(row); + AddSinglePeerRow(row, thread, nullptr, choose); + if (const auto topic = thread->asTopic()) { + *lifetime = topic->destroyed() | rpl::start_with_next(reset); + } else { + *lifetime = rpl::lifetime(); + } + row->resizeToWidth(container->width()); + box->clearButtons(); + box->addButton(tr::lng_send_button(), [=] { send(thread); }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }, info->lifetime()); + + item->history()->owner().itemRemoved( + ) | rpl::start_with_next([=](not_null removed) { + if (removed == item) { + box->closeBox(); + } + }, box->lifetime()); +} + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h new file mode 100644 index 000000000..c2169a7cc --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_confirm_prepared.h @@ -0,0 +1,31 @@ +/* +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 { +class Thread; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace InlineBots { + +void PreparedPreviewBox( + not_null box, + not_null item, + rpl::producer> recipient, + Fn choose, + Fn)> sent); + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp new file mode 100644 index 000000000..d1c82d54a --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.cpp @@ -0,0 +1,421 @@ +/* +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 "inline_bots/inline_bot_downloads.h" + +#include "core/file_utilities.h" +#include "data/data_document.h" +#include "data/data_peer_id.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "storage/file_download_web.h" +#include "storage/serialize_common.h" +#include "storage/storage_account.h" +#include "ui/chat/attach/attach_bot_downloads.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/labels.h" +#include "styles/style_chat.h" + +#include +#include + +#include "base/call_delayed.h" + +namespace InlineBots { +namespace { + +constexpr auto kDownloadsVersion = 1; +constexpr auto kMaxDownloadsBots = 4096; +constexpr auto kMaxDownloadsPerBot = 16384; + +} // namespace + +Downloads::Downloads(not_null session) +: _session(session) { +} + +Downloads::~Downloads() { + base::take(_loaders); + base::take(_lists); +} + +DownloadId Downloads::start(StartArgs &&args) { + read(); + + const auto botId = args.bot->id; + const auto id = ++_autoIncrementId; + auto &list = _lists[botId].list; + list.push_back({ + .id = id, + .url = std::move(args.url), + .path = std::move(args.path), + }); + load(botId, id, list.back()); + return id; +} + +void Downloads::load( + PeerId botId, + DownloadId id, + DownloadsEntry &entry) { + entry.loading = 1; + entry.failed = 0; + + auto &loader = _loaders[id]; + Assert(!loader.loader); + loader.botId = botId; + loader.loader = std::make_unique( + _session, + entry.url, + entry.path, + WebRequestType::FullLoad); + + applyProgress(botId, id, 0, 0); + + loader.loader->updates( + ) | rpl::start_with_next_error_done([=] { + progress(botId, id); + }, [=](FileLoader::Error) { + fail(botId, id); + }, [=] { + done(botId, id); + }, loader.loader->lifetime()); + + loader.loader->start(); +} + +void Downloads::progress(PeerId botId, DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + const auto &loader = i->second.loader; + const auto total = loader->fullSize(); + const auto ready = loader->currentOffset(); + + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + + if (total < 0 || ready > total) { + fail(botId, id); + return; + } else if (ready == total) { + // Wait for 'done' signal. + return; + } + + applyProgress(botId, id, total, ready); +} + +void Downloads::fail(PeerId botId, DownloadId id, bool cancel) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + auto loader = std::move(i->second.loader); + _loaders.erase(i); + loader = nullptr; + + auto &list = _lists[botId].list; + const auto k = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(k != end(list)); + k->loading = 0; + k->failed = 1; + + if (cancel) { + auto copy = *k; + list.erase(k); + applyProgress(botId, copy, 0, 0); + } else { + applyProgress(botId, *k, 0, 0); + } +} + +void Downloads::done(PeerId botId, DownloadId id) { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + const auto total = i->second.loader->fullSize(); + if (total <= 0) { + fail(botId, id); + return; + } + _loaders.erase(i); + + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + j->loading = 0; + + applyProgress(botId, id, total, total); +} + +void Downloads::applyProgress( + PeerId botId, + DownloadId id, + int64 total, + int64 ready) { + Expects(total >= 0); + Expects(ready >= 0 && ready <= total); + + auto &list = _lists[botId].list; + const auto j = ranges::find( + list, + id, + &DownloadsEntry::id); + Assert(j != end(list)); + + applyProgress(botId, *j, total, ready); +} + +void Downloads::applyProgress( + PeerId botId, + DownloadsEntry &entry, + int64 total, + int64 ready) { + auto &progress = _progressView[botId]; + auto current = progress.current(); + auto subtract = int64(0); + if (current.ready == current.total) { + subtract = current.ready; + } + if (entry.total != total) { + const auto delta = total - entry.total; + entry.total = total; + current.total += delta; + } + if (entry.ready != ready) { + const auto delta = ready - entry.ready; + entry.ready = ready; + current.ready += delta; + } + if (subtract > 0 + && current.ready >= subtract + && current.total >= subtract) { + current.ready -= subtract; + current.total -= subtract; + } + if (entry.loading || current.ready < current.total) { + current.loading = 1; + } else { + current.loading = 0; + } + + if (total > 0 && total == ready) { + write(); + } + + progress = current; +} + +void Downloads::action( + not_null bot, + DownloadId id, + DownloadsAction type) { + switch (type) { + case DownloadsAction::Open: { + const auto i = ranges::find( + _lists[bot->id].list, + id, + &DownloadsEntry::id); + if (i == end(_lists[bot->id].list)) { + return; + } + File::ShowInFolder(i->path); + } break; + case DownloadsAction::Cancel: { + const auto i = _loaders.find(id); + if (i == end(_loaders)) { + return; + } + const auto botId = i->second.botId; + fail(botId, id, true); + } break; + case DownloadsAction::Retry: { + const auto i = ranges::find( + _lists[bot->id].list, + id, + &DownloadsEntry::id); + if (i == end(_lists[bot->id].list)) { + return; + } + load(bot->id, id, *i); + } break; + } +} + +[[nodiscard]] auto Downloads::progress(not_null bot) +->rpl::producer { + read(); + + return _progressView[bot->id].value(); +} + +const std::vector &Downloads::list( + not_null bot, + bool forceCheck) { + read(); + + auto &entry = _lists[bot->id]; + if (forceCheck) { + const auto was = int(entry.list.size()); + for (auto i = begin(entry.list); i != end(entry.list);) { + if (i->loading || i->failed) { + ++i; + } else if (auto info = QFileInfo(i->path) + ; !info.exists() || info.size() != i->total) { + i = entry.list.erase(i); + } else { + ++i; + } + } + if (int(entry.list.size()) != was) { + write(); + } + } + return entry.list; +} + +void Downloads::read() { + auto bytes = _session->local().readInlineBotsDownloads(); + if (bytes.isEmpty()) { + return; + } + + Assert(_lists.empty()); + + auto stream = QDataStream(&bytes, QIODevice::ReadOnly); + stream.setVersion(QDataStream::Qt_5_1); + + quint32 version = 0, count = 0; + stream >> version; + if (version != kDownloadsVersion) { + return; + } + stream >> count; + if (!count || count > kMaxDownloadsBots) { + return; + } + auto lists = base::flat_map(); + for (auto i = 0; i != count; ++i) { + quint64 rawBotId = 0; + quint32 count = 0; + stream >> rawBotId >> count; + const auto botId = DeserializePeerId(rawBotId); + if (!botId + || !peerIsUser(botId) + || count > kMaxDownloadsPerBot + || lists.contains(botId)) { + return; + } + auto &list = lists[botId]; + list.list.reserve(count); + for (auto j = 0; j != count; ++j) { + auto entry = DownloadsEntry(); + auto size = int64(); + stream >> entry.url >> entry.path >> size; + entry.total = entry.ready = size; + entry.id = ++_autoIncrementId; + list.list.push_back(std::move(entry)); + } + } + _lists = std::move(lists); +} + +void Downloads::write() { + auto size = sizeof(quint32) // version + + sizeof(quint32); // lists count + + for (const auto &[botId, list] : _lists) { + size += sizeof(quint64) // botId + + sizeof(quint32); // list count + for (const auto &entry : list.list) { + if (entry.total > 0 && entry.ready == entry.total) { + size += Serialize::stringSize(entry.url) + + Serialize::stringSize(entry.path) + + sizeof(quint64); // size + } + } + } + + auto bytes = QByteArray(); + bytes.reserve(size); + auto buffer = QBuffer(&bytes); + buffer.open(QIODevice::WriteOnly); + auto stream = QDataStream(&buffer); + stream.setVersion(QDataStream::Qt_5_1); + + stream << quint32(kDownloadsVersion) << quint32(_lists.size()); + + for (const auto &[botId, list] : _lists) { + stream << SerializePeerId(botId) << quint32(list.list.size()); + for (const auto &entry : list.list) { + if (entry.total > 0 && entry.ready == entry.total) { + stream << entry.url << entry.path << entry.total; + } + } + } + buffer.close(); + + _session->local().writeInlineBotsDownloads(bytes); +} + +void DownloadFileBox(not_null box, DownloadBoxArgs args) { + Expects(!args.name.isEmpty()); + + box->setTitle(tr::lng_bot_download_file()); + box->addRow(object_ptr( + box, + tr::lng_bot_download_file_sure( + lt_bot, + rpl::single(Ui::Text::Bold(args.bot)), + Ui::Text::RichLangValue), + st::botDownloadLabel)); + //box->addRow(MakeFilePreview(box, args)); + const auto done = std::move(args.done); + const auto name = args.name; + const auto session = args.session; + const auto chosen = std::make_shared(); + box->addButton(tr::lng_bot_download_file_button(), [=] { + const auto path = FileNameForSave( + session, + tr::lng_save_file(tr::now), + QString(), + u"file"_q, + name, + false, + QDir()); + if (!path.isEmpty()) { + *chosen = true; + box->closeBox(); + done(path); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + box->boxClosing() | rpl::start_with_next([=] { + if (!*chosen) { + done(QString()); + } + }, box->lifetime()); +} + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h new file mode 100644 index 000000000..1d66e6c8d --- /dev/null +++ b/Telegram/SourceFiles/inline_bots/inline_bot_downloads.h @@ -0,0 +1,105 @@ +/* +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/chat/attach/attach_bot_webview.h" + +class webFileLoader; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace InlineBots { + +using DownloadId = uint32; + +using ::Ui::BotWebView::DownloadsProgress; +using ::Ui::BotWebView::DownloadsEntry; +using ::Ui::BotWebView::DownloadsAction; + +class Downloads final { +public: + explicit Downloads(not_null session); + ~Downloads(); + + struct StartArgs { + not_null bot; + QString url; + QString path; + }; + uint32 start(StartArgs &&args); // Returns download id. + + void action( + not_null bot, + DownloadId id, + DownloadsAction type); + + [[nodiscard]] rpl::producer progress( + not_null bot); + [[nodiscard]] const std::vector &list( + not_null bot, + bool check = false); + +private: + struct List { + std::vector list; + }; + struct Loader { + std::unique_ptr loader; + PeerId botId = 0; + }; + + void read(); + void write(); + + void load( + PeerId botId, + DownloadId id, + DownloadsEntry &entry); + void progress(PeerId botId, DownloadId id); + void fail(PeerId botId, DownloadId id, bool cancel = false); + void done(PeerId botId, DownloadId id); + void applyProgress( + PeerId botId, + DownloadId id, + int64 total, + int64 ready); + void applyProgress( + PeerId botId, + DownloadsEntry &entry, + int64 total, + int64 ready); + + const not_null _session; + + base::flat_map _lists; + base::flat_map _loaders; + + base::flat_map< + PeerId, + rpl::variable> _progressView; + + DownloadId _autoIncrementId = 0; + +}; + +struct DownloadBoxArgs { + not_null session; + QString bot; + QString name; + QString url; + Fn done; +}; +void DownloadFileBox(not_null box, DownloadBoxArgs args); + +} // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 612d35769..7c487f843 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -378,14 +378,20 @@ bool Result::hasThumbDisplay() const { void Result::addToHistory( not_null history, HistoryItemCommonFields &&fields) const { - fields.flags |= MessageFlag::FromInlineBot; + history->addNewLocalMessage(makeMessage(history, std::move(fields))); +} + +not_null Result::makeMessage( + not_null history, + HistoryItemCommonFields &&fields) const { + fields.flags |= MessageFlag::FromInlineBot | MessageFlag::Local; if (_replyMarkup) { fields.markup = *_replyMarkup; if (!fields.markup.isNull()) { fields.flags |= MessageFlag::HasReplyMarkup; } } - sendData->addToHistory(this, history, std::move(fields)); + return sendData->makeMessage(this, history, std::move(fields)); } QString Result::getErrorOnSend(not_null history) const { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index cc7e090ee..e0b89e3d9 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -66,6 +66,9 @@ public: void addToHistory( not_null history, HistoryItemCommonFields &&fields) const; + [[nodiscard]] not_null makeMessage( + not_null history, + HistoryItemCommonFields &&fields) const; QString getErrorOnSend(not_null history) const; // interface for Layout:: usage diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index 0b8e9d8b7..714516b39 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -28,7 +28,7 @@ QString SendData::getLayoutDescription(const Result *owner) const { return owner->_description; } -void SendDataCommon::addToHistory( +not_null SendDataCommon::makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const { @@ -36,7 +36,7 @@ void SendDataCommon::addToHistory( if (fields.replyTo) { fields.flags |= MessageFlag::HasReplyInfo; } - history->addNewLocalMessage( + return history->makeMessage( std::move(fields), std::move(distinct.text), std::move(distinct.media)); @@ -96,14 +96,14 @@ QString SendContact::getLayoutDescription(const Result *owner) const { return result; } -void SendPhoto::addToHistory( +not_null SendPhoto::makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const { - history->addNewLocalMessage( + return history->makeMessage( std::move(fields), _photo, - { _message, _entities }); + TextWithEntities{ _message, _entities }); } QString SendPhoto::getErrorOnSend( @@ -113,14 +113,14 @@ QString SendPhoto::getErrorOnSend( return Data::RestrictionError(history->peer, type).value_or(QString()); } -void SendFile::addToHistory( +not_null SendFile::makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const { - history->addNewLocalMessage( + return history->makeMessage( std::move(fields), _document, - { _message, _entities }); + TextWithEntities{ _message, _entities }); } QString SendFile::getErrorOnSend( @@ -130,11 +130,11 @@ QString SendFile::getErrorOnSend( return Data::RestrictionError(history->peer, type).value_or(QString()); } -void SendGame::addToHistory( +not_null SendGame::makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const { - history->addNewLocalMessage(std::move(fields), _game); + return history->addNewLocalMessage(std::move(fields), _game); } QString SendGame::getErrorOnSend( diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index 502cc006e..9c53b6207 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -40,7 +40,7 @@ public: virtual bool isValid() const = 0; - virtual void addToHistory( + virtual not_null makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const = 0; @@ -75,7 +75,7 @@ public: }; virtual SentMessageFields getSentMessageFields() const = 0; - void addToHistory( + not_null makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const override; @@ -236,7 +236,7 @@ public: return _photo != nullptr; } - void addToHistory( + not_null makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const override; @@ -270,7 +270,7 @@ public: return _document != nullptr; } - void addToHistory( + not_null makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const override; @@ -298,7 +298,7 @@ public: return _game != nullptr; } - void addToHistory( + not_null makeMessage( const Result *owner, not_null history, HistoryItemCommonFields &&fields) const override; diff --git a/Telegram/SourceFiles/intro/intro.style b/Telegram/SourceFiles/intro/intro.style index 60fe09e2f..c2ea8389c 100644 --- a/Telegram/SourceFiles/intro/intro.style +++ b/Telegram/SourceFiles/intro/intro.style @@ -165,9 +165,7 @@ introBackButton: IconButton(defaultIconButton) { rippleAreaPosition: point(8px, 8px); rippleAreaSize: 40px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } introQrTop: -18px; diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index cea363566..a3129f5d2 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -17,9 +17,7 @@ ivMenuToggle: IconButton(defaultIconButton) { rippleAreaPosition: point(6px, 6px); rippleAreaSize: 36px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } ivMenuPosition: point(-2px, 40px); ivBackIcon: icon {{ "box_button_back", menuIconColor }}; @@ -36,9 +34,7 @@ ivPlusMinusZoom: IconButton(ivMenuToggle) { rippleAreaPosition: point(0px, 0px); rippleAreaSize: ivZoomButtonsSize; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + ripple: defaultRippleAnimationBgOver; } ivResetZoomStyle: TextStyle(defaultTextStyle) { font: font(12px); diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index faf297d2c..d262b0d75 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/iv_controller.h" #include "base/platform/base_platform_info.h" +#include "base/qt/qt_key_modifiers.h" #include "base/invoke_queued.h" #include "base/qt_signal_producer.h" #include "base/qthelp_url.h" @@ -21,9 +22,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/menu/menu_action.h" #include "ui/widgets/rp_window.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" #include "ui/basic_click_handlers.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/webview_helpers.h" #include "ui/ui_utility.h" #include "webview/webview_data_stream_memory.h" @@ -56,9 +59,13 @@ namespace Iv { namespace { constexpr auto kZoomStep = int(10); +constexpr auto kZoomSmallStep = int(5); +constexpr auto kZoomTinyStep = int(1); constexpr auto kDefaultZoom = int(100); -class ItemZoom final : public Ui::Menu::Action { +class ItemZoom final + : public Ui::Menu::Action + , public Ui::AbstractTooltipShower { public: ItemZoom( not_null parent, @@ -80,43 +87,21 @@ public: AbstractButton::setDisabled(true); - class SmallButton final : public Ui::IconButton { - public: - SmallButton( - not_null parent, - QChar c, - float64 skip, - const style::color &color) - : Ui::IconButton(parent, st::ivPlusMinusZoom) - , _color(color) - , _skip(style::ConvertFloatScale(skip)) - , _c(c) { - } - - void paintEvent(QPaintEvent *event) override { - auto p = Painter(this); - Ui::RippleButton::paintRipple( - p, - st::ivPlusMinusZoom.rippleAreaPosition); - p.setPen(_color); - p.setFont(st::normalFont); - p.drawText( - QRectF(rect()).translated(0, _skip), - _c, - style::al_center); - } - - private: - const style::color _color; - const float64 _skip; - const QChar _c; - + const auto processTooltip = [=](not_null w) { + w->events() | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Enter) { + Ui::Tooltip::Show(1000, this); + } else if (e->type() == QEvent::Leave) { + Ui::Tooltip::Hide(); + } + }, w->lifetime()); }; const auto reset = Ui::CreateChild( this, rpl::single(QString()), st::ivResetZoom); + processTooltip(reset); const auto resetLabel = Ui::CreateChild( reset, tr::lng_background_reset_default(), @@ -127,25 +112,60 @@ public: _delegate->ivSetZoom(kDefaultZoom); }); reset->show(); - const auto plus = Ui::CreateChild( + const auto plus = Ui::CreateSimpleCircleButton( this, - '+', - 0, - _st.itemFg); - plus->setClickedCallback([this] { - _delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep); + st::defaultRippleAnimationBgOver); + plus->resize(Size(st::ivZoomButtonsSize)); + plus->paintRequest() | rpl::start_with_next([=, fg = _st.itemFg] { + auto p = QPainter(plus); + p.setPen(fg); + p.setFont(st::normalFont); + p.drawText(plus->rect(), QChar('+'), style::al_center); + }, plus->lifetime()); + processTooltip(plus); + const auto step = [] { + return base::IsAltPressed() + ? kZoomTinyStep + : base::IsCtrlPressed() + ? kZoomSmallStep + : kZoomStep; + }; + plus->setClickedCallback([this, step] { + _delegate->ivSetZoom(_delegate->ivZoom() + step()); }); plus->show(); - const auto minus = Ui::CreateChild( + const auto minus = Ui::CreateSimpleCircleButton( this, - QChar(0x2013), - -1, - _st.itemFg); - minus->setClickedCallback([this] { - _delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep); + st::defaultRippleAnimationBgOver); + minus->resize(Size(st::ivZoomButtonsSize)); + minus->paintRequest() | rpl::start_with_next([=, fg = _st.itemFg] { + auto p = QPainter(minus); + const auto r = minus->rect(); + p.setPen(fg); + p.setFont(st::normalFont); + p.drawText( + QRectF(r).translated(0, style::ConvertFloatScale(-1)), + QChar(0x2013), + style::al_center); + }, minus->lifetime()); + processTooltip(minus); + minus->setClickedCallback([this, step] { + _delegate->ivSetZoom(_delegate->ivZoom() - step()); }); minus->show(); + { + const auto maxWidthText = u"000%"_q; + _text.setText(_st.itemStyle, maxWidthText); + Ui::Menu::ItemBase::setMinWidth( + _text.maxWidth() + + st::ivResetZoomInnerPadding + + resetLabel->width() + + plus->width() + + minus->width() + + _st.itemPadding.right() * 2); + } + _delegate->ivZoomValue( ) | rpl::start_with_next([this](int value) { _text.setText(_st.itemStyle, QString::number(value) + '%'); @@ -155,7 +175,7 @@ public: rpl::combine( sizeValue(), reset->sizeValue() - ) | rpl::start_with_next([=, this](const QSize &size, const QSize &) { + ) | rpl::start_with_next([=](const QSize &size, const QSize &) { reset->setFullWidth(0 + resetLabel->width() + st::ivResetZoomInnerPadding); @@ -186,6 +206,22 @@ public: }); } + QString tooltipText() const override { +#ifdef Q_OS_MAC + return tr::lng_iv_zoom_tooltip_cmd(tr::now); +#else + return tr::lng_iv_zoom_tooltip_ctrl(tr::now); +#endif + } + + QPoint tooltipPos() const override { + return QCursor::pos(); + } + + bool tooltipWindowActive() const override { + return true; + } + private: const not_null _delegate; const style::Menu &_st; @@ -349,10 +385,16 @@ Controller::Controller( Fn showShareBox) : _delegate(delegate) , _updateStyles([=] { - const auto zoom = _delegate->ivZoom(); - const auto str = Ui::EscapeForScriptString(ComputeStyles(zoom)); if (_webview) { + const auto webviewZoomController = _webview->zoomController(); + const auto styleZoom = webviewZoomController + ? kDefaultZoom + : _delegate->ivZoom(); + const auto str = Ui::EscapeForScriptString(ComputeStyles(styleZoom)); _webview->eval("IV.updateStyles('" + str + "');"); + if (webviewZoomController) { + webviewZoomController->setZoom(_delegate->ivZoom()); + } } }) , _showShareBox(std::move(showShareBox)) { @@ -619,6 +661,19 @@ void Controller::createWebview(const Webview::StorageId &storageId) { }); const auto raw = _webview.get(); + if (const auto webviewZoomController = raw->zoomController()) { + webviewZoomController->zoomValue( + ) | rpl::start_with_next([this](int value) { + if (value > 0) { + _delegate->ivSetZoom(value); + } + }, lifetime()); + _delegate->ivZoomValue( + ) | rpl::start_with_next([=](int value) { + webviewZoomController->setZoom(value); + }, lifetime()); + } + window->lifetime().add([=] { _ready = false; base::take(_webview); @@ -770,8 +825,12 @@ void Controller::createWebview(const Webview::StorageId &storageId) { || index >= _pages.size()) { return Webview::DataResult::Failed; } + const auto webviewZoomController = _webview->zoomController(); + const auto styleZoom = webviewZoomController + ? kDefaultZoom + : _delegate->ivZoom(); return finishWith( - WrapPage(_pages[index], _delegate->ivZoom()), + WrapPage(_pages[index], styleZoom), "text/html; charset=utf-8"); } else if (id.starts_with("page") && id.ends_with(".json")) { auto index = 0; @@ -942,6 +1001,8 @@ void Controller::processKey(const QString &key, const QString &modifier) { minimize(); } else if (key == u"q"_q && modifier == ctrl) { quit(); + } else if (key == u"0"_q && modifier == ctrl) { + _delegate->ivSetZoom(kDefaultZoom); } } diff --git a/Telegram/SourceFiles/iv/iv_prepare.cpp b/Telegram/SourceFiles/iv/iv_prepare.cpp index 2f8a5398b..6fc891a19 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.cpp +++ b/Telegram/SourceFiles/iv/iv_prepare.cpp @@ -389,11 +389,17 @@ QByteArray Parser::block(const MTPDpageBlockUnsupported &data) { } QByteArray Parser::block(const MTPDpageBlockTitle &data) { - return tag("h1", { { "class", "title" } }, rich(data.vtext())); + return tag("h1", { + { "class", "title" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockSubtitle &data) { - return tag("h2", { { "class", "subtitle" } }, rich(data.vtext())); + return tag("h2", { + { "class", "subtitle" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) { @@ -401,23 +407,29 @@ QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) { if (const auto date = data.vpublished_date().v) { inner += " \xE2\x80\xA2 " + tag("time", Date(date)); } - return tag("address", inner); + return tag("address", { { "dir", "auto" } }, inner); } QByteArray Parser::block(const MTPDpageBlockHeader &data) { - return tag("h3", { { "class", "header" } }, rich(data.vtext())); + return tag("h3", { + { "class", "header" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockSubheader &data) { - return tag("h4", { { "class", "subheader" } }, rich(data.vtext())); + return tag("h4", { + { "class", "subheader" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockParagraph &data) { - return tag("p", rich(data.vtext())); + return tag("p", { { "dir", "auto" } }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockPreformatted &data) { - auto list = Attributes(); + auto list = Attributes{ { "dir", "auto" } }; const auto language = utf(data.vlanguage()); if (!language.isEmpty()) { list.push_back({ "data-language", language }); @@ -428,7 +440,10 @@ QByteArray Parser::block(const MTPDpageBlockPreformatted &data) { } QByteArray Parser::block(const MTPDpageBlockFooter &data) { - return tag("footer", { { "class", "footer" } }, rich(data.vtext())); + return tag("footer", { + { "class", "footer" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockDivider &data) { @@ -447,19 +462,21 @@ QByteArray Parser::block(const MTPDpageBlockBlockquote &data) { const auto caption = rich(data.vcaption()); const auto cite = caption.isEmpty() ? QByteArray() - : tag("cite", caption); - return tag("blockquote", rich(data.vtext()) + cite); + : tag("cite", { { "dir", "auto" } }, caption); + return tag("blockquote", { + { "dir", "auto" } + }, rich(data.vtext()) + cite); } QByteArray Parser::block(const MTPDpageBlockPullquote &data) { const auto caption = rich(data.vcaption()); const auto cite = caption.isEmpty() ? QByteArray() - : tag("cite", caption); - return tag( - "div", - { { "class", "pullquote" } }, - rich(data.vtext()) + cite); + : tag("cite", { { "dir", "auto" } }, caption); + return tag("div", { + { "class", "pullquote" }, + { "dir", "auto" }, + }, rich(data.vtext()) + cite); } QByteArray Parser::block( @@ -763,7 +780,10 @@ QByteArray Parser::block(const MTPDpageBlockAudio &data) { } QByteArray Parser::block(const MTPDpageBlockKicker &data) { - return tag("h5", { { "class", "kicker" } }, rich(data.vtext())); + return tag("h5", { + { "class", "kicker" }, + { "dir", "auto" }, + }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageBlockTable &data) { @@ -780,7 +800,7 @@ QByteArray Parser::block(const MTPDpageBlockTable &data) { } auto title = rich(data.vtitle()); if (!title.isEmpty()) { - title = tag("caption", title); + title = tag("caption", { { "dir", "auto" } }, title); } auto result = tag("table", attibutes, title + list(data.vrows())); result = tag("figure", { { "class", "table" } }, result); @@ -800,7 +820,8 @@ QByteArray Parser::block(const MTPDpageBlockDetails &data) { return tag( "details", attributes, - tag("summary", rich(data.vtitle())) + list(data.vblocks())); + (tag("summary", { { "dir", "auto" } }, rich(data.vtitle())) + + list(data.vblocks()))); } QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) { @@ -810,7 +831,10 @@ QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) { } auto title = rich(data.vtitle()); if (!title.isEmpty()) { - title = tag("h4", { { "class", "related-title" } }, title); + title = tag("h4", { + { "class", "related-title" }, + { "dir", "auto" }, + }, title); } return tag("section", { { "class", "related" } }, title + result); } @@ -899,7 +923,7 @@ QByteArray Parser::block(const MTPDpageTableCell &data) { } else { style += "vertical-align:top;"; } - auto attributes = Attributes{ { "style", style } }; + auto attributes = Attributes{ { "style", style }, { "dir", "auto" } }; if (const auto cs = data.vcolspan()) { attributes.push_back({ "colspan", Number(cs->v) }); } @@ -910,7 +934,7 @@ QByteArray Parser::block(const MTPDpageTableCell &data) { } QByteArray Parser::block(const MTPDpageListItemText &data) { - return tag("li", rich(data.vtext())); + return tag("li", { { "dir", "auto" } }, rich(data.vtext())); } QByteArray Parser::block(const MTPDpageListItemBlocks &data) { @@ -920,7 +944,7 @@ QByteArray Parser::block(const MTPDpageListItemBlocks &data) { QByteArray Parser::block(const MTPDpageListOrderedItemText &data) { return tag( "li", - { { "value", utf(data.vnum()) } }, + { { "value", utf(data.vnum()) }, { "dir", "auto" } }, rich(data.vtext())); } @@ -1073,11 +1097,11 @@ QByteArray Parser::caption(const MTPPageCaption &caption) { auto text = rich(caption.data().vtext()); const auto credit = rich(caption.data().vcredit()); if (!credit.isEmpty()) { - text += tag("cite", credit); + text += tag("cite", { { "dir", "auto" } }, credit); } else if (text.isEmpty()) { return QByteArray(); } - return tag("figcaption", text); + return tag("figcaption", { { "dir", "auto" } }, text); } Photo Parser::parse(const MTPPhoto &photo) { diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index 7b1a10acb..2435a77eb 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -929,6 +929,7 @@ ShortenedCount FormatCountToShort(int64 number) { // Update given number. // E.g. 12345 will be 12000. result.number = rounded * divider; + result.shortened = true; }; if (abs >= 1'000'000) { shorten(1'000'000, 'M'); diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 297ae1ff2..b1163b06e 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -22,6 +22,7 @@ constexpr auto kTagReplacementSize = 4; struct ShortenedCount { int64 number = 0; QString string; + bool shortened = false; }; [[nodiscard]] ShortenedCount FormatCountToShort(int64 number); [[nodiscard]] QString FormatCountDecimal(int64 number); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index dc5f6d51f..f325c8ea0 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -42,6 +42,12 @@ int AppConfig::quoteLengthMax() const { return get(u"quote_length_max"_q, 1024); } +int AppConfig::stargiftConvertPeriodMax() const { + return get( + u"stargifts_convert_period_max"_q, + _account->mtp().isTestMode() ? 300 : (90 * 86400)); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index fc02858a3..5768ad89a 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -64,6 +64,7 @@ public: } [[nodiscard]] int quoteLengthMax() const; + [[nodiscard]] int stargiftConvertPeriodMax() const; void refresh(bool force = false); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 795d7135f..5cd766f03 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_history_hider.h" #include "window/window_controller.h" #include "window/window_peer_menu.h" +#include "window/window_session_controller_link_info.h" #include "window/themes/window_theme.h" #include "chat_helpers/bot_command.h" #include "chat_helpers/tabbed_selector.h" // TabbedSelector::refreshStickers @@ -743,14 +744,26 @@ void MainWidget::hideSingleUseKeyboard(FullMsgId replyToId) { _history->hideSingleUseKeyboard(replyToId); } -void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat, PeerData *from) { +void MainWidget::searchMessages( + const QString &query, + Dialogs::Key inChat, + PeerData *searchFrom) { + const auto complex = Data::HashtagWithUsernameFromQuery(query); + if (!complex.username.isEmpty()) { + _controller->showPeerByLink(Window::PeerByLinkInfo{ + .usernameOrId = complex.username, + .text = complex.hashtag, + .resolveType = Window::ResolveType::HashtagSearch, + }); + return; + } auto tags = Data::SearchTagsFromQuery(query); if (_dialogs) { auto state = Dialogs::SearchState{ .inChat = ((tags.empty() || inChat.sublist()) ? inChat : session().data().history(session().user())), - .fromPeer = from, + .fromPeer = inChat ? searchFrom : nullptr, .tags = tags, .query = tags.empty() ? query : QString(), }; @@ -770,12 +783,15 @@ void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat, PeerD controller()->session().user()); } if ((!_mainSection - || !_mainSection->searchInChatEmbedded(inChat, query)) - && !_history->searchInChatEmbedded(inChat, query)) { + || !_mainSection->searchInChatEmbedded(query, inChat, searchFrom)) + && !_history->searchInChatEmbedded(query, inChat, searchFrom)) { 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, from); + controller->content()->searchMessages( + query, + inChat, + searchFrom); controller->widget()->activate(); } } @@ -1082,6 +1098,12 @@ void MainWidget::dialogsCancelled() { _history->activate(); } +void MainWidget::toggleFiltersMenu(bool value) const { + if (_dialogs) { + _dialogs->toggleFiltersMenu(value); + } +} + void MainWidget::setChatBackground( const Data::WallPaper &background, QImage &&image) { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 4eac9fd2e..5d6379896 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -163,7 +163,10 @@ public: void sendBotCommand(Bot::SendCommandRequest request); void hideSingleUseKeyboard(FullMsgId replyToId); - void searchMessages(const QString &query, Dialogs::Key inChat, PeerData *from = nullptr); + void searchMessages( + const QString &query, + Dialogs::Key inChat, + PeerData *searchFrom = nullptr); void setChatBackground( const Data::WallPaper &background, @@ -210,6 +213,7 @@ public: void showNonPremiumLimitToast(bool download); void dialogsCancelled(); + void toggleFiltersMenu(bool value) const; private: void paintEvent(QPaintEvent *e) override; diff --git a/Telegram/SourceFiles/media/audio/media_audio.cpp b/Telegram/SourceFiles/media/audio/media_audio.cpp index 2b4c57084..86fe5ce21 100644 --- a/Telegram/SourceFiles/media/audio/media_audio.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio.cpp @@ -806,10 +806,9 @@ void Mixer::externalSoundProgress(const AudioMsgId &audio) { } bool Mixer::checkCurrentALError(AudioMsgId::Type type) { - if (!Audio::PlaybackErrorHappened()) return true; - - const auto data = trackForType(type); - if (!data) { + if (!Audio::PlaybackErrorHappened()) { + return true; + } else if (const auto data = trackForType(type)) { setStoppedState(data, State::StoppedAtError); onError(data->state.id); } diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp index 1bbf9b10c..d46f8789c 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp @@ -88,13 +88,15 @@ public: void start( Webrtc::DeviceResolvedId id, Fn updated, - Fn error); + Fn error, + Fn externalProcessing); void stop(Fn callback = nullptr); void pause(bool value, Fn callback); private: void process(); + bool initializeFFmpeg(); [[nodiscard]] bool processFrame(int32 offset, int32 framesize); void fail(); @@ -104,6 +106,7 @@ private: // Returns number of packets written or -1 on error [[nodiscard]] int writePackets(); + Fn _externalProcessing; Fn _updated; Fn _error; @@ -131,7 +134,7 @@ Instance::Instance() : _inner(std::make_unique(&_thread)) { _thread.start(); } -void Instance::start() { +void Instance::start(Fn externalProcessing) { _updates.fire_done(); const auto id = Audio::Current().captureDeviceId(); InvokeQueued(_inner.get(), [=] { @@ -141,9 +144,9 @@ void Instance::start() { }); }, [=] { crl::on_main(this, [=] { - _updates.fire_error({}); + _updates.fire_error(Error::Other); }); - }); + }, externalProcessing); crl::on_main(this, [=] { _started = true; }); @@ -167,13 +170,15 @@ void Instance::stop(Fn callback) { } void Instance::pause(bool value, Fn callback) { - Expects(callback != nullptr || !value); InvokeQueued(_inner.get(), [=] { - _inner->pause(value, [=](Result &&result) { - crl::on_main([=, result = std::move(result)]() mutable { - callback(std::move(result)); - }); - }); + auto done = callback + ? [=](Result &&result) { + crl::on_main([=, result = std::move(result)]() mutable { + callback(std::move(result)); + }); + } + : std::move(callback); + _inner->pause(value, std::move(done)); }); } @@ -304,7 +309,9 @@ void Instance::Inner::fail() { void Instance::Inner::start( Webrtc::DeviceResolvedId id, Fn updated, - Fn error) { + Fn error, + Fn externalProcessing) { + _externalProcessing = std::move(externalProcessing); _updated = std::move(updated); _error = std::move(error); if (_paused) { @@ -329,8 +336,19 @@ void Instance::Inner::start( d->device = nullptr; fail(); return; + } else if (!_externalProcessing) { + if (!initializeFFmpeg()) { + fail(); + return; + } } + _timer.callEach(50); + _captured.clear(); + _captured.reserve(kCaptureBufferSlice); + DEBUG_LOG(("Audio Capture: started!")); +} +bool Instance::Inner::initializeFFmpeg() { // Create encoding context d->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize); @@ -347,14 +365,12 @@ void Instance::Inner::start( } if (!fmt) { LOG(("Audio Error: Unable to find opus AVOutputFormat for capture")); - fail(); - return; + return false; } if ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)fmt, 0, 0)) < 0) { LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } d->fmtContext->pb = d->ioContext; d->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO; @@ -364,21 +380,18 @@ void Instance::Inner::start( d->codec = avcodec_find_encoder(fmt->audio_codec); if (!d->codec) { LOG(("Audio Error: Unable to avcodec_find_encoder for capture")); - fail(); - return; + return false; } d->stream = avformat_new_stream(d->fmtContext, d->codec); if (!d->stream) { LOG(("Audio Error: Unable to avformat_new_stream for capture")); - fail(); - return; + return false; } d->stream->id = d->fmtContext->nb_streams - 1; d->codecContext = avcodec_alloc_context3(d->codec); if (!d->codecContext) { LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture")); - fail(); - return; + return false; } av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0); @@ -401,8 +414,7 @@ void Instance::Inner::start( // Open audio stream if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) { LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } // Alloc source samples @@ -443,39 +455,27 @@ void Instance::Inner::start( #endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT if (res < 0 || !d->swrContext) { LOG(("Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } else if ((res = swr_init(d->swrContext)) < 0) { LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - d->maxDstSamples = d->srcSamples; if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) { LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } d->dstSamplesSize = av_samples_get_buffer_size(0, d->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0); - if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) { LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - // Write file header if ((res = avformat_write_header(d->fmtContext, 0)) < 0) { LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); - fail(); - return; + return false; } - - _timer.callEach(50); - _captured.clear(); - _captured.reserve(kCaptureBufferSlice); - DEBUG_LOG(("Audio Capture: started!")); + return true; } void Instance::Inner::pause(bool value, Fn callback) { @@ -483,11 +483,16 @@ void Instance::Inner::pause(bool value, Fn callback) { if (!_paused) { return; } - callback({ - d->fullSamples ? d->data : QByteArray(), - d->fullSamples ? CollectWaveform(d->waveform) : VoiceWaveform(), - qint32(d->fullSamples), - }); + if (callback) { + callback({ + .bytes = d->fullSamples ? d->data : QByteArray(), + .waveform = (d->fullSamples + ? CollectWaveform(d->waveform) + : VoiceWaveform()), + .duration = ((d->fullSamples * crl::time(1000)) + / int64(kCaptureFrequency)), + }); + } } void Instance::Inner::stop(Fn callback) { @@ -559,7 +564,7 @@ void Instance::Inner::stop(Fn callback) { _captured = QByteArray(); // Finish stream - if (needResult && hadDevice) { + if (needResult && hadDevice && d->fmtContext) { av_write_trailer(d->fmtContext); } @@ -622,7 +627,11 @@ void Instance::Inner::stop(Fn callback) { } if (needResult) { - callback({ result, waveform, samples }); + callback({ + .bytes = result, + .waveform = waveform, + .duration = (samples * crl::time(1000)) / kCaptureFrequency, + }); } } @@ -658,6 +667,13 @@ void Instance::Inner::process() { if (ErrorHappened(d->device)) { fail(); return; + } else if (_externalProcessing) { + _externalProcessing({ + .finished = crl::now(), + .samples = base::take(_captured), + .frequency = kCaptureFrequency, + }); + return; } // Count new recording level and update view diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.h b/Telegram/SourceFiles/media/audio/media_audio_capture.h index 2b46bd7fa..fb79b39dd 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.h @@ -7,16 +7,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include #include -struct AVFrame; - namespace Media { namespace Capture { struct Update { int samples = 0; ushort level = 0; + + bool finished = false; +}; + +enum class Error : uchar { + Other, + AudioInit, + VideoInit, + AudioTimeout, + VideoTimeout, + Encoding, +}; + +struct Chunk { + crl::time finished = 0; + QByteArray samples; + int frequency = 0; }; struct Result; @@ -34,7 +50,7 @@ public: return _available; } - [[nodiscard]] rpl::producer updated() const { + [[nodiscard]] rpl::producer updated() const { return _updates.events(); } @@ -45,9 +61,9 @@ public: return _started.changes(); } - void start(); + void start(Fn externalProcessing = nullptr); void stop(Fn callback = nullptr); - void pause(bool value, Fn callback); + void pause(bool value, Fn callback = nullptr); private: class Inner; @@ -55,7 +71,7 @@ private: bool _available = false; rpl::variable _started = false; - rpl::event_stream _updates; + rpl::event_stream _updates; QThread _thread; std::unique_ptr _inner; diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture_common.h b/Telegram/SourceFiles/media/audio/media_audio_capture_common.h index 370259f58..e276ad4e9 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture_common.h +++ b/Telegram/SourceFiles/media/audio/media_audio_capture_common.h @@ -12,7 +12,8 @@ namespace Media::Capture { struct Result { QByteArray bytes; VoiceWaveform waveform; - int samples = 0; + crl::time duration; + bool video = false; }; } // namespace Media::Capture diff --git a/Telegram/SourceFiles/media/media_common.h b/Telegram/SourceFiles/media/media_common.h index c155a4084..1f757e581 100644 --- a/Telegram/SourceFiles/media/media_common.h +++ b/Telegram/SourceFiles/media/media_common.h @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" +#include + namespace Media { enum class RepeatMode { @@ -23,6 +25,18 @@ enum class OrderMode { Shuffle, }; +struct VideoQuality { + uint32 manual : 1 = 0; + uint32 height : 31 = 0; + + friend inline constexpr auto operator<=>( + VideoQuality, + VideoQuality) = default; + friend inline constexpr bool operator==( + VideoQuality, + VideoQuality) = default; +}; + inline constexpr auto kSpeedMin = 0.5; inline constexpr auto kSpeedMax = 2.5; inline constexpr auto kSpedUpDefault = 1.7; diff --git a/Telegram/SourceFiles/media/player/media_player.style b/Telegram/SourceFiles/media/player/media_player.style index 048eb4549..dae713684 100644 --- a/Telegram/SourceFiles/media/player/media_player.style +++ b/Telegram/SourceFiles/media/player/media_player.style @@ -29,6 +29,7 @@ MediaPlayerButton { MediaSpeedMenu { dropdown: DropdownMenu; + qualityMenu: Menu; activeCheck: icon; activeCheckSkip: pixels; sliderStyle: TextStyle; @@ -165,16 +166,20 @@ mediaPlayerMenu: DropdownMenu(defaultDropdownMenu) { } mediaPlayerMenuCheck: icon {{ "player/player_check", mediaPlayerActiveFg }}; +mediaPlayerSpeedMenuInner: Menu(menuWithIcons) { + separator: MenuSeparator(defaultMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + itemPadding: margins(54px, 7px, 54px, 9px); + itemFgDisabled: mediaPlayerActiveFg; +} mediaPlayerSpeedMenu: MediaSpeedMenu { dropdown: DropdownMenu(mediaPlayerMenu) { - menu: Menu(menuWithIcons) { - separator: MenuSeparator(defaultMenuSeparator) { - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } - itemPadding: margins(54px, 7px, 54px, 9px); - itemFgDisabled: mediaPlayerActiveFg; - } + menu: mediaPlayerSpeedMenuInner; + } + qualityMenu: Menu(mediaPlayerSpeedMenuInner) { + itemPadding: margins(17px, 7px, 54px, 9px); } activeCheck: mediaPlayerMenuCheck; activeCheckSkip: 8px; diff --git a/Telegram/SourceFiles/media/player/media_player_button.cpp b/Telegram/SourceFiles/media/player/media_player_button.cpp index 0e9ab424a..2fc6b8877 100644 --- a/Telegram/SourceFiles/media/player/media_player_button.cpp +++ b/Telegram/SourceFiles/media/player/media_player_button.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/ripple_animation.h" #include "ui/painter.h" #include "styles/style_media_player.h" +#include "styles/style_media_view.h" #include @@ -307,7 +308,7 @@ SpeedButton::SpeedButton(QWidget *parent, const style::MediaSpeedButton &st) resize(_st.size); } -void SpeedButton::setSpeed(float64 speed, anim::type animated) { +void SpeedButton::setSpeed(float64 speed) { _isDefault = EqualSpeeds(speed, 1.); _layout.setSpeed(speed); update(); @@ -337,4 +338,168 @@ QImage SpeedButton::prepareRippleMask() const { _st.rippleRadius); } +SettingsButton::SettingsButton( + QWidget *parent, + const style::MediaSpeedButton &st) +: RippleButton(parent, st.ripple) +, _st(st) +, _isDefaultSpeed(true) { + resize(_st.size); +} + +void SettingsButton::setSpeed(float64 speed) { + if (_speed != speed) { + _speed = speed; + _isDefaultSpeed = EqualSpeeds(speed, 1.); + update(); + } +} + +void SettingsButton::setQuality(int quality) { + if (_quality != quality) { + _quality = quality; + update(); + } +} + +void SettingsButton::setActive(bool active) { + if (_active == active) { + return; + } + _active = active; + _activeAnimation.start([=] { + update(); + }, active ? 0. : 1., active ? 1. : 0., st::mediaviewOverDuration); +} + +void SettingsButton::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + const auto nowOver = isOver(); + const auto wasOver = static_cast(was & StateFlag::Over); + if (nowOver != wasOver) { + _overAnimation.start([=] { + update(); + }, nowOver ? 0. : 1., nowOver ? 1. : 0., st::mediaviewOverDuration); + } +} + +void SettingsButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + paintRipple( + p, + QPoint(_st.padding.left(), _st.padding.top()), + _isDefaultSpeed ? nullptr : &_st.rippleActiveColor->c); + + prepareFrame(); + p.drawImage(0, 0, _frameCache); +} + +void SettingsButton::prepareFrame() { + const auto ratio = style::DevicePixelRatio(); + if (_frameCache.size() != _st.size * ratio) { + _frameCache = QImage( + _st.size * ratio, + QImage::Format_ARGB32_Premultiplied); + _frameCache.setDevicePixelRatio(ratio); + } + _frameCache.fill(Qt::transparent); + auto p = QPainter(&_frameCache); + + const auto inner = QRect( + QPoint(), + _st.size + ).marginsRemoved(_st.padding); + + auto hq = std::optional(); + const auto over = _overAnimation.value(isOver() ? 1. : 0.); + const auto color = anim::color(_st.fg, _st.overFg, over); + const auto active = _activeAnimation.value(_active ? 1. : 0.); + if (active > 0.) { + const auto shift = QRectF(inner).center(); + p.save(); + p.translate(shift); + p.rotate(active * 60.); + p.translate(-shift); + hq.emplace(p); + } + _st.icon.paintInCenter(p, inner, color); + if (active > 0.) { + p.restore(); + hq.reset(); + } + + const auto rounded = int(base::SafeRound(_speed * 10)); + if (rounded != 10) { + const auto text = (rounded % 10) + ? QString::number(rounded / 10.) + : u"%1X"_q.arg(rounded / 10); + paintBadge(p, text, RectPart::TopLeft, color); + } + const auto text = (!_quality) + ? QString() + : (_quality > 2000) + ? u"4K"_q + : (_quality > 1000) + ? u"FHD"_q + : (_quality > 700) + ? u"HD"_q + : u"SD"_q; + if (!text.isEmpty()) { + paintBadge(p, text, RectPart::BottomRight, color); + } +} + +void SettingsButton::paintBadge( + QPainter &p, + const QString &text, + RectPart origin, + QColor color) { + auto hq = PainterHighQualityEnabler(p); + const auto xpadding = style::ConvertScale(2.); + const auto ypadding = 0; + const auto skip = style::ConvertScale(2.); + const auto width = _st.font->width(text); + const auto height = _st.font->height; + const auto radius = height / 3.; + const auto left = (origin == RectPart::TopLeft) + || (origin == RectPart::BottomLeft); + const auto top = (origin == RectPart::TopLeft) + || (origin == RectPart::TopRight); + const auto x = left ? 0 : (_st.size.width() - width - 2 * xpadding); + const auto y = top + ? skip + : (_st.size.height() - height - 2 * ypadding - skip); + p.setCompositionMode(QPainter::CompositionMode_Source); + const auto stroke = style::ConvertScaleExact(1.); + p.setPen(QPen(Qt::transparent, stroke)); + p.setFont(_st.font); + p.setBrush(color); + p.drawRoundedRect( + QRectF( + x - stroke / 2., + y - stroke / 2., + width + 2 * xpadding + stroke, + height + 2 * ypadding + stroke), + radius, + radius); + p.setPen(Qt::transparent); + p.drawText(x + xpadding, y + ypadding + _st.font->ascent, text); +} + +QPoint SettingsButton::prepareRippleStartPosition() const { + const auto inner = rect().marginsRemoved(_st.padding); + const auto result = mapFromGlobal(QCursor::pos()) - inner.topLeft(); + return inner.contains(result) + ? result + : DisabledRippleStartPosition(); +} + +QImage SettingsButton::prepareRippleMask() const { + return Ui::RippleAnimation::RoundRectMask( + rect().marginsRemoved(_st.padding).size(), + _st.rippleRadius); +} + } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/player/media_player_button.h b/Telegram/SourceFiles/media/player/media_player_button.h index 750b533eb..0c667b099 100644 --- a/Telegram/SourceFiles/media/player/media_player_button.h +++ b/Telegram/SourceFiles/media/player/media_player_button.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/widgets/buttons.h" +#include "ui/rect_part.h" #include @@ -87,7 +88,7 @@ public: return _st; } - void setSpeed(float64 speed, anim::type animated = anim::type::normal); + void setSpeed(float64 speed); private: void paintEvent(QPaintEvent *e) override; @@ -101,4 +102,42 @@ private: }; +class SettingsButton final : public Ui::RippleButton { +public: + SettingsButton(QWidget *parent, const style::MediaSpeedButton &st); + + [[nodiscard]] const style::MediaSpeedButton &st() const { + return _st; + } + + void setSpeed(float64 speed); + void setQuality(int quality); + void setActive(bool active); + +private: + void paintEvent(QPaintEvent *e) override; + + QPoint prepareRippleStartPosition() const override; + QImage prepareRippleMask() const override; + + void onStateChanged(State was, StateChangeSource source) override; + + void paintBadge( + QPainter &p, + const QString &text, + RectPart origin, + QColor color); + void prepareFrame(); + + const style::MediaSpeedButton &_st; + Ui::Animations::Simple _activeAnimation; + Ui::Animations::Simple _overAnimation; + QImage _frameCache; + float _speed = 1.; + int _quality = 0; + bool _isDefaultSpeed = false; + bool _active = false; + +}; + } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp index fb72a9f42..1182eb7b1 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.cpp +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.cpp @@ -177,7 +177,8 @@ void FillSpeedMenu( not_null menu, const style::MediaSpeedMenu &st, rpl::producer value, - Fn callback) { + Fn callback, + bool onlySlider) { auto slider = base::make_unique_q( menu, st, @@ -198,6 +199,11 @@ void FillSpeedMenu( )); menu->addAction(std::move(slider)); + + if (onlySlider) { + return; + } + menu->addSeparator(&st.dropdown.menu.separator); struct SpeedPoint { @@ -552,6 +558,10 @@ void WithDropdownController::updateDropdownGeometry() { _menu->move(position); } +rpl::producer WithDropdownController::menuToggledValue() const { + return _menuToggled.value(); +} + void WithDropdownController::hideTemporarily() { if (_menu && !_menu->isHidden()) { _temporarilyHidden = true; @@ -584,10 +594,20 @@ void WithDropdownController::showMenu() { } }, _menu->lifetime()); _menu->setHiddenCallback([=]{ + if (_menu.get() == raw) { + _menuToggled = false; + } Ui::PostponeCall(raw, [this] { _menu = nullptr; + _menuToggled = false; }); }); + _menu->setShowStartCallback([=] { + _menuToggled = true; + }); + _menu->setHideStartCallback([=] { + _menuToggled = false; + }); _button->installEventFilter(raw); fillMenu(raw); updateDropdownGeometry(); @@ -602,6 +622,7 @@ void WithDropdownController::showMenu() { Unexpected("Menu align value."); }(); _menu->showAnimated(origin); + _menuToggled = true; } OrderController::OrderController( @@ -689,44 +710,54 @@ void OrderController::updateIcon() { } SpeedController::SpeedController( - not_null button, + not_null button, + const style::MediaSpeedButton &st, not_null menuParent, Fn menuOverCallback, Fn value, - Fn change) + Fn change, + std::vector qualities, + Fn quality, + Fn changeQuality) : WithDropdownController( button, menuParent, - button->st().menu.dropdown, - button->st().menuAlign, + st.menu.dropdown, + st.menuAlign, std::move(menuOverCallback)) -, _st(button->st()) +, _st(st) , _lookup(std::move(value)) -, _change(std::move(change)) { +, _change(std::move(change)) +, _qualities(std::move(qualities)) +, _lookupQuality(std::move(quality)) +, _changeQuality(std::move(changeQuality)) { + Expects(_qualities.empty() || (_lookupQuality && _changeQuality)); + button->setClickedCallback([=] { - toggleDefault(); - save(); - if (const auto current = menu()) { - current->otherEnter(); + if (_lookup && !_lookupQuality && !_changeQuality) { + toggleDefault(); + save(); + if (const auto current = menu()) { + current->otherEnter(); + } + } else { + showMenu(); } }); - - setSpeed(_lookup(false)); - _speed = _lookup(true); - - button->setSpeed(_speed, anim::type::instant); - - _speedChanged.events_starting_with( - speed() - ) | rpl::start_with_next([=](float64 speed) { - button->setSpeed(speed); - }, button->lifetime()); + if (const auto lookup = _lookup) { + setSpeed(lookup(false)); + _speed = lookup(true); + } } rpl::producer<> SpeedController::saved() const { return _saved.events(); } +rpl::producer SpeedController::realtimeValue() const { + return _speedChanged.events_starting_with(speed()); +} + float64 SpeedController::speed() const { return _isDefault ? 1. : _speed; } @@ -752,16 +783,83 @@ void SpeedController::setSpeed(float64 newSpeed) { } void SpeedController::save() { - _change(speed()); + if (const auto change = _change) { + change(speed()); + } _saved.fire({}); } +void SpeedController::setQuality(VideoQuality quality) { + _quality = quality; + _changeQuality(quality.manual ? quality.height : 0); +} + void SpeedController::fillMenu(not_null menu) { - FillSpeedMenu( - menu->menu(), - _st.menu, - _speedChanged.events_starting_with(speed()), - [=](float64 speed) { setSpeed(speed); save(); }); + if (_lookup) { + FillSpeedMenu( + menu->menu(), + _st.menu, + _speedChanged.events_starting_with(speed()), + [=](float64 speed) { setSpeed(speed); save(); }, + !_qualities.empty()); + } + if (_qualities.empty()) { + return; + } + _quality = _lookupQuality(); + const auto raw = menu->menu(); + const auto &st = _st.menu; + if (_lookup) { + raw->addSeparator(&st.dropdown.menu.separator); + } + + const auto add = [&](int quality) { + const auto automatic = tr::lng_mediaview_quality_auto(tr::now); + const auto text = quality ? u"%1p"_q.arg(quality) : automatic; + auto action = base::make_unique_q( + raw, + st.qualityMenu, + Ui::Menu::CreateAction( + raw, + text, + [=] { _changeQuality(quality); }), + nullptr, + nullptr); + const auto raw = action.get(); + const auto check = Ui::CreateChild(raw); + check->resize(st.activeCheck.size()); + check->paintRequest( + ) | rpl::start_with_next([check, icon = &st.activeCheck] { + auto p = QPainter(check); + icon->paint(p, 0, 0, check->width()); + }, check->lifetime()); + raw->sizeValue( + ) | rpl::start_with_next([=, skip = st.activeCheckSkip](QSize size) { + check->moveToRight( + skip, + (size.height() - check->height()) / 2, + size.width()); + }, check->lifetime()); + check->setAttribute(Qt::WA_TransparentForMouseEvents); + _quality.value( + ) | rpl::start_with_next([=](VideoQuality now) { + const auto chosen = now.manual + ? (now.height == quality) + : !quality; + raw->action()->setEnabled(!chosen); + if (!quality) { + raw->action()->setText(automatic + + (now.manual ? QString() : u"\t%1p"_q.arg(now.height))); + } + check->setVisible(chosen); + }, raw->lifetime()); + menu->addAction(std::move(action)); + }; + + add(0); + for (const auto quality : _qualities) { + add(quality); + } } } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/player/media_player_dropdown.h b/Telegram/SourceFiles/media/player/media_player_dropdown.h index 63197dbb8..1a0e0bb08 100644 --- a/Telegram/SourceFiles/media/player/media_player_dropdown.h +++ b/Telegram/SourceFiles/media/player/media_player_dropdown.h @@ -30,8 +30,6 @@ class Menu; namespace Media::Player { -class SpeedButton; - class Dropdown final : public Ui::RpWidget { public: explicit Dropdown(QWidget *parent); @@ -82,6 +80,7 @@ public: Ui::DropdownMenu *menu() const; void updateDropdownGeometry(); + [[nodiscard]] rpl::producer menuToggledValue() const; void hideTemporarily(); void showBack(); @@ -98,6 +97,7 @@ private: const Qt::Alignment _menuAlign = Qt::AlignTop | Qt::AlignRight; const Fn _menuOverCallback; base::unique_qptr _menu; + rpl::variable _menuToggled; bool _temporarilyHidden = false; bool _overButton = false; @@ -125,13 +125,18 @@ private: class SpeedController final : public WithDropdownController { public: SpeedController( - not_null button, + not_null button, + const style::MediaSpeedButton &st, not_null menuParent, Fn menuOverCallback, Fn value, - Fn change); + Fn change, + std::vector qualities = {}, + Fn quality = nullptr, + Fn changeQuality = nullptr); [[nodiscard]] rpl::producer<> saved() const; + [[nodiscard]] rpl::producer realtimeValue() const; private: void fillMenu(not_null menu) override; @@ -141,6 +146,7 @@ private: [[nodiscard]] float64 lastNonDefaultSpeed() const; void toggleDefault(); void setSpeed(float64 newSpeed); + void setQuality(VideoQuality quality); void save(); const style::MediaSpeedButton &_st; @@ -151,6 +157,11 @@ private: rpl::event_stream _speedChanged; rpl::event_stream<> _saved; + std::vector _qualities; + Fn _lookupQuality; + Fn _changeQuality; + rpl::variable _quality; + }; } // namespace Media::Player diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index 4dc687a27..12792f370 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -869,7 +869,7 @@ void Instance::pause(AudioMsgId::Type type) { } } -void Instance::stop(AudioMsgId::Type type) { +void Instance::stop(AudioMsgId::Type type, bool asFinished) { if (const auto data = getData(type)) { if (data->streamed) { clearStreamed(data); @@ -877,6 +877,9 @@ void Instance::stop(AudioMsgId::Type type) { data->resumeOnCallEnd = false; _playerStopped.fire_copy({type}); } + if (asFinished) { + _tracksFinished.fire_copy(type); + } } void Instance::stopAndClear(not_null data) { @@ -1200,6 +1203,21 @@ Streaming::Instance *Instance::roundVideoStreamed(HistoryItem *item) const { return nullptr; } +Streaming::Instance *Instance::roundVideoPreview( + not_null document) const { + if (const auto data = getData(AudioMsgId::Type::Voice)) { + if (const auto streamed = data->streamed.get()) { + if (streamed->id.audio() == document) { + const auto player = &streamed->instance.player(); + if (player->ready() && !player->videoSize().isEmpty()) { + return &streamed->instance; + } + } + } + } + return nullptr; +} + View::PlaybackProgress *Instance::roundVideoPlayback( HistoryItem *item) const { return roundVideoStreamed(item) @@ -1285,7 +1303,7 @@ void Instance::handleStreamingUpdate( Streaming::Update &&update) { using namespace Streaming; - v::match(update.data, [&](Information &update) { + v::match(update.data, [&](const Information &update) { if (!update.video.size.isEmpty()) { data->streamed->progress.setValueChangedCallback([=]( float64, @@ -1297,16 +1315,17 @@ void Instance::handleStreamingUpdate( requestRoundVideoResize(); } emitUpdate(data->type); - }, [&](PreloadedVideo &update) { + }, [&](PreloadedVideo) { //emitUpdate(data->type, [](AudioMsgId) { return true; }); - }, [&](UpdateVideo &update) { + }, [&](UpdateVideo) { emitUpdate(data->type); - }, [&](PreloadedAudio &update) { + }, [&](PreloadedAudio) { //emitUpdate(data->type, [](AudioMsgId) { return true; }); - }, [&](UpdateAudio &update) { + }, [&](UpdateAudio) { emitUpdate(data->type); - }, [&](WaitingForData) { - }, [&](MutedByOther) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { }, [&](Finished) { emitUpdate(data->type); if (data->streamed && data->streamed->instance.player().finished()) { diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index fce26855b..ebe7d3d4c 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -72,7 +72,7 @@ public: void play(AudioMsgId::Type type); void pause(AudioMsgId::Type type); - void stop(AudioMsgId::Type type); + void stop(AudioMsgId::Type type, bool asFinished = false); void playPause(AudioMsgId::Type type); bool next(AudioMsgId::Type type); bool previous(AudioMsgId::Type type); @@ -109,6 +109,9 @@ public: [[nodiscard]] View::PlaybackProgress *roundVideoPlayback( HistoryItem *item) const; + [[nodiscard]] Streaming::Instance *roundVideoPreview( + not_null document) const; + [[nodiscard]] AudioMsgId current(AudioMsgId::Type type) const { if (const auto data = getData(type)) { return data->current; diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp index 5d79eda36..46a255634 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.cpp +++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp @@ -73,10 +73,17 @@ Widget::Widget( , _speedController( std::make_unique( _speedToggle.data(), + _speedToggle->st(), dropdownsParent, [=](bool over) { markOver(over); }, [=](bool lastNonDefault) { return speedLookup(lastNonDefault); }, [=](float64 speed) { saveSpeed(speed); })) { + _speedController->realtimeValue( + ) | rpl::start_with_next([=](float64 speed) { + _speedToggle->setSpeed(speed); + }, _speedToggle->lifetime()); + _speedToggle->finishAnimating(); + setAttribute(Qt::WA_OpaquePaintEvent); setMouseTracking(true); resize(width(), st::mediaPlayerHeight + st::lineWidth); @@ -717,6 +724,8 @@ void Widget::handleSongChange() { 0, name.size(), QString())); + } else if (document->isVideoMessage()) { + textWithEntities.text = tr::lng_media_round(tr::now); } else { textWithEntities.text = tr::lng_media_audio(tr::now); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 136c77b7a..c034d61dc 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -244,6 +244,7 @@ void ReplyArea::sendVoice(VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _controls->clearListenState(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index f91afead2..9f9b01c9d 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -93,6 +93,11 @@ struct WaitingForData { bool waiting = false; }; +struct SpeedEstimate { + int bytesPerSecond = 0; + bool unreliable = false; +}; + struct MutedByOther { }; @@ -107,6 +112,7 @@ struct Update { PreloadedAudio, UpdateAudio, WaitingForData, + SpeedEstimate, MutedByOther, Finished> data; }; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp index 3ecad745b..da364c315 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp @@ -29,13 +29,16 @@ constexpr auto kWaitingFastDuration = crl::time(200); constexpr auto kWaitingShowDuration = crl::time(500); constexpr auto kWaitingShowDelay = crl::time(500); constexpr auto kGoodThumbQuality = 100; +constexpr auto kSwitchQualityUpPreloadedThreshold = 4 * crl::time(1000); +constexpr auto kSwitchQualityUpSpeedMultiplier = 1.2; } // namespace Document::Document( not_null document, - std::shared_ptr reader) -: Document(std::move(reader), document, nullptr) { + std::shared_ptr reader, + std::vector otherQualities) +: Document(std::move(reader), document, {}, std::move(otherQualities)) { _player.fullInCache( ) | rpl::start_with_next([=](bool fullInCache) { _document->setLoadedInMediaCache(fullInCache); @@ -44,24 +47,27 @@ Document::Document( Document::Document( not_null photo, - std::shared_ptr reader) -: Document(std::move(reader), nullptr, photo) { + std::shared_ptr reader, + std::vector otherQualities) +: Document(std::move(reader), {}, photo, {}) { } Document::Document(std::unique_ptr loader) -: Document(std::make_shared(std::move(loader)), nullptr, nullptr) { +: Document(std::make_shared(std::move(loader)), {}, {}, {}) { } Document::Document( std::shared_ptr reader, DocumentData *document, - PhotoData *photo) + PhotoData *photo, + std::vector otherQualities) : _document(document) , _photo(photo) , _player(std::move(reader)) , _radial( - [=] { waitingCallback(); }, - st::defaultInfiniteRadialAnimation) { + [=] { waitingCallback(); }, + st::defaultInfiniteRadialAnimation) +, _otherQualities(std::move(otherQualities)) { resubscribe(); } @@ -138,20 +144,27 @@ Ui::RadialState Document::waitingState() const { return _radial.computeState(); } +rpl::producer Document::switchQualityRequests() const { + return _switchQualityRequests.events(); +} + void Document::handleUpdate(Update &&update) { v::match(update.data, [&](Information &update) { ready(std::move(update)); - }, [&](const PreloadedVideo &update) { + }, [&](PreloadedVideo update) { _info.video.state.receivedTill = update.till; - }, [&](const UpdateVideo &update) { + checkSwitchToHigherQuality(); + }, [&](UpdateVideo update) { _info.video.state.position = update.position; - }, [&](const PreloadedAudio &update) { + }, [&](PreloadedAudio update) { _info.audio.state.receivedTill = update.till; - }, [&](const UpdateAudio &update) { + }, [&](UpdateAudio update) { _info.audio.state.position = update.position; - }, [&](const WaitingForData &update) { + }, [&](WaitingForData update) { waitingChange(update.waiting); - }, [&](MutedByOther) { + }, [&](SpeedEstimate update) { + checkForQualitySwitch(update); + }, [](MutedByOther) { }, [&](Finished) { const auto finishTrack = [](TrackState &state) { state.position = state.receivedTill = state.duration; @@ -161,6 +174,76 @@ void Document::handleUpdate(Update &&update) { }); } +void Document::setOtherQualities(std::vector value) { + _otherQualities = std::move(value); + checkForQualitySwitch(_lastSpeedEstimate); +} + +void Document::checkForQualitySwitch(SpeedEstimate estimate) { + _lastSpeedEstimate = estimate; + if (!checkSwitchToHigherQuality()) { + checkSwitchToLowerQuality(); + } +} + +bool Document::checkSwitchToHigherQuality() { + if (_otherQualities.empty() + || (_info.video.state.duration == kTimeUnknown) + || (_info.video.state.duration == kDurationUnavailable) + || (_info.video.state.position == kTimeUnknown) + || (_info.video.state.receivedTill == kTimeUnknown) + || !_lastSpeedEstimate.bytesPerSecond + || _lastSpeedEstimate.unreliable + || (_info.video.state.receivedTill + < std::min( + _info.video.state.duration, + (_info.video.state.position + + kSwitchQualityUpPreloadedThreshold)))) { + return false; + } + const auto size = _player.fileSize(); + Assert(size >= 0 && size <= std::numeric_limits::max()); + auto to = QualityDescriptor{ .sizeInBytes = uint32(size) }; + const auto duration = _info.video.state.duration / 1000.; + const auto speed = _player.speed(); + const auto multiplier = speed * kSwitchQualityUpSpeedMultiplier; + for (const auto &descriptor : _otherQualities) { + const auto perSecond = descriptor.sizeInBytes / duration; + if (descriptor.sizeInBytes > to.sizeInBytes + && _lastSpeedEstimate.bytesPerSecond >= perSecond * multiplier) { + to = descriptor; + } + } + if (!to.height) { + return false; + } + _switchQualityRequests.fire_copy(to.height); + return true; +} + +bool Document::checkSwitchToLowerQuality() { + if (_otherQualities.empty() + || !_waiting + || !_radial.animating() + || !_lastSpeedEstimate.bytesPerSecond) { + return false; + } + const auto size = _player.fileSize(); + Assert(size >= 0 && size <= std::numeric_limits::max()); + auto to = QualityDescriptor(); + for (const auto &descriptor : _otherQualities) { + if (descriptor.sizeInBytes < size + && descriptor.sizeInBytes > to.sizeInBytes) { + to = descriptor; + } + } + if (!to.height) { + return false; + } + _switchQualityRequests.fire_copy(to.height); + return true; +} + void Document::handleError(Error &&error) { if (_document) { if (error == Error::NotStreamable) { @@ -192,11 +275,11 @@ void Document::waitingChange(bool waiting) { _radial.start( st::defaultInfiniteRadialAnimation.sineDuration); } - _fading.start( - [=] { waitingCallback(); }, - _waiting ? 0. : 1., - _waiting ? 1. : 0., - duration); + _fading.start([=] { + waitingCallback(); + }, _waiting ? 0. : 1., _waiting ? 1. : 0., duration); + + checkSwitchToLowerQuality(); }; if (waiting) { if (_radial.animating()) { diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.h b/Telegram/SourceFiles/media/streaming/media_streaming_document.h index f192aebb1..bdb579593 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_document.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.h @@ -14,20 +14,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class DocumentData; -namespace Media { -namespace Streaming { +namespace Media::Streaming { class Instance; class Loader; +struct QualityDescriptor { + uint32 sizeInBytes = 0; + uint32 height = 0; +}; + class Document { public: Document( not_null document, - std::shared_ptr reader); + std::shared_ptr reader, + std::vector otherQualities = {}); Document( not_null photo, - std::shared_ptr reader); + std::shared_ptr reader, + std::vector otherQualities = {}); explicit Document(std::unique_ptr loader); void play(const PlaybackOptions &options); @@ -41,11 +47,15 @@ public: [[nodiscard]] float64 waitingOpacity() const; [[nodiscard]] Ui::RadialState waitingState() const; + void setOtherQualities(std::vector value); + [[nodiscard]] rpl::producer switchQualityRequests() const; + private: Document( std::shared_ptr reader, DocumentData *document, - PhotoData *photo); + PhotoData *photo, + std::vector otherQualities); friend class Instance; @@ -54,6 +64,9 @@ private: void refreshPlayerPriority(); void waitingCallback(); + void checkForQualitySwitch(SpeedEstimate estimate); + bool checkSwitchToHigherQuality(); + bool checkSwitchToLowerQuality(); void handleUpdate(Update &&update); void handleError(Error &&error); @@ -71,14 +84,15 @@ private: rpl::lifetime _subscription; - bool _waiting = false; mutable Ui::InfiniteRadialAnimation _radial; Ui::Animations::Simple _fading; base::Timer _timer; base::flat_set> _instances; + std::vector _otherQualities; + rpl::event_stream _switchQualityRequests; + SpeedEstimate _lastSpeedEstimate; + bool _waiting = false; }; - -} // namespace Streaming -} // namespace Media +} // namespace Media::Streaming diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp index 22427c2ff..9e5ee8498 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.cpp @@ -490,6 +490,14 @@ void File::setLoaderPriority(int priority) { _reader->setLoaderPriority(priority); } +int64 File::size() const { + return _reader->size(); +} + +rpl::producer File::speedEstimate() const { + return _reader->speedEstimate(); +} + File::~File() { stop(); } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_file.h b/Telegram/SourceFiles/media/streaming/media_streaming_file.h index 38af45537..c17a8d195 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_file.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_file.h @@ -42,6 +42,9 @@ public: [[nodiscard]] bool isRemoteLoader() const; void setLoaderPriority(int priority); + [[nodiscard]] int64 size() const; + [[nodiscard]] rpl::producer speedEstimate() const; + ~File(); private: diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp index 898bf7678..4d4149ce3 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp @@ -17,6 +17,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media { namespace Streaming { +Instance::Instance(const Instance &other) +: _shared(other._shared) +, _waitingCallback(other._waitingCallback) +, _priority(other._priority) +, _playerLocked(other._playerLocked) { + if (_shared) { + _shared->registerInstance(this); + if (_playerLocked) { + _shared->player().lock(); + } + } +} + Instance::Instance( std::shared_ptr shared, Fn waitingCallback) @@ -36,6 +49,21 @@ Instance::Instance( std::move(waitingCallback)) { } +Instance::Instance( + not_null quality, + not_null original, + HistoryItem *context, + Data::FileOrigin origin, + Fn waitingCallback) +: Instance( + quality->owner().streaming().sharedDocument( + quality, + original, + context, + origin), + std::move(waitingCallback)) { +} + Instance::Instance( not_null photo, Data::FileOrigin origin, @@ -72,6 +100,10 @@ const Information &Instance::info() const { return _shared->info(); } +rpl::producer Instance::switchQualityRequests() const { + return _shared->switchQualityRequests(); +} + void Instance::play(const PlaybackOptions &options) { Expects(_shared != nullptr); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h index 126cf9db0..314779401 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h @@ -27,6 +27,7 @@ class Player; class Instance { public: + Instance(const Instance &other); Instance( std::shared_ptr shared, Fn waitingCallback); @@ -34,6 +35,12 @@ public: not_null document, Data::FileOrigin origin, Fn waitingCallback); + Instance( + not_null quality, + not_null original, + HistoryItem *context, + Data::FileOrigin origin, + Fn waitingCallback); Instance( not_null photo, Data::FileOrigin origin, @@ -45,6 +52,7 @@ public: [[nodiscard]] const Player &player() const; [[nodiscard]] const Information &info() const; + [[nodiscard]] rpl::producer switchQualityRequests() const; void play(const PlaybackOptions &options); void pause(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_loader.h b/Telegram/SourceFiles/media/streaming/media_streaming_loader.h index 55845c520..f95183bab 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_loader.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_loader.h @@ -7,12 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "media/streaming/media_streaming_common.h" + namespace Storage { class StreamedFileDownloader; } // namespace Storage -namespace Media { -namespace Streaming { +namespace Media::Streaming { struct LoadedPart { int64 offset = 0; @@ -41,6 +42,8 @@ public: // Parts will be sent from the main thread. [[nodiscard]] virtual rpl::producer parts() const = 0; + [[nodiscard]] virtual auto speedEstimate() const + -> rpl::producer = 0; virtual void attachDownloader( not_null downloader) = 0; @@ -74,5 +77,4 @@ private: }; -} // namespace Streaming -} // namespace Media +} // namespace Media::Streaming diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.cpp index a85dbe775..868296d7b 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.cpp @@ -84,6 +84,10 @@ rpl::producer LoaderLocal::parts() const { return _parts.events(); } +rpl::producer LoaderLocal::speedEstimate() const { + return rpl::never(); +} + void LoaderLocal::attachDownloader( not_null downloader) { Unexpected("Downloader attached to a local streaming loader."); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.h b/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.h index 9531f6683..efb6d41bd 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_loader_local.h @@ -33,6 +33,7 @@ public: // Parts will be sent from the main thread. [[nodiscard]] rpl::producer parts() const override; + [[nodiscard]] rpl::producer speedEstimate() const override; void attachDownloader( not_null downloader) override; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.cpp index 15f881403..d6f869c58 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.cpp @@ -14,6 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media { namespace Streaming { +namespace { + +constexpr auto kCheckStatsInterval = crl::time(1000); +constexpr auto kInitialStatsWait = 5 * crl::time(1000); + +} // namespace LoaderMtproto::LoaderMtproto( not_null owner, @@ -22,7 +28,8 @@ LoaderMtproto::LoaderMtproto( Data::FileOrigin origin) : DownloadMtprotoTask(owner, location, origin) , _size(size) -, _api(&api().instance()) { +, _api(&api().instance()) +, _statsTimer([=] { checkStats(); }) { } Storage::Cache::Key LoaderMtproto::baseCacheKey() const { @@ -121,12 +128,32 @@ bool LoaderMtproto::readyToRequest() const { int64 LoaderMtproto::takeNextRequestOffset() { const auto offset = _requested.take(); + Assert(offset.has_value()); + + const auto time = crl::now(); + if (!_firstRequestStart) { + _firstRequestStart = time; + } + _stats.push_back({ .start = crl::now(), .offset = *offset }); Ensures(offset.has_value()); return *offset; } bool LoaderMtproto::feedPart(int64 offset, const QByteArray &bytes) { + const auto time = crl::now(); + for (auto &entry : _stats) { + if (entry.offset == offset && entry.start < time) { + entry.end = time; + if (!_statsTimer.isActive()) { + const auto checkAt = std::max( + time + kCheckStatsInterval, + _firstRequestStart + kInitialStatsWait); + _statsTimer.callOnce(checkAt - time); + } + break; + } + } _parts.fire({ offset, bytes }); return true; } @@ -139,5 +166,56 @@ rpl::producer LoaderMtproto::parts() const { return _parts.events(); } +rpl::producer LoaderMtproto::speedEstimate() const { + return _speedEstimate.events(); +} + +void LoaderMtproto::checkStats() { + const auto time = crl::now(); + const auto from = time - kInitialStatsWait; + { // Erase all stats entries that are too old. + for (auto i = begin(_stats); i != end(_stats);) { + if (i->start >= from) { + break; + } else if (i->end && i->end < from) { + i = _stats.erase(i); + } else { + ++i; + } + } + } + if (_stats.empty()) { + return; + } + // Count duration for which at least one request was in progress. + // This is the time we should consider for download speed. + // We don't count time when no requests were in progress. + auto durationCountedTill = _stats.front().start; + auto duration = crl::time(0); + auto received = int64(0); + for (const auto &entry : _stats) { + if (entry.start > durationCountedTill) { + durationCountedTill = entry.start; + } + const auto till = entry.end ? entry.end : time; + if (till > durationCountedTill) { + duration += (till - durationCountedTill); + durationCountedTill = till; + } + if (entry.end) { + received += Storage::kDownloadPartSize; + } + } + if (duration) { + _speedEstimate.fire({ + .bytesPerSecond = int(std::clamp( + int64(received * 1000 / duration), + int64(0), + int64(64 * 1024 * 1024))), + .unreliable = (received < 3 * Storage::kDownloadPartSize), + }); + } +} + } // namespace Streaming } // namespace Media diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.h b/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.h index 93635efb1..c3a025a1e 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_loader_mtproto.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" #include "media/streaming/media_streaming_loader.h" #include "mtproto/sender.h" #include "data/data_file_origin.h" @@ -36,12 +37,19 @@ public: // Parts will be sent from the main thread. [[nodiscard]] rpl::producer parts() const override; + [[nodiscard]] rpl::producer speedEstimate() const override; void attachDownloader( not_null downloader) override; void clearAttachedDownloader() override; private: + struct StatsEntry { + crl::time start = 0; + crl::time end = 0; + int64 offset = 0; + }; + bool readyToRequest() const override; int64 takeNextRequestOffset() override; bool feedPart(int64 offset, const QByteArray &bytes) override; @@ -50,6 +58,8 @@ private: void cancelForOffset(int64 offset); void addToQueueWithPriority(); + void checkStats(); + const int64 _size = 0; int _priority = 0; @@ -57,6 +67,11 @@ private: PriorityQueue _requested; rpl::event_stream _parts; + rpl::event_stream _speedEstimate; + + std::vector _stats; + crl::time _firstRequestStart = 0; + base::Timer _statsTimer; Storage::StreamedFileDownloader *_downloader = nullptr; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index 699cbdf58..0005d1754 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -715,6 +715,10 @@ void Player::start() { _stage = Stage::Started; const auto guard = base::make_weak(&_sessionGuard); + _file->speedEstimate() | rpl::start_with_next([=](SpeedEstimate value) { + _updates.fire({ value }); + }, _sessionLifetime); + rpl::merge( _audio ? _audio->waitingForData() : nullptr, _video ? _video->waitingForData() : nullptr @@ -881,6 +885,10 @@ rpl::producer Player::fullInCache() const { return _fullInCache.events(); } +int64 Player::fileSize() const { + return _file->size(); +} + QSize Player::videoSize() const { return _information.video.size; } @@ -958,7 +966,9 @@ Media::Player::TrackState Player::prepareLegacyState() const { if (duration > 0) { result.length = duration; } else { - result.length = std::max(crl::time(result.position), crl::time(0)); + result.length = std::max( + crl::time(result.position), + crl::time(0)); } } return result; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h index 148dc7a11..0b8257dae 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h @@ -60,6 +60,7 @@ public: [[nodiscard]] rpl::producer updates() const; [[nodiscard]] rpl::producer fullInCache() const; + [[nodiscard]] int64 fileSize() const; [[nodiscard]] QSize videoSize() const; [[nodiscard]] QImage frame( const FrameRequest &request, diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp index 95654e590..1788f6dbb 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.cpp @@ -1099,6 +1099,10 @@ void Reader::continueDownloaderFromMainThread() { } } +rpl::producer Reader::speedEstimate() const { + return _loader->speedEstimate(); +} + void Reader::setLoaderPriority(int priority) { if (_realPriority == priority) { return; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h index 179829846..a86a55fcd 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_reader.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_reader.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "media/streaming/media_streaming_common.h" #include "media/streaming/media_streaming_loader.h" #include "base/bytes.h" #include "base/weak_ptr.h" @@ -78,6 +79,7 @@ public: void cancelForDownloader( not_null downloader); void continueDownloaderFromMainThread(); + [[nodiscard]] rpl::producer speedEstimate() const; ~Reader(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp new file mode 100644 index 000000000..56beb61ec --- /dev/null +++ b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.cpp @@ -0,0 +1,62 @@ +/* +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 "media/streaming/media_streaming_round_preview.h" + +namespace Media::Streaming { + +RoundPreview::RoundPreview(const QByteArray &bytes, int size) +: _bytes(bytes) +, _reader( + Clip::MakeReader(_bytes, [=](Clip::Notification update) { + clipCallback(update); + })) +, _size(size) { +} + +std::shared_ptr RoundPreview::clone() { + Unexpected("RoundPreview::clone."); +} + +QImage RoundPreview::image(int size) { + if (!_reader || !_reader->started()) { + return QImage(); + } + return _reader->current({ + .frame = QSize(_size, _size), + .factor = style::DevicePixelRatio(), + .radius = ImageRoundRadius::Ellipse, + }, crl::now()); +} + +void RoundPreview::subscribeToUpdates(Fn callback) { + _repaint = std::move(callback); +} + +void RoundPreview::clipCallback(Clip::Notification notification) { + switch (notification) { + case Clip::Notification::Reinit: { + if (_reader->state() == ::Media::Clip::State::Error) { + _reader.setBad(); + } else if (_reader->ready() && !_reader->started()) { + _reader->start({ + .frame = QSize(_size, _size), + .factor = style::DevicePixelRatio(), + .radius = ImageRoundRadius::Ellipse, + }); + } + } break; + + case Clip::Notification::Repaint: break; + } + + if (const auto onstack = _repaint) { + onstack(); + } +} + +} // namespace Media::Streaming diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h new file mode 100644 index 000000000..64440bc49 --- /dev/null +++ b/Telegram/SourceFiles/media/streaming/media_streaming_round_preview.h @@ -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 +*/ +#pragma once + +#include "ui/dynamic_image.h" + +#include "media/clip/media_clip_reader.h" + +namespace Media::Streaming { + +class RoundPreview final : public Ui::DynamicImage { +public: + RoundPreview(const QByteArray &bytes, int size); + + std::shared_ptr clone() override; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + void clipCallback(Clip::Notification notification); + + const QByteArray _bytes; + Clip::ReaderPointer _reader; + Fn _repaint; + int _size = 0; + +}; + +} // namespace Media::Streaming diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index d94d87b63..179d57624 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -307,17 +307,21 @@ mediaviewTitleMaximizeMacPadding: margins(0px, 4px, 8px, 4px); mediaviewShadowTop: icon{{ "mediaview/shadow_top", windowShadowFg }}; mediaviewShadowBottom: icon{{ "mediaview/shadow_bottom", windowShadowFg }}; +mediaviewSpeedMenuInner: Menu(mediaviewMenu) { + separator: MenuSeparator(mediaviewMenuSeparator) { + fg: groupCallMenuBgOver; + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + itemPadding: margins(54px, 7px, 54px, 9px); + itemFgDisabled: mediaviewTextLinkFg; +} mediaviewSpeedMenu: MediaSpeedMenu(mediaPlayerSpeedMenu) { dropdown: DropdownMenu(mediaviewDropdownMenu) { - menu: Menu(mediaviewMenu) { - separator: MenuSeparator(mediaviewMenuSeparator) { - fg: groupCallMenuBgOver; - padding: margins(0px, 4px, 0px, 4px); - width: 6px; - } - itemPadding: margins(54px, 7px, 54px, 9px); - itemFgDisabled: mediaviewTextLinkFg; - } + menu: mediaviewSpeedMenuInner; + } + qualityMenu: Menu(mediaviewSpeedMenuInner) { + itemPadding: margins(17px, 7px, 54px, 9px); } activeCheck: icon {{ "player/player_check", mediaviewTextLinkFg }}; slider: MediaSlider(defaultContinuousSlider) { @@ -347,10 +351,11 @@ mediaviewSpeedMenu: MediaSpeedMenu(mediaPlayerSpeedMenu) { mediaviewSpeedButton: MediaSpeedButton(mediaPlayerSpeedButton) { size: size(32px, 32px); padding: margins(0px, 0px, 0px, 0px); + font: font(8px bold); fg: mediaviewPlaybackIconFg; overFg: mediaviewPlaybackIconFgOver; activeFg: mediaviewTextLinkFg; - icon: icon{{ "player/player_speed", mediaviewPlaybackIconFg }}; + icon: icon{{ "player/player_settings", mediaviewPlaybackIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: mediaviewPlaybackIconRipple; } @@ -480,6 +485,8 @@ storiesLike: IconButton(storiesAttach) { } storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; +storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }}; +storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }}; storiesRemoveSet: IconButton(stickerPanRemoveSet) { icon: icon {{ "simple_close", storiesComposeGrayIcon }}; iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; @@ -686,6 +693,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } record: storiesRecordVoice; recordOver: storiesRecordVoiceOver; + round: storiesRecordRound; + roundOver: storiesRecordRoundOver; sendDisabledFg: storiesComposeGrayText; } attach: storiesAttach; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1e713bcca..21a6be546 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_opengl.h" #include "media/stories/media_stories_view.h" +#include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_player.h" #include "media/player/media_player_instance.h" #include "history/history.h" @@ -324,7 +325,9 @@ struct OverlayWidget::Collage { struct OverlayWidget::Streamed { Streamed( - not_null document, + not_null quality, + not_null original, + HistoryItem *context, Data::FileOrigin origin, Fn waitingCallback); Streamed( @@ -336,6 +339,7 @@ struct OverlayWidget::Streamed { std::unique_ptr controls; std::unique_ptr powerSaveBlocker; + bool ready = false; bool withSound = false; bool pausedBySeek = false; bool resumeOnCallEnd = false; @@ -345,6 +349,10 @@ struct OverlayWidget::PipWrap { PipWrap( QWidget *parent, not_null document, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, std::shared_ptr shared, FnMut closeAndContinue, FnMut destroy); @@ -452,10 +460,12 @@ private: }; OverlayWidget::Streamed::Streamed( - not_null document, + not_null quality, + not_null original, + HistoryItem *context, Data::FileOrigin origin, Fn waitingCallback) -: instance(document, origin, std::move(waitingCallback)) { +: instance(quality, original, context, origin, std::move(waitingCallback)) { } OverlayWidget::Streamed::Streamed( @@ -468,6 +478,10 @@ OverlayWidget::Streamed::Streamed( OverlayWidget::PipWrap::PipWrap( QWidget *parent, not_null document, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, std::shared_ptr shared, FnMut closeAndContinue, FnMut destroy) @@ -475,6 +489,10 @@ OverlayWidget::PipWrap::PipWrap( , wrapped( &delegate, document, + origin, + chosenQuality, + context, + quality, std::move(shared), std::move(closeAndContinue), std::move(destroy)) { @@ -493,6 +511,7 @@ OverlayWidget::OverlayWidget() , _widget(_surface->rpWidget()) , _fullscreen(Core::App().settings().mediaViewPosition().maximized == 2) , _windowed(Core::App().settings().mediaViewPosition().maximized == 0) +, _quality(Core::App().settings().videoQuality()) , _layerBg(std::make_unique(_body)) , _docDownload(_body, tr::lng_media_download(tr::now), st::mediaviewFileLink) , _docSaveAs(_body, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) @@ -1109,13 +1128,18 @@ bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) { } bool OverlayWidget::videoShown() const { - return _streamed && !_streamed->instance.info().video.cover.isNull(); + return _streamed + && _streamed->ready + && !_streamed->instance.info().video.cover.isNull(); } QSize OverlayWidget::videoSize() const { Expects(videoShown()); - return flipSizeByRotation(_streamed->instance.info().video.size); + const auto use = (_document && _chosenQuality != _document) + ? _document->dimensions + : _streamed->instance.info().video.size; + return flipSizeByRotation(use); } bool OverlayWidget::streamingRequiresControls() const { @@ -1180,6 +1204,9 @@ void OverlayWidget::setStaticContent(QImage image) { image = std::move(image).convertToFormat(kGood); } image.setDevicePixelRatio(style::DevicePixelRatio()); + if (_flip) { + image = image.mirrored(_flip & Qt::Horizontal, _flip & Qt::Vertical); + } _staticContent = std::move(image); _staticContentTransparent = IsSemitransparent(_staticContent); } @@ -1211,7 +1238,8 @@ void OverlayWidget::documentUpdated(not_null document) { if (_document != document) { return; } else if (documentBubbleShown()) { - if ((_document->loading() && _docCancel->isHidden()) || (!_document->loading() && !_docCancel->isHidden())) { + if ((_document->loading() && _docCancel->isHidden()) + || (!_document->loading() && !_docCancel->isHidden())) { updateControls(); } else if (_document->loading()) { updateDocSize(); @@ -1583,7 +1611,8 @@ void OverlayWidget::fillContextMenuActions( if (_message && _message->isSponsored()) { if (const auto window = findWindow()) { const auto show = window->uiShow(); - Menu::FillSponsored(_body, addAction, show, _message, true); + const auto fullId = _message->fullId(); + Menu::FillSponsored(_body, addAction, show, fullId, true); } return; } @@ -1790,7 +1819,10 @@ void OverlayWidget::fillContextMenuActions( }, &st::mediaMenuIconStats); } } - if (_stories && _stories->allowStealthMode()) { + if (_stories + && _stories->allowStealthMode() + && story + && story->peer()->isUser()) { const auto now = base::unixtime::now(); const auto stealth = _session->data().stories().stealthMode(); addAction(tr::lng_stealth_mode_menu_item(tr::now), [=] { @@ -2267,14 +2299,20 @@ OverlayWidget::~OverlayWidget() { void OverlayWidget::assignMediaPointer(DocumentData *document) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _flip = {}; _photo = nullptr; _photoMedia = nullptr; if (_document != document) { + _streamedQualityChangeFrame = QImage(); + _streamedQualityChangeFinished = false; if ((_document = document)) { + _quality = Core::App().settings().videoQuality(); + _chosenQuality = _document->chooseQuality(_message, _quality); _documentMedia = _document->createMediaView(); _documentMedia->goodThumbnailWanted(); _documentMedia->thumbnailWanted(fileOrigin()); } else { + _chosenQuality = nullptr; _documentMedia = nullptr; } _documentLoadingTo = QString(); @@ -2283,10 +2321,14 @@ void OverlayWidget::assignMediaPointer(DocumentData *document) { void OverlayWidget::assignMediaPointer(not_null photo) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; + _chosenQuality = nullptr; + _streamedQualityChangeFrame = QImage(); + _streamedQualityChangeFinished = false; _document = nullptr; _documentMedia = nullptr; _documentLoadingTo = QString(); if (_photo != photo) { + _flip = {}; _photo = photo; _photoMedia = _photo->createMediaView(); _photoMedia->wanted(Data::PhotoSize::Small, fileOrigin()); @@ -3833,14 +3875,29 @@ bool OverlayWidget::initStreaming(const StartStreaming &startStreaming) { handleStreamingError(std::move(error)); }, _streamed->instance.lifetime()); + _streamed->instance.switchQualityRequests( + ) | rpl::filter([=](int quality) { + return !_quality.manual && _quality.height != quality; + }) | rpl::start_with_next([=](int quality) { + applyVideoQuality({ + .manual = 0, + .height = uint32(quality), + }); + }, _streamed->instance.lifetime()); + + const auto continuing = startStreaming.continueStreaming + && _pip + && (_pip->wrapped.shared().get() + == _streamed->instance.shared().get()); if (startStreaming.continueStreaming) { _pip = nullptr; } - if (!startStreaming.continueStreaming + if (!continuing || (!_streamed->instance.player().active() && !_streamed->instance.player().finished())) { startStreamingPlayer(startStreaming); } else { + _streamed->ready = _streamed->instance.player().ready(); updatePlaybackState(); } return true; @@ -3853,6 +3910,7 @@ void OverlayWidget::startStreamingPlayer( const auto &player = _streamed->instance.player(); if (player.playing()) { if (!_streamed->withSound) { + _streamed->ready = true; return; } _pip = nullptr; @@ -3862,12 +3920,12 @@ void OverlayWidget::startStreamingPlayer( return; } - const auto position = _document + _streamedPosition = _document ? startStreaming.startTime : _photo ? _photo->videoStartPosition() : 0; - restartAtSeekPosition(position); + restartAtSeekPosition(_streamedPosition); } void OverlayWidget::initStreamingThumbnail() { @@ -3906,9 +3964,15 @@ void OverlayWidget::initStreamingThumbnail() { : good ? good->size() : _document->dimensions; - if (!good && !thumbnail && !blurred) { + if (size.isEmpty()) { return; - } else if (size.isEmpty()) { + } else if (!_streamedQualityChangeFrame.isNull()) { + setStaticContent(_streamedQualityChangeFrame.scaled( + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + return; + } else if (!good && !thumbnail && !blurred) { return; } const auto options = VideoThumbOptions(_document); @@ -3929,8 +3993,10 @@ void OverlayWidget::initStreamingThumbnail() { } void OverlayWidget::streamingReady(Streaming::Information &&info) { + _streamed->ready = true; if (videoShown()) { applyVideoSize(); + _streamedQualityChangeFrame = QImage(); } else { updateContentRect(); } @@ -3952,8 +4018,14 @@ bool OverlayWidget::createStreamingObjects() { const auto origin = fileOrigin(); const auto callback = [=] { waitingAnimationCallback(); }; - if (_document) { - _streamed = std::make_unique(_document, origin, callback); + const auto video = _chosenQuality ? _chosenQuality : _document; + if (video) { + _streamed = std::make_unique( + video, + _document, + _message, + origin, + callback); } else { _streamed = std::make_unique(_photo, origin, callback); } @@ -3964,8 +4036,8 @@ bool OverlayWidget::createStreamingObjects() { ++_streamedCreated; _streamed->instance.setPriority(kOverlayLoaderPriority); _streamed->instance.lockPlayer(); - _streamed->withSound = _document - && !_document->isSilentVideo() + _streamed->withSound = video + && !video->isSilentVideo() && (_document->isAudioFile() || _document->isVideoFile() || _document->isVoiceMessage() @@ -4026,18 +4098,20 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { v::match(update.data, [&](Information &update) { streamingReady(std::move(update)); - }, [&](const PreloadedVideo &update) { + }, [&](PreloadedVideo) { updatePlaybackState(); - }, [&](const UpdateVideo &update) { + }, [&](UpdateVideo update) { updateContentRect(); Core::App().updateNonIdle(); updatePlaybackState(); - }, [&](const PreloadedAudio &update) { + _streamedPosition = update.position; + }, [&](PreloadedAudio) { updatePlaybackState(); - }, [&](const UpdateAudio &update) { + }, [&](UpdateAudio) { updatePlaybackState(); - }, [&](WaitingForData) { - }, [&](MutedByOther) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { }, [&](Finished) { updatePlaybackState(); }); @@ -4258,7 +4332,9 @@ void OverlayWidget::playbackPauseResume() { redisplayContent(); } } else if (_streamed->instance.player().finished() - || !_streamed->instance.player().active()) { + || !_streamed->instance.player().active() + || _streamedQualityChangeFinished) { + _streamedQualityChangeFinished = false; _streamingStartPaused = false; restartAtSeekPosition(0); } else if (_streamed->instance.player().paused()) { @@ -4298,9 +4374,11 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { _rotation = saved; updateContentRect(); } + const auto overrideDuration = _stories + || (_chosenQuality && _chosenQuality != _document); auto options = Streaming::PlaybackOptions{ .position = position, - .durationOverride = ((_stories + .durationOverride = ((overrideDuration && _document && _document->hasDuration()) ? _document->duration() @@ -4327,6 +4405,7 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { _streamed->instance.pause(); } else { playbackPauseMusic(); + _streamedQualityChangeFinished = false; } _streamed->pausedBySeek = false; @@ -4395,6 +4474,72 @@ float64 OverlayWidget::playbackControlsCurrentSpeed(bool lastNonDefault) { return Core::App().settings().videoPlaybackSpeed(lastNonDefault); } +std::vector OverlayWidget::playbackControlsQualities() { + if (!_document) { + return {}; + } + const auto &list = _document->resolveQualities(_message); + if (list.empty()) { + return {}; + } + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &quality : list) { + result.push_back(quality->resolveVideoQuality()); + } + return result; +} + +VideoQuality OverlayWidget::playbackControlsCurrentQuality() { + return _chosenQuality + ? VideoQuality{ + .manual = _quality.manual, + .height = uint32(_chosenQuality->resolveVideoQuality()), + } + : _quality; +} + +void OverlayWidget::playbackControlsQualityChanged(int quality) { + applyVideoQuality({ + .manual = (quality > 0), + .height = quality ? uint32(quality) : _quality.height, + }); +} + +void OverlayWidget::applyVideoQuality(VideoQuality value) { + if (_quality == value) { + return; + } + _quality = value; + Core::App().settings().setVideoQuality(value); + Core::App().saveSettingsDelayed(); + + if (!_document) { + return; + } + const auto resolved = _document->chooseQuality(_message, _quality); + if (_chosenQuality == resolved) { + return; + } + _chosenQuality = resolved; + if (_streamed && _streamed->instance.ready()) { + _streamedQualityChangeFrame = currentVideoFrameImage(); + } + if (_streamed + && (!_streamed->instance.player().active() + || _streamed->instance.player().finished())) { + _streamedQualityChangeFinished = true; + } + _streamingStartPaused = _streamedQualityChangeFinished + || (_streamed && _streamed->instance.player().paused()); + clearStreaming(); + const auto time = _streamedPosition; + const auto startStreaming = StartStreaming(false, time); + if (!canInitStreaming() || !initStreaming(startStreaming)) { + redisplayContent(); + } +} + void OverlayWidget::switchToPip() { Expects(_streamed != nullptr); Expects(_document != nullptr); @@ -4415,6 +4560,10 @@ void OverlayWidget::switchToPip() { _pip = std::make_unique( _window, document, + fileOrigin(), + _chosenQuality ? _chosenQuality : document, + _message, + _quality, _streamed->instance.shared(), closeAndContinue, [=] { _pip = nullptr; }); @@ -4642,6 +4791,7 @@ void OverlayWidget::updatePlaybackState() { } const auto state = _streamed->instance.player().prepareLegacyState(); if (state.position != kTimeUnknown && state.length != kTimeUnknown) { + _streamedPosition = state.position; if (_streamed->controls) { _streamed->controls->updatePlayback(state); _touchbarTrackState.fire_copy(state); @@ -5332,6 +5482,26 @@ void OverlayWidget::handleKeyPress(not_null e) { activateControls(); } moveToNext(-1); + } else if (key == Qt::Key_H) { + if (_flip & Qt::Horizontal) { + _flip &= ~Qt::Horizontal; + } else { + _flip |= Qt::Horizontal; + } + if (_photo) { + validatePhotoCurrentImage(); + redisplayContent(); + } + } else if (key == Qt::Key_V) { + if (_flip & Qt::Vertical) { + _flip &= ~Qt::Vertical; + } else { + _flip |= Qt::Vertical; + } + if (_photo) { + validatePhotoCurrentImage(); + redisplayContent(); + } } else if (key == Qt::Key_Right) { if (_controlsHideTimer.isActive()) { activateControls(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 5114b169c..1a5064cfe 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -16,9 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user_photos.h" #include "data/data_web_page.h" #include "data/data_cloud_themes.h" // Data::CloudTheme. +#include "media/stories/media_stories_delegate.h" #include "media/view/media_view_playback_controls.h" #include "media/view/media_view_open_common.h" -#include "media/stories/media_stories_delegate.h" +#include "media/media_common.h" class History; @@ -236,6 +237,9 @@ private: void playbackControlsVolumeChangeFinished() override; void playbackControlsSpeedChanged(float64 speed) override; float64 playbackControlsCurrentSpeed(bool lastNonDefault) override; + std::vector playbackControlsQualities() override; + VideoQuality playbackControlsCurrentQuality() override; + void playbackControlsQualityChanged(int quality) override; void playbackControlsToFullScreen() override; void playbackControlsFromFullScreen() override; void playbackControlsToPictureInPicture() override; @@ -315,11 +319,11 @@ private: void checkForSaveLoaded(); void showPremiumDownloadPromo(); - Entity entityForUserPhotos(int index) const; - Entity entityForSharedMedia(int index) const; - Entity entityForCollage(int index) const; - Entity entityByIndex(int index) const; - Entity entityForItemId(const FullMsgId &itemId) const; + [[nodiscard]] Entity entityForUserPhotos(int index) const; + [[nodiscard]] Entity entityForSharedMedia(int index) const; + [[nodiscard]] Entity entityForCollage(int index) const; + [[nodiscard]] Entity entityByIndex(int index) const; + [[nodiscard]] Entity entityForItemId(const FullMsgId &itemId) const; bool moveToEntity(const Entity &entity, int preloadDelta = 0); void setContext(std::variant< @@ -335,23 +339,23 @@ private: struct SharedMedia; using SharedMediaType = SharedMediaWithLastSlice::Type; using SharedMediaKey = SharedMediaWithLastSlice::Key; - std::optional sharedMediaType() const; - std::optional sharedMediaKey() const; - std::optional computeOverviewType() const; + [[nodiscard]] std::optional sharedMediaType() const; + [[nodiscard]] std::optional sharedMediaKey() const; + [[nodiscard]] std::optional computeOverviewType() const; bool validSharedMedia() const; void validateSharedMedia(); void handleSharedMediaUpdate(SharedMediaWithLastSlice &&update); struct UserPhotos; using UserPhotosKey = UserPhotosSlice::Key; - std::optional userPhotosKey() const; + [[nodiscard]] std::optional userPhotosKey() const; bool validUserPhotos() const; void validateUserPhotos(); void handleUserPhotosUpdate(UserPhotosSlice &&update); struct Collage; using CollageKey = WebPageCollage::Item; - std::optional collageKey() const; + [[nodiscard]] std::optional collageKey() const; bool validCollage() const; void validateCollage(); @@ -430,11 +434,11 @@ private: void contentSizeChanged(); // Radial animation interface. - float64 radialProgress() const; - bool radialLoading() const; - QRect radialRect() const; + [[nodiscard]] float64 radialProgress() const; + [[nodiscard]] bool radialLoading() const; + [[nodiscard]] QRect radialRect() const; void radialStart(); - crl::time radialTimeShift() const; + [[nodiscard]] crl::time radialTimeShift() const; void updateHeader(); void snapXY(); @@ -524,6 +528,7 @@ private: void clearStreaming(bool savePosition = true); [[nodiscard]] bool canInitStreaming() const; [[nodiscard]] bool saveControlLocked() const; + void applyVideoQuality(VideoQuality value); [[nodiscard]] bool topShadowOnTheRight() const; void applyHideWindowWorkaround(); @@ -551,6 +556,8 @@ private: rpl::lifetime _sessionLifetime; PhotoData *_photo = nullptr; DocumentData *_document = nullptr; + DocumentData *_chosenQuality = nullptr; + Media::VideoQuality _quality; QString _documentLoadingTo; std::shared_ptr _photoMedia; std::shared_ptr _documentMedia; @@ -625,9 +632,14 @@ private: std::unique_ptr _streamed; std::unique_ptr _pip; + QImage _streamedQualityChangeFrame; + crl::time _streamedPosition = 0; int _streamedCreated = 0; + bool _streamedQualityChangeFinished = false; bool _showAsPip = false; + Qt::Orientations _flip; + std::unique_ptr _stories; std::shared_ptr _cachedShow; rpl::event_stream<> _storiesChanged; diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index e13d1ce67..9d271ed70 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -889,18 +889,30 @@ void PipPanel::updateDecorations() { Pip::Pip( not_null delegate, not_null data, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, std::shared_ptr shared, FnMut closeAndContinue, FnMut destroy) : _delegate(delegate) , _data(data) -, _instance(std::move(shared), [=] { waitingAnimationCallback(); }) +, _origin(origin) +, _chosenQuality(chosenQuality) +, _context(context) +, _quality(quality) +, _instance( + std::in_place, + std::move(shared), + [=] { waitingAnimationCallback(); }) , _panel( _delegate->pipParentWidget(), [=](Ui::GL::Capabilities capabilities) { return chooseRenderer(capabilities); }) , _playbackProgress(std::make_unique()) +, _dataMedia(_data->createMediaView()) , _rotation(data->owner().mediaRotation().get(data)) , _lastPositiveVolume((Core::App().settings().videoVolume() > 0.) ? Core::App().settings().videoVolume() @@ -915,15 +927,28 @@ Pip::Pip( ) | rpl::start_with_next([=] { _destroy(); }, _panel.rp()->lifetime()); + + if (_context) { + _data->owner().itemRemoved( + ) | rpl::start_with_next([=](not_null data) { + if (_context != data) { + _context = nullptr; + } + }, _panel.rp()->lifetime()); + } } Pip::~Pip() = default; +std::shared_ptr Pip::shared() const { + return _instance->shared(); +} + void Pip::setupPanel() { _panel.init(); const auto size = [&] { - if (!_instance.info().video.size.isEmpty()) { - return _instance.info().video.size; + if (!_instance->info().video.size.isEmpty()) { + return _instance->info().video.size; } const auto media = _data->activeMediaView(); if (media) { @@ -1148,8 +1173,8 @@ void Pip::seekProgress(float64 value) { _lastDurationMs); if (_seekPositionMs != positionMs) { _seekPositionMs = positionMs; - if (!_instance.player().paused() - && !_instance.player().finished()) { + if (!_instance->player().paused() + && !_instance->player().finished()) { _pausedBySeek = true; playbackPauseResume(); } @@ -1167,7 +1192,7 @@ void Pip::seekFinish(float64 value) { crl::time(0), _lastDurationMs); _seekPositionMs = -1; - _startPaused = !_pausedBySeek && !_instance.player().finished(); + _startPaused = !_pausedBySeek && !_instance->player().finished(); restartAtSeekPosition(positionMs); } @@ -1304,18 +1329,71 @@ void Pip::updatePlayPauseResumeState(const Player::TrackState &state) { } void Pip::setupStreaming() { - _instance.setPriority(kPipLoaderPriority); - _instance.lockPlayer(); + _instance->setPriority(kPipLoaderPriority); + _instance->lockPlayer(); - _instance.player().updates( + _instance->switchQualityRequests( + ) | rpl::filter([=](int quality) { + return !_quality.manual && _quality.height != quality; + }) | rpl::start_with_next([=](int quality) { + applyVideoQuality({ + .manual = 0, + .height = uint32(quality), + }); + }, _instance->lifetime()); + + _instance->player().updates( ) | rpl::start_with_next_error([=](Streaming::Update &&update) { handleStreamingUpdate(std::move(update)); }, [=](Streaming::Error &&error) { handleStreamingError(std::move(error)); - }, _instance.lifetime()); + }, _instance->lifetime()); updatePlaybackState(); } +void Pip::applyVideoQuality(VideoQuality value) { + if (_quality == value + || !_dataMedia->canBePlayed(_context)) { + return; + } + const auto resolved = _data->chooseQuality(_context, value); + if (_chosenQuality == resolved) { + return; + } + auto instance = Streaming::Instance( + resolved, + _data, + _context, + _origin, + [=] { waitingAnimationCallback(); }); + if (!instance.valid()) { + return; + } + + if (_instance->ready()) { + _qualityChangeFrame = currentVideoFrameImage(); + } + if (!_instance->player().active() + || _instance->player().finished()) { + _qualityChangeFinished = true; + } + _startPaused = _qualityChangeFinished || _instance->player().paused(); + + _quality = value; + Core::App().settings().setVideoQuality(value); + Core::App().saveSettingsDelayed(); + _chosenQuality = resolved; + _instance.emplace(std::move(instance)); + setupStreaming(); + restartAtSeekPosition(_lastUpdatePosition); +} + +QImage Pip::currentVideoFrameImage() const { + return _instance->player().ready() + ? _instance->player().currentFrameImage() + : _instance->info().video.cover; +} + Ui::GL::ChosenRenderer Pip::chooseRenderer( Ui::GL::Capabilities capabilities) { const auto use = Platform::IsMac() @@ -1346,12 +1424,12 @@ void Pip::paint(not_null renderer) const { .fade = controlsShown, .outer = _panel.widget()->size(), .rotation = _rotation, - .videoRotation = _instance.info().video.rotation, + .videoRotation = _instance->info().video.rotation, .useTransparency = _panel.useTransparency(), }; if (canUseVideoFrame()) { renderer->paintTransformedVideoFrame(geometry); - _instance.markFrameShown(); + _instance->markFrameShown(); } else { const auto content = staticContent(); if (_preparedCoverState == ThumbState::Cover) { @@ -1359,7 +1437,7 @@ void Pip::paint(not_null renderer) const { } renderer->paintTransformedStaticContent(content, geometry); } - if (_instance.waitingShown()) { + if (_instance->waitingShown()) { renderer->paintRadialLoading(countRadialRect(), controlsShown); } if (controlsShown > 0) { @@ -1542,28 +1620,31 @@ void Pip::paintVolumeControllerContent( void Pip::handleStreamingUpdate(Streaming::Update &&update) { using namespace Streaming; - v::match(update.data, [&](Information &update) { + v::match(update.data, [&](const Information &update) { _panel.setAspectRatio( FlipSizeByRotation(update.video.size, _rotation)); - }, [&](const PreloadedVideo &update) { + _qualityChangeFrame = QImage(); + }, [&](PreloadedVideo) { updatePlaybackState(); - }, [&](const UpdateVideo &update) { + }, [&](UpdateVideo update) { _panel.update(); Core::App().updateNonIdle(); updatePlaybackState(); - }, [&](const PreloadedAudio &update) { + _lastUpdatePosition = update.position; + }, [&](PreloadedAudio) { updatePlaybackState(); - }, [&](const UpdateAudio &update) { + }, [&](UpdateAudio) { updatePlaybackState(); - }, [&](WaitingForData) { - }, [&](MutedByOther) { + }, [](WaitingForData) { + }, [](SpeedEstimate) { + }, [](MutedByOther) { }, [&](Finished) { updatePlaybackState(); }); } void Pip::updatePlaybackState() { - const auto state = _instance.player().prepareLegacyState(); + const auto state = _instance->player().prepareLegacyState(); updatePlayPauseResumeState(state); if (state.position == kTimeUnknown || state.length == kTimeUnknown @@ -1628,61 +1709,65 @@ void Pip::handleStreamingError(Streaming::Error &&error) { } void Pip::playbackPauseResume() { - if (_instance.player().failed()) { + if (_instance->player().failed()) { _panel.widget()->close(); - } else if (_instance.player().finished() - || !_instance.player().active()) { + } else if (_instance->player().finished() + || !_instance->player().active()) { _startPaused = false; restartAtSeekPosition(0); - } else if (_instance.player().paused()) { - _instance.resume(); + } else if (_instance->player().paused()) { + _instance->resume(); updatePlaybackState(); } else { - _instance.pause(); + _instance->pause(); updatePlaybackState(); } } void Pip::restartAtSeekPosition(crl::time position) { - if (!_instance.info().video.cover.isNull()) { + _lastUpdatePosition = position; + + if (!_instance->info().video.cover.isNull()) { _preparedCoverStorage = QImage(); _preparedCoverState = ThumbState::Empty; - _instance.saveFrameToCover(); + _instance->saveFrameToCover(); } auto options = Streaming::PlaybackOptions(); options.position = position; options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo(); - options.audioId = _instance.player().prepareLegacyState().id; + options.audioId = _instance->player().prepareLegacyState().id; options.speed = _delegate->pipPlaybackSpeed(); - _instance.play(options); + _instance->play(options); if (_startPaused) { - _instance.pause(); + _instance->pause(); } _pausedBySeek = false; updatePlaybackState(); } bool Pip::canUseVideoFrame() const { - return _instance.player().ready() - && !_instance.info().video.cover.isNull(); + return _instance->player().ready() + && !_instance->info().video.cover.isNull(); } QImage Pip::videoFrame(const FrameRequest &request) const { Expects(canUseVideoFrame()); - return _instance.frame(request); + return _instance->frame(request); } Streaming::FrameWithInfo Pip::videoFrameWithInfo() const { Expects(canUseVideoFrame()); - return _instance.frameWithInfo(); + return _instance->frameWithInfo(); } QImage Pip::staticContent() const { - const auto &cover = _instance.info().video.cover; + const auto &cover = !_qualityChangeFrame.isNull() + ? _qualityChangeFrame + : _instance->info().video.cover; const auto media = _data->activeMediaView(); const auto use = media ? media @@ -1710,7 +1795,7 @@ QImage Pip::staticContent() const { } _preparedCoverState = state; if (state == ThumbState::Cover) { - _preparedCoverStorage = _instance.info().video.cover; + _preparedCoverStorage = cover; } else { _preparedCoverStorage = (good ? good @@ -1736,7 +1821,7 @@ void Pip::paintRadialLoadingContent( st::radialLine, st::radialLine, st::radialLine)); - p.setOpacity(_instance.waitingOpacity()); + p.setOpacity(_instance->waitingOpacity()); p.setPen(Qt::NoPen); p.setBrush(st::radialBg); { @@ -1746,7 +1831,7 @@ void Pip::paintRadialLoadingContent( p.setOpacity(1.); Ui::InfiniteRadialAnimation::Draw( p, - _instance.waitingState(), + _instance->waitingState(), arc.topLeft(), arc.size(), _panel.widget()->width(), diff --git a/Telegram/SourceFiles/media/view/media_view_pip.h b/Telegram/SourceFiles/media/view/media_view_pip.h index d0156e1bd..503973896 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.h +++ b/Telegram/SourceFiles/media/view/media_view_pip.h @@ -7,13 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_file_origin.h" #include "media/streaming/media_streaming_instance.h" +#include "media/media_common.h" #include "ui/effects/animations.h" #include "ui/round_rect.h" #include "ui/rp_widget.h" #include +class HistoryItem; + namespace base { class PowerSaveBlocker; } // namespace base @@ -32,12 +36,15 @@ struct Capabilities; } // namespace GL } // namespace Ui -namespace Media { -namespace Player { +namespace Media::Player { struct TrackState; -} // namespace Player +} // namespace Media::Player -namespace View { +namespace Media::Streaming { +class Document; +} // namespace Media::Streaming + +namespace Media::View { class PlaybackProgress; @@ -134,11 +141,17 @@ public: Pip( not_null delegate, not_null data, + Data::FileOrigin origin, + not_null chosenQuality, + HistoryItem *context, + VideoQuality quality, std::shared_ptr shared, FnMut closeAndContinue, FnMut destroy); ~Pip(); + [[nodiscard]] std::shared_ptr shared() const; + private: enum class OverState { None, @@ -245,6 +258,8 @@ private: QRect outer, float64 shown) const; [[nodiscard]] QRect countRadialRect() const; + void applyVideoQuality(VideoQuality value); + [[nodiscard]] QImage currentVideoFrameImage() const; void seekUpdate(QPoint position); void seekProgress(float64 value); @@ -252,7 +267,11 @@ private: const not_null _delegate; const not_null _data; - Streaming::Instance _instance; + const Data::FileOrigin _origin; + DocumentData *_chosenQuality = nullptr; + HistoryItem *_context = nullptr; + Media::VideoQuality _quality; + std::optional _instance; bool _opengl = false; PipPanel _panel; QSize _size; @@ -260,6 +279,10 @@ private: std::unique_ptr _playbackProgress; std::shared_ptr _dataMedia; + QImage _qualityChangeFrame; + bool _qualityChangeFinished = false; + crl::time _lastUpdatePosition = 0; + bool _showPause = false; bool _startPaused = false; bool _pausedBySeek = false; @@ -288,5 +311,4 @@ private: }; -} // namespace View -} // namespace Media +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp index 896a66ec7..5ecb20da4 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp @@ -29,13 +29,15 @@ PlaybackControls::PlaybackControls( not_null delegate) : RpWidget(parent) , _delegate(delegate) +, _speedControllable(Media::Audio::SupportsSpeedControl()) +, _qualitiesList(_delegate->playbackControlsQualities()) , _playPauseResume(this, st::mediaviewPlayButton) , _playbackSlider(this, st::mediaviewPlayback) , _playbackProgress(std::make_unique()) , _volumeToggle(this, st::mediaviewVolumeToggle) , _volumeController(this, st::mediaviewPlayback) -, _speedToggle(Media::Audio::SupportsSpeedControl() - ? object_ptr(this, st::mediaviewSpeedButton) +, _speedToggle((_speedControllable || !_qualitiesList.empty()) + ? object_ptr(this, st::mediaviewSpeedButton) : nullptr) , _fullScreenToggle(this, st::mediaviewFullScreenButton) , _pictureInPicture(this, st::mediaviewPipButton) @@ -44,10 +46,20 @@ PlaybackControls::PlaybackControls( , _speedController(_speedToggle ? std::make_unique( _speedToggle.data(), + _speedToggle->st(), parent, [=](bool) {}, - [=](bool lastNonDefault) { return speedLookup(lastNonDefault); }, - [=](float64 speed) { saveSpeed(speed); }) + (_speedControllable + ? [=](bool lastNonDefault) { + return speedLookup(lastNonDefault); + } + : Fn()), + (_speedControllable + ? [=](float64 speed) { saveSpeed(speed); } + : Fn()), + _qualitiesList, + [=] { return _delegate->playbackControlsCurrentQuality(); }, + [=](int quality) { saveQuality(quality); }) : nullptr) , _fadeAnimation(std::make_unique(this)) { _fadeAnimation->show(); @@ -58,6 +70,18 @@ PlaybackControls::PlaybackControls( fadeUpdated(opacity); }); + _speedToggle->setSpeed(_speedControllable + ? _delegate->playbackControlsCurrentSpeed(false) + : 1.); + updateSpeedToggleQuality(); + + if (const auto controller = _speedController.get()) { + controller->menuToggledValue( + ) | rpl::start_with_next([=](bool toggled) { + _speedToggle->setActive(toggled); + }, _speedToggle->lifetime()); + } + _pictureInPicture->addClickHandler([=] { _delegate->playbackControlsToPictureInPicture(); }); @@ -189,9 +213,20 @@ float64 PlaybackControls::speedLookup(bool lastNonDefault) const { } void PlaybackControls::saveSpeed(float64 speed) { + _speedToggle->setSpeed(speed); _delegate->playbackControlsSpeedChanged(speed); } +void PlaybackControls::saveQuality(int quality) { + _speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality); + _delegate->playbackControlsQualityChanged(quality); +} + +void PlaybackControls::updateSpeedToggleQuality() { + const auto quality = _delegate->playbackControlsCurrentQuality(); + _speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality.height); +} + void PlaybackControls::updatePlaybackSpeed(float64 speed) { DEBUG_LOG(("Media playback speed: update to %1.").arg(speed)); _delegate->playbackControlsSpeedChanged(speed); diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h index ac5e15baa..a91b8ad2e 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "base/object_ptr.h" +#include "media/media_common.h" namespace Ui { class LabelSimple; @@ -21,7 +22,7 @@ class PopupMenu; namespace Media { namespace Player { struct TrackState; -class SpeedButton; +class SettingsButton; class SpeedController; } // namespace Player @@ -44,6 +45,11 @@ public: virtual void playbackControlsSpeedChanged(float64 speed) = 0; [[nodiscard]] virtual float64 playbackControlsCurrentSpeed( bool lastNonDefault) = 0; + [[nodiscard]] virtual auto playbackControlsQualities() + -> std::vector = 0; + [[nodiscard]] virtual auto playbackControlsCurrentQuality() + -> VideoQuality = 0; + virtual void playbackControlsQualityChanged(int quality) = 0; virtual void playbackControlsToFullScreen() = 0; virtual void playbackControlsFromFullScreen() = 0; virtual void playbackControlsToPictureInPicture() = 0; @@ -90,8 +96,14 @@ private: [[nodiscard]] float64 speedLookup(bool lastNonDefault) const; void saveSpeed(float64 speed); + void saveQuality(int quality); + void updateSpeedToggleQuality(); + const not_null _delegate; + bool _speedControllable = false; + std::vector _qualitiesList; + bool _inFullScreen = false; bool _showPause = false; bool _childrenHidden = false; @@ -108,7 +120,7 @@ private: std::unique_ptr _receivedTillProgress; object_ptr _volumeToggle; object_ptr _volumeController; - object_ptr _speedToggle; + object_ptr _speedToggle; object_ptr _fullScreenToggle; object_ptr _pictureInPicture; object_ptr _playedAlready; diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index 7e4927544..631f4aa8f 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -27,9 +27,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.h" +#include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_media_view.h" #include "styles/style_menu_icons.h" @@ -41,11 +43,13 @@ namespace { void AboutBox( not_null box, - std::shared_ptr show) { + std::shared_ptr show, + const FullMsgId &fullId) { constexpr auto kUrl = "https://promote.telegram.org"_cs; box->setNoContentMargin(true); + const auto isChannel = peerIsChannel(fullId.peer); const auto session = &show->session(); const auto content = box->verticalLayout().get(); @@ -131,29 +135,41 @@ void AboutBox( }; addEntry( tr::lng_sponsored_revenued_info1_title(), - tr::lng_sponsored_revenued_info1_description( - Ui::Text::RichLangValue), + (isChannel + ? tr::lng_sponsored_revenued_info1_description + : tr::lng_sponsored_revenued_info1_bot_description)( + Ui::Text::RichLangValue), st::sponsoredAboutPrivacyIcon); Ui::AddSkip(content); Ui::AddSkip(content); addEntry( - tr::lng_sponsored_revenued_info2_title(), - tr::lng_sponsored_revenued_info2_description( - Ui::Text::RichLangValue), + (isChannel + ? tr::lng_sponsored_revenued_info2_title + : tr::lng_sponsored_revenued_info2_bot_title)(), + (isChannel + ? tr::lng_sponsored_revenued_info2_description + : tr::lng_sponsored_revenued_info2_bot_description)( + Ui::Text::RichLangValue), st::sponsoredAboutSplitIcon); Ui::AddSkip(content); Ui::AddSkip(content); + auto link = tr::lng_settings_privacy_premium_link( + ) | rpl::map([](QString t) { + return Ui::Text::Link(std::move(t), u"internal:"_q); + }); addEntry( tr::lng_sponsored_revenued_info3_title(), - tr::lng_sponsored_revenued_info3_description( - lt_count, - rpl::single(float64(levels)), - lt_link, - tr::lng_settings_privacy_premium_link( - ) | rpl::map([=](QString t) { - return Ui::Text::Link(std::move(t), u"internal:"_q); - }), - Ui::Text::RichLangValue), + isChannel + ? tr::lng_sponsored_revenued_info3_description( + lt_count, + rpl::single(float64(levels)), + lt_link, + std::move(link), + Ui::Text::RichLangValue) + : tr::lng_sponsored_revenued_info3_bot_description( + lt_link, + std::move(link), + Ui::Text::RichLangValue), st::sponsoredAboutRemoveIcon)->setClickHandlerFilter([=]( const auto &...) { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); @@ -185,16 +201,18 @@ void AboutBox( box->addRow( Ui::CreateLabelWithCustomEmoji( content, - tr::lng_sponsored_revenued_footer_description( - 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), kUrl.utf16()); - }), - Ui::Text::RichLangValue), + (isChannel + ? tr::lng_sponsored_revenued_footer_description + : tr::lng_sponsored_revenued_footer_bot_description)( + lt_link, + tr::lng_channel_earn_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([=](TextWithEntities t) { + return Ui::Text::Link(std::move(t), kUrl.utf16()); + }), + Ui::Text::RichLangValue), { .session = session }, st::channelEarnLearnDescription))->resizeToWidth(available); } @@ -214,14 +232,53 @@ void AboutBox( box->addButton(std::move(button)); } + if (!isChannel) { + const auto top = Ui::CreateChild( + box, + st::infoTopBarMenu); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + top->raise(); + top->moveToLeft( + width - top->width() - st::defaultScrollArea.width, + 0); + }, top->lifetime()); + using MenuPtr = base::unique_qptr; + const auto menu = top->lifetime().make_state(); + top->setClickedCallback([=] { + *menu = base::make_unique_q( + box->window(), + st::popupMenuWithIcons); + const auto raw = menu->get(); + raw->animatePhaseValue( + ) | rpl::start_with_next([=](Ui::PopupMenu::AnimatePhase phase) { + top->setForceRippled(phase == Ui::PopupMenu::AnimatePhase::Shown + || phase == Ui::PopupMenu::AnimatePhase::StartShow); + }, top->lifetime()); + FillSponsored( + top, + Ui::Menu::CreateAddActionCallback(menu->get()), + show, + fullId, + false, + true); + const auto global = top->mapToGlobal( + QPoint(top->width() / 4 * 3, top->height() / 2)); + raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + raw->popup( + QPoint( + global.x(), + std::max(global.y(), QCursor::pos().y()))); + return true; + }); + } + } void ShowReportSponsoredBox( std::shared_ptr show, - not_null item) { - const auto peer = item->history()->peer; - auto &sponsoredMessages = peer->session().sponsoredMessages(); - const auto fullId = item->fullId(); + const FullMsgId &fullId) { + auto &sponsoredMessages = show->session().sponsoredMessages(); const auto report = sponsoredMessages.createReportCallback(fullId); const auto guideLink = Ui::Text::Link( tr::lng_report_sponsored_reported_link(tr::now), @@ -306,32 +363,74 @@ void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, - not_null item, - bool mediaViewer) { - Expects(item->isSponsored()); + const FullMsgId &fullId, + bool mediaViewer, + bool skipAbout) { + const auto session = &show->session(); + const auto details = session->sponsoredMessages().lookupDetails(fullId); + const auto &info = details.info; - const auto session = &item->history()->session(); + if (!mediaViewer && !info.empty()) { + auto fillSubmenu = [&](not_null menu) { + const auto allText = ranges::accumulate( + info, + TextWithEntities(), + [](TextWithEntities a, TextWithEntities b) { + return a.text.isEmpty() ? b : a.append('\n').append(b); + }).text; + const auto callback = [=] { + TextUtilities::SetClipboardText({ allText }); + show->showToast(tr::lng_text_copied(tr::now)); + }; + for (const auto &i : info) { + auto item = base::make_unique_q( + menu, + st::defaultMenu, + st::historySponsorInfoItem, + st::historyHasCustomEmojiPosition, + base::duplicate(i)); + item->clicks( + ) | rpl::start_with_next(callback, menu->lifetime()); + menu->addAction(std::move(item)); + if (i != details.info.back()) { + menu->addSeparator(); + } + } + }; + addAction({ + .text = tr::lng_sponsored_info_menu(tr::now), + .handler = nullptr, + .icon = &st::menuIconChannel, + .fillSubmenu = std::move(fillSubmenu), + }); + addAction({ + .separatorSt = &st::expandedMenuSeparator, + .isSeparator = true, + }); + } + if (details.canReport) { + if (!skipAbout) { + addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { + show->show(Box(AboutBox, show, fullId)); + }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); + } - addAction(tr::lng_sponsored_menu_revenued_about(tr::now), [=] { - show->show(Box(AboutBox, show)); - }, (mediaViewer ? &st::mediaMenuIconInfo : &st::menuIconInfo)); - - addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { - ShowReportSponsoredBox(show, item); - }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); - - addAction({ - .separatorSt = (mediaViewer - ? &st::mediaviewMenuSeparator - : &st::expandedMenuSeparator), - .isSeparator = true, - }); + addAction(tr::lng_sponsored_menu_revenued_report(tr::now), [=] { + ShowReportSponsoredBox(show, fullId); + }, (mediaViewer ? &st::mediaMenuIconBlock : &st::menuIconBlock)); + addAction({ + .separatorSt = (mediaViewer + ? &st::mediaviewMenuSeparator + : &st::expandedMenuSeparator), + .isSeparator = true, + }); + } addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { if (session->premium()) { using Result = Data::SponsoredReportResult; session->sponsoredMessages().createReportCallback( - item->fullId())(Result::Id("-1"), [](const auto &) {}); + fullId)(Result::Id("-1"), [](const auto &) {}); } else { ShowPremiumPreviewBox(show, PremiumFeature::NoAds); } @@ -341,9 +440,7 @@ void FillSponsored( void ShowSponsored( not_null parent, std::shared_ptr show, - not_null item) { - Expects(item->isSponsored()); - + const FullMsgId &fullId) { const auto menu = Ui::CreateChild( parent.get(), st::popupMenuWithIcons); @@ -352,15 +449,17 @@ void ShowSponsored( parent, Ui::Menu::CreateAddActionCallback(menu), show, - item, + fullId, false); menu->popup(QCursor::pos()); } -void ShowSponsoredAbout(std::shared_ptr show) { +void ShowSponsoredAbout( + std::shared_ptr show, + const FullMsgId &fullId) { show->showBox(Box([=](not_null box) { - AboutBox(box, show); + AboutBox(box, show, fullId); })); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.h b/Telegram/SourceFiles/menu/menu_sponsored.h index 3085aad95..4061271a9 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.h +++ b/Telegram/SourceFiles/menu/menu_sponsored.h @@ -26,14 +26,17 @@ void FillSponsored( not_null parent, const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, - not_null item, - bool mediaViewer); + const FullMsgId &fullId, + bool mediaViewer, + bool skipAbout = false); void ShowSponsored( not_null parent, std::shared_ptr show, - not_null item); + const FullMsgId &fullId); -void ShowSponsoredAbout(std::shared_ptr show); +void ShowSponsoredAbout( + std::shared_ptr show, + const FullMsgId &fullId); } // namespace Menu diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 0f20d25b3..0206faf32 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/129.0.0.0 Safari/537.36"); + "Chrome/130.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 72acb3884..db18d216e 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -116,7 +116,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#94345242 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = Message; +message#94345242 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -183,7 +183,7 @@ messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int = UserFull; +userFull#1f58e369 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -353,7 +353,7 @@ updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; updatePeerLocated#b4afcfb0 peers:Vector = Update; updateNewScheduledMessage#39a51dfb message:Message = Update; -updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; +updateDeleteScheduledMessages#f2a71983 flags:# peer:Peer messages:Vector sent_messages:flags.0?Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; @@ -426,6 +426,7 @@ updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long conne updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; updatePaidReactionPrivacy#51ca7aec private:Bool = Update; +updateBotSubscriptionExpire#2d13c6ee user_id:long payload:string invoice_slug:string until_date:int qts:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -532,6 +533,7 @@ inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey; +inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -544,6 +546,7 @@ privacyKeyAddedByPhone#42ffd42b = PrivacyKey; privacyKeyVoiceMessages#697f414 = PrivacyKey; privacyKeyAbout#a486b761 = PrivacyKey; privacyKeyBirthday#2000a518 = PrivacyKey; +privacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -555,6 +558,8 @@ inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivac inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule; inputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule; +inputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule; +inputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule; privacyValueAllowContacts#fffe1bac = PrivacyRule; privacyValueAllowAll#65427b82 = PrivacyRule; @@ -566,6 +571,8 @@ privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule; privacyValueAllowPremium#ece9814b = PrivacyRule; +privacyValueAllowBots#21461b5d = PrivacyRule; +privacyValueDisallowBots#f6a5f82f = PrivacyRule; account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; @@ -635,7 +642,7 @@ messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; -botInfo#82437e74 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string = BotInfo; +botInfo#36607333 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings = BotInfo; keyboardButton#a2fa4880 text:string = KeyboardButton; keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; @@ -893,7 +900,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#5db95a15 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string = Invoice; +invoice#49ee584 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string subscription_period:flags.11?int = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -1436,7 +1443,7 @@ attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector attachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector = AttachMenuBotsBot; -webViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true query_id:flags.0?long url:string = WebViewResult; +webViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true fullscreen:flags.2?true query_id:flags.0?long url:string = WebViewResult; webViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent; @@ -1821,10 +1828,11 @@ starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; starsTransactionPeerAds#60682812 = StarsTransactionPeer; +starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#a9ee4c2 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?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 subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift = StarsTransaction; +starsTransaction#35d4f276 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?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 subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int = StarsTransaction; payments.starsStatus#bbfa316c flags:# balance:long subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; @@ -1854,7 +1862,7 @@ bots.previewInfo#ca71d64 media:Vector lang_codes:Vector starsSubscriptionPricing#5416d58 period:int amount:long = StarsSubscriptionPricing; -starsSubscription#538ecf18 flags:# canceled:flags.0?true can_refulfill:flags.1?true missing_balance:flags.2?true id:string peer:Peer until_date:int pricing:StarsSubscriptionPricing chat_invite_hash:flags.3?string = StarsSubscription; +starsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?true missing_balance:flags.2?true bot_canceled:flags.7?true id:string peer:Peer until_date:int pricing:StarsSubscriptionPricing chat_invite_hash:flags.3?string title:flags.4?string photo:flags.5?WebDocument invoice_slug:flags.6?string = StarsSubscription; messageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor; @@ -1862,7 +1870,7 @@ starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; -starGift#aea174ee flags:# limited:flags.0?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int convert_stars:long = StarGift; +starGift#49c577cd flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#901689ea hash:int gifts:Vector = payments.StarGifts; @@ -1877,6 +1885,12 @@ reportResultChooseOption#f0e4e0b6 title:string options:Vector cache_time:int users:Vector = messages.PreparedInlineMessage; + +botAppSettings#c99b1950 flags:# placeholder_path:flags.0?bytes background_color:flags.1?int background_dark_color:flags.2?int header_color:flags.3?int header_dark_color:flags.4?int = BotAppSettings; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2068,9 +2082,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector option:bytes message:string = ReportResult; @@ -2148,7 +2162,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -2220,9 +2234,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 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.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true compact:flags.7?true fullscreen:flags.8?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#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.requestSimpleWebView#413a3e73 flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true compact:flags.7?true fullscreen:flags.8?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; @@ -2244,7 +2258,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#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.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?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; @@ -2272,10 +2286,16 @@ messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; -messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; +messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; messages.sendPaidReaction#9dd6a67b flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?Bool = Updates; messages.togglePaidReactionPrivacy#849ad397 peer:InputPeer msg_id:int private:Bool = Bool; messages.getPaidReactionPrivacy#472455aa = Updates; +messages.viewSponsoredMessage#673ad8f1 peer:InputPeer random_id:bytes = Bool; +messages.clickSponsoredMessage#f093465 flags:# media:flags.0?true fullscreen:flags.1?true peer:InputPeer random_id:bytes = Bool; +messages.reportSponsoredMessage#1af3dbb8 peer:InputPeer random_id:bytes option:bytes = channels.SponsoredMessageReportResult; +messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; +messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector = messages.BotPreparedInlineMessage; +messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2357,8 +2377,6 @@ channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint addr channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; -channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool; -channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; @@ -2377,14 +2395,12 @@ channels.reorderPinnedForumTopics#2950a18f flags:# force:flags.0?true channel:In channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; -channels.clickSponsoredMessage#1445d75 flags:# media:flags.0?true fullscreen:flags.1?true channel:InputChannel random_id:bytes = Bool; channels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates; channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats; channels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates; channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates; channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; -channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; @@ -2411,6 +2427,9 @@ bots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector = Bool; bots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo; bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; +bots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool; +bots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool; +bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2446,6 +2465,7 @@ payments.getStarGifts#c4563590 hash:int = payments.StarGifts; payments.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = payments.UserStarGifts; payments.saveStarGift#87acf08e flags:# unsave:flags.0?true user_id:InputUser msg_id:int = Bool; payments.convertStarGift#421e027 user_id:InputUser msg_id:int = Bool; +payments.botCancelStarsSubscription#57f9ece6 flags:# restore:flags.0?true user_id:InputUser invoice_slug:flags.1?string charge_id:flags.2?string = Bool; 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; @@ -2506,9 +2526,9 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; -stats.getBroadcastRevenueStats#75dfb671 flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastRevenueStats; -stats.getBroadcastRevenueWithdrawalUrl#2a65ef73 channel:InputChannel password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl; -stats.getBroadcastRevenueTransactions#69280f channel:InputChannel offset:int limit:int = stats.BroadcastRevenueTransactions; +stats.getBroadcastRevenueStats#f788ee19 flags:# dark:flags.0?true peer:InputPeer = stats.BroadcastRevenueStats; +stats.getBroadcastRevenueWithdrawalUrl#9df4faad peer:InputPeer password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl; +stats.getBroadcastRevenueTransactions#70990b6d peer:InputPeer offset:int limit:int = stats.BroadcastRevenueTransactions; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; @@ -2547,7 +2567,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; +stories.searchPosts#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer 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; @@ -2565,4 +2585,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 190 +// LAYER 193 diff --git a/Telegram/SourceFiles/mtproto/session.cpp b/Telegram/SourceFiles/mtproto/session.cpp index 0a50a4561..842541e77 100644 --- a/Telegram/SourceFiles/mtproto/session.cpp +++ b/Telegram/SourceFiles/mtproto/session.cpp @@ -399,12 +399,10 @@ int32 Session::getState() const { if (_private) { const auto s = _private->getState(); - if (s == ConnectedState) { + if (s == ConnectedState + || s == ConnectingState + || s == DisconnectedState) { return s; - } else if (s == ConnectingState || s == DisconnectedState) { - if (result < 0) { - return s; - } } else if (s < 0) { if (result < 0 && s > result) { result = s; diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index f3a498575..df494e425 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -108,9 +108,7 @@ passportContactErrorMargin: margins(0px, 0px, 0px, 14px); passportRowPadding: margins(22px, 8px, 25px, 8px); passportRowIconSkip: 10px; passportRowSkip: 2px; -passportRowRipple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; -} +passportRowRipple: defaultRippleAnimationBgOver; passportRowReadyIcon: icon {{ "passport_ready", windowActiveTextFg }}; passportRowEmptyIcon: icon {{ "passport_empty", menuIconFgOver }}; passportRowTitleFg: windowFg; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index fd9e57bab..2369181db 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -150,3 +150,7 @@ botWebViewBottomButton: RoundButton(paymentsPanelSubmit) { } textTop: 11px; } +botWebViewRadialStroke: 3px; +botWebViewMenu: PopupMenu(popupMenuWithIcons) { + maxHeight: 360px; +} diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 2bc023e8c..b0e1bcc0c 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -550,7 +550,7 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { _webview = std::make_unique( container, Webview::WindowConfig{ - .opaqueBg = params.opaqueBg, + .opaqueBg = params.bodyBg, .storageId = _delegate->panelWebviewStorageId(), }); @@ -919,7 +919,7 @@ void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) { return; } _webview->window.updateTheme( - params.opaqueBg, + params.bodyBg, params.scrollBg, params.scrollBgOver, params.scrollBarBg, diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index c874eb3a9..77342222e 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -30,7 +30,8 @@ namespace Settings { [[nodiscard]] not_null AddBalanceWidget( not_null parent, rpl::producer balanceValue, - bool rightAlign); + bool rightAlign, + rpl::producer opacityValue = nullptr); } // namespace Settings namespace Ui { diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 8a7db94ed..e048a2d05 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -204,7 +204,7 @@ LinuxIntegration::LinuxIntegration() return value->get_uint32() == 1; } return std::nullopt; -}) +}()) , _darkModeWatcher( "org.freedesktop.appearance", "color-scheme", diff --git a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp index 5bf883658..e07298811 100644 --- a/Telegram/SourceFiles/platform/win/file_utilities_win.cpp +++ b/Telegram/SourceFiles/platform/win/file_utilities_win.cpp @@ -375,7 +375,9 @@ bool Get( dialog.setFileMode(QFileDialog::AnyFile); dialog.setAcceptMode(QFileDialog::AcceptSave); } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) dialog.show(); +#endif // Qt < 6.0.0 auto realLastPath = [=] { // If we're given some non empty path containing a folder - use it. diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index 99cd1b907..0e50281cb 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -37,7 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#ifndef __MINGW32__ #include #include #include @@ -48,12 +47,9 @@ using namespace winrt::Windows::UI::Notifications; using namespace winrt::Windows::Data::Xml::Dom; using namespace winrt::Windows::Foundation; using winrt::com_ptr; -#endif // !__MINGW32__ namespace Platform { namespace Notifications { - -#ifndef __MINGW32__ namespace { constexpr auto kQuerySettingsEachMs = 1000; @@ -367,6 +363,7 @@ bool SkipSoundForCustom() { return (UserNotificationState == QUNS_NOT_PRESENT) || (UserNotificationState == QUNS_PRESENTATION_MODE) + || (FocusAssistBlocks && Core::App().settings().skipToastsInFocus()) || Core::App().screenIsLocked(); } @@ -375,7 +372,6 @@ bool SkipFlashBounceForCustom() { } } // namespace -#endif // !__MINGW32__ void MaybePlaySoundForCustom(Fn playSound) { if (!SkipSoundForCustom()) { @@ -387,7 +383,8 @@ bool SkipToastForCustom() { QuerySystemNotificationSettings(); return (UserNotificationState == QUNS_PRESENTATION_MODE) - || (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN); + || (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN) + || (FocusAssistBlocks && Core::App().settings().skipToastsInFocus()); } void MaybeFlashBounceForCustom(Fn flashBounce) { @@ -403,15 +400,11 @@ bool WaitForInputForCustom() { } bool Supported() { -#ifndef __MINGW32__ if (!Checked) { Checked = true; Check(); } return InitSucceeded; -#endif // !__MINGW32__ - - return false; } bool Enforced() { @@ -423,7 +416,6 @@ bool ByDefault() { } void Create(Window::Notifications::System *system) { -#ifndef __MINGW32__ if (Core::App().settings().nativeNotifications() && Supported()) { auto result = std::make_unique(system); if (result->init()) { @@ -431,11 +423,9 @@ void Create(Window::Notifications::System *system) { return; } } -#endif // !__MINGW32__ system->setManager(nullptr); } -#ifndef __MINGW32__ class Manager::Private { public: explicit Private(Manager *instance); @@ -990,7 +980,6 @@ void Manager::doMaybeFlashBounce(Fn flashBounce) { flashBounce(); } } -#endif // !__MINGW32__ } // namespace Notifications } // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.h b/Telegram/SourceFiles/platform/win/notifications_manager_win.h index eba557177..6fb62e414 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.h +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.h @@ -14,8 +14,6 @@ struct ToastActivation; namespace Platform { namespace Notifications { -#ifndef __MINGW32__ - class Manager : public Window::Notifications::NativeManager { public: Manager(Window::Notifications::System *system); @@ -54,7 +52,6 @@ private: const std::unique_ptr _private; }; -#endif // !__MINGW32__ } // namespace Notifications } // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.h b/Telegram/SourceFiles/platform/win/windows_dlls.h index 9ebed92fc..6ed503c8d 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.h +++ b/Telegram/SourceFiles/platform/win/windows_dlls.h @@ -16,10 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#ifdef __MINGW32__ -#define __in -#endif - namespace Platform { namespace Dlls { diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index 3c75cf9ba..cd439e139 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -226,6 +226,9 @@ Main::Session &PreviewController::session() const { [[nodiscard]] QString ExtractUsername(QString text) { text = text.trimmed(); + if (text.startsWith(QChar('@'))) { + return text.mid(1); + } static const auto expression = QRegularExpression( "^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)"); const auto match = expression.match(text); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 1610aafee..d1dfbbad5 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1200,6 +1200,7 @@ void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { data.bytes, data.waveform, data.duration, + data.video, std::move(action)); _composeControls->cancelReplyMessage(); diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index de7c34e3e..d7e2947bd 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -679,7 +679,7 @@ object_ptr ChooseCameraDeviceBox( const style::Radio *radioSt) { return Box( ChooseMediaDeviceBox, - tr::lng_settings_call_device_default(), + tr::lng_settings_call_camera(), Core::App().mediaDevices().devicesValue(DeviceType::Camera), std::move(currentId), std::move(chosen), diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index b653b6e0f..24c610f62 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -902,24 +902,10 @@ void SetupMessages( Quick::React, tr::lng_settings_chat_quick_action_react(tr::now)); - class EmptyButton final : public Ui::IconButton { - public: - EmptyButton(not_null p, const style::IconButton &st) - : Ui::IconButton(p, st) - , _rippleAreaPosition(st.rippleAreaPosition) { - } - protected: - void paintEvent(QPaintEvent *e) override { - auto p = QPainter(this); - - paintRipple(p, _rippleAreaPosition, nullptr); - } - private: - const QPoint _rippleAreaPosition; - }; - const auto buttonRight = Ui::CreateChild( + const auto buttonRight = Ui::CreateSimpleCircleButton( inner, - st::stickersRemove); + st::stickersRemove.ripple); + buttonRight->resize(st::stickersRemove.width, st::stickersRemove.height); const auto toggleButtonRight = [=](bool value) { buttonRight->setAttribute(Qt::WA_TransparentForMouseEvents, !value); }; diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index b36f1df0f..b4cd7396d 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" +#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" @@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" #include "styles/style_credits.h" +#include "styles/style_giveaway.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_premium.h" @@ -100,6 +102,13 @@ Credits::Credits( , _star(Ui::GenerateStars(st::creditsTopupButton.height, 1)) , _balanceStar(Ui::GenerateStars(st::creditsBalanceStarHeight, 1)) { setupContent(); + + _controller->session().premiumPossibleValue( + ) | rpl::start_with_next([=](bool premiumPossible) { + if (!premiumPossible) { + _showBack.fire({}); + } + }, lifetime()); } rpl::producer Credits::title() { @@ -350,20 +359,129 @@ void Credits::setupContent() { Ui::StartFireworks(_parent); } }; - const auto self = _controller->session().user(); - FillCreditOptions(_controller->uiShow(), content, self, 0, paid); + Ui::AddSkip(content); + Ui::AddSkip(content); + const auto balanceLine = content->add( + object_ptr>( + content, + object_ptr(content)))->entity(); + const auto balanceIcon = CreateSingleStarWidget( + balanceLine, + st::creditsSettingsBigBalance.style.font->height); + const auto balanceAmount = Ui::CreateChild( + balanceLine, + _controller->session().credits().balanceValue( + ) | rpl::map(Lang::FormatCountDecimal), + st::creditsSettingsBigBalance); + balanceAmount->sizeValue() | rpl::start_with_next([=] { + balanceLine->resize( + balanceIcon->width() + + st::creditsSettingsBigBalanceSkip + + balanceAmount->textMaxWidth(), + balanceIcon->height()); + }, balanceLine->lifetime()); + balanceLine->widthValue() | rpl::start_with_next([=] { + balanceAmount->moveToRight(0, 0); + }, balanceLine->lifetime()); + Ui::AddSkip(content); + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_credits_balance_me(), + st::infoTopBar.subtitle))); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + + struct State final { + rpl::variable confirmButtonBusy = false; + std::optional api; + }; + const auto state = content->lifetime().make_state(); + + const auto button = content->add( + object_ptr( + content, + rpl::conditional( + state->confirmButtonBusy.value(), + rpl::single(QString()), + tr::lng_credits_buy_button()), + st::creditsSettingsBigBalanceButton), + st::boxRowPadding); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto show = _controller->uiShow(); + const auto optionsBox = [=](not_null box) { + box->setStyle(st::giveawayGiftCodeBox); + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_credits_summary_options_subtitle()); + const auto inner = box->verticalLayout(); + const auto self = show->session().user(); + const auto options = state->api + ? state->api->options() + : Data::CreditTopupOptions(); + FillCreditOptions(show, inner, self, 0, paid, nullptr, options); + + const auto button = box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); + const auto buttonWidth = st::boxWideWidth + - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); + }; + button->setClickedCallback([=] { + if (state->api && !state->api->options().empty()) { + state->confirmButtonBusy = false; + show->show(Box(optionsBox)); + } else { + state->confirmButtonBusy = true; + state->api.emplace(show->session().user()); + state->api->request( + ) | rpl::start_with_error_done([=](const QString &error) { + state->confirmButtonBusy = false; + show->showToast(error); + }, [=] { + state->confirmButtonBusy = false; + show->show(Box(optionsBox)); + }, content->lifetime()); + } + }); { - Ui::AddSkip(content); - const auto giftButton = AddButtonWithIcon( + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + button, + button->height() / 2); + AddChildToWidgetCenter(button, loadingAnimation); + loadingAnimation->showOn(state->confirmButtonBusy.value()); + } + const auto paddings = rect::m::sum::h(st::boxRowPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != (content->width() - paddings)); + }) | rpl::start_with_next([=] { + button->resizeToWidth(content->width() - paddings); + }, button->lifetime()); + + Ui::AddSkip(content); + + const auto gift = content->add( + object_ptr( content, tr::lng_credits_gift_button(), - st::settingsButtonLightNoIcon); - Ui::AddSkip(content); - Ui::AddDivider(content); - giftButton->setClickedCallback([=] { - Ui::ShowGiftCreditsBox(_controller, paid); - }); - } + st::creditsSettingsBigBalanceButtonGift), + st::boxRowPadding); + gift->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + gift->setClickedCallback([=, controller = _controller] { + Ui::ShowGiftCreditsBox(controller, paid); + }); + + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddDivider(content); setupSubscriptions(content); setupHistory(content); @@ -422,7 +540,12 @@ QPointer Credits::createPinnedToTop( const auto balance = AddBalanceWidget( content, _controller->session().credits().balanceValue(), - true); + true, + content->heightValue() | rpl::map([=](int height) { + const auto ratio = float64(height - content->minimumHeight()) + / (content->maximumHeight() - content->minimumHeight()); + return (1. - ratio / 0.35); + })); _controller->session().credits().load(true); rpl::combine( balance->sizeValue(), diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 1e92b697b..fc1b1d8e8 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -52,10 +52,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "statistics/widgets/chart_header_widget.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" #include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/effects/premium_top_bar.h" +#include "ui/effects/toggle_arrow.h" #include "ui/image/image_prepare.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" @@ -172,17 +175,17 @@ void ToggleStarGiftSaved( sender->inputUser, MTP_int(itemId.bare) )).done([=] { + done(true); if (const auto strong = weak.get()) { strong->showToast((save ? tr::lng_gift_display_done : tr::lng_gift_display_done_hide)(tr::now)); } - done(true); }).fail([=](const MTP::Error &error) { + done(false); if (const auto strong = weak.get()) { strong->showToast(error.type()); } - done(false); }).send(); } @@ -190,14 +193,28 @@ void ConfirmConvertStarGift( std::shared_ptr show, QString name, int stars, + int daysLeft, Fn convert) { - show->show(Ui::MakeConfirmBox({ - .text = tr::lng_gift_convert_sure_text( + auto text = rpl::combine( + tr::lng_gift_convert_sure_confirm( lt_count, rpl::single(stars * 1.), lt_user, rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue), + tr::lng_gift_convert_sure_limit( + lt_count, + rpl::single(daysLeft * 1.), + Ui::Text::RichLangValue), + tr::lng_gift_convert_sure_caution(Ui::Text::RichLangValue) + ) | rpl::map([]( + TextWithEntities &&a, + TextWithEntities &&b, + TextWithEntities &&c) { + return a.append("\n\n").append(b).append("\n\n").append(c); + }); + show->show(Ui::MakeConfirmBox({ + .text = std::move(text), .confirmed = [=](Fn close) { close(); convert(); }, .confirmText = tr::lng_gift_convert_sure(), .title = tr::lng_gift_convert_sure_title(), @@ -279,6 +296,7 @@ void AddViewMediaHandler( state->item, owner->document(item.id), true, // skipPremiumEffect + false, // hasQualitiesList false, // spoiler 0)); // ttlSeconds } @@ -380,7 +398,9 @@ void FillCreditOptions( not_null container, not_null peer, int minimumCredits, - Fn paid) { + Fn paid, + rpl::producer subtitle, + std::vector preloadedTopupOptions) { const auto options = container->add( object_ptr>( container, @@ -397,9 +417,26 @@ void FillCreditOptions( while (content->count()) { delete content->widgetAt(0); } - Ui::AddSubsectionTitle( - content, - tr::lng_credits_summary_options_subtitle()); + if (subtitle) { + Ui::AddSubsectionTitle(content, std::move(subtitle)); + } + + const auto buttons = content->add( + object_ptr(content)); + + const auto showMoreWrap = content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_credits_more_options(), + st::statisticsShowMoreButton))); + const auto showMore = showMoreWrap->entity(); + showMore->setClickedCallback([=] { + showMoreWrap->toggle(false, anim::type::instant); + }); + Ui::AddToggleUpDownArrowToMoreButton(showMore); + const auto &st = st::creditsTopupButton; const auto diffBetweenTextAndStar = st.padding.left() - st.iconLeft @@ -414,10 +451,24 @@ void FillCreditOptions( if (option.credits < minCredits) { continue; } - const auto button = content->add(object_ptr( - content, - rpl::never(), - st)); + const auto button = [&] { + auto owned = object_ptr( + buttons, + rpl::never(), + st); + if (!option.extended) { + return buttons->add(std::move(owned)); + } + const auto wrap = buttons->add( + object_ptr>( + buttons, + std::move(owned))); + wrap->toggle(false, anim::type::instant); + showMore->clicks() | rpl::start_with_next([=] { + wrap->toggle(true, anim::type::normal); + }, wrap->lifetime()); + return wrap->entity(); + }(); const auto text = button->lifetime().make_state( st.style, tr::lng_credits_summary_options_credits( @@ -502,12 +553,16 @@ void FillCreditOptions( const auto apiCredits = content->lifetime().make_state(peer); if (show->session().premiumPossible()) { - apiCredits->request( - ) | rpl::start_with_error_done([=](const QString &error) { - show->showToast(error); - }, [=] { - fill(apiCredits->options()); - }, content->lifetime()); + if (preloadedTopupOptions.empty()) { + apiCredits->request( + ) | rpl::start_with_error_done([=](const QString &error) { + show->showToast(error); + }, [=] { + fill(apiCredits->options()); + }, content->lifetime()); + } else { + fill(std::move(preloadedTopupOptions)); + } } show->session().premiumPossibleValue( @@ -521,29 +576,41 @@ void FillCreditOptions( not_null AddBalanceWidget( not_null parent, rpl::producer balanceValue, - bool rightAlign) { + bool rightAlign, + rpl::producer opacityValue) { + struct State final { + QImage star; + float64 opacity = 1.0; + Ui::Text::String label; + Ui::Text::String count; + }; const auto balance = Ui::CreateChild(parent); - const auto balanceStar = balance->lifetime().make_state( - Ui::GenerateStars(st::creditsBalanceStarHeight, 1)); - const auto starSize = balanceStar->size() / style::DevicePixelRatio(); - const auto label = balance->lifetime().make_state( + const auto state = balance->lifetime().make_state(); + state->star = QImage(Ui::GenerateStars(st::creditsBalanceStarHeight, 1)); + const auto starSize = state->star.size() / style::DevicePixelRatio(); + state->label = Ui::Text::String( st::defaultTextStyle, tr::lng_credits_summary_balance(tr::now)); - const auto count = balance->lifetime().make_state( + state->count = Ui::Text::String( st::semiboldTextStyle, tr::lng_contacts_loading(tr::now)); - const auto diffBetweenStarAndCount = count->style()->font->spacew; + if (opacityValue) { + std::move(opacityValue) | rpl::start_with_next([=](float64 value) { + state->opacity = value; + }, balance->lifetime()); + } + const auto diffBetweenStarAndCount = state->count.style()->font->spacew; const auto resize = [=] { balance->resize( std::max( - label->maxWidth(), - count->maxWidth() + state->label.maxWidth(), + state->count.maxWidth() + starSize.width() + diffBetweenStarAndCount), - label->style()->font->height + starSize.height()); + state->label.style()->font->height + starSize.height()); }; std::move(balanceValue) | rpl::start_with_next([=](uint64 value) { - count->setText( + state->count.setText( st::semiboldTextStyle, Lang::FormatCountToShort(value).string); balance->setBalance(value); @@ -553,32 +620,33 @@ not_null AddBalanceWidget( ) | rpl::start_with_next([=] { auto p = QPainter(balance); + p.setOpacity(state->opacity); p.setPen(st::boxTextFg); - label->draw(p, { + state->label.draw(p, { .position = QPoint( - rightAlign ? (balance->width() - label->maxWidth()) : 0, + rightAlign ? (balance->width() - state->label.maxWidth()) : 0, 0), .availableWidth = balance->width(), }); - count->draw(p, { + state->count.draw(p, { .position = QPoint( (rightAlign - ? (balance->width() - count->maxWidth()) + ? (balance->width() - state->count.maxWidth()) : (starSize.width() + diffBetweenStarAndCount)), - label->minHeight() - + (starSize.height() - count->minHeight()) / 2), + state->label.minHeight() + + (starSize.height() - state->count.minHeight()) / 2), .availableWidth = balance->width(), }); p.drawImage( (rightAlign ? (balance->width() - - count->maxWidth() + - state->count.maxWidth() - starSize.width() - diffBetweenStarAndCount) : 0), - label->minHeight(), - *balanceStar); + state->label.minHeight(), + state->star); }, balance->lifetime()); return balance; } @@ -729,7 +797,7 @@ void ReceiptCreditsBox( const Data::SubscriptionEntry &s) { const auto item = controller->session().data().message( PeerId(e.barePeerId), MsgId(e.bareMsgId)); - const auto isStarGift = (e.convertStars > 0); + const auto isStarGift = e.stargift || e.soldOutInfo; const auto creditsHistoryStarGift = isStarGift && !e.id.isEmpty(); const auto sentStarGift = creditsHistoryStarGift && !e.in; const auto convertedStarGift = creditsHistoryStarGift && e.converted; @@ -739,9 +807,19 @@ void ReceiptCreditsBox( : (isStarGift && e.in) ? controller->session().data().peer(PeerId(e.barePeerId))->asUser() : nullptr; - const auto canConvert = gotStarGift && !e.converted && starGiftSender; + const auto convertLast = base::unixtime::serialize(e.date) + + controller->session().appConfig().stargiftConvertPeriodMax(); + const auto timeLeft = int64(convertLast) - int64(base::unixtime::now()); + const auto timeExceeded = (timeLeft <= 0); + const auto forConvert = gotStarGift + && e.starsConverted + && !e.converted + && starGiftSender; + const auto canConvert = forConvert && !timeExceeded; + const auto couldConvert = forConvert && timeExceeded; + const auto nonConvertible = (gotStarGift && !e.starsConverted); - box->setStyle(canConvert ? st::starGiftBox : st::giveawayGiftCodeBox); + box->setStyle(st::giveawayGiftCodeBox); box->setNoContentMargin(true); const auto content = box->verticalLayout(); @@ -763,6 +841,8 @@ void ReceiptCreditsBox( ? session->data().peer(PeerId(s.barePeerId)).get() : (e.peerType == Type::PremiumBot) ? nullptr + : e.bareActorId + ? session->data().peer(PeerId(e.bareActorId)).get() : e.barePeerId ? session->data().peer(PeerId(e.barePeerId)).get() : nullptr; @@ -772,9 +852,15 @@ void ReceiptCreditsBox( GenericEntryPhoto(content, callback, stUser.photoSize))); AddViewMediaHandler(thumb->entity(), controller, e); } else if (peer && !e.gift) { - content->add(object_ptr>( - content, - object_ptr(content, peer, stUser))); + if (e.subscriptionUntil.isNull() && s.until.isNull()) { + content->add(object_ptr>( + content, + object_ptr(content, peer, stUser))); + } else { + content->add(object_ptr>( + content, + SubscriptionUserpic(content, peer, stUser.photoSize))); + } } else if (e.gift || isPrize) { struct State final { DocumentData *sticker = nullptr; @@ -872,6 +958,8 @@ void ReceiptCreditsBox( ? tr::lng_credits_box_history_entry_subscription(tr::now) : !e.title.isEmpty() ? e.title + : e.soldOutInfo + ? tr::lng_credits_box_history_entry_gift_unavailable(tr::now) : sentStarGift ? tr::lng_credits_box_history_entry_gift_sent(tr::now) : convertedStarGift @@ -887,7 +975,7 @@ void ReceiptCreditsBox( Ui::AddSkip(content); - { + if (!isStarGift || creditsHistoryStarGift || e.soldOutInfo) { constexpr auto kMinus = QChar(0x2212); auto &lifetime = content->lifetime(); const auto text = lifetime.make_state(); @@ -912,7 +1000,11 @@ void ReceiptCreditsBox( .session = session, .customEmojiRepaint = [=] { amount->update(); }, }; - if (s) { + if (e.soldOutInfo) { + text->setText( + st::defaultTextStyle, + tr::lng_credits_box_history_entry_gift_sold_out(tr::now)); + } else if (s) { text->setMarkedText( st::defaultTextStyle, tr::lng_credits_subscription_subtitle( @@ -952,7 +1044,9 @@ void ReceiptCreditsBox( amount->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(amount); - p.setPen(s + p.setPen(e.soldOutInfo + ? st::menuIconAttentionColor + : s ? st::windowSubTextFg : e.pending ? st::creditsStroke @@ -1018,17 +1112,25 @@ void ReceiptCreditsBox( box, object_ptr( box, - rpl::combine( - (canConvert - ? tr::lng_action_gift_got_stars_text - : tr::lng_gift_got_stars)( - lt_count, - rpl::single(e.convertStars * 1.), - Ui::Text::RichLangValue), - tr::lng_paid_about_link() - ) | rpl::map([](TextWithEntities text, QString link) { - return text.append(' ').append(Ui::Text::Link(link)); - }), + ((couldConvert || nonConvertible) + ? (e.savedToProfile + ? tr::lng_action_gift_can_remove_text + : tr::lng_action_gift_got_gift_text)( + Ui::Text::WithEntities) + : rpl::combine( + (canConvert + ? tr::lng_action_gift_got_stars_text + : tr::lng_gift_got_stars)( + lt_count, + rpl::single(e.starsConverted * 1.), + Ui::Text::RichLangValue), + tr::lng_paid_about_link() + ) | rpl::map([]( + TextWithEntities text, + QString link) { + return text.append(' ').append( + Ui::Text::Link(link)); + })), st::creditsBoxAbout)))->entity(); about->setClickHandlerFilter([=](const auto &...) { Core::App().iv().openWithIvPreferred( @@ -1075,8 +1177,62 @@ void ReceiptCreditsBox( Ui::AddSkip(content); Ui::AddSkip(content); + struct State final { + rpl::variable confirmButtonBusy; + rpl::variable convertButtonBusy; + }; + const auto state = box->lifetime().make_state(); + const auto weakWindow = base::make_weak(controller); + if (isStarGift && e.id.isEmpty()) { - AddStarGiftTable(controller, content, e); + const auto convert = [=, weak = Ui::MakeWeak(box)] { + const auto stars = e.starsConverted; + const auto days = canConvert ? ((timeLeft + 86399) / 86400) : 0; + const auto name = starGiftSender->shortName(); + ConfirmConvertStarGift(box->uiShow(), name, stars, days, [=] { + if (state->convertButtonBusy.current() + || state->confirmButtonBusy.current()) { + return; + } + state->convertButtonBusy = true; + const auto window = weakWindow.get(); + const auto itemId = MsgId(e.bareMsgId); + if (window && stars) { + const auto done = [=](bool ok) { + if (const auto window = weakWindow.get()) { + if (ok) { + using GiftAction = Data::GiftUpdate::Action; + window->session().data().notifyGiftUpdate({ + .itemId = FullMsgId( + starGiftSender->id, + itemId), + .action = GiftAction::Convert, + }); + } + } + if (const auto strong = weak.data()) { + if (ok) { + strong->closeBox(); + } else { + state->convertButtonBusy = false; + } + } + }; + ConvertStarGift( + window, + starGiftSender, + itemId, + stars, + done); + } + }); + }; + + AddStarGiftTable( + controller, + content, + e, + canConvert ? convert : Fn()); } else { AddCreditsHistoryEntryTable(controller, content, e); AddSubscriptionEntryTable(controller, content, s); @@ -1115,16 +1271,19 @@ void ReceiptCreditsBox( } if (s) { Ui::AddSkip(content); - box->addRow(object_ptr>( + auto label = object_ptr( box, - object_ptr( - box, - s.cancelled - ? tr::lng_credits_subscription_off_about() - : tr::lng_credits_subscription_on_about( - lt_date, - rpl::single(langDayOfMonthFull(s.until.date()))), - st::creditsBoxAboutDivider))); + s.cancelled + ? tr::lng_credits_subscription_off_about() + : tr::lng_credits_subscription_on_about( + lt_date, + rpl::single(langDayOfMonthFull(s.until.date()))), + st::creditsBoxAboutDivider); + if (s.cancelled) { + label->setTextColorOverride(st::menuIconAttentionColor->c); + } + box->addRow( + object_ptr>(box, std::move(label))); } Ui::AddSkip(content); @@ -1159,11 +1318,6 @@ void ReceiptCreditsBox( const auto toRenew = (s.cancelled || s.expired) && !s.inviteHash.isEmpty(); const auto toCancel = !toRenew && s; - struct State final { - rpl::variable confirmButtonBusy; - rpl::variable convertButtonBusy; - }; - const auto state = box->lifetime().make_state(); auto confirmText = rpl::conditional( state->confirmButtonBusy.value(), rpl::single(QString()), @@ -1171,14 +1325,13 @@ void ReceiptCreditsBox( ? tr::lng_credits_subscription_off_button() : toCancel ? tr::lng_credits_subscription_on_button() - : canConvert + : (canConvert || couldConvert || nonConvertible) ? (e.savedToProfile ? tr::lng_gift_display_on_page_hide() : tr::lng_gift_display_on_page()) : tr::lng_box_ok())); - const auto weakWindow = base::make_weak(controller); const auto send = [=, weak = Ui::MakeWeak(box)] { - if (canConvert) { + if (canConvert || couldConvert || nonConvertible) { const auto save = !e.savedToProfile; const auto window = weakWindow.get(); const auto showSection = !e.fromGiftsList; @@ -1252,7 +1405,12 @@ void ReceiptCreditsBox( return; } state->confirmButtonBusy = true; - if ((toRenew || toCancel || canConvert) && peer) { + if (peer + && (toRenew + || toCancel + || canConvert + || couldConvert + || nonConvertible)) { send(); } else { box->closeBox(); @@ -1274,85 +1432,6 @@ void ReceiptCreditsBox( }) | rpl::start_with_next([=] { button->resizeToWidth(buttonWidth); }, button->lifetime()); - - if (canConvert) { - using namespace Ui; - auto convertText = rpl::conditional( - state->convertButtonBusy.value(), - rpl::single(QString()), - tr::lng_gift_convert_to_stars( - lt_count, - rpl::single(e.convertStars * 1.))); - const auto convert = CreateChild( - button->parentWidget(), - std::move(convertText), - st::defaultLightButton); - convert->setTextTransform(RoundButton::TextTransform::NoTransform); - convert->widthValue() | rpl::filter([=] { - return convert->widthNoMargins() != buttonWidth; - }) | rpl::start_with_next([=] { - convert->resizeToWidth(buttonWidth); - }, convert->lifetime()); - button->positionValue( - ) | rpl::start_with_next([=](QPoint position) { - convert->move( - position.x(), - (position.y() - + st::starGiftBox.buttonHeight - + st::starGiftBox.buttonPadding.bottom() - - st::starGiftBox.buttonPadding.top() - - convert->height())); - }, convert->lifetime()); - convert->setClickedCallback([=, weak = Ui::MakeWeak(box)] { - const auto stars = e.convertStars; - const auto name = starGiftSender->shortName(); - ConfirmConvertStarGift(box->uiShow(), name, stars, [=] { - if (state->convertButtonBusy.current() - || state->confirmButtonBusy.current()) { - return; - } - state->convertButtonBusy = true; - const auto window = weakWindow.get(); - const auto itemId = MsgId(e.bareMsgId); - if (window && stars) { - const auto done = [=](bool ok) { - if (const auto window = weakWindow.get()) { - if (ok) { - using GiftAction = Data::GiftUpdate::Action; - window->session().data().notifyGiftUpdate({ - .itemId = FullMsgId( - starGiftSender->id, - itemId), - .action = GiftAction::Convert, - }); - } - } - if (const auto strong = weak.data()) { - if (ok) { - strong->closeBox(); - } else { - state->convertButtonBusy = false; - } - } - }; - ConvertStarGift( - window, - starGiftSender, - itemId, - stars, - done); - } - }); - }); - - using namespace Info::Statistics; - const auto loadingAnimation = InfiniteRadialAnimationWidget( - convert, - convert->height() / 2, - &st::starConvertButtonLoading); - AddChildToWidgetCenter(convert, loadingAnimation); - loadingAnimation->showOn(state->convertButtonBusy.value()); - } } void GiftedCreditsBox( @@ -1416,18 +1495,17 @@ void UserStarGiftBox( Data::CreditsHistoryEntry{ .description = data.message, .date = base::unixtime::parse(data.date), - .credits = uint64(data.gift.stars), + .credits = uint64(data.info.stars), .bareMsgId = uint64(data.messageId.bare), .barePeerId = data.fromId.value, - .bareGiftStickerId = (data.gift.document - ? data.gift.document->id - : 0), + .bareGiftStickerId = data.info.document->id, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, - .limitedCount = data.gift.limitedCount, - .limitedLeft = data.gift.limitedLeft, - .convertStars = int(data.gift.convertStars), + .limitedCount = data.info.limitedCount, + .limitedLeft = data.info.limitedLeft, + .starsConverted = int(data.info.starsConverted), .converted = false, .anonymous = data.anonymous, + .stargift = true, .savedToProfile = !data.hidden, .fromGiftsList = true, .in = data.mine, @@ -1455,9 +1533,10 @@ void StarGiftViewBox( .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.limitedCount, .limitedLeft = data.limitedLeft, - .convertStars = data.convertStars, + .starsConverted = data.starsConverted, .converted = data.converted, .anonymous = data.anonymous, + .stargift = true, .savedToProfile = data.saved, .in = true, .gift = true, @@ -1541,6 +1620,35 @@ object_ptr PaidMediaThumbnail( photoSize); } +object_ptr SubscriptionUserpic( + not_null parent, + not_null peer, + int photoSize) { + auto widget = object_ptr(parent); + const auto raw = widget.data(); + widget->resize(photoSize, photoSize); + const auto userpicMedia = Ui::MakeUserpicThumbnail(peer, false); + userpicMedia->subscribeToUpdates([=] { raw->update(); }); + const auto creditsIconSize = photoSize / 3; + const auto creditsIconCallback = + Ui::PaintOutlinedColoredCreditsIconCallback( + creditsIconSize, + 1.5); + widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + p.fillRect(Rect(Size(photoSize)), Qt::transparent); + auto image = userpicMedia->image(photoSize); + { + auto q = QPainter(&image); + q.translate(photoSize, photoSize); + q.translate(-creditsIconSize, -creditsIconSize); + creditsIconCallback(q); + } + p.drawImage(0, 0, image); + }, widget->lifetime()); + return widget; +} + void SmallBalanceBox( not_null box, std::shared_ptr show, @@ -1620,7 +1728,9 @@ void SmallBalanceBox( box->verticalLayout(), show->session().user(), credits - show->session().credits().balance(), - [=] { show->session().credits().load(true); }); + [=] { show->session().credits().load(true); }, + tr::lng_credits_summary_options_subtitle(), + {}); content->setMaximumHeight(st::creditsLowBalancePremiumCoverHeight); content->setMinimumHeight(st::infoLayerTopBarHeight); @@ -1811,44 +1921,48 @@ void AddWithdrawalWidget( }, 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); + const auto lockedLabel = Ui::CreateChild(button); + lockedLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + struct LockedState final { + Ui::Text::String text; + bool locked = false; + bool dateIsNull = false; + rpl::lifetime dateUpdateLifetime; + }; + const auto state = lockedLabel->lifetime().make_state(); 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()); + button->sizeValue() + ) | rpl::start_with_next([=](bool locked, const QSize &s) { + state->locked = locked; + lockedLabel->resize(s); + }, lockedLabel->lifetime()); + lockedLabel->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(lockedLabel); + p.setPen(state->locked ? QPen(lockedColor) : stButton.textFg->p); + if (state->dateIsNull) { + p.setFont(st::channelEarnSemiboldLabel.style.font); + p.drawText( + lockedLabel->rect(), + style::al_center, + tr::lng_bot_earn_balance_button_locked(tr::now)); + return; + } + state->text.draw(p, { + .position = QPoint( + 0, + (lockedLabel->height() - state->text.minHeight()) / 2), + .outerWidth = lockedLabel->width(), + .availableWidth = lockedLabel->width(), + .align = style::al_center, + }); + }, lockedLabel->lifetime()); - const auto dateUpdateLifetime - = lockedLabelBottom->lifetime().make_state(); std::move( dateValue ) | rpl::start_with_next([=](const QDateTime &dt) { - dateUpdateLifetime->destroy(); + state->dateUpdateLifetime.destroy(); + state->dateIsNull = dt.isNull(); if (dt.isNull()) { return; } @@ -1857,7 +1971,7 @@ void AddWithdrawalWidget( const auto context = Core::MarkedTextContext{ .session = session, - .customEmojiRepaint = [=] { lockedLabelBottom->update(); }, + .customEmojiRepaint = [=] { lockedLabel->update(); }, }; const auto emoji = Ui::Text::SingleCustomEmoji( session->data().customEmojiManager().registerInternalEmoji( @@ -1885,11 +1999,18 @@ void AddWithdrawalWidget( : (u"%1:%2"_q) .arg(minutes, 2, 10, kZero) .arg(seconds, 2, 10, kZero); - lockedLabelBottom->setMarkedText( - base::duplicate(emoji).append(formatted), + state->text.setMarkedText( + st::botEarnLockedButtonLabel.style, + TextWithEntities() + .append(tr::lng_bot_earn_balance_button_locked(tr::now)) + .append('\n') + .append(emoji) + .append(formatted), + kMarkupTextOptions, context); - }, *dateUpdateLifetime); - }, lockedLabelBottom->lifetime()); + lockedLabel->update(); + }, state->dateUpdateLifetime); + }, lockedLabel->lifetime()); Api::HandleWithdrawalButton( Api::RewardReceiver{ diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index bfc266e3e..608f22cf0 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -21,6 +21,7 @@ struct Boost; struct CreditsHistoryEntry; struct SubscriptionEntry; struct GiftCode; +struct CreditTopupOption; } // namespace Data namespace Main { @@ -58,12 +59,15 @@ void FillCreditOptions( not_null container, not_null peer, int minCredits, - Fn paid); + Fn paid, + rpl::producer subtitle, + std::vector preloadedTopupOptions); [[nodiscard]] not_null AddBalanceWidget( not_null parent, rpl::producer balanceValue, - bool rightAlign); + bool rightAlign, + rpl::producer opacityValue = nullptr); void AddWithdrawalWidget( not_null container, @@ -126,6 +130,11 @@ void ShowRefundInfoBox( int totalCount, int photoSize); +[[nodiscard]] object_ptr SubscriptionUserpic( + not_null parent, + not_null peer, + int photoSize); + struct SmallBalanceBot { UserId botId = 0; }; diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 7d9c3707a..552215041 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" @@ -843,6 +844,43 @@ void SetupTopContent( } +void SetupView( + not_null controller, + not_null content) { + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + wrap->toggleOn(controller->enoughSpaceForFiltersValue()); + content = wrap->entity(); + + Ui::AddDivider(content); + Ui::AddSkip(content); + Ui::AddSubsectionTitle(content, tr::lng_filters_view_subtitle()); + + const auto group = std::make_shared>( + Core::App().settings().chatFiltersHorizontal()); + const auto addSend = [&](bool value, const QString &text) { + content->add( + object_ptr>( + content, + group, + value, + text, + st::settingsSendType), + st::settingsSendTypePadding); + }; + addSend(false, tr::lng_filters_vertical(tr::now)); + addSend(true, tr::lng_filters_horizontal(tr::now)); + + group->setChangedCallback([=](bool value) { + Core::App().settings().setChatFiltersHorizontal(value); + Core::App().saveSettingsDelayed(); + }); + Ui::AddSkip(content); + Ui::AddSkip(content); +} + } // namespace Folders::Folders( @@ -871,6 +909,8 @@ void Folders::setupContent(not_null controller) { _save = SetupFoldersContent(controller, content); + SetupView(controller, content); + Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 041ce1c14..7d0c806c2 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/popup_menu.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/new_badges.h" @@ -149,6 +150,20 @@ Cover::Cover( _phone->setSelectable(true); _phone->setContextCopyText(tr::ayu_ContextCopyID(tr::now)); + const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) { + if (request.selection.empty()) { + const auto c = [=] { + auto phone = rpl::variable( + Info::Profile::PhoneValue(_user)).current().text; + phone.replace(' ', QString()).replace('-', QString()); + TextUtilities::SetClipboardText({ phone }); + }; + request.menu->addAction(tr::lng_profile_copy_phone(tr::now), c); + } else { + _phone->fillContextMenu(request); + } + }; + _phone->setContextMenuHook(hook); initViewers(); setupChildGeometry(); diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index e652b5e96..2361e08bc 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -890,6 +890,27 @@ NotifyViewCheckboxes SetupNotifyViewOptions( void SetupAdvancedNotifications( not_null controller, not_null container) { + if (Platform::IsWindows()) { + const auto skipInFocus = container->add(object_ptr