diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8c04e71228..24fbcb9080 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Clone. - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive diff --git a/.gitignore b/.gitignore index 3db1a60aed..0ae30de9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,16 @@ stage .idea/ cmake-build-debug/ +# Local configuration files +settings.local.json +*.local.json +.env +.env.local +.env.*.local + +# Cursor IDE local settings (but keep .cursor/rules/) +.cursor/* +!.cursor/rules/ + # Project specific Telegram/SourceFiles/_other/packer_private.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e25ce766d1..be68112aec 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -659,6 +659,8 @@ PRIVATE data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp data/notify/data_peer_notify_settings.h + data/notify/data_peer_notify_volume.cpp + data/notify/data_peer_notify_volume.h data/stickers/data_custom_emoji.cpp data/stickers/data_custom_emoji.h data/stickers/data_stickers_set.cpp @@ -748,6 +750,7 @@ PRIVATE data/data_peer_bot_command.h data/data_peer_bot_commands.cpp data/data_peer_bot_commands.h + data/data_peer_common.h data/data_peer_id.cpp data/data_peer_id.h data/data_peer_values.cpp @@ -769,6 +772,8 @@ PRIVATE data/data_report.h data/data_saved_messages.cpp data/data_saved_messages.h + data/data_saved_music.cpp + data/data_saved_music.h data/data_saved_sublist.cpp data/data_saved_sublist.h data/data_search_controller.cpp @@ -781,6 +786,7 @@ PRIVATE data/data_shared_media.h data/data_sparse_ids.cpp data/data_sparse_ids.h + data/data_star_gift.cpp data/data_star_gift.h data/data_statistics.h data/data_stories.cpp @@ -843,6 +849,8 @@ PRIVATE dialogs/dialogs_search_from_controllers.h dialogs/dialogs_search_tags.cpp dialogs/dialogs_search_tags.h + dialogs/dialogs_search_posts.cpp + dialogs/dialogs_search_posts.h dialogs/dialogs_top_bar_suggestion.cpp dialogs/dialogs_top_bar_suggestion.h dialogs/dialogs_widget.cpp @@ -945,6 +953,8 @@ PRIVATE history/view/media/history_view_poll.h history/view/media/history_view_premium_gift.cpp history/view/media/history_view_premium_gift.h + history/view/media/history_view_save_document_action.cpp + history/view/media/history_view_save_document_action.h history/view/media/history_view_service_box.cpp history/view/media/history_view_service_box.h history/view/media/history_view_similar_channels.cpp @@ -1008,6 +1018,8 @@ PRIVATE history/view/history_view_fake_items.h history/view/history_view_group_call_bar.cpp history/view/history_view_group_call_bar.h + history/view/history_view_group_members_widget.cpp + history/view/history_view_group_members_widget.h history/view/history_view_item_preview.h history/view/history_view_list_widget.cpp history/view/history_view_list_widget.h @@ -1142,6 +1154,8 @@ PRIVATE info/media/info_media_widget.h info/members/info_members_widget.cpp info/members/info_members_widget.h + info/peer_gifts/info_peer_gifts_collections.cpp + info/peer_gifts/info_peer_gifts_collections.h info/peer_gifts/info_peer_gifts_common.cpp info/peer_gifts/info_peer_gifts_common.h info/peer_gifts/info_peer_gifts_widget.cpp @@ -1176,6 +1190,11 @@ PRIVATE 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_music_common.h + info/saved/info_saved_music_provider.cpp + info/saved/info_saved_music_provider.h + info/saved/info_saved_music_widget.cpp + info/saved/info_saved_music_widget.h info/saved/info_saved_sublists_widget.cpp info/saved/info_saved_sublists_widget.h info/settings/info_settings_widget.cpp @@ -1192,6 +1211,9 @@ PRIVATE info/statistics/info_statistics_tag.h info/statistics/info_statistics_widget.cpp info/statistics/info_statistics_widget.h + info/stories/info_stories_albums.cpp + info/stories/info_stories_albums.h + info/stories/info_stories_common.h info/stories/info_stories_inner_widget.cpp info/stories/info_stories_inner_widget.h info/stories/info_stories_provider.cpp @@ -1246,6 +1268,8 @@ PRIVATE inline_bots/inline_results_widget.h intro/intro_code.cpp intro/intro_code.h + intro/intro_email.cpp + intro/intro_email.h intro/intro_password_check.cpp intro/intro_password_check.h intro/intro_phone.cpp @@ -1399,6 +1423,8 @@ PRIVATE menu/menu_antispam_validator.h menu/menu_item_download_files.cpp menu/menu_item_download_files.h + menu/menu_item_rate_transcribe_session.cpp + menu/menu_item_rate_transcribe_session.h menu/menu_mute.cpp menu/menu_mute.h menu/menu_send.cpp @@ -1432,6 +1458,8 @@ PRIVATE mtproto/special_config_request.cpp mtproto/special_config_request.h mtproto/type_utils.h + overview/overview_checkbox.cpp + overview/overview_checkbox.h overview/overview_layout.cpp overview/overview_layout.h overview/overview_layout_delegate.h @@ -1552,10 +1580,6 @@ PRIVATE platform/platform_window_title.h profile/profile_back_button.cpp profile/profile_back_button.h - profile/profile_block_group_members.cpp - profile/profile_block_group_members.h - profile/profile_block_peer_list.cpp - profile/profile_block_peer_list.h profile/profile_block_widget.cpp profile/profile_block_widget.h profile/profile_cover_drop_area.cpp @@ -1750,8 +1774,6 @@ PRIVATE ui/text/format_song_document_name.h ui/widgets/expandable_peer_list.cpp 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/widgets/peer_bubble.cpp @@ -1784,6 +1806,8 @@ PRIVATE window/window_adaptive.h window/window_chat_preview.cpp window/window_chat_preview.h + window/window_chat_switch_process.cpp + window/window_chat_switch_process.h window/window_connecting_widget.cpp window/window_connecting_widget.h window/window_controller.cpp diff --git a/Telegram/Resources/animations/transcribe_loading.tgs b/Telegram/Resources/animations/transcribe_loading.tgs new file mode 100644 index 0000000000..f63b6e9a02 Binary files /dev/null and b/Telegram/Resources/animations/transcribe_loading.tgs differ diff --git a/Telegram/Resources/icons/chat/mini_ton_bold.svg b/Telegram/Resources/icons/chat/mini_ton_bold.svg new file mode 100644 index 0000000000..64dc4fa576 --- /dev/null +++ b/Telegram/Resources/icons/chat/mini_ton_bold.svg @@ -0,0 +1,7 @@ + + + Icon / Mini / mini_ton_bold + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level10_inner.svg b/Telegram/Resources/icons/levels/level10_inner.svg new file mode 100644 index 0000000000..8ce8b2c9b2 --- /dev/null +++ b/Telegram/Resources/icons/levels/level10_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level10_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level1_inner.svg b/Telegram/Resources/icons/levels/level1_inner.svg new file mode 100644 index 0000000000..37959bcb5d --- /dev/null +++ b/Telegram/Resources/icons/levels/level1_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level1_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level20_inner.svg b/Telegram/Resources/icons/levels/level20_inner.svg new file mode 100644 index 0000000000..055e99e5c3 --- /dev/null +++ b/Telegram/Resources/icons/levels/level20_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level20_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level2_inner.svg b/Telegram/Resources/icons/levels/level2_inner.svg new file mode 100644 index 0000000000..2cd67761cc --- /dev/null +++ b/Telegram/Resources/icons/levels/level2_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level2_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level30_inner.svg b/Telegram/Resources/icons/levels/level30_inner.svg new file mode 100644 index 0000000000..b74a2e6efb --- /dev/null +++ b/Telegram/Resources/icons/levels/level30_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level30_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level3_inner.svg b/Telegram/Resources/icons/levels/level3_inner.svg new file mode 100644 index 0000000000..3bc0ef3505 --- /dev/null +++ b/Telegram/Resources/icons/levels/level3_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level3_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level40_inner.svg b/Telegram/Resources/icons/levels/level40_inner.svg new file mode 100644 index 0000000000..f7fef2c167 --- /dev/null +++ b/Telegram/Resources/icons/levels/level40_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level40_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level4_inner.svg b/Telegram/Resources/icons/levels/level4_inner.svg new file mode 100644 index 0000000000..b23971ce55 --- /dev/null +++ b/Telegram/Resources/icons/levels/level4_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level4_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level50_inner.svg b/Telegram/Resources/icons/levels/level50_inner.svg new file mode 100644 index 0000000000..23aa9e3107 --- /dev/null +++ b/Telegram/Resources/icons/levels/level50_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level50_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level5_inner.svg b/Telegram/Resources/icons/levels/level5_inner.svg new file mode 100644 index 0000000000..e28cd650ab --- /dev/null +++ b/Telegram/Resources/icons/levels/level5_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level5_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level60_inner.svg b/Telegram/Resources/icons/levels/level60_inner.svg new file mode 100644 index 0000000000..b006ec3280 --- /dev/null +++ b/Telegram/Resources/icons/levels/level60_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level60_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level6_inner.svg b/Telegram/Resources/icons/levels/level6_inner.svg new file mode 100644 index 0000000000..1b8a68d0da --- /dev/null +++ b/Telegram/Resources/icons/levels/level6_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level6_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level70_inner.svg b/Telegram/Resources/icons/levels/level70_inner.svg new file mode 100644 index 0000000000..d6bc1b8b4a --- /dev/null +++ b/Telegram/Resources/icons/levels/level70_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level70_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level7_inner.svg b/Telegram/Resources/icons/levels/level7_inner.svg new file mode 100644 index 0000000000..d0a0c79299 --- /dev/null +++ b/Telegram/Resources/icons/levels/level7_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level7_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level80_inner.svg b/Telegram/Resources/icons/levels/level80_inner.svg new file mode 100644 index 0000000000..a469b74988 --- /dev/null +++ b/Telegram/Resources/icons/levels/level80_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level80_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level8_inner.svg b/Telegram/Resources/icons/levels/level8_inner.svg new file mode 100644 index 0000000000..f4a0d3b7d1 --- /dev/null +++ b/Telegram/Resources/icons/levels/level8_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level8_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level90_inner.svg b/Telegram/Resources/icons/levels/level90_inner.svg new file mode 100644 index 0000000000..d25a731d93 --- /dev/null +++ b/Telegram/Resources/icons/levels/level90_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level90_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level9_inner.svg b/Telegram/Resources/icons/levels/level9_inner.svg new file mode 100644 index 0000000000..27fe8c25a2 --- /dev/null +++ b/Telegram/Resources/icons/levels/level9_inner.svg @@ -0,0 +1,7 @@ + + + Badge / level9_inner + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/levels/level_warning.svg b/Telegram/Resources/icons/levels/level_warning.svg new file mode 100644 index 0000000000..cba1db0076 --- /dev/null +++ b/Telegram/Resources/icons/levels/level_warning.svg @@ -0,0 +1,7 @@ + + + Icon / Filled / Folder / filled_warning + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/limits/filled_rating_crown.svg b/Telegram/Resources/icons/limits/filled_rating_crown.svg new file mode 100644 index 0000000000..e7cc2e124f --- /dev/null +++ b/Telegram/Resources/icons/limits/filled_rating_crown.svg @@ -0,0 +1,7 @@ + + + Icon / Filled / Folder / filled_rating_crown + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/limits/filled_understood.svg b/Telegram/Resources/icons/limits/filled_understood.svg new file mode 100644 index 0000000000..337b43cc71 --- /dev/null +++ b/Telegram/Resources/icons/limits/filled_understood.svg @@ -0,0 +1,7 @@ + + + Icon / Filled / Folder / filled_understood + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/limits/mini_gift_lock.svg b/Telegram/Resources/icons/limits/mini_gift_lock.svg new file mode 100644 index 0000000000..2887a84d4c --- /dev/null +++ b/Telegram/Resources/icons/limits/mini_gift_lock.svg @@ -0,0 +1,7 @@ + + + Mini / mini_gift_lock + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/rating_gifts.svg b/Telegram/Resources/icons/menu/rating_gifts.svg new file mode 100644 index 0000000000..ed3aa9d4b1 --- /dev/null +++ b/Telegram/Resources/icons/menu/rating_gifts.svg @@ -0,0 +1,11 @@ + + + Icon / Menu / rating_gifts + + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/rating_refund.svg b/Telegram/Resources/icons/menu/rating_refund.svg new file mode 100644 index 0000000000..05434da66a --- /dev/null +++ b/Telegram/Resources/icons/menu/rating_refund.svg @@ -0,0 +1,9 @@ + + + Icon / Menu / rating_refund + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/users_stars.svg b/Telegram/Resources/icons/menu/users_stars.svg new file mode 100644 index 0000000000..0a068f774a --- /dev/null +++ b/Telegram/Resources/icons/menu/users_stars.svg @@ -0,0 +1,10 @@ + + + Icon / Menu / users_stars + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/payments/ton_emoji.svg b/Telegram/Resources/icons/payments/ton_emoji.svg new file mode 100644 index 0000000000..7de741c10e --- /dev/null +++ b/Telegram/Resources/icons/payments/ton_emoji.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/Telegram/Resources/icons/settings/filled_verify_age.svg b/Telegram/Resources/icons/settings/filled_verify_age.svg new file mode 100644 index 0000000000..2b13be56ca --- /dev/null +++ b/Telegram/Resources/icons/settings/filled_verify_age.svg @@ -0,0 +1,12 @@ + + + Filled / filled_verify_age + + + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/cloud_lang.strings b/Telegram/Resources/langs/cloud_lang.strings index c7e33531c0..3776c023a4 100644 --- a/Telegram/Resources/langs/cloud_lang.strings +++ b/Telegram/Resources/langs/cloud_lang.strings @@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "cloud_lng_topup_purpose_subs" = "Buy **Stars** to keep your channel subscriptions."; +"cloud_lng_age_verify_about_gb#one" = "To access such content, you must confirm that you are at least **{count}** year old as required by UK law."; +"cloud_lng_age_verify_about_gb#other" = "To access such content, you must confirm that you are at least **{count}** years old as required by UK law."; + "cloud_lng_passport_in_ar" = "Arabic"; "cloud_lng_passport_in_az" = "Azerbaijani"; "cloud_lng_passport_in_bg" = "Bulgarian"; diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 17adb92724..9d59b42ca8 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -377,6 +377,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_intro_fragment_about" = "Get the code for {phone_number} in the Anonymous Numbers section on Fragment."; "lng_intro_fragment_button" = "Open Fragment"; +"lng_intro_email_setup_title" = "Choose a login email"; +"lng_intro_email_confirm_subtitle" = "Please check your email {email} (don't forget the spam folder) and enter the code we just sent you."; + "lng_phone_title" = "Your Phone Number"; "lng_phone_desc" = "Please confirm your country code\nand enter your phone number."; "lng_phone_to_qr" = "Quick log in using QR code"; @@ -512,6 +515,7 @@ 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_master_volume_notifications" = "Volume"; "lng_settings_native_title" = "System integration"; "lng_settings_use_windows" = "Use Windows notifications"; "lng_settings_skip_in_focus" = "Respect system Focus mode"; @@ -552,12 +556,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_notification_title_private_chats" = "Notifications for private chats"; "lng_notification_about_private_chats#one" = "Please note that **{count} chat** is listed as an exception and won't be affected by this change."; "lng_notification_about_private_chats#other" = "Please note that **{count} chats** are listed as exceptions and won't be affected by this change."; +"lng_notification_volume_private_chats" = "Notifications volume for private chats"; "lng_notification_title_groups" = "Notifications for groups"; "lng_notification_about_groups#one" = "Please note that **{count} group** is listed as an exception and won't be affected by this change."; "lng_notification_about_groups#other" = "Please note that **{count} groups** are listed as exceptions and won't be affected by this change."; +"lng_notification_volume_groups" = "Notifications volume for groups"; "lng_notification_title_channels" = "Notifications for channels"; "lng_notification_about_channels#one" = "Please note that **{count} channel** is listed as an exception and won't be affected by this change."; "lng_notification_about_channels#other" = "Please note that **{count} channels** are listed as exceptions and won't be affected by this change."; +"lng_notification_volume_channel" = "Notifications volume for channels"; "lng_notification_exceptions_view" = "View exceptions"; "lng_notification_enable" = "Enable notifications"; "lng_notification_sound" = "Sound"; @@ -687,6 +694,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_shortcuts_media_fullscreen" = "Toggle video fullscreen"; "lng_shortcuts_show_chat_menu" = "Show chat menu"; "lng_shortcuts_show_chat_preview" = "Show chat preview"; +"lng_shortcuts_record_voice_message" = "Record Voice Message"; +"lng_shortcuts_record_round_message" = "Record Round Message"; "lng_settings_chat_reactions_title" = "Quick Reaction"; "lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; @@ -1238,6 +1247,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_quick_dialog_action_toast_archive_success" = "The chat has been archived."; "lng_quick_dialog_action_toast_unarchive_success" = "The chat has been unarchived."; +"lng_archive_hint_title" = "This is your Archive"; +"lng_archive_hint_about" = "Archived chats will remain in the Archive when you receive a new message. {link}"; +"lng_archive_hint_about_unmuted" = "When you receive a new message, muted chats will remain in the Archive, while unmuted chats will be moved to Chats. {link}"; +"lng_archive_hint_about_link" = "Tap to change {emoji}"; +"lng_archive_hint_section_1" = "Archived Chats"; +"lng_archive_hint_section_1_info" = "Move any chat into your Archive and back by swiping on it."; +"lng_archive_hint_section_2" = "Hiding Archive"; +"lng_archive_hint_section_2_info" = "Hide the Archive from your Main screen by swiping on it."; +"lng_archive_hint_section_3" = "Stories"; +"lng_archive_hint_section_3_info" = "Archive Stories from your contacts separately from chats with them."; +"lng_archive_hint_button" = "Got it"; + "lng_settings_generic_subscribe" = "Subscribe to {link} to use this setting."; "lng_settings_generic_subscribe_link" = "Telegram Premium"; @@ -1638,6 +1659,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_media_type_audios" = "Voice messages"; "lng_media_type_links" = "Shared links"; "lng_media_type_rounds" = "Video messages"; +"lng_media_saved_music_your" = "Your playlist"; +"lng_media_saved_music_title" = "Playlist"; "lng_profile_common_groups_section" = "Groups in common"; "lng_info_edit_contact" = "Edit contact"; "lng_info_delete_contact" = "Delete contact"; @@ -1876,6 +1899,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_star_ref_revoked_title" = "Link removed"; "lng_star_ref_revoked_text" = "It will no longer work."; +"lng_stars_rating_title" = "Rating"; +"lng_stars_rating_future" = "Future Rating"; +"lng_stars_rating_updates#one" = "in {count} day"; +"lng_stars_rating_updates#other" = "in {count} days"; +"lng_stars_rating_pending#one" = "The rating will update {when}.\n{count} point is pending. {link}"; +"lng_stars_rating_pending#other" = "The rating will update {when}.\n{count} points are pending. {link}"; +"lng_stars_rating_pending_preview" = "Preview {arrow}"; +"lng_stars_rating_pending_back" = "Back {arrow}"; +"lng_stars_rating_negative_label" = "Negative Rating"; +"lng_stars_rating_negative" = "A negative rating indicates that **{name}'s** payments are unreliable."; +"lng_stars_rating_negative_your#one" = "A negative rating indicates that your payments are unreliable. Spend **{count} Star** to fix this issue."; +"lng_stars_rating_negative_your#other" = "A negative rating indicates that your payments are unreliable. Spend **{count} Stars** to fix this issue."; +"lng_stars_rating_about" = "This rating reflects **{name}'s** activity on Telegram. What affects it:"; +"lng_stars_rating_about_your" = "This rating reflects your activity on Telegram. What affects it:"; +"lng_stars_title_gifts_telegram" = "Gifts from Telegram"; +"lng_stars_about_gifts_telegram" = "{emoji} 100% of the Stars spent on gifts purchased from Telegram."; +"lng_stars_title_gifts_users" = "Gifts and Posts from Users"; +"lng_stars_about_gifts_users" = "{emoji} 20% of the Stars spent on resold gifts, paid messages and channel posts."; +"lng_stars_title_refunds" = "Refunds and Conversions"; +"lng_stars_about_refunds" = "{emoji} 10x of refunded Stars and 85% of bought gifts converted to Stars."; +"lng_stars_rating_added" = "Added"; +"lng_stars_rating_deducted" = "Deducted"; +"lng_stars_rating_understood" = "Understood"; + "lng_manage_discussion_group" = "Discussion"; "lng_manage_discussion_group_add" = "Add a group"; "lng_manage_linked_channel" = "Linked channel"; @@ -1903,6 +1950,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_monoforum_price" = "Price for each message"; "lng_manage_monoforum_about" = "Allow users to send messages to your channel, with the option to charge a fee for each message."; "lng_manage_monoforum_price_about" = "Your channel will receive {percent} of the selected fee ({amount}) for each incoming message."; +"lng_manage_monoforum_link_subtitle" = "Link to direct messages"; "lng_manage_history_visibility_title" = "Chat history for new members"; "lng_manage_history_visibility_shown" = "Visible"; @@ -2031,6 +2079,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sure_delete_contact" = "Are you sure you want to delete {contact} from your contact list?"; "lng_sure_delete_history" = "Are you sure you want to delete all message history with {contact}?\n\nThis action cannot be undone."; "lng_sure_delete_group_history" = "Are you sure you want to delete all messages in \"{group}\"?\n\nThis action cannot be undone."; +"lng_sure_delete_channel_history" = "Are you sure you want to delete all messages in \"{channel}\"?\n\n**This action cannot be undone.**"; "lng_sure_delete_and_exit" = "Are you sure you want to delete all message history and leave «{group}»?\n\nThis action cannot be undone."; "lng_sure_leave_channel" = "Are you sure you want to leave\nthis channel?"; "lng_sure_delete_channel" = "Are you sure you want to delete this channel? All subscribers will be removed and all messages will be lost."; @@ -2143,6 +2192,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_you_proximity_reached" = "You are now within {distance} from {user}"; "lng_action_you_theme_changed" = "You changed the chat theme to {emoji}"; "lng_action_theme_changed" = "{from} changed the chat theme to {emoji}"; +"lng_action_you_gift_theme_changed" = "You set {name} as a new theme for this chat."; +"lng_action_gift_theme_changed" = "{from} set {name} as a new theme for this chat."; "lng_action_you_theme_disabled" = "You disabled the chat theme"; "lng_action_theme_disabled" = "{from} disabled the chat theme"; "lng_action_proximity_distance_m#one" = "{count} meter"; @@ -2159,6 +2210,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_upgraded_self_channel" = "You turned this gift to {channel} into a unique collectible"; "lng_action_gift_upgraded_mine" = "You turned the gift from {user} into a unique collectible"; "lng_action_gift_upgraded_self" = "You turned this gift into a unique collectible"; +"lng_action_gift_sent_upgrade_other" = "{from} sent an upgrade worth {cost} for the gift you received from {user}."; +"lng_action_gift_sent_upgrade_self_other" = "You sent an upgrade worth {cost} for the gift {name} received from {user}."; +"lng_action_gift_sent_upgrade" = "{from} sent an upgrade worth {cost} for your gift."; +"lng_action_gift_sent_upgrade_self" = "You sent an upgrade worth {cost} for this gift."; +"lng_action_gift_sent_upgrade_self_channel" = "You sent an upgrade worth {cost} for your gift to {name}."; +"lng_action_gift_upgraded_helped" = "{user} unpacked the gift that you helped to upgrade."; +"lng_action_gift_upgraded_helped_self" = "You unpacked the gift that {user} helped to upgrade."; "lng_action_gift_transferred" = "{user} transferred you a gift"; "lng_action_gift_transferred_channel" = "{user} transferred a gift to {channel}"; "lng_action_gift_transferred_unknown" = "Someone transferred you a gift"; @@ -2179,6 +2237,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_gift_channel_about_unique" = "You can display this gift in channel's Gifts or turn it into unique collectible."; "lng_action_gift_for_stars#one" = "{count} Star"; "lng_action_gift_for_stars#other" = "{count} Stars"; +"lng_action_gift_for_ton#one" = "{count} TON"; +"lng_action_gift_for_ton#other" = "{count} TON"; "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."; @@ -2313,6 +2373,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_peer_gifts_filter_by_value" = "Sort by Value"; "lng_peer_gifts_filter_by_date" = "Sort by Date"; "lng_peer_gifts_filter_unlimited" = "Unlimited"; +"lng_peer_gifts_filter_upgradable" = "Upgradeable"; "lng_peer_gifts_filter_limited" = "Limited"; "lng_peer_gifts_filter_unique" = "Unique"; "lng_peer_gifts_filter_saved" = "Displayed"; @@ -2589,6 +2650,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_unlock_emoji" = "Unlock Animated Emoji"; "lng_premium_unlock_status" = "Unlock Emoji Status"; +"lng_premium_subscribe_months_24" = "2-Year"; "lng_premium_subscribe_months_12" = "Annual"; "lng_premium_subscribe_months_6" = "Semiannual"; "lng_premium_subscribe_months_1" = "Monthly"; @@ -2875,6 +2937,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_ads" = "Ads Platform"; "lng_credits_box_history_entry_premium_bot" = "Stars Top-Up"; "lng_credits_box_history_entry_currency_in" = "TON Top-Up"; +"lng_credits_box_history_entry_posts_search" = "Posts Search"; "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"; @@ -2939,6 +3002,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}."; "lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages."; "lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}."; +"lng_credits_small_balance_for_search" = "Buy **Stars** to search through public posts."; "lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; "lng_credits_enough" = "You have enough stars at the moment. {link}"; @@ -3508,6 +3572,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_ton_amount#one" = "{count} TON"; "lng_gift_ton_amount#other" = "{count} TON"; +"lng_gift_premium_title" = "Premium Gift"; +"lng_gift_premium_text#one" = "Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features."; +"lng_gift_premium_text#other" = "Subscribe to **Telegram Premium** to send up to **{count}** of these gifts and unlock access to multiple additional features."; "lng_gift_premium_or_stars" = "Gift Premium or Stars"; "lng_gift_premium_subtitle" = "Gift Premium"; "lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}"; @@ -3521,6 +3588,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_resale" = "resale"; "lng_gift_stars_on_sale" = "on sale"; +"lng_gift_stars_premium" = "premium"; "lng_gift_stars_tabs_all" = "All Gifts"; "lng_gift_stars_tabs_my" = "My Gifts"; "lng_gift_stars_tabs_limited" = "Limited"; @@ -3549,6 +3617,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_send_button_self" = "Buy a Gift for {cost}"; "lng_gift_buy_resale_title" = "Buy {name}"; "lng_gift_buy_resale_button" = "Buy for {cost}"; +"lng_gift_buy_resale_equals" = "Equals to {cost}"; +"lng_gift_buy_resale_only_ton" = "The seller only accepts TON as payment."; +"lng_gift_buy_resale_pay_stars" = "Pay in Stars"; +"lng_gift_buy_resale_pay_ton" = "Pay in TON"; "lng_gift_buy_resale_confirm" = "Do you want to buy {name} for {price} and gift it to {user}?"; "lng_gift_buy_resale_confirm_self" = "Do you want to buy {name} for {price}?"; "lng_gift_buy_price_change_title" = "Price change!"; @@ -3558,6 +3630,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sent_resale_done_self" = "{gift} is now yours."; "lng_gift_sent_about#one" = "You spent **{count}** Star from your balance."; "lng_gift_sent_about#other" = "You spent **{count}** Stars from your balance."; +"lng_gift_sent_finished#one" = "You've already sent **{count}** of these gifts, and it's the limit."; +"lng_gift_sent_finished#other" = "You've already sent **{count}** of these gifts, and it's the limit."; +"lng_gift_sent_remains#one" = "You can send **{count}** more."; +"lng_gift_sent_remains#other" = "You can send **{count}** more."; "lng_gift_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; "lng_gift_collectible_tag" = "gift"; @@ -3570,8 +3646,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_hidden_hint_channel" = "This gift is hidden from visitors of your channel."; "lng_gift_visible_hint_channel" = "This gift is visible in your channel's Gifts."; "lng_gift_in_blockchain" = "This gift is in TON blockchain. {link}"; -"lng_gift_in_blockchain_link" = "View >"; -"lng_gift_visible_hide" = "Hide >"; +"lng_gift_in_blockchain_link_arrow" = "View {arrow}"; +"lng_gift_visible_hide_arrow" = "Hide {arrow}"; +"lng_gift_visible_show_arrow" = "Show {arrow}"; "lng_gift_show_on_page" = "Display on my Page"; "lng_gift_show_on_channel" = "Display in channel's Gifts"; "lng_gift_availability" = "Availability"; @@ -3590,7 +3667,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_unique_address_copied" = "Address copied to clipboard."; "lng_gift_unique_status" = "Status"; "lng_gift_unique_status_non" = "Non-Unique"; -"lng_gift_unique_status_upgrade" = "upgrade"; +"lng_gift_unique_upgrade" = "Upgrade"; +"lng_gift_unique_upgrade_next" = "Upgrade Next Gift"; +"lng_gift_unique_gift_upgrade" = "Gift an Upgrade"; "lng_gift_unique_number" = "Collectible #{index}"; "lng_gift_unique_number_by" = "Collectible #{index} by {name}"; "lng_gift_unique_model" = "Model"; @@ -3600,6 +3679,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_unique_availability_label" = "Quantity"; "lng_gift_unique_availability#one" = "{count} of {amount} issued"; "lng_gift_unique_availability#other" = "{count} of {amount} issued"; +"lng_gift_unique_value" = "Value"; +"lng_gift_unique_value_learn_more" = "learn more"; "lng_gift_unique_info" = "Gifted to {recipient} on {date}."; "lng_gift_unique_info_sender" = "Gifted by {from} to {recipient} on {date}."; "lng_gift_unique_info_sender_comment" = "Gifted by {from} to {recipient} on {date} with the comment \"{text}\"."; @@ -3608,6 +3689,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_availability_left#one" = "{count} of {amount} left"; "lng_gift_availability_left#other" = "{count} of {amount} left"; "lng_gift_availability_none" = "None of {amount} left"; +"lng_gift_value_about_average" = "This is the average sale price of {gift} gifts on Telegram and Fragment over the past month."; +"lng_gift_value_about_last" = "This is the price at which {gift} was last sold on {platform}."; +"lng_gift_value_initial_sale" = "Initial Sale"; +"lng_gift_value_initial_price" = "Initial Price"; +"lng_gift_value_initial_price_value" = "{stars} ({amount})"; +"lng_gift_value_last_sale" = "Last Sale"; +"lng_gift_value_last_price" = "Last Price"; +"lng_gift_value_minimum_price" = "Minimum Price"; +"lng_gift_value_minimum_price_tooltip" = "{amount} is the floor price for {gift} gifts listed on Telegram and Fragment."; +"lng_gift_vlaue_average_price" = "Average Price"; +"lng_gift_value_average_price_tooltip" = "{amount} is the average sale price of {gift} gifts on Telegram and Fragment over the past month."; +"lng_gift_value_availability#one" = "{count} {emoji} for sale on {platform} {arrow}"; +"lng_gift_value_availability#other" = "{count} {emoji} for sale on {platform} {arrow}"; +"lng_gift_value_telegram" = "Telegram"; +"lng_gift_value_fragment" = "Fragment"; "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"; @@ -3643,10 +3739,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_upgrade_preview_about_channel" = "Let the admins of {name} turn your gift into a unique collectible."; "lng_gift_upgrade_unique_title" = "Unique"; "lng_gift_upgrade_unique_about" = "Get a unique number, model, backdrop and symbol for your gift."; +"lng_gift_upgrade_unique_about_user" = "{name} will get a unique number, model, backdrop and symbol for your gift."; +"lng_gift_upgrade_unique_about_channel" = "Admins of {name} will get a unique number, model, backdrop and symbol for your gift."; "lng_gift_upgrade_transferable_title" = "Transferable"; "lng_gift_upgrade_transferable_about" = "Send your upgraded gift to any of your friends on Telegram."; +"lng_gift_upgrade_transferable_about_user" = "{name} will be able to send the gift to anyone on Telegram."; +"lng_gift_upgrade_transferable_about_channel" = "Admins of {name} will be able to send the gift to anyone on Telegram."; "lng_gift_upgrade_tradable_title" = "Tradable"; "lng_gift_upgrade_tradable_about" = "Sell or auction your gift on third-party NFT marketplaces."; +"lng_gift_upgrade_tradable_about_user" = "{name} will be able to sell the gift on Telegram and NFT marketplaces."; +"lng_gift_upgrade_tradable_about_channel" = "Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces."; "lng_gift_upgrade_button" = "Upgrade for {price}"; "lng_gift_upgrade_free" = "Upgrade for Free"; "lng_gift_upgrade_confirm" = "Confirm"; @@ -3656,6 +3758,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_upgrade_add_comment" = "Add sender's name and comment"; "lng_gift_upgraded_title" = "Gift Upgraded"; "lng_gift_upgraded_about" = "Your gift {name} now has unique attributes and can be transferred to others"; +"lng_gift_upgrade_gifted_title" = "Upgrade Gifted"; +"lng_gift_upgrade_gifted_about" = "Now {name} can turn your gift into a unique collectible."; +"lng_gift_upgrade_gifted_about_channel" = "Now the admins of {name} can turn your gift into a unique collectible."; "lng_gift_transferred_title" = "Gift Transferred"; "lng_gift_transferred_about" = "{name} was successfully transferred to {recipient}."; "lng_gift_transfer_title" = "Transfer {name}"; @@ -3682,6 +3787,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_transfer_sure_for" = "Do you want to transfer ownership of {name} to {recipient} for {price}?"; "lng_gift_transfer_button" = "Transfer"; "lng_gift_transfer_button_for" = "Transfer for {price}"; +"lng_gift_transfer_set_theme" = "Set as Theme in..."; +"lng_gift_transfer_choose" = "Choose Chat"; "lng_gift_transfer_wear" = "Wear"; "lng_gift_transfer_take_off" = "Take Off"; "lng_gift_transfer_sell" = "Sell"; @@ -3690,12 +3797,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_sell_unlist_title" = "Unlist {name}"; "lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?"; "lng_gift_sell_title" = "Price in Stars"; -"lng_gift_sell_placeholder" = "Enter price in Stars"; "lng_gift_sell_about" = "You will receive {percent} of the selected amount."; "lng_gift_sell_amount#one" = "You will receive **{count}** Star."; "lng_gift_sell_amount#other" = "You will receive **{count}** Stars."; "lng_gift_sell_min_price#one" = "Minimum price is {count} Star."; "lng_gift_sell_min_price#other" = "Minimum price is {count} Stars."; +"lng_gift_sell_only_ton" = "Only Accept TON"; +"lng_gift_sell_only_ton_about" = "If the buyer pays you in TON, there's no risk of refunds, unlike Stars payments."; +"lng_gift_sell_amount_ton#one" = "You will receive **{count}** TON."; +"lng_gift_sell_amount_ton#other" = "You will receive **{count}** TON."; +"lng_gift_sell_min_price_ton#one" = "Minimum price is {count} TON."; +"lng_gift_sell_min_price_ton#other" = "Minimum price is {count} TON."; +"lng_gift_sell_title_ton" = "Price in TON"; "lng_gift_sell_put" = "Put for Sale"; "lng_gift_sell_update" = "Update the Price"; "lng_gift_sell_toast" = "{name} is now for sale!"; @@ -3735,9 +3848,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_resale_symbol" = "Symbol"; "lng_gift_resale_symbols#one" = "{count} Symbol"; "lng_gift_resale_symbols#other" = "{count} Symbols"; +"lng_gift_resale_switch_to_ton" = "Switch to Ton"; +"lng_gift_resale_switch_to_stars" = "Switch to Stars"; "lng_gift_resale_early" = "You will be able to resell this gift in {duration}."; "lng_gift_transfer_early" = "You will be able to transfer this gift in {duration}."; "lng_gift_resale_transfer_early_title" = "Try Later"; +"lng_gift_collection_add" = "Add Collection"; +"lng_gift_collection_new_title" = "Create a New Collection"; +"lng_gift_collection_new_button" = "New Collection"; +"lng_gift_collection_new_text" = "Choose a name for your collection and start adding your gifts there."; +"lng_gift_collection_new_ph" = "Title"; +"lng_gift_collection_new_create" = "Create"; +"lng_gift_collection_empty_title" = "Organize Your Gifts"; +"lng_gift_collection_empty_text" = "Add some of your gifts to this collection."; +"lng_gift_collection_all" = "All Gifts"; +"lng_gift_collection_add_title" = "Add Gifts"; +"lng_gift_collection_add_button" = "Add Gifts"; +"lng_gift_collection_share" = "Share Collection"; +"lng_gift_collection_edit" = "Edit Name"; +"lng_gift_collection_limit_title" = "Limit Reached"; +"lng_gift_collection_limit_text" = "Please remove one of the existing collections to add a new one."; +"lng_gift_collection_delete" = "Delete Collection"; +"lng_gift_collection_delete_sure" = "Are you sure you want to delete this collection?"; +"lng_gift_collection_delete_button" = "Delete"; +"lng_gift_collection_add_to" = "Add to Collection"; +"lng_gift_locked_title" = "Gift Locked"; "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; @@ -4056,6 +4191,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_dialogs_suggestions_credits_sub_low_title#other" = "{emoji} {count} Stars needed for {channels}"; "lng_dialogs_suggestions_credits_sub_low_about" = "Insufficient funds to cover your subscription."; +"lng_unconfirmed_auth_title" = "Someone just got access to your messages!"; +"lng_unconfirmed_auth_confirm" = "Yes, it’s me"; +"lng_unconfirmed_auth_deny" = "No, it’s not me!"; +"lng_unconfirmed_auth_single" = "We detected a new login to your account from {from}, {country}. Is it you?"; +"lng_unconfirmed_auth_multiple#one" = "We detected new {count} login to your account. Is it you?"; +"lng_unconfirmed_auth_multiple#other" = "We detected new {count} logins to your account. Is it you?"; +"lng_unconfirmed_auth_multiple_from#one" = "We detected new {count} login to your account from {country}. Is it you?"; +"lng_unconfirmed_auth_multiple_from#other" = "We detected new {count} logins to your account from {country}. Is it you?"; +"lng_unconfirmed_auth_denied_title#one" = "New Login Prevented"; +"lng_unconfirmed_auth_denied_title#other" = "New Logins Prevented"; +"lng_unconfirmed_auth_denied_single" = "We have terminated the login attempt from {country}."; +"lng_unconfirmed_auth_denied_multiple" = "We have terminated the login attempts from: {country}"; +"lng_unconfirmed_auth_denied_warning" = "Never send your login code to anyone or you can lose your Telegram account!"; +"lng_unconfirmed_auth_confirmed" = "New Login Allowed"; +"lng_unconfirmed_auth_confirmed_message" = "You can check the list of your active logins in {link}."; + "lng_about_random" = "Send a {emoji} emoji to any chat to try your luck."; "lng_about_random_send" = "Send"; @@ -4223,6 +4374,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_archive_to_list" = "Move to chat list"; "lng_context_archive_to_menu_info" = "Archive moved to the main menu!\nRight click the archive button to return the Archive to your chat list."; "lng_context_archive_settings" = "Archive settings"; +"lng_context_archive_how_does_it_work" = "How does it work?"; "lng_context_mute" = "Mute notifications"; "lng_context_unmute" = "Unmute"; @@ -4234,6 +4386,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_remove_from_group" = "Remove from group"; "lng_context_add_to_group" = "Add to group"; +"lng_context_rate_transcription" = "Rate transcription"; +"lng_toast_sent_rate_transcription" = "Thank you for your feedback!"; + "lng_context_copy_link" = "Copy Link"; "lng_context_copy_message_link" = "Copy Message Link"; "lng_context_copy_post_link" = "Copy Post Link"; @@ -4253,6 +4408,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_pack_info" = "View Sticker Set"; "lng_context_pack_add" = "Add Stickers"; "lng_context_save_file" = "Save As..."; +"lng_context_save_music_to" = "Save to..."; +"lng_context_save_music_profile" = "... Profile"; +"lng_context_save_music_saved" = "... Saved Messages"; +"lng_context_save_music_folder" = "... Downloads"; +"lng_context_save_music_about" = "Choose where you want this audio to be saved."; "lng_context_copy_text" = "Copy Text"; "lng_context_open_gif" = "Open GIF"; "lng_context_save_gif" = "Add to GIFs"; @@ -4667,6 +4827,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_selected_delete_sure_this" = "Do you want to delete this message?"; "lng_selected_delete_sure#one" = "Do you want to delete {count} message?"; "lng_selected_delete_sure#other" = "Do you want to delete {count} messages?"; +"lng_selected_remove_saved_music" = "Do you want to remove this file from your profile?"; +"lng_saved_music_added" = "Audio added to your Profile."; +"lng_saved_music_removed" = "Audio removed from your Profile."; "lng_delete_photo_sure" = "Do you want to delete this photo?"; "lng_delete_for_everyone_hint#one" = "This will delete it for everyone in this chat."; "lng_delete_for_everyone_hint#other" = "This will delete them for everyone in this chat."; @@ -6192,6 +6355,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chat_theme_change_wallpaper" = "Change Wallpaper"; "lng_chat_theme_title" = "Select theme"; "lng_chat_theme_cant_voice" = "Sorry, you can't change the chat theme while you have an unsent voice message."; +"lng_chat_theme_gift_replace" = "This gift is already your theme in the chat with {name}. Remove it there and use it here instead?"; "lng_photo_editor_menu_delete" = "Delete"; "lng_photo_editor_menu_flip" = "Flip"; @@ -6224,10 +6388,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_emojipack" = "View emoji"; "lng_view_button_collectible" = "View collectible"; "lng_view_button_call" = "Join call"; +"lng_view_button_storyalbum" = "View Album"; +"lng_view_button_collection" = "View Collection"; "lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_title" = "What are sponsored messages?"; -"lng_sponsored_info_description1" = "Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.\n\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:"; +"lng_sponsored_info_description1_linked" = "Unlike other apps, Telegram never uses your private data to target ads. {link}\n\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:"; +"lng_sponsored_info_description1_link" = "Learn more in the Privacy Policy"; +"lng_sponsored_info_description1_url" = "https://telegram.org/privacy#5-6-no-ads-based-on-user-data"; "lng_sponsored_info_description2" = "Sponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\n\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate – together."; "lng_sponsored_info_menu" = "Advertiser info"; "lng_sponsored_info_submenu" = "Advertiser: {text}"; @@ -6264,6 +6432,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ringtones_box_title" = "Notification Sound"; "lng_ringtones_box_cloud_subtitle" = "Choose your tone"; +"lng_ringtones_box_volume" = "Volume"; "lng_ringtones_box_upload_choose" = "Choose a tone"; "lng_ringtones_box_upload_button" = "Upload Sound"; "lng_ringtones_box_about" = "Right click on any short voice note or MP3 file in chat and select \"Save for Notifications\". It will appear here."; @@ -6388,6 +6557,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_archive_title" = "Story Archive"; "lng_stories_archive_about" = "Only you can see archived stories unless you choose to post them to your profile."; "lng_stories_channel_archive_about" = "Only admins of the channel can see archived stories unless they are posted to the channel page."; +"lng_stories_empty" = "Your stories will be here."; +"lng_stories_empty_channel" = "Channel posts will be here."; "lng_stories_reply_sent" = "Message sent."; "lng_stories_hidden_to_contacts" = "Stories from {user} were moved to the **Archive**."; "lng_stories_shown_in_chats" = "Stories from {user} were returned from the **Archive**."; @@ -6423,6 +6594,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_save_promo" = "Subscribe to {link} to save other people's stories that are not protected."; "lng_stories_reaction_as_message" = "Send reaction as a private message"; +"lng_stories_album_add" = "Add Album"; +"lng_stories_album_new_title" = "Create a New Album"; +"lng_stories_album_new_button" = "New Album"; +"lng_stories_album_new_text" = "Choose a name for your album and start adding your stories there."; +"lng_stories_album_new_ph" = "Title"; +"lng_stories_album_new_create" = "Create"; +"lng_stories_album_empty_title" = "Organize Your Stories"; +"lng_stories_album_empty_text" = "Add some of your stories to this album."; +"lng_stories_album_all" = "All Stories"; +"lng_stories_album_add_title" = "Add Stories"; +"lng_stories_album_add_button" = "Add Stories"; +"lng_stories_album_share" = "Share Album"; +"lng_stories_album_edit" = "Edit Name"; +"lng_stories_album_limit_title" = "Limit Reached"; +"lng_stories_album_limit_text" = "Please remove one of the existing albums to add a new one."; +"lng_stories_album_delete" = "Delete Album"; +"lng_stories_album_delete_sure" = "Are you sure you want to delete this album?"; +"lng_stories_album_delete_button" = "Delete"; +"lng_stories_album_add_to" = "Add to Album"; + "lng_stealth_mode_menu_item" = "Hide My View"; "lng_stealth_mode_title" = "Stealth Mode"; "lng_stealth_mode_unlock_about" = "Subscribe to Telegram Premium to watch stories without appearing in the list of viewers."; @@ -6679,6 +6870,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_chats" = "Chats"; "lng_recent_channels" = "Channels"; "lng_recent_apps" = "Apps"; +"lng_recent_posts" = "Posts"; "lng_all_photos" = "Photos"; "lng_all_videos" = "Videos"; "lng_all_downloads" = "Downloads"; @@ -6696,6 +6888,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_apps_popular" = "Grossing apps"; "lng_bot_apps_which" = "Which apps are included here? {link}"; "lng_bot_apps_which_link" = "Learn >"; +"lng_posts_title" = "Global Search"; +"lng_posts_start" = "Type a keyword to search all posts from public channels."; +"lng_posts_subscribe" = "Subscribe to Premium"; +"lng_posts_need_subscribe" = "Global search is a Premium feature."; +"lng_posts_search_button" = "Search {query}"; +"lng_posts_remaining#one" = "{count} free search remaining today"; +"lng_posts_remaining#other" = "{count} free searches remaining today"; +"lng_posts_subtitle_empty" = "Telegram News"; +"lng_posts_subtitle" = "Public posts"; +"lng_posts_limit_reached" = "Limit Reached"; +"lng_posts_limit_about#one" = "You can make up to {count} search query per day."; +"lng_posts_limit_about#other" = "You can make up to {count} search queries per day."; +"lng_posts_limit_search_paid" = "Search for {cost}"; +"lng_posts_limit_unlocks" = "free search unlocks in {duration}"; +"lng_posts_paid_spent#one" = "**{count} Star** spent on extra search."; +"lng_posts_paid_spent#other" = "**{count} Stars** spent on extra search."; "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."; @@ -6756,6 +6964,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_frozen_text3" = "Appeal via {link} before {date}, or your account will be deleted."; "lng_frozen_appeal_button" = "Submit an Appeal"; +"lng_age_verify_title" = "Age Verification"; +"lng_age_verify_mobile" = "Please open this media in the official Telegram app for Android or iOS to verify your age."; +"lng_age_verify_here" = "This is a one-time process using your phone's camera. Your selfie will not be stored by Telegram."; +"lng_age_verify_button" = "Verify My Age"; +"lng_age_verify_sorry_title" = "Age Check Failed"; +"lng_age_verify_sorry_text" = "Sorry, you can't view 18+ content."; + "lng_context_bank_card_copy" = "Copy Card Number"; "lng_context_bank_card_copied" = "Card number copied to clipboard."; diff --git a/Telegram/Resources/picker_html/picker.js b/Telegram/Resources/picker_html/picker.js index e44fd51a95..628a224637 100644 --- a/Telegram/Resources/picker_html/picker.js +++ b/Telegram/Resources/picker_html/picker.js @@ -51,8 +51,8 @@ var LocationPicker = { }, init: function (params) { mapboxgl.accessToken = params.token; - if (params.protocol) { - mapboxgl.config.API_URL = params.protocol + '://domain/api.mapbox.com'; + if (location.hostname != 'desktop-app-resource') { + mapboxgl.config.API_URL = location.protocol + '//' + location.host + '/api.mapbox.com'; } var options = { container: 'map', config: { diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index cc9c2313ed..4e206e01ca 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -39,6 +39,7 @@ ../../animations/edit_peers/topics_list.tgs ../../animations/edit_peers/direct_messages.tgs ../../animations/no_chats.tgs + ../../animations/transcribe_loading.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/Resources/qrc/telegram/sounds.qrc b/Telegram/Resources/qrc/telegram/sounds.qrc index d9f7cf8b5a..1ffc48b052 100644 --- a/Telegram/Resources/qrc/telegram/sounds.qrc +++ b/Telegram/Resources/qrc/telegram/sounds.qrc @@ -10,5 +10,6 @@ ../../sounds/group_call_connect.mp3 ../../sounds/group_call_end.mp3 ../../sounds/group_call_allowed.mp3 + ../../sounds/group_call_recording_start.mp3 diff --git a/Telegram/Resources/sounds/group_call_recording_start.mp3 b/Telegram/Resources/sounds/group_call_recording_start.mp3 new file mode 100644 index 0000000000..0060e82c37 Binary files /dev/null and b/Telegram/Resources/sounds/group_call_recording_start.mp3 differ diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index d63d433f73..a95c99b9d6 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="6.1.3.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 218aa82970..580e404a52 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,16,4,0 - PRODUCTVERSION 5,16,4,0 + FILEVERSION 6,1,3,0 + PRODUCTVERSION 6,1,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "5.16.4.0" + VALUE "FileVersion", "6.1.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.16.4.0" + VALUE "ProductVersion", "6.1.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 81f44604c6..3b6df7a214 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,16,4,0 - PRODUCTVERSION 5,16,4,0 + FILEVERSION 6,1,3,0 + PRODUCTVERSION 6,1,3,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.16.4.0" + VALUE "FileVersion", "6.1.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.16.4.0" + VALUE "ProductVersion", "6.1.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 6cd28824e6..01caa44e61 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -9,10 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" -#include "core/changelogs.h" #include "core/application.h" +#include "core/changelogs.h" #include "core/core_settings.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session_settings.h" +#include "main/main_session.h" namespace Api { namespace { @@ -83,7 +86,19 @@ Authorizations::Entry ParseEntry(const MTPDauthorization &data) { } // namespace Authorizations::Authorizations(not_null api) -: _api(&api->instance()) { +: _api(&api->instance()) +, _autoconfirmPeriod([=] { + constexpr auto kFallbackCount = 604800; + return api->session().appConfig().get( + u"authorization_autoconfirm_period"_q, + kFallbackCount); +}) +, _saveUnreviewed([=] { + api->session().settings().setUnreviewed(_unreviewed); + api->session().saveSettingsDelayed(); +}) { + _unreviewed = api->session().settings().unreviewed(); + removeExpiredUnreviewed(); Core::App().settings().deviceModelChanges( ) | rpl::start_with_next([=](const QString &model) { auto changed = false; @@ -119,6 +134,7 @@ void Authorizations::reload() { ) | ranges::views::transform([](const MTPAuthorization &auth) { return ParseEntry(auth.data()); }) | ranges::to; + removeExpiredUnreviewed(); refreshCallsDisabledHereFromCloud(); _listChanges.fire({}); }).fail([=] { @@ -217,11 +233,7 @@ void Authorizations::toggleCallsDisabled(uint64 hash, bool disabled) { MTP_bool(disabled) )).done([=] { _toggleCallsDisabledRequests.remove(hash); - }).fail([=](const MTP::Error &error) { - LOG(("API Error: toggle calls %1. Hash: %2. %3.") - .arg(disabled ? u"disabled"_q : u"enabled"_q) - .arg(hash) - .arg(error.type())); + }).fail([=] { _toggleCallsDisabledRequests.remove(hash); }).send(); _toggleCallsDisabledRequests.emplace(hash, id); @@ -265,4 +277,129 @@ crl::time Authorizations::lastReceivedTime() { return _lastReceived; } +const std::vector &Authorizations::unreviewed() { + removeExpiredUnreviewed(); + return _unreviewed; +} + +void Authorizations::removeExpiredUnreviewed() { + const auto now = base::unixtime::now(); + const auto period = _autoconfirmPeriod(); + + const auto oldSize = _unreviewed.size(); + _unreviewed.erase( + std::remove_if(_unreviewed.begin(), _unreviewed.end(), + [=](const auto &auth) { + return (now - auth.date) >= period; + }), + _unreviewed.end()); + + if (_unreviewed.size() != oldSize) { + _saveUnreviewed(); + } +} + +void Authorizations::review(const std::vector &hashes, bool confirm) { + for (const auto hash : hashes) { + if (const auto sent = _reviewRequests.take(hash)) { + _api.request(*sent).cancel(); + } + } + + const auto checkComplete = [=] { + if (_reviewRequests.empty()) { + _saveUnreviewed(); + _unreviewedChanges.fire({}); + } + }; + + for (const auto hash : hashes) { + const auto removeFromUnreviewed = [=] { + _unreviewed.erase( + std::remove_if(_unreviewed.begin(), _unreviewed.end(), + [hash](const auto &auth) { return auth.hash == hash; }), + _unreviewed.end()); + _reviewRequests.remove(hash); + checkComplete(); + }; + + if (confirm) { + using Flag = MTPaccount_ChangeAuthorizationSettings::Flag; + const auto id = _api.request(MTPaccount_ChangeAuthorizationSettings( + MTP_flags(Flag::f_confirmed), + MTP_long(hash), + MTPBool(), // encrypted_requests_disabled + MTPBool() // call_requests_disabled + )).done([=] { + removeFromUnreviewed(); + }).fail([=] { + removeFromUnreviewed(); + }).send(); + _reviewRequests.emplace(hash, id); + } else { + const auto id = _api.request(MTPaccount_ResetAuthorization( + MTP_long(hash) + )).done([=](const MTPBool &result) { + if (mtpIsTrue(result)) { + _list.erase( + ranges::remove(_list, hash, &Entry::hash), + end(_list)); + _listChanges.fire({}); + } + removeFromUnreviewed(); + }).fail([=] { + removeFromUnreviewed(); + }).send(); + _reviewRequests.emplace(hash, id); + } + } +} + +rpl::producer<> Authorizations::unreviewedChanges() const { + return _unreviewedChanges.events(); +} + +void Authorizations::apply(const MTPUpdate &update) { + removeExpiredUnreviewed(); + update.match([&](const MTPDupdateNewAuthorization &data) { + auto unreviewed = Data::UnreviewedAuth{ + .hash = data.vhash().v, + .unconfirmed = data.is_unconfirmed(), + .date = data.vdate().value_or_empty(), + .device = qs(data.vdevice().value_or_empty()), + .location = qs(data.vlocation().value_or_empty()) + }; + if (!unreviewed.unconfirmed) { + const auto hash = unreviewed.hash; + const auto was = _unreviewed.size(); + _unreviewed.erase( + std::remove_if( + _unreviewed.begin(), + _unreviewed.end(), + [hash](const auto &auth) { return auth.hash == hash; }), + _unreviewed.end()); + if (was != _unreviewed.size()) { + _saveUnreviewed(); + _unreviewedChanges.fire({}); + } + return; + } + + for (auto &auth : _unreviewed) { + if (auth.hash == unreviewed.hash) { + auth = std::move(unreviewed); + _saveUnreviewed(); + _unreviewedChanges.fire({}); + return; + } + } + + _unreviewed.push_back(std::move(unreviewed)); + _saveUnreviewed(); + _unreviewedChanges.fire({}); + }, [](auto&&) { + Unexpected("Update in Authorizations::apply."); + }); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_authorizations.h b/Telegram/SourceFiles/api/api_authorizations.h index 5e2a41c9f9..25c97f3945 100644 --- a/Telegram/SourceFiles/api/api_authorizations.h +++ b/Telegram/SourceFiles/api/api_authorizations.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_authorization.h" #include "mtproto/sender.h" class ApiWrap; @@ -35,6 +36,8 @@ public: Fn &&fail, std::optional hash = std::nullopt); + void apply(const MTPUpdate &update); + [[nodiscard]] crl::time lastReceivedTime(); [[nodiscard]] List list() const; @@ -42,6 +45,11 @@ public: [[nodiscard]] int total() const; [[nodiscard]] rpl::producer totalValue() const; + [[nodiscard]] const std::vector &unreviewed(); + [[nodiscard]] rpl::producer<> unreviewedChanges() const; + + void review(const std::vector &hashes, bool confirm); + void updateTTL(int days); [[nodiscard]] rpl::producer ttlDays() const; @@ -57,6 +65,7 @@ public: private: void refreshCallsDisabledHereFromCloud(); + void removeExpiredUnreviewed(); MTP::Sender _api; mtpRequestId _requestId = 0; @@ -64,10 +73,16 @@ private: List _list; rpl::event_stream<> _listChanges; + Fn _autoconfirmPeriod; + std::vector _unreviewed; + rpl::event_stream<> _unreviewedChanges; + Fn _saveUnreviewed; + mtpRequestId _ttlRequestId = 0; rpl::variable _ttlDays = 0; base::flat_map _toggleCallsDisabledRequests; + base::flat_map _reviewRequests; rpl::variable _callsDisabledHere; crl::time _lastReceived = 0; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index b86ce1a70c..05c0d46928 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -245,7 +245,7 @@ void SendBotCallbackDataWithPassword( fields.customSubmitButton = tr::lng_passcode_submit(); fields.customCheckCallback = [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { if (const auto button = getButton()) { if (button->requestId) { return; diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp index fe96accd61..ffd3fb0f79 100644 --- a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.cpp @@ -57,7 +57,7 @@ void RemoveChatFilter( RemoveComplexChatFilter::RemoveComplexChatFilter() = default; void RemoveComplexChatFilter::request( - QPointer widget, + base::weak_qptr widget, base::weak_ptr weak, FilterId id) { const auto session = &weak->session(); diff --git a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h index ce92b2df3c..6bd6ce7fa3 100644 --- a/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h +++ b/Telegram/SourceFiles/api/api_chat_filters_remove_manager.h @@ -22,7 +22,7 @@ public: RemoveComplexChatFilter(); void request( - QPointer widget, + base::weak_qptr widget, base::weak_ptr weak, FilterId id); diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index b67e148677..415c1a8296 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -142,13 +142,12 @@ void ConfirmSubscriptionBox( const auto content = box->verticalLayout(); Ui::AddSkip(content, st::confirmInvitePhotoTop); - const auto userpicWrap = content->add( - object_ptr>( - content, - object_ptr(content))); - const auto userpic = userpicWrap->entity(); + const auto userpic = content->add( + object_ptr(content), + style::al_top); const auto photoSize = st::confirmInvitePhotoSize; userpic->resize(Size(photoSize)); + userpic->setNaturalWidth(photoSize); const auto creditsIconSize = photoSize / 3; const auto creditsIconCallback = Ui::PaintOutlinedColoredCreditsIconCallback( @@ -188,8 +187,8 @@ void ConfirmSubscriptionBox( } auto p = QPainter(userpic); p.drawImage(0, 0, state->frame); - }, userpicWrap->lifetime()); - userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents); + }, userpic->lifetime()); + userpic->setAttribute(Qt::WA_TransparentForMouseEvents); if (photo) { state->photoMedia = photo->createMediaView(); state->photoMedia->wanted(Data::PhotoSize::Small, Data::FileOrigin()); @@ -197,7 +196,7 @@ void ConfirmSubscriptionBox( session->downloaderTaskFinished( ) | rpl::start_with_next([=] { userpic->update(); - }, userpicWrap->entity()->lifetime()); + }, userpic->lifetime()); } } else { state->photoEmpty = std::make_unique( @@ -215,43 +214,40 @@ void ConfirmSubscriptionBox( 2.); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_channel_invite_subscription_title(), - st::inviteLinkSubscribeBoxTitle))); + tr::lng_channel_invite_subscription_title(), + st::inviteLinkSubscribeBoxTitle), + style::al_top); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_channel_invite_subscription_about( - lt_channel, - rpl::single(Ui::Text::Bold(name)), - lt_price, - tr::lng_credits_summary_options_credits( - lt_count, - rpl::single(amount) | tr::to_count(), - Ui::Text::Bold), - Ui::Text::WithEntities), - st::inviteLinkSubscribeBoxAbout))); + tr::lng_channel_invite_subscription_about( + lt_channel, + rpl::single(Ui::Text::Bold(name)), + lt_price, + tr::lng_credits_summary_options_credits( + lt_count, + rpl::single(amount) | tr::to_count(), + Ui::Text::Bold), + Ui::Text::WithEntities), + st::inviteLinkSubscribeBoxAbout), + style::al_top); Ui::AddSkip(content); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_channel_invite_subscription_terms( - lt_link, - rpl::combine( - tr::lng_paid_react_agree_link(), - tr::lng_group_invite_subscription_about_url() - ) | rpl::map([](const QString &text, const QString &url) { - return Ui::Text::Link(text, url); - }), - Ui::Text::RichLangValue), - st::inviteLinkSubscribeBoxTerms))); + tr::lng_channel_invite_subscription_terms( + lt_link, + rpl::combine( + tr::lng_paid_react_agree_link(), + tr::lng_group_invite_subscription_about_url() + ) | rpl::map([](const QString &text, const QString &url) { + return Ui::Text::Link(text, url); + }), + Ui::Text::RichLangValue), + st::inviteLinkSubscribeBoxTerms), + style::al_top); { const auto balance = Settings::AddBalanceWidget( @@ -272,7 +268,7 @@ void ConfirmSubscriptionBox( }, balance->lifetime()); } - const auto sendCredits = [=, weak = Ui::MakeWeak(box)] { + const auto sendCredits = [=, weak = base::make_weak(box)] { const auto show = box->uiShow(); const auto buttonWidth = state->saveButton ? state->saveButton->width() @@ -280,7 +276,7 @@ void ConfirmSubscriptionBox( const auto finish = [=] { state->api = std::nullopt; state->loading.force_assign(false); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -294,7 +290,7 @@ void ConfirmSubscriptionBox( }, [](const MTPDpayments_paymentVerificationNeeded &data) { }); const auto refill = session->data().activeCreditsSubsRebuilder(); - const auto strong = weak.data(); + const auto strong = weak.get(); if (!strong) { return; } diff --git a/Telegram/SourceFiles/api/api_confirm_phone.cpp b/Telegram/SourceFiles/api/api_confirm_phone.cpp index a3b41fefc9..7379681138 100644 --- a/Telegram/SourceFiles/api/api_confirm_phone.cpp +++ b/Telegram/SourceFiles/api/api_confirm_phone.cpp @@ -87,7 +87,7 @@ void ConfirmPhone::resolve( sentCodeLength, fragmentUrl, timeout); - const auto boxWeak = Ui::MakeWeak(box.data()); + const auto boxWeak = base::make_weak(box.data()); using LoginCode = rpl::event_stream; const auto codeHandles = box->lifetime().make_state(); controller->session().account().setHandleLoginCode([=]( diff --git a/Telegram/SourceFiles/api/api_credits_history_entry.cpp b/Telegram/SourceFiles/api/api_credits_history_entry.cpp index c6acdfeb6d..dbf616471d 100644 --- a/Telegram/SourceFiles/api/api_credits_history_entry.cpp +++ b/Telegram/SourceFiles/api/api_credits_history_entry.cpp @@ -153,6 +153,7 @@ Data::CreditsHistoryEntry CreditsHistoryEntryFromTL( .floodSkip = int(tl.data().vfloodskip_number().value_or(0)), .converted = stargift && incoming, .stargift = stargift.has_value(), + .postsSearch = tl.data().is_posts_search(), .giftUpgraded = tl.data().is_stargift_upgrade(), .giftResale = tl.data().is_stargift_resale(), .reaction = tl.data().is_reaction(), diff --git a/Telegram/SourceFiles/api/api_earn.cpp b/Telegram/SourceFiles/api/api_earn.cpp index 9df407aad2..3b8272fc4e 100644 --- a/Telegram/SourceFiles/api/api_earn.cpp +++ b/Telegram/SourceFiles/api/api_earn.cpp @@ -82,7 +82,7 @@ void HandleWithdrawalButton( fields.customSubmitButton = tr::lng_passcode_submit(); fields.customCheckCallback = crl::guard(button, [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { const auto done = [=](const QString &result) { if (!result.isEmpty()) { UrlClickHandler::Open(result); diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 5065541e27..545ef29be3 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -45,19 +45,45 @@ namespace { auto options = PremiumSubscriptionOptionsFromTL(tlOptions); for (auto i = 0; i < options.size(); i++) { const auto &tlOption = tlOptions[i].data(); + const auto currency = qs(tlOption.vcurrency()); const auto perUserText = Ui::FillAmountAndCurrency( tlOption.vamount().v / float64(tlOption.vusers().v), - qs(tlOption.vcurrency()), + currency, false); options[i].costPerMonth = perUserText + ' ' + QChar(0x00D7) + ' ' + QString::number(tlOption.vusers().v); + options[i].currency = currency; } return options; } +[[nodiscard]] int FindStarsForResale(const MTPVector *list) { + if (!list) { + return 0; + } + for (const auto &amount : list->v) { + if (amount.type() == mtpc_starsAmount) { + return int(amount.c_starsAmount().vamount().v); + } + } + return 0; +} + +[[nodiscard]] int64 FindTonForResale(const MTPVector *list) { + if (!list) { + return 0; + } + for (const auto &amount : list->v) { + if (amount.type() == mtpc_starsTonAmount) { + return int64(amount.c_starsTonAmount().vamount().v); + } + } + return 0; +} + } // namespace Premium::Premium(not_null api) @@ -589,24 +615,32 @@ std::vector PremiumGiftCodeOptions::optionsForPeer() const { return result; } -Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::options(int amount) { - const auto it = _subscriptionOptions.find(amount); +Data::PremiumSubscriptionOptions PremiumGiftCodeOptions::optionsForGiveaway( + int usersCount) { + const auto skipForStars = [&](Data::PremiumSubscriptionOptions options) { + const auto proj = &Data::PremiumSubscriptionOption::currency; + options.erase( + ranges::remove(options, Ui::kCreditsCurrency, proj), + end(options)); + return options; + }; + const auto it = _subscriptionOptions.find(usersCount); if (it != end(_subscriptionOptions)) { - return it->second; + return skipForStars(it->second); } else { auto tlOptions = QVector(); for (auto i = 0; i < _optionsForOnePerson.months.size(); i++) { tlOptions.push_back(MTP_premiumGiftCodeOption( MTP_flags(MTPDpremiumGiftCodeOption::Flags(0)), - MTP_int(amount), + MTP_int(usersCount), MTP_int(_optionsForOnePerson.months[i]), MTPstring(), MTPint(), MTP_string(_optionsForOnePerson.currencies[i]), - MTP_long(_optionsForOnePerson.totalCosts[i] * amount))); + MTP_long(_optionsForOnePerson.totalCosts[i] * usersCount))); } - _subscriptionOptions[amount] = GiftCodesFromTL(tlOptions); - return _subscriptionOptions[amount]; + _subscriptionOptions[usersCount] = GiftCodesFromTL(tlOptions); + return skipForStars(_subscriptionOptions[usersCount]); } } @@ -825,8 +859,12 @@ std::optional FromTL( .resellCount = int(data.vavailability_resale().value_or_empty()), .limitedLeft = remaining.value_or_empty(), .limitedCount = total.value_or_empty(), + .perUserTotal = data.vper_user_total().value_or_empty(), + .perUserRemains = data.vper_user_remains().value_or_empty(), .firstSaleDate = data.vfirst_sale_date().value_or_empty(), .lastSaleDate = data.vlast_sale_date().value_or_empty(), + .lockedUntilDate = data.vlocked_until_date().value_or_empty(), + .requirePremium = data.is_require_premium(), .upgradable = data.vupgrade_stars().has_value(), .birthday = data.is_birthday(), .soldOut = data.is_sold_out(), @@ -853,13 +891,20 @@ std::optional FromTL( const auto releasedById = data.vreleased_by() ? peerFromMTP(*data.vreleased_by()) : PeerId(); + const auto themeUserId = data.vtheme_peer() + ? peerFromMTP(*data.vtheme_peer()) + : PeerId(); const auto releasedBy = releasedById ? session->data().peer(releasedById).get() : nullptr; + const auto themeUser = themeUserId + ? session->data().peer(themeUserId).get() + : nullptr; auto result = Data::StarGift{ - .id = uint64(data.vid().v), + .id = data.vid().v, .unique = std::make_shared(Data::UniqueGift{ .id = data.vid().v, + .initialGiftId = data.vgift_id().v, .slug = qs(data.vslug()), .title = qs(data.vtitle()), .ownerAddress = qs(data.vowner_address().value_or_empty()), @@ -868,15 +913,30 @@ std::optional FromTL( ? peerFromMTP(*data.vowner_id()) : PeerId()), .releasedBy = releasedBy, + .themeUser = themeUser, + .nanoTonForResale = FindTonForResale(data.vresell_amount()), + .starsForResale = FindStarsForResale(data.vresell_amount()), .number = data.vnum().v, - .starsForResale = int(data.vresell_stars().value_or_empty()), + .onlyAcceptTon = data.is_resale_ton_only(), + .canBeTheme = data.is_theme_available(), .model = *model, .pattern = *pattern, + .value = (data.vvalue_amount() + ? std::make_shared( + Data::UniqueGiftValue{ + .currency = qs( + data.vvalue_currency().value_or_empty()), + .valuePrice = int64( + data.vvalue_amount().value_or_empty()), + }) + : nullptr), }), .document = model->document, .releasedBy = releasedBy, .limitedLeft = (total - data.vavailability_issued().v), .limitedCount = total, + .resellTonOnly = data.is_resale_ton_only(), + .requirePremium = data.is_require_premium(), }; const auto unique = result.unique.get(); for (const auto &attribute : data.vattributes().v) { @@ -913,21 +973,26 @@ std::optional FromTL( .manageId = (to->isUser() ? Id::User(data.vmsg_id().value_or_empty()) : Id::Chat(to, data.vsaved_id().value_or_empty())), + .collectionIds = (data.vcollection_id() + ? (data.vcollection_id()->v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector) + : std::vector()), .message = (data.vmessage() - ? TextWithEntities{ - .text = qs(data.vmessage()->data().vtext()), - .entities = Api::EntitiesFromMTP( - session, - data.vmessage()->data().ventities().v), - } + ? Api::ParseTextWithEntities( + session, + *data.vmessage()) : TextWithEntities()), .starsConverted = int64(data.vconvert_stars().value_or_empty()), .starsUpgradedBySender = int64( data.vupgrade_stars().value_or_empty()), + .giftPrepayUpgradeHash = qs( + data.vprepaid_upgrade_hash().value_or_empty()), .fromId = (data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId()), .date = data.vdate().v, + .upgradeSeparate = data.is_upgrade_separate(), .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), .pinned = data.is_pinned_to_top() && hasUnique, diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 2b692a4849..e3eb8f1e45 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -180,7 +180,8 @@ public: [[nodiscard]] rpl::producer request(); [[nodiscard]] std::vector optionsForPeer() const; - [[nodiscard]] Data::PremiumSubscriptionOptions options(int amount); + [[nodiscard]] Data::PremiumSubscriptionOptions optionsForGiveaway( + int usersCount); [[nodiscard]] const std::vector &availablePresets() const; [[nodiscard]] int monthsFromPreset(int monthsIndex); [[nodiscard]] Payments::InvoicePremiumGiftCode invoice( diff --git a/Telegram/SourceFiles/api/api_sensitive_content.cpp b/Telegram/SourceFiles/api/api_sensitive_content.cpp index 31f83f5eba..e1e25950e8 100644 --- a/Telegram/SourceFiles/api/api_sensitive_content.cpp +++ b/Telegram/SourceFiles/api/api_sensitive_content.cpp @@ -25,7 +25,7 @@ SensitiveContent::SensitiveContent(not_null api) } void SensitiveContent::preload() { - if (!_loaded) { + if (!_loaded && !_loadRequestId) { reload(); } } @@ -37,7 +37,6 @@ void SensitiveContent::reload(bool force) { } return; } - _loaded = true; _loadRequestId = _api.request(MTPaccount_GetContentSettings( )).done([=](const MTPaccount_ContentSettings &result) { _loadRequestId = 0; @@ -50,6 +49,10 @@ void SensitiveContent::reload(bool force) { _enabled = enabled; _canChange = canChange; } + if (!_loaded) { + _loaded = true; + _loadedChanged.fire({}); + } if (base::take(_appConfigReloadForce) || changed) { _appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout); } @@ -61,6 +64,19 @@ void SensitiveContent::reload(bool force) { }).send(); } +bool SensitiveContent::loaded() const { + return _loaded; +} + +rpl::producer SensitiveContent::loadedValue() const { + if (_loaded) { + return rpl::single(true); + } + return rpl::single(false) | rpl::then( + _loadedChanged.events() | rpl::map_to(true) + ); +} + bool SensitiveContent::enabledCurrent() const { return _enabled.current(); } @@ -69,6 +85,10 @@ rpl::producer SensitiveContent::enabled() const { return _enabled.value(); } +bool SensitiveContent::canChangeCurrent() const { + return _canChange.current(); +} + rpl::producer SensitiveContent::canChange() const { return _canChange.value(); } diff --git a/Telegram/SourceFiles/api/api_sensitive_content.h b/Telegram/SourceFiles/api/api_sensitive_content.h index 576bf275f3..e35f8cbf46 100644 --- a/Telegram/SourceFiles/api/api_sensitive_content.h +++ b/Telegram/SourceFiles/api/api_sensitive_content.h @@ -26,12 +26,16 @@ public: void reload(bool force = false); void update(bool enabled); + [[nodiscard]] bool loaded() const; + [[nodiscard]] rpl::producer loadedValue() const; [[nodiscard]] bool enabledCurrent() const; [[nodiscard]] rpl::producer enabled() const; + [[nodiscard]] bool canChangeCurrent() const; [[nodiscard]] rpl::producer canChange() const; private: const not_null _session; + rpl::event_stream<> _loadedChanged; MTP::Sender _api; mtpRequestId _loadRequestId = 0; mtpRequestId _saveRequestId = 0; diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 7af4fbc84d..05d0304452 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -223,7 +223,7 @@ void ConfirmApproval( .confirmText = tr::lng_suggest_accept_send(), .title = tr::lng_suggest_accept_title(), }); - *callback = [=, weak = Ui::MakeWeak(box)] { + *callback = [=, weak = base::make_weak(box)] { if (const auto onstack = accepted) { onstack(); } @@ -232,7 +232,7 @@ void ConfirmApproval( return; } SendApproval(show, item, scheduleDate); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -283,9 +283,9 @@ void RequestApprovalDate( std::shared_ptr show, not_null item) { const auto id = item->fullId(); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto close = [=] { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -339,13 +339,13 @@ void RequestDeclineComment( box->setFocusCallback([=] { reason->setFocusFast(); }); - *callback = [=, weak = Ui::MakeWeak(box)] { + *callback = [=, weak = base::make_weak(box)] { const auto item = show->session().data().message(id); if (!item) { return; } SendDecline(show, item, reason->getLastText().trimmed()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -422,14 +422,14 @@ void SuggestApprovalDate( } const auto id = item->fullId(); const auto state = std::make_shared(); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto done = [=](TimeId result) { const auto item = show->session().data().message(id); if (!item) { return; } const auto close = [=] { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -458,14 +458,14 @@ void SuggestOfferForMessage( HistoryView::SuggestMode mode) { const auto id = item->fullId(); const auto state = std::make_shared(); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto done = [=](SuggestPostOptions result) { const auto item = show->session().data().message(id); if (!item) { return; } const auto close = [=] { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/api/api_transcribes.cpp b/Telegram/SourceFiles/api/api_transcribes.cpp index dd983dc37b..17681cc849 100644 --- a/Telegram/SourceFiles/api/api_transcribes.cpp +++ b/Telegram/SourceFiles/api/api_transcribes.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "main/main_app_config.h" #include "main/main_session.h" +#include "main/main_session_settings.h" namespace Api { @@ -25,6 +26,32 @@ Transcribes::Transcribes(not_null api) , _api(&api->instance()) { } +bool Transcribes::isRated(not_null item) const { + const auto fullId = item->fullId(); + for (const auto &[transcribeId, id] : _ids) { + if (id == fullId) { + return _session->settings().isTranscriptionRated(transcribeId); + } + } + return false; +} + +void Transcribes::rate(not_null item, bool isGood) { + const auto fullId = item->fullId(); + for (const auto &[transcribeId, id] : _ids) { + if (id == fullId) { + _api.request(MTPmessages_RateTranscribedAudio( + item->history()->peer->input, + MTP_int(item->id), + MTP_long(transcribeId), + MTP_bool(isGood))).send(); + _session->settings().markTranscriptionAsRated(transcribeId); + _session->saveSettings(); + return; + } + } +} + bool Transcribes::freeFor(not_null item) const { if (const auto channel = item->history()->peer->asMegagroup()) { const auto owner = &channel->owner(); diff --git a/Telegram/SourceFiles/api/api_transcribes.h b/Telegram/SourceFiles/api/api_transcribes.h index b074e42f13..93b42401df 100644 --- a/Telegram/SourceFiles/api/api_transcribes.h +++ b/Telegram/SourceFiles/api/api_transcribes.h @@ -37,6 +37,8 @@ public: void apply(const MTPDupdateTranscribedAudio &update); [[nodiscard]] bool freeFor(not_null item) const; + [[nodiscard]] bool isRated(not_null item) const; + void rate(not_null item, bool isGood); [[nodiscard]] bool trialsSupport(); [[nodiscard]] TimeId trialsRefreshAt(); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 1a5d0b9f6e..35ac496c51 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2221,6 +2221,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { } } break; + case mtpc_updateNewAuthorization: { + session().api().authorizations().apply(update); + } break; + case mtpc_updateServiceNotification: { const auto &d = update.c_updateServiceNotification(); const auto text = TextWithEntities { diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index bb8aa13441..91c94e7c07 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -240,13 +240,13 @@ struct State { [[nodiscard]] rpl::producer WhoReadIds( not_null item, not_null context) { - auto weak = QPointer(context.get()); + auto weak = base::make_weak(context); const auto session = &item->history()->session(); return [=](auto consumer) { if (!weak) { return rpl::lifetime(); } - const auto context = PreparedContextAt(weak.data(), session); + const auto context = PreparedContextAt(weak.get(), session); auto &entry = context->cacheRead(item); if (entry.requestId) { } else if (const auto user = item->history()->peer->asUser()) { @@ -325,13 +325,13 @@ struct State { not_null item, const ReactionId &reaction, not_null context) { - auto weak = QPointer(context.get()); + auto weak = base::make_weak(context); const auto session = &item->history()->session(); return [=](auto consumer) { if (!weak) { return rpl::lifetime(); } - const auto context = PreparedContextAt(weak.data(), session); + const auto context = PreparedContextAt(weak.get(), session); auto &entry = context->cacheReacted(item, reaction); if (!entry.requestId) { using Flag = MTPmessages_GetMessageReactionsList::Flag; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 2536944edc..2ee82a0f11 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_forum.h" #include "data/data_saved_messages.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "data/data_search_controller.h" #include "data/data_session.h" @@ -2497,7 +2498,17 @@ void ApiWrap::refreshFileReference( v::match(origin.data, [&](Data::FileOriginMessage data) { if (const auto item = _session->data().message(data)) { const auto media = item->media(); - const auto storyId = media ? media->storyId() : FullStoryId(); + const auto mediaStory = media ? media->storyId() : FullStoryId(); + const auto storyId = mediaStory + ? mediaStory + : FullStoryId{ + (IsStoryMsgId(item->id) + ? item->history()->peer->id + : PeerId()), + (IsStoryMsgId(item->id) + ? StoryIdFromMsgId(item->id) + : StoryId()) + }; if (storyId) { request(MTPstories_GetStoriesByID( _session->data().peer(storyId.peer)->input, @@ -2508,6 +2519,17 @@ void ApiWrap::refreshFileReference( request(MTPmessages_GetScheduledMessages( item->history()->peer->input, MTP_vector(1, MTP_int(realId)))); + } else if (item->isSavedMusicItem()) { + const auto user = item->history()->peer->asUser(); + const auto media = item->media(); + const auto document = media ? media->document() : nullptr; + if (user && document) { + request(MTPusers_GetSavedMusicByID( + user->inputUser, + MTP_vector(1, document->mtpInput()))); + } else { + fail(); + } } else if (item->isBusinessShortcut()) { const auto &shortcuts = _session->data().shortcutMessages(); const auto realId = shortcuts.lookupId(item); @@ -3379,8 +3401,8 @@ void ApiWrap::finishForwarding(const SendAction &action) { return; } - forwardMessages(std::move(toForward), action); history->setForwardDraft(topicRootId, monoforumPeerId, {}); + forwardMessages(std::move(toForward), action); } _session->data().sendHistoryChangeNotifications(); @@ -3417,6 +3439,22 @@ void ApiWrap::forwardMessages( auto &histories = _session->data().histories(); + for (auto i = begin(draft.items); i != end(draft.items);) { + const auto item = *i; + if (item->isSavedMusicItem()) { + SendExistingDocument(MessageToSend(action), item->media()->document()); + i = draft.items.erase(i); + } else { + ++i; + } + } + if (draft.items.empty()) { + if (successCallback) { + successCallback(); + } + return; + } + struct SharedCallback { int requestsLeft = 0; FnMut callback; diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 5eabc79096..0a2a728eee 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -28,7 +28,7 @@ namespace Data { struct UpdatedFileReferences; class WallPaper; struct ResolvedForwardDraft; -enum class DefaultNotify; +enum class DefaultNotify : uint8_t; enum class StickersType : uchar; class Forum; class ForumTopic; diff --git a/Telegram/SourceFiles/boxes/about_box.cpp b/Telegram/SourceFiles/boxes/about_box.cpp index 4fdf2c7c8e..c3d2fef1af 100644 --- a/Telegram/SourceFiles/boxes/about_box.cpp +++ b/Telegram/SourceFiles/boxes/about_box.cpp @@ -13,12 +13,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/update_checker.h" #include "lang/lang_keys.h" #include "ui/boxes/confirm_box.h" +#include "ui/painter.h" +#include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/wrap/vertical_layout.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" +#include "styles/style_channel_earn.h" +#include "styles/style_chat.h" +#include "styles/style_dialogs.h" +#include "styles/style_menu_icons.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" #include #include @@ -108,3 +116,151 @@ QString currentVersionText() { #endif return result; } + +void ArchiveHintBox( + not_null box, + bool unarchiveOnNewMessage, + Fn onUnarchive) { + box->setNoContentMargin(true); + + const auto content = box->verticalLayout().get(); + + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + { + const auto &icon = st::dialogsArchiveUserpic; + const auto rect = Rect(icon.size() * 2); + auto owned = object_ptr(content); + owned->resize(rect.size()); + owned->setNaturalWidth(rect.width()); + const auto widget = box->addRow(std::move(owned), style::al_top); + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(widget); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::activeButtonBg); + p.drawEllipse(rect); + icon.paintInCenter(p, rect); + }, widget->lifetime()); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + box->addRow( + object_ptr( + content, + tr::lng_archive_hint_title(), + st::boxTitle), + style::al_top); + Ui::AddSkip(content); + Ui::AddSkip(content); + { + const auto label = box->addRow( + object_ptr( + content, + (unarchiveOnNewMessage + ? tr::lng_archive_hint_about_unmuted + : tr::lng_archive_hint_about)( + lt_link, + tr::lng_archive_hint_about_link( + lt_emoji, + rpl::single( + Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link(std::move(text), 1); + }), + Ui::Text::RichLangValue), + st::channelEarnHistoryRecipientLabel)); + label->resizeToWidth(box->width() + - rect::m::sum::h(st::boxRowPadding)); + label->setLink( + 1, + std::make_shared([=](ClickContext context) { + if (context.button == Qt::LeftButton) { + onUnarchive(); + } + })); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + { + const auto padding = QMargins( + st::settingsButton.padding.left(), + st::boxRowPadding.top(), + st::boxRowPadding.right(), + st::boxRowPadding.bottom()); + const auto addEntry = [&]( + rpl::producer title, + rpl::producer about, + const style::icon &icon) { + const auto top = content->add( + object_ptr( + content, + std::move(title), + st::channelEarnSemiboldLabel), + padding); + Ui::AddSkip(content, st::channelEarnHistoryThreeSkip); + content->add( + object_ptr( + content, + std::move(about), + st::channelEarnHistoryRecipientLabel), + padding); + const auto left = Ui::CreateChild( + box->verticalLayout().get()); + left->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(left); + icon.paint(p, 0, 0, left->width()); + }, left->lifetime()); + left->resize(icon.size()); + top->geometryValue( + ) | rpl::start_with_next([=](const QRect &g) { + left->moveToLeft( + (g.left() - left->width()) / 2, + g.top() + st::channelEarnHistoryThreeSkip); + }, left->lifetime()); + }; + addEntry( + tr::lng_archive_hint_section_1(), + tr::lng_archive_hint_section_1_info(), + st::menuIconArchive); + Ui::AddSkip(content); + Ui::AddSkip(content); + addEntry( + tr::lng_archive_hint_section_2(), + tr::lng_archive_hint_section_2_info(), + st::menuIconStealth); + Ui::AddSkip(content); + Ui::AddSkip(content); + addEntry( + tr::lng_archive_hint_section_3(), + tr::lng_archive_hint_section_3_info(), + st::menuIconStoriesSavedSection); + Ui::AddSkip(content); + Ui::AddSkip(content); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + { + const auto &st = st::premiumPreviewDoubledLimitsBox; + box->setStyle(st); + auto button = object_ptr( + box, + tr::lng_archive_hint_button(), + st::defaultActiveButton); + button->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + button->resizeToWidth(box->width() + - st.buttonPadding.left() + - st.buttonPadding.left()); + button->setClickedCallback([=] { box->closeBox(); }); + box->addButton(std::move(button)); + } +} + diff --git a/Telegram/SourceFiles/boxes/about_box.h b/Telegram/SourceFiles/boxes/about_box.h index 8bdb04cdd4..429c25cb43 100644 --- a/Telegram/SourceFiles/boxes/about_box.h +++ b/Telegram/SourceFiles/boxes/about_box.h @@ -14,5 +14,9 @@ class SessionController; } void AboutBox(not_null box, Window::SessionController* controller); +void ArchiveHintBox( + not_null box, + bool unarchiveOnNewMessage, + Fn onUnarchive); QString currentVersionText(); diff --git a/Telegram/SourceFiles/boxes/about_sponsored_box.cpp b/Telegram/SourceFiles/boxes/about_sponsored_box.cpp index 8c932df6af..e4e3515c04 100644 --- a/Telegram/SourceFiles/boxes/about_sponsored_box.cpp +++ b/Telegram/SourceFiles/boxes/about_sponsored_box.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/file_utilities.h" #include "lang/lang_keys.h" #include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "styles/style_boxes.h" @@ -18,7 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr auto kUrl = "https://promote.telegram.org"_cs; +constexpr auto kUrl = "https://ads.telegram.org"_cs; } // namespace @@ -54,8 +55,16 @@ void AboutSponsoredBox(not_null box) { }; const auto &stLabel = st::aboutLabel; - const auto info1 = box->addRow(object_ptr(box, stLabel)); - info1->setText(tr::lng_sponsored_info_description1(tr::now)); + auto text1 = tr::lng_sponsored_info_description1_linked( + lt_link, + rpl::combine( + tr::lng_sponsored_info_description1_link(), + tr::lng_sponsored_info_description1_url() + ) | rpl::map([](const QString &text, const QString &url) { + return Ui::Text::Link(text, url); + }), + Ui::Text::RichLangValue); + box->addRow(object_ptr(box, std::move(text1), stLabel)); box->addSkip(st::sponsoredUrlButtonSkip); addUrl(); diff --git a/Telegram/SourceFiles/boxes/abstract_box.h b/Telegram/SourceFiles/boxes/abstract_box.h index d48e93108f..119875a293 100644 --- a/Telegram/SourceFiles/boxes/abstract_box.h +++ b/Telegram/SourceFiles/boxes/abstract_box.h @@ -37,11 +37,11 @@ void showBox( } // namespace internal template -QPointer show( +base::weak_qptr show( object_ptr content, Ui::LayerOptions options = Ui::LayerOption::CloseOther, anim::type animated = anim::type::normal) { - auto result = QPointer(content.data()); + auto result = base::weak_qptr(content.data()); internal::showBox(std::move(content), options, animated); return result; } diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 792eff79b4..6e9bc23955 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -194,16 +194,16 @@ void ShowAddParticipantsError( && channel->canAddAdmins()) { const auto makeAdmin = [=](Fn close) { const auto user = forbidden.users.front(); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto done = [=](auto&&...) { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->uiShow()->showToast( tr::lng_box_done(tr::now)); strong->closeBox(); } }; const auto fail = [=] { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -446,7 +446,7 @@ void AddContactBox::save() { firstName = lastName; lastName = QString(); } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto session = _session; _sentName = firstName; _contactId = base::RandomValue(); @@ -716,7 +716,7 @@ TimeId GroupInfoBox::ttlPeriod() const { } void GroupInfoBox::createGroup( - QPointer selectUsersBox, + base::weak_qptr selectUsersBox, const QString &title, const std::vector> &users) { if (_creationRequestId) { @@ -750,8 +750,8 @@ void GroupInfoBox::createGroup( _creationRequestId = 0; const auto controller = _navigation->parentController(); if (type == u"NO_CHAT_TITLE"_q) { - const auto weak = Ui::MakeWeak(this); - if (const auto strong = selectUsersBox.data()) { + const auto weak = base::make_weak(this); + if (const auto strong = selectUsersBox.get()) { strong->closeBox(); } if (weak) { @@ -792,10 +792,10 @@ void GroupInfoBox::submit() { } else if (_canAddBot) { createGroup(nullptr, title, { not_null(_canAddBot) }); } else { - auto initBox = [title, weak = Ui::MakeWeak(this)]( + auto initBox = [title, weak = base::make_weak(this)]( not_null box) { auto create = [box, title, weak] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->createGroup( box.get(), title, diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index 9bc7de6520..4032653564 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -129,7 +129,7 @@ protected: private: void createChannel(const QString &title, const QString &description); void createGroup( - QPointer selectUsersBox, + base::weak_qptr selectUsersBox, const QString &title, const std::vector> &users); void submitName(); diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp index 59729b7fed..8723572da2 100644 --- a/Telegram/SourceFiles/boxes/background_box.cpp +++ b/Telegram/SourceFiles/boxes/background_box.cpp @@ -345,7 +345,7 @@ void BackgroundBox::resetForPeer() { api->applyUpdates(result); }).send(); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _forPeer->setWallPaper({}); if (weak) { _controller->finishChatThemeEdit(_forPeer); @@ -358,7 +358,7 @@ bool BackgroundBox::forChannel() const { void BackgroundBox::removePaper(const Data::WallPaper &paper) { const auto session = &_controller->session(); - const auto remove = [=, weak = Ui::MakeWeak(this)](Fn &&close) { + const auto remove = [=, weak = base::make_weak(this)](Fn &&close) { close(); if (weak) { weak->_inner->removePaper(paper); @@ -451,12 +451,12 @@ auto BackgroundBox::Inner::resolveResetCustomPaper() const return {}; } const auto nonCustom = Window::Theme::Background()->paper(); - const auto themeEmoji = _forPeer->themeEmoji(); - if (forChannel() || themeEmoji.isEmpty()) { + const auto themeToken = _forPeer->themeToken(); + if (forChannel() || themeToken.isEmpty()) { return nonCustom; } const auto &themes = _forPeer->owner().cloudThemes(); - const auto theme = themes.themeForEmoji(themeEmoji); + const auto theme = themes.themeForToken(themeToken); if (!theme) { return nonCustom; } diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 0faa677720..24d4fd1835 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -161,7 +161,7 @@ constexpr auto kMaxWallPaperSlugLength = 255; return paper; } const auto &themes = session->data().cloudThemes(); - if (const auto theme = themes.themeForEmoji(paper.emojiId())) { + if (const auto theme = themes.themeForToken(paper.emojiId())) { using Type = Data::CloudThemeType; const auto type = dark ? Type::Dark : Type::Light; const auto i = theme->settings.find(type); @@ -241,12 +241,12 @@ BackgroundPreviewBox::BackgroundPreviewBox( }, lifetime()); const auto prepare = [=](bool dark, auto pointer) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); crl::async([=] { auto result = std::make_unique(); Window::Theme::PreparePaletteCallback(dark, {})(*result); crl::on_main([=, result = std::move(result)]() mutable { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->*pointer = std::move(result); strong->paletteReady(); } @@ -685,7 +685,7 @@ void BackgroundPreviewBox::checkLevelForChannel() { const auto show = _controller->uiShow(); _forPeerLevelCheck = true; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); CheckBoostLevel(show, _forPeer, [=](int level) { if (!weak) { return std::optional(); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index c159627e71..4d3512711b 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -758,7 +758,7 @@ backgroundCheckbox: Checkbox(defaultCheckbox) { textFg: msgServiceFg; textFgActive: msgServiceFg; - width: -50px; + width: -10px; margin: margins(0px, 0px, 0px, 0px); textPosition: point(0px, 6px); @@ -1116,8 +1116,16 @@ moderateBoxExpandInnerSkip: 2px; moderateBoxExpandFont: font(11px); moderateBoxExpandToggleSize: 4px; moderateBoxExpandToggleFourStrokes: 3px; -moderateBoxExpandIcon: icon{{ "info/edit/expand_arrow_small-flip_vertical", windowActiveTextFg }}; -moderateBoxExpandIconDown: icon{{ "info/edit/expand_arrow_small", windowActiveTextFg }}; +moderateBoxExpandIcon: IconEmoji{ + icon: icon{{ "info/edit/expand_arrow_small-flip_vertical", windowActiveTextFg }}; + padding: margins(-2px, -1px, 0px, 0px); + useIconColor: true; +} +moderateBoxExpandIconDown: IconEmoji{ + icon: icon{{ "info/edit/expand_arrow_small", windowActiveTextFg }}; + padding: margins(-2px, -1px, 0px, 0px); + useIconColor: true; +} moderateBoxDividerLabel: FlatLabel(boxDividerLabel) { palette: TextPalette(defaultTextPalette) { selectLinkFg: windowActiveTextFg; diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index b2bc215cd3..9f824b8486 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/rect.h" #include "ui/wrap/slide_wrap.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -119,16 +120,12 @@ void DeleteMessagesBox::prepare() { Ui::Text::RichLangValue); deleteStyle = &st::attentionBoxButton; } else if (_wipeHistoryJustClear) { - const auto isChannel = peer->isBroadcast(); - const auto isPublicGroup = peer->isMegagroup() - && peer->asChannel()->isPublic(); - if (isChannel || isPublicGroup) { - canDelete = false; - } - details.text = isChannel - ? tr::lng_no_clear_history_channel(tr::now) - : isPublicGroup - ? tr::lng_no_clear_history_group(tr::now) + _revokeJustClearForChannel = true; + details.text = (peer->isChannel() && !peer->isMegagroup()) + ? tr::lng_sure_delete_channel_history( + tr::now, + lt_channel, + peer->name()) : peer->isSelf() ? tr::lng_sure_delete_saved_messages(tr::now) : peer->isUser() @@ -164,7 +161,8 @@ void DeleteMessagesBox::prepare() { } deleteStyle = &st::attentionBoxButton; } - if (auto revoke = revokeText(peer)) { + if (_revokeJustClearForChannel) { + } else if (auto revoke = revokeText(peer)) { _revoke.create( this, revoke->checkbox, @@ -234,12 +232,14 @@ void DeleteMessagesBox::prepare() { search->searchMessages({ .from = _moderateFrom }); } } else { - details.text = (_ids.size() == 1) + details.text = hasSavedMusicMessages() + ? tr::lng_selected_remove_saved_music(tr::now) + : (_ids.size() == 1) ? tr::lng_selected_delete_sure_this(tr::now) : tr::lng_selected_delete_sure(tr::now, lt_count, _ids.size()); if (const auto peer = checkFromSinglePeer()) { auto count = int(_ids.size()); - if (hasScheduledMessages()) { + if (hasScheduledMessages() || hasSavedMusicMessages()) { } else if (auto revoke = revokeText(peer)) { const auto &settings = Core::App().settings(); const auto revokeByDefault @@ -293,6 +293,7 @@ void DeleteMessagesBox::prepare() { } } _text.create(this, rpl::single(std::move(details)), st::boxLabel); + _text->resizeToWidth(st::boxWidth - rect::m::sum::h(st::boxPadding)); if (_wipeHistoryJustClear && _wipeHistoryPeer) { const auto validator = TTLMenu::TTLValidator( @@ -322,28 +323,36 @@ void DeleteMessagesBox::prepare() { addButton(tr::lng_about_done(), [=] { closeBox(); }); } - auto fullHeight = st::boxPadding.top() - + _text->height() - + st::boxPadding.bottom(); - if (_moderateFrom) { - fullHeight += st::boxMediumSkip; - if (_banUser) { - fullHeight += _banUser->heightNoMargins() + st::boxLittleSkip; + const auto &padding = st::boxPadding; + rpl::combine( + widthValue(), + _text->naturalWidthValue() + ) | rpl::start_with_next([=](int full, int) { + _text->resizeToNaturalWidth(full - padding.left() - padding.right()); + + auto fullHeight = st::boxPadding.top() + + _text->height() + + st::boxPadding.bottom(); + if (_moderateFrom) { + fullHeight += st::boxMediumSkip; + if (_banUser) { + fullHeight += _banUser->heightNoMargins() + st::boxLittleSkip; + } + fullHeight += _reportSpam->heightNoMargins(); + if (_deleteAll) { + fullHeight += st::boxLittleSkip + _deleteAll->heightNoMargins(); + } + } else if (_revoke) { + fullHeight += st::boxMediumSkip + _revoke->heightNoMargins(); } - fullHeight += _reportSpam->heightNoMargins(); - if (_deleteAll) { - fullHeight += st::boxLittleSkip + _deleteAll->heightNoMargins(); + if (_autoDeleteSettings) { + fullHeight += st::boxMediumSkip + + _autoDeleteSettings->height() + + st::boxLittleSkip; } - } else if (_revoke) { - fullHeight += st::boxMediumSkip + _revoke->heightNoMargins(); - } - if (_autoDeleteSettings) { - fullHeight += st::boxMediumSkip - + _autoDeleteSettings->height() - + st::boxLittleSkip; - } - setDimensions(st::boxWidth, fullHeight); - _fullHeight = fullHeight; + setDimensions(st::boxWidth, fullHeight); + _fullHeight = fullHeight; + }, lifetime()); } bool DeleteMessagesBox::hasScheduledMessages() const { @@ -357,6 +366,17 @@ bool DeleteMessagesBox::hasScheduledMessages() const { return false; } +bool DeleteMessagesBox::hasSavedMusicMessages() const { + for (const auto &fullId : _ids) { + if (const auto item = _session->data().message(fullId)) { + if (item->isSavedMusicItem()) { + return true; + } + } + } + return false; +} + PeerData *DeleteMessagesBox::checkFromSinglePeer() const { auto result = (PeerData*)nullptr; for (const auto &fullId : _ids) { @@ -530,10 +550,10 @@ void DeleteMessagesBox::deleteAndClear() { ? PaidPostType::None : paidPostType(); if (warnPaidType != PaidPostType::None) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto callback = [=](Fn close) { close(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->_confirmedDeletePaidSuggestedPosts = true; strong->deleteAndClear(); } @@ -562,16 +582,21 @@ void DeleteMessagesBox::deleteAndClear() { !_revoke->checked()); Core::App().saveSettingsDelayed(); } - const auto revoke = _revoke ? _revoke->checked() : _revokeForBot; + const auto revoke = _revoke + ? _revoke->checked() + : (_revokeForBot || _revokeJustClearForChannel); const auto session = _session; const auto invokeCallbackAndClose = [&] { // deleteMessages can initiate closing of the current section, // which will cause this box to be destroyed. - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); + if (hasSavedMusicMessages()) { + uiShow()->showToast(tr::lng_saved_music_removed(tr::now)); + } if (const auto callback = _deleteConfirmedCallback) { callback(); } - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.h b/Telegram/SourceFiles/boxes/delete_messages_box.h index 3762212b0d..635a1fd362 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.h +++ b/Telegram/SourceFiles/boxes/delete_messages_box.h @@ -58,6 +58,7 @@ private: void deleteAndClear(); [[nodiscard]] PeerData *checkFromSinglePeer() const; [[nodiscard]] bool hasScheduledMessages() const; + [[nodiscard]] bool hasSavedMusicMessages() const; [[nodiscard]] std::optional revokeText( not_null peer) const; [[nodiscard]] PaidPostType paidPostType() const; @@ -75,6 +76,7 @@ private: bool _moderateDeleteAll = false; bool _revokeForBot = false; + bool _revokeJustClearForChannel = false; object_ptr _text = { nullptr }; object_ptr _revoke = { nullptr }; diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp index 72cfd29c0d..8881449fcb 100644 --- a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp +++ b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp @@ -282,7 +282,7 @@ auto AddButtonWithLoader( ) | rpl::start_with_next([=](bool toggled) { const auto &state = buttonState->current(); if (toggled && (v::is(state) || v::is(state))) { - const auto weak = Ui::MakeWeak(button); + const auto weak = base::make_weak(button); setLocalLoader(base::make_unique_q( QCoreApplication::instance(), session, diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index cb27997d66..3e48724b7f 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -1075,7 +1075,7 @@ void EditCaptionBox::save() { } void EditCaptionBox::closeAfterSave() { - const auto weak = MakeWeak(this); + const auto weak = base::make_weak(this); if (_saved) { _saved(); } diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 86816ba15b..411f4b1e36 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -8,37 +8,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/edit_privacy_box.h" #include "api/api_global_privacy.h" +#include "apiwrap.h" #include "boxes/filters/edit_filter_chats_list.h" -#include "ui/effects/premium_graphics.h" -#include "ui/layers/generic_box.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/continuous_sliders.h" -#include "ui/widgets/shadow.h" -#include "ui/text/format_values.h" -#include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/painter.h" -#include "ui/vertical_list.h" +#include "boxes/peers/edit_peer_invite_link.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_peer_values.h" +#include "data/data_user.h" #include "history/history.h" -#include "boxes/peer_list_controllers.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" #include "settings/settings_premium.h" #include "settings/settings_privacy_controllers.h" #include "settings/settings_privacy_security.h" -#include "calls/calls_instance.h" -#include "lang/lang_keys.h" -#include "apiwrap.h" -#include "main/main_app_config.h" -#include "main/main_session.h" -#include "data/data_user.h" -#include "data/data_chat.h" -#include "data/data_channel.h" -#include "data/data_peer_values.h" +#include "ui/boxes/peer_qr_box.h" +#include "ui/controls/invite_link_buttons.h" +#include "ui/controls/invite_link_label.h" +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" -#include "styles/style_settings.h" +#include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" #include "styles/style_window.h" namespace { @@ -1293,7 +1298,7 @@ void EditDirectMessagesPriceBox( object_ptr>( box, object_ptr(box)), - {}); + style::margins()); wrap->toggle(savedValue.has_value(), anim::type::instant); wrap->toggleOn(toggle->toggledChanges()); @@ -1312,10 +1317,62 @@ void EditDirectMessagesPriceBox( *result = stars; }, box->lifetime()); + if (const auto username = channel->username(); !username.isEmpty()) { + Ui::AddSkip(inner); + Ui::AddSubsectionTitle( + inner, + tr::lng_manage_monoforum_link_subtitle()); + + constexpr auto kDirectParam = "?direct"_cs; + const auto link = channel->session().createInternalLinkFull(username) + + kDirectParam.utf8(); + const auto copyLink = [=] { + TextUtilities::SetClipboardText(TextForMimeData::Simple(link)); + box->uiShow()->showToast(tr::lng_group_invite_copied(tr::now)); + }; + const auto shareLink = [=] { + box->uiShow()->showBox(ShareInviteLinkBox(channel, link)); + }; + const auto createMenu = [=] { + auto result = base::make_unique_q( + inner, + st::popupMenuWithIcons); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + [=] { + box->uiShow()->showBox(Box([=]( + not_null qrBox) { + Ui::FillPeerQrBox(qrBox, channel, link, nullptr); + })); + }, + &st::menuIconQrCode); + return result; + }; + + auto linkText = Ui::Text::StripUrlProtocol(link); + const auto label = inner->lifetime().make_state( + inner, + rpl::single(std::move(linkText)), + createMenu); + inner->add( + label->take(), + st::inviteLinkFieldPadding); + + label->clicks() | rpl::start_with_next(copyLink, label->lifetime()); + + Ui::AddSkip(inner); + + AddCopyShareLinkButtons(inner, copyLink, shareLink); + Ui::AddSkip(inner); + Ui::AddSkip(inner); + + Ui::AddDivider(inner); + } + box->addButton(tr::lng_settings_save(), [=] { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); callback(toggle->toggled() ? *result : std::optional()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); diff --git a/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp index 65591aaf9a..2f1add532b 100644 --- a/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp @@ -143,6 +143,11 @@ private: int id, TextWithEntities text, anim::type animated); + void insertTask( + int beforeIndex, + int id, + TextWithEntities text, + anim::type animated); void initTaskField(not_null task, TextWithEntities text); void checkLastTask(); void validateState(); @@ -150,6 +155,9 @@ private: void destroy(std::unique_ptr task); void removeDestroyed(not_null field); int findField(not_null field) const; + void handlePaste( + not_null field, + const QStringList &list); not_null _box; not_null _container; @@ -187,6 +195,27 @@ void InitField( options); } +[[nodiscard]] QStringList ParsePastedList(const QString &text) { + auto list = QStringView(text).split('\n'); + for (auto i = list.begin(); i != list.end();) { + auto text = i->trimmed(); + if (text.isEmpty() && (i + 1 != list.end())) { + i = list.erase(i); + } else { + *i++ = text; + } + } + if (list.size() < 2) { + return {}; + } + auto result = QStringList(); + result.reserve(list.size()); + for (const auto &view : list) { + result.push_back(view.toString()); + } + return result; +} + not_null CreateWarningLabel( not_null parent, not_null field, @@ -263,7 +292,7 @@ Tasks::Task::Task( session->user()->isPremium() ? st::createPollOptionFieldPremium : st::createPollOptionField, - Ui::InputField::Mode::NoNewlines, + Ui::InputField::Mode::MultiLine, tr::lng_todo_create_list_add())) , _limit(session->appConfig().todoListItemTextLimit()) { InitField(outer, _field, session); @@ -629,24 +658,35 @@ void Tasks::addTask( int id, TextWithEntities text, anim::type animated) { + insertTask(_list.size(), id, std::move(text), animated); +} + +void Tasks::insertTask( + int beforeIndex, + int id, + TextWithEntities text, + anim::type animated) { if (full()) { return; } + Assert(beforeIndex >= 0 && beforeIndex <= _list.size()); if (_list.size() > 1) { (*(_list.end() - 2))->removePlaceholder(); (*(_list.end() - 2))->toggleRemoveAlways(true); } const auto locked = id && _existingLocked; - _list.push_back(std::make_unique( - _box, - _container, - &_controller->session(), - id, - _position + _list.size() + _destroyed.size(), - locked)); - const auto field = _list.back()->field(); + const auto i = _list.insert( + begin(_list) + beforeIndex, + std::make_unique( + _box, + _container, + &_controller->session(), + id, + _position + beforeIndex + _destroyed.size(), + locked)); + const auto field = i->get()->field(); if (!locked) { - initTaskField(_list.back().get(), std::move(text)); + initTaskField(i->get(), std::move(text)); } else { InitMessageFieldHandlers( _controller, @@ -659,7 +699,7 @@ void Tasks::addTask( }); } field->finishAnimating(); - _list.back()->show(animated); + i->get()->show(animated); fixShadows(); } @@ -706,6 +746,14 @@ void Tasks::initTaskField(not_null task, TextWithEntities text) { }, field->lifetime()); field->changes( ) | rpl::start_with_next([=] { + auto list = ParsePastedList(field->getLastText()); + if (!list.empty()) { + field->setText(list.front()); + field->forceProcessContentsChanges(); + + list.pop_front(); + handlePaste(field, list); + } Ui::PostponeCall(crl::guard(field, [=] { validateState(); })); @@ -793,6 +841,27 @@ int Tasks::findField(not_null field) const { return result; } +void Tasks::handlePaste( + not_null field, + const QStringList &list) { + const auto index = findField(field); + for (auto i = 0, count = int(list.size()); i != count; ++i) { + insertTask( + index + 1 + i, + 0, // id + TextWithEntities{ list[i] }, + anim::type::instant); + } + const auto last = std::min( + int(index + list.size()), + int(_list.size()) - 1); + const auto add = _list[last]->field(); + crl::on_main(add, [=] { + add->setCursorPosition(add->getLastText().size()); + add->setFocus(); + }); +} + void Tasks::checkLastTask() { removeEmptyTail(); addEmptyTask(); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index be4531aec5..7584c9f137 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/filter_icons.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" @@ -593,7 +594,7 @@ void EditFilterBox( ) | rpl::start_with_next([=](const QRect &r) { const auto h = st::normalFont->height; preview->setGeometry( - colors->x(), + rect::right(colors) - st::settingsFilterTagPreviewSkip, r.y() + (r.height() - h) / 2 + st::lineWidth, colors->width(), h); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 90dd083bd1..d420be8351 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -538,25 +538,24 @@ void LinkController::addHeader(not_null container) { const auto isStatic = _filterTitle.isStatic; verticalLayout->add( - object_ptr>( + object_ptr( verticalLayout, - object_ptr( - verticalLayout, - (_data.url.isEmpty() - ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) - : tr::lng_filters_link_share_about( - lt_folder, - rpl::single(Ui::Text::Wrapped( - _filterTitle.text, - EntityType::Bold)), - Ui::Text::WithEntities)), - st::settingsFilterDividerLabel, - st::defaultPopupMenu, - Core::TextContext({ - .session = &_window->session(), - .customEmojiLoopLimit = isStatic ? -1 : 0, - }))), - st::filterLinkDividerLabelPadding); + (_data.url.isEmpty() + ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) + : tr::lng_filters_link_share_about( + lt_folder, + rpl::single(Ui::Text::Wrapped( + _filterTitle.text, + EntityType::Bold)), + Ui::Text::WithEntities)), + st::settingsFilterDividerLabel, + st::defaultPopupMenu, + Core::TextContext({ + .session = &_window->session(), + .customEmojiLoopLimit = isStatic ? -1 : 0, + })), + st::filterLinkDividerLabelPadding, + style::al_top)->setTryMakeSimilarLines(true); verticalLayout->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { @@ -582,7 +581,7 @@ void LinkController::addLinkBlock(not_null container) { using namespace Settings; const auto link = _data.url; - const auto weak = Ui::MakeWeak(container); + const auto weak = base::make_weak(container); const auto copyLink = crl::guard(weak, [=] { CopyInviteLink(delegate()->peerListUiShow(), link); }); diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 8df343cec4..da7aef097f 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -24,7 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_channel_earn.h" @@ -53,9 +52,8 @@ void GiftCreditsBox( Ui::AddSkip(content); const auto &stUser = st::premiumGiftsUserpicButton; const auto userpicWrap = content->add( - object_ptr>( - content, - object_ptr(content, peer, stUser))); + object_ptr(content, peer, stUser), + style::al_top); userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents); Ui::AddSkip(content); Ui::AddSkip(content); @@ -78,19 +76,17 @@ void GiftCreditsBox( u"internal:stars_examples"_q); }); content->add( - object_ptr>( + object_ptr( content, - Ui::CreateLabelWithCustomEmoji( - content, - tr::lng_credits_box_history_entry_gift_out_about( - lt_user, - rpl::single(TextWithEntities{ peer->shortName() }), - lt_link, - std::move(link), - Ui::Text::RichLangValue), - Core::TextContext({ .session = &peer->session() }), - st::creditsBoxAbout)), - st::boxRowPadding); + tr::lng_credits_box_history_entry_gift_out_about( + lt_user, + rpl::single(TextWithEntities{ peer->shortName() }), + lt_link, + std::move(link), + Ui::Text::RichLangValue), + st::creditsBoxAbout), + st::boxRowPadding, + style::al_top); } Ui::AddSkip(content); Ui::AddSkip(box->verticalLayout()); @@ -101,6 +97,7 @@ void GiftCreditsBox( peer, CreditsAmount(), [=] { gifted(); box->uiShow()->hideLayer(); }, + box->showFinishes(), tr::lng_credits_summary_options_subtitle(), {}); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 425e816af4..9d674b9059 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_premium_option.h" #include "apiwrap.h" +#include "base/event_filter.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "base/weak_ptr.h" @@ -31,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. +#include "info/channel_statistics/earn/earn_icons.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" @@ -54,6 +56,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" @@ -75,9 +79,81 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { -constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); +constexpr auto kTooltipDuration = 3 * crl::time(1000); +constexpr auto kPriceTooltipDuration = 6 * crl::time(1000); constexpr auto kHorizontalBar = QChar(0x2015); +struct InfoTooltipData { + not_null parent; + Ui::ImportantTooltip *raw = nullptr; +}; + +void ShowInfoTooltip( + std::shared_ptr data, + not_null target, + rpl::producer text, + int duration) { + if (data->raw) { + data->raw->toggleAnimated(false); + } + const auto parent = data->parent; + const auto tooltip = Ui::CreateChild( + parent, + Ui::MakeNiceTooltipLabel( + parent, + std::move(text), + st::boxWideWidth, + st::defaultImportantTooltipLabel), + st::defaultImportantTooltip); + tooltip->toggleFast(false); + + base::install_event_filter(tooltip, qApp, [=](not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + tooltip->toggleAnimated(false); + } + return base::EventFilterResult::Continue; + }); + + const auto update = [=] { + const auto geometry = Ui::MapFrom(parent, target, target->rect()); + const auto countPosition = [=](QSize size) { + const auto left = geometry.x() + + (geometry.width() - size.width()) / 2; + const auto right = parent->width() + - st::normalFont->spacew; + return QPoint( + std::max(std::min(left, right - size.width()), 0), + geometry.y() - size.height() - st::normalFont->descent); + }; + tooltip->pointAt(geometry, RectPart::Top, countPosition); + }; + parent->widthValue( + ) | rpl::start_with_next(update, tooltip->lifetime()); + + update(); + tooltip->toggleAnimated(true); + + data->raw = tooltip; + tooltip->shownValue() | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next([=] { + crl::on_main(tooltip, [=] { + if (tooltip->isHidden()) { + if (data->raw == tooltip) { + data->raw = nullptr; + } + delete tooltip; + } + }); + }, tooltip->lifetime()); + + base::timer_once( + duration + ) | rpl::start_with_next([=] { + tooltip->toggleAnimated(false); + }, tooltip->lifetime()); +} + [[nodiscard]] QString CreateMessageLink( not_null session, PeerId peerId, @@ -110,6 +186,33 @@ constexpr auto kHorizontalBar = QChar(0x2015); }; } +[[nodiscard]] TextWithEntities FormatValuePrice( + int64 price, + QString currency, + bool approximately = false) { + auto result = TextWithEntities(); + if (approximately) { + result.append('~'); + } + return result.append(Ui::FillAmountAndCurrency(price, currency)); +} + +[[nodiscard]] TextWithEntities FormatValueDate(TimeId date) { + const auto parsed = base::unixtime::parse(date).date(); + const auto day = parsed.day(); + const auto month = parsed.month(); + const auto year = parsed.year(); + return { tr::lng_month_day_year( + tr::now, + lt_month, + Lang::MonthDay(month)(tr::now), + lt_day, + QString::number(day), + lt_year, + QString::number(year)) + }; +} + [[nodiscard]] object_ptr MakeLinkCopyIcon( not_null parent) { auto result = object_ptr(parent); @@ -156,6 +259,61 @@ constexpr auto kHorizontalBar = QChar(0x2015); : st::giveawayGiftCodeValueMultiline)); } +[[nodiscard]] object_ptr MakeValueWithSmallButton( + not_null table, + not_null value, + rpl::producer buttonText, + Fn button)> handler = nullptr, + int topSkip = 0) { + class MarginedWidget final : public Ui::RpWidget { + public: + using RpWidget::RpWidget; + QMargins getMargins() const override { + return { 0, 0, 0, st::giveawayGiftCodePeerMargin.bottom() }; + } + }; + auto result = object_ptr(table); + const auto raw = result.data(); + + value->setParent(raw); + value->show(); + + const auto button = Ui::CreateChild( + raw, + std::move(buttonText), + table->st().smallButton); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + if (handler) { + button->setClickedCallback([button, handler = std::move(handler)] { + handler(button); + }); + } else { + button->setAttribute(Qt::WA_TransparentForMouseEvents); + } + rpl::combine( + raw->widthValue(), + button->widthValue(), + value->naturalWidthValue() + ) | rpl::start_with_next([=](int width, int buttonWidth, int) { + const auto buttonSkip = st::normalFont->spacew + buttonWidth; + value->resizeToNaturalWidth(width - buttonSkip); + value->moveToLeft(0, 0, width); + button->moveToLeft( + rect::right(value) + st::normalFont->spacew, + (topSkip + + (table->st().defaultValue.style.font->ascent + - table->st().smallButton.style.font->ascent)), + width); + }, value->lifetime()); + + value->heightValue() | rpl::start_with_next([=](int height) { + const auto bottom = st::giveawayGiftCodePeerMargin.bottom(); + raw->resize(raw->width(), height + bottom); + }, raw->lifetime()); + + return result; +} + [[nodiscard]] object_ptr MakePeerTableValue( not_null table, std::shared_ptr show, @@ -174,38 +332,18 @@ constexpr auto kHorizontalBar = QChar(0x2015); raw, (button && handler) ? peer->shortName() : peer->name(), table->st().defaultValue); - const auto send = (button && handler) - ? Ui::CreateChild( - raw, - std::move(button), - table->st().smallButton) - : nullptr; - if (send) { - send->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - send->setClickedCallback(std::move(handler)); - } - rpl::combine( - raw->widthValue(), - send ? send->widthValue() : rpl::single(0) - ) | rpl::start_with_next([=](int width, int sendWidth) { + + raw->widthValue() | rpl::start_with_next([=](int width) { const auto position = st::giveawayGiftCodeNamePosition; - const auto sendSkip = sendWidth - ? (st::normalFont->spacew + sendWidth) - : 0; - label->resizeToNaturalWidth(width - position.x() - sendSkip); + label->resizeToNaturalWidth(width - position.x()); 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() - + table->st().defaultValue.style.font->ascent - - table->st().smallButton.style.font->ascent), - width); - } }, label->lifetime()); + label->naturalWidthValue() | rpl::start_with_next([=](int width) { + raw->setNaturalWidth(st::giveawayGiftCodeNamePosition.x() + width); + }, label->lifetime()); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); label->setAttribute(Qt::WA_TransparentForMouseEvents); label->setTextColorOverride(table->st().defaultValue.palette.linkFg->c); @@ -214,7 +352,15 @@ constexpr auto kHorizontalBar = QChar(0x2015); show->showBox(PrepareShortInfoBox(peer, show)); }); - return result; + if (!button || !handler) { + return result; + } + return MakeValueWithSmallButton( + table, + result.release(), + std::move(button), + [=](not_null button) { handler(); }, + st::giveawayGiftCodeNamePosition.y()); } [[nodiscard]] object_ptr MakePeerWithStatusValue( @@ -222,24 +368,21 @@ constexpr auto kHorizontalBar = QChar(0x2015); std::shared_ptr show, PeerId id, Fn, EmojiStatusId)> pushStatusId) { - auto result = object_ptr(table); + auto result = object_ptr(table); const auto raw = result.data(); - const auto &st = st::giveawayGiftCodeUserpic; - raw->resize(raw->width(), st.photoSize); + const auto peerLabel = MakePeerTableValue(table, show, id).release(); + peerLabel->setParent(raw); + peerLabel->show(); - const auto peer = show->session().data().peer(id); - const auto userpic = Ui::CreateChild(raw, peer, st); - const auto label = Ui::CreateChild( - raw, - peer->name(), - table->st().defaultValue); + raw->resize(raw->width(), peerLabel->height()); using namespace Info::Profile; struct State { rpl::variable content; }; - const auto state = label->lifetime().make_state(); + const auto peer = show->session().data().peer(id); + const auto state = peerLabel->lifetime().make_state(); state->content = EmojiStatusIdValue( peer ) | rpl::map([=](EmojiStatusId emojiStatusId) { @@ -252,7 +395,7 @@ constexpr auto kHorizontalBar = QChar(0x2015); .emojiStatusId = emojiStatusId, }; }); - const auto badge = label->lifetime().make_state( + const auto badge = peerLabel->lifetime().make_state( raw, st::infoPeerBadge, &peer->session(), @@ -270,32 +413,19 @@ constexpr auto kHorizontalBar = QChar(0x2015); raw->widthValue(), rpl::single(rpl::empty) | rpl::then(badge->updated()) ) | rpl::start_with_next([=](int width, const auto &) { - const auto position = st::giveawayGiftCodeNamePosition; const auto badgeWidget = badge->widget(); const auto badgeSkip = badgeWidget ? (st::normalFont->spacew + badgeWidget->width()) : 0; - label->resizeToNaturalWidth(width - position.x() - badgeSkip); - label->moveToLeft(position.x(), position.y(), width); - const auto top = (raw->height() - userpic->height()) / 2; - userpic->moveToLeft(0, top, width); + peerLabel->resizeToNaturalWidth(width - badgeSkip); + peerLabel->moveToLeft(0, 0, width); if (badgeWidget) { badgeWidget->moveToLeft( - position.x() + label->width() + st::normalFont->spacew, - (position.y() - + table->st().defaultValue.style.font->ascent - - table->st().smallButton.style.font->ascent), + peerLabel->width() + st::normalFont->spacew, + st::giftBoxByStarsStarTop, width); } - }, label->lifetime()); - - userpic->setAttribute(Qt::WA_TransparentForMouseEvents); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - label->setTextColorOverride(table->st().defaultValue.palette.linkFg->c); - - raw->setClickedCallback([=] { - show->showBox(PrepareShortInfoBox(peer, show)); - }); + }, raw->lifetime()); return result; } @@ -340,7 +470,7 @@ void AddTableRow( not_null table, rpl::producer label, object_ptr value, - style::margins valueMargins) { + style::margins valueMargins = st::giveawayGiftCodeValueMargin) { table->addRow( (label ? object_ptr( @@ -353,55 +483,108 @@ void AddTableRow( valueMargins); } +[[nodiscard]] object_ptr MakePriceWithChangePercentValue( + not_null table, + const std::shared_ptr &value) { + auto label = object_ptr( + table, + rpl::single(FormatValuePrice(value->lastSalePrice, value->currency)), + table->st().defaultValue); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto initial = value->initialSalePrice; + if (!initial) { + return label; + } + + const auto diff = (100 * (value->lastSalePrice - initial)) + / float64(initial); + const auto use = (std::abs(diff) >= 10.) + ? base::SafeRound(diff) + : (int(base::SafeRound(diff * 100)) / 100.); + const auto prefix = (use > 0) ? u"+"_q : QString(); + const auto percent = Lang::FormatExactCountDecimal(use) + '%'; + auto text = rpl::single(prefix + percent); + return MakeValueWithSmallButton(table, label, std::move(text)); +} + +[[nodiscard]] object_ptr MakePriceValueWithTooltip( + not_null table, + std::shared_ptr data, + TextWithEntities price, + TextWithEntities tooltip) { + const auto label = Ui::CreateChild( + table, + rpl::single(price), + table->st().defaultValue); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto handler = [=](not_null button) { + ShowInfoTooltip( + data, + button, + rpl::single(tooltip), + kPriceTooltipDuration); + }; + auto text = rpl::single(u"?"_q); + return MakeValueWithSmallButton(table, label, std::move(text), handler); +} + +[[nodiscard]] object_ptr MakeMinimumPriceValue( + not_null table, + std::shared_ptr tooltip, + const std::shared_ptr &unique) { + const auto &value = unique->value; + const auto text = FormatValuePrice(value->minimumPrice, value->currency); + return MakePriceValueWithTooltip( + table, + std::move(tooltip), + text, + tr::lng_gift_value_minimum_price_tooltip( + tr::now, + lt_amount, + Ui::Text::Bold(text.text), + lt_gift, + Ui::Text::Bold(unique->title), + Ui::Text::WithEntities)); +} + +[[nodiscard]] object_ptr MakeAveragePriceValue( + not_null table, + std::shared_ptr tooltip, + const std::shared_ptr &unique) { + const auto &value = unique->value; + const auto text = FormatValuePrice(value->averagePrice, value->currency); + return MakePriceValueWithTooltip( + table, + std::move(tooltip), + text, + tr::lng_gift_value_average_price_tooltip( + tr::now, + lt_amount, + Ui::Text::Bold(text.text), + lt_gift, + Ui::Text::Bold(unique->title), + Ui::Text::WithEntities)); +} + [[nodiscard]] object_ptr MakeAttributeValue( not_null table, const Data::UniqueGiftAttribute &attribute, Fn, int)> showTooltip) { - auto result = object_ptr(table); - const auto raw = result.data(); - const auto label = Ui::CreateChild( - raw, + table, attribute.name, table->st().defaultValue); - const auto permille = attribute.rarityPermille; - - const auto text = QString::number(permille / 10.) + '%'; - const auto rarity = Ui::CreateChild( - raw, - rpl::single(text), - table->st().smallButton); - rarity->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - - rpl::combine( - raw->widthValue(), - rarity->widthValue() - ) | 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); - rarity->moveToLeft( - label->width() + st::normalFont->spacew, - (table->st().defaultValue.style.font->ascent - - table->st().smallButton.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); - rarity->setClickedCallback([=] { - showTooltip(rarity, permille); - }); + const auto permille = attribute.rarityPermille; + auto text = rpl::single(QString::number(permille / 10.) + '%'); - return result; + const auto handler = [=](not_null button) { + showTooltip(button, permille); + }; + return MakeValueWithSmallButton(table, label, std::move(text), handler); } [[nodiscard]] object_ptr MakeStarGiftStarsValue( @@ -409,113 +592,88 @@ void AddTableRow( std::shared_ptr show, const Data::CreditsHistoryEntry &entry, Fn convertToStars) { - auto result = object_ptr(table); - const auto raw = result.data(); - - const auto star = Ui::CreateSingleStarWidget( - raw, - table->st().defaultValue.style.font->height); - const auto label = Ui::CreateChild( - raw, - Lang::FormatCreditsAmountDecimal(entry.credits), + auto helper = Ui::Text::CustomEmojiHelper(); + const auto price = helper.paletteDependent(Ui::Earn::IconCreditsEmoji( + )).append(' ').append(Lang::FormatCreditsAmountDecimal(entry.credits)); + auto label = object_ptr( + table, + rpl::single(price), table->st().defaultValue, - st::defaultPopupMenu); - - const auto convert = convertToStars - ? Ui::CreateChild( - raw, - tr::lng_gift_sell_small( - lt_count_decimal, - rpl::single(entry.starsConverted * 1.)), - table->st().smallButton) - : nullptr; - if (convert) { - using namespace Ui; - convert->setTextTransform(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; - const auto labelLeft = rect::right(star) + st::normalFont->spacew; - label->resizeToNaturalWidth(width - convertSkip - labelLeft); - star->moveToLeft(0, 0, width); - label->moveToLeft(labelLeft, 0, width); - if (convert) { - convert->moveToLeft( - rect::right(label) + st::normalFont->spacew, - (table->st().defaultValue.style.font->ascent - - table->st().smallButton.style.font->ascent), - width); - } - }, label->lifetime()); - - label->heightValue() | rpl::start_with_next([=](int height) { - raw->resize( - raw->width(), - height + st::giveawayGiftCodeValueMargin.bottom()); - }, raw->lifetime()); - + st::defaultPopupMenu, + helper.context()); label->setAttribute(Qt::WA_TransparentForMouseEvents); - return result; + if (!convertToStars) { + return label; + } + const auto handler = [=](not_null button) { + convertToStars(); + }; + auto text = tr::lng_gift_sell_small( + lt_count_decimal, + rpl::single(entry.starsConverted * 1.)); + return MakeValueWithSmallButton( + table, + label.release(), + std::move(text), + handler); } -[[nodiscard]] object_ptr MakeNonUniqueStatusTableValue( +[[nodiscard]] object_ptr MakeUniqueGiftValueValue( not_null table, - Fn startUpgrade) { - auto result = object_ptr(table); - const auto raw = result.data(); + std::shared_ptr show, + const Data::CreditsHistoryEntry &entry, + Settings::CreditsEntryBoxStyleOverrides st) { + const auto unique = entry.uniqueGift; + const auto value = unique ? unique->value : nullptr; + const auto loading = std::make_shared(false); const auto label = Ui::CreateChild( - raw, - tr::lng_gift_unique_status_non(), + table, + rpl::single( + FormatValuePrice(value->valuePrice, value->currency, true)), table->st().defaultValue, st::defaultPopupMenu); - - const auto upgrade = startUpgrade - ? Ui::CreateChild( - raw, - tr::lng_gift_unique_status_upgrade(), - table->st().smallButton) - : (Ui::RoundButton*)(nullptr); - if (upgrade) { - using namespace Ui; - upgrade->setTextTransform(RoundButton::TextTransform::NoTransform); - upgrade->setClickedCallback(startUpgrade); - } - - rpl::combine( - raw->widthValue(), - upgrade ? upgrade->widthValue() : rpl::single(0) - ) | rpl::start_with_next([=](int width, int toggleWidth) { - const auto toggleSkip = toggleWidth - ? (st::normalFont->spacew + toggleWidth) - : 0; - label->resizeToNaturalWidth(width - toggleSkip); - label->moveToLeft(0, 0, width); - if (upgrade) { - upgrade->moveToLeft( - label->width() + st::normalFont->spacew, - (table->st().defaultValue.style.font->ascent - - table->st().smallButton.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; + const auto handler = [=](not_null button) { + if (value->initialPriceStars) { + show->show(Box(Settings::UniqueGiftValueBox, show, entry, st)); + } else if (*loading) { + return; + } + *loading = true; + show->session().api().request(MTPpayments_GetUniqueStarGiftValueInfo( + MTP_string(unique->slug) + )).done([=](const MTPpayments_UniqueStarGiftValueInfo &result) { + *loading = false; + + const auto &data = result.data(); + value->currency = qs(data.vcurrency()); + value->valuePrice = data.vvalue().v; + value->initialSaleDate = data.vinitial_sale_date().v; + value->initialPriceStars = CreditsAmount( + data.vinitial_sale_stars().v); + value->initialSalePrice = data.vinitial_sale_price().v; + value->lastSaleDate = data.vlast_sale_date().value_or_empty(); + value->lastSalePrice = data.vlast_sale_price().value_or_empty(); + value->lastSaleFragment = data.is_last_sale_on_fragment(); + value->minimumPrice = data.vfloor_price().value_or_empty(); + value->averagePrice = data.vaverage_price().value_or_empty(); + value->forSaleOnTelegram = data.vlisted_count().value_or_empty(); + value->forSaleOnFragment = int( + data.vfragment_listed_count().value_or_empty()); + value->fragmentUrl = qs( + data.vfragment_listed_url().value_or_empty()); + + show->show(Box(Settings::UniqueGiftValueBox, show, entry, st)); + }).send(); + }; + return MakeValueWithSmallButton( + table, + label, + tr::lng_gift_unique_value_learn_more(), + handler); } not_null AddTableRow( @@ -530,11 +688,7 @@ not_null AddTableRow( st::defaultPopupMenu, context); const auto result = widget.data(); - AddTableRow( - table, - std::move(label), - std::move(widget), - st::giveawayGiftCodeValueMargin); + AddTableRow(table, std::move(label), std::move(widget)); return result; } @@ -766,7 +920,8 @@ void GiftCodeBox( std::move(shareLink), Ui::Text::WithEntities)), st::giveawayGiftCodeFooter), - st::giveawayGiftCodeFooterMargin); + st::giveawayGiftCodeFooterMargin, + style::al_top); footer->setClickHandlerFilter([=](const auto &...) { ShareWithFriend(controller, slug); return false; @@ -783,7 +938,7 @@ void GiftCodeBox( close->moveToRight(0, 0); }, box->lifetime()); - const auto button = box->addButton(rpl::conditional( + box->addButton(rpl::conditional( state->used.value(), tr::lng_box_ok(), tr::lng_gift_link_use() @@ -812,15 +967,6 @@ void GiftCodeBox( controller->session().api().premium().applyGiftCode(slug, done); } }); - const auto buttonPadding = st::giveawayGiftCodeBox.buttonPadding; - const auto buttonWidth = st::boxWideWidth - - buttonPadding.left() - - buttonPadding.right(); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); } void GiftCodePendingBox( @@ -908,7 +1054,8 @@ void GiftCodePendingBox( box, tr::lng_gift_link_pending_footer(), st::giveawayGiftCodeFooter), - st::giveawayGiftCodeFooterMargin); + st::giveawayGiftCodeFooterMargin, + style::al_top); const auto close = Ui::CreateChild( box.get(), @@ -920,16 +1067,7 @@ void GiftCodePendingBox( close->moveToRight(0, 0); }, box->lifetime()); - const auto button = box->addButton(tr::lng_close(), closeCallback); - const auto buttonPadding = st::giveawayGiftCodeBox.buttonPadding; - const auto buttonWidth = st::boxWideWidth - - buttonPadding.left() - - buttonPadding.right(); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); + box->addButton(tr::lng_close(), closeCallback); } void ResolveGiftCode( @@ -1013,12 +1151,11 @@ void GiveawayInfoBox( label->setTextColorOverride(st::windowActiveTextFg->c); } const auto result = box->addRow( - object_ptr>>( + object_ptr>( box.get(), - object_ptr>( - box.get(), - std::move(label)), - QMargins(0, skip, 0, skip))); + std::move(label), + QMargins(0, skip, 0, skip)), + style::al_justify); result->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(result); p.setPen(Qt::NoPen); @@ -1211,7 +1348,8 @@ void GiveawayInfoBox( : tr::lng_prizes_cancelled()), st::giveawayRefundedLabel), st::giveawayRefundedPadding), - { padding.left(), 0, padding.right(), padding.bottom() }); + { padding.left(), 0, padding.right(), padding.bottom() }, + style::al_top); const auto bg = wrap->lifetime().make_state( st::boxRadius, st::attentionBoxButton.textBgOver); @@ -1281,64 +1419,13 @@ void AddStarGiftTable( const auto giftToChannel = entry.giftChannelSavedId && peerIsChannel(PeerId(entry.bareEntryOwnerId)); - const auto raw = std::make_shared(nullptr); + const auto tooltip = std::make_shared(InfoTooltipData{ + .parent = container, + }); const auto showTooltip = [=]( not_null widget, rpl::producer text) { - if (*raw) { - (*raw)->toggleAnimated(false); - } - const auto tooltip = Ui::CreateChild( - container, - Ui::MakeNiceTooltipLabel( - container, - std::move(text), - st::boxWideWidth, - st::defaultImportantTooltipLabel), - st::defaultImportantTooltip); - tooltip->toggleFast(false); - - const auto update = [=] { - const auto geometry = Ui::MapFrom( - container, - widget, - widget->rect()); - const auto countPosition = [=](QSize size) { - const auto left = geometry.x() - + (geometry.width() - size.width()) / 2; - const auto right = container->width() - - st::normalFont->spacew; - return QPoint( - std::max(std::min(left, right - size.width()), 0), - geometry.y() - size.height() - st::normalFont->descent); - }; - tooltip->pointAt(geometry, RectPart::Top, countPosition); - }; - container->widthValue( - ) | rpl::start_with_next(update, tooltip->lifetime()); - - update(); - tooltip->toggleAnimated(true); - - *raw = tooltip; - tooltip->shownValue() | rpl::filter( - !rpl::mappers::_1 - ) | rpl::start_with_next([=] { - crl::on_main(tooltip, [=] { - if (tooltip->isHidden()) { - if (*raw == tooltip) { - *raw = nullptr; - } - delete tooltip; - } - }); - }, tooltip->lifetime()); - - base::timer_once( - kRarityTooltipDuration - ) | rpl::start_with_next([=] { - tooltip->toggleAnimated(false); - }, tooltip->lifetime()); + ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration); }; if (unique && entry.bareGiftResaleRecipientId) { @@ -1395,8 +1482,7 @@ void AddStarGiftTable( AddTableRow( table, tr::lng_gift_unique_owner(), - std::move(label), - st::giveawayGiftCodeValueMargin); + std::move(label)); } } else if (giftToChannel) { AddTableRow( @@ -1457,8 +1543,6 @@ void AddStarGiftTable( tr::lng_gift_link_label_date(), rpl::single(Ui::Text::WithEntities(langDateTime(entry.date)))); } - const auto marginWithButton = st::giveawayGiftCodeValueMargin - - QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom()); if (unique) { const auto showRarity = [=]( not_null widget, @@ -1472,18 +1556,15 @@ void AddStarGiftTable( AddTableRow( table, tr::lng_gift_unique_model(), - MakeAttributeValue(table, unique->model, showRarity), - marginWithButton); + MakeAttributeValue(table, unique->model, showRarity)); AddTableRow( table, tr::lng_gift_unique_backdrop(), - MakeAttributeValue(table, unique->backdrop, showRarity), - marginWithButton); + MakeAttributeValue(table, unique->backdrop, showRarity)); AddTableRow( table, tr::lng_gift_unique_symbol(), - MakeAttributeValue(table, unique->pattern, showRarity), - marginWithButton); + MakeAttributeValue(table, unique->pattern, showRarity)); } else { AddTableRow( table, @@ -1492,8 +1573,7 @@ void AddStarGiftTable( table, show, entry, - std::move(convertToStars)), - marginWithButton); + std::move(convertToStars))); } if (entry.limitedCount > 0 && !entry.giftRefunded) { auto amount = rpl::single(TextWithEntities{ @@ -1525,10 +1605,15 @@ void AddStarGiftTable( AddTableRow( table, tr::lng_gift_unique_status(), - MakeNonUniqueStatusTableValue(table, std::move(startUpgrade)), - marginWithButton); + tr::lng_gift_unique_status_non(Ui::Text::WithEntities)); } if (unique) { + if (unique->value) { + AddTableRow( + table, + tr::lng_gift_unique_value(), + MakeUniqueGiftValueValue(table, show, entry, st)); + } const auto &original = unique->originalDetails; if (original.recipientId) { const auto owner = &show->session().data(); @@ -1674,7 +1759,8 @@ void AddCreditsHistoryEntryTable( show, peerId); } - if (actorId || (!entry.starrefCommission && peerId)) { + if (!entry.postsSearch + && (actorId || (!entry.starrefCommission && peerId))) { auto text = entry.starrefCommission ? tr::lng_credits_box_history_entry_referred() : entry.in @@ -1712,8 +1798,7 @@ void AddCreditsHistoryEntryTable( (entry.reaction ? tr::lng_credits_box_history_entry_message : tr::lng_credits_box_history_entry_media)(), - std::move(label), - st::giveawayGiftCodeValueMargin); + std::move(label)); } } using Type = Data::CreditsHistoryEntry::PeerType; @@ -1820,8 +1905,7 @@ void AddCreditsHistoryEntryTable( AddTableRow( table, tr::lng_credits_box_history_entry_id(), - std::move(label), - st::giveawayGiftCodeValueMargin); + std::move(label)); } if (entry.floodSkip) { AddTableRow( @@ -2009,7 +2093,71 @@ void AddChannelEarnTable( AddTableRow( table, tr::lng_credits_box_history_entry_id(), - std::move(label), - st::giveawayGiftCodeValueMargin); + std::move(label)); + } +} + +void AddUniqueGiftValueTable( + std::shared_ptr show, + not_null container, + Settings::CreditsEntryBoxStyleOverrides st, + const Data::CreditsHistoryEntry &entry) { + const auto value = entry.uniqueGift ? entry.uniqueGift->value : nullptr; + auto table = container->add( + object_ptr( + container, + st.table ? *st.table : st::giveawayGiftCodeTable), + st::giveawayGiftCodeTableMargin); + if (value->initialSaleDate) { + AddTableRow( + table, + tr::lng_gift_value_initial_sale(), + rpl::single(FormatValueDate(value->initialSaleDate))); + } + auto helper = Ui::Text::CustomEmojiHelper(); + auto starIcon = helper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); + AddTableRow( + table, + tr::lng_gift_value_initial_price(), + tr::lng_gift_value_initial_price_value( + lt_stars, + rpl::single(starIcon.append(' ').append( + Lang::FormatCreditsAmountDecimal(value->initialPriceStars) + )), + lt_amount, + rpl::single(FormatValuePrice( + value->initialSalePrice, + value->currency, + true)), + Ui::Text::WithEntities), + helper.context()); + if (value->lastSaleDate) { + AddTableRow( + table, + tr::lng_gift_value_last_sale(), + rpl::single(FormatValueDate(value->lastSaleDate))); + } + if (value->lastSalePrice) { + AddTableRow( + table, + tr::lng_gift_value_last_price(), + MakePriceWithChangePercentValue(table, value)); + } + + const auto tooltip = std::make_shared(InfoTooltipData{ + .parent = container, + }); + if (value->minimumPrice) { + AddTableRow( + table, + tr::lng_gift_value_minimum_price(), + MakeMinimumPriceValue(table, tooltip, entry.uniqueGift)); + } + if (value->averagePrice) { + AddTableRow( + table, + tr::lng_gift_vlaue_average_price(), + MakeAveragePriceValue(table, tooltip, entry.uniqueGift)); } } diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index df0c17c1c1..a46bf92c64 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -106,3 +106,9 @@ void AddChannelEarnTable( std::shared_ptr show, not_null container, const Data::CreditsHistoryEntry &entry); + +void AddUniqueGiftValueTable( + std::shared_ptr show, + not_null container, + Settings::CreditsEntryBoxStyleOverrides st, + const Data::CreditsHistoryEntry &entry); diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index bf8c08c3ea..1b493eca61 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -403,22 +403,10 @@ void CreateModerateMessagesBox( const auto container = wrap->entity(); wrap->toggle(false, anim::type::instant); - const auto session = &participants.front()->session(); - const auto emojiMargin = QMargins( - -st::moderateBoxExpandInnerSkip, - -st::moderateBoxExpandInnerSkip / 2, - 0, - 0); - const auto emojiUp = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::moderateBoxExpandIcon, - emojiMargin, - false)); - const auto emojiDown = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::moderateBoxExpandIconDown, - emojiMargin, - false)); + const auto emojiUp = Ui::Text::IconEmoji( + &st::moderateBoxExpandIcon); + const auto emojiDown = Ui::Text::IconEmoji( + &st::moderateBoxExpandIconDown); auto label = object_ptr( inner, @@ -463,9 +451,7 @@ void CreateModerateMessagesBox( Ui::Text::WithEntities); }) | rpl::flatten_latest( ) | rpl::start_with_next([=](const TextWithEntities &text) { - raw->setMarkedText( - Ui::Text::Link(text, u"internal:"_q), - Core::TextContext({ .session = session })); + raw->setMarkedText(Ui::Text::Link(text, u"internal:"_q)); }, label->lifetime()); Ui::AddSkip(inner); diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index 96d5f2d332..a235dc2d7a 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -49,7 +49,7 @@ void SetCloudPassword( session->api().cloudPassword().state( ) | rpl::start_with_next([=] { using namespace Settings; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); if (CheckEditCloudPassword(session)) { box->getDelegate()->show( EditCloudPasswordBox(session)); @@ -103,11 +103,11 @@ void StartPendingReset( not_null session, not_null context, Fn close) { - const auto weak = Ui::MakeWeak(context.get()); + const auto weak = base::make_weak(context.get()); auto lifetime = std::make_shared(); auto finish = [=](const QString &message) mutable { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { if (!message.isEmpty()) { strong->getDelegate()->show(Ui::MakeInformBox(message)); } @@ -136,7 +136,7 @@ void StartPendingReset( : hours ? tr::lng_hours(tr::now, lt_count, hours) : tr::lng_minutes(tr::now, lt_count, minutes); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->getDelegate()->show(Ui::MakeInformBox( tr::lng_cloud_password_reset_later( tr::now, @@ -447,7 +447,7 @@ void PasscodeBox::recoverPasswordDone( _replacedBy->closeBox(); } _setRequest = 0; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _newAuthorization.fire_copy(result); if (weak) { _newPasswordSet.fire_copy(newPasswordBytes); @@ -466,7 +466,7 @@ void PasscodeBox::setPasswordDone(const QByteArray &newPasswordBytes) { _replacedBy->closeBox(); } _setRequest = 0; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _newPasswordSet.fire_copy(newPasswordBytes); if (weak) { auto text = _reenterPasscode->isHidden() @@ -567,9 +567,9 @@ void PasscodeBox::validateEmail( } else if (error.type() == u"CODE_INVALID"_q) { errors->fire(tr::lng_signin_wrong_code(tr::now)); } else if (error.type() == u"EMAIL_HASH_EXPIRED"_q) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _clearUnconfirmedPassword.fire({}); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->getDelegate()->show( Ui::MakeInformBox( Lang::Hard::EmailConfirmationExpired()), @@ -607,7 +607,7 @@ void PasscodeBox::validateEmail( box->boxClosing( ) | rpl::filter([=] { return !*set; - }) | start_with_next([=, weak = Ui::MakeWeak(this)] { + }) | start_with_next([=, weak = base::make_weak(this)] { if (weak) { weak->_clearUnconfirmedPassword.fire({}); } @@ -710,7 +710,7 @@ void PasscodeBox::save(bool force) { } } else { closeReplacedBy(); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); cSetPasscodeBadTries(0); _session->domain().local().setPasscode(pwd.toUtf8()); Core::App().localPasscodeChanged(); @@ -742,7 +742,7 @@ void PasscodeBox::submitOnlyCheckCloudPassword(const QString &oldPassword) { void PasscodeBox::sendOnlyCheckCloudPassword(const QString &oldPassword) { checkPassword(oldPassword, [=](const Core::CloudPasswordResult &check) { if (const auto onstack = _cloudFields.customCheckCallback) { - onstack(check, Ui::MakeWeak(this)); + onstack(check, base::make_weak(this)); } else { Assert(_cloudFields.turningOff); sendClearCloudPassword(check); @@ -1105,7 +1105,7 @@ void PasscodeBox::recover() { return; } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto box = getDelegate()->show(Box( &_api.instance(), _session, @@ -1343,7 +1343,7 @@ void RecoverBox::proceedToChange(const QString &code) { box->boxClosing( ) | rpl::start_with_next([=] { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); if (const auto onstack = _closeParent) { onstack(); } @@ -1399,7 +1399,7 @@ RecoveryEmailValidation ConfirmRecoveryEmail( const auto errors = std::make_shared>(); const auto resent = std::make_shared>(); const auto requestId = std::make_shared(0); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto reloads = std::make_shared>(); const auto cancels = std::make_shared>(); diff --git a/Telegram/SourceFiles/boxes/passcode_box.h b/Telegram/SourceFiles/boxes/passcode_box.h index 09f3335617..5ab6413fc2 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.h +++ b/Telegram/SourceFiles/boxes/passcode_box.h @@ -53,7 +53,7 @@ public: // Check cloud password for some action. using CustomCheck = Fn)>; + base::weak_qptr)>; CustomCheck customCheckCallback; rpl::producer customTitle; std::optional customDescription; @@ -158,7 +158,7 @@ private: QString _pattern; - QPointer _replacedBy; + base::weak_qptr _replacedBy; bool _turningOff = false; bool _cloudPwd = false; CloudFields _cloudFields; diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 41b85c2810..792b087991 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -1470,7 +1470,8 @@ void PeerListContent::setSearchMode(PeerListSearchMode mode) { _loadingAnimation = Ui::CreateLoadingPeerListItemWidget( this, _st.item, - 2); + 2, + _controller->computeListSt().bg->c); } } } else { diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 37ab361a8f..0f0a8e0da3 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -831,7 +831,7 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { auto guard = base::make_weak(this); const auto peer = row->peer(); if (const auto forum = peer->forum()) { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); auto callback = [=](not_null topic) { const auto exists = guard.get(); if (!exists) { @@ -870,7 +870,7 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { delegate()->peerListUiShow()->showBox(std::move(owned)); return; } else if (const auto monoforum = peer->monoforum()) { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); auto callback = [=](not_null sublist) { const auto exists = guard.get(); if (!exists) { diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index ab05d9bccb..a5f2ec3c30 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -233,7 +233,8 @@ void FillUpgradeToPremiumCover( container, rpl::single(text), st::inviteForbiddenInfo), - st::inviteForbiddenInfoPadding); + st::inviteForbiddenInfoPadding, + style::al_top); } void SimpleForbiddenBox( @@ -511,7 +512,8 @@ void InviteForbiddenController::setComplexCover() { if (_can) { container->add( MakeShowOrLabel(container, tr::lng_invite_upgrade_or()), - st::inviteForbiddenOrLabelPadding); + st::inviteForbiddenOrLabelPadding, + style::al_justify); } container->add( object_ptr( @@ -520,7 +522,8 @@ void InviteForbiddenController::setComplexCover() { ? tr::lng_invite_upgrade_via_title() : tr::lng_via_link_cant()), st::inviteForbiddenTitle), - st::inviteForbiddenTitlePadding); + st::inviteForbiddenTitlePadding, + style::al_top); const auto about = _can ? (_peer->isBroadcast() @@ -544,7 +547,8 @@ void InviteForbiddenController::setComplexCover() { container, rpl::single(about), st::inviteForbiddenInfo), - st::inviteForbiddenInfoPadding); + st::inviteForbiddenInfoPadding, + style::al_top); } delegate()->peerListSetAboveWidget(std::move(cover)); } @@ -898,9 +902,9 @@ bool AddParticipantsBoxController::needsInviteLinkButton() { return _peer->asChat()->canHaveInviteLink(); } -QPointer AddParticipantsBoxController::showBox( +base::weak_qptr AddParticipantsBoxController::showBox( object_ptr box) const { - const auto weak = Ui::MakeWeak(box.data()); + const auto weak = base::make_weak(box.data()); delegate()->peerListUiShow()->showBox(std::move(box)); return weak; } @@ -976,7 +980,7 @@ void AddParticipantsBoxController::inviteSelectedUsers( tr::lng_participant_invite_history(), true, st::defaultBoxCheckbox); - const auto weak = Ui::MakeWeak(checkbox.data()); + const auto weak = base::make_weak(checkbox.data()); auto text = (users.size() == 1) ? tr::lng_participant_invite_sure( @@ -1205,9 +1209,9 @@ void AddSpecialBoxController::migrate( _additional.migrate(chat, channel); } -QPointer AddSpecialBoxController::showBox( +base::weak_qptr AddSpecialBoxController::showBox( object_ptr box) const { - const auto weak = Ui::MakeWeak(box.data()); + const auto weak = base::make_weak(box.data()); delegate()->peerListUiShow()->showBox(std::move(box)); return weak; } diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.h b/Telegram/SourceFiles/boxes/peers/add_participants_box.h index 24d5e1031d..dc06e8040f 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.h @@ -56,7 +56,7 @@ private: base::flat_set> &&alreadyIn, bool justCreated); - QPointer showBox(object_ptr box) const; + base::weak_qptr showBox(object_ptr box) const; void addInviteLinkButton(); void inviteSelectedUsers( @@ -147,7 +147,7 @@ private: void subscribeToMigration(); void migrate(not_null chat, not_null channel); - QPointer showBox(object_ptr box) const; + base::weak_qptr showBox(object_ptr box) const; not_null _peer; MTP::Sender _api; @@ -158,7 +158,7 @@ private: ParticipantsAdditionalData _additional; std::unique_ptr _onlineSorter; Ui::BoxPointer _editBox; - QPointer _editParticipantBox; + base::weak_qptr _editParticipantBox; AdminDoneCallback _adminDoneCallback; BannedDoneCallback _bannedDoneCallback; diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 7257a3c3f8..ca10840712 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -246,10 +246,10 @@ object_ptr CreatePeerByQueryBox( not_null bot, RequestPeerQuery query, Fn>)> done) { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); auto callback = [=](not_null peer) { done({ peer }); - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -506,11 +506,11 @@ void ShowChoosePeerBox( }); return; } - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); auto callback = [=, done = std::move(chosen)]( std::vector> peers) { done(std::move(peers)); - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp index 4bc2848c78..cc378e6095 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp @@ -37,7 +37,7 @@ QString UserPhone(not_null user) { } void SendRequest( - QPointer box, + base::weak_qptr box, not_null user, bool sharePhone, const QString &first, @@ -219,7 +219,7 @@ void Controller::initNameFields( } }; SendRequest( - Ui::MakeWeak(_box), + base::make_weak(_box), user, _sharePhone && _sharePhone->checked(), firstValue, diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index 45ad6b1931..e651ae7d05 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -77,7 +77,9 @@ DefaultIconEmoji::DefaultIconEmoji( std::move(value) | rpl::start_with_next([=](DefaultIcon value) { _icon = value; _image = QImage(); - repaint(); + if (repaint) { + repaint(); + } }, _lifetime); } @@ -554,7 +556,7 @@ void EditForumTopicBox( } else { using Flag = MTPchannels_EditForumTopic::Flag; const auto api = &forum->session().api(); - const auto weak = Ui::MakeWeak(box.get()); + const auto weak = base::make_weak(box); state->requestId = api->request(MTPchannels_EditForumTopic( MTP_flags(Flag::f_title | (topic->isGeneral() ? Flag() : Flag::f_icon_emoji_id)), @@ -566,11 +568,11 @@ void EditForumTopicBox( MTPBool() // hidden )).done([=](const MTPUpdates &result) { api->applyUpdates(result); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }).fail([=](const MTP::Error &error) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { if (error.type() == u"TOPIC_NOT_MODIFIED") { strong->closeBox(); } else { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 50c3254a37..84c47cd928 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -450,9 +450,9 @@ void EditAdminBox::prepare() { if (!_saveCallback) { return; } else if (_addAsAdmin && !_addAsAdmin->checked()) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); AddBotToGroup(show, user(), peer(), _addingBot->token); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } return; @@ -661,7 +661,7 @@ void EditAdminBox::requestTransferPassword(not_null channel) { fields.customSubmitButton = tr::lng_passcode_submit(); fields.customCheckCallback = crl::guard(this, [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { sendTransferRequestFrom(box, channel, result); }); getDelegate()->show(Box(&channel->session(), fields)); @@ -669,13 +669,13 @@ void EditAdminBox::requestTransferPassword(not_null channel) { } void EditAdminBox::sendTransferRequestFrom( - QPointer box, + base::weak_qptr box, not_null channel, const Core::CloudPasswordResult &result) { if (_transferRequestId) { return; } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto user = this->user(); const auto api = &channel->session().api(); _transferRequestId = api->request(MTPchannels_EditCreator( @@ -726,7 +726,7 @@ void EditAdminBox::sendTransferRequestFrom( || type.startsWith(u"PASSWORD_TOO_FRESH_"_q) || type.startsWith(u"SESSION_TOO_FRESH_"_q); }(); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); getDelegate()->show(Ui::MakeInformBox(problem)); if (box) { box->closeBox(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h index 1e5c9fd8ae..b67efcd140 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h @@ -104,7 +104,7 @@ private: bool handleTransferPasswordError(const QString &error); void requestTransferPassword(not_null channel); void sendTransferRequestFrom( - QPointer box, + base::weak_qptr box, not_null channel, const Core::CloudPasswordResult &result); bool canSave() const { @@ -124,7 +124,7 @@ private: ChatAdminRightsInfo, const QString &rank)> _saveCallback; - QPointer _confirmBox; + base::weak_qptr _confirmBox; Ui::Checkbox *_addAsAdmin = nullptr; Ui::SlideWrap *_adminControlsWrap = nullptr; Ui::InputField *_rank = nullptr; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 03fbc87c68..15a5f1aee1 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -1324,9 +1324,9 @@ void ParticipantsBoxController::rebuild() { refreshRows(); } -QPointer ParticipantsBoxController::showBox( +base::weak_qptr ParticipantsBoxController::showBox( object_ptr box) const { - const auto weak = Ui::MakeWeak(box.data()); + const auto weak = base::make_weak(box.data()); delegate()->peerListUiShow()->showBox(std::move(box)); return weak; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index b993c740c5..6e14120302 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -240,7 +240,7 @@ private: Role role, not_null additional); - QPointer showBox(object_ptr box) const; + base::weak_qptr showBox(object_ptr box) const; void prepareChatRows(not_null chat); void rebuildChatRows(not_null chat); @@ -304,7 +304,7 @@ private: rpl::variable _fullCountValue; Ui::BoxPointer _editBox; Ui::BoxPointer _addBox; - QPointer _editParticipantBox; + base::weak_qptr _editParticipantBox; std::unique_ptr _stories; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 29019c1091..a13b197a6d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -15,7 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_box.h" #include "boxes/stickers_box.h" #include "chat_helpers/compose/compose_show.h" -#include "core/ui_integration.h" // TextContext #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "data/data_changes.h" @@ -82,7 +81,6 @@ public: bool selected); [[nodiscard]] uint8 index() const; - int naturalWidth() const override; void setSelected(bool selected); @@ -164,7 +162,6 @@ private: void updateText(); const uint32 _level; - const TextWithEntities _icon; const Ui::Text::MarkedContext _context; Ui::Text::String _text; bool _minimal = false; @@ -183,6 +180,15 @@ ColorSample::ColorSample( colorIndex ) | rpl::start_with_next([=](uint8 index) { _index = index; + setNaturalWidth([&] { + if (_name.isEmpty() || _style->colorPatternIndex(_index)) { + return st::settingsColorSampleSize; + } + const auto padding = st::settingsColorSamplePadding; + return std::max( + padding.left() + _name.maxWidth() + padding.right(), + padding.top() + st::semiboldFont->height + padding.bottom()); + }()); update(); }, lifetime()); } @@ -197,6 +203,7 @@ ColorSample::ColorSample( , _index(colorIndex) , _selected(selected) , _simple(true) { + setNaturalWidth(st::settingsColorSampleSize); } void ColorSample::setSelected(bool selected) { @@ -278,16 +285,6 @@ uint8 ColorSample::index() const { return _index; } -int ColorSample::naturalWidth() const { - if (_name.isEmpty() || _style->colorPatternIndex(_index)) { - return st::settingsColorSampleSize; - } - const auto padding = st::settingsColorSamplePadding; - return std::max( - padding.left() + _name.maxWidth() + padding.right(), - padding.top() + st::semiboldFont->height + padding.bottom()); -} - PreviewWrap::PreviewWrap( not_null box, std::shared_ptr style, @@ -460,22 +457,12 @@ LevelBadge::LevelBadge( uint32 level, not_null session) : Ui::RpWidget(parent) -, _level(level) -, _icon(Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::settingsLevelBadgeLock, - QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0), - false))) -, _context(Core::TextContext({ - .session = session, - .repaint = [this] { update(); }, -})) { +, _level(level) { updateText(); } void LevelBadge::updateText() { - auto text = _icon; - text.append(' '); + auto text = Ui::Text::IconEmoji(&st::settingsLevelBadgeLock).append(' '); if (!_minimal) { text.append(tr::lng_boost_level( tr::now, @@ -490,7 +477,7 @@ void LevelBadge::updateText() { st, text, kMarkupTextOptions, - _context); + Ui::Text::MarkedContext{ .repaint = [=] { update(); } }); const auto &padding = st::settingsColorSamplePadding; QWidget::resize( _text.maxWidth() + rect::m::sum::h(padding), @@ -1213,7 +1200,7 @@ void EditPeerColorBox( peer, state->index.value(), state->emojiId.value() - ), {}); + ), style::margins()); auto indices = peer->session().api().peerColors().suggestedValue(); const auto margin = st::settingsColorRadioMargin; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index b8e78881fe..5b59b48f03 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -904,7 +904,7 @@ void Controller::showEditDiscussionLinkBox() { return; } - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto channel = _peer->asChannel(); const auto callback = [=](ChannelData *result) { if (*box) { @@ -1250,12 +1250,12 @@ void Controller::fillAutoTranslateButton() { _autotranslateSavedValue = value; } else if (value) { state->toggled.fire(false); - auto weak = Ui::MakeWeak(autotranslate); + auto weak = base::make_weak(autotranslate); CheckBoostLevel( _navigation->uiShow(), _peer, [=](int level) { - if (const auto strong = weak.data()) { + if (weak.get()) { state->isLocked = (level < requiredLevel); } return (level < requiredLevel) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index aba3714395..3058d4c815 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item_helpers.h" // GetErrorForSending. #include "history/view/history_view_group_call_bar.h" // GenerateUserpics... +#include "info/channel_statistics/earn/earn_icons.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "qr/qr_generate.h" @@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/userpic_button.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -247,6 +249,9 @@ private: const Role _role = Role::Joined; rpl::variable _data; + Ui::Text::CustomEmojiHelper _emojiHelper; + TextWithEntities _creditsEmoji; + base::unique_qptr _menu; rpl::event_stream _processed; @@ -408,6 +413,8 @@ Controller::Controller( const auto current = _data.current(); _link = current.link; _revoked = current.revoked; + _creditsEmoji = _emojiHelper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); } rpl::producer Controller::dataValue() const { @@ -424,7 +431,7 @@ void Controller::addHeaderBlock(not_null container) { const auto revoked = current.revoked; const auto link = current.link; const auto admin = current.admin; - const auto weak = Ui::MakeWeak(container); + const auto weak = base::make_weak(container); const auto copyLink = crl::guard(weak, [=] { CopyInviteLink(delegate()->peerListUiShow(), link); }); @@ -725,7 +732,7 @@ void Controller::setupAboveJoinedWidget() { ? tr::lng_group_invite_subscription_info_title( tr::now, lt_emoji, - session().data().customEmojiManager().creditsEmoji(), + _creditsEmoji, lt_price, { QString::number(current.subscription.credits) }, lt_multiplier, @@ -736,15 +743,12 @@ void Controller::setupAboveJoinedWidget() { : tr::lng_group_invite_subscription_info_title_none( tr::now, lt_emoji, - session().data().customEmojiManager().creditsEmoji(), + _creditsEmoji, lt_price, { QString::number(current.subscription.credits) }, Ui::Text::WithEntities), kMarkupTextOptions, - Core::TextContext({ - .session = &session(), - .repaint = [=] { widget->update(); }, - })); + _emojiHelper.context([=] { widget->update(); })); auto &lifetime = widget->lifetime(); const auto rateValue = lifetime.make_state>( session().credits().rateValue(_peer)); @@ -965,43 +969,41 @@ void Controller::rowClicked(not_null row) { const auto photoSize = st::boostReplaceUserpic.photoSize; const auto session = &row->peer()->session(); - content->add(object_ptr>( - content, - Settings::SubscriptionUserpic(content, channel, photoSize))); + content->add( + Settings::SubscriptionUserpic(content, channel, photoSize), + style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, tr::lng_credits_box_subscription_title(), - st::creditsBoxAboutTitle))); + st::creditsBoxAboutTitle), + style::al_top); Ui::AddSkip(content); const auto subtitle1 = box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - st::creditsTopupPrice)))->entity(); + st::creditsTopupPrice), + style::al_top); subtitle1->setMarkedText( tr::lng_credits_subscription_subtitle( tr::now, lt_emoji, - session->data().customEmojiManager().creditsEmoji(), + _creditsEmoji, lt_cost, { QString::number(data.subscription.credits) }, Ui::Text::WithEntities), - Core::TextContext({ .session = session })); + _emojiHelper.context()); const auto subtitle2 = box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - st::creditsTopupPrice)))->entity(); + st::creditsTopupPrice), + style::al_top); session->credits().rateValue( channel ) | rpl::start_with_next([=, currency = u"USD"_q](float64 rate) { @@ -1023,8 +1025,7 @@ void Controller::rowClicked(not_null row) { Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, tr::lng_credits_box_out_about( @@ -1033,18 +1034,12 @@ void Controller::rowClicked(not_null row) { ) | Ui::Text::ToLink( tr::lng_credits_box_out_about_link(tr::now)), Ui::Text::WithEntities), - st::creditsBoxAboutDivider))); + st::creditsBoxAboutDivider), + style::al_top); - const auto button = box->addButton(tr::lng_box_ok(), [=] { + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); - const auto buttonWidth = st::boxWidth - - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); })); } @@ -1290,7 +1285,7 @@ void AddPermanentLinkBlock( return LinkData{ link.link, link.usage }; }); } - const auto weak = Ui::MakeWeak(container); + const auto weak = base::make_weak(container); const auto copyLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { CopyInviteLink(show, current.link); @@ -1471,7 +1466,7 @@ object_ptr ShareInviteLinkBox( const QString &link, const QString &copied) { const auto sending = std::make_shared(); - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto showToast = [=](const QString &text) { if (*box) { @@ -1551,7 +1546,7 @@ object_ptr ShareInviteLinkBox( .filterCallback = std::move(filterCallback), .moneyRestrictionError = ShareMessageMoneyRestrictionError(), }); - *box = Ui::MakeWeak(object.data()); + *box = base::make_weak(object.data()); return object; } @@ -1573,7 +1568,7 @@ object_ptr EditLinkBox( constexpr auto kPeriod = 3600 * 24 * 30; constexpr auto kTestModePeriod = 300; const auto creating = data.link.isEmpty(); - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); using Fields = Ui::InviteLinkFields; const auto done = [=](Fields result) { const auto finish = [=](Api::InviteLink finished) { @@ -1645,7 +1640,7 @@ object_ptr EditLinkBox( done); } }); - *box = Ui::MakeWeak(object.data()); + *box = base::make_weak(object.data()); return object; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 09b3bdce3c..b008afc25d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -888,32 +888,18 @@ rpl::producer AddSlowmodeSlider( return secondsCount->value(); } -void AddBoostsUnrestrictLabels( - not_null container, - not_null session) { +void AddBoostsUnrestrictLabels(not_null container) { const auto labels = container->add( object_ptr(container, st::normalFont->height), st::slowmodeLabelsMargin); - const auto manager = &session->data().customEmojiManager(); - const auto one = Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - st::boostMessageIcon, - st::boostMessageIconPadding)); - const auto many = Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - st::boostsMessageIcon, - st::boostsMessageIconPadding)); - const auto context = Core::TextContext({ - .session = session, - .customEmojiLoopLimit = 1, - }); + const auto one = Ui::Text::IconEmoji(&st::boostMessageIcon); + const auto many = Ui::Text::IconEmoji(&st::boostsMessageIcon); for (auto i = 0; i != kBoostsUnrestrictValues; ++i) { const auto label = Ui::CreateChild( labels, st::boostsUnrestrictLabel); label->setMarkedText( - TextWithEntities(i ? many : one).append(QString::number(i + 1)), - context); + TextWithEntities(i ? many : one).append(QString::number(i + 1))); rpl::combine( labels->widthValue(), label->widthValue() @@ -979,7 +965,7 @@ rpl::producer AddBoostsUnrestrictSlider( const auto inner = outer->entity(); - AddBoostsUnrestrictLabels(inner, &peer->session()); + AddBoostsUnrestrictLabels(inner); const auto slider = inner->add( object_ptr(inner, st::localStorageLimitSlider), diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index 85be6dd1ce..f76b4cc0b0 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -221,7 +221,8 @@ void Controller::prepare() { Ui::Text::RichLangValue), Ui::Text::RichLangValue), st::boostReassignText), - st::boxRowPadding); + st::boxRowPadding, + style::al_top); delegate()->peerListSetAboveWidget(std::move(above)); const auto now = base::unixtime::now(); diff --git a/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp b/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp index b368f5dc39..f3e71b6f5e 100644 --- a/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp @@ -127,11 +127,11 @@ void Controller::confirmAdd(not_null peer) { return; } state->sent = true; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto description = modify ? state->description : QString(); Setup(bot, peer, description, [=](QString error) { if (error.isEmpty()) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } show->showToast({ @@ -204,10 +204,10 @@ void Controller::confirmRemove(not_null peer) { return; } *sent = true; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); Remove(bot, peer, [=](QString error) { if (error.isEmpty()) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } show->showToast(tr::lng_bot_verify_remove_done(tr::now)); diff --git a/Telegram/SourceFiles/boxes/phone_banned_box.cpp b/Telegram/SourceFiles/boxes/phone_banned_box.cpp index b29388e96c..c1e9fb512d 100644 --- a/Telegram/SourceFiles/boxes/phone_banned_box.cpp +++ b/Telegram/SourceFiles/boxes/phone_banned_box.cpp @@ -49,7 +49,7 @@ Locale: ") + ::Platform::SystemLanguage(); void ShowPhoneBannedError( not_null controller, const QString &phone) { - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto close = [=] { if (*box) { (*box)->closeBox(); diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.cpp b/Telegram/SourceFiles/boxes/pin_messages_box.cpp index c2012bde43..03b08e14d1 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/pin_messages_box.cpp @@ -50,8 +50,8 @@ void PinMessageBox( not_null box, not_null item) { struct State { - QPointer pinForPeer; - QPointer notify; + base::weak_qptr pinForPeer; + base::weak_qptr notify; mtpRequestId requestId = 0; }; @@ -81,7 +81,7 @@ void PinMessageBox( false, st::urlAuthCheckbox); object->setAllowTextLines(); - state->pinForPeer = Ui::MakeWeak(object.data()); + state->pinForPeer = base::make_weak(object.data()); return object; } else if (!pinningOld && (peer->isChat() || peer->isMegagroup()) @@ -92,7 +92,7 @@ void PinMessageBox( false, st::urlAuthCheckbox); object->setAllowTextLines(); - state->notify = Ui::MakeWeak(object.data()); + state->notify = base::make_weak(object.data()); return object; } return { nullptr }; diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index da4120f0f7..f88a525d9f 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -585,14 +585,14 @@ void ChannelsLimitBox( const auto content = box->addRow( object_ptr(box, controller), - {}); + style::margins()); delegate->setContent(content); controller->setDelegate(delegate); const auto count = 100; const auto placeholder = box->addRow( object_ptr(box, count, st::defaultPeerList), - {}); + style::margins()); using namespace rpl::mappers; controller->countValue( @@ -676,14 +676,14 @@ void PublicLinksLimitBox( const auto content = box->addRow( object_ptr(box, controller), - {}); + style::margins()); delegate->setContent(content); controller->setDelegate(delegate); const auto count = defaultLimit; const auto placeholder = box->addRow( object_ptr(box, count, st::defaultPeerList), - {}); + style::margins()); using namespace rpl::mappers; controller->countValue( diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index da6f461d4b..53fa77aee0 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -902,7 +902,7 @@ void PreviewBox( const auto outer = box->addRow( ChatBackPreview(box, size.height(), back), - {}); + style::margins()); struct Hiding { not_null widget; @@ -1079,26 +1079,21 @@ void PreviewBox( auto text = state->selected.value( ) | rpl::map(SectionAbout) | rpl::flatten_latest(); - const auto padding = st::premiumPreviewAboutPadding; - const auto available = size.width() - padding.left() - padding.right(); - auto titleLabel = object_ptr( - box, - std::move(title), - st::premiumPreviewAboutTitle); - titleLabel->resizeToWidth(available); box->addRow( - object_ptr>( + object_ptr( box, - std::move(titleLabel)), - st::premiumPreviewAboutTitlePadding); - auto textLabel = object_ptr( - box, - std::move(text), - st::premiumPreviewAbout); - textLabel->resizeToWidth(available); + std::move(title), + st::premiumPreviewAboutTitle), + st::premiumPreviewAboutTitlePadding, + style::al_top); box->addRow( - object_ptr>(box, std::move(textLabel)), - padding); + object_ptr( + box, + std::move(text), + st::premiumPreviewAbout), + st::premiumPreviewAboutPadding, + style::al_top + )->setTryMakeSimilarLines(true); box->addRow( CreateSwitch(box->verticalLayout(), &state->selected, state->order), st::premiumDotsMargin); diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp index 4c299c162b..ef945ba793 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.cpp +++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp @@ -7,8 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/ringtones_box.h" +#include "data/notify/data_peer_notify_volume.h" +#include "data/notify/data_peer_notify_settings.h" #include "api/api_ringtones.h" #include "apiwrap.h" +#include "ui/widgets/continuous_sliders.h" #include "base/call_delayed.h" #include "base/event_filter.h" #include "base/timer_rpl.h" @@ -21,11 +24,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "data/data_document_resolver.h" #include "data/data_thread.h" +#include "data/data_peer.h" #include "data/data_session.h" #include "data/notify/data_notify_settings.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "main/main_session_settings.h" #include "media/audio/media_audio.h" +#include "ui/wrap/slide_wrap.h" #include "platform/platform_notifications_manager.h" #include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" @@ -111,7 +117,8 @@ void RingtonesBox( not_null box, not_null session, Data::NotifySound selected, - Fn save) { + Fn save, + Data::VolumeController volumeController) { box->setTitle(tr::lng_ringtones_box_title()); const auto container = box->verticalLayout(); @@ -128,12 +135,17 @@ void RingtonesBox( QPointer defaultButton; QPointer chosenButton; std::vector> buttons; + ushort presavedVolume = 0; }; const auto state = container->lifetime().make_state(State{ .group = std::make_shared(), .chosen = selected, }); + const auto volumeOverride = [volume = volumeController.volume] { + return volume ? (0.01 * volume()) : -1; + }; + const auto addToGroup = [=]( not_null verticalLayout, int value, @@ -156,7 +168,10 @@ void RingtonesBox( if (value == kDefaultValue) { state->defaultButton = button; button->setClickedCallback([=] { - Core::App().notifications().playSound(session, 0); + Core::App().notifications().playSound( + session, + 0, + volumeOverride()); }); } if (value < 0) { @@ -170,7 +185,8 @@ void RingtonesBox( if (media->loaded()) { Core::App().notifications().playSound( session, - media->owner()->id); + media->owner()->id, + volumeOverride()); } }); base::install_event_filter(button, [=](not_null e) { @@ -320,6 +336,31 @@ void RingtonesBox( })); }); + if (volumeController.volume && volumeController.saveVolume) { + auto saveAndTestVolume = [=](ushort currentVolume) { + state->presavedVolume = currentVolume; + const auto value = state->group->current(); + if (value != kNoSoundValue) { + Core::App().notifications().playSound( + session, + (value == kDefaultValue) + ? 0 + : state->medias[value]->owner()->id, + 0.01 * currentVolume); + } + }; + Ui::AddRingtonesVolumeSlider( + container, + state->group->value() | rpl::map([=](int value) { + return value != kNoSoundValue; + }), + tr::lng_ringtones_box_volume(), + Data::VolumeController{ + base::take(volumeController.volume), + std::move(saveAndTestVolume), + }); + } + box->addSkip(st::ringtonesBoxSkip); Ui::AddDividerText(container, tr::lng_ringtones_box_about()); @@ -333,6 +374,9 @@ void RingtonesBox( : (value == kNoSoundValue) ? Data::NotifySound{ .none = true } : Data::NotifySound{ .id = state->medias[value]->owner()->id }; + if (state->presavedVolume) { + volumeController.saveVolume(state->presavedVolume); + } save(sound); box->closeBox(); }); @@ -345,5 +389,5 @@ void ThreadRingtonesBox( const auto now = thread->owner().notifySettings().sound(thread); RingtonesBox(box, &thread->session(), now, [=](Data::NotifySound sound) { thread->owner().notifySettings().update(thread, {}, {}, sound); - }); + }, Data::ThreadRingtonesVolumeController(thread)); } diff --git a/Telegram/SourceFiles/boxes/ringtones_box.h b/Telegram/SourceFiles/boxes/ringtones_box.h index 13599efea0..bbc9349767 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.h +++ b/Telegram/SourceFiles/boxes/ringtones_box.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct NotifySound; class Thread; +enum class DefaultNotify : uint8_t; +struct VolumeController; } // namespace Data namespace Main { @@ -28,7 +30,8 @@ void RingtonesBox( not_null box, not_null session, Data::NotifySound selected, - Fn save); + Fn save, + Data::VolumeController volumeController); void ThreadRingtonesBox( not_null box, diff --git a/Telegram/SourceFiles/boxes/self_destruction_box.cpp b/Telegram/SourceFiles/boxes/self_destruction_box.cpp index c4a070c16e..e9946c9f5a 100644 --- a/Telegram/SourceFiles/boxes/self_destruction_box.cpp +++ b/Telegram/SourceFiles/boxes/self_destruction_box.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/passcode_box.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/rect.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" #include "ui/text/text_utilities.h" @@ -56,7 +57,7 @@ void AddDeleteAccount( fields.customSubmitButton = tr::lng_theme_delete(); fields.customCheckCallback = [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { session->api().request(MTPaccount_DeleteAccount( MTP_flags(MTPaccount_DeleteAccount::Flag::f_password), MTP_string("Manual"), @@ -148,6 +149,8 @@ void SelfDestructionBox::showContent() { : tr::lng_self_destruct_sessions_description(tr::now)), st::boxLabel); _description->moveToLeft(st::boxPadding.left(), y); + _description->resizeToWidth(st::boxWidth + - rect::m::sum::h(st::boxPadding)); y += _description->height() + st::boxMediumSkip; for (const auto value : _ttlValues) { @@ -200,6 +203,8 @@ void SelfDestructionBox::prepare() { ? tr::lng_self_destruct_description(tr::now) : tr::lng_self_destruct_sessions_description(tr::now)), st::boxLabel); + fake->resizeToWidth(st::boxWidth + - rect::m::sum::h(st::boxPadding)); const auto boxHeight = st::boxOptionListPadding.top() + fake->height() + st::boxMediumSkip + (_ttlValues.size() diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index 9e33609715..8cfcdc1deb 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -348,9 +348,9 @@ void SendCreditsBox( }, ministarsContainer->lifetime()); } - const auto thumb = box->addRow(object_ptr>( - content, - SendCreditsThumbnail(content, session, form.get(), photoSize))); + const auto thumb = box->addRow( + SendCreditsThumbnail(content, session, form.get(), photoSize), + style::al_top); thumb->setAttribute(Qt::WA_TransparentForMouseEvents); if (form->invoice.subscriptionPeriod) { const auto badge = SendCreditsBadge(content, form->invoice.amount); @@ -364,31 +364,30 @@ void SendCreditsBox( } Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, form->invoice.subscriptionPeriod ? rpl::single(form->title) : tr::lng_credits_box_out_title(), - st::settingsPremiumUserTitle))); + st::settingsPremiumUserTitle), + style::al_top); if (form->invoice.subscriptionPeriod && form->botId && form->photo) { Ui::AddSkip(content); Ui::AddSkip(content); const auto bot = session->data().user(form->botId); box->addRow( - object_ptr>( - box, - Ui::CreatePeerBubble(box, bot))); + Ui::CreatePeerBubble(box, bot), + style::al_top); Ui::AddSkip(content); } Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, SendCreditsConfirmText(session, form.get()), - st::creditsBoxAbout))); + st::creditsBoxAbout), + style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); @@ -397,7 +396,7 @@ void SendCreditsBox( return; } const auto show = box->uiShow(); - const auto weak = MakeWeak(box.get()); + const auto weak = base::make_weak(box.get()); state->confirmButtonBusy = true; session->api().request( MTPpayments_SendStarsForm( @@ -422,7 +421,7 @@ void SendCreditsBox( auto error = ::Ui::MakeInformBox( tr::lng_payments_precheckout_stars_failed(tr::now)); error->boxClosing() | rpl::start_with_next([=] { - if (const auto paybox = weak.data()) { + if (const auto paybox = weak.get()) { paybox->closeBox(); } }, error->lifetime()); @@ -455,7 +454,7 @@ void SendCreditsBox( lt_count, rpl::single(form->invoice.amount) | tr::to_count(), lt_emoji, - rpl::single(CreditsEmojiSmall(session)), + rpl::single(CreditsEmojiSmall()), Ui::Text::RichLangValue), state->confirmButtonBusy.value() ) | rpl::map([](TextWithEntities &&text, bool busy) { @@ -465,14 +464,6 @@ void SendCreditsBox( st::creditsBoxButtonLabel, &box->getDelegate()->style().button.textFg); - const auto buttonWidth = st::boxWidth - - rect::m::sum::h(stBox.buttonPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); - { const auto close = Ui::CreateChild( content, @@ -502,16 +493,13 @@ void SendCreditsBox( } } -TextWithEntities CreditsEmoji(not_null session) { - return Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::settingsPremiumIconStar, - QMargins{ 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, - true), +TextWithEntities CreditsEmoji() { + return Ui::Text::IconEmoji( + &st::starIconEmojiLarge, QString(QChar(0x2B50))); } -TextWithEntities CreditsEmojiSmall(not_null session) { +TextWithEntities CreditsEmojiSmall() { return Ui::Text::IconEmoji( &st::starIconEmoji, QString(QChar(0x2B50))); diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index 6dcaef1f8e..43fb75b033 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -32,11 +32,9 @@ void SendCreditsBox( std::shared_ptr data, Fn sent); -[[nodiscard]] TextWithEntities CreditsEmoji( - not_null session); +[[nodiscard]] TextWithEntities CreditsEmoji(); -[[nodiscard]] TextWithEntities CreditsEmojiSmall( - not_null session); +[[nodiscard]] TextWithEntities CreditsEmojiSmall(); not_null SetButtonMarkedLabel( not_null button, diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 03ac619822..3aaca4e870 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -187,9 +187,9 @@ void EditPriceBox( field->showError(); return; } - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); apply(now); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -612,7 +612,7 @@ void SendFilesBox::enqueueNextPrepare() { } auto file = std::move(_list.filesToProcess.front()); _list.filesToProcess.pop_front(); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _preparing = true; const auto sideLimit = PhotoSideLimit(); // Get on main thread. crl::async([weak, sideLimit, file = std::move(file)]() mutable { @@ -822,7 +822,7 @@ void SendFilesBox::toggleSpoilers(bool enabled) { } void SendFilesBox::changePrice() { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto session = &_show->session(); const auto now = _price.current(); _show->show(Box(EditPriceBox, session, now, [=](uint64 price) { @@ -862,9 +862,8 @@ void SendFilesBox::refreshPriceTag() { QPainter(raw).drawImage(0, 0, _priceTagBg); }, raw->lifetime()); - const auto session = &_show->session(); auto price = _price.value() | rpl::map([=](uint64 amount) { - auto result = Ui::Text::Colorized(Ui::CreditsEmoji(session)); + auto result = Ui::Text::Colorized(Ui::CreditsEmoji()); result.append(Lang::FormatCountDecimal(amount)); return result; }); @@ -876,10 +875,10 @@ void SendFilesBox::refreshPriceTag() { raw, QString(), st::paidTagLabel); - std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) { - label->setMarkedText(text, Core::TextContext({ - .session = session, - })); + std::move( + text + ) | rpl::start_with_next([=](const TextWithEntities &text) { + label->setMarkedText(text); }, label->lifetime()); label->show(); label->sizeValue() | rpl::start_with_next([=](QSize size) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 9d7984f357..ba7b15aa00 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -683,7 +683,7 @@ void ShareBox::submit(Api::SendOptions options) { _submitLifetime.destroy(); auto threads = _inner->selected(); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto field = _comment->entity(); auto comment = field->getTextWithAppliedMarkdown(); const auto checkPaid = [=] { @@ -1377,8 +1377,8 @@ void ShareBox::Inner::changeCheckState(Chat *chat) { } void ShareBox::Inner::chooseForumTopic(not_null forum) { - const auto guard = Ui::MakeWeak(this); - const auto weak = std::make_shared>(); + const auto guard = base::make_weak(this); + const auto weak = std::make_shared>(); auto chosen = [=](not_null topic) { if (const auto strong = *weak) { strong->closeBox(); @@ -1425,8 +1425,8 @@ void ShareBox::Inner::chooseForumTopic(not_null forum) { void ShareBox::Inner::chooseMonoforumSublist( not_null monoforum) { - const auto guard = Ui::MakeWeak(this); - const auto weak = std::make_shared>(); + const auto guard = base::make_weak(this); + const auto weak = std::make_shared>(); auto chosen = [=](not_null sublist) { if (const auto strong = *weak) { strong->closeBox(); @@ -1990,6 +1990,29 @@ void FastShareMessage( }), Ui::LayerOption::CloseOther); } +void FastShareMessageToSelf( + std::shared_ptr show, + not_null item) { + const auto self = show->session().user(); + const auto donePhraseArgs = ChatHelpers::ForwardedMessagePhraseArgs{ + .toCount = 1, + .singleMessage = true, + .to1 = self, + .to2 = nullptr, + }; + auto sendAction = Api::SendAction(self->owner().history(self)); + sendAction.clearDraft = false; + show->session().api().forwardMessages( + Data::ResolvedForwardDraft{ .items = {item} }, + std::move(sendAction), + [=] { + auto phrase = rpl::variable( + ChatHelpers::ForwardedMessagePhrase( + donePhraseArgs)).current(); + show->showToast(std::move(phrase)); + }); +} + void FastShareMessage( not_null controller, not_null item, @@ -2008,7 +2031,7 @@ void FastShareLink( std::shared_ptr show, const QString &url, ShareBoxStyleOverrides st) { - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto sending = std::make_shared(); auto copyCallback = [=] { QGuiApplication::clipboard()->setText(url); diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index 5cbfb794d7..ec31a835b5 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -71,6 +71,9 @@ struct ShareBoxStyleOverrides { }; [[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle(); +void FastShareMessageToSelf( + std::shared_ptr show, + not_null item); void FastShareMessage( std::shared_ptr show, not_null item, diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 5ba5079c4f..0a89d2b02b 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "api/api_global_privacy.h" #include "api/api_premium.h" +#include "api/api_text_entities.h" #include "base/event_filter.h" #include "base/qt_signal_producer.h" #include "base/random.h" @@ -47,12 +48,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/admin_log/history_admin_log_item.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_unique_gift.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" @@ -68,9 +71,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits_graphics.h" #include "settings/settings_premium.h" #include "ui/boxes/boost_box.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/controls/emoji_button.h" +#include "ui/controls/ton_common.h" #include "ui/controls/userpic_button.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" @@ -80,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/new_badges.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -777,6 +783,19 @@ void ShowSentToast( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { + if (gift.info.perUserTotal && gift.info.perUserRemains < 2) { + return tr::lng_gift_sent_finished( + tr::now, + lt_count, + gift.info.perUserTotal, + Text::RichLangValue); + } else if (gift.info.perUserTotal) { + return tr::lng_gift_sent_remains( + tr::now, + lt_count, + gift.info.perUserRemains - 1, + Text::RichLangValue); + } const auto amount = gift.info.stars + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( @@ -1112,8 +1131,11 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { const auto user = session->user(); const auto requestId = session->api().request( MTPpayments_GetSavedStarGifts( - MTP_flags(Flag::f_exclude_limited | Flag::f_exclude_unlimited), + MTP_flags(Flag::f_exclude_upgradable + | Flag::f_exclude_unupgradable + | Flag::f_exclude_unlimited), user->input, + MTP_int(0), // collection_id MTP_string(offset), MTP_int(kMyGiftsPerPage) )).done([=](const MTPpayments_SavedStarGifts &result) { @@ -1147,8 +1169,9 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { } [[nodiscard]] Text::String TabTextForPrice( - not_null session, - int price) { + int price, + TextWithEntities creditsIcon, + Ui::Text::MarkedContext context) { const auto simple = [](const QString &text) { return Text::String(st::semiboldTextStyle, text); }; @@ -1163,13 +1186,12 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { } else if (price == kPriceTabResale) { return simple(tr::lng_gift_stars_tabs_resale(tr::now)); } - auto &manager = session->data().customEmojiManager(); auto result = Text::String(); result.setMarkedText( st::semiboldTextStyle, - manager.creditsEmoji().append(QString::number(price)), + creditsIcon.append(QString::number(price)), kMarkupTextOptions, - Core::TextContext({ .session = session })); + context); return result; } @@ -1207,7 +1229,7 @@ struct ResaleTabs { object_ptr widget; }; [[nodiscard]] ResaleTabs MakeResaleTabs( - not_null window, + std::shared_ptr show, not_null peer, const ResaleGiftsDescriptor &info, rpl::producer filter) { @@ -1289,7 +1311,7 @@ struct ResaleTabs { action->setClickedCallback(std::move(callback)); menu->addAction(std::move(action)); }; - auto context = Core::TextContext({ .session = &window->session() }); + auto context = Core::TextContext({ .session = &show->session() }); context.customEmojiFactory = [original = context.customEmojiFactory]( QStringView data, const Ui::Text::MarkedContext &context) { @@ -1589,7 +1611,6 @@ struct GiftPriceTabs { object_ptr widget; }; [[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs( - not_null window, not_null peer, rpl::producer> gifts, bool hasMyUnique) { @@ -1698,7 +1719,6 @@ struct GiftPriceTabs { state->priceTab = state->buttons[index].price; }; - const auto session = &peer->session(); state->prices.value( ) | rpl::start_with_next([=](const std::vector &prices) { auto x = st::giftBoxTabsMargin.left(); @@ -1712,12 +1732,18 @@ struct GiftPriceTabs { currentPrice = kPriceTabAll; } state->active = -1; + auto helper = Ui::Text::CustomEmojiHelper(); + const auto creditsIcon = helper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); for (auto i = 0, count = int(prices.size()); i != count; ++i) { const auto price = prices[i]; auto &button = state->buttons[i]; if (button.text.isEmpty() || button.price != price) { button.price = price; - button.text = TabTextForPrice(session, price); + button.text = TabTextForPrice( + price, + creditsIcon, + helper.context()); } button.active = (price == currentPrice); if (button.active) { @@ -1984,6 +2010,7 @@ void SendGift( .message = details.text, .recipient = peer, .limitedCount = gift.info.limitedCount, + .perUserLimit = gift.info.perUserTotal, .anonymous = details.anonymous, .upgraded = details.upgraded, }, done, processNonPanelPaymentFormFactory); @@ -2031,6 +2058,23 @@ void ShowGiftUpgradedToast( } } +void ShowUpgradeGiftedToast( + base::weak_ptr weak, + not_null peer) { + if (const auto strong = weak.get()) { + strong->showToast({ + .title = tr::lng_gift_upgrade_gifted_title(tr::now), + .text = { (peer->isBroadcast() + ? tr::lng_gift_upgrade_gifted_about_channel + : tr::lng_gift_upgrade_gifted_about)( + tr::now, + lt_name, + peer->shortName()) }, + .duration = kUpgradeDoneToastDuration, + }); + } +} + void SendStarsFormRequest( std::shared_ptr show, Settings::SmallBalanceResult result, @@ -2047,6 +2091,8 @@ void SendStarsFormRequest( )).done([=](const MTPpayments_PaymentResult &result) { result.match([&](const MTPDpayments_paymentResult &data) { session->api().applyUpdates(data.vupdates()); + session->credits().tonLoad(true); + session->credits().load(true); done(Payments::CheckoutResult::Paid, &data.vupdates()); }, [&](const MTPDpayments_paymentVerificationNeeded &data) { done(Payments::CheckoutResult::Failed, nullptr); @@ -2073,9 +2119,17 @@ void UpgradeGift( auto formDone = [=]( Payments::CheckoutResult result, const MTPUpdates *updates) { - if (result == Payments::CheckoutResult::Paid && updates) { + if (result == Payments::CheckoutResult::Paid) { if (const auto strong = weak.get()) { - ShowGiftUpgradedToast(strong, session, *updates); + const auto owner = savedId.isUser() + ? strong->session().user().get() + : savedId.chat(); + if (owner) { + owner->owner().nextForUpgradeGiftInvalidate(owner); + } + } + if (updates) { + ShowGiftUpgradedToast(weak, session, *updates); } } done(result); @@ -2105,6 +2159,29 @@ void UpgradeGift( std::move(formDone)); } +void GiftUpgrade( + not_null window, + not_null peer, + QString giftPrepayUpgradeHash, + int stars, + Fn done) { + const auto weak = base::make_weak(window); + auto formDone = [=]( + Payments::CheckoutResult result, + const MTPUpdates *updates) { + if (result == Payments::CheckoutResult::Paid) { + ShowUpgradeGiftedToast(weak, peer); + } + done(result); + }; + RequestStarsFormAndSubmit( + window->uiShow(), + MTP_inputInvoiceStarGiftPrepaidUpgrade( + peer->input, + MTP_string(giftPrepayUpgradeHash)), + std::move(formDone)); +} + void SoldOutBox( not_null box, not_null window, @@ -2128,7 +2205,6 @@ void SoldOutBox( void AddUpgradeButton( not_null container, - not_null session, int cost, not_null peer, Fn toggled, @@ -2141,7 +2217,8 @@ void AddUpgradeButton( button->toggleOn(rpl::single(false))->toggledValue( ) | rpl::start_with_next(toggled, button->lifetime()); - auto star = session->data().customEmojiManager().creditsEmoji(); + auto helper = Ui::Text::CustomEmojiHelper(); + auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); const auto label = Ui::CreateChild( button, tr::lng_gift_send_unique( @@ -2152,7 +2229,7 @@ void AddUpgradeButton( Text::WithEntities), st::boxLabel, st::defaultPopupMenu, - Core::TextContext({ .session = session })); + helper.context()); label->show(); label->setAttribute(Qt::WA_TransparentForMouseEvents); button->widthValue() | rpl::start_with_next([=](int outer) { @@ -2259,6 +2336,27 @@ void AddSoldLeftSlider( }, slider->lifetime()); } +void CheckMaybeGiftLocked( + not_null window, + uint64 giftId, + Fn send) { + const auto session = &window->session(); + session->api().request(MTPpayments_CheckCanSendGift( + MTP_long(giftId) + )).done(crl::guard(window, [=]( + const MTPpayments_CheckCanSendGiftResult &result) { + result.match([&](const MTPDpayments_checkCanSendGiftResultOk &) { + send(); + }, [&](const MTPDpayments_checkCanSendGiftResultFail &data) { + window->show(Ui::MakeInformBox({ + .text = Api::ParseTextWithEntities(session, data.vreason()), + .title = tr::lng_gift_locked_title(), + })); + }); + })).fail(crl::guard(window, [=] { + })).send(); +} + void SendGiftBox( not_null box, not_null window, @@ -2306,7 +2404,7 @@ void SendGiftBox( }); auto cost = state->details.value( - ) | rpl::map([session](const GiftDetails &details) { + ) | rpl::map([](const GiftDetails &details) { return v::match(details.descriptor, [&](const GiftTypePremium &data) { const auto stars = (details.byStars && data.stars) ? data.stars @@ -2314,7 +2412,7 @@ void SendGiftBox( ? data.cost : 0; if (stars) { - return CreditsEmojiSmall(session).append( + return CreditsEmojiSmall().append( Lang::FormatCountDecimal(std::abs(stars))); } return TextWithEntities{ @@ -2323,7 +2421,7 @@ void SendGiftBox( }, [&](const GiftTypeStars &data) { const auto amount = std::abs(data.info.stars) + (details.upgraded ? data.info.starsToUpgrade : 0); - return CreditsEmojiSmall(session).append( + return CreditsEmojiSmall().append( Lang::FormatCountDecimal(amount)); }); }); @@ -2399,8 +2497,7 @@ void SendGiftBox( const auto showing = std::make_shared(); AddDivider(container); AddSkip(container); - AddUpgradeButton(container, session, costToUpgrade, peer, [=]( - bool on) { + AddUpgradeButton(container, costToUpgrade, peer, [=](bool on) { auto now = state->details.current(); now.upgraded = on; state->details = std::move(now); @@ -2503,9 +2600,6 @@ void SendGiftBox( rpl::single(peer->shortName())))); }); - const auto buttonWidth = st::boxWideWidth - - st::giftBox.buttonPadding.left() - - st::giftBox.buttonPadding.right(); const auto button = box->addButton(rpl::single(QString()), [=] { if (state->submitting) { return; @@ -2515,18 +2609,19 @@ void SendGiftBox( if (!state->messageAllowed.current()) { details.text = {}; } - const auto weak = MakeWeak(box); + const auto copy = state->media; // Let media outlive the box. + const auto weak = base::make_weak(box); const auto done = [=](Payments::CheckoutResult result) { if (result == Payments::CheckoutResult::Paid) { if (details.byStars || v::is(details.descriptor)) { window->session().credits().load(true); } - const auto copy = state->media; + const auto another = copy; // Let media outlive the box. window->showPeerHistory(peer); ShowSentToast(window, details.descriptor, details); } - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -2546,12 +2641,6 @@ void SendGiftBox( session, st::creditsBoxButtonLabel, &st::giftBox.button.textFg); - button->resizeToWidth(buttonWidth); - button->widthValue() | rpl::start_with_next([=](int width) { - if (width != buttonWidth) { - button->resizeToWidth(buttonWidth); - } - }, button->lifetime()); } [[nodiscard]] rpl::producer ResaleGiftsSlice( @@ -2641,13 +2730,6 @@ void SendGiftBox( }; } -[[nodiscard]] rpl::lifetime ShowStarGiftResale( - not_null controller, - not_null peer, - uint64 giftId, - QString title, - Fn finishRequesting); - [[nodiscard]] object_ptr MakeGiftsList( not_null window, not_null peer, @@ -2738,17 +2820,42 @@ void SendGiftBox( raw, &state->delegate); } + const auto raw = button.get(); if (validated[index]) { return; } - button->show(); + raw->show(); validated[index] = true; const auto &descriptor = state->list[state->order[index]]; - button->setDescriptor(descriptor, GiftButton::Mode::Full); - button->setClickedCallback([=] { + raw->setDescriptor(descriptor, GiftButton::Mode::Full); + raw->setClickedCallback([=] { const auto star = std::get_if(&descriptor); + const auto send = crl::guard(raw, [=] { + window->show(Box( + SendGiftBox, + window, + peer, + state->api, + descriptor)); + }); const auto unique = star ? star->info.unique : nullptr; - if (unique && star->mine && !peer->isSelf()) { + const auto premiumNeeded = star && star->info.requirePremium; + if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium(window, star->info); + } else if (star + && star->info.lockedUntilDate + && star->info.lockedUntilDate > base::unixtime::now()) { + const auto ready = crl::guard(raw, [=] { + if (premiumNeeded && !peer->session().premium()) { + Settings::ShowPremiumGiftPremium( + window, + v::get(descriptor).info); + } else { + send(); + } + }); + CheckMaybeGiftLocked(window, star->info.id, ready); + } else if (unique && star->mine && !peer->isSelf()) { if (ShowTransferGiftLater(window->uiShow(), unique)) { return; } @@ -2764,11 +2871,17 @@ void SendGiftBox( using Payments::CheckoutResult; const auto formReady = [=]( uint64 formId, - uint64 price, + CreditsAmount price, std::optional failure) { state->transferRequested = nullptr; - if (!failure || *failure == CheckoutResult::Free) { - unique->starsForTransfer = price; + if (!failure && !price.stars()) { + LOG(("API Error: " + "Bad transfer invoice currenct.")); + } else if (!failure + || *failure == CheckoutResult::Free) { + unique->starsForTransfer = failure + ? 0 + : price.whole(); ShowTransferToBox( window, peer, @@ -2779,18 +2892,21 @@ void SendGiftBox( done(); } }; - RequestStarsForm( + RequestOurForm( window->uiShow(), MTP_inputInvoiceStarGiftTransfer( Api::InputSavedStarGiftId(savedId, unique), peer->input), formReady); - } else if (star && star->info.unique && star->resale) { + } else if (unique && star->resale) { window->show(Box( Settings::GlobalStarGiftBox, window->uiShow(), star->info, - peer->id, + Settings::StarGiftResaleInfo{ + .recipientId = peer->id, + .forceTon = star->forceTon, + }, Settings::CreditsEntryBoxStyleOverrides())); } else if (star && star->resale) { const auto id = star->info.id; @@ -2806,16 +2922,21 @@ void SendGiftBox( [=] { state->resaleRequestingId = 0; }); } else if (star && IsSoldOut(star->info)) { window->show(Box(SoldOutBox, window, *star)); + } else if (star + && star->info.perUserTotal + && !star->info.perUserRemains) { + window->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + star->info.perUserTotal, + Ui::Text::RichLangValue), + }); } else { - window->show(Box( - SendGiftBox, - window, - peer, - state->api, - descriptor)); + send(); } }); - button->setGeometry(QRect(QPoint(x, y), single), extend); + raw->setGeometry(QRect(QPoint(x, y), single), extend); }; y += rowFrom * singleh; for (auto row = rowFrom; row != rowTill; ++row) { @@ -2921,13 +3042,15 @@ void AddBlock( content, std::move(args.subtitle), st::giftBoxSubtitle), - st::giftBoxSubtitleMargin); + st::giftBoxSubtitleMargin, + style::al_top); const auto about = content->add( object_ptr( content, std::move(args.about), st::giftBoxAbout), - st::giftBoxAboutMargin); + st::giftBoxAboutMargin, + style::al_top); about->setClickHandlerFilter(std::move(args.aboutFilter)); content->add(std::move(args.content)); } @@ -2972,7 +3095,6 @@ void AddBlock( state->gifts = GiftsStars(&window->session(), peer); auto tabs = MakeGiftsPriceTabs( - window, peer, state->gifts.value(), !state->my.list.empty() && !peer->isSelf()); @@ -3090,10 +3212,9 @@ void GiftBox( && uniqueDisallowed; content->add( - object_ptr>( - content, - object_ptr(content, peer, stUser)) - )->entity()->setClickedCallback([=] { window->showPeerInfo(peer); }); + object_ptr(content, peer, stUser), + style::al_top + )->setClickedCallback([=] { window->showPeerInfo(peer); }); AddSkip(content); AddSkip(content); @@ -3180,7 +3301,6 @@ void GiftResaleBox( not_null peer, ResaleGiftsDescriptor descriptor) { box->setWidth(st::boxWideWidth); - box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); // Create a proper vertical layout for the title const auto titleWrap = box->setPinnedToTopContent( @@ -3221,12 +3341,26 @@ void GiftResaleBox( rpl::event_stream<> updated; ResaleGiftsDescriptor data; rpl::variable filter; + rpl::variable ton; rpl::lifetime loading; int lastMinHeight = 0; }; const auto state = content->lifetime().make_state(); state->data = std::move(descriptor); + box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); }); + +#ifndef OS_MAC_STORE + const auto currency = box->addLeftButton(rpl::single(QString()), [=] { + state->ton = !state->ton.current(); + state->updated.fire({}); + }); + currency->setText(rpl::conditional( + state->ton.value(), + tr::lng_gift_resale_switch_to_stars(), + tr::lng_gift_resale_switch_to_ton())); +#endif + box->heightValue() | rpl::start_with_next([=](int height) { if (height > state->lastMinHeight) { state->lastMinHeight = height; @@ -3235,7 +3369,7 @@ void GiftResaleBox( }, content->lifetime()); auto tabs = MakeResaleTabs( - window, + window->uiShow(), peer, state->data, state->filter.value()); @@ -3288,9 +3422,11 @@ void GiftResaleBox( ) | rpl::map([=] { auto result = GiftsDescriptor(); const auto selfId = window->session().userPeerId(); + const auto forceTon = state->ton.current(); for (const auto &gift : state->data.list) { result.list.push_back(GiftTypeStars{ .info = gift, + .forceTon = forceTon, .resale = true, .mine = (gift.unique->ownerId == selfId), }); @@ -3319,30 +3455,6 @@ void GiftResaleBox( })); } -[[nodiscard]] rpl::lifetime ShowStarGiftResale( - not_null controller, - not_null peer, - uint64 giftId, - QString title, - Fn finishRequesting) { - const auto weak = base::make_weak(controller); - const auto session = &controller->session(); - return ResaleGiftsSlice( - session, - giftId - ) | rpl::start_with_next([=](ResaleGiftsDescriptor &&info) { - if (const auto onstack = finishRequesting) { - onstack(); - } - if (!info.giftId || !info.count) { - return; - } - info.title = title; - controller->show( - Box(GiftResaleBox, controller, peer, std::move(info))); - }); -} - struct CustomList { object_ptr content = { nullptr }; Fn overrideKey; @@ -3863,7 +3975,7 @@ void ShowStarGiftBox( void SetupResalePriceButton( not_null parent, rpl::producer background, - rpl::producer price, + rpl::producer price, Fn click) { const auto resale = Ui::CreateChild< Ui::FadeWrapScaled @@ -3884,11 +3996,13 @@ void SetupResalePriceButton( }, button->lifetime()); text->setTextColorOverride(QColor(255, 255, 255, 255)); - std::move(price) | rpl::start_with_next([=](int value) { - if (value > 0) { - text->setMarkedText( - Ui::Text::IconEmoji(&st::starIconEmoji).append( - Lang::FormatCountDecimal(value))); + std::move(price) | rpl::start_with_next([=](CreditsAmount value) { + if (value) { + text->setMarkedText(value.ton() + ? Ui::Text::IconEmoji(&st::tonIconEmoji).append( + Lang::FormatCreditsAmountDecimal(value)) + : Ui::Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCountDecimal(value.whole()))); resale->toggle(true, anim::type::normal); } else { resale->toggle(false, anim::type::normal); @@ -3924,7 +4038,7 @@ void AddUniqueGiftCover( not_null container, rpl::producer data, rpl::producer subtitleOverride, - rpl::producer resalePrice, + rpl::producer resalePrice, Fn resaleClick) { const auto cover = container->add(object_ptr(container)); @@ -4302,13 +4416,15 @@ void ShowUniqueGiftWearBox( lt_name, rpl::single(UniqueGiftName(gift))), st.title ? *st.title : st::uniqueGiftTitle), - st::settingsPremiumRowTitlePadding); + st::settingsPremiumRowTitlePadding, + style::al_top); content->add( object_ptr( content, tr::lng_gift_wear_about(), st.subtitle ? *st.subtitle : st::uniqueGiftSubtitle), - st::settingsPremiumRowAboutPadding); + st::settingsPremiumRowAboutPadding, + style::al_top); infoRow( tr::lng_gift_wear_badge_title(), (channel @@ -4336,12 +4452,12 @@ void ShowUniqueGiftWearBox( return; } *checking = true; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); CheckBoostLevel(show, peer, [=](int level) { const auto limits = Data::LevelLimits(&peer->session()); const auto wanted = limits.channelEmojiStatusLevelMin(); if (level >= wanted) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } emojiStatuses->set(peer, id); @@ -4370,11 +4486,7 @@ void ShowUniqueGiftWearBox( u"wear_collectibles"_q); } }); - const auto lock = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::historySendDisabledIcon, - st::giftBoxLockMargins, - true)); + const auto lock = Ui::Text::IconEmoji(&st::giftBoxLock); auto label = rpl::combine( tr::lng_gift_wear_start(), Data::AmPremiumValue(&show->session()) @@ -4392,19 +4504,6 @@ void ShowUniqueGiftWearBox( session, st::creditsBoxButtonLabel, &st::giftBox.button.textFg); - - rpl::combine( - box->widthValue(), - button->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - const auto padding = st::giftBox.buttonPadding; - const auto wanted = outer - padding.left() - padding.right(); - if (inner != wanted) { - button->resizeToWidth(wanted); - button->moveToLeft(padding.left(), padding.top()); - } - }, box->lifetime()); - AddUniqueCloseButton(box, {}); })); } @@ -4462,24 +4561,44 @@ void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, Data::SavedStarGiftId savedId, - int price) { - const auto was = unique->starsForResale; + CreditsAmount price) { + const auto wasOnResale = (unique->starsForResale > 0); const auto session = &show->session(); session->api().request(MTPpayments_UpdateStarGiftPrice( Api::InputSavedStarGiftId(savedId, unique), - MTP_long(price) + (price + ? StarsAmountToTL(price) + : MTP_starsAmount(MTP_long(0), MTP_int(0))) )).done([=](const MTPUpdates &result) { session->api().applyUpdates(result); show->showToast((!price ? tr::lng_gift_sell_removed - : (was > 0) + : wasOnResale ? tr::lng_gift_sell_updated : tr::lng_gift_sell_toast)( tr::now, lt_name, Data::UniqueGiftName(*unique))); - - unique->starsForResale = price; + const auto setStars = [&](CreditsAmount amount) { + unique->starsForResale = amount.whole(); + }; + const auto setTon = [&](CreditsAmount amount) { + unique->nanoTonForResale = amount.whole() * Ui::kNanosInOne + + amount.nano(); + }; + if (!price) { + setStars({}); + setTon({}); + unique->onlyAcceptTon = false; + } else if (price.ton()) { + setStars(StarsFromTon(session, price)); + setTon(price); + unique->onlyAcceptTon = true; + } else { + setStars(price); + setTon(TonFromStars(session, price)); + unique->onlyAcceptTon = false; + } session->data().notifyGiftUpdate({ .id = savedId, .slug = unique->slug, @@ -4506,80 +4625,98 @@ void UniqueGiftSellBox( Data::SavedStarGiftId savedId, int price, Settings::GiftWearBoxStyleOverride st) { - box->setTitle(tr::lng_gift_sell_title()); + const auto session = &show->session(); + const auto &appConfig = session->appConfig(); + const auto starsMin = appConfig.giftResaleStarsMin(); + const auto nanoTonMin = appConfig.giftResaleNanoTonMin(); + const auto starsThousandths = appConfig.giftResaleStarsThousandths(); + const auto nanoTonThousandths = appConfig.giftResaleNanoTonThousandths(); + + struct State { + rpl::variable onlyTon; + rpl::variable price; + Fn()> computePrice; + rpl::event_stream<> errors; + }; + const auto state = box->lifetime().make_state(); + state->onlyTon = unique->onlyAcceptTon; + const auto priceNow = Data::UniqueGiftResaleAsked(*unique); + state->price = priceNow + ? priceNow + : price + ? CreditsAmount(price) + : CreditsAmount(starsMin); + + box->setTitle(rpl::conditional( + state->onlyTon.value(), + tr::lng_gift_sell_title_ton(), + tr::lng_gift_sell_title())); box->setStyle(st.box ? *st.box : st::upgradeGiftBox); box->setWidth(st::boxWideWidth); box->addTopButton(st.close ? *st.close : st::boxTitleClose, [=] { box->closeBox(); }); - const auto priceNow = unique->starsForResale; const auto name = Data::UniqueGiftName(*unique); const auto slug = unique->slug; - const auto session = &show->session(); - AddSubsectionTitle( - box->verticalLayout(), - tr::lng_gift_sell_placeholder(), - (st::boxRowPadding - QMargins( - st::defaultSubsectionTitlePadding.left(), - 0, - st::defaultSubsectionTitlePadding.right(), - st::defaultSubsectionTitlePadding.bottom()))); - const auto &appConfig = session->appConfig(); - const auto limit = appConfig.giftResalePriceMax(); - const auto minimal = appConfig.giftResalePriceMin(); - const auto thousandths = appConfig.giftResaleReceiveThousandths(); - const auto wrap = box->addRow(object_ptr( - box, - st::editTagField.heightMin)); - auto owned = object_ptr( - wrap, - st::editTagField, - rpl::single(QString()), - QString::number(priceNow ? priceNow : price ? price : minimal), - limit); - const auto field = owned.data(); - wrap->widthValue() | rpl::start_with_next([=](int width) { - field->move(0, 0); - field->resize(width, field->height()); - wrap->resize(width, field->height()); - }, wrap->lifetime()); - field->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(field); - st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); - }, field->lifetime()); - field->selectAll(); - box->setFocusCallback([=] { - field->setFocusFast(); + const auto container = box->verticalLayout(); + auto priceInput = HistoryView::AddStarsTonPriceInput(container, { + .session = session, + .showTon = state->onlyTon.value(), + .price = state->price.current(), + .starsMin = starsMin, + .starsMax = appConfig.giftResaleStarsMax(), + .nanoTonMin = nanoTonMin, + .nanoTonMax = appConfig.giftResaleNanoTonMax(), }); + state->price = std::move(priceInput.result); + state->computePrice = std::move(priceInput.computeResult); + box->setFocusCallback(std::move(priceInput.focusCallback)); - const auto errors = box->lifetime().make_state< - rpl::event_stream<> - >(); auto goods = rpl::merge( rpl::single(rpl::empty) | rpl::map_to(true), - base::qt_signal_producer( - field, - &Ui::NumberInput::changed - ) | rpl::map_to(true), - errors->events() | rpl::map_to(false) + std::move(priceInput.updates) | rpl::map_to(true), + state->errors.events() | rpl::map_to(false) ) | rpl::start_spawning(box->lifetime()); auto text = rpl::duplicate(goods) | rpl::map([=](bool good) { - const auto value = field->getLastText().toInt(); - const auto receive = (int64(value) * thousandths) / 1000; - return !good - ? tr::lng_gift_sell_min_price( - tr::now, - lt_count, - minimal, - Ui::Text::RichLangValue) - : (value >= minimal) - ? tr::lng_gift_sell_amount( - tr::now, - lt_count, - receive, - Ui::Text::RichLangValue) + const auto value = state->computePrice(); + const auto amount = value ? value->value() : 0.; + const auto tonMin = nanoTonMin / float64(Ui::kNanosInOne); + const auto enough = value + && (amount >= (value->ton() ? tonMin : starsMin)); + const auto receive = !value + ? 0 + : value->ton() + ? ((amount * nanoTonThousandths) / 1000.) + : ((int64(amount) * starsThousandths) / 1000); + const auto thousandths = state->onlyTon.current() + ? nanoTonThousandths + : starsThousandths; + return (!good || !value) + ? (state->onlyTon.current() + ? tr::lng_gift_sell_min_price_ton( + tr::now, + lt_count, + nanoTonMin / float64(Ui::kNanosInOne), + Ui::Text::RichLangValue) + : tr::lng_gift_sell_min_price( + tr::now, + lt_count, + starsMin, + Ui::Text::RichLangValue)) + : enough + ? (value->ton() + ? tr::lng_gift_sell_amount_ton( + tr::now, + lt_count, + receive, + Ui::Text::RichLangValue) + : tr::lng_gift_sell_amount( + tr::now, + lt_count, + receive, + Ui::Text::RichLangValue)) : tr::lng_gift_sell_about( tr::now, lt_percent, @@ -4592,37 +4729,50 @@ void UniqueGiftSellBox( box->verticalLayout()->resizeToWidth(box->width()); }), st::boxLabel)); - Ui::AddSkip(box->verticalLayout()); + + Ui::AddSkip(container); + Ui::AddSkip(container); + box->addRow(object_ptr(box)); + Ui::AddSkip(container); + Ui::AddSkip(container); + + const auto onlyTon = box->addRow( + object_ptr( + box, + tr::lng_gift_sell_only_ton(tr::now), + state->onlyTon.current(), + st::defaultCheckbox)); + state->onlyTon = onlyTon->checkedValue(); + + Ui::AddSkip(container); + box->addRow( + object_ptr( + container, + tr::lng_gift_sell_only_ton_about(Ui::Text::RichLangValue), + st::boxDividerLabel)); + Ui::AddSkip(container); rpl::duplicate(goods) | rpl::start_with_next([=](bool good) { details->setTextColorOverride( good ? st::windowSubTextFg->c : st::boxTextFgError->c); }, details->lifetime()); - QObject::connect(field, &NumberInput::submitted, [=] { - const auto count = field->getLastText().toInt(); - if (count < minimal) { - field->showError(); - errors->fire({}); + const auto submit = [=] { + const auto value = state->computePrice(); + if (!value) { + state->errors.fire({}); return; } box->closeBox(); - UpdateGiftSellPrice(show, unique, savedId, count); - }); - const auto button = box->addButton(priceNow + UpdateGiftSellPrice(show, unique, savedId, *value); + }; + std::move( + priceInput.submits + ) | rpl::start_with_next(submit, details->lifetime()); + auto submitText = priceNow ? tr::lng_gift_sell_update() - : tr::lng_gift_sell_put(), [=] { field->submitted({}); }); - rpl::combine( - box->widthValue(), - button->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - const auto padding = st::giftBox.buttonPadding; - const auto wanted = outer - padding.left() - padding.right(); - if (inner != wanted) { - button->resizeToWidth(wanted); - button->moveToLeft(padding.left(), padding.top()); - } - }, box->lifetime()); + : tr::lng_gift_sell_put(); + box->addButton(std::move(submitText), submit); } void ShowUniqueGiftSellBox( @@ -4778,15 +4928,33 @@ void UpgradeBox( infoRow( tr::lng_gift_upgrade_unique_title(), - tr::lng_gift_upgrade_unique_about(), + (args.savedId + ? tr::lng_gift_upgrade_unique_about() + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_unique_about_channel + : tr::lng_gift_upgrade_unique_about_user)( + lt_name, + rpl::single(args.peer->shortName()))), &st::menuIconUnique); infoRow( tr::lng_gift_upgrade_transferable_title(), - tr::lng_gift_upgrade_transferable_about(), + (args.savedId + ? tr::lng_gift_upgrade_transferable_about() + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_transferable_about_channel + : tr::lng_gift_upgrade_transferable_about_user)( + lt_name, + rpl::single(args.peer->shortName()))), &st::menuIconReplace); infoRow( tr::lng_gift_upgrade_tradable_title(), - tr::lng_gift_upgrade_tradable_about(), + (args.savedId + ? tr::lng_gift_upgrade_tradable_about() + : (args.peer->isBroadcast() + ? tr::lng_gift_upgrade_tradable_about_channel + : tr::lng_gift_upgrade_tradable_about_user)( + lt_name, + rpl::single(args.peer->shortName()))), &st::menuIconTradable); struct State { @@ -4794,36 +4962,39 @@ void UpgradeBox( bool preserveDetails = false; }; const auto state = std::make_shared(); - const auto preview = !args.savedId; + const auto gifting = !args.savedId + && !args.giftPrepayUpgradeHash.isEmpty(); + const auto preview = !args.savedId && !gifting; - if (!preview) { + if (!preview && !gifting) { const auto skip = st::defaultVerticalListSkip; container->add( object_ptr(container), st::boxRowPadding + QMargins(0, skip, 0, skip)); const auto checkbox = container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - (args.canAddComment - ? tr::lng_gift_upgrade_add_comment(tr::now) - : args.canAddSender - ? tr::lng_gift_upgrade_add_sender(tr::now) - : args.canAddMyComment - ? tr::lng_gift_upgrade_add_my_comment(tr::now) - : tr::lng_gift_upgrade_add_my(tr::now)), - args.addDetailsDefault)), - st::defaultCheckbox.margin)->entity(); + (args.canAddComment + ? tr::lng_gift_upgrade_add_comment(tr::now) + : args.canAddSender + ? tr::lng_gift_upgrade_add_sender(tr::now) + : args.canAddMyComment + ? tr::lng_gift_upgrade_add_my_comment(tr::now) + : tr::lng_gift_upgrade_add_my(tr::now)), + args.addDetailsDefault), + st::defaultCheckbox.margin, + style::al_top); checkbox->checkedChanges() | rpl::start_with_next([=](bool checked) { state->preserveDetails = checked; }, checkbox->lifetime()); } box->setStyle(preview ? st::giftBox : st::upgradeGiftBox); + if (gifting) { + box->setWidth(st::boxWideWidth); + } const auto cost = args.cost; - const auto session = &controller->session(); auto buttonText = preview ? tr::lng_box_ok() : rpl::single(QString()); const auto button = box->addButton(std::move(buttonText), [=] { if (preview) { @@ -4834,46 +5005,44 @@ void UpgradeBox( } state->sent = true; const auto keepDetails = state->preserveDetails; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto done = [=](Payments::CheckoutResult result) { if (result != Payments::CheckoutResult::Paid) { state->sent = false; } else { controller->showPeerHistory(args.peer); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } } }; - UpgradeGift(controller, args.savedId, keepDetails, cost, done); + if (gifting) { + GiftUpgrade( + controller, + args.peer, + args.giftPrepayUpgradeHash, + cost, + done); + } else { + UpgradeGift(controller, args.savedId, keepDetails, cost, done); + } }); if (!preview) { - auto star = session->data().customEmojiManager().creditsEmoji(); SetButtonMarkedLabel( button, (cost ? tr::lng_gift_upgrade_button( lt_price, - rpl::single(star.append( - ' ' + Lang::FormatCreditsAmountDecimal( - CreditsAmount{ cost }))), + rpl::single(Ui::Text::IconEmoji( + &st::starIconEmoji + ).append(' ').append(Lang::FormatCreditsAmountDecimal( + CreditsAmount{ cost }))), Ui::Text::WithEntities) : tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)), - &controller->session(), + {}, st::creditsBoxButtonLabel, &st::giftBox.button.textFg); } - rpl::combine( - box->widthValue(), - button->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - const auto padding = st::giftBox.buttonPadding; - const auto wanted = outer - padding.left() - padding.right(); - if (inner != wanted) { - button->resizeToWidth(wanted); - button->moveToLeft(padding.left(), padding.top()); - } - }, box->lifetime()); AddUniqueCloseButton(box, {}); } @@ -5093,15 +5262,49 @@ void SubmitStarsForm( ready); } -void RequestStarsForm( +void SubmitTonForm( + std::shared_ptr show, + MTPInputInvoice invoice, + uint64 formId, + CreditsAmount ton, + Fn done) { + const auto ready = [=] { + SendStarsFormRequest( + show, + Settings::SmallBalanceResult::Already, + formId, + invoice, + done); + }; + struct State { + rpl::lifetime lifetime; + bool success = false; + }; + const auto state = std::make_shared(); + + const auto session = &show->session(); + session->credits().tonLoad(); + session->credits().tonLoadedValue( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { + state->lifetime.destroy(); + + if (session->credits().tonBalance() < ton) { + show->show(Box(InsufficientTonBox, session->user(), ton)); + } else { + ready(); + } + }, state->lifetime); +} + +void RequestOurForm( std::shared_ptr show, MTPInputInvoice invoice, Fn failure)> done) { const auto fail = [=](Payments::CheckoutResult failure) { - done(0, 0, failure); + done(0, {}, failure); }; show->session().api().request(MTPpayments_GetPaymentForm( MTP_flags(0), @@ -5109,10 +5312,24 @@ void RequestStarsForm( MTPDataJSON() // theme_params )).done([=](const MTPpayments_PaymentForm &result) { result.match([&](const MTPDpayments_paymentFormStarGift &data) { - const auto prices = data.vinvoice().data().vprices().v; + const auto &invoice = data.vinvoice().data(); + const auto prices = invoice.vprices().v; if (show->valid() && !prices.isEmpty()) { const auto price = prices.front().data().vamount().v; - done(data.vform_id().v, price, std::nullopt); + const auto currency = qs(invoice.vcurrency()); + const auto amount = (currency == Ui::kCreditsCurrency) + ? CreditsAmount(price) + : (currency == u"TON"_q) + ? CreditsAmount( + price / Ui::kNanosInOne, + price % Ui::kNanosInOne, + CreditsType::Ton) + : std::optional(); + if (amount) { + done(data.vform_id().v, *amount, std::nullopt); + } else { + fail(Payments::CheckoutResult::Failed); + } } else { fail(Payments::CheckoutResult::Failed); } @@ -5136,14 +5353,16 @@ void RequestStarsFormAndSubmit( std::shared_ptr show, MTPInputInvoice invoice, Fn done) { - RequestStarsForm(show, invoice, [=]( + RequestOurForm(show, invoice, [=]( uint64 formId, - uint64 price, + CreditsAmount price, std::optional failure) { - if (!failure) { - SubmitStarsForm(show, invoice, formId, price, done); - } else { + if (failure) { done(*failure, nullptr); + } else if (!price.stars()) { + done(Payments::CheckoutResult::Failed, nullptr); + } else { + SubmitStarsForm(show, invoice, formId, price.whole(), done); } }); } @@ -5184,4 +5403,61 @@ void ShowResaleGiftBoughtToast( .duration = kUpgradeDoneToastDuration, }); } + +rpl::lifetime ShowStarGiftResale( + not_null controller, + not_null peer, + uint64 giftId, + QString title, + Fn finishRequesting) { + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + return ResaleGiftsSlice( + session, + giftId + ) | rpl::start_with_next([=](ResaleGiftsDescriptor &&info) { + if (const auto onstack = finishRequesting) { + onstack(); + } + if (!info.giftId || !info.count) { + return; + } + info.title = title; + if (const auto strong = weak.get()) { + strong->show(Box(GiftResaleBox, strong, peer, std::move(info))); + } + }); +} + +CreditsAmount StarsFromTon( + not_null session, + CreditsAmount ton) { + const auto appConfig = &session->appConfig(); + const auto starsRate = appConfig->starsWithdrawRate() / 100.; + const auto tonRate = appConfig->currencyWithdrawRate(); + if (!starsRate) { + return {}; + } + const auto count = (ton.value() * tonRate) / starsRate; + return CreditsAmount(int(base::SafeRound(count))); +} + +CreditsAmount TonFromStars( + not_null session, + CreditsAmount stars) { + const auto appConfig = &session->appConfig(); + const auto starsRate = appConfig->starsWithdrawRate() / 100.; + const auto tonRate = appConfig->currencyWithdrawRate(); + if (!tonRate) { + return {}; + } + const auto count = (stars.value() * starsRate) / tonRate; + const auto whole = int(std::floor(count)); + const auto cents = int(base::SafeRound((count - whole) * 100)); + return CreditsAmount( + whole, + cents * (Ui::kNanosInOne / 100), + CreditsType::Ton); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index b3df5b5b87..dac00b9b5f 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -59,7 +59,7 @@ void AddUniqueGiftCover( not_null container, rpl::producer data, rpl::producer subtitleOverride = nullptr, - rpl::producer resalePrice = nullptr, + rpl::producer resalePrice = nullptr, Fn resaleClick = nullptr); void AddWearGiftCover( not_null container, @@ -78,7 +78,7 @@ void UpdateGiftSellPrice( std::shared_ptr show, std::shared_ptr unique, Data::SavedStarGiftId savedId, - int price); + CreditsAmount price); void ShowUniqueGiftSellBox( std::shared_ptr show, std::shared_ptr unique, @@ -110,6 +110,7 @@ struct StarGiftUpgradeArgs { Fn ready; not_null peer; Data::SavedStarGiftId savedId; + QString giftPrepayUpgradeHash; int cost = 0; bool canAddSender = false; bool canAddComment = false; @@ -129,12 +130,18 @@ void SubmitStarsForm( uint64 formId, uint64 price, Fn done); -void RequestStarsForm( +void SubmitTonForm( + std::shared_ptr show, + MTPInputInvoice invoice, + uint64 formId, + CreditsAmount ton, + Fn done); +void RequestOurForm( std::shared_ptr show, MTPInputInvoice invoice, Fn failure)> done); void RequestStarsFormAndSubmit( std::shared_ptr show, @@ -151,4 +158,18 @@ void ShowResaleGiftBoughtToast( not_null to, const Data::UniqueGift &gift); +[[nodiscard]] rpl::lifetime ShowStarGiftResale( + not_null controller, + not_null peer, + uint64 giftId, + QString title, + Fn finishRequesting); + +[[nodiscard]] CreditsAmount StarsFromTon( + not_null session, + CreditsAmount ton); +[[nodiscard]] CreditsAmount TonFromStars( + not_null session, + CreditsAmount stars); + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 3375799084..d8d9ff12b2 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -502,7 +502,7 @@ StickerSetBox::StickerSetBox( : StickerSetBox(parent, std::move(show), set->identifier(), set->type()) { } -QPointer StickerSetBox::Show( +base::weak_qptr StickerSetBox::Show( std::shared_ptr show, not_null document) { if (const auto sticker = document->sticker()) { @@ -511,7 +511,7 @@ QPointer StickerSetBox::Show( show, sticker->set, sticker->setType); - const auto result = QPointer(box.data()); + const auto result = base::make_weak(box.data()); show->showBox(std::move(box)); return result; } @@ -649,20 +649,11 @@ void ChangeSetNameBox( const auto it = sets.find(input.id); return (it == sets.end()) ? QString() : it->second->title; }(); - const auto wrap = box->addRow(object_ptr( + const auto field = box->addRow(object_ptr( box, - st::editStickerSetNameField.heightMin)); - auto owned = object_ptr( - wrap, st::editStickerSetNameField, tr::lng_stickers_context_edit_name(), - wasName); - const auto field = owned.data(); - wrap->widthValue() | rpl::start_with_next([=](int width) { - field->move(0, 0); - field->resize(width, field->height()); - wrap->resize(width, field->height()); - }, wrap->lifetime()); + wasName)); field->selectAll(); constexpr auto kMaxSetNameLength = 50; field->setMaxLength(kMaxSetNameLength); @@ -1583,7 +1574,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox( int index) { Expects(index >= 0 || index < _pack.size()); const auto document = _pack[index]; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto show = _show; const auto container = box->verticalLayout(); @@ -1608,7 +1599,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox( sticker->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(sticker); - if ([[maybe_unused]] const auto strong = weak.data()) { + if ([[maybe_unused]] const auto strong = weak.get()) { const auto paused = On(PowerSaving::kStickersPanel) || show->paused(ChatHelpers::PauseReason::Layer); paintSticker(p, index, QPoint(), paused, crl::now()); @@ -1649,7 +1640,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox( if (state->requestId.current()) { return; } - const auto weakBox = Ui::MakeWeak(box); + const auto weakBox = base::make_weak(box); const auto buttonWidth = state->saveButton ? state->saveButton->width() : 0; @@ -1662,14 +1653,14 @@ void StickerSetBox::Inner::fillDeleteStickerBox( Data::StickersType::Stickers); }, [](const auto &) { }); - if ([[maybe_unused]] const auto strong = weak.data()) { + if ([[maybe_unused]] const auto strong = weak.get()) { applySet(result); } - if (const auto strongBox = weakBox.data()) { + if (const auto strongBox = weakBox.get()) { strongBox->closeBox(); } }).fail([=](const MTP::Error &error) { - if (const auto strongBox = weakBox.data()) { + if (const auto strongBox = weakBox.get()) { strongBox->uiShow()->showToast(error.type()); } }).send(); diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index 07e0168e2b..85fbc2a404 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -70,7 +70,7 @@ public: std::shared_ptr show, not_null set); - static QPointer Show( + static base::weak_qptr Show( std::shared_ptr show, not_null document); diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 778db60af9..65d3c400bb 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -2058,7 +2058,7 @@ void StickersBox::Inner::checkGroupLevel(Fn done) { } _checkingGroupLevel = true; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); CheckBoostLevel(_show, peer, [=](int level) { if (!weak) { return std::optional(); diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp index d94c9d01e7..6ee2a4977d 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp @@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_cloud_password.h" #include "base/unixtime.h" #include "boxes/passcode_box.h" +#include "data/data_cloud_themes.h" #include "data/data_session.h" #include "data/data_star_gift.h" +#include "data/data_thread.h" #include "data/data_user.h" #include "boxes/filters/edit_filter_chats_list.h" // CreatePe...tionSubtitle. #include "boxes/peer_list_box.h" @@ -23,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "payments/payments_checkout_process.h" #include "ui/boxes/confirm_box.h" +#include "ui/controls/sub_tabs.h" +#include "ui/controls/ton_common.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -90,8 +94,8 @@ void ConfirmExportBox( Ui::Text::WithEntities), st::boxLabel)); box->addButton(tr::lng_gift_transfer_confirm_button(), [=] { - confirmed([weak = Ui::MakeWeak(box)] { - if (const auto strong = weak.data()) { + confirmed([weak = base::make_weak(box)] { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -143,7 +147,7 @@ void ExportOnBlockchain( fields.customSubmitButton = tr::lng_passcode_submit(); fields.customCheckCallback = crl::guard(parent, [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { using ExportUrl = MTPpayments_StarGiftWithdrawalUrl; session->api().request( MTPpayments_GetStarGiftWithdrawalUrl( @@ -180,7 +184,7 @@ void ExportOnBlockchain( const auto state = std::make_shared(); const auto activate = [=] { const auto now = base::unixtime::now(); - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto left = (when > now) ? (when - now) : 0; const auto hours = left ? std::max((left + 1800) / 3600, 1) : 0; if (!hours) { @@ -193,7 +197,7 @@ void ExportOnBlockchain( state->exporting = false; close(); }, [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } close(); @@ -486,6 +490,7 @@ void BuyResaleGift( std::shared_ptr show, not_null to, std::shared_ptr gift, + CreditsType type, Fn done) { auto paymentDone = [=]( Payments::CheckoutResult result, @@ -501,22 +506,44 @@ void BuyResaleGift( } }; + using Flag = MTPDinputInvoiceStarGiftResale::Flag; const auto invoice = MTP_inputInvoiceStarGiftResale( + MTP_flags((type == CreditsType::Ton) ? Flag::f_ton : Flag()), MTP_string(gift->slug), to->input); - Ui::RequestStarsForm(show, invoice, [=]( + Ui::RequestOurForm(show, invoice, [=]( uint64 formId, - uint64 price, + CreditsAmount price, std::optional failure) { + if ((type == CreditsType::Ton && price.stars()) + || (type == CreditsType::Stars && price.ton())) { + paymentDone(Payments::CheckoutResult::Failed, nullptr); + return; + } const auto submit = [=] { - SubmitStarsForm(show, invoice, formId, price, paymentDone); + if (price.stars()) { + SubmitStarsForm( + show, + invoice, + formId, + price.whole(), + paymentDone); + } else { + SubmitTonForm(show, invoice, formId, price, paymentDone); + } }; + const auto was = (type == CreditsType::Ton) + ? Data::UniqueGiftResaleTon(*gift) + : Data::UniqueGiftResaleStars(*gift); if (failure) { paymentDone(*failure, nullptr); - } else if (price != gift->starsForResale) { - const auto cost = Ui::Text::IconEmoji(&st::starIconEmoji).append( - Lang::FormatCountDecimal(price)); + } else if (price != was) { + const auto cost = price.ton() + ? Ui::Text::IconEmoji(&st::tonIconEmoji).append( + Lang::FormatCreditsAmountDecimal(price)) + : Ui::Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCountDecimal(price.whole())); const auto cancelled = [=](Fn close) { paymentDone(Payments::CheckoutResult::Cancelled, nullptr); close(); @@ -572,11 +599,11 @@ void ShowTransferToBox( return; } state->sent = true; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto done = [=](Payments::CheckoutResult result) { if (result == Payments::CheckoutResult::Cancelled) { closeParentBox(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } } else if (result != Payments::CheckoutResult::Paid) { @@ -586,7 +613,7 @@ void ShowTransferToBox( controller->showPeerHistory(peer); } closeParentBox(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } } @@ -648,33 +675,175 @@ void ShowTransferGiftBox( Ui::LayerOption::KeepOther); } +void SetThemeFromUniqueGift( + not_null window, + std::shared_ptr unique) { + class Controller final : public ChooseRecipientBoxController { + public: + Controller( + not_null window, + std::shared_ptr unique) + : ChooseRecipientBoxController({ + .session = &window->session(), + .callback = [=](not_null thread) { + const auto weak = base::make_weak(window); + const auto peer = thread->peer(); + SendPeerThemeChangeRequest(window, peer, QString(), unique); + if (weak) window->showPeerHistory(peer); + if (weak) window->hideLayer(anim::type::normal); + }, + .filter = [=](not_null thread) { + return thread->peer()->isUser(); + }, + .moneyRestrictionError = WriteMoneyRestrictionError, + }) { + } + + private: + void prepareViewHook() override { + ChooseRecipientBoxController::prepareViewHook(); + delegate()->peerListSetTitle(tr::lng_gift_transfer_choose()); + } + + }; + + window->show( + Box( + std::make_unique(window, std::move(unique)), + [](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + })); +} + +void SendPeerThemeChangeRequest( + not_null controller, + not_null peer, + const QString &token, + const std::shared_ptr &unique, + bool locallySet) { + const auto api = &peer->session().api(); + + api->request(MTPmessages_SetChatWallPaper( + MTP_flags(0), + peer->input, + MTPInputWallPaper(), + MTPWallPaperSettings(), + MTPint() + )).afterDelay(10).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + + api->request(MTPmessages_SetChatTheme( + peer->input, + (unique + ? MTP_inputChatThemeUniqueGift(MTP_string(unique->slug)) + : MTP_inputChatTheme(MTP_string(token))) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + if (!locallySet) { + peer->updateFullForced(); + } + }).send(); +} + +void SetPeerTheme( + not_null controller, + not_null peer, + const QString &token, + const std::shared_ptr &theme) { + const auto giftTheme = token.startsWith(u"gift:"_q) + ? peer->owner().cloudThemes().themeForToken(token) + : std::optional(); + + peer->setThemeToken(token); + const auto dropWallPaper = (peer->wallPaper() != nullptr); + if (dropWallPaper) { + peer->setWallPaper({}); + } + + if (theme) { + // Remember while changes propagate through event loop. + controller->pushLastUsedChatTheme(theme); + } + + SendPeerThemeChangeRequest( + controller, + peer, + token, + giftTheme ? giftTheme->unique : nullptr, + true); +} + void ShowBuyResaleGiftBox( std::shared_ptr show, std::shared_ptr gift, + bool forceTon, not_null to, Fn closeParentBox) { show->show(Box([=](not_null box) { - box->setTitle(tr::lng_gift_buy_resale_title( - lt_name, - rpl::single(UniqueGiftName(*gift)))); - - auto transfer = tr::lng_gift_buy_resale_button( - lt_cost, - rpl::single( - Ui::Text::IconEmoji(&st::starIconEmoji).append( - Lang::FormatCountDecimal(gift->starsForResale))), - Ui::Text::WithEntities); - struct State { + rpl::variable ton; bool sent = false; }; const auto state = std::make_shared(); + state->ton = gift->onlyAcceptTon || forceTon; + + if (gift->onlyAcceptTon) { + box->addRow( + object_ptr( + box, + tr::lng_gift_buy_resale_only_ton( + Ui::Text::RichLangValue), + st::resaleConfirmTonOnly), + st::boxRowPadding + st::resaleConfirmTonOnlyMargin); + } else { + const auto tabs = box->addRow( + object_ptr( + box, + Ui::SubTabsOptions{ + .selected = (state->ton.current() + ? u"ton"_q + : u"stars"_q), + .centered = true, + }, + std::vector{ + { + u"stars"_q, + tr::lng_gift_buy_resale_pay_stars( + tr::now, + Ui::Text::WithEntities), + }, + { + u"ton"_q, + tr::lng_gift_buy_resale_pay_ton( + tr::now, + Ui::Text::WithEntities), + }, + }), + st::boxRowPadding + st::resaleConfirmTonOnlyMargin); + tabs->activated() | rpl::start_with_next([=](QString id) { + tabs->setActiveTab(id); + state->ton = (id == u"ton"_q); + }, tabs->lifetime()); + } + + auto transfer = state->ton.value() | rpl::map([=](bool ton) { + return tr::lng_gift_buy_resale_button( + lt_cost, + rpl::single(ton + ? Data::FormatGiftResaleTon(*gift) + : Data::FormatGiftResaleStars(*gift)), + Ui::Text::WithEntities); + }) | rpl::flatten_latest(); + auto callback = [=](Fn close) { if (state->sent) { return; } state->sent = true; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto done = [=](Payments::CheckoutResult result) { if (result == Payments::CheckoutResult::Cancelled) { closeParentBox(); @@ -682,33 +851,41 @@ void ShowBuyResaleGiftBox( } else if (result != Payments::CheckoutResult::Paid) { state->sent = false; } else { - show->showToast(u"done!"_q); closeParentBox(); close(); } }; - BuyResaleGift(show, to, gift, done); + const auto type = state->ton.current() + ? CreditsType::Ton + : CreditsType::Stars; + BuyResaleGift(show, to, gift, type, done); }; + auto price = state->ton.value() | rpl::map([=](bool ton) { + return ton + ? tr::lng_action_gift_for_ton( + lt_count_decimal, + rpl::single(gift->nanoTonForResale + / float64(Ui::kNanosInOne)), + Ui::Text::Bold) + : tr::lng_action_gift_for_stars( + lt_count_decimal, + rpl::single(gift->starsForResale * 1.), + Ui::Text::Bold); + }) | rpl::flatten_latest(); Ui::ConfirmBox(box, { .text = to->isSelf() ? tr::lng_gift_buy_resale_confirm_self( lt_name, rpl::single(Ui::Text::Bold(UniqueGiftName(*gift))), lt_price, - tr::lng_action_gift_for_stars( - lt_count, - rpl::single(gift->starsForResale * 1.), - Ui::Text::Bold), + std::move(price), Ui::Text::WithEntities) : tr::lng_gift_buy_resale_confirm( lt_name, rpl::single(Ui::Text::Bold(UniqueGiftName(*gift))), lt_price, - tr::lng_action_gift_for_stars( - lt_count, - rpl::single(gift->starsForResale * 1.), - Ui::Text::Bold), + std::move(price), lt_user, rpl::single(Ui::Text::Bold(to->shortName())), Ui::Text::WithEntities), diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.h b/Telegram/SourceFiles/boxes/transfer_gift_box.h index 27430b43e6..285c2149fb 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.h +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.h @@ -20,6 +20,10 @@ struct UniqueGift; class SavedStarGiftId; } // namespace Data +namespace Ui { +class ChatTheme; +} // namespace Ui + void ShowTransferToBox( not_null controller, not_null peer, @@ -35,6 +39,7 @@ void ShowTransferGiftBox( void ShowBuyResaleGiftBox( std::shared_ptr show, std::shared_ptr gift, + bool forceTon, not_null to, Fn closeParentBox); @@ -44,3 +49,18 @@ bool ShowResaleGiftLater( bool ShowTransferGiftLater( std::shared_ptr show, std::shared_ptr gift); + +void SetThemeFromUniqueGift( + not_null window, + std::shared_ptr unique); +void SendPeerThemeChangeRequest( + not_null controller, + not_null peer, + const QString &token, + const std::shared_ptr &unique, + bool locallySet = false); +void SetPeerTheme( + not_null controller, + not_null peer, + const QString &token, + const std::shared_ptr &theme); diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index 5cc8f730ff..d5d7bff6a1 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -212,7 +212,7 @@ void TranslateBox( box, CreateLoadingTextWidget( box, - st::aboutLabel, + st::aboutLabel.style, std::min(original->entity()->height() / lineHeight, kMaxLines), state->to.value() | rpl::map([=](LanguageId id) { return id.locale().textDirection() == Qt::RightToLeft; @@ -253,12 +253,9 @@ void TranslateBox( showText( Ui::Text::Italic(tr::lng_translate_box_error(tr::now))); } else { - showText(TextWithEntities{ - .text = qs(list.front().data().vtext()), - .entities = Api::EntitiesFromMTP( - &peer->session(), - list.front().data().ventities().v), - }); + showText(Api::ParseTextWithEntities( + &peer->session(), + list.front())); } }).fail([=](const MTP::Error &error) { showText( @@ -312,7 +309,7 @@ object_ptr EditSkipTranslationLanguages() { auto title = tr::lng_translate_settings_choose(); const auto selected = std::make_shared>( Core::App().settings().skipTranslationLanguages()); - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto check = [=](LanguageId id) { const auto already = ranges::contains(*selected, id); if (already) { @@ -321,7 +318,7 @@ object_ptr EditSkipTranslationLanguages() { selected->push_back(id); } if (already && selected->empty()) { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->showToast( tr::lng_translate_settings_one(tr::now), kSkipAtLeastOneDuration); diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index 3dc29d9222..3047bbb771 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -134,7 +134,7 @@ void UrlAuthBox::Request( const auto bot = request.is_request_write_access() ? session->data().processUser(request.vbot()).get() : nullptr; - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto finishWithUrl = [=](const QString &url) { if (*box) { (*box)->closeBox(); @@ -185,7 +185,7 @@ void UrlAuthBox::Request( const auto bot = request.is_request_write_access() ? session->data().processUser(request.vbot()).get() : nullptr; - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto finishWithUrl = [=](const QString &url) { if (*box) { (*box)->closeBox(); diff --git a/Telegram/SourceFiles/boxes/username_box.cpp b/Telegram/SourceFiles/boxes/username_box.cpp index 7df856d8db..24635f9074 100644 --- a/Telegram/SourceFiles/boxes/username_box.cpp +++ b/Telegram/SourceFiles/boxes/username_box.cpp @@ -324,7 +324,7 @@ void UsernamesBox( const auto editor = box->addRow( object_ptr(box, peer), - {}); + style::margins()); editor->setEnabled(!isBot); box->setFocusCallback([=] { editor->setInnerFocus(); }); @@ -366,7 +366,7 @@ void UsernamesBox( !isBot ? [=] { box->scrollToY(0); editor->setInnerFocus(); } : Fn(nullptr)), - {}); + style::margins()); const auto finish = [=] { list->save( diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index 372aa37c53..794291dc28 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -711,7 +711,7 @@ std::unique_ptr BoxController::createRow( void ClearCallsBox( not_null box, not_null<::Window::SessionController*> window) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); box->addRow( object_ptr( box, @@ -754,7 +754,7 @@ void ClearCallsBox( self(revoke, self); } else { api->session().data().destroyAllCallItems(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } } @@ -825,7 +825,7 @@ void ShowCallsBox(not_null<::Window::SessionController*> window) { object_ptr>( box, object_ptr(box)), - {}); + style::margins()); groupCalls->hide(anim::type::instant); groupCalls->toggleOn(state->groupCallsController.shownValue()); @@ -833,8 +833,7 @@ void ShowCallsBox(not_null<::Window::SessionController*> window) { groupCalls->entity(), tr::lng_call_box_groupcalls_subtitle()); state->groupCallsDelegate.setContent(groupCalls->entity()->add( - object_ptr(box, &state->groupCallsController), - {})); + object_ptr(box, &state->groupCallsController))); state->groupCallsController.setDelegate(&state->groupCallsDelegate); Ui::AddSkip(groupCalls->entity()); Ui::AddDivider(groupCalls->entity()); @@ -853,7 +852,7 @@ void ShowCallsBox(not_null<::Window::SessionController*> window) { const auto content = box->addRow( object_ptr(box, &state->callsController), - {}); + style::margins()); state->callsDelegate.setContent(content); state->callsController.setDelegate(&state->callsDelegate); diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index a36dff6044..58b8de6010 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -604,9 +604,9 @@ void SetupFingerprintTooltip(not_null widget) { st::confcallFingerprintTooltipLabel), st::confcallFingerprintTooltip); const auto raw = state->tooltip.get(); - const auto weak = QPointer(raw); + const auto weak = base::make_weak(raw); const auto destroy = [=] { - delete weak.data(); + delete weak.get(); }; raw->setAttribute(Qt::WA_TransparentForMouseEvents); raw->setHiddenCallback(destroy); diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index d79a317018..a04db8bdb7 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -164,6 +164,8 @@ void Instance::Delegate::groupCallPlaySound(GroupCallSound sound) { case GroupCallSound::Ended: return "group_call_end"; case GroupCallSound::AllowedToSpeak: return "group_call_allowed"; case GroupCallSound::Connecting: return "group_call_connect"; + case GroupCallSound::RecordingStarted: + return "group_call_recording_start"; } Unexpected("GroupCallSound in Instance::groupCallPlaySound."); }()); diff --git a/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp index a3f57de101..137417a9e0 100644 --- a/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp +++ b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp @@ -393,7 +393,7 @@ void ChooseJoinAsProcess::finish(JoinInfo info) { const auto box = _request->box; _request = nullptr; done(std::move(info)); - if (const auto strong = box.data()) { + if (const auto strong = box.get()) { strong->closeBox(); } } diff --git a/Telegram/SourceFiles/calls/group/calls_choose_join_as.h b/Telegram/SourceFiles/calls/group/calls_choose_join_as.h index eb899b5568..673546ffa1 100644 --- a/Telegram/SourceFiles/calls/group/calls_choose_join_as.h +++ b/Telegram/SourceFiles/calls/group/calls_choose_join_as.h @@ -51,7 +51,7 @@ private: std::shared_ptr show; Fn done; base::has_weak_ptr guard; - QPointer box; + base::weak_qptr box; rpl::lifetime lifetime; Context context = Context(); mtpRequestId id = 0; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 8663c534c3..96b2f7a043 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -2685,6 +2685,10 @@ void GroupCall::emitShareScreenError(Error error) { _errors.fire_copy(error); } +void GroupCall::playSoundRecordingStarted() const { + _delegate->groupCallPlaySound(Delegate::GroupCallSound::RecordingStarted); +} + void GroupCall::setupOutgoingVideo() { using Webrtc::VideoState; @@ -3978,7 +3982,7 @@ void GroupCall::inviteUsers( } }; - if (const auto call = _conferenceCall.get()) { + if (_conferenceCall.get()) { for (const auto &request : requests) { inviteToConference(request, [=] { return &state->result; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index e727173cd3..a78d3ab258 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -205,6 +205,7 @@ public: Connecting, AllowedToSpeak, Ended, + RecordingStarted, }; virtual void groupCallPlaySound(GroupCallSound sound) = 0; virtual auto groupCallGetVideoCapture(const QString &deviceId) @@ -283,6 +284,7 @@ public: const QString &title, bool video, bool videoPortrait); + void playSoundRecordingStarted() const; [[nodiscard]] bool recordingStoppedByMe() const { return _recordingStoppedByMe; } diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.cpp b/Telegram/SourceFiles/calls/group/calls_group_common.cpp index fab26cc837..eabcca6a35 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_common.cpp @@ -116,13 +116,12 @@ void ConferenceCallJoinConfirm( st::boxRowPadding + st::confcallLinkHeaderIconPadding); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_confcall_join_title(), - st::boxTitle)), - st::boxRowPadding + st::confcallLinkTitlePadding); + tr::lng_confcall_join_title(), + st::boxTitle), + st::boxRowPadding + st::confcallLinkTitlePadding, + style::al_top); const auto wrapName = [&](not_null peer) { return rpl::single(Ui::Text::Bold(peer->shortName())); }; @@ -136,7 +135,8 @@ void ConferenceCallJoinConfirm( Ui::Text::RichLangValue) : tr::lng_confcall_join_text(Ui::Text::RichLangValue)), st::confcallLinkCenteredText), - st::boxRowPadding + st::boxRowPadding, + style::al_top )->setTryMakeSimilarLines(true); const auto &participants = call->participants(); @@ -209,12 +209,13 @@ void ConferenceCallJoinConfirm( box, std::move(text), st::confcallLinkCenteredText), - st::boxRowPadding + st::boxRowPadding, + style::al_top )->setTryMakeSimilarLines(true); } const auto joinAndClose = [=] { - join([weak = Ui::MakeWeak(box)] { - if (const auto strong = weak.data()) { + join([weak = base::make_weak(box)] { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -275,7 +276,7 @@ void ShowConferenceCallLinkBox( } state->resetting = true; using Flag = MTPphone_ToggleGroupCallSettings::Flag; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); call->session().api().request( MTPphone_ToggleGroupCallSettings( MTP_flags(Flag::f_reset_invite_hash), @@ -284,7 +285,7 @@ void ShowConferenceCallLinkBox( ).done([=](const MTPUpdates &result) { call->session().api().applyUpdates(result); ShowConferenceCallLinkBox(show, call, args); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } show->showToast({ @@ -321,13 +322,12 @@ void ShowConferenceCallLinkBox( Info::BotStarRef::CreateLinkHeaderIcon(box, &call->session()), st::boxRowPadding + st::confcallLinkHeaderIconPadding); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_confcall_link_title(), - st.box ? st.box->title : st::boxTitle)), - st::boxRowPadding + st::confcallLinkTitlePadding); + tr::lng_confcall_link_title(), + st.box ? st.box->title : st::boxTitle), + st::boxRowPadding + st::confcallLinkTitlePadding, + style::al_top); box->addRow( object_ptr( box, @@ -335,7 +335,8 @@ void ShowConferenceCallLinkBox( (st.centerLabel ? *st.centerLabel : st::confcallLinkCenteredText)), - st::boxRowPadding + st::boxRowPadding, + style::al_top )->setTryMakeSimilarLines(true); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 2); diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index cd04eb32fa..46fa70f933 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -943,7 +943,7 @@ object_ptr PrepareInviteBox( : (nonMembers.size() < users.size()) ? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name) : tr::lng_group_call_add_to_group_all(tr::now, lt_group, name); - const auto shared = std::make_shared>(); + const auto shared = std::make_shared>(); const auto finishWithConfirm = [=] { if (*shared) { (*shared)->closeBox(); @@ -983,7 +983,7 @@ object_ptr PrepareInviteBox( return !controller->hasRowFor(user); }) | ranges::to_vector; - const auto finish = [box = Ui::MakeWeak(box)]() { + const auto finish = [box = base::make_weak(box)]() { if (box) { box->closeBox(); } @@ -1042,19 +1042,19 @@ not_null CreateReActivateHeader(not_null parent) { st::boxRowPadding + st::confcallLinkHeaderIconPadding); result->add( - object_ptr>( + object_ptr( result, - object_ptr( - result, - tr::lng_confcall_inactive_title(), - st::boxTitle)), - st::boxRowPadding + st::confcallLinkTitlePadding); + tr::lng_confcall_inactive_title(), + st::boxTitle), + st::boxRowPadding + st::confcallLinkTitlePadding, + style::al_top); result->add( object_ptr( result, tr::lng_confcall_inactive_about(), st::confcallLinkCenteredText), - st::boxRowPadding + st::confcallLinkTitlePadding + st::boxRowPadding + st::confcallLinkTitlePadding, + style::al_top )->setTryMakeSimilarLines(true); Ui::AddDivider(result); @@ -1098,14 +1098,14 @@ object_ptr PrepareInviteToEmptyBox( }, box->lifetime()); const auto join = [=] { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); auto selected = raw->requests(box->collectSelectedRows()); Core::App().calls().startOrJoinConferenceCall({ .call = call, .joinMessageId = inviteMsgId, .invite = std::move(selected), }); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index d5ce0e5791..67a82ccb7d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1346,16 +1346,16 @@ base::unique_qptr Members::Controller::createRowContextMenu( : addVolumeItem ? st::groupCallPopupMenuWithVolume : st::groupCallPopupMenu)); - const auto weakMenu = Ui::MakeWeak(result.get()); + const auto weakMenu = base::make_weak(result.get()); const auto withActiveWindow = [=](auto callback) { if (const auto window = Core::App().activePrimaryWindow()) { - if (const auto menu = weakMenu.data()) { + if (const auto menu = weakMenu.get()) { menu->discardParentReActivate(); // We must hide PopupMenu before we activate the MainWindow, // otherwise we set focus in field inside MainWindow and then // PopupMenu::hide activates back the group call panel :( - delete weakMenu; + delete weakMenu.get(); } window->invokeForSessionController( account, diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 3a40c36f49..ce9c69b195 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -407,7 +407,7 @@ void Panel::startScheduledNow() { } else if (now + kStartNoConfirmation >= date) { _call->startScheduledNow(); } else { - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto done = [=] { if (*box) { (*box)->closeBox(); @@ -1175,6 +1175,7 @@ void Panel::subscribeToChanges(not_null real) { const auto isVideo = real->recordVideo(); if (recorded) { *startedAsVideo = isVideo; + _call->playSoundRecordingStarted(); } validateRecordingMark(recorded); uiShow()->showToast((recorded @@ -1376,7 +1377,7 @@ void Panel::chooseShareScreenSource() { tr::now, lt_user, screencastFromPeer->shortName()); - const auto shared = std::make_shared>(); + const auto shared = std::make_shared>(); const auto done = [=] { if (*shared) { base::take(*shared)->closeBox(); @@ -2071,9 +2072,9 @@ void Panel::showNiceTooltip( ? st::groupCallStickedTooltip : st::groupCallNiceTooltip)); const auto tooltip = _niceTooltip.data(); - const auto weak = QPointer(tooltip); + const auto weak = base::make_weak(tooltip); const auto destroy = [=] { - delete weak.data(); + delete weak.get(); }; if (type != NiceTooltipType::Sticked) { tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -2097,9 +2098,9 @@ void Panel::updateTooltipGeometry() { return; } const auto geometry = _niceTooltipControl->geometry(); - const auto weak = QPointer(_niceTooltip); + const auto weak = base::make_weak(_niceTooltip); const auto countPosition = [=](QSize size) { - const auto strong = weak.data(); + const auto strong = weak.get(); const auto wide = (_mode.current() == PanelMode::Wide); const auto top = geometry.y() - (wide ? st::groupCallNiceTooltipTop : 0) diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp index db94f72cb0..9c35ed529d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp @@ -133,7 +133,7 @@ void StartRtmpProcess::start( void StartRtmpProcess::close() { if (_request) { _request->peer->session().api().request(_request->id).cancel(); - if (const auto strong = _request->box.data()) { + if (const auto strong = _request->box.get()) { strong->closeBox(); } _request = nullptr; @@ -199,7 +199,7 @@ void StartRtmpProcess::createBox() { ) | rpl::start_with_next([=] { _request = nullptr; }, _request->lifetime); - _request->box = Ui::MakeWeak(object.data()); + _request->box = base::make_weak(object.data()); _request->show->showBox(std::move(object)); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.h b/Telegram/SourceFiles/calls/group/calls_group_rtmp.h index eb5d4b5fa6..c3cadab12d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_rtmp.h +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.h @@ -64,7 +64,7 @@ private: std::shared_ptr show; Fn done; base::has_weak_ptr guard; - QPointer box; + base::weak_qptr box; rpl::lifetime lifetime; mtpRequestId id = 0; }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 0adbb8e3e9..cb70c06e6d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -110,7 +110,7 @@ object_ptr ShareInviteLinkBox( const QString &linkListener, std::shared_ptr show) { const auto sending = std::make_shared(); - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); auto bottom = linkSpeaker.isEmpty() ? nullptr @@ -220,7 +220,7 @@ void SettingsBox( using namespace Settings; const auto weakCall = base::make_weak(call); - const auto weakBox = Ui::MakeWeak(box); + const auto weakBox = base::make_weak(box); struct State { std::unique_ptr deviceId; diff --git a/Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.cpp b/Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.cpp index 513fa203c9..8f269593a4 100644 --- a/Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.cpp +++ b/Telegram/SourceFiles/calls/group/ui/calls_group_recording_box.cpp @@ -196,22 +196,13 @@ void RecordingInfo::prepareVideo() { } void RecordingInfo::setLabel(const QString &text) { - const auto label = _container->add( + _container->add( object_ptr( _container, text, st::groupCallRecordingSubLabel), - st::groupCallRecordingSubLabelMargins); - - rpl::combine( - sizeValue(), - label->sizeValue() - ) | rpl::start_with_next([=](QSize my, QSize labelSize) { - label->moveToLeft( - (my.width() - labelSize.width()) / 2, - label->y(), - my.width()); - }, label->lifetime()); + st::groupCallRecordingSubLabelMargins, + style::al_top); } RecordingType RecordingInfo::type() const { 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 a122f09a24..40712391b8 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -351,18 +351,18 @@ void ChooseSourceProcess::setupPanel() { if (_selectedId.isEmpty()) { return; } - const auto weak = MakeWeak(_window.get()); + const auto weak = base::make_weak(_window.get()); _delegate->chooseSourceAccepted( _selectedId, !_withAudio->isHidden() && _withAudio->checked()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->close(); } }); _finish->setClickedCallback([=] { - const auto weak = MakeWeak(_window.get()); + const auto weak = base::make_weak(_window.get()); _delegate->chooseSourceStop(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->close(); } }); diff --git a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp index 6d47c3f168..67900ea5aa 100644 --- a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp +++ b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp @@ -126,7 +126,7 @@ int Style::minButtonWidth(HistoryMessageMarkupButton::Type type) const { BotKeyboard::BotKeyboard( not_null controller, QWidget *parent) -: TWidget(parent) +: RpWidget(parent) , _controller(controller) , _st(&st::botKbButton) { setGeometry(0, 0, _st->margin, st::botKbScroll.deltat); diff --git a/Telegram/SourceFiles/chat_helpers/bot_keyboard.h b/Telegram/SourceFiles/chat_helpers/bot_keyboard.h index 622bbd5bef..712d79b009 100644 --- a/Telegram/SourceFiles/chat_helpers/bot_keyboard.h +++ b/Telegram/SourceFiles/chat_helpers/bot_keyboard.h @@ -21,7 +21,7 @@ class SessionController; } // namespace Window class BotKeyboard - : public TWidget + : public Ui::RpWidget , public Ui::AbstractTooltipShower , public ClickHandlerHost { public: @@ -44,7 +44,7 @@ public: void step_selected(crl::time ms, bool timer); void resizeToWidth(int newWidth, int maxOuterHeight) { _maxOuterHeight = maxOuterHeight; - return TWidget::resizeToWidth(newWidth); + return RpWidget::resizeToWidth(newWidth); } [[nodiscard]] bool maximizeSize() const; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index d0bbb89a26..9bd75350bf 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1297,6 +1297,7 @@ historyRecordLockPosition: point(1px, 22px); historyRecordCancelButtonWidth: 100px; historyRecordCancelButtonFg: lightButtonFg; +historyRecordTooltipSkip: 8px; historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) { padding: margins(4px, 4px, 4px, 4px); radius: 11px; @@ -1489,9 +1490,6 @@ editTagField: InputField(defaultInputField) { heightMin: 36px; } -editTagLimit: FlatLabel(defaultFlatLabel) { - textFg: windowSubTextFg; -} editStickerSetNameField: InputField(defaultInputField) { textMargins: margins(0px, 8px, 26px, 4px); @@ -1631,4 +1629,17 @@ frozenInfoBox: Box(defaultBox) { shadowIgnoreTopSkip: true; } +menuTranscribeItemPadding: margins(0px, 10px, 0px, 10px); +menuTranscribeDummyButton: IconButton(defaultIconButton) { + width: 40px; + height: 40px; + + icon: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + + rippleAreaPosition: point(3px, 3px); + rippleAreaSize: 34px; + ripple: defaultRippleAnimationBgOver; +} + roundVideoFont: font(14px semibold); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp index 2f88f8202d..38b54e3518 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp @@ -173,7 +173,7 @@ Loader::Loader( void Loader::unpack(const QString &path) { const auto folder = internal::SetDataPath(id()); - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); crl::async([=] { if (UnpackSet(path, folder)) { QFile(path).remove(); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index 87fd388861..b0a1cd5f24 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -704,12 +704,12 @@ void SuggestionsWidget::enterEventHook(QEnterEvent *e) { if (!inner().contains(mapToInner(QCursor::pos()))) { clearMouseSelection(); } - return TWidget::enterEventHook(e); + return RpWidget::enterEventHook(e); } void SuggestionsWidget::leaveEventHook(QEvent *e) { clearMouseSelection(); - return TWidget::leaveEventHook(e); + return RpWidget::leaveEventHook(e); } SuggestionsController::SuggestionsController( @@ -984,7 +984,7 @@ void SuggestionsController::replaceCurrent( const auto position = cursor.position(); const auto suggestion = getEmojiQuery(); if (v::is(suggestion)) { - const auto weak = Ui::MakeWeak(_container.get()); + const auto weak = base::make_weak(_container.get()); const auto count = std::max(_emojiQueryLength, 1); for (auto i = 0; i != count; ++i) { const auto start = position - count + i; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index abf2fb7195..5fdbe58b3b 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -204,7 +204,7 @@ void EditLinkBox( url->showError(); return; } - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); callback(linkText, linkUrl); if (weak) { box->closeBox(); @@ -306,9 +306,9 @@ void EditCodeLanguageBox( const auto name = field->getLastText().trimmed(); const auto check = QRegularExpression("^[a-zA-Z0-9\\+\\-]*$"); if (check.match(name).hasMatch()) { - auto weak = Ui::MakeWeak(box); + auto weak = base::make_weak(box); save(name); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } } else { @@ -403,7 +403,7 @@ Fn show, not_null field, const style::InputField *fieldStyle) { - const auto weak = Ui::MakeWeak(field); + const auto weak = base::make_weak(field); return [=]( EditLinkSelection selection, TextWithTags text, @@ -414,7 +414,7 @@ FncommitMarkdownLinkEdit(selection, text, link); } }; @@ -486,7 +486,7 @@ void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) { EditLinkAction action)> FactcheckEditLinkCallback( std::shared_ptr show, not_null field) { - const auto weak = Ui::MakeWeak(field); + const auto weak = base::make_weak(field); return [=]( EditLinkSelection selection, TextWithTags text, @@ -505,7 +505,7 @@ void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) { return IsGoodFactcheckUrl(link); } auto callback = [=](const TextWithTags &text, const QString &link) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->commitMarkdownLinkEdit(selection, text, link); } }; @@ -698,9 +698,9 @@ void InitMessageFieldFade( bottomFade->move( 0, size.height() - st::historyComposeFieldFadeHeight); - }, [t = Ui::MakeWeak(topFade), b = Ui::MakeWeak(bottomFade)] { - Ui::DestroyChild(t.data()); - Ui::DestroyChild(b.data()); + }, [t = base::make_weak(topFade), b = base::make_weak(bottomFade)] { + Ui::DestroyChild(t.get()); + Ui::DestroyChild(b.get()); }, topFade->lifetime()); const auto descent = field->st().style.font->descent; @@ -1532,7 +1532,8 @@ void FrozenInfoBox( content, tr::lng_frozen_title(), st.title ? *st.title : st::uniqueGiftTitle), - st::settingsPremiumRowTitlePadding); + st::settingsPremiumRowTitlePadding, + style::al_top); Ui::AddSkip(content, st::defaultVerticalListSkip * 3); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index e3f2c7da7b..bc2d186f01 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -1760,8 +1760,8 @@ void StickersListWidget::showStickerSetBox( base::timer_once(kTimeout), document->owner().stickers().updated( Data::StickersType::Stickers) - ) | rpl::start_with_next([=, weak = Ui::MakeWeak(this)] { - if (weak.data()) { + ) | rpl::start_with_next([=, weak = base::make_weak(this)] { + if (weak.get()) { showStickerSetBox(document, setId); } lifetime->destroy(); diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index c8291d1436..09bc769da6 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -279,7 +279,7 @@ void TabbedPanel::leaveEventHook(QEvent *e) { } else { _hideTimer.callOnce(kHideTimeoutMs); } - return TWidget::leaveEventHook(e); + return RpWidget::leaveEventHook(e); } void TabbedPanel::otherEnter() { diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 33ec7ef78d..98ab236c59 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -1472,13 +1472,13 @@ void TabbedSelector::Inner::disableScroll(bool disabled) { void TabbedSelector::Inner::checkHideWithBox( object_ptr box) { - const auto raw = QPointer(box.data()); + const auto raw = base::make_weak(box.data()); _show->showBox(std::move(box)); if (!raw) { return; } _preventHideWithBox = true; - connect(raw, &QObject::destroyed, this, [=] { + connect(raw.get(), &QObject::destroyed, this, [=] { _preventHideWithBox = false; _checkForHide.fire({}); }); diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 170e064fb0..2fa2183371 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -658,6 +658,8 @@ bool Application::eventFilter(QObject *object, QEvent *e) { if (base::Platform::GlobalShortcuts::IsToggleFullScreenKey(event) && toggleActiveWindowFullScreen()) { return true; + } else if (Shortcuts::HandlePossibleChatSwitch(event)) { + return true; } } break; case QEvent::MouseButtonPress: @@ -666,8 +668,20 @@ bool Application::eventFilter(QObject *object, QEvent *e) { updateNonIdle(); } break; + case QEvent::KeyRelease: { + const auto event = static_cast(e); + if (Shortcuts::HandlePossibleChatSwitch(event)) { + return true; + } + } break; + case QEvent::ShortcutOverride: { - // handle shortcuts ourselves + // Ctrl+Tab/Ctrl+Shift+Tab chat switch is a special shortcut case, + // because it not only does an action on the shortcut activation, + // but also keeps the UI visible until you release the Ctrl key. + Shortcuts::HandlePossibleChatSwitch(static_cast(e)); + + // Handle all the shortcut management manually. return true; } break; @@ -912,7 +926,7 @@ void Application::forceLogOut( box->setCloseByEscape(false); box->setCloseByOutsideClick(false); const auto weak = base::make_weak(account); - connect(box, &QObject::destroyed, [=] { + connect(box.get(), &QObject::destroyed, [=] { crl::on_main(weak, [=] { account->forcedLogOut(); }); @@ -1650,7 +1664,7 @@ void Application::registerLeaveSubscription(not_null widget) { if (e->type() == QEvent::Leave) { if (const auto taken = _leaveFilters.take(window)) { for (const auto &weak : taken->registered) { - if (const auto widget = weak.data()) { + if (const auto widget = weak.get()) { QEvent ev(QEvent::Leave); QCoreApplication::sendEvent(widget, &ev); } diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index a40f2cceca..61da5f2b40 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -438,7 +438,7 @@ private: const std::unique_ptr _langCloudManager; const std::unique_ptr _emojiKeywords; std::unique_ptr _translator; - QPointer _badProxyDisableBox; + base::weak_qptr _badProxyDisableBox; const std::unique_ptr _tray; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 9e290988f9..824f7816b2 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "ui/boxes/confirm_box.h" #include "ui/toast/toast.h" +#include "ui/widgets/popup_menu.h" #include "base/qthelp_regex.h" #include "base/qt/qt_key_modifiers.h" #include "storage/storage_account.h" @@ -33,6 +34,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +[[nodiscard]] TextWithEntities BoldDomainInUrl(const QString &url) { + auto result = TextWithEntities{ .text = url }; + + if (const auto parsedUrl = QUrl(url); parsedUrl.isValid()) { + if (const auto host = parsedUrl.host(); !host.isEmpty()) { + if (const auto hostPos = url.indexOf(host); hostPos != -1) { + auto boldEntity = EntityInText( + EntityType::Bold, + hostPos, + host.length()); + + if (host.startsWith("www.")) { + boldEntity = EntityInText( + EntityType::Bold, + hostPos + 4, + host.length() - 4); + } + + result.entities.push_back(boldEntity); + } + } + } + return result; +} + // Possible context owners: media viewer, profile, history widget. void SearchByHashtag(ClickContext context, const QString &tag) { @@ -155,7 +181,39 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { : st::boxLabel; box->addSkip(st.style.lineHeight - st::boxPadding.bottom()); const auto url = box->addRow( - object_ptr(box, displayUrl, st)); + object_ptr( + box, + rpl::single(BoldDomainInUrl(displayUrl)), + st)); + url->setContextMenuHook([=]( + Ui::FlatLabel::ContextMenuRequest request) { + const auto copyContextText = [=] { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(displayUrl)); + }; + if (request.fullSelection) { + request.menu->addAction( + tr::lng_context_copy_link(tr::now), + copyContextText); + } else if (request.uponSelection + && !request.fullSelection) { + const auto selection = request.selection; + const auto copySelectedText = [=] { + TextUtilities::SetClipboardText( + TextForMimeData::Simple( + displayUrl.mid( + selection.from, + selection.to - selection.from))); + }; + request.menu->addAction( + tr::lng_context_copy_selected(tr::now), + copySelectedText); + } else if (request.selection.empty()) { + request.menu->addAction( + tr::lng_context_copy_link(tr::now), + copyContextText); + } + }); url->setSelectable(true); url->setContextCopyText(tr::lng_context_copy_link(tr::now)); }); diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 1db0a3a47b..fda3817855 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -244,7 +244,8 @@ QByteArray Settings::serialize() const { + Serialize::stringSize(_customFontFamily) + sizeof(qint32) * 3 + Serialize::bytearraySize(_tonsiteStorageToken) - + sizeof(qint32) * 8; + + sizeof(qint32) * 8 + + sizeof(ushort); auto result = QByteArray(); result.reserve(size); @@ -406,7 +407,8 @@ QByteArray Settings::serialize() const { << SerializeVideoQuality(_videoQuality) << qint32(_ivZoom.current()) << qint32(_systemDarkModeEnabled.current() ? 1 : 0) - << qint32(_quickDialogAction); + << qint32(_quickDialogAction) + << _notificationsVolume; } Ensures(result.size() == size); @@ -538,6 +540,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { quint32 videoQuality = SerializeVideoQuality(_videoQuality); quint32 chatFiltersHorizontal = _chatFiltersHorizontal.current() ? 1 : 0; quint32 quickDialogAction = quint32(_quickDialogAction); + ushort notificationsVolume = _notificationsVolume; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -869,6 +872,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> quickDialogAction; } + if (!stream.atEnd()) { + stream >> notificationsVolume; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1092,6 +1098,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _videoQuality = DeserializeVideoQuality(videoQuality); _chatFiltersHorizontal = (chatFiltersHorizontal == 1); _quickDialogAction = Dialogs::Ui::QuickDialogAction(quickDialogAction); + _notificationsVolume = notificationsVolume; } QString Settings::getSoundPath(const QString &key) const { @@ -1484,6 +1491,7 @@ void Settings::resetOnLastLogout() { _videoQuality = {}; _chatFiltersHorizontal = false; _quickDialogAction = Dialogs::Ui::QuickDialogAction::Disabled; + _notificationsVolume = 100; _recentEmojiPreload.clear(); _recentEmoji.clear(); diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index f8dc8a188c..4367f0ab41 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -956,6 +956,13 @@ public: [[nodiscard]] Dialogs::Ui::QuickDialogAction quickDialogAction() const; void setQuickDialogAction(Dialogs::Ui::QuickDialogAction); + [[nodiscard]] ushort notificationsVolume() const { + return _notificationsVolume; + } + void setNotificationsVolume(ushort value) { + _notificationsVolume = value; + } + void resetOnLastLogout(); private: @@ -1099,6 +1106,8 @@ private: Dialogs::Ui::QuickDialogAction _quickDialogAction = Dialogs::Ui::QuickDialogAction::Disabled; + ushort _notificationsVolume = 100; + QByteArray _photoEditorBrush; }; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 162f329a00..7138e9583a 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -637,6 +637,10 @@ bool ResolveUsernameOrPhone( } const auto storyParam = params.value(u"story"_q); const auto storyId = storyParam.toInt(); + const auto storyAlbumParam = params.value(u"album"_q); + const auto storyAlbumId = storyAlbumParam.toInt(); + const auto giftCollectionParam = params.value(u"collection"_q); + const auto giftCollectionId = giftCollectionParam.toInt(); const auto appname = webChannelPreviewLink ? QString() : appnameParam; const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); @@ -646,7 +650,9 @@ bool ResolveUsernameOrPhone( const auto threadId = topicId ? topicId : threadParam.toInt(); const auto gameParam = params.value(u"game"_q); const auto videot = params.value(u"t"_q); - + if (params.contains(u"direct"_q)) { + resolveType = ResolveType::ChannelDirect; + } if (!gameParam.isEmpty() && validDomain(gameParam)) { startToken = gameParam; resolveType = ResolveType::ShareGame; @@ -663,6 +669,8 @@ bool ResolveUsernameOrPhone( .phone = phone, .messageId = post, .storyId = storyId, + .storyAlbumId = storyAlbumId, + .giftCollectionId = giftCollectionId, .videoTimestamp = (!videot.isEmpty() ? ParseVideoTimestamp(videot) : std::optional()), @@ -706,6 +714,8 @@ bool ResolveUsernameOrPhone( : std::nullopt), .clickFromMessageId = myContext.itemId, .clickFromBotWebviewContext = myContext.botWebviewContext, + .historyInNewWindow = + (params.value(u"tdesktop_target"_q) == u"blank"_q), }); return true; } @@ -1085,7 +1095,17 @@ bool ShowCollectibleUsername( } const auto username = match->captured(1); const auto peerId = PeerId(match->captured(2).toULongLong()); - controller->resolveCollectible(peerId, username); + const auto weak = base::make_weak(controller); + controller->resolveCollectible(peerId, username, [=](const QString &e) { + if (e == u"COLLECTIBLE_NOT_FOUND"_q) { + if (const auto strong = weak.get()) { + TextUtilities::SetClipboardText({ + strong->session().createInternalLinkFull(username) + }); + strong->showToast(tr::lng_username_copied(tr::now)); + } + } + }); return true; } @@ -1304,8 +1324,9 @@ bool ResolveTestChatTheme( qthelp::UrlParamNameTransform::ToLower); if (const auto history = controller->activeChatCurrent().history()) { controller->clearCachedChatThemes(); - const auto theme = history->owner().cloudThemes().updateThemeFromLink( - history->peer->themeEmoji(), + const auto owner = &history->owner(); + const auto theme = owner->cloudThemes().updateThemeFromLink( + history->peer->themeToken(), params); if (theme) { if (!params["export"].isEmpty()) { @@ -1534,6 +1555,18 @@ bool ResolveStarsSettings( return true; } +bool ResolveTonSettings( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + controller->showSettings(::Settings::CurrencyId()); + controller->window().activate(); + return true; +} + } // namespace const std::vector &LocalUrlHandlers() { @@ -1638,6 +1671,10 @@ const std::vector &LocalUrlHandlers() { u"^stars/?(^\\?.*)?(#|$)"_q, ResolveStarsSettings }, + { + u"^ton/?(^\\?.*)?(#|$)"_q, + ResolveTonSettings + }, { u"^user\\?(.+)(#|$)"_q, AyuUrlHandlers::ResolveUser @@ -1843,6 +1880,8 @@ QString TryConvertUrlToLocal(QString url) { "/[a-zA-Z0-9\\.\\_\\-]+/?(\\?|$)|" "/\\d+/?(\\?|$)|" "/s/\\d+/?(\\?|$)|" + "/a/\\d+/?(\\?|$)|" + "/c/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { const auto domain = usernameMatch->captured(1); @@ -1865,6 +1904,10 @@ QString TryConvertUrlToLocal(QString url) { added = u"&post="_q + postMatch->captured(1); } else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&story="_q + storyMatch->captured(1); + } else if (const auto albumMatch = regex_match(u"^/a/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + added = u"&album="_q + albumMatch->captured(1); + } else if (const auto collectionMatch = regex_match(u"^/c/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + added = u"&collection="_q + collectionMatch->captured(1); } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_\\-]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&appname="_q + appNameMatch->captured(1); } @@ -1939,7 +1982,12 @@ void ResolveAndShowUniqueGift( session->data().processUsers(data.vusers()); if (const auto gift = Api::FromTL(session, data.vgift())) { using namespace ::Settings; - show->show(Box(GlobalStarGiftBox, show, *gift, PeerId(), st)); + show->show(Box( + GlobalStarGiftBox, + show, + *gift, + StarGiftResaleInfo(), + st)); } }).fail([=](const MTP::Error &error) { clear(); diff --git a/Telegram/SourceFiles/core/phone_click_handler.cpp b/Telegram/SourceFiles/core/phone_click_handler.cpp index 3fbfe9b917..cb769131e5 100644 --- a/Telegram/SourceFiles/core/phone_click_handler.cpp +++ b/Telegram/SourceFiles/core/phone_click_handler.cpp @@ -371,7 +371,7 @@ void PhoneClickHandler::onClick(ClickContext context) const { if (Trim(phone) != Trim(controller->session().user()->phone())) { menu->addAction( tr::lng_info_add_as_contact(tr::now), - [=, raw = Ui::MakeWeak(resolvePhoneAction.get())] { + [=, raw = base::make_weak(resolvePhoneAction.get())] { controller->show( Box( &controller->session(), diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index 9a3a09a444..476c53a828 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -7,14 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/shortcuts.h" +#include "base/platform/base_platform_info.h" +#include "base/event_filter.h" +#include "base/parse_helper.h" +#include "core/application.h" #include "mainwindow.h" #include "mainwidget.h" #include "window/window_controller.h" -#include "core/application.h" #include "media/player/media_player_instance.h" -#include "base/platform/base_platform_info.h" #include "platform/platform_specific.h" -#include "base/parse_helper.h" #include #include @@ -30,6 +31,19 @@ constexpr auto kCountLimit = 2048; // How many shortcuts can be in json file. rpl::event_stream> RequestsStream; bool Paused/* = false*/; +const auto kChatSwitchSpecialKeys = std::array{ + Qt::Key_Left, + Qt::Key_Right, + Qt::Key_Up, + Qt::Key_Down, + Qt::Key_Q, +}; +Qt::Key ChatSwitchModifier/* = Qt::Key()*/; +bool ChatSwitchStarted/* = false*/; +QObject *ChatSwitchFilter/* = nullptr*/; +rpl::event_stream ChatSwitchStream; +std::array ChatSwitchKeyPressHandled; + const auto AutoRepeatCommands = base::flat_set{ Command::MediaPrevious, Command::MediaNext, @@ -112,6 +126,8 @@ const auto CommandByName = base::flat_map{ { u"show_chat_menu"_q , Command::ShowChatMenu }, { u"show_chat_preview"_q , Command::ShowChatPreview }, + { u"record_voice"_q , Command::RecordVoice }, + // Shortcuts that have no default values. { u"message"_q , Command::JustSendMessage }, { u"message_silently"_q , Command::SendSilentMessage }, @@ -119,6 +135,7 @@ const auto CommandByName = base::flat_map{ { u"media_viewer_video_fullscreen"_q , Command::MediaViewerFullscreen }, { u"show_scheduled"_q , Command::ShowScheduled }, { u"archive_chat"_q , Command::ArchiveChat }, + { u"record_round"_q , Command::RecordRound }, // }; @@ -140,6 +157,7 @@ const base::flat_map &CommandNames() { Command::MediaViewerFullscreen, Command::ShowScheduled, Command::ArchiveChat, + Command::RecordRound, }; class Manager { @@ -152,6 +170,7 @@ public: void toggleMedia(bool toggled); void toggleSupport(bool toggled); void listen(not_null widget); + [[nodiscard]] bool handles(const QKeySequence &sequence) const; [[nodiscard]] const QStringList &errors() const; @@ -357,6 +376,10 @@ void Manager::listen(not_null widget) { } } +bool Manager::handles(const QKeySequence &sequence) const { + return _shortcuts.contains(sequence); +} + void Manager::pruneListened() { for (auto i = begin(_listened); i != end(_listened);) { if (i->data()) { @@ -466,10 +489,6 @@ void Manager::fillDefaults() { set(u"ctrl+pgup"_q, Command::ChatPrevious); set(u"alt+up"_q, Command::ChatPrevious); - set(u"%1+tab"_q.arg(ctrl), Command::ChatNext); - set(u"%1+shift+tab"_q.arg(ctrl), Command::ChatPrevious); - set(u"%1+backtab"_q.arg(ctrl), Command::ChatPrevious); - set(u"ctrl+alt+home"_q, Command::ChatFirst); set(u"ctrl+alt+end"_q, Command::ChatLast); @@ -509,6 +528,8 @@ void Manager::fillDefaults() { set(u"ctrl+\\"_q, Command::ShowChatMenu); set(u"ctrl+]"_q, Command::ShowChatPreview); + set(u"ctrl+r"_q, Command::RecordVoice); + _defaults = keysCurrents(); } @@ -788,12 +809,169 @@ const QStringList &Errors() { return Data.errors(); } +bool MarkChatSwitchKeyPressHandled(Qt::Key key, bool handled) { + if (!key) { + return false; + } else if (handled) { + if (ranges::contains(ChatSwitchKeyPressHandled, key)) { + return false; + } + const auto i = ranges::find(ChatSwitchKeyPressHandled, Qt::Key(0)); + Assert(i != end(ChatSwitchKeyPressHandled)); + *i = key; + return true; + } + const auto i = ranges::find(ChatSwitchKeyPressHandled, key); + if (i == end(ChatSwitchKeyPressHandled)) { + return false; + } + *i = Qt::Key(0); + return true; +} + +bool CancelChatSwitch(Qt::Key result) { + ChatSwitchModifier = Qt::Key(); + if (!ChatSwitchStarted) { + return false; + } + ChatSwitchStarted = false; + delete base::take(ChatSwitchFilter); + ChatSwitchStream.fire({ .action = result }); + return true; +} + +bool NavigateChatSwitch(Qt::Key result) { + if (!ChatSwitchStarted) { + return false; + } + ChatSwitchStream.fire({ .action = result }); + return true; +} + +bool CheckChatSwitchEvent(Qt::Key key) { + const auto plain = Qt::Key(int(key) + & ~(Qt::ControlModifier + | Qt::ShiftModifier + | Qt::AltModifier + | Qt::MetaModifier)); + if (MarkChatSwitchKeyPressHandled(plain, false)) { + return true; + } else if (plain == Qt::Key_Escape) { + return CancelChatSwitch(Qt::Key_Escape); + } else if (plain == Qt::Key_Return || plain == Qt::Key_Enter) { + return CancelChatSwitch(Qt::Key_Enter); + } else if (ranges::contains(kChatSwitchSpecialKeys, plain)) { + return NavigateChatSwitch(Qt::Key(plain)); + } + return false; +} + bool HandleEvent( not_null object, not_null event) { + if (ChatSwitchStarted) { + const auto full = event->key(); + for (auto i = 0; i != full.count(); ++i) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const auto raw = full[i].key(); +#else // Qt >= 6.0.0 + const auto raw = full[i]; +#endif // Qt < 6.0.0 + if (CheckChatSwitchEvent(Qt::Key(raw))) { + return true; + } + } + return false; + } return Launch(Data.lookup(object)); } +rpl::producer ChatSwitchRequests() { + return ChatSwitchStream.events(); +} + +bool HandlePossibleChatSwitch(not_null event) { + const auto type = event->type(); + if (Paused) { + return false; + } else if (type == QEvent::ShortcutOverride) { + const auto key = Qt::Key(event->key()); + const auto ctrl = Platform::IsMac() + ? Qt::MetaModifier + : Qt::ControlModifier; + if ((event->modifiers() & ctrl) + && ranges::contains(kChatSwitchSpecialKeys, key)) { + // For Ctrl+Q in case of active chat switch we have + // on Windows always ShortcutOverride + KeyPress, + // while on macOS we have just ShortcutOverride for + // the first press and KeyPress only for repeat events. + // + // This complex scheme allows to handle both cases with + // firing the switch event Key_Q exactly once initially. + if (CheckChatSwitchEvent(key)) { + MarkChatSwitchKeyPressHandled(key, true); + } + crl::on_main([=] { + MarkChatSwitchKeyPressHandled(key, false); + }); + } + if (Data.handles(QKeySequence(ctrl | Qt::Key_Tab)) + && (Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Backtab) + || Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab) + || Data.handles(QKeySequence(ctrl | Qt::Key_Backtab)))) { + return false; + } else if (key == Qt::Key_Control || key == Qt::Key_Meta) { + ChatSwitchModifier = key; + } else if (key == Qt::Key_Tab || key == Qt::Key_Backtab) { + const auto modifiers = event->modifiers(); + if (modifiers & ctrl) { + if (ChatSwitchModifier == Qt::Key()) { + ChatSwitchModifier = Platform::IsMac() + ? Qt::Key_Meta + : Qt::Key_Control; + } + const auto action = (modifiers & Qt::ShiftModifier) + ? Qt::Key_Backtab + : key; + if (Data.handles(modifiers | key)) { + return false; + } else if (action == Qt::Key_Tab + && Data.handles(QKeySequence(ctrl | Qt::Key_Tab))) { + return false; + } else if (action == Qt::Key_Backtab + && Data.handles(QKeySequence(ctrl | Qt::Key_Backtab))) { + return false; + } else if (action == Qt::Key_Backtab + && Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)) { + return false; + } + const auto started = !std::exchange(ChatSwitchStarted, true); + if (started) { + Assert(!ChatSwitchFilter); + ChatSwitchFilter = base::install_event_filter(qApp, [=](not_null e) { + return (e->type() == QEvent::InputMethod) + ? base::EventFilterResult::Cancel + : base::EventFilterResult::Continue; + }); + } + ChatSwitchStream.fire({ + .action = action, + .started = started, + }); + return true; + } + } + } else if (type == QEvent::KeyPress) { + return CheckChatSwitchEvent(Qt::Key(event->key())); + } else if (type == QEvent::KeyRelease) { + const auto key = Qt::Key(event->key()); + if (key == ChatSwitchModifier) { + CancelChatSwitch(Qt::Key_Enter); + } + } + return false; +} + void ToggleMediaShortcuts(bool toggled) { Data.toggleMedia(toggled); } diff --git a/Telegram/SourceFiles/core/shortcuts.h b/Telegram/SourceFiles/core/shortcuts.h index 7be77d4eca..dffb31ec63 100644 --- a/Telegram/SourceFiles/core/shortcuts.h +++ b/Telegram/SourceFiles/core/shortcuts.h @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class QKeyEvent; +class QShortcutEvent; + namespace Shortcuts { enum class Command { @@ -66,6 +69,9 @@ enum class Command { SendSilentMessage, ScheduleMessage, + RecordVoice, + RecordRound, + ReadChat, ArchiveChat, @@ -119,7 +125,7 @@ private: }; -rpl::producer> Requests(); +[[nodiscard]] rpl::producer> Requests(); void Start(); void Finish(); @@ -129,7 +135,15 @@ void Listen(not_null widget); bool Launch(Command command); bool HandleEvent(not_null object, not_null event); -const QStringList &Errors(); +bool HandlePossibleChatSwitch(not_null event); + +struct ChatSwitchRequest { + Qt::Key action = Qt::Key_Tab; // Key_Tab, Key_Backtab or Key_Escape. + bool started = false; +}; +[[nodiscard]] rpl::producer ChatSwitchRequests(); + +[[nodiscard]] const QStringList &Errors(); // Media shortcuts are not enabled by default, because other // applications also use them. They are enabled only when diff --git a/Telegram/SourceFiles/core/utils.cpp b/Telegram/SourceFiles/core/utils.cpp index c4d0b76fab..caeb3a19b3 100644 --- a/Telegram/SourceFiles/core/utils.cpp +++ b/Telegram/SourceFiles/core/utils.cpp @@ -15,7 +15,6 @@ extern "C" { #include #include #include -#include #include #include #include diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 9c475823cd..693c6b6e37 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 = 5016004; -constexpr auto AppVersionStr = "5.16.4"; +constexpr auto AppVersion = 6001003; +constexpr auto AppVersionStr = "6.1.3"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/components/recent_peers.cpp b/Telegram/SourceFiles/data/components/recent_peers.cpp index 0997cc1507..2185fd1ac2 100644 --- a/Telegram/SourceFiles/data/components/recent_peers.cpp +++ b/Telegram/SourceFiles/data/components/recent_peers.cpp @@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/recent_peers.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history.h" #include "main/main_session.h" #include "storage/serialize_common.h" #include "storage/serialize_peer.h" @@ -16,6 +19,7 @@ namespace Data { namespace { constexpr auto kLimit = 48; +constexpr auto kMaxRememberedOpenChats = 32; } // namespace @@ -133,4 +137,30 @@ void RecentPeers::applyLocal(QByteArray serialized) { ("Suggestions: RecentPeers read OK, count: %1").arg(_list.size())); } +std::vector> RecentPeers::collectChatOpenHistory() const { + _session->local().readSearchSuggestions(); + return _opens; +} + +void RecentPeers::chatOpenPush(not_null thread) { + const auto i = ranges::find(_opens, thread); + if (i == end(_opens)) { + while (_opens.size() >= kMaxRememberedOpenChats) { + _opens.pop_back(); + } + _opens.insert(begin(_opens), thread); + } else if (i != begin(_opens)) { + ranges::rotate(begin(_opens), i, i + 1); + } +} + +void RecentPeers::chatOpenRemove(not_null thread) { + _opens.erase(ranges::remove(_opens, thread), end(_opens)); +} + +void RecentPeers::chatOpenKeepUserpics( + base::flat_map, Ui::PeerUserpicView> userpics) { + _chatOpenUserpicsCache = std::move(userpics); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/recent_peers.h b/Telegram/SourceFiles/data/components/recent_peers.h index e30942720e..9b2ad907b6 100644 --- a/Telegram/SourceFiles/data/components/recent_peers.h +++ b/Telegram/SourceFiles/data/components/recent_peers.h @@ -7,12 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/userpic_view.h" + namespace Main { class Session; } // namespace Main namespace Data { +class Thread; + class RecentPeers final { public: explicit RecentPeers(not_null session); @@ -28,10 +32,22 @@ public: [[nodiscard]] QByteArray serialize() const; void applyLocal(QByteArray serialized); + [[nodiscard]] auto collectChatOpenHistory() const + -> std::vector>; + void chatOpenPush(not_null thread); + void chatOpenRemove(not_null thread); + void chatOpenKeepUserpics( + base::flat_map, Ui::PeerUserpicView> userpics); + private: const not_null _session; std::vector> _list; + std::vector> _opens; + base::flat_map< + not_null, + Ui::PeerUserpicView> _chatOpenUserpicsCache; + rpl::event_stream<> _updates; }; diff --git a/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp index 87bccbd272..3d2442b2d4 100644 --- a/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp +++ b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp @@ -49,6 +49,7 @@ void RecentSharedMediaGifts::request( MTPpayments_GetSavedStarGifts( MTP_flags(0), peer->input, + MTP_int(0), // collection_id MTP_string(QString()), MTP_int(kMaxGifts) )).done([=](const MTPpayments_SavedStarGifts &result) { diff --git a/Telegram/SourceFiles/data/data_authorization.h b/Telegram/SourceFiles/data/data_authorization.h new file mode 100644 index 0000000000..8f4add2e1d --- /dev/null +++ b/Telegram/SourceFiles/data/data_authorization.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { + +struct UnreviewedAuth { + uint64 hash = 0; + bool unconfirmed = false; + TimeId date = 0; + QString device; + QString location; +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 190f3f554c..443d242cb9 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -66,7 +66,7 @@ struct PeerUpdate { Notifications = (1ULL << 4), Migration = (1ULL << 5), UnavailableReason = (1ULL << 6), - ChatThemeEmoji = (1ULL << 7), + ChatThemeToken = (1ULL << 7), ChatWallPaper = (1ULL << 8), IsBlocked = (1ULL << 9), MessagesTTL = (1ULL << 10), @@ -99,28 +99,29 @@ struct PeerUpdate { StarRefProgram = (1ULL << 35), PaysPerMessage = (1ULL << 36), GiftSettings = (1ULL << 37), + StarsRating = (1ULL << 38), // For chats and channels - InviteLinks = (1ULL << 38), - Members = (1ULL << 39), - Admins = (1ULL << 40), - BannedUsers = (1ULL << 41), - Rights = (1ULL << 42), - PendingRequests = (1ULL << 43), - Reactions = (1ULL << 44), + InviteLinks = (1ULL << 39), + Members = (1ULL << 40), + Admins = (1ULL << 41), + BannedUsers = (1ULL << 42), + Rights = (1ULL << 43), + PendingRequests = (1ULL << 44), + Reactions = (1ULL << 45), // For channels - ChannelAmIn = (1ULL << 45), - StickersSet = (1ULL << 46), - EmojiSet = (1ULL << 47), - DiscussionLink = (1ULL << 48), - MonoforumLink = (1ULL << 49), - ChannelLocation = (1ULL << 50), - Slowmode = (1ULL << 51), - GroupCall = (1ULL << 52), + ChannelAmIn = (1ULL << 46), + StickersSet = (1ULL << 47), + EmojiSet = (1ULL << 48), + DiscussionLink = (1ULL << 49), + MonoforumLink = (1ULL << 50), + ChannelLocation = (1ULL << 51), + Slowmode = (1ULL << 52), + GroupCall = (1ULL << 53), // For iteration - LastUsedBit = (1ULL << 52), + LastUsedBit = (1ULL << 53), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index dd00cc0954..52a1ef738d 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -235,7 +235,8 @@ void ChannelData::setFlags(ChannelDataFlags which) { | Flag::CallNotEmpty | Flag::SimilarExpanded | Flag::Signatures - | Flag::SignatureProfiles)) { + | Flag::SignatureProfiles + | Flag::ForumTabs)) { if (const auto history = this->owner().historyLoaded(this)) { if (diff & Flag::CallNotEmpty) { history->updateChatListEntry(); @@ -262,6 +263,9 @@ void ChannelData::setFlags(ChannelDataFlags which) { if (diff & (Flag::Signatures | Flag::SignatureProfiles)) { session().changes().peerUpdated(this, UpdateFlag::Rights); } + if (diff & Flag::ForumTabs) { + history->forumTabsChanged(which & Flag::ForumTabs); + } } } if (const auto raw = takenForum.get()) { @@ -1406,7 +1410,7 @@ void ApplyChannelUpdate( update.vboosts_applied().value_or_empty(), update.vboosts_unrestrict().value_or_empty()); } - channel->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + channel->setThemeToken(qs(update.vtheme_emoticon().value_or_empty())); channel->setTranslationDisabled(update.is_translations_disabled()); const auto reactionsLimit = update.vreactions_limit().value_or_empty(); diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index 25a14f56a1..e1c8e6eb1a 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -490,7 +490,7 @@ void ApplyChatUpdate(not_null chat, const MTPDchatFull &update) { SetTopPinnedMessageId(chat, pinned->v); } chat->checkFolder(update.vfolder_id().value_or_empty()); - chat->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + chat->setThemeToken(qs(update.vtheme_emoticon().value_or_empty())); chat->setTranslationDisabled(update.is_translations_disabled()); const auto reactionsLimit = update.vreactions_limit().value_or_empty(); if (const auto allowed = update.vavailable_reactions()) { diff --git a/Telegram/SourceFiles/data/data_cloud_themes.cpp b/Telegram/SourceFiles/data/data_cloud_themes.cpp index 56c7647dac..1a8fce9a91 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.cpp +++ b/Telegram/SourceFiles/data/data_cloud_themes.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_cloud_themes.h" +#include "api/api_premium.h" #include "window/themes/window_theme.h" #include "window/themes/window_theme_preview.h" #include "window/themes/window_theme_editor_box.h" @@ -26,6 +27,7 @@ namespace { constexpr auto kFirstReloadTimeout = 10 * crl::time(1000); constexpr auto kReloadTimeout = 3600 * crl::time(1000); +constexpr auto kGiftThemesLimit = 24; bool IsTestingColors/* = false*/; @@ -37,44 +39,38 @@ CloudTheme CloudTheme::Parse( bool parseSettings) { const auto document = data.vdocument(); const auto paper = [&](const MTPThemeSettings &settings) { - return settings.match([&](const MTPDthemeSettings &data) { - return data.vwallpaper() - ? WallPaper::Create(session, *data.vwallpaper()) - : std::nullopt; - }); + const auto &data = settings.data(); + return data.vwallpaper() + ? WallPaper::Create(session, *data.vwallpaper()) + : std::nullopt; }; const auto outgoingMessagesColors = [&]( const MTPThemeSettings &settings) { auto result = std::vector(); - settings.match([&](const MTPDthemeSettings &data) { - if (const auto colors = data.vmessage_colors()) { - for (const auto &color : colors->v) { - result.push_back(Ui::ColorFromSerialized(color)); - } + const auto &data = settings.data(); + if (const auto colors = data.vmessage_colors()) { + for (const auto &color : colors->v) { + result.push_back(Ui::ColorFromSerialized(color)); } - }); + } return result; }; const auto accentColor = [&](const MTPThemeSettings &settings) { - return settings.match([&](const MTPDthemeSettings &data) { - return Ui::ColorFromSerialized(data.vaccent_color()); - }); + const auto &data = settings.data(); + return Ui::ColorFromSerialized(data.vaccent_color()); }; const auto outgoingAccentColor = [&](const MTPThemeSettings &settings) { - return settings.match([&](const MTPDthemeSettings &data) { - return Ui::MaybeColorFromSerialized(data.voutbox_accent_color()); - }); + const auto &data = settings.data(); + return Ui::MaybeColorFromSerialized(data.voutbox_accent_color()); }; const auto basedOnDark = [&](const MTPThemeSettings &settings) { - return settings.match([&](const MTPDthemeSettings &data) { - return data.vbase_theme().match([]( - const MTPDbaseThemeNight &) { - return true; - }, [](const MTPDbaseThemeTinted &) { - return true; - }, [](const auto &) { - return false; - }); + const auto &data = settings.data(); + return data.vbase_theme().match([](const MTPDbaseThemeNight &) { + return true; + }, [](const MTPDbaseThemeTinted &) { + return true; + }, [](const auto &) { + return false; }); }; const auto settings = [&] { @@ -111,6 +107,80 @@ CloudTheme CloudTheme::Parse( }; } +CloudTheme CloudTheme::Parse( + not_null session, + const MTPDchatThemeUniqueGift &data, + bool parseSettings) { + const auto gift = Api::FromTL(session, data.vgift()); + if (!gift || !gift->unique) { + return {}; + } + const auto paper = [&](const MTPThemeSettings &settings) { + const auto &data = settings.data(); + return data.vwallpaper() + ? WallPaper::Create(session, *data.vwallpaper()) + : std::nullopt; + }; + const auto basedOnDark = [&](const MTPThemeSettings &settings) { + const auto &data = settings.data(); + return data.vbase_theme().match([](const MTPDbaseThemeNight &) { + return true; + }, [](const MTPDbaseThemeTinted &) { + return true; + }, [](const auto &) { + return false; + }); + }; + const auto outgoingMessagesColors = [&]( + const MTPThemeSettings &settings) { + auto result = std::vector(); + const auto &data = settings.data(); + if (const auto colors = data.vmessage_colors()) { + for (const auto &color : colors->v) { + result.push_back(Ui::ColorFromSerialized(color)); + } + //} else if (basedOnDark(settings)) { + // result.push_back(gift->unique->backdrop.edgeColor); + //} else { + // result.push_back(anim::color( + // gift->unique->backdrop.patternColor, + // QColor(255, 255, 255, 255), + // 0.75)); + } + return result; + }; + const auto accentColor = [&](const MTPThemeSettings &settings) { + const auto &data = settings.data(); + return Ui::ColorFromSerialized(data.vaccent_color()); + }; + const auto outgoingAccentColor = [&](const MTPThemeSettings &settings) { + const auto &data = settings.data(); + return Ui::MaybeColorFromSerialized( + data.voutbox_accent_color() + );// .value_or(gift->unique->backdrop.patternColor); + }; + const auto settings = [&] { + auto result = base::flat_map(); + for (const auto &fields : data.vtheme_settings().v) { + const auto type = basedOnDark(fields) ? Type::Dark : Type::Light; + result.emplace(type, Settings{ + .paper = paper(fields), + .accentColor = accentColor(fields), + .outgoingAccentColor = outgoingAccentColor(fields), + .outgoingMessagesColors = outgoingMessagesColors(fields), + }); + } + return result; + }; + return { + .id = gift->unique->id, + .unique = gift->unique, + .settings = (parseSettings + ? settings() + : base::flat_map()), + }; +} + CloudTheme CloudTheme::Parse( not_null session, const MTPTheme &data, @@ -382,9 +452,16 @@ rpl::producer<> CloudThemes::chatThemesUpdated() const { return _chatThemesUpdates.events(); } -std::optional CloudThemes::themeForEmoji( - const QString &emoticon) const { - const auto emoji = Ui::Emoji::Find(emoticon); +std::optional CloudThemes::themeForToken( + const QString &token) const { + if (token.startsWith(u"gift:"_q)) { + const auto id = QStringView(token).mid(5).toULongLong(); + const auto i = _giftThemes.find(id); + return (i != end(_giftThemes)) + ? i->second + : std::optional(); + } + const auto emoji = Ui::Emoji::Find(token); if (!emoji) { return {}; } @@ -394,18 +471,21 @@ std::optional CloudThemes::themeForEmoji( return (i != end(_chatThemes)) ? std::make_optional(*i) : std::nullopt; } -rpl::producer> CloudThemes::themeForEmojiValue( - const QString &emoticon) { +rpl::producer> CloudThemes::themeForTokenValue( + const QString &token) { + if (token.startsWith(u"gift:"_q)) { + return rpl::single(themeForToken(token)); + } const auto testing = TestingColors(); - if (!Ui::Emoji::Find(emoticon)) { + if (!Ui::Emoji::Find(token)) { return rpl::single>(std::nullopt); - } else if (auto result = themeForEmoji(emoticon)) { + } else if (auto result = themeForToken(token)) { if (testing) { return rpl::single( std::move(result) ) | rpl::then(chatThemesUpdated( ) | rpl::map([=] { - return themeForEmoji(emoticon); + return themeForToken(token); }) | rpl::filter([](const std::optional &theme) { return theme.has_value(); })); @@ -418,12 +498,88 @@ rpl::producer> CloudThemes::themeForEmojiValue( std::nullopt ) | rpl::then(chatThemesUpdated( ) | rpl::map([=] { - return themeForEmoji(emoticon); + return themeForToken(token); }) | rpl::filter([](const std::optional &theme) { return theme.has_value(); }) | rpl::take(limit)); } +void CloudThemes::myGiftThemesLoadMore(bool reload) { + if (reload && !_myGiftThemesTokens.empty()) { + _session->api().request(base::take(_myGiftThemesRequestId)).cancel(); + } + if (_myGiftThemesRequestId || (!reload && _myGiftThemesLoaded)) { + return; + } + _myGiftThemesRequestId = _session->api().request( + MTPaccount_GetUniqueGiftChatThemes( + MTP_int(reload ? 0 : _myGiftThemesTokens.size()), + MTP_int(kGiftThemesLimit), + MTP_long(_myGiftThemesHash)) + ).done([=](const MTPaccount_ChatThemes &result) { + _myGiftThemesRequestId = 0; + result.match([&](const MTPDaccount_chatThemes &data) { + if (reload || _myGiftThemesTokens.empty()) { + _myGiftThemesHash = data.vhash().v; + _myGiftThemesTokens.clear(); + _myGiftThemesLoaded = false; + } + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + const auto &list = data.vthemes().v; + const auto got = int(list.size()); + _myGiftThemesTokens.reserve(_myGiftThemesTokens.size() + got); + for (const auto &theme : list) { + theme.match([](const MTPDchatTheme &) { + }, [&](const MTPDchatThemeUniqueGift &data) { + _myGiftThemesTokens.push_back( + processGiftThemeGetToken(data)); + }); + } + _myGiftThemesLoaded = (got < kGiftThemesLimit); + _myGiftThemesUpdates.fire({}); + }, [&](const MTPDaccount_chatThemesNotModified &) { + if (!reload) { + _myGiftThemesLoaded = true; + _myGiftThemesUpdates.fire({}); + } + }); + }).fail([=] { + _myGiftThemesRequestId = 0; + _myGiftThemesLoaded = true; + }).send(); +} + +const std::vector &CloudThemes::myGiftThemesTokens() const { + return _myGiftThemesTokens; +} + +bool CloudThemes::myGiftThemesReady() const { + return !_myGiftThemesTokens.empty() || _myGiftThemesLoaded; +} + +rpl::producer<> CloudThemes::myGiftThemesUpdated() const { + return _myGiftThemesUpdates.events(); +} + +QString CloudThemes::processGiftThemeGetToken( + const MTPDchatThemeUniqueGift &data) { + auto parsed = CloudTheme::Parse(_session, data, true); + if (parsed.unique) { + const auto id = parsed.unique->id; + _giftThemes[id] = std::move(parsed); + return u"gift:%1"_q.arg(id); + } + return QString(); +} + +void CloudThemes::refreshChatThemesFor(const QString &token) { + if (token.startsWith(u"gift:"_q)) { + return; + } + refreshChatThemes(); +} + bool CloudThemes::TestingColors() { return IsTestingColors; } diff --git a/Telegram/SourceFiles/data/data_cloud_themes.h b/Telegram/SourceFiles/data/data_cloud_themes.h index 1ac4ceff19..deb3160863 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.h +++ b/Telegram/SourceFiles/data/data_cloud_themes.h @@ -22,6 +22,7 @@ class Controller; namespace Data { +struct UniqueGift; class DocumentMedia; enum class CloudThemeType { @@ -38,6 +39,7 @@ struct CloudTheme { UserId createdBy = 0; int usersCount = 0; QString emoticon; + std::shared_ptr unique; using Type = CloudThemeType; struct Settings { @@ -52,6 +54,10 @@ struct CloudTheme { not_null session, const MTPDtheme &data, bool parseSettings = false); + static CloudTheme Parse( + not_null session, + const MTPDchatThemeUniqueGift &data, + bool parseSettings = false); static CloudTheme Parse( not_null session, const MTPTheme &data, @@ -73,9 +79,18 @@ public: void refreshChatThemes(); [[nodiscard]] const std::vector &chatThemes() const; [[nodiscard]] rpl::producer<> chatThemesUpdated() const; - [[nodiscard]] std::optional themeForEmoji( - const QString &emoticon) const; - [[nodiscard]] auto themeForEmojiValue(const QString &emoticon) + + void myGiftThemesLoadMore(bool reload = false); + [[nodiscard]] const std::vector &myGiftThemesTokens() const; + [[nodiscard]] rpl::producer<> myGiftThemesUpdated() const; + [[nodiscard]] QString processGiftThemeGetToken( + const MTPDchatThemeUniqueGift &data); + [[nodiscard]] bool myGiftThemesReady() const; + + void refreshChatThemesFor(const QString &token); + [[nodiscard]] std::optional themeForToken( + const QString &token) const; + [[nodiscard]] auto themeForTokenValue(const QString &token) -> rpl::producer>; [[nodiscard]] static bool TestingColors(); @@ -140,6 +155,13 @@ private: std::vector _chatThemes; rpl::event_stream<> _chatThemesUpdates; + mtpRequestId _myGiftThemesRequestId = 0; + base::flat_map _giftThemes; + std::vector _myGiftThemesTokens; + rpl::event_stream<> _myGiftThemesUpdates; + uint64 _myGiftThemesHash = 0; + bool _myGiftThemesLoaded = false; + base::Timer _reloadCurrentTimer; LoadingDocument _updatingFrom; LoadingDocument _previewFrom; diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 0b0b9382ed..5102d58a4b 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct UniqueGift; +struct UniqueGiftValue; struct CreditTopupOption final { uint64 credits = 0; @@ -71,8 +72,11 @@ struct CreditsHistoryEntry final { uint64 bareEntryOwnerId = 0; uint64 giftChannelSavedId = 0; uint64 stargiftId = 0; + QString giftPrepayUpgradeHash; std::shared_ptr uniqueGift; Fn()> pinnedSavedGifts; + uint64 nextToUpgradeStickerId = 0; + Fn nextToUpgradeShow; CreditsAmount starrefAmount; int starrefCommission = 0; uint64 starrefRecipientId = 0; @@ -98,10 +102,13 @@ struct CreditsHistoryEntry final { bool converted : 1 = false; bool anonymous : 1 = false; bool stargift : 1 = false; + bool postsSearch : 1 = false; bool giftTransferred : 1 = false; bool giftRefunded : 1 = false; bool giftUpgraded : 1 = false; + bool giftUpgradeSeparate : 1 = false; bool giftResale : 1 = false; + bool giftResaleForceTon : 1 = false; bool giftPinned : 1 = false; bool savedToProfile : 1 = false; bool fromGiftsList : 1 = false; diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index c6befff047..937ebd892f 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -1429,6 +1429,17 @@ Image *DocumentData::getReplyPreview( Data::FileOrigin origin, not_null context, bool spoiler) { + if (v::is(origin.data)) { + if (const auto item = _owner->message( + v::get(origin.data))) { + if (const auto cover = LookupVideoCover(this, item)) { + return cover->getReplyPreview( + std::move(origin), + context, + spoiler); + } + } + } if (!hasThumbnail()) { return nullptr; } else if (!_replyPreview) { @@ -1701,6 +1712,10 @@ void DocumentData::forceIsStreamedAnimation() { setMaybeSupportsStreaming(true); } +bool DocumentData::isMusicForProfile() const { + return isSong(); +} + bool DocumentData::isVoiceMessage() const { return (type == VoiceDocument); } @@ -1729,6 +1744,7 @@ bool DocumentData::isTheme() const { || _filename.endsWith(u".tdesktop-palette"_q, Qt::CaseInsensitive) || (hasMimeType(u"application/x-tgtheme-tdesktop"_q) && (_filename.isEmpty() + || !_filename.contains('.') || _nameType == Core::NameType::ThemeFile)); } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index eb01b60765..21f0edfba1 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -182,6 +182,7 @@ public: [[nodiscard]] const VideoData *video() const; void forceIsStreamedAnimation(); + [[nodiscard]] bool isMusicForProfile() const; [[nodiscard]] bool isVoiceMessage() const; [[nodiscard]] bool isVideoMessage() const; [[nodiscard]] bool isSong() const; diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 13b30f0bd2..248daec9a9 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -57,7 +57,7 @@ void ConfirmDontWarnBox( std::move(check), false, st::defaultBoxCheckbox); - const auto weak = Ui::MakeWeak(checkbox.data()); + const auto weak = base::make_weak(checkbox.data()); auto confirmed = crl::guard(weak, [=, callback = std::move(callback)] { const auto checked = weak->checked(); box->closeBox(); @@ -169,7 +169,11 @@ base::binary_guard ReadBackgroundImageAsync( guard = result.make_guard(), callback = std::move(done) ]() mutable { - auto image = Ui::ReadBackgroundImage(path, bytes, gzipSvg); + auto image = Ui::ReadBackgroundImage(path, bytes, gzipSvg).image; + if (image.isNull()) { + image = QImage(1, 1, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + } if (postprocess) { image = postprocess(std::move(image)); } diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index e92845840f..a543c601f9 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -61,6 +61,8 @@ struct FileReferenceAccumulator { push(data.vstickers()); }, [&](const MTPDwebPageAttributeUniqueStarGift &data) { push(data.vgift()); + }, [&](const MTPDwebPageAttributeStarGiftCollection &data) { + push(data.vicons()); }); } void push(const MTPStarGift &data) { @@ -202,6 +204,12 @@ struct FileReferenceAccumulator { void push(const MTPstories_Stories &data) { push(data.data().vstories()); } + void push(const MTPusers_SavedMusic &data) { + data.match([&](const MTPDusers_savedMusic &data) { + push(data.vdocuments()); + }, [](const MTPDusers_savedMusicNotModified &data) { + }); + } UpdatedFileReferences result; }; @@ -271,6 +279,10 @@ UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) { return GetFileReferencesHelper(data); } +UpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data) { + return GetFileReferencesHelper(data); +} + UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) { return GetFileReferencesHelper(data); } diff --git a/Telegram/SourceFiles/data/data_file_origin.h b/Telegram/SourceFiles/data/data_file_origin.h index 33e6a3f0d7..283a0fe034 100644 --- a/Telegram/SourceFiles/data/data_file_origin.h +++ b/Telegram/SourceFiles/data/data_file_origin.h @@ -221,6 +221,7 @@ UpdatedFileReferences GetFileReferences( UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data); UpdatedFileReferences GetFileReferences(const MTPmessages_WebPage &data); UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data); +UpdatedFileReferences GetFileReferences(const MTPusers_SavedMusic &data); // Admin Log Event. UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data); diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 3890b4c0ed..4fa805b6e3 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -7,12 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_forum.h" +#include "data/components/recent_peers.h" #include "data/data_channel.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_forum_icons.h" #include "data/data_forum_topic.h" +#include "data/data_replies_list.h" #include "data/notify/data_notify_settings.h" #include "history/history.h" #include "history/history_item.h" @@ -193,6 +195,7 @@ void Forum::applyTopicDeleted(MsgId rootId) { _activeSubsectionTopic = nullptr; } _topicDestroyed.fire(raw); + _history->session().recentPeers().chatOpenRemove(raw); session().changes().topicUpdated( raw, Data::TopicUpdate::Flag::Destroyed); @@ -276,6 +279,33 @@ Thread *Forum::activeSubsectionThread() const { return _activeSubsectionTopic; } +void Forum::markUnreadCountsUnknown(MsgId readTillId) { + if (!channel()->useSubsectionTabs()) { + return; + } + for (const auto &[rootId, topic] : _topics) { + const auto replies = topic->replies(); + if (replies->unreadCountCurrent() > 0) { + replies->setInboxReadTill(readTillId, std::nullopt); + } + } +} + +void Forum::updateUnreadCounts( + MsgId readTillId, + const base::flat_map, int> &counts) { + if (!channel()->useSubsectionTabs()) { + return; + } + for (const auto &[rootId, topic] : _topics) { + const auto raw = topic.get(); + const auto replies = raw->replies(); + const auto i = counts.find(raw); + const auto count = (i != end(counts)) ? i->second : 0; + replies->setInboxReadTill(readTillId, count); + } +} + void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) { if (from || to) { reorderLastTopics(); diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h index ae0cc2d1a5..cccacadeef 100644 --- a/Telegram/SourceFiles/data/data_forum.h +++ b/Telegram/SourceFiles/data/data_forum.h @@ -99,6 +99,11 @@ public: void saveActiveSubsectionThread(not_null thread); [[nodiscard]] Thread *activeSubsectionThread() const; + void markUnreadCountsUnknown(MsgId readTillId); + void updateUnreadCounts( + MsgId readTillId, + const base::flat_map, int> &counts); + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 799190714d..a8d05913e0 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/color_int_conversion.h" #include "ui/text/text_custom_emoji.h" +#include "ui/text/text_utilities.h" #include "styles/style_dialogs.h" #include "styles/style_chat_helpers.h" @@ -756,6 +757,16 @@ TextWithEntities ForumTopic::titleWithIcon() const { return ForumTopicIconWithTitle(_rootId, _iconId, _title); } +TextWithEntities ForumTopic::titleWithIconOrLogo() const { + if (_iconId || isGeneral()) { + return titleWithIcon(); + } + return Ui::Text::SingleCustomEmoji(Data::TopicIconEmojiEntity({ + .title = _title, + .colorId = _colorId, + })).append(' ').append(_title); +} + int ForumTopic::titleVersion() const { return _titleVersion; } diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index aafa12a788..6b8d81ff0a 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -148,6 +148,7 @@ public: [[nodiscard]] QString title() const; [[nodiscard]] TextWithEntities titleWithIcon() const; + [[nodiscard]] TextWithEntities titleWithIconOrLogo() const; [[nodiscard]] int titleVersion() const; void applyTitle(const QString &title); [[nodiscard]] DocumentId iconId() const; diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 2ebbc0dfdf..45899f59b4 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -10,13 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" -#include "data/data_saved_sublist.h" -#include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" +#include "data/data_document.h" #include "data/data_folder.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_saved_music.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" #include "data/data_user.h" #include "base/unixtime.h" #include "base/random.h" @@ -729,7 +731,7 @@ void Histories::sendReadRequest(not_null history, State &state) { } else { Assert(!state->sentReadTill || state->sentReadTill > tillId); } - history->validateMonoforumUnread(tillId); + history->validateMonoAndForumUnread(tillId); sendReadRequests(); finish(); }; @@ -901,10 +903,10 @@ void Histories::deleteMessagesByDates( } void Histories::deleteMessagesByDates( - not_null history, - TimeId minDate, - TimeId maxDate, - bool revoke) { + not_null history, + TimeId minDate, + TimeId maxDate, + bool revoke) { sendRequest(history, RequestType::Delete, [=](Fn finish) { const auto peer = history->peer; using Flag = MTPmessages_DeleteHistory::Flag; @@ -937,10 +939,14 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { base::flat_map, QVector> idsByPeer; base::flat_map, QVector> scheduledIdsByPeer; base::flat_map> quickIdsByShortcut; + base::flat_set> savedMusic; for (const auto &itemId : ids) { if (const auto item = _owner->message(itemId)) { const auto history = item->history(); - if (item->isScheduled()) { + if (item->isSavedMusicItem()) { + savedMusic.emplace(item->media()->document()); + continue; + } else if (item->isScheduled()) { const auto wasOnServer = !item->isSending() && !item->hasFailed(); auto &scheduled = _owner->session().scheduledMessages(); @@ -989,6 +995,9 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { api->applyUpdates(result); }).send(); } + for (const auto &document : savedMusic) { + document->owner().savedMusic().remove(document); + } for (const auto item : remove) { const auto history = item->history(); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index c8a1d35d07..6f1001b20c 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -225,8 +225,8 @@ template Expects(media->owner()->hasThumbnail()); const auto document = media->owner(); - const auto readyCacheKey = CountCacheKey(document, radius, spoiler); if (const auto thumbnail = media->thumbnail()) { + const auto readyCacheKey = CountCacheKey(document, radius, spoiler); return { PreparePreviewImage(thumbnail, radius, spoiler), readyCacheKey, @@ -259,7 +259,8 @@ template bool spoiler) { auto result = PreparePhotoPreviewImage(item, media, radius, spoiler); if (!result.data.isNull() - && media->owner()->extendedMediaVideoDuration().has_value()) { + && (media->owner()->extendedMediaVideoDuration().has_value() + || (item->media() && item->media()->videoCover()))) { result.data = PutPlayIcon(std::move(result.data)); } return result; @@ -1157,7 +1158,28 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { const auto radius = _document->isVideoMessage() ? ImageRoundRadius::Ellipse : ImageRoundRadius::Small; - if (auto found = FindCachedPreview( + if (_videoCover) { + if (auto found = FindCachedPreview( + existing, + not_null{ _videoCover }, + radius, + spoilered)) { + images.push_back(std::move(found)); + } else { + const auto media = _videoCover->createMediaView(); + if (auto prepared = PreparePhotoPreview( + parent(), + media, + radius, + _spoiler) + ; prepared || !prepared.cacheKey) { + images.push_back(std::move(prepared)); + if (!prepared.cacheKey) { + context = media; + } + } + } + } else if (auto found = FindCachedPreview( existing, _document, radius, @@ -2210,8 +2232,7 @@ ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const { ? parent()->translatedText() : parent()->originalText()); const auto hasMiniImages = !images.empty(); - auto nice = Ui::Text::Colorized( - Ui::CreditsEmojiSmall(&parent()->history()->session())); + auto nice = Ui::Text::Colorized(Ui::CreditsEmojiSmall()); nice.append(WithCaptionNotificationText(type, caption, hasMiniImages)); return { .text = std::move(nice), @@ -2564,7 +2585,11 @@ std::unique_ptr MediaGiftBox::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - if (const auto &unique = _data.unique) { + if (_data.type == GiftType::ChatTheme) { + return std::make_unique( + message, + std::make_unique(message, this)); + } else if (const auto &unique = _data.unique) { return std::make_unique( message, HistoryView::GenerateUniqueGiftMedia(message, replacing, unique), diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index bf5d003830..a8fce546d0 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -138,6 +138,7 @@ enum class GiftType : uchar { Credits, // count - credits Ton, // count - nano tons StarGift, // count - stars + ChatTheme, }; struct GiftCode { @@ -150,8 +151,9 @@ struct GiftCode { ChannelData *channel = nullptr; PeerData *channelFrom = nullptr; uint64 channelSavedId = 0; + QString giftPrepayUpgradeHash; MsgId giveawayMsgId = 0; - MsgId upgradeMsgId = 0; + MsgId realGiftMsgId = 0; int starsConverted = 0; int starsToUpgrade = 0; int starsUpgradedBySender = 0; @@ -161,6 +163,7 @@ struct GiftCode { GiftType type = GiftType::Premium; bool viaGiveaway : 1 = false; bool transferred : 1 = false; + bool upgradeSeparate : 1 = false; bool upgradable : 1 = false; bool unclaimed : 1 = false; bool anonymous : 1 = false; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 97683de736..8605ae3d16 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -679,6 +679,27 @@ bool PeerData::canManageTopics() const { return false; } +bool PeerData::canPostStories() const { + if (const auto channel = asChannel()) { + return channel->canPostStories(); + } + return isSelf(); +} + +bool PeerData::canEditStories() const { + if (const auto channel = asChannel()) { + return channel->canEditStories(); + } + return isSelf(); +} + +bool PeerData::canDeleteStories() const { + if (const auto channel = asChannel()) { + return channel->canDeleteStories(); + } + return isSelf(); +} + bool PeerData::canManageGifts() const { if (const auto channel = asChannel()) { return channel->canPostMessages(); @@ -1671,6 +1692,13 @@ int PeerData::starsPerMessageChecked() const { return starsPerMessage(); } +Data::StarsRating PeerData::starsRating() const { + if (const auto user = asUser()) { + return user->starsRating(); + } + return {}; +} + Data::GroupCall *PeerData::groupCall() const { if (const auto chat = asChat()) { return chat->groupCall(); @@ -1689,24 +1717,23 @@ PeerId PeerData::groupCallDefaultJoinAs() const { return 0; } -void PeerData::setThemeEmoji(const QString &emoticon) { - if (_themeEmoticon == emoticon) { +void PeerData::setThemeToken(const QString &token) { + if (_themeToken == token) { + return; + } else if (!token.startsWith(u"gift:"_q) + && Ui::Emoji::Find(_themeToken) == Ui::Emoji::Find(token)) { + _themeToken = token; return; } - if (Ui::Emoji::Find(_themeEmoticon) == Ui::Emoji::Find(emoticon)) { - _themeEmoticon = emoticon; - return; + _themeToken = token; + if (!token.isEmpty() && !owner().cloudThemes().themeForToken(token)) { + owner().cloudThemes().refreshChatThemesFor(token); } - _themeEmoticon = emoticon; - if (!emoticon.isEmpty() - && !owner().cloudThemes().themeForEmoji(emoticon)) { - owner().cloudThemes().refreshChatThemes(); - } - session().changes().peerUpdated(this, UpdateFlag::ChatThemeEmoji); + session().changes().peerUpdated(this, UpdateFlag::ChatThemeToken); } -const QString &PeerData::themeEmoji() const { - return _themeEmoticon; +const QString &PeerData::themeToken() const { + return _themeToken; } void PeerData::setWallPaper( diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 67113ffce9..95b68a2544 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -7,10 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/notify/data_peer_notify_settings.h" #include "data/data_types.h" #include "data/data_flags.h" -#include "data/notify/data_peer_notify_settings.h" #include "data/data_cloud_file.h" +#include "data/data_peer_common.h" #include "ui/userpic_view.h" struct BotInfo; @@ -291,6 +292,7 @@ public: [[nodiscard]] int starsPerMessage() const; [[nodiscard]] int starsPerMessageChecked() const; + [[nodiscard]] Data::StarsRating starsRating() const; [[nodiscard]] UserData *asBot(); [[nodiscard]] const UserData *asBot() const; @@ -433,6 +435,9 @@ public: [[nodiscard]] bool canCreateTodoLists() const; [[nodiscard]] bool canCreateTopics() const; [[nodiscard]] bool canManageTopics() const; + [[nodiscard]] bool canPostStories() const; + [[nodiscard]] bool canEditStories() const; + [[nodiscard]] bool canDeleteStories() const; [[nodiscard]] bool canManageGifts() const; [[nodiscard]] bool canTransferGifts() const; [[nodiscard]] bool canExportChatHistory() const; @@ -526,8 +531,8 @@ public: [[nodiscard]] Data::GroupCall *groupCall() const; [[nodiscard]] PeerId groupCallDefaultJoinAs() const; - void setThemeEmoji(const QString &emoticon); - [[nodiscard]] const QString &themeEmoji() const; + void setThemeToken(const QString &token); + [[nodiscard]] const QString &themeToken() const; void setWallPaper( std::optional paper, @@ -611,7 +616,7 @@ private: uint8 _userpicHasVideo : 1 = 0; QString _about; - QString _themeEmoticon; + QString _themeToken; std::unique_ptr _wallPaper; }; diff --git a/Telegram/SourceFiles/data/data_peer_common.h b/Telegram/SourceFiles/data/data_peer_common.h new file mode 100644 index 0000000000..656f089f9f --- /dev/null +++ b/Telegram/SourceFiles/data/data_peer_common.h @@ -0,0 +1,34 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { + +struct StarsRating { + int level = 0; + int stars = 0; + int thisLevelStars = 0; + int nextLevelStars = 0; + + explicit operator bool() const { + return level != 0 || thisLevelStars != 0; + } + + friend inline bool operator==(StarsRating, StarsRating) = default; +}; + +struct StarsRatingPending { + StarsRating value; + TimeId date = 0; + + explicit operator bool() const { + return value && date; + } +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index d418191fbb..bdbe2a43b8 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -527,6 +527,23 @@ bool ChannelHasSubscriptionUntilDate(ChannelData *channel) { return channel && channel->subscriptionUntilDate() > 0; } +rpl::producer StarsRatingValue( + not_null peer) { + if (const auto user = peer->asUser()) { + return user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::StarsRating + ) | rpl::map([=] { + auto result = user->starsRating(); + if (!user->isSelf() && result.level < 0) { + result.stars = 0; + } + return result; + }); + } + return rpl::single(Data::StarsRating()); +} + rpl::producer PeerUserpicImageValue( not_null peer, int size, diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index b726746be0..322e1dda39 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "data/data_peer.h" +#include "data/data_peer_values.h" #include "data/data_chat_participant_status.h" enum class ImageRoundRadius; @@ -166,6 +167,9 @@ inline auto PeerFullFlagValue( [[nodiscard]] bool ChannelHasActiveCall(not_null channel); [[nodiscard]] bool ChannelHasSubscriptionUntilDate(ChannelData *channel); +[[nodiscard]] rpl::producer StarsRatingValue( + not_null peer); + [[nodiscard]] rpl::producer PeerUserpicImageValue( not_null peer, int size, diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index 64a3176d55..658f7c209f 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -70,12 +70,9 @@ bool PollData::closeByTimer() { bool PollData::applyChanges(const MTPDpoll &poll) { Expects(poll.vid().v == id); - const auto newQuestion = TextWithEntities{ - .text = qs(poll.vquestion().data().vtext()), - .entities = Api::EntitiesFromMTP( - &session(), - poll.vquestion().data().ventities().v), - }; + const auto newQuestion = Api::ParseTextWithEntities( + &session(), + poll.vquestion()); const auto newFlags = (poll.is_closed() ? Flag::Closed : Flag(0)) | (poll.is_public_voters() ? Flag::PublicVotes : Flag(0)) | (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0)) @@ -88,12 +85,9 @@ bool PollData::applyChanges(const MTPDpoll &poll) { return data.match([&](const MTPDpollAnswer &answer) { auto result = PollAnswer(); result.option = answer.voption().v; - result.text = TextWithEntities{ - .text = qs(answer.vtext().data().vtext()), - .entities = Api::EntitiesFromMTP( - &session(), - answer.vtext().data().ventities().v), - }; + result.text = Api::ParseTextWithEntities( + &session(), + answer.vtext()); return result; }); }) | ranges::views::take( diff --git a/Telegram/SourceFiles/data/data_premium_subscription_option.h b/Telegram/SourceFiles/data/data_premium_subscription_option.h index 5b11a532f0..42fa985d5a 100644 --- a/Telegram/SourceFiles/data/data_premium_subscription_option.h +++ b/Telegram/SourceFiles/data/data_premium_subscription_option.h @@ -16,6 +16,7 @@ struct PremiumSubscriptionOption { QString costPerMonth; QString costNoDiscount; QString costTotal; + QString currency; QString total; QString botUrl; }; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index a7ee009257..48db91d4a9 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "core/application.h" +#include "data/components/recent_peers.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_histories.h" @@ -452,6 +453,7 @@ void SavedMessages::applySublistDeleted(not_null sublistPeer) { } _sublistDestroyed.fire(raw); + _owner->session().recentPeers().chatOpenRemove(raw); session().changes().sublistUpdated( raw, Data::SublistUpdate::Flag::Destroyed); diff --git a/Telegram/SourceFiles/data/data_saved_music.cpp b/Telegram/SourceFiles/data/data_saved_music.cpp new file mode 100644 index 0000000000..0d0e9d325e --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_music.cpp @@ -0,0 +1,384 @@ +/* +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_saved_music.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_document.h" +#include "data/data_file_origin.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "main/main_session.h" +#include "ui/ui_utility.h" + +namespace Data { +namespace { + +constexpr auto kPerPage = 50; +constexpr auto kReloadIdsEvery = 30 * crl::time(1000); + +[[nodiscard]] not_null ItemDocument( + not_null item) { + return item->media()->document(); +} + +} // namespace + +SavedMusic::SavedMusic(not_null owner) +: _owner(owner) { +} + +SavedMusic::~SavedMusic() { + Expects(_entries.empty()); +} + +void SavedMusic::clear() { + base::take(_entries); +} + +bool SavedMusic::Supported(PeerId peerId) { + return peerId && peerIsUser(peerId); +} + +not_null SavedMusic::musicIdToMsg( + PeerId peerId, + Entry &entry, + not_null id) { + const auto i = entry.musicIdToMsg.find(id); + if (i != end(entry.musicIdToMsg)) { + return i->second.get(); + } else if (!entry.history) { + entry.history = _owner->history(peerId); + } + return entry.musicIdToMsg.emplace(id, entry.history->makeMessage({ + .id = entry.history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | MessageFlag::SavedMusicItem), + .from = entry.history->peer->id, + .date = base::unixtime::now(), + }, id, TextWithEntities())).first->second.get(); +} + +void SavedMusic::loadIds() { + if (_loadIdsRequest + || (_lastReceived + && (crl::now() - _lastReceived < kReloadIdsEvery))) { + return; + } + _loadIdsRequest = _owner->session().api().request( + MTPaccount_GetSavedMusicIds(MTP_long(Api::CountHash(_myIds))) + ).done([=](const MTPaccount_SavedMusicIds &result) { + _loadIdsRequest = 0; + _lastReceived = crl::now(); + result.match([&](const MTPDaccount_savedMusicIds &data) { + _myIds = data.vids().v + | ranges::views::transform(&MTPlong::v) + | ranges::to_vector; + }, [](const MTPDaccount_savedMusicIdsNotModified &) { + }); + }).fail([=] { + _loadIdsRequest = 0; + _lastReceived = crl::now(); + }).send(); +} + +bool SavedMusic::has(not_null document) const { + return ranges::contains(_myIds, document->id); +} + +void SavedMusic::save( + not_null document, + Data::FileOrigin origin) { + const auto peerId = _owner->session().userPeerId(); + auto &entry = _entries[peerId]; + if (entry.list.empty() && !entry.loaded) { + loadMore(peerId); + } + if (has(document)) { + return; + } + const auto item = musicIdToMsg(peerId, entry, document); + entry.list.insert(begin(entry.list), item); + if (entry.total >= 0) { + ++entry.total; + } + _myIds.insert(begin(_myIds), document->id); + + const auto send = [=](auto resend) -> void { + const auto usedFileReference = document->fileReference(); + _owner->session().api().request(MTPaccount_SaveMusic( + MTP_flags(0), + document->mtpInput(), + MTPInputDocument() + )).fail([=](const MTP::Error &error) { + if (error.code() == 400 + && error.type().startsWith(u"FILE_REFERENCE_"_q)) { + document->session().api().refreshFileReference(origin, [=]( + const auto &) { + if (document->fileReference() != usedFileReference) { + resend(resend); + } + }); + } + }).send(); + }; + send(send); + + _changed.fire_copy(peerId); +} + +void SavedMusic::remove(not_null document) { + const auto peerId = _owner->session().userPeerId(); + auto &entry = _entries[peerId]; + const auto i = ranges::find(entry.list, document, ItemDocument); + if (i != end(entry.list)) { + entry.musicIdFromMsgId.remove((*i)->id); + entry.list.erase(i); + if (entry.total > 0) { + entry.total = std::max(entry.total - 1, 0); + } + } + entry.musicIdToMsg.remove(document); + _myIds.erase(ranges::remove(_myIds, document->id), end(_myIds)); + _owner->session().api().request(MTPaccount_SaveMusic( + MTP_flags(MTPaccount_SaveMusic::Flag::f_unsave), + document->mtpInput(), + MTPInputDocument() + )).send(); + _changed.fire_copy(peerId); +} + +void SavedMusic::apply(not_null user, const MTPDocument *last) { + const auto peerId = user->id; + auto &entry = _entries[peerId]; + if (!last) { + if (const auto requestId = base::take(entry.requestId)) { + _owner->session().api().request(requestId).cancel(); + } + entry = Entry{ .total = 0, .loaded = true }; + _changed.fire_copy(peerId); + return; + } + const auto document = _owner->processDocument(*last); + const auto i = ranges::find(entry.list, document, ItemDocument); + if (i != end(entry.list)) { + if (i == begin(entry.list)) { + return; + } + ranges::rotate(begin(entry.list), i, i + 1); + _changed.fire_copy(peerId); + loadMore(peerId, true); + return; + } + entry.list.insert( + begin(entry.list), + musicIdToMsg(peerId, entry, document)); + _changed.fire_copy(peerId); + if (entry.loaded) { + loadMore(peerId, true); + } +} + +bool SavedMusic::countKnown(PeerId peerId) const { + if (!Supported(peerId)) { + return true; + } + const auto entry = lookupEntry(peerId); + return entry && entry->total >= 0; +} + +int SavedMusic::count(PeerId peerId) const { + if (!Supported(peerId)) { + return 0; + } + const auto entry = lookupEntry(peerId); + return entry ? std::max(entry->total, 0) : 0; +} + +const std::vector> &SavedMusic::list( + PeerId peerId) const { + static const auto empty = std::vector>(); + if (!Supported(peerId)) { + return empty; + } + + const auto entry = lookupEntry(peerId); + return entry ? entry->list : empty; +} + +void SavedMusic::loadMore(PeerId peerId) { + loadMore(peerId, false); +} + +void SavedMusic::loadMore(PeerId peerId, bool reload) { + if (!Supported(peerId)) { + return; + } + + auto &entry = _entries[peerId]; + if (!entry.reloading && reload) { + _owner->session().api().request( + base::take(entry.requestId)).cancel(); + } + if ((!reload && entry.loaded) || entry.requestId) { + return; + } + const auto user = _owner->peer(peerId)->asUser(); + Assert(user != nullptr); + + entry.reloading = reload; + entry.requestId = _owner->session().api().request(MTPusers_GetSavedMusic( + user->inputUser, + MTP_int(reload ? 0 : entry.list.size()), + MTP_int(kPerPage), + MTP_long(reload ? firstPageHash(entry) : 0) + )).done([=](const MTPusers_SavedMusic &result) { + auto &entry = _entries[peerId]; + entry.requestId = 0; + const auto reloaded = base::take(entry.reloading); + result.match([&](const MTPDusers_savedMusicNotModified &) { + }, [&](const MTPDusers_savedMusic &data) { + const auto list = data.vdocuments().v; + const auto count = int(list.size()); + entry.total = std::max(count, data.vcount().v); + if (reloaded) { + entry.list.clear(); + } + for (const auto &item : list) { + const auto document = _owner->processDocument(item); + if (!ranges::contains(entry.list, document, ItemDocument)) { + entry.list.push_back( + musicIdToMsg(peerId, entry, document)); + } + } + entry.loaded = list.empty() || (count == entry.list.size()); + }); + _changed.fire_copy(peerId); + }).fail([=](const MTP::Error &error) { + auto &entry = _entries[peerId]; + entry.requestId = 0; + entry.total = int(entry.list.size()); + entry.loaded = true; + _changed.fire_copy(peerId); + }).send(); +} + +uint64 SavedMusic::firstPageHash(const Entry &entry) const { + return Api::CountHash(entry.list + | ranges::views::transform(ItemDocument) + | ranges::views::transform(&DocumentData::id) + | ranges::views::take(kPerPage)); +} + +rpl::producer SavedMusic::changed() const { + return _changed.events(); +} + +SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) { + if (!Supported(peerId)) { + return nullptr; + } + + auto it = _entries.find(peerId); + if (it == end(_entries)) { + return nullptr; + } + return &it->second; +} + +const SavedMusic::Entry *SavedMusic::lookupEntry(PeerId peerId) const { + return const_cast(this)->lookupEntry(peerId); +} + +rpl::producer SavedMusicList( + not_null peer, + HistoryItem *aroundId, + int limit) { + if (!peer->isUser()) { + return rpl::single(SavedMusicSlice({}, 0, 0, 0)); + } + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + SavedMusicSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto peerId = peer->id; + const auto savedMusic = &peer->owner().savedMusic(); + if (!savedMusic->countKnown(peerId)) { + return; + } + const auto &loaded = savedMusic->list(peerId); + const auto count = savedMusic->count(peerId); + auto i = aroundId + ? ranges::find(loaded, not_null(aroundId)) + : begin(loaded); + if (i == end(loaded)) { + i = begin(loaded); + } + const auto hasBefore = int(i - begin(loaded)); + const auto hasAfter = int(end(loaded) - i); + if (hasAfter < limit) { + savedMusic->loadMore(peerId); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = std::vector>(); + ids.reserve(takeBefore + takeAfter); + for (auto j = i - takeBefore; j != i + takeAfter; ++j) { + ids.push_back(*j); + } + const auto added = int(ids.size()); + state->slice = SavedMusicSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto peerId = peer->id; + const auto savedMusic = &peer->owner().savedMusic(); + savedMusic->changed( + ) | rpl::filter( + rpl::mappers::_1 == peerId + ) | rpl::start_with_next(schedule, lifetime); + + if (!savedMusic->countKnown(peerId)) { + savedMusic->loadMore(peerId); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_music.h b/Telegram/SourceFiles/data/data_saved_music.h new file mode 100644 index 0000000000..e3711f901e --- /dev/null +++ b/Telegram/SourceFiles/data/data_saved_music.h @@ -0,0 +1,87 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_abstract_sparse_ids.h" + +#include "history/history_item.h" + +class PeerData; + +namespace Data { + +class Session; +struct FileOrigin; + +class SavedMusic final { +public: + explicit SavedMusic(not_null owner); + ~SavedMusic(); + + [[nodiscard]] static bool Supported(PeerId peerId); + + [[nodiscard]] bool countKnown(PeerId peerId) const; + [[nodiscard]] int count(PeerId peerId) const; + [[nodiscard]] const std::vector> &list( + PeerId peerId) const; + void loadMore(PeerId peerId); + + [[nodiscard]] rpl::producer changed() const; + + void loadIds(); + [[nodiscard]] bool has(not_null document) const; + void save(not_null document, FileOrigin origin); + void remove(not_null document); + + void apply(not_null user, const MTPDocument *last); + + void clear(); + +private: + using OwnedItem = std::unique_ptr; + + struct Entry { + base::flat_map> musicIdFromMsgId; + base::flat_map, OwnedItem> musicIdToMsg; + std::vector> list; + History *history = nullptr; + mtpRequestId requestId = 0; + int total = -1; + bool loaded = false; + bool reloading = false; + }; + + void loadMore(PeerId peerId, bool reload); + [[nodiscard]] Entry *lookupEntry(PeerId peerId); + [[nodiscard]] const Entry *lookupEntry(PeerId peerId) const; + [[nodiscard]] uint64 firstPageHash(const Entry &entry) const; + [[nodiscard]] not_null musicIdToMsg( + PeerId peerId, + Entry &entry, + not_null id); + + const not_null _owner; + + std::vector _myIds; + crl::time _lastReceived = 0; + mtpRequestId _loadIdsRequest = 0; + + std::unordered_map _entries; + rpl::event_stream _changed; + +}; + +using SavedMusicSlice = AbstractSparseIds< + std::vector>>; + +[[nodiscard]] rpl::producer SavedMusicList( + not_null peer, + HistoryItem *aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 6037051148..39bc416790 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -66,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" #include "data/data_saved_messages.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "data/data_streaming.h" @@ -91,6 +92,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { namespace { +constexpr auto kNextForUpgradeGiftTimeout = 5 * crl::time(1000); + using ViewElement = HistoryView::Element; // s: box 100x100 @@ -254,6 +257,7 @@ Session::Session(not_null session) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) , _stories(std::make_unique(this)) +, _savedMusic(std::make_unique(this)) , _savedMessages(std::make_unique(this)) , _chatbots(std::make_unique(this)) , _businessInfo(std::make_unique(this)) @@ -415,6 +419,7 @@ void Session::clear() { channel->setFlags(channel->flags() & ~(ChannelDataFlag::Forum | ChannelDataFlag::MonoforumAdmin)); } + _savedMusic->clear(); _savedMessages->clear(); _sendActionManager->clear(); @@ -1413,6 +1418,9 @@ UserData *Session::userByPhone(const QString &phone) const { PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); + if (uname.isEmpty()) { + return nullptr; + } for (const auto &[peerId, peer] : _peers) { if (peer->isLoaded() && !peer->username().compare(uname, Qt::CaseInsensitive)) { @@ -1854,6 +1862,14 @@ rpl::producer Session::giftUpdates() const { return _giftUpdates.events(); } +void Session::notifyGiftsUpdate(GiftsUpdate &&update) { + _giftsUpdates.fire(std::move(update)); +} + +rpl::producer Session::giftsUpdates() const { + return _giftsUpdates.events(); +} + HistoryItem *Session::changeMessageId(PeerId peerId, MsgId wasId, MsgId nowId) { const auto list = messagesListForInsert(peerId); const auto i = list->find(wasId); @@ -2158,6 +2174,57 @@ rpl::producer<> Session::pinnedDialogsOrderUpdated() const { return _pinnedDialogsOrderUpdated.events(); } +void Session::nextForUpgradeGiftInvalidate(not_null owner) { + _nextForUpgradeGifts.remove(owner); +} + +void Session::nextForUpgradeGiftRequest( + not_null owner, + Fn)> done) { + auto &entry = _nextForUpgradeGifts[owner]; + if (entry.requestId) { + entry.done = std::move(done); + return; + } else if (crl::now() - entry.received < kNextForUpgradeGiftTimeout) { + done(entry.gift); + return; + } + entry.done = std::move(done); + + const auto finishWith = [=](std::optional gift) { + auto &entry = _nextForUpgradeGifts[owner]; + entry.requestId = 0; + entry.gift = std::move(gift); + entry.received = crl::now(); + base::take(entry.done)(entry.gift); + }; + using Flag = MTPpayments_GetSavedStarGifts::Flag; + entry.requestId = _session->api().request( + MTPpayments_GetSavedStarGifts( + MTP_flags(Flag::f_exclude_unique + | Flag::f_exclude_unlimited + | Flag::f_exclude_unupgradable), + owner->input, + MTPint(), // collection_id + MTP_string(), // offset + MTP_int(1)) // limit + ).done([=](const MTPpayments_SavedStarGifts &result) { + const auto &data = result.data(); + processUsers(data.vusers()); + processChats(data.vchats()); + const auto &list = data.vgifts().v; + if (list.empty()) { + finishWith(std::nullopt); + } else if (auto parsed = Api::FromTL(owner, list[0])) { + finishWith(std::move(*parsed)); + } else { + finishWith(std::nullopt); + } + }).fail([=](const MTP::Error &error) { + finishWith(std::nullopt); + }).send(); +} + Session::CreditsSubsRebuilderPtr Session::createCreditsSubsRebuilder() { if (auto result = activeCreditsSubsRebuilder()) { return result; @@ -3848,6 +3915,8 @@ void Session::webpageApplyFields( return (DocumentData*)nullptr; }, [](const MTPDwebPageAttributeUniqueStarGift &) { return (DocumentData*)nullptr; + }, [](const MTPDwebPageAttributeStarGiftCollection &) { + return (DocumentData*)nullptr; }); if (result) { return result; @@ -3870,6 +3939,14 @@ void Session::webpageApplyFields( result->items.push_back(processDocument(tl)); } return result; + }, [&](const MTPDwebPageAttributeStarGiftCollection &data) { + auto result = std::make_unique(); + result->isEmoji = false; + result->isTextColor = false; + for (const auto &tl : data.vicons().v) { + result->items.push_back(processDocument(tl)); + } + return result; }, [](const auto &) { return WebPageStickerSetPtr(nullptr); }); @@ -4375,6 +4452,9 @@ void Session::unregisterPhotoItem( void Session::registerDocumentItem( not_null document, not_null item) { + if (document->isMusicForProfile()) { + document->owner().savedMusic().loadIds(); + } _documentItems[document].insert(item); } @@ -5228,6 +5308,16 @@ int Session::commonStarsPerMessage( return (i != end(_commonStarsPerMessage)) ? i->second : 0; } +void Session::setPendingStarsRating(StarsRatingPending value) { + _pendingStarsRating = value + ? std::make_unique(value) + : nullptr; +} + +StarsRatingPending Session::pendingStarsRating() const { + return _pendingStarsRating ? *_pendingStarsRating : StarsRatingPending(); +} + void Session::clearLocalStorage() { _cache->close(); _cache->clear(); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 701eaf08db..fba78f17ba 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -66,12 +66,14 @@ class GroupCall; class NotifySettings; class CustomEmojiManager; class Stories; +class SavedMusic; class SavedMessages; class Chatbots; class BusinessInfo; struct ReactionId; struct UnavailableReason; struct CreditsStatusSlice; +struct StarsRatingPending; struct UniqueGift; struct RepliesReadTillUpdate { @@ -103,6 +105,12 @@ struct GiftUpdate { QString slug; Action action = {}; }; +struct GiftsUpdate { + not_null peer; + int collectionId = 0; + std::vector added; + std::vector removed; +}; struct SentToScheduled { not_null history; @@ -177,6 +185,9 @@ public: [[nodiscard]] Stories &stories() const { return *_stories; } + [[nodiscard]] SavedMusic &savedMusic() const { + return *_savedMusic; + } [[nodiscard]] SavedMessages &savedMessages() const { return *_savedMessages; } @@ -334,6 +345,8 @@ public: [[nodiscard]] rpl::producer> newItemAdded() const; void notifyGiftUpdate(GiftUpdate &&update); [[nodiscard]] rpl::producer giftUpdates() const; + void notifyGiftsUpdate(GiftsUpdate &&update); + [[nodiscard]] rpl::producer giftsUpdates() const; void requestItemRepaint(not_null item); [[nodiscard]] rpl::producer> itemRepaintRequest() const; void requestViewRepaint(not_null view); @@ -367,6 +380,11 @@ public: void notifyPinnedDialogsOrderUpdated(); [[nodiscard]] rpl::producer<> pinnedDialogsOrderUpdated() const; + void nextForUpgradeGiftInvalidate(not_null owner); + void nextForUpgradeGiftRequest( + not_null owner, + Fn)> done); + using CreditsSubsRebuilder = rpl::event_stream; using CreditsSubsRebuilderPtr = std::shared_ptr; [[nodiscard]] CreditsSubsRebuilderPtr createCreditsSubsRebuilder(); @@ -864,11 +882,21 @@ public: [[nodiscard]] int commonStarsPerMessage( not_null channel) const; + void setPendingStarsRating(StarsRatingPending value); + [[nodiscard]] StarsRatingPending pendingStarsRating() const; + void clearLocalStorage(); private: using Messages = std::unordered_map>; + struct NextToUpgradeGift { + std::optional gift; + Fn)> done; + crl::time received = 0; + mtpRequestId requestId = 0; + }; + void suggestStartExport(); void setupMigrationViewer(); @@ -1008,7 +1036,7 @@ private: Storage::DatabasePointer _bigFileCache; TimeId _exportAvailableAt = 0; - QPointer _exportSuggestion; + base::weak_qptr _exportSuggestion; rpl::variable _contactsLoaded = false; rpl::variable _groupFreeTranscribeLevel; @@ -1022,6 +1050,7 @@ private: rpl::event_stream> _viewLayoutChanges; rpl::event_stream> _newItemAdded; rpl::event_stream _giftUpdates; + rpl::event_stream _giftsUpdates; rpl::event_stream> _itemRepaintRequest; rpl::event_stream> _viewRepaintRequest; rpl::event_stream> _itemResizeRequest; @@ -1210,6 +1239,7 @@ private: const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; + const std::unique_ptr _savedMusic; const std::unique_ptr _savedMessages; const std::unique_ptr _chatbots; const std::unique_ptr _businessInfo; @@ -1217,6 +1247,12 @@ private: MsgId _nonHistoryEntryId = ShortcutMaxMsgId; + std::unique_ptr _pendingStarsRating; + + base::flat_map< + not_null, + NextToUpgradeGift> _nextForUpgradeGifts; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 5e0c3735d2..3ee5f54785 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -7,18 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_shared_media.h" -#include -#include "main/main_session.h" #include "apiwrap.h" -#include "storage/storage_facade.h" -#include "history/history.h" -#include "history/history_item.h" +#include "core/crash_reports.h" #include "data/components/scheduled_messages.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_photo.h" +#include "data/data_saved_music.h" #include "data/data_session.h" -#include "core/crash_reports.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "storage/storage_facade.h" namespace { @@ -233,6 +233,39 @@ rpl::producer SharedScheduledMediaViewer( }); } +rpl::producer SavedMusicMediaViewer( + not_null session, + SharedMediaMergedKey key, + int limitBefore, + int limitAfter) { + Expects((key.mergedKey.universalId != 0) + || (limitBefore == 0 && limitAfter == 0)); + + const auto peerId = key.mergedKey.peerId; + const auto item = key.mergedKey.universalId + ? session->data().message(peerId, key.mergedKey.universalId) + : nullptr; + + return Data::SavedMusicList( + session->data().peer(peerId), + item, + std::max(limitBefore, limitAfter) + ) | rpl::map([=](const Data::SavedMusicSlice &slice) { + auto list = std::vector(); + list.reserve(slice.size()); + for (auto i = 0, count = int(slice.size()); i != count; ++i) { + list.push_back(slice[i]->id); + } + return SparseIdsMergedSlice( + key.mergedKey, + SparseUnsortedIdsSlice( + std::move(list), + slice.fullCount(), + slice.skippedBefore(), + slice.skippedAfter())); + }); +} + rpl::producer SharedMediaMergedViewer( not_null session, SharedMediaMergedKey key, @@ -490,6 +523,19 @@ rpl::producer SharedMediaWithLastViewer( std::move(update), std::nullopt)); }); + } else if (key.topicRootId == SharedMediaWithLastSlice::kSavedMusicTopicId) { + return SavedMusicMediaViewer( + session, + std::move(viewerKey), + limitBefore, + limitAfter + ) | rpl::start_with_next([=](SparseIdsMergedSlice &&update) { + consumer.put_next(SharedMediaWithLastSlice( + session, + key, + std::move(update), + std::nullopt)); + }); } return rpl::combine( SharedMediaMergedViewer( diff --git a/Telegram/SourceFiles/data/data_shared_media.h b/Telegram/SourceFiles/data/data_shared_media.h index 8f53b4026c..cb01f14f46 100644 --- a/Telegram/SourceFiles/data/data_shared_media.h +++ b/Telegram/SourceFiles/data/data_shared_media.h @@ -52,6 +52,12 @@ rpl::producer SharedScheduledMediaViewer( int limitBefore, int limitAfter); +rpl::producer SavedMusicMediaViewer( + not_null session, + SharedMediaMergedKey key, + int limitBefore, + int limitAfter); + rpl::producer SharedMediaMergedViewer( not_null session, SharedMediaMergedKey key, @@ -70,6 +76,8 @@ public: static constexpr auto kScheduledTopicId = SparseIdsMergedSlice::kScheduledTopicId; + static constexpr auto kSavedMusicTopicId + = SparseIdsMergedSlice::kSavedMusicTopicId; struct Key { Key( PeerId peerId, diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 3328ee5a40..10dbe9037e 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -28,6 +28,7 @@ class SparseIdsMergedSlice { public: using UniversalMsgId = MsgId; static constexpr MsgId kScheduledTopicId = ScheduledMaxMsgId; + static constexpr MsgId kSavedMusicTopicId = ScheduledMaxMsgId + 1; struct Key { Key( diff --git a/Telegram/SourceFiles/data/data_star_gift.cpp b/Telegram/SourceFiles/data/data_star_gift.cpp new file mode 100644 index 0000000000..998c79950b --- /dev/null +++ b/Telegram/SourceFiles/data/data_star_gift.cpp @@ -0,0 +1,56 @@ +/* +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_star_gift.h" + +#include "lang/lang_tag.h" +#include "ui/controls/ton_common.h" +#include "ui/text/text_utilities.h" +#include "styles/style_credits.h" + +namespace Data { + +QString UniqueGiftName(const UniqueGift &gift) { + return gift.title + u" #"_q + QString::number(gift.number); +} + +CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift) { + return CreditsAmount(gift.starsForResale); +} + +CreditsAmount UniqueGiftResaleTon(const UniqueGift &gift) { + return CreditsAmount( + gift.nanoTonForResale / Ui::kNanosInOne, + gift.nanoTonForResale % Ui::kNanosInOne, + CreditsType::Ton); +} + +CreditsAmount UniqueGiftResaleAsked(const UniqueGift &gift) { + return gift.onlyAcceptTon + ? UniqueGiftResaleTon(gift) + : UniqueGiftResaleStars(gift); +} + +TextWithEntities FormatGiftResaleStars(const UniqueGift &gift) { + return Ui::Text::IconEmoji( + &st::starIconEmoji + ).append(Lang::FormatCountDecimal(gift.starsForResale)); +} + +TextWithEntities FormatGiftResaleTon(const UniqueGift &gift) { + return Ui::Text::IconEmoji( + &st::tonIconEmoji + ).append(Lang::FormatCreditsAmountDecimal(UniqueGiftResaleTon(gift))); +} + +TextWithEntities FormatGiftResaleAsked(const UniqueGift &gift) { + return gift.onlyAcceptTon + ? FormatGiftResaleTon(gift) + : FormatGiftResaleStars(gift); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index df0aacc953..c9e41a84cd 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -37,17 +37,38 @@ struct UniqueGiftOriginalDetails { TextWithEntities message; }; +struct UniqueGiftValue { + QString currency; + int64 valuePrice = 0; + CreditsAmount initialPriceStars; + int64 initialSalePrice = 0; + TimeId initialSaleDate = 0; + int64 lastSalePrice = 0; + TimeId lastSaleDate = 0; + int64 averagePrice = 0; + int64 minimumPrice = 0; + int forSaleOnTelegram = 0; + int forSaleOnFragment = 0; + QString fragmentUrl; + bool lastSaleFragment = false; +}; + struct UniqueGift { CollectibleId id = 0; + uint64 initialGiftId = 0; QString slug; QString title; QString ownerAddress; QString ownerName; PeerId ownerId = 0; PeerData *releasedBy = nullptr; - int number = 0; - int starsForTransfer = -1; + PeerData *themeUser = nullptr; + int64 nanoTonForResale = -1; int starsForResale = -1; + int starsForTransfer = -1; + int number = 0; + bool onlyAcceptTon = false; + bool canBeTheme = false; TimeId exportAt = 0; TimeId canTransferAt = 0; TimeId canResellAt = 0; @@ -55,11 +76,18 @@ struct UniqueGift { UniqueGiftPattern pattern; UniqueGiftBackdrop backdrop; UniqueGiftOriginalDetails originalDetails; + std::shared_ptr value; }; -[[nodiscard]] inline QString UniqueGiftName(const UniqueGift &gift) { - return gift.title + u" #"_q + QString::number(gift.number); -} +[[nodiscard]] QString UniqueGiftName(const UniqueGift &gift); + +[[nodiscard]] CreditsAmount UniqueGiftResaleStars(const UniqueGift &gift); +[[nodiscard]] CreditsAmount UniqueGiftResaleTon(const UniqueGift &gift); +[[nodiscard]] CreditsAmount UniqueGiftResaleAsked(const UniqueGift &gift); + +[[nodiscard]] TextWithEntities FormatGiftResaleStars(const UniqueGift &gift); +[[nodiscard]] TextWithEntities FormatGiftResaleTon(const UniqueGift &gift); +[[nodiscard]] TextWithEntities FormatGiftResaleAsked(const UniqueGift &gift); struct StarGift { uint64 id = 0; @@ -74,11 +102,16 @@ struct StarGift { int resellCount = 0; int limitedLeft = 0; int limitedCount = 0; + int perUserTotal = 0; + int perUserRemains = 0; TimeId firstSaleDate = 0; TimeId lastSaleDate = 0; - bool upgradable = false; - bool birthday = false; - bool soldOut = false; + TimeId lockedUntilDate = 0; + bool resellTonOnly : 1 = false; + bool requirePremium : 1 = false; + bool upgradable : 1 = false; + bool birthday : 1 = false; + bool soldOut : 1 = false; friend inline bool operator==( const StarGift &, @@ -123,8 +156,11 @@ public: } friend inline bool operator==( - const SavedStarGiftId &a, - const SavedStarGiftId &b) = default; + const SavedStarGiftId &, + const SavedStarGiftId &) = default; + friend inline auto operator<=>( + const SavedStarGiftId &, + const SavedStarGiftId &) = default; private: PeerData *peer = nullptr; @@ -135,11 +171,14 @@ private: struct SavedStarGift { StarGift info; SavedStarGiftId manageId; + std::vector collectionIds; TextWithEntities message; int64 starsConverted = 0; int64 starsUpgradedBySender = 0; + QString giftPrepayUpgradeHash; PeerId fromId = 0; TimeId date = 0; + bool upgradeSeparate = false; bool upgradable = false; bool anonymous = false; bool pinned = false; @@ -147,4 +186,12 @@ struct SavedStarGift { bool mine = false; }; +struct GiftCollection { + int id = 0; + int count = 0; + QString title; + DocumentData *icon = nullptr; + uint64 hash = 0; +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index f72efbc1ee..ddbbb7f2c3 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -82,11 +82,54 @@ using UpdateFlag = StoryUpdate::Flag; }, [](const auto &) { return std::optional(); }); } +[[nodiscard]] StoryAlbum FromTL( + not_null owner, + const MTPStoryAlbum &album) { + const auto &data = album.data(); + return { + .id = data.valbum_id().v, + .title = qs(data.vtitle()), + .iconPhoto = (data.vicon_photo() + ? owner->processPhoto(*data.vicon_photo()).get() + : nullptr), + .iconVideo = (data.vicon_video() + ? owner->processDocument(*data.vicon_video()).get() + : nullptr), + }; +} + +bool InsertSorted(std::vector &list, StoryId id) { + const auto i = ranges::lower_bound(list, id, std::greater<>()); + if (i != end(list) && *i == id) { + return false; + } + list.insert(i, id); + return true; +} + +bool RemoveSorted(std::vector &list, StoryId id) { + const auto i = ranges::lower_bound(list, id, std::greater<>()); + if (i != end(list) && *i == id) { + list.erase(i); + return true; + } + return false; +} + +bool RemoveUnsorted(std::vector &list, StoryId id) { + const auto i = ranges::find(list, id); + if (i != end(list)) { + list.erase(i); + return true; + } + return false; +} + } // namespace std::vector RespectingPinned(const StoriesIds &ids) { if (ids.pinnedToTop.empty()) { - return ids.list | ranges::to_vector; + return ids.list; } auto result = std::vector(); result.reserve(ids.list.size()); @@ -238,7 +281,7 @@ void Stories::apply(not_null peer, const MTPPeerStories *data) { _sourceChanged.fire_copy(peer->id); updatePeerStoriesState(peer); } else { - parseAndApply(*data); + parseAndApply(*data, ParseSource::DirectRequest); } } @@ -276,7 +319,7 @@ void Stories::requestPeerStories( const auto &data = result.data(); _owner->processUsers(data.vusers()); _owner->processChats(data.vchats()); - parseAndApply(data.vstories()); + parseAndApply(data.vstories(), ParseSource::DirectRequest); finish(); }).fail([=] { applyDeletedFromSources(peer->id, StorySourcesList::NotHidden); @@ -341,15 +384,7 @@ void Stories::processExpired() { } Stories::Set *Stories::lookupArchive(not_null peer) { - const auto peerId = peer->id; - if (hasArchive(peer)) { - const auto i = _archive.find(peerId); - return (i != end(_archive)) - ? &i->second - : &_archive.emplace(peerId, Set()).first->second; - } - clearArchive(peer); - return nullptr; + return albumIdsSet(peer->id, kStoriesAlbumIdArchive); } void Stories::clearArchive(not_null peer) { @@ -367,10 +402,12 @@ void Stories::clearArchive(not_null peer) { } } } - _archiveChanged.fire_copy(peerId); + _albumIdsChanged.fire({ peerId, kStoriesAlbumIdArchive }); } -void Stories::parseAndApply(const MTPPeerStories &stories) { +void Stories::parseAndApply( + const MTPPeerStories &stories, + ParseSource source) { const auto &data = stories.data(); const auto peerId = peerFromMTP(data.vpeer()); const auto already = _readTill.find(peerId); @@ -424,9 +461,25 @@ void Stories::parseAndApply(const MTPPeerStories &stories) { } sort(list); }; - if (result.peer->isSelf() - || (result.peer->isChannel() && result.peer->asChannel()->amIn()) - || result.peer->isUser()) { + const auto appendToStrip = [](not_null peer, ParseSource source) { + if (peer->isSelf()) { + return true; + } else if (const auto channel = peer->asChannel()) { + return channel->amIn(); + } else if (const auto user = peer->asUser()) { + return (source == ParseSource::MyStrip) + || user->storiesCorrespondent() + || user->isServiceUser() + || user->isContact() + || user->isBot(); + } + return false; + }; + if (appendToStrip(result.peer, source)) { + if (source == ParseSource::MyStrip + && !appendToStrip(result.peer, ParseSource::DirectRequest)) { + result.peer->asUser()->setStoriesCorrespondent(true); + } const auto hidden = result.peer->hasStoriesHidden(); using List = StorySourcesList; add(hidden ? List::Hidden : List::NotHidden); @@ -455,6 +508,19 @@ Story *Stories::parseAndApply( return nullptr; } const auto id = data.vid().v; + auto albumInfo = (Albums*)nullptr; + auto list = std::optional>(); + if (const auto albums = data.valbums()) { + list.emplace(); + if (!albums->v.empty()) { + albumInfo = &_albums[peer->id]; + list->reserve(albums->v.size()); + for (const auto &albumId : albums->v) { + albumInfo->sets[albumId.v].albumKnownInArchive.emplace(id); + list->emplace(albumId.v); + } + } + } const auto fullId = FullStoryId{ peer->id, id }; auto &stories = _stories[peer->id]; const auto i = stories.find(id); @@ -462,6 +528,21 @@ Story *Stories::parseAndApply( const auto result = i->second.get(); const auto mediaChanged = (result->media() != *media); result->applyChanges(*media, data, now); + if (list) { + const auto &was = result->albumIds(); + if (*list != was) { + if (!albumInfo && !was.empty()) { + albumInfo = &_albums[peer->id]; + } + for (const auto wasId : result->albumIds()) { + if (!list->contains(wasId)) { + albumInfo->sets[wasId].albumKnownInArchive.remove(id); + } + } + result->setAlbumIds(*base::take(list)); + } + } + const auto j = _pollingSettings.find(result); if (j != end(_pollingSettings)) { maybeSchedulePolling(result, j->second, now); @@ -486,14 +567,17 @@ Story *Stories::parseAndApply( data, now )).first->second.get(); + if (list) { + result->setAlbumIds(*base::take(list)); + } if (const auto archive = lookupArchive(peer)) { - const auto added = archive->ids.list.emplace(id).second; + const auto added = InsertSorted(archive->ids.list, id); if (added) { if (archive->total >= 0 && id > archive->lastId) { ++archive->total; } - _archiveChanged.fire_copy(peer->id); + _albumIdsChanged.fire({ peer->id, kStoriesAlbumIdArchive }); } } @@ -580,20 +664,20 @@ void Stories::savedStateChanged(not_null story) { const auto inProfile = story->inProfile(); if (inProfile) { auto &saved = _saved[peer]; - const auto added = saved.ids.list.emplace(id).second; + const auto added = InsertSorted(saved.ids.list, id); if (added) { if (saved.total >= 0 && id > saved.lastId) { ++saved.total; } - _savedChanged.fire_copy(peer); + _albumIdsChanged.fire({ peer, kStoriesAlbumIdSaved }); } } else if (const auto i = _saved.find(peer); i != end(_saved)) { auto &saved = i->second; - if (saved.ids.list.remove(id)) { + if (RemoveSorted(saved.ids.list, id)) { if (saved.total > 0) { --saved.total; } - _savedChanged.fire_copy(peer); + _albumIdsChanged.fire({ peer, kStoriesAlbumIdSaved }); } } } @@ -621,7 +705,7 @@ void Stories::loadMore(StorySourcesList list) { _sourcesStates[index] = qs(data.vstate()); _sourcesLoaded[index] = !data.is_has_more(); for (const auto &single : data.vpeer_stories().v) { - parseAndApply(single); + parseAndApply(single, ParseSource::MyStrip); } }, [](const MTPDstories_allStoriesNotModified &) { }); @@ -648,6 +732,8 @@ void Stories::preloadListsMore() { const auto index = static_cast(list); return _sourcesLoaded[index] || !_sourcesStates[index].isEmpty(); }; + const auto selfId = _owner->session().userPeerId(); + constexpr auto archive = kStoriesAlbumIdArchive; if (loading(StorySourcesList::NotHidden) || loading(StorySourcesList::Hidden)) { return; @@ -655,8 +741,8 @@ void Stories::preloadListsMore() { loadMore(StorySourcesList::NotHidden); } else if (!countLoaded(StorySourcesList::Hidden)) { loadMore(StorySourcesList::Hidden); - } else if (!archiveCountKnown(_owner->session().userPeerId())) { - archiveLoadMore(_owner->session().userPeerId()); + } else if (!albumIdsCountKnown(selfId, archive)) { + albumIdsLoadMore(selfId, archive); } } @@ -801,28 +887,32 @@ void Stories::applyDeleted(not_null peer, StoryId id) { story.get(), UpdateFlag::Destroyed); removeDependencyStory(story.get()); - if (hasArchive(story->peer())) { - if (const auto k = _archive.find(peerId) - ; k != end(_archive)) { - const auto archive = &k->second; - if (archive->ids.list.remove(id)) { - if (archive->total > 0) { - --archive->total; + const auto removeFromAlbum = [&](int albumId) { + if (const auto set = albumIdsSet(peerId, albumId, true)) { + set->albumKnownInArchive.remove(id); + RemoveUnsorted(set->ids.pinnedToTop, id); + + const auto sorted = (albumId == kStoriesAlbumIdSaved) + || (albumId == kStoriesAlbumIdArchive); + const auto removed = sorted + ? RemoveSorted(set->ids.list, id) + : RemoveUnsorted(set->ids.list, id); + if (removed) { + if (set->total > 0) { + --set->total; } - _archiveChanged.fire_copy(peerId); + _albumIdsChanged.fire({ peerId, albumId }); } } + }; + if (hasArchive(story->peer())) { + removeFromAlbum(kStoriesAlbumIdArchive); } if (story->inProfile()) { - if (const auto k = _saved.find(peerId); k != end(_saved)) { - const auto saved = &k->second; - if (saved->ids.list.remove(id)) { - if (saved->total > 0) { - --saved->total; - } - _savedChanged.fire_copy(peerId); - } - } + removeFromAlbum(kStoriesAlbumIdSaved); + } + for (const auto id : story->albumIds()) { + removeFromAlbum(id); } if (_preloading && _preloading->id() == fullId) { _preloading = nullptr; @@ -1070,10 +1160,8 @@ void Stories::resolve(FullStoryId id, Fn done, bool force) { } void Stories::loadAround(FullStoryId id, StoriesContext context) { - if (v::is(context.data)) { - return; - } else if (v::is(context.data) - || v::is(context.data)) { + if (v::is(context.data) + || v::is(context.data)) { return; } const auto i = _all.find(id.peer); @@ -1201,7 +1289,9 @@ void Stories::toggleHidden( && !peer->asUser()->isServiceUser(); const auto justRemove = (byHints || peer->isServiceUser()) && hidden; if (peer->hasStoriesHidden() != hidden) { - if (!justRemove) { + if (byHints && hidden) { + peer->asUser()->setStoriesCorrespondent(false); + } else if (!justRemove) { peer->setStoriesHidden(hidden); } session().api().request(MTPstories_TogglePeerStoriesHidden( @@ -1458,7 +1548,7 @@ void Stories::loadReactionsSlice( slice.list.push_back({ .peer = story->peer(), .repostId = story->id(), - }); + }); } }, [&](const MTPDstoryReactionPublicForward &data) { const auto item = _owner->addNewMessage( @@ -1606,155 +1696,346 @@ bool Stories::hasArchive(not_null peer) const { return false; } -const StoriesIds &Stories::archive(PeerId peerId) const { +const Stories::Set *Stories::albumIdsSet(PeerId peerId, int albumId) const { + return const_cast(this)->albumIdsSet(peerId, albumId, true); +} + +Stories::Set *Stories::albumIdsSet(PeerId peerId, int albumId, bool lazy) { + if (albumId == kStoriesAlbumIdArchive) { + const auto peer = _owner->peer(peerId); + if (!hasArchive(peer)) { + clearArchive(peer); + return nullptr; + } + const auto i = _archive.find(peerId); + return (i != end(_archive)) + ? &i->second + : lazy + ? nullptr + : &_archive.emplace(peerId, Set()).first->second; + } else if (albumId == kStoriesAlbumIdSaved) { + const auto i = _saved.find(peerId); + return (i != end(_saved)) + ? &i->second + : lazy + ? nullptr + : &_saved.emplace(peerId, Set()).first->second; + } + auto i = _albums.find(peerId); + if (i == end(_albums)) { + if (lazy) { + return nullptr; + } + i = _albums.emplace(peerId, Albums()).first; + } + const auto j = i->second.sets.find(albumId); + return (j != end(i->second.sets)) + ? &j->second + : lazy + ? nullptr + : &i->second.sets.emplace(albumId).first->second; +} + +const StoriesIds &Stories::albumIds(PeerId peerId, int albumId) const { static const auto empty = StoriesIds(); - const auto i = _archive.find(peerId); - return (i != end(_archive)) ? i->second.ids : empty; + const auto set = albumIdsSet(peerId, albumId); + return set ? set->ids : empty; } -rpl::producer Stories::archiveChanged() const { - return _archiveChanged.events(); +rpl::producer Stories::albumIdsChanged() const { + return _albumIdsChanged.events(); } -int Stories::archiveCount(PeerId peerId) const { - const auto i = _archive.find(peerId); - return (i != end(_archive)) ? i->second.total : 0; +int Stories::albumIdsCount(PeerId peerId, int albumId) const { + const auto set = albumIdsSet(peerId, albumId); + return set ? std::max(set->total, 0) : 0; } -bool Stories::archiveCountKnown(PeerId peerId) const { - const auto i = _archive.find(peerId); - return (i != end(_archive)) && (i->second.total >= 0); +bool Stories::albumIdsCountKnown(PeerId peerId, int albumId) const { + const auto set = albumIdsSet(peerId, albumId); + return set && (set->total >= 0); } -bool Stories::archiveLoaded(PeerId peerId) const { - const auto i = _archive.find(peerId); - return (i != end(_archive)) && i->second.loaded; +bool Stories::albumIdsLoaded(PeerId peerId, int albumId) const { + const auto set = albumIdsSet(peerId, albumId); + return set && set->loaded; } -const StoriesIds &Stories::saved(PeerId peerId) const { - static const auto empty = StoriesIds(); - const auto i = _saved.find(peerId); - return (i != end(_saved)) ? i->second.ids : empty; +void Stories::albumIdsLoadMore(PeerId peerId, int albumId) { + albumIdsLoadMore(peerId, albumId, false); } -rpl::producer Stories::savedChanged() const { - return _savedChanged.events(); -} +void Stories::albumIdsLoadMore(PeerId peerId, int albumId, bool reload) { + Expects(!reload || albumId > 0); -int Stories::savedCount(PeerId peerId) const { - const auto i = _saved.find(peerId); - return (i != end(_saved)) ? i->second.total : 0; -} - -bool Stories::savedCountKnown(PeerId peerId) const { - const auto i = _saved.find(peerId); - return (i != end(_saved)) && (i->second.total >= 0); -} - -bool Stories::savedLoaded(PeerId peerId) const { - const auto i = _saved.find(peerId); - return (i != end(_saved)) && i->second.loaded; -} - -void Stories::archiveLoadMore(PeerId peerId) { const auto peer = _owner->peer(peerId); - const auto archive = lookupArchive(peer); - if (!archive || archive->requestId || archive->loaded) { + const auto set = albumIdsSet(peerId, albumId); + if (set && reload) { + _owner->session().api().request(base::take(set->requestId)).cancel(); + } + if (!set || set->requestId || (!reload && set->loaded)) { return; } const auto api = &_owner->session().api(); - archive->requestId = api->request(MTPstories_GetStoriesArchive( - peer->input, - MTP_int(archive->lastId), - MTP_int(archive->lastId ? kArchivePerPage : kArchiveFirstPerPage) - )).done([=](const MTPstories_Stories &result) { - const auto archive = lookupArchive(peer); - if (!archive) { + const auto done = [=](const MTPstories_Stories &result) { + const auto set = albumIdsSet(peerId, albumId); + if (!set) { return; } - archive->requestId = 0; - - const auto &data = result.data(); - const auto now = base::unixtime::now(); - archive->total = data.vcount().v; - for (const auto &story : data.vstories().v) { - const auto id = story.match([&](const auto &id) { - return id.vid().v; - }); - archive->ids.list.emplace(id); - archive->lastId = id; - if (!parseAndApply(peer, story, now)) { - archive->ids.list.remove(id); - if (archive->total > 0) { - --archive->total; - } - } + set->requestId = 0; + if (reload) { + set->ids.list.clear(); + set->ids.pinnedToTop.clear(); + set->lastId = StoryId(); } - const auto ids = int(archive->ids.list.size()); - archive->loaded = data.vstories().v.empty(); - archive->total = archive->loaded ? ids : std::max(archive->total, ids); - _archiveChanged.fire_copy(peerId); - }).fail([=] { - const auto archive = lookupArchive(peer); - if (!archive) { - return; - } - archive->requestId = 0; - archive->loaded = true; - archive->total = int(archive->ids.list.size()); - _archiveChanged.fire_copy(peerId); - }).send(); -} - -void Stories::savedLoadMore(PeerId peerId) { - auto &saved = _saved[peerId]; - if (saved.requestId || saved.loaded) { - return; - } - const auto api = &_owner->session().api(); - const auto peer = _owner->peer(peerId); - saved.requestId = api->request(MTPstories_GetPinnedStories( - peer->input, - MTP_int(saved.lastId), - MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) - )).done([=](const MTPstories_Stories &result) { - auto &saved = _saved[peerId]; - saved.requestId = 0; - const auto &data = result.data(); const auto now = base::unixtime::now(); auto pinnedToTopIds = data.vpinned_to_top().value_or_empty(); auto pinnedToTop = pinnedToTopIds | ranges::views::transform(&MTPint::v) | ranges::to_vector; - saved.total = data.vcount().v; + const auto ordered = (albumId == kStoriesAlbumIdSaved) + || (albumId == kStoriesAlbumIdArchive); + set->total = data.vcount().v; for (const auto &story : data.vstories().v) { const auto id = story.match([&](const auto &id) { return id.vid().v; }); - saved.ids.list.emplace(id); - saved.lastId = id; + if (ordered) { + InsertSorted(set->ids.list, id); + } else { + set->ids.list.push_back(id); + } + set->lastId = id; if (!parseAndApply(peer, story, now)) { - saved.ids.list.remove(id); - if (saved.total > 0) { - --saved.total; + if (ordered) { + RemoveSorted(set->ids.list, id); + } else { + set->ids.list.pop_back(); + } + if (set->total > 0) { + --set->total; } } } - const auto ids = int(saved.ids.list.size()); - saved.loaded = data.vstories().v.empty(); - saved.total = saved.loaded ? ids : std::max(saved.total, ids); - setPinnedToTop(peerId, std::move(pinnedToTop)); - _savedChanged.fire_copy(peerId); + const auto count = int(set->ids.list.size()); + set->loaded = data.vstories().v.empty(); + set->total = set->loaded ? count : std::max(set->total, count); + if (albumId == kStoriesAlbumIdSaved) { + setPinnedToTop(peerId, std::move(pinnedToTop)); + } + _albumIdsChanged.fire({ peerId, albumId }); + }; + const auto fail = [=] { + const auto set = albumIdsSet(peerId, albumId); + if (!set) { + return; + } + set->requestId = 0; + set->loaded = true; + set->total = int(set->ids.list.size()); + _albumIdsChanged.fire({ peerId, albumId }); + }; + set->requestId = (albumId == kStoriesAlbumIdArchive) + ? api->request(MTPstories_GetStoriesArchive( + peer->input, + MTP_int(set->lastId), + MTP_int(set->lastId ? kArchivePerPage : kArchiveFirstPerPage) + )).done(done).fail(fail).send() + : (albumId == kStoriesAlbumIdSaved) + ? api->request(MTPstories_GetPinnedStories( + peer->input, + MTP_int(set->lastId), + MTP_int(set->lastId ? kSavedPerPage : kSavedFirstPerPage) + )).done(done).fail(fail).send() + : api->request(MTPstories_GetAlbumStories( + peer->input, + MTP_int(albumId), + MTP_int(reload ? 0 : set->ids.list.size()), + MTP_int((reload || set->ids.list.empty()) + ? kSavedFirstPerPage + : kSavedPerPage) + )).done(done).fail(fail).send(); +} + +const base::flat_set &Stories::albumKnownInArchive( + PeerId peerId, + int albumId) const { + static const auto empty = base::flat_set(); + + const auto i = _albums.find(peerId); + if (i == end(_albums)) { + return empty; + } + const auto j = i->second.sets.find(albumId); + return (j != end(i->second.sets)) + ? j->second.albumKnownInArchive + : empty; +} + +auto Stories::albumsListValue(PeerId peerId) +-> rpl::producer> { + auto &albums = _albums[peerId]; + if (!albums.requestId) { + loadAlbums(_owner->peer(peerId), albums); + } + return albums.list.value(); +} + +void Stories::loadAlbums(not_null peer, Albums &albums) { + const auto api = &_owner->session().api(); + api->request(base::take(albums.requestId)).cancel(); + albums.requestId = api->request(MTPstories_GetAlbums( + peer->input, + MTP_long(albums.hash) + )).done([=](const MTPstories_Albums &result) { + auto &albums = _albums[peer->id]; + albums.requestId = 0; + result.match([&](const MTPDstories_albums &data) { + albums.hash = data.vhash().v; + auto parsed = std::vector(); + const auto &list = data.valbums().v; + parsed.reserve(list.size()); + for (const auto &album : list) { + parsed.push_back(FromTL(_owner, album)); + } + albums.list = std::move(parsed); + }, [](const MTPDstories_albumsNotModified &) { + }); }).fail([=] { - auto &saved = _saved[peerId]; - saved.requestId = 0; - saved.loaded = true; - saved.total = int(saved.ids.list.size()); - _savedChanged.fire_copy(peerId); + auto &albums = _albums[peer->id]; + albums.requestId = 0; + albums.hash = 0; }).send(); } +void Stories::albumCreate( + not_null peer, + const QString &title, + StoryId addId, + Fn done, + Fn fail) { + auto ids = QVector(); + if (addId) { + ids.push_back(MTP_int(addId)); + } + _owner->session().api().request(MTPstories_CreateAlbum( + peer->input, + MTP_string(title), + MTP_vector(ids) + )).done([=](const MTPStoryAlbum &result) { + auto &albums = _albums[peer->id]; + auto current = albums.list.current(); + auto parsed = FromTL(_owner, result); + current.push_back(parsed); + albums.list = std::move(current); + loadAlbums(peer, albums); + if (const auto onstack = done) { + onstack(std::move(parsed)); + } + }).fail([=](const MTP::Error &error) { + if (const auto onstack = fail) { + onstack(error.type()); + } + }).send(); +} + +void Stories::albumRename( + not_null peer, + int id, + const QString &title, + Fn done, + Fn fail) { + using Flag = MTPstories_UpdateAlbum::Flag; + _owner->session().api().request(MTPstories_UpdateAlbum( + MTP_flags(Flag::f_title), + peer->input, + MTP_int(id), + MTP_string(title), + MTPVector(), + MTPVector(), + MTPVector() + )).done([=](const MTPStoryAlbum &result) { + auto &albums = _albums[peer->id]; + auto current = albums.list.current(); + auto parsed = FromTL(_owner, result); + current.push_back(parsed); + albums.list = std::move(current); + loadAlbums(peer, albums); + if (const auto onstack = done) { + onstack(std::move(parsed)); + } + }).fail([=](const MTP::Error &error) { + if (const auto onstack = fail) { + onstack(error.type()); + } + }).send(); +} + +void Stories::albumDelete(not_null peer, int id) { + _owner->session().api().request(MTPstories_DeleteAlbum( + peer->input, + MTP_int(id) + )).send(); + + auto &albums = _albums[peer->id]; + auto current = albums.list.current(); + current.erase( + ranges::remove(current, id, &StoryAlbum::id), + end(current)); + albums.list = std::move(current); + + const auto i = albums.sets.find(id); + if (i != end(albums.sets)) { + _owner->session().api().request( + base::take(i->second.requestId)).cancel(); + for (const auto storyId : i->second.albumKnownInArchive) { + if (const auto story = lookup({ peer->id, storyId })) { + auto now = (*story)->albumIds(); + if (now.remove(id)) { + (*story)->setAlbumIds(std::move(now)); + } + } + } + albums.sets.erase(i); + } +} + +void Stories::notifyAlbumUpdate(StoryAlbumUpdate &&update) { + const auto peerId = update.peer->id; + const auto i = _albums.find(peerId); + if (i != end(_albums)) { + const auto albumId = update.albumId; + const auto j = i->second.sets.find(albumId); + if (j != end(i->second.sets)) { + for (const auto &id : update.added) { + j->second.albumKnownInArchive.emplace(id); + if (const auto story = lookup({ peerId, id })) { + auto now = (*story)->albumIds(); + if (now.emplace(albumId).second) { + (*story)->setAlbumIds(std::move(now)); + } + } + } + for (const auto &id : update.removed) { + j->second.albumKnownInArchive.remove(id); + if (const auto story = lookup({ peerId, id })) { + auto now = (*story)->albumIds(); + if (now.remove(albumId)) { + (*story)->setAlbumIds(std::move(now)); + } + } + } + albumIdsLoadMore(peerId, albumId, true); + } + } + _albumUpdates.fire(std::move(update)); +} + +rpl::producer Stories::albumUpdates() const { + return _albumUpdates.events(); +} + void Stories::setPinnedToTop( PeerId peerId, std::vector &&pinnedToTop) { @@ -1845,12 +2126,12 @@ void Stories::toggleInProfileList( const auto add = loaded || (id.v >= lastId); if (!add) { dirty = true; - } else if (saved.ids.list.emplace(id.v).second) { + } else if (InsertSorted(saved.ids.list, id.v)) { if (saved.total >= 0) { ++saved.total; } } - } else if (saved.ids.list.remove(id.v)) { + } else if (RemoveSorted(saved.ids.list, id.v)) { if (saved.total > 0) { --saved.total; } @@ -1862,9 +2143,9 @@ void Stories::toggleInProfileList( } } if (dirty) { - savedLoadMore(peerId); + albumIdsLoadMore(peerId, kStoriesAlbumIdSaved); } else { - _savedChanged.fire_copy(peerId); + _albumIdsChanged.fire({ peerId, kStoriesAlbumIdSaved }); } }).send(); } @@ -1933,7 +2214,7 @@ void Stories::togglePinnedList( setPinnedToTop(peerId, list | ranges::views::transform(&MTPint::v) | ranges::to_vector); - _savedChanged.fire_copy(peerId); + _albumIdsChanged.fire({ peerId, kStoriesAlbumIdSaved }); }).send(); } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 4e6b0ce88d..c8ea41036b 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -23,15 +23,19 @@ class Show; namespace Data { +inline constexpr auto kStoriesAlbumIdSaved = 0; +inline constexpr auto kStoriesAlbumIdArchive = -1; + class Folder; class Session; struct StoryView; struct StoryIdDates; class Story; +struct StoryAlbum; class StoryPreload; struct StoriesIds { - base::flat_set> list; + std::vector list; // flat_set for saved/archive. std::vector pinnedToTop; friend inline bool operator==( @@ -90,26 +94,20 @@ struct StoriesContextPeer { friend inline bool operator==(StoriesContextPeer, StoriesContextPeer) = default; }; -struct StoriesContextSaved { - friend inline auto operator<=>( - StoriesContextSaved, - StoriesContextSaved) = default; - friend inline bool operator==(StoriesContextSaved, StoriesContextSaved) = default; -}; +struct StoriesContextAlbum { + int id = 0; -struct StoriesContextArchive { friend inline auto operator<=>( - StoriesContextArchive, - StoriesContextArchive) = default; - friend inline bool operator==(StoriesContextArchive, StoriesContextArchive) = default; + StoriesContextAlbum, + StoriesContextAlbum) = default; + friend inline bool operator==(StoriesContextAlbum, StoriesContextAlbum) = default; }; struct StoriesContext { std::variant< StoriesContextSingle, StoriesContextPeer, - StoriesContextSaved, - StoriesContextArchive, + StoriesContextAlbum, StorySourcesList> data; friend inline auto operator<=>( @@ -126,8 +124,27 @@ struct StealthMode { friend inline bool operator==(StealthMode, StealthMode) = default; }; +struct StoryAlbumUpdate { + not_null peer; + int albumId = 0; + std::vector added; + std::vector removed; +}; + inline constexpr auto kStorySourcesListCount = 2; +struct StoryAlbumIdsKey { + PeerId peerId; + int albumId = 0; + + friend inline auto operator<=>( + StoryAlbumIdsKey, + StoryAlbumIdsKey) = default; + friend inline bool operator==( + StoryAlbumIdsKey, + StoryAlbumIdsKey) = default; +}; + class Stories final : public base::has_weak_ptr { public: explicit Stories(not_null owner); @@ -192,19 +209,35 @@ public: [[nodiscard]] bool hasArchive(not_null peer) const; - [[nodiscard]] const StoriesIds &archive(PeerId peerId) const; - [[nodiscard]] rpl::producer archiveChanged() const; - [[nodiscard]] int archiveCount(PeerId peerId) const; - [[nodiscard]] bool archiveCountKnown(PeerId peerId) const; - [[nodiscard]] bool archiveLoaded(PeerId peerId) const; - void archiveLoadMore(PeerId peerId); + [[nodiscard]] const StoriesIds &albumIds( + PeerId peerId, + int albumId) const; + [[nodiscard]] rpl::producer albumIdsChanged() const; + [[nodiscard]] int albumIdsCount(PeerId peerId, int albumId) const; + [[nodiscard]] bool albumIdsCountKnown(PeerId peerId, int albumId) const; + [[nodiscard]] bool albumIdsLoaded(PeerId peerId, int albumId) const; + void albumIdsLoadMore(PeerId peerId, int albumId); + [[nodiscard]] const base::flat_set &albumKnownInArchive( + PeerId peerId, + int albumId) const; - [[nodiscard]] const StoriesIds &saved(PeerId peerId) const; - [[nodiscard]] rpl::producer savedChanged() const; - [[nodiscard]] int savedCount(PeerId peerId) const; - [[nodiscard]] bool savedCountKnown(PeerId peerId) const; - [[nodiscard]] bool savedLoaded(PeerId peerId) const; - void savedLoadMore(PeerId peerId); + [[nodiscard]] auto albumsListValue(PeerId peerId) + -> rpl::producer>; + void albumCreate( + not_null peer, + const QString &title, + StoryId addId, + Fn done, + Fn fail); + void albumRename( + not_null peer, + int id, + const QString &title, + Fn done, + Fn fail); + void albumDelete(not_null peer, int id); + void notifyAlbumUpdate(StoryAlbumUpdate &&update); + [[nodiscard]] rpl::producer albumUpdates() const; void deleteList(const std::vector &ids); void toggleInProfileList( @@ -257,18 +290,30 @@ public: private: struct Set { StoriesIds ids; + base::flat_set albumKnownInArchive; int total = -1; StoryId lastId = 0; bool loaded = false; mtpRequestId requestId = 0; }; + struct Albums { + rpl::variable> list; + base::flat_map sets; + uint64 hash = 0; + mtpRequestId requestId = 0; + }; struct PollingSettings { int chat = 0; int viewer = 0; }; + enum class ParseSource : uchar { + MyStrip, + DirectRequest, + }; - void parseAndApply(const MTPPeerStories &stories); + void albumIdsLoadMore(PeerId peerId, int albumId, bool reload); + void parseAndApply(const MTPPeerStories &stories, ParseSource source); [[nodiscard]] Story *parseAndApply( not_null peer, const MTPDstoryItem &data, @@ -287,6 +332,9 @@ private: [[nodiscard]] Set *lookupArchive(not_null peer); void clearArchive(not_null peer); + const Set *albumIdsSet(PeerId peerId, int albumId) const; + Set *albumIdsSet(PeerId peerId, int albumId, bool lazy = false); + void applyDeleted(not_null peer, StoryId id); void applyExpired(FullStoryId id); void applyRemovedFromActive(FullStoryId id); @@ -331,6 +379,8 @@ private: void sendViewsSliceRequest(); void sendViewsCountsRequest(); + void loadAlbums(not_null peer, Albums &albums); + const not_null _owner; std::unordered_map< PeerId, @@ -369,10 +419,10 @@ private: rpl::event_stream _itemsChanged; std::unordered_map _archive; - rpl::event_stream _archiveChanged; - std::unordered_map _saved; - rpl::event_stream _savedChanged; + std::unordered_map _albums; + rpl::event_stream _albumUpdates; + rpl::event_stream _albumIdsChanged; base::flat_set _markReadPending; base::Timer _markReadTimer; diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp index d2ac334272..130cb5e327 100644 --- a/Telegram/SourceFiles/data/data_stories_ids.cpp +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -16,8 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { -rpl::producer SavedStoriesIds( +rpl::producer AlbumStoriesIds( not_null peer, + int albumId, StoryId aroundId, int limit) { return [=](auto consumer) { @@ -35,22 +36,20 @@ rpl::producer SavedStoriesIds( const auto peerId = peer->id; const auto stories = &peer->owner().stories(); - if (!stories->savedCountKnown(peerId)) { + if (!stories->albumIdsCountKnown(peerId, albumId)) { return; } - - const auto &saved = stories->saved(peerId); - const auto sorted = RespectingPinned(saved); - const auto count = stories->savedCount(peerId); + const auto &loaded = stories->albumIds(peerId, albumId); + const auto sorted = RespectingPinned(loaded); + const auto count = stories->albumIdsCount(peerId, albumId); auto i = ranges::find(sorted, aroundId); if (i == end(sorted)) { - const auto j = saved.list.lower_bound(aroundId); - i = begin(sorted) + int(j - begin(saved.list)); + i = begin(sorted); } const auto hasBefore = int(i - begin(sorted)); const auto hasAfter = int(end(sorted) - i); if (hasAfter < limit) { - stories->savedLoadMore(peerId); + stories->albumIdsLoadMore(peerId, albumId); } const auto takeBefore = std::min(hasBefore, limit); const auto takeAfter = std::min(hasAfter, limit); @@ -81,91 +80,13 @@ rpl::producer SavedStoriesIds( const auto peerId = peer->id; const auto stories = &peer->owner().stories(); - stories->savedChanged( + stories->albumIdsChanged( ) | rpl::filter( - rpl::mappers::_1 == peerId + rpl::mappers::_1 == Data::StoryAlbumIdsKey{ peerId, albumId } ) | rpl::start_with_next(schedule, lifetime); - if (!stories->savedCountKnown(peerId)) { - stories->savedLoadMore(peerId); - } - - push(); - - return lifetime; - }; -} - -rpl::producer ArchiveStoriesIds( - not_null peer, - StoryId aroundId, - int limit) { - return [=](auto consumer) { - auto lifetime = rpl::lifetime(); - - struct State { - StoriesIdsSlice slice; - base::has_weak_ptr guard; - bool scheduled = false; - }; - const auto state = lifetime.make_state(); - - const auto push = [=] { - state->scheduled = false; - - const auto peerId = peer->id; - const auto stories = &peer->owner().stories(); - if (!stories->archiveCountKnown(peerId)) { - return; - } - - const auto &archive = stories->archive(peerId); - const auto sorted = RespectingPinned(archive); - const auto count = stories->savedCount(peerId); - auto i = ranges::find(sorted, aroundId); - if (i == end(sorted)) { - const auto j = archive.list.lower_bound(aroundId); - i = begin(sorted) + int(j - begin(archive.list)); - } - const auto hasBefore = int(i - begin(sorted)); - const auto hasAfter = int(end(sorted) - i); - if (hasAfter < limit) { - stories->archiveLoadMore(peerId); - } - const auto takeBefore = std::min(hasBefore, limit); - const auto takeAfter = std::min(hasAfter, limit); - auto ids = std::vector(); - ids.reserve(takeBefore + takeAfter); - for (auto j = i - takeBefore; j != i + takeAfter; ++j) { - ids.push_back(*j); - } - const auto added = int(ids.size()); - state->slice = StoriesIdsSlice( - std::move(ids), - count, - (hasBefore - takeBefore), - count - hasBefore - added); - consumer.put_next_copy(state->slice); - }; - const auto schedule = [=] { - if (state->scheduled) { - return; - } - state->scheduled = true; - Ui::PostponeCall(&state->guard, [=] { - if (state->scheduled) { - push(); - } - }); - }; - - const auto peerId = peer->id; - const auto stories = &peer->owner().stories(); - stories->archiveChanged( - ) | rpl::start_with_next(schedule, lifetime); - - if (!stories->archiveCountKnown(peerId)) { - stories->archiveLoadMore(peerId); + if (!stories->albumIdsCountKnown(peerId, albumId)) { + stories->albumIdsLoadMore(peerId, albumId); } push(); diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h index 88a5103b3b..67d651ca53 100644 --- a/Telegram/SourceFiles/data/data_stories_ids.h +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -19,13 +19,9 @@ namespace Data { using StoriesIdsSlice = AbstractSparseIds>; -[[nodiscard]] rpl::producer SavedStoriesIds( - not_null peer, - StoryId aroundId, - int limit); - -[[nodiscard]] rpl::producer ArchiveStoriesIds( +[[nodiscard]] rpl::producer AlbumStoriesIds( not_null peer, + int albumId, StoryId aroundId, int limit); diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index d5a5fc57ea..1c92ecaf0d 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -367,7 +367,12 @@ bool Story::pinnedToTop() const { } void Story::setInProfile(bool value) { - _inProfile = value; + if (_inProfile != value) { + _inProfile = value; + if (const auto item = _peer->owner().stories().lookupItem(this)) { + item->setStoryInProfile(value); + } + } } bool Story::inProfile() const { @@ -414,11 +419,7 @@ bool Story::canShare() const { } bool Story::canDelete() const { - if (const auto channel = _peer->asChannel()) { - return channel->canDeleteStories() - || (out() && channel->canPostStories()); - } - return _peer->isSelf(); + return _peer->canDeleteStories() || (out() && _peer->canPostStories()); } bool Story::canReport() const { @@ -903,6 +904,14 @@ StoryId Story::repostSourceId() const { return _repostSourceId; } +const base::flat_set &Story::albumIds() const { + return _albumIds; +} + +void Story::setAlbumIds(base::flat_set ids) { + _albumIds = std::move(ids); +} + PeerData *Story::fromPeer() const { return _fromPeer; } diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index a1de910faf..bcb425c2ca 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -237,6 +237,9 @@ public: [[nodiscard]] QString repostSourceName() const; [[nodiscard]] StoryId repostSourceId() const; + [[nodiscard]] const base::flat_set &albumIds() const; + void setAlbumIds(base::flat_set ids); + [[nodiscard]] PeerData *fromPeer() const; private: @@ -265,6 +268,7 @@ private: PeerData * const _repostSourcePeer = nullptr; const QString _repostSourceName; const StoryId _repostSourceId = 0; + base::flat_set _albumIds; PeerData * const _fromPeer = nullptr; Data::ReactionId _sentReactionId; StoryMedia _media; @@ -308,4 +312,15 @@ private: }; +struct StoryAlbum { + int id = 0; + QString title; + PhotoData *iconPhoto = nullptr; + DocumentData *iconVideo = nullptr; + + friend inline bool operator==( + const StoryAlbum &, + const StoryAlbum &) = default; +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_todo_list.cpp b/Telegram/SourceFiles/data/data_todo_list.cpp index d50bcf6296..8400eb249d 100644 --- a/Telegram/SourceFiles/data/data_todo_list.cpp +++ b/Telegram/SourceFiles/data/data_todo_list.cpp @@ -45,12 +45,9 @@ Main::Session &TodoListData::session() const { } bool TodoListData::applyChanges(const MTPDtodoList &todolist) { - const auto newTitle = TextWithEntities{ - .text = qs(todolist.vtitle().data().vtext()), - .entities = Api::EntitiesFromMTP( - &session(), - todolist.vtitle().data().ventities().v), - }; + const auto newTitle = Api::ParseTextWithEntities( + &session(), + todolist.vtitle()); const auto newFlags = (todolist.is_others_can_append() ? Flag::OthersCanAppend : Flag()) @@ -222,12 +219,7 @@ TodoListItem TodoListItemFromMTP( const MTPTodoItem &item) { const auto &data = item.data(); return { - .text = TextWithEntities{ - .text = qs(data.vtitle().data().vtext()), - .entities = Api::EntitiesFromMTP( - session, - data.vtitle().data().ventities().v), - }, + .text = Api::ParseTextWithEntities(session, data.vtitle()), .id = data.vid().v, }; } diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 753b79d1a8..18ee1b37fc 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -357,6 +357,9 @@ enum class MessageFlag : uint64 { StarsPaidSuggested = (1ULL << 52), TonPaidSuggested = (1ULL << 53), + StoryInProfile = (1ULL << 54), + SavedMusicItem = (1ULL << 55), + AyuNoForwards = (1ULL << 63), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 84d74f25ac..9169337f43 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -19,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_business_common.h" #include "data/business/data_business_info.h" #include "data/components/credits.h" +#include "data/data_cloud_themes.h" +#include "data/data_saved_music.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -69,6 +71,20 @@ bool ApplyBotVerifierSettings( return false; } +[[nodiscard]] Data::StarsRating ParseStarsRating( + const MTPStarsRating *rating) { + if (!rating) { + return {}; + } + const auto &data = rating->data(); + return { + .level = data.vlevel().v, + .stars = int(data.vstars().v), + .thisLevelStars = int(data.vcurrent_level_stars().v), + .nextLevelStars = int(data.vnext_level_stars().value_or_empty()), + }; +} + } // namespace BotInfo::BotInfo() = default; @@ -557,6 +573,18 @@ int UserData::starsPerMessage() const { return _starsPerMessage; } +void UserData::setStoriesCorrespondent(bool is) { + if (is) { + _flags.add(UserDataFlag::StoriesCorrespondent); + } else { + _flags.remove(UserDataFlag::StoriesCorrespondent); + } +} + +bool UserData::storiesCorrespondent() const { + return (_flags.current() & UserDataFlag::StoriesCorrespondent); +} + void UserData::setStarsPerMessage(int stars) { if (_starsPerMessage != stars) { _starsPerMessage = stars; @@ -565,6 +593,17 @@ void UserData::setStarsPerMessage(int stars) { checkTrustedPayForMessage(); } +void UserData::setStarsRating(Data::StarsRating value) { + if (_starsRating != value) { + _starsRating = value; + session().changes().peerUpdated(this, UpdateFlag::StarsRating); + } +} + +Data::StarsRating UserData::starsRating() const { + return _starsRating; +} + bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } @@ -765,7 +804,16 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setCommonChatsCount(update.vcommon_chats_count().v); user->setPeerGiftsCount(update.vstargifts_count().value_or_empty()); user->checkFolder(update.vfolder_id().value_or_empty()); - user->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); + if (const auto theme = update.vtheme()) { + theme->match([&](const MTPDchatTheme &data) { + user->setThemeToken(qs(data.vemoticon())); + }, [&](const MTPDchatThemeUniqueGift &data) { + user->setThemeToken( + user->owner().cloudThemes().processGiftThemeGetToken(data)); + }); + } else { + user->setThemeToken(QString()); + } user->setTranslationDisabled(update.is_translations_disabled()); user->setPrivateForwardName( update.vprivate_forward_name().value_or_empty()); @@ -850,6 +898,13 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { } user->setBotVerifyDetails( ParseBotVerifyDetails(update.vbot_verification())); + user->setStarsRating(ParseStarsRating(update.vstars_rating())); + if (user->isSelf()) { + user->owner().setPendingStarsRating({ + .value = ParseStarsRating(update.vstars_my_pending_rating()), + .date = update.vstars_my_pending_rating_date().value_or_empty(), + }); + } if (const auto gifts = update.vdisallowed_gifts()) { const auto &data = gifts->data(); @@ -877,6 +932,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { } user->owner().stories().apply(user, update.vstories()); + user->owner().savedMusic().apply(user, update.vsaved_music()); user->fullUpdated(); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index a47340132f..435692cda6 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -121,6 +121,7 @@ enum class UserDataFlag : uint32 { HasStarsPerMessage = (1 << 23), MessageMoneyRestrictionsKnown = (1 << 24), ReadDatesPrivate = (1 << 25), + StoriesCorrespondent = (1 << 26), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -188,9 +189,15 @@ public: [[nodiscard]] bool canSendIgnoreMoneyRestrictions() const; [[nodiscard]] bool readDatesPrivate() const; + void setStoriesCorrespondent(bool is); + [[nodiscard]] bool storiesCorrespondent() const; + void setStarsPerMessage(int stars); [[nodiscard]] int starsPerMessage() const; + void setStarsRating(Data::StarsRating value); + [[nodiscard]] Data::StarsRating starsRating() const; + [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; @@ -296,6 +303,7 @@ private: QString _phone; QString _privateForwardName; std::unique_ptr _botVerifyDetails; + Data::StarsRating _starsRating; ChannelId _personalChannelId = 0; MsgId _personalChannelMessageId = 0; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 508a45d7c0..9c63bb1118 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -173,6 +173,10 @@ WebPageType ParseWebPageType( return WebPageType::Giftcode; } else if (type == u"telegram_stickerset"_q) { return WebPageType::StickerSet; + } else if (type == u"telegram_story_album"_q) { + return WebPageType::StoryAlbum; + } else if (type == u"telegram_collection"_q) { + return WebPageType::GiftCollection; } else if (hasIV) { return WebPageType::ArticleWithIV; } else { diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index e6c004e64e..68abfe1c98 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -49,6 +49,8 @@ enum class WebPageType : uint8 { Theme, Story, StickerSet, + StoryAlbum, + GiftCollection, Article, ArticleWithIV, diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.h b/Telegram/SourceFiles/data/notify/data_notify_settings.h index b1930f45bd..689964299c 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.h @@ -21,11 +21,6 @@ class Thread; class Forum; class ForumTopic; -enum class DefaultNotify { - User, - Group, - Broadcast, -}; [[nodiscard]] DefaultNotify DefaultNotifyType( not_null peer); diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h index b5de9e1195..49e3c9dc81 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h @@ -11,6 +11,12 @@ namespace Data { class NotifyPeerSettingsValue; +enum class DefaultNotify : uint8_t { + User, + Group, + Broadcast, +}; + struct NotifySound { QString title; QString data; diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_volume.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_volume.cpp new file mode 100644 index 0000000000..90ce0856d6 --- /dev/null +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_volume.cpp @@ -0,0 +1,117 @@ +/* +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/notify/data_peer_notify_volume.h" + +#include "data/data_peer.h" +#include "data/data_thread.h" +#include "data/notify/data_peer_notify_settings.h" +#include "main/main_session.h" +#include "main/main_session_settings.h" +#include "core/application.h" +#include "window/notifications_manager.h" +#include "settings/settings_common.h" +#include "ui/vertical_list.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_settings.h" + +namespace Data { + +Data::VolumeController DefaultRingtonesVolumeController( + not_null session, + Data::DefaultNotify defaultNotify) { + return Data::VolumeController{ + .volume = [=]() -> ushort { + const auto volume = session->settings().ringtoneVolume( + defaultNotify); + return volume ? volume : 100; + }, + .saveVolume = [=](ushort volume) { + session->settings().setRingtoneVolume(defaultNotify, volume); + session->saveSettingsDelayed(); + }}; +} + +Data::VolumeController ThreadRingtonesVolumeController( + not_null thread) { + return Data::VolumeController{ + .volume = [=]() -> ushort { + const auto volume = thread->session().settings().ringtoneVolume( + thread->peer()->id, + thread->topicRootId(), + thread->monoforumPeerId()); + return volume ? volume : 100; + }, + .saveVolume = [=](ushort volume) { + thread->session().settings().setRingtoneVolume( + thread->peer()->id, + thread->topicRootId(), + thread->monoforumPeerId(), + volume); + thread->session().saveSettingsDelayed(); + }}; +} + +} // namespace Data + +namespace Ui { + +void AddRingtonesVolumeSlider( + not_null container, + rpl::producer toggleOn, + rpl::producer subtitle, + Data::VolumeController volumeController) { + Expects(volumeController.volume && volumeController.saveVolume); + + const auto volumeWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + volumeWrap->toggleOn( + rpl::combine( + Core::App().notifications().volumeSupportedValue(), + std::move(toggleOn) + ) | rpl::map( + rpl::mappers::_1 && rpl::mappers::_2 + ) | rpl::distinct_until_changed(), + anim::type::normal); + volumeWrap->finishAnimating(); + + Ui::AddSubsectionTitle(volumeWrap->entity(), std::move(subtitle)); + auto sliderWithLabel = Settings::MakeSliderWithLabel( + volumeWrap->entity(), + st::settingsScale, + st::settingsScaleLabel, + st::normalFont->spacew * 2, + st::settingsScaleLabel.style.font->width("100%"), + true); + const auto slider = sliderWithLabel.slider; + const auto label = sliderWithLabel.label; + + volumeWrap->entity()->add( + std::move(sliderWithLabel.widget), + st::settingsBigScalePadding); + + const auto updateLabel = [=](int volume) { + label->setText(QString::number(volume) + '%'); + }; + + slider->setPseudoDiscrete( + 100, + [=](int index) { return index + 1; }, + int(volumeController.volume()), + updateLabel, + [saveVolume = volumeController.saveVolume](int volume) { + saveVolume(volume); + }); + updateLabel(volumeController.volume()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_volume.h b/Telegram/SourceFiles/data/notify/data_peer_notify_volume.h new file mode 100644 index 0000000000..cc6833e7a1 --- /dev/null +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_volume.h @@ -0,0 +1,45 @@ +/* +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 Main { +class Session; +} // namespace Main + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Data { + +enum class DefaultNotify : uint8_t; +class Thread; + +struct VolumeController { + Fn volume = nullptr; + Fn saveVolume = nullptr; +}; + +[[nodiscard]] VolumeController DefaultRingtonesVolumeController( + not_null session, + Data::DefaultNotify defaultNotify); + +[[nodiscard]] VolumeController ThreadRingtonesVolumeController( + not_null thread); + +} // namespace Data + +namespace Ui { + +void AddRingtonesVolumeSlider( + not_null container, + rpl::producer toggleOn, + rpl::producer subtitle, + Data::VolumeController volumeController); + +} // namespace Ui diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index eb34f30dfb..a923f44db1 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_frame_generator.h" #include "ffmpeg/ffmpeg_frame_generator.h" #include "chat_helpers/stickers_lottie.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "storage/file_download.h" // kMaxFileInMemory #include "ui/chat/chats_filter_tag.h" #include "ui/effects/premium_stars_colored.h" @@ -40,7 +41,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" -#include "styles/style_credits.h" // giftBoxByStarsStyle namespace Data { namespace { @@ -100,10 +100,6 @@ private: : FrameSizeFromTag(tag); } -[[nodiscard]] QString InternalPrefix() { - return u"internal:"_q; -} - [[nodiscard]] QString UserpicEmojiPrefix() { return u"userpic:"_q; } @@ -569,8 +565,6 @@ std::unique_ptr CustomEmojiManager::create( const auto original = data.mid(ForceStaticPrefix().size()); return std::make_unique( create(original, std::move(update), tag, sizeOverride)); - } else if (data.startsWith(InternalPrefix())) { - return internal(data); } else if (data.startsWith(UserpicEmojiPrefix())) { const auto ratio = style::DevicePixelRatio(); const auto size = EmojiSizeFromTag(tag) / ratio; @@ -618,26 +612,6 @@ std::unique_ptr CustomEmojiManager::create( }); } -std::unique_ptr CustomEmojiManager::internal( - QStringView data) { - const auto v = data.mid(InternalPrefix().size()).split(','); - if (v.size() != 5 && v.size() != 1) { - return nullptr; - } - const auto index = v[0].toInt(); - Assert(index >= 0 && index < _internalEmoji.size()); - - auto &info = _internalEmoji[index]; - const auto padding = (v.size() == 5) - ? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt()) - : QMargins(); - return std::make_unique( - data.toString(), - info.image, - padding, - info.textColor); -} - std::unique_ptr CustomEmojiManager::userpic( QStringView data, Fn update, @@ -1020,57 +994,6 @@ uint64 CustomEmojiManager::coloredSetId() const { return _coloredSetId; } -TextWithEntities CustomEmojiManager::creditsEmoji(QMargins padding) { - return Ui::Text::SingleCustomEmoji( - registerInternalEmoji( - Ui::GenerateStars(st::normalFont->height, 1), - padding, - false)); -} - -TextWithEntities CustomEmojiManager::ministarEmoji(QMargins padding) { - return Ui::Text::SingleCustomEmoji( - registerInternalEmoji( - Ui::GenerateStars(st::giftBoxByStarsStyle.font->height, 1), - padding, - false)); -} - -QString CustomEmojiManager::registerInternalEmoji( - QImage emoji, - QMargins padding, - bool textColor) { - _internalEmoji.push_back({ std::move(emoji), textColor }); - return InternalPrefix() - + QString::number(_internalEmoji.size() - 1) - + InternalPadding(padding); -} - -QString CustomEmojiManager::registerInternalEmoji( - const style::icon &icon, - QMargins padding, - bool textColor) { - const auto i = _iconEmoji.find(&icon); - if (i != end(_iconEmoji)) { - return i->second + InternalPadding(padding); - } - auto image = QImage( - icon.size() * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - image.fill(Qt::transparent); - image.setDevicePixelRatio(style::DevicePixelRatio()); - auto p = QPainter(&image); - icon.paint(p, 0, 0, icon.width()); - p.end(); - - const auto result = registerInternalEmoji( - std::move(image), - QMargins{}, - textColor); - _iconEmoji.emplace(&icon, result); - return result + InternalPadding(padding); -} - [[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData( not_null peer, QMargins padding, @@ -1108,7 +1031,10 @@ TextWithEntities SingleCustomEmoji(DocumentId id) { } TextWithEntities SingleCustomEmoji(not_null document) { - return SingleCustomEmoji(document->id); + const auto sticker = document->sticker(); + return Ui::Text::SingleCustomEmoji( + SerializeCustomEmojiId(document), + sticker ? sticker->alt : QString()); } bool AllowEmojiWithoutPremium( diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index b01f7561e4..c35d7520fb 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -83,15 +83,6 @@ public: [[nodiscard]] Main::Session &session() const; [[nodiscard]] Session &owner() const; - [[nodiscard]] QString registerInternalEmoji( - QImage emoji, - QMargins padding = {}, - bool textColor = true); - [[nodiscard]] QString registerInternalEmoji( - const style::icon &icon, - QMargins padding = {}, - bool textColor = true); - [[nodiscard]] QString peerUserpicEmojiData( not_null peer, QMargins padding = {}, @@ -99,9 +90,6 @@ public: [[nodiscard]] uint64 coloredSetId() const; - [[nodiscard]] TextWithEntities creditsEmoji(QMargins padding = {}); - [[nodiscard]] TextWithEntities ministarEmoji(QMargins padding = {}); - private: static constexpr auto kSizeCount = int(SizeTag::kCount); @@ -152,8 +140,6 @@ private: SizeTag tag, int sizeOverride, LoaderFactory factory); - [[nodiscard]] std::unique_ptr internal( - QStringView data); [[nodiscard]] std::unique_ptr userpic( QStringView data, Fn update, @@ -190,9 +176,6 @@ private: bool _repaintTimerScheduled = false; bool _requestSetsScheduled = false; - std::vector _internalEmoji; - base::flat_map, QString> _iconEmoji; - #if 0 // inject-to-on_main crl::time _repaintsLastAdded = 0; rpl::lifetime _repaintsLifetime; diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 9436d4e7c9..c2c7937293 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -556,7 +556,10 @@ dialogsMiniForward: DialogsMiniIcon { skipMedia: 2px; } -dialogsMiniReplyIcon: icon {{ "mini_forward-flip_horizontal", attentionButtonFg, point(0px, 2px) }}; +dialogsMiniReplyIcon: IconEmoji { + icon: icon {{ "mini_forward-flip_horizontal", attentionButtonFg }}; + padding: margins(0px, 2px, 0px, 0px); +} dialogsMiniReplyStory: DialogsMiniIcon { icon: ThreeStateIcon { @@ -731,7 +734,7 @@ dialogsTabsScroll: ScrollArea(defaultScrollArea) { barHidden: true; } dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) { - padding: 8px; + padding: 9px; height: 33px; barTop: 30px; barSkip: 0px; @@ -799,8 +802,10 @@ dialogsSearchTagSkip: point(8px, 4px); dialogsSearchTagBottom: 10px; dialogsSearchTagLocked: icon{{ "dialogs/mini_tag_lock", lightButtonFgOver }}; dialogsSearchTagPromo: defaultTextStyle; -dialogsSearchTagArrow: icon{{ "dialogs/mini_arrow", windowSubTextFg }}; -dialogsSearchTagArrowPadding: margins(-6px, 3px, 0px, 0px); +dialogsSearchTagArrow: IconEmoji { + icon: icon{{ "dialogs/mini_arrow", windowSubTextFg }}; + padding: margins(-6px, 3px, 0px, 0px); +} dialogsSearchTagPromoLeft: 6px; dialogsSearchTagPromoRight: 1px; dialogsSearchTagPromoSkip: 6px; @@ -825,9 +830,82 @@ dialogsSponsoredButton: DialogRightButton(dialogRowOpenBot) { margin: margins(0px, 9px, 10px, 0px); } +dialogsTopBarLeftPadding: 18px; dialogsTopBarSuggestionTitleStyle: TextStyle(defaultTextStyle) { font: font(semibold 12px); } dialogsTopBarSuggestionAboutStyle: TextStyle(defaultTextStyle) { font: font(11px); } + +dialogsMiniQuoteIcon: icon{{ "chat/mini_quote", dialogsTextFg }}; + +postsSearchIntroTitle: FlatLabel(defaultFlatLabel) { + textFg: windowBoldFg; + minWidth: 64px; + style: TextStyle(semiboldTextStyle) { + font: font(semibold 16px); + } + align: align(top); +} +postsSearchIntroTitleMargin: margins(20px, 0px, 20px, 4px); +postsSearchIntroSubtitle: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 64px; + align: align(top); +} +postsSearchIntroSubtitleMargin: margins(20px, 4px, 20px, 16px); +postsSearchIntroButton: RoundButton(defaultActiveButton) { + width: 200px; + height: 42px; + textTop: 12px; + style: semiboldTextStyle; +} +postsSearchIntroFooter: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 64px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(12px); + } +} +postsSearchIntroFooterMargin: margins(20px, 12px, 20px, 0px); +postsSearchIcon: icon {{ "box_search", windowFgActive }}; +postsSearchIconPadding: margins(12px, 1px, 6px, 0px); +postsSearchArrow: icon {{ "dialogs/mini_arrow", windowFgActive }}; +postsSearchArrowPadding: margins(2px, 1px, 8px, 0px); + +dialogsUnconfirmedAuthPadding: margins(10px, 0px, 10px, 0px); +dialogsUnconfirmedAuthTitle: FlatLabel(boxLabel) { + minWidth: 70px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(12px); + } +} +dialogsUnconfirmedAuthAbout: FlatLabel(boxLabel) { + minWidth: 70px; + align: align(top); + textFg: windowSubTextFg; + style: TextStyle(defaultTextStyle) { + font: font(11px); + } +} +dialogsUnconfirmedAuthButton: RoundButton(defaultBoxButton) { + width: -16px; + height: 26px; + textTop: 5px; + radius: 10px; + style: TextStyle(semiboldTextStyle) { + font: font(11px semibold); + } +} +dialogsUnconfirmedAuthButtonNo: RoundButton(dialogsUnconfirmedAuthButton) { + textFg: attentionButtonFg; + textFgOver: attentionButtonFgOver; + textBgOver: attentionButtonBgOver; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: attentionButtonBgRipple; + } +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index f863378136..9298a8d7fb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -230,7 +230,7 @@ uint64 Entry::computeSortPosition(FilterId filterId) const { void Entry::updateChatListExistence() { if (const auto history = asHistory()) { - if (const auto channel = history->peer->asMonoforum()) { + if (history->peer->asMonoforum()) { if (!folderKnown()) { history->clearFolder(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 19894fd4c0..669d4e944d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1244,6 +1244,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { auto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _previewResults.size()); p.translate(0, from * _st->height); if (from < _previewResults.size()) { + const auto searchLowerText = (_searchHashOrCashtag == HashOrCashtag::None) + ? _searchState.query.toLower() + : QString(); for (; from < to; ++from) { const auto &result = _previewResults[from]; const auto active = isSearchResultActive(result.get(), activeEntry); @@ -1261,6 +1264,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .currentBg = currentBg(), .filter = _filterId, .now = ms, + .searchLowerText = QStringView(searchLowerText), .width = fullWidth, .active = active, .selected = selected, @@ -1280,10 +1284,17 @@ void InnerWidget::paintEvent(QPaintEvent *e) { if (!_searchResults.empty()) { const auto text = showUnreadInSearchResults ? u"Search results"_q + : (_searchState.tab == ChatSearchTab::PublicPosts && !_searchIn) + ? (_searchState.query.isEmpty() + ? tr::lng_posts_subtitle_empty(tr::now) + : tr::lng_posts_subtitle(tr::now)) : tr::lng_search_found_results( tr::now, lt_count, _searchedMigratedCount + _searchedCount); + const auto searchLowerText = (_searchHashOrCashtag == HashOrCashtag::None) + ? _searchState.query.toLower() + : QString(); p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg); p.setFont(st::searchedBarFont); p.setPen(st::searchedBarFg); @@ -1331,6 +1342,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .currentBg = currentBg(), .filter = _filterId, .now = ms, + .searchLowerText = QStringView(searchLowerText), .width = fullWidth, .active = active, .selected = selected, @@ -1978,9 +1990,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { }); } else if (_pressed) { auto row = _pressed; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto updateCallback = [weak, row] { - const auto strong = weak.data(); + const auto strong = weak.get(); if (!strong || !strong->_pinnedShiftAnimation.animating()) { row->entry()->updateChatListEntry(); } @@ -3101,7 +3113,8 @@ void InnerWidget::fillArchiveSearchMenu(not_null menu) { || (_searchState.tab == ChatSearchTab::PublicPosts); if (!folder || !folder->chatsList()->fullSize().current() - || (!globalSearch && _searchState.inChat)) { + || (!globalSearch && _searchState.inChat) + || (_searchState.tab == ChatSearchTab::PublicPosts)) { return; } const auto skip = session().settings().skipArchiveInSearch(); @@ -3480,7 +3493,8 @@ void InnerWidget::applySearchState(SearchState state) { _filter = newFilter; if (_filter.isEmpty() && !_searchState.fromPeer - && _searchState.tags.empty()) { + && _searchState.tags.empty() + && _searchState.tab != ChatSearchTab::PublicPosts) { clearFilter(); } else { setState(WidgetState::Filtered); @@ -4095,7 +4109,7 @@ void InnerWidget::refreshEmpty() { _empty->setVisible(_state == WidgetState::Default); if (_emptyList) { _emptyList->setVisible(_state == WidgetState::Default); - _empty->setVisible(!_emptyList->isVisible()); + _empty->setVisible(false); } if (_emptyButton) { _emptyButton->setVisible(_state == WidgetState::Default); @@ -4153,14 +4167,14 @@ void InnerWidget::refreshEmpty() { .name = u"no_chats"_q, .sizeOverride = Size(st::changePhoneIconSize), }); - _emptyList->add( - object_ptr>(_emptyList, std::move(icon.widget))); + _emptyList->add(std::move(icon.widget), style::al_top); Ui::AddSkip(_emptyList); _emptyList->add( object_ptr( _emptyList, tr::lng_no_conversations(), - st::dialogEmptyButtonLabel)); + st::dialogEmptyButtonLabel), + style::al_top); if (_state == WidgetState::Default) { icon.animate(anim::repeat::once); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp new file mode 100644 index 0000000000..626c96151f --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp @@ -0,0 +1,347 @@ +/* +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 "dialogs/dialogs_search_posts.h" + +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_session.h" +#include "data/data_peer_values.h" +#include "history/history.h" +#include "main/main_session.h" + +namespace Dialogs { +namespace { + +constexpr auto kQueryDelay = crl::time(500); +constexpr auto kPerPage = 50; + +[[nodiscard]] const QRegularExpression &SearchSplitter() { + static const auto result = QRegularExpression(QString::fromLatin1("" + "[\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\!\\_\\;\\\"\\'\\x0]")); + return result; +} + +} // namespace + +PostsSearch::PostsSearch(not_null session) +: _session(session) +, _api(&_session->api().instance()) +, _timer([=] { applyQuery(); }) +, _recheckTimer([=] { recheck(); }) { + Data::AmPremiumValue(_session) | rpl::start_with_next([=] { + maybePushPremiumUpdate(); + }, _lifetime); +} + +rpl::producer PostsSearch::stateUpdates() const { + return _stateUpdates.events(); +} + +rpl::producer PostsSearch::pagesUpdates() const { + return _pagesUpdates.events(); +} + +void PostsSearch::requestMore() { + if (!_query) { + return; + } + auto &entry = _entries[*_query]; + if (_queryPushed != *_query || !entry.pagesPushed) { + return; + } else if (entry.pagesPushed < entry.pages.size()) { + _pagesUpdates.fire(PostsSearchState{ + .page = entry.pages[entry.pagesPushed++], + .totalCount = entry.totalCount, + }); + } else { + requestSearch(*_query); + } +} + +void PostsSearch::setQuery(const QString &query) { + const auto words = TextUtilities::PrepareSearchWords( + query, + &SearchSplitter()); + const auto prepared = words.isEmpty() ? QString() : words.join(' '); + if (_queryExact == query) { + return; + } + _queryExact = query; + _query = prepared; + const auto i = _entries.find(prepared); + if (i != end(_entries)) { + pushStateUpdate(i->second); + } else if (prepared.isEmpty()) { + applyQuery(); + } else { + _timer.callOnce(kQueryDelay); + } +} + +int PostsSearch::setAllowedStars(int stars) { + if (!_query) { + return 0; + } else if (_floodState) { + if (_floodState->freeSearchesLeft > 0) { + stars = 0; + } else if (_floodState->nextFreeSearchTime > 0 + && _floodState->nextFreeSearchTime <= base::unixtime::now()) { + stars = 0; + } else { + stars = std::min(int(_floodState->starsPerPaidSearch), stars); + } + } + _entries[*_query].allowedStars = stars; + requestSearch(*_query); + return stars; +} + +void PostsSearch::pushStateUpdate(const Entry &entry) { + Expects(_query.has_value()); + + const auto initial = (_queryPushed != *_query); + if (initial) { + _queryPushed = *_query; + entry.pagesPushed = 0; + } else if (entry.pagesPushed > 0) { + if (entry.pagesPushed < entry.pages.size()) { + _pagesUpdates.fire(PostsSearchState{ + .page = entry.pages[entry.pagesPushed++], + .totalCount = entry.totalCount, + }); + } + return; + } + const auto empty = entry.pages.empty() + || (entry.pages.size() == 1 && entry.pages.front().empty()); + if (!empty || (entry.loaded && !_query->isEmpty())) { + if (!entry.pages.empty()) { + ++entry.pagesPushed; + } + _stateUpdates.fire(PostsSearchState{ + .page = (entry.pages.empty() + ? std::vector>() + : entry.pages.front()), + .totalCount = entry.totalCount, + }); + } else if (entry.checkId || entry.searchId) { + _stateUpdates.fire(PostsSearchState{ + .loading = true, + }); + } else { + Assert(_floodState.has_value()); + auto copy = _floodState; + copy->query = *_queryExact; + copy->needsPremium = !_session->premium(); + _stateUpdates.fire(PostsSearchState{ + .intro = std::move(copy), + }); + } +} + +void PostsSearch::maybePushPremiumUpdate() { + if (!_floodState || !_query) { + return; + } + auto &entry = _entries[*_query]; + if (!entry.pages.empty() + || entry.loaded + || entry.checkId + || entry.searchId) { + return; + } + pushStateUpdate(entry); +} + +void PostsSearch::applyQuery() { + Expects(_query.has_value()); + + _timer.cancel(); + if (_query->isEmpty()) { + requestSearch(QString()); + } else { + requestState(*_query); + } +} + +void PostsSearch::requestSearch(const QString &query) { + auto &entry = _entries[query]; + if (entry.searchId || entry.loaded) { + return; + } + + const auto useStars = entry.allowedStars; + entry.allowedStars = 0; + + using Flag = MTPchannels_SearchPosts::Flag; + entry.searchId = _api.request(MTPchannels_SearchPosts( + MTP_flags(Flag::f_query + | (useStars ? Flag::f_allow_paid_stars : Flag())), + MTP_string(), // hashtag + MTP_string(query), + MTP_int(entry.offsetRate), + (entry.offsetPeer ? entry.offsetPeer->input : MTP_inputPeerEmpty()), + MTP_int(entry.offsetId), + MTP_int(kPerPage), + MTP_long(useStars) + )).done([=](const MTPmessages_Messages &result) { + auto &entry = _entries[query]; + entry.searchId = 0; + + const auto initial = !entry.offsetId; + const auto owner = &_session->data(); + const auto processList = [&](const MTPVector &messages) { + auto result = std::vector>(); + for (const auto &message : messages.v) { + const auto msgId = IdFromMessage(message); + const auto peerId = PeerFromMessage(message); + const auto lastDate = DateFromMessage(message); + if (const auto peer = owner->peerLoaded(peerId)) { + if (lastDate) { + const auto item = owner->addNewMessage( + message, + MessageFlags(), + NewMessageType::Existing); + result.push_back(item); + } + entry.offsetPeer = peer; + } else { + LOG(("API Error: a search results with not loaded peer %1" + ).arg(peerId.value)); + } + entry.offsetId = msgId; + } + return result; + }; + auto totalCount = 0; + auto messages = result.match([&](const MTPDmessages_messages &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + entry.loaded = true; + auto list = processList(data.vmessages()); + totalCount = list.size(); + return list; + }, [&](const MTPDmessages_messagesSlice &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + auto list = processList(data.vmessages()); + const auto nextRate = data.vnext_rate(); + const auto rateUpdated = nextRate + && (nextRate->v != entry.offsetRate); + const auto finished = list.empty(); + if (rateUpdated) { + entry.offsetRate = nextRate->v; + } + if (finished) { + entry.loaded = true; + } + totalCount = data.vcount().v; + if (const auto flood = data.vsearch_flood()) { + setFloodStateFrom(flood->data()); + } + return list; + }, [&](const MTPDmessages_channelMessages &data) { + LOG(("API Error: " + "received messages.channelMessages when no channel " + "was passed! (PostsSearch::performSearch)")); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + auto list = processList(data.vmessages()); + if (list.empty()) { + entry.loaded = true; + } + totalCount = data.vcount().v; + return list; + }, [&](const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(PostsSearch::performSearch)")); + entry.loaded = true; + return std::vector>(); + }); + if (initial) { + entry.pages.clear(); + } + entry.pages.push_back(std::move(messages)); + const auto count = int(ranges::accumulate( + entry.pages, + size_type(), + ranges::plus(), + &std::vector>::size)); + const auto full = entry.loaded ? count : std::max(count, totalCount); + entry.totalCount = full; + if (_query == query) { + pushStateUpdate(entry); + } + }).fail([=](const MTP::Error &error) { + auto &entry = _entries[query]; + entry.searchId = 0; + + const auto initial = !entry.offsetId; + const auto &type = error.type(); + if (initial && type.startsWith(u"FLOOD_WAIT_"_q)) { + requestState(query); + } else { + entry.loaded = true; + } + }).handleFloodErrors().send(); +} + +void PostsSearch::setFloodStateFrom(const MTPDsearchPostsFlood &data) { + _recheckTimer.cancel(); + const auto left = std::max(data.vremains().v, 0); + const auto next = data.vwait_till().value_or_empty(); + if (!left && next > 0) { + const auto now = base::unixtime::now(); + const auto delay = std::clamp(next - now, 1, 86401); + _recheckTimer.callOnce(delay * crl::time(1000)); + } + _floodState = PostsSearchIntroState{ + .freeSearchesPerDay = data.vtotal_daily().v, + .freeSearchesLeft = left, + .nextFreeSearchTime = next, + .starsPerPaidSearch = uint32(data.vstars_amount().v), + }; +} + +void PostsSearch::recheck() { + requestState(*_query, true); +} + +void PostsSearch::requestState(const QString &query, bool force) { + auto &entry = _entries[query]; + if (force) { + _api.request(base::take(entry.checkId)).cancel(); + } else if (entry.checkId || entry.loaded) { + return; + } + + using Flag = MTPchannels_CheckSearchPostsFlood::Flag; + entry.checkId = _api.request(MTPchannels_CheckSearchPostsFlood( + MTP_flags(Flag::f_query), + MTP_string(query) + )).done([=](const MTPSearchPostsFlood &result) { + auto &entry = _entries[query]; + entry.checkId = 0; + + const auto &data = result.data(); + setFloodStateFrom(data); + if (data.is_query_is_free()) { + if (!entry.loaded) { + requestSearch(query); + } + } else if (_query == query) { + pushStateUpdate(entry); + } + }).fail([=](const MTP::Error &error) { + auto &entry = _entries[query]; + entry.checkId = 0; + entry.loaded = true; + }).handleFloodErrors().send(); +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_posts.h b/Telegram/SourceFiles/dialogs/dialogs_search_posts.h new file mode 100644 index 0000000000..5f48b98eb0 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/dialogs_search_posts.h @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/timer.h" +#include "dialogs/ui/posts_search_intro.h" +#include "mtproto/sender.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Dialogs { + +struct PostsSearchState { + std::optional intro; + std::vector> page; + int totalCount = 0; + bool loading = false; +}; + +class PostsSearch final { +public: + explicit PostsSearch(not_null session); + + [[nodiscard]] rpl::producer stateUpdates() const; + [[nodiscard]] rpl::producer pagesUpdates() const; + + void setQuery(const QString &query); + int setAllowedStars(int stars); + void requestMore(); + +private: + struct Entry { + std::vector>> pages; + int totalCount = 0; + mtpRequestId searchId = 0; + mtpRequestId checkId = 0; + PeerData *offsetPeer = nullptr; + MsgId offsetId = 0; + int offsetRate = 0; + int allowedStars = 0; + mutable int pagesPushed = 0; + bool loaded = false; + }; + + void recheck(); + void applyQuery(); + void requestSearch(const QString &query); + void requestState(const QString &query, bool force = false); + void setFloodStateFrom(const MTPDsearchPostsFlood &data); + void pushStateUpdate(const Entry &entry); + void maybePushPremiumUpdate(); + + const not_null _session; + + MTP::Sender _api; + + base::Timer _timer; + base::Timer _recheckTimer; + base::flat_map _entries; + std::optional _queryExact; + std::optional _query; + QString _queryPushed; + + std::optional _floodState; + + rpl::event_stream _stateUpdates; + rpl::event_stream _pagesUpdates; + + rpl::lifetime _lifetime; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index 6b105f0943..61272816a0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -52,24 +52,14 @@ namespace { }); } -[[nodiscard]] Ui::Text::String FillAdditionalText( - not_null owner, - int width) { - auto emoji = Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - st::dialogsSearchTagArrow, - st::dialogsSearchTagArrowPadding)); +[[nodiscard]] Ui::Text::String FillAdditionalText(int width) { + auto emoji = Ui::Text::IconEmoji(&st::dialogsSearchTagArrow); auto result = Ui::Text::String(); - const auto context = Core::TextContext({ - .session = &owner->session(), - .customEmojiLoopLimit = 1, - }); const auto attempt = [&](const auto &phrase) { result.setMarkedText( st::dialogsSearchTagPromo, phrase(tr::now, lt_arrow, emoji, Ui::Text::WithEntities), - kMarkupTextOptions, - context); + kMarkupTextOptions); return result.maxWidth() < width; }; if (attempt(tr::lng_add_tag_phrase_long) @@ -230,7 +220,7 @@ void SearchTags::layout() { if (_tags.size() == 1 && _tags.front().promo) { _additionalLeft = x - skip.x() + st::dialogsSearchTagPromoSkip; const auto additionalWidth = _width - _additionalLeft; - _additionalText = FillAdditionalText(_owner, additionalWidth); + _additionalText = FillAdditionalText(additionalWidth); } else { _additionalText = {}; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp index b4f028eb12..8d25e709f7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_top_bar_suggestion.h" +#include "api/api_authorizations.h" #include "api/api_credits.h" #include "api/api_peer_photo.h" #include "api/api_premium.h" @@ -16,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" +#include "data/components/promo_suggestions.h" #include "data/data_birthday.h" #include "data/data_changes.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. @@ -25,16 +27,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_group_call_bar.h" #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" -#include "data/components/promo_suggestions.h" #include "main/main_session.h" +#include "settings/settings_active_sessions.h" #include "settings/settings_credits_graphics.h" #include "settings/settings_premium.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/effects/credits_graphics.h" #include "ui/layers/generic_box.h" +#include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/ui_utility.h" +#include "ui/vertical_list.h" +#include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -42,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" +#include "styles/style_layers.h" namespace Dialogs { namespace { @@ -53,6 +61,92 @@ namespace { return window->sessionController(); } +[[nodiscard]] QString FormatAuthInfo(const Data::UnreviewedAuth &auth) { + const auto location = auth.location.isEmpty() + ? QString() + : "\U0001F30D " + auth.location; + const auto device = auth.device.isEmpty() + ? QString() + : "\U0001F4F1 " + auth.device; + + if (!location.isEmpty() && !device.isEmpty()) { + return location + " (" + device + ")"; + } else if (!location.isEmpty()) { + return location; + } else if (!device.isEmpty()) { + return device; + } + return QString(); +} + +void ShowAuthToast( + not_null parent, + not_null session, + const std::vector &list, + bool confirmed) { + if (confirmed) { + auto text = tr::lng_unconfirmed_auth_confirmed_message( + tr::now, + lt_link, + Ui::Text::Link(tr::lng_settings_sessions_title(tr::now)), + Ui::Text::RichLangValue); + auto filter = [=]( + ClickHandlerPtr handler, + Qt::MouseButton button) { + if (const auto controller = FindSessionController(parent)) { + session->api().authorizations().reload(); + controller->showSettings(Settings::Sessions::Id()); + return false; + } + return true; + }; + Ui::Toast::Show(parent->window(), Ui::Toast::Config{ + .title = tr::lng_unconfirmed_auth_confirmed(tr::now), + .text = std::move(text), + .filter = std::move(filter), + .duration = crl::time(5000), + }); + } else { + auto messageText = QString(); + if (list.size() == 1) { + messageText = tr::lng_unconfirmed_auth_denied_single( + tr::now, + lt_country, + FormatAuthInfo(list.front())); + } else { + auto authList = QString('\n'); + for (auto i = 0; i < std::min(int(list.size()), 10); ++i) { + const auto info = FormatAuthInfo(list[i]); + if (!info.isEmpty()) { + authList += "• " + info + "\n"; + } + } + messageText = tr::lng_unconfirmed_auth_denied_multiple( + tr::now, + lt_country, + authList); + } + if (const auto controller = FindSessionController(parent)) { + const auto count = float64(list.size()); + controller->show(Box([=](not_null box) { + box->setTitle(tr::lng_unconfirmed_auth_denied_title( + lt_count, + rpl::single(count))); + Ui::InformBox(box, { + .text = TextWithEntities() + .append(messageText) + .append('\n') + .append( + tr::lng_unconfirmed_auth_denied_warning( + tr::now, + Ui::Text::Bold)), + .confirmText = tr::lng_archive_hint_button(tr::now), + }); + })); + } + } +} + constexpr auto kSugSetBirthday = "BIRTHDAY_SETUP"_cs; constexpr auto kSugBirthdayContacts = "BIRTHDAY_CONTACTS_TODAY"_cs; constexpr auto kSugPremiumAnnual = "PREMIUM_ANNUAL"_cs; @@ -62,6 +156,7 @@ constexpr auto kSugPremiumGrace = "PREMIUM_GRACE"_cs; constexpr auto kSugSetUserpic = "USERPIC_SETUP"_cs; constexpr auto kSugLowCreditsSubs = "STARS_SUBSCRIPTION_LOW_BALANCE"_cs; + } // namespace rpl::producer*> TopBarSuggestionValue( @@ -79,7 +174,9 @@ rpl::producer*> TopBarSuggestionValue( struct State { TopBarSuggestionContent *content = nullptr; - Ui::SlideWrap *wrap = nullptr; + Ui::SlideWrap *unconfirmedWarning = nullptr; + base::unique_qptr> wrap; + rpl::variable leftPadding; rpl::variable desiredWrapToggle; rpl::variable outerWrapToggle; rpl::lifetime birthdayLifetime; @@ -92,7 +189,9 @@ rpl::producer*> TopBarSuggestionValue( const auto state = lifetime.make_state(); state->outerWrapToggle = rpl::duplicate(outerWrapToggleValue); - const auto ensureWrap = [=] { + state->leftPadding = rpl::variable( + rpl::single(st::dialogsTopBarLeftPadding)); + const auto ensureContent = [=] { if (!state->content) { state->content = Ui::CreateChild( parent); @@ -103,15 +202,27 @@ rpl::producer*> TopBarSuggestionValue( state->content->resize(width, height); }, state->content->lifetime()); } + }; + const auto ensureWrap = [=](not_null child) { if (!state->wrap) { - state->wrap = Ui::CreateChild>( - parent, - object_ptr::fromRaw(state->content)); + state->wrap + = base::make_unique_q>( + parent, + object_ptr::fromRaw(child)); state->desiredWrapToggle.force_assign( Toggle{ false, anim::type::instant }); } }; + const auto setLeftPaddingRelativeTo = [=]( + not_null content, + not_null relativeTo) { + content->setLeftPadding(state->leftPadding.value( + ) | rpl::map([w = relativeTo->width()](int padding) { + return w + padding * 2; + })); + }; + const auto processCurrentSuggestion = [=](auto repeat) -> void { state->birthdayLifetime.destroy(); state->premiumLifetime.destroy(); @@ -119,14 +230,52 @@ rpl::producer*> TopBarSuggestionValue( state->giftsLifetime.destroy(); state->creditsLifetime.destroy(); - ensureWrap(); + if (!session->api().authorizations().unreviewed().empty()) { + state->content = nullptr; + state->wrap = nullptr; + const auto &list + = session->api().authorizations().unreviewed(); + const auto hashes = ranges::views::all( + list + ) | ranges::views::transform([](const auto &auth) { + return auth.hash; + }) | ranges::to_vector; + + const auto content = CreateUnconfirmedAuthContent( + parent, + list, + [=](bool confirmed) { + ShowAuthToast(parent, session, list, confirmed); + session->api().authorizations().review( + hashes, + confirmed); + }); + ensureWrap(content); + const auto wasUnconfirmedWarning = state->unconfirmedWarning; + state->unconfirmedWarning = content; + state->desiredWrapToggle.force_assign(Toggle{ + true, + (state->unconfirmedWarning != wasUnconfirmedWarning) + ? anim::type::instant + : anim::type::normal, + }); + return; + } else { + if (state->unconfirmedWarning) { + state->unconfirmedWarning = nullptr; + state->wrap = nullptr; + } + } + + ensureContent(); + ensureWrap(state->content); const auto content = state->content; - const auto wrap = state->wrap; + const auto wrap = state->wrap.get(); using RightIcon = TopBarSuggestionContent::RightIcon; const auto promo = &session->promoSuggestions(); if (const auto custom = promo->custom()) { content->setRightIcon(RightIcon::Close); - content->setLeftPadding(0); + content->setLeftPadding(state->leftPadding.value()); content->setClickedCallback([=] { const auto controller = FindSessionController(parent); UrlClickHandler::Open( @@ -150,7 +299,7 @@ rpl::producer*> TopBarSuggestionValue( } else if (session->premiumCanBuy() && promo->current(kSugPremiumGrace.utf8())) { content->setRightIcon(RightIcon::Close); - content->setLeftPadding(0); + content->setLeftPadding(state->leftPadding.value()); content->setClickedCallback([=] { const auto controller = FindSessionController(parent); UrlClickHandler::Open( @@ -187,7 +336,7 @@ rpl::producer*> TopBarSuggestionValue( return; } content->setRightIcon(RightIcon::Close); - content->setLeftPadding(0); + content->setLeftPadding(state->leftPadding.value()); content->setClickedCallback([=] { const auto controller = FindSessionController(parent); controller->uiShow()->show(Box( @@ -298,8 +447,6 @@ rpl::producer*> TopBarSuggestionValue( tr::now, TextWithEntities::Simple); content->setContent(std::move(title), std::move(text)); - const auto leftPadding - = st::defaultDialogRow.padding.left(); state->giftsLifetime.destroy(); if (!isSingle) { struct UserViews { @@ -348,11 +495,16 @@ rpl::producer*> TopBarSuggestionValue( s->inRow, st, 3); - content->setLeftPadding(leftPadding - + (users.size() * st.size - st.shift)); + const auto v = int(users.size() * st.size + - st.shift); + content->setLeftPadding( + state->leftPadding.value( + ) | rpl::map([v](int padding) { + return padding * 2 + v; + })); } p.drawImage( - leftPadding, + state->leftPadding.current(), (widget->height() - (s->userpics.height() / style::DevicePixelRatio())) / 2, @@ -368,15 +520,17 @@ rpl::producer*> TopBarSuggestionValue( st::uploadUserpicButton)); const auto fake = ptr->get(); fake->setAttribute(Qt::WA_TransparentForMouseEvents); - content->sizeValue() | rpl::filter_size( - ) | rpl::start_with_next([=](const QSize &s) { + rpl::combine( + state->leftPadding.value(), + content->sizeValue() | rpl::filter_size() + ) | rpl::start_with_next([=](int p, const QSize &s) { fake->raise(); fake->show(); fake->moveToLeft( - leftPadding, + p, (s.height() - fake->height()) / 2); }, fake->lifetime()); - content->setLeftPadding(fake->width() + leftPadding); + setLeftPaddingRelativeTo(content, fake); } state->desiredWrapToggle.force_assign( @@ -386,7 +540,7 @@ rpl::producer*> TopBarSuggestionValue( } else if (promo->current(kSugSetBirthday.utf8()) && !Data::IsBirthdayToday(session->user()->birthday())) { content->setRightIcon(RightIcon::Close); - content->setLeftPadding(0); + content->setLeftPadding(state->leftPadding.value()); content->setClickedCallback([=] { const auto controller = FindSessionController(parent); Core::App().openInternalUrl( @@ -461,7 +615,7 @@ rpl::producer*> TopBarSuggestionValue( }; if (isPremiumAnnual || isPremiumRestore || isPremiumUpgrade) { content->setRightIcon(RightIcon::Arrow); - content->setLeftPadding(0); + content->setLeftPadding(state->leftPadding.value()); const auto api = &session->api().premium(); api->statusTextValue() | rpl::start_with_next([=] { for (const auto &o : api->subscriptionOptions()) { @@ -485,16 +639,17 @@ rpl::producer*> TopBarSuggestionValue( &controller->window(), Ui::UserpicButton::Role::ChoosePhoto, st::uploadUserpicButton); - const auto leftPadding = st::defaultDialogRow.padding.left(); - content->sizeValue() | rpl::filter_size( - ) | rpl::start_with_next([=](const QSize &s) { + rpl::combine( + state->leftPadding.value(), + content->sizeValue() | rpl::filter_size() + ) | rpl::start_with_next([=](int padding, const QSize &s) { upload->raise(); upload->show(); upload->moveToLeft( - leftPadding, + padding, (s.height() - upload->height()) / 2); }, content->lifetime()); - content->setLeftPadding(upload->width() + leftPadding); + setLeftPaddingRelativeTo(content, upload); upload->chosenImages() | rpl::start_with_next([=]( Ui::UserpicButton::ChosenImage &&chosen) { if (chosen.type == Ui::UserpicButton::ChosenType::Set) { @@ -577,9 +732,10 @@ rpl::producer*> TopBarSuggestionValue( rpl::merge( session->promoSuggestions().value(), + session->api().authorizations().unreviewedChanges(), Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty ) | rpl::start_with_next([=] { - const auto was = state->wrap; + const auto was = state->wrap.get(); processCurrentSuggestion(processCurrentSuggestion); if (was != state->wrap) { consumer.put_next_copy(state->wrap); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 757da6b31e..8de8b8e5dc 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "api/api_authorizations.h" #include "api/api_chat_filters.h" #include "apiwrap.h" #include "chat_helpers/message_field.h" @@ -1016,7 +1017,7 @@ void Widget::setGeometryWithTopMoved( _topDelta = topDelta; bool willBeResized = (size() != newGeometry.size()); if (geometry() != newGeometry) { - auto weak = Ui::MakeWeak(this); + auto weak = base::make_weak(this); setGeometry(newGeometry); if (!weak) { return; @@ -1067,6 +1068,10 @@ void Widget::setupTopBarSuggestions(not_null dialogs) { } using namespace rpl::mappers; crl::on_main(&session(), [=, session = &session()] { + session->api().authorizations().unreviewedChanges( + ) | rpl::start_with_next([=] { + updateForceDisplayWide(); + }, lifetime()); (session->data().chatsListLoaded(nullptr) ? rpl::single(nullptr) : session->data().chatsListLoadedEvents() @@ -1749,7 +1754,7 @@ void Widget::updateSuggestions(anim::type animated) { _suggestions = nullptr; _hidingSuggestions.clear(); storiesExplicitCollapse(); - updateStoriesVisibility(); + updateControlsVisibility(); _scroll->show(); } } else if (suggest && !_suggestions) { @@ -2750,6 +2755,11 @@ void Widget::showMainMenu() { } void Widget::searchMessages(SearchState state) { + if (const auto peer = state.inChat.peer()) { + if (_openedForum && peer->forum() != _openedForum) { + controller()->closeForum(); + } + } applySearchState(std::move(state)); session().local().saveRecentSearchHashtags(_searchState.query); } @@ -2925,15 +2935,19 @@ void Widget::requestPublicPosts(bool fromStart) { .posts = true, .start = fromStart, }; + using Flag = MTPchannels_SearchPosts::Flag; _postsProcess.requestId = session().api().request( MTPchannels_SearchPosts( + MTP_flags(Flag::f_hashtag), MTP_string(_searchState.query.trimmed().mid(1)), + MTP_string(), // query MTP_int(fromStart ? 0 : _postsProcess.nextRate), (fromStart ? MTP_inputPeerEmpty() : _postsProcess.lastPeer->input), MTP_int(fromStart ? 0 : _postsProcess.lastId), - MTP_int(kSearchPerPage)) + MTP_int(kSearchPerPage), + MTP_long(0)) // allow_paid_stars ).done([=](const MTPmessages_Messages &result) { searchReceived(type, result, &_postsProcess); }).fail([=](const MTP::Error &error) { @@ -3312,7 +3326,8 @@ void Widget::updateForceDisplayWide() { || (_subsectionTopBar && _subsectionTopBar->searchHasFocus()) || _searchSuggestionsLocked || !_searchState.query.isEmpty() - || _searchState.inChat); + || _searchState.inChat + || !session().api().authorizations().unreviewed().empty()); } void Widget::showForum( diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index ab4685101f..faf7b89370 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -331,7 +331,7 @@ private: base::unique_qptr _chatFilters; - Ui::SlideWrap *_topBarSuggestion = nullptr; + QPointer> _topBarSuggestion; rpl::event_stream _topBarSuggestionHeightChanged; rpl::event_stream _searchStateForTopBarSuggestion; rpl::event_stream _openedFolderOrForumChanges; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 1c6a0df81b..09b93657bf 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -598,13 +598,9 @@ void PaintRow( }), Text::WithEntities); if (draft && draft->reply) { - auto &data = thread->owner().customEmojiManager(); draftText = Ui::Text::Colorized( - Ui::Text::SingleCustomEmoji( - data.registerInternalEmoji( - st::dialogsMiniReplyIcon, - {}, - true))).append(std::move(draftText)); + Ui::Text::IconEmoji(&st::dialogsMiniReplyIcon) + ).append(std::move(draftText)); } const auto context = Core::TextContext({ .session = &thread->session(), @@ -1153,6 +1149,7 @@ void RowPainter::Paint( return {}; }(); previewOptions.ignoreGroup = true; + previewOptions.searchLowerText = context.searchLowerText; const auto badgesState = context.displayUnreadInfo ? entry->chatListBadgesState() @@ -1257,7 +1254,8 @@ void PaintCollapsedRow( + st::dialogsUnreadFont->ascent; const auto left = context.narrow ? ((context.width - st::semiboldFont->width(text)) / 2) - : context.st->padding.left(); + : st::dialogsTopBarLeftPadding; + // : context.st->padding.left(); p.drawText(left, textBaseline, text); } else { folder->paintUserpic( diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index 50002c4847..d3cc032cc3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -64,6 +64,7 @@ struct PaintContext { FilterId filter = 0; float64 topicsExpanded = 0.; crl::time now = 0; + QStringView searchLowerText; int width = 0; bool active = false; bool selected = false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index 71859a700e..8a584067ac 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -9,15 +9,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" +#include "history/view/history_view_element.h" #include "history/view/history_view_item_preview.h" #include "main/main_session.h" #include "dialogs/dialogs_three_state_icon.h" #include "dialogs/ui/dialogs_layout.h" #include "dialogs/ui/dialogs_topics_view.h" #include "ui/effects/spoiler_mess.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/power_saving.h" #include "core/ui_integration.h" #include "lang/lang_keys.h" @@ -186,7 +189,7 @@ void MessageView::prepare( : nullptr; const auto hasImages = !preview.images.empty(); const auto history = item->history(); - const auto context = Core::TextContext({ + auto context = Core::TextContext({ .session = &history->session(), .repaint = customEmojiRepaint, .customEmojiLoopLimit = kEmojiLoopCount, @@ -207,13 +210,105 @@ void MessageView::prepare( } TextUtilities::Trim(preview.text); auto textToCache = DialogsPreviewText(std::move(preview.text)); + + if (!options.searchLowerText.isEmpty()) { + static constexpr auto kLeftShift = 15; + auto minFrom = std::numeric_limits::max(); + + const auto words = Ui::Text::Words(options.searchLowerText); + textToCache.entities.reserve(textToCache.entities.size() + + words.size()); + + for (const auto &word : words) { + const auto selection = HistoryView::FindSearchQueryHighlight( + textToCache.text, + word); + if (!selection.empty()) { + minFrom = std::min(minFrom, selection.from); + textToCache.entities.push_back(EntityInText{ + EntityType::Colorized, + selection.from, + selection.to - selection.from + }); + } + } + + if (minFrom == std::numeric_limits::max() + && !item->replyTo().quote.empty()) { + auto textQuote = TextWithEntities(); + for (const auto &word : words) { + const auto selection = HistoryView::FindSearchQueryHighlight( + item->replyTo().quote.text, + word); + if (!selection.empty()) { + minFrom = 0; + if (textQuote.empty()) { + textQuote = item->replyTo().quote; + } + textQuote.entities.push_back(EntityInText{ + EntityType::Colorized, + selection.from, + selection.to - selection.from + }); + } + } + if (!textQuote.empty()) { + auto helper = Ui::Text::CustomEmojiHelper(context); + const auto factory = Ui::Text::PaletteDependentEmoji{ + .factory = [=] { + const auto &icon = st::dialogsMiniQuoteIcon; + auto image = QImage( + icon.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + { + auto p = Painter(&image); + icon.paintInCenter( + p, + Rect(icon.size()), + st::dialogsTextFg->c); + } + return image; + }, + .margin = QMargins( + st::lineWidth * 2, + 0, + st::lineWidth * 2, + 0), + }; + textToCache = textQuote + .append(helper.paletteDependent(factory)) + .append(std::move(textToCache)); + context = helper.context(customEmojiRepaint); + } + } + + if (!words.empty() && minFrom != std::numeric_limits::max()) { + std::sort( + textToCache.entities.begin(), + textToCache.entities.end(), + [](const auto &a, const auto &b) { + return a.offset() < b.offset(); + }); + + const auto textSize = textToCache.text.size(); + minFrom = (minFrom > textSize || minFrom < kLeftShift) + ? 0 + : minFrom - kLeftShift; + + textToCache = (TextWithEntities{ + minFrom > 0 ? kQEllipsis : QString() + }).append(Text::Mid(std::move(textToCache), minFrom)); + } + } _hasPlainLinkAtBegin = !textToCache.entities.empty() && (textToCache.entities.front().type() == EntityType::Colorized); _textCache.setMarkedText( st::dialogsTextStyle, std::move(textToCache), DialogTextOptions(), - context); + std::move(context)); _textCachedFor = item; _imagesCache = std::move(preview.images); if (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index bea4a1b291..3de988b0e3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -215,7 +215,7 @@ void FillSourceMenu( add(tr::lng_stories_archive_button(tr::now), [=] { controller->showSection(Info::Stories::Make( peer, - Info::Stories::Tab::Archive)); + Info::Stories::ArchiveId())); }, &st::menuIconStoriesArchiveSection); add(tr::lng_stories_my_title(tr::now), [=] { controller->showSection(Info::Stories::Make(peer)); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp index ae4a4768cd..33954d2619 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -13,8 +13,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_key_modifiers.h" #include "boxes/peer_list_box.h" #include "core/application.h" +#include "core/ui_integration.h" #include "data/components/recent_peers.h" #include "data/components/top_peers.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -24,6 +26,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "dialogs/ui/chat_search_empty.h" +#include "dialogs/ui/chat_search_in.h" +#include "dialogs/ui/posts_search_intro.h" +#include "dialogs/dialogs_inner_widget.h" +#include "dialogs/dialogs_search_posts.h" #include "history/history.h" #include "info/downloads/info_downloads_widget.h" #include "info/media/info_media_widget.h" @@ -34,10 +40,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "settings/settings_credits_graphics.h" +#include "settings/settings_premium.h" #include "storage/storage_shared_media.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/swipe_handler.h" #include "ui/effects/ripple_animation.h" +#include "ui/toast/toast.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/custom_emoji_text_badge.h" #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" @@ -61,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" #include "styles/style_window.h" namespace Dialogs { @@ -1331,6 +1343,8 @@ Suggestions::Suggestions( , _appsScroll(std::make_unique(this)) , _appsContent( _appsScroll->setOwnedWidget(object_ptr(this))) +, _postsScroll(std::make_unique(this)) +, _postsWrap(_postsScroll->setOwnedWidget(object_ptr(this))) , _recentApps(setupRecentApps()) , _popularApps(setupPopularApps()) , _searchQueryTimer([=] { applySearchQuery(); }) { @@ -1397,6 +1411,7 @@ void Suggestions::setupTabs() { { Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) }, { Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) }, { Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) }, + { Key{ Tab::Posts }, tr::lng_recent_posts(tr::now) }, { Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) }, { Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) }, { Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) }, @@ -1411,13 +1426,21 @@ void Suggestions::setupTabs() { tr::lng_all_voice(tr::now), }, }; - auto sections = std::vector(); + + auto helper = Ui::Text::CustomEmojiHelper(); + auto sections = std::vector(); for (const auto key : _tabKeys) { const auto i = labels.find(key); Assert(i != end(labels)); - sections.push_back(i->second); + auto text = TextWithEntities{ i->second }; + if (key.tab == Tab::Posts) { + text.append(' ').append(helper.paletteDependent( + Ui::Text::CustomEmojiTextBadge( + tr::lng_premium_summary_new_badge(tr::now)))); + } + sections.push_back(std::move(text)); } - _tabs->setSections(sections); + _tabs->setSections(sections, helper.context()); _tabs->sectionActivated( ) | rpl::start_with_next([=](int section) { Assert(section >= 0 && section < _tabKeys.size()); @@ -1452,7 +1475,7 @@ void Suggestions::setupChats() { _topPeers->showMenuRequests( ) | rpl::start_with_next([=](const ShowTopPeerMenuRequest &request) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto owner = &_controller->session().data(); const auto peer = owner->peer(PeerId(request.id)); const auto removeOne = [=] { @@ -1478,7 +1501,7 @@ void Suggestions::setupChats() { Ui::Text::FixAmpersandInAction), .removeAllConfirm = tr::lng_recent_hide_sure(tr::now), .removeAll = removeAll, - }); + }); }, _topPeers->lifetime()); _topPeers->scrollToRequests( @@ -1496,8 +1519,8 @@ void Suggestions::setupChats() { } void Suggestions::handlePressForChatPreview( - PeerId id, - Fn callback) { + PeerId id, + Fn callback) { callback = crl::guard(this, callback); const auto row = RowDescriptor( _controller->session().data().history(id), @@ -1812,7 +1835,11 @@ bool Suggestions::consumeSearchQuery(const QString &query) { const auto key = _key.current(); const auto tab = key.tab; const auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount; - if (tab != Tab::Downloads + if (tab == Tab::Posts) { + const auto changed = (_searchQuery != query); + setPostsSearchQuery(query); + return changed || !query.isEmpty(); + } else if (tab != Tab::Downloads && type != Type::File && type != Type::Link && type != Type::MusicFile) { @@ -1831,6 +1858,177 @@ bool Suggestions::consumeSearchQuery(const QString &query) { return true; } +void Suggestions::setupPostsSearch() { + _postsSearch = std::make_unique(&_controller->session()); + + _postsSearch->stateUpdates( + ) | rpl::start_with_next([=](const PostsSearchState &state) { + if (state.intro) { + if (!_postsSearchIntro) { + setupPostsIntro(*state.intro); + } else { + _postsSearchIntro->update(*state.intro); + } + return; + } else if (!_postsContent) { + setupPostsResults(); + } + + _postsContent->applySearchState(SearchState{ + .tab = ChatSearchTab::PublicPosts, + .query = _searchQuery, + }); + if (state.loading) { + _postsContent->searchRequested(true); + } else { + _postsContent->searchReceived( + state.page, + nullptr, + { .posts = true, .start = true }, + state.totalCount); + _postsScroll->scrollToY(0); + updatePostsSearchVisibleRange(); + } + }, _postsWrap->lifetime()); + + _postsSearch->pagesUpdates( + ) | rpl::start_with_next([=](const PostsSearchState &state) { + Expects(!state.intro && !state.loading); + + if (!_postsContent) { + return; + } + _postsContent->searchReceived( + state.page, + nullptr, + { .posts = true }, + state.totalCount); + updatePostsSearchVisibleRange(); + }, _postsWrap->lifetime()); +} + +void Suggestions::setPostsSearchQuery(const QString &query) { + if (!_postsSearch) { + setupPostsSearch(); + } + if (!query.isEmpty()) { + _persist = true; + } + _searchQuery = query; + _searchQueryTimer.cancel(); + _postsSearch->setQuery(query); +} + +void Suggestions::setupPostsResults() { + Expects(!_postsContent); + + delete base::take(_postsSearchIntro); + _postsContent = Ui::CreateChild( + _postsWrap.get(), + _controller, + rpl::single(InnerWidget::ChildListShown())); + + _postsContent->applySearchState(SearchState{ + .tab = ChatSearchTab::PublicPosts, + .query = _searchQuery, + }); + _postsContent->searchRequested(true); + + _postsContent->chosenRow( + ) | rpl::start_with_next([=](const ChosenRow &row) { + const auto history = row.key.history(); + if (!history) { + return; + } + _persist = true; + const auto showAtMsgId = row.message.fullId.msg; + auto params = Window::SectionShow( + Window::SectionShow::Way::ClearStack); + params.highlight = Window::SearchHighlightId(_searchQuery); + if (row.newWindow) { + _controller->showInNewWindow(history->peer, showAtMsgId); + } else { + _controller->showThread(history, showAtMsgId, params); + } + }, _postsContent->lifetime()); + + _postsContent->heightValue() | rpl::start_with_next([=](int height) { + _postsWrap->resize(_postsWrap->width(), height); + }, _postsContent->lifetime()); + + rpl::combine( + rpl::single(rpl::empty) | rpl::then(_postsScroll->scrolls()), + _postsScroll->heightValue() + ) | rpl::start_with_next([=] { + updatePostsSearchVisibleRange(); + }, _postsContent->lifetime()); + + _postsContent->setLoadMoreCallback([=] { + _postsSearch->requestMore(); + }); + + _postsContent->setNarrowRatio(0.); + _postsContent->show(); + updateControlsGeometry(); +} + +void Suggestions::updatePostsSearchVisibleRange() { + Expects(_postsContent != nullptr); + + const auto top = _postsScroll->scrollTop(); + const auto height = _postsScroll->height(); + _postsContent->setVisibleTopBottom(top, top + height); +} + +void Suggestions::setupPostsIntro(const PostsSearchIntroState &intro) { + Expects(!_postsSearchIntro); + + delete base::take(_postsContent); + _postsSearchIntro = Ui::CreateChild(_postsWrap, intro); + + _postsSearchIntro->searchWithStars( + ) | rpl::start_with_next([=](int stars) { + if (!_controller->session().premium()) { + Settings::ShowPremium( + _controller, + u"posts_search"_q); + } else if (!stars) { + _postsSearch->setAllowedStars(0); + } else { + using namespace Settings; + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Success + || result == Settings::SmallBalanceResult::Already) { + const auto spent = _postsSearch->setAllowedStars(stars); + if (spent > 0) { + _controller->showToast({ + .text = tr::lng_posts_paid_spent( + tr::now, + lt_count, + spent, + Ui::Text::RichLangValue), + .attach = RectPart::Top, + .duration = Ui::Toast::kDefaultDuration * 2, + }); + } + } + }; + MaybeRequestBalanceIncrease( + _controller->uiShow(), + stars, + SmallBalanceForSearch{}, + done); + } + }, _postsSearchIntro->lifetime()); + + _postsScroll->heightValue() | rpl::start_with_next([=](int height) { + _postsWrap->resize(_postsWrap->width(), height); + }, _postsSearchIntro->lifetime()); + + _postsSearchIntro->show(); + updateControlsGeometry(); +} + void Suggestions::applySearchQuery() { const auto key = _key.current(); const auto controller = _mediaLists[key].wrap->controller(); @@ -1926,7 +2124,10 @@ void Suggestions::switchTab(Key key) { } void Suggestions::ensureContent(Key key) { - if (key.tab != Tab::Downloads && key.tab != Tab::Media) { + if (key.tab == Tab::Posts) { + setPostsSearchQuery(QString()); + return; + } else if (key.tab != Tab::Downloads && key.tab != Tab::Media) { return; } auto &list = _mediaLists[key]; @@ -1958,6 +2159,7 @@ void Suggestions::startSlideAnimation(Key was, Key now) { case Tab::Chats: return _chatsScroll.get(); case Tab::Channels: return _channelsScroll.get(); case Tab::Apps: return _appsScroll.get(); + case Tab::Posts: return _postsScroll.get(); } return _mediaLists[key].wrap; }; @@ -2009,6 +2211,7 @@ void Suggestions::startShownAnimation(bool shown, Fn finish) { _chatsScroll->hide(); _channelsScroll->hide(); _appsScroll->hide(); + _postsScroll->hide(); for (const auto &[key, list] : _mediaLists) { list.wrap->hide(); } @@ -2028,6 +2231,7 @@ void Suggestions::finishShow() { _chatsScroll->setVisible(key == Key{ Tab::Chats }); _channelsScroll->setVisible(key == Key{ Tab::Channels }); _appsScroll->setVisible(key == Key{ Tab::Apps }); + _postsScroll->setVisible(key == Key{ Tab::Posts }); for (const auto &[mediaKey, list] : _mediaLists) { list.wrap->setVisible(key == mediaKey); if (key == mediaKey) { @@ -2042,6 +2246,8 @@ void Suggestions::finishShow() { reinstallSwipe(_channelsScroll.get()); } else if (key == Key{ Tab::Apps }) { reinstallSwipe(_appsScroll.get()); + } else if (key == Key{ Tab::Posts }) { + reinstallSwipe(_postsScroll.get()); } } @@ -2055,6 +2261,7 @@ std::vector Suggestions::TabKeysFor( { Tab::Chats }, { Tab::Channels }, { Tab::Apps }, + { Tab::Posts }, { Tab::Media, MediaType::Photo }, { Tab::Media, MediaType::Video }, { Tab::Downloads }, @@ -2119,6 +2326,16 @@ void Suggestions::updateControlsGeometry() { _appsScroll->setGeometry(content); _appsContent->resizeToWidth(w); + _postsScroll->setGeometry(content); + _postsWrap->resizeToWidth(w); + if (_postsSearchIntro) { + _postsSearchIntro->setGeometry(0, 0, w, height() - tabs); + } else if (_postsContent) { + _postsContent->resizeToWidth(w); + _postsContent->setMinimumHeight(height() - tabs); + _postsContent->refresh(); + } + const auto expanding = false; for (const auto &[key, list] : _mediaLists) { const auto full = !list.wrap->scrollBottomSkip(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h index f269085f13..dbe085eacb 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -32,10 +32,11 @@ namespace Storage { enum class SharedMediaType : signed char; } // namespace Storage -namespace Ui { -namespace Controls { +namespace Ui::Controls { struct SwipeHandlerArgs; -} // namespace Controls +} // namespace Ui::Controls + +namespace Ui { class BoxContent; class ScrollArea; class ElasticScroll; @@ -51,6 +52,10 @@ class SessionController; namespace Dialogs { +class InnerWidget; +class PostsSearch; +class PostsSearchIntro; +struct PostsSearchIntroState; enum class SearchEmptyIcon; struct RecentPeersList { @@ -118,6 +123,7 @@ private: Chats, Channels, Apps, + Posts, Media, Downloads, }; @@ -209,6 +215,12 @@ private: void updateControlsGeometry(); void applySearchQuery(); + void setupPostsSearch(); + void setPostsSearchQuery(const QString &query); + void setupPostsResults(); + void setupPostsIntro(const PostsSearchIntroState &intro); + void updatePostsSearchVisibleRange(); + const not_null _controller; const std::unique_ptr _tabsScroll; @@ -240,6 +252,12 @@ private: const std::unique_ptr _appsScroll; const not_null _appsContent; + std::unique_ptr _postsSearch; + const std::unique_ptr _postsScroll; + const not_null _postsWrap; + PostsSearchIntro *_postsSearchIntro = nullptr; + InnerWidget *_postsContent = nullptr; + rpl::producer<> _recentAppsRefreshed; Fn)> _recentAppsShows; const std::unique_ptr _recentApps; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp index 40d70b2bc2..6e53d71a0d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp @@ -6,16 +6,155 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_top_bar_suggestion_content.h" + +#include "base/call_delayed.h" +#include "data/data_authorization.h" +#include "lang/lang_keys.h" +#include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_custom_emoji.h" #include "ui/ui_rpl_filter.h" +#include "ui/vertical_list.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_settings.h" +#include "styles/style_layers.h" namespace Dialogs { +class UnconfirmedAuthWrap : public Ui::SlideWrap { +public: + UnconfirmedAuthWrap( + not_null parent, + object_ptr &&child) + : Ui::SlideWrap(parent, std::move(child)) { + } + + rpl::producer desiredHeightValue() const override { + return entity()->heightValue(); + } +}; + +not_null*> CreateUnconfirmedAuthContent( + not_null parent, + const std::vector &list, + Fn callback) { + const auto wrap = Ui::CreateChild( + parent, + object_ptr(parent)); + const auto content = wrap->entity(); + content->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(content); + p.fillRect(content->rect(), st::dialogsBg); + }, content->lifetime()); + + const auto padding = st::dialogsUnconfirmedAuthPadding; + + Ui::AddSkip(content); + + content->add( + object_ptr( + content, + tr::lng_unconfirmed_auth_title(), + st::dialogsUnconfirmedAuthTitle), + padding, + style::al_top); + + Ui::AddSkip(content); + + auto messageText = QString(); + if (list.size() == 1) { + const auto &auth = list.at(0); + messageText = tr::lng_unconfirmed_auth_single( + tr::now, + lt_from, + auth.device, + lt_country, + auth.location); + } else { + auto commonLocation = list.at(0).location; + for (auto i = 1; i < list.size(); ++i) { + if (commonLocation != list.at(i).location) { + commonLocation.clear(); + break; + } + } + if (commonLocation.isEmpty()) { + messageText = tr::lng_unconfirmed_auth_multiple( + tr::now, + lt_count, + list.size()); + } else { + messageText = tr::lng_unconfirmed_auth_multiple_from( + tr::now, + lt_count, + list.size(), + lt_country, + commonLocation); + } + } + + content->add( + object_ptr( + content, + rpl::single(messageText), + st::dialogsUnconfirmedAuthAbout), + padding, + style::al_top)->setTryMakeSimilarLines(true); + + Ui::AddSkip(content); + const auto buttons = content->add(object_ptr( + content, + st::dialogsUnconfirmedAuthButton.height)); + const auto yes = Ui::CreateChild( + buttons, + tr::lng_unconfirmed_auth_confirm(), + st::dialogsUnconfirmedAuthButton); + const auto no = Ui::CreateChild( + buttons, + tr::lng_unconfirmed_auth_deny(), + st::dialogsUnconfirmedAuthButtonNo); + yes->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + yes->setClickedCallback([=] { + wrap->toggle(false, anim::type::normal); + base::call_delayed(st::universalDuration, wrap, [=] { + callback(true); + }); + }); + no->setClickedCallback([=] { + wrap->toggle(false, anim::type::normal); + base::call_delayed(st::universalDuration, wrap, [=] { + callback(false); + }); + }); + buttons->sizeValue( + ) | rpl::filter_size( + ) | rpl::start_with_next([=](const QSize &s) { + const auto halfWidth = (s.width() - rect::m::sum::h(padding)) / 2; + yes->moveToLeft( + padding.left() + (halfWidth - yes->width()) / 2, + 0); + no->moveToLeft( + padding.left() + halfWidth + (halfWidth - no->width()) / 2, + 0); + }, buttons->lifetime()); + Ui::AddSkip(content); + content->add(object_ptr(content)); + + return wrap; +} + TopBarSuggestionContent::TopBarSuggestionContent(not_null p) : Ui::RippleButton(p, st::defaultRippleAnimationBgOver) , _titleSt(st::semiboldTextStyle) @@ -66,9 +205,14 @@ void TopBarSuggestionContent::draw(QPainter &p) { const auto r = Ui::RpWidget::rect(); p.fillRect(r, st::historyPinnedBg); + p.fillRect( + r.x(), + r.y() + r.height() - st::lineWidth, + r.width(), + st::lineWidth, + st::shadowFg); Ui::RippleButton::paintRipple(p, 0, 0); - const auto leftPadding = st::defaultDialogRow.padding.left() - + _leftPadding; + const auto leftPadding = _leftPadding; const auto rightPadding = 0; const auto topPadding = st::msgReplyPadding.top(); const auto availableWidthNoPhoto = r.width() @@ -189,9 +333,11 @@ void TopBarSuggestionContent::setHideCallback(Fn hideCallback) { _rightHide->setClickedCallback(std::move(hideCallback)); } -void TopBarSuggestionContent::setLeftPadding(int value) { - _leftPadding = value; - update(); +void TopBarSuggestionContent::setLeftPadding(rpl::producer value) { + std::move(value) | rpl::start_with_next([=](int padding) { + _leftPadding = padding; + update(); + }, lifetime()); } const style::TextStyle & TopBarSuggestionContent::contentTitleSt() const { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h index 8297e37074..432f29fcc2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h @@ -12,14 +12,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class DynamicImage; class IconButton; +class VerticalLayout; +template +class SlideWrap; } // namespace Ui namespace Ui::Text { struct MarkedContext; } // namespace Ui::Text +namespace Data { +struct UnreviewedAuth; +} // namespace Data + namespace Dialogs { +not_null*> CreateUnconfirmedAuthContent( + not_null parent, + const std::vector &list, + Fn callback); + class TopBarSuggestionContent : public Ui::RippleButton { public: enum class RightIcon { @@ -39,7 +51,7 @@ public: void setHideCallback(Fn); void setRightIcon(RightIcon); - void setLeftPadding(int); + void setLeftPadding(rpl::producer); [[nodiscard]] const style::TextStyle &contentTitleSt() const; diff --git a/Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp b/Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp new file mode 100644 index 0000000000..87bda8fe71 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp @@ -0,0 +1,253 @@ +/* +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 "dialogs/ui/posts_search_intro.h" + +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "lang/lang_keys.h" +#include "ui/controls/button_labels.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_credits.h" +#include "styles/style_dialogs.h" + +namespace Dialogs { +namespace { + +[[nodiscard]] rpl::producer FormatCountdownTill(TimeId when) { + return rpl::single(rpl::empty) | rpl::then( + base::timer_each(1000) + ) | rpl::map([=] { + const auto now = base::unixtime::now(); + const auto delta = std::max(when - now, 0); + const auto hours = delta / 3600; + const auto minutes = (delta % 3600) / 60; + const auto seconds = delta % 60; + constexpr auto kZero = QChar('0'); + return (hours > 0) + ? u"%1:%2:%3"_q + .arg(hours) + .arg(minutes, 2, 10, kZero) + .arg(seconds, 2, 10, kZero) + : u"%1:%2"_q + .arg(minutes) + .arg(seconds, 2, 10, kZero); + }); +} + +void SetSearchButtonLabel( + not_null button, + rpl::producer text) { + const auto left = &st::postsSearchIcon; + const auto leftPadding = st::postsSearchIconPadding; + const auto right = &st::postsSearchArrow; + const auto rightPadding = st::postsSearchArrowPadding; + const auto leftSkip = left->size().grownBy(leftPadding).width(); + const auto rightSkip = right->size().grownBy(rightPadding).width(); + + struct State { + State() : linkFg([] { + auto copy = st::windowFgActive->c; + copy.setAlphaF(0.6); + return copy; + }), st(st::resaleButtonTitle) { + } + + style::complex_color linkFg; + style::FlatLabel st; + }; + auto lifetime = rpl::lifetime(); + const auto state = lifetime.make_state(); + state->st.palette.linkFg = state->linkFg.color(); + + const auto label = Ui::CreateChild( + button, + rpl::duplicate(text), + state->st); + label->lifetime().add(std::move(lifetime)); + label->show(); + + const auto icons = Ui::CreateChild(button); + icons->show(); + rpl::combine( + button->sizeValue(), + std::move(text) + ) | rpl::start_with_next([=](QSize size, const auto &) { + icons->setGeometry(QRect(QPoint(), size)); + const auto available = size.width() - leftSkip - rightSkip; + if (available <= 0) { + return; + } + const auto width = std::min(available, label->textMaxWidth()); + label->resizeToWidth(width); + const auto full = leftSkip + width + rightSkip; + const auto x = (size.width() - full) / 2; + const auto y = (size.height() - label->height()) / 2; + label->moveToLeft(x + leftSkip, y, size.width()); + }, icons->lifetime()); + + icons->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icons); + left->paint( + p, + label->x() - leftSkip + leftPadding.left(), + label->y() + leftPadding.top(), + icons->width()); + right->paint( + p, + label->x() + label->width() + rightPadding.left(), + label->y() + rightPadding.top(), + icons->width()); + }, icons->lifetime()); +} + +} // namespace + +PostsSearchIntro::PostsSearchIntro( + not_null parent, + PostsSearchIntroState state) +: RpWidget(parent) +, _state(std::move(state)) +, _content(std::make_unique(this)) { + setup(); +} + +PostsSearchIntro::~PostsSearchIntro() = default; + +void PostsSearchIntro::update(PostsSearchIntroState state) { + _state = std::move(state); +} + +rpl::producer PostsSearchIntro::searchWithStars() const { + return _button->clicks() | rpl::map([=] { + const auto &now = _state.current(); + return (now.needsPremium || now.freeSearchesLeft) + ? 0 + : int(now.starsPerPaidSearch); + }); +} + +void PostsSearchIntro::setup() { + auto title = _state.value( + ) | rpl::map([](const PostsSearchIntroState &state) { + return (state.needsPremium || state.freeSearchesLeft > 0) + ? tr::lng_posts_title() + : tr::lng_posts_limit_reached(); + }) | rpl::flatten_latest(); + + auto subtitle = _state.value( + ) | rpl::map([](const PostsSearchIntroState &state) { + return (state.needsPremium || state.freeSearchesLeft > 0) + ? tr::lng_posts_start() + : tr::lng_posts_limit_about( + lt_count, + rpl::single(state.freeSearchesPerDay * 1.)); + }) | rpl::flatten_latest(); + + auto footer = _state.value( + ) | rpl::map([](const PostsSearchIntroState &state) + -> rpl::producer { + if (state.needsPremium) { + return tr::lng_posts_need_subscribe(); + } else if (state.freeSearchesLeft > 0) { + return tr::lng_posts_remaining( + lt_count, + rpl::single(state.freeSearchesLeft * 1.)); + } else { + return rpl::single(QString()); + } + }) | rpl::flatten_latest(); + + _title = _content->add( + object_ptr( + _content.get(), + std::move(title), + st::postsSearchIntroTitle), + st::postsSearchIntroTitleMargin, + style::al_top); + _title->setTryMakeSimilarLines(true); + _subtitle = _content->add( + object_ptr( + _content.get(), + std::move(subtitle), + st::postsSearchIntroSubtitle), + st::postsSearchIntroSubtitleMargin, + style::al_top); + _subtitle->setTryMakeSimilarLines(true); + _button = _content->add( + object_ptr( + _content.get(), + rpl::single(QString()), + st::postsSearchIntroButton), + style::al_top); + _button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + _footer = _content->add( + object_ptr( + _content.get(), + std::move(footer), + st::postsSearchIntroFooter), + st::postsSearchIntroFooterMargin, + style::al_top); + _footer->setTryMakeSimilarLines(true); + + _state.value( + ) | rpl::start_with_next([=](const PostsSearchIntroState &state) { + if (state.query.trimmed().isEmpty() && !state.needsPremium) { + _button->resize(_button->width(), 0); + _content->resizeToWidth(width()); + return; + } + + auto copy = _button->children(); + for (const auto child : copy) { + delete child; + } + if (state.needsPremium) { + _button->setText(tr::lng_posts_subscribe()); + } else if (state.freeSearchesLeft > 0) { + _button->setText(rpl::single(QString())); + + SetSearchButtonLabel(_button, tr::lng_posts_search_button( + lt_query, + rpl::single(Ui::Text::Colorized(state.query.trimmed())), + Ui::Text::WithEntities)); + } else { + _button->setText(rpl::single(QString())); + + Ui::SetButtonTwoLabels( + _button, + tr::lng_posts_limit_search_paid( + lt_cost, + rpl::single(Ui::Text::IconEmoji( + &st::starIconEmoji + ).append( + Lang::FormatCountDecimal(state.starsPerPaidSearch))), + Ui::Text::WithEntities), + tr::lng_posts_limit_unlocks( + lt_duration, + FormatCountdownTill( + state.nextFreeSearchTime + ) | Ui::Text::ToWithEntities(), + Ui::Text::WithEntities), + st::resaleButtonTitle, + st::resaleButtonSubtitle); + } + _button->resize(_button->width(), st::postsSearchIntroButton.height); + _content->resizeToWidth(width()); + }, _button->lifetime()); +} + +void PostsSearchIntro::resizeEvent(QResizeEvent *e) { + _content->resizeToWidth(width()); + const auto top = std::max(0, (height() - _content->height()) / 3); + _content->move(0, top); +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/posts_search_intro.h b/Telegram/SourceFiles/dialogs/ui/posts_search_intro.h new file mode 100644 index 0000000000..cebd6e512a --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/posts_search_intro.h @@ -0,0 +1,59 @@ +/* +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/rp_widget.h" + +namespace Ui { +class FlatLabel; +class RoundButton; +class VerticalLayout; +} // namespace Ui + +namespace Dialogs { + +struct PostsSearchIntroState { + QString query; + int freeSearchesPerDay = 0; + int freeSearchesLeft = 0; + TimeId nextFreeSearchTime = 0; + uint32 starsPerPaidSearch : 31 = 0; + uint32 needsPremium : 1 = 0; + + friend inline bool operator==( + PostsSearchIntroState, + PostsSearchIntroState) = default; +}; + +class PostsSearchIntro final : public Ui::RpWidget { +public: + PostsSearchIntro( + not_null parent, + PostsSearchIntroState state); + ~PostsSearchIntro(); + + void update(PostsSearchIntroState state); + + [[nodiscard]] rpl::producer searchWithStars() const; + +private: + void resizeEvent(QResizeEvent *e) override; + + void setup(); + + rpl::variable _state; + + std::unique_ptr _content; + Ui::FlatLabel *_title = nullptr; + Ui::FlatLabel *_subtitle = nullptr; + Ui::RoundButton *_button = nullptr; + Ui::FlatLabel *_footer = nullptr; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/editor/editor_layer_widget.cpp b/Telegram/SourceFiles/editor/editor_layer_widget.cpp index baa149d5a1..fc2096fa37 100644 --- a/Telegram/SourceFiles/editor/editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/editor_layer_widget.cpp @@ -157,12 +157,12 @@ void LayerWidget::checkCacheBackground() { void LayerWidget::cacheBackground() { _backgroundCaching = true; - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto night = IsNightMode(); crl::async([weak, night, image = renderBackground()]() mutable { auto result = ProcessBackground(image, night); crl::on_main([weak, night, result = std::move(result)]() mutable { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->backgroundReady(std::move(result), night); } }); diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index 801e122582..0ec6de90b4 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -228,12 +228,12 @@ void InitEditorLayer( layer->closeLayer(); }, editor->lifetime()); - const auto weak = Ui::MakeWeak(layer.get()); + const auto weak = base::make_weak(layer.get()); editor->doneRequests( ) | rpl::start_with_next([=, done = std::move(doneCallback)]( const PhotoModifications &mods) { done(mods); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeLayer(); } }, editor->lifetime()); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 68e8d37cbe..bd16f6d86f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1608,9 +1608,13 @@ ServiceAction ParseServiceAction( .date = data.vschedule_date().v, }; }, [&](const MTPDmessageActionSetChatTheme &data) { - result.content = ActionSetChatTheme{ - .emoji = qs(data.vemoticon()), - }; + data.vtheme().match([&](const MTPDchatTheme &data) { + result.content = ActionSetChatTheme{ + .emoji = qs(data.vemoticon()), + }; + }, [&](const MTPDchatThemeUniqueGift &data) { + result.content = ActionSetChatTheme{}; + }); }, [&](const MTPDmessageActionChatJoinedByRequest &data) { result.content = ActionChatJoinedByRequest(); }, [&](const MTPDmessageActionWebViewDataSentMe &data) { diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index b25c130b7e..e2a3f2956f 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -404,7 +404,9 @@ auto ApiWrap::fileRequest(const Data::FileLocation &location, int64 offset) { }).toDC(MTP::ShiftDcId(location.dcId, MTP::kExportMediaDcShift))); } -ApiWrap::ApiWrap(QPointer weak, Fn)> runner) +ApiWrap::ApiWrap( + base::weak_qptr weak, + Fn)> runner) : _mtp(weak, std::move(runner)) , _fileCache(std::make_unique(kLocationCacheSize)) { } @@ -2066,7 +2068,7 @@ bool ApiWrap::processFileLoad( } else if (!story && (_settings->media.types & type) != type) { file.skipReason = SkipReason::FileType; return true; - } else if (!story && fullSize >= _settings->media.sizeLimit) { + } else if (!story && fullSize > _settings->media.sizeLimit) { // Don't load thumbs for large files that we skip. file.skipReason = SkipReason::FileSize; return true; diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index 6073243152..de76417bc7 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -40,7 +40,9 @@ struct Settings; class ApiWrap { public: - ApiWrap(QPointer weak, Fn)> runner); + ApiWrap( + base::weak_qptr weak, + Fn)> runner); rpl::producer errors() const; rpl::producer ioErrors() const; diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index 4aef9c36e6..d65be8f077 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -96,11 +96,11 @@ Environment PrepareEnvironment(not_null session) { return result; } -QPointer SuggestStart(not_null session) { +base::weak_qptr SuggestStart(not_null session) { ClearSuggestStart(session); return Ui::show( Box(session), - Ui::LayerOption::KeepOther).data(); + Ui::LayerOption::KeepOther).get(); } void ClearSuggestStart(not_null session) { @@ -277,7 +277,7 @@ void PanelController::showCriticalError(const QString &text) { void PanelController::showError(const QString &text) { auto box = Ui::MakeInformBox(text); - const auto weak = Ui::MakeWeak(box.data()); + const auto weak = base::make_weak(box.data()); const auto hidden = _panel->isHidden(); _panel->showBox( std::move(box), diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.h b/Telegram/SourceFiles/export/view/export_view_panel_controller.h index 70c72f8a69..b820202c3d 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.h +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.h @@ -24,7 +24,7 @@ class Session; namespace Export { namespace View { -QPointer SuggestStart(not_null session); +base::weak_qptr SuggestStart(not_null session); void ClearSuggestStart(not_null session); bool IsDefaultPath(not_null session, const QString &path); void ResolveSettings(not_null session, Settings &settings); @@ -79,7 +79,7 @@ private: base::unique_qptr _panel; State _state; - QPointer _confirmStopBox; + base::weak_qptr _confirmStopBox; rpl::event_stream> _panelCloseEvents; bool _stopRequested = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/export/view/export_view_progress.cpp b/Telegram/SourceFiles/export/view/export_view_progress.cpp index dabda8280d..c7d28c7d47 100644 --- a/Telegram/SourceFiles/export/view/export_view_progress.cpp +++ b/Telegram/SourceFiles/export/view/export_view_progress.cpp @@ -51,8 +51,8 @@ private: void hideCurrentInstance(); void setInstanceProgress(Instance &instance, float64 progress); void toggleInstance(Instance &data, bool shown); - void instanceOpacityCallback(QPointer label); - void removeOldInstance(QPointer label); + void instanceOpacityCallback(base::weak_qptr label); + void removeOldInstance(base::weak_qptr label); void paintInstance(QPainter &p, const Instance &data); void updateControlsGeometry(int newWidth); @@ -146,7 +146,7 @@ void ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) { if (instance.hiding != shown) { return; } - const auto label = Ui::MakeWeak(instance.label->entity()); + const auto label = base::make_weak(instance.label->entity()); instance.opacity.start( [=] { instanceOpacityCallback(label); }, shown ? 0. : 1., @@ -158,10 +158,10 @@ void ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) { } void ProgressWidget::Row::instanceOpacityCallback( - QPointer label) { + base::weak_qptr label) { update(); const auto i = ranges::find(_old, label, [](const Instance &instance) { - return Ui::MakeWeak(instance.label->entity()); + return base::make_weak(instance.label->entity()); }); if (i != end(_old) && i->hiding && !i->opacity.animating()) { crl::on_main(this, [=] { @@ -170,9 +170,10 @@ void ProgressWidget::Row::instanceOpacityCallback( } } -void ProgressWidget::Row::removeOldInstance(QPointer label) { +void ProgressWidget::Row::removeOldInstance( + base::weak_qptr label) { const auto i = ranges::find(_old, label, [](const Instance &instance) { - return Ui::MakeWeak(instance.label->entity()); + return base::make_weak(instance.label->entity()); }); if (i != end(_old)) { _old.erase(i); diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index bc25b60394..e610471279 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -320,20 +320,20 @@ void SettingsWidget::addLocationLabel( } void SettingsWidget::chooseFormat() { - const auto shared = std::make_shared>(); + const auto shared = std::make_shared>(); const auto callback = [=](Format format) { changeData([&](Settings &data) { data.format = format; }); - if (const auto weak = shared->data()) { - weak->closeBox(); + if (const auto strong = shared->get()) { + strong->closeBox(); } }; auto box = Box( ChooseFormatBox, readData().format, callback); - *shared = Ui::MakeWeak(box.data()); + *shared = base::make_weak(box.data()); _showBoxCallback(std::move(box)); } @@ -602,18 +602,18 @@ void SettingsWidget::editDateLimit( ? base::unixtime::parse(min).date() : QDate::currentDate(); const auto month = highlighted; - const auto shared = std::make_shared>(); + const auto shared = std::make_shared>(); const auto finalize = [=](not_null box) { box->addLeftButton(std::move(resetLabel), crl::guard(this, [=] { done(0); - if (const auto weak = shared->data()) { + if (const auto weak = shared->get()) { weak->closeBox(); } })); }; const auto callback = crl::guard(this, [=](const QDate &date) { done(base::unixtime::serialize(date.startOfDay())); - if (const auto weak = shared->data()) { + if (const auto weak = shared->get()) { weak->closeBox(); } }); @@ -630,7 +630,7 @@ void SettingsWidget::editDateLimit( ? base::unixtime::parse(max).date() : QDate::currentDate()), }); - *shared = Ui::MakeWeak(box.data()); + *shared = base::make_weak(box.data()); _showBoxCallback(std::move(box)); } 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 babf26203a..d83a0b8a79 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1008,7 +1008,7 @@ void InnerWidget::itemsAdded(Direction direction, int addedCount) { } void InnerWidget::updateSize() { - TWidget::resizeToWidth(width()); + RpWidget::resizeToWidth(width()); restoreScrollPosition(); updateVisibleTopItem(); checkPreloadMore(); @@ -1530,7 +1530,7 @@ void InnerWidget::suggestRestrictParticipant( UserData *by, TimeId since) { auto weak = QPointer(this); - auto weakBox = std::make_shared>(); + auto weakBox = std::make_shared>(); auto box = Box( _channel, user, @@ -1560,7 +1560,7 @@ void InnerWidget::suggestRestrictParticipant( tr::now, lt_user, participant->name()); - auto weakBox = std::make_shared>(); + auto weakBox = std::make_shared>(); const auto sure = crl::guard(this, [=] { restrictParticipant( participant, @@ -1684,7 +1684,7 @@ void InnerWidget::mouseReleaseEvent(QMouseEvent *e) { void InnerWidget::enterEventHook(QEnterEvent *e) { mouseActionUpdate(QCursor::pos()); - return TWidget::enterEventHook(e); + return RpWidget::enterEventHook(e); } void InnerWidget::leaveEventHook(QEvent *e) { @@ -1698,7 +1698,7 @@ void InnerWidget::leaveEventHook(QEvent *e) { _cursor = style::cur_default; setCursor(_cursor); } - return TWidget::leaveEventHook(e); + return RpWidget::leaveEventHook(e); } void InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) { @@ -1810,10 +1810,8 @@ void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton but ActivateClickHandler(window(), activated, { button, QVariant::fromValue(ClickHandlerContext{ - .elementDelegate = [weak = Ui::MakeWeak(this)] { - return weak - ? (ElementDelegate*)weak - : nullptr; + .elementDelegate = [weak = base::make_weak(this)] { + return (ElementDelegate*)weak.get(); }, .sessionWindow = base::make_weak(_controller), }) 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 9f58afb921..bd537fe141 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -77,7 +77,7 @@ public: void resizeToWidth(int newWidth, int minHeight) { _minHeight = minHeight; - return TWidget::resizeToWidth(newWidth); + return RpWidget::resizeToWidth(newWidth); } void saveState(not_null memento); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp index 56fce9db19..b8f2177332 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -37,7 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace AdminLog { -class FixedBar final : public TWidget { +class FixedBar final : public Ui::RpWidget { public: FixedBar( QWidget *parent, @@ -109,7 +109,8 @@ object_ptr SectionMemento::createWidget( FixedBar::FixedBar( QWidget *parent, not_null controller, - not_null channel) : TWidget(parent) + not_null channel) +: RpWidget(parent) , _controller(controller) , _channel(channel) , _field(this, st::defaultMultiSelectSearchField, tr::lng_dlg_filter()) @@ -276,7 +277,7 @@ void FixedBar::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { goBack(); } else { - TWidget::mousePressEvent(e); + RpWidget::mousePressEvent(e); } } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index c974e44b0d..49cddaf528 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" +#include "data/data_cloud_themes.h" #include "data/data_drafts.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" @@ -33,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel_admins.h" #include "data/data_changes.h" #include "data/data_chat_filters.h" +#include "data/data_replies_list.h" #include "data/data_send_action.h" #include "data/data_star_gift.h" #include "data/data_emoji_statuses.h" @@ -1312,7 +1314,12 @@ void History::applyServiceChanges( } } }, [&](const MTPDmessageActionSetChatTheme &data) { - peer->setThemeEmoji(qs(data.vemoticon())); + data.vtheme().match([&](const MTPDchatTheme &data) { + peer->setThemeToken(qs(data.vemoticon())); + }, [&](const MTPDchatThemeUniqueGift &data) { + peer->setThemeToken( + owner().cloudThemes().processGiftThemeGetToken(data)); + }); }, [&](const MTPDmessageActionSetChatWallPaper &data) { if (item->out() || data.is_for_both()) { peer->setWallPaper( @@ -1993,13 +2000,17 @@ bool History::unreadCountKnown() const { return _unreadCount.has_value(); } +bool History::useMyUnreadInParent() const { + return !isForum() && !amMonoforumAdmin(); +} + void History::setUnreadCount(int newUnreadCount) { Expects(folderKnown()); if (_unreadCount == newUnreadCount) { return; } - const auto notifier = unreadStateChangeNotifier(!isForum()); + const auto notifier = unreadStateChangeNotifier(useMyUnreadInParent()); _unreadCount = newUnreadCount; const auto lastOutgoing = [&] { @@ -2038,7 +2049,7 @@ void History::setUnreadMark(bool unread) { return; } const auto notifier = unreadStateChangeNotifier( - !unreadCount() && !isForum()); + useMyUnreadInParent() && !unreadCount()); Thread::setUnreadMarkFlag(unread); } @@ -2070,9 +2081,9 @@ void History::setMuted(bool muted) { if (this->muted() == muted) { return; } else { - const auto state = isForum() - ? Dialogs::BadgesState() - : computeBadgesState(); + const auto state = useMyUnreadInParent() + ? computeBadgesState() + : Dialogs::BadgesState(); const auto notify = (state.unread || state.reaction); const auto notifier = unreadStateChangeNotifier(notify); Thread::setMuted(muted); @@ -3166,6 +3177,58 @@ void History::applyDialogTopMessage(MsgId topMessageId) { } } +void History::tryMarkForumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore) { + if (!isForum() + || !peer->asChannel()->useSubsectionTabs() + || (nowInboxReadBefore <= wasInboxReadBefore)) { + return; + } else if (loadedAtBottom() && nowInboxReadBefore >= minMsgId()) { + // Count for each sublist how many messages are still not read. + auto counts = base::flat_map, int>(); + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < nowInboxReadBefore) { + continue; + } + if (const auto topic = item->topic()) { + ++counts[topic]; + } + } + } + if (const auto forum = peer->forum()) { + forum->updateUnreadCounts(nowInboxReadBefore - 1, counts); + } + } else if (minMsgId() <= wasInboxReadBefore + && maxMsgId() >= nowInboxReadBefore) { + // Count for each sublist how many messages were read. + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < wasInboxReadBefore) { + continue; + } else if (item->id >= nowInboxReadBefore) { + break; + } + if (const auto topic = item->topic()) { + const auto replies = topic->replies(); + const auto unread = replies->unreadCountCurrent(); + if (unread > 0) { + replies->setInboxReadTill(item->id, unread - 1); + } + } + } + } + } else { + // We can't invalidate sublist unread counts here, because no read + // request was yet sent to the server (so it can't return correct + // values yet), we need to do that after we send read request. + _flags |= Flag::MonoAndForumUnreadInvalidatePending; + } +} + void History::tryMarkMonoforumIntervalRead( MsgId wasInboxReadBefore, MsgId nowInboxReadBefore) { @@ -3211,24 +3274,29 @@ void History::tryMarkMonoforumIntervalRead( // We can't invalidate sublist unread counts here, because no read // request was yet sent to the server (so it can't return correct // values yet), we need to do that after we send read request. - _flags |= Flag::MonoforumUnreadInvalidatePending; + _flags |= Flag::MonoAndForumUnreadInvalidatePending; } } -void History::validateMonoforumUnread(MsgId readTillId) { - if (!(_flags & Flag::MonoforumUnreadInvalidatePending)) { +void History::validateMonoAndForumUnread(MsgId readTillId) { + if (!(_flags & Flag::MonoAndForumUnreadInvalidatePending)) { return; } - _flags &= ~Flag::MonoforumUnreadInvalidatePending; - if (!amMonoforumAdmin()) { - return; - } else if (const auto monoforum = peer->monoforum()) { - monoforum->markUnreadCountsUnknown(readTillId); + _flags &= ~Flag::MonoAndForumUnreadInvalidatePending; + if (isForum()) { + if (const auto forum = peer->forum()) { + forum->markUnreadCountsUnknown(readTillId); + } + } else if (amMonoforumAdmin()) { + if (const auto monoforum = peer->monoforum()) { + monoforum->markUnreadCountsUnknown(readTillId); + } } } void History::setInboxReadTill(MsgId upTo) { if (_inboxReadBefore) { + tryMarkForumIntervalRead(*_inboxReadBefore, upTo + 1); tryMarkMonoforumIntervalRead(*_inboxReadBefore, upTo + 1); accumulate_max(*_inboxReadBefore, upTo + 1); } else { @@ -3416,6 +3484,26 @@ bool History::suggestDraftAllowed() const { return peer->isMonoforum() && !peer->amMonoforumAdmin(); } +bool History::hasForumThreadBars() const { + if (amMonoforumAdmin()) { + return true; + } else if (const auto channel = peer->asChannel()) { + return channel->forum() && channel->useSubsectionTabs(); + } + return false; +} + +void History::forumTabsChanged(bool forumTabs) { + for (auto &block : blocks) { + for (auto &view : block->messages) { + view->setPendingResize(); + if (forumTabs || view->Has()) { + view->previousInBlocksChanged(); + } + } + } +} + not_null History::migrateToOrMe() const { if (const auto to = peer->migrateTo()) { return owner().history(to); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 7f556bbc71..d93b8d0614 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -80,6 +80,8 @@ public: void monoforumChanged(Data::SavedMessages *old); [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] bool suggestDraftAllowed() const; + [[nodiscard]] bool hasForumThreadBars() const; + void forumTabsChanged(bool forumTabs); [[nodiscard]] not_null migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; @@ -434,7 +436,10 @@ public: void tryMarkMonoforumIntervalRead( MsgId wasInboxReadBefore, MsgId nowInboxReadBefore); - void validateMonoforumUnread(MsgId readTillId); + void tryMarkForumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore); + void validateMonoAndForumUnread(MsgId readTillId); [[nodiscard]] bool isTopPromoted() const; @@ -480,7 +485,7 @@ private: FakeUnreadWhileOpened = (1 << 5), HasPinnedMessages = (1 << 6), ResolveChatListMessage = (1 << 7), - MonoforumUnreadInvalidatePending = (1 << 8), + MonoAndForumUnreadInvalidatePending = (1 << 8), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { @@ -608,6 +613,7 @@ private: void hasUnreadMentionChanged(bool has) override; void hasUnreadReactionChanged(bool has) override; + [[nodiscard]] bool useMyUnreadInParent() const; const std::unique_ptr _delegateMixin; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 610f6d3f1e..dbb53819f1 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" #include "history/view/controls/history_view_suggest_options.h" +#include "history/view/media/history_view_save_document_action.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" @@ -73,6 +74,8 @@ 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_item_rate_transcribe.h" +#include "menu/menu_item_rate_transcribe_session.h" #include "menu/menu_sponsored.h" #include "core/application.h" #include "apiwrap.h" @@ -84,6 +87,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_document.h" @@ -980,8 +984,8 @@ void HistoryInner::enumerateDates(Method method) { } template -void HistoryInner::enumerateMonoforumSenders(Method method) { - if (!_history->amMonoforumAdmin()) { +void HistoryInner::enumerateForumThreadBars(Method method) { + if (!_history->hasForumThreadBars()) { return; } @@ -998,28 +1002,28 @@ void HistoryInner::enumerateMonoforumSenders(Method method) { // -1 means we didn't find a same-day with previous message yet. auto lowestInOneBunchItemBottom = -1; - auto senderCallback = [&](not_null view, int itemtop, int itembottom) { + auto barCallback = [&](not_null view, int itemtop, int itembottom) { const auto item = view->data(); if (lowestInOneBunchItemBottom < 0 && view->isInOneBunchWithPrevious()) { lowestInOneBunchItemBottom = itembottom - view->marginBottom(); } - // Call method on a sender for all messages that have it and for those who are not showing it + // Call method on a bar for all messages that have it and for those who are not showing it // because they are in a one day together with the previous message if they are top-most visible. - if (view->displayMonoforumSender() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) { + if (view->displayForumThreadBar() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) { if (lowestInOneBunchItemBottom < 0) { lowestInOneBunchItemBottom = itembottom - view->marginBottom(); } - // Attach sender to the top of the visible area with the same margin as it has in service message. - int senderTop = qMax(itemtop + view->displayedDateHeight(), _visibleAreaTop + skip) + st::msgServiceMargin.top(); + // Attach bar to the top of the visible area with the same margin as it has in service message. + int barTop = qMax(itemtop + view->displayedDateHeight(), _visibleAreaTop + skip) + st::msgServiceMargin.top(); - // Do not let the sender go below the single-sender messages pack bottom line. - int senderHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); - senderTop = qMin(senderTop, lowestInOneBunchItemBottom - senderHeight); + // Do not let the bar go below the single-bar messages pack bottom line. + int barHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); + barTop = qMin(barTop, lowestInOneBunchItemBottom - barHeight); // Call the template callback function that was passed // and return if it finished everything it needed. - if (!method(view, itemtop, senderTop)) { + if (!method(view, itemtop, barTop)) { return false; } } @@ -1032,7 +1036,7 @@ void HistoryInner::enumerateMonoforumSenders(Method method) { return true; }; - enumerateItems(senderCallback); + enumerateItems(barCallback); } TextSelection HistoryInner::computeRenderSelection( @@ -1455,31 +1459,31 @@ void HistoryInner::paintEvent(QPaintEvent *e) { }); p.setOpacity(1.); - enumerateMonoforumSenders([&](not_null view, int itemtop, int senderTop) { - // stop the enumeration if the sender is above the painted rect - if (senderTop + dateHeight <= clip.top()) { + enumerateForumThreadBars([&](not_null view, int itemtop, int barTop) { + // stop the enumeration if the bar is above the painted rect + if (barTop + dateHeight <= clip.top()) { return false; } - const auto displaySender = view->displayMonoforumSender(); - auto senderInPlace = displaySender; - if (senderInPlace) { - const auto correctSenderTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top(); - senderInPlace = (senderTop < correctSenderTop + st::msgServiceMargin.top()); + const auto displayBar = view->displayForumThreadBar(); + auto barInPlace = displayBar; + if (barInPlace) { + const auto correctBarTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top(); + barInPlace = (barTop < correctBarTop + st::msgServiceMargin.top()); } - // paint the sender if it intersects the painted rect - if (senderTop < clip.top() + clip.height()) { - const auto senderY = senderTop - st::msgServiceMargin.top(); - if (const auto sender = view->Get()) { - sender->paint(p, context.st, senderY, _contentWidth, _isChatWide, !senderInPlace); + // paint the bar if it intersects the painted rect + if (barTop < clip.top() + clip.height()) { + const auto barY = barTop - st::msgServiceMargin.top(); + if (const auto bar = view->Get()) { + bar->paint(p, context.st, barY, _contentWidth, _isChatWide, !barInPlace); } else { - HistoryView::MonoforumSenderBar::PaintFor( + _forumThreadBarWidth = HistoryView::ForumThreadBar::PaintForGetWidth( p, context.st, view, - _monoforumSenderUserpicView, - senderY, + _forumThreadBarUserpicView, + barY, _contentWidth, _isChatWide); } @@ -1733,7 +1737,7 @@ void HistoryInner::touchEvent(QTouchEvent *e) { _touchInProgress = false; const auto notMoved = (_touchPos - _touchStart).manhattanLength() < QApplication::startDragDistance(); - auto weak = Ui::MakeWeak(this); + auto weak = base::make_weak(this); if (_touchSelect) { if (notMoved || _touchMaybeSelecting.current()) { mouseActionFinish(_touchPos, Qt::RightButton); @@ -2574,6 +2578,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }, &st::menuIconStickers); } }; + auto rateTranscriptionItem = (HistoryItem*)(nullptr); const auto addDocumentActions = [&](not_null document, HistoryItem *item) { if (document->loading()) { _menu->addAction(tr::lng_context_cancel_download(tr::now), [=] { @@ -2582,9 +2587,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { return; } const auto itemId = item ? item->fullId() : FullMsgId(); - const auto lnkIsVideo = document->isVideoFile(); - const auto lnkIsVoice = document->isVoiceMessage(); - const auto lnkIsAudio = document->isAudioFile(); if (document->isGifv()) { const auto notAutoplayedGif = [&] { return item @@ -2617,10 +2619,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { item, document, controller); - _menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), base::fn_delayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] { - saveDocumentToFile(itemId, document); - }), &st::menuIconDownload); - + HistoryView::AddSaveDocumentAction( + Ui::Menu::CreateAddActionCallback(_menu), + item, + document, + controller); HistoryView::AddCopyFilename( _menu, document, @@ -2633,6 +2636,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { document); }, &st::menuIconStickers); } + if (item + && (document->isVoiceMessage() || document->isVideoMessage()) + && Menu::HasRateTranscribeItem(item)) { + rateTranscriptionItem = item; + } }; #ifdef _DEBUG // Sometimes we need to save emoji to files. @@ -3050,6 +3058,12 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { &st::menuIconGiftPremium); } } + } else if (!rateTranscriptionItem && media->document()) { + if ((media->document()->isVoiceMessage() + || media->document()->isVideoMessage()) + && Menu::HasRateTranscribeItem(item)) { + rateTranscriptionItem = item; + } } } if (!item->isService() && view && actionText.isEmpty()) { @@ -3217,6 +3231,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _controller); } + if (!_menu->empty() && rateTranscriptionItem) { + _menu->insertAction(0, base::make_unique_q( + _menu, + _menu->st().menu, + Menu::RateTranscribeCallbackFactory(rateTranscriptionItem))); + } + if (_menu->empty()) { _menu = nullptr; return; @@ -3755,7 +3776,7 @@ void HistoryInner::toggleScrollDateShown() { void HistoryInner::repaintScrollDateCallback() { int updateTop = _visibleAreaTop; int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); - if (_history->amMonoforumAdmin()) { + if (_history->hasForumThreadBars()) { updateHeight *= 2; } update(0, updateTop, width(), updateHeight); @@ -3810,7 +3831,7 @@ void HistoryInner::setShownPinned(HistoryItem *item) { void HistoryInner::enterEventHook(QEnterEvent *e) { _mouseActive = true; mouseActionUpdate(QCursor::pos()); - return TWidget::enterEventHook(e); + return RpWidget::enterEventHook(e); } void HistoryInner::leaveEventHook(QEvent *e) { @@ -3826,7 +3847,7 @@ void HistoryInner::leaveEventHook(QEvent *e) { setCursor(_cursor); } _mouseActive = false; - return TWidget::leaveEventHook(e); + return RpWidget::leaveEventHook(e); } HistoryInner::~HistoryInner() { @@ -3848,7 +3869,7 @@ HistoryInner::~HistoryInner() { bool HistoryInner::focusNextPrevChild(bool next) { if (_selected.empty()) { - return TWidget::focusNextPrevChild(next); + return RpWidget::focusNextPrevChild(next); } else { clearSelected(); return true; @@ -4318,6 +4339,54 @@ void HistoryInner::mouseActionUpdate() { } return true; }); + if (!dragState.link) { + enumerateForumThreadBars([&](not_null view, int itemtop, int barTop) { + // stop the enumeration if the bar is above our point + if (barTop + dateHeight <= point.y()) { + return false; + } + + const auto displayBar = view->displayForumThreadBar(); + auto barInPlace = displayBar; + if (barInPlace) { + const auto correctBarTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top(); + barInPlace = (barTop < correctBarTop + st::msgServiceMargin.top()); + } + + // stop enumeration if we've found a bar under the cursor + if (barTop <= point.y()) { + const auto item = view->data(); + auto barWidth = 0; + if (const auto bar = view->Get()) { + barWidth = bar->width; + } else { + barWidth = _forumThreadBarWidth; + } + auto barLeft = st::msgServiceMargin.left(); + auto maxwidth = _contentWidth; + if (_isChatWide) { + maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); + } + auto widthForBar = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); + + barLeft += (widthForBar - barWidth) / 2; + + if (point.x() >= barLeft && point.x() < barLeft + barWidth) { + if (!_forumThreadBarLink) { + _forumThreadBarLink = std::make_shared(item); + } else { + static_cast(_forumThreadBarLink.get())->update(item); + } + dragState = TextState( + nullptr, + _forumThreadBarLink); + _dragStateItem = session().data().message(dragState.itemId); + lnkhost = view; + } + } + return true; + }); + } if (!dragState.link) { StateRequest request; if (_mouseAction == MouseAction::Selecting) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 36b774e98b..569d97512a 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -315,13 +315,13 @@ private: template void enumerateDates(Method method); - // This function finds all monoforum sender elements that are displayed and calls template method + // This function finds all forum thread bar elements that are displayed and calls template method // for each found date element (from the bottom to the top) using enumerateItems() method. // // Method has "bool (*Method)(not_null view, int itemtop, int dateTop)" signature // if it returns false the enumeration stops immediately. template - void enumerateMonoforumSenders(Method method); + void enumerateForumThreadBars(Method method); void scrollDateCheck(); void scrollDateHideByTimer(); @@ -470,7 +470,8 @@ private: int _contentWidth = 0; int _historyPaddingTop = 0; int _revealHeight = 0; - Ui::PeerUserpicView _monoforumSenderUserpicView; + int _forumThreadBarWidth = 0; + Ui::PeerUserpicView _forumThreadBarUserpicView; // Save visible area coords for painting / pressing userpics. int _visibleAreaTop = 0; @@ -572,6 +573,7 @@ private: Element *_scrollDateLastItem = nullptr; int _scrollDateLastItemTop = 0; ClickHandlerPtr _scrollDateLink; + ClickHandlerPtr _forumThreadBarLink; bool _wasForceClickPreview = false; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 301ca58e8d..f3975b809e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -150,11 +150,10 @@ template } [[nodiscard]] TextWithEntities AmountAndStarCurrency( - not_null session, int64 amount, const QString ¤cy) { if (currency == Ui::kCreditsCurrency) { - return Ui::CreditsEmojiSmall(session).append( + return Ui::CreditsEmojiSmall().append( Lang::FormatCountDecimal(std::abs(amount))); } return { Ui::FillAmountAndCurrency(amount, currency) }; @@ -1698,6 +1697,17 @@ void HistoryItem::setIsPinned(bool pinned) { } } +void HistoryItem::setStoryInProfile(bool inProfile) { + if (storyInProfile() == inProfile) { + return; + } else if (inProfile) { + _flags |= MessageFlag::StoryInProfile; + } else { + _flags &= ~MessageFlag::StoryInProfile; + } + _history->owner().notifyItemDataChange(this); +} + void HistoryItem::returnSavedMedia() { if (!isEditingMedia()) { return; @@ -1889,6 +1899,10 @@ bool HistoryItem::isUserpicSuggestion() const { return (_flags & MessageFlag::IsUserpicSuggestion); } +bool HistoryItem::isSavedMusicItem() const { + return (_flags & MessageFlag::SavedMusicItem); +} + BusinessShortcutId HistoryItem::shortcutId() const { return _shortcutId; } @@ -2082,6 +2096,11 @@ void HistoryItem::setStoryFields(not_null story) { } else { _flags &= ~MessageFlag::Pinned; } + if (story->inProfile()) { + _flags |= MessageFlag::StoryInProfile; + } else { + _flags &= ~MessageFlag::StoryInProfile; + } } void HistoryItem::applyEdition(const MTPDmessageService &message) { @@ -4762,10 +4781,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { payment->recurringInit = data.is_recurring_init(); payment->recurringUsed = data.is_recurring_used(); payment->isCreditsCurrency = (currency == Ui::kCreditsCurrency); - payment->amount = AmountAndStarCurrency( - &_history->session(), - amount, - currency); + payment->amount = AmountAndStarCurrency(amount, currency); payment->invoiceLink = std::make_shared([=]( ClickContext context) { using namespace Payments; @@ -5197,10 +5213,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { : tr::lng_action_payment_bot_done)( tr::now, lt_amount, - AmountAndStarCurrency( - &_history->session(), - data.vtotal_amount().v, - qs(data.vcurrency())), + AmountAndStarCurrency(data.vtotal_amount().v, qs(data.vcurrency())), Ui::Text::WithEntities); return result; }; @@ -5513,38 +5526,63 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareSetChatTheme = [this](const MTPDmessageActionSetChatTheme &action) { auto result = PreparedServiceText(); - const auto text = qs(action.vemoticon()); - if (!text.isEmpty()) { - if (_from->isSelf()) { - result.text = tr::lng_action_you_theme_changed( - tr::now, - lt_emoji, - { .text = text }, - Ui::Text::WithEntities); + action.vtheme().match([&](const MTPDchatTheme &data) { + const auto text = qs(data.vemoticon()); + if (!text.isEmpty()) { + if (_from->isSelf()) { + result.text = tr::lng_action_you_theme_changed( + tr::now, + lt_emoji, + { .text = text }, + Ui::Text::WithEntities); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_theme_changed( + tr::now, + lt_from, + fromLinkText(), // Link 1. + lt_emoji, + { .text = text }, + Ui::Text::WithEntities); + } } else { - result.links.push_back(fromLink()); - result.text = tr::lng_action_theme_changed( - tr::now, - lt_from, - fromLinkText(), // Link 1. - lt_emoji, - { .text = text }, - Ui::Text::WithEntities); + if (_from->isSelf()) { + result.text = tr::lng_action_you_theme_disabled( + tr::now, + Ui::Text::WithEntities); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_theme_disabled( + tr::now, + lt_from, + fromLinkText(), // Link 1. + Ui::Text::WithEntities); + } } - } else { - if (_from->isSelf()) { - result.text = tr::lng_action_you_theme_disabled( - tr::now, - Ui::Text::WithEntities); - } else { - result.links.push_back(fromLink()); - result.text = tr::lng_action_theme_disabled( - tr::now, - lt_from, - fromLinkText(), // Link 1. - Ui::Text::WithEntities); - } - } + }, [&](const MTPDchatThemeUniqueGift &data) { + data.vgift().match([&](const MTPDstarGiftUnique &data) { + const auto giftName = Ui::Text::Bold(qs(data.vtitle()) + + u" #"_q + + QString::number(data.vnum().v)); + if (_from->isSelf()) { + result.text = tr::lng_action_you_gift_theme_changed( + tr::now, + lt_name, + giftName, + Ui::Text::WithEntities); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_gift_theme_changed( + tr::now, + lt_from, + fromLinkText(), + lt_name, + giftName, + Ui::Text::WithEntities); + } + }, [](const MTPDstarGift &) { + }); + }); return result; }; @@ -5578,7 +5616,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { session->giftBoxStickersPacks().load(); const auto amount = action.vamount().v; const auto currency = qs(action.vcurrency()); - const auto cost = AmountAndStarCurrency(session, amount, currency); + const auto cost = AmountAndStarCurrency(amount, currency); const auto anonymous = _from->isServiceUser(); if (anonymous) { result.text = tr::lng_action_gift_received_anonymous( @@ -5805,7 +5843,6 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { const auto isSelf = (_from->id == _from->session().userPeerId()); const auto peer = isSelf ? _history->peer : _from; const auto cost = AmountAndStarCurrency( - &_history->session(), action.vamount().value_or_empty(), qs(action.vcurrency().value_or_empty())); result.links.push_back(peer->createOpenLink()); @@ -5915,7 +5952,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_peer, Ui::Text::Link(refund->peer->name(), 1), // Link 1. lt_amount, - AmountAndStarCurrency(&_history->session(), amount, currency), + AmountAndStarCurrency(amount, currency), Ui::Text::WithEntities); return result; }; @@ -5928,10 +5965,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { _history->session().giftBoxStickersPacks().load(); const auto amount = action.vamount().v; const auto currency = qs(action.vcurrency()); - const auto cost = AmountAndStarCurrency( - &_history->session(), - amount, - currency); + const auto cost = AmountAndStarCurrency(amount, currency); const auto anonymous = _from->isServiceUser(); if (anonymous) { result.text = tr::lng_action_gift_received_anonymous( @@ -5965,10 +5999,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { _history->session().giftBoxStickersPacks().tonLoad(); const auto amount = action.vamount().v; const auto currency = qs(action.vcurrency()); - const auto cost = AmountAndStarCurrency( - &_history->session(), - amount, - currency); + const auto cost = AmountAndStarCurrency(amount, currency); const auto anonymous = _from->isServiceUser(); if (anonymous) { result.text = tr::lng_action_gift_received_anonymous( @@ -6013,12 +6044,18 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareStarGift = [&]( const MTPDmessageActionStarGift &action) { auto result = PreparedServiceText(); + const auto upgradeGifted = action.is_prepaid_upgrade(); + const auto upgradeSeparate = action.is_upgrade_separate(); const auto isSelf = _from->isSelf(); const auto peer = isSelf ? _history->peer : _from; const auto stars = action.vgift().match([&]( const MTPDstarGift &data) { - return uint64(data.vstars().v) - + uint64(action.vupgrade_stars().value_or_empty()); + return upgradeGifted + ? uint64(action.vupgrade_stars().value_or_empty()) + : upgradeSeparate + ? uint64(data.vstars().v) + : (uint64(data.vstars().v) + + uint64(action.vupgrade_stars().value_or_empty())); }, [](const MTPDstarGiftUnique &) { return uint64(); }); @@ -6050,30 +6087,54 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { const auto fromId = action.vfrom_id() ? peerFromMTP(*action.vfrom_id()) : PeerId(); - const auto from = fromId ? peer->owner().peer(fromId) : peer; + const auto from = fromId + ? peer->owner().peer(fromId).get() + : nullptr; const auto channel = peer->owner().channel( peerToChannel(giftPeer)); - if (from->isSelf()) { + if (!from || from->isSelf()) { result.links.push_back(channel->createOpenLink()); - result.text = tr::lng_action_gift_sent_self_channel( - tr::now, - lt_name, - Ui::Text::Link(channel->name(), 1), - lt_cost, - cost, - Ui::Text::WithEntities); + if (upgradeGifted) { + result.text = tr::lng_action_gift_sent_upgrade_self_channel( + tr::now, + lt_cost, + cost, + lt_name, + Ui::Text::Link(channel->name(), 1), + Ui::Text::WithEntities); + } else { + result.text = tr::lng_action_gift_sent_self_channel( + tr::now, + lt_name, + Ui::Text::Link(channel->name(), 1), + lt_cost, + cost, + Ui::Text::WithEntities); + } } else { result.links.push_back(from->createOpenLink()); result.links.push_back(channel->createOpenLink()); - result.text = tr::lng_action_gift_sent_channel( - tr::now, - lt_user, - Ui::Text::Link(from->shortName(), 1), - lt_name, - Ui::Text::Link(channel->name(), 2), - lt_cost, - cost, - Ui::Text::WithEntities); + if (upgradeGifted) { + result.text = tr::lng_action_gift_sent_upgrade_self_other( + tr::now, + lt_cost, + cost, + lt_name, + Ui::Text::Link(channel->name(), 2), + lt_user, + Ui::Text::Link(from->shortName(), 1), + Ui::Text::WithEntities); + } else { + result.text = tr::lng_action_gift_sent_channel( + tr::now, + lt_user, + Ui::Text::Link(from->shortName(), 1), + lt_name, + Ui::Text::Link(channel->name(), 2), + lt_cost, + cost, + Ui::Text::WithEntities); + } } } else if (anonymous || _history->peer->isSelf()) { result.text = (anonymous @@ -6083,6 +6144,57 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { lt_cost, cost, Ui::Text::WithEntities); + } else if (upgradeGifted) { + // Who sent the gift. + const auto fromId = action.vfrom_id() + ? peerFromMTP(*action.vfrom_id()) + : PeerId(); + const auto from = fromId + ? peer->owner().peer(fromId).get() + : nullptr; + if (isSelf) { + result.links.push_back(peer->createOpenLink()); + if (!from || from->isSelf()) { + result.text = tr::lng_action_gift_sent_upgrade_self( + tr::now, + lt_cost, + cost, + Ui::Text::WithEntities); + } else { + result.links.push_back(from->createOpenLink()); + result.text = tr::lng_action_gift_sent_upgrade_self_other( + tr::now, + lt_cost, + cost, + lt_name, + Ui::Text::Link(peer->shortName(), 1), + lt_user, + Ui::Text::Link(from->shortName(), 2), + Ui::Text::WithEntities); + } + } else { + result.links.push_back(peer->createOpenLink()); + if (from && from != peer && !from->isSelf()) { + result.links.push_back(from->createOpenLink()); + result.text = tr::lng_action_gift_sent_upgrade_other( + tr::now, + lt_from, + Ui::Text::Link(peer->shortName(), 1), + lt_cost, + cost, + lt_user, + Ui::Text::Link(from->shortName(), 2), + Ui::Text::WithEntities); + } else { + result.text = tr::lng_action_gift_sent_upgrade( + tr::now, + lt_from, + Ui::Text::Link(peer->shortName(), 1), + lt_cost, + cost, + Ui::Text::WithEntities); + } + } } else { if (!isSelf) { result.links.push_back(peer->createOpenLink()); @@ -6107,16 +6219,24 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { const MTPDmessageActionStarGiftUnique &action) { auto result = PreparedServiceText(); const auto isSelf = _from->isSelf(); - const auto resaleStars = action.vresale_stars().value_or_empty(); - const auto resaleCost = TextWithEntities{ resaleStars - ? tr::lng_action_gift_for_stars(tr::now, lt_count, resaleStars) - : QString() - }; + const auto resale = CreditsAmountFromTL(action.vresale_amount()); + const auto resaleCost = !resale + ? TextWithEntities() + : resale.stars() + ? TextWithEntities{ tr::lng_action_gift_for_stars( + tr::now, + lt_count, + resale.value()) } + : TextWithEntities{ tr::lng_action_gift_for_ton( + tr::now, + lt_count, + resale.value()) }; const auto giftPeer = action.vpeer() ? peerFromMTP(*action.vpeer()) : PeerId(); const auto service = _from->isServiceUser(); const auto toChannel = service && peerIsChannel(giftPeer); + const auto upgradeHelped = action.is_prepaid_upgrade(); const auto peer = isSelf ? _history->peer : _from; const auto fromId = action.vfrom_id() ? peerFromMTP(*action.vfrom_id()) @@ -6127,10 +6247,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { peerToChannel(giftPeer)); if (!from->isServiceUser() && !from->isSelf()) { result.links.push_back(from->createOpenLink()); - if (resaleStars) { + if (resale) { result.links.push_back(channel->createOpenLink()); } - result.text = resaleStars + result.text = resale ? tr::lng_action_gift_sent_channel( tr::now, lt_user, @@ -6150,7 +6270,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { Ui::Text::Link(channel->name(), 2), Ui::Text::WithEntities); } else { - result.text = resaleStars + result.text = resale ? tr::lng_action_gift_sent_self_channel( tr::now, lt_name, @@ -6170,11 +6290,26 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { } result.links.push_back(channel->createOpenLink()); } else { - if (!from->isServiceUser() && !_history->peer->isSelf()) { - if (!resaleStars || !isSelf) { + if (upgradeHelped) { + result.links.push_back(peer->createOpenLink()); + if (isSelf) { + result.text = tr::lng_action_gift_upgraded_helped_self( + tr::now, + lt_user, + Ui::Text::Link(peer->shortName(), 1), + Ui::Text::WithEntities); + } else { + result.text = tr::lng_action_gift_upgraded_helped( + tr::now, + lt_user, + Ui::Text::Link(peer->shortName(), 1), + Ui::Text::WithEntities); + } + } else if (!from->isServiceUser() && !_history->peer->isSelf()) { + if (!resale || !isSelf) { result.links.push_back(from->createOpenLink()); } - result.text = resaleStars + result.text = resale ? (isSelf ? tr::lng_action_gift_sent( tr::now, @@ -6200,7 +6335,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { Ui::Text::Link(from->shortName(), 1), Ui::Text::WithEntities); } else { - result.text = resaleStars + result.text = resale ? tr::lng_action_gift_self_bought( tr::now, lt_cost, @@ -6452,6 +6587,20 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { data.is_for_both()); } } + }, [&](const MTPDmessageActionSetChatTheme &data) { + data.vtheme().match([](const MTPDchatTheme &) { + }, [&](const MTPDchatThemeUniqueGift &data) { + const auto session = &history()->session(); + if (const auto gift = Api::FromTL(session, data.vgift())) { + _media = std::make_unique( + this, + _from, + Data::GiftCode{ + .unique = gift->unique, + .type = Data::GiftType::ChatTheme, + }); + } + }, [](const MTPDchatTheme &) {}); }, [&](const MTPDmessageActionGiftCode &data) { const auto boostedId = data.vboost_peer() ? peerToChannel(peerFromMTP(*data.vboost_peer())) @@ -6462,12 +6611,9 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { Data::GiftCode{ .slug = qs(data.vslug()), .message = (data.vmessage() - ? TextWithEntities{ - .text = qs(data.vmessage()->data().vtext()), - .entities = Api::EntitiesFromMTP( - &history()->session(), - data.vmessage()->data().ventities().v), - } + ? Api::ParseTextWithEntities( + &history()->session(), + *data.vmessage()) : TextWithEntities()), .channel = (boostedId ? history()->owner().channel(boostedId).get() @@ -6511,15 +6657,14 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { const auto to = data.vpeer() ? peerFromMTP(*data.vpeer()) : PeerId(); + const auto upgradeMsgId = data.vupgrade_msg_id().value_or_empty(); + const auto realGiftMsgId = data.vgift_msg_id().value_or_empty(); using Fields = Data::GiftCode; auto fields = Fields{ .message = (data.vmessage() - ? TextWithEntities{ - .text = qs(data.vmessage()->data().vtext()), - .entities = Api::EntitiesFromMTP( - &history()->session(), - data.vmessage()->data().ventities().v), - } + ? Api::ParseTextWithEntities( + &history()->session(), + *data.vmessage()) : TextWithEntities()), .channel = ((service && peerIsChannel(to)) ? history()->owner().channel(peerToChannel(to)).get() @@ -6528,11 +6673,14 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { ? history()->owner().peer(from).get() : nullptr), .channelSavedId = data.vsaved_id().value_or_empty(), - .upgradeMsgId = data.vupgrade_msg_id().value_or_empty(), + .giftPrepayUpgradeHash = qs( + data.vprepaid_upgrade_hash().value_or_empty()), + .realGiftMsgId = (upgradeMsgId ? upgradeMsgId : realGiftMsgId), .starsConverted = int(data.vconvert_stars().value_or_empty()), .starsUpgradedBySender = int( data.vupgrade_stars().value_or_empty()), .type = Data::GiftType::StarGift, + .upgradeSeparate = data.is_upgrade_separate(), .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), .converted = data.is_converted(), diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b107d96512..424d4a7a49 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -192,6 +192,7 @@ public: [[nodiscard]] bool canLookupMessageAuthor() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; + [[nodiscard]] bool isSavedMusicItem() const; [[nodiscard]] BusinessShortcutId shortcutId() const; [[nodiscard]] bool isBusinessShortcut() const; void setRealShortcutId(BusinessShortcutId id); @@ -237,6 +238,9 @@ public: [[nodiscard]] bool invertMedia() const { return _flags & MessageFlag::InvertMedia; } + [[nodiscard]] bool storyInProfile() const { + return _flags & MessageFlag::StoryInProfile; + } [[nodiscard]] bool unread(not_null thread) const; [[nodiscard]] bool showNotification() const; void markClientSideAsRead(); @@ -252,6 +256,7 @@ public: void markMediaAndMentionRead(); bool markContentsRead(bool fromThisClient = false); void setIsPinned(bool isPinned); + void setStoryInProfile(bool inProfile); // For edit media in history_message. void returnSavedMedia(); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index f8bb7e75ec..9399d8874b 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -55,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "support/support_helper.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_credits.h" #include "styles/style_dialogs.h" // dialogsMiniReplyStory. #include "styles/style_settings.h" #include "styles/style_widgets.h" @@ -775,11 +776,6 @@ ReplyKeyboard::ReplyKeyboard( const auto context = _item->fullId(); const auto rowCount = int(markup->data.rows.size()); _rows.reserve(rowCount); - const auto buttonEmoji = Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - st::settingsPremiumIconStar, - QMargins(0, -st::moderateBoxExpandInnerSkip, 0, 0), - true)); for (auto i = 0; i != rowCount; ++i) { const auto &row = markup->data.rows[i]; const auto rowSize = int(row.size()); @@ -814,7 +810,8 @@ ReplyKeyboard::ReplyKeyboard( auto firstPart = true; for (const auto &part : text.split(QChar(0x2B50))) { if (!firstPart) { - result.append(buttonEmoji); + result.append(Ui::Text::IconEmoji( + &st::starIconEmojiLarge)); } result.append(part); firstPart = false; @@ -833,11 +830,7 @@ ReplyKeyboard::ReplyKeyboard( button.text.setMarkedText( _st->textStyle(), TextUtilities::SingleLine(textWithEntities), - kMarkupTextOptions, - Core::TextContext({ - .session = &item->history()->owner().session(), - .repaint = [=] { _st->repaint(item); }, - })); + kMarkupTextOptions); } else { button.text.setText( _st->textStyle(), @@ -1371,11 +1364,7 @@ MessageFactcheck FromMTP( } const auto &data = factcheck->data(); if (const auto text = data.vtext()) { - const auto &data = text->data(); - result.text = { - qs(data.vtext()), - Api::EntitiesFromMTP(session, data.ventities().v), - }; + result.text = Api::ParseTextWithEntities(session, *text); } if (const auto country = data.vcountry()) { result.country = qs(country->v); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 2bc5decb92..9ef6a30600 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -360,7 +360,7 @@ void ShowSendPaidConfirm( const auto messages = details.messages; const auto stars = details.stars; show->showBox(Box([=](not_null box) { - const auto trust = std::make_shared>(); + const auto trust = std::make_shared>(); const auto proceed = [=](Fn close) { if (singlePeer && (*trust)->checked()) { const auto session = &singlePeer->session(); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index b4b2e5f6cd..2f982f1b4d 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -115,6 +115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_pinned_section.h" #include "history/view/history_view_pinned_bar.h" #include "history/view/history_view_group_call_bar.h" +#include "history/view/history_view_group_members_widget.h" #include "history/view/history_view_item_preview.h" #include "history/view/history_view_reply.h" #include "history/view/history_view_requests_bar.h" @@ -122,7 +123,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" -#include "profile/profile_block_group_members.h" #include "core/click_handler_types.h" #include "chat_helpers/field_autocomplete.h" #include "chat_helpers/tabbed_panel.h" @@ -896,7 +896,7 @@ HistoryWidget::HistoryWidget( | PeerUpdateFlag::Slowmode | PeerUpdateFlag::BotStartToken | PeerUpdateFlag::MessagesTTL - | PeerUpdateFlag::ChatThemeEmoji + | PeerUpdateFlag::ChatThemeToken | PeerUpdateFlag::FullInfo | PeerUpdateFlag::StarsPerMessage | PeerUpdateFlag::GiftSettings @@ -970,10 +970,10 @@ HistoryWidget::HistoryWidget( if (flags & PeerUpdateFlag::MessagesTTL) { checkMessagesTTL(); } - if ((flags & PeerUpdateFlag::ChatThemeEmoji) && _list) { - const auto emoji = _peer->themeEmoji(); + if ((flags & PeerUpdateFlag::ChatThemeToken) && _list) { + const auto emoji = _peer->themeToken(); if (Data::CloudThemes::TestingColors() && !emoji.isEmpty()) { - _peer->owner().cloudThemes().themeForEmojiValue( + _peer->owner().cloudThemes().themeForTokenValue( emoji ) | rpl::filter_optional( ) | rpl::take( @@ -1113,7 +1113,7 @@ void HistoryWidget::setGeometryWithTopMoved( _topDelta = topDelta; bool willBeResized = (size() != newGeometry.size()); if (geometry() != newGeometry) { - auto weak = Ui::MakeWeak(this); + auto weak = base::make_weak(this); setGeometry(newGeometry); if (!weak) { return; @@ -2203,6 +2203,18 @@ void HistoryWidget::setupShortcuts() { std::make_shared(_history)); return true; }); + if (showRecordButton()) { + const auto isVoice = request->check(Command::RecordVoice, 1); + const auto isRound = !isVoice + && request->check(Command::RecordRound, 1); + (isVoice || isRound) && request->handle([=] { + if (_voiceRecordBar) { + _voiceRecordBar->startRecordingAndLock(isRound); + return true; + } + return false; + }); + } if (session().supportMode()) { request->check( Command::SupportToggleMuted @@ -4624,7 +4636,7 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) { } } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto history = _history; const auto done = [=](mtpRequestId requestId) { @@ -5349,7 +5361,7 @@ void HistoryWidget::updateOverStates(QPoint pos) { } void HistoryWidget::leaveToChildEvent(QEvent *e, QWidget *child) { -// e -- from enterEvent() of child TWidget +// e -- from enterEvent() of child RpWidget if (hasMouseTracking()) { updateOverStates(mapFromGlobal(QCursor::pos())); } @@ -5529,7 +5541,7 @@ bool HistoryWidget::eventFilter(QObject *obj, QEvent *e) { } } } - return TWidget::eventFilter(obj, e); + return RpWidget::eventFilter(obj, e); } bool HistoryWidget::floatPlayerHandleWheelEvent(QEvent *e) { @@ -5775,7 +5787,7 @@ bool HistoryWidget::searchInChatEmbedded( if (!peer || Window::SeparateId(peer) != controller()->windowId()) { return false; } else if (_peer != peer) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); controller()->showPeerHistory(peer); if (!weak) { return false; @@ -5976,11 +5988,7 @@ void HistoryWidget::showMembersDropdown() { if (!_membersDropdown) { _membersDropdown.create(this, st::membersInnerDropdown); _membersDropdown->setOwnedWidget( - object_ptr( - this, - controller(), - _peer, - st::membersInnerItem)); + object_ptr(this, controller(), _peer)); _membersDropdown->resizeToWidth(st::membersInnerWidth); _membersDropdown->setMaxHeight(countMembersDropdownHeightMax()); @@ -8878,7 +8886,7 @@ void HistoryWidget::editMessage( controller()->showToast(tr::lng_edit_caption_voice(tr::now)); return; } else if (const auto media = item->media()) { - if (const auto todolist = media->todolist()) { + if (media->todolist()) { Window::PeerMenuEditTodoList(controller(), item); return; } @@ -9238,9 +9246,9 @@ void HistoryWidget::forwardSelected() { if (!_list) { return; } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); Window::ShowForwardMessagesBox(controller(), getSelectedItems(), [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->clearSelected(); } }); 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 012e6aea51..0bbddb2681 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/field_autocomplete.h" #include "core/application.h" #include "core/core_settings.h" +#include "core/shortcuts.h" #include "core/ui_integration.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" @@ -1132,9 +1133,13 @@ const HistoryItemsList &ComposeControls::forwardItems() const { } bool ComposeControls::focus() { - if (_wrap->isHidden() || _field->isHidden() || isRecording()) { + if (_wrap->isHidden() || _field->isHidden()) { return false; } + if (isRecording()) { + _wrap->setFocus(); + return true; + } _field->setFocus(); return true; } @@ -1191,6 +1196,17 @@ auto ComposeControls::sendContentRequests(SendRequestType requestType) const { _sendCustomRequests.events()); } +rpl::producer<> ComposeControls::scrollToMaxRequests() const { + return _field->submits() | rpl::filter([=]{ + if (_mode == Mode::Normal + && !_voiceRecordBar->isListenState() + && getTextWithAppliedMarkdown().text.isEmpty()) { + return true; + } + return false; + }) | rpl::to_empty; +} + rpl::producer ComposeControls::sendRequests() const { return sendContentRequests(SendRequestType::Text); } @@ -2248,6 +2264,12 @@ void ComposeControls::initSendButton() { updateSendButtonType(); }, _send->lifetime()); + Core::App().mediaDevices().recordAvailabilityValue( + ) | rpl::start_with_next([=](Webrtc::RecordAvailability value) { + _recordAvailability = value; + updateSendButtonType(); + }, _send->lifetime()); + _send->finishAnimating(); _send->clicks( @@ -2279,12 +2301,6 @@ void ComposeControls::initSendButton() { [=] { return sendButtonMenuDetails(); }, sendAction); - Core::App().mediaDevices().recordAvailabilityValue( - ) | rpl::start_with_next([=](Webrtc::RecordAvailability value) { - _recordAvailability = value; - updateSendButtonType(); - }, _send->lifetime()); - _send->widthValue() | rpl::skip(1) | rpl::start_with_next([=] { updateControlsGeometry(_wrap->size()); }, _send->lifetime()); @@ -2617,6 +2633,23 @@ void ComposeControls::initVoiceRecordBar() { ) | rpl::start_with_next([=] { updateSendButtonType(); }, _wrap->lifetime()); + + Shortcuts::Requests( + ) | rpl::filter([=] { + return Ui::AppInFocus(); + }) | rpl::start_with_next([=](not_null request) { + using Command = Shortcuts::Command; + const auto isVoice = request->check(Command::RecordVoice, 1); + const auto isRound = !isVoice + && request->check(Command::RecordRound, 1); + (isVoice || isRound) && request->handle([=] { + if (_voiceRecordBar) { + _voiceRecordBar->startRecordingAndLock(isRound); + return true; + } + return false; + }); + }, _voiceRecordBar->lifetime()); } void ComposeControls::updateWrappingVisibility() { @@ -3068,7 +3101,7 @@ void ComposeControls::editMessage(not_null item) { _show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice())); return; } else if (const auto media = item->media()) { - if (const auto todolist = media->todolist()) { + if (media->todolist()) { Assert(_regularWindow != nullptr); Window::PeerMenuEditTodoList(_regularWindow, item); 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 4efc974160..e8436c3178 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -183,6 +183,7 @@ public: -> rpl::producer; [[nodiscard]] rpl::producer<> focusRequests() const; [[nodiscard]] rpl::producer<> showScheduledRequests() const; + [[nodiscard]] rpl::producer<> scrollToMaxRequests() const; using MimeDataHook = Fn data, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index 98064d0204..22d3ecba40 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp @@ -54,7 +54,9 @@ using SearchRequest = Api::MessagesSearchMerged::Request; class Row final : public PeerListRow { public: - explicit Row(std::unique_ptr fakeRow); + explicit Row( + std::unique_ptr fakeRow, + not_null query); [[nodiscard]] FullMsgId fullId() const; @@ -73,15 +75,17 @@ public: private: const std::unique_ptr _fakeRow; + not_null _query; int _outerWidth = 0; }; -Row::Row(std::unique_ptr fakeRow) +Row::Row(std::unique_ptr fakeRow, not_null query) : PeerListRow( fakeRow->searchInChat().history()->peer, fakeRow->item()->fullId().msg.bare) -, _fakeRow(std::move(fakeRow)) { +, _fakeRow(std::move(fakeRow)) +, _query(query) { } FullMsgId Row::fullId() const { @@ -116,9 +120,11 @@ void Row::elementsPaint( .st = &st::defaultDialogRow, .currentBg = st::dialogsBg, .now = crl::now(), + .searchLowerText = QStringView(*_query), .width = outerWidth, .selected = selected, .paused = p.inactive(), + .search = true, }); } @@ -134,6 +140,7 @@ public: void loadMoreRows() override; void addItems(const MessageIdsList &ids, bool clear); + void setQuery(const QString &query); [[nodiscard]] rpl::producer showItemRequests() const; [[nodiscard]] rpl::producer<> searchMoreRequests() const; @@ -145,6 +152,8 @@ private: rpl::event_stream<> _searchMoreRequests; rpl::event_stream<> _resetScrollRequests; + QString _query; + }; ListController::ListController(not_null history) @@ -201,7 +210,8 @@ void ListController::addItems(const MessageIdsList &ids, bool clear) { std::make_unique( key, item, - [=] { delegate()->peerListUpdateRow(*shared); })); + [=] { delegate()->peerListUpdateRow(*shared); }), + &_query); *shared = row.get(); delegate()->peerListAppendRow(std::move(row)); } @@ -214,6 +224,10 @@ void ListController::addItems(const MessageIdsList &ids, bool clear) { } } +void ListController::setQuery(const QString &query) { + _query = query; +} + struct List { base::unique_qptr container; std::unique_ptr controller; @@ -256,9 +270,9 @@ List CreateList( }, list.container->lifetime()); list.container->paintRequest( - ) | rpl::start_with_next([weak = Ui::MakeWeak(list.container.get())]( + ) | rpl::start_with_next([weak = base::make_weak(list.container.get())]( const QRect &r) { - auto p = QPainter(weak); + auto p = QPainter(weak.get()); p.fillRect(r, st::dialogsBg); }, list.container->lifetime()); @@ -913,6 +927,9 @@ ComposeSearch::Inner::Inner( search.topMsgId = _topMsgId; _apiSearch.clear(); _apiSearch.search(search); + + _list.controller->addItems({}, true); + _list.controller->setQuery(_apiSearch.request().query); }, _topBar->lifetime()); _topBar->queryChanges( @@ -937,7 +954,7 @@ ComposeSearch::Inner::Inner( _apiSearch.newFounds( ) | rpl::start_with_next([=] { const auto &apiData = _apiSearch.messages(); - const auto weak = Ui::MakeWeak(_bottomBar.get()); + const auto weak = base::make_weak(_bottomBar.get()); _bottomBar->setTotal(apiData.total); if (weak) { // Activating the first search result may switch the chat. @@ -970,7 +987,7 @@ ComposeSearch::Inner::Inner( _pendingJump.data = {}; const auto item = _history->owner().message(messages[index]); if (item) { - const auto weak = Ui::MakeWeak(_topBar.get()); + const auto weak = base::make_weak(_topBar.get()); _activations.fire_copy({ item, _apiSearch.request().query }); if (weak) { hideList(); 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 15125a480b..417548f20d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -819,7 +819,7 @@ void DraftOptionsBox( FullReplyTo result, Data::WebPageDraft webpage, std::optional options) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); auto forward = Data::ForwardDraft(); if (options) { forward.options = *options; @@ -828,7 +828,7 @@ void DraftOptionsBox( } } done(std::move(result), std::move(webpage), std::move(forward)); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -847,7 +847,7 @@ void DraftOptionsBox( }); } - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); Settings::AddButtonWithIcon( bottom, tr::lng_reply_show_in_chat(), @@ -855,7 +855,7 @@ void DraftOptionsBox( { &st::menuIconShowInChat } )->setClickedCallback([=] { highlight(resolveReply()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -949,7 +949,7 @@ void DraftOptionsBox( AddFilledSkip(bottom); if (!hasOnlyForcedForwardedInfo - && !HasOnlyDroppedForwardedInfo(items)) { + && HasDropForwardedInfoSetting(items)) { Settings::AddButtonWithIcon( bottom, (dropNames @@ -1075,7 +1075,7 @@ void DraftOptionsBox( state->wrap = box->addRow( object_ptr(box, args.history), - {}); + style::margins()); state->wrap->draggingScrollDelta( ) | rpl::start_with_next([=](int delta) { box->scrollByDraggingDelta(delta); @@ -1402,10 +1402,10 @@ void ShowReplyToChatBox( }; auto callback = [=, chosen = std::move(chosen)]( Controller::Chosen thread) mutable { - const auto weak = Ui::MakeWeak(state->box); + const auto weak = base::make_weak(state->box); if (!chosen(thread)) { return; - } else if (const auto strong = weak.data()) { + } else if (const auto strong = weak.get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index de53aeb673..5d01148138 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -434,11 +434,11 @@ void EditWebPageOptions( }); box->addButton(tr::lng_settings_save(), [=] { - const auto weak = Ui::MakeWeak(box.get()); + const auto weak = base::make_weak(box.get()); auto result = state->result.current(); result.manual = true; done(result); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -463,11 +463,20 @@ bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) { bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list) { for (const auto &item : list) { - if (!item->computeDropForwardedInfo()) { + if (item->isSavedMusicItem() || !item->computeDropForwardedInfo()) { return false; } } return true; } +bool HasDropForwardedInfoSetting(const HistoryItemsList &list) { + for (const auto &item : list) { + if (!item->computeDropForwardedInfo()) { + return true; + } + } + return false; +} + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index 2e8dcc36d5..b29204047d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -87,5 +87,6 @@ void EditWebPageOptions( [[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list); [[nodiscard]] bool HasOnlyDroppedForwardedInfo(const HistoryItemsList &list); +[[nodiscard]] bool HasDropForwardedInfoSetting(const HistoryItemsList &list); } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index f599f8a63b..618d1b2793 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/unixtime.h" +#include "boxes/star_gift_box.h" #include "chat_helpers/compose/compose_show.h" #include "core/ui_integration.h" #include "data/components/credits.h" @@ -48,6 +49,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" namespace HistoryView { +namespace { + +[[nodiscard]] rpl::producer StarsPriceValue( + rpl::producer full) { + return rpl::single( + CreditsAmount() + ) | rpl::then(std::move( + full + ) | rpl::filter([=](CreditsAmount amount) { + return amount.stars(); + })); +} + +[[nodiscard]] rpl::producer TonPriceValue( + rpl::producer full) { + return rpl::single( + CreditsAmount() + ) | rpl::then(std::move( + full + ) | rpl::filter([=](CreditsAmount amount) { + return amount.ton(); + })); +} + +} // namespace void ChooseSuggestTimeBox( not_null box, @@ -112,6 +138,236 @@ void AddApproximateUsd( usd->widthValue() | rpl::start_with_next(move, usd->lifetime()); } +StarsTonPriceInput AddStarsTonPriceInput( + not_null container, + StarsTonPriceArgs &&args) { + struct State { + rpl::variable ton; + rpl::variable price; + rpl::event_stream<> updates; + rpl::event_stream<> submits; + }; + const auto state = container->lifetime().make_state(); + state->ton = std::move(args.showTon); + state->price = args.price; + + const auto session = args.session; + const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding; + auto helper = Ui::Text::CustomEmojiHelper(); + const auto makeIcon = [&]( + not_null parent, + Ui::Text::PaletteDependentEmoji emoji) { + auto text = helper.paletteDependent(std::move(emoji)); + return Ui::CreateChild( + parent, + rpl::single(std::move(text)), + st::defaultFlatLabel, + st::defaultPopupMenu, + helper.context()); + }; + + const auto starsWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto starsInner = starsWrap->entity(); + + Ui::AddSubsectionTitle( + starsInner, + tr::lng_suggest_options_stars_price(), + QMargins( + added.left(), + 0, + added.right(), + -st::defaultSubsectionTitlePadding.bottom())); + + const auto starsFieldWrap = starsInner->add( + object_ptr( + starsInner, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedStarsField = object_ptr( + starsFieldWrap, + st::editTagField, + rpl::single(u"0"_q), + ((args.price && args.price.stars()) + ? QString::number(args.price.whole()) + : QString()), + args.starsMax); + const auto starsField = ownedStarsField.data(); + const auto starsIcon = makeIcon( + starsField, + Ui::Earn::IconCreditsEmoji()); + + starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + starsIcon->move(st::starsFieldIconPosition); + starsField->move(0, 0); + starsField->resize(width, starsField->height()); + starsFieldWrap->resize(width, starsField->height()); + }, starsFieldWrap->lifetime()); + + AddApproximateUsd( + starsField, + session, + StarsPriceValue(state->price.value())); + + Ui::AddSkip(starsInner); + Ui::AddSkip(starsInner); + if (args.starsAbout) { + Ui::AddDividerText(starsInner, std::move(args.starsAbout)); + } + + const auto tonWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto tonInner = tonWrap->entity(); + + Ui::AddSubsectionTitle( + tonInner, + tr::lng_suggest_options_ton_price(), + QMargins( + added.left(), + 0, + added.right(), + -st::defaultSubsectionTitlePadding.bottom())); + + const auto tonFieldWrap = tonInner->add( + object_ptr( + tonInner, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedTonField = object_ptr::fromRaw( + Ui::CreateTonAmountInput( + tonFieldWrap, + rpl::single('0' + Ui::TonAmountSeparator() + '0'), + ((args.price && args.price.ton()) + ? (args.price.whole() * Ui::kNanosInOne + args.price.nano()) + : 0))); + const auto tonField = ownedTonField.data(); + const auto tonIcon = makeIcon(tonField, Ui::Earn::IconCurrencyEmoji()); + + tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + tonIcon->move(st::tonFieldIconPosition); + tonField->move(0, 0); + tonField->resize(width, tonField->height()); + tonFieldWrap->resize(width, tonField->height()); + }, tonFieldWrap->lifetime()); + + AddApproximateUsd( + tonField, + session, + TonPriceValue(state->price.value())); + + Ui::AddSkip(tonInner); + Ui::AddSkip(tonInner); + if (args.tonAbout) { + Ui::AddDividerText(tonInner, std::move(args.tonAbout)); + } + + tonWrap->toggleOn(state->ton.value(), anim::type::instant); + starsWrap->toggleOn( + state->ton.value() | rpl::map(!rpl::mappers::_1), + anim::type::instant); + + auto computeResult = [=]() -> std::optional { + auto nanos = int64(); + const auto ton = state->ton.current(); + if (ton) { + const auto text = tonField->getLastText(); + const auto now = Ui::ParseTonAmountString(text); + if (now + && *now + && ((*now < args.nanoTonMin) || (*now > args.nanoTonMax))) { + tonField->showError(); + return {}; + } + nanos = now.value_or(0); + } else { + const auto now = starsField->getLastText().toLongLong(); + if (now && (now < args.starsMin || now > args.starsMax)) { + starsField->showError(); + return {}; + } + nanos = now * Ui::kNanosInOne; + } + return CreditsAmount( + nanos / Ui::kNanosInOne, + nanos % Ui::kNanosInOne, + ton ? CreditsType::Ton : CreditsType::Stars); + }; + + const auto updatePrice = [=] { + if (auto result = computeResult()) { + state->price = *result; + } + state->updates.fire({}); + }; + const auto updateTonFromStars = [=] { + if (auto result = computeResult(); result && result->stars()) { + const auto v = Ui::TonFromStars(session, *result); + const auto amount = v.whole() * Ui::kNanosInOne + v.nano(); + tonField->setText( + Ui::FormatTonAmount(amount, Ui::TonFormatFlag::Simple).full); + } + }; + const auto updateStarsFromTon = [=] { + if (auto result = computeResult(); result && result->ton()) { + const auto v = Ui::StarsFromTon(session, *result); + starsField->setText(QString::number(v.whole())); + } + }; + QObject::connect(starsField, &Ui::NumberInput::changed, starsField, [=] { + if (!state->ton.current()) { + updatePrice(); + updateTonFromStars(); + } + }); + tonField->changes( + ) | rpl::start_with_next([=] { + if (state->ton.current()) { + updatePrice(); + updateStarsFromTon(); + } + }, tonField->lifetime()); + + state->ton.changes( + ) | rpl::start_with_next(updatePrice, container->lifetime()); + if (state->ton.current()) { + updateStarsFromTon(); + } else { + updateTonFromStars(); + } + + QObject::connect( + starsField, + &Ui::NumberInput::submitted, + container, + [=] { state->submits.fire({}); }); + tonField->submits( + ) | rpl::to_empty | rpl::start_to_stream( + state->submits, + tonField->lifetime()); + + auto focusCallback = [=] { + if (state->ton.current()) { + tonField->selectAll(); + tonField->setFocusFast(); + } else { + starsField->selectAll(); + starsField->setFocusFast(); + } + }; + + return { + .focusCallback = std::move(focusCallback), + .computeResult = std::move(computeResult), + .submits = state->submits.events(), + .updates = state->updates.events(), + .result = state->price.value(), + }; +} + void ChooseSuggestPriceBox( not_null box, SuggestPriceBoxArgs &&args) { @@ -135,42 +391,17 @@ void ChooseSuggestPriceBox( state->date = args.value.date; state->ton = (args.value.ton != 0); state->price = args.value.price(); - const auto updatePrice = [=] { - if (const auto price = state->computePrice()) { - state->price = *price; - } - }; - - const auto starsPrice = [=] { - return rpl::single( - CreditsAmount() - ) | rpl::then(state->price.value( - ) | rpl::filter([=](CreditsAmount amount) { - return amount.stars(); - })); - }; - const auto tonPrice = [=] { - return rpl::single( - CreditsAmount(0, 0, CreditsType::Ton) - ) | rpl::then(state->price.value( - ) | rpl::filter([=](CreditsAmount amount) { - return amount.ton(); - })); - }; const auto peer = args.peer; const auto admin = peer->amMonoforumAdmin(); const auto broadcast = peer->monoforumBroadcast(); const auto usePeer = broadcast ? broadcast : peer; const auto session = &peer->session(); + const auto &appConfig = session->appConfig(); if (!admin) { session->credits().load(); session->credits().tonLoad(); } - const auto starsMin = session->appConfig().suggestedPostStarsMin(); - const auto starsMax = session->appConfig().suggestedPostStarsMax(); - const auto nanoTonMin = session->appConfig().suggestedPostNanoTonMin(); - const auto nanoTonMax = session->appConfig().suggestedPostNanoTonMax(); const auto container = box->verticalLayout(); box->setStyle(st::suggestPriceBox); @@ -184,12 +415,12 @@ void ChooseSuggestPriceBox( box->setNoContentMargin(true); Ui::AddSkip(container, st::boxTitleHeight * 1.1); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, std::move(title), - st::settingsPremiumUserTitle))); + st::settingsPremiumUserTitle), + style::al_top); } state->buttons.push_back({ @@ -267,7 +498,6 @@ void ChooseSuggestPriceBox( state->buttons[i].active = true; state->buttons[1 - i].active = false; buttons->update(); - updatePrice(); break; } } @@ -299,64 +529,11 @@ void ChooseSuggestPriceBox( Ui::AddSkip(container); - const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding; - const auto manager = &session->data().customEmojiManager(); - const auto makeIcon = [&]( - not_null parent, - TextWithEntities text) { - return Ui::CreateChild( - parent, - rpl::single(text), - st::defaultFlatLabel, - st::defaultPopupMenu, - Core::TextContext({ .session = session })); + const auto computePrice = [session](CreditsAmount amount) { + return PriceAfterCommission(session, amount).value(); }; - - const auto starsWrap = container->add( - object_ptr>( - container, - object_ptr(container))); - const auto starsInner = starsWrap->entity(); - - Ui::AddSubsectionTitle( - starsInner, - tr::lng_suggest_options_stars_price(), - QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom())); - - const auto starsFieldWrap = starsInner->add( - object_ptr( - box, - st::editTagField.heightMin), - st::boxRowPadding); - auto ownedStarsField = object_ptr( - starsFieldWrap, - st::editTagField, - rpl::single(u"0"_q), - ((args.value.exists && args.value.priceWhole && !args.value.ton) - ? QString::number(args.value.priceWhole) - : QString()), - starsMax); - const auto starsField = ownedStarsField.data(); - const auto starsIcon = makeIcon(starsField, manager->creditsEmoji()); - - QObject::connect(starsField, &Ui::NumberInput::changed, updatePrice); - - starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) { - starsIcon->move(st::starsFieldIconPosition); - starsField->move(0, 0); - starsField->resize(width, starsField->height()); - starsFieldWrap->resize(width, starsField->height()); - }, starsFieldWrap->lifetime()); - - AddApproximateUsd(starsField, session, starsPrice()); - - Ui::AddSkip(starsInner); - Ui::AddSkip(starsInner); - const auto computePrice = [peer = args.peer](CreditsAmount amount) { - return PriceAfterCommission(&peer->session(), amount).value(); - }; - const auto formatCommission = [peer = args.peer](CreditsAmount amount) { - return FormatAfterCommissionPercent(&peer->session(), amount); + const auto formatCommission = [session](CreditsAmount amount) { + return FormatAfterCommissionPercent(session, amount); }; const auto youGet = [=](rpl::producer price, bool stars) { return (stars @@ -367,109 +544,34 @@ void ChooseSuggestPriceBox( lt_percent, rpl::duplicate(price) | rpl::map(formatCommission)); }; - Ui::AddDividerText(starsInner, admin + auto starsAbout = admin ? rpl::combine( - youGet(starsPrice(), true), + youGet(StarsPriceValue(state->price.value()), true), tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue) ) | rpl::map([=](const QString &t1, const TextWithEntities &t2) { return TextWithEntities{ t1 }.append("\n\n").append(t2); }) - : tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities)); - - const auto tonWrap = container->add( - object_ptr>( - container, - object_ptr(container))); - const auto tonInner = tonWrap->entity(); - - Ui::AddSubsectionTitle( - tonInner, - tr::lng_suggest_options_ton_price(), - QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom())); - - const auto tonFieldWrap = tonInner->add( - object_ptr( - box, - st::editTagField.heightMin), - st::boxRowPadding); - auto ownedTonField = object_ptr::fromRaw( - Ui::CreateTonAmountInput( - tonFieldWrap, - rpl::single('0' + Ui::TonAmountSeparator() + '0'), - ((args.value.price() && args.value.ton) - ? (int64(args.value.priceWhole) * Ui::kNanosInOne - + int64(args.value.priceNano)) - : 0))); - const auto tonField = ownedTonField.data(); - const auto tonIcon = makeIcon(tonField, Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::tonFieldIconSize, - st::currencyFg->c), - st::channelEarnCurrencyCommonMargins, - false))); - - tonField->changes( - ) | rpl::start_with_next(updatePrice, tonField->lifetime()); - - tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) { - tonIcon->move(st::tonFieldIconPosition); - tonField->move(0, 0); - tonField->resize(width, tonField->height()); - tonFieldWrap->resize(width, tonField->height()); - }, tonFieldWrap->lifetime()); - - AddApproximateUsd(tonField, session, tonPrice()); - - Ui::AddSkip(tonInner); - Ui::AddSkip(tonInner); - Ui::AddDividerText( - tonInner, - (admin - ? youGet(tonPrice(), false) - : tr::lng_suggest_options_ton_price_about())); - - tonWrap->toggleOn(state->ton.value(), anim::type::instant); - starsWrap->toggleOn( - state->ton.value() | rpl::map(!rpl::mappers::_1), - anim::type::instant); - - state->computePrice = [=]() -> std::optional { - auto nanos = int64(); - const auto ton = uint32(state->ton.current() ? 1 : 0); - if (ton) { - const auto text = tonField->getLastText(); - const auto now = Ui::ParseTonAmountString(text); - if (now - && *now - && ((*now < nanoTonMin) || (*now > nanoTonMax))) { - tonField->showError(); - return {}; - } - nanos = now.value_or(0); - } else { - const auto now = starsField->getLastText().toLongLong(); - if (now && (now < starsMin || now > starsMax)) { - starsField->showError(); - return {}; - } - nanos = now * Ui::kNanosInOne; - } - return CreditsAmount( - nanos / Ui::kNanosInOne, - nanos % Ui::kNanosInOne, - ton ? CreditsType::Ton : CreditsType::Stars); - }; - - box->setFocusCallback([=] { - if (state->ton.current()) { - tonField->selectAll(); - tonField->setFocusFast(); - } else { - starsField->selectAll(); - starsField->setFocusFast(); - } + : tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities); + auto tonAbout = admin + ? youGet( + TonPriceValue(state->price.value()), + false + ) | Ui::Text::ToWithEntities() + : tr::lng_suggest_options_ton_price_about(Ui::Text::WithEntities); + auto priceInput = AddStarsTonPriceInput(container, { + .session = session, + .showTon = state->ton.value(), + .price = args.value.price(), + .starsMin = appConfig.suggestedPostStarsMin(), + .starsMax = appConfig.suggestedPostStarsMax(), + .nanoTonMin = appConfig.suggestedPostNanoTonMin(), + .nanoTonMax = appConfig.suggestedPostNanoTonMax(), + .starsAbout = std::move(starsAbout), + .tonAbout = std::move(tonAbout), }); + state->price = std::move(priceInput.result); + state->computePrice = std::move(priceInput.computeResult); + box->setFocusCallback(std::move(priceInput.focusCallback)); Ui::AddSkip(container); @@ -484,13 +586,13 @@ void ChooseSuggestPriceBox( st::settingsButtonNoIcon); time->setClickedCallback([=] { - const auto weak = std::make_shared>(); - const auto parentWeak = Ui::MakeWeak(box); + const auto weak = std::make_shared>(); + const auto parentWeak = base::make_weak(box); const auto done = [=](TimeId result) { if (parentWeak) { state->date = result; } - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -568,22 +670,15 @@ void ChooseSuggestPriceBox( } }, box->lifetime()); - QObject::connect( - starsField, - &Ui::NumberInput::submitted, - box, - state->save); - tonField->submits( - ) | rpl::start_with_next(state->save, tonField->lifetime()); + std::move( + priceInput.submits + ) | rpl::start_with_next(state->save, box->lifetime()); + auto helper = Ui::Text::CustomEmojiHelper(); const auto button = box->addButton(rpl::single(QString()), state->save); - const auto coloredTonIcon = Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::tonFieldIconSize, - st::currencyFg->c), - st::suggestPriceTonIconMargins)); - button->setContext(Core::TextContext({ .session = &peer->session() })); + const auto coloredTonIcon = helper.paletteDependent( + Ui::Earn::IconCurrencyEmoji()); + button->setContext(helper.context()); button->setText(state->price.value( ) | rpl::map([=](CreditsAmount price) { if (args.mode == SuggestMode::Change) { @@ -598,7 +693,7 @@ void ChooseSuggestPriceBox( return tr::lng_suggest_options_offer( tr::now, lt_amount, - TextWithEntities{ coloredTonIcon }.append( + Ui::Text::IconEmoji(&st::tonIconEmoji).append( Lang::FormatCreditsAmountDecimal(price)), Ui::Text::WithEntities); } @@ -717,19 +812,19 @@ void InsufficientTonBox( const auto nano = add.whole() * Ui::kNanosInOne + add.nano(); const auto amount = Ui::FormatTonAmount(nano).full; box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_suggest_low_ton_title(tr::now, lt_amount, amount), - st::boxTitle)), - st::boxRowPadding + st::lowTonTitlePadding); + tr::lng_suggest_low_ton_title(tr::now, lt_amount, amount), + st::boxTitle), + st::boxRowPadding + st::lowTonTitlePadding, + style::al_top); const auto label = box->addRow( object_ptr( box, tr::lng_suggest_low_ton_text(Ui::Text::RichLangValue), st::lowTonText), - st::boxRowPadding + st::lowTonTextPadding); + st::boxRowPadding + st::lowTonTextPadding, + style::al_top); label->setTryMakeSimilarLines(true); label->resizeToWidth( st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right()); @@ -793,12 +888,12 @@ void SuggestOptions::paintLines(QPainter &p, int x, int y, int outerWidth) { } void SuggestOptions::edit() { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto apply = [=](SuggestPostOptions values) { _values = values; updateTexts(); _updates.fire({}); - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }; @@ -824,19 +919,15 @@ void SuggestOptions::updateTexts() { } TextWithEntities SuggestOptions::composeText() const { - const auto manager = &_peer->owner().customEmojiManager(); - const auto top = st::giftBoxByStarsStarTop; + auto helper = Ui::Text::CustomEmojiHelper(); const auto amount = _values.price().ton() - ? Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::suggestBarTonIconSize, - st::currencyFg->c), - st::suggestBarTonIconMargins, - false)).append( - Lang::FormatCreditsAmountDecimal(_values.price())) - : manager->ministarEmoji({ 0, top, 0, 0 }).append( - Lang::FormatCreditsAmountDecimal(_values.price())); + ? helper.paletteDependent(Ui::Earn::IconCurrencyEmoji({ + .size = st::suggestBarTonIconSize, + .margin = st::suggestBarTonIconMargins, + })).append(Lang::FormatCreditsAmountDecimal(_values.price())) + : helper.paletteDependent( + Ui::Earn::IconCreditsEmojiSmall() + ).append(Lang::FormatCreditsAmountDecimal(_values.price())); const auto date = langDateTime(base::unixtime::parse(_values.date)); if (!_values.price() && !_values.date) { return tr::lng_suggest_bar_text(tr::now, Ui::Text::WithEntities); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h index 7b270393fd..65eb6c7f9f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -15,6 +15,7 @@ class Show; namespace Ui { class GenericBox; +class VerticalLayout; } // namespace Ui namespace Main { @@ -43,6 +44,30 @@ void ChooseSuggestTimeBox( not_null box, SuggestTimeBoxArgs &&args); +struct StarsTonPriceInput { + Fn focusCallback; + Fn()> computeResult; + rpl::producer<> submits; + rpl::producer<> updates; + rpl::producer result; +}; + +struct StarsTonPriceArgs { + not_null session; + rpl::producer showTon; + CreditsAmount price; + int starsMin = 0; + int starsMax = 0; + int64 nanoTonMin = 0; + int64 nanoTonMax = 0; + rpl::producer starsAbout; + rpl::producer tonAbout; +}; + +[[nodiscard]] StarsTonPriceInput AddStarsTonPriceInput( + not_null container, + StarsTonPriceArgs &&args); + struct SuggestPriceBoxArgs { not_null peer; bool updating = false; 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 a1e14004e0..3c7512c816 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 @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/dynamic_image.h" #include "ui/painter.h" +#include "ui/widgets/fields/input_field.h" // ShouldSubmit. #include "ui/widgets/tooltip.h" #include "ui/rect.h" #include "ui/ui_utility.h" @@ -135,10 +136,6 @@ void SoundedPreview::subscribeToUpdates(Fn callback) { _repaint = std::move(callback); } -[[nodiscard]] auto InactiveColor(const QColor &c) { - return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha); -} - [[nodiscard]] auto Progress(int low, int high) { return std::clamp(float64(low) / high, 0., 1.); } @@ -447,6 +444,7 @@ TTLButton::TTLButton( r.top() + r.height() - size.height() + - st::historyRecordTooltipSkip + st::historyRecordTooltip.padding.top()); }); }, _tooltip->lifetime()); @@ -611,7 +609,8 @@ ListenWrap::ListenWrap( , _playPauseSt(st::mediaPlayerButton) , _playPauseButton(base::make_unique_q(parent)) , _activeWaveformBar(st::historyRecordVoiceFgActiveIcon->c) -, _inactiveWaveformBar(InactiveColor(_activeWaveformBar)) +, _inactiveWaveformBar( + anim::with_alpha(_activeWaveformBar, kInactiveWaveformBarAlpha)) , _playPause(_playPauseSt, [=] { _playPauseButton->update(); }) { init(); } @@ -1660,6 +1659,7 @@ void VoiceRecordBar::init() { auto callback = [=](float64 value) { _lock->requestPaintLockToStopProgress(value); + _level->requestPaintColor(activeAnimationRatio()); update(); updateTTLGeometry(TTLAnimationType::RightLeft, value); }; @@ -1683,11 +1683,7 @@ void VoiceRecordBar::init() { if (_startRecordingFilter && _startRecordingFilter()) { return; } - _recordingTipRequire = crl::now(); - _recordingVideo = (_send->type() == Ui::SendButton::Type::Round); - _fullRecord = false; - _ttlButton = nullptr; - _lock->setRecordingVideo(_recordingVideo); + prepareOnSendPress(); _startTimer.callOnce(st::universalDuration); } else if (e->type() == QEvent::MouseButtonRelease) { checkTipRequired(); @@ -1717,6 +1713,14 @@ void VoiceRecordBar::init() { initLevelGeometry(); } +void VoiceRecordBar::prepareOnSendPress() { + _recordingTipRequire = crl::now(); + _recordingVideo = (_send->type() == Ui::SendButton::Type::Round); + _fullRecord = false; + _ttlButton = nullptr; + _lock->setRecordingVideo(_recordingVideo); +} + void VoiceRecordBar::activeAnimate(bool active) { const auto to = active ? 1. : 0.; if (_activeAnimation.animating()) { @@ -1807,6 +1811,24 @@ void VoiceRecordBar::initLevelGeometry() { }, lifetime()); } +void VoiceRecordBar::startRecordingAndLock(bool round) { + { + auto sendState = _send->state(); + sendState.type = round + ? Ui::SendButton::Type::Round + : Ui::SendButton::Type::Record; + _send->setState(std::move(sendState)); + } + if (_startRecordingFilter && _startRecordingFilter()) { + return; + } + prepareOnSendPress(); + + _lock->show(); + _lock->requestPaintProgress(1.); + startRecording(); +} + void VoiceRecordBar::startRecording() { if (isRecording()) { return; @@ -1947,6 +1969,40 @@ void VoiceRecordBar::startRecording() { stop(_inField.current()); } }, _recordingLifetime); + + _listenChanges.events_starting_with( + rpl::empty_value() + ) | rpl::filter([=] { + return _listen == nullptr; + }) | rpl::start_with_next([=] { + auto keyFilterCallback = [=](not_null e) { + using Result = base::EventFilterResult; + if (_send->type() != Ui::SendButton::Type::Record + && _send->type() != Ui::SendButton::Type::Round) { + return Result::Continue; + } + switch(e->type()) { + case QEvent::KeyPress: { + if (!_warningShown + && isRecordingLocked() + && Ui::ShouldSubmit( + static_cast(e.get()), + Core::App().settings().sendSubmitWay())) { + stop(true); + return Result::Cancel; + } + return Result::Continue; + } + default: return Result::Continue; + } + }; + + _keyFilterInRecordingState = base::unique_qptr{ + base::install_event_filter( + QCoreApplication::instance(), + std::move(keyFilterCallback)).get() + }; + }, lifetime()); } void VoiceRecordBar::checkTipRequired() { @@ -2020,6 +2076,7 @@ void VoiceRecordBar::hideFast() { _lock->hide(); _level->hide(); [[maybe_unused]] const auto s = takeTTLState(); + _keyFilterInRecordingState = nullptr; } void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { @@ -2087,7 +2144,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { } } else if (type == StopType::Send) { if (_videoRecorder) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); _videoRecorder->hide([=](Ui::RoundVideoResult data) { crl::on_main([=, data = std::move(data)]() mutable { if (weak) { @@ -2175,8 +2232,8 @@ void VoiceRecordBar::startRedCircleAnimation() { if (anim::Disabled()) { return; } - const auto animation = _recordingLifetime - .make_state(); + const auto animation + = _recordingLifetime.make_state(); animation->init([=](crl::time now) { const auto diffTime = now - animation->started(); _redCircleProgress = std::abs(std::sin(diffTime / 400.)); @@ -2353,6 +2410,9 @@ bool VoiceRecordBar::hasDuration() const { } float64 VoiceRecordBar::activeAnimationRatio() const { + if (isRecordingLocked()) { + return 1.; + } return _activeAnimation.value(_inField.current() ? 1. : 0.); } @@ -2407,6 +2467,7 @@ void VoiceRecordBar::orderControls() { } void VoiceRecordBar::installListenStateFilter() { + _keyFilterInRecordingState = nullptr; auto keyFilterCallback = [=](not_null e) { using Result = base::EventFilterResult; if (!(_send->type() == Ui::SendButton::Type::Send 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 7a01c8563e..1b84eea0ec 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 @@ -78,7 +78,8 @@ public: Fn &&callback, anim::type animated = anim::type::instant); - void startRecording(); + void startRecordingAndLock(bool round); + void finishAnimating(); void hideAnimated(); void hideFast(); @@ -145,6 +146,9 @@ private: void startRedCircleAnimation(); void installListenStateFilter(); + void startRecording(); + void prepareOnSendPress(); + [[nodiscard]] bool isTypeRecord() const; [[nodiscard]] bool hasDuration() const; @@ -194,6 +198,8 @@ private: FilterCallback _startRecordingFilter; FilterCallback _hasTTLFilter; + base::unique_qptr _keyFilterInRecordingState; + bool _warningShown = false; bool _pauseInsteadSend = false; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 5e9d3eecdd..353a9acec4 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -941,9 +941,9 @@ ChatPreview MakeChatPreview( result.actions = action->actions(); menu->addAction(std::move(action)); if (const auto topic = thread->asTopic()) { - const auto weak = Ui::MakeWeak(menu); + const auto weak = base::make_weak(menu); topic->destroyed() | rpl::start_with_next([weak] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { LOG(("Preview hidden for a destroyed topic.")); strong->hideMenu(true); } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 279a988e1b..5bb013ccdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -464,7 +464,7 @@ ChatWidget::~ChatWidget() { if (_repliesRootId) { controller()->sendingAnimation().clear(); } - if (_subsectionTabs) { + if (_subsectionTabs && !_subsectionTabs->dying()) { _subsectionTabsLifetime.destroy(); controller()->saveSubsectionTabs(base::take(_subsectionTabs)); } @@ -818,6 +818,11 @@ void ChatWidget::setupComposeControls() { send(options); }, lifetime()); + _composeControls->scrollToMaxRequests( + ) | rpl::start_with_next([=] { + listScrollTo(_scroll->scrollTopMax()); + }, lifetime()); + _composeControls->sendVoiceRequests( ) | rpl::start_with_next([=](const ComposeControls::VoiceToSend &data) { sendVoice(data); @@ -2324,9 +2329,9 @@ bool ChatWidget::preventsClose(Fn &&continueCallback) const { } else if (!_newTopicDiscarded && _topic && _topic->creating()) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); auto sure = [=](Fn &&close) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->_newTopicDiscarded = true; } close(); @@ -2611,7 +2616,7 @@ void ChatWidget::subscribeToSublist() { void ChatWidget::unreadCountUpdated() { if (_sublist && _sublist->unreadMark()) { crl::on_main(this, [=] { - const auto guard = Ui::MakeWeak(this); + const auto guard = base::make_weak(this); controller()->showPeerHistory(_sublist->owningHistory()); if (guard) { closeCurrent(); diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 99c135f6f3..b0a48e1bdb 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -947,8 +947,8 @@ void BusinessBotStatus::Bar::showMenu() { this, st::popupMenuExpandedSeparator); _menu->setDestroyedCallback([ - weak = Ui::MakeWeak(this), - weakButton = Ui::MakeWeak(_settings.data()), + weak = base::make_weak(this), + weakButton = base::make_weak(_settings.data()), menu = _menu.get()] { if (weak && weak->_menu == menu) { if (weakButton) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index ea6e4d0148..6381c425a9 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_text.h" #include "history/view/history_view_schedule_box.h" #include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_save_document_action.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions_list.h" #include "info/info_memento.h" @@ -44,6 +45,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/report_box_graphics.h" #include "ui/ui_utility.h" #include "menu/menu_item_download_files.h" +#include "menu/menu_item_rate_transcribe.h" +#include "menu/menu_item_rate_transcribe_session.h" #include "menu/menu_send.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/show_or_premium_box.h" @@ -240,39 +243,6 @@ void ShowInFolder(not_null document) { } } -void AddSaveDocumentAction( - not_null menu, - HistoryItem *item, - not_null document, - not_null list) { - if (list->hasCopyMediaRestriction(item) || ItemHasTtl(item)) { - return; - } - const auto origin = item ? item->fullId() : FullMsgId(); - const auto save = [=] { - DocumentSaveClickHandler::SaveAndTrack( - origin, - document, - DocumentSaveClickHandler::Mode::ToNewFile); - }; - - menu->addAction( - (document->isVideoFile() - ? tr::lng_context_save_video(tr::now) - : (document->isVoiceMessage() - ? tr::lng_context_save_audio(tr::now) - : (document->isAudioFile() - ? tr::lng_context_save_audio_file(tr::now) - : (document->sticker() - ? tr::lng_context_save_image(tr::now) - : tr::lng_context_save_file(tr::now))))), - base::fn_delayed( - st::defaultDropdownMenu.menu.ripple.hideDuration, - &document->session(), - save), - &st::menuIconDownload); -} - void AddDocumentActions( not_null menu, not_null document, @@ -293,9 +263,9 @@ void AddDocumentActions( item->history()->peer, document); if (notAutoplayedGif) { - const auto weak = Ui::MakeWeak(list.get()); + const auto weak = base::make_weak(list.get()); menu->addAction(tr::lng_context_open_gif(tr::now), [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { OpenGif(strong, contextId); } }, &st::menuIconShowInChat); @@ -344,6 +314,16 @@ void AddDocumentActions( const auto controller = list->controller(); AddSaveSoundForNotifications(menu, item, document, controller); } + if ((document->isVoiceMessage() + || document->isVideoMessage()) + && Menu::HasRateTranscribeItem(item)) { + if (!menu->empty()) { + menu->insertAction(0, base::make_unique_q( + menu, + menu->st().menu, + Menu::RateTranscribeCallbackFactory(item))); + } + } AddSaveDocumentAction(menu, item, document, list); AddCopyFilename( menu, @@ -396,9 +376,9 @@ bool AddForwardSelectedAction( } menu->addAction(tr::lng_context_forward_selected(tr::now), [=] { - const auto weak = Ui::MakeWeak(list); + const auto weak = base::make_weak(list); const auto callback = [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->cancelSelection(); } }; @@ -477,7 +457,7 @@ bool AddSendNowSelectedAction( const auto history = *histories.begin(); menu->addAction(tr::lng_context_send_now_selected(tr::now), [=] { - const auto weak = Ui::MakeWeak(list); + const auto weak = base::make_weak(list); const auto callback = [=] { request.navigation->showBackFromStack(); }; @@ -1167,9 +1147,9 @@ void EditTagBox( field->showError(); return; } - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); controller->session().data().reactions().renameTag(id, text); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }; @@ -1652,10 +1632,10 @@ void AddWhoReactedAction( } const auto whoReadIds = std::make_shared(); - const auto weak = Ui::MakeWeak(menu.get()); + const auto weak = base::make_weak(menu.get()); const auto user = item->history()->peer; const auto showOrPremium = [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->hideMenu(); } const auto type = Ui::ShowOrPremium::ReadTime; @@ -1670,14 +1650,14 @@ void AddWhoReactedAction( }; const auto itemId = item->fullId(); const auto participantChosen = [=](Ui::WhoReadParticipant who) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->hideMenu(); } ShowWhoReadInfo(controller, itemId, who); }; const auto showAllChosen = [=, itemId = item->fullId()]{ // Pressing on an item that has a submenu doesn't hide it :( - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->hideMenu(); } if (const auto item = controller->session().data().message(itemId)) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index f32e227959..e7c9d1418d 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -108,59 +108,6 @@ Element *MousedElement/* = nullptr*/; return session->tryResolveWindow(); } -[[nodiscard]] TextSelection FindSearchQueryHighlight( - const QString &text, - const QString &query) { - const auto lower = query.toLower(); - const auto inside = text.toLower(); - const auto find = [&](QStringView part) { - auto skip = 0; - if (const auto from = inside.indexOf(part, skip); from >= 0) { - if (!from || !inside[from - 1].isLetterOrNumber()) { - return int(from); - } - skip = from + 1; - } - return -1; - }; - if (const auto from = find(lower); from >= 0) { - const auto till = from + query.size(); - if (till >= inside.size() || !inside[till].isLetterOrNumber()) { - return { uint16(from), uint16(till) }; - } - } - const auto tillEndOfWord = [&](int from) { - for (auto till = from + 1; till != inside.size(); ++till) { - if (!inside[till].isLetterOrNumber()) { - return TextSelection{ uint16(from), uint16(till) }; - } - } - return TextSelection{ uint16(from), uint16(inside.size()) }; - }; - const auto words = QStringView(lower).split( - QRegularExpression( - u"[\\W]"_q, - QRegularExpression::UseUnicodePropertiesOption), - Qt::SkipEmptyParts); - for (const auto &word : words) { - const auto length = int(word.size()); - const auto cut = length / 2; - const auto part = word.mid(0, length - cut); - const auto offset = find(part); - if (offset < 0) { - continue; - } - for (auto i = 0; i != cut; ++i) { - const auto part = word.mid(0, length - i); - if (const auto from = find(part); from >= 0) { - return tillEndOfWord(from); - } - } - return tillEndOfWord(offset); - } - return {}; -} - [[nodiscard]] TextSelection ApplyModificationsFrom( TextSelection result, const Ui::Text::String &text) { @@ -513,20 +460,37 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } -void MonoforumSenderBar::init( +void ForumThreadBar::init( not_null parentChat, - not_null peer) { - sender = peer; - text.setText(st::semiboldTextStyle, peer->name()); + not_null thread) { + this->thread = thread; + const auto sublist = thread->asSublist(); + if (sublist) { + text.setText(st::semiboldTextStyle, sublist->sublistPeer()->name()); + } else if (const auto topic = thread->asTopic()) { + text.setMarkedText( + st::semiboldTextStyle, + topic->titleWithIconOrLogo(), + kMarkupTextOptions, + Core::TextContext({ .session = &topic->session() })); + } const auto skip = st::monoforumBarUserpicSkip; - const auto userpic = st::msgServicePadding.top() - + st::msgServiceFont->height - + st::msgServicePadding.bottom() - - 2 * skip; - width = skip + userpic + skip * 2 + text.maxWidth() + st::msgServicePadding.right(); + const auto userpic = sublist + ? (st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip) + : (st::msgServicePadding.left() - 3 * skip); + + width = skip + + userpic + + skip * 2 + + text.maxWidth() + + st::topicButtonArrowSkip + + st::msgServicePadding.right(); } -int MonoforumSenderBar::height() const { +int ForumThreadBar::height() const { return st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height @@ -534,17 +498,29 @@ int MonoforumSenderBar::height() const { + st::msgServiceMargin.bottom(); } -void MonoforumSenderBar::paint( +void ForumThreadBar::paint( Painter &p, not_null st, int y, int w, bool chatWide, bool skipPatternLine) const { - Paint(p, st, sender, text, width, view, y, w, chatWide, skipPatternLine); + if (const auto strong = thread.get()) { + Paint( + p, + st, + strong, + text, + width, + view, + y, + w, + chatWide, + skipPatternLine); + } } -void MonoforumSenderBar::PaintFor( +int ForumThreadBar::PaintForGetWidth( Painter &p, not_null st, not_null itemView, @@ -552,31 +528,49 @@ void MonoforumSenderBar::PaintFor( int y, int w, bool chatWide) { - const auto sublist = itemView->data()->savedSublist(); - const auto sender = (sublist && sublist->parentChat()) - ? sublist->sublistPeer().get() + const auto item = itemView->data(); + const auto topic = item->topic(); + const auto sublist = item->savedSublist(); + const auto sender = topic + ? (Data::Thread*)topic + : (sublist && sublist->parentChat()) + ? (Data::Thread*)sublist : nullptr; - if (!sender || sender->isMonoforum()) { - return; + auto text = Ui::Text::String(); + if (!sender + || !topic + || (sublist && sublist->sublistPeer()->isMonoforum())) { + return 0; + } else if (topic) { + text.setMarkedText( + st::semiboldTextStyle, + topic->titleWithIconOrLogo(), + kMarkupTextOptions, + Core::TextContext({ .session = &topic->session() })); + } else { + text.setText(st::semiboldTextStyle, sublist->sublistPeer()->name()); } - auto text = Ui::Text::String(st::semiboldTextStyle, sender->name()); const auto skip = st::monoforumBarUserpicSkip; - const auto userpic = st::msgServicePadding.top() - + st::msgServiceFont->height - + st::msgServicePadding.bottom() - - 2 * skip; + const auto userpic = sublist + ? (st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip) + : (st::msgServicePadding.left() - 3 * skip); const auto width = skip + userpic + skip * 2 + text.maxWidth() + + st::topicButtonArrowSkip + st::msgServicePadding.right(); Paint(p, st, sender, text, width, userpicView, y, w, chatWide, true); + return width; } -void MonoforumSenderBar::Paint( +void ForumThreadBar::Paint( Painter &p, not_null st, - not_null sender, + not_null thread, const Ui::Text::String &text, int width, Ui::PeerUserpicView &view, @@ -584,8 +578,6 @@ void MonoforumSenderBar::Paint( int w, bool chatWide, bool skipPatternLine) { - Expects(sender != nullptr); - int left = st::msgServiceMargin.left(); const auto maxwidth = chatWide ? std::min(w, WideChatWidth()) @@ -614,24 +606,47 @@ void MonoforumSenderBar::Paint( p.drawLine(left + use, top, 2 * w, top); } - const auto userpic = st::msgServicePadding.top() - + st::msgServiceFont->height - + st::msgServicePadding.bottom() - - 2 * skip; - const auto available = use - (skip + userpic + skip * 2 + st::msgServicePadding.right()); + const auto sublist = thread->asSublist(); + const auto userpic = sublist + ? (st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip) + : (st::msgServicePadding.left() - 3 * skip); + const auto available = use + - (skip + + userpic + + skip * 2 + + st::topicButtonArrowSkip + + st::msgServicePadding.right()); - sender->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); + if (sublist) { + sublist->sublistPeer()->paintUserpic( + p, + view, + left + skip, + y + st::msgServiceMargin.top() + skip, + userpic); + } + const auto textLeft = left + skip + userpic + skip * 2; + const auto textTop = y + + st::msgServiceMargin.top() + + st::msgServicePadding.top(); p.setFont(st::msgServiceFont); p.setPen(st->msgServiceFg()); text.draw(p, { - .position = { - left + skip + userpic + skip * 2, - y + st::msgServiceMargin.top() + st::msgServicePadding.top(), - }, + .position = { textLeft, textTop }, .availableWidth = available, .elisionLines = 1, }); + + st::topicButtonArrow.paint( + p, + textLeft + available + st::topicButtonArrowPosition.x(), + textTop + st::topicButtonArrowPosition.y(), + w, + st->msgServiceFg()->c); } void ServicePreMessage::init( @@ -1457,7 +1472,7 @@ void Element::validateTextSkipBlock(bool has, int width, int height) { } void Element::previousInBlocksChanged() { - recountMonoforumSenderBarInBlocks(); + recountThreadBarInBlocks(); recountDisplayDateInBlocks(); recountAttachToPreviousInBlocks(); } @@ -1494,7 +1509,7 @@ bool Element::computeIsAttachToPrevious(not_null previous) { if (!Has() && !Has() && !Has() - && !Has()) { + && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1610,12 +1625,12 @@ bool Element::isInOneDayWithPrevious() const { return !data()->isEmpty() && !displayDate(); } -bool Element::displayMonoforumSender() const { - return Has(); +bool Element::displayForumThreadBar() const { + return Has(); } bool Element::isInOneBunchWithPrevious() const { - return !data()->isEmpty() && !displayMonoforumSender(); + return !data()->isEmpty() && !displayForumThreadBar(); } void Element::recountAttachToPreviousInBlocks() { @@ -1636,34 +1651,49 @@ void Element::recountAttachToPreviousInBlocks() { setAttachToPrevious(attachToPrevious, previous); } -void Element::recountMonoforumSenderBarInBlocks() { +void Element::recountThreadBarInBlocks() { const auto item = data(); + const auto topic = item->topic(); const auto sublist = item->savedSublist(); - const auto parentChat = sublist ? sublist->parentChat() : nullptr; - const auto barPeer = [&]() -> PeerData* { + const auto parentChat = (topic && topic->channel()->useSubsectionTabs()) + ? topic->channel().get() + : sublist + ? sublist->parentChat() + : nullptr; + const auto barThread = [&]() -> Data::Thread* { if (!parentChat || isHidden() || item->isEmpty() || item->isSponsored()) { return nullptr; } - const auto sublistPeer = sublist->sublistPeer(); if (const auto previous = previousDisplayedInBlocks()) { const auto prev = previous->data(); - if (const auto prevSublist = prev->savedSublist()) { + if (const auto prevTopic = prev->topic()) { + Assert(prevTopic->channel() == parentChat); + const auto topicRootId = topic->rootId(); + if (prevTopic->rootId() == topicRootId) { + return nullptr; + } + } else if (const auto prevSublist = prev->savedSublist()) { Assert(prevSublist->parentChat() == parentChat); + const auto sublistPeer = sublist->sublistPeer(); if (prevSublist->sublistPeer() == sublistPeer) { return nullptr; } } } - return (sublistPeer == parentChat) ? nullptr : sublistPeer.get(); + return topic + ? (Data::Thread*)topic + : (sublist && sublist->sublistPeer() != parentChat) + ? (Data::Thread*)sublist + : nullptr; }(); - if (barPeer && !Has()) { - AddComponents(MonoforumSenderBar::Bit()); - Get()->init(parentChat, barPeer); - } else if (!barPeer && Has()) { - RemoveComponents(MonoforumSenderBar::Bit()); + if (barThread && !Has()) { + AddComponents(ForumThreadBar::Bit()); + Get()->init(parentChat, barThread); + } else if (!barThread && Has()) { + RemoveComponents(ForumThreadBar::Bit()); } } @@ -2569,4 +2599,60 @@ Window::SessionController *ExtractController(const ClickContext &context) { return nullptr; } +TextSelection FindSearchQueryHighlight( + const QString &text, + const QString &query) { + const auto lower = query.toLower(); + return FindSearchQueryHighlight(text, QStringView(lower)); +} + +TextSelection FindSearchQueryHighlight( + const QString &text, + QStringView lower) { + const auto inside = text.toLower(); + const auto find = [&](QStringView part) { + auto skip = 0; + if (const auto from = inside.indexOf(part, skip); from >= 0) { + if (!from || !inside[from - 1].isLetterOrNumber()) { + return int(from); + } + skip = from + 1; + } + return -1; + }; + if (const auto from = find(lower); from >= 0) { + const auto till = from + lower.size(); + if (till >= inside.size() + || !(inside.begin() + till)->isLetterOrNumber()) { + return { uint16(from), uint16(till) }; + } + } + const auto tillEndOfWord = [&](int from) { + for (auto till = from + 1; till != inside.size(); ++till) { + if (!inside[till].isLetterOrNumber()) { + return TextSelection{ uint16(from), uint16(till) }; + } + } + return TextSelection{ uint16(from), uint16(inside.size()) }; + }; + const auto words = Ui::Text::Words(lower); + for (const auto &word : words) { + const auto length = int(word.size()); + const auto cut = length / 2; + const auto part = word.mid(0, length - cut); + const auto offset = find(part); + if (offset < 0) { + continue; + } + for (auto i = 0; i != cut; ++i) { + const auto part = word.mid(0, length - i); + if (const auto from = find(part); from >= 0) { + return tillEndOfWord(from); + } + } + return tillEndOfWord(offset); + } + return {}; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 34bc8679ce..14453f3324 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -20,6 +20,7 @@ struct HistoryMessageReply; struct PreparedServiceText; namespace Data { +class Thread; struct Reaction; struct ReactionId; } // namespace Data @@ -265,8 +266,10 @@ struct DateBadge : RuntimeComponent { }; -struct MonoforumSenderBar : RuntimeComponent { - void init(not_null parentChat, not_null peer); +struct ForumThreadBar : RuntimeComponent { + void init( + not_null parentChat, + not_null thread); int height() const; void paint( @@ -276,7 +279,7 @@ struct MonoforumSenderBar : RuntimeComponent { int w, bool chatWide, bool skipPatternLine) const; - static void PaintFor( + static int PaintForGetWidth( Painter &p, not_null st, not_null itemView, @@ -285,9 +288,8 @@ struct MonoforumSenderBar : RuntimeComponent { int w, bool chatWide); - PeerData *sender = nullptr; + base::weak_ptr thread; Ui::Text::String text; - ClickHandlerPtr link; mutable Ui::PeerUserpicView view; int width = 0; @@ -295,7 +297,7 @@ private: static void Paint( Painter &p, not_null st, - not_null sender, + not_null thread, const Ui::Text::String &text, int width, Ui::PeerUserpicView &view, @@ -475,7 +477,7 @@ public: [[nodiscard]] bool displayDate() const; [[nodiscard]] bool isInOneDayWithPrevious() const; - [[nodiscard]] bool displayMonoforumSender() const; + [[nodiscard]] bool displayForumThreadBar() const; [[nodiscard]] bool isInOneBunchWithPrevious() const; virtual void draw(Painter &p, const PaintContext &context) const = 0; @@ -688,7 +690,7 @@ protected: std::unique_ptr _reactions; private: - void recountMonoforumSenderBarInBlocks(); + void recountThreadBarInBlocks(); // This should be called only from previousInBlocksChanged() // to add required bits to the Composer mask @@ -755,4 +757,12 @@ private: [[nodiscard]] Window::SessionController *ExtractController( const ClickContext &context); +[[nodiscard]] TextSelection FindSearchQueryHighlight( + const QString &text, + const QString &query); + +[[nodiscard]] TextSelection FindSearchQueryHighlight( + const QString &text, + QStringView lower); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_group_members_widget.cpp b/Telegram/SourceFiles/history/view/history_view_group_members_widget.cpp new file mode 100644 index 0000000000..63b090d16d --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_group_members_widget.cpp @@ -0,0 +1,113 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_group_members_widget.h" + +#include "boxes/peers/edit_participants_box.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +class GroupMembersWidgetController : public ParticipantsBoxController { +public: + using ParticipantsBoxController::ParticipantsBoxController; + +protected: + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override { + return nullptr; + } + +}; + +} // namespace + +GroupMembersWidget::GroupMembersWidget( +not_null parent, +not_null navigation, +not_null peer) +: Ui::RpWidget(parent) +, _show(navigation->uiShow()) +, _listController(std::make_unique( + navigation, + peer, + ParticipantsBoxController::Role::Profile)) { + _listController->setStoriesShown(true); + setupList(); + setContent(_list.data()); + _listController->setDelegate(static_cast(this)); +} + +void GroupMembersWidget::setupList() { + const auto topSkip = 0; + _listController->setStyleOverrides(&st::groupMembersWidgetList); + _listController->setStoriesShown(true); + _list = object_ptr(this, _listController.get()); + widthValue() | rpl::start_with_next([this](int newWidth) { + if (newWidth > 0) { + _list->resizeToWidth(newWidth); + } + }, _list->lifetime()); + _list->heightValue() | rpl::start_with_next([=](int listHeight) { + if (const auto newHeight = topSkip + listHeight; newHeight > 0) { + resize(width(), newHeight); + } + }, _list->lifetime()); + _list->moveToLeft(0, topSkip); +} + +void GroupMembersWidget::peerListSetTitle(rpl::producer title) { +} + +void GroupMembersWidget::peerListSetAdditionalTitle( + rpl::producer title) { +} + +bool GroupMembersWidget::peerListIsRowChecked(not_null row) { + return false; +} + +int GroupMembersWidget::peerListSelectedRowsCount() { + return 0; +} + +void GroupMembersWidget::peerListScrollToTop() { +} + +void GroupMembersWidget::peerListAddSelectedPeerInBunch( + not_null peer) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void GroupMembersWidget::peerListAddSelectedRowInBunch( + not_null row) { + Unexpected("Item selection in Info::Profile::Members."); +} + +void GroupMembersWidget::peerListFinishSelectedRowsBunch() { +} + +std::shared_ptr GroupMembersWidget::peerListUiShow() { + return _show; +} + +void GroupMembersWidget::peerListSetDescription( + object_ptr description) { + description.destroy(); +} + +void GroupMembersWidget::peerListShowRowMenu( + not_null row, + bool highlightRow, + Fn)> destroyed) { +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_group_members_widget.h b/Telegram/SourceFiles/history/view/history_view_group_members_widget.h new file mode 100644 index 0000000000..a16b6d6094 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_group_members_widget.h @@ -0,0 +1,58 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "boxes/peer_list_box.h" + +namespace Window { +class SessionNavigation; +} // namespace Window + +class ParticipantsBoxController; + +namespace HistoryView { + +class GroupMembersWidget + : public Ui::RpWidget + , public PeerListContentDelegate { +public: + GroupMembersWidget( + not_null parent, + not_null navigation, + not_null peer); + +private: + void setupList(); + + // 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; + void peerListShowRowMenu( + not_null row, + bool highlightRow, + Fn)> destroyed = nullptr) override; + // PeerListContentDelegate interface. + + std::shared_ptr _show; + object_ptr _list = { nullptr }; + std::unique_ptr _listController; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_item_preview.h b/Telegram/SourceFiles/history/view/history_view_item_preview.h index 9656d701ff..79ff2ba631 100644 --- a/Telegram/SourceFiles/history/view/history_view_item_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_item_preview.h @@ -41,6 +41,7 @@ struct ItemPreview { struct ToPreviewOptions { const std::vector *existing = nullptr; + QStringView searchLowerText; bool hideSender = false; bool hideCaption = false; bool ignoreMessageText = false; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 0217ef24e3..284d12935e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2001,7 +2001,7 @@ void ListWidget::updateSize() { void ListWidget::resizeToWidth(int newWidth, int minHeight) { _minHeight = minHeight; - TWidget::resizeToWidth(newWidth); + RpWidget::resizeToWidth(newWidth); restoreScrollPosition(); } @@ -3133,7 +3133,7 @@ void ListWidget::touchEvent(QTouchEvent *e) { return; } _touchInProgress = false; - auto weak = Ui::MakeWeak(this); + auto weak = base::make_weak(this); const auto notMoved = (_touchPos - _touchStart).manhattanLength() < QApplication::startDragDistance(); if (_touchSelect) { @@ -3215,7 +3215,7 @@ rpl::producer ListWidget::touchMaybeSelectingValue() const { void ListWidget::enterEventHook(QEnterEvent *e) { _mouseActive = true; mouseActionUpdate(QCursor::pos()); - return TWidget::enterEventHook(e); + return RpWidget::enterEventHook(e); } void ListWidget::leaveEventHook(QEvent *e) { @@ -3235,7 +3235,7 @@ void ListWidget::leaveEventHook(QEvent *e) { setCursor(_cursor); } _mouseActive = false; - return TWidget::leaveEventHook(e); + return RpWidget::leaveEventHook(e); } void ListWidget::updateDragSelection() { @@ -3635,10 +3635,8 @@ void ListWidget::mouseActionFinish( ClickHandlerContext ListWidget::prepareClickHandlerContext(FullMsgId id) { return { .itemId = id, - .elementDelegate = [weak = Ui::MakeWeak(this)] { - return weak - ? (ElementDelegate*)weak - : nullptr; + .elementDelegate = [weak = base::make_weak(this)] { + return (ElementDelegate*)weak.get(); }, .sessionWindow = base::make_weak(controller()), }; @@ -4357,9 +4355,9 @@ void ConfirmForwardSelectedItems(not_null widget) { } } auto ids = widget->getSelectedIds(); - const auto weak = Ui::MakeWeak(widget); + const auto weak = base::make_weak(widget); Window::ShowForwardMessagesBox(widget->controller(), std::move(ids), [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->cancelSelection(); } }); @@ -4388,8 +4386,8 @@ void ConfirmSendNowSelectedItems(not_null widget) { if (!history) { return; } - const auto clearSelection = [weak = Ui::MakeWeak(widget)] { - if (const auto strong = weak.data()) { + const auto clearSelection = [weak = base::make_weak(widget)] { + if (const auto strong = weak.get()) { strong->cancelSelection(); } }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 0db7df609f..84e9f7d4c6 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_message.h" #include "api/api_suggest_post.h" +#include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" @@ -600,15 +601,9 @@ void Message::refreshRightBadge() { _rightBadgeHasBoosts = 1; const auto many = (boosts > 1); - const auto &icon = many - ? st::boostsMessageIcon - : st::boostMessageIcon; - const auto padding = many - ? st::boostsMessageIconPadding - : st::boostMessageIconPadding; - const auto owner = &item->history()->owner(); - auto added = Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji(icon, padding) + auto added = Ui::Text::IconEmoji(many + ? &st::boostsMessageIcon + : &st::boostMessageIcon ).append(many ? QString::number(boosts) : QString()); badge.append(' ').append(Ui::Text::Colorized(added, 1)); } @@ -619,15 +614,10 @@ void Message::refreshRightBadge() { if (badge.empty()) { _rightBadge.clear(); } else { - const auto context = Core::TextContext({ - .session = &item->history()->session(), - .customEmojiLoopLimit = 1, - }); _rightBadge.setMarkedText( st::defaultTextStyle, badge, - Ui::NameTextOptions(), - context); + Ui::NameTextOptions()); } } @@ -1132,10 +1122,13 @@ QSize Message::performCountOptimalSize() { void Message::refreshTopicButton() { const auto item = data(); - if (isAttachedToPrevious() - || delegate()->elementHideTopicButton(this)) { + if (isAttachedToPrevious() || delegate()->elementHideTopicButton(this)) { _topicButton = nullptr; } else if (const auto topic = item->topic()) { + if (topic->channel()->useSubsectionTabs()) { + _topicButton = nullptr; + return; + } if (!_topicButton) { _topicButton = std::make_unique(); } @@ -1174,8 +1167,8 @@ int Message::marginTop() const { result += bar->height(); } } - if (const auto monoforumBar = Get()) { - result += monoforumBar->height(); + if (const auto bar = Get()) { + result += bar->height(); } if (const auto service = Get()) { if (!AyuFeatures::MessageShot::isTakingShot()) { @@ -1225,8 +1218,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (const auto date = Get()) { aboveh += date->height(); } - if (const auto sender = Get()) { - aboveh += sender->height(); + if (const auto bar = Get()) { + aboveh += bar->height(); } if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { p.translate(0, aboveh); @@ -4275,6 +4268,8 @@ ClickHandlerPtr Message::prepareRightActionLink() const { savedFromPeer, Window::SectionShow::Way::Forward, savedFromMsgId); + } else if (base::IsCtrlPressed()) { + FastShareMessageToSelf(controller->uiShow(), item); } else { FastShareMessage(controller, item); } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index b996191334..b33cb25ddc 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -184,6 +184,11 @@ PinnedWidget::PinnedWidget( onScroll(); }, lifetime()); + _inner->scrollKeyEvents( + ) | rpl::start_with_next([=](not_null e) { + _scroll->keyPressEvent(e); + }, lifetime()); + setupClearButton(); setupTranslateBar(); Window::SetupSwipeBackSection(this, _scroll.get(), _inner); diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index d69108bfdd..90cce08d31 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/spoiler_mess.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" @@ -107,21 +108,6 @@ constexpr auto kNonExpandedLinesLimit = 5; return style::colorizeImage(result, white); } -[[nodiscard]] TextWithEntities TaskDoneIcon( - not_null session) { - return Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - MakeTaskDoneImage(), - QMargins(0, st::lineWidth, st::lineWidth, 0))); -} - -[[nodiscard]] TextWithEntities TaskIcon(not_null session) { - return Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - MakeTaskImage(), - QMargins(0, st::lineWidth, st::lineWidth, 0))); -} - } // namespace void ValidateBackgroundEmoji( @@ -324,13 +310,23 @@ void Reply::update( && !fields.quote.empty(); _hasQuoteIcon = hasQuoteIcon ? 1 : 0; - const auto session = &view->history()->session(); + const auto repaint = [=] { item->customEmojiRepaint(); }; + auto helper = Ui::Text::CustomEmojiHelper(Core::TextContext({ + .session = &view->history()->session(), + .repaint = repaint, + })); const auto text = (!_displaying && data->unavailable()) ? TextWithEntities() : task ? Ui::Text::Colorized(task->completionDate - ? TaskDoneIcon(session) - : TaskIcon(session)).append(task->text) + ? helper.image({ + .image = MakeTaskDoneImage(), + .margin = QMargins(0, st::lineWidth, st::lineWidth, 0), + }) + : helper.image({ + .image = MakeTaskImage(), + .margin = QMargins(0, st::lineWidth, st::lineWidth, 0), + })).append(task->text) : (message && (fields.quote.empty() || !fields.manualQuote)) ? message->inReplyText() : !fields.quote.empty() @@ -347,16 +343,11 @@ void Reply::update( .ignoreTopic = true, }).text : TextWithEntities(); - const auto repaint = [=] { item->customEmojiRepaint(); }; - const auto context = Core::TextContext({ - .session = &view->history()->session(), - .repaint = repaint, - }); _text.setMarkedText( st::defaultTextStyle, text, _multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(), - context); + helper.context()); updateName(view, data); @@ -552,15 +543,16 @@ void Reply::updateName( : 0; auto nameFull = TextWithEntities(); if (displayAsExternal && !groupNameAdded && !fields.storyId) { - nameFull.append(PeerEmoji(history, sender)); + nameFull.append(PeerEmoji(sender)); } nameFull.append(name); if (groupNameAdded) { - nameFull.append(' ').append(PeerEmoji(history, externalPeer)); + nameFull.append(' ').append(PeerEmoji(externalPeer)); nameFull.append(externalPeer->name()); } else if (originalNameAdded) { - nameFull.append(' ').append(ForwardEmoji(&history->owner())); - nameFull.append(forwarded->originalSender + nameFull.append(' ').append( + st::historyReplyForward + ).append(forwarded->originalSender ? forwarded->originalSender->name() : forwarded->originalHiddenSenderInfo->name); } @@ -940,34 +932,16 @@ void Reply::stopLastRipple() { } } -TextWithEntities Reply::PeerEmoji( - not_null history, - PeerData *peer) { - return PeerEmoji(&history->owner(), peer); -} - -TextWithEntities Reply::PeerEmoji( - not_null owner, - PeerData *peer) { +TextWithEntities Reply::PeerEmoji(PeerData *peer) { using namespace std; const auto icon = !peer - ? pair(&st::historyReplyUser, st::historyReplyUserPadding) + ? &st::historyReplyUser : peer->isBroadcast() - ? pair(&st::historyReplyChannel, st::historyReplyChannelPadding) + ? &st::historyReplyChannel : (peer->isChannel() || peer->isChat()) - ? pair(&st::historyReplyGroup, st::historyReplyGroupPadding) - : pair(&st::historyReplyUser, st::historyReplyUserPadding); - return Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - *icon.first, - icon.second)); -} - -TextWithEntities Reply::ForwardEmoji(not_null owner) { - return Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - st::historyReplyForward, - st::historyReplyForwardPadding)); + ? &st::historyReplyGroup + : &st::historyReplyUser; + return Ui::Text::IconEmoji(icon); } TextWithEntities Reply::ComposePreviewName( @@ -1000,11 +974,11 @@ TextWithEntities Reply::ComposePreviewName( auto nameFull = TextWithEntities(); using namespace HistoryView; if (displayAsExternal && !groupNameAdded) { - nameFull.append(Reply::PeerEmoji(history, sender)); + nameFull.append(Reply::PeerEmoji(sender)); } nameFull.append(shorten ? sender->shortName() : sender->name()); if (groupNameAdded) { - nameFull.append(' ').append(Reply::PeerEmoji(history, toPeer)); + nameFull.append(' ').append(Reply::PeerEmoji(toPeer)); nameFull.append(toPeer->name()); } return (quote diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h index 3e1a55addc..6b78cbca99 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.h +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -99,14 +99,7 @@ public: return _link; } - [[nodiscard]] static TextWithEntities PeerEmoji( - not_null history, - PeerData *peer); - [[nodiscard]] static TextWithEntities PeerEmoji( - not_null owner, - PeerData *peer); - [[nodiscard]] static TextWithEntities ForwardEmoji( - not_null owner); + [[nodiscard]] static TextWithEntities PeerEmoji(PeerData *peer); [[nodiscard]] static TextWithEntities ComposePreviewName( not_null history, not_null to, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 539fb0471f..8980c4da33 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -1349,9 +1349,9 @@ void ScheduledWidget::showProcessingVideoTooltip() { st::defaultImportantTooltipLabel), st::defaultImportantTooltip); const auto tooltip = _processingVideoTooltip.get(); - const auto weak = QPointer(tooltip); + const auto weak = base::make_weak(tooltip); const auto destroy = [=] { - delete weak.data(); + delete weak.get(); }; tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); tooltip->setHiddenCallback([=] { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index ce283943a0..5c96ee4980 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -527,8 +527,8 @@ int Service::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } - if (const auto monoforumBar = Get()) { - result += monoforumBar->height(); + if (const auto bar = Get()) { + result += bar->height(); } if (const auto service = Get()) { result += service->height; @@ -553,8 +553,8 @@ void Service::draw(Painter &p, const PaintContext &context) const { if (const auto date = Get()) { aboveh += date->height(); } - if (const auto sender = Get()) { - aboveh += sender->height(); + if (const auto bar = Get()) { + aboveh += bar->height(); } if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { p.translate(0, aboveh); diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 19036b8788..51ebf3b9e0 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -237,6 +237,12 @@ void SubsectionTabs::setupSlider( slider->requestShown( ) | rpl::start_with_next([=](Ui::ScrollToRequest request) { const auto full = vertical ? scroll->height() : scroll->width(); + const auto tab = request.ymax - request.ymin; + if (tab < full) { + const auto add = std::min(full - tab, tab) / 2; + request.ymax += add; + request.ymin -= add; + } const auto scrollValue = vertical ? scroll->scrollTop() : scroll->scrollLeft(); @@ -510,6 +516,10 @@ void SubsectionTabs::toggleModes() { _layoutRequests.fire({}); } +bool SubsectionTabs::dying() const { + return !UsedFor(_history); +} + rpl::producer<> SubsectionTabs::removeRequests() const { if (const auto forum = _history->peer->forum()) { return forum->destroyed(); @@ -728,7 +738,7 @@ void SubsectionTabs::refreshSlice() { } return thread->chatListBadgesState(); }(); - if (topic) { + if (topic && badges.unreadCounter <= 0) { // Don't show the small indicators for non-visited unread topics. badges.unread = false; } diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index 75a37104ea..d0d78f140c 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -49,6 +49,7 @@ public: [[nodiscard]] static bool UsedFor(not_null thread); + [[nodiscard]] bool dying() const; [[nodiscard]] rpl::producer<> removeRequests() const; void extractToParent(not_null parent); 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 cb6ac38c1c..43a6d55022 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -365,8 +365,8 @@ bool TopBarWidget::createMenu(not_null button) { this, st::popupMenuExpandedSeparator); _menu->setDestroyedCallback([ - weak = Ui::MakeWeak(this), - weakButton = Ui::MakeWeak(button), + weak = base::make_weak(this), + weakButton = base::make_weak(button), menu = _menu.get()] { if (weak && weak->_menu == menu) { if (weakButton) { @@ -862,9 +862,12 @@ void TopBarWidget::setActiveChat( if (const auto channel = peer->asChannel()) { if (channel->canEditStories() - && !channel->owner().stories().archiveCountKnown( - channel->id)) { - channel->owner().stories().archiveLoadMore(channel->id); + && !channel->owner().stories().albumIdsCountKnown( + channel->id, + Data::kStoriesAlbumIdArchive)) { + channel->owner().stories().albumIdsLoadMore( + channel->id, + Data::kStoriesAlbumIdArchive); } } } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 3d9cc9de51..065cf1601f 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -238,7 +238,7 @@ private: object_ptr _admins; base::unique_qptr _menu; - object_ptr _membersShowArea = { nullptr }; + object_ptr _membersShowArea = { nullptr }; rpl::event_stream _membersShowAreaActive; float64 _narrowRatio = 0.; diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 11e9cc8d7a..c263c5f9ee 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -409,8 +409,8 @@ base::unique_qptr TranslateBar::createMenu( st::popupMenuExpandedSeparator); result->setDestroyedCallback([ this, - weak = Ui::MakeWeak(&_wrap), - weakButton = Ui::MakeWeak(button), + weak = base::make_weak(&_wrap), + weakButton = base::make_weak(button), menu = result.get() ] { if (weak && _menu == menu) { @@ -430,7 +430,7 @@ void TranslateBar::showMenu(base::unique_qptr menu) { _menu = std::move(menu); _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - const auto guard = Ui::MakeWeak(&_wrap); + const auto guard = base::make_weak(&_wrap); const auto now = _history->translatedTo(); const auto to = now ? now : Ui::ChooseTranslateTo(_history); const auto weak = base::make_weak(_controller); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 52398fa7c0..deb64fccec 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "storage/localstorage.h" +#include "lottie/lottie_icon.h" #include "main/main_session.h" #include "media/player/media_player_float.h" // Media::Player::RoundPainter. #include "media/audio/media_audio.h" @@ -25,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_common.h" #include "ui/text/format_values.h" #include "ui/text/format_song_document_name.h" +#include "ui/text/text_lottie_custom_emoji.h" #include "ui/text/text_utilities.h" #include "ui/chat/chat_style.h" #include "ui/painter.h" @@ -427,6 +429,7 @@ QSize Document::countOptimalSize() { const auto transcribes = &session->api().transcribes(); if (_parent->data()->media()->ttlSeconds() || _realParent->isScheduled() + || _realParent->isAdminLogEntry() || (!session->premium() && !transcribes->freeFor(_realParent) && !transcribes->trialsSupport()) @@ -446,15 +449,25 @@ QSize Document::countOptimalSize() { voice->transcribe->setLoading( entry.shown && (entry.requestId || entry.pending), update); + const auto pending = entry.pending; + auto descriptor = pending + ? Lottie::IconDescriptor{ + .name = u"transcribe_loading"_q, + .color = &st::historyTextInFg, + .sizeOverride = Size(st::historyTranscribeLoadingSize), + .colorizeUsingAlpha = true, + } + : Lottie::IconDescriptor(); auto text = (entry.requestId || !entry.shown) ? TextWithEntities() : entry.toolong ? Ui::Text::Italic(tr::lng_audio_transcribe_long(tr::now)) : entry.failed ? Ui::Text::Italic(tr::lng_attach_failed(tr::now)) - : TextWithEntities{ - entry.result + (entry.pending ? " [...]" : ""), - }; + : TextWithEntities{ entry.result }.append( + pending + ? Ui::Text::LottieEmoji(descriptor) + : TextWithEntities()); voice->transcribe->setOpened( !text.empty(), creating ? Fn() : update); @@ -467,7 +480,11 @@ QSize Document::countOptimalSize() { voice->transcribeText = Ui::Text::String(minResizeWidth); voice->transcribeText.setMarkedText( st::messageTextStyle, - text); + text, + kMarkupTextOptions, + pending + ? Ui::Text::LottieEmojiContext(std::move(descriptor)) + : Ui::Text::MarkedContext()); hasTranscribe = true; if (const auto skipBlockWidth = _parent->hasVisibleText() ? 0 diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 1a99393ab3..e6edbbf78a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -2213,6 +2213,7 @@ void Gif::ensureTranscribeButton() const { if (_data->isVideoMessage() && !_parent->data()->media()->ttlSeconds() && !_parent->data()->isScheduled() + && !_parent->data()->isAdminLogEntry() && (_data->session().premium() || _data->session().api().transcribes().trialsSupport())) { if (!_transcribe) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 8741c51f69..ae837b3dc1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -241,14 +241,12 @@ void Media::drawPurchasedTag( if (!amount) { return; } - const auto session = &item->history()->session(); - auto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall(session)); + auto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall()); text.append(Lang::FormatCountDecimal(amount)); purchased->text.setMarkedText( st::defaultTextStyle, text, - kMarkupTextOptions, - Core::TextContext({ .session = session })); + kMarkupTextOptions); } const auto st = context.st; @@ -403,8 +401,7 @@ void Media::drawSpoilerTag( tr::lng_sensitive_tag(tr::now)); iconSkip = st::mediaMenuIconStealth.width() * 1.4; } else { - const auto session = &history()->session(); - auto price = Ui::Text::Colorized(Ui::CreditsEmoji(session)); + auto price = Ui::Text::Colorized(Ui::CreditsEmoji()); price.append(Lang::FormatCountDecimal(tag->price)); text.setMarkedText( st::semiboldTextStyle, @@ -413,8 +410,7 @@ void Media::drawSpoilerTag( lt_price, price, Ui::Text::WithEntities), - kMarkupTextOptions, - Core::TextContext({ .session = session })); + kMarkupTextOptions); } const auto width = iconSkip + text.maxWidth(); const auto inner = QRect(0, 0, width, text.minHeight()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp index b899d9fc98..8a9cec6910 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_sensitive_content.h" #include "api/api_views.h" #include "apiwrap.h" +#include "inline_bots/bot_attach_web_view.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" @@ -18,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" #include "ui/painter.h" +#include "core/application.h" #include "core/click_handler_types.h" #include "data/data_document.h" #include "data/data_session.h" @@ -34,18 +36,76 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "mainwindow.h" #include "media/streaming/media_streaming_utility.h" #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" +#include "settings/settings_common.h" +#include "webrtc/webrtc_environment.h" +#include "webview/webview_interface.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" namespace HistoryView { namespace { constexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000); +const auto kVerifyAgeAboutPrefix = "cloud_lng_age_verify_about_"; + +rpl::producer AgeVerifyAbout( + not_null session) { + const auto appConfig = &session->appConfig(); + return rpl::single( + rpl::empty + ) | rpl::then( + Lang::Updated() + ) | rpl::map([=] { + const auto country = appConfig->ageVerifyCountry().toLower(); + const auto age = appConfig->ageVerifyMinAge(); + const auto [shift, string] = Lang::Plural( + Lang::kPluralKeyBaseForCloudValue, + age, + lt_count); + const auto postfixes = { + "#zero", + "#one", + "#two", + "#few", + "#many", + "#other" + }; + Assert(shift >= 0 && shift < postfixes.size()); + const auto postfix = *(begin(postfixes) + shift); + return Ui::Text::RichLangValue(Lang::GetNonDefaultValue( + kVerifyAgeAboutPrefix + country.toUtf8() + postfix + ).replace(u"{count}"_q, string)); + }); +} + +[[nodiscard]] object_ptr AgeVerifyIcon( + not_null parent) { + const auto padding = st::settingsAgeVerifyIconPadding; + const auto full = st::settingsAgeVerifyIcon.size().grownBy(padding); + auto result = object_ptr(parent); + const auto raw = result.data(); + raw->resize(full); + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + const auto x = (raw->width() - full.width()) / 2; + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::windowBgActive); + p.setPen(Qt::NoPen); + const auto inner = QRect(QPoint(x, 0), full); + p.drawEllipse(inner); + st::settingsAgeVerifyIcon.paintInCenter(p, inner); + }, raw->lifetime()); + return result; +} } // namespace @@ -278,61 +338,266 @@ ClickHandlerPtr MakePaidMediaLink(not_null item) { }); } +void ShowAgeVerification( + std::shared_ptr show, + not_null bot, + Fn reveal) { + show->show(Box([=](not_null box) { + box->setNoContentMargin(true); + box->setStyle(st::settingsAgeVerifyBox); + box->setWidth(st::boxWideWidth); + + box->addRow(AgeVerifyIcon(box), st::settingsAgeVerifyIconMargin); + + box->addRow( + object_ptr( + box, + tr::lng_age_verify_title(), + st::settingsAgeVerifyTitle), + st::boxRowPadding + st::settingsAgeVerifyMargin, + style::al_top); + box->addRow( + object_ptr( + box, + AgeVerifyAbout(&bot->session()), + st::settingsAgeVerifyText), + st::boxRowPadding + st::settingsAgeVerifyMargin, + style::al_top); + box->addRow( + object_ptr( + box, + tr::lng_age_verify_here(Ui::Text::RichLangValue), + st::settingsAgeVerifyText), + st::boxRowPadding + st::settingsAgeVerifyMargin, + style::al_top); + + const auto weak = QPointer(box); + const auto done = crl::guard(&bot->session(), [=](int age) { + const auto min = bot->session().appConfig().ageVerifyMinAge(); + if (age >= min) { + reveal(); + bot->session().api().sensitiveContent().update(true); + } else { + show->showToast({ + .title = tr::lng_age_verify_sorry_title(tr::now), + .text = { tr::lng_age_verify_sorry_text(tr::now) }, + .duration = Ui::Toast::kDefaultDuration * 3, + }); + } + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + const auto button = box->addButton(tr::lng_age_verify_button(), [=] { + bot->session().attachWebView().open({ + .bot = bot, + .parentShow = box->uiShow(), + .context = { .maySkipConfirmation = true }, + .source = InlineBots::WebViewSourceAgeVerification{ + .done = done, + }, + }); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::settingsAgeVerifyBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + + const auto close = Ui::CreateChild( + box.get(), + st::boxTitleClose); + close->setClickedCallback([=] { + box->closeBox(); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0); + }, box->lifetime()); + crl::on_main(close, [=] { close->raise(); }); + })); +} + +void ShowAgeVerificationMobile( + std::shared_ptr show, + not_null session) { + show->show(Box([=](not_null box) { + box->setTitle(tr::lng_age_verify_title()); + box->setWidth(st::boxWideWidth); + + const auto size = st::settingsCloudPasswordIconSize; + auto icon = Settings::CreateLottieIcon( + box->verticalLayout(), + { + .name = u"phone"_q, + .sizeOverride = { size, size }, + }, + st::peerAppearanceIconPadding); + + box->showFinishes( + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::once); + }, box->lifetime()); + + box->addRow(std::move(icon.widget)); + + box->addRow( + object_ptr( + box, + AgeVerifyAbout(session), + st::settingsAgeVerifyText), + st::boxRowPadding + st::settingsAgeVerifyMargin, + style::al_top); + box->addRow( + object_ptr( + box, + tr::lng_age_verify_mobile(Ui::Text::RichLangValue), + st::settingsAgeVerifyText), + st::boxRowPadding + st::settingsAgeVerifyMargin, + style::al_top); + + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + })); +} + +void ShowAgeVerificationRequired( + std::shared_ptr show, + not_null session, + Fn reveal) { + struct State { + Fn check; + rpl::lifetime lifetime; + std::optional bot; + }; + const auto state = std::make_shared(); + const auto username = session->appConfig().ageVerifyBotUsername(); + const auto bot = session->data().peerByUsername(username); + if (username.isEmpty() || bot) { + state->bot = bot; + } else { + session->api().request(MTPcontacts_ResolveUsername( + MTP_flags(0), + MTP_string(username), + MTPstring() + )).done([=](const MTPcontacts_ResolvedPeer &result) { + const auto &data = result.data(); + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + const auto botId = peerFromMTP(data.vpeer()); + state->bot = session->data().peerLoaded(botId); + state->check(); + }).fail([=] { + state->bot = nullptr; + state->check(); + }).send(); + } + state->check = [=] { + const auto sensitive = &session->api().sensitiveContent(); + const auto ready = sensitive->loaded(); + if (!ready) { + state->lifetime = sensitive->loadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next(state->check); + return; + } else if (!state->bot.has_value()) { + return; + } + const auto has = Core::App().mediaDevices().recordAvailability(); + const auto available = Webview::Availability(); + const auto bot = (*state->bot)->asUser(); + if (available.error == Webview::Available::Error::None + && has == Webrtc::RecordAvailability::VideoAndAudio + && sensitive->canChangeCurrent() + && bot + && bot->isBot() + && bot->botInfo->hasMainApp) { + ShowAgeVerification(show, bot, reveal); + } else { + ShowAgeVerificationMobile(show, session); + } + state->lifetime.destroy(); + state->check = nullptr; + }; + state->check(); +} + +void ShowSensitiveConfirm( + std::shared_ptr show, + not_null session, + Fn reveal) { + show->show(Box([=](not_null box) { + struct State { + rpl::variable canChange; + Ui::Checkbox *checkbox = nullptr; + }; + const auto state = box->lifetime().make_state(); + const auto sensitive = &session->api().sensitiveContent(); + state->canChange = sensitive->canChange(); + const auto done = [=](Fn close) { + if (state->canChange.current() + && state->checkbox->checked()) { + show->showToast({ + .text = tr::lng_sensitive_toast( + tr::now, + Ui::Text::RichLangValue), + .adaptive = true, + .duration = 5 * crl::time(1000), + }); + sensitive->update(true); + } else { + reveal(); + } + close(); + }; + Ui::ConfirmBox(box, { + .text = tr::lng_sensitive_text(Ui::Text::RichLangValue), + .confirmed = done, + .confirmText = tr::lng_sensitive_view(), + .title = tr::lng_sensitive_title(), + }); + const auto skip = st::defaultCheckbox.margin.bottom(); + const auto wrap = box->addRow( + object_ptr>( + box, + object_ptr( + box, + tr::lng_sensitive_always(tr::now), + false)), + st::boxRowPadding + QMargins(0, 0, 0, skip)); + wrap->toggleOn(state->canChange.value()); + wrap->finishAnimating(); + state->checkbox = wrap->entity(); + })); +} + ClickHandlerPtr MakeSensitiveMediaLink( ClickHandlerPtr reveal, not_null item) { const auto session = &item->history()->session(); + session->api().sensitiveContent().preload(); + return std::make_shared([=](ClickContext context) { + const auto plain = [reveal, context] { + if (const auto raw = reveal.get()) { + raw->onClick(context); + } + }; const auto my = context.other.value(); const auto controller = my.sessionWindow.get(); const auto show = controller ? controller->uiShow() : my.show; if (!show) { - reveal->onClick(context); - return; + plain(); + } else if (session->appConfig().ageVerifyNeeded()) { + ShowAgeVerificationRequired(show, session, plain); + } else { + ShowSensitiveConfirm(show, session, plain); } - show->show(Box([=](not_null box) { - struct State { - rpl::variable canChange; - Ui::Checkbox *checkbox = nullptr; - }; - const auto state = box->lifetime().make_state(); - const auto sensitive = &session->api().sensitiveContent(); - state->canChange = sensitive->canChange(); - const auto done = [=](Fn close) { - if (state->canChange.current() - && state->checkbox->checked()) { - show->showToast({ - .text = tr::lng_sensitive_toast( - tr::now, - Ui::Text::RichLangValue), - .adaptive = true, - .duration = 5 * crl::time(1000), - }); - sensitive->update(true); - } else { - reveal->onClick(context); - } - close(); - }; - Ui::ConfirmBox(box, { - .text = tr::lng_sensitive_text(Ui::Text::RichLangValue), - .confirmed = done, - .confirmText = tr::lng_sensitive_view(), - .title = tr::lng_sensitive_title(), - }); - const auto skip = st::defaultCheckbox.margin.bottom(); - const auto wrap = box->addRow( - object_ptr>( - box, - object_ptr( - box, - tr::lng_sensitive_always(tr::now), - false)), - st::boxRowPadding + QMargins(0, 0, 0, skip)); - wrap->toggleOn(state->canChange.value()); - wrap->finishAnimating(); - state->checkbox = wrap->entity(); - })); }); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_common.h b/Telegram/SourceFiles/history/view/media/history_view_media_common.h index 03646372f5..be8fdba808 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_common.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_common.h @@ -19,10 +19,18 @@ namespace Data { class Media; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace Media::Streaming { struct ExpandDecision; } // namespace Media::Streaming +namespace Ui { +class Show; +} // namespace Ui + namespace HistoryView { class Media; @@ -75,6 +83,11 @@ void PaintInterpolatedIcon( int newWidth, int maxWidth); +void ShowAgeVerificationRequired( + std::shared_ptr show, + not_null session, + Fn reveal); + [[nodiscard]] ClickHandlerPtr MakePaidMediaLink( not_null item); [[nodiscard]] ClickHandlerPtr MakeSensitiveMediaLink( 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 bcbcaf70e2..68ac6ee543 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -504,8 +504,8 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { } const auto data = *gift; const auto itemId = item->fullId(); - const auto openInsteadId = data.upgradeMsgId - ? Data::SavedStarGiftId::User(data.upgradeMsgId) + const auto openInsteadId = data.realGiftMsgId + ? Data::SavedStarGiftId::User(data.realGiftMsgId) : (data.channel && data.channelSavedId) ? Data::SavedStarGiftId::Chat(data.channel, data.channelSavedId) : Data::SavedStarGiftId(); @@ -518,14 +518,7 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { return; } const auto quick = [=](not_null window) { - const auto item = window->session().data().message(itemId); - if (item) { - window->show(Box( - Settings::StarGiftViewBox, - window, - data, - item)); - } + Settings::ShowStarGiftViewBox(window, data, itemId); }; if (!openInsteadId) { quick(controller); @@ -551,12 +544,7 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { if (list.empty()) { quick(window); } else if (auto parsed = Api::FromTL(owner, list[0])) { - window->show(Box( - Settings::SavedStarGiftBox, - window, - owner, - *parsed, - nullptr)); + Settings::ShowSavedStarGiftBox(window, owner, *parsed); } } }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_save_document_action.cpp b/Telegram/SourceFiles/history/view/media/history_view_save_document_action.cpp new file mode 100644 index 0000000000..7e9e41c495 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_save_document_action.cpp @@ -0,0 +1,126 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_save_document_action.h" + +#include "base/call_delayed.h" +#include "data/data_document.h" +#include "data/data_file_click_handler.h" +#include "data/data_file_origin.h" +#include "data/data_peer.h" +#include "data/data_saved_music.h" +#include "data/data_session.h" +#include "history/view/history_view_context_menu.h" +#include "history/view/history_view_list_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.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 "window/window_peer_menu.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" +#include "styles/style_widgets.h" + +namespace HistoryView { + +void AddSaveDocumentAction( + const Ui::Menu::MenuCallback &addAction, + not_null item, + not_null document, + not_null controller) { + const auto contextId = item->fullId(); + const auto fromSaved = item->history()->peer->isSelf(); + const auto savedMusic = &document->owner().savedMusic(); + const auto show = controller->uiShow(); + const auto inProfile = savedMusic->has(document); + const auto &ripple = st::defaultDropdownMenu.menu.ripple; + const auto duration = ripple.hideDuration; + const auto saveAs = base::fn_delayed(duration, controller, [=] { + DocumentSaveClickHandler::SaveAndTrack( + contextId, + document, + DocumentSaveClickHandler::Mode::ToNewFile); + }); + if (!document->isMusicForProfile() || (fromSaved && inProfile)) { + const auto text = document->isVideoFile() + ? tr::lng_context_save_video(tr::now) + : document->isVoiceMessage() + ? tr::lng_context_save_audio(tr::now) + : document->isAudioFile() + ? tr::lng_context_save_audio_file(tr::now) + : document->sticker() + ? tr::lng_context_save_image(tr::now) + : tr::lng_context_save_file(tr::now); + addAction(text, saveAs, &st::menuIconDownload); + return; + } + const auto fill = [&](not_null menu) { + if (!inProfile) { + const auto saved = [=] { + savedMusic->save(document, contextId); + show->showToast(tr::lng_saved_music_added(tr::now)); + }; + menu->addAction( + tr::lng_context_save_music_profile(tr::now), + saved, + &st::menuIconProfile); + } + if (!fromSaved) { + menu->addAction( + tr::lng_context_save_music_saved(tr::now), + [=] { Window::ForwardToSelf(show, { { contextId } }); }, + &st::menuIconSavedMessages); + } + menu->addAction( + tr::lng_context_save_music_folder(tr::now), + saveAs, + &st::menuIconDownload); + + menu->addSeparator(&st::expandedMenuSeparator); + + auto item = base::make_unique_q( + menu, + st::saveMusicInfoMenu, + st::historyHasCustomEmoji, + QPoint( + st::saveMusicInfoMenu.itemPadding.left(), + st::saveMusicInfoMenu.itemPadding.top()), + TextWithEntities{ tr::lng_context_save_music_about(tr::now) }); + item->setAttribute(Qt::WA_TransparentForMouseEvents); + + item->setPointerCursor(false); + menu->addAction(std::move(item)); + }; + addAction(Ui::Menu::MenuCallback::Args{ + .text = tr::lng_context_save_music_to(tr::now), + .handler = nullptr, + .icon = &st::menuIconSoundAdd, + .fillSubmenu = fill, + .submenuSt = &st::popupMenuWithIcons, + }); +} + +void AddSaveDocumentAction( + not_null menu, + HistoryItem *item, + not_null document, + not_null list) { + if (!item || list->hasCopyMediaRestriction(item) || ItemHasTtl(item)) { + return; + } + AddSaveDocumentAction( + Ui::Menu::CreateAddActionCallback(menu), + item, + document, + list->controller()); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_save_document_action.h b/Telegram/SourceFiles/history/view/media/history_view_save_document_action.h new file mode 100644 index 0000000000..d4893fc571 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_save_document_action.h @@ -0,0 +1,41 @@ +/* +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 DocumentData; +class HistoryItem; + +namespace Ui { +class PopupMenu; +} // namespace Ui + +namespace Ui::Menu { +struct MenuCallback; +} // namespace Ui::Menu + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView { + +class ListWidget; + +void AddSaveDocumentAction( + const Ui::Menu::MenuCallback &addAction, + not_null item, + not_null document, + not_null controller); + +void AddSaveDocumentAction( + not_null menu, + HistoryItem *item, + not_null document, + not_null list); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index b8f4fd9f62..ed411a0d34 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_sticker_player.h" +#include "lang/lang_keys.h" #include "ui/image/image.h" #include "ui/chat/chat_style.h" #include "ui/effects/path_shift_gradient.h" @@ -33,6 +34,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "chat_helpers/stickers_lottie.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_menu_icons.h" // AyuGram includes #include "history/view/media/history_view_media.h" @@ -91,7 +94,8 @@ Sticker::Sticker( , _data(data) , _replacements(replacements) , _cachingTag(ChatHelpers::StickerLottieSize::MessageHistory) -, _skipPremiumEffect(skipPremiumEffect) { +, _skipPremiumEffect(skipPremiumEffect) +, _sensitiveBlurred(parent->data()->isMediaSensitive()) { if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); } else { @@ -177,7 +181,7 @@ bool Sticker::readyToDrawAnimationFrame() { return true; } const auto sticker = _data->sticker(); - if (!sticker) { + if (!sticker || _sensitiveBlurred) { return false; } @@ -239,6 +243,43 @@ void Sticker::draw( || !paintPixmap(p, context, r)) { paintPath(p, context, r); } + if (_sensitiveBlurred) { + paintSensitiveTag(p, context, r); + } +} + +void Sticker::paintSensitiveTag( + Painter &p, + const PaintContext &context, + const QRect &r) { + auto text = Ui::Text::String(); + auto iconSkip = 0; + text.setText( + st::semiboldTextStyle, + tr::lng_sensitive_tag(tr::now)); + iconSkip = st::mediaMenuIconStealth.width() * 1.4; + const auto width = iconSkip + text.maxWidth(); + const auto inner = QRect(0, 0, width, text.minHeight()); + const auto outer = style::centerrect( + r, + inner.marginsAdded(st::paidTagPadding)); + const auto size = outer.size(); + const auto real = outer.marginsRemoved(st::paidTagPadding); + const auto radius = std::min(size.width(), size.height()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(context.st->msgServiceBg()); + p.drawRoundedRect(outer, radius, radius); + p.setPen(context.st->msgServiceFg()); + if (iconSkip) { + st::mediaMenuIconStealth.paint( + p, + real.x(), + (outer.y() + + (size.height() - st::mediaMenuIconStealth.height()) / 2), + outer.width(), + context.st->msgServiceFg()->c); + } + text.draw(p, real.x() + iconSkip, real.y(), width); } ClickHandlerPtr Sticker::link() { @@ -417,8 +458,13 @@ QPixmap Sticker::paintedPixmap(const PaintContext &context) const { : context.selected() ? &context.st->msgStickerOverlay() : nullptr; - const auto good = _dataMedia->goodThumbnail(); - if (const auto image = _dataMedia->getStickerLarge()) { + const auto good = _sensitiveBlurred + ? nullptr + : _dataMedia->goodThumbnail(); + const auto image = _sensitiveBlurred + ? nullptr + : _dataMedia->getStickerLarge(); + if (image) { return image->pix(useSize, { .colored = colored, .options = roundOptions }); // // Inline thumbnails can't have alpha channel. @@ -459,7 +505,9 @@ void Sticker::refreshLink() { return; } const auto sticker = _data->sticker(); - if (emojiSticker()) { + if (_sensitiveBlurred) { + _link = MakeSensitiveMediaLink(nullptr, _parent->data()); + } else if (emojiSticker()) { const auto weak = base::make_weak(this); _link = std::make_shared([weak] { if (const auto that = weak.get()) { @@ -535,7 +583,7 @@ void Sticker::dataMediaCreated() const { void Sticker::setDiceIndex(const QString &emoji, int index) { _diceEmoji = emoji; _diceIndex = index; - _playingOnce = (index >= 0); + _playingOnce = (index > 0); _stopOnLastFrame = (index > 0); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index 629d330311..c975a5c1d0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -111,6 +111,10 @@ private: void paintPath(Painter &p, const PaintContext &context, const QRect &r); [[nodiscard]] QPixmap paintedPixmap(const PaintContext &context) const; [[nodiscard]] bool mirrorHorizontal() const; + void paintSensitiveTag( + Painter &p, + const PaintContext &context, + const QRect &r); void ensureDataMediaCreated() const; void dataMediaCreated() const; @@ -145,6 +149,7 @@ private: bool _webpagePart : 1 = false; bool _playingOnce : 1 = false; bool _stopOnLastFrame : 1 = false; + bool _sensitiveBlurred : 1 = false; }; 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 16ca49083b..84add9cfb5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "boxes/background_preview_box.h" +#include "boxes/star_gift_box.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_sticker_player_abstract.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_session.h" @@ -27,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" @@ -37,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "window/themes/window_theme.h" #include "styles/style_chat.h" +#include "styles/style_credits.h" namespace HistoryView { namespace { @@ -605,4 +609,172 @@ void ThemeDocumentBox::unloadHeavyPart() { } } +GiftThemeBox::GiftThemeBox( + not_null parent, + not_null gift) +: _parent(parent) +, _data(*gift->gift()) { +} + +GiftThemeBox::~GiftThemeBox() = default; + +int GiftThemeBox::top() { + return st::msgServiceStarGiftStickerTop; +} + +int GiftThemeBox::width() { + return _data.stargiftReleasedBy + ? st::msgServiceStarGiftByWidth + : st::msgServiceStarGiftBoxWidth; +} + +QSize GiftThemeBox::size() { + return QSize( + st::msgServiceGiftThemeStickerSize, + st::msgServiceGiftThemeStickerSize).grownBy( + st::msgServiceGiftThemeStickerPadding); +} + +TextWithEntities GiftThemeBox::title() { + return {}; +} + +TextWithEntities GiftThemeBox::subtitle() { + const auto giftName = Ui::Text::Bold( + Data::UniqueGiftName(*_data.unique)); + if (_parent->data()->out()) { + return tr::lng_action_you_gift_theme_changed( + tr::now, + lt_name, + giftName, + Ui::Text::WithEntities); + } else { + return tr::lng_action_gift_theme_changed( + tr::now, + lt_from, + Ui::Text::Bold(_parent->data()->from()->shortName()), + lt_name, + giftName, + Ui::Text::WithEntities); + } + return _parent->data()->originalText(); +} + +rpl::producer GiftThemeBox::button() { + return tr::lng_sticker_premium_view(); +} + +ClickHandlerPtr GiftThemeBox::createViewLink() { + return std::make_shared( + u"tg://nft?slug="_q + _data.unique->slug); +} + +int GiftThemeBox::buttonSkip() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +void GiftThemeBox::cacheUniqueBackground(int width, int height) { + if (!_patternEmoji) { + const auto session = &_parent->data()->history()->session(); + _patternEmoji = session->data().customEmojiManager().create( + _data.unique->pattern.document, + [=] { _parent->repaint(); }, + Data::CustomEmojiSizeTag::Large); + [[maybe_unused]] const auto preload = _patternEmoji->ready(); + } + const auto inner = QRect(0, 0, width, height); + const auto ratio = style::DevicePixelRatio(); + if (_backgroundCache.size() != inner.size() * ratio) { + _backgroundCache = QImage( + inner.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + _backgroundCache.fill(Qt::transparent); + _backgroundCache.setDevicePixelRatio(ratio); + + const auto radius = st::giftBoxGiftRadius; + auto p = QPainter(&_backgroundCache); + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient(inner.center(), inner.width() / 2); + gradient.setStops({ + { 0., _data.unique->backdrop.centerColor }, + { 1., _data.unique->backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRoundedRect(inner, radius, radius); + _backroundPatterned = false; + } + if (!_backroundPatterned && _patternEmoji->ready()) { + _backroundPatterned = true; + auto p = QPainter(&_backgroundCache); + p.setClipRect(inner); + const auto skip = inner.width() / 3; + Ui::PaintPoints( + p, + Ui::PatternPointsSmall(), + _patternCache, + _patternEmoji.get(), + *_data.unique, + QRect(-skip, 0, inner.width() + 2 * skip, inner.height())); + } +} + +void GiftThemeBox::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + cacheUniqueBackground(geometry.width(), geometry.height()); + p.drawImage(geometry.topLeft(), _backgroundCache); + + if (_sticker) { + _sticker->draw( + p, + context, + geometry.marginsRemoved(st::msgServiceGiftThemeStickerPadding)); + } else { + ensureStickerCreated(); + } +} + +bool GiftThemeBox::hideServiceText() { + return true; +} + +void GiftThemeBox::stickerClearLoopPlayed() { + if (_sticker) { + _sticker->stickerClearLoopPlayed(); + } +} + +std::unique_ptr GiftThemeBox::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return _sticker + ? _sticker->stickerTakePlayer(data, replacements) + : nullptr; +} + +bool GiftThemeBox::hasHeavyPart() { + return (_sticker ? _sticker->hasHeavyPart() : false); +} + +void GiftThemeBox::unloadHeavyPart() { + if (_sticker) { + _sticker->unloadHeavyPart(); + } +} + +void GiftThemeBox::ensureStickerCreated() const { + if (_sticker) { + return; + } + const auto document = _data.unique->model.document; + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + _sticker.emplace(_parent, document, false, _parent); + _sticker->setPlayingOnce(true); + _sticker->initSize(st::msgServiceGiftThemeStickerSize); + _parent->repaint(); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index 2c7d515d9e..d2f4079c30 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -9,12 +9,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_file.h" #include "history/view/media/history_view_service_box.h" +#include "history/view/media/history_view_sticker.h" class Image; namespace Data { class DocumentMedia; class WallPaper; +class MediaGiftBox; +struct GiftCode; } // namespace Data namespace HistoryView { @@ -128,4 +131,47 @@ private: }; +class GiftThemeBox final : public ServiceBoxContent { +public: + GiftThemeBox( + not_null parent, + not_null gift); + ~GiftThemeBox(); + + int top() override; + int width() override; + QSize size() override; + TextWithEntities title() override; + TextWithEntities subtitle() override; + rpl::producer button() override; + int buttonSkip() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override; + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + void ensureStickerCreated() const; + void cacheUniqueBackground(int width, int height); + + const not_null _parent; + const Data::GiftCode &_data; + std::unique_ptr _patternEmoji; + QImage _backgroundCache; + base::flat_map _patternCache; + bool _backroundPatterned = false; + mutable std::optional _sticker; + +}; + } // namespace HistoryView 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 c20ebcf45b..b0c393383c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -236,14 +236,13 @@ constexpr auto kSponsoredUserpicLines = 2; ? tr::lng_view_button_emojipack(tr::now) : (type == WebPageType::StickerSet) ? tr::lng_view_button_stickerset(tr::now) + : (type == WebPageType::StoryAlbum) + ? tr::lng_view_button_storyalbum(tr::now) + : (type == WebPageType::GiftCollection) + ? tr::lng_view_button_collection(tr::now) : QString()); if (page->iv) { - const auto manager = &page->owner().customEmojiManager(); - const auto &icon = st::historyIvIcon; - const auto padding = st::historyIvIconPadding; - return Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji(icon, padding) - ).append(text); + return Ui::Text::IconEmoji(&st::historyIvIcon).append(text); } return { text }; } @@ -274,7 +273,9 @@ constexpr auto kSponsoredUserpicLines = 2; || ((type == WebPageType::WallPaper) && webpage->document && webpage->document->isWallPaper()) - || (type == WebPageType::StickerSet); + || (type == WebPageType::StickerSet) + || (type == WebPageType::StoryAlbum) + || (type == WebPageType::GiftCollection); } } // namespace 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 620ff8ada9..89747650f5 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -35,7 +35,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/labels.h" #include "ui/widgets/slider_natural_width.h" #include "ui/wrap/slide_wrap.h" diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp index 89dcda70a3..b134eaf526 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp @@ -393,11 +393,7 @@ object_ptr MakeLinkLabel( const QString &link, const style::InputField *stOverride) { const auto &st = stOverride ? *stOverride : st::dialogsFilter; - const auto text = link.startsWith(u"https://"_q) - ? link.mid(8) - : link.startsWith(u"http://"_q) - ? link.mid(7) - : link; + const auto text = Ui::Text::StripUrlProtocol(link); const auto margins = st.textMargins; const auto height = st.heightMin; const auto skip = margins.left(); @@ -446,13 +442,12 @@ object_ptr StarRefLinkBox( CreateLinkIcon(box, &bot->session(), row.state.users), st::boxRowPadding + st::starrefJoinUserpicsPadding); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_star_ref_link_title(), - st::boxTitle)), - st::boxRowPadding + st::starrefJoinTitlePadding); + tr::lng_star_ref_link_title(), + st::boxTitle), + st::boxRowPadding + st::starrefJoinTitlePadding, + style::al_top); box->addRow( object_ptr( box, @@ -470,7 +465,7 @@ object_ptr StarRefLinkBox( FormatForProgramDuration(program.durationMonths), Ui::Text::WithEntities), st::starrefCenteredText), - st::boxRowPadding); + style::al_top); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3); @@ -478,7 +473,8 @@ object_ptr StarRefLinkBox( object_ptr( box, tr::lng_star_ref_link_recipient(), - st::starrefCenteredText)); + st::starrefCenteredText), + style::al_top); Ui::AddSkip(box->verticalLayout()); box->addRow(object_ptr::fromRaw( MakePeerBubbleButton(box, peer).release() @@ -549,7 +545,7 @@ object_ptr JoinStarRefBox( struct State { rpl::variable> recipient; - QPointer weak; + base::weak_qptr weak; bool sent = false; }; const auto state = std::make_shared(State{ @@ -576,13 +572,12 @@ object_ptr JoinStarRefBox( }, box->lifetime()); box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_star_ref_title(), - st::boxTitle)), - st::boxRowPadding + st::starrefJoinTitlePadding); + tr::lng_star_ref_title(), + st::boxTitle), + st::boxRowPadding + st::starrefJoinTitlePadding, + style::al_top); box->addRow( object_ptr( box, @@ -596,13 +591,12 @@ object_ptr JoinStarRefBox( FormatForProgramDuration(program.durationMonths), Ui::Text::WithEntities), st::starrefCenteredText), - st::boxRowPadding); + style::al_top); Ui::AddSkip(box->verticalLayout(), st::defaultVerticalListSkip * 3); if (const auto average = program.revenuePerUser) { const auto layout = box->verticalLayout(); - const auto session = &initialRecipient->session(); - auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session)); + auto text = Ui::Text::Colorized(Ui::CreditsEmoji()); text.append(Lang::FormatCreditsAmountRounded(average)); layout->add( object_ptr( @@ -613,9 +607,9 @@ object_ptr JoinStarRefBox( Ui::Text::Wrapped(text, EntityType::Bold)), Ui::Text::WithEntities), st::starrefRevenueText, - st::defaultPopupMenu, - Core::TextContext({ .session = session })), - st::boxRowPadding); + st::defaultPopupMenu), + st::boxRowPadding, + style::al_top); Ui::AddSkip(layout, st::defaultVerticalListSkip); } @@ -637,7 +631,8 @@ object_ptr JoinStarRefBox( object_ptr( box, tr::lng_star_ref_link_recipient(), - st::starrefCenteredText)); + st::starrefCenteredText), + style::al_top); Ui::AddSkip(box->verticalLayout()); const auto recipientWrap = box->addRow( object_ptr(box), @@ -709,7 +704,7 @@ object_ptr JoinStarRefBox( } } show->show(StarRefLinkBox(info, recipient)); - if (const auto strong = state->weak.data()) { + if (const auto strong = state->weak.get()) { strong->closeBox(); } }, [=](const QString &error) { diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.cpp index 515b275923..0343af54bf 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_join_widget.cpp @@ -1105,7 +1105,7 @@ std::shared_ptr Make(not_null peer) { object_ptr ProgramsListBox( not_null window, not_null peer) { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); const auto initBox = [=](not_null box) { *weak = box; box->addButton(tr::lng_close(), [=] { @@ -1118,7 +1118,7 @@ object_ptr ProgramsListBox( peer, JoinType::Existing); controller->addForBotRequests() | rpl::start_with_next([=] { - if (const auto strong = weak->data()) { + if (const auto strong = weak->get()) { strong->closeBox(); } }, controller->lifetime()); diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.cpp index 5d1772c93a..46183eb688 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_setup_widget.cpp @@ -738,7 +738,7 @@ void InnerWidget::setupEnd() { tr::lng_star_ref_end(), st::settingsAttentionButton)); end->setClickedCallback([=] { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto window = _controller->parentController(); const auto sent = std::make_shared(); window->show(ConfirmEndBox([=] { @@ -751,7 +751,7 @@ void InnerWidget::setupEnd() { *sent = false; if (!success) { return; - } else if ([[maybe_unused]] const auto strong = weak.data()) { + } else if ([[maybe_unused]] const auto strong = weak.get()) { _controller->showBackFromStack(); window->showToast({ .title = tr::lng_star_ref_ended_title(tr::now), @@ -1013,7 +1013,7 @@ std::unique_ptr Widget::setupBottom() { st::boxDividerLabel), QMargins(margins.left(), 0, margins.right(), 0)); save->setClickedCallback([=] { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto user = _state->user; const auto program = _state->program; const auto show = controller()->uiShow(); diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index 09f5cdeb60..6be47fdd92 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -215,7 +215,7 @@ void AddPremiumTopBarWithDefaultTitleBar( + st::defaultVerticalListSkip, st::boxDividerBg, RectPart::Bottom), - {}); + style::margins()); bar->setPaused(true); bar->setRoundEdges(false); bar->setMaximumHeight(st::giveawayGiftCodeTopHeight); @@ -348,12 +348,11 @@ void CreateGiveawayBox( Ui::AddSkip(container); Ui::AddSkip(container); container->add( - object_ptr>( + object_ptr( box, - object_ptr( - box, - tr::lng_contacts_loading(), - st::giveawayLoadingLabel))); + tr::lng_contacts_loading(), + st::giveawayLoadingLabel), + style::al_top); Ui::AddSkip(container); Ui::AddSkip(container); } @@ -1066,7 +1065,7 @@ void CreateGiveawayBox( Ui::Premium::AddGiftOptions( listOptions, durationGroup, - state->apiOptions.options(usersCount), + state->apiOptions.optionsForGiveaway(usersCount), st::giveawayGiftCodeGiftOption, true); @@ -1459,11 +1458,11 @@ void CreateGiveawayBox( } state->confirmButtonBusy = true; const auto show = box->uiShow(); - const auto weak = Ui::MakeWeak(box.get()); + const auto weak = base::make_weak(box.get()); const auto done = [=](Payments::CheckoutResult result) { const auto isPaid = result == Payments::CheckoutResult::Paid; if (result == Payments::CheckoutResult::Pending || isPaid) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->window()->setFocus(); strong->closeBox(); } diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index 445337d33d..956c16c2cc 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -153,6 +153,7 @@ giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px); giveawayGiftCodeBox: Box(defaultBox) { buttonPadding: margins(22px, 11px, 22px, 22px); buttonHeight: 42px; + buttonWide: true; button: RoundButton(defaultActiveButton) { height: 42px; textTop: 12px; @@ -205,11 +206,6 @@ starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) { starGiftSmallButton: defaultTableSmallButton; darkGiftCodeBox: Box(giveawayGiftCodeBox) { - button: RoundButton(defaultActiveButton) { - height: 42px; - textTop: 12px; - style: semiboldTextStyle; - } bg: groupCallMembersBg; title: FlatLabel(boxTitle) { textFg: groupCallMembersFg; @@ -218,6 +214,7 @@ darkGiftCodeBox: Box(giveawayGiftCodeBox) { } darkGiftLink: icon {{ "menu/copy", groupCallMembersFg }}; darkGiftShare: icon {{ "menu/share", groupCallMembersFg }}; +darkGiftTheme: icon {{ "menu/colors", groupCallMembersFg }}; darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }}; darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.cpp index 1faa9289d7..d9cb86645c 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/select_countries_box.cpp @@ -177,12 +177,11 @@ void SelectCountriesBox( Ui::AddSkip(container); Ui::AddSkip(container); container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - tr::lng_search_messages_none(), - st::membersAbout))); + tr::lng_search_messages_none(), + st::membersAbout), + style::al_top); Ui::AddSkip(container); Ui::AddSkip(container); } 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 0f35e31de0..de6862e8bc 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 @@ -174,7 +174,7 @@ void FillShareLink( std::shared_ptr show, const QString &link, not_null peer) { - const auto weak = Ui::MakeWeak(content); + const auto weak = base::make_weak(content); const auto copyLink = crl::guard(weak, [=] { QGuiApplication::clipboard()->setText(link); show->showToast(tr::lng_channel_public_link_copied(tr::now)); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style index 86fa3772d8..f85b36d29d 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style +++ b/Telegram/SourceFiles/info/channel_statistics/earn/channel_earn.style @@ -148,4 +148,8 @@ botEarnLockedButtonLabel: FlatLabel(channelEarnOverviewMajorLabel) { font: font(10px semibold); } } -botEarnButtonLockMargins: margins(-2px, 4px, 0px, 0px); +botEarnButtonLock: IconEmoji { + icon: icon{{ "chat/mini_lock", premiumButtonFg }}; + padding: margins(-2px, 4px, 0px, 0px); +} + diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp index 9e1ba75d52..69ece36f32 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp @@ -7,14 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/channel_statistics/earn/earn_format.h" -namespace Info::ChannelEarn { +#include -using EarnInt = Data::EarnInt; +namespace Info::ChannelEarn { +namespace { constexpr auto kMinorPartLength = 9; constexpr auto kMaxChoppedZero = kMinorPartLength - 2; constexpr auto kZero = QChar('0'); -constexpr auto kDot = QChar('.'); +const auto DecimalPoint = QString() + QLocale().decimalPoint(); + +using EarnInt = Data::EarnInt; + +} // namespace QString MajorPart(EarnInt value) { const auto string = QString::number(value); @@ -28,13 +33,13 @@ QString MajorPart(CreditsAmount value) { QString MinorPart(EarnInt value) { if (!value) { - return QString(kDot) + kZero + kZero; + return DecimalPoint + kZero + kZero; } const auto string = QString::number(value); const auto diff = int(string.size()) - kMinorPartLength; const auto result = (diff < 0) - ? kDot + u"%1"_q.arg(0, std::abs(diff), 10, kZero) + string - : kDot + string.mid(diff); + ? DecimalPoint + u"%1"_q.arg(0, std::abs(diff), 10, kZero) + string + : DecimalPoint + string.mid(diff); const auto begin = (result.constData()); const auto end = (begin + result.size()); auto ch = end - 1; @@ -51,7 +56,19 @@ QString MinorPart(EarnInt value) { } QString MinorPart(CreditsAmount value) { - return QString::number(value.value(), 'f', 2).right(3); + static const int DecimalPointLength = DecimalPoint.length(); + + const auto fractional = std::abs(int(value.value() * 100)) % 100; + auto result = QString(DecimalPointLength + 2, Qt::Uninitialized); + + for (int i = 0; i < DecimalPointLength; ++i) { + result[i] = DecimalPoint[i]; + } + + result[DecimalPointLength] = QChar('0' + fractional / 10); + result[DecimalPointLength + 1] = QChar('0' + fractional % 10); + + return result; } QString ToUsd( @@ -69,7 +86,7 @@ QString ToUsd( return QString(kApproximately) + QChar('$') - + QString::number( + + QLocale().toString( value.value() * rate, 'f', afterFloat ? afterFloat : 2); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp index fa5b2444da..a398532737 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/channel_statistics/earn/earn_icons.h" +#include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" -#include "ui/text/text_custom_emoji.h" +#include "ui/text/custom_emoji_instance.h" #include "ui/rect.h" +#include "styles/style_credits.h" #include "styles/style_menu_icons.h" #include "styles/style_widgets.h" #include "styles/style_info.h" // infoIconReport. @@ -144,9 +146,41 @@ QImage MenuIconCredits() { std::unique_ptr MakeCurrencyIconEmoji( const style::font &font, const QColor &c) { - return std::make_unique( - IconCurrencyColored(font, c), - u"currency_icon:%1:%2"_q.arg(font->height).arg(c.name())); + return std::make_unique( + u"currency_icon:%1:%2"_q.arg(font->height).arg(c.name()), + IconCurrencyColored(font, c)); +} + +Ui::Text::PaletteDependentEmoji IconCreditsEmoji( + IconDescriptor descriptor) { + return { .factory = [=] { + return Ui::GenerateStars( + (descriptor.size + ? descriptor.size + : st::defaultTableLabel.style.font->height), + 1); + }, .margin = descriptor.margin.value_or( + QMargins{ 0, st::giftBoxByStarsSkip, 0, 0 }) }; +} + +Ui::Text::PaletteDependentEmoji IconCurrencyEmoji( + IconDescriptor descriptor) { + return { .factory = [=] { + return IconCurrencyColored( + descriptor.size ? descriptor.size : st::earnTonIconSize, + st::currencyFg->c); + }, .margin = descriptor.margin.value_or(st::earnTonIconMargin) }; +} + +Ui::Text::PaletteDependentEmoji IconCreditsEmojiSmall() { + return IconCreditsEmoji({ + .size = st::giftBoxByStarsStyle.font->height, + .margin = QMargins{ 0, st::giftBoxByStarsStarTop, 0, 0 }, + }); +} + +Ui::Text::PaletteDependentEmoji IconCurrencyEmojiSmall() { + return IconCreditsEmoji({}); } } // 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 index 0e5a419ec7..efbedd7ee2 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/custom_emoji_helper.h" + namespace Ui::Text { class CustomEmoji; } // namespace Ui::Text @@ -26,4 +28,16 @@ std::unique_ptr MakeCurrencyIconEmoji( const style::font &font, const QColor &c); +struct IconDescriptor { + int size = 0; + std::optional margin; +}; +[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmoji( + IconDescriptor descriptor = {}); +[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmoji( + IconDescriptor descriptor = {}); + +[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmojiSmall(); +[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmojiSmall(); + } // 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 1563bef9dd..cbc837690d 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 @@ -58,7 +58,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/peer_bubble.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/slider_natural_width.h" @@ -141,14 +140,12 @@ void AddHeader( } void AddRecipient(not_null box, const TextWithEntities &t) { - const auto wrap = box->addRow( - object_ptr>( + const auto container = box->addRow( + object_ptr( box, - object_ptr( - box, - rpl::single(QString()), - st::channelEarnHistoryRecipientButton))); - const auto container = wrap->entity(); + rpl::single(QString()), + st::channelEarnHistoryRecipientButton), + style::al_top); const auto label = Ui::CreateChild( container, rpl::single(t), @@ -200,6 +197,8 @@ void AddRecipient(not_null box, const TextWithEntities &t) { QLocale().toString(date.time(), QLocale::ShortFormat)); } +constexpr auto kMinus = QChar(0x2212); + } // namespace InnerWidget::InnerWidget( @@ -401,7 +400,6 @@ void InnerWidget::fill() { ); constexpr auto kMinorLength = 3; - constexpr auto kMinus = QChar(0x2212); //constexpr auto kApproximately = QChar(0x2248); const auto multiplier = data.usdRate; @@ -412,63 +410,33 @@ void InnerWidget::fill() { const auto session = &_peer->session(); const auto withdrawalEnabled = WithdrawalEnabled(session); - const auto addEmojiToMajor = [=]( - not_null label, - rpl::producer value, - std::optional isIn, - std::optional margins) { - const auto &st = label->st(); - auto icon = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st.style.font, - !isIn - ? st::currencyFg->c - : (*isIn) - ? st::boxTextFgGood->c - : st::menuIconAttentionColor->c), - margins ? *margins : st::channelEarnCurrencyCommonMargins, - false)); - const auto prepended = !isIn - ? TextWithEntities() - : TextWithEntities::Simple((*isIn) ? QChar('+') : kMinus); - std::move( - value - ) | rpl::start_with_next([=](CreditsAmount v) { - label->setMarkedText( - base::duplicate(prepended).append(icon).append(MajorPart(v)), - Core::TextContext({ .session = session })); - }, label->lifetime()); - }; - const auto bigCurrencyIcon = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::boxTitle.style.font, - st::currencyFg->c), - st::channelEarnCurrencyLearnMargins, - false)); - - const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); const auto addAboutWithLearn = [&](const tr::phrase &text) { - auto label = Ui::CreateLabelWithCustomEmoji( + auto label = object_ptr( container, text( lt_link, tr::lng_channel_earn_about_link( lt_emoji, - rpl::single(arrow), + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link(std::move(text), 1); }), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), st::boxDividerLabel); label->setLink(1, std::make_shared([=] { _show->showBox(Box([=](not_null box) { box->setNoContentMargin(true); + auto emojiHelper = Ui::Text::CustomEmojiHelper(); + const auto bigCurrencyIcon = emojiHelper.paletteDependent({ + .factory = [=] { + return Ui::Earn::IconCurrencyColored( + st::boxTitle.style.font, + st::currencyFg->c); + }, .margin = st::channelEarnCurrencyLearnMargins }); + const auto content = box->verticalLayout().get(); Ui::AddSkip(content); @@ -479,10 +447,10 @@ void InnerWidget::fill() { const auto rect = Rect(icon.size() * 1.4); auto owned = object_ptr(content); owned->resize(rect.size()); + owned->setNaturalWidth(rect.width()); const auto widget = box->addRow( - object_ptr>( - content, - std::move(owned)))->entity(); + std::move(owned), + style::al_top); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); @@ -495,14 +463,13 @@ void InnerWidget::fill() { } Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( + box->addRow(object_ptr( content, - object_ptr( - content, - bot - ? tr::lng_channel_earn_bot_learn_title() - : tr::lng_channel_earn_learn_title(), - st::boxTitle))); + bot + ? tr::lng_channel_earn_bot_learn_title() + : tr::lng_channel_earn_learn_title(), + st::boxTitle), + style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); Ui::AddSkip(content); @@ -570,18 +537,17 @@ void InnerWidget::fill() { Ui::AddSkip(content); { const auto l = box->addRow( - object_ptr>( + object_ptr( content, - Ui::CreateLabelWithCustomEmoji( - content, - tr::lng_channel_earn_learn_coin_title( - lt_emoji, - rpl::single( - Ui::Text::Link(bigCurrencyIcon, 1)), - Ui::Text::RichLangValue - ), - Core::TextContext({ .session = session }), - st::boxTitle)))->entity(); + tr::lng_channel_earn_learn_coin_title( + lt_emoji, + rpl::single( + Ui::Text::Link(bigCurrencyIcon, 1)), + Ui::Text::RichLangValue), + st::boxTitle, + st::defaultPopupMenu, + emojiHelper.context()), + style::al_top); const auto diamonds = l->lifetime().make_state(0); l->setLink(1, std::make_shared([=] { const auto count = (*diamonds); @@ -597,20 +563,19 @@ void InnerWidget::fill() { Ui::AddSkip(content); { const auto label = box->addRow( - Ui::CreateLabelWithCustomEmoji( + object_ptr( content, tr::lng_channel_earn_learn_coin_about( lt_link, tr::lng_channel_earn_about_link( lt_emoji, - rpl::single(arrow), + rpl::single(Ui::Text::IconEmoji( + &st::textMoreIconEmoji)), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link(std::move(text), 1); }), - Ui::Text::RichLangValue - ), - Core::TextContext({ .session = session }), + Ui::Text::RichLangValue), st::channelEarnLearnDescription)); label->resizeToWidth(box->width() - rect::m::sum::h(st::boxRowPadding)); @@ -726,7 +691,7 @@ void InnerWidget::fill() { const auto majorLabel = Ui::CreateChild( line, st::channelEarnOverviewMajorLabel); - addEmojiToMajor( + AddEmojiToMajor( majorLabel, rpl::duplicate(currencyValue), {}, @@ -869,9 +834,8 @@ void InnerWidget::fill() { Ui::AddSkip(container); const auto labels = container->add( - object_ptr>( - container, - object_ptr(container)))->entity(); + object_ptr(container), + style::al_top); const auto majorLabel = Ui::CreateChild( labels, @@ -879,7 +843,7 @@ void InnerWidget::fill() { { const auto &m = st::channelEarnCurrencyCommonMargins; const auto p = QMargins(m.left(), 0, m.right(), m.bottom()); - addEmojiToMajor(majorLabel, rpl::single(value), {}, p); + AddEmojiToMajor(majorLabel, rpl::single(value), {}, p); } majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); const auto minorLabel = Ui::CreateChild( @@ -896,6 +860,8 @@ void InnerWidget::fill() { labels->resize( majorSize.width() + minorSize.width(), majorSize.height()); + labels->setNaturalWidth( + majorSize.width() + minorSize.width()); majorLabel->moveToLeft(0, 0); minorLabel->moveToRight( 0, @@ -905,12 +871,11 @@ void InnerWidget::fill() { Ui::AddSkip(container); container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - ToUsd(value, multiplier, 0), - st::channelEarnOverviewSubMinorLabel))); + ToUsd(value, multiplier, 0), + st::channelEarnOverviewSubMinorLabel), + style::al_top); Ui::AddSkip(container); @@ -920,7 +885,8 @@ void InnerWidget::fill() { container, rpl::never(), stButton), - st::boxRowPadding); + st::boxRowPadding, + style::al_justify); const auto label = Ui::CreateChild( button, @@ -944,11 +910,9 @@ void InnerWidget::fill() { anim::interpolateF(.5, 1., value))); }; colorText(withdrawalEnabled ? 1. : 0.); -#ifndef _DEBUG button->setAttribute( Qt::WA_TransparentForMouseEvents, - !withdrawalEnabled); -#endif + !withdrawalEnabled && (value.value() > 0.01)); Api::HandleWithdrawalButton( { .currencyReceiver = _peer }, @@ -1099,7 +1063,8 @@ void InnerWidget::fill() { if (hasCurrencyTab) { Ui::AddSkip(listsContainer); - const auto historyList = tabCurrencyList->entity(); + const auto historyList = tabCurrencyList->entity()->add( + object_ptr(tabCurrencyList->entity())); const auto addHistoryEntry = [=]( const Data::CreditsHistoryEntry &entry, const tr::phrase<> &text) { @@ -1161,7 +1126,7 @@ void InnerWidget::fill() { const auto majorLabel = Ui::CreateChild( wrap, st::channelEarnHistoryMajorLabel); - addEmojiToMajor( + AddEmojiToMajor( majorLabel, rpl::single(entry.credits), isIn, @@ -1188,14 +1153,13 @@ void InnerWidget::fill() { Ui::AddSkip(box->verticalLayout()); Ui::AddSkip(box->verticalLayout()); const auto labels = box->addRow( - object_ptr>( - box, - object_ptr(box)))->entity(); + object_ptr(box), + style::al_top); const auto majorLabel = Ui::CreateChild( labels, st::channelEarnOverviewMajorLabel); - addEmojiToMajor( + AddEmojiToMajor( majorLabel, rpl::single(entry.credits), isIn, @@ -1219,6 +1183,8 @@ void InnerWidget::fill() { labels->resize( majorSize.width() + minorSize.width(), majorSize.height()); + labels->setNaturalWidth( + majorSize.width() + minorSize.width()); majorLabel->moveToLeft(0, 0); minorLabel->moveToRight( 0, @@ -1226,12 +1192,12 @@ void InnerWidget::fill() { }, box->lifetime()); Ui::AddSkip(box->verticalLayout()); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, dateText, - st::channelEarnHistorySubLabel))); + st::channelEarnHistorySubLabel), + style::al_top); Ui::AddSkip(box->verticalLayout()); Ui::AddSkip(box->verticalLayout()); AddChannelEarnTable( @@ -1240,14 +1206,14 @@ void InnerWidget::fill() { entry); Ui::AddSkip(box->verticalLayout()); Ui::AddSkip(box->verticalLayout()); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, isIn ? tr::lng_channel_earn_history_in_about() : tr::lng_channel_earn_history_out(), - st::channelEarnHistoryDescriptionLabel))); + st::channelEarnHistoryDescriptionLabel), + style::al_top); Ui::AddSkip(box->verticalLayout()); if (isIn) { Ui::AddSkip(box->verticalLayout()); @@ -1258,9 +1224,8 @@ void InnerWidget::fill() { } if (isIn) { box->addRow( - object_ptr>( - box, - Ui::CreatePeerBubble(box, peer))); + Ui::CreatePeerBubble(box, peer), + style::al_top); } const auto closeBox = [=] { box->closeBox(); }; { @@ -1312,9 +1277,10 @@ void InnerWidget::fill() { const auto rightWrapPadding = rect::m::sum::h(padding) + minorLabel->width() + majorLabel->width(); - wrap->setPadding(st::channelEarnHistoryOuter - + QMargins(padding.left(), 0, rightWrapPadding, 0)); - button->resize(g.size()); + const auto additional = st::channelEarnHistoryOuter + + QMargins(padding.left(), 0, rightWrapPadding, 0); + wrap->setPadding(additional); + button->resize((g + additional).size()); button->lower(); }, wrap->lifetime()); }; @@ -1346,23 +1312,17 @@ void InnerWidget::fill() { = lifetime().make_state(_peer); state->token = firstSlice.token; state->showed = firstSlice.list.size(); - const auto max = firstSlice.total; - const auto wrap = listsContainer->add( + const auto wrap = tabCurrencyList->entity()->add( object_ptr>( - listsContainer, + tabCurrencyList->entity(), object_ptr( - listsContainer, - tr::lng_channel_earn_history_show_more( - lt_count, - state->showed.value( - ) | rpl::map( - max - rpl::mappers::_1 - ) | tr::to_count()), + tabCurrencyList->entity(), + tr::lng_channels_your_more(), st::statisticsShowMoreButton))); const auto button = wrap->entity(); Ui::AddToggleUpDownArrowToMoreButton(button); - wrap->toggle(true, anim::type::instant); + wrap->toggle(!firstSlice.allLoaded, anim::type::instant); const auto handleReceived = [=]( Data::EarnHistorySlice slice) { state->loading = false; @@ -1470,10 +1430,10 @@ void InnerWidget::fill() { [] {}); } if (!isLocked) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto show = _controller->uiShow(); const auto failed = [=](const QString &e) { - if (weak.data()) { + if (weak.get()) { toggled->fire(false); show->showToast(e); } @@ -1529,4 +1489,34 @@ not_null InnerWidget::peer() const { return _peer; } +void AddEmojiToMajor( + not_null label, + rpl::producer value, + std::optional isIn, + std::optional margins) { + const auto &st = label->st(); + const auto prepended = !isIn + ? TextWithEntities() + : TextWithEntities::Simple((*isIn) ? QChar('+') : kMinus); + std::move( + value + ) | rpl::start_with_next([=](CreditsAmount v) { + auto helper = Ui::Text::CustomEmojiHelper(); + auto icon = helper.paletteDependent({ .factory = [=] { + return Ui::Earn::IconCurrencyColored( + st.style.font, + !isIn + ? st::currencyFg->c + : (*isIn) + ? st::boxTextFgGood->c + : st::menuIconAttentionColor->c); + }, .margin = margins + ? *margins + : st::channelEarnCurrencyCommonMargins }); + label->setMarkedText( + base::duplicate(prepended).append(icon).append(MajorPart(v)), + helper.context()); + }, label->lifetime()); +} + } // namespace Info::ChannelEarn 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 d67eba3238..0fa417ec72 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 @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class Show; +class FlatLabel; } // namespace Ui namespace Info { @@ -63,4 +64,10 @@ private: }; +void AddEmojiToMajor( + not_null label, + rpl::producer value, + std::optional isIn, + std::optional margins); + } // namespace Info::ChannelEarn diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp index ae781cc349..b9a8149267 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp @@ -16,8 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_session.h" -#include "main/main_session.h" #include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/history.h" @@ -34,7 +35,8 @@ using namespace Media; } // namespace Provider::Provider(not_null controller) -: _controller(controller) { +: _controller(controller) +, _storiesAddToAlbumId(_controller->storiesAddToAlbumId()) { style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &layout : _layouts) { @@ -485,6 +487,9 @@ void Provider::applyDragSelection( return; } const auto search = !_queryWords.isEmpty(); + const auto selectLimit = _storiesAddToAlbumId + ? _controller->session().appConfig().storiesAlbumLimit() + : MaxSelectedItems; auto chosen = base::flat_set>(); chosen.reserve(till - from); for (auto i = from; i != till; ++i) { @@ -496,7 +501,8 @@ void Provider::applyDragSelection( ChangeItemSelection( selected, item, - computeSelectionData(item, FullSelection)); + computeSelectionData(item, FullSelection), + selectLimit); } if (selected.size() != chosen.size()) { for (auto i = begin(selected); i != end(selected);) { diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h index cf7bca91d8..d2f2cc6970 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_provider.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_provider.h @@ -129,6 +129,7 @@ private: std::optional _fullCount; base::flat_set> _downloading; base::flat_set> _downloaded; + int _storiesAddToAlbumId = 0; std::vector _addPostponed; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 3eae2fe12d..c6bf2e6203 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -351,6 +351,8 @@ InfoProfileCover { status: FlatLabel; statusLeft: pixels; statusTop: pixels; + starsRatingLeft: pixels; + starsRatingTop: pixels; showLastSeen: RoundButton; showLastSeenPosition: point; showLastSeenVisible: bool; @@ -384,6 +386,8 @@ infoProfileCover: InfoProfileCover { status: infoProfileStatus; statusLeft: 109px; statusTop: 58px; + starsRatingLeft: 107px; + starsRatingTop: 57px; showLastSeen: RoundButton(defaultActiveButton) { textFg: windowSubTextFg; textFgOver: windowSubTextFg; @@ -426,6 +430,10 @@ infoEditContactCover: InfoProfileCover(infoProfileCover) { } infoEditContactPersonalLeft: 6px; +infoRatingDeductedBadge: RoundButton(customEmojiTextBadge) { + textBg: windowSubTextFg; +} + infoProfileInaccessibleUserpic: icon {{ "info/inaccessible_userpic", historyPeerUserpicFg }}; infoVerifiedCheckPosition: point(4px, 2px); @@ -442,6 +450,9 @@ infoPeerBadge: InfoPeerBadge { position: infoVerifiedCheckPosition; sizeTag: 0; // Normal } +infoBotVerifyBadge: InfoPeerBadge(infoPeerBadge) { + position: point(-2px, 2px); +} infoIconFg: windowBoldFg; @@ -759,7 +770,6 @@ infoEmptyAudio: icon {{ "info/info_media_audio_empty", infoEmptyFg }}; infoEmptyFile: icon {{ "info/info_media_file_empty", infoEmptyFg }}; infoEmptyVoice: icon {{ "info/info_media_voice_empty", infoEmptyFg }}; infoEmptyLink: icon {{ "info/info_media_link_empty", infoEmptyFg }}; -infoEmptyStories: icon {{ "info/info_media_story_empty", infoEmptyFg }}; infoEmptyIconTop: 120px; infoEmptyLabelTop: 40px; infoEmptyLabelSkip: 20px; @@ -1187,3 +1197,102 @@ topicsLayoutButtonIconPadding: margins(4px, 0px, 4px, 0px); topicsLayoutButtonIconSize: 140px; topicsLayoutButtonPadding: margins(4px, 0px, 4px, 12px); topicsLayoutButtonSkip: 0px; + +infoStarsTitle: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + textFg: windowBoldFg; + maxHeight: 24px; + style: TextStyle(boxTextStyle) { + font: font(17px semibold); + } + align: align(top); +} +infoStarsFeatureTitle: FlatLabel(defaultFlatLabel) { + textFg: windowBoldFg; + style: semiboldTextStyle; + minWidth: 10px; + maxHeight: 20px; +} +infoStarsFeatureAbout: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 20px; +} +infoStarsFeatureIconPosition: point(3px, 7px); +infoStarsFeatureMargin: margins(0px, 8px, 0px, 6px); +infoStarsFeatureLabelLeft: 46px; +infoStarsFeatureSkip: 2px; +infoStarsCrown: icon {{ "limits/filled_rating_crown-24x24", windowFgActive }}; +infoStarsUnderstood: IconEmoji{ + icon: icon {{ "limits/filled_understood-20x20", windowFgActive }}; + padding: margins(0px, -1px, 0px, 0px); +} + +collectionAbout: FlatLabel(defaultFlatLabel) { + minWidth: 256px; +} +collectionNameField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(2px, 10px, 32px, 2px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + + heightMin: 36px; +} +collectionEmptyTitle: FlatLabel(boxTitle) { +} +collectionEmptyText: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} +collectionEmptyButton: RoundButton(defaultActiveButton) { + width: -96px; +} +collectionEmptyTitleMargin: margins(10px, 24px, 10px, 4px); +collectionEmptyTextMargin: margins(10px, 4px, 10px, 12px); +collectionEmptyAddMargin: margins(10px, 12px, 10px, 24px); +collectionAddIcon: IconEmoji { + icon: icon {{ "settings/plus", windowFgActive }}; + padding: margins(1px, -2px, 1px, 0px); +} +collectionEditBox: Box(defaultBox) { + buttonPadding: margins(16px, 12px, 16px, 12px); + buttonHeight: 42px; + buttonWide: true; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + style: semiboldTextStyle; + } +} +collectionEditMenuToggle: IconButton(infoTopBarMenu) { + width: boxTitleHeight; + height: boxTitleHeight; + + icon: icon {{ "title_menu_dots", boxTitleCloseFg }}; + iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }}; + iconPosition: point(-1px, -1px); + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: defaultRippleAnimationBgOver; +} + +earnTonIconSize: 16px; +earnTonIconMargin: margins(0px, 2px, 0px, 0px); + +infoMusicButtonRipple: defaultRippleAnimation; +infoMusicButtonPerformer: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + maxHeight: 20px; +} +infoMusicButtonTitle: FlatLabel(defaultFlatLabel) { + textFg: windowBoldFg; + style: semiboldTextStyle; + maxHeight: 20px; +} +infoMusicButtonPadding: margins(48px, 6px, 12px, 6px); +infoMusicButtonBottom: 8px; +infoMusicButtonLine: 2px; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index efbfbdc14a..075bd6a7e2 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -17,6 +17,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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/peer_gifts/info_peer_gifts_common.h" +#include "info/saved/info_saved_music_common.h" +#include "info/stories/info_stories_common.h" #include "info/info_layer_widget.h" #include "info/info_section_widget.h" #include "info/info_controller.h" @@ -476,8 +479,19 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; + } else if (const auto gifts = giftsPeer()) { + return PeerGifts::Tag{ + gifts, + giftsCollectionId(), + }; } else if (const auto stories = storiesPeer()) { - return Stories::Tag{ stories, storiesTab() }; + return Stories::Tag{ + stories, + storiesAlbumId(), + storiesAddToAlbumId(), + }; + } else if (const auto music = musicPeer()) { + return Saved::MusicTag{ music }; } else if (statisticsTag().peer) { return statisticsTag(); } else if (const auto starref = starrefPeer()) { @@ -521,7 +535,17 @@ ContentMemento::ContentMemento(Downloads::Tag downloads) { ContentMemento::ContentMemento(Stories::Tag stories) : _storiesPeer(stories.peer) -, _storiesTab(stories.tab) { +, _storiesAlbumId(stories.albumId) +, _storiesAddToAlbumId(stories.addingToAlbumId) { +} + +ContentMemento::ContentMemento(Saved::MusicTag music) +: _musicPeer(music.peer) { +} + +ContentMemento::ContentMemento(PeerGifts::Tag gifts) +: _giftsPeer(gifts.peer) +, _giftsCollectionId(gifts.collectionId) { } ContentMemento::ContentMemento(Statistics::Tag statistics) diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 29c376dfa4..148d20ae39 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -47,11 +47,6 @@ namespace Info::Downloads { struct Tag; } // namespace Info::Downloads -namespace Info::Stories { -struct Tag; -enum class Tab; -} // namespace Info::Stories - namespace Info::Statistics { struct Tag; } // namespace Info::Statistics @@ -65,6 +60,18 @@ namespace Info::GlobalMedia { struct Tag; } // namespace Info::GlobalMedia +namespace Info::PeerGifts { +struct Tag; +} // namespace Info::PeerGifts + +namespace Info::Stories { +struct Tag; +} // namespace Info::Stories + +namespace Info::Saved { +struct MusicTag; +} // namespace Info::Saved + namespace Info { class ContentMemento; @@ -212,9 +219,11 @@ public: Data::ForumTopic *topic, Data::SavedSublist *sublist, PeerId migratedPeerId); + explicit ContentMemento(PeerGifts::Tag gifts); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); explicit ContentMemento(Stories::Tag stories); + explicit ContentMemento(Saved::MusicTag music); explicit ContentMemento(Statistics::Tag statistics); explicit ContentMemento(BotStarRef::Tag starref); explicit ContentMemento(GlobalMedia::Tag global); @@ -226,65 +235,77 @@ public: std::shared_ptr whoReadIds, FullMsgId contextId, Data::ReactionId selected); + virtual ~ContentMemento() = default; - virtual object_ptr createWidget( + [[nodiscard]] virtual object_ptr createWidget( QWidget *parent, not_null controller, const QRect &geometry) = 0; - PeerData *peer() const { + [[nodiscard]] PeerData *peer() const { return _peer; } - PeerId migratedPeerId() const { + [[nodiscard]] PeerId migratedPeerId() const { return _migratedPeerId; } - Data::ForumTopic *topic() const { + [[nodiscard]] Data::ForumTopic *topic() const { return _topic; } - Data::SavedSublist *sublist() const { + [[nodiscard]] Data::SavedSublist *sublist() const { return _sublist; } - UserData *settingsSelf() const { + [[nodiscard]] UserData *settingsSelf() const { return _settingsSelf; } - PeerData *storiesPeer() const { + [[nodiscard]] PeerData *storiesPeer() const { return _storiesPeer; } - Stories::Tab storiesTab() const { - return _storiesTab; + [[nodiscard]] int storiesAlbumId() const { + return _storiesAlbumId; } - Statistics::Tag statisticsTag() const { + [[nodiscard]] int storiesAddToAlbumId() const { + return _storiesAddToAlbumId; + } + [[nodiscard]] PeerData *musicPeer() const { + return _musicPeer; + } + [[nodiscard]] PeerData *giftsPeer() const { + return _giftsPeer; + } + [[nodiscard]] int giftsCollectionId() const { + return _giftsCollectionId; + } + [[nodiscard]] Statistics::Tag statisticsTag() const { return _statisticsTag; } - PeerData *starrefPeer() const { + [[nodiscard]] PeerData *starrefPeer() const { return _starrefPeer; } - BotStarRef::Type starrefType() const { + [[nodiscard]] BotStarRef::Type starrefType() const { return _starrefType; } - PollData *poll() const { + [[nodiscard]] PollData *poll() const { return _poll; } - FullMsgId pollContextId() const { + [[nodiscard]] FullMsgId pollContextId() const { return _poll ? _pollReactionsContextId : FullMsgId(); } - std::shared_ptr reactionsWhoReadIds() const { + [[nodiscard]] auto reactionsWhoReadIds() const + -> std::shared_ptr { return _reactionsWhoReadIds; } - Data::ReactionId reactionsSelected() const { + [[nodiscard]] Data::ReactionId reactionsSelected() const { return _reactionsSelected; } - FullMsgId reactionsContextId() const { + [[nodiscard]] FullMsgId reactionsContextId() const { return _reactionsWhoReadIds ? _pollReactionsContextId : FullMsgId(); } - UserData *globalMediaSelf() const { + [[nodiscard]] UserData *globalMediaSelf() const { return _globalMediaSelf; } - Key key() const; + [[nodiscard]] Key key() const; - virtual Section section() const = 0; - - virtual ~ContentMemento() = default; + [[nodiscard]] virtual Section section() const = 0; void setScrollTop(int scrollTop) { _scrollTop = scrollTop; @@ -295,19 +316,19 @@ public: void setSearchFieldQuery(const QString &query) { _searchFieldQuery = query; } - QString searchFieldQuery() const { + [[nodiscard]] QString searchFieldQuery() const { return _searchFieldQuery; } void setSearchEnabledByContent(bool enabled) { _searchEnabledByContent = enabled; } - bool searchEnabledByContent() const { + [[nodiscard]] bool searchEnabledByContent() const { return _searchEnabledByContent; } void setSearchStartsFocused(bool focused) { _searchStartsFocused = focused; } - bool searchStartsFocused() const { + [[nodiscard]] bool searchStartsFocused() const { return _searchStartsFocused; } @@ -318,7 +339,11 @@ private: Data::SavedSublist *_sublist = nullptr; UserData * const _settingsSelf = nullptr; PeerData * const _storiesPeer = nullptr; - Stories::Tab _storiesTab = {}; + int _storiesAlbumId = 0; + int _storiesAddToAlbumId = 0; + PeerData * const _musicPeer = nullptr; + PeerData * const _giftsPeer = nullptr; + int _giftsCollectionId = 0; Statistics::Tag _statisticsTag; PeerData * const _starrefPeer = nullptr; BotStarRef::Type _starrefType = {}; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 49054d63f1..42f8972a0a 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "ui/search_field_controller.h" -#include "data/data_shared_media.h" #include "history/history.h" #include "info/info_content_widget.h" #include "info/info_memento.h" @@ -23,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" +#include "data/data_shared_media.h" #include "data/data_media_types.h" #include "data/data_download_manager.h" #include "history/history_item.h" @@ -49,9 +49,15 @@ Key::Key(Downloads::Tag downloads) : _value(downloads) { Key::Key(Stories::Tag stories) : _value(stories) { } +Key::Key(Saved::MusicTag music) : _value(music) { +} + Key::Key(Statistics::Tag statistics) : _value(statistics) { } +Key::Key(PeerGifts::Tag gifts) : _value(gifts) { +} + Key::Key(BotStarRef::Tag starref) : _value(starref) { } @@ -118,11 +124,39 @@ PeerData *Key::storiesPeer() const { return nullptr; } -Stories::Tab Key::storiesTab() const { +int Key::storiesAlbumId() const { if (const auto tag = std::get_if(&_value)) { - return tag->tab; + return tag->albumId; } - return Stories::Tab(); + return 0; +} + +int Key::storiesAddToAlbumId() const { + if (const auto tag = std::get_if(&_value)) { + return tag->addingToAlbumId; + } + return 0; +} + +PeerData *Key::musicPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +PeerData *Key::giftsPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +int Key::giftsCollectionId() const { + if (const auto tag = std::get_if(&_value)) { + return tag->collectionId; + } + return 0; } Statistics::Tag Key::statisticsTag() const { @@ -299,6 +333,10 @@ Controller::Controller( setupTopicViewer(); } +void Controller::replaceKey(Key key) { + _key = key; +} + void Controller::setupMigrationViewer() { const auto peer = _key.peer(); if (_key.topic() @@ -364,6 +402,7 @@ bool Controller::validateMementoPeer( && memento->migratedPeerId() == migratedPeerId() && memento->settingsSelf() == settingsSelf() && memento->storiesPeer() == storiesPeer() + && memento->musicPeer() == musicPeer() && memento->statisticsTag().peer == statisticsTag().peer && memento->starrefPeer() == starrefPeer() && memento->starrefType() == starrefType(); diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index 5b29ac1ce6..14ad24d686 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -9,7 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_message_reaction_id.h" #include "data/data_search_controller.h" +#include "info/peer_gifts/info_peer_gifts_common.h" +#include "info/saved/info_saved_music_common.h" #include "info/statistics/info_statistics_tag.h" +#include "info/stories/info_stories_common.h" #include "window/window_session_controller.h" namespace Api { @@ -54,25 +57,6 @@ struct Tag { } // namespace Info::GlobalMedia -namespace Info::Stories { - -enum class Tab { - Saved, - Archive, -}; - -struct Tag { - explicit Tag(not_null peer, Tab tab = {}) - : peer(peer) - , tab(tab) { - } - - not_null peer; - Tab tab = {}; -}; - -} // namespace Info::Stories - namespace Info::BotStarRef { enum class Type : uchar { @@ -99,7 +83,9 @@ public: Key(Settings::Tag settings); Key(Downloads::Tag downloads); Key(Stories::Tag stories); + Key(Saved::MusicTag music); Key(Statistics::Tag statistics); + Key(PeerGifts::Tag gifts); Key(BotStarRef::Tag starref); Key(GlobalMedia::Tag global); Key(not_null poll, FullMsgId contextId); @@ -108,22 +94,27 @@ public: Data::ReactionId selected, FullMsgId contextId); - PeerData *peer() const; - Data::ForumTopic *topic() const; - Data::SavedSublist *sublist() const; - UserData *settingsSelf() const; - bool isDownloads() const; - bool isGlobalMedia() const; - PeerData *storiesPeer() const; - Stories::Tab storiesTab() const; - Statistics::Tag statisticsTag() const; - PeerData *starrefPeer() const; - BotStarRef::Type starrefType() const; - PollData *poll() const; - FullMsgId pollContextId() const; - std::shared_ptr reactionsWhoReadIds() const; - Data::ReactionId reactionsSelected() const; - FullMsgId reactionsContextId() const; + [[nodiscard]] PeerData *peer() const; + [[nodiscard]] Data::ForumTopic *topic() const; + [[nodiscard]] Data::SavedSublist *sublist() const; + [[nodiscard]] UserData *settingsSelf() const; + [[nodiscard]] bool isDownloads() const; + [[nodiscard]] bool isGlobalMedia() const; + [[nodiscard]] PeerData *storiesPeer() const; + [[nodiscard]] int storiesAlbumId() const; + [[nodiscard]] int storiesAddToAlbumId() const; + [[nodiscard]] PeerData *musicPeer() const; + [[nodiscard]] PeerData *giftsPeer() const; + [[nodiscard]] int giftsCollectionId() const; + [[nodiscard]] Statistics::Tag statisticsTag() const; + [[nodiscard]] PeerData *starrefPeer() const; + [[nodiscard]] BotStarRef::Type starrefType() const; + [[nodiscard]] PollData *poll() const; + [[nodiscard]] FullMsgId pollContextId() const; + [[nodiscard]] auto reactionsWhoReadIds() const + -> std::shared_ptr; + [[nodiscard]] Data::ReactionId reactionsSelected() const; + [[nodiscard]] FullMsgId reactionsContextId() const; private: struct PollKey { @@ -142,7 +133,9 @@ private: Settings::Tag, Downloads::Tag, Stories::Tag, + Saved::MusicTag, Statistics::Tag, + PeerGifts::Tag, BotStarRef::Tag, GlobalMedia::Tag, PollKey, @@ -171,6 +164,7 @@ public: Settings, Downloads, Stories, + SavedMusic, PollResults, Statistics, BotStarRef, @@ -244,8 +238,20 @@ public: [[nodiscard]] PeerData *storiesPeer() const { return key().storiesPeer(); } - [[nodiscard]] Stories::Tab storiesTab() const { - return key().storiesTab(); + [[nodiscard]] int storiesAlbumId() const { + return key().storiesAlbumId(); + } + [[nodiscard]] int storiesAddToAlbumId() const { + return key().storiesAddToAlbumId(); + } + [[nodiscard]] PeerData *musicPeer() const { + return key().musicPeer(); + } + [[nodiscard]] PeerData *giftsPeer() const { + return key().giftsPeer(); + } + [[nodiscard]] int giftsCollectionId() const { + return key().giftsCollectionId(); } [[nodiscard]] Statistics::Tag statisticsTag() const { return key().statisticsTag(); @@ -311,6 +317,7 @@ public: return _section; } + void replaceKey(Key key); [[nodiscard]] bool validateMementoPeer( not_null memento) const; diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index 945bda8736..48d883590b 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -211,8 +211,6 @@ std::shared_ptr Memento::DefaultContent( return std::make_shared(peer); case Section::Type::RequestsList: return std::make_shared(peer); - case Section::Type::PeerGifts: - return std::make_shared(peer); case Section::Type::SavedSublists: return std::make_shared(&peer->session()); case Section::Type::Members: diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 4d689a7721..8395abf99a 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -57,7 +57,7 @@ void TopBar::registerUpdateControlCallback( QObject *guard, Callback &&callback) { _updateControlCallbacks[guard] =[ - weak = Ui::MakeWeak(guard), + weak = base::make_weak(guard), callback = std::forward(callback) ](anim::type animated) { if (!weak) { @@ -589,8 +589,8 @@ void TopBar::setStories(rpl::producer content) { } }, _storiesLifetime); - _storiesLifetime.add([weak = QPointer(label)] { - delete weak.data(); + _storiesLifetime.add([weak = base::make_weak(label)] { + delete weak.get(); }); } else { _storiesCount = 0; @@ -598,10 +598,6 @@ void TopBar::setStories(rpl::producer content) { updateControlsVisibility(anim::type::instant); } -void TopBar::setStoriesArchive(bool archive) { - _storiesArchive = archive; -} - void TopBar::setSelectedItems(SelectedItems &&items) { auto wasSelectionMode = selectionMode(); _selectedItems = std::move(items); @@ -638,10 +634,19 @@ void TopBar::updateSelectionState() { _canDelete = computeCanDelete(); _canForward = computeCanForward(); _canUnpinStories = computeCanUnpinStories(); + _canToggleStoryPin = computeCanToggleStoryPin(); + _allStoriesInProfile = computeAllStoriesInProfile(); _selectionText->entity()->setValue(generateSelectedText()); _delete->toggle(_canDelete, anim::type::instant); _forward->toggle(_canForward, anim::type::instant); _toggleStoryInProfile->toggle(_canToggleStoryPin, anim::type::instant); + _toggleStoryInProfile->entity()->setIconOverride( + (_allStoriesInProfile + ? &_st.storiesArchive.icon + : &_st.storiesSave.icon), + (_allStoriesInProfile + ? &_st.storiesArchive.iconOver + : &_st.storiesSave.iconOver)); _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); _toggleStoryPin->entity()->setIconOverride( _canUnpinStories ? &_st.storiesUnpin.icon : nullptr, @@ -662,6 +667,7 @@ void TopBar::createSelectionControls() { _canForward = computeCanForward(); _canUnpinStories = computeCanUnpinStories(); _canToggleStoryPin = computeCanToggleStoryPin(); + _allStoriesInProfile = computeAllStoriesInProfile(); _cancelSelection = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaCancel), @@ -673,6 +679,7 @@ void TopBar::createSelectionControls() { ) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); + _selectionText = wrap(Ui::CreateChild>( this, object_ptr( @@ -683,6 +690,11 @@ void TopBar::createSelectionControls() { st::infoTopBarScale)); _selectionText->setDuration(st::infoTopBarDuration); _selectionText->entity()->resize(0, _st.height); + _selectionText->naturalWidthValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + updateSelectionControlsGeometry(width()); + }, _selectionText->lifetime()); + _forward = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaForward), @@ -720,16 +732,18 @@ void TopBar::createSelectionControls() { this, object_ptr( this, - _storiesArchive ? _st.storiesSave : _st.storiesArchive), + _allStoriesInProfile ? _st.storiesArchive : _st.storiesSave), st::infoTopBarScale)); registerToggleControlCallback( _toggleStoryInProfile.data(), [this] { return selectionMode() && _canToggleStoryPin; }); _toggleStoryInProfile->setDuration(st::infoTopBarDuration); _toggleStoryInProfile->entity()->clicks( - ) | rpl::map_to( - SelectionAction::ToggleStoryInProfile - ) | rpl::start_to_stream( + ) | rpl::map([=] { + return _allStoriesInProfile + ? SelectionAction::ToggleStoryToArchive + : SelectionAction::ToggleStoryToProfile; + }) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); _toggleStoryInProfile->entity()->setVisible(_canToggleStoryPin); @@ -779,6 +793,12 @@ bool TopBar::computeCanToggleStoryPin() const { &SelectedItem::canToggleStoryPin); } +bool TopBar::computeAllStoriesInProfile() const { + return ranges::all_of( + _selectedItems.list, + &SelectedItem::storyInProfile); +} + Ui::StringWithNumbers TopBar::generateSelectedText() const { return _selectedItems.title(_selectedItems.list.size()); } diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h index 71cbaa1a3d..6bd2e7ebbf 100644 --- a/Telegram/SourceFiles/info/info_top_bar.h +++ b/Telegram/SourceFiles/info/info_top_bar.h @@ -63,7 +63,6 @@ public: void setTitle(TitleDescriptor descriptor); void setStories(rpl::producer content); - void setStoriesArchive(bool archive); void enableBackButton(); void highlight(); @@ -131,6 +130,7 @@ private: [[nodiscard]] bool computeCanForward() const; [[nodiscard]] bool computeCanUnpinStories() const; [[nodiscard]] bool computeCanToggleStoryPin() const; + [[nodiscard]] bool computeAllStoriesInProfile() const; void updateSelectionState(); void createSelectionControls(); @@ -178,7 +178,7 @@ private: bool _canForward = false; bool _canToggleStoryPin = false; bool _canUnpinStories = false; - bool _storiesArchive = false; + bool _allStoriesInProfile = false; QPointer> _cancelSelection; QPointer> _selectionText; QPointer> _forward; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 4788066b9b..609a711a16 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -292,11 +292,19 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const { peer->owner().history(peer), FullMsgId()); } else if (const auto storiesPeer = key().storiesPeer()) { - return (key().storiesTab() == Stories::Tab::Saved) - ? Dialogs::RowDescriptor( + return (key().storiesAlbumId() == Stories::ArchiveId()) + ? Dialogs::RowDescriptor() + : Dialogs::RowDescriptor( storiesPeer->owner().history(storiesPeer), - FullMsgId()) - : Dialogs::RowDescriptor(); + FullMsgId()); + } else if (const auto giftsPeer = key().giftsPeer()) { + return Dialogs::RowDescriptor( + giftsPeer->owner().history(giftsPeer), + FullMsgId()); + } else if (const auto musicPeer = key().musicPeer()) { + return Dialogs::RowDescriptor( + musicPeer->owner().history(musicPeer), + FullMsgId()); } else if (key().settingsSelf() || key().isDownloads() || key().reactionsContextId() @@ -318,7 +326,9 @@ void WrapWidget::forceContentRepaint() { } void WrapWidget::setupTop() { - if (HasCustomTopBar(_controller.get()) || wrap() == Wrap::Search) { + if (HasCustomTopBar(_controller.get()) + || wrap() == Wrap::Search + || wrap() == Wrap::StoryAlbumEdit) { _topBar.destroy(); return; } @@ -443,7 +453,7 @@ void WrapWidget::setupTopBarMenuToggle() { } } else if (key.storiesPeer() && key.storiesPeer()->isSelf() - && key.storiesTab() == Stories::Tab::Saved) { + && key.storiesAlbumId() != Stories::ArchiveId()) { const auto &st = (wrap() == Wrap::Layer) ? st::infoLayerTopBarEdit : st::infoTopBarEdit; @@ -475,7 +485,7 @@ void WrapWidget::setupTopBarMenuToggle() { addTopBarMenuButton(); } }, _topBar->lifetime()); - } else if (section.type() == Section::Type::PeerGifts && key.peer()) { + } else if (key.giftsPeer()) { addTopBarMenuButton(); } } @@ -686,8 +696,6 @@ void WrapWidget::finishShowContent() { .subtitle = _content->subtitle(), }); _topBar->setStories(_content->titleStories()); - _topBar->setStoriesArchive( - _controller->key().storiesTab() == Stories::Tab::Archive); } _desiredHeights.fire(desiredHeightForContent()); _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); @@ -742,7 +750,13 @@ rpl::producer WrapWidget::desiredHeightForContent() const { } rpl::producer WrapWidget::selectedListValue() const { - return _selectedLists.events() | rpl::flatten_latest(); + auto current = _content + ? _content->selectedListValue() + : nullptr; + return _selectedLists.events_starting_with(current + ? std::move(current) + : rpl::single(SelectedItems(Storage::SharedMediaType::Photo)) + ) | rpl::flatten_latest(); } object_ptr WrapWidget::createContent( @@ -825,7 +839,6 @@ bool WrapWidget::showInternal( && (params.way == Window::SectionShow::Way::ClearStack); if (_controller->validateMementoPeer(content)) { if (!skipInternal && _content->showInternal(content)) { - highlightTopBar(); return true; } } @@ -870,7 +883,7 @@ rpl::producer WrapWidget::desiredHeightValue() const { QRect WrapWidget::contentGeometry() const { const auto top = _topBar ? _topBar->height() : 0; - return rect().marginsRemoved({ 0, top, 0, 0 }); + return rect().marginsRemoved({ 0, std::min(top, height()), 0, 0}); } bool WrapWidget::returnToFirstStackFrame( @@ -1025,7 +1038,7 @@ object_ptr WrapWidget::createTopBarSurrogate( Assert(_topBar != nullptr); auto result = object_ptr(parent); - result->addClickHandler([weak = Ui::MakeWeak(this)]{ + result->addClickHandler([weak = base::make_weak(this)]{ if (weak) { weak->_controller->showBackFromStack(); } diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index 40d4459d44..3362ec848d 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -53,6 +53,7 @@ enum class Wrap { Narrow, Side, Search, + StoryAlbumEdit, }; struct SelectedItem { @@ -64,6 +65,7 @@ struct SelectedItem { bool canForward = false; bool canToggleStoryPin = false; bool canUnpinStory = false; + bool storyInProfile = false; }; struct SelectedItems { @@ -79,7 +81,8 @@ enum class SelectionAction { Forward, Delete, ToggleStoryPin, - ToggleStoryInProfile, + ToggleStoryToProfile, + ToggleStoryToArchive, }; class WrapWidget final : public Window::SectionWidget { @@ -143,6 +146,8 @@ public: return _removeRequests.events(); } + [[nodiscard]] rpl::producer selectedListValue() const; + void replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs); ~WrapWidget(); @@ -206,7 +211,6 @@ private: not_null window, not_null memento); - rpl::producer selectedListValue() const; bool requireTopBarSearch() const; void addTopBarMenuButton(); diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index ad908c7e43..fda15a1820 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -23,7 +23,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_chat_section.h" #include "info/info_controller.h" #include "info/info_memento.h" +#include "info/peer_gifts/info_peer_gifts_widget.h" #include "info/profile/info_profile_values.h" +#include "info/saved/info_saved_music_widget.h" #include "info/stories/info_stories_widget.h" #include "main/main_session.h" #include "ui/text/text_utilities.h" @@ -246,8 +248,9 @@ not_null AddStoriesButton( not_null navigation, not_null peer, Ui::MultiSlideTracker &tracker) { - auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds( + auto count = rpl::single(0) | rpl::then(Data::AlbumStoriesIds( peer, + 0, // = Data::kStoriesAlbumIdSaved ServerMaxStoryId - 1, 0 ) | rpl::map([](const Data::StoriesIdsSlice &slice) { @@ -385,10 +388,7 @@ not_null AddPeerGiftsButton( if (navigation->showFrozenError()) { return; } - navigation->showSection( - std::make_shared( - peer, - Section::Type::PeerGifts)); + navigation->showSection(Info::PeerGifts::Make(peer)); }); return wrap->entity(); } diff --git a/Telegram/SourceFiles/info/media/info_media_common.cpp b/Telegram/SourceFiles/info/media/info_media_common.cpp index 6791b13275..47f3e80185 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.cpp +++ b/Telegram/SourceFiles/info/media/info_media_common.cpp @@ -31,7 +31,11 @@ UniversalMsgId GetUniversalId(not_null layout) { bool ChangeItemSelection( ListSelectedMap &selected, not_null item, - ListItemSelectionData selectionData) { + ListItemSelectionData selectionData, + int limit) { + if (!limit) { + limit = MaxSelectedItems; + } const auto changeExisting = [&](auto it) { if (it == selected.cend()) { return false; @@ -41,7 +45,7 @@ bool ChangeItemSelection( } return false; }; - if (selected.size() < MaxSelectedItems) { + if (selected.size() < limit) { const auto &[i, ok] = selected.try_emplace(item, selectionData); if (ok) { return true; diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index b309329a81..b8c4830f26 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -32,6 +32,7 @@ struct ListItemSelectionData { bool canForward = false; bool canToggleStoryPin = false; bool canUnpinStory = false; + bool storyInProfile = false; friend inline bool operator==( ListItemSelectionData, @@ -90,7 +91,8 @@ using UniversalMsgId = MsgId; bool ChangeItemSelection( ListSelectedMap &selected, not_null item, - ListItemSelectionData selectionData); + ListItemSelectionData selectionData, + int limit = 0); class ListSectionDelegate { public: diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index dbf7ef776c..939aa6dea9 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/saved/info_saved_music_provider.h" #include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" @@ -29,15 +30,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_download_manager.h" #include "data/data_forum_topic.h" #include "data/data_saved_sublist.h" -#include "history/history_item.h" -#include "history/history_item_helpers.h" -#include "history/history.h" +#include "history/view/media/history_view_save_document_action.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" #include "media/stories/media_stories_controller.h" // ...TogglePinnedToast. #include "media/stories/media_stories_share.h" // PrepareShareBox. #include "window/window_session_controller.h" #include "window/window_peer_menu.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/popup_menu.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/delete_message_context_action.h" @@ -47,8 +51,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/inactive_press.h" #include "lang/lang_keys.h" -#include "main/main_session.h" #include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" #include "mainwidget.h" #include "mainwindow.h" #include "base/platform/base_platform_info.h" @@ -57,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/player/media_player_instance.h" #include "boxes/delete_messages_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/sticker_set_box.h" // StickerPremiumMark #include "core/file_utilities.h" #include "core/application.h" #include "ui/toast/toast.h" @@ -65,6 +71,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_chat.h" +#include "styles/style_credits.h" // giftBoxHiddenMark #include #include @@ -95,6 +102,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->musicPeer()) { + return std::make_unique(controller); } else if (controller->storiesPeer()) { return std::make_unique(controller); } else if (controller->section().type() == Section::Type::GlobalMedia) { @@ -148,9 +157,15 @@ ListWidget::ListWidget( , _controller(controller) , _provider(MakeProvider(_controller)) , _dateBadge(std::make_unique( - _provider->type(), - [=] { scrollDateCheck(); }, - [=] { scrollDateHide(); })) { + _provider->type(), + [=] { scrollDateCheck(); }, + [=] { scrollDateHide(); })) +, _selectedLimit(MaxSelectedItems) +, _storiesAddToAlbumId(controller->storiesAddToAlbumId()) +, _hiddenMark(std::make_unique( + &_controller->session(), + st::giftBoxHiddenMark, + RectPart::Center)) { start(); } @@ -184,6 +199,10 @@ void ListWidget::start() { _provider->setSearchQuery(std::move(query)); }, lifetime()); } else if (_controller->storiesPeer()) { + setupStoriesTrackIds(); + trackSession(&session()); + restart(); + } else if (_controller->musicPeer()) { trackSession(&session()); restart(); } else { @@ -260,6 +279,68 @@ void ListWidget::setupSelectRestriction() { }, lifetime()); } +void ListWidget::setupStoriesTrackIds() { + if (!_storiesAddToAlbumId) { + return; + } + const auto peerId = _controller->storiesPeer()->id; + const auto stories = &session().data().stories(); + + constexpr auto kArchive = Data::kStoriesAlbumIdArchive; + const auto key = Data::StoryAlbumIdsKey{ peerId, kArchive }; + rpl::single(rpl::empty) | rpl::then( + stories->albumIdsChanged() | rpl::filter( + rpl::mappers::_1 == key + ) | rpl::to_empty + ) | rpl::start_with_next([=] { + const auto albumId = _storiesAddToAlbumId; + const auto &ids = stories->albumKnownInArchive(peerId, albumId); + if (_storiesInAlbum != ids) { + for (const auto id : ids) { + if (_storiesInAlbum.emplace(id).second) { + _storyMsgsToMarkSelected.emplace(StoryIdToMsgId(id)); + } + } + if (_storiesInAlbum.size() > ids.size()) { + for (auto i = begin(_storiesInAlbum); i != end(_storiesInAlbum);) { + if (ids.contains(*i)) { + ++i; + } else { + _storyMsgsToMarkSelected.remove(StoryIdToMsgId(*i)); + i = _storiesInAlbum.erase(i); + } + } + } + } + }, lifetime()); + + if (!stories->albumIdsCountKnown(peerId, _storiesAddToAlbumId)) { + stories->albumIdsLoadMore(peerId, _storiesAddToAlbumId); + } + + const auto akey = Data::StoryAlbumIdsKey{ peerId, _storiesAddToAlbumId }; + rpl::single(rpl::empty) | rpl::then( + stories->albumIdsChanged() | rpl::filter( + rpl::mappers::_1 == akey + ) | rpl::to_empty + ) | rpl::start_with_next([=] { + _storiesAddToAlbumTotal = stories->albumIdsCount( + peerId, + _storiesAddToAlbumId); + + const auto albumId = _storiesAddToAlbumId; + const auto &ids = stories->albumKnownInArchive(peerId, albumId); + const auto loadedCount = int(ids.size()); + const auto total = std::max(_storiesAddToAlbumTotal, loadedCount); + const auto nonLoadedInAlbum = total - loadedCount; + + const auto appConfig = &_controller->session().appConfig(); + const auto totalLimit = appConfig->storiesAlbumLimit(); + + _selectedLimit = std::max(totalLimit - nonLoadedInAlbum, 0); + }, lifetime()); +} + rpl::producer ListWidget::scrollToRequests() const { return _scrollToRequests.events(); } @@ -274,8 +355,11 @@ void ListWidget::selectionAction(SelectionAction action) { case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Delete: deleteSelected(); return; - case SelectionAction::ToggleStoryInProfile: - toggleStoryInProfileSelected(); + case SelectionAction::ToggleStoryToProfile: + toggleStoryInProfileSelected(true); + return; + case SelectionAction::ToggleStoryToArchive: + toggleStoryInProfileSelected(false); return; case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; } @@ -357,6 +441,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { result.canForward = selection.canForward; result.canToggleStoryPin = selection.canToggleStoryPin; result.canUnpinStory = selection.canUnpinStory; + result.storyInProfile = selection.storyInProfile; return result; }; auto transformation = [&](const auto &item) { @@ -489,6 +574,10 @@ bool ListWidget::itemVisible(not_null item) { return true; } +not_null ListWidget::hiddenMark() { + return _hiddenMark.get(); +} + QString ListWidget::tooltipText() const { if (const auto link = ClickHandler::getActive()) { return link->tooltip(); @@ -507,10 +596,10 @@ bool ListWidget::tooltipWindowActive() const { void ListWidget::openPhoto(not_null photo, FullMsgId id) { using namespace Data; - const auto tab = _controller->storiesTab(); - const auto context = (tab == Stories::Tab::Archive) - ? Data::StoriesContext{ Data::StoriesContextArchive() } - : Data::StoriesContext{ Data::StoriesContextSaved() }; + const auto albumId = _controller->storiesAlbumId(); + const auto context = Data::StoriesContext{ + Data::StoriesContextAlbum{ albumId } + }; _controller->parentController()->openPhoto( photo, { id, topicRootId(), monoforumPeerId() }, @@ -521,10 +610,10 @@ void ListWidget::openDocument( not_null document, FullMsgId id, bool showInMediaView) { - const auto tab = _controller->storiesTab(); - const auto context = (tab == Stories::Tab::Archive) - ? Data::StoriesContext{ Data::StoriesContextArchive() } - : Data::StoriesContext{ Data::StoriesContextSaved() }; + const auto albumId = _controller->storiesAlbumId(); + const auto context = Data::StoriesContext{ + Data::StoriesContextAlbum{ albumId } + }; _controller->parentController()->openDocument( document, showInMediaView, @@ -544,6 +633,35 @@ void ListWidget::trackSession(not_null session) { }, lifetime); } +void ListWidget::markStoryMsgsSelected() { + const auto now = int(_storyMsgsToMarkSelected.size()); + const auto guard = gsl::finally([&] { + if (now != int(_storyMsgsToMarkSelected.size())) { + pushSelectedItems(); + } + }); + const auto selection = FullSelection; + for (const auto §ion : _sections) { + for (const auto &entry : section.items()) { + const auto item = entry->getItem(); + const auto id = item->id; + const auto i = _storyMsgsToMarkSelected.find(id); + if (i != end(_storyMsgsToMarkSelected)) { + ChangeItemSelection( + _selected, + item, + _provider->computeSelectionData(item, selection), + _selectedLimit); + repaintItem(item); + _storyMsgsToMarkSelected.erase(i); + if (_storyMsgsToMarkSelected.empty()) { + return; + } + } + } + } +} + void ListWidget::refreshRows() { saveScrollState(); @@ -554,6 +672,8 @@ void ListWidget::refreshRows() { for (const auto &item : _sections.back().items()) { trackSession(&item->getItem()->history()->session()); } + } else if (!_storyMsgsToMarkSelected.empty()) { + markStoryMsgsSelected(); } if (const auto count = _provider->fullCount()) { @@ -819,8 +939,9 @@ void ListWidget::paintEvent(QPaintEvent *e) { const auto window = _controller->parentController(); const auto paused = window->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); + const auto selecting = hasSelectedItems() || _storiesAddToAlbumId; auto context = ListContext{ - Overview::Layout::PaintContext(ms, hasSelectedItems(), paused), + Overview::Layout::PaintContext(ms, selecting, paused), &_selected, &_dragSelected, _dragSelectAction @@ -890,6 +1011,9 @@ void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) { void ListWidget::showContextMenu( QContextMenuEvent *e, ContextMenuSource source) { + if (_storiesAddToAlbumId) { + return; + } if (_contextMenu) { _contextMenu = nullptr; repaintItem(_contextItem); @@ -945,6 +1069,11 @@ void ListWidget::showContextMenu( return !item.second.canToggleStoryPin; }); }; + const auto allInProfile = [&] { + return ranges::all_of(_selected, [](auto &&item) { + return item.second.storyInProfile; + }); + }; const auto canUnpinStoryAll = [&] { return ranges::any_of(_selected, [](auto &&item) { return item.second.canUnpinStory; @@ -976,17 +1105,6 @@ void ListWidget::showContextMenu( link->property(kDocumentLinkMediaProperty).toULongLong()) : nullptr; if (lnkPhoto || lnkDocument) { - auto [isVideo, isVoice, isAudio] = [&] { - if (lnkDocument) { - return std::make_tuple( - lnkDocument->isVideoFile(), - lnkDocument->isVoiceMessage(), - lnkDocument->isAudioFile() - ); - } - return std::make_tuple(false, false, false); - }(); - if (lnkPhoto) { } else { if (lnkDocument->loading()) { @@ -1024,16 +1142,11 @@ void ListWidget::showContextMenu( DocumentSaveClickHandler::Mode::ToNewFile); }); if (_provider->allowSaveFileAs(item, lnkDocument)) { - _contextMenu->addAction( - (isVideo - ? tr::lng_context_save_video(tr::now) - : isVoice - ? tr::lng_context_save_audio(tr::now) - : isAudio - ? tr::lng_context_save_audio_file(tr::now) - : tr::lng_context_save_file(tr::now)), - std::move(handler), - &st::menuIconDownload); + HistoryView::AddSaveDocumentAction( + Ui::Menu::CreateAddActionCallback(_contextMenu), + item, + lnkDocument, + _controller->parentController()); } } } @@ -1050,13 +1163,14 @@ void ListWidget::showContextMenu( } if (overSelected == SelectionState::OverSelectedItems) { if (canToggleStoryPinAll()) { - const auto tab = _controller->key().storiesTab(); - const auto toProfile = (tab == Stories::Tab::Archive); + const auto toProfile = !allInProfile(); _contextMenu->addAction( (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_archived_add)(tr::now), - crl::guard(this, [this] { toggleStoryInProfileSelected(); }), + crl::guard(this, [=] { + toggleStoryInProfileSelected(toProfile); + }), (toProfile ? &st::menuIconStoriesSave : &st::menuIconStoriesArchive)); @@ -1102,14 +1216,15 @@ void ListWidget::showContextMenu( item, FullSelection); if (selectionData.canToggleStoryPin) { - const auto tab = _controller->key().storiesTab(); - const auto toProfile = (tab == Stories::Tab::Archive); + const auto toProfile = !selectionData.storyInProfile; _contextMenu->addAction( (toProfile ? tr::lng_mediaview_save_to_profile : tr::lng_mediaview_archive_story)(tr::now), crl::guard(this, [=] { - toggleStoryInProfile({ 1, globalId.itemId }); + toggleStoryInProfile( + { 1, globalId.itemId }, + toProfile); }), (toProfile ? &st::menuIconStoriesSave @@ -1167,7 +1282,7 @@ void ListWidget::showContextMenu( crl::guard(this, [=] { if (hasSelectedText()) { clearSelected(); - } else if (_selected.size() == MaxSelectedItems) { + } else if (_selected.size() == _selectedLimit) { return; } else if (_selected.empty()) { update(); @@ -1180,6 +1295,10 @@ void ListWidget::showContextMenu( } } + if (_contextMenu->empty()) { + _contextMenu = nullptr; + return; + } _contextMenu->setDestroyedCallback(crl::guard( this, [=] { @@ -1224,8 +1343,8 @@ void ListWidget::forwardItems(MessageIdsList &&items) { { id.peer, StoryIdFromMsgId(id.msg) })); } } else { - auto callback = [weak = Ui::MakeWeak(this)] { - if (const auto strong = weak.data()) { + auto callback = [weak = base::make_weak(this)] { + if (const auto strong = weak.get()) { strong->clearSelected(); } }; @@ -1242,10 +1361,11 @@ void ListWidget::deleteSelected() { })); } -void ListWidget::toggleStoryInProfileSelected() { - toggleStoryInProfile(collectSelectedIds(), crl::guard(this, [=] { - clearSelected(); - })); +void ListWidget::toggleStoryInProfileSelected(bool toProfile) { + toggleStoryInProfile( + collectSelectedIds(), + toProfile, + crl::guard(this, [=] { clearSelected(); })); } void ListWidget::toggleStoryPinSelected() { @@ -1260,6 +1380,7 @@ void ListWidget::toggleStoryPinSelected() { void ListWidget::toggleStoryInProfile( MessageIdsList &&items, + bool toProfile, Fn confirmed) { auto list = std::vector(); for (const auto &id : items) { @@ -1272,7 +1393,6 @@ void ListWidget::toggleStoryInProfile( } const auto channel = peerIsChannel(list.front().peer); const auto count = int(list.size()); - const auto toProfile = (_controller->storiesTab() == Stories::Tab::Archive); const auto controller = _controller; const auto sure = [=](Fn close) { using namespace ::Media::Stories; @@ -1373,7 +1493,7 @@ void ListWidget::deleteItems(SelectedItems &&items, Fn confirmed) { : tr::lng_downloads_delete_in_cloud(tr::now)); const auto deleteSure = [=] { Ui::PostponeCall(this, [=] { - if (const auto box = _actionBoxWeak.data()) { + if (const auto box = _actionBoxWeak.get()) { box->closeBox(); } }); @@ -1433,10 +1553,10 @@ void ListWidget::deleteItems(SelectedItems &&items, Fn confirmed) { } } -void ListWidget::setActionBoxWeak(QPointer box) { +void ListWidget::setActionBoxWeak(base::weak_qptr box) { if ((_actionBoxWeak = box)) { _actionBoxWeakLifetime = _actionBoxWeak->alive( - ) | rpl::start_with_done([weak = Ui::MakeWeak(this)]{ + ) | rpl::start_with_done([weak = base::make_weak(this)]{ if (weak) { weak->_checkForHide.fire({}); } @@ -1490,7 +1610,8 @@ void ListWidget::applyItemSelection( && ChangeItemSelection( _selected, item, - _provider->computeSelectionData(item, selection))) { + _provider->computeSelectionData(item, selection), + _selectedLimit)) { repaintItem(item); pushSelectedItems(); } @@ -1960,22 +2081,21 @@ void ListWidget::mouseActionFinish( auto pressState = base::take(_pressState); repaintItem(pressState.item); + const auto selectionMode = hasSelectedItems() || _storiesAddToAlbumId; auto simpleSelectionChange = pressState.item && pressState.inside && !_pressWasInactive && (button != Qt::RightButton) && (_mouseAction == MouseAction::PrepareDrag || _mouseAction == MouseAction::PrepareSelect); - auto needSelectionToggle = simpleSelectionChange - && hasSelectedItems(); - auto needSelectionClear = simpleSelectionChange - && hasSelectedText(); + auto needSelectionToggle = simpleSelectionChange && selectionMode; + auto needSelectionClear = simpleSelectionChange && hasSelectedText(); auto activated = ClickHandler::unpressed(); if (_mouseAction == MouseAction::Dragging || _mouseAction == MouseAction::Selecting) { activated = nullptr; - } else if (needSelectionToggle) { + } else if (needSelectionToggle || _storiesAddToAlbumId) { activated = nullptr; } @@ -2037,7 +2157,8 @@ void ListWidget::applyDragSelection(SelectedMap &applyTo) const { ChangeItemSelection( applyTo, item, - _provider->computeSelectionData(item, FullSelection)); + _provider->computeSelectionData(item, FullSelection), + _selectedLimit); } } else if (_dragSelectAction == DragSelectAction::Deselecting) { for (auto &[item, data] : _dragSelected) { diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 4f4a78e966..4ff5113d1f 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -84,6 +84,7 @@ public: void unregisterHeavyItem(not_null item) override; void repaintItem(not_null item) override; bool itemVisible(not_null item) override; + not_null hiddenMark() override; // AbstractTooltipShower interface QString tooltipText() const override; @@ -171,6 +172,7 @@ private: void itemLayoutChanged(not_null item); void refreshRows(); + void markStoryMsgsSelected(); void trackSession(not_null session); [[nodiscard]] SelectedItems collectSelectedItems() const; @@ -191,11 +193,12 @@ private: void forwardItems(MessageIdsList &&items); void deleteSelected(); void toggleStoryPinSelected(); - void toggleStoryInProfileSelected(); + void toggleStoryInProfileSelected(bool toProfile); void deleteItem(GlobalMsgId globalId); void deleteItems(SelectedItems &&items, Fn confirmed = nullptr); void toggleStoryInProfile( MessageIdsList &&items, + bool toProfile, Fn confirmed = nullptr); void toggleStoryPin( MessageIdsList &&items, @@ -272,7 +275,9 @@ private: void checkMoveToOtherViewer(); void clearHeavyItems(); - void setActionBoxWeak(QPointer box); + void setActionBoxWeak(base::weak_qptr box); + + void setupStoriesTrackIds(); const not_null _controller; const std::unique_ptr _provider; @@ -305,9 +310,16 @@ private: const std::unique_ptr _dateBadge; + int _selectedLimit = 0; + int _storiesAddToAlbumId = 0; + int _storiesAddToAlbumTotal = 0; + base::flat_set _storiesInAlbum; + base::flat_set _storyMsgsToMarkSelected; + std::unique_ptr _hiddenMark; + base::unique_qptr _contextMenu; rpl::event_stream<> _checkForHide; - QPointer _actionBoxWeak; + base::weak_qptr _actionBoxWeak; rpl::lifetime _actionBoxWeakLifetime; QPoint _trippleClickPoint; diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index ef815a3c05..ef1769a98d 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_list_section.h" #include "info/info_controller.h" #include "layout/layout_selection.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "lang/lang_keys.h" #include "history/history.h" @@ -64,6 +65,16 @@ Provider::Provider(not_null controller) layout.second.item->invalidateCache(); } }, _lifetime); + + _controller->session().appConfig().ignoredRestrictionReasonsChanges( + ) | rpl::start_with_next([=](std::vector &&changed) { + const auto sensitive = Data::UnavailableReason::Sensitive(); + if (ranges::contains(changed, sensitive.reason)) { + for (auto &[id, layout] : _layouts) { + layout.item->maybeClearSensitiveSpoiler(); + } + } + }, _lifetime); } Type Provider::type() { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.cpp new file mode 100644 index 0000000000..a237403e60 --- /dev/null +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.cpp @@ -0,0 +1,159 @@ +/* +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/peer_gifts/info_peer_gifts_collections.h" + +#include "api/api_credits.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_star_gift.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/fields/input_field.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_info.h" + +namespace Info::PeerGifts { +namespace { + +constexpr auto kCollectionNameLimit = 12; + +void EditCollectionBox( + not_null box, + not_null navigation, + not_null peer, + int id, + Data::SavedStarGiftId addId, + QString currentName, + Fn finished) { + box->setTitle(id + ? tr::lng_gift_collection_edit() + : tr::lng_gift_collection_new_title()); + + if (!id) { + box->addRow( + object_ptr( + box, + tr::lng_gift_collection_new_text(), + st::collectionAbout)); + } + const auto title = box->addRow( + object_ptr( + box, + st::collectionNameField, + tr::lng_gift_collection_new_ph(), + currentName)); + title->setMaxLength(kCollectionNameLimit * 2); + box->setFocusCallback([=] { + title->setFocusFast(); + }); + + Ui::AddLengthLimitLabel(title, kCollectionNameLimit); + + const auto show = navigation->uiShow(); + const auto session = &peer->session(); + + const auto creating = std::make_shared(false); + const auto submit = [=] { + if (*creating) { + return; + } + const auto text = title->getLastText().trimmed(); + if (text.isEmpty() || text.size() > kCollectionNameLimit) { + title->showError(); + return; + } + + *creating = true; + auto ids = QVector(); + if (addId) { + ids.push_back(Api::InputSavedStarGiftId(addId)); + } + const auto weak = base::make_weak(box); + const auto done = [=](const MTPStarGiftCollection &result) { + *creating = false; + if (const auto onstack = finished) { + onstack(result); + } + if (const auto strong = weak.get()) { + strong->closeBox(); + } + }; + const auto fail = [=](const MTP::Error &error) { + *creating = false; + const auto &type = error.type(); + if (type == u"COLLECTIONS_TOO_MANY"_q) { + show->show(Ui::MakeInformBox({ + .text = tr::lng_gift_collection_limit_text(), + .confirmText = tr::lng_box_ok(), + .title = tr::lng_gift_collection_limit_title(), + })); + if (const auto strong = weak.get()) { + strong->closeBox(); + } + } else { + show->showToast(error.type()); + } + }; + if (id) { + using Flag = MTPpayments_UpdateStarGiftCollection::Flag; + session->api().request(MTPpayments_UpdateStarGiftCollection( + MTP_flags(Flag::f_title), + peer->input, + MTP_int(id), + MTP_string(text), + MTPVector(), + MTPVector(), + MTPVector() + )).done(done).fail(fail).send(); + } else { + session->api().request(MTPpayments_CreateStarGiftCollection( + peer->input, + MTP_string(text), + MTP_vector(ids) + )).done(done).fail(fail).send(); + } + }; + title->submits() | rpl::start_with_next(submit, title->lifetime()); + auto text = id + ? tr::lng_settings_save() + : tr::lng_gift_collection_new_create(); + box->addButton(std::move(text), submit); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace + +void NewCollectionBox( + not_null box, + not_null navigation, + not_null peer, + Data::SavedStarGiftId addId, + Fn added) { + EditCollectionBox(box, navigation, peer, 0, addId, QString(), added); +} + +void EditCollectionNameBox( + not_null box, + not_null navigation, + not_null peer, + int id, + QString current, + Fn done) { + EditCollectionBox(box, navigation, peer, id, {}, current, [=]( + const MTPStarGiftCollection &result) { + done(qs(result.data().vtitle())); + }); +} + +} // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.h new file mode 100644 index 0000000000..c60c32e312 --- /dev/null +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_collections.h @@ -0,0 +1,39 @@ +/* +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 SavedStarGiftId; +} // namespace Data + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window + +namespace Info::PeerGifts { + +void NewCollectionBox( + not_null box, + not_null navigation, + not_null peer, + Data::SavedStarGiftId addId, + Fn added); + +void EditCollectionNameBox( + not_null box, + not_null navigation, + not_null peer, + int id, + QString current, + Fn done); + +} // namespace Info::PeerGifts 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 51c83a2f77..940f1c2671 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_common.h" +#include "base/unixtime.h" #include "boxes/send_credits_box.h" // SetButtonMarkedLabel #include "boxes/star_gift_box.h" #include "boxes/sticker_set_box.h" @@ -20,7 +21,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "main/main_session.h" +#include "overview/overview_checkbox.h" #include "settings/settings_credits_graphics.h" #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" @@ -33,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_credits.h" #include "styles/style_layers.h" +#include "styles/style_overview.h" #include "styles/style_premium.h" namespace Info::PeerGifts { @@ -70,13 +74,26 @@ GiftButton::GiftButton( QWidget *parent, not_null delegate) : AbstractButton(parent) -, _delegate(delegate) { +, _delegate(delegate) +, _lockedTimer([=] { refreshLocked(); }) { } GiftButton::~GiftButton() { unsubscribe(); } +void GiftButton::onStateChanged(State was, StateChangeSource source) { + if (_check) { + const auto diff = state() ^ was; + if (diff & State::Enum::Over) { + _check->setActive(state() & State::Enum::Over); + } + if (diff & State::Enum::Down) { + _check->setPressed(state() & State::Enum::Down); + } + } +} + void GiftButton::unsubscribe() { if (base::take(_subscribed)) { _userpic->subscribeToUpdates(nullptr); @@ -84,6 +101,16 @@ void GiftButton::unsubscribe() { } void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { + _mode = mode; + + if (_mode != GiftButtonMode::Selection) { + _check = nullptr; + } else if (!_check) { + _check = std::make_unique( + [=] { update(); }, + st::overviewSmallCheck); + } + const auto unique = v::is(descriptor) ? v::get(descriptor).info.unique.get() : nullptr; @@ -91,15 +118,20 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { if (_descriptor == descriptor && _resalePrice == resalePrice) { return; } - auto player = base::take(_player); const auto starsType = Ui::Premium::MiniStarsType::SlowStars; - _mediaLifetime.destroy(); unsubscribe(); + update(); + + const auto format = [=](int64 number) { + const auto onlyK = (number < 100'000'000); + return (number >= 1'000'000) + ? Lang::FormatCountToShort(number, onlyK).string + : Lang::FormatCountDecimal(number); + }; _descriptor = descriptor; _resalePrice = resalePrice; const auto resale = (_resalePrice > 0); - _small = (mode != Mode::Full); v::match(descriptor, [&](const GiftTypePremium &data) { const auto months = data.months; _text = Ui::Text::String(st::giftBoxGiftHeight / 4); @@ -136,16 +168,17 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { { 0., anim::with_alpha(st::windowActiveTextFg->c, .3) }, { 1., st::windowActiveTextFg->c }, }); + _lockedUntilDate = 0; }, [&](const GiftTypeStars &data) { const auto soldOut = data.info.limitedCount && !data.userpic && !data.info.limitedLeft; - _userpic = !data.userpic + _userpic = (!data.userpic || _mode == GiftButtonMode::Selection) ? nullptr : data.from ? Ui::MakeUserpicThumbnail(data.from) : Ui::MakeHiddenAuthorThumbnail(); - if (_small && !resale) { + if (small() && !resale) { _price = {}; _stars.reset(); return; @@ -153,22 +186,22 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { _price.setMarkedText( st::semiboldTextStyle, (data.resale - ? (unique + ? ((unique && data.forceTon) + ? Data::FormatGiftResaleTon(*unique) + : (unique ? _delegate->monostar() - : _delegate->star()).append(' ').append( - Lang::FormatCountDecimal(unique - ? unique->starsForResale - : data.info.starsResellMin) - ).append(data.info.resellCount > 1 ? "+" : "") - : (_small && unique && unique->starsForResale) - ? _delegate->monostar().append(' ').append( - Lang::FormatCountDecimal(unique->starsForResale)) + : _delegate->star()).append(' ').append( + format(unique + ? unique->starsForResale + : data.info.starsResellMin) + ).append(data.info.resellCount > 1 ? "+" : "")) + : (small() && unique && unique->starsForResale) + ? Data::FormatGiftResaleAsked(*unique) : unique ? tr::lng_gift_transfer_button( tr::now, Ui::Text::WithEntities) - : _delegate->star().append( - ' ' + Lang::FormatCountDecimal(data.info.stars))), + : _delegate->star().append(' ' + format(data.info.stars))), kMarkupTextOptions, _delegate->textContext()); if (!_stars) { @@ -189,18 +222,28 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { _stars->setColorOverride( Ui::Premium::CreditsIconGradientStops()); } + _lockedUntilDate = data.info.lockedUntilDate; }); - _delegate->sticker( + + refreshLocked(); + + _resolvedDocument = nullptr; + _documentLifetime = _delegate->sticker( descriptor ) | rpl::start_with_next([=](not_null document) { + _documentLifetime.destroy(); setDocument(document); - }, lifetime()); + }); + if (_resolvedDocument) { + _documentLifetime.destroy(); + } + _patterned = false; _uniqueBackgroundCache = QImage(); _uniquePatternEmoji = nullptr; _uniquePatternCache.clear(); - if (_small && !resale) { + if (small() && !resale) { _button = QRect(); return; } @@ -211,7 +254,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { QSize(buttonw, buttonh) ).marginsAdded(st::giftBoxButtonPadding); const auto skipy = _delegate->buttonSize().height() - - (_small + - (small() ? st::giftBoxButtonBottomSmall : _byStars.isEmpty() ? st::giftBoxButtonBottom @@ -226,16 +269,35 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { } } -bool GiftButton::documentResolved() const { - return _player || _mediaLifetime; +void GiftButton::refreshLocked() { + _lockedTimer.cancel(); + + const auto lockedFor = _lockedUntilDate + ? std::max(_lockedUntilDate - base::unixtime::now(), TimeId()) + : TimeId(); + const auto locked = (lockedFor > 0); + if (locked) { + _lockedTimer.callOnce(std::min(lockedFor, 86'400) * crl::time(1000)); + } + if (_locked != locked) { + _locked = locked; + update(); + } } void GiftButton::setDocument(not_null document) { + _resolvedDocument = document; + if (_playerDocument == document) { + return; + } + const auto media = document->createMediaView(); media->checkStickerLarge(); media->goodThumbnailWanted(); - rpl::single() | rpl::then( + const auto destroyed = base::take(_player); + _playerDocument = nullptr; + _mediaLifetime = rpl::single() | rpl::then( document->owner().session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); @@ -263,9 +325,13 @@ void GiftButton::setDocument(not_null document) { st::giftBoxStickerSize); } result->setRepaintCallback([=] { update(); }); + _playerDocument = media->owner(); _player = std::move(result); update(); - }, _mediaLifetime); + }); + if (_playerDocument) { + _mediaLifetime.destroy(); + } } void GiftButton::setGeometry(QRect inner, QMargins extend) { @@ -274,17 +340,36 @@ void GiftButton::setGeometry(QRect inner, QMargins extend) { } QMargins GiftButton::currentExtend() const { - const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + const auto progress = (_mode == Mode::Selection) + ? 0. + : _selectedAnimation.value(_selected ? 1. : 0.); const auto added = anim::interpolate(0, st::giftBoxSelectSkip, progress); return _extend + QMargins(added, added, added, added); } -void GiftButton::toggleSelected(bool selected) { +bool GiftButton::small() const { + return _mode != GiftButtonMode::Full; +} + +void GiftButton::toggleSelected(bool selected, anim::type animated) { if (_selected == selected) { + if (animated == anim::type::instant) { + _selectedAnimation.stop(); + } return; } const auto duration = st::defaultRoundCheckbox.duration; _selected = selected; + if (animated == anim::type::instant) { + if (_check) { + _check->finishAnimating(); + } + _selectedAnimation.stop(); + return; + } else if (_check) { + _check->setChecked(selected, animated); + return; + } _selectedAnimation.start([=] { update(); }, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc); @@ -330,7 +415,9 @@ void GiftButton::paintBackground(QPainter &p, const QImage &background) { } auto hq = PainterHighQualityEnabler(p); - const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + const auto progress = (_mode == Mode::Selection) + ? 0. + : _selectedAnimation.value(_selected ? 1. : 0.); if (progress < 0.01) { return; } @@ -418,9 +505,13 @@ void GiftButton::paintEvent(QPaintEvent *e) { const auto unique = v::is(_descriptor) ? v::get(_descriptor).info.unique.get() : nullptr; - const auto onsale = (unique && unique->starsForResale && _small); + const auto onsale = unique && unique->starsForResale && small(); + const auto requirePremium = v::is(_descriptor) + && !v::get(_descriptor).userpic + && !v::get(_descriptor).info.unique + && v::get(_descriptor).info.requirePremium; const auto hidden = v::is(_descriptor) - && v::get(_descriptor).hidden;; + && v::get(_descriptor).hidden; const auto extend = currentExtend(); const auto position = QPoint(extend.left(), extend.top()); const auto background = _delegate->background(); @@ -430,6 +521,22 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (unique) { cacheUniqueBackground(unique, width, background.height() / dpr); p.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache); + } else if (requirePremium) { + auto hq = PainterHighQualityEnabler(p); + auto pen = st::creditsFg->p; + pen.setWidth(style::ConvertScaleExact(2.)); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const auto outer = QRect(0, 0, width, background.height() / dpr); + const auto extend = currentExtend(); + const auto radius = st::giftBoxGiftRadius; + p.drawRoundedRect(outer.marginsRemoved(extend), radius, radius); + } + if (_locked) { + st::giftBoxLockIcon.paint( + p, + position + st::giftBoxLockIconPosition, + width); } if (_userpic) { @@ -440,6 +547,14 @@ void GiftButton::paintEvent(QPaintEvent *e) { const auto image = _userpic->image(st::giftBoxUserpicSize); const auto skip = st::giftBoxUserpicSkip; p.drawImage(extend.left() + skip, extend.top() + skip, image); + } else if (_check) { + const auto skip = st::giftBoxUserpicSkip; + _check->paint( + p, + QPoint(extend.left() + skip, extend.top() + skip), + width, + _selected, + true); } auto frame = QImage(); @@ -460,7 +575,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { p.drawImage( QRect( (width - size.width()) / 2, - (_small + (small() ? st::giftBoxSmallStickerTop : _text.isEmpty() ? st::giftBoxStickerStarTop @@ -474,7 +589,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (hidden) { const auto topleft = QPoint( (width - st::giftBoxStickerSize.width()) / 2, - (_small + (small() ? st::giftBoxSmallStickerTop : _text.isEmpty() ? st::giftBoxStickerStarTop @@ -515,6 +630,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { const auto pinned = data.pinned || data.pinnedSelection; if (count || pinned) { const auto soldOut = !pinned + && !unique && !data.userpic && !data.info.limitedLeft; return GiftBadge{ @@ -526,6 +642,10 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? tr::lng_gift_stars_resale(tr::now) : soldOut ? tr::lng_gift_stars_sold_out(tr::now) + : (!data.userpic + && !data.info.unique + && data.info.requirePremium) + ? tr::lng_gift_stars_premium(tr::now) : (!data.userpic && !data.info.unique) ? tr::lng_gift_stars_limited(tr::now) : (count == 1) @@ -544,6 +664,8 @@ void GiftButton::paintEvent(QPaintEvent *e) { ? st::boxTextFgGood->c : soldOut ? st::attentionButtonFg->c + : (!data.userpic && data.info.requirePremium) + ? st::creditsFg->c : st::windowActiveTextFg->c), .bg2 = (onsale ? QColor(0, 0, 0, 0) @@ -580,7 +702,8 @@ void GiftButton::paintEvent(QPaintEvent *e) { v::match(_descriptor, [](const GiftTypePremium &) { }, [&](const GiftTypeStars &data) { - if (unique && data.pinned) { + if (!unique) { + } else if (data.pinned) { auto hq = PainterHighQualityEnabler(p); const auto &icon = st::giftBoxPinIcon; const auto skip = st::giftBoxUserpicSkip; @@ -592,6 +715,26 @@ void GiftButton::paintEvent(QPaintEvent *e) { QSize(icon.width() + 2 * add, icon.height() + 2 * add)); p.drawEllipse(rect); icon.paintInCenter(p, rect); + } else if (!data.forceTon + && unique->nanoTonForResale + && unique->onlyAcceptTon) { + if (_tonIcon.isNull()) { + _tonIcon = st::tonIconEmojiLarge.icon.instance( + QColor(255, 255, 255)); + } + const auto size = _tonIcon.size() / _tonIcon.devicePixelRatio(); + const auto skip = st::giftBoxUserpicSkip; + const auto add = (st::giftBoxUserpicSize - size.width()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(unique->backdrop.patternColor); + const auto rect = QRect( + QPoint(extend.left() + skip, extend.top() + skip), + QSize(size.width() + 2 * add, size.height() + 2 * add)); + p.drawEllipse(rect); + p.drawImage( + extend.left() + skip + add, + extend.top() + skip + add, + _tonIcon); } }); @@ -672,6 +815,10 @@ Delegate::Delegate(not_null session, GiftButtonMode mode) st::giftBoxHiddenMark, RectPart::Center)) , _mode(mode) { + _ministarEmoji = _emojiHelper.paletteDependent( + Ui::Earn::IconCreditsEmojiSmall()); + _starEmoji = _emojiHelper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); } Delegate::Delegate(Delegate &&other) = default; @@ -679,21 +826,23 @@ Delegate::Delegate(Delegate &&other) = default; Delegate::~Delegate() = default; TextWithEntities Delegate::star() { - return _session->data().customEmojiManager().creditsEmoji(); + return _starEmoji; } TextWithEntities Delegate::monostar() { return Ui::Text::IconEmoji(&st::starIconEmoji); } +TextWithEntities Delegate::monoton() { + return Ui::Text::IconEmoji(&st::tonIconEmoji); +} + TextWithEntities Delegate::ministar() { - const auto owner = &_session->data(); - const auto top = st::giftBoxByStarsStarTop; - return owner->customEmojiManager().ministarEmoji({ 0, top, 0, 0 }); + return _ministarEmoji; } Ui::Text::MarkedContext Delegate::textContext() { - return Core::TextContext({ .session = _session }); + return _emojiHelper.context(); } QSize Delegate::buttonSize() { @@ -705,7 +854,7 @@ QSize Delegate::buttonSize() { const auto available = width - padding.left() - padding.right(); const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) / kGiftsPerRow; - const auto minimal = (_mode == GiftButtonMode::Minimal); + const auto minimal = (_mode != GiftButtonMode::Full); _single = QSize( singlew, minimal ? st::giftBoxGiftSmall : st::giftBoxGiftHeight); @@ -933,13 +1082,15 @@ void SelectGiftToUnpin( box, tr::lng_gift_many_pinned_title(), st::giftBoxSubtitle), - st::giftBoxSubtitleMargin); + st::giftBoxSubtitleMargin, + style::al_top); box->addRow( object_ptr( box, tr::lng_gift_many_pinned_choose(), st::giftTooManyPinnedChoose), - st::giftBoxAboutMargin); + st::giftBoxAboutMargin, + style::al_top); const auto gifts = box->addRow( object_ptr(box), @@ -1015,9 +1166,9 @@ void SelectGiftToUnpin( } Assert(index < int(pinned.size())); const auto &entry = pinned[index]; - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); chosen(::Settings::EntryToSavedStarGiftId(session, entry)); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); 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 7c8b006dda..5f02908e7a 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/qt/qt_compare.h" +#include "base/timer.h" #include "data/data_star_gift.h" #include "ui/abstract_button.h" #include "ui/effects/premium_stars_colored.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/text.h" class StickerPremiumMark; @@ -33,6 +35,10 @@ namespace Main { class Session; } // namespace Main +namespace Overview::Layout { +class Checkbox; +} // namespace Overview::Layout + namespace Ui { class DynamicImage; } // namespace Ui @@ -47,6 +53,16 @@ class SessionController; namespace Info::PeerGifts { +struct Tag { + explicit Tag(not_null peer, int collectionId = 0) + : peer(peer) + , collectionId(collectionId) { + } + + not_null peer; + int collectionId = 0; +}; + struct GiftTypePremium { int64 cost = 0; QString currency; @@ -65,6 +81,7 @@ struct GiftTypeStars { PeerData *from = nullptr; TimeId date = 0; bool pinnedSelection : 1 = false; + bool forceTon : 1 = false; bool userpic : 1 = false; bool pinned : 1 = false; bool hidden : 1 = false; @@ -106,15 +123,17 @@ struct GiftBadge { const GiftBadge &) = default; }; -enum class GiftButtonMode { +enum class GiftButtonMode : uint8 { Full, Minimal, + Selection, }; class GiftButtonDelegate { public: [[nodiscard]] virtual TextWithEntities star() = 0; [[nodiscard]] virtual TextWithEntities monostar() = 0; + [[nodiscard]] virtual TextWithEntities monoton() = 0; [[nodiscard]] virtual TextWithEntities ministar() = 0; [[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0; [[nodiscard]] virtual QSize buttonSize() = 0; @@ -139,7 +158,9 @@ public: void setDescriptor(const GiftDescriptor &descriptor, Mode mode); void setGeometry(QRect inner, QMargins extend); - void toggleSelected(bool selected); + void toggleSelected( + bool selected, + anim::type animated = anim::type::normal); [[nodiscard]] rpl::producer contextMenuRequests() const { return _contextMenuRequests.events(); @@ -156,10 +177,12 @@ private: int width, int height); + void refreshLocked(); void setDocument(not_null document); - [[nodiscard]] bool documentResolved() const; [[nodiscard]] QMargins currentExtend() const; + [[nodiscard]] bool small() const; + void onStateChanged(State was, StateChangeSource source) override; void unsubscribe(); const not_null _delegate; @@ -171,21 +194,31 @@ private: Ui::Text::String _byStars; std::shared_ptr _userpic; QImage _uniqueBackgroundCache; + QImage _tonIcon; std::unique_ptr _uniquePatternEmoji; base::flat_map _uniquePatternCache; std::optional _stars; Ui::Animations::Simple _selectedAnimation; + std::unique_ptr _check; int _resalePrice = 0; + GiftButtonMode _mode = GiftButtonMode::Full; bool _subscribed = false; bool _patterned = false; bool _selected = false; - bool _small = false; + bool _locked = false; + + base::Timer _lockedTimer; + TimeId _lockedUntilDate = 0; QRect _button; QMargins _extend; + DocumentData *_resolvedDocument = nullptr; + std::unique_ptr _player; + DocumentData *_playerDocument = nullptr; rpl::lifetime _mediaLifetime; + rpl::lifetime _documentLifetime; }; @@ -197,6 +230,7 @@ public: TextWithEntities star() override; TextWithEntities monostar() override; + TextWithEntities monoton() override; TextWithEntities ministar() override; Ui::Text::MarkedContext textContext() override; QSize buttonSize() override; @@ -218,6 +252,9 @@ private: QSize _single; QImage _bg; GiftButtonMode _mode = GiftButtonMode::Full; + Ui::Text::CustomEmojiHelper _emojiHelper; + TextWithEntities _ministarEmoji; + TextWithEntities _starEmoji; }; 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 8fd731a9e5..d3a649be30 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -7,18 +7,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_widget.h" +#include "api/api_credits.h" +#include "api/api_hash.h" #include "api/api_premium.h" #include "apiwrap.h" +#include "boxes/share_box.h" #include "boxes/star_gift_box.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_session.h" #include "data/data_user.h" +#include "info/peer_gifts/info_peer_gifts_collections.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "info/info_controller.h" +#include "info/info_memento.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/sub_tabs.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" @@ -60,15 +70,43 @@ constexpr auto kPerPage = 50; }; } +[[nodiscard]] Data::GiftCollection FromTL( + not_null session, + const MTPStarGiftCollection &collection) { + const auto &data = collection.data(); + return { + .id = data.vcollection_id().v, + .count = data.vgifts_count().v, + .title = qs(data.vtitle()), + .icon = (data.vicon() + ? session->data().processDocument(*data.vicon()).get() + : nullptr), + .hash = data.vhash().v, + }; +} + +[[nodiscard]] std::vector FromTL( + not_null session, + const MTPDpayments_starGiftCollections &data) { + auto result = std::vector(); + + const auto &list = data.vcollections().v; + result.reserve(list.size()); + for (const auto &collection : list) { + result.push_back(FromTL(session, collection)); + } + return result; +} + } // namespace class InnerWidget final : public Ui::BoxContentDivider { public: InnerWidget( QWidget *parent, - not_null controller, + not_null window, not_null peer, - rpl::producer filter); + rpl::producer descriptor); [[nodiscard]] not_null peer() const { return _peer; @@ -76,13 +114,29 @@ public: [[nodiscard]] rpl::producer notifyEnabled() const { return _notifyEnabled.events(); } - [[nodiscard]] rpl::producer<> resetFilterRequests() const { - return _resetFilterRequests.events(); + [[nodiscard]] rpl::producer descriptorChanges() const { + return _descriptorChanges.events(); } [[nodiscard]] rpl::producer<> scrollToTop() const { return _scrollToTop.events(); } + [[nodiscard]] rpl::producer changes() const { + return _collectionChanges.value(); + } + + [[nodiscard]] rpl::producer collectionEmptyValue() const { + return _collectionEmpty.value(); + } + + void reloadCollection(int id); + void editCollectionGifts(int id); + void shareCollectionLink(const QString &username, int id); + void editCollectionName(int id); + void confirmDeleteCollection(int id); + void collectionAdded(MTPStarGiftCollection result); + void fillMenu(const Ui::Menu::MenuCallback &addAction); + void saveState(not_null memento); void restoreState(not_null memento); @@ -91,6 +145,12 @@ private: Data::SavedStarGift gift; GiftDescriptor descriptor; }; + struct Entries { + std::vector list; + std::optional filter; + int total = 0; + bool allLoaded = false; + }; struct View { std::unique_ptr button; Data::SavedStarGiftId manageId; @@ -98,18 +158,37 @@ private: int index = 0; }; +public: + InnerWidget( + QWidget *parent, + not_null window, + not_null peer, + rpl::producer descriptor, + int addingToCollectionId, + Entries all); + +private: void visibleTopBottomUpdated( int visibleTop, int visibleBottom) override; void paintEvent(QPaintEvent *e) override; void subscribeToUpdates(); + void applyUpdateTo(Entries &entries, const Data::GiftUpdate &update); + void loadCollections(); void loadMore(); + void loaded(const MTPpayments_SavedStarGifts &result); + void markInCollection(const Data::SavedStarGift &gift); void refreshButtons(); void validateButtons(); void showGift(int index); void showMenuFor(not_null button, QPoint point); + void showMenuForCollection(int id); void refreshAbout(); + void refreshCollectionsTabs(); + + void collectionRenamed(int id, QString name); + void collectionRemoved(int id); void markPinned(std::vector::iterator i); void markUnpinned(std::vector::iterator i); @@ -120,23 +199,34 @@ private: -> Fn()>; const not_null _window; - rpl::variable _filter; - Delegate _delegate; - not_null _controller; - std::unique_ptr _about; const not_null _peer; - std::vector _entries; - int _totalCount = 0; + const int _addingToCollectionId = 0; + const GiftButtonMode _mode; + + rpl::variable _descriptor; + Delegate _delegate; + std::unique_ptr _collectionsTabs; + std::unique_ptr _about; rpl::event_stream<> _scrollToTop; + rpl::variable _collectionEmpty; + + std::vector _collections; + + Entries _all; + std::map _perCollection; + not_null _entries; + not_null*> _list; + rpl::variable _collectionChanges; + base::flat_set _inCollection; MTP::Sender _api; mtpRequestId _loadMoreRequestId = 0; + Fn _collectionsLoadedCallback; QString _offset; - bool _allLoaded = false; bool _reloading = false; - bool _aboutFiltered = false; + bool _collectionsLoaded = false; - rpl::event_stream<> _resetFilterRequests; + rpl::event_stream _descriptorChanges; rpl::event_stream _notifyEnabled; std::vector _views; int _viewsForWidth = 0; @@ -155,16 +245,41 @@ private: InnerWidget::InnerWidget( QWidget *parent, - not_null controller, + not_null window, not_null peer, - rpl::producer filter) + rpl::producer descriptor) +: InnerWidget( + parent, + window, + peer, + std::move(descriptor), + 0, + { .total = peer->peerGiftsCount() }) { +} + +InnerWidget::InnerWidget( + QWidget *parent, + not_null window, + not_null peer, + rpl::producer descriptor, + int addingToCollectionId, + Entries all) : BoxContentDivider(parent) -, _window(controller->parentController()) -, _filter(std::move(filter)) -, _delegate(&_window->session(), GiftButtonMode::Minimal) -, _controller(controller) +, _window(window) , _peer(peer) -, _totalCount(_peer->peerGiftsCount()) +, _addingToCollectionId(addingToCollectionId) +, _mode(_addingToCollectionId + ? GiftButtonMode::Selection + : GiftButtonMode::Minimal) +, _descriptor(std::move(descriptor)) +, _delegate(&_window->session(), _mode) +, _all(std::move(all)) +, _entries(&_all) +, _list(&_entries->list) +, _collectionChanges(Data::GiftsUpdate{ + .peer = _peer, + .collectionId = addingToCollectionId, +}) , _api(&_peer->session().mtp()) { _singleMin = _delegate.buttonSize(); @@ -172,90 +287,170 @@ InnerWidget::InnerWidget( subscribeToUpdates(); } - _filter.value() | rpl::start_with_next([=] { - _reloading = true; + for (const auto &entry : _all.list) { + markInCollection(entry.gift); + } + + loadCollections(); + + _window->session().data().giftsUpdates( + ) | rpl::start_with_next([=](const Data::GiftsUpdate &update) { + if (update.peer != _peer) { + return; + } + const auto added = base::flat_set{ + begin(update.added), + end(update.added) + }; + const auto removed = base::flat_set{ + begin(update.removed), + end(update.removed) + }; + const auto id = update.collectionId; + const auto process = [&](Entries &entries) { + for (auto &entry : entries.list) { + if (added.contains(entry.gift.manageId)) { + entry.gift.collectionIds.push_back(id); + } else if (removed.contains(entry.gift.manageId)) { + entry.gift.collectionIds.erase( + ranges::remove(entry.gift.collectionIds, id), + end(entry.gift.collectionIds)); + } + } + }; + for (auto &[_, entries] : _perCollection) { + process(entries); + } + process(_all); + }, lifetime()); + + _descriptor.value( + ) | rpl::start_with_next([=](Descriptor now) { + const auto id = now.collectionId; + _collectionsLoadedCallback = nullptr; _api.request(base::take(_loadMoreRequestId)).cancel(); - _allLoaded = false; + _entries = id ? &_perCollection[id] : &_all; + _list = &_entries->list; + refreshButtons(); refreshAbout(); loadMore(); }, lifetime()); } +void InnerWidget::loadCollections() { + if (_addingToCollectionId) { + return; + } + _api.request(MTPpayments_GetStarGiftCollections( + _peer->input, + MTP_long(Api::CountHash(_collections + | ranges::views::transform(&Data::GiftCollection::hash))) + )).done([=](const MTPpayments_StarGiftCollections &result) { + result.match([&](const MTPDpayments_starGiftCollections &data) { + _collections = FromTL(&_window->session(), data); + + refreshCollectionsTabs(); + }, [&](const MTPDpayments_starGiftCollectionsNotModified &) { + }); + _collectionsLoaded = true; + if (const auto onstack = base::take(_collectionsLoadedCallback)) { + onstack(); + } + }).fail([=] { + _collectionsLoaded = true; + if (const auto onstack = base::take(_collectionsLoadedCallback)) { + onstack(); + } + }).send(); +} + void InnerWidget::subscribeToUpdates() { _peer->owner().giftUpdates( ) | rpl::start_with_next([=](const Data::GiftUpdate &update) { - const auto savedId = [](const Entry &entry) { - return entry.gift.manageId; - }; - const auto bySlug = [](const Entry &entry) { - return entry.gift.info.unique - ? entry.gift.info.unique->slug - : QString(); - }; - const auto i = update.id - ? ranges::find(_entries, update.id, savedId) - : ranges::find(_entries, update.slug, bySlug); - if (i == end(_entries)) { - return; - } - const auto index = int(i - begin(_entries)); + applyUpdateTo(_all, update); using Action = Data::GiftUpdate::Action; - if (update.action == Action::Convert - || update.action == Action::Transfer - || update.action == Action::Delete) { - _entries.erase(i); - if (_totalCount > 0) { - --_totalCount; + if (update.action == Action::Pin || update.action == Action::Unpin) { + for (auto &[_, entries] : _perCollection) { + applyUpdateTo(entries, update); } - for (auto &view : _views) { - if (view.index >= index) { - --view.index; - } - } - } else if (update.action == Action::Save - || update.action == Action::Unsave) { - i->gift.hidden = (update.action == Action::Unsave); - - const auto unpin = i->gift.hidden && i->gift.pinned; - v::match(i->descriptor, [](GiftTypePremium &) { - }, [&](GiftTypeStars &data) { - data.hidden = i->gift.hidden; - }); - for (auto &view : _views) { - if (view.index == index) { - view.index = -1; - view.manageId = {}; - } - } - if (unpin) { - markUnpinned(i); - } - } else if (update.action == Action::Pin - || update.action == Action::Unpin) { - if (update.action == Action::Pin) { - markPinned(i); - } else { - markUnpinned(i); - } - } else if (update.action == Action::ResaleChange) { - for (auto &view : _views) { - if (view.index == index) { - view.index = -1; - view.manageId = {}; - } - } - } else { - return; - } - refreshButtons(); - if (update.action == Action::Pin) { - _scrollToTop.fire({}); - } + }; }, lifetime()); } +void InnerWidget::applyUpdateTo( + Entries &entries, + const Data::GiftUpdate &update) { + using Action = Data::GiftUpdate::Action; + const auto savedId = [](const Entry &entry) { + return entry.gift.manageId; + }; + const auto bySlug = [](const Entry &entry) { + return entry.gift.info.unique + ? entry.gift.info.unique->slug + : QString(); + }; + const auto i = update.id + ? ranges::find(*_list, update.id, savedId) + : ranges::find(*_list, update.slug, bySlug); + if (i == end(*_list)) { + return; + } + const auto index = int(i - begin(*_list)); + if (update.action == Action::Convert + || update.action == Action::Transfer + || update.action == Action::Delete) { + _list->erase(i); + if (entries.total > 0) { + --entries.total; + } + for (auto &view : _views) { + if (view.index >= index) { + --view.index; + } + } + } else if (update.action == Action::Save + || update.action == Action::Unsave) { + i->gift.hidden = (update.action == Action::Unsave); + + const auto unpin = i->gift.hidden && i->gift.pinned; + v::match(i->descriptor, [](GiftTypePremium &) { + }, [&](GiftTypeStars &data) { + data.hidden = i->gift.hidden; + }); + for (auto &view : _views) { + if (view.index == index) { + view.index = -1; + view.manageId = {}; + } + } + if (unpin) { + markUnpinned(i); + } + } else if (update.action == Action::Pin + || update.action == Action::Unpin) { + if (update.action == Action::Pin) { + markPinned(i); + } else { + markUnpinned(i); + } + } else if (update.action == Action::ResaleChange) { + for (auto &view : _views) { + if (view.index == index) { + view.index = -1; + view.manageId = {}; + } + } + } else { + return; + } + refreshButtons(); + if (update.action == Action::Pin) { + _scrollToTop.fire({}); + } +} + void InnerWidget::markPinned(std::vector::iterator i) { - const auto index = int(i - begin(_entries)); + const auto index = int(i - begin(*_list)); i->gift.pinned = true; v::match(i->descriptor, [](const GiftTypePremium &) { @@ -263,13 +458,13 @@ void InnerWidget::markPinned(std::vector::iterator i) { data.pinned = true; }); if (index) { - std::rotate(begin(_entries), i, i + 1); + std::rotate(begin(*_list), i, i + 1); } - auto unpin = end(_entries); + auto unpin = end(*_list); const auto session = &_window->session(); const auto limit = session->appConfig().pinnedGiftsLimit(); - if (limit < _entries.size()) { - const auto j = begin(_entries) + limit; + if (limit < _list->size()) { + const auto j = begin(*_list) + limit; if (j->gift.pinned) { unpin = j; } @@ -280,13 +475,13 @@ void InnerWidget::markPinned(std::vector::iterator i) { view.manageId = {}; } } - if (unpin != end(_entries)) { + if (unpin != end(_entries->list)) { markUnpinned(unpin); } } void InnerWidget::markUnpinned(std::vector::iterator i) { - const auto index = int(i - begin(_entries)); + const auto index = int(i - begin(*_list)); i->gift.pinned = false; v::match(i->descriptor, [](const GiftTypePremium &) { @@ -294,19 +489,19 @@ void InnerWidget::markUnpinned(std::vector::iterator i) { data.pinned = false; }); auto after = index + 1; - for (auto j = i + 1; j != end(_entries); ++j) { + for (auto j = i + 1; j != end(*_list); ++j) { if (!j->gift.pinned && j->gift.date <= i->gift.date) { break; } ++after; } - if (after == _entries.size() && !_allLoaded) { + if (after == _list->size() && !_entries->allLoaded) { // We don't know if the correct position is exactly in the end // of the loaded part or later, so we hide it for now, let it // be loaded later while scrolling. - _entries.erase(i); + _list->erase(i); } else if (after > index + 1) { - std::rotate(i, i + 1, begin(_entries) + after); + std::rotate(i, i + 1, begin(*_list) + after); } for (auto &view : _views) { if (view.index >= index) { @@ -342,68 +537,132 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } +void InnerWidget::collectionAdded(MTPStarGiftCollection result) { + _collections.push_back(FromTL(&_window->session(), result)); + const auto id = _collections.back().id; + refreshCollectionsTabs(); + + _collectionsTabs->setActiveTab(QString::number(id)); + + auto now = _descriptor.current(); + now.collectionId = id; + _descriptorChanges.fire(std::move(now)); +} + void InnerWidget::loadMore() { - if (_allLoaded || _loadMoreRequestId) { + const auto descriptor = _descriptor.current(); + const auto filter = descriptor.filter; + const auto filterChanged = (_entries->filter != filter); + const auto allLoaded = !filterChanged && _entries->allLoaded; + if (allLoaded || _loadMoreRequestId) { return; } using Flag = MTPpayments_GetSavedStarGifts::Flag; - const auto filter = _filter.current(); + const auto collectionId = descriptor.collectionId; _loadMoreRequestId = _api.request(MTPpayments_GetSavedStarGifts( MTP_flags((filter.sortByValue ? Flag::f_sort_by_value : Flag()) - | (filter.skipLimited ? Flag::f_exclude_limited : Flag()) + | (filter.skipLimited ? Flag::f_exclude_unupgradable : Flag()) + | (filter.skipUpgradable ? Flag::f_exclude_upgradable : Flag()) | (filter.skipUnlimited ? Flag::f_exclude_unlimited : Flag()) | (filter.skipUnique ? Flag::f_exclude_unique : Flag()) | (filter.skipSaved ? Flag::f_exclude_saved : Flag()) - | (filter.skipUnsaved ? Flag::f_exclude_unsaved : Flag())), + | (filter.skipUnsaved ? Flag::f_exclude_unsaved : Flag()) + | (collectionId ? Flag::f_collection_id : Flag())), _peer->input, - MTP_string(_reloading ? QString() : _offset), + MTP_int(collectionId), + MTP_string(filterChanged ? QString() : _offset), MTP_int(kPerPage) )).done([=](const MTPpayments_SavedStarGifts &result) { - _loadMoreRequestId = 0; const auto &data = result.data(); - if (const auto enabled = data.vchat_notifications_enabled()) { - _notifyEnabled.fire(mtpIsTrue(*enabled)); - } - if (const auto next = data.vnext_offset()) { - _offset = qs(*next); - } else { - _allLoaded = true; - } - if (!filter.skipsSomething()) { - _totalCount = data.vcount().v; - } const auto owner = &_peer->owner(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); - if (base::take(_reloading)) { - _entries.clear(); - } - _entries.reserve(_entries.size() + data.vgifts().v.size()); - auto hasUnique = false; - for (const auto &gift : data.vgifts().v) { - if (auto parsed = Api::FromTL(_peer, gift)) { - auto descriptor = DescriptorForGift(_peer, *parsed); - _entries.push_back({ - .gift = std::move(*parsed), - .descriptor = std::move(descriptor), - }); - hasUnique = (parsed->info.unique != nullptr); - } - } - refreshButtons(); - refreshAbout(); - - if (hasUnique) { - Ui::PreloadUniqueGiftResellPrices(&_peer->session()); + if (_addingToCollectionId || _collectionsLoaded) { + loaded(result); + } else { + _collectionsLoadedCallback = [=] { + loaded(result); + }; } }).fail([=] { _loadMoreRequestId = 0; - _allLoaded = true; + _collectionsLoadedCallback = nullptr; + _entries->filter = _descriptor.current().filter; + _entries->allLoaded = true; }).send(); } +void InnerWidget::loaded(const MTPpayments_SavedStarGifts &result) { + const auto &data = result.data(); + + _loadMoreRequestId = 0; + _collectionsLoadedCallback = nullptr; + if (const auto enabled = data.vchat_notifications_enabled()) { + _notifyEnabled.fire(mtpIsTrue(*enabled)); + } + if (const auto next = data.vnext_offset()) { + _offset = qs(*next); + } else { + _entries->allLoaded = true; + } + const auto descriptor = _descriptor.current(); + const auto filter = descriptor.filter; + if (!filter.skipsSomething()) { + _entries->total = data.vcount().v; + } + if (_entries->filter != filter) { + _entries->filter = filter; + _list->clear(); + } + _list->reserve(_list->size() + data.vgifts().v.size()); + + const auto i = ranges::find( + _collections, + descriptor.collectionId, + &Data::GiftCollection::id); + const auto collection = (i != end(_collections)) ? &*i : nullptr; + + auto hasUnique = false; + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(_peer, gift)) { + if (collection && !collection->icon) { + collection->icon = parsed->info.document; + refreshCollectionsTabs(); + } + markInCollection(*parsed); + auto descriptor = DescriptorForGift(_peer, *parsed); + _list->push_back({ + .gift = std::move(*parsed), + .descriptor = std::move(descriptor), + }); + hasUnique = (parsed->info.unique != nullptr); + } + } + if (_entries->allLoaded) { + _entries->total = _entries->list.size(); + } + refreshButtons(); + refreshAbout(); + + if (hasUnique) { + Ui::PreloadUniqueGiftResellPrices(&_peer->session()); + } +} + +void InnerWidget::markInCollection(const Data::SavedStarGift &gift) { + if (const auto collectionId = _addingToCollectionId) { + const auto id = gift.manageId; + if (ranges::contains(gift.collectionIds, collectionId)) { + const auto &changes = _collectionChanges.current(); + if (!ranges::contains(changes.removed, id)) { + _inCollection.emplace(id); + } + } + } +} + void InnerWidget::refreshButtons() { _viewsForWidth = 0; _viewsFromRow = 0; @@ -417,7 +676,9 @@ void InnerWidget::validateButtons() { return; } const auto padding = st::giftBoxPadding; - const auto vskip = padding.bottom(); + const auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden()) + ? (padding.top() + _collectionsTabs->height() + padding.top()) + : padding.bottom(); const auto row = _single.height() + st::giftBoxGiftSkip.y(); const auto fromRow = std::max(_visibleFrom - vskip, 0) / row; const auto tillRow = (_visibleTill - vskip + row - 1) / row; @@ -436,7 +697,6 @@ void InnerWidget::validateButtons() { const auto fullw = _perRow * (_single.width() + skipw) - skipw; const auto left = padding.left() + (available - fullw) / 2; const auto oneh = _single.height() + st::giftBoxGiftSkip.y(); - const auto mode = GiftButton::Mode::Minimal; auto x = left; auto y = vskip + fromRow * oneh; auto views = std::vector(); @@ -445,9 +705,9 @@ void InnerWidget::validateButtons() { for (auto j = row; j != tillRow; ++j) { for (auto i = column; i != _perRow; ++i) { const auto index = j * _perRow + i; - if (index >= _entries.size()) { + if (index >= _list->size()) { return false; - } else if (_entries[index].gift.info.id == giftId) { + } else if ((*_list)[index].gift.info.id == giftId) { return true; } } @@ -457,12 +717,14 @@ void InnerWidget::validateButtons() { }; const auto add = [&](int column, int row) { const auto index = row * _perRow + column; - if (index >= _entries.size()) { + if (index >= _list->size()) { return false; } - const auto giftId = _entries[index].gift.info.id; - const auto manageId = _entries[index].gift.manageId; - const auto &descriptor = _entries[index].descriptor; + const auto &entry = (*_list)[index]; + const auto &gift = entry.gift; + const auto giftId = gift.info.id; + const auto manageId = gift.manageId; + const auto &descriptor = entry.descriptor; const auto already = ranges::find(_views, giftId, &View::giftId); if (already != end(_views)) { views.push_back(base::take(*already)); @@ -490,7 +752,10 @@ void InnerWidget::validateButtons() { view.index = index; view.manageId = manageId; view.giftId = giftId; - view.button->setDescriptor(descriptor, mode); + view.button->toggleSelected( + _addingToCollectionId && _inCollection.contains(manageId), + anim::type::instant); + view.button->setDescriptor(descriptor, _mode); view.button->setClickedCallback(callback); return true; }; @@ -517,7 +782,7 @@ auto InnerWidget::pinnedSavedGifts() std::shared_ptr unique; }; auto entries = std::vector(); - for (const auto &entry : _entries) { + for (const auto &entry : *_list) { if (entry.gift.pinned) { Assert(entry.gift.info.unique != nullptr); entries.push_back({ @@ -545,8 +810,73 @@ auto InnerWidget::pinnedSavedGifts() }; } +void InnerWidget::showMenuForCollection(int id) { + if (_menu || _addingToCollectionId) { + return; + } + _menu = base::make_unique_q(this, st::popupMenuWithIcons); + const auto addAction = Ui::Menu::CreateAddActionCallback(_menu); + addAction(tr::lng_gift_collection_add_button(tr::now), [=] { + editCollectionGifts(id); + }, &st::menuIconGiftPremium); + if (const auto username = _peer->username(); !username.isEmpty()) { + addAction(tr::lng_stories_album_share(tr::now), [=] { + shareCollectionLink(username, id); + }, &st::menuIconShare); + } + addAction(tr::lng_gift_collection_edit(tr::now), [=] { + editCollectionName(id); + }, &st::menuIconEdit); + addAction({ + .text = tr::lng_gift_collection_delete(tr::now), + .handler = [=] { confirmDeleteCollection(id); }, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + _menu->popup(QCursor::pos()); +} + +void InnerWidget::shareCollectionLink(const QString &username, int id) { + const auto url = _window->session().createInternalLinkFull( + username + u"/c/"_q + QString::number(id)); + FastShareLink(_window, url); +} + +void InnerWidget::editCollectionName(int id) { + const auto done = [=](QString name) { + collectionRenamed(id, name); + }; + const auto i = ranges::find(_collections, id, &Data::GiftCollection::id); + if (i == end(_collections)) { + return; + } + _window->uiShow()->show(Box( + EditCollectionNameBox, + _window, + peer(), + id, + i->title, + done)); +} + +void InnerWidget::confirmDeleteCollection(int id) { + const auto done = [=](Fn close) { + _window->session().api().request( + MTPpayments_DeleteStarGiftCollection(_peer->input, MTP_int(id)) + ).send(); + collectionRemoved(id); + close(); + }; + _window->uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_gift_collection_delete_sure(), + .confirmed = crl::guard(this, done), + .confirmText = tr::lng_gift_collection_delete_button(), + .confirmStyle = &st::attentionBoxButton, + })); +} + void InnerWidget::showMenuFor(not_null button, QPoint point) { - if (_menu) { + if (_menu || _addingToCollectionId) { return; } const auto index = [&] { @@ -563,11 +893,11 @@ void InnerWidget::showMenuFor(not_null button, QPoint point) { auto entry = ::Settings::SavedStarGiftEntry( _peer, - _entries[index].gift); + (*_list)[index].gift); entry.pinnedSavedGifts = pinnedSavedGifts(); _menu = base::make_unique_q(this, st::popupMenuWithIcons); ::Settings::FillSavedStarGiftMenu( - _controller->uiShow(), + _window->uiShow(), _menu.get(), entry, ::Settings::SavedStarGiftMenuType::List); @@ -578,55 +908,122 @@ void InnerWidget::showMenuFor(not_null button, QPoint point) { } void InnerWidget::showGift(int index) { - Expects(index >= 0 && index < _entries.size()); + Expects(index >= 0 && index < _list->size()); - _window->show(Box( - ::Settings::SavedStarGiftBox, + if (const auto id = _addingToCollectionId) { + auto &gift = (*_list)[index].gift; + auto changes = _collectionChanges.current(); + const auto selected = _inCollection.contains(gift.manageId); + if (selected) { + _inCollection.remove(gift.manageId); + if (ranges::contains(gift.collectionIds, id)) { + changes.removed.push_back(gift.manageId); + } else { + changes.added.erase( + ranges::remove(changes.added, gift.manageId), + end(changes.added)); + } + } else { + _inCollection.emplace(gift.manageId); + if (ranges::contains(gift.collectionIds, id)) { + changes.removed.erase( + ranges::remove(changes.removed, gift.manageId), + end(changes.removed)); + } else { + changes.added.push_back(gift.manageId); + } + } + _collectionChanges = std::move(changes); + + const auto view = ranges::find(_views, index, &View::index); + if (view != end(_views)) { + view->button->toggleSelected(!selected); + } + return; + } + ::Settings::ShowSavedStarGiftBox( _window, _peer, - _entries[index].gift, - pinnedSavedGifts())); + (*_list)[index].gift, + pinnedSavedGifts()); } void InnerWidget::refreshAbout() { - const auto filter = _filter.current(); - const auto filteredEmpty = _allLoaded - && _entries.empty() - && filter.skipsSomething(); - + const auto descriptor = _descriptor.current(); + const auto filter = descriptor.filter; + const auto collectionId = descriptor.collectionId; + const auto maybeEmpty = _list->empty(); + const auto knownEmpty = maybeEmpty + && (_entries->allLoaded || !_entries->total); + const auto filteredEmpty = knownEmpty && filter.skipsSomething(); + const auto collectionCanAdd = knownEmpty + && descriptor.collectionId != 0 + && _peer->canManageGifts(); + _collectionEmpty = !filteredEmpty && collectionCanAdd; if (filteredEmpty) { auto text = tr::lng_peer_gifts_empty_search( tr::now, Ui::Text::RichLangValue); - if (_totalCount > 0) { - text.append("\n\n").append(Ui::Text::Link( - tr::lng_peer_gifts_view_all(tr::now))); - } - _about = std::make_unique( + text.append("\n\n").append(Ui::Text::Link( + tr::lng_peer_gifts_view_all(tr::now))); + auto about = std::make_unique( this, rpl::single(text), st::giftListAbout); - _about->setClickHandlerFilter([=](const auto &...) { - _resetFilterRequests.fire({}); + about->setClickHandlerFilter([=](const auto &...) { + auto now = _descriptor.current(); + now.filter = Filter(); + _descriptorChanges.fire(std::move(now)); return false; }); - _about->show(); - _aboutFiltered = true; + about->show(); + _about = std::move(about); resizeToWidth(width()); - } else if (_aboutFiltered) { - _about = nullptr; - _aboutFiltered = false; - } + } else if (collectionCanAdd) { + auto about = std::make_unique(this); + about->add( + object_ptr( + about.get(), + tr::lng_gift_collection_empty_title(), + st::collectionEmptyTitle), + st::collectionEmptyTitleMargin, + style::al_top); + about->add( + object_ptr( + about.get(), + tr::lng_gift_collection_empty_text(), + st::collectionEmptyText), + st::collectionEmptyTextMargin, + style::al_top); - if (!_peer->isSelf() && _peer->canManageGifts() && !_entries.empty()) { - if (_about) { - _about = nullptr; - resizeToWidth(width()); - } - } else if (!_about) { + const auto button = about->add( + object_ptr( + about.get(), + rpl::single(QString()), + st::collectionEmptyButton), + st::collectionEmptyAddMargin, + style::al_top); + button->setText(tr::lng_gift_collection_add_button( + ) | rpl::map([](const QString &text) { + return Ui::Text::IconEmoji(&st::collectionAddIcon).append(text); + })); + button->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + button->setClickedCallback([=] { + editCollectionGifts(collectionId); + }); + + about->show(); + _about = std::move(about); + resizeToWidth(width()); + } else if ((!collectionId && _peer->isSelf()) + || (!collectionId && !_peer->canManageGifts()) + || maybeEmpty) { _about = std::make_unique( this, - (_peer->isSelf() + ((maybeEmpty && !knownEmpty) + ? tr::lng_contacts_loading(Ui::Text::WithEntities) + : _peer->isSelf() ? tr::lng_peer_gifts_about_mine(Ui::Text::RichLangValue) : tr::lng_peer_gifts_about( lt_user, @@ -635,12 +1032,267 @@ void InnerWidget::refreshAbout() { st::giftListAbout); _about->show(); resizeToWidth(width()); + } else if (_about) { + _about = nullptr; + resizeToWidth(width()); + } +} + +void InnerWidget::reloadCollection(int id) { + _perCollection[id].filter = std::optional(); + _perCollection[id].allLoaded = false; + + auto now = _descriptor.current(); + now.filter = Filter(); + now.collectionId = id; + _descriptorChanges.fire(std::move(now)); + + _api.request(base::take(_loadMoreRequestId)).cancel(); + _collectionsLoadedCallback = nullptr; + refreshButtons(); + refreshAbout(); + loadMore(); +} + +void InnerWidget::editCollectionGifts(int id) { + const auto weak = base::make_weak(this); + _window->uiShow()->show(Box([=](not_null box) { + box->setTitle(tr::lng_gift_collection_add_title()); + box->setWidth(st::boxWideWidth); + box->setStyle(st::collectionEditBox); + + struct State { + rpl::variable descriptor; + rpl::variable changes; + base::unique_qptr menu; + bool saving = false; + }; + const auto state = box->lifetime().make_state(State{ + .changes = Data::GiftsUpdate{ + .peer = _peer, + .collectionId = id, + }, + }); + const auto content = box->addRow( + object_ptr( + box, + _window, + _peer, + state->descriptor.value(), + id, + (_all.filter == Filter()) ? _all : Entries()), + style::margins()); + state->changes = content->changes(); + + content->descriptorChanges( + ) | rpl::start_with_next([=](Descriptor now) { + state->descriptor = now; + }, content->lifetime()); + + content->scrollToTop() | rpl::start_with_next([=] { + box->scrollToY(0); + }, content->lifetime()); + + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + box->addTopButton(st::collectionEditMenuToggle, [=] { + state->menu = base::make_unique_q( + box, + st::popupMenuWithIcons); + content->fillMenu( + Ui::Menu::CreateAddActionCallback(state->menu)); + state->menu->popup(QCursor::pos()); + }); + const auto weakBox = base::make_weak(box); + auto text = state->changes.value( + ) | rpl::map([=](const Data::GiftsUpdate &update) { + return (!update.added.empty() && update.removed.empty()) + ? tr::lng_gift_collection_add_button() + : tr::lng_settings_save(); + }) | rpl::flatten_latest(); + box->addButton(std::move(text), [=] { + if (state->saving) { + return; + } + auto add = QVector(); + auto remove = QVector(); + const auto &changes = state->changes.current(); + for (const auto &id : changes.added) { + add.push_back(Api::InputSavedStarGiftId(id)); + } + for (const auto &id : changes.removed) { + remove.push_back(Api::InputSavedStarGiftId(id)); + } + if (add.empty() && remove.empty()) { + box->closeBox(); + return; + } + state->saving = true; + const auto session = &_window->session(); + using Flag = MTPpayments_UpdateStarGiftCollection::Flag; + session->api().request( + MTPpayments_UpdateStarGiftCollection( + MTP_flags(Flag() + | (add.isEmpty() ? Flag() : Flag::f_add_stargift) + | (remove.isEmpty() + ? Flag() + : Flag::f_delete_stargift)), + _peer->input, + MTP_int(id), + MTPstring(), + MTP_vector(remove), + MTP_vector(add), + MTPVector()) + ).done([=] { + if (const auto strong = weakBox.get()) { + state->saving = false; + strong->closeBox(); + } + session->data().notifyGiftsUpdate(base::duplicate(changes)); + if (const auto strong = weak.get()) { + strong->reloadCollection(id); + } + }).fail([=](const MTP::Error &error) { + if (const auto strong = weakBox.get()) { + state->saving = false; + strong->uiShow()->showToast(error.type()); + } + }).send(); + }); + })); +} + +void InnerWidget::refreshCollectionsTabs() { + if (_collections.empty() || _addingToCollectionId) { + if (base::take(_collectionsTabs)) { + resizeToWidth(width()); + } + return; + } + + auto tabs = std::vector(); + tabs.push_back({ + .id = u"all"_q, + .text = tr::lng_gift_collection_all(tr::now, Ui::Text::WithEntities), + }); + for (const auto &collection : _collections) { + auto &per = _perCollection[collection.id]; + if (!per.allLoaded) { + per.total = collection.count; + } + + auto title = TextWithEntities(); + if (collection.icon) { + title.append( + Data::SingleCustomEmoji(collection.icon) + ).append(' '); + } + title.append(collection.title); + tabs.push_back({ + .id = QString::number(collection.id), + .text = std::move(title), + }); + } + if (_peer->canManageGifts()) { + tabs.push_back({ + .id = u"add"_q, + .text = { '+' + tr::lng_gift_collection_add(tr::now) }, + }); + } + const auto context = Core::TextContext({ + .session = &_window->session(), + }); + if (!_collectionsTabs) { + const auto selectedId = _descriptor.current().collectionId; + const auto selected = (selectedId > 0 + && ranges::contains( + _collections, + selectedId, + &Data::GiftCollection::id)) + ? QString::number(selectedId) + : u"all"_q; + _collectionsTabs = std::make_unique( + this, + Ui::SubTabs::Options{ .selected = selected, .centered = true }, + std::move(tabs), + context); + _collectionsTabs->show(); + + _collectionsTabs->activated( + ) | rpl::start_with_next([=](const QString &id) { + if (id == u"add"_q) { + const auto added = [=](MTPStarGiftCollection result) { + collectionAdded(result); + }; + _window->uiShow()->show(Box( + NewCollectionBox, + _window, + peer(), + Data::SavedStarGiftId(), + added)); + } else { + _collectionsTabs->setActiveTab(id); + + auto now = _descriptor.current(); + now.collectionId = (id == u"all"_q) ? 0 : id.toInt(); + _descriptorChanges.fire(std::move(now)); + } + }, _collectionsTabs->lifetime()); + + _collectionsTabs->contextMenuRequests( + ) | rpl::start_with_next([=](const QString &id) { + if (id == u"add"_q + || id == u"all"_q + || !_peer->canManageGifts()) { + return; + } + showMenuForCollection(id.toInt()); + }, _collectionsTabs->lifetime()); + } else { + _collectionsTabs->setTabs(std::move(tabs), context); + } + resizeToWidth(width()); +} + +void InnerWidget::collectionRenamed(int id, QString name) { + const auto i = ranges::find(_collections, id, &Data::GiftCollection::id); + if (i != end(_collections)) { + i->title = name; + refreshCollectionsTabs(); + } +} + +void InnerWidget::collectionRemoved(int id) { + auto now = _descriptor.current(); + if (now.collectionId == id) { + now.collectionId = 0; + _descriptorChanges.fire(std::move(now)); + } + Assert(_entries != &_perCollection[id]); + _perCollection.erase(id); + const auto removeFrom = [&](Entries &entries) { + for (auto &entry : entries.list) { + entry.gift.collectionIds.erase( + ranges::remove(entry.gift.collectionIds, id), + end(entry.gift.collectionIds)); + } + }; + removeFrom(_all); + for (auto &[_, entries] : _perCollection) { + removeFrom(entries); + } + + const auto i = ranges::find(_collections, id, &Data::GiftCollection::id); + if (i != end(_collections)) { + _collections.erase(i); + refreshCollectionsTabs(); } } int InnerWidget::resizeGetHeight(int width) { - const auto count = int(_entries.size()); const auto padding = st::giftBoxPadding; + const auto count = int(_list->size()); const auto available = width - padding.left() - padding.right(); const auto skipw = st::giftBoxGiftSkip.x(); _perRow = std::min( @@ -649,6 +1301,16 @@ int InnerWidget::resizeGetHeight(int width) { if (!_perRow) { return 0; } + auto result = 0; + if (_collectionsTabs && !_collectionsTabs->isHidden()) { + result += padding.top(); + _collectionsTabs->resizeToWidth(width); + _collectionsTabs->move(0, result); + result += _collectionsTabs->height(); + } else { + result += padding.bottom(); + } + const auto singlew = std::min( ((available + skipw) / _perRow) - skipw, 2 * _singleMin.width()); @@ -657,10 +1319,17 @@ int InnerWidget::resizeGetHeight(int width) { _single = QSize(singlew, singleh); const auto rows = (count + _perRow - 1) / _perRow; + const auto rowsPerCount = rows + ? rows + : ((std::min(_entries->total, kPerPage) + _perRow - 1) / _perRow); const auto skiph = st::giftBoxGiftSkip.y(); - auto result = rows - ? (padding.bottom() * 2 + rows * (singleh + skiph) - skiph) + const auto resultPerCount = result + + (rowsPerCount + ? (padding.bottom() + rowsPerCount * (singleh + skiph) - skiph) + : 0); + result += rows + ? (padding.bottom() + rows * (singleh + skiph) - skiph) : 0; if (const auto about = _about.get()) { @@ -670,7 +1339,7 @@ int InnerWidget::resizeGetHeight(int width) { result += margin.top() + about->height() + margin.bottom(); } - return result; + return std::max(result, resultPerCount); } void InnerWidget::saveState(not_null memento) { @@ -684,8 +1353,135 @@ void InnerWidget::restoreState(not_null memento) { } } -Memento::Memento(not_null peer) -: ContentMemento(peer, nullptr, nullptr, PeerId()) { +void InnerWidget::fillMenu(const Ui::Menu::MenuCallback &addAction) { + const auto canManage = _peer->canManageGifts(); + const auto descriptor = _descriptor.current(); + const auto filter = descriptor.filter; + const auto change = [=](Fn update) { + auto now = _descriptor.current(); + update(now.filter); + _descriptorChanges.fire(std::move(now)); + }; + + const auto collectionId = descriptor.collectionId; + if (!collectionId) { + if (filter.sortByValue) { + addAction(tr::lng_peer_gifts_filter_by_date(tr::now), [=] { + change([](Filter &filter) { filter.sortByValue = false; }); + }, &st::menuIconSchedule); + } else { + addAction(tr::lng_peer_gifts_filter_by_value(tr::now), [=] { + change([](Filter &filter) { filter.sortByValue = true; }); + }, &st::menuIconEarn); + } + if (canManage && !_addingToCollectionId) { + const auto peer = _peer; + const auto weak = base::make_weak(_window); + addAction(tr::lng_gift_collection_add(tr::now), [=] { + if (const auto strong = weak.get()) { + const auto added = [=](MTPStarGiftCollection result) { + collectionAdded(result); + }; + strong->uiShow()->show(Box( + NewCollectionBox, + strong, + peer, + Data::SavedStarGiftId(), + crl::guard(this, added))); + } + }, &st::menuIconAddToFolder); + } + } else if (canManage) { + addAction(tr::lng_gift_collection_add_button(tr::now), [=] { + editCollectionGifts(collectionId); + }, &st::menuIconGiftPremium); + + addAction({ + .text = tr::lng_gift_collection_delete(tr::now), + .handler = [=] { confirmDeleteCollection(collectionId); }, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + } + + if (canManage || !collectionId) { + addAction({ .isSeparator = true }); + } + + addAction(tr::lng_peer_gifts_filter_unlimited(tr::now), [=] { + change([](Filter &filter) { + filter.skipUnlimited = !filter.skipUnlimited; + if (filter.skipUnlimited + && filter.skipLimited + && filter.skipUnique) { + filter.skipLimited = false; + } + }); + }, filter.skipUnlimited ? nullptr : &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_limited(tr::now), [=] { + change([](Filter &filter) { + filter.skipLimited = !filter.skipLimited; + if (filter.skipUpgradable + && filter.skipUnlimited + && filter.skipLimited + && filter.skipUnique) { + filter.skipUnlimited = false; + } + }); + }, filter.skipLimited ? nullptr : &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_upgradable(tr::now), [=] { + change([](Filter &filter) { + filter.skipUpgradable = !filter.skipUpgradable; + if (filter.skipUpgradable + && filter.skipUnlimited + && filter.skipLimited + && filter.skipUnique) { + filter.skipUnlimited = false; + } + }); + }, filter.skipUpgradable ? nullptr: &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_unique(tr::now), [=] { + change([](Filter &filter) { + filter.skipUnique = !filter.skipUnique; + if (filter.skipUpgradable + && filter.skipUnlimited + && filter.skipLimited + && filter.skipUnique) { + filter.skipUnlimited = false; + } + }); + }, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck); + + if (canManage) { + addAction({ .isSeparator = true }); + + addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { + change([](Filter &filter) { + filter.skipSaved = !filter.skipSaved; + if (filter.skipSaved && filter.skipUnsaved) { + filter.skipUnsaved = false; + } + }); + }, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck); + addAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] { + change([](Filter &filter) { + filter.skipUnsaved = !filter.skipUnsaved; + if (filter.skipSaved && filter.skipUnsaved) { + filter.skipSaved = false; + } + }); + }, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck); + } +} + +Memento::Memento(not_null controller) +: ContentMemento(Tag{ + controller->giftsPeer(), + controller->giftsCollectionId() }) { +} + +Memento::Memento(not_null peer, int collectionId) +: ContentMemento(Tag{ peer, collectionId }) { } Section Memento::section() const { @@ -696,7 +1492,7 @@ object_ptr Memento::createWidget( QWidget *parent, not_null controller, const QRect &geometry) { - auto result = object_ptr(parent, controller, peer()); + auto result = object_ptr(parent, controller); result->setInternalState(geometry, this); return result; } @@ -711,24 +1507,121 @@ std::unique_ptr Memento::listState() { Memento::~Memento() = default; -Widget::Widget( - QWidget *parent, - not_null controller, - not_null peer) -: ContentWidget(parent, controller) { +Widget::Widget(QWidget *parent, not_null controller) +: ContentWidget(parent, controller) +, _descriptor(Descriptor{ + .collectionId = controller->giftsCollectionId(), +}) { _inner = setInnerWidget( - object_ptr(this, controller, peer, _filter.value())); + object_ptr( + this, + controller->parentController(), + controller->giftsPeer(), + _descriptor.value())); + _emptyCollectionShown = _inner->collectionEmptyValue(); _inner->notifyEnabled( ) | rpl::take(1) | rpl::start_with_next([=](bool enabled) { - setupNotifyCheckbox(enabled); + _notifyEnabled = enabled; + refreshBottom(); }, _inner->lifetime()); - _inner->resetFilterRequests( - ) | rpl::start_with_next([=] { - _filter = Filter(); + _inner->descriptorChanges( + ) | rpl::start_with_next([=](Descriptor descriptor) { + _descriptor = descriptor; }, _inner->lifetime()); _inner->scrollToTop() | rpl::start_with_next([=] { scrollTo({ 0, 0 }); }, _inner->lifetime()); + + rpl::combine( + _descriptor.value(), + _emptyCollectionShown.value() + ) | rpl::start_with_next([=] { + refreshBottom(); + }, _inner->lifetime()); +} + +void Widget::refreshBottom() { + const auto notify = _notifyEnabled.has_value(); + const auto descriptor = _descriptor.current(); + const auto shownId = descriptor.collectionId; + const auto withButton = shownId + && peer()->canManageGifts() + && !_emptyCollectionShown.current(); + const auto wasBottom = _pinnedToBottom ? _pinnedToBottom->height() : 0; + delete _pinnedToBottom.data(); + if (!notify && !withButton) { + setScrollBottomSkip(0); + _hasPinnedToBottom = false; + } else if (withButton) { + setupBottomButton(wasBottom); + } else { + setupNotifyCheckbox(wasBottom, *_notifyEnabled); + } +} + +void Widget::setupBottomButton(int wasBottomHeight) { + _pinnedToBottom = Ui::CreateChild>( + this, + object_ptr(this)); + const auto wrap = _pinnedToBottom.data(); + wrap->toggle(false, anim::type::instant); + + const auto bottom = wrap->entity(); + bottom->show(); + + const auto button = Ui::CreateChild( + bottom, + rpl::single(QString()), + st::collectionEditBox.button); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->setText(tr::lng_gift_collection_add_button( + ) | rpl::map([](const QString &text) { + return Ui::Text::IconEmoji(&st::collectionAddIcon).append(text); + })); + button->show(); + _hasPinnedToBottom = true; + + button->setClickedCallback([=] { + if (const auto id = _descriptor.current().collectionId) { + _inner->editCollectionGifts(id); + } else { + refreshBottom(); + } + }); + + const auto buttonTop = st::boxRadius; + bottom->widthValue() | rpl::start_with_next([=](int width) { + const auto normal = width - 2 * buttonTop; + button->resizeToWidth(normal); + const auto buttonLeft = (width - normal) / 2; + button->moveToLeft(buttonLeft, buttonTop); + }, button->lifetime()); + + button->heightValue() | rpl::start_with_next([=](int height) { + bottom->resize(bottom->width(), st::boxRadius + height); + }, button->lifetime()); + + const auto processHeight = [=] { + setScrollBottomSkip(wrap->height()); + wrap->moveToLeft(wrap->x(), height() - wrap->height()); + }; + + _inner->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + wrap->resizeToWidth(s.width()); + crl::on_main(wrap, processHeight); + }, wrap->lifetime()); + + rpl::combine( + wrap->heightValue(), + heightValue() + ) | rpl::start_with_next(processHeight, wrap->lifetime()); + + if (_shown) { + wrap->toggle( + true, + wasBottomHeight ? anim::type::instant : anim::type::normal); + } } void Widget::showFinished() { @@ -738,7 +1631,7 @@ void Widget::showFinished() { } } -void Widget::setupNotifyCheckbox(bool enabled) { +void Widget::setupNotifyCheckbox(int wasBottomHeight, bool enabled) { _pinnedToBottom = Ui::CreateChild>( this, object_ptr(this)); @@ -799,82 +1692,15 @@ void Widget::setupNotifyCheckbox(bool enabled) { ) | rpl::start_with_next(processHeight, wrap->lifetime()); if (_shown) { - wrap->toggle(true, anim::type::normal); + wrap->toggle( + true, + wasBottomHeight ? anim::type::instant : anim::type::normal); } _hasPinnedToBottom = true; } void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { - const auto filter = _filter.current(); - const auto change = [=](Fn update) { - auto now = _filter.current(); - update(now); - _filter = now; - }; - - if (filter.sortByValue) { - addAction(tr::lng_peer_gifts_filter_by_date(tr::now), [=] { - change([](Filter &filter) { filter.sortByValue = false; }); - }, &st::menuIconSchedule); - } else { - addAction(tr::lng_peer_gifts_filter_by_value(tr::now), [=] { - change([](Filter &filter) { filter.sortByValue = true; }); - }, &st::menuIconEarn); - } - - addAction({ .isSeparator = true }); - - addAction(tr::lng_peer_gifts_filter_unlimited(tr::now), [=] { - change([](Filter &filter) { - filter.skipUnlimited = !filter.skipUnlimited; - if (filter.skipUnlimited - && filter.skipLimited - && filter.skipUnique) { - filter.skipLimited = false; - } - }); - }, filter.skipUnlimited ? nullptr : &st::mediaPlayerMenuCheck); - addAction(tr::lng_peer_gifts_filter_limited(tr::now), [=] { - change([](Filter &filter) { - filter.skipLimited = !filter.skipLimited; - if (filter.skipUnlimited - && filter.skipLimited - && filter.skipUnique) { - filter.skipUnlimited = false; - } - }); - }, filter.skipLimited ? nullptr : &st::mediaPlayerMenuCheck); - addAction(tr::lng_peer_gifts_filter_unique(tr::now), [=] { - change([](Filter &filter) { - filter.skipUnique = !filter.skipUnique; - if (filter.skipUnlimited - && filter.skipLimited - && filter.skipUnique) { - filter.skipUnlimited = false; - } - }); - }, filter.skipUnique ? nullptr : &st::mediaPlayerMenuCheck); - - if (_inner->peer()->canManageGifts()) { - addAction({ .isSeparator = true }); - - addAction(tr::lng_peer_gifts_filter_saved(tr::now), [=] { - change([](Filter &filter) { - filter.skipSaved = !filter.skipSaved; - if (filter.skipSaved && filter.skipUnsaved) { - filter.skipUnsaved = false; - } - }); - }, filter.skipSaved ? nullptr : &st::mediaPlayerMenuCheck); - addAction(tr::lng_peer_gifts_filter_unsaved(tr::now), [=] { - change([](Filter &filter) { - filter.skipUnsaved = !filter.skipUnsaved; - if (filter.skipSaved && filter.skipUnsaved) { - filter.skipSaved = false; - } - }); - }, filter.skipUnsaved ? nullptr : &st::mediaPlayerMenuCheck); - } + _inner->fillMenu(addAction); } rpl::producer Widget::title() { @@ -911,7 +1737,7 @@ void Widget::setInternalState( } std::shared_ptr Widget::doCreateMemento() { - auto result = std::make_shared(peer()); + auto result = std::make_shared(controller()); saveState(result.get()); return result; } @@ -926,4 +1752,11 @@ void Widget::restoreState(not_null memento) { scrollTopRestore(memento->scrollTop()); } +std::shared_ptr Make(not_null peer, int albumId) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer, albumId))); +} + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h index 2ef1fb67cd..754069ebb3 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h @@ -30,6 +30,7 @@ struct Filter { bool sortByValue : 1 = false; bool skipUnlimited : 1 = false; bool skipLimited : 1 = false; + bool skipUpgradable : 1 = false; bool skipUnique : 1 = false; bool skipSaved : 1 = false; bool skipUnsaved : 1 = false; @@ -39,17 +40,29 @@ struct Filter { || skipUnlimited || skipSaved || skipUnsaved + || skipUpgradable || skipUnique; } friend inline bool operator==(Filter, Filter) = default; }; +struct Descriptor { + Filter filter; + int collectionId = 0; + + friend inline bool operator==( + const Descriptor &, + const Descriptor &) = default; +}; + class InnerWidget; class Memento final : public ContentMemento { public: - explicit Memento(not_null peer); + Memento(not_null controller); + Memento(not_null peer, int collectionId); + ~Memento(); object_ptr createWidget( QWidget *parent, @@ -61,8 +74,6 @@ public: void setListState(std::unique_ptr state); std::unique_ptr listState(); - ~Memento(); - private: std::unique_ptr _listState; @@ -70,10 +81,7 @@ private: class Widget final : public ContentWidget { public: - Widget( - QWidget *parent, - not_null controller, - not_null peer); + Widget(QWidget *parent, not_null controller); [[nodiscard]] not_null peer() const; @@ -98,14 +106,22 @@ private: std::shared_ptr doCreateMemento() override; - void setupNotifyCheckbox(bool enabled); + void setupNotifyCheckbox(int wasBottomHeight, bool enabled); + void setupBottomButton(int wasBottomHeight); + void refreshBottom(); InnerWidget *_inner = nullptr; QPointer> _pinnedToBottom; rpl::variable _hasPinnedToBottom; - rpl::variable _filter; + rpl::variable _emptyCollectionShown; + rpl::variable _descriptor; + std::optional _notifyEnabled; bool _shown = false; }; +[[nodiscard]] std::shared_ptr Make( + not_null peer, + int collectionId = 0); + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/profile/info_levels.style b/Telegram/SourceFiles/info/profile/info_levels.style new file mode 100644 index 0000000000..861b31e7fd --- /dev/null +++ b/Telegram/SourceFiles/info/profile/info_levels.style @@ -0,0 +1,81 @@ +/* +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 +*/ +using "ui/basic.style"; + +LevelShape { + icon: icon; + position: point; +} +levelStyle: TextStyle(defaultTextStyle) { + font: font(9px semibold); +} +levelTextFg: windowFgActive; +levelMargin: margins(4px, 3px, 2px, 2px); + +levelBase: LevelShape { + position: point(0px, 6px); +} +level1: LevelShape(levelBase) { + position: point(-1px, 6px); + icon: icon {{ "levels/level1_inner-26x26", windowBgActive }}; +} +level2: LevelShape(level1) { + icon: icon {{ "levels/level2_inner-26x26", windowBgActive }}; +} +level3: LevelShape(level1) { + icon: icon {{ "levels/level3_inner-26x26", windowBgActive }}; +} +level4: LevelShape(level1) { + icon: icon {{ "levels/level4_inner-26x26", windowBgActive }}; +} +level5: LevelShape(level1) { + icon: icon {{ "levels/level5_inner-26x26", windowBgActive }}; +} +level6: LevelShape(level1) { + icon: icon {{ "levels/level6_inner-26x26", windowBgActive }}; +} +level7: LevelShape(level1) { + icon: icon {{ "levels/level7_inner-26x26", windowBgActive }}; +} +level8: LevelShape(level1) { + icon: icon {{ "levels/level8_inner-26x26", windowBgActive }}; +} +level9: LevelShape(level1) { + icon: icon {{ "levels/level9_inner-26x26", windowBgActive }}; +} +level10: LevelShape(levelBase) { + icon: icon {{ "levels/level10_inner-26x26", windowBgActive }}; +} +level20: LevelShape(levelBase) { + icon: icon {{ "levels/level20_inner-26x26", windowBgActive }}; +} +level30: LevelShape(levelBase) { + icon: icon {{ "levels/level30_inner-26x26", windowBgActive }}; +} +level40: LevelShape(levelBase) { + icon: icon {{ "levels/level40_inner-26x26", windowBgActive }}; +} +level50: LevelShape(levelBase) { + icon: icon {{ "levels/level50_inner-26x26", windowBgActive }}; +} +level60: LevelShape(levelBase) { + icon: icon {{ "levels/level60_inner-26x26", windowBgActive }}; +} +level70: LevelShape(levelBase) { + icon: icon {{ "levels/level70_inner-26x26", windowBgActive }}; +} +level80: LevelShape(levelBase) { + icon: icon {{ "levels/level80_inner-26x26", windowBgActive }}; +} +level90: LevelShape(levelBase) { + icon: icon {{ "levels/level90_inner-26x26", windowBgActive }}; +} +levelNegative: LevelShape(levelBase) { + icon: icon {{ "levels/level_warning-18x18", attentionButtonFg, point(6px, 5px) }}; +} +levelNegativeBubble: icon {{ "levels/level_warning-28x28", windowFgActive }}; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 4fd2822aee..75aadc8bad 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -863,6 +863,7 @@ rpl::producer AddCurrencyAction( not_null controller) { struct State final { rpl::variable balance; + Ui::Text::CustomEmojiHelper helper; }; const auto state = wrap->lifetime().make_state(); const auto parentController = controller->parentController(); @@ -893,13 +894,13 @@ rpl::producer AddCurrencyAction( state->balance = balance; } { - const auto weak = Ui::MakeWeak(wrap); + const auto weak = base::make_weak(wrap); const auto currencyLoadLifetime = std::make_shared(); const auto currencyLoad = currencyLoadLifetime->make_state(user); const auto done = [=](CreditsAmount balance) { - if ([[maybe_unused]] const auto strong = weak.data()) { + if ([[maybe_unused]] const auto strong = weak.get()) { state->balance = balance; currencyLoadLifetime->destroy(); } @@ -914,13 +915,11 @@ rpl::producer AddCurrencyAction( 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)); + const auto icon = state->helper.paletteDependent({ .factory = [=] { + return Ui::Earn::IconCurrencyColored( + st.rightLabel.style.font, + st.rightLabel.textFg->c); + }, .margin = st::channelEarnCurrencyCommonMargins }); name->show(); rpl::combine( button->widthValue(), @@ -939,10 +938,7 @@ rpl::producer AddCurrencyAction( .append(QChar(' ')) .append(Info::ChannelEarn::MajorPart(balance)) .append(Info::ChannelEarn::MinorPart(balance)), - Core::TextContext({ - .session = &user->session(), - .repaint = [=] { name->update(); }, - })); + state->helper.context()); name->resizeToNaturalWidth(available); name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); }, name->lifetime()); @@ -995,7 +991,10 @@ rpl::producer AddCreditsAction( 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(); + + auto helper = Ui::Text::CustomEmojiHelper(); + const auto icon = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + const auto context = helper.context([=] { name->update(); }); name->show(); rpl::combine( button->widthValue(), @@ -1013,10 +1012,7 @@ rpl::producer AddCreditsAction( base::duplicate(icon) .append(QChar(' ')) .append(Lang::FormatCreditsAmountDecimal(balance)), - Core::TextContext({ - .session = &user->session(), - .repaint = [=] { name->update(); }, - })); + context); name->resizeToNaturalWidth(available); name->moveToRight(st::settingsButtonRightSkip, st.padding.top()); }, name->lifetime()); @@ -1507,8 +1503,11 @@ object_ptr DetailsFiller::setupInfo() { if (!user->isBot()) { tracker.track(result->add( - CreateBirthday(result, controller, user))); - tracker.track(result->add(CreateWorkingHours(result, user))); + CreateBirthday(result, controller, user), + {}, + style::al_justify)); + tracker.track(result->add( + CreateWorkingHours(result, user), {}, style::al_justify)); auto locationText = user->session().changes().peerFlagsValue( user, @@ -2105,7 +2104,8 @@ void DetailsFiller::setupMainApp() { _wrap, tr::lng_profile_open_app(), st::infoOpenApp), - st::infoOpenAppMargin); + st::infoOpenAppMargin, + style::al_justify); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); const auto user = _peer->asUser(); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index d9ae910661..5b4dad2aee 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -18,29 +18,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_changes.h" +#include "data/data_saved_music.h" #include "data/data_session.h" #include "data/data_forum_topic.h" #include "data/stickers/data_custom_emoji.h" -#include "info/profile/info_profile_values.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" +#include "info/profile/info_profile_music_button.h" +#include "info/profile/info_profile_values.h" +#include "info/saved/info_saved_music_widget.h" #include "info/info_controller.h" +#include "info/info_memento.h" #include "boxes/peers/edit_forum_topic_box.h" #include "boxes/report_messages_box.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "ui/boxes/show_or_premium_box.h" +#include "ui/controls/stars_rating.h" #include "ui/controls/userpic_button.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/text/text_utilities.h" +#include "ui/basic_click_handlers.h" #include "ui/ui_utility.h" #include "ui/painter.h" #include "base/event_filter.h" #include "base/unixtime.h" #include "window/window_controller.h" #include "window/window_session_controller.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_premium.h" #include "chat_helpers/stickers_lottie.h" @@ -64,15 +71,18 @@ constexpr auto kGiftBadgeGlares = 3; constexpr auto kGlareDurationStep = crl::time(320); constexpr auto kGlareTimeout = crl::time(1000); -auto MembersStatusText(int count) { +[[nodiscard]] auto MembersStatusText(int count) { return tr::lng_chat_status_members(tr::now, lt_count_decimal, count); }; -auto OnlineStatusText(int count) { +[[nodiscard]] auto OnlineStatusText(int count) { return tr::lng_chat_status_online(tr::now, lt_count_decimal, count); }; -auto ChatStatusText(int fullCount, int onlineCount, bool isGroup) { +[[nodiscard]] auto ChatStatusText( + int fullCount, + int onlineCount, + bool isGroup) { if (onlineCount > 1 && onlineCount <= fullCount) { return tr::lng_chat_status_members_online( tr::now, @@ -119,6 +129,22 @@ auto ChatStatusText(int fullCount, int onlineCount, bool isGroup) { return { left, left, right, right }; } +[[nodiscard]] MusicButtonData DocumentMusicButtonData( + not_null document) { + if (const auto song = document->song()) { + if (!song->performer.isEmpty() || !song->title.isEmpty()) { + return { + .performer = song->performer, + .title = song->title, + }; + } + } + const auto name = document->filename(); + return { + .title = !name.isEmpty() ? name : tr::lng_all_music(tr::now), + }; +} + } // namespace class Cover::BadgeTooltip final : public Ui::RpWidget { @@ -595,7 +621,7 @@ Cover::Cover( , _botVerify( std::make_unique( this, - st::infoPeerBadge, + st::infoBotVerifyBadge, &peer->session(), BotVerifyBadgeForPeer(peer), nullptr, @@ -660,6 +686,16 @@ Cover::Cover( ? object_ptr(this, controller, topic) : nullptr) , _name(this, _st.name) +, _starsRating(_peer->isUser() + ? std::make_unique( + this, + _controller->uiShow(), + _peer->isSelf() ? QString() : _peer->shortName(), + Data::StarsRatingValue(_peer), + (_peer->isSelf() + ? [=] { return _peer->owner().pendingStarsRating(); } + : Fn())) + : nullptr) , _status(this, _st.status) , _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { @@ -673,6 +709,13 @@ Cover::Cover( if (!_peer->isMegagroup()) { _status->setAttribute(Qt::WA_TransparentForMouseEvents); + if (const auto rating = _starsRating.get()) { + _statusShift = rating->widthValue(); + _statusShift.changes() | rpl::start_with_next([=] { + refreshStatusGeometry(width()); + }, _status->lifetime()); + rating->raise(); + } } setupShowLastSeen(); @@ -703,6 +746,7 @@ Cover::Cover( initViewers(std::move(title)); setupChildGeometry(); setupUniqueBadgeTooltip(); + setupSavedMusic(); if (_userpic) { } else if (topic->canEdit()) { @@ -805,6 +849,43 @@ void Cover::setupChildGeometry() { }, lifetime()); } +void Cover::setupSavedMusic() { + if (!Data::SavedMusic::Supported(_peer->id)) { + return; + } + Data::SavedMusicList( + _peer, + nullptr, + 1 + ) | rpl::map([=](const Data::SavedMusicSlice &data) { + return data.size() ? data[0].get() : nullptr; + }) | rpl::start_with_next([=](HistoryItem *item) { + const auto media = item ? item->media() : nullptr; + const auto document = media ? media->document() : nullptr; + if (!document) { + _musicButton = nullptr; + resize(width(), _st.height); + } else if (!_musicButton) { + using namespace Info::Saved; + _musicButton = std::make_unique( + this, + DocumentMusicButtonData(document), + [=] { _controller->showSection(MakeMusic(_peer)); }); + _musicButton->show(); + + widthValue( + ) | rpl::start_with_next([=](int newWidth) { + _musicButton->resizeToWidth(newWidth); + const auto skip = st::infoMusicButtonBottom; + _musicButton->moveToLeft(0, _st.height - skip, newWidth); + resize(width(), _st.height + _musicButton->height()); + }, _musicButton->lifetime()); + } else { + _musicButton->updateData(DocumentMusicButtonData(document)); + } + }, lifetime()); +} + Cover *Cover::setOnlineCount(rpl::producer &&count) { _onlineCount = std::move(count); return this; @@ -1093,10 +1174,14 @@ void Cover::refreshNameGeometry(int newWidth) { } void Cover::refreshStatusGeometry(int newWidth) { - auto statusWidth = newWidth - _st.statusLeft - _st.rightSkip; - _status->resizeToWidth(statusWidth); - _status->moveToLeft(_st.statusLeft, _st.statusTop, newWidth); - const auto left = _st.statusLeft + _status->textMaxWidth(); + if (const auto rating = _starsRating.get()) { + rating->moveTo(_st.starsRatingLeft, _st.starsRatingTop); + } + const auto statusLeft = _st.statusLeft + _statusShift.current(); + auto statusWidth = newWidth - statusLeft - _st.rightSkip; + _status->resizeToNaturalWidth(statusWidth); + _status->moveToLeft(statusLeft, _st.statusTop, newWidth); + const auto left = statusLeft + _status->textMaxWidth(); _showLastSeen->moveToLeft( left + _st.showLastSeenPosition.x(), _st.showLastSeenPosition.y(), diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 5efa7adc97..5b14719500 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -22,6 +22,7 @@ class FlatLabel; template class SlideWrap; class RoundButton; +class StarsRating; } // namespace Ui namespace HistoryView { @@ -44,6 +45,7 @@ struct InfoProfileCover; namespace Info::Profile { class EmojiStatusPanel; +class MusicButton; class Badge; class TopicIconView final { @@ -139,6 +141,7 @@ private: void setupShowLastSeen(); void setupChildGeometry(); + void setupSavedMusic(); void initViewers(rpl::producer title); void refreshStatusText(); void refreshNameGeometry(int newWidth); @@ -172,11 +175,15 @@ private: std::optional _personalChosen; object_ptr _iconButton; object_ptr _name = { nullptr }; + std::unique_ptr _starsRating; object_ptr _status = { nullptr }; + rpl::variable _statusShift = 0; object_ptr _showLastSeen = { nullptr }; //object_ptr _dropArea = { nullptr }; base::Timer _refreshStatusTimer; + std::unique_ptr _musicButton; + rpl::event_stream
_showSection; }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index ecf24b2796..de39c8ed1b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -269,7 +269,7 @@ void EmojiStatusPanel::create(const Descriptor &descriptor) { _panel->hideAnimated(); }, _panel->lifetime()); } else { - const auto weak = Ui::MakeWeak(_panel.get()); + const auto weak = base::make_weak(_panel.get()); const auto accept = [=](Chosen chosen) { Expects(chosen.until != Selector::kPickCustomTimeId); diff --git a/Telegram/SourceFiles/info/profile/info_profile_icon.cpp b/Telegram/SourceFiles/info/profile/info_profile_icon.cpp index 9f8dee9109..174b144cf8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_icon.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_icon.cpp @@ -25,14 +25,10 @@ FloatingIcon::FloatingIcon( : RpWidget(parent) , _icon(&icon) , _point(position) { - resize( - _point.x() + _icon->width(), - _point.y() + _icon->height()); + setGeometry(QRect( + QPoint(0, 0), + QSize(_point.x() + _icon->width(), _point.y() + _icon->height()))); setAttribute(Qt::WA_TransparentForMouseEvents); - parent->widthValue( - ) | rpl::start_with_next( - [this] { moveToLeft(0, 0); }, - lifetime()); } void FloatingIcon::paintEvent(QPaintEvent *e) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 00ee314ae7..e989e8ad6b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_file_origin.h" #include "data/data_user.h" +#include "data/data_saved_music.h" #include "data/data_saved_sublist.h" #include "main/main_session.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp b/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp new file mode 100644 index 0000000000..3a9433f0d0 --- /dev/null +++ b/Telegram/SourceFiles/info/profile/info_profile_music_button.cpp @@ -0,0 +1,98 @@ +/* +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/profile/info_profile_music_button.h" + +#include "ui/widgets/labels.h" +#include "ui/painter.h" +#include "styles/style_info.h" + +namespace Info::Profile { + +MusicButton::MusicButton( + QWidget *parent, + MusicButtonData data, + Fn handler) +: RippleButton(parent, st::infoMusicButtonRipple) +, _performer(std::make_unique( + this, + u"- "_q + data.performer, + st::infoMusicButtonPerformer)) +, _title(std::make_unique( + this, + data.title, + st::infoMusicButtonTitle)) { + rpl::combine( + _title->naturalWidthValue(), + _performer->naturalWidthValue() + ) | rpl::start_with_next([=] { + resizeToWidth(widthNoMargins()); + }, lifetime()); + + _title->setAttribute(Qt::WA_TransparentForMouseEvents); + _performer->setAttribute(Qt::WA_TransparentForMouseEvents); + + setClickedCallback(std::move(handler)); +} + +MusicButton::~MusicButton() = default; + +void MusicButton::updateData(MusicButtonData data) { + _performer->setText(u"- "_q + data.performer); + _title->setText(data.title); + resizeToWidth(widthNoMargins()); +} + +void MusicButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + p.fillRect(e->rect(), st::windowBgOver); + paintRipple(p, QPoint()); + + auto pen = st::windowBoldFg->p; + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(st::infoMusicButtonLine); + p.setPen(pen); + + const auto line = st::infoMusicButtonLine; + const auto length = height() / 4.; + const auto half = height() / 2.; + const auto left = st::infoProfileCover.photoLeft + (line / 2.); + + auto hq = PainterHighQualityEnabler(p); + p.drawLine( + left, half - length / 2., + left, half + length / 2.); + p.drawLine( + left + 2.5 * line, half - length, + left + 2.5 * line, half + length); + p.drawLine( + left + 5 * line, half - length * 3 / 4., + left + 5 * line, half + length * 3 / 4.); +} + +int MusicButton::resizeGetHeight(int newWidth) { + const auto padding = st::infoMusicButtonPadding; + const auto &font = st::infoMusicButtonTitle.style.font; + + const auto top = padding.top(); + const auto skip = st::normalFont->spacew; + const auto available = newWidth - padding.left() - padding.right(); + _title->resizeToNaturalWidth(available); + _title->moveToLeft(padding.left(), top); + if (const auto left = available - _title->width() - skip; left > 0) { + _performer->show(); + _performer->resizeToNaturalWidth(left); + _performer->moveToLeft(padding.left() + _title->width() + skip, top); + } else { + _performer->hide(); + } + + return padding.top() + font->height + padding.bottom(); +} + +} // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_music_button.h b/Telegram/SourceFiles/info/profile/info_profile_music_button.h new file mode 100644 index 0000000000..2e909478d5 --- /dev/null +++ b/Telegram/SourceFiles/info/profile/info_profile_music_button.h @@ -0,0 +1,39 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/widgets/buttons.h" + +namespace Ui { +class FlatLabel; +} // namespace Ui + +namespace Info::Profile { + +struct MusicButtonData { + QString performer; + QString title; +}; + +class MusicButton final : public Ui::RippleButton { +public: + MusicButton(QWidget *parent, MusicButtonData data, Fn handler); + ~MusicButton(); + + void updateData(MusicButtonData data); + +private: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + std::unique_ptr _performer; + std::unique_ptr _title; + +}; + +} // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_common.h b/Telegram/SourceFiles/info/saved/info_saved_music_common.h new file mode 100644 index 0000000000..f40ffae8bc --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_common.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Info::Saved { + +struct MusicTag { + explicit MusicTag(not_null peer) + : peer(peer) { + } + + not_null peer; +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp new file mode 100644 index 0000000000..605a58e3a7 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.cpp @@ -0,0 +1,401 @@ +/* +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/saved/info_saved_music_provider.h" + +#include "base/unixtime.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_document.h" +#include "data/data_media_types.h" +#include "data/data_saved_music.h" +#include "data/data_session.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "history/history.h" +#include "info/media/info_media_widget.h" +#include "info/media/info_media_list_section.h" +#include "info/info_controller.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "layout/layout_selection.h" +#include "storage/storage_shared_media.h" +#include "styles/style_info.h" +#include "styles/style_overview.h" + +namespace Info::Saved { +namespace { + +using namespace Media; + +constexpr auto kPreloadedScreensCount = 4; +constexpr auto kPreloadedScreensCountFull + = kPreloadedScreensCount + 1 + kPreloadedScreensCount; + +[[nodiscard]] int MinStoryHeight(int width) { + auto itemsLeft = st::infoMediaSkip; + auto itemsInRow = (width - itemsLeft) + / (st::infoMediaMinGridSize + st::infoMediaSkip); + return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow; +} + +} // namespace + +MusicProvider::MusicProvider(not_null controller) +: _controller(controller) +, _peer(controller->key().musicPeer()) +, _history(_peer->owner().history(_peer)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &layout : _layouts) { + layout.second.item->invalidateCache(); + } + }, _lifetime); +} + +MusicProvider::~MusicProvider() { + clear(); +} + +Type MusicProvider::type() { + return Type::MusicFile; +} + +bool MusicProvider::hasSelectRestriction() { + //if (_peer->session().frozen()) { + // return true; + //} + return true; +} + +rpl::producer MusicProvider::hasSelectRestrictionChanges() { + return rpl::never(); +} + +bool MusicProvider::sectionHasFloatingHeader() { + return false; +} + +QString MusicProvider::sectionTitle(not_null item) { + return QString(); +} + +bool MusicProvider::sectionItemBelongsHere( + not_null item, + not_null previous) { + return true; +} + +bool MusicProvider::isPossiblyMyItem(not_null item) { + return true; +} + +std::optional MusicProvider::fullCount() { + return _slice.fullCount(); +} + +void MusicProvider::clear() { + _layouts.clear(); + _aroundId = nullptr; + _idsLimit = kMinimalIdsLimit; + _slice = Data::SavedMusicSlice(); +} + +void MusicProvider::restart() { + clear(); + refreshViewer(); +} + +void MusicProvider::checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) { + const auto visibleWidth = viewport.width(); + const auto visibleHeight = viewport.height(); + const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight; + const auto minItemHeight = MinStoryHeight(visibleWidth); + const auto preloadedCount = preloadedHeight / minItemHeight; + const auto preloadIdsLimitMin = (preloadedCount / 2) + 1; + const auto preloadIdsLimit = preloadIdsLimitMin + + (visibleHeight / minItemHeight); + const auto after = _slice.skippedAfter(); + const auto topLoaded = after && (*after == 0); + const auto before = _slice.skippedBefore(); + const auto bottomLoaded = before && (*before == 0); + + const auto minScreenDelta = kPreloadedScreensCount + - kPreloadIfLessThanScreens; + const auto minIdDelta = (minScreenDelta * visibleHeight) + / minItemHeight; + const auto preloadAroundItem = [&](not_null layout) { + auto preloadRequired = false; + const auto item = layout->getItem(); + if (!preloadRequired) { + preloadRequired = (_idsLimit < preloadIdsLimitMin); + } + if (!preloadRequired) { + auto delta = _slice.distance(_aroundId, item); + Assert(delta != std::nullopt); + preloadRequired = (qAbs(*delta) >= minIdDelta); + } + if (preloadRequired) { + _idsLimit = preloadIdsLimit; + _aroundId = item; + refreshViewer(); + } + }; + + if (preloadTop && !topLoaded) { + preloadAroundItem(topLayout); + } else if (preloadBottom && !bottomLoaded) { + preloadAroundItem(bottomLayout); + } +} + +void MusicProvider::setSearchQuery(QString query) { +} + +void MusicProvider::refreshViewer() { + _viewerLifetime.destroy(); + const auto aroundId = _aroundId; + auto ids = Data::SavedMusicList(_peer, aroundId, _idsLimit); + std::move( + ids + ) | rpl::start_with_next([=](Data::SavedMusicSlice &&slice) { + if (!slice.fullCount()) { + // Don't display anything while full count is unknown. + return; + } + _slice = std::move(slice); + + auto nearestId = (HistoryItem*)nullptr; + for (auto i = 0; i != _slice.size(); ++i) { + if (_slice[i] == aroundId) { + nearestId = aroundId; + break; + } + } + if (!nearestId && _slice.size() > 0) { + _aroundId = _slice[_slice.size() / 2]; + } + _refreshed.fire({}); + }, _viewerLifetime); +} + +rpl::producer<> MusicProvider::refreshed() { + return _refreshed.events(); +} + +std::vector MusicProvider::fillSections( + not_null delegate) { + markLayoutsStale(); + const auto guard = gsl::finally([&] { clearStaleLayouts(); }); + + auto result = std::vector(); + auto section = ListSection(Type::MusicFile, sectionDelegate()); + auto count = _slice.size(); + for (auto i = 0; i != count; ++i) { + const auto item = _slice[i]; + if (const auto layout = getLayout(item, delegate)) { + if (!section.addItem(layout)) { + section.finishSection(); + result.push_back(std::move(section)); + section = ListSection(Type::MusicFile, sectionDelegate()); + section.addItem(layout); + } + } + } + if (!section.empty()) { + section.finishSection(); + result.push_back(std::move(section)); + } + return result; +} + +void MusicProvider::markLayoutsStale() { + for (auto &layout : _layouts) { + layout.second.stale = true; + } +} + +void MusicProvider::clearStaleLayouts() { + for (auto i = _layouts.begin(); i != _layouts.end();) { + if (i->second.stale) { + _layoutRemoved.fire(i->second.item.get()); + i = _layouts.erase(i); + } else { + ++i; + } + } +} + +rpl::producer> MusicProvider::layoutRemoved() { + return _layoutRemoved.events(); +} + +BaseLayout *MusicProvider::lookupLayout(const HistoryItem *item) { + return nullptr; +} + +bool MusicProvider::isMyItem(not_null item) { + return item->isSavedMusicItem() && (item->history()->peer == _peer); +} + +bool MusicProvider::isAfter( + not_null a, + not_null b) { + return (a->id < b->id); +} + +BaseLayout *MusicProvider::getLayout( + not_null item, + not_null delegate) { + auto it = _layouts.find(item); + if (it == _layouts.end()) { + if (auto layout = createLayout(item, delegate)) { + layout->initDimensions(); + it = _layouts.emplace(item, std::move(layout)).first; + } else { + return nullptr; + } + } + it->second.stale = false; + return it->second.item.get(); +} + +std::unique_ptr MusicProvider::createLayout( + not_null item, + not_null delegate) { + using namespace Overview::Layout; + if (const auto media = item->media()) { + if (const auto file = media->document()) { + return std::make_unique( + delegate, + item, + DocumentFields{ file }, + st::overviewFileLayout); + } + } + return nullptr; +} + +ListItemSelectionData MusicProvider::computeSelectionData( + not_null item, + TextSelection selection) { + auto result = ListItemSelectionData(selection); + result.canDelete = item->history()->peer->isSelf(); + result.canForward = true;// item->allowsForward(); + return result; +} + +void MusicProvider::applyDragSelection( + ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) { + const auto fromId = fromItem->id - (skipFrom ? 1 : 0); + const auto tillId = tillItem->id - (skipTill ? 0 : 1); + for (auto i = selected.begin(); i != selected.end();) { + const auto itemId = i->first->id; + if (itemId > fromId || itemId <= tillId) { + i = selected.erase(i); + } else { + ++i; + } + } + for (auto &layoutItem : _layouts) { + const auto item = layoutItem.first; + if (item->id <= fromId && item->id > tillId) { + ChangeItemSelection( + selected, + item, + computeSelectionData(item, FullSelection)); + } + } +} + +bool MusicProvider::allowSaveFileAs( + not_null item, + not_null document) { + return true; +} + +QString MusicProvider::showInFolderPath( + not_null item, + not_null document) { + return document->filepath(true); +} + +int64 MusicProvider::scrollTopStatePosition(not_null item) { + return item->id.bare; +} + +HistoryItem *MusicProvider::scrollTopStateItem(ListScrollTopState state) { + if (state.item && _slice.indexOf(state.item)) { + return state.item; + //} else if (const auto id = _slice.nearest(state.position)) { + // const auto full = FullMsgId(_peer->id, StoryIdToMsgId(*id)); + // if (const auto item = _controller->session().data().message(full)) { + // return item; + // } + } + + auto nearestId = (HistoryItem*)nullptr; //AssertIsDebug(); + //for (auto i = 0; i != _slice.size(); ++i) { + // if (!nearestId + // || std::abs(*nearestId - state.position) + // > std::abs(_slice[i] - state.position)) { + // nearestId = _slice[i]; + // } + //} + if (nearestId) { + const auto full = nearestId->fullId(); + if (const auto item = _controller->session().data().message(full)) { + return item; + } + } + + return state.item; +} + +void MusicProvider::saveState( + not_null memento, + ListScrollTopState scrollState) { + if (_aroundId != nullptr && scrollState.item) { + //AssertIsDebug(); + //memento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) }); + //memento->setIdsLimit(_idsLimit); + //memento->setScrollTopItem(scrollState.item->globalId()); + //memento->setScrollTopItemPosition(scrollState.position); + //memento->setScrollTopShift(scrollState.shift); + } +} + +void MusicProvider::restoreState( + not_null memento, + Fn restoreScrollState) { + if (const auto limit = memento->idsLimit()) { + const auto wasAroundId = memento->aroundId(); + if (wasAroundId.peer == _peer->id) { + _idsLimit = limit; + //AssertIsDebug(); + //_aroundId = StoryIdFromMsgId(wasAroundId.msg); + restoreScrollState({ + .position = memento->scrollTopItemPosition(), + .item = MessageByGlobalId(memento->scrollTopItem()), + .shift = memento->scrollTopShift(), + }); + refreshViewer(); + } + } +} + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_provider.h b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h new file mode 100644 index 0000000000..77e4e33d0f --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_provider.h @@ -0,0 +1,129 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/weak_ptr.h" +#include "data/data_saved_music.h" +#include "info/media/info_media_common.h" +#include "info/saved/info_saved_music_common.h" +#include "history/history_item.h" + +class DocumentData; +class HistoryItem; +class PeerData; +class History; + +namespace Info { +class AbstractController; +} // namespace Info + +namespace Info::Saved { + +class MusicProvider final + : public Media::ListProvider + , private Media::ListSectionDelegate + , public base::has_weak_ptr { +public: + explicit MusicProvider(not_null controller); + ~MusicProvider(); + + Media::Type type() override; + bool hasSelectRestriction() override; + rpl::producer hasSelectRestrictionChanges() override; + bool isPossiblyMyItem(not_null item) override; + + std::optional fullCount() override; + + void restart() override; + void checkPreload( + QSize viewport, + not_null topLayout, + not_null bottomLayout, + bool preloadTop, + bool preloadBottom) override; + void refreshViewer() override; + rpl::producer<> refreshed() override; + + void setSearchQuery(QString query) override; + + std::vector fillSections( + not_null delegate) override; + rpl::producer> layoutRemoved() override; + Media::BaseLayout *lookupLayout(const HistoryItem *item) override; + bool isMyItem(not_null item) override; + bool isAfter( + not_null a, + not_null b) override; + + Media::ListItemSelectionData computeSelectionData( + not_null item, + TextSelection selection) override; + void applyDragSelection( + Media::ListSelectedMap &selected, + not_null fromItem, + bool skipFrom, + not_null tillItem, + bool skipTill) override; + + bool allowSaveFileAs( + not_null item, + not_null document) override; + QString showInFolderPath( + not_null item, + not_null document) override; + + int64 scrollTopStatePosition(not_null item) override; + HistoryItem *scrollTopStateItem( + Media::ListScrollTopState state) override; + void saveState( + not_null memento, + Media::ListScrollTopState scrollState) override; + void restoreState( + not_null memento, + Fn restoreScrollState) override; + +private: + static constexpr auto kMinimalIdsLimit = 16; + + bool sectionHasFloatingHeader() override; + QString sectionTitle(not_null item) override; + bool sectionItemBelongsHere( + not_null item, + not_null previous) override; + + void markLayoutsStale(); + void clearStaleLayouts(); + void clear(); + + [[nodiscard]] Media::BaseLayout *getLayout( + not_null item, + not_null delegate); + [[nodiscard]] std::unique_ptr createLayout( + not_null item, + not_null delegate); + + const not_null _controller; + const not_null _peer; + const not_null _history; + + HistoryItem *_aroundId = nullptr; + int _idsLimit = kMinimalIdsLimit; + Data::SavedMusicSlice _slice; + + std::unordered_map, Media::CachedItem> _layouts; + rpl::event_stream> _layoutRemoved; + rpl::event_stream<> _refreshed; + + bool _started = false; + + rpl::lifetime _lifetime; + rpl::lifetime _viewerLifetime; + +}; + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp new file mode 100644 index 0000000000..208b6ec680 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_widget.cpp @@ -0,0 +1,340 @@ +/* +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/saved/info_saved_music_widget.h" + +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_saved_music.h" +#include "data/data_session.h" +#include "info/media/info_media_list_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "lang/lang_keys.h" +#include "ui/ui_utility.h" +#include "styles/style_credits.h" // giftListAbout +#include "styles/style_info.h" +#include "styles/style_layers.h" + +namespace Info::Saved { + +class MusicInner final : public Ui::RpWidget { +public: + MusicInner(QWidget *parent, not_null controller); + ~MusicInner(); + + bool showInternal(not_null memento); + void setIsStackBottom(bool isStackBottom) { + _isStackBottom = isStackBottom; + } + + void saveState(not_null memento); + void restoreState(not_null memento); + + void setScrollHeightValue(rpl::producer value); + + rpl::producer scrollToRequests() const; + rpl::producer selectedListValue() const; + void selectionAction(SelectionAction action); + +protected: + int resizeGetHeight(int newWidth) override; + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + +private: + int recountHeight(); + void refreshHeight(); + void refreshEmpty(); + + void setupList(); + void setupEmpty(); + + const not_null _controller; + const not_null _peer; + + base::unique_qptr _menu; + + object_ptr _list = { nullptr }; + object_ptr _empty = { nullptr }; + int _lastNonLoadingHeight = 0; + bool _emptyLoading = false; + + bool _inResize = false; + bool _isStackBottom = false; + + rpl::event_stream _scrollToRequests; + rpl::event_stream> _selectedLists; + rpl::event_stream> _listTops; + rpl::variable _topHeight; + rpl::variable _albumEmpty; + +}; + +MusicInner::MusicInner(QWidget *parent, not_null controller) +: RpWidget(parent) +, _controller(controller) +, _peer(controller->key().musicPeer()) { + setupList(); + setupEmpty(); +} + +MusicInner::~MusicInner() = default; + +void MusicInner::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +bool MusicInner::showInternal(not_null memento) { + if (memento->section().type() == Section::Type::SavedMusic) { + restoreState(memento); + return true; + } + return false; +} + +void MusicInner::setupList() { + Expects(!_list); + + _list = object_ptr(this, _controller); + const auto raw = _list.data(); + + using namespace rpl::mappers; + raw->scrollToRequests( + ) | rpl::map([=](int to) { + return Ui::ScrollToRequest { + raw->y() + to, + -1 + }; + }) | rpl::start_to_stream(_scrollToRequests, raw->lifetime()); + _selectedLists.fire(raw->selectedListValue()); + _listTops.fire(raw->topValue()); + + raw->show(); +} + +void MusicInner::setupEmpty() { + _list->resizeToWidth(width()); + + const auto savedMusic = &_controller->session().data().savedMusic(); + rpl::combine( + rpl::single( + rpl::empty + ) | rpl::then( + savedMusic->changed() | rpl::filter( + rpl::mappers::_1 == _peer->id + ) | rpl::to_empty + ), + _list->heightValue() + ) | rpl::start_with_next([=](auto, int listHeight) { + const auto padding = st::infoMediaMargin; + if (const auto raw = _empty.release()) { + raw->hide(); + raw->deleteLater(); + } + _emptyLoading = false; + if (listHeight <= padding.bottom() + padding.top()) { + refreshEmpty(); + } else { + _albumEmpty = false; + } + refreshHeight(); + }, _list->lifetime()); +} + +void MusicInner::refreshEmpty() { + const auto savedMusic = &_controller->session().data().savedMusic(); + const auto knownEmpty = savedMusic->countKnown(_peer->id); + _empty = object_ptr( + this, + (!knownEmpty + ? tr::lng_contacts_loading(Ui::Text::WithEntities) + : rpl::single( + tr::lng_media_song_empty(tr::now, Ui::Text::WithEntities))), + st::giftListAbout); + _empty->show(); + _emptyLoading = !knownEmpty; + resizeToWidth(width()); +} + +void MusicInner::saveState(not_null memento) { + _list->saveState(&memento->media()); +} + +void MusicInner::restoreState(not_null memento) { + _list->restoreState(&memento->media()); +} + +rpl::producer MusicInner::selectedListValue() const { + return _selectedLists.events_starting_with( + _list->selectedListValue() + ) | rpl::flatten_latest(); +} + +void MusicInner::selectionAction(SelectionAction action) { + _list->selectionAction(action); +} + +int MusicInner::resizeGetHeight(int newWidth) { + if (!newWidth) { + return 0; + } + _inResize = true; + auto guard = gsl::finally([this] { _inResize = false; }); + + if (_list) { + _list->resizeToWidth(newWidth); + } + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->resizeToWidth(newWidth - margin.left() - margin.right()); + } + + return recountHeight(); +} + +void MusicInner::refreshHeight() { + if (_inResize) { + return; + } + resize(width(), recountHeight()); +} + +int MusicInner::recountHeight() { + auto top = 0; + auto listHeight = 0; + if (_list) { + _list->moveToLeft(0, top); + listHeight = _list->heightNoMargins(); + top += listHeight; + } + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->moveToLeft(margin.left(), top + margin.top()); + top += margin.top() + empty->height() + margin.bottom(); + } + if (_emptyLoading) { + top = std::max(top, _lastNonLoadingHeight); + } else { + _lastNonLoadingHeight = top; + } + return top; +} + +void MusicInner::setScrollHeightValue(rpl::producer value) { +} + +rpl::producer MusicInner::scrollToRequests() const { + return _scrollToRequests.events(); +} + +MusicMemento::MusicMemento(not_null controller) +: ContentMemento(MusicTag{ controller->musicPeer() }) +, _media(controller) { +} + +MusicMemento::MusicMemento(not_null peer) +: ContentMemento(MusicTag{ peer }) +, _media(peer, 0, Media::Type::MusicFile) { +} + +MusicMemento::~MusicMemento() = default; + +Section MusicMemento::section() const { + return Section(Section::Type::SavedMusic); +} + +object_ptr MusicMemento::createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) { + auto result = object_ptr(parent, controller); + result->setInternalState(geometry, this); + return result; +} + +MusicWidget::MusicWidget( + QWidget *parent, + not_null controller) +: ContentWidget(parent, controller) { + _inner = setInnerWidget(object_ptr(this, controller)); + _inner->setScrollHeightValue(scrollHeightValue()); + _inner->scrollToRequests( + ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { + scrollTo(request); + }, _inner->lifetime()); +} + +void MusicWidget::setIsStackBottom(bool isStackBottom) { + ContentWidget::setIsStackBottom(isStackBottom); + _inner->setIsStackBottom(isStackBottom); +} + +bool MusicWidget::showInternal(not_null memento) { + if (!controller()->validateMementoPeer(memento)) { + return false; + } + return true; +} + +void MusicWidget::setInternalState( + const QRect &geometry, + not_null memento) { + setGeometry(geometry); + Ui::SendPendingMoveResizeEvents(this); + restoreState(memento); +} + +std::shared_ptr MusicWidget::doCreateMemento() { + auto result = std::make_shared(controller()); + saveState(result.get()); + return result; +} + +void MusicWidget::saveState(not_null memento) { + memento->setScrollTop(scrollTopSave()); + _inner->saveState(memento); +} + +void MusicWidget::restoreState(not_null memento) { + _inner->restoreState(memento); + scrollTopRestore(memento->scrollTop()); +} + +rpl::producer MusicWidget::selectedListValue() const { + return _inner->selectedListValue(); +} + +void MusicWidget::selectionAction(SelectionAction action) { + _inner->selectionAction(action); +} + +rpl::producer MusicWidget::title() { + return controller()->key().musicPeer()->isSelf() + ? tr::lng_media_saved_music_your() + : tr::lng_media_saved_music_title(); +} + +std::shared_ptr MakeMusic(not_null peer) { + return std::make_shared( + std::vector>( + 1, + std::make_shared(peer))); +} + +} // namespace Info::Saved + diff --git a/Telegram/SourceFiles/info/saved/info_saved_music_widget.h b/Telegram/SourceFiles/info/saved/info_saved_music_widget.h new file mode 100644 index 0000000000..5ecef46511 --- /dev/null +++ b/Telegram/SourceFiles/info/saved/info_saved_music_widget.h @@ -0,0 +1,81 @@ +/* +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" +#include "info/media/info_media_widget.h" +#include "info/stories/info_stories_common.h" + +namespace Ui { +template +class SlideWrap; +} // namespace Ui + +namespace Info::Saved { + +class MusicInner; + +class MusicMemento final : public ContentMemento { +public: + MusicMemento(not_null controller); + MusicMemento(not_null peer); + ~MusicMemento(); + + object_ptr createWidget( + QWidget *parent, + not_null controller, + const QRect &geometry) override; + + Section section() const override; + + [[nodiscard]] Media::Memento &media() { + return _media; + } + [[nodiscard]] const Media::Memento &media() const { + return _media; + } + +private: + Media::Memento _media; + int _addingToAlbumId = 0; + +}; + +class MusicWidget final : public ContentWidget { +public: + MusicWidget(QWidget *parent, not_null controller); + + void setIsStackBottom(bool isStackBottom) override; + + bool showInternal( + not_null memento) override; + + void setInternalState( + const QRect &geometry, + not_null memento); + + rpl::producer selectedListValue() const override; + void selectionAction(SelectionAction action) override; + + rpl::producer title() override; + +private: + void saveState(not_null memento); + void restoreState(not_null memento); + + std::shared_ptr doCreateMemento() override; + + MusicInner *_inner = nullptr; + bool _shown = false; + +}; + +[[nodiscard]] std::shared_ptr MakeMusic( + not_null peer); + +} // namespace Info::Saved diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index e1aef6b78e..b08a4281f8 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -104,8 +104,8 @@ private: rpl::event_stream<> backButtonEnables; } _flexibleScroll; not_null<::Settings::AbstractSection*> _inner; - QPointer _pinnedToTop; - QPointer _pinnedToBottom; + base::weak_qptr _pinnedToTop; + base::weak_qptr _pinnedToBottom; rpl::event_stream> _removesFromStack; diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index f02f2f07c6..2c44348561 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -590,30 +590,29 @@ void FillLoading( content->add(std::move(icon.widget)); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - (type == LoadingType::Boosts) - ? tr::lng_stats_boosts_loading() - : (type == LoadingType::Earn) - ? tr::lng_stats_earn_loading() - : tr::lng_stats_loading(), - st::changePhoneTitle)), - st::changePhoneTitlePadding + st::boxRowPadding); + (type == LoadingType::Boosts) + ? tr::lng_stats_boosts_loading() + : (type == LoadingType::Earn) + ? tr::lng_stats_earn_loading() + : tr::lng_stats_loading(), + st::changePhoneTitle), + st::changePhoneTitlePadding + st::boxRowPadding, + style::al_top); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - (type == LoadingType::Boosts) - ? tr::lng_stats_boosts_loading_subtext() - : (type == LoadingType::Earn) - ? tr::lng_stats_earn_loading_subtext() - : tr::lng_stats_loading_subtext(), - st::statisticsLoadingSubtext)), - st::changePhoneDescriptionPadding + st::boxRowPadding); + (type == LoadingType::Boosts) + ? tr::lng_stats_boosts_loading_subtext() + : (type == LoadingType::Earn) + ? tr::lng_stats_earn_loading_subtext() + : tr::lng_stats_loading_subtext(), + st::statisticsLoadingSubtext), + st::changePhoneDescriptionPadding + st::boxRowPadding, + style::al_top + )->setTryMakeSimilarLines(true); Ui::AddSkip(content, st::settingsBlockedListIconPadding.top()); } diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index a956b763ea..a486a546e9 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -887,6 +887,8 @@ void CreditsRow::init() { tr::now, lt_count, _entry.paidMessagesCount) + : _entry.postsSearch + ? tr::lng_credits_box_history_entry_posts_search(tr::now) : ((!_entry.subscriptionUntil.isNull() && !isSpecial) || (_entry.giftUpgraded && !isSpecial) || (_entry.giftResale && !isSpecial) @@ -997,14 +999,9 @@ void CreditsRow::init() { } } if (!_paintUserpicCallback) { - _paintUserpicCallback = /*_entry.stargift - ? Ui::GenerateGiftStickerUserpicCallback( - _session, - _entry.bareGiftStickerId, - _context.repaint) - : */!isSpecial - ? PeerListRow::generatePaintUserpicCallback(false) - : Ui::GenerateCreditsPaintUserpicCallback(_entry); + _paintUserpicCallback = (isSpecial || _entry.postsSearch) + ? Ui::GenerateCreditsPaintUserpicCallback(_entry) + : PeerListRow::generatePaintUserpicCallback(false); } } @@ -1215,13 +1212,13 @@ CreditsController::CreditsController(CreditsDescriptor d) if (data.startsWith(u"ton"_q)) { const auto in = data.split(u":"_q)[1].startsWith(u"in"_q); return std::make_unique( - std::make_unique( + std::make_unique( + data.toString(), Ui::Earn::IconCurrencyColored( st::tonFieldIconSize, in ? st::boxTextFgGood->c - : st::menuIconAttentionColor->c), - data.toString()), + : st::menuIconAttentionColor->c)), QPoint(0, st::lineWidth)); } const auto desc = DeserializeCreditsRowDescriptionData( diff --git a/Telegram/SourceFiles/info/stories/info_stories_albums.cpp b/Telegram/SourceFiles/info/stories/info_stories_albums.cpp new file mode 100644 index 0000000000..b8751c8db6 --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_albums.cpp @@ -0,0 +1,151 @@ +/* +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/stories/info_stories_albums.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_story.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/fields/input_field.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_info.h" + +namespace Info::Stories { +namespace { + +constexpr auto kAlbumNameLimit = 12; + +void EditAlbumBox( + not_null box, + not_null navigation, + not_null peer, + int id, + StoryId addId, + QString currentName, + Fn finished) { + box->setTitle(id + ? tr::lng_stories_album_edit() + : tr::lng_stories_album_new_title()); + + if (!id) { + box->addRow( + object_ptr( + box, + tr::lng_stories_album_new_text(), + st::collectionAbout)); + } + const auto title = box->addRow( + object_ptr( + box, + st::collectionNameField, + tr::lng_stories_album_new_ph(), + currentName)); + title->setMaxLength(kAlbumNameLimit * 2); + box->setFocusCallback([=] { + title->setFocusFast(); + }); + + Ui::AddLengthLimitLabel(title, kAlbumNameLimit); + + const auto show = navigation->uiShow(); + const auto session = &peer->session(); + + const auto creating = std::make_shared(false); + const auto submit = [=] { + if (*creating) { + return; + } + const auto text = title->getLastText().trimmed(); + if (text.isEmpty() || text.size() > kAlbumNameLimit) { + title->showError(); + return; + } + + *creating = true; + const auto weak = base::make_weak(box); + const auto done = [=](Data::StoryAlbum result) { + *creating = false; + if (const auto onstack = finished) { + onstack(result); + } + if (const auto strong = weak.get()) { + strong->closeBox(); + } + }; + const auto fail = [=](QString type) { + *creating = false; + if (type == u"ALBUMS_TOO_MANY"_q) { + show->show(Ui::MakeInformBox({ + .text = tr::lng_stories_album_limit_text(), + .confirmText = tr::lng_box_ok(), + .title = tr::lng_stories_album_limit_title(), + })); + if (const auto strong = weak.get()) { + strong->closeBox(); + } + } else { + show->showToast(type); + } + }; + if (id) { + session->data().stories().albumRename( + peer, + id, + text, + done, + fail); + } else { + session->data().stories().albumCreate( + peer, + text, + addId, + done, + fail); + } + }; + title->submits() | rpl::start_with_next(submit, title->lifetime()); + auto text = id + ? tr::lng_settings_save() + : tr::lng_stories_album_new_create(); + box->addButton(std::move(text), submit); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace + +void NewAlbumBox( + not_null box, + not_null navigation, + not_null peer, + StoryId addId, + Fn added) { + EditAlbumBox(box, navigation, peer, 0, addId, QString(), added); +} + +void EditAlbumNameBox( + not_null box, + not_null navigation, + not_null peer, + int id, + QString current, + Fn done) { + EditAlbumBox(box, navigation, peer, id, {}, current, [=]( + Data::StoryAlbum result) { + done(result.title); + }); +} + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_albums.h b/Telegram/SourceFiles/info/stories/info_stories_albums.h new file mode 100644 index 0000000000..d1ef52fc2a --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_albums.h @@ -0,0 +1,39 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +struct StoryAlbum; +} // namespace Data + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window + +namespace Info::Stories { + +void NewAlbumBox( + not_null box, + not_null navigation, + not_null peer, + StoryId addId, + Fn added); + +void EditAlbumNameBox( + not_null box, + not_null navigation, + not_null peer, + int id, + QString current, + Fn done); + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_common.h b/Telegram/SourceFiles/info/stories/info_stories_common.h new file mode 100644 index 0000000000..afa0f13eff --- /dev/null +++ b/Telegram/SourceFiles/info/stories/info_stories_common.h @@ -0,0 +1,29 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Info::Stories { + +[[nodiscard]] int ArchiveId(); + +struct Tag { + explicit Tag( + not_null peer, + int albumId = 0, + int addingToAlbumId = 0) + : peer(peer) + , albumId(albumId) + , addingToAlbumId(addingToAlbumId) { + } + + not_null peer; + int albumId = 0; + int addingToAlbumId = 0; +}; + +} // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index 4848fd3355..8dc057a5ed 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/stories/info_stories_inner_widget.h" +#include "apiwrap.h" +#include "boxes/share_box.h" #include "data/data_peer.h" +#include "data/data_saved_music.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -15,112 +18,266 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_list.h" #include "info/media/info_media_buttons.h" #include "info/media/info_media_list_widget.h" +#include "info/peer_gifts/info_peer_gifts_widget.h" #include "info/profile/info_profile_actions.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_widget.h" +#include "info/stories/info_stories_albums.h" #include "info/stories/info_stories_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/sub_tabs.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" +#include "styles/style_credits.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" namespace Info::Stories { +namespace { -class EmptyWidget : public Ui::RpWidget { +class EditAlbumBox final : public Ui::BoxContent { public: - EmptyWidget(QWidget *parent); - - void setFullHeight(rpl::producer fullHeightValue); - -protected: - int resizeGetHeight(int newWidth) override; - - void paintEvent(QPaintEvent *e) override; + EditAlbumBox( + QWidget*, + not_null controller, + not_null peer, + Fn reload, + int albumId); private: - object_ptr _text; - int _height = 0; + void prepare() override; + + void resizeEvent(QResizeEvent *e) override; + + const not_null _window; + const not_null _content; + rpl::variable _changes; + Fn _reload; + bool _saving = false; }; -EmptyWidget::EmptyWidget(QWidget *parent) -: RpWidget(parent) -, _text(this, st::infoEmptyLabel) { -} - -void EmptyWidget::setFullHeight(rpl::producer fullHeightValue) { - std::move( - fullHeightValue - ) | rpl::start_with_next([this](int fullHeight) { - // Make icon center be on 1/3 height. - auto iconCenter = fullHeight / 3; - auto iconHeight = st::infoEmptyStories.height(); - auto iconTop = iconCenter - iconHeight / 2; - _height = iconTop + st::infoEmptyIconTop; - resizeToWidth(width()); +EditAlbumBox::EditAlbumBox( + QWidget*, + not_null controller, + not_null peer, + Fn reload, + int albumId) +: _window(controller->parentController()) +, _content(Ui::CreateChild( + this, + _window, + Wrap::StoryAlbumEdit, + std::make_shared( + std::vector>( + 1, + std::make_shared( + peer, + Data::kStoriesAlbumIdArchive, + albumId))).get())) +, _changes(Data::StoryAlbumUpdate{ .peer = peer, .albumId = albumId }) +, _reload(std::move(reload)) { + _content->selectedListValue( + ) | rpl::start_with_next([=](const SelectedItems &selection) { + const auto stories = &_window->session().data().stories(); + auto ids = stories->albumKnownInArchive(peer->id, albumId); + auto now = _changes.current(); + now.added.clear(); + now.added.reserve(selection.list.size()); + now.removed.clear(); + now.removed.reserve(ids.size()); + for (const auto &entry : selection.list) { + const auto id = StoryIdFromMsgId(entry.globalId.itemId.msg); + if (!ids.remove(id)) { + now.added.push_back(id); + } + } + for (const auto id : ids) { + now.removed.push_back(id); + } + _changes = std::move(now); }, lifetime()); } -int EmptyWidget::resizeGetHeight(int newWidth) { - auto labelTop = _height - st::infoEmptyLabelTop; - auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip; - _text->resizeToNaturalWidth(labelWidth); +void EditAlbumBox::prepare() { + setTitle(tr::lng_stories_album_add_title()); + setStyle(st::collectionEditBox); - auto labelLeft = (newWidth - _text->width()) / 2; - _text->moveToLeft(labelLeft, labelTop, newWidth); + _content->desiredHeightValue( + ) | rpl::start_with_next([=](int height) { + setDimensions(st::boxWideWidth, height); + }, _content->lifetime()); - update(); - return _height; + addTopButton(st::boxTitleClose, [=] { + closeBox(); + }); + + const auto weakBox = base::make_weak(this); + auto text = _changes.value( + ) | rpl::map([=](const Data::StoryAlbumUpdate &update) { + return (!update.added.empty() && update.removed.empty()) + ? tr::lng_stories_album_add_button() + : tr::lng_settings_save(); + }) | rpl::flatten_latest(); + addButton(std::move(text), [=] { + if (_saving) { + return; + } + auto add = QVector(); + auto remove = QVector(); + const auto &changes = _changes.current(); + for (const auto &id : changes.added) { + add.push_back(MTP_int(id)); + } + for (const auto &id : changes.removed) { + remove.push_back(MTP_int(id)); + } + if (add.empty() && remove.empty()) { + closeBox(); + return; + } + _saving = true; + const auto session = &_window->session(); + const auto reload = _reload; + using Flag = MTPstories_UpdateAlbum::Flag; + session->api().request( + MTPstories_UpdateAlbum( + MTP_flags(Flag() + | (add.isEmpty() ? Flag() : Flag::f_add_stories) + | (remove.isEmpty() + ? Flag() + : Flag::f_delete_stories)), + changes.peer->input, + MTP_int(changes.albumId), + MTPstring(), + MTP_vector(remove), + MTP_vector(add), + MTPVector()) + ).done([=] { + if (const auto strong = weakBox.get()) { + strong->_saving = false; + strong->closeBox(); + } + session->data().stories().notifyAlbumUpdate( + base::duplicate(changes)); + if (const auto onstack = reload) { + onstack(); + } + }).fail([=](const MTP::Error &error) { + if (const auto strong = weakBox.get()) { + strong->_saving = false; + strong->uiShow()->showToast(error.type()); + } + }).send(); + }); } -void EmptyWidget::paintEvent(QPaintEvent *e) { - auto p = QPainter(this); - - const auto iconLeft = (width() - st::infoEmptyStories.width()) / 2; - const auto iconTop = height() - st::infoEmptyIconTop; - st::infoEmptyStories.paint(p, iconLeft, iconTop, width()); +void EditAlbumBox::resizeEvent(QResizeEvent *e) { + _content->setGeometry(rect()); } +} // namespace + InnerWidget::InnerWidget( QWidget *parent, - not_null controller) + not_null controller, + rpl::producer albumId, + int addingToAlbumId) : RpWidget(parent) , _controller(controller) -, _empty(this) { - _empty->heightValue( - ) | rpl::start_with_next( - [this] { refreshHeight(); }, - _empty->lifetime()); - _list = setupList(); +, _peer(controller->key().storiesPeer()) +, _addingToAlbumId(addingToAlbumId) +, _albumId(std::move(albumId)) +, _albumChanges(Data::StoryAlbumUpdate{ + .peer = _peer, + .albumId = _addingToAlbumId, +}) { + preloadArchiveCount(); + + _albumId.value( + ) | rpl::start_with_next([=](int albumId) { + if (_albumsTabs + && (albumId == Data::kStoriesAlbumIdSaved + || ranges::contains( + _albums, + albumId, + &Data::StoryAlbum::id))) { + _albumsTabs->setActiveTab((albumId == Data::kStoriesAlbumIdSaved) + ? u"all"_q + : QString::number(albumId)); + } + _controller->replaceKey(Key(Tag(_peer, albumId, _addingToAlbumId))); + reload(); + }, lifetime()); } +void InnerWidget::preloadArchiveCount() { + constexpr auto kArchive = Data::kStoriesAlbumIdArchive; + const auto stories = &_peer->owner().stories(); + if (!_peer->canEditStories() + || stories->albumIdsCountKnown(_peer->id, kArchive)) { + return; + } + const auto key = Data::StoryAlbumIdsKey{ _peer->id, kArchive }; + stories->albumIdsLoadMore(_peer->id, kArchive); + stories->albumIdsChanged() | rpl::filter( + rpl::mappers::_1 == key + ) | rpl::take_while([=] { + return !stories->albumIdsCountKnown(_peer->id, kArchive); + }) | rpl::start_with_next([=] { + refreshAlbumsTabs(); + }, lifetime()); +} + +void InnerWidget::setupAlbums() { + Ui::AddSkip(_top); + _albumsWrap = _top->add(object_ptr(_top)); + + _peer->owner().stories().albumsListValue( + _peer->id + ) | rpl::start_with_next([=](std::vector &&albums) { + _albums = std::move(albums); + refreshAlbumsTabs(); + }, lifetime()); +} + +InnerWidget::~InnerWidget() = default; + void InnerWidget::setupTop() { - const auto key = _controller->key(); - const auto peer = key.storiesPeer(); - if (peer && key.storiesTab() == Stories::Tab::Saved && _isStackBottom) { - if (peer->isSelf()) { + const auto albumId = _albumId.current(); + if (_addingToAlbumId) { + return; + } else if (albumId == Data::kStoriesAlbumIdArchive) { + createAboutArchive(); + } else if (_isStackBottom) { + if (_peer->isSelf()) { createProfileTop(); - } else if (peer->owner().stories().hasArchive(peer)) { + } else if (_peer->owner().stories().hasArchive(_peer)) { createButtons(); } else { - _top.destroy(); - refreshHeight(); + startTop(); + finalizeTop(); } - } else if (peer && key.storiesTab() == Stories::Tab::Archive) { - createAboutArchive(); } else { - _top.destroy(); - refreshHeight(); + startTop(); + finalizeTop(); } } @@ -131,14 +288,11 @@ void InnerWidget::startTop() { } void InnerWidget::createProfileTop() { - const auto key = _controller->key(); - const auto peer = key.storiesPeer(); - startTop(); using namespace Profile; - AddCover(_top, _controller, peer, nullptr, nullptr); - AddDetails(_top, _controller, peer, nullptr, nullptr, { v::null }); + AddCover(_top, _controller, _peer, nullptr, nullptr); + AddDetails(_top, _controller, _peer, nullptr, nullptr, { v::null }); auto tracker = Ui::MultiSlideTracker(); const auto dividerWrap = _top->add( @@ -169,23 +323,22 @@ void InnerWidget::createButtons() { void InnerWidget::addArchiveButton(Ui::MultiSlideTracker &tracker) { Expects(_top != nullptr); - const auto key = _controller->key(); - const auto peer = key.storiesPeer(); - const auto stories = &peer->owner().stories(); + const auto stories = &_peer->owner().stories(); - if (!stories->archiveCountKnown(peer->id)) { - stories->archiveLoadMore(peer->id); + constexpr auto kArchive = Data::kStoriesAlbumIdArchive; + if (!stories->albumIdsCountKnown(_peer->id, kArchive)) { + stories->albumIdsLoadMore(_peer->id, kArchive); } auto count = rpl::single( rpl::empty ) | rpl::then( - stories->archiveChanged( + stories->albumIdsChanged( ) | rpl::filter( - rpl::mappers::_1 == peer->id + rpl::mappers::_1 == Data::StoryAlbumIdsKey{ _peer->id, kArchive } ) | rpl::to_empty ) | rpl::map([=] { - return stories->archiveCount(peer->id); + return stories->albumIdsCount(_peer->id, kArchive); }) | rpl::start_spawning(_top->lifetime()); const auto archiveWrap = _top->add( @@ -201,9 +354,7 @@ void InnerWidget::addArchiveButton(Ui::MultiSlideTracker &tracker) { const auto archive = archiveWrap->entity(); archive->addClickHandler([=] { - _controller->showSection(Info::Stories::Make( - _controller->key().storiesPeer(), - Stories::Tab::Archive)); + _controller->showSection(Make(_peer, kArchive)); }); auto label = rpl::duplicate( count @@ -227,8 +378,6 @@ void InnerWidget::addArchiveButton(Ui::MultiSlideTracker &tracker) { void InnerWidget::addRecentButton(Ui::MultiSlideTracker &tracker) { Expects(_top != nullptr); - const auto key = _controller->key(); - const auto peer = key.storiesPeer(); const auto recentWrap = _top->add( object_ptr>( _top, @@ -239,7 +388,7 @@ void InnerWidget::addRecentButton(Ui::MultiSlideTracker &tracker) { using namespace Dialogs::Stories; auto last = LastForPeer( - peer + _peer ) | rpl::map([=](Content &&content) { for (auto &element : content.elements) { element.unreadCount = 0; @@ -272,7 +421,7 @@ void InnerWidget::addRecentButton(Ui::MultiSlideTracker &tracker) { }, thumbs->lifetime()); thumbs->setAttribute(Qt::WA_TransparentForMouseEvents); recent->addClickHandler([=] { - _controller->parentController()->openPeerStories(peer->id); + _controller->parentController()->openPeerStories(_peer->id); }); object_ptr( recent, @@ -289,9 +438,7 @@ void InnerWidget::addRecentButton(Ui::MultiSlideTracker &tracker) { void InnerWidget::addGiftsButton(Ui::MultiSlideTracker &tracker) { Expects(_top != nullptr); - const auto key = _controller->key(); - const auto peer = key.storiesPeer(); - const auto user = peer->asUser(); + const auto user = _peer->asUser(); Assert(user != nullptr); auto count = Profile::PeerGiftsCountValue( @@ -311,10 +458,7 @@ void InnerWidget::addGiftsButton(Ui::MultiSlideTracker &tracker) { const auto gifts = giftsWrap->entity(); gifts->addClickHandler([=] { - _controller->showSection( - std::make_shared( - user, - Section::Type::PeerGifts)); + _controller->showSection(PeerGifts::Make(_peer)); }); auto label = rpl::duplicate( count @@ -336,9 +480,11 @@ void InnerWidget::addGiftsButton(Ui::MultiSlideTracker &tracker) { } void InnerWidget::finalizeTop() { - Ui::AddSkip(_top, st::infoProfileSkip); - Ui::AddDivider(_top); - + const auto addPossibleAlbums = !_addingToAlbumId + && (_albumId.current() != Data::kStoriesAlbumIdArchive); + if (addPossibleAlbums) { + setupAlbums(); + } _top->resizeToWidth(width()); _top->heightValue( @@ -350,12 +496,11 @@ void InnerWidget::finalizeTop() { void InnerWidget::createAboutArchive() { startTop(); - const auto peer = _controller->key().storiesPeer(); _top->add(object_ptr( _top, object_ptr( _top, - (peer->isChannel() + (_peer->isChannel() ? tr::lng_stories_channel_archive_about : tr::lng_stories_archive_about)(), st::infoStoriesAboutArchive), @@ -378,27 +523,341 @@ bool InnerWidget::showInternal(not_null memento) { return false; } -object_ptr InnerWidget::setupList() { - auto result = object_ptr( +void InnerWidget::setupList() { + Expects(!_list); + + _list = object_ptr( this, _controller); - result->heightValue( - ) | rpl::start_with_next( - [this] { refreshHeight(); }, - result->lifetime()); + const auto raw = _list.data(); + using namespace rpl::mappers; - result->scrollToRequests( - ) | rpl::map([widget = result.data()](int to) { + raw->scrollToRequests( + ) | rpl::map([=](int to) { return Ui::ScrollToRequest { - widget->y() + to, + raw->y() + to, -1 }; - }) | rpl::start_to_stream( - _scrollToRequests, - result->lifetime()); - _selectedLists.fire(result->selectedListValue()); - _listTops.fire(result->topValue()); - return result; + }) | rpl::start_to_stream(_scrollToRequests, raw->lifetime()); + _selectedLists.fire(raw->selectedListValue()); + _listTops.fire(raw->topValue()); + + raw->show(); +} + +void InnerWidget::setupEmpty() { + _list->resizeToWidth(width()); + + const auto stories = &_controller->session().data().stories(); + const auto key = Data::StoryAlbumIdsKey{ _peer->id, _albumId.current() }; + rpl::combine( + rpl::single( + rpl::empty + ) | rpl::then( + stories->albumIdsChanged() | rpl::filter( + rpl::mappers::_1 == key + ) | rpl::to_empty + ), + _list->heightValue() + ) | rpl::start_with_next([=](auto, int listHeight) { + const auto padding = st::infoMediaMargin; + if (const auto raw = _empty.release()) { + raw->hide(); + raw->deleteLater(); + } + _emptyLoading = false; + if (listHeight <= padding.bottom() + padding.top()) { + refreshEmpty(); + } else { + _albumEmpty = false; + } + refreshHeight(); + }, _list->lifetime()); +} + +void InnerWidget::refreshEmpty() { + const auto albumId = _albumId.current(); + const auto stories = &_controller->session().data().stories(); + const auto knownEmpty = stories->albumIdsCountKnown(_peer->id, albumId); + const auto albumCanAdd = knownEmpty + && albumId + && (albumId != Data::kStoriesAlbumIdArchive) + && _peer->canEditStories(); + _albumEmpty = albumCanAdd; + if (albumCanAdd) { + auto empty = object_ptr(this); + empty->add( + object_ptr( + empty.get(), + tr::lng_stories_album_empty_title(), + st::collectionEmptyTitle), + st::collectionEmptyTitleMargin, + style::al_top); + empty->add( + object_ptr( + empty.get(), + tr::lng_stories_album_empty_text(), + st::collectionEmptyText), + st::collectionEmptyTextMargin, + style::al_top); + + const auto button = empty->add( + object_ptr( + empty.get(), + rpl::single(QString()), + st::collectionEmptyButton), + st::collectionEmptyAddMargin, + style::al_top); + button->setText(tr::lng_stories_album_add_button( + ) | rpl::map([](const QString &text) { + return Ui::Text::IconEmoji(&st::collectionAddIcon).append(text); + })); + button->setTextTransform( + Ui::RoundButton::TextTransform::NoTransform); + button->setClickedCallback([=] { + editAlbumStories(albumId); + }); + empty->show(); + _empty = std::move(empty); + } else { + _empty = object_ptr( + this, + (!knownEmpty + ? tr::lng_contacts_loading(Ui::Text::WithEntities) + : _peer->isSelf() + ? tr::lng_stories_empty(Ui::Text::RichLangValue) + : tr::lng_stories_empty_channel(Ui::Text::RichLangValue)), + st::giftListAbout); + _empty->show(); + } + _emptyLoading = !albumCanAdd && !knownEmpty; + resizeToWidth(width()); +} + +void InnerWidget::refreshAlbumsTabs() { + Expects(!_addingToAlbumId); + Expects(_albumsWrap != nullptr); + + const auto has = _peer->canEditStories() + && _peer->owner().stories().albumIdsCount( + _peer->id, + Data::kStoriesAlbumIdArchive); + if (_albums.empty() && !has) { + if (base::take(_albumsTabs)) { + resizeToWidth(width()); + } + return; + } + auto tabs = std::vector(); + auto selected = QString(); + if (!_albums.empty()) { + tabs.push_back({ + .id = u"all"_q, + .text = tr::lng_stories_album_all( + tr::now, + Ui::Text::WithEntities), + }); + for (const auto &album : _albums) { + auto title = TextWithEntities(); + title.append(album.title); + tabs.push_back({ + .id = QString::number(album.id), + .text = std::move(title), + }); + if (_albumId.current() == album.id) { + selected = tabs.back().id; + } + } + if (selected.isEmpty()) { + selected = tabs.front().id; + } + } + if (has) { + tabs.push_back({ + .id = u"add"_q, + .text = { '+' + tr::lng_stories_album_add(tr::now) }, + }); + } + if (!_albumsTabs) { + _albumsTabs = std::make_unique( + _albumsWrap, + Ui::SubTabs::Options{ + .selected = selected, + .centered = true, + }, + std::move(tabs)); + _albumsTabs->show(); + + const auto padding = st::giftBoxPadding; + _albumsWrap->resize( + _albumsWrap->width(), + padding.top() + _albumsTabs->height() + padding.top()); + _albumsWrap->widthValue() | rpl::start_with_next([=](int width) { + _albumsTabs->resizeToWidth(width); + }, _albumsTabs->lifetime()); + _albumsTabs->move(0, padding.top()); + + _albumsTabs->activated( + ) | rpl::start_with_next([=](const QString &id) { + if (id == u"add"_q) { + const auto added = [=](Data::StoryAlbum album) { + albumAdded(album); + }; + _controller->uiShow()->show(Box( + NewAlbumBox, + _controller, + _peer, + StoryId(), + added)); + } else { + _albumIdChanges.fire((id == u"all"_q) ? 0 : id.toInt()); + } + }, _albumsTabs->lifetime()); + + _albumsTabs->contextMenuRequests( + ) | rpl::start_with_next([=](const QString &id) { + if (id == u"add"_q || id == u"all"_q) { + return; + } + showMenuForAlbum(id.toInt()); + }, _albumsTabs->lifetime()); + } else { + _albumsTabs->setTabs(std::move(tabs)); + if (!selected.isEmpty()) { + _albumsTabs->setActiveTab(selected); + } + } + resizeToWidth(width()); +} + +void InnerWidget::showMenuForAlbum(int id) { + Expects(id > 0); + + if (_menu || _addingToAlbumId) { + return; + } + _menu = base::make_unique_q(this, st::popupMenuWithIcons); + const auto addAction = Ui::Menu::CreateAddActionCallback(_menu); + if (_peer->canEditStories()) { + addAction(tr::lng_stories_album_add_button(tr::now), [=] { + editAlbumStories(id); + }, &st::menuIconStoriesSave); + } + if (const auto username = _peer->username(); !username.isEmpty()) { + addAction(tr::lng_stories_album_share(tr::now), [=] { + shareAlbumLink(username, id); + }, &st::menuIconShare); + } + if (_peer->canEditStories()) { + addAction(tr::lng_stories_album_edit(tr::now), [=] { + editAlbumName(id); + }, &st::menuIconEdit); + addAction({ + .text = tr::lng_stories_album_delete(tr::now), + .handler = [=] { confirmDeleteAlbum(id); }, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + } + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(QCursor::pos()); + } +} + +rpl::producer InnerWidget::albumIdChanges() const { + return _albumIdChanges.events(); +} + +rpl::producer InnerWidget::changes() const { + return _albumChanges.value(); +} + +void InnerWidget::reload() { + auto old = std::exchange(_list, object_ptr(nullptr)); + setupList(); + setupEmpty(); + old.destroy(); + + resizeToWidth(width()); +} + +void InnerWidget::editAlbumStories(int id) { + const auto weak = base::make_weak(this); + auto box = Box(_controller, _peer, crl::guard(this, [=] { + if (_albumId.current() == id) { + reload(); + } + }), id); + + _controller->uiShow()->show(std::move(box)); +} + +void InnerWidget::shareAlbumLink(const QString &username, int id) { + const auto url = _controller->session().createInternalLinkFull( + username + u"/a/"_q + QString::number(id)); + FastShareLink(_controller->parentController(), url); +} + +void InnerWidget::editAlbumName(int id) { + const auto done = [=](QString name) { + albumRenamed(id, name); + }; + const auto i = ranges::find(_albums, id, &Data::StoryAlbum::id); + if (i == end(_albums)) { + return; + } + _controller->uiShow()->show(Box( + EditAlbumNameBox, + _controller->parentController(), + _peer, + id, + i->title, + done)); +} + +void InnerWidget::confirmDeleteAlbum(int id) { + const auto done = [=](Fn close) { + albumRemoved(id); + + const auto stories = &_controller->session().data().stories(); + stories->albumDelete(_peer, id); + + close(); + }; + _controller->uiShow()->show(Ui::MakeConfirmBox({ + .text = tr::lng_stories_album_delete_sure(), + .confirmed = crl::guard(this, done), + .confirmText = tr::lng_stories_album_delete_button(), + .confirmStyle = &st::attentionBoxButton, + })); +} + +void InnerWidget::albumAdded(Data::StoryAlbum result) { + Expects(ranges::contains(_albums, result.id, &Data::StoryAlbum::id)); + + _albumIdChanges.fire_copy(result.id); +} + +void InnerWidget::albumRenamed(int id, QString name) { + const auto i = ranges::find(_albums, id, &Data::StoryAlbum::id); + if (i != end(_albums)) { + i->title = name; + refreshAlbumsTabs(); + } +} + +void InnerWidget::albumRemoved(int id) { + auto now = _albumId.current(); + if (now == id) { + _albumIdChanges.fire_copy(0); + } + const auto i = ranges::find(_albums, id, &Data::StoryAlbum::id); + if (i != end(_albums)) { + _albums.erase(i); + refreshAlbumsTabs(); + } } void InnerWidget::saveState(not_null memento) { @@ -419,17 +878,24 @@ void InnerWidget::selectionAction(SelectionAction action) { _list->selectionAction(action); } -InnerWidget::~InnerWidget() = default; - int InnerWidget::resizeGetHeight(int newWidth) { + if (!newWidth) { + return 0; + } _inResize = true; auto guard = gsl::finally([this] { _inResize = false; }); if (_top) { _top->resizeToWidth(newWidth); } - _list->resizeToWidth(newWidth); - _empty->resizeToWidth(newWidth); + if (_list) { + _list->resizeToWidth(newWidth); + } + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->resizeToWidth(newWidth - margin.left() - margin.right()); + } + return recountHeight(); } @@ -452,25 +918,20 @@ int InnerWidget::recountHeight() { listHeight = _list->heightNoMargins(); top += listHeight; } - if (listHeight > 0) { - _empty->hide(); + if (const auto empty = _empty.get()) { + const auto margin = st::giftListAboutMargin; + empty->moveToLeft(margin.left(), top + margin.top()); + top += margin.top() + empty->height() + margin.bottom(); + } + if (_emptyLoading) { + top = std::max(top, _lastNonLoadingHeight); } else { - _empty->show(); - _empty->moveToLeft(0, top); - top += _empty->heightNoMargins(); + _lastNonLoadingHeight = top; } return top; } void InnerWidget::setScrollHeightValue(rpl::producer value) { - using namespace rpl::mappers; - _empty->setFullHeight(rpl::combine( - std::move(value), - _listTops.events_starting_with( - _list->topValue() - ) | rpl::flatten_latest(), - _topHeight.value(), - _1 - _2 + _3)); } rpl::producer InnerWidget::scrollToRequests() const { diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h index 75d3654273..809148653d 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.h @@ -7,11 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" +#include "data/data_stories.h" +#include "info/stories/info_stories_common.h" #include "ui/rp_widget.h" #include "ui/widgets/scroll_area.h" -#include "base/unique_qptr.h" + +namespace Data { +struct StoryAlbum; +} // namespace Data namespace Ui { +class SubTabs; +class PopupMenu; class VerticalLayout; class MultiSlideTracker; } // namespace Ui @@ -26,6 +34,10 @@ namespace Info::Media { class ListWidget; } // namespace Info::Media +namespace Window { +class SessionNavigation; +} // namespace Window + namespace Info::Stories { class Memento; @@ -35,7 +47,10 @@ class InnerWidget final : public Ui::RpWidget { public: InnerWidget( QWidget *parent, - not_null controller); + not_null controller, + rpl::producer albumId, + int addingToAlbumId = 0); + ~InnerWidget(); bool showInternal(not_null memento); void setIsStackBottom(bool isStackBottom) { @@ -52,7 +67,19 @@ public: rpl::producer selectedListValue() const; void selectionAction(SelectionAction action); - ~InnerWidget(); + void reload(); + void editAlbumStories(int id); + void shareAlbumLink(const QString &username, int id); + void editAlbumName(int id); + void confirmDeleteAlbum(int id); + void albumAdded(Data::StoryAlbum result); + + [[nodiscard]] rpl::producer albumEmptyValue() const { + return _albumEmpty.value(); + } + + [[nodiscard]] rpl::producer albumIdChanges() const; + [[nodiscard]] rpl::producer changes() const; protected: int resizeGetHeight(int newWidth) override; @@ -63,8 +90,13 @@ protected: private: int recountHeight(); void refreshHeight(); + void refreshEmpty(); + void preloadArchiveCount(); void setupTop(); + void setupList(); + void setupEmpty(); + void setupAlbums(); void createButtons(); void createProfileTop(); void createAboutArchive(); @@ -75,13 +107,30 @@ private: void addGiftsButton(Ui::MultiSlideTracker &tracker); void finalizeTop(); - object_ptr setupList(); + void refreshAlbumsTabs(); + void showMenuForAlbum(int id); + + void albumRenamed(int id, QString name); + void albumRemoved(int id); const not_null _controller; + const not_null _peer; + const int _addingToAlbumId = 0; + + std::vector _albums; + rpl::variable _albumId; + rpl::event_stream _albumIdChanges; + Ui::RpWidget *_albumsWrap = nullptr; + std::unique_ptr _albumsTabs; + rpl::variable _albumChanges; + + base::unique_qptr _menu; object_ptr _top = { nullptr }; object_ptr _list = { nullptr }; - object_ptr _empty; + object_ptr _empty = { nullptr }; + int _lastNonLoadingHeight = 0; + bool _emptyLoading = false; bool _inResize = false; bool _isStackBottom = false; @@ -90,6 +139,7 @@ private: rpl::event_stream> _selectedLists; rpl::event_stream> _listTops; rpl::variable _topHeight; + rpl::variable _albumEmpty; }; diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp index f6863bee8a..e0e07fdadd 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.cpp @@ -23,7 +23,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/history.h" #include "core/application.h" -#include "storage/storage_shared_media.h" #include "layout/layout_selection.h" #include "styles/style_info.h" @@ -49,7 +48,8 @@ Provider::Provider(not_null controller) : _controller(controller) , _peer(controller->key().storiesPeer()) , _history(_peer->owner().history(_peer)) -, _tab(controller->key().storiesTab()) { +, _albumId(controller->key().storiesAlbumId()) +, _addingToAlbumId(controller->key().storiesAddToAlbumId()) { style::PaletteChanged( ) | rpl::start_with_next([=] { for (auto &layout : _layouts) { @@ -179,10 +179,8 @@ void Provider::setSearchQuery(QString query) { void Provider::refreshViewer() { _viewerLifetime.destroy(); - const auto idForViewer = _aroundId; - auto ids = (_tab == Tab::Saved) - ? Data::SavedStoriesIds(_peer, idForViewer, _idsLimit) - : Data::ArchiveStoriesIds(_peer, idForViewer, _idsLimit); + const auto aroundId = _aroundId; + auto ids = Data::AlbumStoriesIds(_peer, _albumId, aroundId, _idsLimit); std::move( ids ) | rpl::start_with_next([=](Data::StoriesIdsSlice &&slice) { @@ -195,8 +193,8 @@ void Provider::refreshViewer() { auto nearestId = std::optional(); for (auto i = 0; i != _slice.size(); ++i) { if (!nearestId - || std::abs(*nearestId - idForViewer) - > std::abs(_slice[i] - idForViewer)) { + || std::abs(*nearestId - aroundId) + > std::abs(_slice[i] - aroundId)) { nearestId = _slice[i]; } } @@ -344,10 +342,20 @@ std::unique_ptr Provider::createLayout( } return nullptr; }; + + const auto peer = item->history()->peer; + const auto channel = peer->asChannel(); + const auto showPinned = (_albumId == Data::kStoriesAlbumIdSaved); + const auto showHidden = peer->isSelf() + || (channel && channel->canEditStories()); + using namespace Overview::Layout; const auto options = MediaOptions{ - .pinned = item->isPinned(), .story = true, + .storyPinned = showPinned && item->isPinned(), + .storyShowPinned = showPinned, + .storyHidden = showHidden && !item->storyInProfile(), + .storyShowHidden = showHidden, }; if (const auto photo = getPhoto()) { return std::make_unique(delegate, item, photo, options); @@ -368,7 +376,7 @@ ListItemSelectionData Provider::computeSelectionData( TextSelection selection) { auto result = ListItemSelectionData(selection); const auto id = item->id; - if (!IsStoryMsgId(id)) { + if (_addingToAlbumId || !IsStoryMsgId(id)) { return result; } const auto peer = item->history()->peer; @@ -380,6 +388,7 @@ ListItemSelectionData Provider::computeSelectionData( result.canForward = peer->isSelf() && story->canShare(); result.canDelete = story->canDelete(); result.canUnpinStory = story->pinnedToTop(); + result.storyInProfile = story->inProfile(); } result.canToggleStoryPin = peer->isSelf() || (channel && channel->canEditStories()); @@ -465,7 +474,7 @@ void Provider::saveState( not_null memento, ListScrollTopState scrollState) { if (_aroundId != kDefaultAroundId && scrollState.item) { - memento->setAroundId({ _peer->id, _aroundId }); + memento->setAroundId({ _peer->id, StoryIdToMsgId(_aroundId) }); memento->setIdsLimit(_idsLimit); memento->setScrollTopItem(scrollState.item->globalId()); memento->setScrollTopItemPosition(scrollState.position); diff --git a/Telegram/SourceFiles/info/stories/info_stories_provider.h b/Telegram/SourceFiles/info/stories/info_stories_provider.h index b43350d971..26cd64a3ba 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_provider.h +++ b/Telegram/SourceFiles/info/stories/info_stories_provider.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "data/data_stories_ids.h" #include "info/media/info_media_common.h" +#include "info/stories/info_stories_common.h" class DocumentData; class HistoryItem; @@ -26,8 +27,6 @@ class AbstractController; namespace Info::Stories { -enum class Tab; - class Provider final : public Media::ListProvider , private Media::ListSectionDelegate @@ -117,7 +116,8 @@ private: const not_null _controller; const not_null _peer; const not_null _history; - const Tab _tab; + const int _albumId = 0; + const int _addingToAlbumId = 0; StoryId _aroundId = kDefaultAroundId; int _idsLimit = kMinimalIdsLimit; diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp index 30052bc921..203d83cac1 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_widget.cpp @@ -8,22 +8,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/stories/info_stories_widget.h" #include "data/data_peer.h" +#include "data/data_stories.h" #include "info/stories/info_stories_inner_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" #include "lang/lang_keys.h" #include "ui/ui_utility.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" namespace Info::Stories { +int ArchiveId() { + return Data::kStoriesAlbumIdArchive; +} + Memento::Memento(not_null controller) -: ContentMemento(Tag{ controller->storiesPeer(), controller->storiesTab() }) +: ContentMemento(Tag{ + controller->storiesPeer(), + controller->storiesAlbumId(), + controller->storiesAddToAlbumId() }) , _media(controller) { } -Memento::Memento(not_null peer, Tab tab) -: ContentMemento(Tag{ peer, tab }) +Memento::Memento(not_null peer, int albumId, int addingToAlbumId) +: ContentMemento(Tag{ peer, albumId, addingToAlbumId }) , _media(peer, 0, Media::Type::PhotoVideo) { } @@ -45,15 +58,31 @@ object_ptr Memento::createWidget( Widget::Widget( QWidget *parent, not_null controller) -: ContentWidget(parent, controller) { +: ContentWidget(parent, controller) +, _albumId(controller->key().storiesAlbumId()) { _inner = setInnerWidget(object_ptr( this, - controller)); + controller, + _albumId.value(), + controller->key().storiesAddToAlbumId())); + _emptyAlbumShown = _inner->albumEmptyValue(); + _inner->albumIdChanges() | rpl::start_with_next([=](int id) { + controller->showSection( + Make(controller->storiesPeer(), id), + Window::SectionShow::Way::Backward); + }, _inner->lifetime()); _inner->setScrollHeightValue(scrollHeightValue()); _inner->scrollToRequests( ) | rpl::start_with_next([this](Ui::ScrollToRequest request) { scrollTo(request); }, _inner->lifetime()); + + rpl::combine( + _albumId.value(), + _emptyAlbumShown.value() + ) | rpl::start_with_next([=] { + refreshBottom(); + }, _inner->lifetime()); } void Widget::setIsStackBottom(bool isStackBottom) { @@ -66,10 +95,15 @@ bool Widget::showInternal(not_null memento) { return false; } if (auto storiesMemento = dynamic_cast(memento.get())) { - const auto tab = controller()->key().storiesTab(); - if (storiesMemento->storiesTab() == tab) { + const auto myId = controller()->key().storiesAlbumId(); + const auto hisId = storiesMemento->storiesAlbumId(); + constexpr auto kArchive = Data::kStoriesAlbumIdArchive; + if (myId == hisId) { restoreState(storiesMemento); return true; + } else if (myId != kArchive && hisId != kArchive) { + _albumId = hisId; + return true; } } return false; @@ -99,6 +133,94 @@ void Widget::restoreState(not_null memento) { scrollTopRestore(memento->scrollTop()); } +void Widget::refreshBottom() { + const auto albumId = _albumId.current(); + const auto withButton = (albumId != Data::kStoriesAlbumIdSaved) + && (albumId != Data::kStoriesAlbumIdArchive) + && controller()->storiesPeer()->canEditStories() + && !_emptyAlbumShown.current(); + const auto wasBottom = _pinnedToBottom ? _pinnedToBottom->height() : 0; + delete _pinnedToBottom.data(); + if (!withButton) { + setScrollBottomSkip(0); + _hasPinnedToBottom = false; + } else { + setupBottomButton(wasBottom); + } +} + +void Widget::setupBottomButton(int wasBottomHeight) { + _pinnedToBottom = Ui::CreateChild>( + this, + object_ptr(this)); + const auto wrap = _pinnedToBottom.data(); + wrap->toggle(false, anim::type::instant); + + const auto bottom = wrap->entity(); + bottom->show(); + + const auto button = Ui::CreateChild( + bottom, + rpl::single(QString()), + st::collectionEditBox.button); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->setText(tr::lng_stories_album_add_button( + ) | rpl::map([](const QString &text) { + return Ui::Text::IconEmoji(&st::collectionAddIcon).append(text); + })); + button->show(); + _hasPinnedToBottom = true; + + button->setClickedCallback([=] { + if (const auto id = _albumId.current()) { + _inner->editAlbumStories(id); + } else { + refreshBottom(); + } + }); + + const auto buttonTop = st::boxRadius; + bottom->widthValue() | rpl::start_with_next([=](int width) { + const auto normal = width - 2 * buttonTop; + button->resizeToWidth(normal); + const auto buttonLeft = (width - normal) / 2; + button->moveToLeft(buttonLeft, buttonTop); + }, button->lifetime()); + + button->heightValue() | rpl::start_with_next([=](int height) { + bottom->resize(bottom->width(), st::boxRadius + height); + }, button->lifetime()); + + const auto processHeight = [=] { + setScrollBottomSkip(wrap->height()); + wrap->moveToLeft(wrap->x(), height() - wrap->height()); + }; + + _inner->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + wrap->resizeToWidth(s.width()); + crl::on_main(wrap, processHeight); + }, wrap->lifetime()); + + rpl::combine( + wrap->heightValue(), + heightValue() + ) | rpl::start_with_next(processHeight, wrap->lifetime()); + + if (_shown) { + wrap->toggle( + true, + wasBottomHeight ? anim::type::instant : anim::type::normal); + } +} + +void Widget::showFinished() { + _shown = true; + if (const auto bottom = _pinnedToBottom.data()) { + bottom->toggle(true, anim::type::normal); + } +} + rpl::producer Widget::selectedListValue() const { return _inner->selectedListValue(); } @@ -109,18 +231,22 @@ void Widget::selectionAction(SelectionAction action) { rpl::producer Widget::title() { const auto peer = controller()->key().storiesPeer(); - return (controller()->key().storiesTab() == Tab::Archive) + return (controller()->key().storiesAlbumId() == ArchiveId()) ? tr::lng_stories_archive_title() : (peer && peer->isSelf()) ? tr::lng_menu_my_profile() : tr::lng_stories_my_title(); } -std::shared_ptr Make(not_null peer, Tab tab) { +rpl::producer Widget::desiredBottomShadowVisibility() { + return _hasPinnedToBottom.value(); +} + +std::shared_ptr Make(not_null peer, int albumId) { return std::make_shared( std::vector>( 1, - std::make_shared(peer, tab))); + std::make_shared(peer, albumId, 0))); } } // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/stories/info_stories_widget.h b/Telegram/SourceFiles/info/stories/info_stories_widget.h index 45df73f2ba..e8cb608600 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_widget.h +++ b/Telegram/SourceFiles/info/stories/info_stories_widget.h @@ -9,16 +9,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_content_widget.h" #include "info/media/info_media_widget.h" +#include "info/stories/info_stories_common.h" + +namespace Ui { +template +class SlideWrap; +} // namespace Ui namespace Info::Stories { class InnerWidget; -enum class Tab; class Memento final : public ContentMemento { public: Memento(not_null controller); - Memento(not_null peer, Tab tab); + Memento(not_null peer, int albumId, int addingToAlbumId); ~Memento(); object_ptr createWidget( @@ -37,6 +42,7 @@ public: private: Media::Memento _media; + int _addingToAlbumId = 0; }; @@ -58,18 +64,30 @@ public: rpl::producer title() override; + rpl::producer desiredBottomShadowVisibility() override; + + void showFinished() override; + private: void saveState(not_null memento); void restoreState(not_null memento); + void setupBottomButton(int wasBottomHeight); + void refreshBottom(); + std::shared_ptr doCreateMemento() override; + rpl::variable _albumId; InnerWidget *_inner = nullptr; + QPointer> _pinnedToBottom; + rpl::variable _hasPinnedToBottom; + rpl::variable _emptyAlbumShown; + bool _shown = false; }; [[nodiscard]] std::shared_ptr Make( not_null peer, - Tab tab = {}); + int albumId = 0); } // namespace Info::Stories diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp index ffb0204fce..afeb8584df 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_bubble_wrap.cpp @@ -52,10 +52,11 @@ QRect BubbleWrapInnerRect(const QRect &r) { not_null AddBubbleWrap( not_null container, const QSize &size) { - const auto bubble = container->add(object_ptr>( - container, - object_ptr(container)))->entity(); + const auto bubble = container->add( + object_ptr(container), + style::al_top); bubble->resize(size); + bubble->setNaturalWidth(size.width()); auto cached = QImage( size * style::DevicePixelRatio(), diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.cpp index fbfa8fb7f3..cb7846dfae 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_colors_editor.cpp @@ -245,12 +245,11 @@ object_ptr CreateGradientEditor( std::vector colors; }; const auto preview = container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - Size(st::defaultUserpicButton.photoSize), - false)))->entity(); + Size(st::defaultUserpicButton.photoSize), + false), + style::al_top); preview->setDuration(0); if (document) { preview->setDocument(document); @@ -271,10 +270,12 @@ object_ptr CreateGradientEditor( Ui::AddDivider(container); Ui::AddSkip(container); - const auto editor = container->add(object_ptr( + auto ownedEditor = object_ptr( container, ColorEditor::Mode::HSL, - state->colors.back())); + state->colors.back()); + container->resizeToWidth(ownedEditor->width()); + const auto editor = container->add(std::move(ownedEditor)); buttonsContainer->chosenChanges( ) | rpl::start_with_next([=](ColorsLine::Chosen *chosen) { @@ -306,7 +307,6 @@ object_ptr CreateGradientEditor( save(); }, container->lifetime()); - container->resizeToWidth(editor->width()); buttonsContainer->init(); return container; diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp index 5728091459..8ee29755da 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_preview.cpp @@ -166,6 +166,7 @@ EmojiUserpic::EmojiUserpic( , _painter(size.width()) , _duration(st::slideWrapDuration) { resize(size); + setNaturalWidth(size.width()); } void EmojiUserpic::setDocument(not_null document) { diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp index 5f77c5512c..2bed5f5620 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp @@ -178,7 +178,7 @@ void ShowGradientEditor( }, }); box->setWidth(content->width()); - box->addRow(std::move(content), {}); + box->addRow(std::move(content), style::margins()); })); } @@ -408,13 +408,12 @@ not_null CreateUserpicBuilder( const auto state = container->lifetime().make_state(); const auto preview = container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - Size(st::settingsInfoPhotoSize), - data.isForum)), - st::userpicBuilderEmojiPreviewPadding)->entity(); + Size(st::settingsInfoPhotoSize), + data.isForum), + st::userpicBuilderEmojiPreviewPadding, + style::al_top); if (const auto id = data.documentId) { const auto document = controller->session().data().document(id); if (document && document->sticker()) { @@ -423,13 +422,12 @@ not_null CreateUserpicBuilder( } container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - tr::lng_userpic_builder_color_subtitle(), - st::userpicBuilderEmojiSubtitle)), - st::userpicBuilderEmojiSubtitlePadding); + tr::lng_userpic_builder_color_subtitle(), + st::userpicBuilderEmojiSubtitle), + st::userpicBuilderEmojiSubtitlePadding, + style::al_top); const auto paletteBg = Ui::AddBubbleWrap( container, @@ -515,13 +513,12 @@ not_null CreateUserpicBuilder( }, palette->lifetime()); container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - tr::lng_userpic_builder_emoji_subtitle(), - st::userpicBuilderEmojiSubtitle)), - st::userpicBuilderEmojiSubtitlePadding); + tr::lng_userpic_builder_emoji_subtitle(), + st::userpicBuilderEmojiSubtitle), + st::userpicBuilderEmojiSubtitlePadding, + style::al_top); const auto selectorBg = Ui::AddBubbleWrap( container, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index c15f13cc0a..af70e4da16 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -72,7 +72,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/dropdown_menu.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/menu/menu_item_base.h" #include "ui/widgets/popup_menu.h" #include "webview/webview_interface.h" @@ -223,7 +222,7 @@ void ShowChooseBox( PeerTypes types, Fn)> callback, rpl::producer titleOverride = nullptr) { - const auto weak = std::make_shared>(); + const auto weak = std::make_shared>(); auto done = [=](not_null thread) mutable { if (const auto strong = *weak) { strong->closeBox(); @@ -388,7 +387,7 @@ void FillBotUsepic( not_null box, not_null bot, base::weak_ptr weak) { - auto aboutLabel = Ui::CreateLabelWithCustomEmoji( + auto aboutLabel = object_ptr( box->verticalLayout(), tr::lng_allow_bot_webview_details( lt_emoji, @@ -397,7 +396,6 @@ void FillBotUsepic( ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link(std::move(text), u"internal:"_q); }), - Core::TextContext({ .session = &bot->session() }), st::defaultFlatLabel); const auto userpic = Ui::CreateChild( box->verticalLayout(), @@ -515,15 +513,17 @@ void ConfirmEmojiStatusAccessBox( 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->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), + style::al_top); box->addButton(tr::lng_bot_emoji_status_access_allow(), [=] { if (!CheckEmojiStatusPremium(bot)) { @@ -561,19 +561,23 @@ void ConfirmEmojiStatusBox( box->closeBox(); }); - box->addRow(object_ptr( - box, - tr::lng_bot_emoji_status_title(), - st::botEmojiStatusTitle)); + box->addRow( + object_ptr( + box, + tr::lng_bot_emoji_status_title(), + st::botEmojiStatusTitle), + style::al_top); 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)); + 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), + style::al_top); AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip); @@ -894,7 +898,28 @@ void WebViewInstance::activate() { } } +void WebViewInstance::requestFullBot() { + if (_bot->isFullLoaded()) { + return; + } + _bot->updateFull(); + _bot->session().changes().peerUpdates( + _bot, + Data::PeerUpdate::Flag::FullInfo + ) | rpl::start_with_next([=] { + if (_botFullWaitingArgs.has_value()) { + auto args = *base::take(_botFullWaitingArgs); + if (args.url.isEmpty()) { + showGame(); + } else { + show(std::move(args)); + } + } + }, _lifetime); +} + void WebViewInstance::resolve() { + requestFullBot(); v::match(_source, [&](WebViewSourceButton data) { confirmOpen([=] { if (data.simple) { @@ -940,6 +965,8 @@ void WebViewInstance::resolve() { requestMain(); }); } + }, [&](WebViewSourceAgeVerification) { + requestMain(); }); } @@ -1314,6 +1341,10 @@ void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) { } void WebViewInstance::show(ShowArgs &&args) { + if (!_bot->isFullLoaded()) { + _botFullWaitingArgs.emplace(std::move(args)); + return; + } auto title = args.title.isEmpty() ? Info::Profile::NameValue(_bot) : rpl::single(args.title); @@ -1380,6 +1411,10 @@ void WebViewInstance::show(ShowArgs &&args) { void WebViewInstance::showGame() { Expects(v::is(_source)); + if (!_bot->isFullLoaded()) { + _botFullWaitingArgs.emplace(); + return; + } const auto game = v::get(_source); _panelUrl = QString::fromUtf8(_button.url); _panel = Ui::BotWebView::Show({ @@ -1823,8 +1858,8 @@ void WebViewInstance::botSendPreparedMessage( .viaBotId = peerToUser(bot->id), }); struct State { - QPointer preview; - QPointer choose; + base::weak_qptr preview; + base::weak_qptr choose; rpl::event_stream> recipient; Fn send; SendPaymentHelper sendPayment; @@ -1844,10 +1879,10 @@ void WebViewInstance::botSendPreparedMessage( const auto weak1 = state->preview; const auto weak2 = state->choose; const auto close = [=] { - if (const auto strong = weak1.data()) { + if (const auto strong = weak1.get()) { strong->closeBox(); } - if (const auto strong = weak2.data()) { + if (const auto strong = weak2.get()) { strong->closeBox(); } }; @@ -1857,9 +1892,9 @@ void WebViewInstance::botSendPreparedMessage( } if (success) { *failed = -1; - if (const auto strong2 = weak2.data()) { + if (const auto strong2 = weak2.get()) { strong2->showToast({ tr::lng_share_done(tr::now) }); - } else if (const auto strong1 = weak1.data()) { + } else if (const auto strong1 = weak1.get()) { strong1->showToast({ tr::lng_share_done(tr::now) }); } base::call_delayed(Ui::Toast::kDefaultDuration, close); @@ -2006,6 +2041,12 @@ void WebViewInstance::botDownloadFile( }).send(); } +void WebViewInstance::botVerifyAge(int age) { + if (v::is(_source)) { + v::get(_source).done(age); + } +} + void WebViewInstance::botOpenPrivacyPolicy() { const auto bot = _bot; const auto weak = _context.controller; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 442cc3fc78..7487de27e1 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -163,6 +163,16 @@ struct WebViewSourceBotProfile { WebViewSourceBotProfile) = default; }; +struct WebViewSourceAgeVerification { + Fn done; + + friend inline bool operator==( + WebViewSourceAgeVerification, + WebViewSourceAgeVerification) { + return true; + } +}; + struct WebViewSource : std::variant< WebViewSourceButton, WebViewSourceSwitch, @@ -173,7 +183,8 @@ struct WebViewSource : std::variant< WebViewSourceAttachMenu, WebViewSourceBotMenu, WebViewSourceGame, - WebViewSourceBotProfile> { + WebViewSourceBotProfile, + WebViewSourceAgeVerification> { using variant::variant; }; @@ -220,6 +231,7 @@ public: private: void resolve(); + void requestFullBot(); bool openAppFromBotMenuLink(); @@ -291,6 +303,7 @@ private: Ui::BotWebView::SetEmojiStatusRequest request) override; void botDownloadFile( Ui::BotWebView::DownloadFileRequest request) override; + void botVerifyAge(int age) override; void botOpenPrivacyPolicy() override; void botClose() override; @@ -301,6 +314,8 @@ private: const WebViewButton _button; const WebViewSource _source; + std::optional _botFullWaitingArgs; + BotAppData *_app = nullptr; QString _appStartParam; bool _dataSent = false; diff --git a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp index 2ab5ae60b5..285ad6ec72 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_widget.cpp @@ -80,7 +80,7 @@ Widget::Widget( // Inner widget has OpaquePaintEvent attribute so it doesn't repaint on scroll. // But we should force it to repaint so that GIFs will continue to animate without update() calls. // We do that by creating a transparent widget above our _inner. - auto forceRepaintOnScroll = object_ptr(this); + auto forceRepaintOnScroll = object_ptr(this); forceRepaintOnScroll->setGeometry(innerRect().x() + st::roundRadiusSmall, innerRect().y() + st::roundRadiusSmall, st::roundRadiusSmall, st::roundRadiusSmall); forceRepaintOnScroll->setAttribute(Qt::WA_TransparentForMouseEvents); forceRepaintOnScroll->show(); diff --git a/Telegram/SourceFiles/intro/intro_code.cpp b/Telegram/SourceFiles/intro/intro_code.cpp index f870d52cb6..e41810e8fd 100644 --- a/Telegram/SourceFiles/intro/intro_code.cpp +++ b/Telegram/SourceFiles/intro/intro_code.cpp @@ -84,16 +84,19 @@ void CodeWidget::updateDescText() { const auto byTelegram = getData()->codeByTelegram; const auto isFragment = !getData()->codeByFragmentUrl.isEmpty(); _isFragment = isFragment; - setDescriptionText( - isFragment - ? tr::lng_intro_fragment_about( - lt_phone_number, - rpl::single(TextWithEntities{ - .text = Ui::FormatPhone(getData()->phone) - }), - Ui::Text::RichLangValue) - : (byTelegram ? tr::lng_code_from_telegram : tr::lng_code_desc)( - Ui::Text::RichLangValue)); + setDescriptionText(isEmailVerification() + ? tr::lng_intro_email_confirm_subtitle( + lt_email, + rpl::single(Ui::Text::WrapEmailPattern(getData()->emailPattern)), + Ui::Text::WithEntities) + : isFragment + ? tr::lng_intro_fragment_about( + lt_phone_number, + rpl::single( + TextWithEntities::Simple(Ui::FormatPhone(getData()->phone))), + Ui::Text::RichLangValue) + : (byTelegram ? tr::lng_code_from_telegram : tr::lng_code_desc)( + Ui::Text::RichLangValue)); if (getData()->codeByTelegram) { _noTelegramCode->show(); _callTimer.cancel(); @@ -350,11 +353,13 @@ void CodeWidget::submitCode(const QString &text) { _code->setEnabled(false); getData()->pwdState = Core::CloudPasswordState(); _sentRequest = api().request(MTPauth_SignIn( - MTP_flags(MTPauth_SignIn::Flag::f_phone_code), + MTP_flags(isEmailVerification() + ? MTPauth_SignIn::Flag::f_email_verification + : MTPauth_SignIn::Flag::f_phone_code), MTP_string(getData()->phone), MTP_bytes(getData()->phoneHash), MTP_string(_sentCode), - MTPEmailVerification() + MTP_emailVerificationCode(MTP_string(_sentCode)) )).done([=](const MTPauth_Authorization &result) { codeSubmitDone(result); }).fail([=](const MTP::Error &error) { @@ -362,6 +367,10 @@ void CodeWidget::submitCode(const QString &text) { }).handleFloodErrors().send(); } +bool CodeWidget::isEmailVerification() const { + return !getData()->emailPattern.isEmpty(); +} + rpl::producer CodeWidget::nextButtonText() const { return _isFragment.value( ) | rpl::map([=](bool isFragment) { diff --git a/Telegram/SourceFiles/intro/intro_code.h b/Telegram/SourceFiles/intro/intro_code.h index 53a94b50dc..a9d22ad31b 100644 --- a/Telegram/SourceFiles/intro/intro_code.h +++ b/Telegram/SourceFiles/intro/intro_code.h @@ -54,6 +54,8 @@ private: int errorTop() const override; + [[nodiscard]] bool isEmailVerification() const; + void updateCallText(); void refreshLang(); void updateControlsGeometry(); diff --git a/Telegram/SourceFiles/intro/intro_code_input.cpp b/Telegram/SourceFiles/intro/intro_code_input.cpp index a3964d3cf7..476d7ecf91 100644 --- a/Telegram/SourceFiles/intro/intro_code_input.cpp +++ b/Telegram/SourceFiles/intro/intro_code_input.cpp @@ -177,6 +177,7 @@ void CodeInput::setDigitsCountMax(int digitsCount) { + st::introCodeDigitSkip * (digitsCount - 1) + padding.right(), st::introCodeDigitHeight); + setNaturalWidth(width()); for (auto i = 0; i < digitsCount; i++) { const auto widget = Ui::CreateChild(this); diff --git a/Telegram/SourceFiles/intro/intro_email.cpp b/Telegram/SourceFiles/intro/intro_email.cpp new file mode 100644 index 0000000000..b80077bb17 --- /dev/null +++ b/Telegram/SourceFiles/intro/intro_email.cpp @@ -0,0 +1,172 @@ +/* +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 "intro/intro_email.h" + +#include "intro/intro_code.h" +#include "intro/intro_code_input.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "settings/cloud_password/settings_cloud_password_common.h" +#include "settings/settings_common.h" // CreateLottieIcon. +#include "ui/rect.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_intro.h" +#include "styles/style_settings.h" + +namespace Intro { +namespace details { + +EmailWidget::EmailWidget( + QWidget *parent, + not_null account, + not_null data) +: Step(parent, account, data) +, _inner(this) { + const auto content = _inner.get(); + widthValue() | rpl::start_with_next([=](int w) { + content->resizeToWidth(st::introNextButton.width); + content->moveToLeft((w - content->width()) / 2, contentTop()); + }, content->lifetime()); + + content->add( + object_ptr( + content, + tr::lng_intro_email_setup_title(), + st::introTitle), + style::margins(), + style::al_left); + Ui::AddSkip(content, st::lineWidth * 2); + content->add( + object_ptr( + content, + tr::lng_settings_cloud_login_email_about(), + st::introDescription), + style::margins(), + style::al_left); + + { + const auto lottie = u"cloud_password/email"_q; + const auto size = st::settingsCloudPasswordIconSize / 3 * 2; + auto icon = Settings::CreateLottieIcon( + content, + { .name = lottie, .sizeOverride = Size(size) }, + style::margins()); + content->add(std::move(icon.widget)); + _showFinished.events( + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::once); + }, lifetime()); + } + + const auto newInput = Settings::CloudPassword::AddWrappedField( + content, + tr::lng_settings_cloud_login_email_placeholder(), + QString()); + Ui::AddSkip(content); + const auto error = Settings::CloudPassword::AddError(content, nullptr); + newInput->changes() | rpl::start_with_next([=] { + error->hide(); + }, newInput->lifetime()); + newInput->setText(getData()->email); + if (newInput->hasText()) { + newInput->selectAll(); + } + _setFocus.events() | rpl::start_with_next([=] { + newInput->setFocus(); + }, newInput->lifetime()); + + _submitCallback = [=] { + const auto send = [=](const QString &email) { + getData()->email = email; + + const auto done = [=](int length, const QString &pattern) { + _sentRequest = 0; + getData()->codeLength = length; + getData()->emailPattern = pattern; + goNext(); + }; + const auto fail = [=](const QString &type) { + _sentRequest = 0; + + newInput->setFocus(); + newInput->showError(); + newInput->selectAll(); + error->show(); + + if (MTP::IsFloodError(type)) { + error->setText(tr::lng_flood_error(tr::now)); + } else if (type == u"EMAIL_NOT_ALLOWED"_q) { + error->setText( + tr::lng_settings_error_email_not_alowed(tr::now)); + } else if (type == u"EMAIL_INVALID"_q) { + error->setText(tr::lng_cloud_password_bad_email(tr::now)); + } else if (type == u"EMAIL_HASH_EXPIRED"_q) { + // Show box? + error->setText(Lang::Hard::EmailConfirmationExpired()); + } else { + error->setText(Lang::Hard::ServerError()); + } + }; + + _sentRequest = api().request(MTPaccount_SendVerifyEmailCode( + MTP_emailVerifyPurposeLoginSetup( + MTP_string(getData()->phone), + MTP_bytes(getData()->phoneHash)), + MTP_string(email) + )).done([=](const MTPaccount_SentEmailCode &result) { + done( + result.data().vlength().v, + qs(result.data().vemail_pattern())); + }).fail([=](const MTP::Error &error) { + fail(error.type()); + }).send(); + }; + const auto newText = newInput->getLastText(); + if (newText.isEmpty()) { + newInput->setFocus(); + newInput->showError(); + } else { + send(newText); + } + }; +} + +void EmailWidget::submit() { + if (_submitCallback) { + _submitCallback(); + } +} + +void EmailWidget::setInnerFocus() { + _setFocus.fire({}); +} + +void EmailWidget::activate() { + Step::activate(); + showChildren(); + setInnerFocus(); + _showFinished.fire({}); +} + +void EmailWidget::finished() { + Step::finished(); + cancelled(); +} + +void EmailWidget::cancelled() { + api().request(base::take(_sentRequest)).cancel(); +} + +} // namespace details +} // namespace Intro diff --git a/Telegram/SourceFiles/intro/intro_email.h b/Telegram/SourceFiles/intro/intro_email.h new file mode 100644 index 0000000000..aa21da3ab6 --- /dev/null +++ b/Telegram/SourceFiles/intro/intro_email.h @@ -0,0 +1,50 @@ +/* +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 "intro/intro_step.h" + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace MTP { +class Sender; +} // namespace MTP + +namespace Intro { +namespace details { + +class EmailWidget final : public Step { +public: + EmailWidget( + QWidget *parent, + not_null account, + not_null data); + + void setInnerFocus() override; + void activate() override; + void finished() override; + void cancelled() override; + void submit() override; + + bool hasBack() const override { + return true; + } + +private: + object_ptr _inner; + Fn _submitCallback = nullptr; + rpl::event_stream<> _showFinished; + rpl::event_stream<> _setFocus; + mtpRequestId _sentRequest = 0; + +}; + +} // namespace details +} // namespace Intro diff --git a/Telegram/SourceFiles/intro/intro_password_check.cpp b/Telegram/SourceFiles/intro/intro_password_check.cpp index 3555c95ef8..748e304fe1 100644 --- a/Telegram/SourceFiles/intro/intro_password_check.cpp +++ b/Telegram/SourceFiles/intro/intro_password_check.cpp @@ -230,7 +230,7 @@ void PasswordCheckWidget::codeSubmitDone( fields.mtp.curRequest = {}; fields.hasPassword = false; auto box = Box(&api().instance(), nullptr, fields); - const auto boxShared = std::make_shared>(); + const auto boxShared = std::make_shared>(); box->newAuthorization( ) | rpl::start_with_next([=](const MTPauth_Authorization &result) { diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 261c533112..21299c41e9 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "intro/intro_code.h" +#include "intro/intro_email.h" #include "intro/intro_qr.h" #include "styles/style_intro.h" #include "ui/widgets/buttons.h" @@ -237,6 +238,9 @@ void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) { fillSentCodeData(data); getData()->phone = DigitsOnly(_sentPhone); getData()->phoneHash = qba(data.vphone_code_hash()); + if (getData()->emailStatus == EmailStatus::SetupRequired) { + return goNext(); + } const auto next = data.vnext_type(); if (next && next->type() == mtpc_auth_codeTypeCall) { getData()->callStatus = CallStatus::Waiting; diff --git a/Telegram/SourceFiles/intro/intro_step.cpp b/Telegram/SourceFiles/intro/intro_step.cpp index e37b6a3144..5dfed66385 100644 --- a/Telegram/SourceFiles/intro/intro_step.cpp +++ b/Telegram/SourceFiles/intro/intro_step.cpp @@ -379,7 +379,7 @@ void Step::fillSentCodeData(const MTPDauth_sentCode &data) { }, [&](const MTPDauth_sentCodeTypeSmsPhrase &) { bad("SmsPhrase"); }, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) { - bad("SetUpEmailRequired"); + getData()->emailStatus = EmailStatus::SetupRequired; }); } diff --git a/Telegram/SourceFiles/intro/intro_widget.cpp b/Telegram/SourceFiles/intro/intro_widget.cpp index 4fad74954d..07afdec084 100644 --- a/Telegram/SourceFiles/intro/intro_widget.cpp +++ b/Telegram/SourceFiles/intro/intro_widget.cpp @@ -402,7 +402,7 @@ void Widget::historyMove(StackAction action, Animate animate) { } void Widget::hideAndDestroy(object_ptr> widget) { - const auto weak = Ui::MakeWeak(widget.data()); + const auto weak = base::make_weak(widget.data()); widget->hide(anim::type::normal); widget->shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -624,7 +624,7 @@ void Widget::showTerms(Fn callback) { if (getData()->termsLock.text.text.isEmpty()) { return; } - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto box = Ui::show(callback ? Box( getData()->termsLock, diff --git a/Telegram/SourceFiles/intro/intro_widget.h b/Telegram/SourceFiles/intro/intro_widget.h index fa46639211..ad61ca0882 100644 --- a/Telegram/SourceFiles/intro/intro_widget.h +++ b/Telegram/SourceFiles/intro/intro_widget.h @@ -43,6 +43,11 @@ enum class CallStatus { Disabled, }; +enum class EmailStatus { + None, + SetupRequired, +}; + struct Data { // Required for the UserpicButton. const not_null controller; @@ -58,6 +63,10 @@ struct Data { bool codeByTelegram = false; QString codeByFragmentUrl; + EmailStatus emailStatus = EmailStatus::None; + QString email; + QString emailPattern; + Core::CloudPasswordState pwdState; Window::TermsLock termsLock; diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp index de1b77ed7c..39b7ad5918 100644 --- a/Telegram/SourceFiles/iv/iv_controller.cpp +++ b/Telegram/SourceFiles/iv/iv_controller.cpp @@ -912,10 +912,12 @@ void Controller::showWebviewError() { } void Controller::showWebviewError(TextWithEntities text) { - auto error = Ui::CreateChild>( - _container, + const auto wrap = Ui::CreateChild(_container); + + const auto error = Ui::CreateChild>( + wrap, object_ptr( - _container, + wrap, rpl::single(text), st::paymentsCriticalError), st::paymentsCriticalErrorPadding); @@ -929,10 +931,16 @@ void Controller::showWebviewError(TextWithEntities text) { File::OpenUrl(entity.data); return false; }); - error->show(); + wrap->show(); + + wrap->widthValue() | rpl::start_with_next([=](int width) { + error->resizeToWidth(width); + wrap->resize(width, error->height()); + }, wrap->lifetime()); + _container->sizeValue() | rpl::start_with_next([=](QSize size) { - error->setGeometry(0, 0, size.width(), size.height() * 2 / 3); - }, error->lifetime()); + wrap->setGeometry(0, 0, size.width(), size.height() * 2 / 3); + }, wrap->lifetime()); } void Controller::showInWindow( @@ -1105,7 +1113,7 @@ void Controller::showMenu() { } _menu->setDestroyedCallback(crl::guard(_window.get(), [ this, - weakButton = Ui::MakeWeak(_menuToggle.data()), + weakButton = base::make_weak(_menuToggle.data()), menu = _menu.get()] { if (_menu == menu && weakButton) { weakButton->setForceRippled(false); diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 09a7e2e383..dec3f36cf9 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -315,11 +315,11 @@ ShareBoxResult Shown::shareBox(ShareBoxDescriptor &&descriptor) { }; const auto state = wrap->lifetime().make_state(); - const auto weak = QPointer(wrap); + const auto weak = base::make_weak(wrap); const auto lookup = crl::guard(weak, [state] { return state->stack; }); const auto layer = Ui::CreateChild( wrap.get(), - [=] { return std::make_shared(weak.data(), lookup); }); + [=] { return std::make_shared(weak.get(), lookup); }); state->stack = layer; const auto show = layer->showFactory()(); diff --git a/Telegram/SourceFiles/iv/iv_pch.h b/Telegram/SourceFiles/iv/iv_pch.h index 4ad8da65a9..15ad9d2fe7 100644 --- a/Telegram/SourceFiles/iv/iv_pch.h +++ b/Telegram/SourceFiles/iv/iv_pch.h @@ -22,9 +22,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flat_map.h" #include "base/flat_set.h" +#include "base/weak_qptr.h" #include "ui/qt_object_factory.h" -#include "ui/qt_weak_factory.h" #include "scheme.h" #include "logs.h" diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index 02620f2e1d..d05d91709c 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -917,7 +917,7 @@ int NonZeroPartToInt(QString value) { : (value.isEmpty() ? 0 : value.toInt()); } -ShortenedCount FormatCountToShort(int64 number) { +ShortenedCount FormatCountToShort(int64 number, bool onlyK) { auto result = ShortenedCount{ number }; const auto abs = std::abs(number); const auto shorten = [&](int64 divider, char multiplier) { @@ -934,7 +934,7 @@ ShortenedCount FormatCountToShort(int64 number) { result.number = rounded * divider; result.shortened = true; }; - if (abs >= 1'000'000) { + if (!onlyK && abs >= 1'000'000) { shorten(1'000'000, 'M'); } else if (abs >= 10'000) { shorten(1'000, 'K'); @@ -1004,14 +1004,11 @@ PluralResult Plural( const auto t = f; const auto useNonDefaultPlural = (ChoosePlural != ChoosePluralDefault) - && Lang::details::IsNonDefaultPlural(keyBase); - const auto shift = (useNonDefaultPlural ? ChoosePlural : ChoosePluralDefault)( - (integer ? i : -1), - i, - v, - w, - f, - t); + && (keyBase == kPluralKeyBaseForCloudValue + || Lang::details::IsNonDefaultPlural(keyBase)); + const auto shift = (useNonDefaultPlural + ? ChoosePlural + : ChoosePluralDefault)((integer ? i : -1), i, v, w, f, t); if (integer) { const auto round = qRound(value); if (type == lt_count_short) { diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 40f3ef9972..74088d9602 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -26,7 +26,9 @@ struct ShortenedCount { QString string; bool shortened = false; }; -[[nodiscard]] ShortenedCount FormatCountToShort(int64 number); +[[nodiscard]] ShortenedCount FormatCountToShort( + int64 number, + bool onlyK = false); [[nodiscard]] QString FormatCountDecimal(int64 number); [[nodiscard]] QString FormatExactCountDecimal(float64 number); [[nodiscard]] ShortenedCount FormatCreditsAmountToShort( @@ -38,6 +40,7 @@ struct PluralResult { int keyShift = 0; QString replacement; }; +inline constexpr auto kPluralKeyBaseForCloudValue = ushort(-1); PluralResult Plural( ushort keyBase, float64 value, diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index b65c104555..844c68dfbd 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -121,6 +121,14 @@ int AppConfig::pinnedGiftsLimit() const { return get(u"stargifts_pinned_to_top_limit"_q, 6); } +int AppConfig::giftCollectionsLimit() const { + return get(u"stargifts_collections_limit"_q, 10); +} + +int AppConfig::giftCollectionGiftsLimit() const { + return get(u"stargifts_collection_gifts_limit"_q, 500); +} + bool AppConfig::callsDisabledForSession() const { const auto authorizations = _account->sessionExists() ? &_account->session().api().authorizations() @@ -140,18 +148,32 @@ bool AppConfig::confcallPrioritizeVP8() const { return get(u"confcall_use_vp8"_q, false); } -int AppConfig::giftResalePriceMax() const { - return get(u"stars_stargift_resale_amount_max"_q, 35000); -} - -int AppConfig::giftResalePriceMin() const { +int AppConfig::giftResaleStarsMin() const { return get(u"stars_stargift_resale_amount_min"_q, 125); } -int AppConfig::giftResaleReceiveThousandths() const { +int AppConfig::giftResaleStarsMax() const { + return get(u"stars_stargift_resale_amount_max"_q, 35000); +} + +int AppConfig::giftResaleStarsThousandths() const { return get(u"stars_stargift_resale_commission_permille"_q, 800); } +int64 AppConfig::giftResaleNanoTonMin() const { + return get(u"ton_stargift_resale_amount_min"_q, 250'000'000LL); +} + +int64 AppConfig::giftResaleNanoTonMax() const { + return get( + u"ton_stargift_resale_amount_max"_q, + 1'000'000'000'000'000LL); +} + +int AppConfig::giftResaleNanoTonThousandths() const { + return get(u"ton_stargift_resale_commission_permille"_q, 800); +} + int AppConfig::pollOptionsLimit() const { return get(u"poll_answers_max"_q, 12); } @@ -208,6 +230,30 @@ TimeId AppConfig::suggestedPostAgeMin() const { return get(u"stars_suggested_post_age_min"_q, 86400); } +bool AppConfig::ageVerifyNeeded() const { + return get(u"need_age_video_verification"_q, false); +} + +QString AppConfig::ageVerifyCountry() const { + return get(u"verify_age_country"_q, QString()); +} + +int AppConfig::ageVerifyMinAge() const { + return get(u"verify_age_min"_q, 18); +} + +QString AppConfig::ageVerifyBotUsername() const { + return get(u"verify_age_bot_username"_q, QString()); +} + +int AppConfig::storiesAlbumsLimit() const { + return get(u"stories_albums_limit"_q, 100); +} + +int AppConfig::storiesAlbumLimit() const { + return get(u"stories_album_stories_limit"_q, 1000); +} + 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 a2ea65f426..91fac6690b 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -78,14 +78,19 @@ public: [[nodiscard]] int paidMessageChannelStarsDefault() const; [[nodiscard]] int pinnedGiftsLimit() const; + [[nodiscard]] int giftCollectionsLimit() const; + [[nodiscard]] int giftCollectionGiftsLimit() const; [[nodiscard]] bool callsDisabledForSession() const; [[nodiscard]] int confcallSizeLimit() const; [[nodiscard]] bool confcallPrioritizeVP8() const; - [[nodiscard]] int giftResalePriceMax() const; - [[nodiscard]] int giftResalePriceMin() const; - [[nodiscard]] int giftResaleReceiveThousandths() const; + [[nodiscard]] int giftResaleStarsMin() const; + [[nodiscard]] int giftResaleStarsMax() const; + [[nodiscard]] int giftResaleStarsThousandths() const; + [[nodiscard]] int64 giftResaleNanoTonMin() const; + [[nodiscard]] int64 giftResaleNanoTonMax() const; + [[nodiscard]] int giftResaleNanoTonThousandths() const; [[nodiscard]] int pollOptionsLimit() const; [[nodiscard]] int todoListItemsLimit() const; @@ -102,6 +107,14 @@ public: [[nodiscard]] int suggestedPostDelayMax() const; [[nodiscard]] TimeId suggestedPostAgeMin() const; + [[nodiscard]] bool ageVerifyNeeded() const; + [[nodiscard]] QString ageVerifyCountry() const; + [[nodiscard]] int ageVerifyMinAge() const; + [[nodiscard]] QString ageVerifyBotUsername() const; + + [[nodiscard]] int storiesAlbumsLimit() const; + [[nodiscard]] int storiesAlbumLimit() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 7f562e3e48..543bb26d5e 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -33,7 +33,7 @@ SessionSettings::SessionSettings() QByteArray SessionSettings::serialize() const { const auto autoDownload = _autoDownload.serialize(); - const auto size = sizeof(qint32) * 4 + auto size = sizeof(qint32) * 4 + _groupStickersSectionHidden.size() * sizeof(quint64) + sizeof(qint32) * 4 + Serialize::bytearraySize(autoDownload) @@ -44,7 +44,25 @@ QByteArray SessionSettings::serialize() const { + sizeof(qint32) * 3 + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4) + sizeof(qint32) - + _verticalSubsectionTabs.size() * sizeof(quint64); + + _verticalSubsectionTabs.size() * sizeof(quint64) + + sizeof(qint32) // _ringtoneDefaultVolumes size + + (_ringtoneDefaultVolumes.size() + * (0 + + sizeof(uint8_t) * 1 // Data::DefaultNotify + + sizeof(ushort))) // Volume + + sizeof(qint32) // _ringtoneVolumes size + + (_ringtoneVolumes.size() + * (0 + + sizeof(quint64) * 3 // ThreadId + + sizeof(ushort))) // Volume + + sizeof(qint32) // _ratedTranscriptions size + + (_ratedTranscriptions.size() * sizeof(quint64)) + + sizeof(qint32); // _unreviewed size + for (const auto &auth : _unreviewed) { + size += sizeof(quint64) + sizeof(qint32) + sizeof(qint32) + + Serialize::stringSize(auth.device) + + Serialize::stringSize(auth.location); + } auto result = QByteArray(); result.reserve(size); @@ -100,6 +118,31 @@ QByteArray SessionSettings::serialize() const { for (const auto &peerId : _verticalSubsectionTabs) { stream << SerializePeerId(peerId); } + stream << qint32(_ringtoneDefaultVolumes.size()); + for (const auto &[key, value] : _ringtoneDefaultVolumes) { + stream << uint8_t(key) << ushort(value); + } + stream << qint32(_ringtoneVolumes.size()); + for (const auto &[key, value] : _ringtoneVolumes) { + stream + << SerializePeerId(key.peerId) + << qint64(key.topicRootId.bare) + << SerializePeerId(key.monoforumPeerId) + << ushort(value); + } + stream << qint32(_ratedTranscriptions.size()); + for (const auto &transcriptionId : _ratedTranscriptions) { + stream << quint64(transcriptionId); + } + stream << qint32(_unreviewed.size()); + for (const auto &auth : _unreviewed) { + stream + << quint64(auth.hash) + << qint32(auth.unconfirmed ? 1 : 0) + << qint32(auth.date) + << auth.device + << auth.location; + } } Ensures(result.size() == size); @@ -167,6 +210,10 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { qint32 legacySkipPremiumStickersSet = 0; qint32 lastNonPremiumLimitDownload = 0; qint32 lastNonPremiumLimitUpload = 0; + base::flat_map ringtoneDefaultVolumes; + base::flat_map ringtoneVolumes; + base::flat_set ratedTranscriptions; + std::vector unreviewed; stream >> versionTag; if (versionTag == kVersionTag) { @@ -395,7 +442,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { stream >> count; if (stream.status() == QDataStream::Ok) { for (auto i = 0; i != count; ++i) { - quint64 period; + auto period = quint64(); stream >> period; mutePeriods.emplace_back(period); } @@ -489,6 +536,97 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { } } } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto keyDefaultNotify = uint8_t(); + auto value = ushort(); + stream + >> keyDefaultNotify + >> value; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + ringtoneDefaultVolumes.emplace( + static_cast(keyDefaultNotify), + value); + } + } + } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto keyPeerId = quint64(); + auto keyTopicRootId = qint64(); + auto keyMonoforumPeerId = quint64(); + auto value = ushort(); + stream + >> keyPeerId + >> keyTopicRootId + >> keyMonoforumPeerId + >> value; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + ringtoneVolumes.emplace(ThreadId{ + DeserializePeerId(keyPeerId), + keyTopicRootId, + DeserializePeerId(keyMonoforumPeerId), + }, value); + } + } + } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto transcriptionId = quint64(); + stream >> transcriptionId; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()" + "with ratedTranscriptions")); + return; + } + ratedTranscriptions.emplace(transcriptionId); + } + } + } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto hash = quint64(); + auto unconfirmed = qint32(); + auto date = qint32(); + QString device, location; + stream >> hash >> unconfirmed >> date >> device >> location; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()" + "with unreviewed")); + return; + } + unreviewed.emplace_back(Data::UnreviewedAuth{ + .hash = hash, + .unconfirmed = (unconfirmed == 1), + .date = TimeId(date), + .device = device, + .location = location, + }); + } + } + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()")); @@ -536,6 +674,10 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { _lastNonPremiumLimitDownload = lastNonPremiumLimitDownload; _lastNonPremiumLimitUpload = lastNonPremiumLimitUpload; _verticalSubsectionTabs = std::move(verticalSubsectionTabs); + _ringtoneDefaultVolumes = std::move(ringtoneDefaultVolumes); + _ringtoneVolumes = std::move(ringtoneVolumes); + _ratedTranscriptions = std::move(ratedTranscriptions); + _unreviewed = std::move(unreviewed); if (version < 2) { app.setLastSeenWarningSeen(appLastSeenWarningSeen == 1); @@ -710,4 +852,58 @@ void SessionSettings::addMutePeriod(TimeId period) { } } +ushort SessionSettings::ringtoneVolume( + Data::DefaultNotify defaultNotify) const { + const auto i = _ringtoneDefaultVolumes.find(defaultNotify); + return (i != end(_ringtoneDefaultVolumes)) ? i->second : 0; +} + +void SessionSettings::setRingtoneVolume( + Data::DefaultNotify defaultNotify, + ushort volume) { + if (volume > 0 && volume <= 100) { + _ringtoneDefaultVolumes[defaultNotify] = volume; + } else { + _ringtoneDefaultVolumes.remove(defaultNotify); + } +} + +ushort SessionSettings::ringtoneVolume( + PeerId peerId, + MsgId topicRootId, + PeerId monoforumPeerId) const { + const auto i = _ringtoneVolumes.find( + ThreadId{ peerId, topicRootId, monoforumPeerId }); + return (i != end(_ringtoneVolumes)) ? i->second : 0; +} + +void SessionSettings::setRingtoneVolume( + PeerId peerId, + MsgId topicRootId, + PeerId monoforumPeerId, + ushort volume) { + const auto id = ThreadId{ peerId, topicRootId, monoforumPeerId }; + if (volume > 0 && volume <= 100) { + _ringtoneVolumes[id] = volume; + } else { + _ringtoneVolumes.remove(id); + } +} + +void SessionSettings::markTranscriptionAsRated(uint64 transcriptionId) { + _ratedTranscriptions.emplace(transcriptionId); +} + +bool SessionSettings::isTranscriptionRated(uint64 transcriptionId) const { + return _ratedTranscriptions.contains(transcriptionId); +} + +void SessionSettings::setUnreviewed(std::vector auths) { + _unreviewed = std::move(auths); +} + +const std::vector &SessionSettings::unreviewed() const { + return _unreviewed; +} + } // namespace Main diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index ee6218e69a..24996b8191 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "data/data_auto_download.h" +#include "data/notify/data_peer_notify_settings.h" +#include "data/data_authorization.h" #include "ui/rect_part.h" namespace Support { @@ -146,6 +148,26 @@ public: void setLastNonPremiumLimitUpload(TimeId when) { _lastNonPremiumLimitUpload = when; } + void setRingtoneVolume( + Data::DefaultNotify defaultNotify, + ushort volume); + [[nodiscard]] ushort ringtoneVolume( + Data::DefaultNotify defaultNotify) const; + void setRingtoneVolume( + PeerId peerId, + MsgId topicRootId, + PeerId monoforumPeerId, + ushort volume); + [[nodiscard]] ushort ringtoneVolume( + PeerId peerId, + MsgId topicRootId, + PeerId monoforumPeerId) const; + + void markTranscriptionAsRated(uint64 transcriptionId); + [[nodiscard]] bool isTranscriptionRated(uint64 transcriptionId) const; + + void setUnreviewed(std::vector auths); + [[nodiscard]] const std::vector &unreviewed() const; private: static constexpr auto kDefaultSupportChatsLimitSlice = 7 * 24 * 60 * 60; @@ -171,6 +193,8 @@ private: rpl::variable _skipArchiveInSearch = false; base::flat_map _hiddenPinnedMessages; base::flat_set _verticalSubsectionTabs; + base::flat_map _ringtoneDefaultVolumes; + base::flat_map _ringtoneVolumes; bool _dialogsFiltersEnabled = false; int _photoEditorHintShowsCount = 0; std::vector _mutePeriods; @@ -185,6 +209,10 @@ private: = kDefaultSupportChatsLimitSlice; rpl::variable _supportAllSearchResults = false; + base::flat_set _ratedTranscriptions; + + std::vector _unreviewed; + }; } // namespace Main diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index e7ed0c618d..773c4fb3a6 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -259,7 +259,7 @@ void MainWindow::setupMain( const auto animated = _intro || (_passcodeLock && !Core::App().passcodeLocked()); const auto weakAnimatedLayer = (_main && _layer && !_passcodeLock) - ? Ui::MakeWeak(_layer.get()) + ? base::make_weak(_layer.get()) : nullptr; if (weakAnimatedLayer) { Assert(!animated); @@ -288,7 +288,7 @@ void MainWindow::setupMain( Core::App().checkStartUrl(); } fixOrder(); - if (const auto strong = weakAnimatedLayer.data()) { + if (const auto strong = weakAnimatedLayer.get()) { strong->hideAllAnimatedRun(); } } diff --git a/Telegram/SourceFiles/media/audio/media_audio_track.cpp b/Telegram/SourceFiles/media/audio/media_audio_track.cpp index bbf2ea5bd7..4cef903464 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_track.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_track.cpp @@ -142,7 +142,7 @@ void Track::fillFromFile(const QString &filePath) { } } -void Track::playWithLooping(bool looping) { +void Track::playWithLooping(bool looping, float64 volumeOverride) { _active = true; if (failed() || _samples.empty()) { finish(); @@ -152,7 +152,12 @@ void Track::playWithLooping(bool looping) { alSourceStop(_alSource); _looping = looping; alSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0); - alSourcef(_alSource, AL_GAIN, _volume); + alSourcef( + _alSource, + AL_GAIN, + (volumeOverride > 0) + ? volumeOverride + : float64(Core::App().settings().notificationsVolume()) / 100.); alSourcePlay(_alSource); _instance->trackStarted(this); } diff --git a/Telegram/SourceFiles/media/audio/media_audio_track.h b/Telegram/SourceFiles/media/audio/media_audio_track.h index 5016a19c93..5b37fb5e9f 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_track.h +++ b/Telegram/SourceFiles/media/audio/media_audio_track.h @@ -30,11 +30,11 @@ public: void fillFromFile(const Core::FileLocation &location); void fillFromFile(const QString &filePath); - void playOnce() { - playWithLooping(false); + void playOnce(float64 volumeOverride = -1) { + playWithLooping(false, volumeOverride); } - void playInLoop() { - playWithLooping(true); + void playInLoop(float64 volumeOverride = -1) { + playWithLooping(true, volumeOverride); } bool isLooping() const { @@ -61,7 +61,7 @@ public: private: void finish(); void ensureSourceCreated(); - void playWithLooping(bool looping); + void playWithLooping(bool looping, float64 volumeOverride); not_null _instance; diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index c73c34e5ef..51e30ce824 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -88,6 +88,7 @@ struct Instance::ShuffleData { PeerId monoforumPeerId = 0; History *migrated = nullptr; bool scheduled = false; + bool savedMusic = false; int indexInPlayedIds = 0; bool allLoaded = false; rpl::lifetime nextSliceLifetime; @@ -387,6 +388,8 @@ void Instance::validatePlaylist(not_null data) { const auto sharedMediaViewer = (key->topicRootId == SparseIdsMergedSlice::kScheduledTopicId) ? SharedScheduledMediaViewer + : (key->topicRootId == SparseIdsMergedSlice::kSavedMusicTopicId) + ? SavedMusicMediaViewer : SharedMediaMergedViewer; sharedMediaViewer( &data->history->session(), @@ -413,7 +416,10 @@ auto Instance::playlistKey(not_null data) const return {}; } const auto item = data->history->owner().message(contextId); - if (!item || (!item->isRegular() && !item->isScheduled())) { + if (!item + || (!item->isRegular() + && !item->isScheduled() + && !item->isSavedMusicItem())) { return {}; } @@ -424,6 +430,8 @@ auto Instance::playlistKey(not_null data) const data->history->peer->id, (item->isScheduled() ? SparseIdsMergedSlice::kScheduledTopicId + : item->isSavedMusicItem() + ? SparseIdsMergedSlice::kSavedMusicTopicId : data->topicRootId), data->monoforumPeerId, data->migrated ? data->migrated->peer->id : 0, @@ -908,14 +916,18 @@ void Instance::validateShuffleData(not_null data) { const auto key = playlistKey(data); const auto scheduled = key && (key->topicRootId == SparseIdsMergedSlice::kScheduledTopicId); + const auto savedMusic = key + && (key->topicRootId == SparseIdsMergedSlice::kSavedMusicTopicId); if (raw->history != data->history || raw->topicRootId != data->topicRootId || raw->monoforumPeerId != data->monoforumPeerId || raw->migrated != data->migrated - || raw->scheduled != scheduled) { + || raw->scheduled != scheduled + || raw->savedMusic != savedMusic) { raw->history = data->history; raw->migrated = data->migrated; raw->scheduled = scheduled; + raw->savedMusic = savedMusic; raw->nextSliceLifetime.destroy(); raw->allLoaded = false; raw->playlist.clear(); diff --git a/Telegram/SourceFiles/media/player/media_player_panel.cpp b/Telegram/SourceFiles/media/player/media_player_panel.cpp index 798d9b0153..c7610f95ad 100644 --- a/Telegram/SourceFiles/media/player/media_player_panel.cpp +++ b/Telegram/SourceFiles/media/player/media_player_panel.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/player/media_player_instance.h" #include "info/media/info_media_list_widget.h" +#include "info/saved/info_saved_music_widget.h" #include "history/history.h" #include "history/history_item.h" #include "data/data_session.h" @@ -99,7 +100,7 @@ void Panel::updateControlsGeometry() { if (scrollHeight > 0) { _scroll->setGeometryToRight(contentRight(), scrollTop, width, scrollHeight); } - if (const auto widget = static_cast(_scroll->widget())) { + if (const auto widget = static_cast(_scroll->widget())) { widget->resizeToWidth(width); } } @@ -227,6 +228,7 @@ void Panel::ensureCreated() { void Panel::refreshList() { const auto current = instance()->current(AudioMsgId::Type::Song); const auto contextId = current.contextId(); + auto savedMusicItem = false; const auto peer = [&]() -> PeerData* { if (const auto document = current.audio()) { if (&document->session() != &session()) { @@ -241,9 +243,12 @@ void Panel::refreshList() { const auto document = media ? media->document() : nullptr; if (!document || !document->isSharedMediaMusic() - || (!item->isRegular() && !item->isScheduled())) { + || (!item->isRegular() + && !item->isScheduled() + && !item->isSavedMusicItem())) { return nullptr; } + savedMusicItem = item->isSavedMusicItem(); const auto result = item->history()->peer; if (const auto migrated = result->migrateTo()) { return migrated; @@ -251,13 +256,19 @@ void Panel::refreshList() { return result; }(); const auto migrated = peer ? peer->migrateFrom() : nullptr; - if (_listPeer != peer || _listMigratedPeer != migrated) { + const auto listPeer = savedMusicItem ? nullptr : peer; + const auto listMusicPeer = savedMusicItem ? peer : nullptr; + const auto listMigratedPeer = savedMusicItem ? nullptr : migrated; + if (_listPeer != listPeer + || _listMusicPeer != listMusicPeer + || _listMigratedPeer != listMigratedPeer) { _scroll->takeWidget().destroy(); - _listPeer = _listMigratedPeer = nullptr; + _listPeer = _listMusicPeer = _listMigratedPeer = nullptr; } - if (peer && !_listPeer) { - _listPeer = peer; - _listMigratedPeer = migrated; + if ((listPeer && !_listPeer) || (listMusicPeer && !_listMusicPeer)) { + _listPeer = listPeer; + _listMusicPeer = listMusicPeer; + _listMigratedPeer = listMigratedPeer; auto list = object_ptr(this, infoController()); const auto weak = _scroll->setOwnedWidget(std::move(list)); @@ -292,10 +303,14 @@ void Panel::refreshList() { weak->setVisibleTopBottom(top, bottom); }, weak->lifetime()); - auto memento = Info::Media::Memento( + auto musicMemento = Info::Saved::MusicMemento(peer); + auto mediaMemento = Info::Media::Memento( peer, migratedPeerId(), - section().mediaType()); + (listMusicPeer + ? Storage::SharedMediaType::MusicFile + : section().mediaType())); + auto &memento = listMusicPeer ? musicMemento.media() : mediaMemento; memento.setAroundId(contextId); memento.setIdsLimit(kPlaylistIdsLimit); memento.setScrollTopItem({ contextId, peer->session().uniqueId() }); @@ -308,12 +323,14 @@ void Panel::performDestroy() { if (!_scroll->widget()) return; _scroll->takeWidget().destroy(); - _listPeer = _listMigratedPeer = nullptr; + _listPeer = _listMusicPeer = _listMigratedPeer = nullptr; _refreshListLifetime.destroy(); } Info::Key Panel::key() const { - return Info::Key(_listPeer); + return _listMusicPeer + ? Info::Key(Info::Saved::MusicTag{ _listMusicPeer }) + : Info::Key(_listPeer); } PeerData *Panel::migrated() const { @@ -321,7 +338,9 @@ PeerData *Panel::migrated() const { } Info::Section Panel::section() const { - return Info::Section(Info::Section::MediaType::MusicFile); + return _listMusicPeer + ? Info::Section(Info::Section::Type::SavedMusic) + : Info::Section(Info::Section::MediaType::MusicFile); } void Panel::startShow() { diff --git a/Telegram/SourceFiles/media/player/media_player_panel.h b/Telegram/SourceFiles/media/player/media_player_panel.h index 9123834cee..c46b499c3a 100644 --- a/Telegram/SourceFiles/media/player/media_player_panel.h +++ b/Telegram/SourceFiles/media/player/media_player_panel.h @@ -101,6 +101,7 @@ private: rpl::lifetime _refreshListLifetime; PeerData *_listPeer = nullptr; + PeerData *_listMusicPeer = nullptr; PeerData *_listMigratedPeer = nullptr; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 216eec7f2f..39018ff55e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_stealth.h" #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" +#include "info/stories/info_stories_common.h" #include "settings/settings_credits_graphics.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/report_box_graphics.h" @@ -703,44 +704,25 @@ void Controller::rebuildFromContext( }, [&](StoriesContextPeer) { source = stories.source(peerId); hideSiblings(); - }, [&](StoriesContextSaved) { - if (stories.savedCountKnown(peerId)) { - const auto &saved = stories.saved(peerId); - auto sorted = RespectingPinned(saved); + }, [&](StoriesContextAlbum album) { + const auto known = stories.albumIdsCountKnown(peerId, album.id); + if (known) { + const auto &ids = stories.albumIds(peerId, album.id); + auto sorted = RespectingPinned(ids); const auto i = ranges::find(sorted, id); const auto tillEnd = int(end(sorted) - i); if (tillEnd > 0) { _index = int(i - begin(sorted)); + const auto total = stories.albumIdsCount(peerId, album.id); list = StoriesList{ .peer = peer, - .ids = saved, + .ids = ids, .sorted = std::move(sorted), - .total = stories.savedCount(peerId), + .total = total, }; - if (saved.list.size() < list->total + if (ids.list.size() < list->total && tillEnd < kPreloadStoriesCount) { - stories.savedLoadMore(peerId); - } - } - } - hideSiblings(); - }, [&](StoriesContextArchive) { - if (stories.archiveCountKnown(peerId)) { - const auto &archive = stories.archive(peerId); - auto sorted = RespectingPinned(archive); - const auto i = ranges::find(sorted, id); - const auto tillEnd = int(end(sorted) - i); - if (tillEnd > 0) { - _index = int(i - begin(sorted)); - list = StoriesList{ - .peer = peer, - .ids = archive, - .sorted = std::move(sorted), - .total = stories.archiveCount(peerId), - }; - if (archive.list.size() < list->total - && tillEnd < kPreloadStoriesCount) { - stories.archiveLoadMore(peerId); + stories.albumIdsLoadMore(peerId, album.id); } } } @@ -860,15 +842,10 @@ void Controller::show( v::match(_context.data, [&](Data::StoriesContextSingle) { }, [&](Data::StoriesContextPeer) { subscribeToSource(); - }, [&](Data::StoriesContextSaved) { - stories.savedChanged() | rpl::filter( - rpl::mappers::_1 == storyId.peer - ) | rpl::start_with_next([=] { - rebuildFromContext(peer, storyId); - checkMoveByDelta(); - }, _contextLifetime); - }, [&](Data::StoriesContextArchive) { - stories.archiveChanged( + }, [&](Data::StoriesContextAlbum album) { + const auto key = Data::StoryAlbumIdsKey{ storyId.peer, album.id }; + stories.albumIdsChanged() | rpl::filter( + rpl::mappers::_1 == key ) | rpl::start_with_next([=] { rebuildFromContext(peer, storyId); checkMoveByDelta(); @@ -1601,10 +1578,8 @@ void Controller::loadMoreToList() { const auto peer = shownPeer(); const auto peerId = _shown.peer; auto &stories = peer->owner().stories(); - v::match(_context.data, [&](StoriesContextSaved) { - stories.savedLoadMore(peerId); - }, [&](StoriesContextArchive) { - stories.archiveLoadMore(peerId); + v::match(_context.data, [&](StoriesContextAlbum album) { + stories.albumIdsLoadMore(peerId, album.id); }, [](const auto &) { }); } @@ -1763,7 +1738,10 @@ void Controller::toggleInProfileRequested(bool inProfile) { if (!story || !story->peer()->isSelf()) { return; } - if (!inProfile && v::is(_context.data)) { + if (!inProfile + && v::is(_context.data) + && (v::get(_context.data).id + != Data::kStoriesAlbumIdArchive)) { moveFromShown(); } story->owner().stories().toggleInProfileList( diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index a77c700b25..b2cf0c30fe 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -276,10 +276,7 @@ struct MadePrivacyBadge { not_null owner, PeerData *peer, QString name) { - auto result = Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - st::storiesRepostIcon, - st::storiesRepostIconPadding)); + auto result = Ui::Text::IconEmoji(&st::storiesRepostIcon); if (peer) { result.append(Ui::Text::SingleCustomEmoji( owner->customEmojiManager().peerUserpicEmojiData( @@ -741,9 +738,9 @@ void Header::toggleTooltip(Tooltip type, bool show) { st::storiesInfoTooltipLabel), st::storiesInfoTooltip); const auto tooltip = _tooltip.get(); - const auto weak = QPointer(tooltip); + const auto weak = base::make_weak(tooltip); const auto destroy = [=] { - delete weak.data(); + delete weak.get(); }; tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); tooltip->setHiddenCallback(destroy); diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp index 0335bd9b74..d053ca631a 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp @@ -246,7 +246,7 @@ void RepostView::recountDimensions() { } auto nameFull = TextWithEntities(); - nameFull.append(HistoryView::Reply::PeerEmoji(owner, _sourcePeer)); + nameFull.append(HistoryView::Reply::PeerEmoji(_sourcePeer)); nameFull.append(name); auto context = Core::TextContext({ .session = &_story->session(), diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp index b64de9eafd..0f6dc3569c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -327,8 +327,8 @@ struct Feature { box->setWidth(st::boxWideWidth); box->setStyle(st::storiesStealthBox); box->addRow(MakeLogo(box)); - box->addRow(MakeTitle(box)); - box->addRow(MakeAbout(box, data->state.value())); + box->addRow(MakeTitle(box), style::al_top); + box->addRow(MakeAbout(box, data->state.value()), style::al_top); box->addRow(MakeFeature(box, FeaturePast())); box->addRow( MakeFeature(box, FeatureNext()), diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 73fd0fa725..273369f763 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -1075,8 +1075,10 @@ storiesRepostSimpleStyle: QuoteStyle(defaultQuoteStyle) { outline: 0px; radius: 10px; } -storiesRepostIcon: icon {{ "mediaview/mini_repost", windowFg }}; -storiesRepostIconPadding: margins(0px, 4px, 2px, 0px); +storiesRepostIcon: IconEmoji{ + icon: icon {{ "mediaview/mini_repost", windowFg }}; + padding: margins(0px, 4px, 2px, 0px); +} storiesRepostUserpicPadding: margins(0px, 1px, 4px, 0px); mediaviewSponsoredButton: RoundButton(defaultActiveButton) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f6d64f14c6..fdf41a8a4f 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3390,7 +3390,7 @@ void OverlayWidget::refreshCaption() { : Ui::ItemTextDefaultOptions()), context); if (_caption.hasSpoilers()) { - const auto weak = Ui::MakeWeak(widget()); + const auto weak = base::make_weak(widget()); _caption.setSpoilerLinkFilter([=](const ClickContext &context) { return (weak != nullptr); }); @@ -4301,7 +4301,7 @@ void OverlayWidget::initThemePreview() { const auto weakSession = base::make_weak(&_document->session()); const auto path = _document->location().name(); const auto id = _themePreviewId = base::RandomValue(); - const auto weak = Ui::MakeWeak(_widget); + const auto weak = base::make_weak(_widget); crl::async([=, data = std::move(current)]() mutable { auto preview = GeneratePreview( bytes, @@ -6463,7 +6463,7 @@ bool OverlayWidget::handleTouchEvent(not_null e) { if (!_touchPress) { break; } - auto weak = Ui::MakeWeak(_widget); + auto weak = base::make_weak(_widget); if (!_touchMove) { const auto button = _touchRightButton ? Qt::RightButton diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 2044d9caae..2ec83512be 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -109,15 +109,6 @@ public: void show(OpenRequest request); - //void leaveToChildEvent(QEvent *e, QWidget *child) override { - // // e -- from enterEvent() of child TWidget - // updateOverState(Over::None); - //} - //void enterFromChildEvent(QEvent *e, QWidget *child) override { - // // e -- from leaveEvent() of child TWidget - // updateOver(mapFromGlobal(QCursor::pos())); - //} - void activateControls(); void close(); void minimize(); diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index 4cb3c7543f..fc5aebdd1f 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -1019,7 +1019,7 @@ void Pip::handleLeave() { } void Pip::handleMouseMove(QPoint position) { - const auto weak = Ui::MakeWeak(_panel.widget()); + const auto weak = base::make_weak(_panel.widget()); const auto guard = gsl::finally([&] { if (weak) { _panel.handleMouseMove(position); @@ -1097,7 +1097,7 @@ Pip::OverState Pip::ResolveShownOver(OverState state) { } void Pip::handleMousePress(QPoint position, Qt::MouseButton button) { - const auto weak = Ui::MakeWeak(_panel.widget()); + const auto weak = base::make_weak(_panel.widget()); const auto guard = gsl::finally([&] { if (weak) { _panel.handleMousePress(position, button); @@ -1115,7 +1115,7 @@ void Pip::handleMousePress(QPoint position, Qt::MouseButton button) { } void Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) { - const auto weak = Ui::MakeWeak(_panel.widget()); + const auto weak = base::make_weak(_panel.widget()); const auto guard = gsl::finally([&] { if (weak) { _panel.handleMouseRelease(position, button); diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.cpp b/Telegram/SourceFiles/menu/menu_item_download_files.cpp index 57b84210f4..f2646dbfe2 100644 --- a/Telegram/SourceFiles/menu/menu_item_download_files.cpp +++ b/Telegram/SourceFiles/menu/menu_item_download_files.cpp @@ -232,8 +232,8 @@ void AddDownloadFilesAction( std::sort(photos.begin(), photos.end(), [](const auto &a, const auto &b) { return a.second < b.second; }); - const auto done = [weak = Ui::MakeWeak(list)] { - if (const auto strong = weak.data()) { + const auto done = [weak = base::make_weak(list)] { + if (const auto strong = weak.get()) { strong->cancelSelection(); } }; @@ -265,8 +265,8 @@ void AddDownloadFilesAction( std::sort(photos.begin(), photos.end(), [](const auto &a, const auto &b) { return a.second < b.second; }); - const auto done = [weak = Ui::MakeWeak(list)] { - if (const auto strong = weak.data()) { + const auto done = [weak = base::make_weak(list)] { + if (const auto strong = weak.get()) { strong->clearSelected(); } }; diff --git a/Telegram/SourceFiles/menu/menu_item_rate_transcribe.cpp b/Telegram/SourceFiles/menu/menu_item_rate_transcribe.cpp new file mode 100644 index 0000000000..75567c7692 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_rate_transcribe.cpp @@ -0,0 +1,166 @@ +/* +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 "menu/menu_item_rate_transcribe.h" + +#include "base/call_delayed.h" +#include "lang/lang_keys.h" +#include "ui/rect.h" +#include "ui/toast/toast.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +namespace Menu { +namespace { + +constexpr auto kDuration = crl::time(5000); + +} // namespace + +RateTranscribe::RateTranscribe( + not_null parent, + const style::Menu &st, + Fn rate) +: Ui::Menu::ItemBase(parent, st) +, _dummyAction(Ui::CreateChild(this)) { + setAcceptBoth(true); + + initResizeHook(parent->sizeValue()); + + enableMouseSelecting(); + + const auto content = Ui::CreateChild(this); + + Ui::AddSkip(content); + + const auto label = content->add( + object_ptr( + content, + tr::lng_context_rate_transcription(), + st::boxDividerLabel), + style::margins(), + style::al_top); + setMinWidth( + label->st().style.font->width( + tr::lng_context_rate_transcription(tr::now))); + widthValue() | rpl::start_with_next([=](int w) { + content->resizeToWidth(parentWidget()->width()); + }, content->lifetime()); + Ui::AddSkip(content); + + // const auto leftButton = Ui::CreateChild( + // this, + // st::menuTranscribeDummyButton); + // const auto rightButton = Ui::CreateChild( + // this, + // st::menuTranscribeDummyButton); + const auto leftButton = Ui::CreateSimpleCircleButton( + this, + st::defaultRippleAnimation); + { + leftButton->resize(Size(st::menuTranscribeDummyButton.width)); + const auto label = Ui::CreateChild( + leftButton, + QString::fromUtf8("\U0001F44D")); + label->setAttribute(Qt::WA_TransparentForMouseEvents, true); + leftButton->sizeValue() | rpl::start_with_next([=](QSize s) { + label->moveToLeft( + (s.width() - label->width()) / 2, + (s.height() - label->height()) / 2); + }, label->lifetime()); + } + const auto rightButton = Ui::CreateSimpleCircleButton( + this, + st::defaultRippleAnimation); + { + rightButton->resize(Size(st::menuTranscribeDummyButton.width)); + const auto label = Ui::CreateChild( + rightButton, + QString::fromUtf8("\U0001F44E")); + label->setAttribute(Qt::WA_TransparentForMouseEvents, true); + rightButton->sizeValue() | rpl::start_with_next([=](QSize s) { + label->moveToLeft( + (s.width() - label->width()) / 2, + (s.height() - label->height()) / 2); + }, label->lifetime()); + } + { + const auto showToast = [=, + weak = base::make_weak(parent->parentWidget())]{ + if (const auto strong = weak.get()) { + base::call_delayed( + st::universalDuration * 1.1, + crl::guard(strong, [=] { + Ui::Toast::Show(strong->window(), { + .text = tr::lng_toast_sent_rate_transcription( + tr::now, + TextWithEntities::Simple), + .duration = kDuration, + }); + })); + } + }; + const auto hideMenu = [=, weak = base::make_weak(parent)] { + if (const auto strong = weak.get()) { + base::call_delayed( + st::universalDuration, + crl::guard(strong, [=] { strong->hideMenu(false); })); + } + }; + leftButton->setClickedCallback([=] { + rate(true); + showToast(); + hideMenu(); + }); + rightButton->setClickedCallback([=] { + rate(false); + showToast(); + hideMenu(); + }); + } + _desiredHeight = rect::m::sum::v(st::menuTranscribeItemPadding) + + st::menuTranscribeDummyButton.height + + label->st().style.font->height; + rpl::combine( + content->geometryValue(), + label->geometryValue() + ) | rpl::start_with_next([=]( + const QRect &contentRect, + const QRect &labelRect) { + leftButton->moveToLeft( + labelRect.x(), + rect::bottom(contentRect)); + rightButton->moveToLeft( + rect::right(labelRect) - rightButton->width(), + rect::bottom(contentRect)); + _desiredHeight = rect::m::sum::v(st::menuTranscribeItemPadding) + + leftButton->height() + + labelRect.height(); + }, leftButton->lifetime()); + leftButton->show(); + rightButton->show(); +} + +not_null RateTranscribe::action() const { + return _dummyAction; +} + +bool RateTranscribe::isEnabled() const { + return true; +} + +int RateTranscribe::contentHeight() const { + return _desiredHeight; +} + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_item_rate_transcribe.h b/Telegram/SourceFiles/menu/menu_item_rate_transcribe.h new file mode 100644 index 0000000000..1a147c2f50 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_rate_transcribe.h @@ -0,0 +1,41 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/widgets/menu/menu_item_base.h" + +class HistoryItem; + +namespace Ui { +class PopupMenu; +} // namespace Ui + +namespace Menu { + +class RateTranscribe final : public Ui::Menu::ItemBase { +public: + RateTranscribe( + not_null parent, + const style::Menu &st, + Fn rate); + + not_null action() const override; + bool isEnabled() const override; + +protected: + int contentHeight() const override; + +private: + int _desiredHeight = 0; + not_null _dummyAction; + +}; + +bool HasRateTranscribeItem(not_null); + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp new file mode 100644 index 0000000000..95ce62c335 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.cpp @@ -0,0 +1,46 @@ +/* +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 "menu/menu_item_rate_transcribe_session.h" + +#include "api/api_transcribes.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/rect.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +namespace Menu { + +Fn RateTranscribeCallbackFactory( + not_null item) { + return [item](bool good) { + item->history()->peer->owner().session().api().transcribes().rate( + item, + good); + }; +} + +bool HasRateTranscribeItem(not_null item) { + const auto &peer = item->history()->peer; + if (!peer->owner().session().api().transcribes().entry( + item).result.isEmpty()) { + return !peer->owner().session().api().transcribes().isRated(item); + } + return false; +} + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.h b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.h new file mode 100644 index 0000000000..92ce16aeb2 --- /dev/null +++ b/Telegram/SourceFiles/menu/menu_item_rate_transcribe_session.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 + +class HistoryItem; + +namespace Menu { + +[[nodiscard]] Fn RateTranscribeCallbackFactory( + not_null); + +[[nodiscard]] bool HasRateTranscribeItem(not_null); + +} // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_mute.cpp b/Telegram/SourceFiles/menu/menu_mute.cpp index 661ef06727..caf5964c18 100644 --- a/Telegram/SourceFiles/menu/menu_mute.cpp +++ b/Telegram/SourceFiles/menu/menu_mute.cpp @@ -253,6 +253,7 @@ Descriptor ThreadDescriptor(not_null thread) { .currentSound = currentSound, .updateSound = updateSound, .updateMutePeriod = updateMutePeriod, + .volumeController = Data::ThreadRingtonesVolumeController(thread), }; } @@ -290,6 +291,7 @@ Descriptor DefaultDescriptor( .currentSound = currentSound, .updateSound = updateSound, .updateMutePeriod = updateMutePeriod, + .volumeController = DefaultRingtonesVolumeController(session, type), }; } @@ -304,7 +306,8 @@ void FillMuteMenu( RingtonesBox, session, *currentSound, - descriptor.updateSound)); + descriptor.updateSound, + descriptor.volumeController)); } }; menu->addAction( diff --git a/Telegram/SourceFiles/menu/menu_mute.h b/Telegram/SourceFiles/menu/menu_mute.h index a6d8186686..9599de209e 100644 --- a/Telegram/SourceFiles/menu/menu_mute.h +++ b/Telegram/SourceFiles/menu/menu_mute.h @@ -7,10 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/notify/data_peer_notify_volume.h" // VolumeController + namespace Data { class Thread; struct NotifySound; -enum class DefaultNotify; +enum class DefaultNotify : uint8_t; +struct VolumeController; } // namespace Data namespace Main { @@ -31,6 +34,7 @@ struct Descriptor { Fn()> currentSound; Fn updateSound; Fn updateMutePeriod; + Data::VolumeController volumeController; }; [[nodiscard]] Descriptor ThreadDescriptor(not_null thread); diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index fa35fdf1bd..a6510d6990 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -593,7 +593,7 @@ void EffectPreview::toggle(bool shown) { Fn DefaultCallback( std::shared_ptr show, Fn send) { - const auto guard = Ui::MakeWeak(show->toastParent()); + const auto guard = base::make_weak(show->toastParent()); return [=](Action action, Details details) { if (action.type == ActionType::Send) { send(action.options); @@ -605,9 +605,9 @@ Fn DefaultCallback( details, send, action.options); - const auto weak = Ui::MakeWeak(box.data()); + const auto weak = base::make_weak(box.data()); show->showBox(std::move(box)); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->setCloseByOutsideClick(false); } }; @@ -622,7 +622,7 @@ FillMenuResult AttachSendMenuEffect( Expects(show != nullptr); using namespace HistoryView::Reactions; - const auto effect = std::make_shared>(); + const auto effect = std::make_shared>(); const auto position = desiredPositionOverride.value_or(QCursor::pos()); const auto selector = (show && details.effectAllowed) ? AttachSelectorToMenu( @@ -649,13 +649,13 @@ FillMenuResult AttachSendMenuEffect( const auto &effects = reactions.list(Data::Reactions::Type::Effects); const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id); if (i != end(effects)) { - if (const auto strong = effect->data()) { + if (const auto strong = effect->get()) { strong->hideAnimated(); } - const auto weak = Ui::MakeWeak(menu); + const auto weak = base::make_weak(menu); const auto done = [=] { - delete effect->data(); - if (const auto strong = weak.data()) { + delete effect->get(); + if (const auto strong = weak.get()) { strong->hideMenu(true); } }; diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index 2073239170..f3637a5149 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -25,7 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/menu/menu_multiline_action.h" @@ -57,6 +56,7 @@ void AboutBox( constexpr auto kUrl = "https://promote.telegram.org"_cs; box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); const auto isChannel = (phrases == SponsoredPhrases::Channel); const auto isSearch = (phrases == SponsoredPhrases::Search); @@ -74,9 +74,8 @@ void AboutBox( const auto rect = Rect(icon.size() * 1.4); auto owned = object_ptr(content); owned->resize(rect.size()); - const auto widget = box->addRow(object_ptr>( - content, - std::move(owned)))->entity(); + owned->setNaturalWidth(rect.width()); + const auto widget = box->addRow(std::move(owned), style::al_top); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); @@ -89,19 +88,19 @@ void AboutBox( } Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( - content, + box->addRow( object_ptr( content, tr::lng_sponsored_menu_revenued_about(), - st::boxTitle))); + st::boxTitle), + style::al_top); Ui::AddSkip(content); - box->addRow(object_ptr>( - content, + box->addRow( object_ptr( content, tr::lng_sponsored_revenued_subtitle(), - st::channelEarnLearnDescription))); + st::channelEarnLearnDescription), + style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); { @@ -211,12 +210,11 @@ void AboutBox( Ui::AddSkip(content); { box->addRow( - object_ptr>( + object_ptr( content, - object_ptr( - content, - tr::lng_sponsored_revenued_footer_title(), - st::boxTitle))); + tr::lng_sponsored_revenued_footer_title(), + st::boxTitle), + style::al_top); } Ui::AddSkip(content); { @@ -224,7 +222,7 @@ void AboutBox( const auto available = box->width() - rect::m::sum::h(st::boxRowPadding); box->addRow( - Ui::CreateLabelWithCustomEmoji( + object_ptr( content, (isChannel ? tr::lng_sponsored_revenued_footer_description @@ -240,7 +238,6 @@ void AboutBox( return Ui::Text::Link(std::move(t), kUrl.utf16()); }), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), st::channelEarnLearnDescription))->resizeToWidth(available); } Ui::AddSkip(content); diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 6f092df424..bd8dddb55c 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/137.0.0.0 Safari/537.36"); + "Chrome/139.0.0.0 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.cpp b/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.cpp index 5bb4d5d205..89ad7d9625 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.cpp @@ -76,7 +76,7 @@ auto ConcurrentSender::with_instance(Method &&method) weak = _weak, method = std::forward(method) ]() mutable { - if (const auto instance = weak.data()) { + if (const auto instance = weak.get()) { std::move(method)(instance); } }); @@ -136,7 +136,7 @@ mtpRequestId ConcurrentSender::RequestBuilder::send() { } ConcurrentSender::ConcurrentSender( - QPointer weak, + base::weak_qptr weak, Fn)> runner) : _weak(weak) , _runner(runner) { diff --git a/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.h b/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.h index 47daaf7a6c..f96d0fe3ac 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.h +++ b/Telegram/SourceFiles/mtproto/mtproto_concurrent_sender.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/bytes.h" #include "base/weak_ptr.h" +#include "base/weak_qptr.h" #include "base/flat_map.h" #include "mtproto/core_types.h" #include "mtproto/details/mtproto_serialized_request.h" @@ -83,7 +84,7 @@ class ConcurrentSender : public base::has_weak_ptr { public: ConcurrentSender( - QPointer weak, + base::weak_qptr weak, Fn)> runner); template @@ -184,7 +185,7 @@ private: void senderRequestCancelAll(); void senderRequestDetach(mtpRequestId requestId); - const QPointer _weak; + const base::weak_qptr _weak; const Fn)> _runner; base::flat_map _requests; diff --git a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp index 8ed267514c..e6d06a5666 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp @@ -153,7 +153,7 @@ ProxyData::Status ProxyData::status() const { } bool ProxyData::supportsCalls() const { - return (type == Type::Socks5); + return false;// (type == Type::Socks5); } bool ProxyData::tryCustomResolve() const { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index c6e4c22b2c..4803ec36b5 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -104,7 +104,7 @@ channel#fe685355 flags:# creator:flags.0?true left:flags.2?true broadcast:flags. channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#e07429de flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long = ChatFull; +channelFull#e4e0b29d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -167,7 +167,7 @@ messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; -messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; +messageActionSetChatTheme#b91bbd3a theme:ChatTheme = MessageAction; messageActionChatJoinedByRequest#ebbca3cb = MessageAction; messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; @@ -185,8 +185,8 @@ messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector = MessageAction; @@ -215,7 +215,7 @@ geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radiu auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; auth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode; -auth.sentCodePaymentRequired#d7cef980 store_product:string phone_code_hash:string = auth.SentCode; +auth.sentCodePaymentRequired#d7a2fcf9 store_product:string phone_code_hash:string support_email_address:string support_email_subject:string = auth.SentCode; auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; @@ -248,7 +248,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#99e78045 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 display_gifts_button:flags2.16?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 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 starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings = UserFull; +userFull#c577b5ad 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 display_gifts_button:flags2.16?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:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights 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 starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -269,7 +269,7 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector< messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; +messages.messagesSlice#762b263d flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int search_flood:flags.3?SearchPostsFlood messages:Vector chats:Vector users:Vector = messages.Messages; messages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; messages.messagesNotModified#74535f21 count:int = messages.Messages; @@ -1289,6 +1289,7 @@ webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settin webPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute; webPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags.1?true stickers:Vector = WebPageAttribute; webPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute; +webPageAttributeStarGiftCollection#31cad303 icons:Vector = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -1416,6 +1417,12 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; +chatTheme#c3dffc04 emoticon:string = ChatTheme; +chatThemeUniqueGift#3458f9c8 gift:StarGift theme_settings:Vector = ChatTheme; + +account.chatThemesNotModified#e011e1c4 = account.ChatThemes; +account.chatThemes#16484857 flags:# hash:long themes:Vector chats:Vector users:Vector next_offset:flags.0?int = account.ChatThemes; + sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage; messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; @@ -1502,7 +1509,8 @@ inputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true inputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice; inputInvoicePremiumGiftStars#dabab2ef flags:# user_id:InputUser months:int message:flags.0?TextWithEntities = InputInvoice; inputInvoiceBusinessBotTransferStars#f4997e42 bot:InputUser stars:long = InputInvoice; -inputInvoiceStarGiftResale#63cbc38c slug:string to_id:InputPeer = InputInvoice; +inputInvoiceStarGiftResale#c39f5324 flags:# ton:flags.0?true slug:string to_id:InputPeer = InputInvoice; +inputInvoiceStarGiftPrepaidUpgrade#9a0b48b8 peer:InputPeer hash:string = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1514,7 +1522,7 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#fb790393 flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long message:flags.1?TextWithEntities = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; -inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStarsTopup#f9a2a6cb flags:# stars:long currency:string amount:long spend_purpose_peer:flags.0?InputPeer = InputStorePaymentPurpose; inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose; inputStorePaymentAuthCode#9bb2636d flags:# restore:flags.0?true phone_number:string phone_code_hash:string currency:string amount:long = InputStorePaymentPurpose; @@ -1634,7 +1642,7 @@ storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_co storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#79b26a24 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; +storyItem#edf164f1 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction albums:flags.19?Vector = StoryItem; stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector chats:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; @@ -1852,7 +1860,7 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount 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 starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; +starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true stargift_prepaid_upgrade:flags.25?true id:string amount:StarsAmount 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 starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount 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; @@ -1890,8 +1898,8 @@ 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#7f853c12 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 availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer = StarGift; -starGiftUnique#f63778ae flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift; +starGift#80ac53c3 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int locked_until_date:flags.9?int = StarGift; +starGiftUnique#1befe865 flags:# require_premium:flags.6?true resale_ton_only:flags.7?true theme_available:flags.9?true id:long gift_id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector released_by:flags.5?Peer value_amount:flags.8?long value_currency:flags.8?string theme_peer:flags.10?Peer = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#2ed82995 hash:int gifts:Vector chats:Vector users:Vector = payments.StarGifts; @@ -1936,11 +1944,11 @@ payments.starGiftUpgradePreview#167bd90b sample_attributes:Vector = users.Users; users.usersSlice#315a4974 count:int users:Vector = users.Users; -payments.uniqueStarGift#caa2f60b gift:StarGift users:Vector = payments.UniqueStarGift; +payments.uniqueStarGift#416c56e8 gift:StarGift chats:Vector users:Vector = payments.UniqueStarGift; -messages.webPagePreview#b53e8b21 media:MessageMedia users:Vector = messages.WebPagePreview; +messages.webPagePreview#8c9a88ac media:MessageMedia chats:Vector users:Vector = messages.WebPagePreview; -savedStarGift#dfda0499 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int = SavedStarGift; +savedStarGift#19a9b572 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true upgrade_separate:flags.17?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector prepaid_upgrade_hash:flags.16?string = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; @@ -1989,6 +1997,44 @@ todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; +starsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating; + +starGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection; + +payments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections; +payments.starGiftCollections#8a2932f3 collections:Vector = payments.StarGiftCollections; + +storyAlbum#9325705a flags:# album_id:int title:string icon_photo:flags.0?Photo icon_video:flags.1?Document = StoryAlbum; + +stories.albumsNotModified#564edaeb = stories.Albums; +stories.albums#c3987a3a hash:long albums:Vector = stories.Albums; + +searchPostsFlood#3e0b5b6a flags:# query_is_free:flags.0?true total_daily:int remains:int wait_till:flags.1?int stars_amount:long = SearchPostsFlood; + +payments.uniqueStarGiftValueInfo#512fe446 flags:# last_sale_on_fragment:flags.1?true value_is_average:flags.6?true currency:string value:long initial_sale_date:int initial_sale_stars:long initial_sale_price:long last_sale_date:flags.0?int last_sale_price:flags.0?long floor_price:flags.2?long average_price:flags.3?long listed_count:flags.4?int fragment_listed_count:flags.5?int fragment_listed_url:flags.5?string = payments.UniqueStarGiftValueInfo; + +profileTabPosts#b98cd696 = ProfileTab; +profileTabGifts#4d4bd46a = ProfileTab; +profileTabMedia#72c64955 = ProfileTab; +profileTabFiles#ab339c00 = ProfileTab; +profileTabMusic#9f27d26e = ProfileTab; +profileTabVoice#e477092e = ProfileTab; +profileTabLinks#d3656499 = ProfileTab; +profileTabGifs#a2c0f695 = ProfileTab; + +users.savedMusicNotModified#e3878aa4 count:int = users.SavedMusic; +users.savedMusic#34a2f297 count:int documents:Vector = users.SavedMusic; + +account.savedMusicIdsNotModified#4fc81d6e = account.SavedMusicIds; +account.savedMusicIds#998d6636 ids:Vector = account.SavedMusicIds; + +payments.checkCanSendGiftResultOk#374fa7ad = payments.CheckCanSendGiftResult; +payments.checkCanSendGiftResultFail#d5e58274 reason:TextWithEntities = payments.CheckCanSendGiftResult; + +inputChatThemeEmpty#83268483 = InputChatTheme; +inputChatTheme#c93de95c emoticon:string = InputChatTheme; +inputChatThemeUniqueGift#87e5dfe4 slug:string = InputChatTheme; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2142,11 +2188,17 @@ account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = R account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue; account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool; +account.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool; +account.saveMusic#b26732a9 flags:# unsave:flags.0?true id:InputDocument after_id:flags.1?InputDocument = Bool; +account.getSavedMusicIds#e09d5faf hash:long = account.SavedMusicIds; +account.getUniqueGiftChatThemes#fe74ef9f offset:int limit:int hash:long = account.ChatThemes; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; users.getRequirementsToContact#d89a83a3 id:Vector = Vector; +users.getSavedMusic#788d7fe3 id:InputUser offset:int limit:int hash:long = users.SavedMusic; +users.getSavedMusicByID#7573a4e9 id:InputUser documents:Vector = users.SavedMusic; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; @@ -2316,7 +2368,7 @@ messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithI messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true subscription_expired:flags.3?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; -messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; +messages.setChatTheme#81202c9 peer:InputPeer theme:InputChatTheme = Updates; messages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector; messages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; messages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; @@ -2512,10 +2564,12 @@ channels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatu channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates; channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; 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; +channels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?string offset_rate:int offset_peer:InputPeer offset_id:int limit:int allow_paid_stars:flags.2?long = messages.Messages; channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; +channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; +channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2590,14 +2644,21 @@ payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgr payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates; payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates; payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; -payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts; +payments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_unique:flags.4?true sort_by_value:flags.5?true exclude_upgradable:flags.7?true exclude_unupgradable:flags.8?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts; payments.getSavedStarGift#b455a106 stargift:Vector = payments.SavedStarGifts; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; payments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool; payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector offset:string limit:int = payments.ResaleStarGifts; -payments.updateStarGiftPrice#3baea4e1 stargift:InputSavedStarGift resell_stars:long = Updates; +payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates; +payments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector = StarGiftCollection; +payments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector add_stargift:flags.2?Vector order:flags.3?Vector = StarGiftCollection; +payments.reorderStarGiftCollections#c32af4cc peer:InputPeer order:Vector = Bool; +payments.deleteStarGiftCollection#ad5648e8 peer:InputPeer collection_id:int = Bool; +payments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections; +payments.getUniqueStarGiftValueInfo#4365af6b slug:string = payments.UniqueStarGiftValueInfo; +payments.checkCanSendGift#c0c4edc9 gift_id:long = payments.CheckCanSendGiftResult; 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; @@ -2678,7 +2739,7 @@ chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector

= Updates; stories.canSendStory#30eb63f0 peer:InputPeer = stories.CanSendStoryCount; -stories.sendStory#e4e6694b flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int = Updates; +stories.sendStory#737fc2ec flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int albums:flags.8?Vector = Updates; stories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#ae59db5f peer:InputPeer id:Vector = Vector; stories.togglePinned#9a75a1ef peer:InputPeer id:Vector pinned:Bool = Vector; @@ -2703,6 +2764,12 @@ 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#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer offset:string limit:int = stories.FoundStories; +stories.createAlbum#a36396e5 peer:InputPeer title:string stories:Vector = StoryAlbum; +stories.updateAlbum#5e5259b6 flags:# peer:InputPeer album_id:int title:flags.0?string delete_stories:flags.1?Vector add_stories:flags.2?Vector order:flags.3?Vector = StoryAlbum; +stories.reorderAlbums#8535fbd9 peer:InputPeer order:Vector = Bool; +stories.deleteAlbum#8d3456d0 peer:InputPeer album_id:int = Bool; +stories.getAlbums#25b3eac7 peer:InputPeer hash:long = stories.Albums; +stories.getAlbumStories#ac806d61 peer:InputPeer album_id:int offset:int limit:int = stories.Stories; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; @@ -2720,4 +2787,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 209 +// LAYER 214 diff --git a/Telegram/SourceFiles/overview/overview_checkbox.cpp b/Telegram/SourceFiles/overview/overview_checkbox.cpp new file mode 100644 index 0000000000..9722aa21fe --- /dev/null +++ b/Telegram/SourceFiles/overview/overview_checkbox.cpp @@ -0,0 +1,60 @@ +/* +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 "overview/overview_checkbox.h" + +#include "styles/style_overview.h" +#include "styles/style_widgets.h" + +namespace Overview::Layout { + +void Checkbox::paint( + QPainter &p, + QPoint position, + int outerWidth, + bool selected, + bool selecting) { + _check.setDisplayInactive(selecting); + _check.setChecked(selected); + const auto pression = _pression.value((_active && _pressed) ? 1. : 0.); + const auto scale = 1. - (1. - st::overviewCheckPressedSize) * pression; + _check.paint(p, position.x(), position.y(), outerWidth, scale); +} + +void Checkbox::setActive(bool active) { + _active = active; + if (_pressed) { + startAnimation(); + } +} + +void Checkbox::setPressed(bool pressed) { + _pressed = pressed; + if (_active) { + startAnimation(); + } +} + +void Checkbox::setChecked(bool checked, anim::type animated) { + _check.setChecked(checked, animated); +} + +void Checkbox::startAnimation() { + const auto showPressed = (_pressed && _active); + _pression.start( + _updateCallback, + showPressed ? 0. : 1., + showPressed ? 1. : 0., + st::overviewCheck.duration); +} + +void Checkbox::finishAnimating() { + _pression.stop(); + _check.finishAnimating(); +} + +} // namespace Overview::Layout diff --git a/Telegram/SourceFiles/overview/overview_checkbox.h b/Telegram/SourceFiles/overview/overview_checkbox.h new file mode 100644 index 0000000000..2da919e232 --- /dev/null +++ b/Telegram/SourceFiles/overview/overview_checkbox.h @@ -0,0 +1,54 @@ +/* +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/effects/round_checkbox.h" + +namespace style { +struct RoundCheckbox; +} // namespace style + +namespace Overview::Layout { + +class Checkbox { +public: + template + Checkbox(UpdateCallback callback, const style::RoundCheckbox &st) + : _updateCallback(callback) + , _check(st, _updateCallback) { + } + + void paint( + QPainter &p, + QPoint position, + int outerWidth, + bool selected, + bool selecting); + + void setActive(bool active); + void setPressed(bool pressed); + void setChecked(bool checked, anim::type animated = anim::type::normal); + void finishAnimating(); + + void invalidateCache() { + _check.invalidateCache(); + } + +private: + void startAnimation(); + + Fn _updateCallback; + Ui::RoundCheckbox _check; + + Ui::Animations::Simple _pression; + bool _active = false; + bool _pressed = false; + +}; + +} // namespace Overview::Layout diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index af3317ca02..7f73fddbc9 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "overview/overview_layout.h" +#include "overview/overview_checkbox.h" #include "overview/overview_layout_delegate.h" #include "core/ui_integration.h" // TextContext #include "data/data_document.h" @@ -30,8 +31,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "history/view/history_view_cursor_state.h" +#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover #include "base/unixtime.h" +#include "boxes/sticker_set_box.h" #include "ui/effects/round_checkbox.h" #include "ui/effects/spoiler_mess.h" #include "ui/image/image.h" @@ -47,8 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_overview.h" -namespace Overview { -namespace Layout { +namespace Overview::Layout { namespace { using TextState = HistoryView::TextState; @@ -98,64 +100,29 @@ constexpr auto kStoryRatio = 1.46; } } +void PaintSensitiveTag(Painter &p, QRect r) { + auto text = Ui::Text::String(); + text.setText( + st::semiboldTextStyle, + tr::lng_sensitive_tag(tr::now)); + const auto width = text.maxWidth(); + const auto inner = QRect(0, 0, width, text.minHeight()); + const auto outer = style::centerrect(r, inner.marginsAdded(st::paidTagPadding)); + const auto size = outer.size(); + const auto radius = std::min(size.width(), size.height()) / 2; + auto hq = PainterHighQualityEnabler(p); + + p.setPen(Qt::NoPen); + p.setBrush(st::radialBg); + p.drawRoundedRect(outer, radius, radius); + p.setPen(st::radialFg); + text.draw(p, { + .position = outer.marginsRemoved(st::paidTagPadding).topLeft(), + }); +} + } // namespace -class Checkbox { -public: - template - Checkbox(UpdateCallback callback, const style::RoundCheckbox &st) - : _updateCallback(callback) - , _check(st, _updateCallback) { - } - - void paint(Painter &p, QPoint position, int outerWidth, bool selected, bool selecting); - - void setActive(bool active); - void setPressed(bool pressed); - - void invalidateCache() { - _check.invalidateCache(); - } - -private: - void startAnimation(); - - Fn _updateCallback; - Ui::RoundCheckbox _check; - - Ui::Animations::Simple _pression; - bool _active = false; - bool _pressed = false; - -}; - -void Checkbox::paint(Painter &p, QPoint position, int outerWidth, bool selected, bool selecting) { - _check.setDisplayInactive(selecting); - _check.setChecked(selected); - const auto pression = _pression.value((_active && _pressed) ? 1. : 0.); - const auto masterScale = 1. - (1. - st::overviewCheckPressedSize) * pression; - _check.paint(p, position.x(), position.y(), outerWidth, masterScale); -} - -void Checkbox::setActive(bool active) { - _active = active; - if (_pressed) { - startAnimation(); - } -} - -void Checkbox::setPressed(bool pressed) { - _pressed = pressed; - if (_active) { - startAnimation(); - } -} - -void Checkbox::startAnimation() { - auto showPressed = (_pressed && _active); - _pression.start(_updateCallback, showPressed ? 0. : 1., showPressed ? 1. : 0., st::overviewCheck.duration); -} - ItemBase::ItemBase( not_null delegate, not_null parent) @@ -327,18 +294,24 @@ Photo::Photo( MediaOptions options) : ItemBase(delegate, parent) , _data(photo) -, _link(std::make_shared( - photo, - crl::guard(this, [=](FullMsgId id) { - clearSpoiler(); - delegate->openPhoto(photo, id); - }), - parent->fullId())) -, _spoiler(options.spoiler ? std::make_unique([=] { - delegate->repaintItem(this); -}) : nullptr) -, _pinned(options.pinned) -, _story(options.story) { +, _spoiler((options.spoiler || parent->isMediaSensitive()) + ? std::make_unique([=] { + delegate->repaintItem(this); + }) + : nullptr) +, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0) +, _story(options.story) +, _storyPinned(options.storyPinned) +, _storyShowPinned(options.storyShowPinned) +, _storyHidden(options.storyHidden) +, _storyShowHidden(options.storyShowHidden) +, _link(_sensitiveSpoiler + ? HistoryView::MakeSensitiveMediaLink( + std::make_shared(crl::guard(this, [=] { + maybeClearSensitiveSpoiler(); + })), + parent) + : makeOpenPhotoHandler()) { if (_data->inlineThumbnailBytes().isEmpty() && (_data->hasExact(Data::PhotoSize::Small) || _data->hasExact(Data::PhotoSize::Thumbnail))) { @@ -348,6 +321,16 @@ Photo::Photo( Photo::~Photo() = default; +ClickHandlerPtr Photo::makeOpenPhotoHandler() { + return std::make_shared( + _data, + crl::guard(this, [=](FullMsgId id) { + clearSpoiler(); + delegate()->openPhoto(_data, id); + }), + parent()->fullId()); +} + void Photo::initDimensions() { _maxw = 2 * st::overviewPhotoMinSize; _minh = _story ? qRound(_maxw * kStoryRatio) : _maxw; @@ -373,7 +356,7 @@ void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const || _dataMedia->image(Data::PhotoSize::Thumbnail)); if ((good && !_goodLoaded) || widthChanged) { _goodLoaded = good; - _pix = QPixmap(); + _pix = QImage(); if (_goodLoaded) { setPixFrom(_dataMedia->image(Data::PhotoSize::Large) ? _dataMedia->image(Data::PhotoSize::Large) @@ -391,7 +374,7 @@ void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const if (_pix.isNull()) { p.fillRect(0, 0, _width, _height, st::overviewPhotoBg); } else { - p.drawPixmap(0, 0, _pix); + p.drawImage(0, 0, _pix); } if (_spoiler) { @@ -401,13 +384,27 @@ void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const QRect(0, 0, _width, _height), Ui::DefaultImageSpoiler().frame( _spoiler->index(context->ms, paused))); + + if (_sensitiveSpoiler) { + PaintSensitiveTag(p, QRect(0, 0, _width, _height)); + } + } + + if (_storyHidden) { + delegate()->hiddenMark()->paint( + p, + _pix, + _hiddenBgCache, + QPoint(), + QSize(_width, _height), + _width); } if (selected) { p.fillRect(0, 0, _width, _height, st::overviewPhotoSelectOverlay); } - if (_pinned) { + if (_storyPinned) { const auto &icon = selected ? st::storyPinnedIconSelected : st::storyPinnedIcon; @@ -427,8 +424,7 @@ void Photo::setPixFrom(not_null image) { if (!_goodLoaded) { img = Images::Blur(std::move(img)); } - _pix = Ui::PixmapFromImage( - CropMediaFrame(std::move(img), _width, _height)); + _pix = CropMediaFrame(std::move(img), _width, _height); // In case we have inline thumbnail we can unload all images and we still // won't get a blank image in the media viewer when the photo is opened. @@ -453,15 +449,25 @@ void Photo::ensureDataMediaCreated() const { void Photo::clearSpoiler() { if (_spoiler) { _spoiler = nullptr; - _pix = QPixmap(); + _sensitiveSpoiler = false; + _pix = QImage(); delegate()->repaintItem(this); } } +void Photo::maybeClearSensitiveSpoiler() { + if (_sensitiveSpoiler) { + clearSpoiler(); + _link = makeOpenPhotoHandler(); + } +} + void Photo::itemDataChanged() { - const auto pinned = parent()->isPinned(); - if (_pinned != pinned) { - _pinned = pinned; + const auto pinned = _storyShowPinned && parent()->isPinned(); + const auto hidden = _storyShowHidden && !parent()->storyInProfile(); + if (_storyPinned != pinned || _storyHidden != hidden) { + _storyPinned = pinned; + _storyHidden = hidden; delegate()->repaintItem(this); } } @@ -488,12 +494,26 @@ Video::Video( , _data(video) , _videoCover(LookupVideoCover(video, parent)) , _duration(Ui::FormatDurationText(_data->duration() / 1000)) -, _spoiler(options.spoiler ? std::make_unique([=] { - delegate->repaintItem(this); -}) : nullptr) -, _pinned(options.pinned) -, _story(options.story) { +, _spoiler((options.spoiler || parent->isMediaSensitive()) + ? std::make_unique([=] { + delegate->repaintItem(this); + }) + : nullptr) +, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0) +, _story(options.story) +, _storyPinned(options.storyPinned) +, _storyShowPinned(options.storyShowPinned) +, _storyHidden(options.storyHidden) +, _storyShowHidden(options.storyShowHidden) { setDocumentLinks(_data); + if (_sensitiveSpoiler) { + _openl = HistoryView::MakeSensitiveMediaLink( + std::make_shared(crl::guard(this, [=] { + clearSpoiler(); + setDocumentLinks(_data); + })), + parent); + } if (!_videoCover) { _data->loadThumbnail(parent->fullId()); } else if (_videoCover->inlineThumbnailBytes().isEmpty() @@ -519,7 +539,11 @@ int32 Video::resizeGetHeight(int32 width) { return _height; } -void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) { +void Video::paint( + Painter &p, + const QRect &clip, + TextSelection selection, + const PaintContext *context) { ensureDataMediaCreated(); const auto selected = (selection == FullSelection); @@ -556,15 +580,14 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const : thumbnail ? thumbnail->original() : Images::Blur(blurred->original()); - _pix = Ui::PixmapFromImage( - CropMediaFrame(std::move(img), _width, _height)); + _pix = CropMediaFrame(std::move(img), _width, _height); _pixBlurred = !(thumbnail || good); } if (_pix.isNull()) { p.fillRect(0, 0, _width, _height, st::overviewPhotoBg); } else { - p.drawPixmap(0, 0, _pix); + p.drawImage(0, 0, _pix); } if (_spoiler) { @@ -574,13 +597,27 @@ void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const QRect(0, 0, _width, _height), Ui::DefaultImageSpoiler().frame( _spoiler->index(context->ms, paused))); + + if (_sensitiveSpoiler) { + PaintSensitiveTag(p, QRect(0, 0, _width, _height)); + } + } + + if (_storyHidden) { + delegate()->hiddenMark()->paint( + p, + _pix, + _hiddenBgCache, + QPoint(), + QSize(_width, _height), + _width); } if (selected) { p.fillRect(QRect(0, 0, _width, _height), st::overviewPhotoSelectOverlay); } - if (_pinned) { + if (_storyPinned) { const auto &icon = selected ? st::storyPinnedIconSelected : st::storyPinnedIcon; @@ -660,15 +697,25 @@ void Video::ensureDataMediaCreated() const { void Video::clearSpoiler() { if (_spoiler) { _spoiler = nullptr; - _pix = QPixmap(); + _sensitiveSpoiler = false; + _pix = QImage(); delegate()->repaintItem(this); } } +void Video::maybeClearSensitiveSpoiler() { + if (_sensitiveSpoiler) { + clearSpoiler(); + setDocumentLinks(_data); + } +} + void Video::itemDataChanged() { - const auto pinned = parent()->isPinned(); - if (_pinned != pinned) { - _pinned = pinned; + const auto pinned = _storyShowPinned && parent()->isPinned(); + const auto hidden = _storyShowHidden && !parent()->storyInProfile(); + if (_storyPinned != pinned || _storyHidden != hidden) { + _storyPinned = pinned; + _storyHidden = hidden; delegate()->repaintItem(this); } } @@ -700,7 +747,9 @@ TextState Video::getState( StateRequest request) const { if (hasPoint(point)) { ensureDataMediaCreated(); - const auto link = (_data->loading() || _data->uploading()) + const auto link = _sensitiveSpoiler + ? _openl + : (_data->loading() || _data->uploading()) ? _cancell : (dataLoaded() || _dataMedia->canBePlayed(parent())) ? _openl @@ -2036,8 +2085,22 @@ Gif::Gif( not_null parent, not_null gif) : RadialProgressItem(delegate, parent) -, _data(gif) { +, _data(gif) +, _spoiler(parent->isMediaSensitive() + ? std::make_unique([=] { + delegate->repaintItem(this); + }) + : nullptr) +, _sensitiveSpoiler(parent->isMediaSensitive() ? 1 : 0) { setDocumentLinks(_data, true); + if (_sensitiveSpoiler) { + _openl = HistoryView::MakeSensitiveMediaLink( + std::make_shared(crl::guard(this, [=] { + clearSpoiler(); + setDocumentLinks(_data, true); + })), + parent); + } _data->loadThumbnail(parent->fullId()); } @@ -2140,6 +2203,23 @@ void Gif::clipCallback(Media::Clip::Notification notification) { } } +void Gif::clearSpoiler() { + if (_spoiler) { + _spoiler = nullptr; + _sensitiveSpoiler = false; + _thumb = QImage(); + _thumbGood = false; + delegate()->repaintItem(this); + } +} + +void Gif::maybeClearSensitiveSpoiler() { + if (_sensitiveSpoiler) { + clearSpoiler(); + setDocumentLinks(_data); + } +} + void Gif::validateThumbnail( Image *image, QSize size, @@ -2165,7 +2245,9 @@ void Gif::prepareThumbnail(QSize size, QSize frame) { Assert(document != nullptr); ensureDataMediaCreated(); - validateThumbnail(_dataMedia->thumbnail(), size, frame, true); + if (!_spoiler) { + validateThumbnail(_dataMedia->thumbnail(), size, frame, true); + } validateThumbnail(_dataMedia->thumbnailInline(), size, frame, false); } @@ -2194,7 +2276,7 @@ void Gif::paint( }); } - const auto animating = (_gif && _gif->started()); + const auto animating = !_spoiler && (_gif && _gif->started()); if (displayLoading) { ensureRadial(); if (!_radial->animating()) { @@ -2224,6 +2306,19 @@ void Gif::paint( } } + if (_spoiler) { + const auto paused = context->paused || On(PowerSaving::kChatSpoiler); + Ui::FillSpoilerRect( + p, + r, + Ui::DefaultImageSpoiler().frame( + _spoiler->index(context->ms, paused))); + + if (_sensitiveSpoiler) { + PaintSensitiveTag(p, r); + } + } + const auto selected = (selection == FullSelection); if (radial @@ -2350,5 +2445,4 @@ void Gif::updateStatusText() { } } -} // namespace Layout -} // namespace Overview +} // namespace Overview::Layout diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index ff0273d1d6..7a068d1cd1 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -31,8 +31,7 @@ namespace Ui { class SpoilerAnimation; } // namespace Ui -namespace Overview { -namespace Layout { +namespace Overview::Layout { class Checkbox; class ItemBase; @@ -76,6 +75,9 @@ public: virtual void clearHeavyPart() { } + virtual void maybeClearSensitiveSpoiler() { + } + protected: [[nodiscard]] not_null parent() const { return _parent; @@ -187,8 +189,11 @@ struct Info : RuntimeComponent { struct MediaOptions { bool spoiler = false; - bool pinned = false; bool story = false; + bool storyPinned = false; + bool storyShowPinned = false; + bool storyHidden = false; + bool storyShowHidden = false; }; class Photo final : public ItemBase { @@ -210,20 +215,29 @@ public: void itemDataChanged() override; void clearHeavyPart() override; + void maybeClearSensitiveSpoiler() override; + private: void ensureDataMediaCreated() const; void setPixFrom(not_null image); + [[nodiscard]] ClickHandlerPtr makeOpenPhotoHandler(); void clearSpoiler(); const not_null _data; mutable std::shared_ptr _dataMedia; - ClickHandlerPtr _link; std::unique_ptr _spoiler; - QPixmap _pix; - bool _goodLoaded = false; - bool _pinned = false; - bool _story = false; + QImage _pix; + QImage _hiddenBgCache; + bool _goodLoaded : 1 = false; + bool _sensitiveSpoiler : 1 = false; + bool _story : 1 = false; + bool _storyPinned : 1 = false; + bool _storyShowPinned : 1 = false; + bool _storyHidden : 1 = false; + bool _storyShowHidden : 1 = false; + + ClickHandlerPtr _link; }; @@ -249,6 +263,9 @@ public: void clearHeavyPart() override; void setPosition(int32 position) override; + void clearSpoiler() override; + void maybeClearSensitiveSpoiler() override; + protected: float64 dataProgress() const override; bool dataFinished() const override; @@ -279,9 +296,11 @@ private: const not_null _data; mutable std::shared_ptr _dataMedia; StatusText _status; + std::unique_ptr _spoiler; QImage _thumb; bool _thumbGood = false; + bool _sensitiveSpoiler = false; }; @@ -305,6 +324,8 @@ public: void clearHeavyPart() override; void clearSpoiler() override; + void maybeClearSensitiveSpoiler() override; + protected: float64 dataProgress() const override; bool dataFinished() const override; @@ -324,10 +345,15 @@ private: QString _duration; std::unique_ptr _spoiler; - QPixmap _pix; - bool _pixBlurred = true; - bool _pinned = false; - bool _story = false; + QImage _pix; + QImage _hiddenBgCache; + bool _pixBlurred : 1 = true; + bool _sensitiveSpoiler : 1 = false; + bool _story : 1 = false; + bool _storyPinned : 1 = false; + bool _storyShowPinned : 1 = false; + bool _storyHidden : 1 = false; + bool _storyShowHidden : 1 = false; }; @@ -485,5 +511,4 @@ private: }; -} // namespace Layout -} // namespace Overview +} // namespace Overview::Layout diff --git a/Telegram/SourceFiles/overview/overview_layout_delegate.h b/Telegram/SourceFiles/overview/overview_layout_delegate.h index e2fc7717d6..ff129a2366 100644 --- a/Telegram/SourceFiles/overview/overview_layout_delegate.h +++ b/Telegram/SourceFiles/overview/overview_layout_delegate.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Overview { -namespace Layout { +class StickerPremiumMark; + +namespace Overview::Layout { class ItemBase; @@ -19,6 +20,8 @@ public: virtual void repaintItem(not_null item) = 0; virtual bool itemVisible(not_null item) = 0; + [[nodiscard]] virtual not_null hiddenMark() = 0; + virtual void openPhoto(not_null photo, FullMsgId id) = 0; virtual void openDocument( not_null document, @@ -27,5 +30,4 @@ public: }; -} // namespace Layout -} // namespace Overview +} // namespace Overview::Layout diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h index b2a22fbdbc..590d7c2109 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h @@ -63,11 +63,11 @@ public: } template - QPointer show( + base::weak_qptr show( object_ptr box, Ui::LayerOptions options = Ui::LayerOption::KeepOther, anim::type animated = anim::type::normal) { - auto result = QPointer(box.data()); + auto result = base::weak_qptr(box.data()); showBox(std::move(box), options, animated); return result; } diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index e4979bf3b2..1abfa686be 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -910,7 +910,7 @@ void PanelController::suggestReset(Fn callback) { .confirmed = [=] { resetPassport(callback); }, .cancelled = [=] { cancelReset(); }, .confirmText = Lang::Hard::PassportCorruptedReset(), - })).data()); + })).get()); } void PanelController::resetPassport(Fn callback) { @@ -921,7 +921,7 @@ void PanelController::resetPassport(Fn callback) { .confirmText = Lang::Hard::PassportCorruptedReset(), .confirmStyle = &st::attentionBoxButton, })); - _resetBox = Ui::BoxPointer(box.data()); + _resetBox = Ui::BoxPointer(box.get()); } void PanelController::cancelReset() { @@ -1210,7 +1210,7 @@ void PanelController::startScopeEdit( std::move(scans), std::move(translations), PrepareSpecialFiles(*_editDocument)); - const auto weak = Ui::MakeWeak(result.data()); + const auto weak = base::make_weak(result.data()); _panelHasUnsavedChanges = [=] { return weak ? weak->hasUnsavedChanges() : false; }; @@ -1229,7 +1229,7 @@ void PanelController::startScopeEdit( std::move(preferredLanguage)), _editValue->error, _editValue->data.parsedInEdit); - const auto weak = Ui::MakeWeak(result.data()); + const auto weak = base::make_weak(result.data()); _panelHasUnsavedChanges = [=] { return weak ? weak->hasUnsavedChanges() : false; }; diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h index 5c3d296b47..c08645056e 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.h +++ b/Telegram/SourceFiles/passport/passport_panel_controller.h @@ -174,7 +174,7 @@ private: std::unique_ptr _panel; Fn _panelHasUnsavedChanges; - QPointer _confirmForgetChangesBox; + base::weak_qptr _confirmForgetChangesBox; std::vector _editScopeBoxes; Scope *_editScope = nullptr; const Value *_editValue = nullptr; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index bac92c2c80..723e37f146 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -488,8 +488,9 @@ not_null PanelEditDocument::setupContent( }, lifetime()); } - inner->add( - object_ptr(inner, st::passportDetailsSkip)); + inner->add(object_ptr( + inner, + st::passportDetailsSkip)); } if (auto text = _controller->deleteValueLabel()) { inner->add( diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index 3c3d725338..07a673cb28 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -64,39 +64,29 @@ not_null PanelForm::setupContent() { inner->resizeToWidth(width); }, inner->lifetime()); - const auto userpicWrap = inner->add( - object_ptr( - inner, - st::passportFormUserpic.size.height()), - st::passportFormUserpicPadding); - _userpic = Ui::AttachParentChild( - userpicWrap, + _userpic = inner->add( object_ptr( - userpicWrap, + inner, bot, - st::passportFormUserpic)); - userpicWrap->widthValue( - ) | rpl::start_with_next([=](int width) { - _userpic->move((width - _userpic->width()) / 2, _userpic->y()); - }, _userpic->lifetime()); + st::passportFormUserpic), + st::passportFormUserpicPadding, + style::al_top); _about1 = inner->add( - object_ptr>( + object_ptr( inner, - object_ptr( - inner, - tr::lng_passport_request1(tr::now, lt_bot, bot->name()), - st::passportPasswordLabelBold)), - st::passportFormAbout1Padding)->entity(); + tr::lng_passport_request1(tr::now, lt_bot, bot->name()), + st::passportPasswordLabelBold), + st::passportFormAbout1Padding, + style::al_top); _about2 = inner->add( - object_ptr>( + object_ptr( inner, - object_ptr( - inner, - tr::lng_passport_request2(tr::now), - st::passportPasswordLabel)), - st::passportFormAbout2Padding)->entity(); + tr::lng_passport_request2(tr::now), + st::passportPasswordLabel), + st::passportFormAbout2Padding, + style::al_top); inner->add(object_ptr( inner, diff --git a/Telegram/SourceFiles/passport/passport_panel_password.cpp b/Telegram/SourceFiles/passport/passport_panel_password.cpp index 7046f14f08..15163dcbde 100644 --- a/Telegram/SourceFiles/passport/passport_panel_password.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_password.cpp @@ -171,46 +171,42 @@ void PanelNoPassword::setupContent() { }, _inner->lifetime()); _inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - tr::lng_passport_request1( - tr::now, - lt_bot, - _controller->bot()->name()), - st::passportPasswordLabelBold)), - st::passportPasswordAbout1Padding)->entity(); + tr::lng_passport_request1( + tr::now, + lt_bot, + _controller->bot()->name()), + st::passportPasswordLabelBold), + st::passportPasswordAbout1Padding, + style::al_top); _inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - tr::lng_passport_request2(tr::now), - st::passportPasswordLabel)), - st::passportPasswordAbout2Padding)->entity(); + tr::lng_passport_request2(tr::now), + st::passportPasswordLabel), + st::passportPasswordAbout2Padding, + style::al_top); const auto iconWrap = _inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - st::passportPasswordIconHeight))); - iconWrap->entity()->resizeToWidth(st::passportPasswordIcon.width()); + st::passportPasswordIconHeight), + style::al_top); + iconWrap->setNaturalWidth(st::passportPasswordIcon.width()); Ui::CreateChild( - iconWrap->entity(), + iconWrap, st::passportPasswordIcon, QPoint(0, 0)); _inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - tr::lng_passport_create_password(tr::now), - st::passportPasswordSetupLabel)), - st::passportFormAbout2Padding)->entity(); + tr::lng_passport_create_password(tr::now), + st::passportPasswordSetupLabel), + st::passportFormAbout2Padding, + style::al_top); refreshBottom(); } @@ -218,24 +214,22 @@ void PanelNoPassword::setupContent() { void PanelNoPassword::refreshBottom() { const auto pattern = _controller->unconfirmedEmailPattern(); _about.reset(_inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - (pattern.isEmpty() - ? tr::lng_passport_about_password(tr::now) - : tr::lng_passport_code_sent(tr::now, lt_email, pattern)), - st::passportPasswordSetupLabel)), - st::passportFormAbout2Padding)->entity()); + (pattern.isEmpty() + ? tr::lng_passport_about_password(tr::now) + : tr::lng_passport_code_sent(tr::now, lt_email, pattern)), + st::passportPasswordSetupLabel), + st::passportFormAbout2Padding, + style::al_top)); if (pattern.isEmpty()) { const auto button = _inner->add( - object_ptr>( + object_ptr( _inner, - object_ptr( - _inner, - tr::lng_passport_password_create(), - st::defaultBoxButton))); - button->entity()->addClickHandler([=] { + tr::lng_passport_password_create(), + st::defaultBoxButton), + style::al_top); + button->addClickHandler([=] { _controller->setupPassword(); }); } else { diff --git a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp index 55739b7dbb..0fa946fd96 100644 --- a/Telegram/SourceFiles/passport/ui/passport_details_row.cpp +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp @@ -571,7 +571,7 @@ DateRow::DateRow( GetYear(value)) , _value(valueCurrent()) { const auto focused = [=](const object_ptr &field) { - return [this, pointer = MakeWeak(field.data())]{ + return [this, pointer = base::make_weak(field.data())]{ _borderAnimationStart = pointer->borderAnimationStart() + pointer->x() - _day->x(); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 379036e6e9..d27161660e 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -410,7 +410,7 @@ void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { UnregisterPaymentStart(this); _submitState = SubmitState::Validated; _panel->showWarning(data.bot->name(), data.provider->name()); - if (const auto box = _enterPasswordBox.data()) { + if (const auto box = _enterPasswordBox.get()) { box->closeBox(); } }, [&](const VerificationNeeded &data) { @@ -534,7 +534,7 @@ void CheckoutProcess::handleError(const Error &error) { showToast({ "SmartGlocal Error: " + id }); } break; case Error::Type::TmpPassword: - if (const auto box = _enterPasswordBox.data()) { + if (const auto box = _enterPasswordBox.get()) { if (!box->handleCustomCheckError(id)) { showToast({ "Error: Could not generate tmp password." }); } @@ -542,7 +542,7 @@ void CheckoutProcess::handleError(const Error &error) { break; case Error::Type::Send: _sendFormFailed = true; - if (const auto box = _enterPasswordBox.data()) { + if (const auto box = _enterPasswordBox.get()) { box->closeBox(); } if (_submitState == SubmitState::Finishing) { @@ -857,7 +857,7 @@ void CheckoutProcess::requestPassword() { fields.customSubmitButton = tr::lng_payments_password_submit(); fields.customCheckCallback = [=]( const Core::CloudPasswordResult &result, - QPointer box) { + base::weak_qptr box) { _enterPasswordBox = box; _form->submit(result); }; diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 2f52caf23f..ad6648c500 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -189,7 +189,7 @@ private: const not_null _session; const std::unique_ptr

_form; const std::unique_ptr _panel; - QPointer _enterPasswordBox; + base::weak_qptr _enterPasswordBox; Fn _reactivate; Fn _nonPanelPaymentFormProcess; SubmitState _submitState = SubmitState::None; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 20bebbef6e..ff005327ad 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -383,9 +383,11 @@ MTPInputInvoice Form::inputInvoice() const { } return MTP_inputInvoiceStars( MTP_inputStorePaymentStarsTopup( + MTP_flags(0), MTP_long(credits->credits), MTP_string(credits->currency), - MTP_long(credits->amount))); + MTP_long(credits->amount), + MTPInputPeer())); } else if (const auto gift = std::get_if(&_id.value)) { using Flag = MTPDinputInvoiceStarGift::Flag; return MTP_inputInvoiceStarGift( @@ -531,6 +533,7 @@ void Form::requestForm() { .invoice = invoice, .inputInvoice = inputInvoice(), .starGiftLimitedCount = gift ? gift->limitedCount : 0, + .starGiftPerUserLimit = gift ? gift->perUserLimit : 0, .starGiftForm = true, }; _updates.fire(CreditsPaymentStarted{ .data = formData }); diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 598f9a23d8..f857ce500d 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -179,6 +179,7 @@ struct InvoiceStarGift { TextWithEntities message; not_null recipient; int limitedCount = 0; + int perUserLimit = 0; bool anonymous = false; bool upgraded = false; }; @@ -202,6 +203,7 @@ struct CreditsFormData { InvoiceCredits invoice; MTPInputInvoice inputInvoice; int starGiftLimitedCount = 0; + int starGiftPerUserLimit = 0; bool starGiftForm = false; }; diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index d63b505a98..09b3bc1f50 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -77,6 +77,14 @@ void ProcessCreditsPayment( show->showToast( tr::lng_gift_sold_out_title(tr::now)); } + } else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) { + show->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + std::max(form->starGiftPerUserLimit, 1), + Ui::Text::RichLangValue), + }); } else { show->showToast(*error); } diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index 4af87b93d9..13160ca109 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -140,7 +140,7 @@ void ShowPaidReactionDetails( const auto chosen = std::clamp(kDefaultPerReaction, 1, max); struct State { - QPointer selectBox; + base::weak_qptr selectBox; bool ignoreShownPeerSwitch = false; bool sending = false; }; @@ -155,7 +155,7 @@ void ShowPaidReactionDetails( state->sending = false; if (success && count > 0) { state->ignoreShownPeerSwitch = true; - if (const auto strong = state->selectBox.data()) { + if (const auto strong = state->selectBox.get()) { strong->closeBox(); } } @@ -176,19 +176,13 @@ void ShowPaidReactionDetails( auto submitText = [=](rpl::producer amount) { auto nice = std::move(amount) | rpl::map([=](int count) { - return Ui::CreditsEmojiSmall(session).append( + return Ui::CreditsEmojiSmall().append( Lang::FormatCountDecimal(count)); }); return tr::lng_paid_react_send( lt_price, std::move(nice), - Ui::Text::RichLangValue - ) | rpl::map([=](TextWithEntities &&text) { - return Ui::TextWithContext{ - .text = std::move(text), - .context = Core::TextContext({ .session = session }), - }; - }); + Ui::Text::RichLangValue); }; auto top = std::vector(); const auto add = [&](const Data::MessageReactionsTopPaid &entry) { @@ -260,7 +254,7 @@ void ShowPaidReactionDetails( }, })); - if (const auto strong = state->selectBox.data()) { + if (const auto strong = state->selectBox.get()) { session->data().itemRemoved( ) | rpl::start_with_next([=](not_null removed) { if (removed == item) { diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index ae68d8bdde..cd7d8a8e23 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -119,10 +119,12 @@ bool FormSummary::showCriticalError(const TextWithEntities &text) { return false; } Ui::AddSkip(_layout.get(), st::paymentsPricesTopSkip); - _layout->add(object_ptr( - _layout.get(), - rpl::single(text), - st::paymentsCriticalError)); + _layout->add( + object_ptr( + _layout.get(), + rpl::single(text), + st::paymentsCriticalError), + style::al_top); return true; } diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index b0e1bcc0cb..6e7f049bc1 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -881,10 +881,13 @@ void Panel::showCriticalError(const TextWithEntities &text) { _progress = nullptr; _webviewProgress = false; if (!_weakFormSummary || !_weakFormSummary->showCriticalError(text)) { - auto error = base::make_unique_q>( - _widget.get(), + auto wrap = base::make_unique_q(_widget.get()); + const auto raw = wrap.get(); + + const auto error = CreateChild>( + raw, object_ptr( - _widget.get(), + raw, rpl::single(text), st::paymentsCriticalError), st::paymentsCriticalErrorPadding); @@ -898,7 +901,13 @@ void Panel::showCriticalError(const TextWithEntities &text) { _delegate->panelOpenUrl(entity.data); return false; }); - _widget->showInner(std::move(error)); + + raw->widthValue() | rpl::start_with_next([=](int width) { + error->resizeToWidth(width); + raw->resize(width, error->height()); + }, raw->lifetime()); + + _widget->showInner(std::move(wrap)); } } diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 67ad2c7cc0..639cd1c70b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -236,7 +236,7 @@ void AddArrowDown(not_null widget) { } void SelectShownPeer( - std::shared_ptr> menu, + std::shared_ptr> menu, not_null parent, const std::vector &mine, uint64 selected, @@ -311,7 +311,7 @@ void FillTopReactors( bool chosenChanged = false; }; const auto state = wrap->lifetime().make_state(); - const auto menu = std::make_shared>(); + const auto menu = std::make_shared>(); rpl::combine( std::move(chosen), @@ -481,7 +481,8 @@ void PaidReactionsBox( box, tr::lng_paid_react_title(), st::boostCenteredTitle), - st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0)); + st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0), + style::al_top); const auto labelWrap = box->addRow( object_ptr(box), (st::boxRowPadding @@ -517,13 +518,13 @@ void PaidReactionsBox( state->shownPeer = state->savedShownPeer = barePeerId; }); - const auto named = box->addRow(object_ptr>( - box, + const auto named = box->addRow( object_ptr( box, tr::lng_paid_react_show_in_top(tr::now), - state->shownPeer.current() != 0))); - named->entity()->checkedValue( + state->shownPeer.current() != 0), + style::al_top); + named->checkedValue( ) | rpl::start_with_next([=](bool show) { state->shownPeer = show ? state->savedShownPeer : 0; }, named->lifetime()); @@ -545,10 +546,8 @@ void PaidReactionsBox( st::creditsBoxButtonLabel); args.submit( state->chosen.value() - ) | rpl::start_with_next([=](const TextWithContext &text) { - buttonLabel->setMarkedText( - text.text, - text.context); + ) | rpl::start_with_next([=](const TextWithEntities &text) { + buttonLabel->setMarkedText(text); }, buttonLabel->lifetime()); buttonLabel->setTextColorOverride( box->getDelegate()->style().button.textFg->c); diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index bf52952ac3..fb6c2df863 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -23,11 +23,6 @@ class BoxContent; class GenericBox; class DynamicImage; -struct TextWithContext { - TextWithEntities text; - Text::MarkedContext context; -}; - struct PaidReactionTop { QString name; std::shared_ptr photo; @@ -45,7 +40,7 @@ struct PaidReactionBoxArgs { not_null session; QString channel; - Fn(rpl::producer amount)> submit; + Fn(rpl::producer amount)> submit; rpl::producer balanceValue; Fn send; }; diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index f36bfb6326..0bb06f5508 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -176,7 +176,18 @@ LinuxIntegration::LinuxIntegration() QObject::connect( QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, - [] { malloc_trim(0); }); + [] { + static auto timer = [] { + QElapsedTimer timer; + timer.start(); + return timer; + }(); + + if (timer.hasExpired(10000)) { + malloc_trim(0); + timer.start(); + } + }); #endif // __GLIBC__ } diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index f73fceab05..f2f9b02cac 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "core/application.h" #include "core/sandbox.h" -#include "core/core_settings.h" #include "data/data_forum_topic.h" #include "data/data_saved_sublist.h" #include "data/data_peer.h" @@ -233,15 +232,21 @@ bool ByDefault() { }, HasCapability); } +bool VolumeSupported() { + return UseGNotification() || !HasCapability("sound"); +} + void Create(Window::Notifications::System *system) { static const auto ServiceWatcher = CreateServiceWatcher(); const auto managerSetter = [=]( XdgNotifications::NotificationsProxy proxy) { - system->setManager([=] { - auto manager = std::make_unique(system); - manager->_private->init(proxy); - return manager; + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + system->setManager([=] { + auto manager = std::make_unique(system); + manager->_private->init(proxy); + return manager; + }); }); }; @@ -263,15 +268,7 @@ void Create(Window::Notifications::System *system) { res, nullptr); - if (!proxy) { - ServiceRegistered = false; - CurrentServerInformation = {}; - CurrentCapabilities = {}; - managerSetter(nullptr); - return; - } - - ServiceRegistered = bool(proxy.get_name_owner()); + ServiceRegistered = proxy ? bool(proxy.get_name_owner()) : false; if (!ServiceRegistered) { CurrentServerInformation = {}; CurrentCapabilities = {}; @@ -927,11 +924,7 @@ bool Manager::doSkipToast() const { } void Manager::doMaybePlaySound(Fn playSound) { - if (UseGNotification() - || !HasCapability("sound") - || !Core::App().settings().desktopNotify()) { - _private->invokeIfNotInhibited(std::move(playSound)); - } + _private->invokeIfNotInhibited(std::move(playSound)); } void Manager::doMaybeFlashBounce(Fn flashBounce) { diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index aff3834b82..1fe639e316 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -712,11 +712,10 @@ void start() { GLib::set_prgname(cExeName().toStdString()); GLib::set_application_name(AppName.data()); - Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-%4"_q.arg( + Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-{}"_q.arg( QDir::tempPath(), HashForSocketPath(), - u"TD"_q,//QCoreApplication::applicationName(), - make path smaller. - u"%1"_q).toStdString()); + u"TD"_q).toStdString()); InstallLauncher(); } diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index b92cd90f55..ded4f852f5 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -205,6 +205,10 @@ bool ByDefault() { return Supported(); } +bool VolumeSupported() { + return false; +} + void Create(Window::Notifications::System *system) { system->setManager([=] { return std::make_unique(system); }); } @@ -575,10 +579,7 @@ bool Manager::doSkipToast() const { } void Manager::doMaybePlaySound(Fn playSound) { - // Play through native notification system if toasts are enabled. - if (!Core::App().settings().desktopNotify()) { - playSound(); - } + playSound(); } void Manager::doMaybeFlashBounce(Fn flashBounce) { diff --git a/Telegram/SourceFiles/platform/platform_notifications_manager.h b/Telegram/SourceFiles/platform/platform_notifications_manager.h index 3ddb70a540..da41f6010e 100644 --- a/Telegram/SourceFiles/platform/platform_notifications_manager.h +++ b/Telegram/SourceFiles/platform/platform_notifications_manager.h @@ -20,6 +20,7 @@ void MaybeFlashBounceForCustom(Fn flashBounce); [[nodiscard]] bool Supported(); [[nodiscard]] bool Enforced(); [[nodiscard]] bool ByDefault(); +[[nodiscard]] bool VolumeSupported(); void Create(Window::Notifications::System *system); } // namespace Notifications diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index d132ed4f32..2692923efd 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -417,6 +417,10 @@ bool ByDefault() { return false; } +bool VolumeSupported() { + return true; +} + void Create(Window::Notifications::System *system) { system->setManager([=] { auto result = std::make_unique(system); diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index f2696bbcd0..a77bdf978e 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -367,6 +367,9 @@ void start() { } // namespace ThirdParty void start() { + const auto supported = base::WinRT::Supported(); + LOG(("WinRT Supported: %1").arg(Logs::b(supported))); + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale#utf-8-support setlocale(LC_ALL, ".UTF8"); diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.cpp b/Telegram/SourceFiles/profile/profile_block_group_members.cpp deleted file mode 100644 index e155a19f98..0000000000 --- a/Telegram/SourceFiles/profile/profile_block_group_members.cpp +++ /dev/null @@ -1,484 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#include "profile/profile_block_group_members.h" - -#include "api/api_chat_participants.h" -#include "styles/style_profile.h" -#include "ui/widgets/labels.h" -#include "ui/boxes/confirm_box.h" -#include "boxes/peers/edit_participant_box.h" -#include "boxes/peers/edit_participants_box.h" -#include "base/unixtime.h" -#include "ui/widgets/popup_menu.h" -#include "mtproto/mtproto_config.h" -#include "data/data_peer_values.h" -#include "data/data_channel.h" -#include "data/data_chat.h" -#include "data/data_user.h" -#include "data/data_changes.h" -#include "mainwidget.h" -#include "apiwrap.h" -#include "main/main_session.h" -#include "lang/lang_keys.h" -#include "window/window_session_controller.h" - -namespace Profile { -namespace { - -using UpdateFlag = Data::PeerUpdate::Flag; - -} // namespace - -GroupMembersWidget::Member::Member(not_null user) : Item(user) { -} - -not_null GroupMembersWidget::Member::user() const { - return static_cast(peer.get()); -} - -GroupMembersWidget::GroupMembersWidget( - QWidget *parent, - not_null controller, - not_null peer, - const style::PeerListItem &st) -: PeerListWidget(parent, peer, QString(), st, tr::lng_profile_kick(tr::now)) -, _controller(controller) -, _updateOnlineTimer([=] { updateOnlineDisplay(); }) { - peer->session().changes().peerUpdates( - UpdateFlag::Admins - | UpdateFlag::Members - | UpdateFlag::OnlineStatus - ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { - notifyPeerUpdated(update); - }, lifetime()); - - setRemovedCallback([=](PeerData *selectedPeer) { - removePeer(selectedPeer); - }); - setSelectedCallback([=](PeerData *selectedPeer) { - controller->showPeerInfo(selectedPeer); - }); - setUpdateItemCallback([=](Item *item) { - updateItemStatusText(item); - }); - setPreloadMoreCallback([=] { - preloadMore(); - }); - - refreshMembers(); -} - -void GroupMembersWidget::removePeer(PeerData *selectedPeer) { - const auto user = selectedPeer->asUser(); - Assert(user != nullptr); - - const auto text = tr::lng_profile_sure_kick( - tr::now, - lt_user, user->firstName); - const auto currentRestrictedRights = [&]() -> ChatRestrictionsInfo { - if (auto channel = peer()->asMegagroup()) { - auto it = channel->mgInfo->lastRestricted.find(user); - if (it != channel->mgInfo->lastRestricted.cend()) { - return it->second.rights; - } - } - return ChatRestrictionsInfo(); - }(); - - const auto peer = this->peer(); - const auto callback = [=, controller = _controller] { - controller->hideLayer(); - if (const auto chat = peer->asChat()) { - chat->session().api().chatParticipants().kick(chat, user); - controller->showPeerHistory( - chat->id, - Window::SectionShow::Way::ClearStack, - ShowAtTheEndMsgId); - } else if (const auto channel = peer->asChannel()) { - channel->session().api().chatParticipants().kick( - channel, - user, - currentRestrictedRights); - } - }; - _controller->show(Ui::MakeConfirmBox({ - .text = text, - .confirmed = crl::guard(&peer->session(), callback), - .confirmText = tr::lng_box_remove(), - })); -} - -void GroupMembersWidget::notifyPeerUpdated(const Data::PeerUpdate &update) { - if (update.peer != peer()) { - if (update.flags & UpdateFlag::OnlineStatus) { - if (auto user = update.peer->asUser()) { - refreshUserOnline(user); - } - } - return; - } - - if (update.flags & UpdateFlag::Members) { - refreshMembers(); - contentSizeUpdated(); - } - if (update.flags & UpdateFlag::Admins) { - if (const auto chat = peer()->asChat()) { - for (const auto item : items()) { - setItemFlags(getMember(item), chat); - } - } else if (const auto megagroup = peer()->asMegagroup()) { - for (const auto item : items()) { - setItemFlags(getMember(item), megagroup); - } - } - } - this->update(); -} - -void GroupMembersWidget::refreshUserOnline(UserData *user) { - auto it = _membersByUser.find(user); - if (it == _membersByUser.cend()) return; - - _now = base::unixtime::now(); - - auto member = getMember(it->second); - member->lastseen = user->lastseen(); - member->statusHasOnlineColor = Data::OnlineTextActive(user, _now); - member->onlineForSort = user->isSelf() - ? std::numeric_limits::max() - : Data::SortByOnlineValue(user, _now); - member->statusText = QString(); - - sortMembers(); - update(); -} - -void GroupMembersWidget::preloadMore() { - // - // This can cause a ddos, because lastParticipants may never reach members count. - // - //if (auto megagroup = peer()->asMegagroup()) { - // auto &megagroupInfo = megagroup->mgInfo; - // if (!megagroupInfo->lastParticipants.isEmpty() && megagroupInfo->lastParticipants.size() < megagroup->membersCount()) { - // peer()->session().api().requestLast(megagroup, false); - // } - //} -} - -void GroupMembersWidget::updateItemStatusText(Item *item) { - auto member = getMember(item); - auto user = member->user(); - if (member->statusText.isEmpty() || (member->onlineTextTill <= _now)) { - if (user->isBot()) { - const auto seesAllMessages = user->botInfo->readsAllHistory - || member->rank.has_value(); - member->statusText = seesAllMessages - ? tr::lng_status_bot_reads_all(tr::now) - : tr::lng_status_bot_not_reads_all(tr::now); - member->onlineTextTill = _now + 86400; - } else { - member->statusHasOnlineColor = member->lastseen.isOnline(_now); - member->statusText = Data::OnlineText(member->lastseen, _now); - const auto changeInMs = Data::OnlineChangeTimeout( - member->lastseen, - _now); - member->onlineTextTill = _now + TimeId(changeInMs / 1000); - } - } - if (_updateOnlineAt <= _now || _updateOnlineAt > member->onlineTextTill) { - _updateOnlineAt = member->onlineTextTill; - _updateOnlineTimer.callOnce((_updateOnlineAt - _now + 1) * 1000); - } -} - -void GroupMembersWidget::refreshMembers() { - _now = base::unixtime::now(); - if (const auto chat = peer()->asChat()) { - checkSelfAdmin(chat); - if (chat->noParticipantInfo()) { - chat->session().api().requestFullPeer(chat); - } - fillChatMembers(chat); - } else if (const auto megagroup = peer()->asMegagroup()) { - if (megagroup->lastParticipantsRequestNeeded()) { - megagroup->session().api().chatParticipants().requestLast( - megagroup); - } - fillMegagroupMembers(megagroup); - } - sortMembers(); - - refreshVisibility(); -} - -void GroupMembersWidget::checkSelfAdmin(not_null chat) { - if (chat->participants.empty()) { - return; - } - - //const auto self = chat->session().user(); - //if (chat->amAdmin() && !chat->admins.contains(self)) { - // chat->admins.insert(self); - //} else if (!chat->amAdmin() && chat->admins.contains(self)) { - // chat->admins.remove(self); - //} -} - -void GroupMembersWidget::sortMembers() { - if (!_sortByOnline || !itemsCount()) return; - - sortItems([this](Item *a, Item *b) { - return getMember(a)->onlineForSort > getMember(b)->onlineForSort; - }); - - updateOnlineCount(); -} - -void GroupMembersWidget::updateOnlineCount() { - bool onlyMe = true; - int newOnlineCount = 0; - for (const auto item : items()) { - auto member = getMember(item); - auto user = member->user(); - auto isOnline = !user->isBot() - && member->lastseen.isOnline(_now); - if (member->statusHasOnlineColor != isOnline) { - member->statusHasOnlineColor = isOnline; - member->statusText = QString(); - } - if (member->statusHasOnlineColor) { - ++newOnlineCount; - if (!user->isSelf()) { - onlyMe = false; - } - } - } - if (newOnlineCount == 1 && onlyMe) { - newOnlineCount = 0; - } - if (_onlineCount != newOnlineCount) { - _onlineCount = newOnlineCount; - } -} - -auto GroupMembersWidget::addUser( - not_null chat, - not_null user) --> not_null { - const auto member = computeMember(user); - setItemFlags(member, chat); - addItem(member); - return member; -} - -void GroupMembersWidget::fillChatMembers(not_null chat) { - if (chat->participants.empty()) { - return; - } - - clearItems(); - if (!chat->amIn()) { - return; - } - - _sortByOnline = true; - - reserveItemsForSize(chat->participants.size()); - addUser(chat, chat->session().user())->onlineForSort - = std::numeric_limits::max(); - for (const auto &user : chat->participants) { - if (!user->isSelf()) { - addUser(chat, user); - } - } -} - -void GroupMembersWidget::setItemFlags( - not_null item, - not_null chat) { - const auto user = getMember(item)->user(); - const auto isCreator = (peerFromUser(chat->creator) == item->peer->id); - const auto isAdmin = (item->peer->isSelf() && chat->hasAdminRights()) - || chat->admins.contains(user); - const auto rank = isCreator - ? tr::lng_owner_badge(tr::now) - : isAdmin - ? tr::lng_admin_badge(tr::now) - : std::optional(); - item->rank = rank; - item->rankWidth = rank ? st::normalFont->width(*rank) : 0; - if (item->peer->id == chat->session().userPeerId()) { - item->hasRemoveLink = false; - } else if (chat->amCreator() - || ((chat->adminRights() & ChatAdminRight::BanUsers) - && !rank.has_value())) { - item->hasRemoveLink = true; - } else if (chat->invitedByMe.contains(user) && !rank.has_value()) { - item->hasRemoveLink = true; - } else { - item->hasRemoveLink = false; - } -} - -auto GroupMembersWidget::addUser( - not_null megagroup, - not_null user) --> not_null { - const auto member = computeMember(user); - setItemFlags(member, megagroup); - addItem(member); - return member; -} - -void GroupMembersWidget::fillMegagroupMembers( - not_null megagroup) { - Expects(megagroup->mgInfo != nullptr); - - if (megagroup->mgInfo->lastParticipants.empty()) { - return; - } else if (!megagroup->canViewMembers()) { - clearItems(); - return; - } - - _sortByOnline = (megagroup->membersCount() > 0) - && (megagroup->membersCount() - <= megagroup->session().serverConfig().chatSizeMax); - - auto &membersList = megagroup->mgInfo->lastParticipants; - if (_sortByOnline) { - clearItems(); - reserveItemsForSize(membersList.size()); - if (megagroup->amIn()) { - addUser(megagroup, megagroup->session().user())->onlineForSort - = std::numeric_limits::max(); - } - } else if (membersList.size() >= itemsCount()) { - if (addUsersToEnd(megagroup)) { - return; - } - } - if (!_sortByOnline) { - clearItems(); - reserveItemsForSize(membersList.size()); - } - for (const auto user : membersList) { - if (!_sortByOnline || !user->isSelf()) { - addUser(megagroup, user); - } - } -} - -bool GroupMembersWidget::addUsersToEnd(not_null megagroup) { - auto &membersList = megagroup->mgInfo->lastParticipants; - auto &itemsList = items(); - for (int i = 0, count = itemsList.size(); i < count; ++i) { - if (itemsList[i]->peer != membersList.at(i)) { - return false; - } - } - reserveItemsForSize(membersList.size()); - for (int i = itemsCount(), count = membersList.size(); i < count; ++i) { - addUser(megagroup, membersList.at(i)); - } - return true; -} - -void GroupMembersWidget::setItemFlags( - not_null item, - not_null megagroup) { - const auto amCreator = item->peer->isSelf() && megagroup->amCreator(); - const auto amAdmin = item->peer->isSelf() && megagroup->hasAdminRights(); - const auto user = getMember(item)->user(); - const auto adminIt = megagroup->mgInfo->lastAdmins.find(user); - const auto isAdmin = (adminIt != megagroup->mgInfo->lastAdmins.cend()); - const auto isCreator = (megagroup->mgInfo->creator == item->peer); - const auto rankIt = megagroup->mgInfo->admins.find(peerToUser(user->id)); - const auto adminCanEdit = isAdmin && adminIt->second.canEdit; - const auto rank = (amCreator || isCreator) - ? (!megagroup->mgInfo->creatorRank.isEmpty() - ? megagroup->mgInfo->creatorRank - : tr::lng_owner_badge(tr::now)) - : (amAdmin || isAdmin) - ? ((rankIt != megagroup->mgInfo->admins.end() - && !rankIt->second.isEmpty()) - ? rankIt->second - : tr::lng_admin_badge(tr::now)) - : std::optional(); - if (item->rank != rank) { - item->rank = rank; - item->rankWidth = rank ? st::normalFont->width(*rank) : 0; - auto user = item->peer->asUser(); - Assert(user != nullptr); - if (user->isBot()) { - // Update "has access to messages" status. - item->statusText = QString(); - updateItemStatusText(item); - } - } - if (item->peer->isSelf()) { - item->hasRemoveLink = false; - } else if (megagroup->amCreator() - || (megagroup->canBanMembers() - && (!rank.has_value() || adminCanEdit))) { - item->hasRemoveLink = true; - } else { - item->hasRemoveLink = false; - } -} - -auto GroupMembersWidget::computeMember(not_null user) --> not_null { - auto it = _membersByUser.find(user); - if (it == _membersByUser.cend()) { - auto member = new Member(user); - it = _membersByUser.emplace(user, member).first; - member->lastseen = user->lastseen(); - member->statusHasOnlineColor = !user->isBot() - && member->lastseen.isOnline(_now); - member->onlineForSort = Data::SortByOnlineValue(user, _now); - } - return it->second; -} - -void GroupMembersWidget::updateOnlineDisplay() { - if (_sortByOnline) { - _now = base::unixtime::now(); - - bool changed = false; - for (const auto item : items()) { - if (!item->statusHasOnlineColor) { - if (!item->peer->isSelf()) { - continue; - } else { - break; - } - } - auto member = getMember(item); - bool isOnline = !member->user()->isBot() - && member->lastseen.isOnline(_now); - if (!isOnline) { - changed = true; - } - } - if (changed) { - updateOnlineCount(); - } - } - update(); -} - -GroupMembersWidget::~GroupMembersWidget() { - auto members = base::take(_membersByUser); - for (const auto &[_, member] : members) { - delete member; - } -} - -} // namespace Profile diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.h b/Telegram/SourceFiles/profile/profile_block_group_members.h deleted file mode 100644 index 89ae411594..0000000000 --- a/Telegram/SourceFiles/profile/profile_block_group_members.h +++ /dev/null @@ -1,94 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#pragma once - -#include "base/timer.h" -#include "data/data_lastseen_status.h" -#include "profile/profile_block_peer_list.h" - -namespace Ui { -class FlatLabel; -} // namespace Ui - -namespace Data { -struct PeerUpdate; -} // namespace Data - -namespace Window { -class SessionController; -} // namespace Window - -namespace Profile { - -class GroupMembersWidget : public PeerListWidget { - -public: - GroupMembersWidget( - QWidget *parent, - not_null controller, - not_null peer, - const style::PeerListItem &st); - - int onlineCount() const { - return _onlineCount; - } - - ~GroupMembersWidget(); - -private: - void updateOnlineDisplay(); - - // Observed notifications. - void notifyPeerUpdated(const Data::PeerUpdate &update); - - void removePeer(PeerData *selectedPeer); - void refreshMembers(); - void fillChatMembers(not_null chat); - void fillMegagroupMembers(not_null megagroup); - void sortMembers(); - void updateOnlineCount(); - void checkSelfAdmin(not_null chat); - void preloadMore(); - - void refreshUserOnline(UserData *user); - - struct Member : public Item { - explicit Member(not_null user); - not_null user() const; - - TimeId onlineTextTill = 0; - Data::LastseenStatus lastseen; - TimeId onlineForSort = 0; - }; - Member *getMember(Item *item) { - return static_cast(item); - } - - void updateItemStatusText(Item *item); - not_null computeMember(not_null user); - not_null addUser(not_null chat, not_null user); - not_null addUser(not_null megagroup, not_null user); - void setItemFlags(not_null item, not_null chat); - void setItemFlags( - not_null item, - not_null megagroup); - bool addUsersToEnd(not_null megagroup); - - const not_null _controller; - - base::flat_map _membersByUser; - bool _sortByOnline = false; - TimeId _now = 0; - - int _onlineCount = 0; - TimeId _updateOnlineAt = 0; - base::Timer _updateOnlineTimer; - -}; - -} // namespace Profile diff --git a/Telegram/SourceFiles/profile/profile_block_peer_list.cpp b/Telegram/SourceFiles/profile/profile_block_peer_list.cpp deleted file mode 100644 index 130692e474..0000000000 --- a/Telegram/SourceFiles/profile/profile_block_peer_list.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#include "profile/profile_block_peer_list.h" - -#include "ui/effects/ripple_animation.h" -#include "ui/text/text_options.h" -#include "ui/painter.h" -#include "data/data_peer.h" -#include "data/data_cloud_file.h" -#include "main/main_session.h" -#include "styles/style_profile.h" -#include "styles/style_widgets.h" - -namespace Profile { - -PeerListWidget::Item::Item(not_null peer) : peer(peer) { -} - -PeerListWidget::Item::~Item() = default; - -PeerListWidget::PeerListWidget( - QWidget *parent, - PeerData *peer, - const QString &title, - const style::PeerListItem &st, - const QString &removeText) -: BlockWidget(parent, peer, title) -, _st(st) -, _removeText(removeText) -, _removeWidth(st::normalFont->width(_removeText)) { - setMouseTracking(true); - - peer->session().downloaderTaskFinished( - ) | rpl::start_with_next([=] { - update(); - }, lifetime()); -} - -int PeerListWidget::resizeGetHeight(int newWidth) { - auto newHeight = getListTop(); - - newHeight += _items.size() * _st.height; - - return newHeight + _st.bottom; -} - -void PeerListWidget::visibleTopBottomUpdated(int visibleTop, int visibleBottom) { - _visibleTop = visibleTop; - _visibleBottom = visibleBottom; - - if (_preloadMoreCallback) { - if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) { - _preloadMoreCallback(); - } - } -} - -void PeerListWidget::paintContents(Painter &p) { - auto left = getListLeft(); - auto top = getListTop(); - - const auto count = int(_items.size()); - auto from = floorclamp(_visibleTop - top, _st.height, 0, count); - auto to = ceilclamp(_visibleBottom - top, _st.height, 0, count); - for (auto i = from; i < to; ++i) { - auto y = top + i * _st.height; - auto selected = (_pressed >= 0) ? (i == _pressed) : (i == _selected); - auto selectedRemove = selected && _selectedRemove; - if (_pressed >= 0 && !_pressedRemove) { - selectedRemove = false; - } - paintItem(p, left, y, _items[i], selected, selectedRemove); - } -} - -void PeerListWidget::paintItem(Painter &p, int x, int y, Item *item, bool selected, bool selectedKick) { - if (_updateItemCallback) { - _updateItemCallback(item); - } - - auto memberRowWidth = rowWidth(); - if (selected) { - paintItemRect(p, x, y, memberRowWidth, _st.height); - } - if (auto &ripple = item->ripple) { - ripple->paint(p, x, y, width()); - if (ripple->empty()) { - ripple.reset(); - } - } - int skip = _st.photoPosition.x(); - - item->peer->paintUserpicLeft(p, item->userpic, x + _st.photoPosition.x(), y + _st.photoPosition.y(), width(), _st.photoSize); - - if (item->name.isEmpty()) { - item->name.setText( - st::semiboldTextStyle, - item->peer->name(), - Ui::NameTextOptions()); - } - int nameLeft = x + _st.namePosition.x(); - int nameTop = y + _st.namePosition.y(); - int nameWidth = memberRowWidth - _st.namePosition.x() - skip; - if (item->hasRemoveLink && selected) { - p.setFont(selectedKick ? st::normalFont->underline() : st::normalFont); - p.setPen(st::windowActiveTextFg); - p.drawTextLeft(nameLeft + nameWidth - _removeWidth, nameTop, width(), _removeText, _removeWidth); - nameWidth -= _removeWidth + skip; - } else if (item->rank) { - p.setFont(st::normalFont); - p.setPen(st::windowActiveTextFg); - p.drawTextLeft(nameLeft + nameWidth - item->rankWidth, nameTop, width(), *item->rank, item->rankWidth); - nameWidth -= item->rankWidth + skip; - } - p.setPen(st::profileMemberNameFg); - item->name.drawLeftElided(p, nameLeft, nameTop, nameWidth, width()); - - if (item->statusHasOnlineColor) { - p.setPen(_st.statusFgActive); - } else { - p.setPen(selected ? _st.statusFgOver : _st.statusFg); - } - p.setFont(st::normalFont); - p.drawTextLeft(x + _st.statusPosition.x(), y + _st.statusPosition.y(), width(), item->statusText); -} - -void PeerListWidget::paintItemRect(Painter &p, int x, int y, int w, int h) const { - p.fillRect(style::rtlrect(x, y, w, h, width()), _st.button.textBgOver); -} - -void PeerListWidget::mouseMoveEvent(QMouseEvent *e) { - _mousePosition = e->globalPos(); - updateSelection(); -} - -void PeerListWidget::mousePressEvent(QMouseEvent *e) { - _mousePosition = e->globalPos(); - updateSelection(); - - _pressButton = e->button(); - _pressed = _selected; - _pressedRemove = _selectedRemove; - if (_pressed >= 0 && !_pressedRemove) { - const auto item = _items[_pressed]; - if (!item->ripple) { - auto memberRowWidth = rowWidth(); - auto mask = Ui::RippleAnimation::RectMask(QSize(memberRowWidth, _st.height)); - item->ripple = std::make_unique(_st.button.ripple, std::move(mask), [this, index = _pressed] { - repaintRow(index); - }); - } - auto left = getListLeft(); - auto top = getListTop() + _st.height * _pressed; - item->ripple->add(e->pos() - QPoint(left, top)); - } -} - -void PeerListWidget::mouseReleaseEvent(QMouseEvent *e) { - mousePressReleased(e->button()); -} - -void PeerListWidget::mousePressReleased(Qt::MouseButton button) { - repaintRow(_pressed); - auto pressed = std::exchange(_pressed, -1); - auto pressedRemove = base::take(_pressedRemove); - if (pressed >= 0 && pressed < _items.size()) { - if (const auto &ripple = _items[pressed]->ripple) { - ripple->lastStop(); - } - if (pressed == _selected && pressedRemove == _selectedRemove && button == Qt::LeftButton) { - InvokeQueued(this, [this, pressedRemove, peer = _items[pressed]->peer] { - if (auto &callback = (pressedRemove ? _removedCallback : _selectedCallback)) { - callback(peer); - } - }); - } - } - setCursor(_selectedRemove ? style::cur_pointer : style::cur_default); - repaintSelectedRow(); -} - -void PeerListWidget::enterEventHook(QEnterEvent *e) { - _mousePosition = QCursor::pos(); - updateSelection(); -} - -void PeerListWidget::leaveEventHook(QEvent *e) { - _mousePosition = QPoint(-1, -1); - updateSelection(); -} - -void PeerListWidget::updateSelection() { - auto selected = -1; - auto selectedKick = false; - - auto mouse = mapFromGlobal(_mousePosition); - if (rtl()) mouse.setX(width() - mouse.x()); - auto left = getListLeft(); - auto top = getListTop(); - auto memberRowWidth = rowWidth(); - if (mouse.x() >= left && mouse.x() < left + memberRowWidth && mouse.y() >= top) { - selected = (mouse.y() - top) / _st.height; - if (selected >= _items.size()) { - selected = -1; - } else if (_items[selected]->hasRemoveLink) { - int skip = _st.photoPosition.x(); - int nameLeft = left + _st.namePosition.x(); - int nameTop = top + _selected * _st.height + _st.namePosition.y(); - int nameWidth = memberRowWidth - _st.namePosition.x() - skip; - if (mouse.x() >= nameLeft + nameWidth - _removeWidth && mouse.x() < nameLeft + nameWidth) { - if (mouse.y() >= nameTop && mouse.y() < nameTop + st::normalFont->height) { - selectedKick = true; - } - } - } - } - - setSelected(selected, selectedKick); -} - -void PeerListWidget::setSelected(int selected, bool selectedRemove) { - if (_selected == selected && _selectedRemove == selectedRemove) { - return; - } - - repaintSelectedRow(); - if (_selectedRemove != selectedRemove) { - _selectedRemove = selectedRemove; - if (_pressed < 0) { - setCursor(_selectedRemove ? style::cur_pointer : style::cur_default); - } - } - if (_selected != selected) { - _selected = selected; - repaintSelectedRow(); - } -} - -void PeerListWidget::repaintSelectedRow() { - repaintRow(_selected); -} - -void PeerListWidget::repaintRow(int index) { - if (index >= 0) { - auto left = getListLeft(); - rtlupdate(left, getListTop() + index * _st.height, width() - left, _st.height); - } -} - -int PeerListWidget::getListLeft() const { - return _st.left; -} - -int PeerListWidget::rowWidth() const { - return _st.maximalWidth - ? qMin(width() - getListLeft(), _st.maximalWidth) - : width() - getListLeft(); -} - -void PeerListWidget::preloadPhotos() { - int top = getListTop(); - int preloadFor = (_visibleBottom - _visibleTop) * PreloadHeightsCount; - int from = floorclamp(_visibleTop - top, _st.height, 0, _items.size()); - int to = ceilclamp(_visibleBottom + preloadFor - top, _st.height, 0, _items.size()); - for (int i = from; i < to; ++i) { - _items[i]->peer->loadUserpic(); - } -} - -void PeerListWidget::refreshVisibility() { - setVisible(!_items.empty()); -} - -} // namespace Profile diff --git a/Telegram/SourceFiles/profile/profile_block_peer_list.h b/Telegram/SourceFiles/profile/profile_block_peer_list.h deleted file mode 100644 index 94e6544964..0000000000 --- a/Telegram/SourceFiles/profile/profile_block_peer_list.h +++ /dev/null @@ -1,142 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#pragma once - -#include "profile/profile_block_widget.h" -#include "ui/userpic_view.h" - -namespace Ui { -class RippleAnimation; -class PopupMenu; -} // namespace Ui - -namespace style { -struct PeerListItem; -} // namespace style - -namespace Profile { - -class PeerListWidget : public BlockWidget { -public: - PeerListWidget(QWidget *parent, PeerData *peer, const QString &title, const style::PeerListItem &st, const QString &removeText); - - struct Item { - explicit Item(not_null peer); - ~Item(); - - const not_null peer; - Ui::PeerUserpicView userpic; - Ui::Text::String name; - QString statusText; - bool statusHasOnlineColor = false; - std::optional rank; - int rankWidth = 0; - bool hasRemoveLink = false; - std::unique_ptr ripple; - }; - int getListTop() const { - return contentTop(); - } - - int getListLeft() const; - - const std::vector &items() const { - return _items; - } - int itemsCount() const { - return _items.size(); - } - - // Does not take ownership of item. - void addItem(Item *item) { - if (!item) return; - _items.push_back(item); - } - void clearItems() { - _items.clear(); - } - void reserveItemsForSize(int size) { - _items.reserve(size); - } - template - void sortItems(Predicate predicate) { - std::sort(_items.begin(), _items.end(), std::move(predicate)); - } - - void setPreloadMoreCallback(Fn callback) { - _preloadMoreCallback = std::move(callback); - } - void setSelectedCallback(Fn callback) { - _selectedCallback = std::move(callback); - } - void setRemovedCallback(Fn callback) { - _removedCallback = std::move(callback); - } - void setUpdateItemCallback(Fn callback) { - _updateItemCallback = std::move(callback); - } - -protected: - int resizeGetHeight(int newWidth) override; - void visibleTopBottomUpdated( - int visibleTop, - int visibleBottom) override; - - void paintItemRect(Painter &p, int x, int y, int w, int h) const; - void refreshVisibility(); - - void paintContents(Painter &p) override; - - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void enterEventHook(QEnterEvent *e) override; - void enterFromChildEvent(QEvent *e, QWidget *child) override { - enterEventHook(nullptr); - } - void leaveEventHook(QEvent *e) override; - void leaveToChildEvent(QEvent *e, QWidget *child) override { - leaveEventHook(e); - } - -private: - void mousePressReleased(Qt::MouseButton button); - void updateSelection(); - void setSelected(int selected, bool selectedRemove); - void repaintSelectedRow(); - void repaintRow(int index); - void preloadPhotos(); - int rowWidth() const; - - void paintItem(Painter &p, int x, int y, Item *item, bool selected, bool selectedRemove); - - const style::PeerListItem &_st; - - Fn _preloadMoreCallback; - Fn _selectedCallback; - Fn _removedCallback; - Fn _updateItemCallback; - - std::vector _items; - - int _visibleTop = 0; - int _visibleBottom = 0; - - int _selected = -1; - int _pressed = -1; - Qt::MouseButton _pressButton = Qt::LeftButton; - bool _selectedRemove = false; - bool _pressedRemove = false; - QPoint _mousePosition; - - QString _removeText; - int _removeWidth = 0; - -}; - -} // namespace Profile diff --git a/Telegram/SourceFiles/profile/profile_cover_drop_area.cpp b/Telegram/SourceFiles/profile/profile_cover_drop_area.cpp index 6b10b827a3..4f686130bf 100644 --- a/Telegram/SourceFiles/profile/profile_cover_drop_area.cpp +++ b/Telegram/SourceFiles/profile/profile_cover_drop_area.cpp @@ -12,7 +12,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Profile { -CoverDropArea::CoverDropArea(QWidget *parent, const QString &title, const QString &subtitle) : TWidget(parent) +CoverDropArea::CoverDropArea( + QWidget *parent, + const QString &title, + const QString &subtitle) +: RpWidget(parent) , _title(title) , _subtitle(subtitle) , _titleWidth(st::profileDropAreaTitleFont->width(_title)) diff --git a/Telegram/SourceFiles/profile/profile_cover_drop_area.h b/Telegram/SourceFiles/profile/profile_cover_drop_area.h index 156d53adea..ee5b455518 100644 --- a/Telegram/SourceFiles/profile/profile_cover_drop_area.h +++ b/Telegram/SourceFiles/profile/profile_cover_drop_area.h @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Profile { -class CoverDropArea : public TWidget { +class CoverDropArea : public Ui::RpWidget { public: CoverDropArea(QWidget *parent, const QString &title, const QString &subtitle); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 58f3a61656..181582fa3f 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -119,10 +119,10 @@ void AddAwayScheduleSelector( Fn max, Fn done) { using namespace Ui; - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto save = [=](TimeId time) { done(time); - if (const auto strong = box->data()) { + if (const auto strong = box->get()) { strong->closeBox(); } }; diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index a15e492597..98cffdc6eb 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -560,7 +560,7 @@ void ChatIntro::setupContent( content, session, IntroWithRandomSticker(session, _intro.value())), - {}); + style::margins()); const auto title = AddPartInput( content, diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 8437acc7c0..a65d6946dd 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -89,9 +89,9 @@ void EditPeriodBox( const auto take = TimePickerBox(box, values, phrases, days); box->addButton(tr::lng_settings_save(), [=] { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); save(take()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp index e59ef7061d..9c4bcf11bb 100644 --- a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -220,8 +220,8 @@ void EditShortcutNameBox( if (!ValidShortcutName(name)) { field->showError(); } else { - submit(name, [weak = Ui::MakeWeak(box)] { - if (const auto strong = weak.data()) { + submit(name, [weak = base::make_weak(box)] { + if (const auto strong = weak.get()) { strong->closeBox(); } }); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index ce93f3d09a..9205b90978 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -178,7 +178,7 @@ private: bool cornerButtonsUnreadMayBeShown() override; bool cornerButtonsHas(CornerButtonType type) override; - QPointer createPinnedToBottom( + base::weak_qptr createPinnedToBottom( not_null parent) override; void setupComposeControls(); void processScroll(); @@ -773,7 +773,7 @@ void ShortcutMessages::setupComposeControls() { }, lifetime()); } -QPointer ShortcutMessages::createPinnedToBottom( +base::weak_qptr ShortcutMessages::createPinnedToBottom( not_null parent) { auto placeholder = rpl::deferred([=] { return _shortcutId.value(); diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp index 6e0bf6f504..00e4c291c0 100644 --- a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -255,12 +255,12 @@ void EditTimeBox( }); box->addButton(tr::lng_settings_save(), [=] { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); save(std::clamp( ((*minutesStart) / 60 + minutes->current()->index()) * 60, low, high)); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -398,9 +398,9 @@ void EditDayBox( AddDividerText(container, tr::lng_hours_about_day()); box->addButton(tr::lng_settings_save(), [=] { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); save(state->data.current()); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -457,9 +457,9 @@ void ChooseTimezoneBox( }); } group->setChangedCallback([=](int index) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); save(list[index].id); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp index e8bb2607ab..87e0a2a4d0 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.cpp @@ -80,7 +80,7 @@ BottomButton CreateBottomDisableButton( divider->show(); return { - .content = Ui::MakeWeak(not_null{ content }), + .content = base::make_weak(content), .isBottomFillerShown = divider->geometryValue( ) | rpl::map([](const QRect &r) { return r.height() > 0; @@ -126,29 +126,25 @@ void SetupHeader( Ui::AddSkip(content); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - std::move(subtitle), - st::changePhoneTitle)), - st::changePhoneTitlePadding); + std::move(subtitle), + st::changePhoneTitle), + st::changePhoneTitlePadding, + style::al_top); { const auto &st = st::settingLocalPasscodeDescription; - const auto wrap = content->add( - object_ptr>( + const auto description = content->add( + object_ptr( content, - object_ptr( - content, - v::text::take_marked(std::move(about)), - st, - st::defaultPopupMenu)), - st::changePhoneDescriptionPadding); - wrap->setAttribute(Qt::WA_TransparentForMouseEvents); - wrap->resize( - wrap->width(), - st::settingLocalPasscodeDescriptionHeight); + v::text::take_marked(std::move(about)), + st, + st::defaultPopupMenu), + st::changePhoneDescriptionPadding, + style::al_top); + description->setTryMakeSimilarLines(true); + description->setAttribute(Qt::WA_TransparentForMouseEvents); } } @@ -174,24 +170,24 @@ not_null AddPasswordField( return field; } -not_null*> AddWrappedField( +not_null AddWrappedField( not_null content, rpl::producer &&placeholder, const QString &text) { - return content->add(object_ptr>( - content, + return content->add( object_ptr( content, st::settingLocalPasscodeInputField, std::move(placeholder), - text))); + text), + style::al_top); } not_null AddLinkButton( - not_null*> wrap, + not_null input, rpl::producer &&text) { const auto button = Ui::CreateChild( - wrap->parentWidget(), + input->parentWidget(), QString()); std::move( text @@ -199,9 +195,8 @@ not_null AddLinkButton( button->setText(text); }, button->lifetime()); - wrap->geometryValue( + input->geometryValue( ) | rpl::start_with_next([=](QRect r) { - r.translate(wrap->entity()->pos().x(), 0); button->moveToLeft(r.x(), r.y() + r.height() + st::passcodeTextLine); }, button->lifetime()); return button; @@ -211,14 +206,12 @@ not_null AddError( not_null content, Ui::PasswordInput *input) { const auto error = content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - // Set any text to resize. - tr::lng_language_name(tr::now), - st::settingLocalPasscodeError)), - st::changePhoneDescriptionPadding)->entity(); + QString(), + st::settingLocalPasscodeError), + st::changePhoneDescriptionPadding, + style::al_top); error->hide(); if (input) { QObject::connect(input, &Ui::MaskedInputField::changed, [=] { @@ -232,13 +225,12 @@ not_null AddDoneButton( not_null content, rpl::producer &&text) { const auto button = content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - std::move(text), - st::changePhoneButton)), - st::settingLocalPasscodeButtonPadding)->entity(); + std::move(text), + st::changePhoneButton), + st::settingLocalPasscodeButtonPadding, + style::al_top); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); return button; } diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h index a87c1aaed5..b7480ce11c 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h @@ -11,8 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/box_content_divider.h" namespace Ui { -template -class CenterWrap; class FlatLabel; class InputField; class LinkButton; @@ -61,7 +59,7 @@ void SetupHeader( rpl::producer &&placeholder, const QString &text); -[[nodiscard]] not_null*> AddWrappedField( +[[nodiscard]] not_null AddWrappedField( not_null content, rpl::producer &&placeholder, const QString &text); @@ -75,14 +73,14 @@ void SetupHeader( rpl::producer &&text); [[nodiscard]] not_null AddLinkButton( - not_null*> wrap, + not_null input, rpl::producer &&text); void AddSkipInsteadOfField(not_null content); void AddSkipInsteadOfError(not_null content); struct BottomButton { - QPointer content; + base::weak_qptr content; rpl::producer isBottomFillerShown; }; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.cpp index cbc9db4fde..f43096d9ad 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email.cpp @@ -84,11 +84,10 @@ void Email::setupContent() { Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip); - const auto wrap = AddWrappedField( + const auto newInput = AddWrappedField( content, tr::lng_cloud_password_email(), currentStepDataEmail); - const auto newInput = wrap->entity(); const auto error = AddError(content, nullptr); newInput->changes( ) | rpl::start_with_next([=] { @@ -173,7 +172,7 @@ void Email::setupContent() { }; const auto skip = AddLinkButton( - wrap, + newInput, tr::lng_cloud_password_skip_email()); skip->setClickedCallback([=] { confirm(QString()); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp index 2e482f7619..e9e5e799c1 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp @@ -155,11 +155,9 @@ void EmailConfirm::setupContent() { content, st::settingLocalPasscodeInputField, tr::lng_change_phone_code_title()); - const auto newInput = objectInput.data(); - const auto wrap = content->add( - object_ptr>( - content, - std::move(objectInput))); + const auto newInput = content->add( + std::move(objectInput), + style::al_top); const auto error = AddError(content, nullptr); newInput->changes( @@ -184,7 +182,9 @@ void EmailConfirm::setupContent() { } }, resendInfo->lifetime()); - const auto resend = AddLinkButton(wrap, tr::lng_cloud_password_resend()); + const auto resend = AddLinkButton( + newInput, + tr::lng_cloud_password_resend()); resend->setClickedCallback([=] { if (_requestLifetime) { return; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.cpp index 7f59c7a936..b505ffa084 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_hint.cpp @@ -75,11 +75,10 @@ void Hint::setupContent() { Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip); - const auto wrap = AddWrappedField( + const auto newInput = AddWrappedField( content, tr::lng_cloud_password_hint(), currentStepDataHint); - const auto newInput = wrap->entity(); const auto error = AddError(content, nullptr); newInput->changes( ) | rpl::start_with_next([=] { @@ -122,7 +121,7 @@ void Hint::setupContent() { }; AddLinkButton( - wrap, + newInput, tr::lng_settings_cloud_password_skip_hint() )->setClickedCallback([=] { save(QString()); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp index 7719fc2ddd..2986c05c4e 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp @@ -126,7 +126,7 @@ public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer title() override; - [[nodiscard]] QPointer createPinnedToTop( + [[nodiscard]] base::weak_qptr createPinnedToTop( not_null parent) override; void setupContent(); void setupValidateGood(); @@ -156,7 +156,7 @@ rpl::producer Input::title() { return tr::lng_settings_cloud_password_password_title(); } -QPointer Input::createPinnedToTop( +base::weak_qptr Input::createPinnedToTop( not_null parent) { _parent = parent; return nullptr; @@ -170,11 +170,8 @@ void Input::setupValidateGood() { } if (auto owned = CreateValidateGoodIcon(&controller()->session())) { - owned->setParent(content); content->add( - object_ptr>( - content, - std::move(owned)), + std::move(owned), QMargins(0, st::lineWidth * 75, 0, 0)); } @@ -254,10 +251,10 @@ void Input::setupContent() { const auto newInput = AddPasswordField( content, - isCheck + (isCheck ? tr::lng_cloud_password_enter_old() - : tr::lng_cloud_password_enter_new(), - currentStepDataPassword); + : tr::lng_cloud_password_enter_new()), + currentStepDataPassword); const auto reenterInput = isCheck ? (Ui::PasswordInput*)(nullptr) : AddPasswordField( @@ -280,9 +277,12 @@ void Input::setupContent() { tr::lng_signin_hint(tr::now, lt_password_hint, hint), st::defaultFlatLabel); hintInfo->setVisible(!hint.isEmpty()); - error->geometryValue( - ) | rpl::start_with_next([=](const QRect &r) { - hintInfo->setGeometry(r); + rpl::combine( + error->geometryValue(), + newInput->geometryValue() + ) | rpl::start_with_next([=](QRect r, QRect input) { + hintInfo->setGeometry( + { input.x(), r.y(), input.width(), r.height() }); }, hintInfo->lifetime()); error->shownValue( ) | rpl::start_with_next([=](bool shown) { diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.cpp index ee2de55865..eccada08f8 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email.cpp @@ -63,11 +63,10 @@ void LoginEmail::setupContent() { Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip); - const auto wrap = AddWrappedField( + const auto newInput = AddWrappedField( content, tr::lng_settings_cloud_login_email_placeholder(), QString()); - const auto newInput = wrap->entity(); const auto error = AddError(content, nullptr); newInput->changes() | rpl::start_with_next([=] { error->hide(); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.cpp index 211c6dff48..e3213f95a7 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_login_email_confirm.cpp @@ -106,11 +106,9 @@ void LoginEmailConfirm::setupContent() { Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip); - const auto wrap = content->add( - object_ptr>( - content, - object_ptr(content))); - const auto newInput = wrap->entity(); + const auto newInput = content->add( + object_ptr(content), + style::al_top); newInput->setDigitsCountMax(currentStepDataCodeLength); Ui::AddSkip(content); @@ -124,14 +122,14 @@ void LoginEmailConfirm::setupContent() { newInput->setFocus(); newInput->showError(); } else { - const auto weak = Ui::MakeWeak(controller()->content()); + const auto weak = base::make_weak(controller()->content()); const auto done = [=] { _api.reset(); _processFinishes.fire({}); cloudPassword().reload(); setStepData(StepData()); showBack(); - if (const auto strong = weak) { + if (const auto strong = weak.get()) { Ui::StartFireworks(strong); } }; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp index 1f7a876738..1c6c484ef6 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp @@ -53,7 +53,7 @@ public: [[nodiscard]] rpl::producer title() override; void setupContent(); - [[nodiscard]] QPointer createPinnedToBottom( + [[nodiscard]] base::weak_qptr createPinnedToBottom( not_null parent) override; protected: @@ -184,7 +184,7 @@ void Manage::setupContent() { Ui::ResizeFitChild(this, content); } -QPointer Manage::createPinnedToBottom( +base::weak_qptr Manage::createPinnedToBottom( not_null parent) { const auto disable = [=](Fn close) { diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp index 7ea244e8ff..d8441c5a8f 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp @@ -67,6 +67,7 @@ object_ptr CreateValidateGoodIcon( }, widget->lifetime()); const auto padding = st::settingLocalPasscodeIconPadding; widget->resize((Rect(Size(size)) + padding).size()); + widget->setNaturalWidth(padding.left() + size + padding.right()); return owned; } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index b153dbe371..b7b39b372a 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -286,6 +286,7 @@ chatThemeEntryMargin: margins(16px, 10px, 16px, 8px); chatThemeEmptyPreviewTop: 16px; chatThemeEmojiBottom: 12px; chatThemeButtonMargin: margins(10px, 0px, 10px, 8px); +chatThemeGiftTaken: icon {{ "mediaview/mini_repost", msgFileInBg }}; autoDownloadLimitButton: SettingsButton(settingsButtonNoIcon) { padding: margins(22px, 10px, 22px, 0px); @@ -302,7 +303,7 @@ settingsAccentColorSkip: 4px; settingsAccentColorLine: 3px; settingsFilterDividerLabel: FlatLabel(boxDividerLabel) { - minWidth: 258px; + minWidth: 200px; maxHeight: 0px; align: align(top); } @@ -310,6 +311,8 @@ settingsFilterDividerLabelPadding: margins(0px, 16px, 0px, 22px); settingsFilterIconSize: 74px; settingsFilterIconPadding: margins(0px, 17px, 0px, 5px); +settingsFilterTagPreviewSkip: 14px; + settingsDeviceName: InputField(defaultInputField) { textBg: transparent; textMargins: margins(1px, 3px, 1px, 4px); @@ -597,8 +600,11 @@ settingsColorButton: SettingsButton(settingsButton) { settingsColorRadioMargin: 17px; settingsColorRadioSkip: 13px; settingsColorRadioStroke: 2px; -settingsLevelBadgeLock: icon {{ "chat/mini_lock", premiumButtonFg }}; -settingsLevelBadgeLockSkip: 4px; +settingsLevelBadgeLock: IconEmoji{ + icon: icon {{ "chat/mini_lock", premiumButtonFg }}; + padding: margins(0px, 4px, 0px, 0px); + useIconColor: true; +} messagePrivacyTopSkip: 8px; messagePrivacyRadioSkip: 6px; @@ -705,3 +711,18 @@ settingsCreditsButton: SettingsButton(settingsButton) { } settingsButtonIconGift: icon {{ "settings/gift", menuIconColor }}; settingsButtonIconEarn: icon {{ "settings/earn", menuIconColor }}; + +settingsAgeVerifyBox: Box(filterInviteBox) { + buttonPadding: margins(24px, 24px, 24px, 24px); +} +settingsAgeVerifyText: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + align: align(top); +} +settingsAgeVerifyMargin: margins(0px, 6px, 0px, 6px); +settingsAgeVerifyTitle: FlatLabel(boxTitle) { + align: align(top); +} +settingsAgeVerifyIcon: icon {{ "settings/filled_verify_age-48x48", windowFgActive }}; +settingsAgeVerifyIconPadding: margins(16px, 16px, 16px, 16px); +settingsAgeVerifyIconMargin: margins(0px, 24px, 0px, 14px); diff --git a/Telegram/SourceFiles/settings/settings_active_sessions.cpp b/Telegram/SourceFiles/settings/settings_active_sessions.cpp index 2b8368b4df..c358456083 100644 --- a/Telegram/SourceFiles/settings/settings_active_sessions.cpp +++ b/Telegram/SourceFiles/settings/settings_active_sessions.cpp @@ -418,48 +418,31 @@ void SessionInfoBox( shown->fire({}); }); - const auto userpicWrap = box->addRow( - object_ptr(box, st::sessionBigUserpicSize), - st::sessionBigCoverPadding); const auto big = GenerateUserpicBig( - userpicWrap, + box, shown->events(), TypeFromEntry(data)); - userpicWrap->sizeValue( - ) | rpl::start_with_next([=](QSize size) { - big->move((size.width() - big->width()) / 2, 0); - }, userpicWrap->lifetime()); + big->setNaturalWidth(big->width()); + box->addRow( + object_ptr::fromRaw(big), + st::sessionBigCoverPadding, + style::al_top); - const auto nameWrap = box->addRow( - object_ptr( + box->addRow( + object_ptr( box, - st::sessionBigName.maxHeight)); - const auto name = Ui::CreateChild( - nameWrap, - rpl::single(data.name), - st::sessionBigName); - nameWrap->widthValue( - ) | rpl::start_with_next([=](int width) { - name->resizeToWidth(width); - name->move((width - name->width()) / 2, 0); - }, name->lifetime()); + rpl::single(data.name), + st::sessionBigName), + style::al_top); - const auto dateWrap = box->addRow( - object_ptr( + box->addRow( + object_ptr( box, - st::sessionDateLabel.style.font->height), - style::margins(0, 0, 0, st::sessionDateSkip)); - const auto date = Ui::CreateChild( - dateWrap, - rpl::single( - langDateTimeFull(base::unixtime::parse(data.activeTime))), - st::sessionDateLabel); - rpl::combine( - dateWrap->widthValue(), - date->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - date->move((outer - inner) / 2, 0); - }, date->lifetime()); + rpl::single( + langDateTimeFull(base::unixtime::parse(data.activeTime))), + st::sessionDateLabel), + style::margins(0, 0, 0, st::sessionDateSkip), + style::al_top); using namespace Settings; const auto container = box->verticalLayout(); @@ -496,7 +479,7 @@ void SessionInfoBox( box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); if (const auto hash = data.hash) { box->addLeftButton(tr::lng_sessions_terminate(), [=] { - const auto weak = Ui::MakeWeak(box.get()); + const auto weak = base::make_weak(box.get()); terminate(hash); if (weak) { box->closeBox(); @@ -652,7 +635,7 @@ private: Full _data; object_ptr _inner; - QPointer _terminateBox; + base::weak_qptr _terminateBox; base::Timer _shortPollTimer; @@ -843,12 +826,12 @@ void SessionsContent::terminate(Fn terminateRequest, QString message) { .confirmText = tr::lng_settings_reset_button(), .confirmStyle = &st::attentionBoxButton, }); - _terminateBox = Ui::MakeWeak(box.data()); + _terminateBox = base::make_weak(box.data()); _controller->show(std::move(box)); } void SessionsContent::terminateOne(uint64 hash) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); auto callback = [=] { auto done = crl::guard(weak, [=](const MTPBool &result) { if (mtpIsFalse(result)) { @@ -877,7 +860,7 @@ void SessionsContent::terminateOne(uint64 hash) { } void SessionsContent::terminateAll() { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); auto callback = [=] { const auto reset = crl::guard(weak, [=] { _authorizations->cancelCurrentRequest(); diff --git a/Telegram/SourceFiles/settings/settings_blocked_peers.cpp b/Telegram/SourceFiles/settings/settings_blocked_peers.cpp index c0b2a47463..3ba6000a54 100644 --- a/Telegram/SourceFiles/settings/settings_blocked_peers.cpp +++ b/Telegram/SourceFiles/settings/settings_blocked_peers.cpp @@ -40,15 +40,13 @@ Blocked::Blocked( { auto padding = st::changePhoneIconPadding; padding.setBottom(padding.top()); - _loading = base::make_unique_q>( + _loading = base::make_unique_q>( this, - object_ptr>( + object_ptr( this, - object_ptr( - this, - tr::lng_contacts_loading(), - st::changePhoneDescription), - std::move(padding))); + tr::lng_contacts_loading(), + st::changePhoneDescription), + std::move(padding)); Ui::ResizeFitChild( this, _loading.get(), @@ -73,7 +71,7 @@ rpl::producer Blocked::title() { return tr::lng_settings_blocked_users(); } -QPointer Blocked::createPinnedToTop(not_null parent) { +base::weak_qptr Blocked::createPinnedToTop(not_null parent) { const auto content = Ui::CreateChild(parent.get()); Ui::AddSkip(content); @@ -118,7 +116,7 @@ QPointer Blocked::createPinnedToTop(not_null parent) { }, subtitle->lifetime()); } - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } void Blocked::setupContent() { @@ -186,22 +184,20 @@ void Blocked::setupContent() { }, content->lifetime()); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - tr::lng_blocked_list_empty_title(), - st::changePhoneTitle)), - st::changePhoneTitlePadding); + tr::lng_blocked_list_empty_title(), + st::changePhoneTitle), + st::changePhoneTitlePadding, + style::al_top); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - tr::lng_blocked_list_empty_description(), - st::changePhoneDescription)), - st::changePhoneDescriptionPadding); + tr::lng_blocked_list_empty_description(), + st::changePhoneDescription), + st::changePhoneDescriptionPadding, + style::al_top); Ui::AddSkip(content, st::settingsBlockedListIconPadding.top()); } diff --git a/Telegram/SourceFiles/settings/settings_blocked_peers.h b/Telegram/SourceFiles/settings/settings_blocked_peers.h index b534cd87ec..e8a8e7c2da 100644 --- a/Telegram/SourceFiles/settings/settings_blocked_peers.h +++ b/Telegram/SourceFiles/settings/settings_blocked_peers.h @@ -29,7 +29,7 @@ public: [[nodiscard]] rpl::producer title() override; - [[nodiscard]] QPointer createPinnedToTop( + [[nodiscard]] base::weak_qptr createPinnedToTop( not_null parent) override; private: diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp index a59c30b63f..384a11faaf 100644 --- a/Telegram/SourceFiles/settings/settings_business.cpp +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -41,7 +41,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. #include "ui/widgets/gradient_round_button.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -332,9 +331,9 @@ public: [[nodiscard]] rpl::producer title() override; - [[nodiscard]] QPointer createPinnedToTop( + [[nodiscard]] base::weak_qptr createPinnedToTop( not_null parent) override; - [[nodiscard]] QPointer createPinnedToBottom( + [[nodiscard]] base::weak_qptr createPinnedToBottom( not_null parent) override; void showFinished() override; @@ -524,24 +523,23 @@ void Business::setupContent() { const auto session = &_controller->session(); { - const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); inner->add(object_ptr( inner, - Ui::CreateLabelWithCustomEmoji( + object_ptr( inner, tr::lng_business_about_sponsored( lt_link, rpl::combine( tr::lng_business_about_sponsored_link( lt_emoji, - rpl::single(arrow), + rpl::single(Ui::Text::IconEmoji( + &st::textMoreIconEmoji)), Ui::Text::RichLangValue), tr::lng_business_about_sponsored_url() ) | rpl::map([](TextWithEntities text, QString url) { return Ui::Text::Link(text, url); }), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), st::boxDividerLabel), st::defaultBoxDividerLabelPadding, RectPart::Top | RectPart::Bottom)); @@ -582,7 +580,7 @@ void Business::setupContent() { Ui::ResizeFitChild(this, content); } -QPointer Business::createPinnedToTop( +base::weak_qptr Business::createPinnedToTop( not_null parent) { auto title = tr::lng_business_title(); auto about = [&]() -> rpl::producer { @@ -671,14 +669,14 @@ QPointer Business::createPinnedToTop( } }, content->lifetime()); - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } void Business::showFinished() { _showFinished.fire({}); } -QPointer Business::createPinnedToBottom( +base::weak_qptr Business::createPinnedToBottom( not_null parent) { const auto content = Ui::CreateChild(parent.get()); @@ -747,7 +745,7 @@ QPointer Business::createPinnedToBottom( _subscribe->setVisible(!premium && premiumPossible); }, _subscribe->lifetime()); - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } } // namespace diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 16d3f3a249..d80f394a24 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -483,7 +483,7 @@ void ChooseMediaDeviceBox( state->currentId = std::move(currentId); const auto choose = [=](const QString &id) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); chosen(id); if (weak) { box->closeBox(); diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index d7f6139220..67ab82a18e 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -227,14 +227,13 @@ void AddDividerTextWithLottie( if (descriptor.about) { verticalLayout->add( - object_ptr>( + object_ptr( verticalLayout, - object_ptr( - verticalLayout, - std::move(descriptor.about), - st::settingsFilterDividerLabel)), + std::move(descriptor.about), + st::settingsFilterDividerLabel), descriptor.aboutMargins.value_or( - st::settingsFilterDividerLabelPadding)); + st::settingsFilterDividerLabelPadding), + style::al_top)->setTryMakeSimilarLines(true); } verticalLayout->geometryValue( diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 6a9e214951..bf4102f391 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -88,11 +88,11 @@ public: [[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const { return nullptr; } - [[nodiscard]] virtual QPointer createPinnedToTop( + [[nodiscard]] virtual base::weak_qptr createPinnedToTop( not_null parent) { return nullptr; } - [[nodiscard]] virtual QPointer createPinnedToBottom( + [[nodiscard]] virtual base::weak_qptr createPinnedToBottom( not_null parent) { return nullptr; } diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 1ed8c28c51..2fd4e114d2 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits.h" #include "api/api_credits.h" +#include "api/api_earn.h" +#include "api/api_statistics.h" #include "base/call_delayed.h" -#include "boxes/star_gift_box.h" #include "boxes/gift_credits_box.h" #include "boxes/gift_premium_box.h" +#include "boxes/star_gift_box.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "core/click_handler_types.h" #include "data/components/credits.h" @@ -25,22 +27,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #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/info_memento.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" -#include "info/info_memento.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common_session.h" #include "settings/settings_credits_graphics.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/boxes/boost_box.h" // Ui::StartFireworks. +#include "ui/effects/animation_value_f.h" #include "ui/effects/credits_graphics.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.h" -#include "ui/text/format_values.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/custom_emoji_instance.h" +#include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" @@ -73,7 +78,7 @@ public: [[nodiscard]] rpl::producer title() override; - [[nodiscard]] QPointer createPinnedToTop( + [[nodiscard]] base::weak_qptr createPinnedToTop( not_null parent) override; void showFinished() override; @@ -419,13 +424,12 @@ void Credits::setupContent() { { const auto button = content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - nullptr, - st::creditsSettingsBigBalanceButton)), - st::boxRowPadding)->entity(); + nullptr, + st::creditsSettingsBigBalanceButton), + st::boxRowPadding, + style::al_top); button->setContext([&]() -> Ui::Text::MarkedContext { auto customEmojiFactory = [=](const auto &...) { const auto &icon = st::settingsIconAdd; @@ -444,9 +448,9 @@ void Credits::setupContent() { p.drawEllipse(r); icon.paintInCenter(p, r, st::windowBgActive->c); } - return std::make_unique( - std::move(image), - u"topup_button"_q); + return std::make_unique( + u"topup_button"_q, + std::move(image)); }; return { .customEmojiFactory = std::move(customEmojiFactory) }; }()); @@ -489,11 +493,11 @@ void Credits::setupContent() { auto customEmojiFactory = [=](const auto &...) { return std::make_unique( isCurrency - ? std::make_unique( + ? std::make_unique( + u"currency_icon:%1"_q.arg(height), Ui::Earn::IconCurrencyColored( st::tonFieldIconSize, - st::currencyFg->c), - u"currency_icon:%1"_q.arg(height)) + st::currencyFg->c)) : Ui::MakeCreditsIconEmoji(height, 1), isCurrency ? QPoint(0, st::lineWidth * 2) @@ -502,39 +506,37 @@ void Credits::setupContent() { return { .customEmojiFactory = std::move(customEmojiFactory) }; }(); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - tr::lng_credits_balance_me_count( - lt_emoji, - rpl::single(Ui::MakeCreditsIconEntity()), - lt_amount, - (isCurrency - ? _controller->session().credits().tonBalanceValue() - : _controller->session().credits().balanceValue() - ) | rpl::map( - Lang::FormatCreditsAmountDecimal - ) | rpl::map(Ui::Text::Bold), - Ui::Text::WithEntities), - textSt, - st::defaultPopupMenu, - std::move(context)))); + tr::lng_credits_balance_me_count( + lt_emoji, + rpl::single(Ui::MakeCreditsIconEntity()), + lt_amount, + (isCurrency + ? _controller->session().credits().tonBalanceValue() + : _controller->session().credits().balanceValue() + ) | rpl::map( + Lang::FormatCreditsAmountDecimal + ) | rpl::map(Ui::Text::Bold), + Ui::Text::WithEntities), + textSt, + st::defaultPopupMenu, + std::move(context)), + style::al_top); if (isCurrency) { const auto rate = _controller->session().credits().usdRate(); const auto wrap = content->add( object_ptr>( content, - object_ptr>( + object_ptr( content, - object_ptr( - content, - _controller->session().credits().tonBalanceValue( - ) | rpl::map([=](CreditsAmount value) { - using namespace Info::ChannelEarn; - return value ? ToUsd(value, rate, 3) : QString(); - }), - st::channelEarnOverviewSubMinorLabel)))); + _controller->session().credits().tonBalanceValue( + ) | rpl::map([=](CreditsAmount value) { + using namespace Info::ChannelEarn; + return value ? ToUsd(value, rate, 3) : QString(); + }), + st::channelEarnOverviewSubMinorLabel)), + style::al_top); wrap->toggleOn(_controller->session().credits().tonBalanceValue( ) | rpl::map(rpl::mappers::_1 > CreditsAmount(0))); wrap->finishAnimating(); @@ -591,6 +593,136 @@ void Credits::setupContent() { controller->showSection(Info::BotStarRef::Join::Make(self)); }); } + if (isCurrency) { + using namespace Info::ChannelEarn; + const auto fill = [=]( + not_null container, + CreditsAmount value, + float64 multiplier) { + Ui::AddSkip(container); + Ui::AddSkip(container); + + const auto labels = container->add( + object_ptr(container), + style::al_top); + + const auto majorLabel = Ui::CreateChild( + labels, + st::channelEarnBalanceMajorLabel); + { + const auto &m = st::channelEarnCurrencyCommonMargins; + const auto p = QMargins( + m.left(), + -m.top(), + m.right(), + m.bottom()); + AddEmojiToMajor(majorLabel, rpl::single(value), {}, p); + } + majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto minorLabel = Ui::CreateChild( + labels, + MinorPart(value), + st::channelEarnBalanceMinorLabel); + minorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + majorLabel->sizeValue(), + minorLabel->sizeValue() + ) | rpl::start_with_next([=]( + const QSize &majorSize, + const QSize &minorSize) { + labels->resize( + majorSize.width() + minorSize.width(), + majorSize.height()); + labels->setNaturalWidth( + majorSize.width() + minorSize.width()); + majorLabel->moveToLeft(0, 0); + minorLabel->moveToRight( + 0, + st::channelEarnBalanceMinorLabelSkip); + }, labels->lifetime()); + Ui::ToggleChildrenVisibility(labels, true); + + Ui::AddSkip(container); + container->add( + object_ptr( + container, + ToUsd(value, multiplier, 0), + st::channelEarnOverviewSubMinorLabel), + style::al_top); + + Ui::AddSkip(container); + + const auto &stButton = st::creditsSettingsBigBalanceButton; + const auto button = container->add( + object_ptr( + container, + rpl::never(), + stButton), + st::boxRowPadding, + style::al_top); + + const auto label = Ui::CreateChild( + button, + tr::lng_channel_earn_balance_button(tr::now), + st::channelEarnSemiboldLabel); + label->setTextColorOverride(stButton.textFg->c); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + button->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](const QSize &b, const QSize &l) { + label->moveToLeft( + (b.width() - l.width()) / 2, + (b.height() - l.height()) / 2); + }, label->lifetime()); + + const auto colorText = [=](float64 value) { + label->setTextColorOverride( + anim::with_alpha( + stButton.textFg->c, + anim::interpolateF(.5, 1., value))); + }; + const auto withdrawalEnabled = true; + colorText(withdrawalEnabled ? 1. : 0.); + button->setAttribute( + Qt::WA_TransparentForMouseEvents, + !withdrawalEnabled); + + Api::HandleWithdrawalButton( + { .currencyReceiver = self }, + button, + _controller->uiShow()); + Ui::ToggleChildrenVisibility(button, true); + + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_credits_currency_summary_subtitle()); + Ui::AddSkip(container); + }; + + const auto self = _controller->session().user(); + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + const auto apiLifetime = wrap->lifetime().make_state(); + const auto api = apiLifetime->make_state(self); + wrap->toggle(false, anim::type::instant); + api->request() | rpl::start_with_error_done([] { + }, [=] { + if (!api->data().availableBalance.empty()) { + wrap->toggle(true, anim::type::normal); + fill( + wrap->entity(), + api->data().availableBalance, + api->data().usdRate); + content->resizeToWidth(content->width()); + } + }, *apiLifetime); + } if (!isCurrency) { Ui::AddSkip(content, st::lineWidth * 4); @@ -603,7 +735,7 @@ void Credits::setupContent() { Ui::ResizeFitChild(this, content); } -QPointer Credits::createPinnedToTop( +base::weak_qptr Credits::createPinnedToTop( not_null parent) { _parent = parent; const auto isCurrency = _creditsType == CreditsType::Ton; @@ -722,7 +854,7 @@ QPointer Credits::createPinnedToTop( } }, content->lifetime()); - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } void Credits::showFinished() { @@ -799,26 +931,19 @@ Fn BuyStarsHandler::handler( ? _api->options() : Data::CreditTopupOptions(); const auto amount = CreditsAmount(); - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); FillCreditOptions(show, inner, self, amount, [=] { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } if (const auto onstack = paid) { onstack(); } - }, nullptr, options); + }, box->showFinishes(), nullptr, options); - const auto button = box->addButton(tr::lng_close(), [=] { + 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()); }; return crl::guard(this, [=] { if (_api && !_api->options().empty()) { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 2850e85a01..85f29f0f8f 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/earn/earn_format.h" #include "info/channel_statistics/earn/earn_icons.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "info/peer_gifts/info_peer_gifts_widget.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "info/info_controller.h" @@ -62,10 +63,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits.h" #include "statistics/widgets/chart_header_widget.h" #include "ui/boxes/confirm_box.h" +#include "ui/controls/button_labels.h" +#include "ui/controls/ton_common.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/loading_element.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/effects/premium_top_bar.h" @@ -74,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/custom_emoji_helper.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" @@ -82,7 +87,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/fields/number_input.h" #include "ui/widgets/popup_menu.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" #include "ui/wrap/fade_wrap.h" @@ -466,15 +470,15 @@ SubscriptionRightLabel PaintSubscriptionRightLabelCallback( not_null session, const style::PeerListItem &st, int amount) { + auto helper = Ui::Text::CustomEmojiHelper(); + auto starIcon = helper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); const auto text = std::make_shared(); text->setMarkedText( st::semiboldTextStyle, - TextWithEntities() - .append(session->data().customEmojiManager().creditsEmoji()) - .append(QChar::Space) - .append(Lang::FormatCountDecimal(amount)), + starIcon.append(' ').append(Lang::FormatCountDecimal(amount)), kMarkupTextOptions, - Core::TextContext({ .session = session })); + helper.context()); const auto &font = text->style()->font; const auto &statusFont = st::contactsStatusFont; const auto status = tr::lng_group_invite_joined_right(tr::now); @@ -510,6 +514,7 @@ void FillCreditOptions( not_null peer, CreditsAmount minimumCredits, Fn paid, + rpl::producer<> showFinishes, rpl::producer subtitle, std::vector preloadedTopupOptions) { const auto options = container->add( @@ -518,16 +523,77 @@ void FillCreditOptions( object_ptr(container))); const auto content = options->entity(); - Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); - const auto singleStarWidth = Ui::GenerateStars( st::creditsTopupButton.height, 1).width() / style::DevicePixelRatio(); + struct StarsState { + base::flat_map cache; + rpl::variable ready = false; + }; + const auto starsState = content->lifetime().make_state(); + const auto weak = base::make_weak(content); + crl::async([=]() mutable { + constexpr auto kPreloadCount = 10; + auto cache = base::flat_map(); + for (auto i = 1; i <= kPreloadCount; ++i) { + cache[i] = Ui::GenerateStars(st::creditsTopupButton.height, i); + } + crl::on_main(weak, [=, result = std::move(cache)]() mutable { + starsState->cache = std::move(result); + starsState->ready = true; + }); + }); + + const auto loadingContainer = content->add( + object_ptr>( + content, + object_ptr(content))); + loadingContainer->toggle(true, anim::type::instant); + const auto fillLoading = [=] { + Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); + if (subtitle) { + Ui::AddSubsectionTitle(content, std::move(subtitle)); + } + const auto &st = st::creditsTopupButton; + const auto loadingList = content; + const auto isRtl = QLocale().textDirection() == Qt::RightToLeft; + using WidgetPtr = object_ptr; + for (auto i = 0; i < 4; i++) { + auto owned = object_ptr( + loadingList, + rpl::never(), + st); + owned->setAttribute(Qt::WA_TransparentForMouseEvents); + auto loadingLeft = Ui::CreateLoadingTextWidget( + owned, + st.style, + 1, + rpl::single(isRtl)); + content->widthValue( + ) | rpl::start_with_next([=, raw = loadingLeft.get()](int w) { + if (w > 0) { + const auto availableWidth = w + - st.iconLeft + - st::boxRowPadding.right(); + raw->resize(availableWidth, st.style.font->height); + } + }, loadingLeft->lifetime()); + loadingLeft->moveToLeft( + st.iconLeft, + st.padding.top() - st::lineWidth * 2); + owned->lifetime().make_state(std::move(loadingLeft)); + Ui::ToggleChildrenVisibility(owned, true); + loadingList->add(std::move(owned)); + } + content->resizeToWidth(container->width()); + }; + const auto fill = [=](Data::CreditTopupOptions options) { while (content->count()) { delete content->widgetAt(0); } + Ui::AddSkip(content, st::settingsPremiumOptionsPadding.top()); if (subtitle) { Ui::AddSubsectionTitle(content, std::move(subtitle)); } @@ -591,7 +657,14 @@ void FillCreditOptions( Ui::FillAmountAndCurrency(option.amount, option.currency), st::creditsTopupPrice); const auto inner = Ui::CreateChild(button); - const auto stars = Ui::GenerateStars(st.height, (i + 1)); + const auto getStars = [=] { + const auto starIndex = i + 1; + if (starsState->cache.contains(starIndex)) { + return starsState->cache[starIndex]; + } + return Ui::GenerateStars(st.height, starIndex); + }; + const auto stars = getStars(); const auto textLeft = diffBetweenTextAndStar + stars.width() / style::DevicePixelRatio(); inner->paintRequest( @@ -625,9 +698,9 @@ void FillCreditOptions( .giftPeerId = PeerId(option.giftBarePeerId), }; - const auto weak = Ui::MakeWeak(button); + const auto weak = base::make_weak(button); const auto done = [=](Payments::CheckoutResult result) { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->window()->setFocus(); if (result == Payments::CheckoutResult::Paid) { if (const auto onstack = paid) { @@ -665,11 +738,18 @@ void FillCreditOptions( if (show->session().premiumPossible()) { if (preloadedTopupOptions.empty()) { - apiCredits->request( - ) | rpl::start_with_error_done([=](const QString &error) { - show->showToast(error); - }, [=] { - fill(apiCredits->options()); + fillLoading(); + std::move(showFinishes) | rpl::start_with_next([=] { + apiCredits->request( + ) | rpl::start_with_error_done([=](const QString &error) { + show->showToast(error); + }, [=] { + starsState->ready.value() | rpl::filter( + rpl::mappers::_1 + ) | rpl::start_with_next([=] { + fill(apiCredits->options()); + }, content->lifetime()); + }, content->lifetime()); }, content->lifetime()); } else { fill(std::move(preloadedTopupOptions)); @@ -718,19 +798,14 @@ not_null AddBalanceWidget( balanceValue ) | rpl::start_with_next([=](CreditsAmount value) { auto text = TextWithEntities(); - const auto manager = &session->data().customEmojiManager(); + auto helper = Ui::Text::CustomEmojiHelper(); if (value.ton()) { - text.append(Ui::Text::SingleCustomEmoji( - manager->registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::tonFieldIconSize, - st::currencyFg->c), - st::channelEarnCurrencyLearnMargins, - false)) + text.append( + helper.paletteDependent(Ui::Earn::IconCurrencyEmoji()) ).append(' ').append(Lang::FormatCreditsAmountDecimal(value)); } else { text.append( - manager->creditsEmoji() + helper.paletteDependent(Ui::Earn::IconCreditsEmoji()) ).append(' ').append( Lang::FormatCreditsAmountToShort(value).string); } @@ -738,10 +813,7 @@ not_null AddBalanceWidget( st::semiboldTextStyle, text, kMarkupTextOptions, - Core::TextContext({ - .session = session, - .repaint = [=] { balance->update(); }, - })); + helper.context([=] { balance->update(); })); balance->setBalance(value); resize(); }, balance->lifetime()); @@ -776,7 +848,6 @@ void BoostCreditsBox( box->setNoContentMargin(true); const auto content = box->verticalLayout(); - const auto session = &controller->session(); Ui::AddSkip(content); { const auto &stUser = st::premiumGiftsUserpicButton; @@ -797,14 +868,13 @@ void BoostCreditsBox( }, widget->lifetime()); } content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - tr::lng_gift_stars_title( - lt_count, - rpl::single(float64(b.credits))), - st::boxTitle))); + tr::lng_gift_stars_title( + lt_count, + rpl::single(float64(b.credits))), + st::boxTitle), + style::al_top); Ui::AddSkip(content); if (b.multiplier) { const auto &st = st::statisticsDetailsBottomCaptionStyle; @@ -816,21 +886,16 @@ void BoostCreditsBox( - st::boxRowPadding.right()); auto textWithEntities = TextWithEntities(); textWithEntities.append( - Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::boostsListMiniIcon, - { st.font->descent * 2, st.font->descent / 2, 0, 0 }, - true))); - textWithEntities.append( + Ui::Text::IconEmoji(&st::boostsListEntryIcon) + ).append( tr::lng_boosts_list_title(tr::now, lt_count, b.multiplier)); text->setMarkedText( st, std::move(textWithEntities), kMarkupTextOptions, - Core::TextContext({ - .session = session, + Ui::Text::MarkedContext{ .repaint = [=] { badge->update(); }, - })); + }); badge->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(badge); @@ -863,8 +928,7 @@ void BoostCreditsBox( AddCreditsBoostTable(controller->uiShow(), content, {}, b); Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, tr::lng_credits_box_out_about( @@ -873,26 +937,20 @@ void BoostCreditsBox( ) | Ui::Text::ToLink( tr::lng_credits_box_out_about_link(tr::now)), Ui::Text::WithEntities), - st::creditsBoxAboutDivider))); + st::creditsBoxAboutDivider), + style::al_top); Ui::AddSkip(content); - const auto button = box->addButton(tr::lng_box_ok(), [=] { + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); - const auto buttonWidth = st::boxWidth - - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - button->resizeToWidth(buttonWidth); - }, button->lifetime()); } void ProcessReceivedSubscriptions( - QPointer weak, + base::weak_qptr weak, not_null session) { const auto rebuilder = session->data().activeCreditsSubsRebuilder(); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { if (!rebuilder) { return strong->closeBox(); } @@ -903,7 +961,7 @@ void ProcessReceivedSubscriptions( true); api->requestSubscriptions({}, [=](Data::CreditsStatusSlice first) { rebuilder->fire(std::move(first)); - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { strong->closeBox(); } }); @@ -1036,6 +1094,13 @@ void FillUniqueGiftMenu( if (!unique) { return; } + if (unique->canBeTheme) { + menu->addAction(tr::lng_gift_transfer_set_theme(tr::now), [=] { + if (const auto window = show->resolveWindow()) { + SetThemeFromUniqueGift(window, unique); + } + }, st.theme ? st.theme : &st::menuIconChangeColors); + } const auto transfer = savedId && (savedId.isUser() ? e.in : savedId.chat()->canTransferGifts()) && (unique->starsForTransfer >= 0); @@ -1066,8 +1131,8 @@ void FillUniqueGiftMenu( } } if (CanResellGift(&show->session(), e)) { - const auto resalePrice = unique->starsForResale; - const auto editPrice = (resalePrice > 0 + const auto inResale = (unique->starsForResale > 0); + const auto editPrice = (inResale ? tr::lng_gift_transfer_update : tr::lng_gift_transfer_sell)(tr::now); menu->addAction(editPrice, [=] { @@ -1076,12 +1141,12 @@ void FillUniqueGiftMenu( : GiftWearBoxStyleOverride(); ShowUniqueGiftSellBox(show, unique, savedId, style); }, st.resell ? st.resell : &st::menuIconTagSell); - if (resalePrice > 0) { + if (inResale) { menu->addAction(tr::lng_gift_transfer_unlist(tr::now), [=] { const auto name = UniqueGiftName(*unique); const auto confirm = [=](Fn close) { close(); - Ui::UpdateGiftSellPrice(show, unique, savedId, 0); + Ui::UpdateGiftSellPrice(show, unique, savedId, {}); }; show->show(Ui::MakeConfirmBox({ .text = tr::lng_gift_sell_unlist_sure(), @@ -1118,6 +1183,7 @@ CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() { .tableValueMessage = &st::darkGiftTableMessage, .link = &st::darkGiftLink, .share = &st::darkGiftShare, + .theme = &st::darkGiftTheme, .transfer = &st::darkGiftTransfer, .wear = &st::darkGiftNftWear, .takeoff = &st::darkGiftNftTakeOff, @@ -1232,14 +1298,17 @@ void GenericCreditsEntryBox( box->setNoContentMargin(true); const auto slug = uniqueGift->slug; + const auto forceTon = e.giftResaleForceTon; auto price = rpl::single( rpl::empty ) | rpl::then(session->data().giftUpdates( ) | rpl::filter([=](const Data::GiftUpdate &update) { return (update.action == Data::GiftUpdate::Action::ResaleChange) && (update.slug == slug); - }) | rpl::to_empty) | rpl::map([unique = e.uniqueGift] { - return unique->starsForResale; + }) | rpl::to_empty) | rpl::map([forceTon, unique = e.uniqueGift] { + return forceTon + ? Data::UniqueGiftResaleTon(*unique) + : Data::UniqueGiftResaleAsked(*unique); }); auto change = [=] { const auto style = st.giftWearBox @@ -1270,10 +1339,10 @@ void GenericCreditsEntryBox( Ui::PreloadUniqueGiftResellPrices(session); } } else if (const auto callback = Ui::PaintPreviewCallback(session, e)) { - const auto thumb = content->add(object_ptr>( - content, - GenericEntryPhoto(content, callback, stUser.photoSize))); - AddViewMediaHandler(thumb->entity(), show, e); + const auto thumb = content->add( + GenericEntryPhoto(content, callback, stUser.photoSize), + style::al_top); + AddViewMediaHandler(thumb, show, e); } else if (s.photoId || (e.photoId && !e.subscriptionUntil.isNull())) { if (!(s.cancelled || s.expired || s.cancelledByBot)) { const auto widget = Ui::CreateChild(content); @@ -1286,18 +1355,21 @@ void GenericCreditsEntryBox( owner->photo(photoId), std::move(update)); }; - content->add(object_ptr>( - content, - GenericEntryPhoto(content, callback, stUser.photoSize))); - } else if (peer && !e.gift && !e.premiumMonthsForStars) { + content->add( + GenericEntryPhoto(content, callback, stUser.photoSize), + style::al_top); + } else if (peer + && !e.gift + && !e.premiumMonthsForStars + && !e.postsSearch) { if (e.subscriptionUntil.isNull() && s.until.isNull()) { - content->add(object_ptr>( - content, - object_ptr(content, peer, stUser))); + content->add( + object_ptr(content, peer, stUser), + style::al_top); } else { - content->add(object_ptr>( - content, - SubscriptionUserpic(content, peer, stUser.photoSize))); + content->add( + SubscriptionUserpic(content, peer, stUser.photoSize), + style::al_top); } } else if (e.gift || isPrize || e.premiumMonthsForStars) { struct State final { @@ -1375,15 +1447,15 @@ void GenericCreditsEntryBox( ? st::creditsHistoryEntryStarGiftSkip : st::creditsHistoryEntryGiftStickerSkip); }, icon->lifetime()); - } else { + } else if (!e.postsSearch) { const auto widget = content->add( - object_ptr>( - content, - object_ptr(content)))->entity(); + object_ptr(content), + style::al_top); using Draw = Fn; const auto draw = widget->lifetime().make_state( Ui::GenerateCreditsPaintUserpicCallback(e)); widget->resize(Size(stUser.photoSize)); + widget->setNaturalWidth(stUser.photoSize); widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); @@ -1395,8 +1467,7 @@ void GenericCreditsEntryBox( Ui::AddSkip(content); Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, rpl::single(!s.title.isEmpty() @@ -1412,6 +1483,8 @@ void GenericCreditsEntryBox( tr::now, lt_count, e.paidMessagesCount) + : e.postsSearch + ? tr::lng_credits_box_history_entry_posts_search(tr::now) : e.premiumMonthsForStars ? tr::lng_premium_summary_title(tr::now) : !e.title.isEmpty() @@ -1436,23 +1509,23 @@ void GenericCreditsEntryBox( : (peer && !e.reaction) ? peer->name() : Ui::GenerateEntryName(e).text), - st::creditsBoxAboutTitle))); + st::creditsBoxAboutTitle), + style::al_top); Ui::AddSkip(content); } if (e.bareGiftReleasedById && !e.uniqueGift) { const auto peer = owner->peer(PeerId(e.bareGiftReleasedById)); const auto released = content->add( - object_ptr>( - box, - object_ptr( - content, - tr::lng_credits_box_history_entry_gift_released( - lt_name, - rpl::single(Ui::Text::Link('@' + peer->username())), - Ui::Text::WithEntities), - st::creditsReleasedByLabel))); - released->entity()->setClickHandlerFilter([=](const auto &...) { + object_ptr( + content, + tr::lng_credits_box_history_entry_gift_released( + lt_name, + rpl::single(Ui::Text::Link('@' + peer->username())), + Ui::Text::WithEntities), + st::creditsReleasedByLabel), + style::al_top); + released->setClickHandlerFilter([=](const auto &...) { Ui::GiftReleasedByHandler(peer); return false; }); @@ -1478,10 +1551,9 @@ void GenericCreditsEntryBox( object_ptr( content, st::defaultTextStyle.font->height)); - const auto context = Core::TextContext({ - .session = session, - .repaint = [=] { amount->update(); }, - }); + auto helper = Ui::Text::CustomEmojiHelper(); + const auto starEmoji = helper.paletteDependent( + Ui::Earn::IconCreditsEmoji()); if (e.soldOutInfo) { text->setText( st::defaultTextStyle, @@ -1492,12 +1564,12 @@ void GenericCreditsEntryBox( tr::lng_credits_subscription_subtitle( tr::now, lt_emoji, - owner->customEmojiManager().creditsEmoji(), + starEmoji, lt_cost, { QString::number(s.subscription.credits) }, Ui::Text::WithEntities), kMarkupTextOptions, - context); + helper.context([=] { amount->update(); })); } else if (e.credits.stars()) { auto t = TextWithEntities() .append((e.in && (creditsHistoryStarGift || !isStarGift)) @@ -1507,12 +1579,12 @@ void GenericCreditsEntryBox( : kMinus) .append(Lang::FormatCreditsAmountDecimal(e.credits.abs())) .append(QChar(' ')) - .append(owner->customEmojiManager().creditsEmoji()); + .append(starEmoji); text->setMarkedText( st::semiboldTextStyle, std::move(t), kMarkupTextOptions, - context); + helper.context([=] { amount->update(); })); } else if (e.credits.ton()) { auto t = TextWithEntities() .append((e.in ? QChar('+') : kMinus)) @@ -1521,27 +1593,18 @@ void GenericCreditsEntryBox( st::channelEarnHistoryMajorLabel.style, std::move(t), kMarkupTextOptions, - context); + helper.context([=] { amount->update(); })); auto minor = TextWithEntities() .append(Info::ChannelEarn::MinorPart(e.credits.abs())) .append(QChar(' ')) - .append( - Ui::Text::SingleCustomEmoji( - owner->customEmojiManager().registerInternalEmoji( - Ui::Earn::IconCurrencyColored( - st::tonFieldIconSize, - (e.in - ? st::boxTextFgGood->c - : st::menuIconAttentionColor->c)), - QMargins(0, st::lineWidth * 1, 0, 0), - false))); + .append(Ui::Text::IconEmoji(&st::tonIconEmojiInSmall)); minorText = lifetime.make_state(); minorText->setMarkedText( st::channelEarnHistoryMinorLabel.style, std::move(minor), kMarkupTextOptions, - context); + helper.context([=] { amount->update(); })); } const auto font = text->style()->font; const auto roundedFont = st::defaultTextStyle.font; @@ -1619,68 +1682,67 @@ void GenericCreditsEntryBox( if (!isStarGift && !e.description.empty()) { Ui::AddSkip(content); - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, rpl::single(e.description), - st::creditsBoxAbout))); + st::creditsBoxAbout), + style::al_top); } const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); if (!uniqueGift && (starGiftCanManage || e.converted)) { Ui::AddSkip(content); const auto about = box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - (e.giftRefunded - ? tr::lng_action_gift_refunded( - Ui::Text::RichLangValue) - : e.starsUpgradedBySender - ? tr::lng_action_gift_got_upgradable_text( - Ui::Text::RichLangValue) - : (e.starsToUpgrade - && giftToSelf - && !e.giftTransferred) - ? tr::lng_action_gift_self_about_unique( - Ui::Text::WithEntities) - : (e.starsToUpgrade - && giftToChannelCanManage - && !e.giftTransferred) - ? tr::lng_action_gift_channel_about_unique( - Ui::Text::WithEntities) - : ((canConvert || e.converted) - ? rpl::combine( - (canConvert - ? (giftToSelf - ? tr::lng_action_gift_self_about - : giftToChannelCanTransfer - ? tr::lng_action_gift_channel_about - : tr::lng_action_gift_got_stars_text) - : (giftToChannel - ? tr::lng_gift_channel_got - : 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)); - }) - : (e.savedToProfile - ? (giftToChannel - ? tr::lng_action_gift_can_remove_channel - : tr::lng_action_gift_can_remove_text) + (e.giftRefunded + ? tr::lng_action_gift_refunded( + Ui::Text::RichLangValue) + : e.starsUpgradedBySender + ? tr::lng_action_gift_got_upgradable_text( + Ui::Text::RichLangValue) + : (e.starsToUpgrade + && giftToSelf + && !e.giftTransferred) + ? tr::lng_action_gift_self_about_unique( + Ui::Text::WithEntities) + : (e.starsToUpgrade + && giftToChannelCanManage + && !e.giftTransferred) + ? tr::lng_action_gift_channel_about_unique( + Ui::Text::WithEntities) + : ((canConvert || e.converted) + ? rpl::combine( + (canConvert + ? (giftToSelf + ? tr::lng_action_gift_self_about + : giftToChannelCanTransfer + ? tr::lng_action_gift_channel_about + : tr::lng_action_gift_got_stars_text) : (giftToChannel - ? tr::lng_action_gift_got_gift_channel - : tr::lng_action_gift_got_gift_text))( - Ui::Text::WithEntities))), - st::creditsBoxAbout)))->entity(); + ? tr::lng_gift_channel_got + : 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)); + }) + : (e.savedToProfile + ? (giftToChannel + ? tr::lng_action_gift_can_remove_channel + : tr::lng_action_gift_can_remove_text) + : (giftToChannel + ? tr::lng_action_gift_got_gift_channel + : tr::lng_action_gift_got_gift_text))( + Ui::Text::WithEntities))), + st::creditsBoxAbout), + style::al_top); about->setClickHandlerFilter([=](const auto &...) { Core::App().iv().openWithIvPreferred( session, @@ -1702,9 +1764,8 @@ void GenericCreditsEntryBox( std::move(text), u"internal:stars_examples"_q); }); - box->addRow(object_ptr>( - box, - Ui::CreateLabelWithCustomEmoji( + box->addRow( + object_ptr( box, (!e.in && peer) ? tr::lng_credits_box_history_entry_gift_out_about( @@ -1717,8 +1778,8 @@ void GenericCreditsEntryBox( lt_link, std::move(link), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), - st::creditsBoxAbout))); + st::creditsBoxAbout), + style::al_top); } else if (e.paidMessagesCommission && e.barePeerId) { Ui::AddSkip(content); auto link = tr::lng_credits_paid_messages_fee_about_link( @@ -1731,9 +1792,8 @@ void GenericCreditsEntryBox( u"internal:edit_paid_messages_fee/"_q + QString::number(id)); }); const auto percent = 100. - (e.paidMessagesCommission / 10.); - box->addRow(object_ptr>( - box, - Ui::CreateLabelWithCustomEmoji( + box->addRow( + object_ptr( box, tr::lng_credits_paid_messages_fee_about( lt_percent, @@ -1742,8 +1802,8 @@ void GenericCreditsEntryBox( lt_link, std::move(link), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), - st::creditsBoxAbout))); + st::creditsBoxAbout), + style::al_top); } Ui::AddSkip(content); @@ -1759,19 +1819,17 @@ void GenericCreditsEntryBox( && !e.giftTransferred && !e.giftRefunded && !e.converted; - const auto toggleVisibility = [=, weak = Ui::MakeWeak(box)](bool save) { + const auto toggleVisibility = [=, weak = base::make_weak(box)](bool save) { const auto showSection = !e.fromGiftsList; const auto savedId = EntryToSavedStarGiftId(&show->session(), e); const auto done = [=](bool ok) { if (ok && showSection) { if (const auto window = show->resolveWindow()) { window->showSection( - std::make_shared( - window->session().user(), - Info::Section::Type::PeerGifts)); + Info::PeerGifts::Make(window->session().user())); } } - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { if (ok) { strong->closeBox(); } else { @@ -1782,6 +1840,14 @@ void GenericCreditsEntryBox( ToggleStarGiftSaved(show, savedId, save, done); }; + const auto canUpgrade = e.stargiftId + && e.canUpgradeGift + && (e.in || giftToSelf || giftToChannelCanManage) + && !e.uniqueGift; + const auto canUpgradeFree = canUpgrade && (e.starsUpgradedBySender > 0); + const auto canGiftUpgrade = !e.uniqueGift + && !e.in + && !e.giftPrepayUpgradeHash.isEmpty(); const auto upgradeGuard = std::make_shared(); const auto upgrade = [=] { const auto window = show->resolveWindow(); @@ -1790,7 +1856,7 @@ void GenericCreditsEntryBox( } *upgradeGuard = true; const auto savedId = EntryToSavedStarGiftId(&window->session(), e); - const auto openWhenDone = giftToChannel + const auto openWhenDone = (giftToChannel || canGiftUpgrade) ? window->session().data().peer(PeerId(e.bareGiftOwnerId)).get() : starGiftSender; using namespace Ui; @@ -1800,6 +1866,7 @@ void GenericCreditsEntryBox( .ready = [=](bool) { *upgradeGuard = false; }, .peer = openWhenDone, .savedId = savedId, + .giftPrepayUpgradeHash = e.giftPrepayUpgradeHash, .cost = e.starsUpgradedBySender ? 0 : e.starsToUpgrade, .canAddSender = !giftToSelf && !e.anonymous, .canAddComment = (!giftToSelf @@ -1807,17 +1874,14 @@ void GenericCreditsEntryBox( && e.hasGiftComment), .canAddMyComment = (giftToSelf && e.hasGiftComment), .addDetailsDefault = (giftToSelf - || (e.starsUpgradedBySender && !e.anonymous)), + || (e.starsUpgradedBySender + && !e.giftUpgradeSeparate + && !e.anonymous)), }); }; - const auto canUpgrade = e.stargiftId - && e.canUpgradeGift - && (e.in || giftToSelf || giftToChannelCanManage) - && !e.uniqueGift; - const auto canUpgradeFree = canUpgrade && (e.starsUpgradedBySender > 0); if (isStarGift && e.id.isEmpty()) { - const auto convert = [=, weak = Ui::MakeWeak(box)] { + const auto convert = [=, weak = base::make_weak(box)] { const auto stars = e.starsConverted; const auto days = canConvert ? ((timeLeft + 86399) / 86400) : 0; auto text = giftToChannelCanManage @@ -1851,7 +1915,7 @@ void GenericCreditsEntryBox( .action = GiftAction::Convert, }); } - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { if (ok) { strong->closeBox(); } else { @@ -1877,9 +1941,9 @@ void GenericCreditsEntryBox( Ui::AddSkip(content); + const auto showNextToUpgrade = e.nextToUpgradeShow; if (!isStarGift && e.credits.stars()) { - box->addRow(object_ptr>( - box, + box->addRow( object_ptr( box, tr::lng_credits_box_out_about( @@ -1888,7 +1952,8 @@ void GenericCreditsEntryBox( ) | Ui::Text::ToLink( tr::lng_credits_box_out_about_link(tr::now)), Ui::Text::WithEntities), - st::creditsBoxAboutDivider))); + st::creditsBoxAboutDivider), + style::al_top); } else if (starGiftCanManage) { const auto hiddenPhrase = giftToChannelCanManage ? tr::lng_gift_hidden_hint_channel @@ -1898,14 +1963,31 @@ void GenericCreditsEntryBox( const auto visiblePhrase = giftToChannelCanManage ? tr::lng_gift_visible_hint_channel : tr::lng_gift_visible_hint; + auto withShow = rpl::combine( + hiddenPhrase(), + tr::lng_gift_visible_show_arrow( + lt_arrow, + rpl::single(arrow), + Ui::Text::WithEntities) + ) | rpl::map([=](QString &&hint, const TextWithEntities &hide) { + return TextWithEntities{ std::move(hint) }.append(' ').append( + Ui::Text::Link(hide)); + }); auto withHide = rpl::combine( visiblePhrase(), - tr::lng_gift_visible_hide() - ) | rpl::map([](QString &&hint, QString &&hide) { + tr::lng_gift_visible_hide_arrow( + lt_arrow, + rpl::single(arrow), + Ui::Text::WithEntities) + ) | rpl::map([](QString &&hint, const TextWithEntities &hide) { return TextWithEntities{ std::move(hint) }.append(' ').append( - Ui::Text::Link(std::move(hide))); + Ui::Text::Link(hide)); }); - auto text = !e.savedToProfile + auto text = (!e.savedToProfile + && canToggle + && (canUpgrade || showNextToUpgrade)) + ? std::move(withShow) + : !e.savedToProfile ? hiddenPhrase(Ui::Text::WithEntities) : canToggle ? std::move(withHide) @@ -1924,7 +2006,8 @@ void GenericCreditsEntryBox( object_ptr( box, std::move(text), - st::creditsBoxAboutDivider)); + st::creditsBoxAboutDivider), + style::al_top); label->setClickHandlerFilter([=](const auto &...) { toggleVisibility(!e.savedToProfile); return false; @@ -1935,9 +2018,14 @@ void GenericCreditsEntryBox( box, tr::lng_gift_in_blockchain( lt_link, - tr::lng_gift_in_blockchain_link() | Ui::Text::ToLink(), + tr::lng_gift_in_blockchain_link_arrow( + lt_arrow, + rpl::single(arrow), + Ui::Text::WithEntities + ) | Ui::Text::ToLink(), Ui::Text::WithEntities), - st::creditsBoxAboutDivider)); + st::creditsBoxAboutDivider), + style::al_top); label->setClickHandlerFilter([=](const auto &...) { UrlClickHandler::Open( TonAddressUrl(session, uniqueGift->ownerAddress)); @@ -1973,7 +2061,7 @@ void GenericCreditsEntryBox( if (button != Qt::LeftButton) { return false; } - const auto done = [=, weak = Ui::MakeWeak(box)] { + const auto done = [=, weak = base::make_weak(box)] { ProcessReceivedSubscriptions(weak, session); }; const auto fail = [=, s = box->uiShow()](const QString &e) { @@ -1989,8 +2077,7 @@ void GenericCreditsEntryBox( } else if (s.cancelled || s.cancelledByBot) { label->setTextColorOverride(st::menuIconAttentionColor->c); } - box->addRow( - object_ptr>(box, std::move(label))); + box->addRow(std::move(label), style::al_top); } Ui::AddSkip(content); @@ -2010,6 +2097,9 @@ void GenericCreditsEntryBox( const auto toRejoin = (s.cancelled || s.expired) && rejoinBySlug && !s.cancelledByBot; + //const auto suggestUpgradeNext = uniqueGift + // && canToggle + // && e.savedToProfile; auto confirmText = rpl::conditional( state->confirmButtonBusy.value(), rpl::single(QString()), @@ -2019,12 +2109,16 @@ void GenericCreditsEntryBox( ? tr::lng_credits_subscription_off_rejoin_button() : canUpgradeFree ? tr::lng_gift_upgrade_free() + : canUpgrade + ? tr::lng_gift_unique_upgrade() + : canGiftUpgrade + ? tr::lng_gift_unique_gift_upgrade() : (canToggle && !e.savedToProfile) ? (e.giftChannelSavedId ? tr::lng_gift_show_on_channel : tr::lng_gift_show_on_page)() : tr::lng_box_ok())); - const auto send = [=, weak = Ui::MakeWeak(box)] { + const auto send = [=, weak = base::make_weak(box)] { if (toRejoin && !toRenew) { if (const auto window = show->resolveWindow()) { const auto finish = [=](Payments::CheckoutResult&&) { @@ -2049,7 +2143,7 @@ void GenericCreditsEntryBox( ProcessReceivedSubscriptions(weak, session); }; const auto fail = [=, show = box->uiShow()](const QString &e) { - if ([[maybe_unused]] const auto strong = weak.data()) { + if ([[maybe_unused]] const auto strong = weak.get()) { state->confirmButtonBusy = false; } show->showToast(e); @@ -2070,7 +2164,10 @@ void GenericCreditsEntryBox( } const auto button = box->addButton(std::move(confirmText), [=] { - if (state->confirmButtonBusy.current() + if (showNextToUpgrade) { + showNextToUpgrade(); + return; + } else if (state->confirmButtonBusy.current() || state->convertButtonBusy.current()) { return; } @@ -2085,23 +2182,61 @@ void GenericCreditsEntryBox( ShowBuyResaleGiftBox( show, e.uniqueGift, + e.giftResaleForceTon, to, crl::guard(box, [=] { box->closeBox(); })); - } else if (canUpgradeFree) { + } else if (canUpgrade || canGiftUpgrade) { upgrade(); } else if (canToggle && !e.savedToProfile) { toggleVisibility(true); } else { box->closeBox(); } - }); + }, showNextToUpgrade + ? st::defaultLightButton + : st::giveawayGiftCodeBox.button); if (canBuyResold) { - button->setText(tr::lng_gift_buy_resale_button( - lt_cost, - rpl::single( - Ui::Text::IconEmoji(&st::starIconEmoji).append( + if (uniqueGift->onlyAcceptTon || e.giftResaleForceTon) { + button->setText(rpl::single(QString())); + Ui::SetButtonTwoLabels( + button, + tr::lng_gift_buy_resale_button( + lt_cost, + rpl::single(Data::FormatGiftResaleTon(*uniqueGift)), + Ui::Text::WithEntities), + tr::lng_gift_buy_resale_equals( + lt_cost, + rpl::single(Ui::Text::IconEmoji( + &st::starIconEmojiSmall + ).append(Lang::FormatCountDecimal( + uniqueGift->starsForResale))), + Ui::Text::WithEntities), + st::resaleButtonTitle, + st::resaleButtonSubtitle); + } else { + button->setText(tr::lng_gift_buy_resale_button( + lt_cost, + rpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append( Lang::FormatCountDecimal(uniqueGift->starsForResale))), - Ui::Text::WithEntities)); + Ui::Text::WithEntities)); + } + } else if (showNextToUpgrade) { + const auto session = &show->session(); + const auto sticker = e.nextToUpgradeStickerId + ? session->data().document(e.nextToUpgradeStickerId).get() + : nullptr; + const auto document = (sticker && sticker->sticker()) + ? sticker + : nullptr; + button->setContext(Core::TextContext({ .session = session })); + button->setText(tr::lng_gift_unique_upgrade_next( + ) | rpl::map([=](const QString &text) { + auto result = TextWithEntities{ text }; + if (document) { + result.append(' ').append(Data::SingleCustomEmoji(document)); + } + return result; + })); } { using namespace Info::Statistics; @@ -2111,14 +2246,194 @@ void GenericCreditsEntryBox( AddChildToWidgetCenter(button, loadingAnimation); loadingAnimation->showOn(state->confirmButtonBusy.value()); } - 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()); +void UniqueGiftValueBox( + not_null box, + std::shared_ptr show, + const Data::CreditsHistoryEntry &e, + CreditsEntryBoxStyleOverrides st) { + box->setStyle(st.box ? *st.box : st::giveawayGiftCodeBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + + const auto unique = e.uniqueGift; + const auto value = unique ? unique->value : nullptr; + Assert(unique && value); + + const auto showLastPrice = (value->lastSalePrice > value->averagePrice); + + const auto content = box->verticalLayout(); + + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddSkip(content); + + struct State final { + DocumentData *sticker = nullptr; + std::shared_ptr media; + std::unique_ptr lottie; + rpl::lifetime downloadLifetime; + rpl::lifetime buyLifetime; + }; + Ui::AddSkip(content, st::creditsHistoryEntryStarGiftSpace); + + const auto icon = Ui::CreateChild(content); + icon->resize(Size(st::creditsHistoryEntryStarGiftSize)); + const auto state = icon->lifetime().make_state(); + const auto document = unique->model.document; + if (document && document->sticker()) { + const auto origin = document->stickerOrGifOrigin(); + state->sticker = document; + state->media = document->createMediaView(); + state->media->thumbnailWanted(origin); + state->media->automaticLoad(origin, nullptr); + rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return state->media->loaded(); + }) | rpl::start_with_next([=] { + state->lottie = ChatHelpers::LottiePlayerFromDocument( + state->media.get(), + ChatHelpers::StickerLottieSize::MessageHistory, + icon->size(), + Lottie::Quality::High); + state->lottie->updates() | rpl::start_with_next([=] { + icon->update(); + }, icon->lifetime()); + state->downloadLifetime.destroy(); + }, state->downloadLifetime); + } + icon->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(icon); + const auto &lottie = state->lottie; + const auto factor = style::DevicePixelRatio(); + const auto request = Lottie::FrameRequest{ + .box = icon->size() * factor, + }; + const auto frame = (lottie && lottie->ready()) + ? lottie->frameInfo(request) + : Lottie::Animation::FrameInfo(); + if (!frame.image.isNull()) { + p.drawImage( + QRect(QPoint(), frame.image.size() / factor), + frame.image); + if (lottie->frameIndex() < lottie->framesCount() - 1) { + lottie->markFrameShown(); + } + } + }, icon->lifetime()); + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &size) { + icon->move( + (size.width() - icon->width()) / 2, + st::creditsHistoryEntryStarGiftSkip); + }, icon->lifetime()); + + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto bubble = box->addRow( + object_ptr( + box, + rpl::single( + Ui::FillAmountAndCurrency(value->valuePrice, value->currency)), + st::uniqueGiftValuePrice), + style::al_top); + bubble->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(bubble); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::windowBgActive); + p.setPen(Qt::NoPen); + const auto rect = bubble->rect(); + const auto radius = std::min(rect.width(), rect.height()) / 2.; + p.drawRoundedRect(rect, radius, radius); + }, bubble->lifetime()); + + Ui::AddSkip(content); + + const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); + Ui::AddSkip(content); + box->addRow( + object_ptr( + box, + (showLastPrice + ? tr::lng_gift_value_about_last( + lt_gift, + rpl::single(Ui::Text::Bold( + Data::UniqueGiftName(*unique))), + lt_platform, + (value->lastSaleFragment + ? tr::lng_gift_value_fragment + : tr::lng_gift_value_telegram)( + Ui::Text::WithEntities), + Ui::Text::RichLangValue) + : tr::lng_gift_value_about_average( + lt_gift, + rpl::single(Ui::Text::Bold(unique->title)), + Ui::Text::RichLangValue)), + st::uniqueGiftValueAbout) + )->setTryMakeSimilarLines(true); + + Ui::AddSkip(content); + Ui::AddSkip(content); + + AddUniqueGiftValueTable(show, content, st, e); + + Ui::AddSkip(content); + + const auto addAvailability = [&](int count, tr::phrase<> platform) { + return box->addRow( + object_ptr( + box, + tr::lng_gift_value_availability( + lt_count_decimal, + rpl::single(count * 1.), + lt_emoji, + rpl::single(Data::SingleCustomEmoji(document)), + lt_platform, + platform(Ui::Text::WithEntities), + lt_arrow, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + [](const QString &text) { return Ui::Text::Link(text); }), + st::uniqueGiftValueAvailableLink, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })), + st::boxRowPadding + st::uniqueGiftValueAvailableMargin, + style::al_top); + }; + + if (const auto count = value->forSaleOnTelegram; count > 0) { + addAvailability( + count, + tr::lng_gift_value_telegram + )->setClickHandlerFilter([=](const auto &...) { + if (const auto window = show->resolveWindow()) { + state->buyLifetime = Ui::ShowStarGiftResale( + window, + window->session().user(), + unique->initialGiftId, + unique->title, + crl::guard(box, [=] { state->buyLifetime.destroy(); })); + } + return false; + }); + } + if (const auto count = value->forSaleOnFragment; count > 0) { + const auto url = value->fragmentUrl; + addAvailability( + count, + tr::lng_gift_value_fragment + )->setClickHandlerFilter([=](const auto &...) { + UrlClickHandler::Open(url); + return false; + }); + } + + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); } void ReceiptCreditsBox( @@ -2184,7 +2499,7 @@ void GlobalStarGiftBox( not_null box, std::shared_ptr show, const Data::StarGift &data, - PeerId resaleRecipientId, + StarGiftResaleInfo resale, CreditsEntryBoxStyleOverrides st) { const auto selfId = show->session().userPeerId(); const auto ownerId = data.unique ? data.unique->ownerId.value : 0; @@ -2195,8 +2510,8 @@ void GlobalStarGiftBox( .credits = CreditsAmount(data.stars), .bareGiftStickerId = data.document->id, .bareGiftOwnerId = ownerId, - .bareGiftResaleRecipientId = ((resaleRecipientId != selfId) - ? resaleRecipientId.value + .bareGiftResaleRecipientId = ((resale.recipientId != selfId) + ? resale.recipientId.value : 0), .stargiftId = data.id, .uniqueGift = data.unique, @@ -2204,6 +2519,7 @@ void GlobalStarGiftBox( .limitedCount = data.limitedCount, .limitedLeft = data.limitedLeft, .stargift = true, + .giftResaleForceTon = resale.forceTon, .fromGiftSlug = true, .in = (ownerId == show->session().userPeerId().value), .gift = true, @@ -2228,6 +2544,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( .bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0, .giftChannelSavedId = data.manageId.chatSavedId(), .stargiftId = data.info.id, + .giftPrepayUpgradeHash = data.giftPrepayUpgradeHash, .uniqueGift = data.info.unique, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.info.limitedCount, @@ -2238,6 +2555,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( .converted = false, .anonymous = data.anonymous, .stargift = true, + .giftUpgradeSeparate = data.upgradeSeparate, .giftPinned = data.pinned, .savedToProfile = !data.hidden, .fromGiftsList = true, @@ -2250,7 +2568,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( Data::SavedStarGiftId EntryToSavedStarGiftId( not_null session, const Data::CreditsHistoryEntry &entry) { - return !entry.stargift + return (!entry.stargift || (!entry.in && !entry.giftChannelSavedId)) ? Data::SavedStarGiftId() : (entry.bareEntryOwnerId && entry.giftChannelSavedId) ? Data::SavedStarGiftId::Chat( @@ -2259,19 +2577,20 @@ Data::SavedStarGiftId EntryToSavedStarGiftId( : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); } -void SavedStarGiftBox( - not_null box, +void ShowSavedStarGiftBox( not_null controller, not_null owner, const Data::SavedStarGift &data, Fn()> pinned) { - auto entry = SavedStarGiftEntry(owner, data); - entry.pinnedSavedGifts = std::move(pinned); - Settings::ReceiptCreditsBox( - box, - controller, - std::move(entry), - Data::SubscriptionEntry()); + controller->show(Box([=](not_null box) { + auto entry = SavedStarGiftEntry(owner, data); + entry.pinnedSavedGifts = std::move(pinned); + Settings::ReceiptCreditsBox( + box, + controller, + std::move(entry), + Data::SubscriptionEntry()); + })); } void FillSavedStarGiftMenu( @@ -2283,28 +2602,42 @@ void FillSavedStarGiftMenu( FillUniqueGiftMenu(show, menu, e, type, st); } -void StarGiftViewBox( - not_null box, +void ShowStarGiftViewBox( not_null controller, const Data::GiftCode &data, - not_null item) { + FullMsgId itemId, + std::optional upgradeNext) { + const auto item = controller->session().data().message(itemId); + if (!item) { + return; + } const auto peer = item->history()->peer; const auto toChannel = peer->isServiceUser() && data.channel; const auto incoming = !toChannel && (data.upgrade ? item->out() : !item->out()); const auto fromId = incoming ? peer->id : peer->session().userPeerId(); const auto toId = incoming ? peer->session().userPeerId() : peer->id; + const auto ownerId = data.unique ? data.unique->ownerId : toId; + const auto nextToUpgradeStickerId = upgradeNext + ? upgradeNext->info.document->id + : uint64(); + const auto nextToUpgradeShow = upgradeNext + ? [=] { ShowSavedStarGiftBox( + controller, + controller->session().data().peer(ownerId), + *upgradeNext); } + : Fn(); const auto entry = Data::CreditsHistoryEntry{ .id = data.slug, .description = data.message, .date = base::unixtime::parse(item->date()), .credits = CreditsAmount(data.count), - .bareMsgId = uint64(item->id.bare), + .bareMsgId = uint64(data.realGiftMsgId + ? data.realGiftMsgId.bare + : item->id.bare), .barePeerId = fromId.value, .bareGiftStickerId = data.document ? data.document->id : 0, - .bareGiftOwnerId = (data.unique - ? data.unique->ownerId.value - : toId.value), + .bareGiftOwnerId = ownerId.value, .bareGiftReleasedById = (data.stargiftReleasedBy ? data.stargiftReleasedBy->id.value : 0), @@ -2312,7 +2645,10 @@ void StarGiftViewBox( .bareEntryOwnerId = (toChannel ? data.channel->id.value : 0), .giftChannelSavedId = data.channelSavedId, .stargiftId = data.stargiftId, + .giftPrepayUpgradeHash = data.giftPrepayUpgradeHash, .uniqueGift = data.unique, + .nextToUpgradeStickerId = nextToUpgradeStickerId, + .nextToUpgradeShow = std::move(nextToUpgradeShow), .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = data.limitedCount, .limitedLeft = data.limitedLeft, @@ -2324,17 +2660,50 @@ void StarGiftViewBox( .stargift = true, .giftTransferred = data.transferred, .giftRefunded = data.refunded, + .giftUpgradeSeparate = data.upgradeSeparate, .savedToProfile = data.saved, .canUpgradeGift = data.upgradable, .hasGiftComment = !data.message.empty(), .in = incoming, .gift = true, }; - Settings::ReceiptCreditsBox( - box, - controller, - entry, - Data::SubscriptionEntry()); + controller->show(Box([=](not_null box) { + Settings::ReceiptCreditsBox( + box, + controller, + entry, + Data::SubscriptionEntry()); + })); +} + +void ShowStarGiftViewBox( + not_null controller, + const Data::GiftCode &data, + FullMsgId itemId) { + const auto item = controller->session().data().message(itemId); + if (!item) { + return; + } + const auto peer = item->history()->peer; + const auto toChannel = peer->isServiceUser() && data.channel; + const auto incoming = !toChannel + && (data.upgrade ? item->out() : !item->out()); + const auto toId = incoming ? peer->session().userPeerId() : peer->id; + const auto ownerId = data.unique ? data.unique->ownerId : toId; + const auto owner = peer->owner().peer(ownerId); + if (data.unique && owner->canManageGifts()) { + const auto weak = base::make_weak(controller); + owner->owner().nextForUpgradeGiftRequest(owner, crl::guard(weak, [=]( + std::optional nextToUpgrade) { + ShowStarGiftViewBox( + controller, + data, + itemId, + std::move(nextToUpgrade)); + })); + } else { + ShowStarGiftViewBox(controller, data, itemId, std::nullopt); + } } void ShowRefundInfoBox( @@ -2371,6 +2740,7 @@ object_ptr GenericEntryPhoto( auto owned = object_ptr(parent); const auto widget = owned.data(); widget->resize(Size(photoSize)); + widget->setNaturalWidth(photoSize); const auto draw = callback( crl::guard(widget, [=] { widget->update(); })); @@ -2420,6 +2790,7 @@ object_ptr SubscriptionUserpic( auto widget = object_ptr(parent); const auto raw = widget.data(); widget->resize(photoSize, photoSize); + widget->setNaturalWidth(photoSize); const auto userpicMedia = Ui::MakeUserpicThumbnail(peer, false); userpicMedia->subscribeToUpdates([=] { raw->update(); }); const auto creditsIconSize = photoSize / 3; @@ -2468,7 +2839,7 @@ void SmallBalanceBox( return owner->peer(peerFromChannel(value.channelId))->name(); }, [](SmallBalanceSubscription value) { return value.name; - }, [](SmallBalanceDeepLink value) { + }, [](SmallBalanceDeepLink) { return QString(); }, [&](SmallBalanceStarGift value) { return owner->peer(value.recipientId)->shortName(); @@ -2480,6 +2851,8 @@ void SmallBalanceBox( return value.recipientId ? owner->peer(value.recipientId)->shortName() : QString(); + }, [](SmallBalanceForSearch) { + return QString(); }); auto needed = show->session().credits().balanceValue( @@ -2531,6 +2904,9 @@ void SmallBalanceBox( lt_channel, rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue) + : v::is(source) + ? tr::lng_credits_small_balance_for_search( + Ui::Text::RichLangValue) : name.isEmpty() ? tr::lng_credits_small_balance_fallback( Ui::Text::RichLangValue) @@ -2549,6 +2925,7 @@ void SmallBalanceBox( show->session().user(), credits - show->session().credits().balance(), [=] { show->session().credits().load(true); }, + box->showFinishes(), tr::lng_credits_summary_options_subtitle(), {}); @@ -2604,9 +2981,8 @@ void AddWithdrawalWidget( Ui::AddSkip(container); const auto labels = container->add( - object_ptr>( - container, - object_ptr(container)))->entity(); + object_ptr(container), + style::al_top); const auto majorLabel = Ui::CreateChild( labels, @@ -2626,18 +3002,18 @@ void AddWithdrawalWidget( labels->resize( majorSize.width() + icon->width() + skip, majorSize.height()); + labels->setNaturalWidth(majorSize.width() + icon->width() + skip); majorLabel->moveToLeft(icon->width() + skip, 0); }, labels->lifetime()); Ui::ToggleChildrenVisibility(labels, true); Ui::AddSkip(container); container->add( - object_ptr>( + object_ptr( container, - object_ptr( - container, - std::move(usdValue), - st::channelEarnOverviewSubMinorLabel))); + std::move(usdValue), + st::channelEarnOverviewSubMinorLabel), + style::al_top); Ui::AddSkip(container); @@ -2713,11 +3089,6 @@ void AddWithdrawalWidget( tr::lng_channel_earn_balance_button(tr::now), st::channelEarnSemiboldLabel); const auto processInputChange = [&] { - const auto buttonEmoji = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::settingsPremiumIconStar, - { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, - true)); using Balance = rpl::variable; const auto currentBalance = input->lifetime().make_state( rpl::duplicate(availableBalanceValue)); @@ -2733,9 +3104,8 @@ void AddWithdrawalWidget( lt_count, amount, lt_emoji, - buttonEmoji, - Ui::Text::RichLangValue), - Core::TextContext({ .session = session })); + Ui::Text::IconEmoji(&st::starIconEmojiLarge), + Ui::Text::RichLangValue)); } }; QObject::connect(input, &Ui::MaskedInputField::changed, process); @@ -2803,15 +3173,10 @@ void AddWithdrawalWidget( constexpr auto kDateUpdateInterval = crl::time(250); const auto was = base::unixtime::serialize(dt); - const auto context = Core::TextContext({ - .session = session, + const auto context = Ui::Text::MarkedContext{ .repaint = [=] { lockedLabel->update(); }, - }); - const auto emoji = Ui::Text::SingleCustomEmoji( - session->data().customEmojiManager().registerInternalEmoji( - st::chatSimilarLockedIcon, - st::botEarnButtonLockMargins, - true)); + }; + const auto emoji = Ui::Text::IconEmoji(&st::botEarnButtonLock); rpl::single( rpl::empty @@ -2883,8 +3248,7 @@ void AddWithdrawalWidget( Ui::AddSkip(container); Ui::AddSkip(container); - const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); - auto about = Ui::CreateLabelWithCustomEmoji( + auto about = object_ptr( container, (peer->isSelf() ? tr::lng_self_earn_learn_credits_out_about @@ -2892,7 +3256,7 @@ void AddWithdrawalWidget( lt_link, tr::lng_channel_earn_about_link( lt_emoji, - rpl::single(arrow), + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), Ui::Text::RichLangValue ) | rpl::map([](TextWithEntities text) { return Ui::Text::Link( @@ -2900,7 +3264,6 @@ void AddWithdrawalWidget( tr::lng_bot_earn_balance_about_url(tr::now)); }), Ui::Text::RichLangValue), - Core::TextContext({ .session = session }), st::boxDividerLabel); Ui::AddSkip(container); container->add(object_ptr( diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 9789278fac..d4af6f0977 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -74,6 +74,7 @@ void FillCreditOptions( not_null peer, CreditsAmount minCredits, Fn paid, + rpl::producer<> showFinishes, rpl::producer subtitle, std::vector preloadedTopupOptions); @@ -114,6 +115,7 @@ struct CreditsEntryBoxStyleOverrides { const style::FlatLabel *tableValueMessage = nullptr; const style::icon *link = nullptr; const style::icon *share = nullptr; + const style::icon *theme = nullptr; const style::icon *transfer = nullptr; const style::icon *wear = nullptr; const style::icon *takeoff = nullptr; @@ -134,6 +136,11 @@ void GenericCreditsEntryBox( const Data::CreditsHistoryEntry &e, const Data::SubscriptionEntry &s, CreditsEntryBoxStyleOverrides st = {}); +void UniqueGiftValueBox( + not_null box, + std::shared_ptr show, + const Data::CreditsHistoryEntry &e, + CreditsEntryBoxStyleOverrides st = {}); void ReceiptCreditsBox( not_null box, not_null controller, @@ -155,11 +162,16 @@ void CreditsPrizeBox( not_null controller, const Data::GiftCode &data, TimeId date); + +struct StarGiftResaleInfo { + PeerId recipientId; + bool forceTon = false; +}; void GlobalStarGiftBox( not_null box, std::shared_ptr show, const Data::StarGift &data, - PeerId resaleRecipientId, + StarGiftResaleInfo resale, CreditsEntryBoxStyleOverrides st = {}); [[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry( @@ -168,8 +180,7 @@ void GlobalStarGiftBox( [[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId( not_null session, const Data::CreditsHistoryEntry &entry); -void SavedStarGiftBox( - not_null box, +void ShowSavedStarGiftBox( not_null controller, not_null owner, const Data::SavedStarGift &data, @@ -185,11 +196,10 @@ void FillSavedStarGiftMenu( SavedStarGiftMenuType type, CreditsEntryBoxStyleOverrides st = {}); -void StarGiftViewBox( - not_null box, +void ShowStarGiftViewBox( not_null controller, const Data::GiftCode &data, - not_null item); + FullMsgId itemId); void ShowRefundInfoBox( not_null controller, FullMsgId refundItemId); @@ -237,6 +247,8 @@ struct SmallBalanceForMessage { struct SmallBalanceForSuggest { PeerId recipientId; }; +struct SmallBalanceForSearch { +}; struct SmallBalanceSource : std::variant< SmallBalanceBot, SmallBalanceReaction, @@ -244,7 +256,8 @@ struct SmallBalanceSource : std::variant< SmallBalanceDeepLink, SmallBalanceStarGift, SmallBalanceForMessage, - SmallBalanceForSuggest> { + SmallBalanceForSuggest, + SmallBalanceForSearch> { using variant::variant; }; diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index bd5d8be137..c8ac855a84 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -353,7 +353,7 @@ void FilterRowButton::paintEvent(QPaintEvent *e) { not_null*> tagsButtonEnabled) { auto &lifetime = container->lifetime(); - const auto weak = Ui::MakeWeak(container); + const auto weak = base::make_weak(container); const auto session = &controller->session(); const auto limit = [=] { return Data::PremiumLimits(session).dialogFiltersCurrent(); @@ -862,13 +862,12 @@ void SetupTopContent( verticalLayout->add(std::move(icon.widget)); verticalLayout->add( - object_ptr>( + object_ptr( verticalLayout, - object_ptr( - verticalLayout, - tr::lng_filters_about(), - st::settingsFilterDividerLabel)), - st::settingsFilterDividerLabelPadding); + tr::lng_filters_about(), + st::settingsFilterDividerLabel), + st::settingsFilterDividerLabelPadding, + style::al_top)->setTryMakeSimilarLines(true); verticalLayout->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { @@ -909,9 +908,9 @@ void SetupTagContent( tagsButton->setToggleLocked(!value); }, tagsButton->lifetime()); - const auto send = [=, weak = Ui::MakeWeak(tagsButton)](bool checked) { + const auto send = [=, weak = base::make_weak(tagsButton)](bool checked) { session->data().chatsFilters().requestToggleTags(checked, [=] { - if ([[maybe_unused]] const auto strong = weak.data()) { + if ([[maybe_unused]] const auto strong = weak.get()) { state->tagsTurnOff.fire(!checked); } }); diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp index 8f639c30a6..3bab6d26e8 100644 --- a/Telegram/SourceFiles/settings/settings_local_passcode.cpp +++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp @@ -136,25 +136,24 @@ void LocalPasscodeEnter::setupContent() { Ui::AddSkip(content); content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - isCreate - ? tr::lng_passcode_create_title() - : isCheck - ? tr::lng_passcode_check_title() - : tr::lng_passcode_change_title(), - st::changePhoneTitle)), - st::changePhoneTitlePadding); + isCreate + ? tr::lng_passcode_create_title() + : isCheck + ? tr::lng_passcode_check_title() + : tr::lng_passcode_change_title(), + st::changePhoneTitle), + st::changePhoneTitlePadding, + style::al_top); const auto addDescription = [&](rpl::producer &&text) { const auto &st = st::settingLocalPasscodeDescription; content->add( - object_ptr>( - content, - object_ptr(content, std::move(text), st)), - st::changePhoneDescriptionPadding); + object_ptr(content, std::move(text), st), + st::changePhoneDescriptionPadding, + style::al_top + )->setTryMakeSimilarLines(true); }; addDescription(tr::lng_passcode_about1()); @@ -183,14 +182,13 @@ void LocalPasscodeEnter::setupContent() { const auto addError = [&](not_null input) { const auto error = content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - // Set any text to resize. - tr::lng_language_name(tr::now), - st::settingLocalPasscodeError)), - st::changePhoneDescriptionPadding)->entity(); + // Set any text to resize. + tr::lng_language_name(tr::now), + st::settingLocalPasscodeError), + st::changePhoneDescriptionPadding, + style::al_top); error->hide(); QObject::connect(input.get(), &Ui::MaskedInputField::changed, [=] { error->hide(); @@ -208,17 +206,16 @@ void LocalPasscodeEnter::setupContent() { const auto error = addError(isCheck ? newPasscode : reenterPasscode); const auto button = content->add( - object_ptr>( + object_ptr( content, - object_ptr( - content, - (isCreate - ? tr::lng_passcode_create_button() - : isCheck - ? tr::lng_passcode_check_button() - : tr::lng_passcode_change_button()), - st::changePhoneButton)), - st::settingLocalPasscodeButtonPadding)->entity(); + (isCreate + ? tr::lng_passcode_create_button() + : isCheck + ? tr::lng_passcode_check_button() + : tr::lng_passcode_change_button()), + st::changePhoneButton), + st::settingLocalPasscodeButtonPadding, + style::al_top); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); button->setClickedCallback([=] { const auto newText = newPasscode->text(); @@ -404,7 +401,7 @@ public: [[nodiscard]] rpl::producer> removeFromStack() override; - [[nodiscard]] QPointer createPinnedToBottom( + [[nodiscard]] base::weak_qptr createPinnedToBottom( not_null parent) override; private: @@ -606,7 +603,7 @@ void LocalPasscodeManage::setupContent() { Ui::ResizeFitChild(this, content); } -QPointer LocalPasscodeManage::createPinnedToBottom( +base::weak_qptr LocalPasscodeManage::createPinnedToBottom( not_null parent) { auto callback = [=] { _controller->show( diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 05c81e0075..cf116a1380 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -513,7 +513,7 @@ void SetupValidatePhoneNumberSuggestion( st::inviteLinkButton); no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); no->setClickedCallback([=] { - const auto sharedLabel = std::make_shared>(); + const auto sharedLabel = std::make_shared>(); const auto height = st::boxLabel.style.font->height; const auto customEmojiFactory = [=]( QStringView data, @@ -816,30 +816,12 @@ void SetupPremium( ? Lang::FormatCreditsAmountToShort(c).string : QString(); }), - st::settingsButton); + st::settingsButton, + { &st::menuIconTon }); button->addClickHandler([=] { controller->setPremiumRef("settings"); showOther(CurrencyId()); }); - - const auto badge = Ui::CreateChild(button.get()); - const auto image = Ui::Earn::IconCurrencyColored( - st::tonFieldIconSize, - st::menuIconColor->c); - - badge->resize(Size(st::tonFieldIconSize)); - badge->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(badge); - p.drawImage(0, 0, image); - }, badge->lifetime()); - - button->sizeValue() | rpl::start_with_next([=](const QSize &s) { - badge->moveToLeft( - button->st().iconLeft - + (st::menuIconShop.width() - badge->width()) / 2, - (s.height() - badge->height()) / 2); - }, badge->lifetime()); } const auto button = AddButtonWithIcon( container, diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index 2361e08bce..a2b6b1cee1 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_notifications.h" +#include "ui/widgets/continuous_sliders.h" #include "settings/settings_notifications_type.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/chat_service_checkbox.h" #include "ui/effects/animations.h" +#include "data/notify/data_peer_notify_volume.h" #include "ui/text/text_utilities.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" @@ -1043,6 +1045,25 @@ void SetupNotificationsContent( { &st::menuIconUnmute }, soundAllowed->events_starting_with(allowed())); + Ui::AddRingtonesVolumeSlider( + container, + rpl::single(true), + tr::lng_settings_master_volume_notifications(), + Data::VolumeController{ + .volume = []() -> ushort { + const auto volume + = Core::App().settings().notificationsVolume(); + return volume ? volume : 100; + }, + .saveVolume = [=](ushort volume) { + Core::App().notifications().playSound( + session, + 0, + volume / 100.); + Core::App().settings().setNotificationsVolume(volume); + Core::App().saveSettingsDelayed(); + }}); + Ui::AddSkip(container); const auto checkboxes = SetupNotifyViewOptions( @@ -1112,6 +1133,8 @@ void SetupNotificationsContent( container, tr::lng_settings_notifications_calls_title()); const auto authorizations = &session->api().authorizations(); + // Request valid value of calls disabled flag. + authorizations->reload(); const auto acceptCalls = addCheckbox( tr::lng_settings_call_accept_calls(), { &st::menuIconCallsReceive }, diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index df18b65ee1..4f3f9e9ae0 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/ringtones_box.h" #include "boxes/peer_list_box.h" +#include "core/application.h" +#include "data/notify/data_peer_notify_volume.h" #include "boxes/peer_list_controllers.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" @@ -21,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_mute.h" +#include "platform/platform_notifications_manager.h" #include "ui/boxes/confirm_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" @@ -379,6 +382,15 @@ void ExceptionsController::sort() { Unexpected("Type in Title."); } +[[nodiscard]] rpl::producer VolumeSubtitle(Notify type) { + switch (type) { + case Notify::User: return tr::lng_notification_volume_private_chats(); + case Notify::Group: return tr::lng_notification_volume_groups(); + case Notify::Broadcast: return tr::lng_notification_volume_channel(); + } + Unexpected("Type in VolumeSubtitle."); +} + void SetupChecks( not_null container, not_null controller, @@ -476,6 +488,24 @@ void SetupChecks( st::settingsButton, { &st::menuIconSoundOn }); + { + auto controller = DefaultRingtonesVolumeController(session, type); + Ui::AddRingtonesVolumeSlider( + toneInner, + rpl::single(true), + VolumeSubtitle(type), + Data::VolumeController{ + .volume = base::take(controller.volume), + .saveVolume = [=](ushort volume) { + Core::App().notifications().playSound( + session, + toneValue().id, + 0.01 * volume); + controller.saveVolume(volume); + }, + }); + } + enabled->toggledValue( ) | rpl::filter([=](bool value) { return (value != NotificationsEnabledForType(session, type)); @@ -499,6 +529,8 @@ void SetupChecks( controller->show(Box(RingtonesBox, session, toneValue(), [=]( Data::NotifySound sound) { settings->defaultUpdate(type, {}, {}, sound); + }, Data::VolumeController{ + DefaultRingtonesVolumeController(session, type).volume, })); }); } @@ -530,7 +562,7 @@ void SetupExceptions( state->controller->setDelegate(state->delegate.get()); add->setClickedCallback([=] { - const auto box = std::make_shared>(); + const auto box = std::make_shared>(); const auto done = [=](not_null peer) { state->controller->bringToTop(peer); if (*box) { diff --git a/Telegram/SourceFiles/settings/settings_power_saving.cpp b/Telegram/SourceFiles/settings/settings_power_saving.cpp index beaf466134..6c4e7768da 100644 --- a/Telegram/SourceFiles/settings/settings_power_saving.cpp +++ b/Telegram/SourceFiles/settings/settings_power_saving.cpp @@ -59,7 +59,7 @@ void PowerSavingBox(not_null box) { state->forceDisabledMessage.value()); const auto controlsRaw = checkboxes.data(); - box->addRow(std::move(checkboxes), {}); + box->addRow(std::move(checkboxes), style::margins()); auto automatic = (Ui::SettingsButton*)nullptr; if (batterySaving.has_value()) { diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 45ae6993eb..f533464b41 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -95,6 +95,8 @@ using SectionCustomTopBarData = Info::Settings::SectionCustomTopBarData; option.duration = tr::lng_premium_subscribe_months_6(tr::now); } else if (option.duration == tr::lng_years(tr::now, lt_count, 1)) { option.duration = tr::lng_premium_subscribe_months_12(tr::now); + } else if (option.duration == tr::lng_years(tr::now, lt_count, 2)) { + option.duration = tr::lng_premium_subscribe_months_24(tr::now); } } return result; @@ -168,6 +170,45 @@ struct Data { } } // namespace EmojiStatus + +namespace PremiumGift { + +struct Data { + DocumentId documentId = 0; + int perUserTotal = 0; + + explicit operator bool() const { + return documentId != 0; + } +}; + +[[nodiscard]] QString Serialize(const Data &gift) { + return u"premiumgift_:%1,peruser_:%2"_q + .arg(gift.documentId) + .arg(gift.perUserTotal); +} + +[[nodiscard]] Data Parse(QStringView data) { + if (data.startsWith(u"premiumgift_:"_q)) { + const auto components = data.split(','); + if (components.size() != 2) { + return {}; + } + const auto first = components[0].split(':'); + const auto second = components[1].split(':'); + if (first.size() != 2 || second.size() != 2) { + return {}; + } + return { + .documentId = DocumentId(first[1].toULongLong()), + .perUserTotal = second[1].toInt(), + }; + } + return {}; +} + +} // namespace PremiumGift + } // namespace Ref struct Entry { @@ -378,7 +419,6 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_business(), tr::lng_premium_summary_about_business(), PremiumFeature::Business, - true, }, }, { @@ -397,6 +437,7 @@ using Order = std::vector; tr::lng_premium_summary_subtitle_todo_lists(), tr::lng_premium_summary_about_todo_lists(), PremiumFeature::TodoLists, + true, }, }, }; @@ -546,13 +587,30 @@ void EmojiStatusTopBar::paint(QPainter &p) { } } -class TopBarUser final : public Ui::Premium::TopBarAbstract { +enum class TopBarWithStickerType { + EmojiStatus, + PremiumGift, +}; + +struct TopBarWithStickerArgs { + rpl::producer stickerValue; + rpl::producer nameValue; + rpl::producer aboutValue; + TopBarWithStickerType type = TopBarWithStickerType::EmojiStatus; +}; + +class TopBarWithSticker final : public Ui::Premium::TopBarAbstract { public: - TopBarUser( + TopBarWithSticker( not_null parent, not_null controller, not_null peer, rpl::producer<> showFinished); + TopBarWithSticker( + not_null parent, + not_null controller, + TopBarWithStickerArgs args, + rpl::producer<> showFinished); void setPaused(bool paused) override; void setTextPosition(int x, int y) override; @@ -566,10 +624,13 @@ protected: private: void updateTitle( DocumentData *document, - TextWithEntities name, + const TextWithEntities &name, not_null controller); - void updateAbout(DocumentData *document) const; + void updateAbout( + DocumentData *document, + const TextWithEntities &about) const; + TopBarWithStickerType _type = TopBarWithStickerType::EmojiStatus; object_ptr _content; object_ptr _title; object_ptr _about; @@ -590,12 +651,35 @@ private: }; -TopBarUser::TopBarUser( +TopBarWithSticker::TopBarWithSticker( not_null parent, not_null controller, not_null peer, rpl::producer<> showFinished) +: TopBarWithSticker(parent, controller, { + .stickerValue = Info::Profile::EmojiStatusIdValue( + peer + ) | rpl::map([=](EmojiStatusId id) -> DocumentData* { + const auto documentId = id.collectible + ? id.collectible->documentId + : id.documentId; + const auto document = documentId + ? controller->session().data().document(documentId).get() + : nullptr; + return (document && document->sticker()) ? document : nullptr; + }), + .nameValue = Info::Profile::NameValue(peer), + .type = TopBarWithStickerType::EmojiStatus, +}, std::move(showFinished)) { +} + +TopBarWithSticker::TopBarWithSticker( + not_null parent, + not_null controller, + TopBarWithStickerArgs args, + rpl::producer<> showFinished) : TopBarAbstract(parent, st::userPremiumCover) +, _type(args.type) , _content(this) , _title(_content, st::settingsPremiumUserTitle) , _about(_content, st::userPremiumCover.about) @@ -608,6 +692,10 @@ TopBarUser::TopBarUser( }) { _starRect = TopBarAbstract::starRect(1., 1.); + if (_type == TopBarWithStickerType::PremiumGift) { + _ministars.setColorOverride(Ui::Premium::CreditsIconGradientStops()); + } + rpl::single() | rpl::then( style::PaletteChanged() ) | rpl::start_with_next([=] { @@ -615,24 +703,20 @@ TopBarUser::TopBarUser( update(); }, lifetime()); - auto documentValue = Info::Profile::EmojiStatusIdValue( - peer - ) | rpl::map([=](EmojiStatusId id) -> DocumentData* { - const auto documentId = id.collectible - ? id.collectible->documentId - : id.documentId; - const auto document = documentId - ? controller->session().data().document(documentId).get() - : nullptr; - return (document && document->sticker()) ? document : nullptr; - }); - rpl::combine( - std::move(documentValue), - Info::Profile::NameValue(peer) + (args.stickerValue + ? std::move(args.stickerValue) + : rpl::single((DocumentData*)nullptr)), + (args.nameValue + ? std::move(args.nameValue) + : rpl::single(QString())), + (args.aboutValue + ? std::move(args.aboutValue) + : rpl::single(TextWithEntities())) ) | rpl::start_with_next([=]( DocumentData *document, - const QString &name) { + const QString &name, + const TextWithEntities &about) { if (document) { _emojiStatus = std::make_unique( document, @@ -645,13 +729,21 @@ TopBarUser::TopBarUser( } updateTitle(document, { name }, controller); - updateAbout(document); + updateAbout(document, about); auto event = QResizeEvent(size(), size()); resizeEvent(&event); update(); }, lifetime()); + _title->naturalWidthValue() | rpl::start_with_next([=] { + _title->resizeToNaturalWidth(st::settingsPremiumUserTitle.minWidth); + }, _title->lifetime()); + + _about->naturalWidthValue() | rpl::start_with_next([=] { + _about->resizeToNaturalWidth(st::userPremiumCover.about.minWidth); + }, _about->lifetime()); + rpl::combine( _title->sizeValue(), _about->sizeValue(), @@ -750,14 +842,15 @@ TopBarUser::TopBarUser( p.drawImage(_starRect.topLeft(), _imageStar); } }, lifetime()); - } -void TopBarUser::updateTitle( +void TopBarWithSticker::updateTitle( DocumentData *document, - TextWithEntities name, + const TextWithEntities &name, not_null controller) { - if (!document) { + if (_type == TopBarWithStickerType::PremiumGift) { + return _title->setMarkedText(name); + } else if (!document) { return _title->setMarkedText( tr::lng_premium_summary_user_title( tr::now, @@ -820,36 +913,40 @@ void TopBarUser::updateTitle( _title->setLink(linkIndex, std::move(link)); } -void TopBarUser::updateAbout(DocumentData *document) const { - _about->setMarkedText((document - ? tr::lng_premium_emoji_status_about - : tr::lng_premium_summary_user_about)( - tr::now, - Ui::Text::RichLangValue)); +void TopBarWithSticker::updateAbout( + DocumentData *document, + const TextWithEntities &about) const { + _about->setMarkedText((_type == TopBarWithStickerType::PremiumGift) + ? about + : (document + ? tr::lng_premium_emoji_status_about + : tr::lng_premium_summary_user_about)( + tr::now, + Ui::Text::RichLangValue)); } -void TopBarUser::setPaused(bool paused) { +void TopBarWithSticker::setPaused(bool paused) { _ministars.setPaused(paused); if (_emojiStatus) { _emojiStatus->setPaused(paused); } } -void TopBarUser::setTextPosition(int x, int y) { +void TopBarWithSticker::setTextPosition(int x, int y) { _smallTop.position = { x, y }; } -rpl::producer TopBarUser::additionalHeight() const { +rpl::producer TopBarWithSticker::additionalHeight() const { return rpl::never(); } -void TopBarUser::paintEvent(QPaintEvent *e) { +void TopBarWithSticker::paintEvent(QPaintEvent *e) { auto p = QPainter(this); TopBarAbstract::paintEdges(p); } -void TopBarUser::resizeEvent(QResizeEvent *e) { +void TopBarWithSticker::resizeEvent(QResizeEvent *e) { _starRect = TopBarAbstract::starRect(1., 1.); _ministars.setCenter(_starRect.toRect()); @@ -867,9 +964,9 @@ public: [[nodiscard]] rpl::producer title() override; - [[nodiscard]] QPointer createPinnedToTop( + [[nodiscard]] base::weak_qptr createPinnedToTop( not_null parent) override; - [[nodiscard]] QPointer createPinnedToBottom( + [[nodiscard]] base::weak_qptr createPinnedToBottom( not_null parent) override; void showFinished() override; @@ -1027,7 +1124,7 @@ void Premium::setupContent() { } -QPointer Premium::createPinnedToTop( +base::weak_qptr Premium::createPinnedToTop( not_null parent) { auto title = _controller->session().premium() ? tr::lng_premium_summary_title() @@ -1063,7 +1160,9 @@ QPointer Premium::createPinnedToTop( }(); const auto emojiStatusData = Ref::EmojiStatus::Parse(_ref); + const auto premiumGiftData = Ref::PremiumGift::Parse(_ref); const auto isEmojiStatus = (!!emojiStatusData); + const auto isPremiumGift = (!!premiumGiftData); auto peerWithPremium = [&]() -> PeerData* { if (isEmojiStatus) { @@ -1074,14 +1173,35 @@ QPointer Premium::createPinnedToTop( } return nullptr; }(); + auto premiumGift = [&]() -> DocumentData* { + if (isPremiumGift) { + auto &data = _controller->session().data(); + return data.document(premiumGiftData.documentId); + } + return nullptr; + }(); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { if (peerWithPremium) { - return Ui::CreateChild( + return Ui::CreateChild( parent.get(), _controller, peerWithPremium, _showFinished.events()); + } else if (premiumGift) { + return Ui::CreateChild( + parent.get(), + _controller, + TopBarWithStickerArgs{ + .stickerValue = rpl::single(premiumGift), + .nameValue = tr::lng_gift_premium_title(), + .aboutValue = tr::lng_gift_premium_text( + lt_count, + rpl::single(premiumGiftData.perUserTotal * 1.), + Ui::Text::RichLangValue), + .type = TopBarWithStickerType::PremiumGift, + }, + _showFinished.events()); } const auto weak = base::make_weak(_controller); const auto clickContextOther = [=] { @@ -1112,7 +1232,7 @@ QPointer Premium::createPinnedToTop( }, content->lifetime()); const auto calculateMaximumHeight = [=] { - return isEmojiStatus + return (isEmojiStatus || isPremiumGift) ? st::settingsPremiumUserHeight + TopTransitionSkip() : st::settingsPremiumTopHeight; }; @@ -1178,14 +1298,14 @@ QPointer Premium::createPinnedToTop( } }, content->lifetime()); - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } void Premium::showFinished() { _showFinished.fire({}); } -QPointer Premium::createPinnedToBottom( +base::weak_qptr Premium::createPinnedToBottom( not_null parent) { const auto content = Ui::CreateChild(parent.get()); @@ -1288,7 +1408,7 @@ QPointer Premium::createPinnedToBottom( _subscribe->setVisible(!premium && premiumPossible); }, _subscribe->lifetime()); - return Ui::MakeWeak(not_null{ content }); + return base::make_weak(not_null{ content }); } } // namespace @@ -1362,6 +1482,15 @@ void ShowEmojiStatusPremium( } } +void ShowPremiumGiftPremium( + not_null controller, + const Data::StarGift &gift) { + ShowPremium(controller, Ref::PremiumGift::Serialize({ + .documentId = gift.document->id, + .perUserTotal = gift.perUserTotal, + })); +} + void StartPremiumPayment( not_null controller, const QString &ref) { diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 2effb1e1a4..19333ddee2 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -20,6 +20,10 @@ namespace ChatHelpers { class Show; } // namespace ChatHelpers +namespace Data { +struct StarGift; +} // namespace Data + namespace Ui { class RpWidget; class RoundButton; @@ -52,6 +56,9 @@ void ShowGiftPremium( void ShowEmojiStatusPremium( not_null controller, not_null peer); +void ShowPremiumGiftPremium( + not_null controller, + const Data::StarGift &gift); void StartPremiumPayment( not_null controller, diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 4c1910297e..57e794d736 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/self_destruction_box.h" #include "core/application.h" #include "core/core_settings.h" +#include "history/view/media/history_view_media_common.h" #include "ui/chat/chat_style.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" @@ -605,6 +606,19 @@ void SetupLoginEmail( button->addClickHandler([=, email = std::move(email)] { controller->uiShow()->show(Box([=](not_null box) { + { + box->getDelegate()->setTitle(rpl::duplicate( + email + ) | rpl::map(Ui::Text::WrapEmailPattern)); + for (const auto &child : ranges::views::reverse( + box->parentWidget()->children())) { + if (child && child->isWidgetType()) { + (static_cast(child))->setAttribute( + Qt::WA_TransparentForMouseEvents); + break; + } + } + } Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ .text = tr::lng_settings_cloud_login_email_box_about(), .confirmed = [=](Fn close) { @@ -613,9 +627,6 @@ void SetupLoginEmail( }, .confirmText = tr::lng_settings_cloud_login_email_box_ok(), }); - box->getDelegate()->setTitle(rpl::duplicate( - email - ) | rpl::map(Ui::Text::WrapEmailPattern)); })); }); @@ -971,7 +982,9 @@ void SetupSensitiveContent( Ui::AddSkip(inner); Ui::AddSubsectionTitle(inner, tr::lng_settings_sensitive_title()); + const auto show = controller->uiShow(); const auto session = &controller->session(); + const auto disable = inner->lifetime().make_state>(); std::move( updateTrigger @@ -982,13 +995,23 @@ void SetupSensitiveContent( inner, tr::lng_settings_sensitive_disable_filtering(), st::settingsButtonNoIcon - ))->toggleOn( - session->api().sensitiveContent().enabled() - )->toggledChanges( + ))->toggleOn(rpl::merge( + session->api().sensitiveContent().enabled(), + disable->events() | rpl::map_to(false) + ))->toggledChanges( ) | rpl::filter([=](bool toggled) { return toggled != session->api().sensitiveContent().enabledCurrent(); }) | rpl::start_with_next([=](bool toggled) { - session->api().sensitiveContent().update(toggled); + if (toggled && session->appConfig().ageVerifyNeeded()) { + disable->fire({}); + + HistoryView::ShowAgeVerificationRequired( + show, + session, + [] {}); + } else { + session->api().sensitiveContent().update(toggled); + } }, container->lifetime()); Ui::AddSkip(inner); diff --git a/Telegram/SourceFiles/settings/settings_shortcuts.cpp b/Telegram/SourceFiles/settings/settings_shortcuts.cpp index 9d5135f614..d65f96b4cd 100644 --- a/Telegram/SourceFiles/settings/settings_shortcuts.cpp +++ b/Telegram/SourceFiles/settings/settings_shortcuts.cpp @@ -104,6 +104,9 @@ struct Labeled { { C::SendSilentMessage, tr::lng_shortcuts_silent_send() }, { C::ScheduleMessage, tr::lng_shortcuts_schedule() }, separator, + { C::RecordVoice, tr::lng_shortcuts_record_voice_message() }, + { C::RecordRound, tr::lng_shortcuts_record_round_message() }, + separator, { C::MediaViewerFullscreen, tr::lng_shortcuts_media_fullscreen() }, separator, { C::MediaPlay, tr::lng_shortcuts_media_play() }, @@ -194,7 +197,7 @@ struct Labeled { }; checkModified(); - const auto menu = std::make_shared>(); + const auto menu = std::make_shared>(); const auto fill = [=](Entry &entry) { auto index = 0; if (entry.original.empty()) { diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp index 7f495998f0..f772945de9 100644 --- a/Telegram/SourceFiles/settings/settings_websites.cpp +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -112,45 +112,29 @@ void InfoBox( }); const auto userpic = box->addRow( - object_ptr>( + object_ptr( box, - object_ptr( - box, - data.bot, - st::websiteBigUserpic)), - st::sessionBigCoverPadding)->entity(); + data.bot, + st::websiteBigUserpic), + st::sessionBigCoverPadding, + style::al_top); userpic->overrideShape(Ui::PeerUserpicShape::Forum); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); - const auto nameWrap = box->addRow( - object_ptr( + box->addRow( + object_ptr( box, - st::sessionBigName.maxHeight)); - const auto name = Ui::CreateChild( - nameWrap, - rpl::single(data.bot->name()), - st::sessionBigName); - nameWrap->widthValue( - ) | rpl::start_with_next([=](int width) { - name->resizeToWidth(width); - name->move((width - name->width()) / 2, 0); - }, name->lifetime()); + rpl::single(data.bot->name()), + st::sessionBigName), + style::al_top); - const auto domainWrap = box->addRow( - object_ptr( + box->addRow( + object_ptr( box, - st::sessionDateLabel.style.font->height), - style::margins(0, 0, 0, st::sessionDateSkip)); - const auto domain = Ui::CreateChild( - domainWrap, - rpl::single(data.domain), - st::sessionDateLabel); - rpl::combine( - domainWrap->widthValue(), - domain->widthValue() - ) | rpl::start_with_next([=](int outer, int inner) { - domain->move((outer - inner) / 2, 0); - }, domain->lifetime()); + rpl::single(data.domain), + st::sessionDateLabel), + style::margins(0, 0, 0, st::sessionDateSkip), + style::al_top); using namespace Settings; const auto container = box->verticalLayout(); @@ -182,7 +166,7 @@ void InfoBox( box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); if (const auto hash = data.hash) { box->addLeftButton(tr::lng_settings_disconnect(), [=] { - const auto weak = Ui::MakeWeak(box.get()); + const auto weak = base::make_weak(box.get()); terminate(hash); if (weak) { box->closeBox(); @@ -334,7 +318,7 @@ private: Api::Websites::List _data; object_ptr _inner; - QPointer _terminateBox; + base::weak_qptr _terminateBox; base::Timer _shortPollTimer; @@ -492,7 +476,7 @@ void Content::terminate( rpl::producer title, rpl::producer text, QString blockText) { - if (const auto strong = _terminateBox.data()) { + if (const auto strong = _terminateBox.get()) { strong->deleteLater(); } auto box = Box([=](not_null box) { @@ -517,12 +501,12 @@ void Content::terminate( *block = box->addRow(object_ptr(box, blockText)); } }); - _terminateBox = Ui::MakeWeak(box.data()); + _terminateBox = base::make_weak(box.data()); _controller->show(std::move(box)); } void Content::terminateOne(uint64 hash) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); const auto i = ranges::find(_data, hash, &EntryData::hash); if (i == end(_data)) { return; @@ -552,7 +536,7 @@ void Content::terminateOne(uint64 hash) { } void Content::terminateAll() { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); auto callback = [=](bool block) { const auto reset = crl::guard(weak, [=] { _websites->cancelCurrentRequest(); diff --git a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp index 6241901804..c559a23684 100644 --- a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp +++ b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_tag.h" +#include + namespace Statistic { namespace { @@ -25,7 +27,7 @@ constexpr auto kStep = 5.; static constexpr auto kTooMuch = ChartValue(10'000); return (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string - : QString::number(absoluteValue); + : QLocale().toString(absoluteValue); } } // namespace diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index 8cc22a9b96..8f7395a340 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -159,6 +159,10 @@ getBoostsButtonIcon: icon {{ "menu/gift_premium", lightButtonFg }}; boostsListMiniIcon: icon{{ "boosts/boost_mini2", premiumButtonFg }}; boostsListMiniIconPadding: margins(1px, 0px, 0px, 0px); boostsListMiniIconSkip: 1px; +boostsListEntryIcon: IconEmoji { + icon: boostsListMiniIcon; + padding: margins(6px, 2px, 0px, 0px); +} boostsListBadgeTextPadding: margins(16px, 1px, 6px, 0px); boostsListBadgePadding: margins(4px, 1px, 4px, 0px); boostsListBadgeHeight: 16px; diff --git a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp index cdc8dc29f2..9034ed1d26 100644 --- a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp +++ b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp @@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_basic.h" #include "styles/style_statistics.h" +#include + namespace Statistic { namespace { @@ -25,7 +27,7 @@ namespace { ? u"0"_q : (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string - : QString::number(absoluteValue); + : QLocale().toString(absoluteValue); } } // namespace diff --git a/Telegram/SourceFiles/statistics/view/stack_linear_chart_common.cpp b/Telegram/SourceFiles/statistics/view/stack_linear_chart_common.cpp index b7727bd714..50b86284f1 100644 --- a/Telegram/SourceFiles/statistics/view/stack_linear_chart_common.cpp +++ b/Telegram/SourceFiles/statistics/view/stack_linear_chart_common.cpp @@ -83,6 +83,8 @@ PiePartData PiePartsPercentageByIndices( for (const auto &line : chartData.lines) { auto sum = ChartValue(0); for (auto i = xIndices.min; i <= xIndices.max; i++) { + const auto index = int(base::SafeRound(i)); + Assert(index >= 0 && index < line.y.size()); sum += line.y[i]; } if (linesFilter) { diff --git a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp index c000d919b9..b5e7daa6b2 100644 --- a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp +++ b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp @@ -303,6 +303,7 @@ void PointDetailsWidget::setXIndex(int xIndex) { if (xIndex < 0) { return; } + Assert(xIndex < _chartData.x.size()); { constexpr auto kOneDay = 3600 * 24 * 1000; const auto timestamp = _chartData.x[xIndex]; @@ -328,6 +329,7 @@ void PointDetailsWidget::setXIndex(int xIndex) { = (_chartData.currency == Data::StatisticalCurrency::Credits); for (auto i = 0; i < _chartData.lines.size(); i++) { const auto &dataLine = _chartData.lines[i]; + Assert(xIndex < dataLine.y.size()); auto textLine = Line(); textLine.id = dataLine.id; if (_maxPercentageWidth) { diff --git a/Telegram/SourceFiles/stdafx.h b/Telegram/SourceFiles/stdafx.h index 6647362881..1bc3da35d3 100644 --- a/Telegram/SourceFiles/stdafx.h +++ b/Telegram/SourceFiles/stdafx.h @@ -118,6 +118,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/optional.h" #include "base/variant.h" #include "base/weak_ptr.h" +#include "base/weak_qptr.h" #include "scheme.h" #include "mtproto/type_utils.h" @@ -128,7 +129,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/arc_angles.h" #include "ui/emoji_config.h" #include "ui/qt_object_factory.h" -#include "ui/qt_weak_factory.h" #include "ui/ui_rpl_filter.h" #include "styles/palette.h" diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 45ad83edde..8122a476a2 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -520,7 +520,7 @@ void ConfirmContactBox::prepare() { _contact->initDimensions(); _submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) { - const auto weak = Ui::MakeWeak(this); + const auto weak = base::make_weak(this); original(m); if (weak) { closeBox(); diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index d2cfeafca4..6dab099149 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -451,7 +451,7 @@ void BoostBox( : tr::lng_boost_channel_button(); }) | rpl::flatten_latest(); - const auto button = box->addButton(rpl::duplicate(submit), [=] { + box->addButton(rpl::duplicate(submit), [=] { if (state->submitted) { return; } else if (state->data.current().nextLevelBoosts > 0 @@ -519,17 +519,6 @@ void BoostBox( box->closeBox(); } }); - - rpl::combine( - std::move(submit), - box->widthValue() - ) | rpl::start_with_next([=](const QString &, int width) { - const auto &padding = st::boostBox.buttonPadding; - button->resizeToWidth(width - - padding.left() - - padding.right()); - button->moveToLeft(padding.left(), button->y()); - }, button->lifetime()); } object_ptr MakeLinkLabel( @@ -763,14 +752,16 @@ void AskBoostBox( box, std::move(title), st::boostCenteredTitle), - st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0)); + st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0), + style::al_top); box->addRow( object_ptr( box, std::move(text), st::boostText), (st::boxRowPadding - + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), + style::al_top); auto stats = object_ptr(box, st::boostLinkStatsButton); stats->setClickedCallback(openStatistics); @@ -788,20 +779,10 @@ void AskBoostBox( data.group); auto submit = tr::lng_boost_channel_ask_button(); - const auto button = box->addButton(rpl::duplicate(submit), [=] { + box->addButton(rpl::duplicate(submit), [=] { QGuiApplication::clipboard()->setText(data.link); box->uiShow()->showToast(tr::lng_username_copied(tr::now)); }); - rpl::combine( - std::move(submit), - box->widthValue() - ) | rpl::start_with_next([=](const QString &, int width) { - const auto &padding = st::boostBox.buttonPadding; - button->resizeToWidth(width - - padding.left() - - padding.right()); - button->moveToLeft(padding.left(), button->y()); - }, button->lifetime()); } void FillBoostLimit( diff --git a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp index 8651b5b010..0dfc516056 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp @@ -147,7 +147,7 @@ ChooseDateTimeBoxDescriptor ChooseDateTimeBox( }, content->lifetime()); const auto calendar - = content->lifetime().make_state>(); + = content->lifetime().make_state>(); const auto calendarStyle = args.style.calendarStyle; state->day->focusedChanges( ) | rpl::start_with_next([=](bool focused) { diff --git a/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp index 97f65b65d9..a891c422f1 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_font_box.cpp @@ -819,7 +819,7 @@ void PreviewPainter::layout() { const auto ratio = state->preview.devicePixelRatio(); raw->resize(state->preview.size() / int(ratio)); } else { - const auto weak = Ui::MakeWeak(raw); + const auto weak = base::make_weak(raw); const auto request = PrepareRequest(family); crl::async([=, bg = state->bg] { crl::on_main([ @@ -827,7 +827,7 @@ void PreviewPainter::layout() { state, preview = GeneratePreview(bg, request) ]() mutable { - if (const auto strong = weak.data()) { + if (const auto strong = weak.get()) { state->preview = std::move(preview); const auto ratio = state->preview.devicePixelRatio(); strong->resize( diff --git a/Telegram/SourceFiles/ui/boxes/choose_time.cpp b/Telegram/SourceFiles/ui/boxes/choose_time.cpp index aa7f2836e2..ba2bdba9a7 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_time.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_time.cpp @@ -76,9 +76,9 @@ ChooseTimeResult ChooseTimeWidget( QString::number(startMinutes)), }); - const auto day = Ui::MakeWeak(state->day); - const auto hour = Ui::MakeWeak(state->hour); - const auto minute = Ui::MakeWeak(state->minute); + const auto day = base::make_weak(state->day); + const auto hour = base::make_weak(state->hour); + const auto minute = base::make_weak(state->minute); if (hiddenDaysInput) { day->setVisible(false); @@ -88,24 +88,24 @@ ChooseTimeResult ChooseTimeWidget( day->setMaxValue(31); day->setWheelStep(1); day->putNext() | rpl::start_with_next([=](QChar ch) { - putNext(hour, ch); + putNext(hour.get(), ch); }, content->lifetime()); hour->setPhrase(tr::lng_hours); hour->setMaxValue(23); hour->setWheelStep(1); hour->putNext() | rpl::start_with_next([=](QChar ch) { - putNext(minute, ch); + putNext(minute.get(), ch); }, content->lifetime()); hour->erasePrevious() | rpl::start_with_next([=] { - erasePrevious(day); + erasePrevious(day.get()); }, content->lifetime()); minute->setPhrase(tr::lng_minutes); minute->setMaxValue(59); minute->setWheelStep(10); minute->erasePrevious() | rpl::start_with_next([=] { - erasePrevious(hour); + erasePrevious(hour.get()); }, content->lifetime()); content->sizeValue( @@ -127,9 +127,9 @@ ChooseTimeResult ChooseTimeWidget( rpl::merge( rpl::single(rpl::empty), - base::qt_signal_producer(day.data(), &MaskedInputField::changed), - base::qt_signal_producer(hour.data(), &MaskedInputField::changed), - base::qt_signal_producer(minute.data(), &MaskedInputField::changed) + base::qt_signal_producer(day.get(), &MaskedInputField::changed), + base::qt_signal_producer(hour.get(), &MaskedInputField::changed), + base::qt_signal_producer(minute.get(), &MaskedInputField::changed) ) | rpl::start_with_next([=] { state->valueInSeconds = 0 + day->getLastText().toUInt() * 3600 * 24 diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp index fb0300664e..f9c24efb5e 100644 --- a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "settings/settings_common.h" #include "styles/style_boxes.h" +#include "styles/style_credits.h" #include "styles/style_layers.h" #include @@ -47,16 +48,14 @@ constexpr auto kTonMultiplier = uint64(1000000000); return langDateTime(base::unixtime::parse(date)); } -[[nodiscard]] TextWithEntities FormatPrice( - const CollectibleInfo &info, - const CollectibleDetails &details) { +[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) { auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount); if (minor.size() == 1 && minor.at(0) == '.') { minor += '0'; } auto price = (info.cryptoCurrency == u"TON"_q) - ? base::duplicate( - details.tonEmoji + ? Ui::Text::IconEmoji( + &st::tonIconEmoji ).append( Info::ChannelEarn::MajorPart(info.cryptoAmount) ).append(minor) @@ -123,8 +122,7 @@ CollectibleType DetectCollectibleType(const QString &entity) { void CollectibleInfoBox( not_null box, - CollectibleInfo info, - CollectibleDetails details) { + CollectibleInfo info) { box->setWidth(st::boxWideWidth); box->setStyle(st::collectibleBox); @@ -203,7 +201,8 @@ void CollectibleInfoBox( box, rpl::single(header), st::collectibleHeader), - st::collectibleHeaderPadding + st::collectibleHeaderPadding, + style::al_top )->setClickHandlerFilter([copyCallback](const auto &...) { copyCallback(false); return false; @@ -218,15 +217,14 @@ void CollectibleInfoBox( lt_date, TextWithEntities{ FormatDate(info.date) }, lt_price, - FormatPrice(info, details), + FormatPrice(info), Ui::Text::RichLangValue); const auto label = box->addRow( object_ptr(box, st::collectibleInfo), - st::collectibleInfoPadding); + st::collectibleInfoPadding, + style::al_top); label->setAttribute(Qt::WA_TransparentForMouseEvents); - auto context = details.tonEmojiContext; - context.repaint = [label] { label->update(); }; - label->setMarkedText(text, context); + label->setMarkedText(text); const auto more = box->addRow( object_ptr( diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h index 9080819c98..2fc94dfc55 100644 --- a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h @@ -32,14 +32,6 @@ struct CollectibleInfo { TimeId date = 0; }; -struct CollectibleDetails { - TextWithEntities tonEmoji; - Text::MarkedContext tonEmojiContext; -}; - -void CollectibleInfoBox( - not_null box, - CollectibleInfo info, - CollectibleDetails details); +void CollectibleInfoBox(not_null box, CollectibleInfo info); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp index 0c06fb5262..6d9f25c984 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { void ConfirmBox(not_null box, ConfirmBoxArgs &&args) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); const auto lifetime = box->lifetime().make_state(); const auto withTitle = !v::is_null(args.title); diff --git a/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp index 94d63e6fe6..e217bb1a19 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp @@ -161,7 +161,7 @@ void EditBirthdayBox( const auto daysCount = (year == maxYear && month == max.month()) ? max.day() : (month == 2) - ? ((!year || ((year % 4) && (!(year % 100) || (year % 400)))) + ? ((!year || (!(year % 4) && ((year % 100) || !(year % 400)))) ? 29 : 28) : ((month == 4) || (month == 6) || (month == 9) || (month == 11)) diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp index 49905b87e2..4b0bf0c136 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp @@ -286,7 +286,7 @@ void EditInviteLinkBox( } }; - const auto guard = MakeWeak(box); + const auto guard = base::make_weak(box); expireGroup->setChangedCallback([=](int value) { if (value) { state->expireValue = value; diff --git a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp index f868cc3e97..2f09147979 100644 --- a/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp +++ b/Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp @@ -24,7 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/number_input.h" -#include "ui/widgets/label_with_custom_emoji.h" #include "ui/widgets/labels.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -129,7 +128,7 @@ InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( state->usdRate = peer->session().credits().rateValue(peer); - auto about = Ui::CreateLabelWithCustomEmoji( + auto about = object_ptr( container, tr::lng_group_invite_subscription_about( lt_link, @@ -143,7 +142,6 @@ InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle( tr::lng_group_invite_subscription_about_url(tr::now)); }), Ui::Text::RichLangValue), - Core::TextContext({ .session = &peer->session() }), st::boxDividerLabel); Ui::AddSkip(wrap->entity()); Ui::AddSkip(wrap->entity()); diff --git a/Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp b/Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp index e49e457454..37f3913584 100644 --- a/Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp @@ -843,14 +843,7 @@ void FillPeerQrBox( if (state->saveButtonBusy.current()) { return; } - const auto buttonWidth = state->saveButton - ? state->saveButton->width() - : 0; state->saveButtonBusy = true; - if (state->saveButton) { - state->saveButton->resizeToWidth(buttonWidth); - } - const auto userpicToggled = state->userpicToggled.current(); const auto backgroundToggled = state->backgroundToggled.current(); const auto scale = style::kScaleDefault @@ -897,7 +890,7 @@ void FillPeerQrBox( const auto top = photoSize ? userpicMedia->image(photoSize) : QImage(); - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); crl::async([=] { const auto qrImage = TelegramQr( @@ -973,13 +966,6 @@ void FillPeerQrBox( loadingAnimation->showOn(state->saveButtonBusy.value()); } - const auto buttonWidth = box->width() - - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding); - state->saveButton->widthValue() | rpl::filter([=] { - return (state->saveButton->widthNoMargins() != buttonWidth); - }) | rpl::start_with_next([=] { - state->saveButton->resizeToWidth(buttonWidth); - }, state->saveButton->lifetime()); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); } diff --git a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp index 426ddce42e..f99b9ff439 100644 --- a/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp @@ -69,7 +69,7 @@ object_ptr MakeShowOrLabel( const auto full = st::showOrLineWidth; const auto left = (raw->width() - full) / 2; - const auto text = raw->textMaxWidth() + 2 * st::showOrLabelSkip; + const auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip; const auto fill = (full - text) / 2; const auto stroke = st::lineWidth; const auto top = st::showOrLineTop; @@ -152,13 +152,15 @@ void ShowOrPremiumBox( box, std::move(skin.showTitle), st::boostCenteredTitle), - st::showOrTitlePadding + buttonPadding); + st::showOrTitlePadding + buttonPadding, + style::al_top); box->addRow( object_ptr( box, std::move(skin.showAbout), st::boostText), - st::showOrAboutPadding + buttonPadding); + st::showOrAboutPadding + buttonPadding, + style::al_top); const auto show = box->addRow( object_ptr( box, @@ -168,19 +170,22 @@ void ShowOrPremiumBox( show->setTextTransform(RoundButton::TextTransform::NoTransform); box->addRow( MakeShowOrLabel(box, std::move(skin.orPremium)), - st::showOrLabelPadding + buttonPadding); + st::showOrLabelPadding + buttonPadding, + style::al_justify); box->addRow( object_ptr( box, std::move(skin.premiumTitle), st::boostCenteredTitle), - st::showOrTitlePadding + buttonPadding); + st::showOrTitlePadding + buttonPadding, + style::al_top); box->addRow( object_ptr( box, std::move(skin.premiumAbout), st::boostText), - st::showOrPremiumAboutPadding + buttonPadding); + st::showOrPremiumAboutPadding + buttonPadding, + style::al_top); const auto premium = CreateChild( box.get(), diff --git a/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp index 2083a0f483..10303d64a9 100644 --- a/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp @@ -46,7 +46,7 @@ void SingleChoiceBox( } const auto callback = args.callback.value(); group->setChangedCallback([=](int value) { - const auto weak = Ui::MakeWeak(box); + const auto weak = base::make_weak(box); callback(value); if (weak) { box->closeBox(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 1891e7fb9c..46bfbd89e7 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -1026,6 +1026,16 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { secureStorageFailed(arguments); } else if (command == "web_app_secure_storage_clear") { secureStorageFailed(arguments); + } else if (command == "web_app_verify_age") { + const auto passed = arguments["passed"]; + const auto detected = arguments["age"]; + const auto valid = passed.isBool() + && passed.toBool() + && detected.isDouble(); + const auto age = valid + ? int(std::floor(detected.toDouble())) + : 0; + _delegate->botVerifyAge(age); } else if (command == "share_score") { _delegate->botHandleMenuButton(MenuButton::ShareGame); } @@ -2019,10 +2029,13 @@ void Panel::hideLayer(anim::type animated) { void Panel::showCriticalError(const TextWithEntities &text) { _progress = nullptr; _webviewProgress = false; - auto error = base::make_unique_q>( - _widget.get(), + auto wrap = base::make_unique_q(_widget.get()); + const auto raw = wrap.get(); + + const auto error = CreateChild>( + raw, object_ptr( - _widget.get(), + raw, rpl::single(text), st::paymentsCriticalError), st::paymentsCriticalErrorPadding); @@ -2036,7 +2049,13 @@ void Panel::showCriticalError(const TextWithEntities &text) { File::OpenUrl(entity.data); return false; }); - _widget->showInner(std::move(error)); + + raw->widthValue() | rpl::start_with_next([=](int width) { + error->resizeToWidth(width); + raw->resize(width, error->height()); + }, raw->lifetime()); + + _widget->showInner(std::move(wrap)); } void Panel::updateThemeParams(const Webview::ThemeParams ¶ms) { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 72edb0b391..63b11fc624 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -105,6 +105,7 @@ public: virtual void botDownloadFile(DownloadFileRequest request) = 0; virtual void botSendPreparedMessage( SendPreparedMessageRequest request) = 0; + virtual void botVerifyAge(int age) = 0; virtual void botOpenPrivacyPolicy() = 0; virtual void botClose() = 0; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index d35739f88d..9b9b8b3da3 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -34,14 +34,22 @@ historyReplyBottom: 2px; historyReplyPreview: 32px; historyReplyPreviewMargin: margins(7px, 4px, 4px, 4px); historyReplyPadding: margins(11px, 2px, 6px, 2px); -historyReplyUser: icon {{ "chat/reply_type_user", windowFg }}; -historyReplyUserPadding: margins(0px, 4px, 4px, 0px); -historyReplyGroup: icon {{ "chat/reply_type_group", windowFg }}; -historyReplyGroupPadding: margins(0px, 4px, 4px, 0px); -historyReplyChannel: icon {{ "chat/reply_type_channel", windowFg }}; -historyReplyChannelPadding: margins(0px, 5px, 4px, 0px); -historyReplyForward: icon {{ "mini_forward", windowFg }}; -historyReplyForwardPadding: margins(0px, 2px, 2px, 0px); +historyReplyUser: IconEmoji { + icon: icon {{ "chat/reply_type_user", windowFg }}; + padding: margins(0px, 4px, 4px, 0px); +} +historyReplyGroup: IconEmoji { + icon: icon {{ "chat/reply_type_group", windowFg }}; + padding: margins(0px, 4px, 4px, 0px); +} +historyReplyChannel: IconEmoji { + icon: icon {{ "chat/reply_type_channel", windowFg }}; + padding: margins(0px, 5px, 4px, 0px); +} +historyReplyForward: IconEmoji { + icon: icon {{ "mini_forward", windowFg }}; + padding: margins(0px, 2px, 2px, 0px); +} msgReplyPadding: margins(6px, 6px, 11px, 6px); msgReplyBarPos: point(1px, 0px); @@ -520,6 +528,7 @@ historyVoiceMessageOutTTLSelected: icon {{ "chat/mini_media_once", historyFileOu historyTranscribeSkip: 10px; historyTranscribeSize: size(28px, 22px); historyTranscribeRadius: 4px; +historyTranscribeLoadingSize: 24px; historyTranscribeInIcon: icon {{ "chat/voice_to_text", msgFileInBg }}; historyTranscribeInIconSelected: icon {{ "chat/voice_to_text", msgFileInBgSelected }}; historyTranscribeOutIcon: icon {{ "chat/voice_to_text", msgFileOutBg }}; @@ -939,6 +948,8 @@ msgServiceStarGiftBoxWidth: 224px; msgServiceStarGiftByWidth: 272px; msgServiceStarGiftStickerTop: 24px; msgServiceStarGiftStickerSize: 100px; +msgServiceGiftThemeStickerSize: 64px; +msgServiceGiftThemeStickerPadding: margins(18px, 18px, 18px, 18px); historySponsorInfoItem: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { @@ -1096,13 +1107,18 @@ repliesComposeControls: ComposeControls(defaultComposeControls) { tabbedHeightMin: 220px; } -boostMessageIcon: icon {{ "stories/boost_mini", windowFg }}; -boostMessageIconPadding: margins(0px, 2px, 0px, 0px); -boostsMessageIcon: icon {{ "stories/boosts_mini", windowFg }}; -boostsMessageIconPadding: margins(0px, 2px, 0px, 0px); - -historyIvIcon: icon{{ "boosts/boost_mini2", windowFg }}; -historyIvIconPadding: margins(2px, 2px, 2px, 0px); +boostMessageIcon: IconEmoji { + icon: icon {{ "stories/boost_mini", windowFg }}; + padding: margins(0px, 2px, 0px, 0px); +} +boostsMessageIcon: IconEmoji { + icon: icon {{ "stories/boosts_mini", windowFg }}; + padding: margins(0px, 2px, 0px, 0px); +} +historyIvIcon: IconEmoji { + icon: icon {{ "boosts/boost_mini2", windowFg }}; + padding: margins(2px, 2px, 2px, 0px); +} chatIntroStickerSize: 96px; chatIntroWidth: 224px; @@ -1373,7 +1389,6 @@ chatTabsOutlineHorizontal: ChatTabsOutline { chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) { } -suggestPriceTonIconMargins: margins(0px, 2px, 0px, 0px); suggestPriceBox: Box(defaultBox) { buttonPadding: margins(22px, 22px, 22px, 22px); buttonHeight: 42px; @@ -1404,7 +1419,7 @@ tonInput: InputField(defaultInputField) { } starsFieldIconPosition: point(0px, 10px); tonFieldIconSize: 16px; -tonFieldIconPosition: point(2px, 9px); +tonFieldIconPosition: point(2px, 11px); suggestBarTonIconSize: 14px; suggestBarTonIconMargins: margins(0px, 3px, 0px, 0px); @@ -1416,3 +1431,26 @@ lowTonText: FlatLabel(defaultFlatLabel) { minWidth: 100px; align: align(top); } + +groupMembersWidgetList: PeerList(defaultPeerList) { + item: PeerListItem(defaultPeerListItem) { + photoPosition: point(12px, 6px); + namePosition: point(68px, 11px); + statusPosition: point(68px, 31px); + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; + } +} + +saveMusicInfoMenu: Menu(defaultMenu) { + itemBgOver: windowBg; + itemFgOver: windowFg; + itemPadding: margins(17px, 4px, 17px, 3px); +} diff --git a/Telegram/SourceFiles/ui/chat/chat_theme.cpp b/Telegram/SourceFiles/ui/chat/chat_theme.cpp index 4979e5c4ad..0d1424d4a1 100644 --- a/Telegram/SourceFiles/ui/chat/chat_theme.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_theme.cpp @@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/chat/chat_theme.h" -#include "ui/image/image_prepare.h" +#include "ui/color_contrast.h" +#include "ui/emoji_config.h" +#include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" #include "ui/chat/message_bubble.h" #include "ui/chat/chat_style.h" -#include "ui/color_contrast.h" +#include "ui/image/image_prepare.h" +#include "ui/text/text_custom_emoji.h" #include "ui/style/style_core_palette.h" #include "ui/style/style_palette_colorizer.h" @@ -53,6 +56,27 @@ constexpr auto kMinAcceptableContrast = 1.14;// 4.5; return (doubled % 2) ? 0.5 : 1.; } +[[nodiscard]] int ChooseGiftSymbolSkip( + const std::vector &symbols) { + if (symbols.empty()) { + return -1; + } + auto maxIndex = -1; + auto maxValue = 0.; + for (auto i = 0, count = int(symbols.size()); i != count; ++i) { + const auto &symbol = symbols[i]; + const auto center = symbol.area.center(); + const auto value = std::min(center.x(), 1. - center.x()) + * std::min(center.y(), 1. - center.y()) + * std::min(symbol.area.width(), symbol.area.height()); + if (maxIndex < 0 || maxValue < value) { + maxIndex = i; + maxValue = value; + } + } + return maxIndex; +} + [[nodiscard]] CacheBackgroundResult CacheBackgroundByRequest( const CacheBackgroundRequest &request) { Expects(!request.area.isEmpty()); @@ -79,18 +103,23 @@ constexpr auto kMinAcceptableContrast = 1.14;// 4.5; Qt::IgnoreAspectRatio, Qt::SmoothTransformation); result.setDevicePixelRatio(ratio); + auto giftArea = QRect(); + float64 giftRotation = 0.; if (!request.background.prepared.isNull()) { QPainter p(&result); if (!gradient.isNull()) { if (request.background.patternOpacity >= 0.) { - p.setCompositionMode(QPainter::CompositionMode_SoftLight); + p.setCompositionMode( + QPainter::CompositionMode_SoftLight); p.setOpacity(request.background.patternOpacity); } else { p.setCompositionMode( QPainter::CompositionMode_DestinationIn); } } - const auto tiled = request.background.isPattern + const auto hasGiftSymbols = request.background.isPattern + && !request.background.giftSymbols.empty(); + auto tiled = request.background.isPattern ? request.background.prepared.scaled( request.area.height() * ratio, request.area.height() * ratio, @@ -99,6 +128,47 @@ constexpr auto kMinAcceptableContrast = 1.14;// 4.5; : request.background.preparedForTiled; const auto w = tiled.width() / float(ratio); const auto h = tiled.height() / float(ratio); + + const auto &giftSymbols = request.background.giftSymbols; + const auto giftSymbolsCount = int(giftSymbols.size()); + const auto giftSymbolSkip = ChooseGiftSymbolSkip(giftSymbols); + const auto &giftSymbolFrame = request.background.giftSymbolFrame; + const auto giftSymbolRect = [&](const QRectF &symbol) { + return QRectF( + symbol.x() * w, + symbol.y() * h, + symbol.width() * w, + symbol.height() * h); + }; + const auto giftSymbolPaint = [&]( + QPainter &p, + ChatThemeGiftSymbol symbol) { + const auto rect = giftSymbolRect(symbol.area); + + const auto esize = Ui::Emoji::GetSizeLarge() / ratio; + const auto custom = Ui::Text::AdjustCustomEmojiSize(esize); + const auto coef = rect.width() / float64(custom); + + p.save(); + p.translate(rect.center()); + p.rotate(symbol.rotation); + p.translate(-rect.center()); + p.translate(rect.topLeft()); + p.scale(coef, coef); + p.translate(-rect.topLeft()); + p.drawImage(rect.topLeft(), giftSymbolFrame); + p.restore(); + }; + if (hasGiftSymbols) { + auto q = QPainter(&tiled); + auto hq = PainterHighQualityEnabler(q); + q.setOpacity(0.8); + for (auto i = 0; i != giftSymbolsCount; ++i) { + if (i != giftSymbolSkip) { + giftSymbolPaint(q, giftSymbols[i]); + } + } + } const auto cx = int(std::ceil(request.area.width() / w)); const auto cy = int(std::ceil(request.area.height() / h)); const auto rows = cy; @@ -109,9 +179,33 @@ constexpr auto kMinAcceptableContrast = 1.14;// 4.5; ? (request.area.width() * ratio - cols * tiled.width()) / 2 : 0; const auto useshift = xshift / float(ratio); + const auto drawTile = [&](int x, int y) { + const auto position = QPointF(useshift + x * w, y * h); + p.drawImage(position, tiled); + }; + + // Skip a symbol in the center for the gift itself. + drawTile(cols / 2, 0); + if (hasGiftSymbols) { + const auto &gift = giftSymbols[giftSymbolSkip]; + auto q = QPainter(&tiled); + auto hq = PainterHighQualityEnabler(q); + q.setOpacity(0.8); + giftSymbolPaint(q, gift); + const auto exact = giftSymbolRect(gift.area); + giftArea = QRect( + useshift + (cols / 2) * w + exact.x(), + exact.y(), + exact.width(), + exact.height()); + giftRotation = gift.rotation; + } + for (auto y = 0; y != rows; ++y) { for (auto x = 0; x != cols; ++x) { - p.drawImage(QPointF(useshift + x * w, y * h), tiled); + if (y || x != (cols / 2)) { + drawTile(x, y); + } } } if (!gradient.isNull() @@ -127,6 +221,8 @@ constexpr auto kMinAcceptableContrast = 1.14;// 4.5; QImage::Format_ARGB32_Premultiplied), .gradient = gradient, .area = request.area, + .giftArea = giftArea, + .giftRotation = giftRotation, .waitingForNegativePattern = request.background.waitingForNegativePattern() }; @@ -199,6 +295,8 @@ CachedBackground::CachedBackground(CacheBackgroundResult &&result) , area(result.area) , x(result.x) , y(result.y) +, giftArea(result.giftArea) +, giftRotation(result.giftRotation) , waitingForNegativePattern(result.waitingForNegativePattern) { } @@ -415,6 +513,8 @@ void ChatTheme::setBackground(ChatThemeBackground &&background) { void ChatTheme::updateBackgroundImageFrom(ChatThemeBackground &&background) { _mutableBackground.key = background.key; _mutableBackground.prepared = std::move(background.prepared); + _mutableBackground.giftSymbols = std::move(background.giftSymbols); + _mutableBackground.giftId = background.giftId; _mutableBackground.preparedForTiled = std::move( background.preparedForTiled); if (!_backgroundState.now.pixmap.isNull()) { @@ -534,7 +634,8 @@ bool ChatTheme::readyForBackgroundRotation() const { } void ChatTheme::generateNextBackgroundRotation() { - if (_backgroundCachingRequest + if (_nextCachingRequest + || _backgroundCachingRequest || !_backgroundNext.image.isNull() || !readyForBackgroundRotation() || background().colors.size() < 3) { @@ -547,8 +648,9 @@ void ChatTheme::generateNextBackgroundRotation() { if (!request) { return; } + _nextCachingRequest = request; cacheBackgroundAsync(request, [=](CacheBackgroundResult &&result) { - const auto forRequest = base::take(_backgroundCachingRequest); + const auto forRequest = base::take(_nextCachingRequest); if (!readyForBackgroundRotation()) { return; } @@ -600,7 +702,9 @@ void ChatTheme::cacheBackgroundNow() { void ChatTheme::cacheBackgroundAsync( const CacheBackgroundRequest &request, Fn done) { - _backgroundCachingRequest = request; + if (!done) { + _backgroundCachingRequest = request; + } const auto weak = base::make_weak(this); crl::async([=] { if (!weak) { @@ -905,19 +1009,90 @@ QImage PrepareImageForTiled(const QImage &prepared) { return result; } -[[nodiscard]] QImage ReadBackgroundImage( +std::vector ParseGiftSymbols( + const QByteArray &rects, + QSize size, + float64 scale) { + if (size.isEmpty()) { + return {}; + } + + auto result = std::vector(); + auto offset = 0; + const auto jumpPast = [&](const QByteArray &what) { + const auto pos = rects.indexOf(what, offset); + if (pos < 0) { + return false; + } + offset = pos + what.size(); + return true; + }; + const auto takeDouble = [&](float64 &value, char finish = '"') { + const auto end = rects.indexOf(finish, offset); + if (end < 0) { + return false; + } + value = rects.mid(offset, end - offset).toDouble(); + offset = end + 1; + return true; + }; + const auto maybeTakeRotation = [&](float64 &value) { + const auto end = rects.indexOf("/>"_q, offset); + const auto pos = rects.indexOf("rotate("_q, offset); + if (pos > offset && end > offset && pos < end) { + offset = pos + 7; + return takeDouble(value, ')'); + } + return true; + }; + const auto cw = scale / float64(size.width()); + const auto ch = scale / float64(size.height()); + while (true) { + auto x = float64(), y = float64(), w = float64(), h = float64(); + auto rotation = float64(); + if (!jumpPast(""_q)) { + break; + } + result.push_back({ .area = { + x * cw, + y * ch, + w * cw, + h * ch, + }, .rotation = rotation }); + } + return result; +} + +BackgroundImageFields ReadBackgroundImage( const QString &path, const QByteArray &content, - bool gzipSvg) { - auto result = Images::Read({ + bool gzipSvg, + bool findGiftSymbols) { + auto read = Images::Read({ .path = path, .content = content, + .svgCutOutId = findGiftSymbols ? "GiftPatterns"_q : QByteArray(), .maxSize = QSize(kMaxSize, kMaxSize), .gzipSvg = gzipSvg, - }).image; - if (result.isNull()) { - result = QImage(1, 1, QImage::Format_ARGB32_Premultiplied); - result.fill(Qt::black); + }); + auto result = BackgroundImageFields{ + .image = std::move(read.image), + }; + if (const auto &rects = read.svgCutOutContent; !rects.isEmpty()) { + result.giftSymbols = ParseGiftSymbols( + rects, + result.image.size(), + read.scale); } return result; } @@ -1022,9 +1197,16 @@ QImage GenerateDitheredGradient( ChatThemeBackground PrepareBackgroundImage( const ChatThemeBackgroundData &data) { - auto prepared = (data.isPattern || data.colors.empty()) - ? PreprocessBackgroundImage( - ReadBackgroundImage(data.path, data.bytes, data.gzipSvg)) + const auto needBackground = (data.isPattern || data.colors.empty()); + auto read = needBackground + ? ReadBackgroundImage( + data.path, + data.bytes, + data.gzipSvg, + !data.giftSymbolFrame.isNull()) + : BackgroundImageFields(); + auto prepared = needBackground + ? PreprocessBackgroundImage(std::move(read.image)) : QImage(); if (data.isPattern && !prepared.isNull()) { if (data.colors.size() < 2) { @@ -1070,10 +1252,33 @@ ChatThemeBackground PrepareBackgroundImage( ? std::nullopt : std::make_optional(data.colors.front())), .colors = data.colors, + .giftSymbols = std::move(read.giftSymbols), + .giftSymbolFrame = data.giftSymbolFrame, + .giftId = data.giftId, .patternOpacity = data.patternOpacity, .gradientRotation = data.generateGradient ? data.gradientRotation : 0, .isPattern = data.isPattern, }; } +[[nodiscard]] QImage PrepareGiftSymbol( + const std::unique_ptr &emoji) { + if (!emoji || !emoji->ready()) { + return QImage(); + } + const auto ratio = style::DevicePixelRatio(); + const auto esize = Ui::Emoji::GetSizeLarge() / ratio; + const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); + auto result = QImage( + QSize(customSize, customSize) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + auto p = QPainter(&result); + emoji->paint(p, { + .textColor = QColor(0, 0, 0), + }); + return result; +} + } // namespace Window::Theme diff --git a/Telegram/SourceFiles/ui/chat/chat_theme.h b/Telegram/SourceFiles/ui/chat/chat_theme.h index 91c2599e78..5872613edf 100644 --- a/Telegram/SourceFiles/ui/chat/chat_theme.h +++ b/Telegram/SourceFiles/ui/chat/chat_theme.h @@ -16,12 +16,21 @@ class palette; struct colorizer; } // namespace style +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace Ui { class ChatStyle; struct ChatPaintContext; struct BubblePattern; +struct ChatThemeGiftSymbol { + QRectF area; + float64 rotation = 0.; +}; + struct ChatThemeBackground { QString key; QImage prepared; @@ -29,6 +38,9 @@ struct ChatThemeBackground { QImage gradientForFill; std::optional colorForFill; std::vector colors; + std::vector giftSymbols; + QImage giftSymbolFrame; + uint64_t giftId = 0; float64 patternOpacity = 1.; int gradientRotation = 0; bool isPattern = false; @@ -46,6 +58,8 @@ struct ChatThemeBackgroundData { QString key; QString path; QByteArray bytes; + QImage giftSymbolFrame; + uint64 giftId = 0; bool gzipSvg = false; std::vector colors; bool isPattern = false; @@ -87,6 +101,8 @@ struct CacheBackgroundResult { QSize area; int x = 0; int y = 0; + QRect giftArea; + float64 giftRotation = 0; bool waitingForNegativePattern = false; }; @@ -101,6 +117,9 @@ struct CachedBackground { QSize area; int x = 0; int y = 0; + QRect giftArea; + float64 giftRotation = 0.; + mutable std::unique_ptr gift; bool waitingForNegativePattern = false; }; @@ -200,7 +219,9 @@ private: BackgroundState _backgroundState; Animations::Simple _backgroundFade; CacheBackgroundRequest _backgroundCachingRequest; + CacheBackgroundRequest _nextCachingRequest; CacheBackgroundResult _backgroundNext; + int _backgroundVersion = 0; QSize _cacheBackgroundArea; crl::time _lastBackgroundAreaChangeTime = 0; std::optional _cacheBackgroundTimer; @@ -238,10 +259,15 @@ struct ChatBackgroundRects { const QImage &image); [[nodiscard]] QImage PrepareImageForTiled(const QImage &prepared); -[[nodiscard]] QImage ReadBackgroundImage( +struct BackgroundImageFields { + QImage image; + std::vector giftSymbols; +}; +[[nodiscard]] BackgroundImageFields ReadBackgroundImage( const QString &path, const QByteArray &content, - bool gzipSvg); + bool gzipSvg, + bool findGiftSymbols = false); [[nodiscard]] QImage GenerateBackgroundImage( QSize size, const std::vector &bg, @@ -260,5 +286,7 @@ struct ChatBackgroundRects { int rotation); [[nodiscard]] ChatThemeBackground PrepareBackgroundImage( const ChatThemeBackgroundData &data); +[[nodiscard]] QImage PrepareGiftSymbol( + const std::unique_ptr &emoji); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp index d7c7f08ab3..be0fa5601b 100644 --- a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp +++ b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp @@ -198,7 +198,7 @@ void ChooseSendAsBox( controller->clicked( ) | rpl::start_with_next([=](not_null peer) { - const auto weak = MakeWeak(box); + const auto weak = base::make_weak(box); if (done(peer) && weak) { box->closeBox(); } diff --git a/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp b/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp index 4314f48f20..ee929cfb2c 100644 --- a/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp +++ b/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp @@ -8,17 +8,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/choose_theme_controller.h" #include "boxes/background_box.h" +#include "boxes/transfer_gift_box.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" #include "ui/rp_widget.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_theme.h" +#include "ui/chat/message_bubble.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/shadow.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" -#include "ui/chat/chat_theme.h" -#include "ui/chat/message_bubble.h" #include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "window/themes/window_theme.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_session.h" #include "data/data_peer.h" #include "data/data_cloud_themes.h" @@ -38,7 +45,14 @@ namespace { const auto kDisableElement = [] { return u"disable"_q; }; -[[nodiscard]] QImage GeneratePreview(not_null theme) { +struct Preview { + QImage preview; + QRect userpic; +}; + +[[nodiscard]] Preview GeneratePreview( + not_null theme, + const std::shared_ptr &takenUserpic) { const auto &background = theme->background(); const auto &colors = background.colors; const auto size = st::chatThemePreviewSize; @@ -71,6 +85,7 @@ const auto kDisableElement = [] { return u"disable"_q; }; QRect(QPoint(), size * style::DevicePixelRatio()), small); }; + auto userpic = QRect(); const auto fullsize = size * style::DevicePixelRatio(); auto result = background.waitingForNegativePattern() ? QImage( @@ -118,8 +133,28 @@ const auto kDisableElement = [] { return u"disable"_q; }; } p.setBrush(theme->palette()->msgInBg()->c); p.drawRoundedRect(received, radius, radius); + + if (takenUserpic) { + const auto border = 2 * st::lineWidth; + const auto inner = received.marginsRemoved( + { border, border, border, border }); + userpic = inner; + userpic.setWidth(userpic.height()); + + st::chatThemeGiftTaken.paintInCenter( + p, + QRect( + inner.x() + inner.width() - inner.height() - border, + inner.y(), + inner.height(), + inner.height()), + theme->palette()->msgFileInBg()->c); + } } - return Images::Round(std::move(result), ImageRoundRadius::Large); + return { + .preview = Images::Round(std::move(result), ImageRoundRadius::Large), + .userpic = userpic, + }; } [[nodiscard]] QImage GenerateEmptyPreview() { @@ -146,11 +181,16 @@ const auto kDisableElement = [] { return u"disable"_q; }; } // namespace struct ChooseThemeController::Entry { + QString token; Ui::ChatThemeKey key; std::shared_ptr theme; std::shared_ptr media; - QImage preview; + std::shared_ptr gift; + std::shared_ptr takenUserpic; + std::unique_ptr custom; EmojiPtr emoji = nullptr; + QImage preview; + QRect userpic; QRect geometry; bool chosen = false; }; @@ -178,6 +218,10 @@ void ChooseThemeController::init(rpl::producer outer) { using namespace rpl::mappers; const auto themes = &_controller->session().data().cloudThemes(); + if (themes->myGiftThemesTokens().empty()) { + themes->myGiftThemesLoadMore(); + } + const auto &list = themes->chatThemes(); if (!list.empty()) { fill(list); @@ -269,7 +313,7 @@ void ChooseThemeController::initButtons() { int applyWidth, int chooseWidth, QString chosen) { - const auto was = _peer->themeEmoji(); + const auto was = _peer->themeToken(); const auto now = (chosen == kDisableElement()) ? QString() : chosen; const auto changed = (now != was); apply->setVisible(changed); @@ -281,40 +325,50 @@ void ChooseThemeController::initButtons() { shown->moveToLeft(left, margin.top()); }, controls->lifetime()); + const auto setTheme = crl::guard(apply, [=]( + const QString &token, + const std::shared_ptr &theme) { + SetPeerTheme(_controller, _peer, token, theme); + _controller->toggleChooseChatTheme(_peer); + }); + const auto confirmTakeGiftTheme = crl::guard(apply, [=]( + const QString &token, + const std::shared_ptr &theme, + not_null nowHasTheme) { + _controller->show(Box([=](not_null box) { + const auto confirmed = [=](Fn close) { + setTheme(token, theme); + close(); + }; + Ui::ConfirmBox(box, { + .text = tr::lng_chat_theme_gift_replace( + lt_name, + rpl::single(Ui::Text::Bold(nowHasTheme->shortName())), + Ui::Text::WithEntities), + .confirmed = confirmed, + .confirmText = tr::lng_box_yes(), + }); + })); + }); apply->setClickedCallback([=] { if (const auto chosen = findChosen()) { - const auto was = _peer->themeEmoji(); + const auto was = _peer->themeToken(); const auto now = chosen->key ? _chosen.current() : QString(); + const auto user = chosen->gift + ? chosen->gift->themeUser + : nullptr; if (was != now) { - _peer->setThemeEmoji(now); - const auto dropWallPaper = (_peer->wallPaper() != nullptr); - if (dropWallPaper) { - _peer->setWallPaper({}); + if (!user || user == _peer) { + setTheme(now, chosen->theme); + } else { + confirmTakeGiftTheme(now, chosen->theme, user); } - - if (chosen->theme) { - // Remember while changes propagate through event loop. - _controller->pushLastUsedChatTheme(chosen->theme); - } - const auto api = &_peer->session().api(); - api->request(MTPmessages_SetChatWallPaper( - MTP_flags(0), - _peer->input, - MTPInputWallPaper(), - MTPWallPaperSettings(), - MTPint() - )).afterDelay(10).done([=](const MTPUpdates &result) { - api->applyUpdates(result); - }).send(); - api->request(MTPmessages_SetChatTheme( - _peer->input, - MTP_string(now) - )).done([=](const MTPUpdates &result) { - api->applyUpdates(result); - }).send(); + } else { + _controller->toggleChooseChatTheme(_peer); } + } else { + _controller->toggleChooseChatTheme(_peer); } - _controller->toggleChooseChatTheme(_peer); }); choose->setClickedCallback([=] { _controller->show(Box(_controller, _peer)); @@ -324,16 +378,34 @@ void ChooseThemeController::initButtons() { void ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) { const auto geometry = entry.geometry; p.drawImage(geometry, entry.preview); + if (const auto userpic = entry.takenUserpic.get()) { + userpic->subscribeToUpdates([=] { + _inner->update(); + }); + p.drawImage( + entry.userpic.translated(geometry.topLeft()), + userpic->image(entry.userpic.height())); + } const auto size = Ui::Emoji::GetSizeLarge(); const auto factor = style::DevicePixelRatio(); - const auto emojiLeft = geometry.x() - + (geometry.width() - (size / factor)) / 2; + const auto esize = size / factor; + const auto emojiLeft = geometry.x() + (geometry.width() - esize) / 2; const auto emojiTop = geometry.y() + geometry.height() - - (size / factor) + - esize - st::chatThemeEmojiBottom; - Ui::Emoji::Draw(p, entry.emoji, size, emojiLeft, emojiTop); + const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); + const auto customSkip = (esize - customSize) / 2; + + if (const auto emoji = entry.emoji) { + Ui::Emoji::Draw(p, emoji, size, emojiLeft, emojiTop); + } else if (const auto custom = entry.custom.get()) { + custom->paint(p, { + .textColor = st::windowFg->c, + .position = { emojiLeft + customSkip, emojiTop + customSkip }, + }); + } if (entry.chosen) { auto hq = PainterHighQualityEnabler(p); @@ -379,7 +451,7 @@ void ChooseThemeController::initList() { if (!entry) { return QString(); } else if (entry->key) { - return entry->emoji->text(); + return entry->token; } else { return kDisableElement(); } @@ -429,7 +501,7 @@ void ChooseThemeController::initList() { _controller->overridePeerTheme( _peer, entry->theme, - entry->emoji); + entry->token); } _inner->update(); } @@ -488,11 +560,15 @@ void ChooseThemeController::updateInnerLeft(int now) { ? (skip / 2) : std::clamp(now, skip, 0); _inner->move(clamped, 0); + const auto visibleTill = -clamped + _content->width(); + if (_giftsFinishAt - visibleTill < _content->width()) { + _peer->owner().cloudThemes().myGiftThemesLoadMore(); + } } void ChooseThemeController::close() { if (const auto chosen = findChosen()) { - if (Ui::Emoji::Find(_peer->themeEmoji()) != chosen->emoji) { + if (_peer->themeToken() != chosen->token) { clearCurrentBackgroundState(); } } @@ -515,7 +591,7 @@ auto ChooseThemeController::findChosen() -> Entry* { for (auto &entry : _entries) { if (!entry.key && chosen == kDisableElement()) { return &entry; - } else if (chosen == entry.emoji->text()) { + } else if (chosen == entry.token) { return &entry; } } @@ -531,36 +607,35 @@ void ChooseThemeController::fill( if (themes.empty()) { return; } - const auto count = int(themes.size()) + 1; const auto single = st::chatThemePreviewSize; const auto skip = st::chatThemeEntrySkip; const auto &margin = st::chatThemeEntryMargin; - const auto full = margin.left() - + single.width() * count - + skip * (count - 1) - + margin.right(); - _inner->resize( - full, - margin.top() + single.height() + margin.bottom()); - - const auto initial = Ui::Emoji::Find(_peer->themeEmoji()); - if (!initial) { + const auto initial = _peer->themeToken(); + if (initial.isEmpty()) { _chosen = kDisableElement(); } - _dark.value( - ) | rpl::start_with_next([=](bool dark) { + const auto cloudThemes = &_controller->session().data().cloudThemes(); + rpl::combine( + _dark.value(), + rpl::single( + rpl::empty + ) | rpl::then(cloudThemes->myGiftThemesUpdated()) + ) | rpl::start_with_next([=](bool dark, auto) { + if (!cloudThemes->myGiftThemesReady()) { + return; + } clearCurrentBackgroundState(); - if (_chosen.current().isEmpty() && initial) { - _chosen = initial->text(); + if (_chosen.current().isEmpty() && !initial.isEmpty()) { + _chosen = initial; } _cachingLifetime.destroy(); const auto old = base::take(_entries); auto x = margin.left(); _entries.push_back({ - .preview = GenerateEmptyPreview(), .emoji = _disabledEmoji, + .preview = GenerateEmptyPreview(), .geometry = QRect(QPoint(x, margin.top()), single), .chosen = (_chosen.current() == kDisableElement()), }); @@ -575,16 +650,36 @@ void ChooseThemeController::fill( : Data::CloudThemeType::Light; x += single.width() + skip; - for (const auto &theme : themes) { - const auto emoji = Ui::Emoji::Find(theme.emoticon); - if (!emoji || !theme.settings.contains(type)) { - continue; + + const auto owner = &_controller->session().data(); + const auto manager = &owner->customEmojiManager(); + const auto push = [&]( + const Data::CloudTheme &theme, + const QString &token) { + if (token.isEmpty() || !theme.settings.contains(type)) { + return; } const auto key = ChatThemeKey{ theme.id, dark }; - const auto isChosen = (_chosen.current() == emoji->text()); + const auto isChosen = (_chosen.current() == token); + const auto themeUser = theme.unique + ? theme.unique->themeUser + : nullptr; _entries.push_back({ + .token = token, .key = key, - .emoji = emoji, + .gift = theme.unique, + .takenUserpic = (themeUser + ? Ui::MakeUserpicThumbnail(themeUser, true) + : nullptr), + .custom = (theme.unique + ? manager->create( + theme.unique->model.document, + [=] { _inner->update(); }, + Data::CustomEmojiSizeTag::Large) + : nullptr), + .emoji = (theme.emoticon.isEmpty() + ? nullptr + : Ui::Emoji::Find(theme.emoticon)), .geometry = QRect(QPoint(x, skip), single), .chosen = isChosen, }); @@ -603,13 +698,13 @@ void ChooseThemeController::fill( return; } const auto theme = data.get(); + const auto token = i->token; i->theme = std::move(data); - i->preview = GeneratePreview(theme); - if (_chosen.current() == i->emoji->text()) { - _controller->overridePeerTheme( - _peer, - i->theme, - i->emoji); + auto generated = GeneratePreview(theme, i->takenUserpic); + i->preview = std::move(generated.preview); + i->userpic = generated.userpic; + if (_chosen.current() == token) { + _controller->overridePeerTheme(_peer, i->theme, token); } _inner->update(); @@ -634,12 +729,40 @@ void ChooseThemeController::fill( if (i == end(_entries)) { return; } - i->preview = GeneratePreview(theme); + auto generated = GeneratePreview(theme, i->takenUserpic); + i->preview = std::move(generated.preview); + i->userpic = generated.userpic; _inner->update(); }, _cachingLifetime); }, _cachingLifetime); x += single.width() + skip; + }; + + _giftsFinishAt = 0; + if (const auto now = cloudThemes->themeForToken(initial)) { + push(*now, initial); } + for (const auto &token : cloudThemes->myGiftThemesTokens()) { + if (const auto found = cloudThemes->themeForToken(token)) { + if (token != initial) { + push(*found, token); + _giftsFinishAt = x; + } + } + } + for (const auto &theme : themes) { + if (const auto emoji = Ui::Emoji::Find(theme.emoticon)) { + const auto token = emoji->text(); + if (token != initial) { + push(theme, token); + } + } + } + + const auto full = x - skip + margin.right(); + _inner->resize( + full, + margin.top() + single.height() + margin.bottom()); if (!_initialInnerLeftApplied && _content->width() > 0) { applyInitialInnerLeft(); diff --git a/Telegram/SourceFiles/ui/chat/choose_theme_controller.h b/Telegram/SourceFiles/ui/chat/choose_theme_controller.h index 603c57839a..3a824ddcdf 100644 --- a/Telegram/SourceFiles/ui/chat/choose_theme_controller.h +++ b/Telegram/SourceFiles/ui/chat/choose_theme_controller.h @@ -73,6 +73,7 @@ private: std::optional _pressPosition; std::optional _dragStartPosition; int _dragStartInnerLeft = 0; + int _giftsFinishAt = 0; bool _initialInnerLeftApplied = false; rpl::variable _shouldBeShown = false; diff --git a/Telegram/SourceFiles/ui/controls/button_labels.cpp b/Telegram/SourceFiles/ui/controls/button_labels.cpp new file mode 100644 index 0000000000..8a3f9783af --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/button_labels.cpp @@ -0,0 +1,59 @@ +/* +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 "ui/controls/button_labels.h" + +#include "ui/widgets/labels.h" + +namespace Ui { + +void SetButtonTwoLabels( + not_null button, + rpl::producer title, + rpl::producer subtitle, + const style::FlatLabel &st, + const style::FlatLabel &subst, + const style::color *textFg) { + const auto buttonTitle = Ui::CreateChild( + button, + std::move(title), + st); + buttonTitle->show(); + const auto buttonSubtitle = Ui::CreateChild( + button, + std::move(subtitle), + subst); + buttonSubtitle->show(); + buttonSubtitle->setOpacity(0.6); + if (textFg) { + buttonTitle->setTextColorOverride((*textFg)->c); + buttonSubtitle->setTextColorOverride((*textFg)->c); + style::PaletteChanged() | rpl::start_with_next([=] { + buttonTitle->setTextColorOverride((*textFg)->c); + buttonSubtitle->setTextColorOverride((*textFg)->c); + }, buttonTitle->lifetime()); + } + rpl::combine( + button->sizeValue(), + buttonTitle->sizeValue(), + buttonSubtitle->sizeValue() + ) | rpl::start_with_next([=](QSize outer, QSize title, QSize subtitle) { + const auto two = title.height() + subtitle.height(); + const auto titleTop = (outer.height() - two) / 2; + const auto subtitleTop = titleTop + title.height(); + buttonTitle->moveToLeft( + (outer.width() - title.width()) / 2, + titleTop); + buttonSubtitle->moveToLeft( + (outer.width() - subtitle.width()) / 2, + subtitleTop); + }, buttonTitle->lifetime()); + buttonTitle->setAttribute(Qt::WA_TransparentForMouseEvents); + buttonSubtitle->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/button_labels.h b/Telegram/SourceFiles/ui/controls/button_labels.h new file mode 100644 index 0000000000..5de559a96c --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/button_labels.h @@ -0,0 +1,26 @@ +/* +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 style { +struct FlatLabel; +} // namespace style + +namespace Ui { + +class RpWidget; + +void SetButtonTwoLabels( + not_null button, + rpl::producer title, + rpl::producer subtitle, + const style::FlatLabel &st, + const style::FlatLabel &subst, + const style::color *textFg = nullptr); + +} // namespace Ui \ No newline at end of file diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp index 6d69efebd3..1ce682318e 100644 --- a/Telegram/SourceFiles/ui/controls/location_picker.cpp +++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp @@ -58,12 +58,6 @@ namespace { constexpr auto kResolveAddressDelay = 3 * crl::time(1000); constexpr auto kSearchDebounceDelay = crl::time(900); -#if defined Q_OS_MAC || defined Q_OS_LINUX -const auto kProtocolOverride = "mapboxapihelper"; -#else // Q_OS_MAC -const auto kProtocolOverride = ""; -#endif // Q_OS_MAC - Core::GeoLocation LastExactLocation; using VenueData = Data::InputVenue; @@ -923,7 +917,6 @@ void LocationPicker::setupWebview() { Webview::WindowConfig{ .opaqueBg = st::windowBg->c, .storageId = _webviewStorageId, - .dataProtocolOverride = kProtocolOverride, }); const auto raw = _webview.get(); @@ -1104,13 +1097,9 @@ void LocationPicker::mapReady() { const auto token = _config.mapsToken.toUtf8(); const auto center = DefaultCenter(_initialProvided); const auto bounds = DefaultBounds(); - const auto protocol = *kProtocolOverride - ? "'"_q + kProtocolOverride + "'" - : "null"; const auto params = "token: '" + token + "'" + ", center: " + center - + ", bounds: " + bounds - + ", protocol: " + protocol; + + ", bounds: " + bounds; _webview->eval("LocationPicker.init({ " + params + " });"); const auto handle = _window->window()->windowHandle(); diff --git a/Telegram/SourceFiles/ui/controls/stars_rating.cpp b/Telegram/SourceFiles/ui/controls/stars_rating.cpp new file mode 100644 index 0000000000..16a211a231 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/stars_rating.cpp @@ -0,0 +1,559 @@ +/* +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 "ui/controls/stars_rating.h" + +#include "base/unixtime.h" +#include "info/profile/info_profile_icon.h" +#include "lang/lang_keys.h" +#include "ui/effects/premium_bubble.h" +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/layers/show.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/custom_emoji_text_badge.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/ui_utility.h" +#include "styles/style_chat.h" // textMoreIconEmoji +#include "styles/style_info.h" +#include "styles/style_info_levels.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" +#include "styles/style_media_view.h" +#include "styles/style_menu_icons.h" + +namespace Ui { +namespace { + +constexpr auto kAutoCollapseTimeout = 4 * crl::time(1000); + +using Counters = Data::StarsRating; + +struct Feature { + const style::icon &icon; + QString title; + TextWithEntities about; +}; + +[[nodiscard]] object_ptr MakeFeature( + QWidget *parent, + Feature feature, + const Text::MarkedContext &context) { + auto result = object_ptr>( + parent, + object_ptr(parent), + st::infoStarsFeatureMargin); + const auto widget = result->entity(); + const auto icon = Ui::CreateChild( + widget, + feature.icon, + st::infoStarsFeatureIconPosition); + const auto title = Ui::CreateChild( + widget, + feature.title, + st::infoStarsFeatureTitle); + const auto about = Ui::CreateChild( + widget, + rpl::single(feature.about), + st::infoStarsFeatureAbout, + st::defaultPopupMenu, + context); + icon->show(); + title->show(); + about->show(); + widget->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto left = st::infoStarsFeatureLabelLeft; + const auto available = width - left; + title->resizeToWidth(available); + about->resizeToWidth(available); + auto top = 0; + title->move(left, top); + top += title->height() + st::infoStarsFeatureSkip; + about->move(left, top); + top += about->height(); + widget->resize(width, top); + }, widget->lifetime()); + return result; +} + +[[nodiscard]] Counters AdjustByReached(Counters data) { + if (data.stars < 0) { + return data; + } + const auto reached = !data.nextLevelStars; + if (reached) { + --data.level; + data.stars = data.nextLevelStars = std::max({ + data.stars, + data.thisLevelStars, + 1 + }); + data.thisLevelStars = 0; + } else { + data.stars = std::max(data.thisLevelStars, data.stars); + data.nextLevelStars = std::max( + data.nextLevelStars, + data.stars + 1); + } + return data; +} + +[[nodiscard]] Fn BubbleTextFactory( + int countForScale, + int nextLevelCounter) { + return [=](int count) { + const auto counter = [&](int count) { + return (countForScale < 10'000) + ? Lang::FormatCountDecimal(count) + : (countForScale < 10'000'000) + ? (Lang::FormatCountDecimal((count / 100) / 10.) + 'K') + : (Lang::FormatCountDecimal((count / 100'000) / 10.) + 'M'); + }; + return Ui::Premium::BubbleText{ + .counter = counter(count), + .additional = (nextLevelCounter + ? (u"/"_q + counter(nextLevelCounter)) + : QString()), + }; + }; +} + +void FillRatingLimit( + rpl::producer<> showFinished, + not_null container, + rpl::producer data, + Premium::BubbleType type, + style::margins limitLinePadding, + int starsForScale, + int nextLevelStars, + bool hideCount) { + const auto addSkip = [&](int skip) { + container->add(object_ptr(container, skip)); + }; + + const auto negative = (type == Premium::BubbleType::NegativeRating); + const auto ratio = [=](Counters rating) { + if (negative) { + return 0.5; + } + const auto min = rating.thisLevelStars; + const auto max = rating.nextLevelStars; + + Assert(rating.stars >= min && rating.stars <= max); + const auto count = (max - min); + const auto index = (rating.stars - min); + if (!index) { + return 0.; + } else if (index == count) { + return 1.; + } else if (count == 2) { + return 0.5; + } + const auto available = st::boxWideWidth + - st::boxPadding.left() + - st::boxPadding.right(); + const auto average = available / float64(count); + const auto levelWidth = [&](int add) { + return st::normalFont->width( + tr::lng_boost_level( + tr::now, + lt_count, + rating.level + add)); + }; + const auto paddings = 2 * st::premiumLineTextSkip; + const auto labelLeftWidth = paddings + levelWidth(0); + const auto labelRightWidth = paddings + levelWidth(1); + const auto first = std::max(average, labelLeftWidth * 1.); + const auto last = std::max(average, labelRightWidth * 1.); + const auto other = (available - first - last) / (count - 2); + return (first + (index - 1) * other) / available; + }; + + auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached); + + auto bubbleRowState = rpl::duplicate( + adjustedData + ) | rpl::combine_previous( + Counters() + ) | rpl::map([=](Counters previous, Counters counters) { + return Premium::BubbleRowState{ + .counter = counters.stars, + .ratio = ratio(counters), + .animateFromZero = (counters.level != previous.level), + .dynamic = true, + }; + }); + Premium::AddBubbleRow( + container, + (hideCount ? st::iconOnlyPremiumBubble : st::starRatingBubble), + std::move(showFinished), + rpl::duplicate(bubbleRowState), + type, + (hideCount + ? [](int) { return Ui::Premium::BubbleText(); } + : BubbleTextFactory(starsForScale, nextLevelStars)), + negative ? &st::levelNegativeBubble : &st::infoStarsCrown, + limitLinePadding); + addSkip(st::premiumLineTextSkip); + + const auto level = [](int level) { + return tr::lng_boost_level(tr::now, lt_count, level); + }; + auto limitState = std::move( + bubbleRowState + ) | rpl::map([negative](const Premium::BubbleRowState &state) { + return Premium::LimitRowState{ + .ratio = negative ? 0.5 : state.ratio, + .animateFromZero = !negative && state.animateFromZero, + .dynamic = state.dynamic + }; + }); + auto left = rpl::duplicate( + adjustedData + ) | rpl::map([=](Counters counters) { + return (counters.level < 0) ? QString() : level(counters.level); + }); + auto right = rpl::duplicate( + adjustedData + ) | rpl::map([=](Counters counters) { + return (counters.level < 0) + ? tr::lng_stars_rating_negative_label(tr::now) + : level(counters.level + 1); + }); + Premium::AddLimitRow( + container, + (negative ? st::negativeStarsLimits : st::boostLimits), + Premium::LimitRowLabels{ + .leftLabel = std::move(left), + .rightLabel = std::move(right), + .activeLineBg = [=] { return negative + ? st::attentionButtonFg->b + : st::windowBgActive->b; + }, + }, + std::move(limitState), + limitLinePadding); +} + +void AboutRatingBox( + not_null box, + const QString &name, + Counters data, + Data::StarsRatingPending pending) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::boostBox); + + struct State { + rpl::variable data; + rpl::variable pending; + rpl::variable full; + }; + const auto state = box->lifetime().make_state(); + state->data = data; + + FillRatingLimit( + BoxShowFinishes(box), + box->verticalLayout(), + state->data.value(), + (data.level < 0 + ? Premium::BubbleType::NegativeRating + : Premium::BubbleType::StarRating), + st::boxRowPadding, + data.stars, + data.nextLevelStars, + (data.level < 0 && !data.stars)); + + box->setMaxHeight(st::boostBoxMaxHeight); + + auto title = rpl::conditional( + state->pending.value(), + tr::lng_stars_rating_future(), + tr::lng_stars_rating_title()); + + auto text = !name.isEmpty() + ? tr::lng_stars_rating_about( + lt_name, + rpl::single(TextWithEntities{ name }), + Ui::Text::RichLangValue) | rpl::type_erased() + : tr::lng_stars_rating_about_your( + Ui::Text::RichLangValue) | rpl::type_erased(); + + if (data.level < 0) { + auto text = (data.stars < 0) + ? tr::lng_stars_rating_negative_your( + lt_count_decimal, + rpl::single(-data.stars * 1.), + Ui::Text::RichLangValue) + : tr::lng_stars_rating_negative( + lt_name, + rpl::single(TextWithEntities{ name }), + Ui::Text::RichLangValue); + box->addRow( + object_ptr( + box, + std::move(text), + st::boostTextNegative), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), + style::al_top + )->setTryMakeSimilarLines(true); + } + + box->addRow( + object_ptr(box, std::move(title), st::infoStarsTitle), + st::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0), + style::al_top); + + if (pending) { + const auto now = base::unixtime::now(); + const auto days = std::max((pending.date - now + 43200) / 86400, 1); + auto text = state->pending.value( + ) | rpl::map([=](bool value) { + return tr::lng_stars_rating_pending( + tr::now, + lt_count_decimal, + pending.value.stars - data.stars, + lt_when, + TextWithEntities{ + tr::lng_stars_rating_updates(tr::now, lt_count, days), + }, + lt_link, + Ui::Text::Link((value + ? tr::lng_stars_rating_pending_back + : tr::lng_stars_rating_pending_preview)( + tr::now, + lt_arrow, + Ui::Text::IconEmoji(&st::textMoreIconEmoji), + Ui::Text::WithEntities)), + Ui::Text::RichLangValue); + }); + const auto aboutPending = box->addRow( + object_ptr( + box, + std::move(text), + st::boostTextPending), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), + style::al_top); + aboutPending->setTryMakeSimilarLines(true); + aboutPending->setClickHandlerFilter([=](const auto &...) { + state->pending = !state->pending.current(); + state->data = state->pending.current() + ? pending.value + : data; + box->verticalLayout()->resizeToWidth(box->width()); + return false; + }); + } + + box->addRow( + object_ptr( + box, + std::move(text), + st::boostText), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)), + style::al_top + )->setTryMakeSimilarLines(true); + + auto helper = Ui::Text::CustomEmojiHelper(); + const auto makeBadge = [&]( + const QString &text, + const style::RoundButton &st) { + return helper.paletteDependent( + Ui::Text::CustomEmojiTextBadge(text, st)); + }; + const auto makeActive = [&](const QString &text) { + return makeBadge(text, st::customEmojiTextBadge); + }; + const auto makeInactive = [&](const QString &text) { + return makeBadge(text, st::infoRatingDeductedBadge); + }; + const auto features = std::vector{ + { + st::menuIconRatingGifts, + tr::lng_stars_title_gifts_telegram(tr::now), + tr::lng_stars_about_gifts_telegram( + tr::now, + lt_emoji, + makeActive(tr::lng_stars_rating_added(tr::now)), + Ui::Text::RichLangValue), + }, + { + st::menuIconRatingUsers, + tr::lng_stars_title_gifts_users(tr::now), + tr::lng_stars_about_gifts_users( + tr::now, + lt_emoji, + makeActive(tr::lng_stars_rating_added(tr::now)), + Ui::Text::RichLangValue), + }, + { + st::menuIconRatingRefund, + tr::lng_stars_title_refunds(tr::now), + tr::lng_stars_about_refunds( + tr::now, + lt_emoji, + makeInactive(tr::lng_stars_rating_deducted(tr::now)), + Ui::Text::RichLangValue), + }, + }; + const auto context = helper.context(); + for (const auto &feature : features) { + box->addRow(MakeFeature(box, feature, context)); + } + box->addButton(rpl::single(QString()), [=] { + box->closeBox(); + })->setText(rpl::single(Ui::Text::IconEmoji( + &st::infoStarsUnderstood + ).append(' ').append(tr::lng_stars_rating_understood(tr::now)))); +} + +[[nodiscard]] not_null SelectShape(int level) { + if (level < 0) { + return &st::levelNegative; + } + struct Shape { + int level = 0; + not_null shape; + }; + const auto list = std::vector{ + { 1, &st::level1 }, + { 2, &st::level2 }, + { 3, &st::level3 }, + { 4, &st::level4 }, + { 5, &st::level5 }, + { 6, &st::level6 }, + { 7, &st::level7 }, + { 8, &st::level8 }, + { 9, &st::level9 }, + { 10, &st::level10 }, + { 20, &st::level20 }, + { 30, &st::level30 }, + { 40, &st::level40 }, + { 50, &st::level50 }, + { 60, &st::level60 }, + { 70, &st::level70 }, + { 80, &st::level80 }, + { 90, &st::level90 }, + }; + const auto i = ranges::lower_bound( + list, + level + 1, + ranges::less(), + &Shape::level); + return (i != begin(list)) ? (i - 1)->shape : list.front().shape; +} + +} // namespace + +StarsRating::StarsRating( + QWidget *parent, + std::shared_ptr show, + const QString &name, + rpl::producer value, + Fn pending) +: _widget(std::make_unique(parent)) +, _show(std::move(show)) +, _name(name) +, _value(std::move(value)) +, _pending(std::move(pending)) { + init(); +} + +StarsRating::~StarsRating() = default; + +void StarsRating::init() { + _widget->setPointerCursor(true); + + _widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_widget.get()); + paint(p); + }, lifetime()); + + _widget->setClickedCallback([=] { + if (!_value.current()) { + return; + } + _show->show(Box(AboutRatingBox, _name, _value.current(), _pending + ? _pending() + : Data::StarsRatingPending())); + }); + + _widget->resize(_widget->width(), st::level1.icon.height()); + + _value.value() | rpl::start_with_next([=](Counters rating) { + updateData(rating); + }, lifetime()); +} + +void StarsRating::updateData(Data::StarsRating rating) { + if (!rating) { + _shape = nullptr; + _widthValue = 0; + } else { + _shape = SelectShape(rating.level); + _collapsedText.setText( + st::levelStyle, + (rating.level < 0 + ? QString() + : Lang::FormatCountDecimal(rating.level))); + const auto &margin = st::levelMargin; + _widthValue = _shape->icon.width() + margin.right() - margin.left(); + } + updateWidth(); +} + +void StarsRating::updateWidth() { + if (const auto widthToRight = _widthValue.current()) { + const auto &margin = st::levelMargin; + _widget->resize(margin.left() + widthToRight, _widget->height()); + _widget->update(); + } else { + _widget->resize(0, _widget->height()); + } +} + +void StarsRating::raise() { + _widget->raise(); +} + +void StarsRating::moveTo(int x, int y) { + _widget->move(x - st::levelMargin.left(), y - st::levelMargin.top()); +} + +void StarsRating::paint(QPainter &p) { + if (!_shape) { + return; + } + _shape->icon.paint(p, 0, 0, _widget->width()); + + const auto x = (_widget->width() - _collapsedText.maxWidth()) / 2; + p.setPen(st::levelTextFg); + _collapsedText.draw(p, { + .position = QPoint(x, 0) + _shape->position, + .availableWidth = _collapsedText.maxWidth(), + }); +} + +rpl::producer StarsRating::widthValue() const { + return _widthValue.value(); +} + +rpl::lifetime &StarsRating::lifetime() { + return _widget->lifetime(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/stars_rating.h b/Telegram/SourceFiles/ui/controls/stars_rating.h new file mode 100644 index 0000000000..a803810c47 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/stars_rating.h @@ -0,0 +1,65 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/weak_ptr.h" +#include "data/data_peer_common.h" + +namespace style { +struct Toast; +struct LevelShape; +} // namespace style + +namespace Ui::Toast { +class Instance; +} // namespace Ui::Toast + +namespace Ui { + +class ImportantTooltip; +class AbstractButton; +class FlatLabel; +class Show; + +class StarsRating final { +public: + StarsRating( + QWidget *parent, + std::shared_ptr show, + const QString &name, + rpl::producer value, + Fn pending); + ~StarsRating(); + + void raise(); + void moveTo(int x, int y); + + [[nodiscard]] rpl::producer widthValue() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void init(); + void paint(QPainter &p); + void updateData(Data::StarsRating rating); + void updateWidth(); + + const std::unique_ptr _widget; + const std::shared_ptr _show; + const QString _name; + + Ui::Text::String _collapsedText; + + rpl::variable _value; + Fn _pending; + rpl::variable _widthValue; + const style::LevelShape *_shape = nullptr; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/sub_tabs.cpp b/Telegram/SourceFiles/ui/controls/sub_tabs.cpp new file mode 100644 index 0000000000..e5c6e616e9 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/sub_tabs.cpp @@ -0,0 +1,297 @@ +/* +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 "ui/controls/sub_tabs.h" + +#include "ui/painter.h" +#include "ui/ui_utility.h" +#include "styles/style_basic.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" + +#include + +namespace Ui { + +SubTabs::SubTabs( + QWidget *parent, + Options options, + std::vector tabs, + Text::MarkedContext context) +: RpWidget(parent) +, _centered(options.centered) { + setMouseTracking(true); + setTabs(std::move(tabs), context); + if (!options.selected.isEmpty()) { + setActiveTab(options.selected); + } +} + +void SubTabs::setTabs( + std::vector tabs, + Text::MarkedContext context) { + auto x = st::giftBoxTabsMargin.left(); + auto y = st::giftBoxTabsMargin.top(); + + setSelected(-1); + _buttons.resize(tabs.size()); + const auto padding = st::giftBoxTabPadding; + auto activeId = (_active >= 0 + && ranges::contains(tabs, _buttons[_active].tab.id, &Tab::id)) + ? _buttons[_active].tab.id + : QString(); + _active = -1; + context.repaint = [=] { update(); }; + for (auto i = 0, count = int(tabs.size()); i != count; ++i) { + auto &tab = tabs[i]; + Assert(!tab.id.isEmpty()); + + auto &button = _buttons[i]; + button.active = (tab.id == activeId); + if (button.tab != tab) { + button.text = Text::String(); + button.text.setMarkedText( + st::semiboldTextStyle, + tab.text, + kMarkupTextOptions, + context); + button.tab = std::move(tab); + } + if (button.active) { + _active = i; + } + const auto width = button.text.maxWidth(); + const auto height = st::giftBoxTabStyle.font->height; + const auto r = QRect(0, 0, width, height).marginsAdded(padding); + button.geometry = QRect(QPoint(x, y), r.size()); + x += r.width() + st::giftBoxTabSkip; + } + const auto width = x + - st::giftBoxTabSkip + + st::giftBoxTabsMargin.right(); + _fullWidth = width; + resizeToWidth(this->width()); + update(); +} + +void SubTabs::setActiveTab(const QString &id) { + if (id.isEmpty()) { + setActive(-1); + return; + } + const auto i = ranges::find( + _buttons, + id, + [](const Button &button) { return button.tab.id; }); + Assert(i != end(_buttons)); + setActive(i - begin(_buttons)); +} + +rpl::producer SubTabs::activated() const { + return _activated.events(); +} + +rpl::producer SubTabs::contextMenuRequests() const { + return _contextMenuRequests.events(); +} + +void SubTabs::setSelected(int index) { + const auto was = (_selected >= 0); + const auto now = (index >= 0); + _selected = index; + if (was != now) { + setCursor(now ? style::cur_pointer : style::cur_default); + } +} + +void SubTabs::setActive(int index) { + const auto was = _active; + if (was == index) { + return; + } + if (was >= 0 && was < _buttons.size()) { + _buttons[was].active = false; + } + _active = index; + _buttons[index].active = true; + const auto geometry = _buttons[index].geometry; + if (width() > 0 + && _fullWidth > width() + && _scrollMax > 0 + && !geometry.isEmpty()) { + const auto added = std::max( + std::min(width() / 8, (width() - geometry.width()) / 2), + 0); + const auto visibleFrom = int(base::SafeRound(_scroll)); + const auto visibleTill = visibleFrom + width(); + if ((visibleTill < geometry.x() + geometry.width() + added) + || (visibleFrom + added > geometry.x())) { + _scrollTo = std::clamp( + geometry.x() + (geometry.width() / 2) - (width() / 2), + 0, + _scrollMax); + _scrollAnimation.start([=] { + _scroll = _scrollAnimation.value(_scrollTo); + update(); + }, _scroll, _scrollTo, crl::time(150), anim::easeOutCirc); + } + } + update(); +} + +int SubTabs::resizeGetHeight(int newWidth) { + if (_centered) { + update(); + const auto fullWidth = _fullWidth; + _fullShift = (fullWidth < newWidth) ? (newWidth - fullWidth) / 2 : 0; + } + _scrollMax = (_fullWidth > newWidth) ? (_fullWidth - newWidth) : 0; + return _buttons.empty() + ? 0 + : (st::giftBoxTabsMargin.top() + + _buttons.back().geometry.height() + + st::giftBoxTabsMargin.bottom()); +} + +bool SubTabs::eventHook(QEvent *e) { + if (e->type() == QEvent::Leave) { + setSelected(-1); + } + return RpWidget::eventHook(e); +} + +void SubTabs::mouseMoveEvent(QMouseEvent *e) { + const auto mousex = e->pos().x(); + const auto drag = QApplication::startDragDistance(); + if (_dragx > 0) { + _scrollAnimation.stop(); + _scroll = std::clamp( + _dragscroll + _dragx - mousex, + 0., + _scrollMax * 1.); + update(); + return; + } else if (_pressx > 0 && std::abs(_pressx - mousex) > drag) { + _dragx = _pressx; + _dragscroll = _scroll; + } + auto selected = -1; + const auto position = e->pos() + scroll(); + for (auto i = 0, c = int(_buttons.size()); i != c; ++i) { + if (_buttons[i].geometry.contains(position)) { + selected = i; + break; + } + } + setSelected(selected); +} + +void SubTabs::wheelEvent(QWheelEvent *e) { + const auto delta = ScrollDeltaF(e); + + const auto phase = e->phase(); + const auto horizontal = (std::abs(delta.x()) > std::abs(delta.y())); + if (phase == Qt::NoScrollPhase) { + _locked = std::nullopt; + } else if (phase == Qt::ScrollBegin) { + _locked = std::nullopt; + } else if (!_locked) { + _locked = horizontal ? Qt::Horizontal : Qt::Vertical; + } + if (horizontal) { + if (_locked == Qt::Vertical) { + return; + } + e->accept(); + } else { + if (_locked == Qt::Horizontal) { + e->accept(); + } else { + e->ignore(); + } + return; + } + _scrollAnimation.stop(); + _scroll = std::clamp(_scroll - delta.x(), 0., _scrollMax * 1.); + update(); +} + +void SubTabs::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + _pressed = _selected; + _pressx = e->pos().x(); +} + +void SubTabs::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + const auto dragx = std::exchange(_dragx, 0); + const auto pressed = std::exchange(_pressed, -1); + _pressx = 0; + if (!dragx + && pressed >= 0 + && _selected == pressed + && pressed < _buttons.size()) { + _activated.fire_copy(_buttons[pressed].tab.id); + } +} + +void SubTabs::contextMenuEvent(QContextMenuEvent *e) { + if (_selected >= 0 && _selected < _buttons.size()) { + _contextMenuRequests.fire_copy(_buttons[_selected].tab.id); + } +} + +void SubTabs::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + const auto padding = st::giftBoxTabPadding; + const auto shift = -scroll(); + for (const auto &button : _buttons) { + const auto geometry = button.geometry.translated(shift); + if (button.active) { + p.setBrush(st::giftBoxTabBgActive); + p.setPen(Qt::NoPen); + const auto radius = geometry.height() / 2.; + p.drawRoundedRect(geometry, radius, radius); + p.setPen(st::giftBoxTabFgActive); + } else { + p.setPen(st::giftBoxTabFg); + } + button.text.draw(p, { + .position = geometry.marginsRemoved(padding).topLeft(), + .availableWidth = button.text.maxWidth(), + }); + } + if (_fullWidth > width()) { + const auto &icon = st::defaultEmojiSuggestions; + const auto w = icon.fadeRight.width(); + const auto &c = st::boxDividerBg->c; + const auto r = QRect(0, 0, w, height()); + const auto s = std::abs(float64(shift.x())); + constexpr auto kF = 0.5; + const auto opacityRight = (_scrollMax - s) + / (icon.fadeRight.width() * kF); + p.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.)); + icon.fadeRight.fill(p, r.translated(width() - w, 0), c); + + const auto opacityLeft = s / (icon.fadeLeft.width() * kF); + p.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.)); + icon.fadeLeft.fill(p, r, c); + } + +} + +QPoint SubTabs::scroll() const { + return QPoint(int(base::SafeRound(_scroll)) - _fullShift, 0); +} + +} // namespace Ui + diff --git a/Telegram/SourceFiles/ui/controls/sub_tabs.h b/Telegram/SourceFiles/ui/controls/sub_tabs.h new file mode 100644 index 0000000000..63bf2c27c2 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/sub_tabs.h @@ -0,0 +1,89 @@ +/* +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/rp_widget.h" + +namespace Ui { + +struct SubTabsOptions { + QString selected; + bool centered = false; +}; + +struct SubTabsTab { + QString id; + TextWithEntities text; + + friend inline bool operator==( + const SubTabsTab &, + const SubTabsTab &) = default; +}; + +class SubTabs : public RpWidget { +public: + using Options = SubTabsOptions; + using Tab = SubTabsTab; + + explicit SubTabs( + QWidget *parent, + Options options = {}, + std::vector tabs = {}, + Text::MarkedContext context = {}); + + void setTabs( + std::vector tabs, + Text::MarkedContext context = {}); + + void setActiveTab(const QString &id); + + [[nodiscard]] rpl::producer activated() const; + [[nodiscard]] rpl::producer contextMenuRequests() const; + +private: + struct Button { + Tab tab; + QRect geometry; + Text::String text; + bool active = false; + }; + + int resizeGetHeight(int newWidth) override; + void wheelEvent(QWheelEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + void paintEvent(QPaintEvent *e) override; + bool eventHook(QEvent *e) override; + + void setSelected(int index); + void setActive(int index); + [[nodiscard]] QPoint scroll() const; + + std::vector