diff --git a/LEGAL b/LEGAL index 5d0f5e8a7..09f48ae72 100644 --- a/LEGAL +++ b/LEGAL @@ -1,7 +1,7 @@ This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. -Copyright (c) 2014-2024 The Telegram Desktop Authors. +Copyright (c) 2014-2025 The Telegram Desktop Authors. Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1697e3f86..d0b1f59d9 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -326,6 +326,8 @@ PRIVATE boxes/peers/prepare_short_info_box.h boxes/peers/replace_boost_box.cpp boxes/peers/replace_boost_box.h + boxes/peers/verify_peers_box.cpp + boxes/peers/verify_peers_box.h boxes/about_box.cpp boxes/about_box.h boxes/about_sponsored_box.cpp @@ -402,8 +404,6 @@ PRIVATE boxes/send_gif_with_caption_box.h boxes/send_files_box.cpp boxes/send_files_box.h - boxes/sessions_box.cpp - boxes/sessions_box.h boxes/share_box.cpp boxes/share_box.h boxes/star_gift_box.cpp @@ -412,6 +412,8 @@ PRIVATE boxes/sticker_set_box.h boxes/stickers_box.cpp boxes/stickers_box.h + boxes/transfer_gift_box.cpp + boxes/transfer_gift_box.h boxes/translate_box.cpp boxes/translate_box.h boxes/url_auth_box.cpp @@ -550,6 +552,7 @@ PRIVATE core/sandbox.h core/shortcuts.cpp core/shortcuts.h + core/stars_amount.h core/ui_integration.cpp core/ui_integration.h core/update_checker.cpp @@ -707,6 +710,7 @@ PRIVATE data/data_shared_media.h data/data_sparse_ids.cpp data/data_sparse_ids.h + data/data_star_gift.h data/data_statistics.h data/data_stories.cpp data/data_stories.h @@ -877,6 +881,8 @@ PRIVATE history/view/media/history_view_story_mention.h history/view/media/history_view_theme_document.cpp history/view/media/history_view_theme_document.h + history/view/media/history_view_unique_gift.cpp + history/view/media/history_view_unique_gift.h history/view/media/history_view_userpic_suggestion.cpp history/view/media/history_view_userpic_suggestion.h history/view/media/history_view_web_page.cpp @@ -999,6 +1005,12 @@ PRIVATE info/bot/earn/info_bot_earn_list.h info/bot/earn/info_bot_earn_widget.cpp info/bot/earn/info_bot_earn_widget.h + info/bot/starref/info_bot_starref_common.cpp + info/bot/starref/info_bot_starref_common.h + info/bot/starref/info_bot_starref_join_widget.cpp + info/bot/starref/info_bot_starref_join_widget.h + info/bot/starref/info_bot_starref_setup_widget.cpp + info/bot/starref/info_bot_starref_setup_widget.h info/channel_statistics/boosts/create_giveaway_box.cpp info/channel_statistics/boosts/create_giveaway_box.h info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp @@ -1021,6 +1033,12 @@ PRIVATE info/downloads/info_downloads_provider.h info/downloads/info_downloads_widget.cpp info/downloads/info_downloads_widget.h + info/global_media/info_global_media_widget.cpp + info/global_media/info_global_media_widget.h + info/global_media/info_global_media_inner_widget.cpp + info/global_media/info_global_media_inner_widget.h + info/global_media/info_global_media_provider.cpp + info/global_media/info_global_media_provider.h info/media/info_media_buttons.h info/media/info_media_common.cpp info/media/info_media_common.h @@ -1471,8 +1489,6 @@ PRIVATE settings/business/settings_recipients_helper.h settings/business/settings_working_hours.cpp settings/business/settings_working_hours.h - settings/cloud_password/settings_cloud_password_common.cpp - settings/cloud_password/settings_cloud_password_common.h settings/cloud_password/settings_cloud_password_email.cpp settings/cloud_password/settings_cloud_password_email.h settings/cloud_password/settings_cloud_password_email_confirm.cpp @@ -1485,6 +1501,10 @@ PRIVATE settings/cloud_password/settings_cloud_password_manage.h settings/cloud_password/settings_cloud_password_start.cpp settings/cloud_password/settings_cloud_password_start.h + settings/cloud_password/settings_cloud_password_step.cpp + settings/cloud_password/settings_cloud_password_step.h + settings/settings_active_sessions.cpp + settings/settings_active_sessions.h settings/settings_advanced.cpp settings/settings_advanced.h settings/settings_blocked_peers.cpp @@ -1646,8 +1666,6 @@ PRIVATE ui/item_text_options.cpp ui/item_text_options.h ui/resize_area.h - ui/search_field_controller.cpp - ui/search_field_controller.h ui/unread_badge.cpp ui/unread_badge.h window/main_window.cpp diff --git a/Telegram/Resources/animations/starref_link.tgs b/Telegram/Resources/animations/starref_link.tgs new file mode 100644 index 000000000..ea6300452 Binary files /dev/null and b/Telegram/Resources/animations/starref_link.tgs differ diff --git a/Telegram/Resources/art/affiliate_logo.png b/Telegram/Resources/art/affiliate_logo.png new file mode 100644 index 000000000..d5f9b5087 Binary files /dev/null and b/Telegram/Resources/art/affiliate_logo.png differ diff --git a/Telegram/Resources/art/verified_bg.webp b/Telegram/Resources/art/verified_bg.webp new file mode 100644 index 000000000..f08059f45 Binary files /dev/null and b/Telegram/Resources/art/verified_bg.webp differ diff --git a/Telegram/Resources/art/verified_fg.webp b/Telegram/Resources/art/verified_fg.webp new file mode 100644 index 000000000..353512349 Binary files /dev/null and b/Telegram/Resources/art/verified_fg.webp differ diff --git a/Telegram/Resources/icons/menu/affiliate_simple.png b/Telegram/Resources/icons/menu/affiliate_simple.png new file mode 100644 index 000000000..c09ce88fc Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_simple.png differ diff --git a/Telegram/Resources/icons/menu/affiliate_simple@2x.png b/Telegram/Resources/icons/menu/affiliate_simple@2x.png new file mode 100644 index 000000000..3e41ea427 Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_simple@2x.png differ diff --git a/Telegram/Resources/icons/menu/affiliate_simple@3x.png b/Telegram/Resources/icons/menu/affiliate_simple@3x.png new file mode 100644 index 000000000..d075a10bd Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_simple@3x.png differ diff --git a/Telegram/Resources/icons/menu/affiliate_transparent.png b/Telegram/Resources/icons/menu/affiliate_transparent.png new file mode 100644 index 000000000..abcd00046 Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_transparent.png differ diff --git a/Telegram/Resources/icons/menu/affiliate_transparent@2x.png b/Telegram/Resources/icons/menu/affiliate_transparent@2x.png new file mode 100644 index 000000000..ffbc05cba Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_transparent@2x.png differ diff --git a/Telegram/Resources/icons/menu/affiliate_transparent@3x.png b/Telegram/Resources/icons/menu/affiliate_transparent@3x.png new file mode 100644 index 000000000..54185c44c Binary files /dev/null and b/Telegram/Resources/icons/menu/affiliate_transparent@3x.png differ diff --git a/Telegram/Resources/icons/menu/bot.png b/Telegram/Resources/icons/menu/bot.png new file mode 100644 index 000000000..5a890cf8b Binary files /dev/null and b/Telegram/Resources/icons/menu/bot.png differ diff --git a/Telegram/Resources/icons/menu/bot@2x.png b/Telegram/Resources/icons/menu/bot@2x.png new file mode 100644 index 000000000..aaa8dc9af Binary files /dev/null and b/Telegram/Resources/icons/menu/bot@2x.png differ diff --git a/Telegram/Resources/icons/menu/bot@3x.png b/Telegram/Resources/icons/menu/bot@3x.png new file mode 100644 index 000000000..59a23832b Binary files /dev/null and b/Telegram/Resources/icons/menu/bot@3x.png differ diff --git a/Telegram/Resources/icons/menu/bot_add.png b/Telegram/Resources/icons/menu/bot_add.png new file mode 100644 index 000000000..aadbec32d Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_add.png differ diff --git a/Telegram/Resources/icons/menu/bot_add@2x.png b/Telegram/Resources/icons/menu/bot_add@2x.png new file mode 100644 index 000000000..0eea54a25 Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_add@2x.png differ diff --git a/Telegram/Resources/icons/menu/bot_add@3x.png b/Telegram/Resources/icons/menu/bot_add@3x.png new file mode 100644 index 000000000..e89bb32eb Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_add@3x.png differ diff --git a/Telegram/Resources/icons/menu/caption_hide.png b/Telegram/Resources/icons/menu/caption_hide.png new file mode 100644 index 000000000..0320b2e2e Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_hide.png differ diff --git a/Telegram/Resources/icons/menu/caption_hide@2x.png b/Telegram/Resources/icons/menu/caption_hide@2x.png new file mode 100644 index 000000000..6b9169008 Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_hide@2x.png differ diff --git a/Telegram/Resources/icons/menu/caption_hide@3x.png b/Telegram/Resources/icons/menu/caption_hide@3x.png new file mode 100644 index 000000000..75d4841d1 Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_hide@3x.png differ diff --git a/Telegram/Resources/icons/menu/caption_show.png b/Telegram/Resources/icons/menu/caption_show.png new file mode 100644 index 000000000..9c7b05c63 Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_show.png differ diff --git a/Telegram/Resources/icons/menu/caption_show@2x.png b/Telegram/Resources/icons/menu/caption_show@2x.png new file mode 100644 index 000000000..962eeaee1 Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_show@2x.png differ diff --git a/Telegram/Resources/icons/menu/caption_show@3x.png b/Telegram/Resources/icons/menu/caption_show@3x.png new file mode 100644 index 000000000..4dd44a609 Binary files /dev/null and b/Telegram/Resources/icons/menu/caption_show@3x.png differ diff --git a/Telegram/Resources/icons/menu/forwarded_status.png b/Telegram/Resources/icons/menu/forwarded_status.png new file mode 100644 index 000000000..66d08a835 Binary files /dev/null and b/Telegram/Resources/icons/menu/forwarded_status.png differ diff --git a/Telegram/Resources/icons/menu/forwarded_status@2x.png b/Telegram/Resources/icons/menu/forwarded_status@2x.png new file mode 100644 index 000000000..8892f4ce3 Binary files /dev/null and b/Telegram/Resources/icons/menu/forwarded_status@2x.png differ diff --git a/Telegram/Resources/icons/menu/forwarded_status@3x.png b/Telegram/Resources/icons/menu/forwarded_status@3x.png new file mode 100644 index 000000000..e2940b163 Binary files /dev/null and b/Telegram/Resources/icons/menu/forwarded_status@3x.png differ diff --git a/Telegram/Resources/icons/menu/name_hide.png b/Telegram/Resources/icons/menu/name_hide.png new file mode 100644 index 000000000..0d6bb287a Binary files /dev/null and b/Telegram/Resources/icons/menu/name_hide.png differ diff --git a/Telegram/Resources/icons/menu/name_hide@2x.png b/Telegram/Resources/icons/menu/name_hide@2x.png new file mode 100644 index 000000000..8903e549d Binary files /dev/null and b/Telegram/Resources/icons/menu/name_hide@2x.png differ diff --git a/Telegram/Resources/icons/menu/name_hide@3x.png b/Telegram/Resources/icons/menu/name_hide@3x.png new file mode 100644 index 000000000..88b94bc5f Binary files /dev/null and b/Telegram/Resources/icons/menu/name_hide@3x.png differ diff --git a/Telegram/Resources/icons/menu/name_show.png b/Telegram/Resources/icons/menu/name_show.png new file mode 100644 index 000000000..93cad5d87 Binary files /dev/null and b/Telegram/Resources/icons/menu/name_show.png differ diff --git a/Telegram/Resources/icons/menu/name_show@2x.png b/Telegram/Resources/icons/menu/name_show@2x.png new file mode 100644 index 000000000..486d0a24d Binary files /dev/null and b/Telegram/Resources/icons/menu/name_show@2x.png differ diff --git a/Telegram/Resources/icons/menu/name_show@3x.png b/Telegram/Resources/icons/menu/name_show@3x.png new file mode 100644 index 000000000..20418ed26 Binary files /dev/null and b/Telegram/Resources/icons/menu/name_show@3x.png differ diff --git a/Telegram/Resources/icons/menu/stars_share.png b/Telegram/Resources/icons/menu/stars_share.png new file mode 100644 index 000000000..30871b111 Binary files /dev/null and b/Telegram/Resources/icons/menu/stars_share.png differ diff --git a/Telegram/Resources/icons/menu/stars_share@2x.png b/Telegram/Resources/icons/menu/stars_share@2x.png new file mode 100644 index 000000000..e431b7ce4 Binary files /dev/null and b/Telegram/Resources/icons/menu/stars_share@2x.png differ diff --git a/Telegram/Resources/icons/menu/stars_share@3x.png b/Telegram/Resources/icons/menu/stars_share@3x.png new file mode 100644 index 000000000..2e7d14973 Binary files /dev/null and b/Telegram/Resources/icons/menu/stars_share@3x.png differ diff --git a/Telegram/Resources/icons/menu/tradable.png b/Telegram/Resources/icons/menu/tradable.png new file mode 100644 index 000000000..588ad29b9 Binary files /dev/null and b/Telegram/Resources/icons/menu/tradable.png differ diff --git a/Telegram/Resources/icons/menu/tradable@2x.png b/Telegram/Resources/icons/menu/tradable@2x.png new file mode 100644 index 000000000..096f5cf7d Binary files /dev/null and b/Telegram/Resources/icons/menu/tradable@2x.png differ diff --git a/Telegram/Resources/icons/menu/tradable@3x.png b/Telegram/Resources/icons/menu/tradable@3x.png new file mode 100644 index 000000000..fa9ab7213 Binary files /dev/null and b/Telegram/Resources/icons/menu/tradable@3x.png differ diff --git a/Telegram/Resources/icons/menu/unique.png b/Telegram/Resources/icons/menu/unique.png new file mode 100644 index 000000000..8b1cc6087 Binary files /dev/null and b/Telegram/Resources/icons/menu/unique.png differ diff --git a/Telegram/Resources/icons/menu/unique@2x.png b/Telegram/Resources/icons/menu/unique@2x.png new file mode 100644 index 000000000..774852ecf Binary files /dev/null and b/Telegram/Resources/icons/menu/unique@2x.png differ diff --git a/Telegram/Resources/icons/menu/unique@3x.png b/Telegram/Resources/icons/menu/unique@3x.png new file mode 100644 index 000000000..e660a8262 Binary files /dev/null and b/Telegram/Resources/icons/menu/unique@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/earn_stars.png b/Telegram/Resources/icons/settings/premium/business/earn_stars.png new file mode 100644 index 000000000..84419b169 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/earn_stars.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/earn_stars@2x.png b/Telegram/Resources/icons/settings/premium/business/earn_stars@2x.png new file mode 100644 index 000000000..3f6b9e30f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/earn_stars@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/earn_stars@3x.png b/Telegram/Resources/icons/settings/premium/business/earn_stars@3x.png new file mode 100644 index 000000000..19823b48b Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/earn_stars@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 19c05d3eb..cbd28cefb 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_activate" = "Use this account"; "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; +"lng_menu_my_profile" = "My Profile"; "lng_menu_my_stories" = "My Stories"; "lng_menu_my_groups" = "My Groups"; "lng_menu_my_channels" = "My Channels"; @@ -288,6 +289,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_error_cant_add_member" = "Sorry, you can't add the bot to this group. Ask a group admin to do it."; "lng_error_cant_add_bot" = "Sorry, this bot can't be added to groups."; "lng_error_cant_add_admin_invite" = "You can't add this user as an admin because they are not a member of this group and you are not allowed to add them."; +"lng_error_you_blocked_user" = "Sorry, you can't add this user or bot to groups because you've blocked them. Please unblock to proceed."; +"lng_error_add_admin_not_member" = "You can't add this user as an admin because they are not a member of this group and you are not allowed to add them."; +"lng_error_user_admin_invalid" = "You can't ban this user because they are an admin in this group and you are not allowed to demote them."; +"lng_error_channel_bots_too_much" = "Sorry, this channel has too many bots."; +"lng_error_group_bots_too_much" = "There are too many bots in this group. Please remove some of the bots you're not using first."; "lng_error_cant_add_admin_unban" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them."; "lng_error_cant_ban_admin" = "You can't ban this user because they are an admin in this group and you are not allowed to demote them."; "lng_error_cant_reply_other" = "This message can't be replied in another chat."; @@ -1475,6 +1481,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_enable_notifications" = "Notifications"; "lng_profile_send_message" = "Send Message"; "lng_profile_open_app" = "Open App"; +"lng_profile_open_app_short" = "Open"; "lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; "lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; "lng_profile_bot_permissions_title" = "Allow access to"; @@ -1544,6 +1551,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_channel_title" = "Manage Channel"; "lng_manage_bot_title" = "Manage Bot"; "lng_manage_peer_recent_actions" = "Recent actions"; +"lng_manage_peer_star_ref" = "Affiliate programs"; "lng_manage_peer_members" = "Members"; "lng_manage_peer_subscribers" = "Subscribers"; "lng_manage_peer_administrators" = "Administrators"; @@ -1617,11 +1625,130 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_bot_balance" = "Balance"; "lng_manage_peer_bot_balance_currency" = "Toncoin"; "lng_manage_peer_bot_balance_credits" = "Stars"; +"lng_manage_peer_bot_star_ref" = "Affiliate Program"; +"lng_manage_peer_bot_star_ref_off" = "Off"; +"lng_manage_peer_bot_star_ref_about" = "Share a link to {bot} with your friends and earn {amount} of their spending there."; +"lng_manage_peer_bot_verify" = "Verify Accounts"; "lng_manage_peer_bot_edit_intro" = "Edit Intro"; "lng_manage_peer_bot_edit_commands" = "Edit Commands"; "lng_manage_peer_bot_edit_settings" = "Change Bot Settings"; "lng_manage_peer_bot_about" = "Use {bot} to manage this bot."; +"lng_bot_verify_title" = "Choose Chat to Verify"; +"lng_bot_verify_bot_title" = "Verify Bot"; +"lng_bot_verify_bot_text" = "Do you want to verify {name} with your verification mark and description?"; +"lng_bot_verify_bot_about" = "You can customize your description for each bot."; +"lng_bot_verify_bot_submit" = "Verify Bot"; +"lng_bot_verify_bot_sent" = "{name} has been notified and will receive your verification mark and description upon accepting."; +"lng_bot_verify_bot_remove" = "This bot is already verified by you. Do you want to remove verification?"; +"lng_bot_verify_user_title" = "Verify User"; +"lng_bot_verify_user_text" = "Do you want to verify {name} with your verification mark and description?"; +"lng_bot_verify_user_about" = "You can customize your description for each account."; +"lng_bot_verify_user_submit" = "Verify User"; +"lng_bot_verify_user_sent" = "{name} has been notified and will receive your verification mark and description upon accepting."; +"lng_bot_verify_user_remove" = "This account is already verified by you. Do you want to remove verification?"; +"lng_bot_verify_channel_title" = "Verify Channel"; +"lng_bot_verify_channel_text" = "Do you want to verify {name} with your verification mark and description?"; +"lng_bot_verify_channel_about" = "You can customize your description for each channel."; +"lng_bot_verify_channel_submit" = "Verify Channel"; +"lng_bot_verify_channel_sent" = "{name} has been notified and will receive your verification mark and description upon accepting."; +"lng_bot_verify_channel_remove" = "This channel is already verified by you. Do you want to remove verification?"; +"lng_bot_verify_group_title" = "Verify Group"; +"lng_bot_verify_group_text" = "Do you want to verify {name} with your verification mark and description?"; +"lng_bot_verify_group_about" = "You can customize your description for each group."; +"lng_bot_verify_group_submit" = "Verify Group"; +"lng_bot_verify_group_sent" = "{name} has been notified and will receive your verification mark and description upon accepting."; +"lng_bot_verify_group_remove" = "This group is already verified by you. Do you want to remove verification?"; +"lng_bot_verify_description_label" = "Description"; +"lng_bot_verify_remove_title" = "Remove verification"; +"lng_bot_verify_remove_submit" = "Remove"; +"lng_bot_verify_remove_done" = "You've removed this verification."; + +"lng_star_ref_title" = "Affiliate Program"; +"lng_star_ref_about" = "Reward those who help grow your user base."; +"lng_star_ref_share_title" = "Share revenue with affiliates"; +"lng_star_ref_share_about" = "Set the commission for revenue generated by users referred to you."; +"lng_star_ref_launch_title" = "Launch your affiliate program"; +"lng_star_ref_launch_about" = "Telegram will feature your program for millions of potential affiliates."; +"lng_star_ref_let_title" = "Let affiliate promote you"; +"lng_star_ref_let_about" = "Affiliates will share your referral link with their audience."; +"lng_star_ref_commission_title" = "Commission"; +"lng_star_ref_commission_about" = "Define the percentage of star revenue your affiliates earn for referring users to your bot."; +"lng_star_ref_duration_title" = "Duration"; +"lng_star_ref_duration_about" = "Set the duration for which affiliates will earn commissions from referred users."; +"lng_star_ref_existing_title" = "View existing programs"; +"lng_star_ref_existing_about" = "Explore what other mini apps offer."; +"lng_star_ref_add_bot" = "Add {bot}"; +"lng_star_ref_end" = "End Affiliate Program"; +"lng_star_ref_start" = "Start Affiliate Program"; +"lng_star_ref_start_disabled" = "Available in {time}"; +"lng_star_ref_start_info" = "By creating an affiliate program, you agree to the {terms} of Affiliate Programs."; +"lng_star_ref_update" = "Update Affiliate Program"; +"lng_star_ref_update_info" = "By updating an affiliate program, you agree to the {terms} of Affiliate Programs."; +"lng_star_ref_button_link" = "terms and conditions"; +"lng_star_ref_tos_url" = "https://telegram.org/tos/mini-apps"; +"lng_star_ref_warning_title" = "Warning"; +"lng_star_ref_warning_text" = "Once you start the affiliate program, you won't be able to decrease its commission or duration. You can only increase these parameters or end the program, which will disable all previously distributed referral links."; +"lng_star_ref_warning_change" = "This change is irreversible. You won't be able to reduce commission or duration. You can only increase these parameters or end the program, which will disable all previously shared referral links."; +"lng_star_ref_warning_start" = "Start"; +"lng_star_ref_warning_update" = "Update"; +"lng_star_ref_warning_if_end" = "If you end your affiliate program:"; +"lng_star_ref_warning_if_end1" = "Any referral links already shared will be disabled in **24** hours."; +"lng_star_ref_warning_if_end2" = "All participating affiliates will be notified."; +"lng_star_ref_warning_if_end3" = "You will be able to start a new affiliate program only in **24** hours."; +"lng_star_ref_warning_end" = "End Anyway"; +"lng_star_ref_created_title" = "Affiliate program started"; +"lng_star_ref_created_text" = "Any Telegram user, channel owner or mini app developer can now join your program."; +"lng_star_ref_updated_title" = "Affiliate program updated"; +"lng_star_ref_updated_text" = "Any Telegram user, channel owner or mini app developer can join your program."; +"lng_star_ref_ended_title" = "Affiliate program ended"; +"lng_star_ref_ended_text" = "Participating affiliates have been notified. All referral links will be disabled in **24** hours."; +"lng_star_ref_list_title" = "Affiliate Programs"; +"lng_star_ref_list_about_channel" = "Promote mini apps to your subscribers and earn a share of their revenue in Stars."; +"lng_star_ref_list_text" = "Earn a commission each time a user who first accessed a mini app through your referral link spends **Stars** within it."; +"lng_star_ref_list_my" = "My Programs"; +"lng_star_ref_list_my_open" = "Open App"; +"lng_star_ref_list_my_copy" = "Copy Link"; +"lng_star_ref_list_my_leave" = "Leave"; +"lng_star_ref_list_subtitle" = "Programs"; +"lng_star_ref_sort_text" = "Sort by {sort}"; +"lng_star_ref_sort_profitability" = "Profitability"; +"lng_star_ref_sort_date" = "Date"; +"lng_star_ref_sort_revenue" = "Revenue"; +"lng_star_ref_reliable_title" = "Reliable"; +"lng_star_ref_reliable_about" = "Receive guaranteed commissions for spending by users you refer."; +"lng_star_ref_transparent_title" = "Transparent"; +"lng_star_ref_transparent_about" = "Track your commissions from referred users in real time."; +"lng_star_ref_simple_title" = "Simple"; +"lng_star_ref_simple_about" = "Choose a mini app below, get your referral link, and start earning Stars."; +"lng_star_ref_duration_forever" = "Forever"; +"lng_star_ref_one_about" = "{app} will share {amount} of the revenue from each user you refer to it {duration}."; +"lng_star_ref_one_about_for_forever" = "for **lifetime**"; +"lng_star_ref_one_about_for_months#one" = "for **{count} month**"; +"lng_star_ref_one_about_for_months#other" = "for **{count} months**"; +"lng_star_ref_one_about_for_years#one" = "for **{count} year**"; +"lng_star_ref_one_about_for_years#other" = "for **{count} years**"; +"lng_star_ref_one_daily_revenue" = "Daily revenue per user: {amount}"; +"lng_star_ref_one_join" = "Join Program"; +"lng_star_ref_one_join_text" = "By joining this program, you agree to the {terms} of Affiliate Programs."; +"lng_star_ref_joined_title" = "Program joined"; +"lng_star_ref_joined_text" = "You can now copy the referral link."; +"lng_star_ref_link_title" = "Referral Link"; +"lng_star_ref_link_about_channel" = "Share this link with your subscribers to earn a {amount} commission on their spending in {app} {duration}."; +"lng_star_ref_link_about_user" = "Share this link with your friends to earn a {amount} commission on their spending in {app} {duration}."; +"lng_star_ref_link_about_bot" = "Share this link with your users to earn a {amount} commission on their spending in {app} {duration}."; +"lng_star_ref_link_recipient" = "Commissions will be sent to:"; +"lng_star_ref_link_copy" = "Copy Link"; +"lng_star_ref_link_copy_none" = "No one have opened {app} through this link."; +"lng_star_ref_link_copy_users#one" = "{count} user have opened {app} through this link."; +"lng_star_ref_link_copy_users#other" = "{count} users have opened {app} through this link."; +"lng_star_ref_link_copied_title" = "Link copied to clipboard"; +"lng_star_ref_link_copied_text" = "Share this link and earn {amount} of what people who use it spend in {app}!"; +"lng_star_ref_stopped" = "This affiliate link is no longer active."; +"lng_star_ref_revoke_title" = "Revoke Link"; +"lng_star_ref_revoke_text" = "Are you sure you want to revoke the link of {bot}?"; +"lng_star_ref_revoked_title" = "Link removed"; +"lng_star_ref_revoked_text" = "It will no longer work."; "lng_manage_discussion_group" = "Discussion"; "lng_manage_discussion_group_add" = "Add a group"; @@ -1889,21 +2016,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_proximity_distance_km#other" = "{count} km"; "lng_action_webview_data_done" = "Data from the \"{text}\" button was transferred to the bot."; "lng_action_gift_received" = "{user} sent you a gift for {cost}"; +"lng_action_gift_unique_received" = "{user} sent you a unique collectible item"; "lng_action_gift_sent" = "You sent a gift for {cost}"; +"lng_action_gift_unique_sent" = "You sent a unique collectible item"; +"lng_action_gift_upgraded" = "{user} turned the gift from you 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_transferred" = "{user} transferred you a gift"; +"lng_action_gift_transferred_mine" = "You transferred a gift to {user}"; "lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}"; +"lng_action_gift_self_bought" = "You bought a gift for {cost}"; +"lng_action_gift_self_subtitle" = "Saved Gift"; +"lng_action_gift_self_about#one" = "Display this gift on your page or convert it to **{count}** Star."; +"lng_action_gift_self_about#other" = "Display this gift on your page or convert it to **{count}** Stars."; +"lng_action_gift_self_about_unique" = "You can display this gift on your page or turn it into unique collectible and send to others."; "lng_action_gift_for_stars#one" = "{count} Star"; "lng_action_gift_for_stars#other" = "{count} Stars"; "lng_action_gift_got_subtitle" = "Gift from {user}"; "lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star."; "lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars."; +"lng_action_gift_got_upgradable_text" = "Upgrade this gift to a unique collectible."; "lng_action_gift_got_gift_text" = "You can keep this gift on your page."; "lng_action_gift_can_remove_text" = "You can remove this gift from your page."; "lng_action_gift_sent_subtitle" = "Gift for {user}"; "lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star."; "lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars."; +"lng_action_gift_sent_upgradable" = "{user} can upgrade this gift to a unique collectible."; "lng_action_gift_premium_months#one" = "{count} Month Premium"; "lng_action_gift_premium_months#other" = "{count} Months Premium"; "lng_action_gift_premium_about" = "Subscription for exclusive Telegram features."; +"lng_action_gift_refunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned."; "lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile."; "lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile."; "lng_action_suggested_photo_button" = "View Photo"; @@ -1952,6 +2094,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_giveaway_results_credits#other" = "{count} winners of the giveaway were randomly selected by Telegram and received their prize."; "lng_action_giveaway_results_credits_some" = "Some winners of the giveaway were randomly selected by Telegram and received their prize."; "lng_action_giveaway_results_none" = "No winners of the giveaway could be selected."; +"lng_action_boost_apply_me" = "You boosted the group"; "lng_action_boost_apply#one" = "{from} boosted the group"; "lng_action_boost_apply#other" = "{from} boosted the group {count} times"; "lng_action_set_chat_intro" = "{from} added the message below for all empty chats. How?"; @@ -2300,11 +2443,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_business" = "Upgrade your account with business features such as location, opening hours and quick replies."; "lng_premium_summary_subtitle_effects" = "Message Effects"; "lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages."; +"lng_premium_summary_subtitle_filter_tags" = "Tag Your Chats"; +"lng_premium_summary_about_filter_tags" = "Display folder names for each chat in the chat list."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; "lng_premium_summary_new_badge" = "NEW"; +"lng_soon_badge" = "Soon"; "lng_premium_success" = "You've successfully subscribed to Telegram Premium!"; "lng_premium_unavailable" = "This feature requires subscription to **Telegram Premium**.\n\nUnfortunately, **Telegram Premium** is not available in your region."; @@ -2432,11 +2578,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_options_about" = "By proceeding and purchasing Stars, you agree with the {link}."; "lng_credits_summary_options_about_link" = "Terms and Conditions"; "lng_credits_summary_options_about_url" = "https://telegram.org/tos/stars"; +"lng_credits_summary_earn_title" = "Earn Stars"; +"lng_credits_summary_earn_about" = "Distribute links to mini apps and earn a share of their revenue in Stars."; "lng_credits_summary_history_tab_full" = "All Transactions"; "lng_credits_summary_history_tab_in" = "Incoming"; "lng_credits_summary_history_tab_out" = "Outgoing"; "lng_credits_summary_history_entry_inner_in" = "In-App Purchase"; "lng_credits_summary_balance" = "Balance"; +"lng_credits_commission" = "{amount} commission"; "lng_credits_more_options" = "More Options"; "lng_credits_balance_me" = "your balance"; "lng_credits_buy_button" = "Buy More Stars"; @@ -2475,6 +2624,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance."; "lng_credits_box_history_entry_peer" = "Recipient"; "lng_credits_box_history_entry_peer_in" = "From"; +"lng_credits_box_history_entry_gift_from" = "Gift From"; "lng_credits_box_history_entry_via" = "Via"; "lng_credits_box_history_entry_play_market" = "Play Store"; "lng_credits_box_history_entry_app_store" = "App Store"; @@ -2484,6 +2634,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_giveaway_name" = "Received Prize"; "lng_credits_box_history_entry_gift_sent" = "Sent Gift"; "lng_credits_box_history_entry_gift_converted" = "Converted Gift"; +"lng_credits_box_history_entry_gift_transfer" = "Gift Transfer"; "lng_credits_box_history_entry_gift_unavailable" = "Unavailable"; "lng_credits_box_history_entry_gift_sold_out" = "This gift has sold out"; "lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}"; @@ -2499,6 +2650,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_via_premium_bot" = "Premium Bot"; "lng_credits_box_history_entry_id" = "Transaction ID"; "lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard."; +"lng_credits_box_history_entry_reason_star_ref" = "Affiliate Program"; +"lng_credits_box_history_entry_affiliate" = "Affiliate"; +"lng_credits_box_history_entry_miniapp" = "Mini App"; +"lng_credits_box_history_entry_referred" = "Referred User"; "lng_credits_box_history_entry_success_date" = "Transaction date"; "lng_credits_box_history_entry_success_url" = "Transaction link"; "lng_credits_box_history_entry_media" = "Media"; @@ -2507,6 +2662,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_about_link" = "here"; "lng_credits_box_history_entry_reaction_name" = "Star Reaction"; "lng_credits_box_history_entry_subscription" = "Monthly subscription fee"; +"lng_credits_box_history_entry_gift_upgrade" = "Collectible Upgrade"; "lng_credits_subscription_section" = "My subscriptions"; "lng_credits_box_subscription_title" = "Subscription"; @@ -3077,27 +3233,59 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_stars_sold_out" = "sold out"; "lng_gift_stars_tabs_all" = "All Gifts"; "lng_gift_stars_tabs_limited" = "Limited"; +"lng_gift_stars_tabs_in_stock" = "In Stock"; "lng_gift_send_title" = "Send a Gift"; "lng_gift_send_message" = "Enter Message"; "lng_gift_send_anonymous" = "Hide My Name"; +"lng_gift_send_anonymous_self" = "Hide my name and message from visitors to my profile."; "lng_gift_send_anonymous_about" = "You can hide your name and message from visitors to {user}'s profile. {recipient} will still see your name and message."; +"lng_gift_send_unique" = "Make Unique for {price}"; +"lng_gift_send_unique_about" = "Enable this to let {user} turn your gift into a unique collectible. {link}"; +"lng_gift_send_unique_link" = "Learn More >"; "lng_gift_send_premium_about" = "Only {user} will see your message."; "lng_gift_send_button" = "Send a Gift for {cost}"; +"lng_gift_send_button_self" = "Buy a Gift for {cost}"; "lng_gift_sent_title" = "Gift Sent!"; "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_limited_of_one" = "unique"; "lng_gift_limited_of_count" = "1 of {amount}"; +"lng_gift_collectible_tag" = "gift"; +"lng_gift_price_unique" = "Unique"; +"lng_gift_view_unpack" = "Unpack"; "lng_gift_anonymous_hint" = "Only you can see the sender's name."; "lng_gift_hidden_hint" = "This gift is hidden. Only you can see it."; "lng_gift_visible_hint" = "This gift is visible to visitors of your page."; "lng_gift_availability" = "Availability"; "lng_gift_from_hidden" = "Hidden User"; +"lng_gift_visibility" = "Visibility"; +"lng_gift_visibility_shown" = "Visible on your page"; +"lng_gift_visibility_hidden" = "Not visible on your page"; +"lng_gift_visibility_show" = "show"; +"lng_gift_visibility_hide" = "hide"; +"lng_gift_self_status" = "buy yourself a gift"; +"lng_gift_self_title" = "Buy a Gift"; +"lng_gift_self_about" = "Buy yourself a gift to display on your page or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others later."; +"lng_gift_unique_owner" = "Owner"; +"lng_gift_unique_owner_change" = "change"; +"lng_gift_unique_status" = "Status"; +"lng_gift_unique_status_non" = "Non-Unique"; +"lng_gift_unique_status_upgrade" = "upgrade"; +"lng_gift_unique_number" = "Collectible #{index}"; +"lng_gift_unique_model" = "Model"; +"lng_gift_unique_backdrop" = "Backdrop"; +"lng_gift_unique_symbol" = "Symbol"; +"lng_gift_unique_rarity" = "Only {percent} of such collectibles have this attribute."; +"lng_gift_unique_availability#one" = "{count} of {amount} issued"; +"lng_gift_unique_availability#other" = "{count} of {amount} issued"; +"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}\"."; +"lng_gift_unique_info_reciever" = "Gifted to {recipient} on {date}."; +"lng_gift_unique_info_reciever_comment" = "Gifted to {recipient} on {date} with the comment \"{text}\"."; "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_display_on_page" = "Display on my Page"; -"lng_gift_display_on_page_hide" = "Hide from my Page"; "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"; @@ -3117,6 +3305,45 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_send_small" = "send a gift"; "lng_gift_sell_small#one" = "sell for {count} Star"; "lng_gift_sell_small#other" = "sell for {count} Stars"; +"lng_gift_upgrade_title" = "Upgrade Gift"; +"lng_gift_upgrade_about" = "Turn your gift into a unique collectible\nthat you can transfer or auction."; +"lng_gift_upgrade_preview_title" = "Make Unique"; +"lng_gift_upgrade_preview_about" = "Let {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_transferable_title" = "Transferable"; +"lng_gift_upgrade_transferable_about" = "Send your upgraded gift to any of your friends 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_button" = "Upgrade for {price}"; +"lng_gift_upgrade_free" = "Upgrade for Free"; +"lng_gift_upgrade_confirm" = "Confirm"; +"lng_gift_upgrade_add_my" = "Add my name to the gift"; +"lng_gift_upgrade_add_my_comment" = "Add my name and comment"; +"lng_gift_upgrade_add_sender" = "Add sender's name to the gift"; +"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_transferred_title" = "Gift Transferred"; +"lng_gift_transferred_about" = "{name} was successfully transferred to {recipient}."; +"lng_gift_transfer_title" = "Transfer {name}"; +"lng_gift_transfer_via_blockchain" = "Send via Blockchain"; +"lng_gift_transfer_unlocks_days#one" = "unlocks in {count} day"; +"lng_gift_transfer_unlocks_days#other" = "unlocks in {count} days"; +"lng_gift_transfer_unlocks_hours#one" = "unlocks in {count} hour"; +"lng_gift_transfer_unlocks_hours#other" = "unlocks in {count} hours"; +"lng_gift_transfer_unlocks_title" = "Unlocking in progress"; +"lng_gift_transfer_unlocks_about" = "{when}, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction."; +"lng_gift_transfer_unlocks_when_days#one" = "In {count} day"; +"lng_gift_transfer_unlocks_when_days#other" = "In {count} days"; +"lng_gift_transfer_unlocks_when_hours#one" = "In {count} hour"; +"lng_gift_transfer_unlocks_when_hours#other" = "In {count} hours"; +"lng_gift_transfer_unlocks_update_title" = "Update required"; +"lng_gift_transfer_unlocks_update_about" = "Please update your Telegram application to the latest version."; +"lng_gift_transfer_sure" = "Do you want to transfer ownership of {name} to {recipient}?"; +"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_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; @@ -3373,6 +3600,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_replies_no_comments" = "No comments here yet..."; "lng_verification_codes" = "Verification Codes"; +"lng_verification_codes_about" = "Third-party services, like websites and stores, can send verification codes to your phone number via Telegram instead of SMS. Such codes will appear in this chat.\n\nIf you didn't request any codes — don't worry! Most likely, someone made a mistake when entering their number."; "lng_archived_name" = "Archived chats"; "lng_archived_add" = "Archive"; @@ -3547,6 +3775,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_mark_read_sure" = "Are you sure you want to mark all chats from this folder as read?"; "lng_context_mark_read_all" = "Mark all chats as read"; "lng_context_mark_read_all_sure" = "Are you sure you want to mark all chats as read?"; +"lng_context_mark_read_all_sure_2" = "**This action cannot be undone.**"; "lng_context_mark_read_mentions_all" = "Mark all mentions as read"; "lng_context_mark_read_reactions_all" = "Read all reactions"; "lng_context_archive_expand" = "Expand"; @@ -3706,6 +3935,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_react_agree_link" = "Terms of Service"; "lng_paid_react_toast#one" = "Star Sent!"; "lng_paid_react_toast#other" = "Stars Sent!"; +"lng_paid_react_toast_anonymous#one" = "Star sent anonymously!"; +"lng_paid_react_toast_anonymous#other" = "Stars sent anonymously!"; "lng_paid_react_toast_text#one" = "You reacted with **{count} Star**."; "lng_paid_react_toast_text#other" = "You reacted with **{count} Stars**."; "lng_paid_react_undo" = "Undo"; @@ -3916,6 +4147,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_messages_from" = "Show messages from"; "lng_search_messages_n_of_amount" = "{n} of {amount}"; "lng_search_messages_none" = "No results"; +"lng_search_filter_all" = "All chats"; +"lng_search_filter_private" = "Private chats"; +"lng_search_filter_group" = "Group chats"; +"lng_search_filter_channel" = "Channels"; "lng_media_save_progress" = "{ready} of {total} {mb}"; "lng_mediaview_save_as" = "Save As..."; @@ -4812,6 +5047,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_forward_show_captions" = "Show captions"; "lng_forward_change_recipient" = "Change recipient"; "lng_forward_sender_names_removed" = "Sender names removed"; +"lng_forward_header_short" = "Forward"; +"lng_forward_action_show_sender" = "Show Sender Name"; +"lng_forward_action_show_senders" = "Show Sender Names"; +"lng_forward_action_hide_sender" = "Hide Sender Name"; +"lng_forward_action_hide_senders" = "Hide Sender Names"; +"lng_forward_action_show_caption" = "Show Caption"; +"lng_forward_action_show_captions" = "Show Captions"; +"lng_forward_action_hide_caption" = "Hide Caption"; +"lng_forward_action_hide_captions" = "Hide Captions"; +"lng_forward_action_change_recipient" = "Change Recipient"; +"lng_forward_action_remove" = "Do Not Forward"; "lng_passport_title" = "Telegram Passport"; "lng_passport_request1" = "{bot} requests access to your personal data"; @@ -5113,6 +5359,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_edit" = "Edit Folder"; "lng_filters_setup_menu" = "Edit Folders"; "lng_filters_new_name" = "Folder name"; +"lng_filters_enable_animations" = "Enable animations"; +"lng_filters_disable_animations" = "Disable animations"; "lng_filters_add_chats" = "Add Chats"; "lng_filters_remove_chats" = "Add Chats to Exclude"; "lng_filters_include" = "Included chats"; @@ -5155,11 +5403,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_view_subtitle" = "Tabs view"; "lng_filters_vertical" = "Tabs on the left"; "lng_filters_horizontal" = "Tabs at the top"; +"lng_filters_enable_tags" = "Show Folder Tags"; +"lng_filters_enable_tags_about" = "Display folder names for each chat in the chat list."; +"lng_filters_enable_tags_about_premium" = "Subscribe to **{link}** to display folder names for each chat in the chat list."; +"lng_filters_tag_color_subtitle" = "Folder color in chat list"; +"lng_filters_tag_color_about" = "Choose a color for the tag of this folder."; +"lng_filters_tag_color_no" = "No Tag"; "lng_filters_delete_sure" = "Are you sure you want to delete this folder? This will also deactivate all the invite links created to share this folder."; "lng_filters_link" = "Share Folder"; "lng_filters_link_has" = "Invite links"; +"lng_filters_checkbox_remove_bot" = "Remove bot from all folders"; +"lng_filters_checkbox_remove_group" = "Remove group from all folders"; +"lng_filters_checkbox_remove_channel" = "Remove channel from all folders"; + "lng_filters_link_create" = "Create an Invite Link"; "lng_filters_link_cant" = "You can’t share folders which include or exclude specific chat types like 'Groups', 'Contacts', etc."; "lng_filters_link_about" = "Share access to some of this folder's groups and channels with others."; @@ -5705,6 +5963,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_recent_chats" = "Chats"; "lng_recent_channels" = "Channels"; "lng_recent_apps" = "Apps"; +"lng_all_photos" = "Photos"; +"lng_all_videos" = "Videos"; +"lng_all_downloads" = "Downloads"; +"lng_all_links" = "Links"; +"lng_all_files" = "Files"; +"lng_all_music" = "Music"; +"lng_all_voice" = "Voice"; "lng_channels_none_title" = "No channels yet..."; "lng_channels_none_about" = "You are not currently subscribed to any channels."; "lng_channels_your_title" = "Channels you joined"; @@ -5738,6 +6003,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_search_tab_no_results_text" = "There were no results for \"{query}\"."; "lng_search_tab_no_results_retry" = "Try another hashtag."; "lng_search_tab_by_hashtag" = "Enter a hashtag to find messages containing it."; +"lng_search_tab_try_in_all" = "Search in All Messages"; "lng_contact_details_button" = "View Contact"; "lng_contact_details_title" = "Contact details"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index f60061f99..e66d847b6 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -29,6 +29,7 @@ ../../animations/search.tgs ../../animations/noresults.tgs ../../animations/hello_status.tgs + ../../animations/starref_link.tgs ../../animations/dice/dice_idle.tgs ../../animations/dice/dart_idle.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 7d0463c4e..7ca5f84b6 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -4,6 +4,7 @@ ../../art/bg_thumbnail.png ../../art/bg_initial.jpg ../../art/business_logo.png + ../../art/affiliate_logo.png ../../art/logo_256.png ../../art/logo_256_no_margin.png ../../art/themeimage.jpg diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 1f5ac3f65..5f8a2f3e8 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="5.10.3.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 38113d8cc..d4fea4b71 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,8,3,0 - PRODUCTVERSION 5,8,3,0 + FILEVERSION 5,10,3,0 + PRODUCTVERSION 5,10,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.8.3.0" - VALUE "LegalCopyright", "Copyright (C) 2014-2024" + VALUE "FileVersion", "5.10.3.0" + VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.8.3.0" + VALUE "ProductVersion", "5.10.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 7a45674d7..db48deca9 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,8,3,0 - PRODUCTVERSION 5,8,3,0 + FILEVERSION 5,10,3,0 + PRODUCTVERSION 5,10,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.8.3.0" - VALUE "LegalCopyright", "Copyright (C) 2014-2024" + VALUE "FileVersion", "5.10.3.0" + VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "5.8.3.0" + VALUE "ProductVersion", "5.10.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 2c8410b80..d7a1636b3 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_filters.h" +#include "api/api_text_entities.h" #include "apiwrap.h" +#include "base/event_filter.h" #include "boxes/peer_list_box.h" #include "boxes/premium_limits_box.h" #include "boxes/filters/edit_filter_links.h" // FilterChatStatusText #include "core/application.h" #include "core/core_settings.h" +#include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_filters.h" @@ -24,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/controls/filter_link_header.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/filter_icons.h" #include "ui/vertical_list.h" @@ -48,7 +52,7 @@ public: ToggleChatsController( not_null window, ToggleAction action, - const QString &title, + Data::ChatFilterTitle title, std::vector> chats, std::vector> additional); @@ -74,7 +78,6 @@ private: Ui::RpWidget *_addedBottomWidget = nullptr; ToggleAction _action = ToggleAction::Adding; - QString _filterTitle; base::flat_set> _checkable; std::vector> _chats; std::vector> _additional; @@ -105,9 +108,9 @@ private: [[nodiscard]] TextWithEntities AboutText( Ui::FilterLinkHeaderType type, - const QString &title) { + TextWithEntities title) { using Type = Ui::FilterLinkHeaderType; - auto boldTitle = Ui::Text::Bold(title); + auto boldTitle = Ui::Text::Wrapped(title, EntityType::Bold); return (type == Type::AddingFilter) ? tr::lng_filters_by_link_sure( tr::now, @@ -137,23 +140,33 @@ void InitFilterLinkHeader( not_null box, Fn adjust, Ui::FilterLinkHeaderType type, - const QString &title, - const QString &iconEmoji, - rpl::producer count) { + Data::ChatFilterTitle title, + QString iconEmoji, + rpl::producer count, + bool horizontalFilters) { const auto icon = Ui::LookupFilterIcon( Ui::LookupFilterIconByEmoji( iconEmoji ).value_or(Ui::FilterIcon::Custom)).active; + const auto isStatic = title.isStatic; + const auto makeContext = [=](Fn repaint) { + return Core::MarkedTextContext{ + .session = &box->peerListUiShow()->session(), + .customEmojiRepaint = std::move(repaint), + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; auto header = Ui::MakeFilterLinkHeader(box, { .type = type, .title = TitleText(type)(tr::now), - .about = AboutText(type, title), - .folderTitle = title, + .about = AboutText(type, title.text), + .makeAboutContext = makeContext, + .folderTitle = title.text, .folderIcon = icon, .badge = (type == Ui::FilterLinkHeaderType::AddingChats ? std::move(count) : rpl::single(0)), - .horizontalFilters = Core::App().settings().chatFiltersHorizontal(), + .horizontalFilters = horizontalFilters, }); const auto widget = header.widget; widget->resizeToWidth(st::boxWideWidth); @@ -246,12 +259,11 @@ void ImportInvite( ToggleChatsController::ToggleChatsController( not_null window, ToggleAction action, - const QString &title, + Data::ChatFilterTitle title, std::vector> chats, std::vector> additional) : _window(window) , _action(action) -, _filterTitle(title) , _chats(std::move(chats)) , _additional(std::move(additional)) { setStyleOverrides(&st::filterLinkChatsList); @@ -527,7 +539,7 @@ void ShowImportError( void ShowImportToast( base::weak_ptr weak, - const QString &title, + Data::ChatFilterTitle title, Ui::FilterLinkHeaderType type, int added) { const auto strong = weak.get(); @@ -538,22 +550,55 @@ void ShowImportToast( const auto phrase = created ? tr::lng_filters_added_title : tr::lng_filters_updated_title; - auto text = Ui::Text::Bold(phrase(tr::now, lt_folder, title)); + auto text = Ui::Text::Wrapped( + phrase(tr::now, lt_folder, title.text, Ui::Text::WithEntities), + EntityType::Bold); if (added > 0) { const auto phrase = created ? tr::lng_filters_added_also : tr::lng_filters_updated_also; text.append('\n').append(phrase(tr::now, lt_count, added)); } - strong->showToast(std::move(text)); + const auto isStatic = title.isStatic; + const auto makeContext = [=](not_null widget) { + return Core::MarkedTextContext{ + .session = &strong->session(), + .customEmojiRepaint = [=] { widget->update(); }, + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; + strong->showToast({ + .text = std::move(text), + .textContext = makeContext, + }); +} + +void HandleEnterInBox(not_null box) { + const auto isEnter = [=](not_null event) { + if (event->type() == QEvent::KeyPress) { + if (const auto k = static_cast(event.get())) { + return (k->key() == Qt::Key_Enter) + || (k->key() == Qt::Key_Return); + } + } + return false; + }; + + base::install_event_filter(box, [=](not_null event) { + if (isEnter(event)) { + box->triggerButton(0); + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }); } void ProcessFilterInvite( base::weak_ptr weak, const QString &slug, FilterId filterId, - const QString &title, - const QString &iconEmoji, + Data::ChatFilterTitle title, + QString iconEmoji, std::vector> peers, std::vector> already) { const auto strong = weak.get(); @@ -572,6 +617,8 @@ void ProcessFilterInvite( title, std::move(peers), std::move(already)); + const auto horizontalFilters = !strong->enoughSpaceForFilters() + || Core::App().settings().chatFiltersHorizontal(); const auto raw = controller.get(); auto initBox = [=](not_null box) { box->setStyle(st::filterInviteBox); @@ -588,14 +635,23 @@ void ProcessFilterInvite( }); InitFilterLinkHeader(box, [=](int min, int max, int addedTop) { raw->adjust(min, max, addedTop); - }, type, title, iconEmoji, rpl::duplicate(badge)); + }, type, title, iconEmoji, rpl::duplicate(badge), horizontalFilters); raw->setRealContentHeight(box->heightValue()); + const auto isStatic = title.isStatic; + const auto makeContext = [=](Fn update) { + return Core::MarkedTextContext{ + .session = &strong->session(), + .customEmojiRepaint = update, + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; auto owned = Ui::FilterLinkProcessButton( box, type, - title, + title.text, + makeContext, std::move(badge)); const auto button = owned.data(); @@ -610,6 +666,8 @@ void ProcessFilterInvite( box->addButton(std::move(owned)); + HandleEnterInBox(box); + struct State { bool importing = false; }; @@ -694,7 +752,7 @@ void CheckFilterInvite( if (!strong) { return; } - auto title = QString(); + auto title = Data::ChatFilterTitle(); auto iconEmoji = QString(); auto filterId = FilterId(); auto peers = std::vector>(); @@ -713,7 +771,8 @@ void CheckFilterInvite( return result; }; result.match([&](const MTPDchatlists_chatlistInvite &data) { - title = qs(data.vtitle()); + title.text = ParseTextWithEntities(session, data.vtitle()); + title.isStatic = data.is_title_noanimate(); iconEmoji = data.vemoticon().value_or_empty(); peers = parseList(data.vpeers()); }, [&](const MTPDchatlists_chatlistInviteAlready &data) { @@ -778,8 +837,8 @@ void ProcessFilterUpdate( void ProcessFilterRemove( base::weak_ptr weak, - const QString &title, - const QString &iconEmoji, + Data::ChatFilterTitle title, + QString iconEmoji, std::vector> all, std::vector> suggest, Fn>)> done) { @@ -798,6 +857,8 @@ void ProcessFilterRemove( title, std::move(suggest), std::move(all)); + const auto horizontalFilters = !strong->enoughSpaceForFilters() + || Core::App().settings().chatFiltersHorizontal(); const auto raw = controller.get(); auto initBox = [=](not_null box) { box->setStyle(st::filterInviteBox); @@ -809,12 +870,21 @@ void ProcessFilterRemove( }); InitFilterLinkHeader(box, [=](int min, int max, int addedTop) { raw->adjust(min, max, addedTop); - }, type, title, iconEmoji, rpl::single(0)); + }, type, title, iconEmoji, rpl::single(0), horizontalFilters); + const auto isStatic = title.isStatic; + const auto makeContext = [=](Fn update) { + return Core::MarkedTextContext{ + .session = &strong->session(), + .customEmojiRepaint = update, + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; auto owned = Ui::FilterLinkProcessButton( box, type, - title, + title.text, + makeContext, std::move(badge)); const auto button = owned.data(); @@ -829,6 +899,8 @@ void ProcessFilterRemove( box->addButton(std::move(owned)); + HandleEnterInBox(box); + raw->selectedValue( ) | rpl::start_with_next([=]( base::flat_set> &&peers) { diff --git a/Telegram/SourceFiles/api/api_chat_filters.h b/Telegram/SourceFiles/api/api_chat_filters.h index 9167a41db..34933a7db 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.h +++ b/Telegram/SourceFiles/api/api_chat_filters.h @@ -17,6 +17,7 @@ class SessionController; namespace Data { class ChatFilter; +struct ChatFilterTitle; } // namespace Data namespace Api { @@ -36,8 +37,8 @@ void ProcessFilterUpdate( void ProcessFilterRemove( base::weak_ptr weak, - const QString &title, - const QString &iconEmoji, + Data::ChatFilterTitle title, + QString iconEmoji, std::vector> all, std::vector> suggest, Fn>)> done); diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index 134e3a005..18cab335e 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -207,32 +207,12 @@ void ConfirmSubscriptionBox( Ui::AddSkip(content); Ui::AddSkip(content); - { - const auto widget = Ui::CreateChild(content); - using ColoredMiniStars = Ui::Premium::ColoredMiniStars; - const auto stars = widget->lifetime().make_state( - widget, - false, - Ui::Premium::MiniStars::Type::BiStars); - stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); - widget->resize( - st::boxWideWidth - photoSize, - photoSize * 2); - content->sizeValue( - ) | rpl::start_with_next([=](const QSize &size) { - widget->moveToLeft(photoSize / 2, 0); - const auto starsRect = Rect(widget->size()); - stars->setPosition(starsRect.topLeft()); - stars->setSize(starsRect.size()); - widget->lower(); - }, widget->lifetime()); - widget->paintRequest( - ) | rpl::start_with_next([=](const QRect &r) { - auto p = QPainter(widget); - p.fillRect(r, Qt::transparent); - stars->paint(p); - }, widget->lifetime()); - } + Settings::AddMiniStars( + content, + Ui::CreateChild(content), + photoSize, + box->width(), + 2.); box->addRow( object_ptr>( diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index 175112c40..7d17ca41e 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_credits.h" +#include "api/api_premium.h" #include "api/api_statistics_data_deserialize.h" #include "api/api_updates.h" #include "apiwrap.h" @@ -73,9 +74,27 @@ constexpr auto kTransactionsLimit = 100; return PeerId(0); }).value; const auto stargift = tl.data().vstargift(); + const auto nonUniqueGift = stargift + ? stargift->match([&](const MTPDstarGift &data) { + return &data; + }, [](const auto &) { return (const MTPDstarGift*)nullptr; }) + : nullptr; const auto reaction = tl.data().is_reaction(); - const auto incoming = (int64(tl.data().vstars().v) >= 0); + const auto amount = Data::FromTL(tl.data().vstars()); + const auto starrefAmount = tl.data().vstarref_amount() + ? Data::FromTL(*tl.data().vstarref_amount()) + : StarsAmount(); + const auto starrefCommission + = tl.data().vstarref_commission_permille().value_or_empty(); + const auto starrefBarePeerId = tl.data().vstarref_peer() + ? peerFromMTP(*tl.data().vstarref_peer()).value + : 0; + const auto incoming = (amount >= StarsAmount()); const auto saveActorId = (reaction || !extended.empty()) && incoming; + const auto parsedGift = stargift + ? FromTL(&peer->session(), *stargift) + : std::optional(); + const auto giftStickerId = parsedGift ? parsedGift->document->id : 0; return Data::CreditsHistoryEntry{ .id = qs(tl.data().vid()), .title = qs(tl.data().vtitle().value_or_empty()), @@ -83,15 +102,17 @@ constexpr auto kTransactionsLimit = 100; .date = base::unixtime::parse(tl.data().vdate().v), .photoId = photo ? photo->id : 0, .extended = std::move(extended), - .credits = tl.data().vstars().v, + .credits = Data::FromTL(tl.data().vstars()), .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()), .barePeerId = saveActorId ? peer->id.value : barePeerId, .bareGiveawayMsgId = uint64( tl.data().vgiveaway_post_id().value_or_empty()), - .bareGiftStickerId = (stargift - ? owner->processDocument(stargift->data().vsticker())->id - : 0), + .bareGiftStickerId = giftStickerId, .bareActorId = saveActorId ? barePeerId : uint64(0), + .uniqueGift = parsedGift ? parsedGift->unique : nullptr, + .starrefAmount = starrefAmount, + .starrefCommission = starrefCommission, + .starrefRecipientId = starrefBarePeerId, .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { return Data::CreditsHistoryEntry::PeerType::Peer; }, [](const MTPDstarsTransactionPeerPlayMarket &) { @@ -117,12 +138,13 @@ constexpr auto kTransactionsLimit = 100; ? base::unixtime::parse(tl.data().vtransaction_date()->v) : QDateTime(), .successLink = qs(tl.data().vtransaction_url().value_or_empty()), - .starsConverted = int(stargift - ? stargift->data().vconvert_stars().v + .starsConverted = int(nonUniqueGift + ? nonUniqueGift->vconvert_stars().v : 0), .floodSkip = int(tl.data().vfloodskip_number().value_or(0)), .converted = stargift && incoming, .stargift = stargift.has_value(), + .giftUpgraded = tl.data().is_stargift_upgrade(), .reaction = tl.data().is_reaction(), .refunded = tl.data().is_refund(), .pending = tl.data().is_pending(), @@ -181,7 +203,7 @@ constexpr auto kTransactionsLimit = 100; return Data::CreditsStatusSlice{ .list = std::move(entries), .subscriptions = std::move(subscriptions), - .balance = status.data().vbalance().v, + .balance = Data::FromTL(status.data().vbalance()), .subscriptionsMissingBalance = status.data().vsubscriptions_missing_balance().value_or_empty(), .allLoaded = !status.data().vnext_offset().has_value() @@ -268,8 +290,8 @@ void CreditsStatus::request( _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input )).done([=](const TLResult &result) { _requestId = 0; - const auto balance = result.data().vbalance().v; - _peer->session().credits().apply(_peer->id, balance); + const auto &balance = result.data().vbalance(); + _peer->session().credits().apply(_peer->id, Data::FromTL(balance)); if (const auto onstack = done) { onstack(StatusFromTL(result, _peer)); } @@ -348,7 +370,9 @@ rpl::producer> PremiumPeerBot( const auto api = lifetime.make_state(&session->mtp()); api->request(MTPcontacts_ResolveUsername( - MTP_string(username) + MTP_flags(0), + MTP_string(username), + MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { session->data().processUsers(result.data().vusers()); session->data().processChats(result.data().vchats()); @@ -380,12 +404,13 @@ rpl::producer CreditsEarnStatistics::request() { )).done([=](const MTPpayments_StarsRevenueStats &result) { const auto &data = result.data(); const auto &status = data.vstatus().data(); + using Data::FromTL; _data = Data::CreditsEarnStatistics{ .revenueGraph = StatisticalGraphFromTL( data.vrevenue_graph()), - .currentBalance = status.vcurrent_balance().v, - .availableBalance = status.vavailable_balance().v, - .overallRevenue = status.voverall_revenue().v, + .currentBalance = FromTL(status.vcurrent_balance()), + .availableBalance = FromTL(status.vavailable_balance()), + .overallRevenue = FromTL(status.voverall_revenue()), .usdRate = data.vusd_rate().v, .isWithdrawalEnabled = status.is_withdrawal_enabled(), .nextWithdrawalAt = status.vnext_withdrawal_at() diff --git a/Telegram/SourceFiles/api/api_messages_search_merged.cpp b/Telegram/SourceFiles/api/api_messages_search_merged.cpp index 8451232f6..1990cfd21 100644 --- a/Telegram/SourceFiles/api/api_messages_search_merged.cpp +++ b/Telegram/SourceFiles/api/api_messages_search_merged.cpp @@ -74,12 +74,17 @@ const FoundMessages &MessagesSearchMerged::messages() const { return _concatedFound; } +const MessagesSearch::Request &MessagesSearchMerged::request() const { + return _request; +} + void MessagesSearchMerged::clear() { _concatedFound = {}; _migratedFirstFound = {}; } void MessagesSearchMerged::search(const Request &search) { + _request = search; if (_migratedSearch) { _waitingForTotal = true; _migratedSearch->searchMessages(search); diff --git a/Telegram/SourceFiles/api/api_messages_search_merged.h b/Telegram/SourceFiles/api/api_messages_search_merged.h index d4c6fbc1c..1e7c0493b 100644 --- a/Telegram/SourceFiles/api/api_messages_search_merged.h +++ b/Telegram/SourceFiles/api/api_messages_search_merged.h @@ -31,6 +31,7 @@ public: void searchMore(); [[nodiscard]] const FoundMessages &messages() const; + [[nodiscard]] const Request &request() const; [[nodiscard]] rpl::producer<> newFounds() const; [[nodiscard]] rpl::producer<> nextFounds() const; @@ -39,6 +40,7 @@ private: void addFound(const FoundMessages &data); MessagesSearch _apiSearch; + Request _request; std::optional _migratedSearch; FoundMessages _migratedFirstFound; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index d88a5ddce..682f34cde 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -601,7 +601,7 @@ auto PremiumGiftCodeOptions::requestStarGifts() _giftsHash = data.vhash().v; const auto &list = data.vgifts().v; const auto session = &_peer->session(); - auto gifts = std::vector(); + auto gifts = std::vector(); gifts.reserve(list.size()); for (const auto &gift : list) { if (auto parsed = FromTL(session, gift)) { @@ -620,7 +620,8 @@ auto PremiumGiftCodeOptions::requestStarGifts() }; } -const std::vector &PremiumGiftCodeOptions::starGifts() const { +auto PremiumGiftCodeOptions::starGifts() const +-> const std::vector & { return _gifts; } @@ -758,31 +759,77 @@ rpl::producer RandomHelloStickerValue( }) | rpl::take(1) | rpl::map(random)); } -std::optional FromTL( +std::optional FromTL( not_null session, const MTPstarGift &gift) { - const auto &data = gift.data(); - const auto document = session->data().processDocument( - data.vsticker()); - const auto remaining = data.vavailability_remains(); - const auto total = data.vavailability_total(); - if (!document->sticker()) { - return {}; - } - return StarGift{ - .id = uint64(data.vid().v), - .stars = int64(data.vstars().v), - .starsConverted = int64(data.vconvert_stars().v), - .document = document, - .limitedLeft = remaining.value_or_empty(), - .limitedCount = total.value_or_empty(), - .firstSaleDate = data.vfirst_sale_date().value_or_empty(), - .lastSaleDate = data.vlast_sale_date().value_or_empty(), - .birthday = data.is_birthday(), - }; + return gift.match([&](const MTPDstarGift &data) { + const auto document = session->data().processDocument( + data.vsticker()); + const auto remaining = data.vavailability_remains(); + const auto total = data.vavailability_total(); + if (!document->sticker()) { + return std::optional(); + } + return std::optional(Data::StarGift{ + .id = uint64(data.vid().v), + .stars = int64(data.vstars().v), + .starsConverted = int64(data.vconvert_stars().v), + .starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()), + .document = document, + .limitedLeft = remaining.value_or_empty(), + .limitedCount = total.value_or_empty(), + .firstSaleDate = data.vfirst_sale_date().value_or_empty(), + .lastSaleDate = data.vlast_sale_date().value_or_empty(), + .upgradable = data.vupgrade_stars().has_value(), + .birthday = data.is_birthday(), + }); + }, [&](const MTPDstarGiftUnique &data) { + const auto total = data.vavailability_total().v; + auto model = std::optional(); + auto pattern = std::optional(); + for (const auto &attribute : data.vattributes().v) { + attribute.match([&](const MTPDstarGiftAttributeModel &data) { + model = FromTL(session, data); + }, [&](const MTPDstarGiftAttributePattern &data) { + pattern = FromTL(session, data); + }, [&](const MTPDstarGiftAttributeBackdrop &data) { + }, [&](const MTPDstarGiftAttributeOriginalDetails &data) { + }); + } + if (!model + || !model->document->sticker() + || !pattern + || !pattern->document->sticker()) { + return std::optional(); + } + auto result = Data::StarGift{ + .id = uint64(data.vid().v), + .unique = std::make_shared(Data::UniqueGift{ + .title = qs(data.vtitle()), + .ownerId = peerFromUser(UserId(data.vowner_id().v)), + .number = data.vnum().v, + .model = *model, + .pattern = *pattern, + }), + .document = model->document, + .limitedLeft = (total - data.vavailability_issued().v), + .limitedCount = total, + }; + const auto unique = result.unique.get(); + for (const auto &attribute : data.vattributes().v) { + attribute.match([&](const MTPDstarGiftAttributeModel &data) { + }, [&](const MTPDstarGiftAttributePattern &data) { + }, [&](const MTPDstarGiftAttributeBackdrop &data) { + unique->backdrop = FromTL(data); + }, [&](const MTPDstarGiftAttributeOriginalDetails &data) { + unique->originalDetails = FromTL(session, data); + }); + } + return std::make_optional(result); + }); } -std::optional FromTL( +std::optional FromTL( not_null to, const MTPuserStarGift &gift) { const auto session = &to->session(); @@ -790,8 +837,11 @@ std::optional FromTL( auto parsed = FromTL(session, data.vgift()); if (!parsed) { return {}; + } else if (const auto unique = parsed->unique.get()) { + unique->starsForTransfer = data.vtransfer_stars().value_or(-1); + unique->exportAt = data.vcan_export_at().value_or_empty(); } - return UserStarGift{ + return Data::UserStarGift{ .info = std::move(*parsed), .message = (data.vmessage() ? TextWithEntities{ @@ -802,15 +852,73 @@ std::optional FromTL( } : TextWithEntities()), .starsConverted = int64(data.vconvert_stars().value_or_empty()), + .starsUpgradedBySender = int64( + data.vupgrade_stars().value_or_empty()), .fromId = (data.vfrom_id() ? peerFromUser(data.vfrom_id()->v) : PeerId()), .messageId = data.vmsg_id().value_or_empty(), .date = data.vdate().v, + .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), .hidden = data.is_unsaved(), .mine = to->isSelf(), }; } +Data::UniqueGiftModel FromTL( + not_null session, + const MTPDstarGiftAttributeModel &data) { + auto result = Data::UniqueGiftModel{ + .document = session->data().processDocument(data.vdocument()), + }; + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + return result; +} + +Data::UniqueGiftPattern FromTL( + not_null session, + const MTPDstarGiftAttributePattern &data) { + auto result = Data::UniqueGiftPattern{ + .document = session->data().processDocument(data.vdocument()), + }; + result.document->overrideEmojiUsesTextColor(true); + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + return result; +} + +Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) { + auto result = Data::UniqueGiftBackdrop(); + result.name = qs(data.vname()); + result.rarityPermille = data.vrarity_permille().v; + result.centerColor = Ui::ColorFromSerialized( + data.vcenter_color()); + result.edgeColor = Ui::ColorFromSerialized( + data.vedge_color()); + result.patternColor = Ui::ColorFromSerialized( + data.vpattern_color()); + result.textColor = Ui::ColorFromSerialized( + data.vtext_color()); + return result; +} + +Data::UniqueGiftOriginalDetails FromTL( + not_null session, + const MTPDstarGiftAttributeOriginalDetails &data) { + auto result = Data::UniqueGiftOriginalDetails(); + result.date = data.vdate().v; + result.senderId = data.vsender_id() + ? peerFromUser( + UserId(data.vsender_id().value_or_empty())) + : PeerId(); + result.recipientId = peerFromUser( + UserId(data.vrecipient_id().v)); + result.message = data.vmessage() + ? ParseTextWithEntities(session, *data.vmessage()) + : TextWithEntities(); + return result; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 23455ba18..4617da755 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "data/data_premium_subscription_option.h" +#include "data/data_star_gift.h" #include "mtproto/sender.h" class History; @@ -73,34 +74,6 @@ struct GiftOptionData { int months = 0; }; -struct StarGift { - uint64 id = 0; - int64 stars = 0; - int64 starsConverted = 0; - not_null document; - int limitedLeft = 0; - int limitedCount = 0; - TimeId firstSaleDate = 0; - TimeId lastSaleDate = 0; - bool birthday = false; - - friend inline bool operator==( - const StarGift &, - const StarGift &) = default; -}; - -struct UserStarGift { - StarGift info; - TextWithEntities message; - int64 starsConverted = 0; - PeerId fromId = 0; - MsgId messageId = 0; - TimeId date = 0; - bool anonymous = false; - bool hidden = false; - bool mine = false; -}; - class Premium final { public: explicit Premium(not_null api); @@ -223,7 +196,7 @@ public: [[nodiscard]] bool giveawayGiftsPurchaseAvailable() const; [[nodiscard]] rpl::producer requestStarGifts(); - [[nodiscard]] const std::vector &starGifts() const; + [[nodiscard]] const std::vector &starGifts() const; private: struct Token final { @@ -253,7 +226,7 @@ private: base::flat_map _stores; int32 _giftsHash = 0; - std::vector _gifts; + std::vector _gifts; MTP::Sender _api; @@ -283,11 +256,23 @@ enum class RequirePremiumState { [[nodiscard]] rpl::producer RandomHelloStickerValue( not_null session); -[[nodiscard]] std::optional FromTL( +[[nodiscard]] std::optional FromTL( not_null session, const MTPstarGift &gift); -[[nodiscard]] std::optional FromTL( +[[nodiscard]] std::optional FromTL( not_null to, const MTPuserStarGift &gift); +[[nodiscard]] Data::UniqueGiftModel FromTL( + not_null session, + const MTPDstarGiftAttributeModel &data); +[[nodiscard]] Data::UniqueGiftPattern FromTL( + not_null session, + const MTPDstarGiftAttributePattern &data); +[[nodiscard]] Data::UniqueGiftBackdrop FromTL( + const MTPDstarGiftAttributeBackdrop &data); +[[nodiscard]] Data::UniqueGiftOriginalDetails FromTL( + not_null session, + const MTPDstarGiftAttributeOriginalDetails &data); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_single_message_search.cpp b/Telegram/SourceFiles/api/api_single_message_search.cpp index 0875091bc..2597c2f05 100644 --- a/Telegram/SourceFiles/api/api_single_message_search.cpp +++ b/Telegram/SourceFiles/api/api_single_message_search.cpp @@ -181,7 +181,9 @@ std::optional SingleMessageSearch::performLookupByUsername( ready(); }; _requestId = _session->api().request(MTPcontacts_ResolveUsername( - MTP_string(username) + MTP_flags(0), + MTP_string(username), + MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { result.match([&](const MTPDcontacts_resolvedPeer &data) { _session->data().processUsers(data.vusers()); diff --git a/Telegram/SourceFiles/api/api_text_entities.cpp b/Telegram/SourceFiles/api/api_text_entities.cpp index 067cc6c0c..cfafeb364 100644 --- a/Telegram/SourceFiles/api/api_text_entities.cpp +++ b/Telegram/SourceFiles/api/api_text_entities.cpp @@ -229,7 +229,7 @@ EntitiesInText EntitiesFromMTP( } MTPVector EntitiesToMTP( - not_null session, + Main::Session *session, const EntitiesInText &entities, ConvertOption option) { auto v = QVector(); @@ -283,6 +283,7 @@ MTPVector EntitiesToMTP( v.push_back(MTP_messageEntityMention(offset, length)); } break; case EntityType::MentionName: { + Assert(session != nullptr); const auto valid = MentionNameEntity( session, offset, @@ -344,4 +345,14 @@ MTPVector EntitiesToMTP( return MTP_vector(std::move(v)); } +TextWithEntities ParseTextWithEntities( + Main::Session *session, + const MTPTextWithEntities &text) { + const auto &data = text.data(); + return { + .text = qs(data.vtext()), + .entities = EntitiesFromMTP(session, data.ventities().v), + }; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_text_entities.h b/Telegram/SourceFiles/api/api_text_entities.h index 95348d616..a2d9adb88 100644 --- a/Telegram/SourceFiles/api/api_text_entities.h +++ b/Telegram/SourceFiles/api/api_text_entities.h @@ -25,8 +25,12 @@ enum class ConvertOption { const QVector &entities); [[nodiscard]] MTPVector EntitiesToMTP( - not_null session, + Main::Session *session, const EntitiesInText &entities, ConvertOption option = ConvertOption::WithLocal); +[[nodiscard]] TextWithEntities ParseTextWithEntities( + Main::Session *session, + const MTPTextWithEntities &text); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index b4775f07b..afcd00979 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1226,7 +1226,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTP_int(d.vttl_period().value_or_empty()), MTPint(), // quick_reply_shortcut_id MTPlong(), // effect - MTPFactCheck()), + MTPFactCheck(), + MTPint()), // report_delivery_until_date MessageFlags(), NewMessageType::Unread); } break; @@ -1263,7 +1264,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTP_int(d.vttl_period().value_or_empty()), MTPint(), // quick_reply_shortcut_id MTPlong(), // effect - MTPFactCheck()), + MTPFactCheck(), + MTPint()), // report_delivery_until_date MessageFlags(), NewMessageType::Unread); } break; diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 86bc8d49b..6149da682 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -756,19 +756,32 @@ rpl::producer WhoReacted( const style::WhoRead &st) { return WhoReacted(item, reaction, context, st, nullptr); } -rpl::producer WhenEdited( + +[[nodiscard]] rpl::producer WhenDate( not_null author, - TimeId date) { + TimeId date, + Ui::WhoReadType type) { return rpl::single(Ui::WhoReadContent{ .participants = { Ui::WhoReadParticipant{ .name = author->name(), .date = FormatReadDate(date, QDateTime::currentDateTime()), .id = author->id.value, } }, - .type = Ui::WhoReadType::Edited, + .type = type, .fullReadCount = 1, }); } +rpl::producer WhenEdited( + not_null author, + TimeId date) { + return WhenDate(author, date, Ui::WhoReadType::Edited); +} + +rpl::producer WhenOriginal( + not_null author, + TimeId date) { + return WhenDate(author, date, Ui::WhoReadType::Original); +} } // namespace Api diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index 0d1cf7234..8e1bc9a3b 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -64,5 +64,8 @@ struct WhoReadList { [[nodiscard]] rpl::producer WhenEdited( not_null author, TimeId date); +[[nodiscard]] rpl::producer WhenOriginal( + not_null author, + TimeId date); } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 22bf7cd0b..7e5e5a0b6 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3263,6 +3263,31 @@ void ApiWrap::sharedMediaDone( } } +mtpRequestId ApiWrap::requestGlobalMedia( + Storage::SharedMediaType type, + const QString &query, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Fn done) { + auto prepared = Api::PrepareGlobalMediaRequest( + _session, + offsetRate, + offsetPosition, + type, + query); + if (!prepared) { + done({}); + return 0; + } + return request( + std::move(*prepared) + ).done([=](const Api::SearchRequestResult &result) { + done(Api::ParseGlobalMediaResult(_session, result)); + }).fail([=] { + done({}); + }).send(); +} + void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled && !action.options.shortcutId @@ -3286,13 +3311,13 @@ void ApiWrap::finishForwarding(const SendAction &action) { const auto topicRootId = action.replyTo.topicRootId; auto toForward = history->resolveForwardDraft(topicRootId); if (!toForward.items.empty()) { - const auto error = GetErrorTextForSending( + const auto error = GetErrorForSending( history->peer, { .topicRootId = topicRootId, .forward = &toForward.items, }); - if (!error.isEmpty()) { + if (error) { return; } diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index f25368293..cfbb71256 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -59,6 +59,7 @@ class Show; namespace Api { struct SearchResult; +struct GlobalMediaResult; class Updates; class Authorizations; @@ -288,6 +289,12 @@ public: Storage::SharedMediaType type, MsgId messageId, SliceType slice); + mtpRequestId requestGlobalMedia( + Storage::SharedMediaType type, + const QString &query, + int32 offsetRate, + Data::MessagePosition offsetPosition, + Fn done); void readFeaturedSetDelayed(uint64 setId); @@ -509,6 +516,10 @@ private: MsgId topicRootId, SharedMediaType type, Api::SearchResult &&parsed); + void globalMediaDone( + SharedMediaType type, + FullMsgId messageId, + Api::GlobalMediaResult &&parsed); void sendSharedContact( const QString &phone, @@ -672,6 +683,17 @@ private: }; base::flat_set _historyRequests; + struct GlobalMediaRequest { + SharedMediaType mediaType = {}; + FullMsgId aroundId; + SliceType sliceType = {}; + + friend inline auto operator<=>( + const GlobalMediaRequest&, + const GlobalMediaRequest&) = default; + }; + base::flat_set _globalMediaRequests; + std::unique_ptr _dialogsLoadState; TimeId _dialogsLoadTill = 0; rpl::variable _dialogsLoadMayBlockByDate = false; diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 0aba9b6a5..020550774 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -262,10 +262,16 @@ void ShowAddParticipantsError( return tr::lng_bot_already_in_group(tr::now); } else if (error == u"BOT_GROUPS_BLOCKED"_q) { return tr::lng_error_cant_add_bot(tr::now); - } else if (error == u"ADMINS_TOO_MUCH"_q) { - return ((chat->isChat() || chat->isMegagroup()) - ? tr::lng_error_admin_limit - : tr::lng_error_admin_limit_channel)(tr::now); + } else if (error == u"YOU_BLOCKED_USER"_q) { + return tr::lng_error_you_blocked_user(tr::now); + } else if (error == u"CHAT_ADMIN_INVITE_REQUIRED"_q) { + return tr::lng_error_add_admin_not_member(tr::now); + } else if (error == u"USER_ADMIN_INVALID"_q) { + return tr::lng_error_user_admin_invalid(tr::now); + } else if (error == u"BOTS_TOO_MUCH"_q) { + return (chat->isChannel() + ? tr::lng_error_channel_bots_too_much + : tr::lng_error_group_bots_too_much)(tr::now); } return tr::lng_failed_add_participant(tr::now); }(); diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 252403af8..685ef547f 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -8,22 +8,108 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/choose_filter_box.h" #include "apiwrap.h" +#include "boxes/filters/edit_filter_box.h" #include "boxes/premium_limits_box.h" #include "core/application.h" // primaryWindow +#include "core/ui_integration.h" #include "data/data_chat_filters.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/empty_userpic.h" +#include "ui/filter_icons.h" +#include "ui/painter.h" +#include "ui/rect.h" #include "ui/text/text_utilities.h" // Ui::Text::Bold +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/menu/menu_action.h" #include "ui/widgets/popup_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" +#include "styles/style_dialogs.h" #include "styles/style_media_player.h" // mediaPlayerMenuCheck +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" namespace { +[[nodiscard]] QImage Icon(const Data::ChatFilter &f) { + constexpr auto kScale = 0.75; + const auto icon = Ui::LookupFilterIcon(Ui::ComputeFilterIcon(f)).normal; + const auto originalWidth = icon->width(); + const auto originalHeight = icon->height(); + + const auto scaledWidth = int(originalWidth * kScale); + const auto scaledHeight = int(originalHeight * kScale); + + auto image = QImage( + scaledWidth * style::DevicePixelRatio(), + scaledHeight * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + + const auto x = int((scaledWidth - originalWidth * kScale) / 2); + const auto y = int((scaledHeight - originalHeight * kScale) / 2); + + p.scale(kScale, kScale); + icon->paint(p, x, y, scaledWidth, st::dialogsUnreadBgMuted->c); + if (const auto color = f.colorIndex()) { + p.resetTransform(); + const auto circleSize = scaledWidth / 3.; + const auto r = QRectF( + x + scaledWidth - circleSize, + y + scaledHeight - circleSize - circleSize / 3., + circleSize, + circleSize); + p.setPen(Qt::NoPen); + p.setCompositionMode(QPainter::CompositionMode_Clear); + p.setBrush(Qt::transparent); + p.drawEllipse(r + Margins(st::lineWidth * 1.5)); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setBrush(Ui::EmptyUserpic::UserpicColor(*color).color2); + p.drawEllipse(r); + } + } + + return image; +} + +class FilterAction : public Ui::Menu::Action { +public: + using Ui::Menu::Action::Action; + + void setIcon(QImage &&image) { + _icon = std::move(image); + } + +protected: + void paintEvent(QPaintEvent *event) override { + Ui::Menu::Action::paintEvent(event); + if (!_icon.isNull()) { + const auto size = _icon.size() / style::DevicePixelRatio(); + auto p = QPainter(this); + p.drawImage( + width() + - size.width() + - st::menuWithIcons.itemPadding.right(), + (height() - size.height()) / 2, + _icon); + } + } + +private: + QImage _icon; + +}; + Data::ChatFilter ChangedFilter( const Data::ChatFilter &filter, not_null history, @@ -85,15 +171,26 @@ void ChangeFilterById( )).done([=, chat = history->peer->name(), name = filter.title()] { const auto account = not_null(&history->session().account()); if (const auto controller = Core::App().windowFor(account)) { - controller->showToast((add - ? tr::lng_filters_toast_add - : tr::lng_filters_toast_remove)( - tr::now, - lt_chat, - Ui::Text::Bold(chat), - lt_folder, - Ui::Text::Bold(name), - Ui::Text::WithEntities)); + const auto isStatic = name.isStatic; + const auto textContext = [=](not_null widget) { + return Core::MarkedTextContext{ + .session = &history->session(), + .customEmojiRepaint = [=] { widget->update(); }, + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; + controller->showToast({ + .text = (add + ? tr::lng_filters_toast_add + : tr::lng_filters_toast_remove)( + tr::now, + lt_chat, + Ui::Text::Bold(chat), + lt_folder, + Ui::Text::Wrapped(name.text, EntityType::Bold), + Ui::Text::WithEntities), + .textContext = textContext, + }); } }).fail([=](const MTP::Error &error) { LOG(("API Error: failed to %1 a dialog to a folder. %2") @@ -126,9 +223,7 @@ bool ChooseFilterValidator::canRemove(FilterId filterId) const { const auto list = _history->owner().chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); if (i != end(list)) { - const auto &filter = *i; - return filter.contains(_history) - && ((filter.always().size() > 1) || filter.flags()); + return Data::CanRemoveFromChatFilter(*i, _history); } return false; } @@ -164,14 +259,15 @@ void FillChooseFilterMenu( not_null history) { const auto weak = base::make_weak(controller); const auto validator = ChooseFilterValidator(history); - for (const auto &filter : history->owner().chatsFilters().list()) { + const auto &list = history->owner().chatsFilters().list(); + const auto showColors = history->owner().chatsFilters().tagsEnabled(); + for (const auto &filter : list) { const auto id = filter.id(); if (!id) { continue; } - const auto contains = filter.contains(history); - const auto action = menu->addAction(filter.title(), [=] { + auto callback = [=] { const auto toAdd = !filter.contains(history); const auto r = validator.limitReached(id, toAdd); if (r.reached) { @@ -188,11 +284,63 @@ void FillChooseFilterMenu( validator.remove(id); } } - }, contains ? &st::mediaPlayerMenuCheck : nullptr); + }; + + const auto contains = filter.contains(history); + const auto title = filter.title(); + auto item = base::make_unique_q( + menu.get(), + st::foldersMenu, + Ui::Menu::CreateAction( + menu.get(), + Ui::Text::FixAmpersandInAction(title.text.text), + std::move(callback)), + contains ? &st::mediaPlayerMenuCheck : nullptr, + contains ? &st::mediaPlayerMenuCheck : nullptr); + const auto context = Core::MarkedTextContext{ + .session = &history->session(), + .customEmojiRepaint = [raw = item.get()] { raw->update(); }, + .customEmojiLoopLimit = title.isStatic ? -1 : 0, + }; + item->setMarkedText(title.text, QString(), context); + + item->setIcon(Icon(showColors ? filter : filter.withColorIndex({}))); + const auto action = menu->addAction(std::move(item)); action->setEnabled(contains ? validator.canRemove(id) : validator.canAdd()); } + + const auto limit = [session = &controller->session()] { + return Data::PremiumLimits(session).dialogFiltersCurrent(); + }; + if ((list.size() - 1) < limit()) { + menu->addAction(tr::lng_filters_create(tr::now), [=] { + const auto strong = weak.get(); + if (!strong) { + return; + } + const auto session = &strong->session(); + const auto count = session->data().chatsFilters().list().size(); + if ((count - 1) >= limit()) { + return; + } + auto filter = + Data::ChatFilter({}, {}, {}, {}, {}, { history }, {}, {}); + const auto send = [=](const Data::ChatFilter &filter) { + session->api().request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), + MTP_int(count), + filter.tl() + )).done([=] { + session->data().chatsFilters().reload(); + }).send(); + }; + strong->uiShow()->show( + Box(EditFilterBox, strong, std::move(filter), send, nullptr)); + }, &st::menuIconShowInFolder); + } + history->owner().chatsFilters().changed( ) | rpl::start_with_next([=] { menu->hideMenu(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index f6e48b221..ae2cbc3d7 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -254,7 +254,7 @@ EditCaptionBox::EditCaptionBox( , _initialList(std::move(list)) , _saved(std::move(saved)) { Expects(!_initialList.files.empty()); - Expects(!item->media() || item->media()->allowsEditCaption()); + Expects(item->allowsEditMedia()); _mediaEditManager.start(item, spoilered, invertCaption); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index df85e6254..f9a05a557 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -7,22 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/filters/edit_filter_box.h" +#include "apiwrap.h" +#include "base/event_filter.h" #include "boxes/filters/edit_filter_chats_list.h" #include "boxes/filters/edit_filter_chats_preview.h" #include "boxes/filters/edit_filter_links.h" #include "boxes/premium_limits_box.h" +#include "boxes/premium_preview_box.h" #include "chat_helpers/emoji_suggestions_widget.h" -#include "ui/layers/generic_box.h" -#include "ui/text/text_utilities.h" -#include "ui/text/text_options.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/fields/input_field.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/effects/panel_animation.h" -#include "ui/filter_icons.h" -#include "ui/filter_icon_panel.h" -#include "ui/painter.h" -#include "ui/vertical_list.h" +#include "chat_helpers/message_field.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_peer.h" @@ -30,22 +26,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_user.h" -#include "core/application.h" -#include "core/core_settings.h" -#include "settings/settings_common.h" -#include "base/event_filter.h" -#include "lang/lang_keys.h" #include "history/history.h" +#include "info/userpic/info_userpic_color_circle_button.h" +#include "lang/lang_keys.h" #include "main/main_session.h" -#include "window/window_session_controller.h" +#include "settings/settings_common.h" +#include "ui/chat/chats_filter_tag.h" +#include "ui/effects/animation_value_f.h" +#include "ui/effects/animations.h" +#include "ui/effects/panel_animation.h" +#include "ui/empty_userpic.h" +#include "ui/filter_icon_panel.h" +#include "ui/filter_icons.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_controller.h" -#include "apiwrap.h" +#include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_boxes.h" +#include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_window.h" #include "styles/style_chat.h" -#include "styles/style_menu_icons.h" +#include "styles/style_info_userpic_builder.h" namespace { @@ -336,6 +344,8 @@ void EditFilterBox( const Data::ChatFilter &data, Fn next)> saveAnd) { using namespace rpl::mappers; + constexpr auto kColorsCount = 8; + constexpr auto kNoTag = kColorsCount - 1; struct State { rpl::variable rules; @@ -343,13 +353,19 @@ void EditFilterBox( rpl::variable hasLinks; rpl::variable chatlist; rpl::variable creating; + rpl::variable title; + rpl::variable staticTitle; + rpl::variable colorIndex; }; const auto owner = &window->session().data(); const auto state = box->lifetime().make_state(State{ .rules = filter, .chatlist = filter.chatlist(), - .creating = filter.title().isEmpty(), + .creating = filter.title().empty(), + .title = filter.titleText(), + .staticTitle = filter.staticTitle(), }); + state->colorIndex = filter.colorIndex().value_or(kNoTag); state->links = owner->chatsFilters().chatlistLinks(filter.id()), state->hasLinks = state->links.value() | rpl::map([=](const auto &v) { return !v.empty(); @@ -385,32 +401,70 @@ void EditFilterBox( tr::lng_filters_edit())); box->setCloseByOutsideClick(false); + const auto session = &window->session(); Data::AmPremiumValue( - &window->session() + session ) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); const auto content = box->verticalLayout(); + const auto current = state->title.current(); const auto name = content->add( object_ptr( box, st::windowFilterNameInput, - tr::lng_filters_new_name(), - filter.title()), + Ui::InputField::Mode::SingleLine, + tr::lng_filters_new_name()), st::markdownLinkFieldPadding); + InitMessageFieldHandlers(window, name, ChatHelpers::PauseReason::Layer); + name->setTextWithTags({ + current.text, + TextUtilities::ConvertEntitiesToTextTags(current.entities), + }, Ui::InputField::HistoryAction::Clear); name->setMaxLength(kMaxFilterTitleLength); - name->setInstantReplaces(Ui::InstantReplaces::Default()); - name->setInstantReplacesEnabled( - Core::App().settings().replaceEmojiValue()); - Ui::Emoji::SuggestionsController::Init( - box->getDelegate()->outerContainer(), - name, - &window->session()); const auto nameEditing = box->lifetime().make_state( NameEditing{ name }); + const auto staticTitle = Ui::CreateChild( + name, + QString()); + staticTitle->setClickedCallback([=] { + state->staticTitle = !state->staticTitle.current(); + }); + state->staticTitle.value() | rpl::start_with_next([=](bool value) { + staticTitle->setText(value + ? tr::lng_filters_enable_animations(tr::now) + : tr::lng_filters_disable_animations(tr::now)); + const auto paused = [=] { + using namespace Window; + return window->isGifPausedAtLeastFor(GifPauseReason::Layer); + }; + name->setCustomTextContext([=](Fn repaint) { + return std::any(Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = std::move(repaint), + .customEmojiLoopLimit = value ? -1 : 0, + }); + }, [paused] { + return On(PowerSaving::kEmojiChat) || paused(); + }, [paused] { + return On(PowerSaving::kChatSpoiler) || paused(); + }); + name->update(); + }, staticTitle->lifetime()); + + rpl::combine( + staticTitle->widthValue(), + name->widthValue() + ) | rpl::start_with_next([=](int inner, int outer) { + staticTitle->moveToRight( + st::windowFilterStaticTitlePosition.x(), + st::windowFilterStaticTitlePosition.y(), + outer); + }, staticTitle->lifetime()); + state->creating.value( ) | rpl::filter(!_1) | rpl::start_with_next([=] { nameEditing->custom = true; @@ -421,7 +475,13 @@ void EditFilterBox( if (!nameEditing->settingDefault) { nameEditing->custom = true; } + auto entered = name->getTextWithTags(); + state->title = TextWithEntities{ + std::move(entered.text), + TextUtilities::ConvertTextTagsToEntities(entered.tags), + }; }, name->lifetime()); + const auto updateDefaultTitle = [=](const Data::ChatFilter &filter) { if (nameEditing->custom) { return; @@ -434,6 +494,11 @@ void EditFilterBox( } }; + state->title.value( + ) | rpl::start_with_next([=](const TextWithEntities &value) { + staticTitle->setVisible(!value.entities.isEmpty()); + }, staticTitle->lifetime()); + const auto outer = box->getDelegate()->outerContainer(); CreateIconSelector( outer, @@ -504,10 +569,184 @@ void EditFilterBox( Ui::AddDividerText(excludeInner, tr::lng_filters_exclude_about()); Ui::AddSkip(excludeInner); + { + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + const auto colors = wrap->entity(); + const auto session = &window->session(); + + wrap->toggleOn( + rpl::combine( + session->premiumPossibleValue(), + session->data().chatsFilters().tagsEnabledValue(), + Data::AmPremiumValue(session) + ) | rpl::map([=] (bool possible, bool tagsEnabled, bool premium) { + return possible && (tagsEnabled || !premium); + }), + anim::type::instant); + + const auto isPremium = session->premium(); + const auto title = Ui::AddSubsectionTitle( + colors, + tr::lng_filters_tag_color_subtitle()); + const auto preview = Ui::CreateChild(colors); + title->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + const auto h = st::normalFont->height; + preview->setGeometry( + colors->x(), + r.y() + (r.height() - h) / 2 + st::lineWidth, + colors->width(), + h); + }, preview->lifetime()); + + struct TagState { + Ui::Animations::Simple animation; + Ui::ChatsFilterTagContext context; + QImage frame; + float64 alpha = 1.; + }; + const auto tag = preview->lifetime().make_state(); + tag->context.textContext = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }; + preview->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(preview); + p.setOpacity(tag->alpha); + const auto size = tag->frame.size() / style::DevicePixelRatio(); + const auto rect = QRect( + preview->width() - size.width() - st::boxRowPadding.right(), + (st::normalFont->height - size.height()) / 2, + size.width(), + size.height()); + p.drawImage(rect.topLeft(), tag->frame); + if (p.opacity() < 1) { + p.setOpacity(1. - p.opacity()); + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText( + preview->rect() - st::boxRowPadding, + tr::lng_filters_tag_color_no(tr::now), + style::al_right); + } + }, preview->lifetime()); + + const auto side = st::userpicBuilderEmojiAccentColorSize; + const auto line = colors->add( + Ui::CreateSkipWidget(colors, side), + st::boxRowPadding); + auto buttons = std::vector>(); + const auto palette = [](int i) { + return Ui::EmptyUserpic::UserpicColor(i).color2; + }; + const auto upperTitle = [=] { + auto value = state->title.current(); + value.text = value.text.toUpper(); + return value; + }; + state->title.changes( + ) | rpl::start_with_next([=] { + tag->context.color = palette(state->colorIndex.current())->c; + tag->frame = Ui::ChatsFilterTag( + upperTitle(), + tag->context); + preview->update(); + }, preview->lifetime()); + for (auto i = 0; i < kColorsCount; ++i) { + const auto button = Ui::CreateChild( + line); + button->resize(side, side); + const auto progress = isPremium + ? (state->colorIndex.current() == i) + : (i == kNoTag); + button->setSelectedProgress(progress); + const auto color = palette(i); + button->setBrush(color); + if (progress == 1) { + tag->context.color = color->c; + tag->frame = Ui::ChatsFilterTag( + upperTitle(), + tag->context); + if (i == kNoTag) { + tag->alpha = 0.; + } + } + buttons.push_back(button); + } + for (auto i = 0; i < kColorsCount; ++i) { + const auto &button = buttons[i]; + button->setClickedCallback([=] { + const auto was = state->colorIndex.current(); + const auto now = i; + if (was != now) { + const auto c1 = palette(was); + const auto c2 = palette(now); + const auto a1 = (was == kNoTag) ? 0. : 1.; + const auto a2 = (now == kNoTag) ? 0. : 1.; + tag->animation.stop(); + tag->animation.start([=](float64 progress) { + if (was >= 0) { + buttons[was]->setSelectedProgress(1. - progress); + } + buttons[now]->setSelectedProgress(progress); + tag->context.color = anim::color(c1, c2, progress); + tag->frame = Ui::ChatsFilterTag( + upperTitle(), + tag->context); + tag->alpha = anim::interpolateF(a1, a2, progress); + preview->update(); + }, 0., 1., st::universalDuration); + } + state->colorIndex = now; + }); + if (!session->premium()) { + button->setClickedCallback([w = window] { + ShowPremiumPreviewToBuy(w, PremiumFeature::FilterTags); + }); + } + } + line->sizeValue() | rpl::start_with_next([=](const QSize &size) { + const auto totalWidth = buttons.size() * side; + const auto spacing = (size.width() - totalWidth) + / (buttons.size() - 1); + for (auto i = 0; i < kColorsCount; ++i) { + const auto &button = buttons[i]; + button->moveToLeft(i * (side + spacing), 0); + } + }, line->lifetime()); + + { + const auto last = buttons.back(); + const auto icon = Ui::CreateChild(last); + icon->resize(side, side); + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + (session->premium() + ? st::windowFilterSmallRemove.icon + : st::historySendDisabledIcon).paintInCenter( + p, + QRectF(icon->rect()), + st::historyPeerUserpicFg->c); + }, icon->lifetime()); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + last->setBrush(st::historyPeerArchiveUserpicBg); + } + + Ui::AddSkip(colors); + Ui::AddSkip(colors); + Ui::AddDividerText(colors, tr::lng_filters_tag_color_about()); + Ui::AddSkip(colors); + } + const auto collect = [=]() -> std::optional { - const auto title = name->getLastText().trimmed(); + auto title = state->title.current(); + const auto staticTitle = !title.entities.isEmpty() + && state->staticTitle.current(); const auto rules = data->current(); - if (title.isEmpty()) { + if (title.empty()) { name->showError(); box->scrollToY(0); return {}; @@ -520,7 +759,13 @@ void EditFilterBox( window->window().showToast(tr::lng_filters_default(tr::now)); return {}; } - return rules.withTitle(title); + const auto rawColorIndex = state->colorIndex.current(); + const auto colorIndex = (rawColorIndex >= kNoTag + ? std::nullopt + : std::make_optional(rawColorIndex)); + return rules.withTitle( + { std::move(title), staticTitle } + ).withColorIndex(colorIndex); }; Ui::AddSubsectionTitle( diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h index 695dbbe92..cfff92907 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h @@ -7,12 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "ui/layers/generic_box.h" - namespace Window { class SessionController; } // namespace Window +namespace Ui { +class GenericBox; +} // namespace Ui + namespace Data { class ChatFilter; } // namespace Data diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index ce1418618..2ed72aa25 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/filters/edit_filter_chats_list.h" +#include "core/ui_integration.h" #include "data/data_chat_filters.h" #include "data/data_premium_limits.h" #include "data/data_session.h" @@ -63,13 +64,27 @@ private: class ExceptionRow final : public ChatsListBoxController::Row { public: - explicit ExceptionRow(not_null history); + ExceptionRow( + not_null history, + not_null delegate); QString generateName() override; QString generateShortName() override; PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; + void paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) override; + +private: + Ui::Text::String _filtersText; + }; class TypeController final : public PeerListController { @@ -126,15 +141,32 @@ Flag TypeRow::flag() const { return static_cast(id() & 0xFFFF); } -ExceptionRow::ExceptionRow(not_null history) : Row(history) { - auto filters = QStringList(); +ExceptionRow::ExceptionRow( + not_null history, + not_null delegate) +: Row(history) { + auto filters = TextWithEntities(); for (const auto &filter : history->owner().chatsFilters().list()) { if (filter.contains(history) && filter.id()) { - filters << filter.title(); + if (!filters.empty()) { + filters.append(u", "_q); + } + auto title = filter.title(); + filters.append(title.isStatic + ? Data::ForceCustomEmojiStatic(std::move(title.text)) + : std::move(title.text)); } } - if (!filters.isEmpty()) { - setCustomStatus(filters.join(", ")); + if (!filters.empty()) { + const auto repaint = [=] { delegate->peerListUpdateRow(this); }; + _filtersText.setMarkedText( + st::defaultTextStyle, + filters, + kMarkupTextOptions, + Core::MarkedTextContext{ + .session = &history->session(), + .customEmojiRepaint = repaint, + }); } else if (peer()->isSelf()) { setCustomStatus(tr::lng_saved_forward_here(tr::now)); } @@ -176,6 +208,37 @@ PaintRoundImageCallback ExceptionRow::generatePaintUserpicCallback( }; } +void ExceptionRow::paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) { + if (_filtersText.isEmpty()) { + Row::paintStatusText( + p, + st, + x, + y, + availableWidth, + outerWidth, + selected); + } else { + p.setPen(selected ? st.statusFgOver : st.statusFg); + _filtersText.draw(p, { + .position = { x, y }, + .outerWidth = outerWidth, + .availableWidth = availableWidth, + .palette = &st::defaultTextPalette, + .now = crl::now(), + .pausedEmoji = false, + .elisionLines = 1, + }); + } +} + TypeController::TypeController( not_null session, Flags options, @@ -418,7 +481,7 @@ void EditFilterChatsListController::prepareViewHook() { const auto rows = std::make_unique[]>(count); auto i = 0; for (const auto &history : _peers) { - rows[i++].emplace(history); + rows[i++].emplace(history, delegate()); } auto pointers = std::vector(); pointers.reserve(count); @@ -499,7 +562,7 @@ auto EditFilterChatsListController::createRow(not_null history) return nullptr; } return history->inChatList() - ? std::make_unique(history) + ? std::make_unique(history, delegate()) : nullptr; } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 591bdf514..61bbd85a0 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_link.h" // InviteLinkQrBox. #include "boxes/peer_list_box.h" #include "boxes/premium_limits_box.h" +#include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_chat_filters.h" @@ -482,7 +483,7 @@ private: const not_null _window; InviteLinkData _data; - QString _filterTitle; + Data::ChatFilterTitle _filterTitle; base::flat_set> _filterChats; base::flat_map, QString> _denied; rpl::variable>> _selected; @@ -535,6 +536,14 @@ void LinkController::addHeader(not_null container) { }, verticalLayout->lifetime()); verticalLayout->add(std::move(icon.widget)); + const auto isStatic = _filterTitle.isStatic; + const auto makeContext = [=](Fn update) { + return Core::MarkedTextContext{ + .session = &_window->session(), + .customEmojiRepaint = update, + .customEmojiLoopLimit = isStatic ? -1 : 0, + }; + }; verticalLayout->add( object_ptr>( verticalLayout, @@ -544,9 +553,13 @@ void LinkController::addHeader(not_null container) { ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) : tr::lng_filters_link_share_about( lt_folder, - rpl::single(Ui::Text::Bold(_filterTitle)), + rpl::single(Ui::Text::Wrapped( + _filterTitle.text, + EntityType::Bold)), Ui::Text::WithEntities)), - st::settingsFilterDividerLabel)), + st::settingsFilterDividerLabel, + st::defaultPopupMenu, + makeContext)), st::filterLinkDividerLabelPadding); verticalLayout->geometryValue( diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 896ecea51..80e859eb1 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -47,6 +47,7 @@ void GiftCreditsBox( const auto content = box->setPinnedToTopContent( object_ptr(box)); + Ui::AddSkip(content); Ui::AddSkip(content); Ui::AddSkip(content); const auto &stUser = st::premiumGiftsUserpicButton; @@ -58,39 +59,19 @@ void GiftCreditsBox( Ui::AddSkip(content); Ui::AddSkip(content); - { - const auto widget = Ui::CreateChild(content); - using ColoredMiniStars = Ui::Premium::ColoredMiniStars; - const auto stars = widget->lifetime().make_state( - widget, - false, - Ui::Premium::MiniStars::Type::BiStars); - stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); - widget->resize( - st::boxWidth - stUser.photoSize, - stUser.photoSize * 2); - content->sizeValue( - ) | rpl::start_with_next([=](const QSize &size) { - widget->moveToLeft(stUser.photoSize / 2, 0); - const auto starsRect = Rect(widget->size()); - stars->setPosition(starsRect.topLeft()); - stars->setSize(starsRect.size()); - widget->lower(); - }, widget->lifetime()); - widget->paintRequest( - ) | rpl::start_with_next([=](const QRect &r) { - auto p = QPainter(widget); - p.fillRect(r, Qt::transparent); - stars->paint(p); - }, widget->lifetime()); - } + Settings::AddMiniStars( + content, + Ui::CreateChild(content), + stUser.photoSize, + box->width(), + 2.); { Ui::AddSkip(content); const auto arrow = Ui::Text::SingleCustomEmoji( peer->owner().customEmojiManager().registerInternalEmoji( st::topicButtonArrow, st::channelEarnLearnArrowMargins, - false)); + true)); auto link = tr::lng_credits_box_history_entry_gift_about_link( lt_emoji, rpl::single(arrow), @@ -122,7 +103,7 @@ void GiftCreditsBox( Main::MakeSessionShow(box->uiShow(), &peer->session()), box->verticalLayout(), peer, - 0, + StarsAmount(), [=] { gifted(); box->uiShow()->hideLayer(); }, 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 542189659..2b396aec4 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/timer_rpl.h" #include "base/unixtime.h" #include "base/weak_ptr.h" #include "boxes/peer_list_controllers.h" // ContactsBoxController. @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/replace_boost_box.h" // BoostsForGift. #include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox. #include "boxes/star_gift_box.h" // ShowStarGiftBox. +#include "boxes/transfer_gift_box.h" // ShowTransferGiftBox. #include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -44,12 +46,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/label_with_custom_emoji.h" +#include "ui/widgets/tooltip.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/table_layout.h" @@ -65,6 +69,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); + [[nodiscard]] QString CreateMessageLink( not_null session, PeerId peerId, @@ -125,7 +131,8 @@ namespace { not_null parent, not_null controller, PeerId id, - bool withSendGiftButton = false) { + rpl::producer button = nullptr, + Fn handler = nullptr) { auto result = object_ptr(parent); const auto raw = result.data(); @@ -136,19 +143,17 @@ namespace { const auto userpic = Ui::CreateChild(raw, peer, st); const auto label = Ui::CreateChild( raw, - withSendGiftButton ? peer->shortName() : peer->name(), + (button && handler) ? peer->shortName() : peer->name(), st::giveawayGiftCodeValue); - const auto send = withSendGiftButton + const auto send = (button && handler) ? Ui::CreateChild( raw, - tr::lng_gift_send_small(), + std::move(button), st::starGiftSmallButton) : nullptr; if (send) { send->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - send->setClickedCallback([=] { - Ui::ShowStarGiftBox(controller->parentController(), peer); - }); + send->setClickedCallback(std::move(handler)); } rpl::combine( raw->widthValue(), @@ -237,7 +242,58 @@ void AddTableRow( valueMargins); } -object_ptr MakeStarGiftStarsValue( +[[nodiscard]] object_ptr MakeAttributeValue( + not_null parent, + const Data::UniqueGiftAttribute &attribute, + Fn, int)> showTooltip) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + attribute.name, + st::giveawayGiftCodeValue); + const auto permille = attribute.rarityPermille; + + const auto text = QString::number(permille / 10.) + '%'; + const auto rarity = Ui::CreateChild( + raw, + rpl::single(text), + st::starGiftSmallButton); + 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, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + }, label->lifetime()); + + label->heightValue() | rpl::start_with_next([=](int height) { + raw->resize( + raw->width(), + height + st::giveawayGiftCodeValueMargin.bottom()); + }, raw->lifetime()); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + rarity->setClickedCallback([=] { + showTooltip(rarity, permille); + }); + + return result; +} + +[[nodiscard]] object_ptr MakeStarGiftStarsValue( not_null parent, not_null controller, const Data::CreditsHistoryEntry &entry, @@ -255,8 +311,8 @@ object_ptr MakeStarGiftStarsValue( auto star = session->data().customEmojiManager().creditsEmoji(); const auto label = Ui::CreateChild( raw, - rpl::single( - star.append(' ' + Lang::FormatCountDecimal(entry.credits))), + rpl::single(star.append( + ' ' + Lang::FormatStarsAmountDecimal(entry.credits))), st::giveawayGiftCodeValue, st::defaultPopupMenu, std::move(makeContext)); @@ -302,6 +358,107 @@ object_ptr MakeStarGiftStarsValue( return result; } +[[nodiscard]] object_ptr MakeVisibilityTableValue( + not_null parent, + not_null controller, + bool savedToProfile, + Fn toggleVisibility) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + (savedToProfile + ? tr::lng_gift_visibility_shown() + : tr::lng_gift_visibility_hidden()), + st::giveawayGiftCodeValue, + st::defaultPopupMenu); + + const auto toggle = Ui::CreateChild( + raw, + (savedToProfile + ? tr::lng_gift_visibility_hide() + : tr::lng_gift_visibility_show()), + st::starGiftSmallButton); + toggle->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + toggle->setClickedCallback([=] { + toggleVisibility(!savedToProfile); + }); + + rpl::combine( + raw->widthValue(), + toggle->widthValue() + ) | 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); + toggle->moveToLeft( + label->width() + st::normalFont->spacew, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + }, label->lifetime()); + + label->heightValue() | rpl::start_with_next([=](int height) { + raw->resize( + raw->width(), + height + st::giveawayGiftCodeValueMargin.bottom()); + }, raw->lifetime()); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + return result; +} + +[[nodiscard]] object_ptr MakeNonUniqueStatusTableValue( + not_null parent, + not_null controller, + Fn startUpgrade) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + tr::lng_gift_unique_status_non(), + st::giveawayGiftCodeValue, + st::defaultPopupMenu); + + const auto upgrade = Ui::CreateChild( + raw, + tr::lng_gift_unique_status_upgrade(), + st::starGiftSmallButton); + upgrade->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + upgrade->setClickedCallback(startUpgrade); + + rpl::combine( + raw->widthValue(), + upgrade->widthValue() + ) | 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); + upgrade->moveToLeft( + label->width() + st::normalFont->spacew, + (st::giveawayGiftCodeValue.style.font->ascent + - st::starGiftSmallButton.style.font->ascent), + width); + }, label->lifetime()); + + label->heightValue() | rpl::start_with_next([=](int height) { + raw->resize( + raw->width(), + height + st::giveawayGiftCodeValueMargin.bottom()); + }, raw->lifetime()); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + + return result; +} + not_null AddTableRow( not_null table, rpl::producer label, @@ -1035,7 +1192,9 @@ void AddStarGiftTable( not_null controller, not_null container, const Data::CreditsHistoryEntry &entry, - Fn convertToStars) { + Fn toggleVisibility, + Fn convertToStars, + Fn startUpgrade) { auto table = container->add( object_ptr( container, @@ -1043,14 +1202,41 @@ void AddStarGiftTable( st::giveawayGiftCodeTableMargin); const auto peerId = PeerId(entry.barePeerId); const auto session = &controller->session(); - if (peerId) { - const auto user = session->data().peer(peerId)->asUser(); - const auto withSendButton = entry.in && user && !user->isBot(); + const auto unique = entry.uniqueGift.get(); + const auto selfBareId = session->userPeerId().value; + const auto giftToSelf = (peerId == session->userPeerId()) + && (entry.in || entry.bareGiftOwnerId == selfBareId); + if (unique) { + const auto ownerId = PeerId(entry.bareGiftOwnerId); + const auto transfer = entry.in + && entry.bareMsgId + && (unique->starsForTransfer >= 0); + auto send = transfer ? tr::lng_gift_unique_owner_change() : nullptr; + auto handler = transfer ? Fn([=] { + ShowTransferGiftBox( + controller->parentController(), + entry.uniqueGift, + MsgId(entry.bareMsgId)); + }) : nullptr; AddTableRow( table, - tr::lng_credits_box_history_entry_peer_in(), - MakePeerTableValue(table, controller, peerId, withSendButton), + tr::lng_gift_unique_owner(), + MakePeerTableValue(table, controller, ownerId, send, handler), st::giveawayGiftCodePeerMargin); + } else if (peerId) { + if (!giftToSelf) { + const auto user = session->data().peer(peerId)->asUser(); + const auto withSendButton = entry.in && user && !user->isBot(); + auto send = withSendButton ? tr::lng_gift_send_small() : nullptr; + auto handler = send ? Fn([=] { + Ui::ShowStarGiftBox(controller->parentController(), user); + }) : nullptr; + AddTableRow( + table, + tr::lng_credits_box_history_entry_peer_in(), + MakePeerTableValue(table, controller, peerId, send, handler), + st::giveawayGiftCodePeerMargin); + } } else if (!entry.soldOutInfo) { AddTableRow( table, @@ -1058,23 +1244,109 @@ void AddStarGiftTable( MakeHiddenPeerTableValue(table, controller), st::giveawayGiftCodePeerMargin); } - if (!entry.firstSaleDate.isNull()) { + if (!unique && !entry.firstSaleDate.isNull()) { AddTableRow( table, tr::lng_gift_link_label_first_sale(), rpl::single(Ui::Text::WithEntities( langDateTime(entry.firstSaleDate)))); } - if (!entry.lastSaleDate.isNull()) { + if (!unique && !entry.lastSaleDate.isNull()) { AddTableRow( table, tr::lng_gift_link_label_last_sale(), rpl::single(Ui::Text::WithEntities( langDateTime(entry.lastSaleDate)))); } - { - const auto margin = st::giveawayGiftCodeValueMargin - - QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom()); + if (!unique && !entry.date.isNull()) { + AddTableRow( + table, + 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 raw = std::make_shared(nullptr); + const auto showTooltip = [=]( + not_null widget, + int rarity) { + if (*raw) { + (*raw)->toggleAnimated(false); + } + const auto text = QString::number(rarity / 10.) + '%'; + const auto tooltip = Ui::CreateChild( + container, + Ui::MakeNiceTooltipLabel( + container, + tr::lng_gift_unique_rarity( + lt_percent, + rpl::single(TextWithEntities{ text }), + Ui::Text::WithEntities), + 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()); + }; + + AddTableRow( + table, + tr::lng_gift_unique_model(), + MakeAttributeValue(container, unique->model, showTooltip), + marginWithButton); + AddTableRow( + table, + tr::lng_gift_unique_backdrop(), + MakeAttributeValue(container, unique->backdrop, showTooltip), + marginWithButton); + AddTableRow( + table, + tr::lng_gift_unique_symbol(), + MakeAttributeValue(container, unique->pattern, showTooltip), + marginWithButton); + } else { AddTableRow( table, tr::lng_gift_link_label_value(), @@ -1083,34 +1355,124 @@ void AddStarGiftTable( controller, entry, std::move(convertToStars)), - margin); + marginWithButton); } - if (!entry.date.isNull()) { + if (toggleVisibility) { AddTableRow( table, - tr::lng_gift_link_label_date(), - rpl::single(Ui::Text::WithEntities(langDateTime(entry.date)))); + tr::lng_gift_visibility(), + MakeVisibilityTableValue( + table, + controller, + entry.savedToProfile, + std::move(toggleVisibility)), + marginWithButton); } - if (entry.limitedCount > 0) { + if (entry.limitedCount > 0 && !entry.giftRefunded) { auto amount = rpl::single(TextWithEntities{ Lang::FormatCountDecimal(entry.limitedCount) }); AddTableRow( table, tr::lng_gift_availability(), - ((entry.limitedLeft > 0) - ? tr::lng_gift_availability_left( - lt_count_decimal, - rpl::single(entry.limitedLeft * 1.), + ((!unique && !entry.limitedLeft) + ? tr::lng_gift_availability_none( lt_amount, std::move(amount), Ui::Text::WithEntities) - : tr::lng_gift_availability_none( - lt_amount, - std::move(amount), - Ui::Text::WithEntities))); + : (unique + ? tr::lng_gift_unique_availability + : tr::lng_gift_availability_left)( + lt_count_decimal, + rpl::single(entry.limitedLeft * 1.), + lt_amount, + std::move(amount), + Ui::Text::WithEntities))); } - if (!entry.description.empty()) { + if (!unique && startUpgrade) { + AddTableRow( + table, + tr::lng_gift_unique_status(), + MakeNonUniqueStatusTableValue( + table, + controller, + std::move(startUpgrade)), + marginWithButton); + } + if (unique) { + const auto &original = unique->originalDetails; + if (original.recipientId) { + const auto owner = &controller->session().data(); + const auto to = owner->peer(original.recipientId); + const auto from = original.senderId + ? owner->peer(original.senderId).get() + : nullptr; + const auto date = base::unixtime::parse(original.date).date(); + const auto dateText = TextWithEntities{ langDayOfMonth(date) }; + const auto makeContext = [=](Fn update) { + return Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = std::move(update), + }; + }; + auto label = object_ptr( + table, + (from + ? (original.message.empty() + ? tr::lng_gift_unique_info_sender( + lt_from, + rpl::single(Ui::Text::Link(from->name(), 2)), + lt_recipient, + rpl::single(Ui::Text::Link(to->name(), 1)), + lt_date, + rpl::single(dateText), + Ui::Text::WithEntities) + : tr::lng_gift_unique_info_sender_comment( + lt_from, + rpl::single(Ui::Text::Link(from->name(), 2)), + lt_recipient, + rpl::single(Ui::Text::Link(to->name(), 1)), + lt_date, + rpl::single(dateText), + lt_text, + rpl::single(original.message), + Ui::Text::WithEntities)) + : (original.message.empty() + ? tr::lng_gift_unique_info_reciever( + lt_recipient, + rpl::single(Ui::Text::Link(to->name(), 1)), + lt_date, + rpl::single(dateText), + Ui::Text::WithEntities) + : tr::lng_gift_unique_info_reciever_comment( + lt_recipient, + rpl::single(Ui::Text::Link(to->name(), 1)), + lt_date, + rpl::single(dateText), + lt_text, + rpl::single(original.message), + Ui::Text::WithEntities))), + st::giveawayGiftMessage, + st::defaultPopupMenu, + makeContext); + const auto showBoxLink = [=](not_null peer) { + return std::make_shared([=] { + controller->uiShow()->showBox( + PrepareShortInfoBox(peer, controller)); + }); + }; + label->setLink(1, showBoxLink(to)); + if (from) { + label->setLink(2, showBoxLink(from)); + } + label->setSelectable(true); + table->addRow( + std::move(label), + nullptr, + st::giveawayGiftCodeLabelMargin, + st::giveawayGiftCodeValueMargin); + } + } else if (!entry.description.empty()) { const auto makeContext = [=](Fn update) { return Core::MarkedTextContext{ .session = session, @@ -1146,10 +1508,46 @@ void AddCreditsHistoryEntryTable( st::giveawayGiftCodeTableMargin); const auto peerId = PeerId(entry.barePeerId); const auto actorId = PeerId(entry.bareActorId); + const auto starrefRecipientId = PeerId(entry.starrefRecipientId); const auto session = &controller->session(); - if (actorId || peerId) { - auto text = entry.in + if (entry.starrefCommission) { + if (entry.starrefAmount) { + AddTableRow( + table, + tr::lng_star_ref_commission_title(), + rpl::single(TextWithEntities{ + QString::number(entry.starrefCommission / 10.) + '%' })); + } else { + AddTableRow( + table, + tr::lng_gift_link_label_reason(), + tr::lng_credits_box_history_entry_reason_star_ref( + Ui::Text::WithEntities)); + } + } + if (starrefRecipientId && entry.starrefAmount) { + AddTableRow( + table, + tr::lng_credits_box_history_entry_affiliate(), + controller, + starrefRecipientId); + } + if (peerId && entry.starrefCommission) { + AddTableRow( + table, + (entry.starrefAmount + ? tr::lng_credits_box_history_entry_referred + : tr::lng_credits_box_history_entry_miniapp)(), + controller, + peerId); + } + if (actorId || (!entry.starrefCommission && peerId)) { + auto text = entry.starrefCommission + ? tr::lng_credits_box_history_entry_referred() + : entry.in ? tr::lng_credits_box_history_entry_peer_in() + : entry.giftUpgraded + ? tr::lng_credits_box_history_entry_gift_from() : tr::lng_credits_box_history_entry_peer(); AddTableRow( table, @@ -1229,7 +1627,7 @@ void AddCreditsHistoryEntryTable( tr::lng_gift_link_label_gift(), tr::lng_gift_stars_title( lt_count, - rpl::single(float64(entry.credits)), + rpl::single(entry.credits.value()), Ui::Text::RichLangValue)); } { diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index 3a2c20498..d29c1fca4 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -58,7 +58,9 @@ void AddStarGiftTable( not_null controller, not_null container, const Data::CreditsHistoryEntry &entry, - Fn convertToStars); + Fn toggleVisibility, + Fn convertToStars, + Fn startUpgrade); void AddCreditsHistoryEntryTable( not_null controller, not_null container, diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index 647d6e0e9..ade6a4ee1 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/local_storage_box.h" -#include "boxes/abstract_box.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/labels.h" @@ -21,8 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/cache/storage_cache_database.h" #include "data/data_session.h" #include "lang/lang_keys.h" -#include "mainwindow.h" #include "main/main_session.h" +#include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -282,19 +281,19 @@ LocalStorageBox::LocalStorageBox( _timeLimit = settings.totalTimeLimit; } -void LocalStorageBox::Show(not_null<::Main::Session*> session) { +void LocalStorageBox::Show(not_null controller) { auto shared = std::make_shared>( - Box(session, CreateTag())); + Box(&controller->session(), CreateTag())); const auto weak = shared->data(); rpl::combine( - session->data().cache().statsOnMain(), - session->data().cacheBigFile().statsOnMain() + controller->session().data().cache().statsOnMain(), + controller->session().data().cacheBigFile().statsOnMain() ) | rpl::start_with_next([=]( Database::Stats &&stats, Database::Stats &&statsBig) { weak->update(std::move(stats), std::move(statsBig)); if (auto &strong = *shared) { - Ui::show(std::move(strong)); + controller->uiShow()->show(std::move(strong)); } }, weak->lifetime()); } diff --git a/Telegram/SourceFiles/boxes/local_storage_box.h b/Telegram/SourceFiles/boxes/local_storage_box.h index e78f10738..cc9e01b57 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.h +++ b/Telegram/SourceFiles/boxes/local_storage_box.h @@ -14,6 +14,10 @@ namespace Main { class Session; } // namespace Main +namespace Window { +class SessionController; +} // namespace Window + namespace Storage { namespace Cache { class Database; @@ -40,7 +44,7 @@ public: not_null session, CreateTag); - static void Show(not_null session); + static void Show(not_null controller); protected: void prepare() override; diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 9c68881a1..49df5b0ea 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat.h" +#include "data/data_chat_filters.h" #include "data/data_chat_participant_status.h" #include "data/data_histories.h" #include "data/data_peer.h" @@ -86,19 +87,35 @@ ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) { return result; } -[[nodiscard]] rpl::producer MessagesCountValue( +[[nodiscard]] rpl::producer> MessagesCountValue( not_null history, - not_null from) { + std::vector> from) { return [=](auto consumer) { auto lifetime = rpl::lifetime(); - auto search = lifetime.make_state(history); - consumer.put_next(0); - - search->messagesFounds( - ) | rpl::start_with_next([=](const Api::FoundMessages &found) { - consumer.put_next_copy(found.total); - }, lifetime); - search->searchMessages({ .from = from }); + struct State final { + base::flat_map messagesCounts; + int index = 0; + rpl::lifetime apiLifetime; + }; + const auto search = lifetime.make_state(history); + const auto state = lifetime.make_state(); + const auto send = [=](auto repeat) -> void { + if (state->index >= from.size()) { + consumer.put_next_copy(state->messagesCounts); + return; + } + const auto peer = from[state->index]; + const auto peerId = peer->id; + state->apiLifetime = search->messagesFounds( + ) | rpl::start_with_next([=](const Api::FoundMessages &found) { + state->messagesCounts[peerId] = found.total; + state->index++; + repeat(repeat); + }); + search->searchMessages({ .from = peer }); + }; + consumer.put_next({}); + send(send); return lifetime; }; @@ -273,15 +290,50 @@ void CreateModerateMessagesBox( false, st::defaultBoxCheckbox), st::boxRowPadding + buttonPadding); - if (isSingle) { - const auto history = items.front()->history(); + const auto history = items.front()->history(); + auto messagesCounts = MessagesCountValue(history, participants); + + const auto controller = box->lifetime().make_state( + Controller::Data{ + .messagesCounts = rpl::duplicate(messagesCounts), + .participants = participants, + }); + Ui::AddExpandablePeerList(deleteAll, controller, inner); + { tr::lng_selected_delete_sure( lt_count, rpl::combine( - MessagesCountValue(history, participants.front()), - deleteAll->checkedValue() - ) | rpl::map([s = items.size()](int all, bool checked) { - return float64((checked && all) ? all : s); + std::move(messagesCounts), + isSingle + ? deleteAll->checkedValue() + : rpl::merge( + controller->toggleRequestsFromInner.events(), + controller->checkAllRequests.events()) + ) | rpl::map([=, s = items.size()](const auto &map, bool c) { + const auto checked = (isSingle && !c) + ? Participants() + : controller->collectRequests + ? controller->collectRequests() + : Participants(); + auto result = 0; + for (const auto &[peerId, count] : map) { + for (const auto &peer : checked) { + if (peer->id == peerId) { + result += count; + break; + } + } + } + for (const auto &item : items) { + for (const auto &peer : checked) { + if (peer->id == item->from()->id) { + result--; + break; + } + } + result++; + } + return float64(result); }) ) | rpl::start_with_next([=](const QString &text) { title->setText(text); @@ -289,10 +341,6 @@ void CreateModerateMessagesBox( - rect::m::sum::h(st::boxRowPadding)); }, title->lifetime()); } - - const auto controller = box->lifetime().make_state( - Controller::Data{ .participants = participants }); - Ui::AddExpandablePeerList(deleteAll, controller, inner); handleSubmition(deleteAll); handleConfirmation(deleteAll, controller, [=]( @@ -512,6 +560,7 @@ void DeleteChatBox(not_null box, not_null peer) { const auto container = box->verticalLayout(); const auto maybeUser = peer->asUser(); + const auto isBot = maybeUser && maybeUser->isBot(); Ui::AddSkip(container); Ui::AddSkip(container); @@ -595,7 +644,7 @@ void DeleteChatBox(not_null box, not_null peer) { }(); const auto maybeBotCheckbox = [&]() -> Ui::Checkbox* { - if (!maybeUser || !maybeUser->isBot()) { + if (!isBot) { return nullptr; } Ui::AddSkip(container); @@ -608,6 +657,40 @@ void DeleteChatBox(not_null box, not_null peer) { st::defaultBoxCheckbox)); }(); + const auto removeFromChatsFilters = [=]( + not_null history) -> std::vector { + auto result = std::vector(); + for (const auto &filter : peer->owner().chatsFilters().list()) { + if (filter.withoutAlways(history) != filter) { + result.push_back(filter.id()); + } + } + return result; + }; + + const auto maybeChatsFiltersCheckbox = [&]() -> Ui::Checkbox* { + const auto history = (isBot || !maybeUser) + ? peer->owner().history(peer).get() + : nullptr; + if (!history || removeFromChatsFilters(history).empty()) { + return nullptr; + } + Ui::AddSkip(container); + Ui::AddSkip(container); + return box->addRow( + object_ptr( + container, + (maybeBotCheckbox + ? tr::lng_filters_checkbox_remove_bot + : (peer->isChannel() && !peer->isMegagroup()) + ? tr::lng_filters_checkbox_remove_channel + : tr::lng_filters_checkbox_remove_group)( + tr::now, + Ui::Text::WithEntities), + false, + st::defaultBoxCheckbox)); + }(); + Ui::AddSkip(container); auto buttonText = maybeUser @@ -622,10 +705,35 @@ void DeleteChatBox(not_null box, not_null peer) { box->addButton(std::move(buttonText), [=] { const auto revoke = maybeCheckbox && maybeCheckbox->checked(); const auto stopBot = maybeBotCheckbox && maybeBotCheckbox->checked(); + const auto removeFromChats = maybeChatsFiltersCheckbox + && maybeChatsFiltersCheckbox->checked(); Core::App().closeChatFromWindows(peer); if (stopBot) { peer->session().api().blockedPeers().block(peer); } + if (removeFromChats) { + const auto history = peer->owner().history(peer).get(); + const auto removeFrom = removeFromChatsFilters(history); + for (const auto &filter : peer->owner().chatsFilters().list()) { + if (!ranges::contains(removeFrom, filter.id())) { + continue; + } + const auto result = filter.withoutAlways(history); + if (result == filter) { + continue; + } + const auto tl = result.tl(); + peer->owner().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), + MTP_int(filter.id()), + tl)); + peer->session().api().request(MTPmessages_UpdateDialogFilter( + MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), + MTP_int(filter.id()), + tl + )).send(); + } + } // Don't delete old history by default, // because Android app doesn't. // diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index 840e4872d..308384e16 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/labels.h" #include "ui/wrap/fade_wrap.h" #include "ui/painter.h" +#include "ui/rect.h" #include "passport/passport_encryption.h" #include "passport/passport_panel_edit_contact.h" #include "settings/settings_privacy_security.h" @@ -171,8 +172,9 @@ PasscodeBox::PasscodeBox( bool turningOff) : _session(session) , _api(&_session->mtp()) +, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5) , _turningOff(turningOff) -, _about(st::boxWidth - st::boxPadding.left() * 1.5) +, _about(_textWidth) , _oldPasscode(this, st::defaultInputField, tr::lng_passcode_enter_old()) , _newPasscode( this, @@ -193,10 +195,11 @@ PasscodeBox::PasscodeBox( const CloudFields &fields) : _session(session) , _api(mtp) +, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5) , _turningOff(fields.turningOff) , _cloudPwd(true) , _cloudFields(fields) -, _about(st::boxWidth - st::boxPadding.left() * 1.5) +, _about(_textWidth) , _oldPasscode(this, st::defaultInputField, tr::lng_cloud_password_enter_old()) , _newPasscode( this, @@ -274,7 +277,7 @@ void PasscodeBox::prepare() { : _cloudPwd ? tr::lng_cloud_password_about(tr::now) : tr::lng_passcode_about(tr::now))); - _aboutHeight = _about.countHeight(st::boxWidth - st::boxPadding.left() * 1.5); + _aboutHeight = _about.countHeight(_textWidth); const auto onlyCheck = onlyCheckCurrent(); if (onlyCheck) { _oldPasscode->show(); @@ -382,28 +385,27 @@ void PasscodeBox::paintEvent(QPaintEvent *e) { Painter p(this); - int32 w = st::boxWidth - st::boxPadding.left() * 1.5; int32 abouty = (_passwordHint->isHidden() ? ((_reenterPasscode->isHidden() ? (_oldPasscode->y() + (_showRecoverLink && !_hintText.isEmpty() ? st::passcodeTextLine : 0)) : _reenterPasscode->y()) + st::passcodeSkip) : _passwordHint->y()) + _oldPasscode->height() + st::passcodeLittleSkip + st::passcodeAboutSkip; p.setPen(st::boxTextFg); - _about.drawLeft(p, st::boxPadding.left(), abouty, w, width()); + _about.drawLeft(p, st::boxPadding.left(), abouty, _textWidth, width()); if (!_hintText.isEmpty() && _oldError.isEmpty()) { - _hintText.drawLeftElided(p, st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height() + ((st::passcodeTextLine - st::normalFont->height) / 2), w, width(), 1, style::al_topleft); + _hintText.drawLeftElided(p, st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height() + ((st::passcodeTextLine - st::normalFont->height) / 2), _textWidth, width(), 1, style::al_topleft); } if (!_oldError.isEmpty()) { p.setPen(st::boxTextFgError); - p.drawText(QRect(st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height(), w, st::passcodeTextLine), _oldError, style::al_left); + p.drawText(QRect(st::boxPadding.left(), _oldPasscode->y() + _oldPasscode->height(), _textWidth, st::passcodeTextLine), _oldError, style::al_left); } if (!_newError.isEmpty()) { p.setPen(st::boxTextFgError); - p.drawText(QRect(st::boxPadding.left(), _reenterPasscode->y() + _reenterPasscode->height(), w, st::passcodeTextLine), _newError, style::al_left); + p.drawText(QRect(st::boxPadding.left(), _reenterPasscode->y() + _reenterPasscode->height(), _textWidth, st::passcodeTextLine), _newError, style::al_left); } if (!_emailError.isEmpty()) { p.setPen(st::boxTextFgError); - p.drawText(QRect(st::boxPadding.left(), _recoverEmail->y() + _recoverEmail->height(), w, st::passcodeTextLine), _emailError, style::al_left); + p.drawText(QRect(st::boxPadding.left(), _recoverEmail->y() + _recoverEmail->height(), _textWidth, st::passcodeTextLine), _emailError, style::al_left); } } @@ -1141,11 +1143,21 @@ RecoverBox::RecoverBox( Fn closeParent) : _session(session) , _api(mtp) -, _pattern(st::normalFont->elided(tr::lng_signin_recover_hint(tr::now, lt_recover_email, pattern), st::boxWidth - st::boxPadding.left() * 1.5)) +, _textWidth(st::boxWidth - st::boxPadding.left() * 1.5) , _cloudFields(fields) , _recoverCode(this, st::defaultInputField, tr::lng_signin_code()) , _noEmailAccess(this, tr::lng_signin_try_password(tr::now)) +, _patternLabel( + this, + tr::lng_signin_recover_hint( + lt_recover_email, + rpl::single(Ui::Text::WrapEmailPattern(pattern)), + Ui::Text::WithEntities), + st::termsContent, + st::defaultPopupMenu, + [=](Fn update) { return CommonTextContext{ std::move(update) }; }) , _closeParent(std::move(closeParent)) { + _patternLabel->setAttribute(Qt::WA_TransparentForMouseEvents); if (_cloudFields.pendingResetDate != 0 || !session) { _noEmailAccess.destroy(); } else { @@ -1176,19 +1188,24 @@ rpl::producer<> RecoverBox::recoveryExpired() const { return _recoveryExpired.events(); } -void RecoverBox::prepare() { - setTitle(tr::lng_signin_recover_title()); - - addButton(tr::lng_passcode_submit(), [=] { submit(); }); - addButton(tr::lng_cancel(), [=] { closeBox(); }); - +void RecoverBox::updateHeight() { setDimensions( st::boxWidth, (st::passcodePadding.top() + st::passcodePadding.bottom() + st::passcodeTextLine + _recoverCode->height() + + _patternLabel->height() + st::passcodeTextLine)); +} + +void RecoverBox::prepare() { + setTitle(tr::lng_signin_recover_title()); + + addButton(tr::lng_passcode_submit(), [=] { submit(); }); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + updateHeight(); _recoverCode->changes( ) | rpl::start_with_next([=] { @@ -1205,23 +1222,42 @@ void RecoverBox::paintEvent(QPaintEvent *e) { p.setFont(st::normalFont); p.setPen(st::boxTextFg); - int32 w = st::boxWidth - st::boxPadding.left() * 1.5; - p.drawText(QRect(st::boxPadding.left(), _recoverCode->y() - st::passcodeTextLine - st::passcodePadding.top(), w, st::passcodePadding.top() + st::passcodeTextLine), _pattern, style::al_left); if (!_error.isEmpty()) { p.setPen(st::boxTextFgError); - p.drawText(QRect(st::boxPadding.left(), _recoverCode->y() + _recoverCode->height(), w, st::passcodeTextLine), _error, style::al_left); + p.drawText( + QRect( + st::boxPadding.left(), + _recoverCode->y() + _recoverCode->height(), + _textWidth, + st::passcodeTextLine), + _error, + style::al_left); } } void RecoverBox::resizeEvent(QResizeEvent *e) { BoxContent::resizeEvent(e); - _recoverCode->resize(st::boxWidth - st::boxPadding.left() - st::boxPadding.right(), _recoverCode->height()); - _recoverCode->moveToLeft(st::boxPadding.left(), st::passcodePadding.top() + st::passcodePadding.bottom() + st::passcodeTextLine); + _patternLabel->resizeToWidth(_textWidth); + _patternLabel->moveToLeft( + st::boxPadding.left(), + st::passcodePadding.top()); + + _recoverCode->resize( + st::boxWidth - st::boxPadding.left() - st::boxPadding.right(), + _recoverCode->height()); + _recoverCode->moveToLeft( + st::boxPadding.left(), + rect::m::sum::v(st::passcodePadding) + _patternLabel->height()); if (_noEmailAccess) { - _noEmailAccess->moveToLeft(st::boxPadding.left(), _recoverCode->y() + _recoverCode->height() + (st::passcodeTextLine - _noEmailAccess->height()) / 2); + _noEmailAccess->moveToLeft( + st::boxPadding.left(), + rect::bottom(_recoverCode) + + (st::passcodeTextLine - _noEmailAccess->height()) / 2); } + + updateHeight(); } void RecoverBox::setInnerFocus() { diff --git a/Telegram/SourceFiles/boxes/passcode_box.h b/Telegram/SourceFiles/boxes/passcode_box.h index b651cda64..09f333561 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.h +++ b/Telegram/SourceFiles/boxes/passcode_box.h @@ -154,6 +154,7 @@ private: Main::Session *_session = nullptr; MTP::Sender _api; + const int _textWidth; QString _pattern; @@ -219,17 +220,18 @@ private: void proceedToChange(const QString &code); void checkSubmitFail(const MTP::Error &error); void setError(const QString &error); + void updateHeight(); Main::Session *_session = nullptr; MTP::Sender _api; + const int _textWidth; mtpRequestId _submitRequest = 0; - QString _pattern; - PasscodeBox::CloudFields _cloudFields; object_ptr _recoverCode; object_ptr _noEmailAccess; + object_ptr _patternLabel; Fn _closeParent; QString _error; diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 5a440ced5..69d304a14 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -120,6 +120,9 @@ void PeerListBox::createMultiSelect() { content()->submitted(); }); _select->entity()->setQueryChangedCallback([=](const QString &query) { + if (_customQueryChangedCallback) { + _customQueryChangedCallback(query); + } searchQueryChanged(query); }); _select->entity()->setItemRemovedCallback([=](uint64 itemId) { @@ -138,6 +141,10 @@ void PeerListBox::createMultiSelect() { _select->moveToLeft(0, 0); } +void PeerListBox::appendQueryChangedCallback(Fn callback) { + _customQueryChangedCallback = std::move(callback); +} + void PeerListBox::setAddedTopScrollSkip(int skip) { _addedTopScrollSkip = skip; _scrollBottomFixed = false; @@ -217,19 +224,7 @@ void PeerListBox::keyPressEvent(QKeyEvent *e) { void PeerListBox::searchQueryChanged(const QString &query) { scrollToY(0); - const auto isEmpty = content()->searchQueryChanged(query); - if (_specialTabsMode.enabled) { - const auto was = _specialTabsMode.searchIsActive; - _specialTabsMode.searchIsActive = !isEmpty; - if (was != _specialTabsMode.searchIsActive) { - if (_specialTabsMode.searchIsActive) { - _specialTabsMode.topSkip = _addedTopScrollSkip; - setAddedTopScrollSkip(0); - } else { - setAddedTopScrollSkip(_specialTabsMode.topSkip); - } - } - } + content()->searchQueryChanged(query); } void PeerListBox::resizeEvent(QResizeEvent *e) { @@ -486,7 +481,7 @@ void PeerListBox::addSelectItem( void PeerListBox::addSelectItem( uint64 itemId, const QString &text, - Ui::MultiSelect::PaintRoundImage paintUserpic, + PaintRoundImageCallback paintUserpic, anim::type animated) { if (!_select) { createMultiSelect(); @@ -561,13 +556,8 @@ rpl::producer PeerListBox::multiSelectHeightValue() const { return _select ? _select->heightValue() : rpl::single(0); } -void PeerListBox::setSpecialTabMode(bool value) { - content()->setIgnoreHiddenRowsOnSearch(value); - if (value) { - _specialTabsMode.enabled = true; - } else { - _specialTabsMode = {}; - } +rpl::producer<> PeerListBox::noSearchSubmits() const { + return content()->noSearchSubmits(); } PeerListRow::PeerListRow(not_null peer) @@ -798,31 +788,29 @@ int PeerListRow::paintNameIconGetWidth( || _isVerifyCodesChat) { return 0; } - return _badge.drawGetWidth( - p, - QRect( + return _badge.drawGetWidth(p, { + .peer = peer(), + .rectForName = QRect( nameLeft, nameTop, availableWidth, st::semiboldFont->height), - nameWidth, - outerWidth, - { - .peer = peer(), - .verified = &(selected - ? st::dialogsVerifiedIconOver - : st::dialogsVerifiedIcon), - .premium = &(selected - ? st::dialogsPremiumIcon.over - : st::dialogsPremiumIcon.icon), - .scam = &(selected ? st::dialogsScamFgOver : st::dialogsScamFg), - .premiumFg = &(selected - ? st::dialogsVerifiedIconBgOver - : st::dialogsVerifiedIconBg), - .customEmojiRepaint = repaint, - .now = now, - .paused = false, - }); + .nameWidth = nameWidth, + .outerWidth = outerWidth, + .verified = &(selected + ? st::dialogsVerifiedIconOver + : st::dialogsVerifiedIcon), + .premium = &(selected + ? st::dialogsPremiumIcon.over + : st::dialogsPremiumIcon.icon), + .scam = &(selected ? st::dialogsScamFgOver : st::dialogsScamFg), + .premiumFg = &(selected + ? st::dialogsVerifiedIconBgOver + : st::dialogsVerifiedIconBg), + .customEmojiRepaint = repaint, + .now = now, + .paused = false, + }); } void PeerListRow::paintStatusText( @@ -1291,6 +1279,9 @@ void PeerListContent::clearAllContent() { = _normalizedSearchQuery = _mentionHighlight = QString(); + if (_controller->hasComplexSearch()) { + _controller->search(QString()); + } } void PeerListContent::convertRowToSearchResult(not_null row) { @@ -1689,6 +1680,10 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) { _controller->rowClicked(row); } } + } else if (button == Qt::MiddleButton && pressed == _selected) { + if (auto row = getRow(pressed.index)) { + _controller->rowMiddleClicked(row); + } } } @@ -2079,7 +2074,7 @@ void PeerListContent::checkScrollForPreload() { } } -PeerListContent::IsEmpty PeerListContent::searchQueryChanged(QString query) { +void PeerListContent::searchQueryChanged(QString query) { const auto searchWordsList = TextUtilities::PrepareSearchWords(query); const auto normalizedQuery = searchWordsList.join(' '); if (_ignoreHiddenRowsOnSearch && !normalizedQuery.isEmpty()) { @@ -2136,7 +2131,6 @@ PeerListContent::IsEmpty PeerListContent::searchQueryChanged(QString query) { } refreshRows(); } - return _normalizedSearchQuery.isEmpty(); } std::unique_ptr PeerListContent::saveState() const { @@ -2211,6 +2205,9 @@ bool PeerListContent::submitted() { _controller->rowClicked(row); return true; } + } else { + _noSearchSubmits.fire({}); + return true; } return false; } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index bd71470a7..c4a79c456 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -482,6 +482,8 @@ public: } virtual void rowClicked(not_null row) = 0; + virtual void rowMiddleClicked(not_null row) { + } virtual void rowRightActionClicked(not_null row) { } @@ -652,8 +654,7 @@ public: [[nodiscard]] bool hasPressed() const; void clearSelection(); - using IsEmpty = bool; - IsEmpty searchQueryChanged(QString query); + void searchQueryChanged(QString query); bool submitted(); PeerListRowId updateFromParentDrag(QPoint globalPosition); @@ -713,7 +714,7 @@ public: update(); } - std::unique_ptr saveState() const; + [[nodiscard]] std::unique_ptr saveState() const; void restoreState(std::unique_ptr state); void showRowMenu( @@ -721,10 +722,14 @@ public: bool highlightRow, Fn)> destroyed); - auto scrollToRequests() const { + [[nodiscard]] auto scrollToRequests() const { return _scrollToRequests.events(); } + [[nodiscard]] auto noSearchSubmits() const { + return _noSearchSubmits.events(); + } + ~PeerListContent(); protected: @@ -891,6 +896,8 @@ private: object_ptr _searchLoading = { nullptr }; object_ptr _loadingAnimation = { nullptr }; + rpl::event_stream<> _noSearchSubmits; + std::vector> _searchRows; base::Timer _repaintByStatus; base::unique_qptr _contextMenu; @@ -1107,8 +1114,7 @@ public: [[nodiscard]] std::vector collectSelectedIds(); [[nodiscard]] std::vector> collectSelectedRows(); [[nodiscard]] rpl::producer multiSelectHeightValue() const; - - void setSpecialTabMode(bool value); + [[nodiscard]] rpl::producer<> noSearchSubmits() const; void peerListSetTitle(rpl::producer title) override { setTitle(std::move(title)); @@ -1133,6 +1139,8 @@ public: void showFinished() override; + void appendQueryChangedCallback(Fn); + protected: void prepare() override; void setInnerFocus() override; @@ -1170,16 +1178,10 @@ private: object_ptr> _select = { nullptr }; const std::shared_ptr _show; + Fn _customQueryChangedCallback; std::unique_ptr _controller; Fn _init; bool _scrollBottomFixed = false; int _addedTopScrollSkip = 0; - struct SpecialTabsMode final { - bool enabled = false; - bool searchIsActive = false; - int topSkip = 0; - }; - SpecialTabsMode _specialTabsMode; - }; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 05b8d6d20..ff3e2722a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_main_list.h" #include "ui/effects/outline_segments.h" #include "ui/wrap/slide_wrap.h" +#include "window/window_separate_id.h" #include "window/window_session_controller.h" // showAddContact() #include "base/unixtime.h" #include "styles/style_boxes.h" @@ -64,6 +65,10 @@ object_ptr PrepareContactsBox( public: using ContactsBoxController::ContactsBoxController; + [[nodiscard]] rpl::producer> wheelClicks() const { + return _wheelClicks.events(); + } + protected: std::unique_ptr createRow( not_null user) override { @@ -72,6 +77,14 @@ object_ptr PrepareContactsBox( : nullptr; } + void rowMiddleClicked( + not_null row) override { + _wheelClicks.fire(row->peer()); + } + + private: + rpl::event_stream> _wheelClicks; + }; auto controller = std::make_unique( &sessionController->session()); @@ -100,6 +113,10 @@ object_ptr PrepareContactsBox( online ? &st::contactsSortOnlineIconOver : nullptr); }); raw->setSortMode(Mode::Online); + + raw->wheelClicks() | rpl::start_with_next([=](not_null p) { + sessionController->showInNewWindow(p); + }, box->lifetime()); }; return Box(std::move(controller), std::move(init)); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index bdd3bdc24..1147922a0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -2282,6 +2282,8 @@ void ParticipantsBoxSearchController::restoreState( _allLoaded = my->allLoaded; _offset = my->offset; _query = my->query; + _timer.cancel(); + _requestId = 0; if (my->wasLoading) { searchOnServer(); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 76fd9856b..7fea20e34 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_reactions.h" #include "boxes/peers/replace_boost_box.h" +#include "boxes/peers/verify_peers_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/stickers_box.h" #include "boxes/username_box.h" @@ -46,6 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "history/admin_log/history_admin_log_section.h" #include "info/bot/earn/info_bot_earn_widget.h" +#include "info/bot/starref/info_bot_starref_join_widget.h" +#include "info/bot/starref/info_bot_starref_setup_widget.h" #include "info/channel_statistics/boosts/info_boosts_widget.h" #include "info/channel_statistics/earn/earn_format.h" #include "info/channel_statistics/earn/earn_icons.h" @@ -60,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/emoji_button.h" #include "ui/controls/userpic_button.h" #include "ui/effects/premium_graphics.h" +#include "ui/new_badges.h" #include "ui/rect.h" #include "ui/rp_widget.h" #include "ui/vertical_list.h" @@ -78,6 +82,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" #include "styles/style_boxes.h" #include "styles/style_info.h" @@ -358,9 +363,11 @@ private: void fillBotUsernamesButton(); void fillBotCurrencyButton(); void fillBotCreditsButton(); + void fillBotAffiliateProgram(); void fillBotEditIntroButton(); void fillBotEditCommandsButton(); void fillBotEditSettingsButton(); + void fillBotVerifyAccounts(); void submitTitle(); void submitDescription(); @@ -1181,6 +1188,7 @@ void Controller::fillManageSection() { fillBotUsernamesButton(); fillBotCurrencyButton(); fillBotCreditsButton(); + fillBotAffiliateProgram(); fillBotEditIntroButton(); fillBotEditCommandsButton(); fillBotEditSettingsButton(); @@ -1200,6 +1208,7 @@ void Controller::fillManageSection() { Ui::Text::RichLangValue), st::boxDividerLabel), st::defaultBoxDividerLabelPadding)); + fillBotVerifyAccounts(); return; } @@ -1238,6 +1247,9 @@ void Controller::fillManageSection() { && (channel->isBroadcast() || channel->isGigagroup()); const auto hasRecentActions = isChannel && (channel->hasAdminRights() || channel->amCreator()); + const auto hasStarRef = Info::BotStarRef::Join::Allowed(_peer) + && isChannel + && channel->canPostMessages(); const auto canEditStickers = isChannel && channel->canEditStickers(); const auto canDeleteChannel = isChannel && channel->canDelete(); const auto canEditColorIndex = isChannel && channel->canEditEmoji(); @@ -1420,10 +1432,21 @@ void Controller::fillManageSection() { AddButtonWithCount( _controls.buttonsLayout, tr::lng_manage_peer_recent_actions(), - rpl::single(QString()), //Empty count. + rpl::single(QString()), // Empty count. std::move(callback), { &st::menuIconGroupLog }); } + if (hasStarRef) { + auto callback = [=] { + _navigation->showSection(Info::BotStarRef::Join::Make(_peer)); + }; + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_star_ref(), + rpl::single(QString()), // Empty count. + std::move(callback), + { .icon = &st::menuIconStarRefShare, .newBadge = true }); + } if (canEditStickers || canDeleteChannel) { ::AddSkip(_controls.buttonsLayout); @@ -1664,7 +1687,7 @@ void Controller::fillBotCreditsButton() { auto &lifetime = _controls.buttonsLayout->lifetime(); const auto state = lifetime.make_state(); if (const auto balance = _peer->session().credits().balance(_peer->id)) { - state->balance = Lang::FormatCountDecimal(balance); + state->balance = Lang::FormatStarsAmountDecimal(balance); } const auto wrap = _controls.buttonsLayout->add( @@ -1689,7 +1712,7 @@ void Controller::fillBotCreditsButton() { if (data.balance) { wrap->toggle(true, anim::type::normal); } - state->balance = Lang::FormatCountDecimal(data.balance); + state->balance = Lang::FormatStarsAmountDecimal(data.balance); }); } { @@ -1711,6 +1734,35 @@ void Controller::fillBotCreditsButton() { } +void Controller::fillBotAffiliateProgram() { + Expects(_isBot); + + if (!Info::BotStarRef::Setup::Allowed(_peer)) { + return; + } + + const auto user = _peer->asUser(); + auto label = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::StarRefProgram + ) | rpl::map([=] { + const auto commission = user->botInfo + ? user->botInfo->starRefProgram.commission + : 0; + return commission + ? Info::BotStarRef::FormatCommission(commission) + : tr::lng_manage_peer_bot_star_ref_off(tr::now); + }); + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_star_ref(), + std::move(label), + [controller = _navigation->parentController(), user] { + controller->showSection(Info::BotStarRef::Setup::Make(user)); + }, + { .icon = &st::menuIconSharing, .newBadge = true }); +} + void Controller::fillBotEditIntroButton() { Expects(_isBot); @@ -1747,6 +1799,39 @@ void Controller::fillBotEditSettingsButton() { { &st::menuIconSettings }); } +void Controller::fillBotVerifyAccounts() { + Expects(_isBot); + + const auto user = _peer->asUser(); + const auto wrap = _controls.buttonsLayout->add( + object_ptr>( + _controls.buttonsLayout, + object_ptr( + _controls.buttonsLayout))); + wrap->toggleOn(rpl::single( + rpl::empty + ) | rpl::then(user->owner().botCommandsChanges( + ) | rpl::filter( + rpl::mappers::_1 == _peer + ) | rpl::to_empty) | rpl::map([=] { + const auto info = user->botInfo.get(); + return info && info->verifierSettings; + })); + + const auto inner = wrap->entity(); + Ui::AddSkip(inner); + AddButtonWithCount( + inner, + tr::lng_manage_peer_bot_verify(), + rpl::never(), + [controller = _navigation->parentController(), user] { + controller->show(MakeVerifyPeersBox(controller, user)); + }, + { &st::menuIconFactcheck }); + Ui::AddSkip(inner); + Ui::AddDivider(inner); +} + void Controller::submitTitle() { Expects(_controls.title != nullptr); @@ -2227,7 +2312,9 @@ void Controller::saveHistoryVisibility() { void Controller::toggleBotManager(const QString &command) { const auto controller = _navigation->parentController(); _api.request(MTPcontacts_ResolveUsername( - MTP_string(kBotManagerUsername.utf16()) + MTP_flags(0), + MTP_string(kBotManagerUsername.utf16()), + MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { _peer->owner().processUsers(result.data().vusers()); _peer->owner().processChats(result.data().vchats()); @@ -2501,6 +2588,13 @@ object_ptr EditPeerInfoBox::CreateButton( st.button); const auto button = result.data(); button->addClickHandler(callback); + + const auto badge = descriptor.newBadge + ? Ui::NewBadge::CreateNewBadge( + button, + tr::lng_premium_summary_new_badge()).get() + : nullptr; + if (descriptor) { AddButtonIcon( button, @@ -2509,7 +2603,7 @@ object_ptr EditPeerInfoBox::CreateButton( } auto labelText = rpl::combine( - std::move(text), + rpl::duplicate(text), std::move(count), button->widthValue() ) | rpl::map([&st](const QString &text, const QString &count, int width) { @@ -2524,11 +2618,40 @@ object_ptr EditPeerInfoBox::CreateButton( : count; }); + if (badge) { + rpl::combine( + std::move(text), + rpl::duplicate(labelText), + button->widthValue() + ) | rpl::start_with_next([=]( + const QString &text, + const QString &label, + int width) { + const auto space = st.button.style.font->spacew; + const auto left = st.button.padding.left() + + st.button.style.font->width(text) + + space; + const auto right = st.labelPosition.x() + + st.label.style.font->width(label) + + (space * 2); + const auto available = width - left - right; + badge->setVisible(available >= badge->width()); + if (!badge->isHidden()) { + const auto top = st.button.padding.top() + + st.button.style.font->ascent + - st::settingsPremiumNewBadge.style.font->ascent + - st::settingsPremiumNewBadgePadding.top(); + badge->moveToLeft(left, top, width); + } + }, badge->lifetime()); + } + const auto label = Ui::CreateChild( button, std::move(labelText), st.label); label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); rpl::combine( button->widthValue(), diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index c8b4f2d48..29504ab63 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -26,7 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/history.h" -#include "history/history_item_helpers.h" // GetErrorTextForSending. +#include "history/history_item_helpers.h" // GetErrorForSending. #include "history/view/history_view_group_call_bar.h" // GenerateUserpics... #include "lang/lang_keys.h" #include "main/main_session.h" @@ -1492,27 +1492,14 @@ object_ptr ShareInviteLinkBox( return; } - const auto error = [&] { - for (const auto thread : result) { - const auto error = GetErrorTextForSending( - thread, - { .text = &comment }); - if (!error.isEmpty()) { - return std::make_pair(error, thread); - } - } - return std::make_pair(QString(), result.front()); - }(); - if (!error.first.isEmpty()) { - auto text = TextWithEntities(); - if (result.size() > 1) { - text.append( - Ui::Text::Bold(error.second->chatListName()) - ).append("\n\n"); - } - text.append(error.first); + const auto errorWithThread = GetErrorForSending( + result, + { .text = &comment }); + if (errorWithThread.error) { if (*box) { - (*box)->uiShow()->showBox(Ui::MakeInformBox(text)); + (*box)->uiShow()->showBox(MakeSendErrorBox( + errorWithThread, + result.size() > 1)); } return; } diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index dc7524e76..0275358f0 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -7,27 +7,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peers/peer_short_info_box.h" -#include "ui/effects/radial_animation.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/scroll_area.h" -#include "ui/wrap/vertical_layout.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/wrap/wrap.h" -#include "ui/image/image_prepare.h" -#include "ui/text/text_utilities.h" -#include "ui/painter.h" +#include "base/event_filter.h" +#include "core/application.h" #include "info/profile/info_profile_text.h" #include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" -#include "base/event_filter.h" -#include "lang/lang_keys.h" +#include "ui/effects/radial_animation.h" +#include "ui/image/image_prepare.h" +#include "ui/painter.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/labels.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/widgets/scroll_area.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/wrap.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" #include "styles/style_boxes.h" -#include "styles/style_layers.h" #include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" namespace { +using MenuCallback = Ui::Menu::MenuCallback; + constexpr auto kShadowMaxAlpha = 80; constexpr auto kInactiveBarOpacity = 0.5; @@ -833,6 +842,24 @@ void PeerShortInfoBox::refreshRoundedTopImage(const QColor &color) { RectPart::TopLeft | RectPart::TopRight); } +rpl::producer PeerShortInfoBox::fillMenuRequests() const { + return _fillMenuRequests.events(); +} + +void PeerShortInfoBox::contextMenuEvent(QContextMenuEvent *e) { + _menuHolder = nullptr; + const auto menu = Ui::CreateChild( + this, + st::popupMenuWithIcons); + _fillMenuRequests.fire(Ui::Menu::CreateAddActionCallback(menu)); + _menuHolder.reset(menu); + if (menu->empty()) { + _menuHolder = nullptr; + return; + } + menu->popup(e->globalPos()); +} + rpl::producer PeerShortInfoBox::nameValue() const { return _fields.value( ) | rpl::map([](const PeerShortInfoFields &fields) { diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index b51ac3e2d..271e1826c 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -15,6 +15,10 @@ struct ShortInfoCover; struct ShortInfoBox; } // namespace style +namespace Ui::Menu { +struct MenuCallback; +} // namespace Ui::Menu + namespace Media::Streaming { class Document; class Instance; @@ -160,6 +164,11 @@ public: [[nodiscard]] rpl::producer<> openRequests() const; [[nodiscard]] rpl::producer moveRequests() const; + [[nodiscard]] auto fillMenuRequests() const + -> rpl::producer; + +protected: + void contextMenuEvent(QContextMenuEvent *e) override; private: void prepare() override; @@ -192,6 +201,9 @@ private: not_null _rows; PeerShortInfoCover _cover; + base::unique_qptr _menuHolder; + rpl::event_stream _fillMenuRequests; + rpl::event_stream<> _openRequests; }; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index 72482cffd..760a821c7 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -7,26 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peers/prepare_short_info_box.h" +#include "base/unixtime.h" #include "boxes/peers/peer_short_info_box.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_file_origin.h" #include "data/data_peer.h" +#include "data/data_peer_values.h" #include "data/data_photo.h" #include "data/data_photo_media.h" -#include "data/data_streaming.h" -#include "data/data_file_origin.h" -#include "data/data_user.h" -#include "data/data_chat.h" -#include "data/data_channel.h" -#include "data/data_peer_values.h" -#include "data/data_user_photos.h" -#include "data/data_changes.h" #include "data/data_session.h" -#include "main/main_session.h" -#include "window/window_session_controller.h" +#include "data/data_streaming.h" +#include "data/data_user.h" +#include "data/data_user_photos.h" #include "info/profile/info_profile_values.h" -#include "ui/text/format_values.h" -#include "base/unixtime.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/delayed_activation.h" // PreventDelayedActivation +#include "ui/text/format_values.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "window/window_session_controller.h" #include "styles/style_info.h" +#include "styles/style_menu_icons.h" namespace { @@ -446,6 +450,7 @@ object_ptr PrepareShortInfoBox( not_null peer, Fn open, Fn videoPaused, + Fn menuFiller, const style::ShortInfoBox *stOverride) { const auto type = peer->isSelf() ? PeerShortInfoType::Self @@ -463,6 +468,13 @@ object_ptr PrepareShortInfoBox( std::move(videoPaused), stOverride); + if (menuFiller) { + result->fillMenuRequests( + ) | rpl::start_with_next([=](Ui::Menu::MenuCallback callback) { + menuFiller(std::move(callback)); + }, result->lifetime()); + } + result->openRequests( ) | rpl::start_with_next(open, result->lifetime()); @@ -481,10 +493,21 @@ object_ptr PrepareShortInfoBox( return navigation->parentController()->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); }; + auto menuFiller = [=](Ui::Menu::MenuCallback addAction) { + const auto controller = navigation->parentController(); + const auto peerSeparateId = Window::SeparateId(peer); + if (controller->windowId() != peerSeparateId) { + addAction(tr::lng_context_new_window(tr::now), [=] { + Ui::PreventDelayedActivation(); + controller->showInNewWindow(peer); + }, &st::menuIconNewWindow); + } + }; return PrepareShortInfoBox( peer, open, videoIsPaused, + std::move(menuFiller), stOverride); } diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h index 327ce373b..2edd5cd92 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h @@ -16,6 +16,10 @@ struct ShortInfoCover; struct ShortInfoBox; } // namespace style +namespace Ui::Menu { +struct MenuCallback; +} // namespace Ui::Menu + namespace Ui { class BoxContent; } // namespace Ui @@ -35,6 +39,7 @@ struct PreparedShortInfoUserpic { not_null peer, Fn open, Fn videoPaused, + Fn menuFiller, const style::ShortInfoBox *stOverride = nullptr); [[nodiscard]] object_ptr PrepareShortInfoBox( diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index a3bc8b69b..634e4a248 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -202,10 +202,11 @@ void Controller::prepare() { const auto session = &_to->session(); auto above = object_ptr((QWidget*)nullptr); above->add( - CreateBoostReplaceUserpics( + CreateUserpicsTransfer( above.data(), _selectedPeers.value(), - _to), + _to, + UserpicsTransferType::BoostReplace), st::boxRowPadding + st::boostReplaceUserpicsPadding); above->add( object_ptr( @@ -366,10 +367,11 @@ object_ptr ReassignBoostSingleBox( }); box->verticalLayout()->insert( 0, - CreateBoostReplaceUserpics( + CreateUserpicsTransfer( box, rpl::single(std::vector{ peer }), - to), + to, + UserpicsTransferType::BoostReplace), st::boxRowPadding + st::boostReplaceUserpicsPadding); }); @@ -536,10 +538,11 @@ object_ptr ReassignBoostsBox( return Box(std::move(controller), std::move(initBox)); } -object_ptr CreateBoostReplaceUserpics( +object_ptr CreateUserpicsTransfer( not_null parent, rpl::producer>> from, - not_null to) { + not_null to, + UserpicsTransferType type) { struct State { std::vector> from; std::vector> buttons; @@ -640,13 +643,18 @@ object_ptr CreateBoostReplaceUserpics( button->render(&q, position, QRegion(), QWidget::DrawChildren); } state->painting = false; + const auto boosting = (type == UserpicsTransferType::BoostReplace); const auto last = state->buttons.back().get(); + const auto back = boosting ? last : right; const auto add = st::boostReplaceIconAdd; - const auto skip = st::boostReplaceIconSkip; - const auto w = st::boostReplaceIcon.width() + 2 * skip; - const auto h = st::boostReplaceIcon.height() + 2 * skip; - const auto x = last->x() + last->width() - w + add.x(); - const auto y = last->y() + last->height() - h + add.y(); + const auto &icon = boosting + ? st::boostReplaceIcon + : st::starrefJoinIcon; + const auto skip = boosting ? st::boostReplaceIconSkip : 0; + const auto w = icon.width() + 2 * skip; + const auto h = icon.height() + 2 * skip; + const auto x = back->x() + back->width() - w + add.x(); + const auto y = back->y() + back->height() - h + add.y(); auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); brush.setStops(Ui::Premium::ButtonGradientStops()); @@ -654,7 +662,7 @@ object_ptr CreateBoostReplaceUserpics( pen.setWidthF(stroke); q.setPen(pen); q.drawEllipse(x - half, y - half, w + stroke, h + stroke); - st::boostReplaceIcon.paint(q, x + skip, y + skip, outerw); + icon.paint(q, x + skip, y + skip, outerw); const auto size = st::boostReplaceArrow.size(); st::boostReplaceArrow.paint( diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h index c84b0d306..6e622479d 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -53,10 +53,15 @@ object_ptr ReassignBoostsBox( Fn slots, int groups, int channels)> reassign, Fn cancel); -[[nodiscard]] object_ptr CreateBoostReplaceUserpics( +enum class UserpicsTransferType { + BoostReplace, + StarRefJoin, +}; +[[nodiscard]] object_ptr CreateUserpicsTransfer( not_null parent, rpl::producer>> from, - not_null to); + not_null to, + UserpicsTransferType type); [[nodiscard]] object_ptr CreateUserpicsWithMoreBadge( not_null parent, diff --git a/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp b/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp new file mode 100644 index 000000000..b368f5dc3 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/verify_peers_box.cpp @@ -0,0 +1,295 @@ +/* +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 "boxes/peers/verify_peers_box.h" + +#include "apiwrap.h" +#include "boxes/peer_list_controllers.h" +#include "data/data_user.h" +#include "history/history.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +namespace { + +constexpr auto kSetupVerificationToastDuration = 4 * crl::time(1000); + +class Controller final : public ChatsListBoxController { +public: + Controller(not_null session, not_null bot) + : ChatsListBoxController(session) + , _bot(bot) { + } + + Main::Session &session() const override; + + void rowClicked(gsl::not_null row) override; + +private: + std::unique_ptr createRow(not_null history) override; + void prepareViewHook() override; + + void confirmAdd(not_null peer); + void confirmRemove(not_null peer); + + const not_null _bot; + +}; + +void Setup( + not_null bot, + not_null peer, + QString description, + Fn done) { + using Flag = MTPbots_SetCustomVerification::Flag; + bot->session().api().request(MTPbots_SetCustomVerification( + MTP_flags(Flag::f_bot + | Flag::f_enabled + | (description.isEmpty() ? Flag() : Flag::f_custom_description)), + bot->inputUser, + peer->input, + MTP_string(description) + )).done([=] { + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); +} + +void Remove( + not_null bot, + not_null peer, + Fn done) { + bot->session().api().request(MTPbots_SetCustomVerification( + MTP_flags(MTPbots_SetCustomVerification::Flag::f_bot), + bot->inputUser, + peer->input, + MTPstring() + )).done([=] { + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); +} + +Main::Session &Controller::session() const { + return _bot->session(); +} + +void Controller::rowClicked(gsl::not_null row) { + const auto peer = row->peer(); + const auto details = peer->botVerifyDetails(); + const auto already = details && (details->botId == peerToUser(_bot->id)); + if (already) { + confirmRemove(peer); + } else { + confirmAdd(peer); + } +} + +void Controller::confirmAdd(not_null peer) { + const auto bot = _bot; + const auto show = delegate()->peerListUiShow(); + show->show(Box([=](not_null box) { + struct State { + Ui::InputField *field = nullptr; + QString description; + bool sent = false; + }; + const auto settings = bot->botInfo + ? bot->botInfo->verifierSettings.get() + : nullptr; + const auto modify = settings && settings->canModifyDescription; + const auto state = std::make_shared(State{ + .description = settings ? settings->customDescription : QString() + }); + + const auto limit = session().appConfig().get( + u"bot_verification_description_length_limit"_q, + 70); + const auto send = [=] { + if (modify && state->description.size() > limit) { + state->field->showError(); + return; + } else if (state->sent) { + return; + } + state->sent = true; + const auto weak = Ui::MakeWeak(box); + const auto description = modify ? state->description : QString(); + Setup(bot, peer, description, [=](QString error) { + if (error.isEmpty()) { + if (const auto strong = weak.data()) { + strong->closeBox(); + } + show->showToast({ + .text = PeerVerifyPhrases(peer).sent( + tr::now, + lt_name, + Ui::Text::Bold(peer->shortName()), + Ui::Text::WithEntities), + .duration = kSetupVerificationToastDuration, + }); + } else { + state->sent = false; + show->showToast(error); + } + }); + }; + + const auto phrases = PeerVerifyPhrases(peer); + Ui::ConfirmBox(box, { + .text = phrases.text( + lt_name, + rpl::single(Ui::Text::Bold(peer->shortName())), + Ui::Text::WithEntities), + .confirmed = send, + .confirmText = phrases.submit(), + .title = phrases.title(), + }); + if (!modify) { + return; + } + + Ui::AddSubsectionTitle( + box->verticalLayout(), + tr::lng_bot_verify_description_label(), + QMargins(0, 0, 0, -st::defaultSubsectionTitlePadding.bottom())); + + const auto field = box->addRow(object_ptr( + box, + st::createPollField, + Ui::InputField::Mode::NoNewlines, + rpl::single(state->description), + state->description + ), st::createPollFieldPadding); + state->field = field; + + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + Ui::AddSkip(box->verticalLayout()); + + field->changes() | rpl::start_with_next([=] { + state->description = field->getLastText(); + }, field->lifetime()); + + field->setMaxLength(limit * 2); + Ui::AddLengthLimitLabel(field, limit, std::nullopt); + + Ui::AddDividerText(box->verticalLayout(), phrases.about()); + })); +} + +void Controller::confirmRemove(not_null peer) { + const auto bot = _bot; + const auto show = delegate()->peerListUiShow(); + show->show(Box([=](not_null box) { + const auto sent = std::make_shared(); + const auto send = [=] { + if (*sent) { + return; + } + *sent = true; + const auto weak = Ui::MakeWeak(box); + Remove(bot, peer, [=](QString error) { + if (error.isEmpty()) { + if (const auto strong = weak.data()) { + strong->closeBox(); + } + show->showToast(tr::lng_bot_verify_remove_done(tr::now)); + } else { + *sent = false; + show->showToast(error); + } + }); + }; + Ui::ConfirmBox(box, { + .text = PeerVerifyPhrases(peer).remove(), + .confirmed = send, + .confirmText = tr::lng_bot_verify_remove_submit(), + .confirmStyle = &st::attentionBoxButton, + .title = tr::lng_bot_verify_remove_title(), + }); + })); +} + +auto Controller::createRow(not_null history) +-> std::unique_ptr { + const auto peer = history->peer; + const auto may = peer->isUser() || peer->isChannel(); + return may ? std::make_unique(history) : nullptr; +} + +void Controller::prepareViewHook() { +} + +} // namespace + +object_ptr MakeVerifyPeersBox( + not_null window, + not_null bot) { + const auto session = &window->session(); + auto controller = std::make_unique(session, bot); + auto init = [=](not_null box) { + box->setTitle(tr::lng_bot_verify_title()); + box->addButton(tr::lng_box_done(), [=] { + box->closeBox(); + }); + }; + return Box(std::move(controller), std::move(init)); +} + +BotVerifyPhrases PeerVerifyPhrases(not_null peer) { + if (const auto user = peer->asUser()) { + if (user->isBot()) { + return { + .title = tr::lng_bot_verify_bot_title, + .text = tr::lng_bot_verify_bot_text, + .about = tr::lng_bot_verify_bot_about, + .submit = tr::lng_bot_verify_bot_submit, + .sent = tr::lng_bot_verify_bot_sent, + .remove = tr::lng_bot_verify_bot_remove, + }; + } else { + return { + .title = tr::lng_bot_verify_user_title, + .text = tr::lng_bot_verify_user_text, + .about = tr::lng_bot_verify_user_about, + .submit = tr::lng_bot_verify_user_submit, + .sent = tr::lng_bot_verify_user_sent, + .remove = tr::lng_bot_verify_user_remove, + }; + } + } else if (peer->isBroadcast()) { + return { + .title = tr::lng_bot_verify_channel_title, + .text = tr::lng_bot_verify_channel_text, + .about = tr::lng_bot_verify_channel_about, + .submit = tr::lng_bot_verify_channel_submit, + .sent = tr::lng_bot_verify_channel_sent, + .remove = tr::lng_bot_verify_channel_remove, + }; + } + return { + .title = tr::lng_bot_verify_group_title, + .text = tr::lng_bot_verify_group_text, + .about = tr::lng_bot_verify_group_about, + .submit = tr::lng_bot_verify_group_submit, + .sent = tr::lng_bot_verify_group_sent, + .remove = tr::lng_bot_verify_group_remove, + }; +} diff --git a/Telegram/SourceFiles/boxes/peers/verify_peers_box.h b/Telegram/SourceFiles/boxes/peers/verify_peers_box.h new file mode 100644 index 000000000..6383255ee --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/verify_peers_box.h @@ -0,0 +1,36 @@ +/* +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/object_ptr.h" +#include "lang/lang_keys.h" + +class PeerData; +class UserData; + +namespace Ui { +class BoxContent; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +[[nodiscard]] object_ptr MakeVerifyPeersBox( + not_null window, + not_null bot); + +struct BotVerifyPhrases { + tr::phrase<> title; + tr::phrase text; + tr::phrase<> about; + tr::phrase<> submit; + tr::phrase sent; + tr::phrase<> remove; +}; +[[nodiscard]] BotVerifyPhrases PeerVerifyPhrases(not_null peer); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 89e0edfed..2ef807604 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -150,6 +150,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_business_subtitle_chat_intro(); case PremiumFeature::ChatLinks: return tr::lng_business_subtitle_chat_links(); + case PremiumFeature::FilterTags: + return tr::lng_premium_summary_subtitle_filter_tags(); } Unexpected("PremiumFeature in SectionTitle."); } @@ -213,6 +215,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_business_about_chat_intro(); case PremiumFeature::ChatLinks: return tr::lng_business_about_chat_links(); + case PremiumFeature::FilterTags: + return tr::lng_premium_summary_about_filter_tags(); } Unexpected("PremiumFeature in SectionTitle."); } @@ -543,6 +547,7 @@ struct VideoPreviewDocument { case PremiumFeature::BusinessBots: return "business_bots"; case PremiumFeature::ChatIntro: return "business_intro"; case PremiumFeature::ChatLinks: return "business_links"; + case PremiumFeature::FilterTags: return "folder_tags"; } return ""; }(); @@ -1642,6 +1647,11 @@ void TelegramBusinessPreviewBox( tr::lng_business_about_chat_links, st::settingsBusinessPromoChatLinks); break; + case PremiumFeature::FilterTags: push( + tr::lng_premium_summary_subtitle_filter_tags, + tr::lng_premium_summary_about_filter_tags, + st::settingsPremiumIconTags); + break; } } diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 4dae5c30d..e631c9789 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -71,6 +71,7 @@ enum class PremiumFeature { MessagePrivacy, Business, Effects, + FilterTags, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/boxes/self_destruction_box.cpp b/Telegram/SourceFiles/boxes/self_destruction_box.cpp index 210735063..c4a070c16 100644 --- a/Telegram/SourceFiles/boxes/self_destruction_box.cpp +++ b/Telegram/SourceFiles/boxes/self_destruction_box.cpp @@ -7,20 +7,89 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/self_destruction_box.h" +#include "api/api_authorizations.h" +#include "api/api_cloud_password.h" +#include "api/api_self_destruct.h" +#include "apiwrap.h" +#include "boxes/passcode_box.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" -#include "apiwrap.h" -#include "api/api_self_destruct.h" -#include "api/api_authorizations.h" -#include "main/main_session.h" -#include "styles/style_layers.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/popup_menu.h" #include "styles/style_boxes.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_widgets.h" namespace { using Type = SelfDestructionBox::Type; +void AddDeleteAccount( + not_null box, + not_null session) { + if (!session->isTestMode()) { + return; + } + const auto maybeState = session->api().cloudPassword().stateCurrent(); + if (!maybeState || !maybeState->hasPassword) { + return; + } + const auto top = box->addTopButton(st::infoTopBarMenu); + const auto menu + = top->lifetime().make_state>(); + const auto handler = [=] { + session->api().cloudPassword().state( + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](const Core::CloudPasswordState &state) { + auto fields = PasscodeBox::CloudFields::From(state); + fields.customTitle = tr::lng_settings_destroy_title(); + fields.customDescription = tr::lng_context_mark_read_all_sure_2( + tr::now, + Ui::Text::RichLangValue).text; + fields.customSubmitButton = tr::lng_theme_delete(); + fields.customCheckCallback = [=]( + const Core::CloudPasswordResult &result, + QPointer box) { + session->api().request(MTPaccount_DeleteAccount( + MTP_flags(MTPaccount_DeleteAccount::Flag::f_password), + MTP_string("Manual"), + result.result + )).done([=] { + if (box) { + box->uiShow()->hideLayer(); + } + }).fail([=](const MTP::Error &error) { + if (box) { + box->handleCustomCheckError(error.type()); + } + }).send(); + }; + box->uiShow()->showBox(Box(session, fields)); + }, top->lifetime()); + }; + top->setClickedCallback([=] { + *menu = base::make_unique_q( + top, + st::popupMenuWithIcons); + + const auto addAction = Ui::Menu::CreateAddActionCallback(menu->get()); + addAction({ + .text = tr::lng_settings_destroy_title(tr::now), + .handler = handler, + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); + (*menu)->popup(QCursor::pos()); + }); +} + [[nodiscard]] std::vector Values(Type type) { switch (type) { case Type::Account: return { 30, 90, 180, 365, 548, 720 }; @@ -151,4 +220,6 @@ void SelfDestructionBox::prepare() { } else { showContent(); } + + AddDeleteAccount(this, _session); } diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index e42151d75..204afadac 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -463,7 +463,7 @@ void SendCreditsBox( }), session, st::creditsBoxButtonLabel, - box->getDelegate()->style().button.textFg->c); + &box->getDelegate()->style().button.textFg); const auto buttonWidth = st::boxWidth - rect::m::sum::h(stBox.buttonPadding); @@ -524,7 +524,7 @@ not_null SetButtonMarkedLabel( rpl::producer text, Fn update)> context, const style::FlatLabel &st, - std::optional textFg) { + const style::color *textFg) { const auto buttonLabel = Ui::CreateChild( button, rpl::single(QString()), @@ -539,7 +539,10 @@ not_null SetButtonMarkedLabel( context([=] { buttonLabel->update(); })); }, buttonLabel->lifetime()); if (textFg) { - buttonLabel->setTextColorOverride(textFg); + buttonLabel->setTextColorOverride((*textFg)->c); + style::PaletteChanged() | rpl::start_with_next([=] { + buttonLabel->setTextColorOverride((*textFg)->c); + }, buttonLabel->lifetime()); } button->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { @@ -561,7 +564,7 @@ not_null SetButtonMarkedLabel( rpl::producer text, not_null session, const style::FlatLabel &st, - std::optional textFg) { + const style::color *textFg) { return SetButtonMarkedLabel(button, text, [=](Fn update) { return Core::MarkedTextContext{ .session = session, diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index 08188ae22..cc84b379e 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -43,14 +43,14 @@ not_null SetButtonMarkedLabel( rpl::producer text, Fn update)> context, const style::FlatLabel &st, - std::optional textFg = {}); + const style::color *textFg = nullptr); not_null SetButtonMarkedLabel( not_null button, rpl::producer text, not_null session, const style::FlatLabel &st, - std::optional textFg = {}); + const style::color *textFg = nullptr); void SendStarGift( not_null session, diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 2337d712c..0031f2d42 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -228,7 +228,7 @@ SendFilesCheck DefaultCheckForPeer( } SendFilesCheck DefaultCheckForPeer( - std::shared_ptr show, + std::shared_ptr show, not_null peer) { return [=]( const Ui::PreparedFile &file, @@ -236,7 +236,7 @@ SendFilesCheck DefaultCheckForPeer( bool silent) { const auto error = Data::FileRestrictionError(peer, file, compress); if (error && !silent) { - show->showToast(*error); + Data::ShowSendErrorToast(show, peer, error); } return !error.has_value(); }; diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 6646ec107..9b4123d1c 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -80,7 +80,7 @@ using SendFilesCheck = Fn controller, not_null peer); [[nodiscard]] SendFilesCheck DefaultCheckForPeer( - std::shared_ptr show, + std::shared_ptr show, not_null peer); using SendFilesConfirmed = FnapplyChatFilter(id); scrollToY(0); - }); + }, + Window::GifPauseReason::Layer); chatsFilters->lower(); chatsFilters->heightValue() | rpl::start_with_next([this](int h) { updateScrollSkips(); @@ -1512,26 +1513,11 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( return; } - const auto error = [&] { - for (const auto thread : result) { - const auto error = GetErrorTextForSending( - thread, - { .forward = &items, .text = &comment }); - if (!error.isEmpty()) { - return std::make_pair(error, thread); - } - } - return std::make_pair(QString(), result.front()); - }(); - if (!error.first.isEmpty()) { - auto text = TextWithEntities(); - if (result.size() > 1) { - text.append( - Ui::Text::Bold(error.second->chatListName()) - ).append("\n\n"); - } - text.append(error.first); - show->showBox(Ui::MakeInformBox(text)); + const auto error = GetErrorForSending( + result, + { .forward = &items, .text = &comment }); + if (error.error) { + show->showBox(MakeSendErrorBox(error, result.size() > 1)); return; } @@ -1748,30 +1734,13 @@ void FastShareLink( return; } - const auto error = [&] { - for (const auto thread : result) { - const auto error = GetErrorTextForSending( - thread, - { .text = &comment }); - if (!error.isEmpty()) { - return std::make_pair(error, thread); - } - } - return std::make_pair(QString(), result.front()); - }(); - if (!error.first.isEmpty()) { - auto text = TextWithEntities(); - if (result.size() > 1) { - text.append( - Ui::Text::Bold(error.second->chatListName()) - ).append("\n\n"); - } - text.append(error.first); + const auto error = GetErrorForSending( + result, + { .text = &comment }); + if (error.error) { if (const auto weak = *box) { - weak->getDelegate()->show(Ui::MakeConfirmBox({ - .text = text, - .inform = true, - })); + weak->getDelegate()->show( + MakeSendErrorBox(error, result.size() > 1)); } return; } diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 781e008d2..2ba9fddc2 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/star_gift_box.h" +#include "apiwrap.h" #include "base/event_filter.h" #include "base/random.h" +#include "base/timer_rpl.h" #include "base/unixtime.h" #include "api/api_premium.h" +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/gift_premium_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/send_credits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -23,16 +27,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_credits.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_origin.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/admin_log/history_admin_log_item.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/peer_gifts/info_peer_gifts_common.h" +#include "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "lottie/lottie_common.h" #include "lottie/lottie_single_player.h" @@ -52,14 +59,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/layers/generic_box.h" +#include "ui/new_badges.h" #include "ui/painter.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/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/shadow.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_session_controller.h" @@ -68,16 +79,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_premium.h" #include "styles/style_settings.h" +#include + namespace Ui { namespace { constexpr auto kPriceTabAll = 0; constexpr auto kPriceTabLimited = -1; +constexpr auto kPriceTabInStock = -2; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); +constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); +constexpr auto kCrossfadeDuration = crl::time(400); +constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); using namespace HistoryView; using namespace Info::PeerGifts; @@ -97,30 +115,31 @@ struct GiftDetails { TextWithEntities text; uint64 randomId = 0; bool anonymous = false; + bool upgraded = false; }; class PreviewDelegate final : public DefaultElementDelegate { public: PreviewDelegate( not_null parent, - not_null st, + not_null st, Fn update); bool elementAnimationsPaused() override; - not_null elementPathShiftGradient() override; + not_null elementPathShiftGradient() override; Context elementContext() override; private: const not_null _parent; - const std::unique_ptr _pathGradient; + const std::unique_ptr _pathGradient; }; -class PreviewWrap final : public Ui::RpWidget { +class PreviewWrap final : public RpWidget { public: PreviewWrap( not_null parent, - not_null session, + not_null recipient, rpl::producer details); ~PreviewWrap(); @@ -131,8 +150,9 @@ private: void prepare(rpl::producer details); const not_null _history; - const std::unique_ptr _theme; - const std::unique_ptr _style; + const not_null _recipient; + const std::unique_ptr _theme; + const std::unique_ptr _style; const std::unique_ptr _delegate; AdminLog::OwnedItem _item; QPoint _position; @@ -156,9 +176,13 @@ private: return is(now) || is(now.addDays(1)) || is(now.addDays(-1)); } +[[nodiscard]] bool IsSoldOut(const Data::StarGift &info) { + return info.limitedCount && !info.limitedLeft; +} + PreviewDelegate::PreviewDelegate( not_null parent, - not_null st, + not_null st, Fn update) : _parent(parent) , _pathGradient(MakePathShiftGradient(st, update)) { @@ -169,7 +193,7 @@ bool PreviewDelegate::elementAnimationsPaused() { } auto PreviewDelegate::elementPathShiftGradient() --> not_null { +-> not_null { return _pathGradient.get(); } @@ -180,6 +204,7 @@ Context PreviewDelegate::elementContext() { auto GenerateGiftMedia( not_null parent, Element *replacing, + not_null recipient, const GiftDetails &data) -> Fn)>)> { return [=](Fn)> push) { @@ -195,9 +220,11 @@ auto GenerateGiftMedia( push(std::make_unique( std::move(text), margins, + st::defaultTextStyle, links, context)); }; + const auto sticker = [=] { using Tag = ChatHelpers::StickerLottieSize; const auto session = &parent->history()->session(); @@ -220,26 +247,38 @@ auto GenerateGiftMedia( lt_count, gift.months); }, [&](const GiftTypeStars &gift) { - return tr::lng_action_gift_got_subtitle( - tr::now, - lt_user, - parent->history()->session().user()->shortName()); + return recipient->isSelf() + ? tr::lng_action_gift_self_subtitle(tr::now) + : tr::lng_action_gift_got_subtitle( + tr::now, + lt_user, + recipient->session().user()->shortName()); }); auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_about( tr::now, - Ui::Text::RichLangValue); + Text::RichLangValue); }, [&](const GiftTypeStars &gift) { - return tr::lng_action_gift_got_stars_text( - tr::now, - lt_count, - gift.info.starsConverted, - Ui::Text::RichLangValue); + return data.upgraded + ? tr::lng_action_gift_got_upgradable_text( + tr::now, + Text::RichLangValue) + : (recipient->isSelf() && gift.info.starsToUpgrade) + ? tr::lng_action_gift_self_about_unique( + tr::now, + Text::RichLangValue) + : (recipient->isSelf() + ? tr::lng_action_gift_self_about + : tr::lng_action_gift_got_stars_text)( + tr::now, + lt_count, + gift.info.starsConverted, + Text::RichLangValue); }); auto description = data.text.empty() ? std::move(textFallback) : data.text; - pushText(Ui::Text::Bold(title), st::giftBoxPreviewTitlePadding); + pushText(Text::Bold(title), st::giftBoxPreviewTitlePadding); pushText( std::move(description), st::giftBoxPreviewTextPadding, @@ -248,17 +287,81 @@ auto GenerateGiftMedia( .session = &parent->history()->session(), .customEmojiRepaint = [parent] { parent->repaint(); }, }); + + push(HistoryView::MakeGenericButtonPart( + (data.upgraded + ? tr::lng_gift_view_unpack(tr::now) + : tr::lng_sticker_premium_view(tr::now)), + st::giftBoxButtonMargin, + [parent] { parent->repaint(); }, + nullptr)); }; } +[[nodiscard]] QImage CreateGradient( + QSize size, + const Data::UniqueGift &gift) { + const auto ratio = style::DevicePixelRatio(); + auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + auto gradient = QRadialGradient( + QRect(QPoint(), size).center(), + size.height() / 2); + gradient.setStops({ + { 0., gift.backdrop.centerColor }, + { 1., gift.backdrop.edgeColor }, + }); + p.setBrush(gradient); + p.setPen(Qt::NoPen); + p.drawRect(QRect(QPoint(), size)); + p.end(); + + const auto mask = Images::CornersMask(st::boxRadius); + return Images::Round(std::move(result), mask, RectPart::FullTop); +} + +void PrepareImage( + QImage &image, + not_null emoji, + const PatternPoint &point, + const Data::UniqueGift &gift) { + if (!image.isNull() || !emoji->ready()) { + return; + } + const auto ratio = style::DevicePixelRatio(); + const auto size = Emoji::GetSizeNormal() / ratio; + image = QImage( + 2 * QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(ratio); + image.fill(Qt::transparent); + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(point.opacity); + if (point.scale < 1.) { + p.translate(size, size); + p.scale(point.scale, point.scale); + p.translate(-size, -size); + } + const auto shift = (2 * size - (Emoji::GetSizeLarge() / ratio)) / 2; + emoji->paint(p, { + .textColor = gift.backdrop.patternColor, + .position = QPoint(shift, shift), + }); +} + PreviewWrap::PreviewWrap( not_null parent, - not_null session, + not_null recipient, rpl::producer details) : RpWidget(parent) -, _history(session->data().history(session->userPeerId())) +, _history(recipient->owner().history(recipient->session().userPeerId())) +, _recipient(recipient) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) -, _style(std::make_unique( +, _style(std::make_unique( _history->session().colorIndicesValue())) , _delegate(std::make_unique( parent, @@ -268,14 +371,14 @@ PreviewWrap::PreviewWrap( _style->apply(_theme.get()); using namespace HistoryView; - session->data().viewRepaintRequest( + _history->owner().viewRepaintRequest( ) | rpl::start_with_next([=](not_null view) { if (view == _item.get()) { update(); } }, lifetime()); - session->downloaderTaskFinished() | rpl::start_with_next([=] { + _history->session().downloaderTaskFinished() | rpl::start_with_next([=] { update(); }, lifetime()); @@ -284,7 +387,8 @@ PreviewWrap::PreviewWrap( void ShowSentToast( not_null window, - const GiftDescriptor &descriptor) { + const GiftDescriptor &descriptor, + const GiftDetails &details) { const auto &st = st::historyPremiumToast; const auto skip = st.padding.top(); const auto size = st.style.font->height * 2; @@ -295,13 +399,15 @@ void ShowSentToast( auto text = v::match(descriptor, [&](const GiftTypePremium &gift) { return tr::lng_action_gift_premium_about( tr::now, - Ui::Text::RichLangValue); + Text::RichLangValue); }, [&](const GiftTypeStars &gift) { + const auto amount = gift.info.stars + + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( tr::now, lt_count, - gift.info.stars, - Ui::Text::RichLangValue); + amount, + Text::RichLangValue); }); const auto strong = window->showToast({ .title = tr::lng_gift_sent_title(tr::now), @@ -315,7 +421,7 @@ void ShowSentToast( return; } const auto widget = strong->widget(); - const auto preview = Ui::CreateChild(widget.get()); + const auto preview = CreateChild(widget.get()); preview->moveToLeft(skip, skip); preview->resize(size, size); preview->show(); @@ -358,17 +464,23 @@ void PreviewWrap::prepare(rpl::producer details) { const auto cost = v::match(descriptor, [&](GiftTypePremium data) { return FillAmountAndCurrency(data.cost, data.currency, true); }, [&](GiftTypeStars data) { - return tr::lng_gift_stars_title( - tr::now, - lt_count, - data.info.stars); + const auto stars = data.info.stars + + (details.upgraded ? data.info.starsToUpgrade : 0); + return stars + ? tr::lng_gift_stars_title(tr::now, lt_count, stars) + : QString(); }); - const auto text = tr::lng_action_gift_received( - tr::now, - lt_user, - _history->session().user()->shortName(), - lt_cost, - cost); + const auto name = _history->session().user()->shortName(); + const auto text = cost.isEmpty() + ? tr::lng_action_gift_unique_received(tr::now, lt_user, name) + : _recipient->isSelf() + ? tr::lng_action_gift_self_bought(tr::now, lt_cost, cost) + : tr::lng_action_gift_received( + tr::now, + lt_user, + name, + lt_cost, + cost); const auto item = _history->makeMessage({ .id = _history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeAboutView @@ -380,7 +492,7 @@ void PreviewWrap::prepare(rpl::producer details) { auto owned = AdminLog::OwnedItem(_delegate.get(), item); owned->overrideMedia(std::make_unique( owned.get(), - GenerateGiftMedia(owned.get(), _item.get(), details), + GenerateGiftMedia(owned.get(), _item.get(), _recipient, details), MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, .service = true, @@ -398,6 +510,13 @@ void PreviewWrap::prepare(rpl::producer details) { }) | rpl::start_with_next([=](int width) { resizeTo(width); }, lifetime()); + + _history->owner().itemResizeRequest( + ) | rpl::start_with_next([=](not_null item) { + if (_item && item == _item->data() && width() >= st::msgMinWidth) { + resizeTo(width()); + } + }, lifetime()); } void PreviewWrap::resizeTo(int width) { @@ -554,6 +673,8 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { return simple(tr::lng_gift_stars_tabs_all(tr::now)); } else if (price == kPriceTabLimited) { return simple(tr::lng_gift_stars_tabs_limited(tr::now)); + } else if (price == kPriceTabInStock) { + return simple(tr::lng_gift_stars_tabs_in_stock(tr::now)); } auto &manager = session->data().customEmojiManager(); auto result = Text::String(); @@ -589,18 +710,33 @@ struct GiftPriceTabs { struct State { rpl::variable> prices; rpl::variable priceTab = kPriceTabAll; + rpl::variable fullWidth; std::vector