diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 294f1ad3ef..1746d0f0fe 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -4,8 +4,6 @@ on: push: branches: [dev] paths: [changelog.txt] - release: - types: [published] workflow_dispatch: permissions: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3c81a42947..e564367b27 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -54,7 +54,7 @@ jobs: linux: name: Rocky Linux 8 - runs-on: ubuntu-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }} strategy: matrix: diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index c80ff4db98..02398ecdca 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -40,7 +40,7 @@ jobs: macos: name: MacOS - runs-on: macos-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }} strategy: matrix: diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 2e91e85491..ecdd2ad71e 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -40,7 +40,7 @@ jobs: macos: name: MacOS - runs-on: macos-latest + runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }} strategy: matrix: diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 0e36af68c5..fa531fb256 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -44,7 +44,7 @@ jobs: windows: name: Windows - runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || 'windows-latest' }} + runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || ((github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-windows-latest-16' || 'windows-latest') }} strategy: matrix: @@ -101,7 +101,7 @@ jobs: git config --global user.email "you@example.com" git config --global user.name "Sample" - - uses: ilammy/msvc-dev-cmd@v1.13.0 + - uses: Eden-CI/msvc-dev-cmd@master name: Native Tools Command Prompt. with: arch: ${{ matrix.arch }} diff --git a/AGENTS.md b/AGENTS.md index ec1650302b..78db1ff125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,18 @@ QString currentTitle = tr::lng_settings_title(tr::now); rpl::producer nameProducer = GetNameProducer(); ``` +**Use `_q` for QString literals:** + +Prefer the project literal `u"..."_q` instead of the verbose `QStringLiteral("...")` macro when creating `QString` values: + +```cpp +// Prefer this: +auto text = u"Settings"_q; + +// Instead of this: +auto text = QStringLiteral("Settings"); +``` + ## API Usage ### API Schema Files diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 139b4f8fdf..675e795e35 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -391,6 +391,10 @@ PRIVATE boxes/transfer_gift_box.h boxes/compose_ai_box.cpp boxes/compose_ai_box.h + boxes/create_ai_tone_box.cpp + boxes/create_ai_tone_box.h + boxes/preview_ai_tone_box.cpp + boxes/preview_ai_tone_box.h boxes/translate_box.cpp boxes/translate_box.h boxes/url_auth_box.cpp @@ -595,6 +599,8 @@ PRIVATE data/components/passkeys.h data/components/promo_suggestions.cpp data/components/promo_suggestions.h + data/components/recent_inline_bots.cpp + data/components/recent_inline_bots.h data/components/recent_peers.cpp data/components/recent_peers.h data/components/recent_shared_media_gifts.cpp @@ -620,6 +626,8 @@ PRIVATE data/data_abstract_sparse_ids.h data/data_abstract_structure.cpp data/data_abstract_structure.h + data/data_ai_compose_tones.cpp + data/data_ai_compose_tones.h data/data_audio_msg_id.cpp data/data_audio_msg_id.h data/data_auto_download.cpp @@ -1802,6 +1810,8 @@ PRIVATE ui/chat/sponsored_message_bar.h ui/controls/compose_ai_button_factory.cpp ui/controls/compose_ai_button_factory.h + ui/controls/custom_emoji_toast_icon.cpp + ui/controls/custom_emoji_toast_icon.h ui/controls/emoji_button_factory.cpp ui/controls/emoji_button_factory.h ui/controls/location_picker.cpp @@ -1812,6 +1822,8 @@ PRIVATE ui/controls/table_rows.h ui/controls/userpic_button.cpp ui/controls/userpic_button.h + ui/controls/warning_tooltip.cpp + ui/controls/warning_tooltip.h ui/effects/credits_graphics.cpp ui/effects/credits_graphics.h ui/effects/emoji_fly_animation.cpp diff --git a/Telegram/Resources/animations/settings/chat_automation.tgs b/Telegram/Resources/animations/settings/chat_automation.tgs new file mode 100644 index 0000000000..703ceb2e61 Binary files /dev/null and b/Telegram/Resources/animations/settings/chat_automation.tgs differ diff --git a/Telegram/Resources/icons/chat/ai_style_tone.svg b/Telegram/Resources/icons/chat/ai_style_tone.svg new file mode 100644 index 0000000000..778574b679 --- /dev/null +++ b/Telegram/Resources/icons/chat/ai_style_tone.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Telegram/Resources/icons/chat/refresh.svg b/Telegram/Resources/icons/chat/refresh.svg new file mode 100644 index 0000000000..10e1e400ac --- /dev/null +++ b/Telegram/Resources/icons/chat/refresh.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/icons/menu/edit_stars_add.svg b/Telegram/Resources/icons/menu/edit_stars_add.svg new file mode 100644 index 0000000000..04b6a8c915 --- /dev/null +++ b/Telegram/Resources/icons/menu/edit_stars_add.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Telegram/Resources/icons/poll/filled/filled_poll_country.svg b/Telegram/Resources/icons/poll/filled/filled_poll_country.svg new file mode 100644 index 0000000000..bf77eb160f --- /dev/null +++ b/Telegram/Resources/icons/poll/filled/filled_poll_country.svg @@ -0,0 +1,13 @@ + + + Filled / filled_poll_country + + + + + + + + + + diff --git a/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg b/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg new file mode 100644 index 0000000000..52b73e4d19 --- /dev/null +++ b/Telegram/Resources/icons/poll/filled/filled_poll_subscribers.svg @@ -0,0 +1,7 @@ + + + Filled / filled_poll_subscribers + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6a39a7e75f..e568d0ba54 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3317,6 +3317,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_box_history_entry_gift_sold_to" = "To"; "lng_credits_box_history_entry_gift_full_price" = "Full Price"; "lng_credits_box_history_entry_gift_bought_from" = "From"; +"lng_credits_box_history_entry_gift_offer" = "Gift Offer"; "lng_credits_subscription_section" = "My subscriptions"; "lng_credits_box_subscription_title" = "Subscription"; @@ -3449,11 +3450,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_business_limit_reached#one" = "Limit of {count} message reached."; "lng_business_limit_reached#other" = "Limit of {count} messages reached."; -"lng_chatbots_title" = "Chatbots"; -"lng_chatbots_about" = "Add a bot to your account to help you automatically process and respond to the messages you receive. {link}"; -"lng_chatbots_about_link" = "Learn more..."; "lng_chatbots_placeholder" = "Enter bot URL or username"; -"lng_chatbots_add_about" = "Enter the link to the Telegram bot that you want to automatically process your chats."; "lng_chatbots_access_title" = "Chats accessible for the bot"; "lng_chatbots_all_except" = "All 1-to-1 Chats Except..."; "lng_chatbots_selected" = "Only Selected Chats"; @@ -3491,11 +3488,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chatbots_manage_stories" = "Manage Stories"; -"lng_chatbots_remove" = "Remove Bot"; "lng_chatbots_not_found" = "Chatbot not found."; "lng_chatbots_not_supported" = "This bot doesn't support Telegram Business yet."; "lng_chatbots_add" = "Add"; -"lng_chatbots_info_url" = "https://telegram.org/blog/telegram-business#chatbots-for-business"; +"lng_chatbots_remove_bot" = "Remove Bot"; +"lng_chatbots_leave_without_added_title" = "No Bot Added"; +"lng_chatbots_leave_without_added_text" = "You haven't added a bot to manage your account. Leave anyway?"; +"lng_chatbots_added_success" = "{bot} now manages your account."; + +"lng_chat_automation_title" = "Chat Automation"; +"lng_chat_automation_about" = "Add a bot to answer messages on your behalf."; +"lng_chat_automation_add_about" = "Choose a bot to manage your chats automatically."; +"lng_settings_chat_automation_label" = "Chat automation"; +"lng_settings_chat_automation_off" = "Off"; "lng_chatbot_status_can_reply" = "bot manages this chat"; "lng_chatbot_status_paused" = "bot paused"; "lng_chatbot_status_views" = "bot has access to this chat"; @@ -4501,6 +4506,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_inline_bot_no_results" = "No results."; "lng_inline_bot_via" = "via {inline_bot}"; +"lng_guest_chat_for" = "for {user}"; "lng_box_remove" = "Remove"; @@ -4528,6 +4534,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_masks_count#other" = "{count} masks"; "lng_custom_emoji_count#one" = "{count} emoji"; "lng_custom_emoji_count#other" = "{count} emoji"; +"lng_search_back_to_results" = "Back to search"; +"lng_search_results_header" = "Search Result"; "lng_stickers_attached_sets" = "Sets of attached stickers"; "lng_custom_emoji_used_sets" = "Sets of used emoji"; "lng_custom_emoji_remove_pack_button" = "Remove Emoji"; @@ -4618,10 +4626,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_audio_count#other" = "{count} audio"; "lng_ban_user" = "Ban User"; +"lng_ban_specific_user" = "Ban {user}"; "lng_ban_users" = "Ban users"; "lng_restrict_users" = "Restrict users"; "lng_delete_all_from_user" = "Delete all from {user}"; -"lng_delete_all_from_users" = "Delete all from users"; "lng_restrict_user#one" = "Restrict user"; "lng_restrict_user#other" = "Restrict users"; "lng_restrict_user_part" = "Partially restrict this user {emoji}"; @@ -5595,6 +5603,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_selected_delete_sure_this" = "Do you want to delete this message?"; "lng_selected_delete_sure#one" = "Do you want to delete {count} message?"; "lng_selected_delete_sure#other" = "Do you want to delete {count} messages?"; +"lng_delete_title_message_one" = "Delete this message"; +"lng_delete_title_message_many#one" = "Delete {count} message"; +"lng_delete_title_message_many#other" = "Delete {count} messages"; +"lng_delete_title_reaction_this" = "Delete this reaction"; +"lng_delete_title_reaction_all" = "Delete all reactions"; +"lng_delete_label_also_this_reaction" = "Also delete this reaction."; +"lng_delete_label_also_some_reactions" = "Also delete all reactions from some participants."; +"lng_delete_label_also_all_reactions" = "Also delete all reactions."; +"lng_delete_sub_messages" = "Delete all messages"; +"lng_delete_sub_reactions" = "Delete all reactions"; +"lng_context_delete_this_reaction" = "Delete this reaction"; "lng_selected_remove_saved_music" = "Do you want to remove this file from your profile?"; "lng_saved_music_added" = "Audio added to your Profile."; "lng_saved_music_removed" = "Audio removed from your Profile."; @@ -6368,7 +6387,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_chat_send_media" = "Send media"; "lng_rights_chat_send_stickers" = "Send stickers & GIFs"; "lng_rights_chat_send_links" = "Embed links"; -"lng_rights_chat_send_polls" = "Send polls"; +"lng_rights_chat_send_reactions" = "Reactions"; +"lng_rights_chat_send_polls" = "Polls"; "lng_rights_chat_add_members" = "Add members"; "lng_rights_chat_photos" = "Photos"; "lng_rights_chat_videos" = "Video files"; @@ -6419,6 +6439,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_restricted_send_gifs" = "The admins of this group have restricted your ability to send GIFs."; "lng_restricted_send_inline" = "The admins of this group have restricted your ability to send inline content."; "lng_restricted_send_polls" = "The admins of this group have restricted your ability to send polls."; +"lng_restricted_send_reactions_click" = "You cannot send reactions in this chat."; "lng_restricted_boost_group" = "Boost this group to send messages"; "lng_restricted_send_message_until" = "The admins of this group have restricted you from sending messages until {date}, {time}."; @@ -6550,6 +6571,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_edited_message" = "{from} edited message:"; "lng_admin_log_previous_message" = "Original message"; "lng_admin_log_deleted_message" = "{from} deleted message:"; +"lng_admin_log_deleted_messages_collapsed#one" = "{from} deleted {count} message from {names} ({link})."; +"lng_admin_log_deleted_messages_collapsed#other" = "{from} deleted {count} messages from {names} ({link})."; +"lng_admin_log_show_all" = "Show all"; +"lng_admin_log_hide_all" = "Hide all"; +"lng_admin_log_expand_more#one" = "Show {count} More Message"; +"lng_admin_log_expand_more#other" = "Show {count} More Messages"; "lng_admin_log_sent_message" = "{from} sent this message:"; "lng_admin_log_participant_joined" = "{from} joined the group"; "lng_admin_log_participant_joined_channel" = "{from} joined the channel"; @@ -6662,6 +6689,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_banned_send_stickers" = "Send stickers & GIFs"; "lng_admin_log_banned_embed_links" = "Embed links"; "lng_admin_log_banned_send_polls" = "Send polls"; +"lng_admin_log_banned_send_reactions" = "Send reactions"; "lng_admin_log_admin_change_info" = "Change info"; "lng_admin_log_admin_post_messages" = "Post messages"; "lng_admin_log_admin_edit_messages" = "Edit messages"; @@ -6990,7 +7018,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_answers_count#other" = "{count} answers"; "lng_polls_answers_none" = "No answers"; "lng_polls_submit_votes" = "Vote"; +"lng_polls_view_stats" = "View Stats"; "lng_polls_view_results" = "View results"; +"lng_polls_stats_title" = "Poll Stats"; "lng_polls_view_votes#one" = "View Votes ({count})"; "lng_polls_view_votes#other" = "View Votes ({count})"; "lng_polls_admin_votes#one" = "{count} vote {arrow}"; @@ -7038,6 +7068,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_create_poll_ends" = "Poll ends"; "lng_polls_create_hide_results" = "Hide results"; "lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes."; +"lng_polls_create_restrict_to_subscribers" = "Restrict to Subscribers"; +"lng_polls_create_restrict_to_subscribers_about" = "Only subscribers who joined 24+ hours ago can vote."; +"lng_polls_create_limit_by_country" = "Limit by Country"; +"lng_polls_create_limit_by_country_about" = "Only users from selected countries can vote."; +"lng_polls_create_allowed_countries" = "Allowed Countries"; +"lng_polls_create_countries_count#one" = "{count} country"; +"lng_polls_create_countries_count#other" = "{count} countries"; +"lng_polls_create_choose_country" = "Please choose at least one country."; +"lng_polls_create_countries_limit#one" = "You can choose up to {count} country."; +"lng_polls_create_countries_limit#other" = "You can choose up to {count} countries."; "lng_polls_create_duration_custom" = "Custom"; "lng_polls_create_deadline_title" = "Deadline"; "lng_polls_create_deadline_button" = "Set Deadline"; @@ -7055,6 +7095,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes."; "lng_polls_media_uploading_toast_title" = "Please wait"; "lng_polls_media_uploading_toast" = "Poll media is still uploading..."; +"lng_polls_vote_restricted_subscribers_channel" = "Only subscribers of {channel} can vote."; +"lng_polls_vote_restricted_subscribers" = "Only subscribers can vote."; +"lng_polls_vote_restricted_subscribers_recent" = "Only subscribers who joined more than 24 hours ago can vote."; +"lng_polls_vote_restricted_countries_list" = "Only users from {countries} can vote."; +"lng_polls_vote_restricted_countries" = "Only users from selected countries can vote."; "lng_polls_ends_toast" = "Results will appear after the poll ends."; "lng_polls_poll_results_title" = "Poll results"; @@ -7308,6 +7353,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_iv" = "Instant View"; "lng_view_button_stickerset" = "View stickers"; "lng_view_button_emojipack" = "View emoji"; +"lng_view_button_style" = "View Style"; "lng_view_button_collectible" = "View collectible"; "lng_view_button_call" = "Join call"; "lng_view_button_storyalbum" = "View Album"; @@ -7933,11 +7979,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_tab_fix" = "Fix"; "lng_ai_compose_original" = "Original"; "lng_ai_compose_result" = "Result"; +"lng_ai_compose_before" = "Before"; +"lng_ai_compose_after" = "After"; "lng_ai_compose_to_language" = "To {language}"; "lng_ai_compose_name_style" = "{name} ({style})"; "lng_ai_compose_style_neutral" = "Neutral"; "lng_ai_compose_emojify" = "emojify"; "lng_ai_compose_error" = "AI request failed."; +"lng_ai_compose_error_too_long" = "Sorry, this text is too long."; +"lng_ai_compose_tone_invalid" = "This style was deleted or the link is invalid."; "lng_ai_compose_tooltip" = "Rewrite, translate, or correct your text using AI."; "lng_ai_compose_flood_title" = "Daily limit reached"; "lng_ai_compose_flood_text" = "Get {link} for **50x** more AI text transformations per day."; @@ -7946,6 +7996,49 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_select_style" = "Select Style"; "lng_ai_compose_apply_style" = "Apply Style"; "lng_ai_compose_style_tooltip" = "Choose Style"; +"lng_ai_compose_create_tone_title" = "New Style"; +"lng_ai_compose_edit_tone_title" = "Edit Style"; +"lng_ai_compose_tone_icon_title" = "Style Icon"; +"lng_ai_compose_tone_name" = "Name"; +"lng_ai_compose_tone_prompt" = "Prompt"; +"lng_ai_compose_tone_save" = "Save"; +"lng_ai_compose_tone_create" = "Create"; +"lng_ai_compose_tone_author" = "Add a link to my account"; +"lng_ai_compose_tone_name_placeholder" = "Style Name (for example, \"Pirate\")"; +"lng_ai_compose_tone_prompt_placeholder" = "Instructions (for example \"write in bold, nautical tone, light slang (aye, matey), vivid sea imagery, playful swagger, rhythmic phrasing, and adventurous mood\")"; +"lng_ai_compose_tone_edit" = "Edit Style"; +"lng_ai_compose_tone_share" = "Share Style"; +"lng_ai_compose_tone_remove" = "Remove Style"; +"lng_ai_compose_tone_delete" = "Delete Style"; +"lng_ai_compose_tone_remove_sure" = "Are you sure you want to remove this style?"; +"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style? It will be removed for everyone who installed it."; +"lng_ai_compose_tone_link_copied" = "Style link copied."; +"lng_ai_compose_author" = "Style by {user}"; +"lng_ai_compose_tone_warn_icon" = "Please choose an icon."; +"lng_ai_compose_tone_warn_name" = "Please choose a name."; +"lng_ai_compose_tone_warn_prompt" = "Please enter instructions."; +"lng_ai_compose_tone_created" = "{title} Style Created!"; +"lng_ai_compose_tone_updated" = "{title} Style Updated!"; +"lng_ai_compose_tone_created_description" = "Right click the style to edit or share the link."; +"lng_ai_compose_tone_preview_about" = "Add this style to instantly rewrite your messages."; +"lng_ai_compose_tone_preview_add" = "Add Style"; +"lng_ai_compose_tone_preview_add_example" = "Another example"; +"lng_ai_compose_tone_preview_used_by#one" = "Used by {count} person."; +"lng_ai_compose_tone_preview_used_by#other" = "Used by {count} people."; +"lng_ai_compose_tone_preview_created_by" = "Created by {user}"; +"lng_ai_compose_tone_added" = "Style Added"; +"lng_ai_compose_tone_removed" = "Style removed."; +"lng_ai_compose_tone_deleted" = "Style deleted."; +"lng_ai_compose_tone_added_description" = "Tap \"AI\" → \"{name}\" when typing your next long message."; +"lng_ai_compose_tone_saved_limit#one" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** style to add another."; +"lng_ai_compose_tone_saved_limit#other" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** styles to add another."; +"lng_ai_compose_tone_saved_limit_link" = "Premium"; +"lng_ai_compose_tone_saved_limit_final#one" = "You can save up to **{count}** style. Delete one to add another."; +"lng_ai_compose_tone_saved_limit_final#other" = "You can save up to **{count}** styles. Delete one to add another."; +"lng_sr_ai_compose_info" = "About AI Editor"; +"lng_sr_ai_compose_copy_result" = "Copy result"; +"lng_sr_ai_compose_expand_original" = "Expand original"; +"lng_sr_ai_compose_collapse_original" = "Collapse original"; "lng_send_as_file_tooltip" = "Send text as a file."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 2242796cb1..5f85d03405 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -25,7 +25,7 @@ ../../animations/sleep.tgs ../../animations/greeting.tgs ../../animations/location.tgs - ../../animations/robot.tgs + ../../animations/settings/chat_automation.tgs ../../animations/writing.tgs ../../animations/hours.tgs ../../animations/phone.tgs diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 19e8423c97..2de58cb177 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="6.8.1.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 4fb123c543..8841c0a0be 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 6,7,9,0 - PRODUCTVERSION 6,7,9,0 + FILEVERSION 6,8,1,0 + PRODUCTVERSION 6,8,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "6.7.9.0" + VALUE "FileVersion", "6.8.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2026" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "6.7.9.0" + VALUE "ProductVersion", "6.8.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 2383194523..a67f949e25 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 6,7,9,0 - PRODUCTVERSION 6,7,9,0 + FILEVERSION 6,8,1,0 + PRODUCTVERSION 6,8,1,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "6.7.9.0" + VALUE "FileVersion", "6.8.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2026" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "6.7.9.0" + VALUE "ProductVersion", "6.8.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.cpp b/Telegram/SourceFiles/api/api_compose_with_ai.cpp index dfebd67e5f..a6a474e762 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.cpp +++ b/Telegram/SourceFiles/api/api_compose_with_ai.cpp @@ -40,8 +40,8 @@ mtpRequestId ComposeWithAi::request( if (!request.translateToLang.isEmpty()) { flags |= Flag::f_translate_to_lang; } - if (!request.changeTone.isEmpty()) { - flags |= Flag::f_change_tone; + if (request.tone) { + flags |= Flag::f_tone; } if (request.emojify) { flags |= Flag::f_emojify; @@ -53,9 +53,14 @@ mtpRequestId ComposeWithAi::request( request.translateToLang.isEmpty() ? MTPstring() : MTP_string(request.translateToLang), - request.changeTone.isEmpty() - ? MTPstring() - : MTP_string(request.changeTone) + request.tone + ? (request.tone->id + ? MTP_inputAiComposeToneID( + MTP_long(request.tone->id), + MTP_long(request.tone->accessHash)) + : MTP_inputAiComposeToneDefault( + MTP_string(request.tone->defaultTone))) + : MTPInputAiComposeTone() )).done([=, done = std::move(done)]( const MTPmessages_ComposedMessageWithAI &result) mutable { const auto &data = result.data(); diff --git a/Telegram/SourceFiles/api/api_compose_with_ai.h b/Telegram/SourceFiles/api/api_compose_with_ai.h index 9a87aa0542..6690ce3d8c 100644 --- a/Telegram/SourceFiles/api/api_compose_with_ai.h +++ b/Telegram/SourceFiles/api/api_compose_with_ai.h @@ -23,12 +23,25 @@ namespace Api { class ComposeWithAi final { public: + struct ToneRef { + QString defaultTone; + uint64 id = 0; + uint64 accessHash = 0; + }; + struct Request { TextWithEntities text; QString translateToLang; - QString changeTone; + std::optional tone; bool proofread = false; bool emojify = false; + + void setDefaultTone(const QString &type) { + tone = ToneRef{ .defaultTone = type }; + } + void setCustomTone(uint64 id, uint64 accessHash) { + tone = ToneRef{ .id = id, .accessHash = accessHash }; + } }; struct DiffEntity { diff --git a/Telegram/SourceFiles/api/api_credits_history_entry.cpp b/Telegram/SourceFiles/api/api_credits_history_entry.cpp index 2aa3423e85..e54537df0c 100644 --- a/Telegram/SourceFiles/api/api_credits_history_entry.cpp +++ b/Telegram/SourceFiles/api/api_credits_history_entry.cpp @@ -158,6 +158,7 @@ Data::CreditsHistoryEntry CreditsHistoryEntryFromTL( .postsSearch = tl.data().is_posts_search(), .giftUpgraded = tl.data().is_stargift_upgrade(), .giftResale = tl.data().is_stargift_resale(), + .giftOffer = tl.data().is_offer(), .reaction = tl.data().is_reaction(), .refunded = tl.data().is_refund(), .pending = tl.data().is_pending(), diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index a1f68a4110..e451c829f2 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -8,19 +8,207 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_polls.h" #include "api/api_common.h" +#include "api/api_statistics_data_deserialize.h" #include "api/api_text_entities.h" #include "api/api_updates.h" #include "apiwrap.h" +#include "base/call_delayed.h" +#include "base/qt/qt_key_modifiers.h" #include "base/random.h" #include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" #include "data/data_poll.h" #include "data/data_session.h" +#include "data/data_statistics_chart.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" // ShouldSendSilent +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "styles/style_polls.h" +#include "ui/toast/toast.h" +#include "window/window_session_controller.h" + +namespace { + +constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000); + +const auto kSubscribersOnlyVoteErrorPatterns = std::array{ + u"POLL_SUBSCRIBERS_ONLY"_q, + u"POLL_MEMBER_RESTRICTED"_q, + u"VOTE_SUBSCRIBERS_ONLY"_q, + u"SUBSCRIBERS_ONLY"_q, + u"SUBSCRIBER_REQUIRED"_q, + u"SUBSCRIBER_ONLY"_q, +}; + +const auto kSubscribersJoinedTooRecentlyVoteErrorPatterns = std::array{ + u"POLL_SUBSCRIBERS_TOO_RECENT"_q, + u"VOTE_SUBSCRIBERS_TOO_RECENT"_q, + u"SUBSCRIBERS_TOO_RECENT"_q, + u"SUBSCRIBER_TOO_RECENT"_q, + u"JOINED_TOO_RECENTLY"_q, + u"24_HOURS"_q, +}; + +const auto kCountriesVoteErrorPatterns = std::array{ + u"POLL_COUNTRIES_ISO2"_q, + u"VOTE_COUNTRIES_ISO2"_q, + u"COUNTRIES_ISO2"_q, + u"COUNTRY_RESTRICTED"_q, + u"COUNTRY_ISO2"_q, +}; + +template +[[nodiscard]] bool MatchesErrorPattern( + const QString &type, + const std::array &patterns) { + for (const auto &pattern : patterns) { + if (!pattern.isEmpty() + && type.contains(pattern, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +[[nodiscard]] PollData::VoteRestriction ParseVoteRestrictionError( + const QString &type) { + if (MatchesErrorPattern( + type, + kSubscribersJoinedTooRecentlyVoteErrorPatterns)) { + return PollData::VoteRestriction::SubscribersJoinedTooRecently; + } else if (MatchesErrorPattern( + type, + kSubscribersOnlyVoteErrorPatterns)) { + return PollData::VoteRestriction::SubscribersOnly; + } else if (MatchesErrorPattern( + type, + kCountriesVoteErrorPatterns)) { + return PollData::VoteRestriction::Countries; + } + return PollData::VoteRestriction::None; +} + +void ShowVoteRestrictionToast( + not_null peer, + not_null poll, + PollData::VoteRestriction restriction) { + if (restriction == PollData::VoteRestriction::None) { + return; + } + auto text = PollVoteRestrictionText(restriction, peer, poll); + if (text.text.isEmpty()) { + return; + } + if (const auto window = peer->session().tryResolveWindow(peer)) { + window->showToast({ + .text = std::move(text), + .iconLottie = u"ban"_q, + .iconLottieSize = st::pollToastIconSize, + .duration = kVoteRestrictionToastDuration, + }); + } +} + +#ifdef _DEBUG +[[nodiscard]] Data::StatisticalGraph GenerateMockupPollStats( + const PollData &poll) { + auto chart = Data::StatisticalChart(); + const auto colorKeys = std::array{ + u"BLUE"_q, + u"GREEN"_q, + u"RED"_q, + u"GOLDEN"_q, + u"LIGHTBLUE"_q, + u"LIGHTGREEN"_q, + u"ORANGE"_q, + u"INDIGO"_q, + u"PURPLE"_q, + u"CYAN"_q, + }; + + constexpr auto kPoints = 14; + constexpr auto kOneDay = float64(24 * 60 * 60 * 1000); + constexpr auto kStart = float64(1704067200000); + chart.x.reserve(kPoints); + for (auto i = 0; i != kPoints; ++i) { + chart.x.push_back(kStart + i * kOneDay); + } + chart.timeStep = kOneDay; + + auto lineId = 0; + chart.lines.reserve(poll.answers.size()); + for (const auto &answer : poll.answers) { + auto line = Data::StatisticalChart::Line(); + line.id = ++lineId; + line.idString = u"answer_%1"_q.arg(line.id); + line.name = answer.text.text.trimmed(); + if (line.name.isEmpty()) { + line.name = QString("#%1").arg(line.id); + } + line.colorKey = colorKeys[(line.id - 1) % int(colorKeys.size())]; + line.y.reserve(kPoints); + + auto seed = int64(13 * line.id + 17); + for (const auto byte : answer.option) { + seed += uchar(byte); + } + const auto base = std::max(int64(answer.votes), int64(1)); + for (auto i = 0; i != kPoints; ++i) { + const auto wave = int64( + ((i + line.id) % 5) * ((i + 2 * line.id) % 4)); + const auto trend = int64((i * (line.id + 1)) / 3); + const auto noise = int64((seed + i * 7 + line.id * 11) % 6); + const auto value = std::max( + base + wave + trend + noise - 2, + int64(1)); + line.y.push_back(value); + line.maxValue = std::max(line.maxValue, value); + line.minValue = std::min(line.minValue, value); + } + chart.lines.push_back(std::move(line)); + } + if (chart.lines.empty()) { + auto line = Data::StatisticalChart::Line(); + line.id = 1; + line.idString = u"votes"_q; + line.name = tr::lng_notification_reactions_poll_votes(tr::now); + line.colorKey = u"BLUE"_q; + line.y.reserve(kPoints); + + const auto base = std::max(int64(poll.totalVoters), int64(1)); + for (auto i = 0; i != kPoints; ++i) { + const auto value = std::max( + base + i * 2 + ((i * 5) % 7), + int64(1)); + line.y.push_back(value); + line.maxValue = std::max(line.maxValue, value); + line.minValue = std::min(line.minValue, value); + } + chart.lines.push_back(std::move(line)); + } + + chart.defaultZoomXIndex = { + .min = std::max(0, kPoints - 8), + .max = kPoints - 1, + }; + chart.measure(); + if (chart.maxValue == chart.minValue) { + if (chart.minValue) { + chart.minValue = 0; + } else { + chart.maxValue = 1; + } + } + return { + .chart = std::move(chart), + }; +} +#endif + +} // namespace namespace Api { @@ -151,6 +339,7 @@ void Polls::sendVotes( if (!item) { return; } + const auto peer = item->history()->peer; const auto showSending = poll && !options.empty(); const auto hideSending = [=] { @@ -164,6 +353,12 @@ void Polls::sendVotes( if (showSending) { poll->sendingVotes = options; _session->data().requestItemRepaint(item); + } else if (poll && options.empty() && poll->voted()) { + for (auto &answer : poll->answers) { + answer.chosen = false; + } + ++poll->version; + _session->data().notifyPollUpdateDelayed(poll); } auto prepared = QVector(); @@ -173,16 +368,33 @@ void Polls::sendVotes( ranges::back_inserter(prepared), [](const QByteArray &option) { return MTP_bytes(option); }); const auto requestId = _api.request(MTPmessages_SendVote( - item->history()->peer->input(), + peer->input(), MTP_int(item->id), MTP_vector(prepared) )).done([=](const MTPUpdates &result) { _pollVotesRequestIds.erase(itemId); hideSending(); + if (poll) { + if (poll->voteRestriction() != PollData::VoteRestriction::None) { + poll->setVoteRestriction(PollData::VoteRestriction::None); + _session->data().notifyPollUpdateDelayed(poll); + } + } _session->updates().applyUpdates(result); - }).fail([=] { + }).fail([=](const MTP::Error &error) { _pollVotesRequestIds.erase(itemId); hideSending(); + if (poll) { + const auto restriction = ParseVoteRestrictionError(error.type()); + if (restriction != PollData::VoteRestriction::None) { + poll->setVoteRestriction(restriction); + _session->data().notifyPollUpdateDelayed(poll); + if (const auto item = _session->data().message(itemId)) { + _session->data().requestItemResize(item); + } + ShowVoteRestrictionToast(peer, poll, restriction); + } + } }).send(); _pollVotesRequestIds.emplace(itemId, requestId); } @@ -305,4 +517,65 @@ void Polls::reloadResults(not_null item) { _pollReloadRequestIds.emplace(itemId, requestId); } +void Polls::requestStats( + FullMsgId itemId, + Fn done, + Fn fail) { + const auto item = _session->data().message(itemId); + const auto media = item ? item->media() : nullptr; + const auto poll = media ? media->poll() : nullptr; + if (!item || !item->isRegular() || !poll) { + if (fail) { + fail(QString()); + } + return; + } +#ifdef _DEBUG + if (base::IsCtrlPressed()) { + auto callback = std::move(done); + if (callback) { + constexpr auto kMockupStatsDelay = 2 * crl::time(1000); + auto graph = GenerateMockupPollStats(*poll); + base::call_delayed(kMockupStatsDelay, _session, [=]() mutable { + callback(std::move(graph)); + }); + } + return; + } +#endif + const auto requestGraph = [=](const QString &token) { + _api.request(MTPstats_LoadAsyncGraph( + MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)), + MTP_string(token), + MTP_long(0) + )).done([=](const MTPStatsGraph &result) { + if (done) { + done(Api::StatisticalGraphFromTL(result)); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error.type()); + } + }).send(); + }; + _api.request(MTPstats_GetPollStats( + MTP_flags(MTPstats_GetPollStats::Flags(0)), + item->history()->peer->input(), + MTP_int(item->id) + )).done([=](const MTPstats_PollStats &result) { + auto graph = Api::StatisticalGraphFromTL(result.data().vvotes_graph()); + if (graph.chart || !graph.error.isEmpty() || graph.zoomToken.isEmpty()) { + if (done) { + done(std::move(graph)); + } + } else { + requestGraph(graph.zoomToken); + } + }).fail([=](const MTP::Error &error) { + if (fail) { + fail(error.type()); + } + }).send(); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_polls.h b/Telegram/SourceFiles/api/api_polls.h index 26d46e09af..a7b1491d5f 100644 --- a/Telegram/SourceFiles/api/api_polls.h +++ b/Telegram/SourceFiles/api/api_polls.h @@ -14,6 +14,9 @@ class ApiWrap; class HistoryItem; struct PollData; struct PollMedia; +namespace Data { +struct StatisticalGraph; +} // namespace Data namespace Main { class Session; @@ -45,6 +48,10 @@ public: void deleteAnswer(FullMsgId itemId, const QByteArray &option); void close(not_null item); void reloadResults(not_null item); + void requestStats( + FullMsgId itemId, + Fn done, + Fn fail); private: const not_null _session; diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp index 05b6d93591..62f2458d6c 100644 --- a/Telegram/SourceFiles/api/api_report.cpp +++ b/Telegram/SourceFiles/api/api_report.cpp @@ -144,6 +144,34 @@ auto CreateReportMessagesOrStoriesCallback( }; } +ReactionReportCapabilities GetReactionReportCapabilities( + not_null group, + not_null participant) { + const auto channel = group->asMegagroup(); + return channel + ? ReactionReportCapabilities{ + .canReport = channel->isPublic() && !participant->isSelf(), + .canBan = channel->canRestrictParticipant(participant), + } + : ReactionReportCapabilities(); +} + +void ReportReaction( + std::shared_ptr show, + not_null group, + MsgId messageId, + not_null participant) { + group->session().api().request(MTPmessages_ReportReaction( + group->input(), + MTP_int(messageId.bare), + participant->input() + )).done([=] { + if (show) { + show->showToast(tr::lng_report_thanks(tr::now)); + } + }).send(); +} + void ReportSpam( not_null sender, const MessageIdsList &ids) { diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h index e503102ec5..0b35c408de 100644 --- a/Telegram/SourceFiles/api/api_report.h +++ b/Telegram/SourceFiles/api/api_report.h @@ -53,6 +53,21 @@ void SendPhotoReport( not_null peer) -> Fn)>; +struct ReactionReportCapabilities final { + bool canReport = false; + bool canBan = false; +}; + +[[nodiscard]] ReactionReportCapabilities GetReactionReportCapabilities( + not_null group, + not_null participant); + +void ReportReaction( + std::shared_ptr show, + not_null group, + MsgId messageId, + not_null participant); + void ReportSpam( not_null sender, const MessageIdsList &ids); diff --git a/Telegram/SourceFiles/api/api_stickers_creator.cpp b/Telegram/SourceFiles/api/api_stickers_creator.cpp index 5b1b85d371..f7a037a35a 100644 --- a/Telegram/SourceFiles/api/api_stickers_creator.cpp +++ b/Telegram/SourceFiles/api/api_stickers_creator.cpp @@ -153,7 +153,7 @@ void FillChooseOwnedSetMenu( const auto identifier = set->identifier(); const auto coverDocument = set->lookupThumbnailDocument(); auto thumbnail = coverDocument - ? Ui::MakeDocumentThumbnail( + ? Ui::MakeDocumentThumbnailFit( coverDocument, Data::FileOriginStickerSet(set->id, set->accessHash)) : nullptr; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 1aa8fc9df8..95837ee07d 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" +#include "data/data_ai_compose_tones.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -1209,6 +1210,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), d.vdate(), d.vmessage(), @@ -1252,6 +1254,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(), MTP_long(d.vvia_bot_id().value_or_empty()), MTPlong(), // via_business_bot_id + MTPPeer(), // guestchat_via_from d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(), d.vdate(), d.vmessage(), @@ -2777,6 +2780,10 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().api().ringtones().applyUpdate(); } break; + case mtpc_updateAiComposeTones: { + session().data().aiComposeTones().applyUpdate(); + } break; + case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index f3af5312fb..5cc833ec41 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -116,6 +116,7 @@ struct Userpic { TimeId date = 0; bool dateReacted = false; QString customEntityData; + ReactionId reaction; mutable Ui::PeerUserpicView view; mutable InMemoryKey uniqueKey; }; @@ -128,6 +129,26 @@ struct State { bool scheduled = false; }; +[[nodiscard]] bool ApplyReactionsRemovedToCachedData( + PeersWithReactions &data, + const Data::ReactionsRemoved &update) { + const auto was = data.list.size(); + data.list.erase( + ranges::remove_if(data.list, [&](const PeerWithReaction &entry) { + return !entry.reaction.empty() + && entry.peerWithDate.peer == update.participant->id; + }), + end(data.list)); + const auto removed = int(was - data.list.size()); + if (!removed) { + return false; + } + data.fullReactionsCount = (data.fullReactionsCount > removed) + ? (data.fullReactionsCount - removed) + : 0; + return true; +} + [[nodiscard]] auto Contexts() -> base::flat_map, std::unique_ptr> & { static auto result = base::flat_map< @@ -188,6 +209,22 @@ struct State { context->cachedReacted.erase(j); } }, context->subscriptions[session]); + session->data().reactionsRemoved( + ) | rpl::on_next([=](const Data::ReactionsRemoved &update) { + for (auto &[item, map] : context->cachedReacted) { + if (item->history()->peer->id != update.peer->id) { + continue; + } else if (update.msgId && item->id != update.msgId) { + continue; + } + for (auto &entry : map) { + auto data = entry.second.data.current(); + if (ApplyReactionsRemovedToCachedData(data, update)) { + entry.second.data = std::move(data); + } + } + } + }, context->subscriptions[session]); Data::AmPremiumValue( session ) | rpl::skip(1) | rpl::filter( @@ -443,12 +480,22 @@ bool UpdateUserpics( return resolved.peer != nullptr; }) | ranges::to_vector; - const auto same = ranges::equal( - state->userpics, - peers, - ranges::equal_to(), - [](const Userpic &u) { return std::pair(u.peer.get(), u.date); }, - [](const ResolvedPeer &r) { return std::pair(r.peer, r.date); }); + const auto same = [&] { + if (state->userpics.size() != peers.size()) { + return false; + } + const auto count = state->userpics.size(); + for (auto i = size_t(); i != count; ++i) { + const auto &userpic = state->userpics[i]; + const auto &resolved = peers[i]; + if ((userpic.peer.get() != resolved.peer) + || (userpic.date != resolved.date) + || (userpic.reaction != resolved.reaction)) { + return false; + } + } + return true; + }(); if (same) { return false; } @@ -461,6 +508,7 @@ bool UpdateUserpics( if (i != end(was) && i->view.cloud) { i->date = resolved.date; i->dateReacted = resolved.dateReacted; + i->reaction = resolved.reaction; now.push_back(std::move(*i)); now.back().customEntityData = data; continue; @@ -470,6 +518,7 @@ bool UpdateUserpics( .date = resolved.date, .dateReacted = resolved.dateReacted, .customEntityData = data, + .reaction = resolved.reaction, }); auto &userpic = now.back(); userpic.uniqueKey = peer->userpicUniqueKey(userpic.view); @@ -512,11 +561,15 @@ void RegenerateParticipants(not_null state, int small, int large) { const auto peer = userpic.peer; const auto date = userpic.date; const auto id = peer->id.value; + const auto self = peer->isSelf(); const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id); if (was != end(old)) { was->name = peer->name(); was->date = FormatReadDate(date, currentDate); was->dateReacted = userpic.dateReacted; + was->self = self; + was->customEntityData = userpic.customEntityData; + was->reaction = userpic.reaction; now.push_back(std::move(*was)); continue; } @@ -524,7 +577,9 @@ void RegenerateParticipants(not_null state, int small, int large) { .name = peer->name(), .date = FormatReadDate(date, currentDate), .dateReacted = userpic.dateReacted, + .self = self, .customEntityData = userpic.customEntityData, + .reaction = userpic.reaction, .userpicLarge = GenerateUserpic(userpic, large), .userpicKey = userpic.uniqueKey, .id = id, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 2aa0b19296..c51bc8bca5 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_message_reaction_id.h" #include "data/data_saved_messages.h" #include "data/data_saved_music.h" #include "data/data_saved_sublist.h" @@ -1422,6 +1423,43 @@ void ApiWrap::deleteAllFromParticipantSend( }).send(); } +void ApiWrap::deleteAllReactionsFromParticipant( + not_null peer, + not_null participant, + MsgId originMsgId, + const Data::ReactionId &originReaction) { + _session->data().removeReactionsFromParticipant( + peer, + 0, + participant, + originReaction, + originMsgId); + request(MTPmessages_DeleteParticipantReactions( + peer->input(), + participant->input() + )).send(); +} + +void ApiWrap::deleteParticipantReaction( + not_null peer, + MsgId msgId, + not_null participant, + const Data::ReactionId &reaction) { + _session->data().removeReactionsFromParticipant( + peer, + msgId, + participant, + reaction, + 0); + request(MTPmessages_DeleteParticipantReaction( + peer->input(), + MTP_int(msgId.bare), + participant->input() + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).send(); +} + void ApiWrap::deleteSublistHistory( not_null channel, not_null sublistPeer) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index d968b988c3..4b56ca89f2 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -25,6 +25,7 @@ class Session; } // namespace Main namespace Data { +struct ReactionId; struct UpdatedFileReferences; class WallPaper; struct ResolvedForwardDraft; @@ -237,6 +238,16 @@ public: void deleteAllFromParticipant( not_null channel, not_null from); + void deleteAllReactionsFromParticipant( + not_null peer, + not_null participant, + MsgId originMsgId, + const Data::ReactionId &originReaction); + void deleteParticipantReaction( + not_null peer, + MsgId msgId, + not_null participant, + const Data::ReactionId &reaction); void deleteSublistHistory( not_null parentChat, not_null sublistPeer); diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index bf1caf551f..e320a4c8b3 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -59,7 +59,6 @@ boxPhotoTitlePosition: point(28px, 20px); boxPhotoPadding: margins(28px, 28px, 28px, 18px); boxPhotoCompressedSkip: 20px; boxPhotoCaptionSkip: 8px; -boxPhotoCaptionReplyOverlap: 5px; defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }}; defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }}; @@ -115,10 +114,8 @@ confirmInviteStatus: FlatLabel(confirmInviteAbout) { } confirmInviteAboutPadding: margins(36px, 4px, 36px, 10px); confirmInviteAboutRequestsPadding: margins(36px, 9px, 36px, 15px); -confirmInviteTitleTop: 141px; confirmInvitePhotoSize: 96px; confirmInvitePhotoTop: 33px; -confirmInviteStatusTop: 164px; confirmInviteUserHeight: 100px; confirmInviteUserPhotoSize: 50px; confirmInviteUserPhotoTop: 210px; @@ -349,7 +346,6 @@ themeWarningHeight: 150px; themeWarningTextTop: 60px; aboutWidth: 390px; -aboutVersionTop: -3px; aboutVersionLink: LinkButton(defaultLinkButton) { color: windowSubTextFg; overColor: windowSubTextFg; @@ -386,7 +382,6 @@ connectionUserInputField: InputField(defaultInputField) { } connectionPasswordInputField: InputField(defaultInputField) { } -connectionIPv6Skip: 11px; autolockWidth: 256px; autolockButton: Checkbox(defaultBoxCheckbox) { @@ -529,7 +524,6 @@ colorResultInput: InputField(colorValueInput) { changePhoneButton: RoundButton(defaultActiveButton) { width: 256px; } -changePhoneButtonPadding: margins(0px, 32px, 0px, 44px); changePhoneTitle: FlatLabel(boxTitle) { } changePhoneTitlePadding: margins(0px, 8px, 0px, 8px); @@ -550,18 +544,6 @@ changePhoneError: FlatLabel(changePhoneLabel) { normalBoxLottieSize: size(120px, 120px); -adminLogFilterUserpicLeft: 15px; -adminLogFilterLittleSkip: 16px; -adminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) { - style: TextStyle(boxTextStyle) { - font: font(boxFontSize semibold); - } -} -adminLogFilterSkip: 32px; -adminLogFilterUserCheckbox: Checkbox(defaultBoxCheckbox) { - margin: margins(8px, 6px, 8px, 6px); - checkPosition: point(8px, 6px); -} rightsCheckbox: Checkbox(defaultCheckbox) { textPosition: point(10px, 1px); rippleBg: attentionButtonBgOver; @@ -588,7 +570,6 @@ rightsButtonToggleWidth: 70px; rightsDividerMargin: margins(0px, 0px, 0px, 20px); rightsHeaderMargin: margins(22px, 13px, 22px, 7px); rightsToggleMargin: margins(22px, 8px, 22px, 8px); -rightsAboutMargin: margins(22px, 8px, 22px, 8px); rightsPhotoButton: UserpicButton(defaultUserpicButton) { size: size(60px, 60px); photoSize: 60px; @@ -1001,8 +982,6 @@ contactsWithStories: PeerList(peerListBox) { nameFgChecked: contactsNameFg; } } -storiesReadLineTwice: 2px; -storiesUnreadLineTwice: 4px; requestsAcceptButton: RoundButton(defaultActiveButton) { width: -28px; height: 30px; @@ -1087,7 +1066,6 @@ collectibleHeaderPadding: margins(24px, 16px, 24px, 12px); collectibleOwnerPadding: margins(24px, 4px, 24px, 8px); collectibleInfo: inviteForbiddenInfo; collectibleInfoPadding: margins(24px, 12px, 24px, 12px); -collectibleInfoTonMargins: margins(0px, 3px, 0px, 0px); collectibleMore: RoundButton(defaultActiveButton) { height: 36px; textTop: 9px; @@ -1110,11 +1088,18 @@ moderateBoxUserpic: UserpicButton(defaultUserpicButton) { photoSize: 34px; photoPosition: point(0px, 4px); } -moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }}; +moderateBoxExpand: IconEmoji { + icon: icon {{ "chat/reply_type_group", boxTextFg }}; + padding: margins(1px, 3px, 1px, 0px); + useIconColor: true; +} moderateBoxExpandHeight: 20px; moderateBoxExpandRight: 10px; moderateBoxExpandInnerSkip: 2px; moderateBoxExpandFont: font(11px); +moderateBoxExpandTextStyle: TextStyle(boxTextStyle) { + font: moderateBoxExpandFont; +} moderateBoxExpandToggleSize: 4px; moderateBoxExpandToggleFourStrokes: 3px; moderateBoxExpandIcon: IconEmoji{ @@ -1134,7 +1119,6 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) { } profileQrFont: font(fsize bold); -profileQrCenterSize: 34px; profileQrBackgroundRadius: 12px; profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }}; profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px); @@ -1147,13 +1131,6 @@ foldersMenu: PopupMenu(popupMenuWithIcons) { } } -fakeUserpicButton: UserpicButton(defaultUserpicButton) { - size: size(1px, 1px); - photoSize: 1px; - changeIcon: icon {{ "settings/photo", transparent }}; - uploadBg: transparent; -} - moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { imageRadius: 12px; imageSmallRadius: 11px; @@ -1213,7 +1190,6 @@ createBotStatusLabel: FlatLabel(aboutRevokePublicLabel) { maxHeight: 20px; } -aiComposeBoxSectionSkip: 8px; aiComposeBoxStyleTabsSkip: 8px; aiComposeContentMargin: margins(16px, 0px, 16px, 0px); @@ -1277,12 +1253,121 @@ aiComposeBadge: RoundButton(customEmojiTextBadge) { } aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px); +aiComposeAddStyleIcon: icon {{ "menu/edit_stars_add", aiComposeButtonFg }}; +aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActive }}; + +aiToneIconPreviewSize: 80px; +aiToneIconPreviewBottomSkip: 12px; +aiToneIconPreviewTopSkip: 4px; +aiToneIconPreviewBg: boxBg; +aiToneIconPreviewInnerSize: 54px; + +aiToneIconPreviewPlaceholder: icon {{ "chat/ai_style_tone", windowSubTextFg }}; + +aiToneFieldBg: boxBg; +aiToneFieldRadius: 12px; +aiToneFieldPadding: margins(16px, 12px, 16px, 12px); +aiToneFieldsMargin: margins(16px, 0px, 16px, 0px); +aiToneFieldsSkip: 8px; + +aiToneNameField: InputField(defaultInputField) { + textBg: transparent; + textBgActive: transparent; + textMargins: margins(16px, 12px, 16px, 12px); + border: 0px; + borderActive: 0px; + heightMin: 44px; +} +aiTonePromptField: InputField(newGroupDescription) { + textBg: transparent; + textBgActive: transparent; + textMargins: margins(16px, 12px, 16px, 12px); + border: 0px; + borderActive: 0px; + heightMin: 140px; + heightMax: 240px; +} +aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 80px; + style: TextStyle(defaultTextStyle) { + font: font(14px); + } +} +aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px); +aiToneDeleteButton: RoundButton(defaultActiveButton) { + textFg: attentionButtonFg; + textFgOver: attentionButtonFg; + textBg: boxBg; + textBgOver: boxBg; + height: 42px; + textTop: 12px; + style: semiboldTextStyle; + ripple: defaultRippleAnimation; +} +aiToneDeleteButtonMargin: margins(16px, 8px, 16px, 0px); +aiComposeToneToastIconSize: size(32px, 32px); +aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px); + +aiTonePreviewTitleLabel: FlatLabel(defaultFlatLabel) { + textFg: windowFg; + minWidth: 0px; + maxHeight: 28px; + align: align(top); + style: TextStyle(semiboldTextStyle) { + font: font(17px semibold); + } +} +aiTonePreviewTitleMargin: margins(16px, 6px, 16px, 0px); + +aiTonePreviewAboutLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 20px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(14px); + } +} +aiTonePreviewAboutMargin: margins(28px, 6px, 28px, 6px); + +aiTonePreviewAttributionLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 0px; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineActive; + } +} +aiTonePreviewAttributionMargin: margins(16px, 10px, 16px, 8px); + +aiTonePreviewExampleCardBg: boxBg; +aiTonePreviewExampleCardRadius: 22px; +aiTonePreviewExampleCardPadding: margins(14px, 12px, 16px, 12px); +aiTonePreviewExampleCardSectionSkip: 24px; +aiTonePreviewExampleCardTitleSkip: 6px; +aiTonePreviewExampleCardMargin: margins(16px, 6px, 16px, 0px); +aiTonePreviewBottomSkip: 6px; + +aiTonePreviewAnotherExampleButton: RoundButton(defaultLightButton) { + width: -12px; + height: 26px; + radius: 8px; + textTop: 4px; + style: TextStyle(defaultTextStyle) { + font: font(12px semibold); + } +} +aiTonePreviewAnotherExampleIcon: IconEmoji { + icon: icon {{ "chat/refresh-18x18", lightButtonFg }}; + padding: margins(0px, 1px, 4px, 0px); +} +aiComposeToneRemovedToastIcon: icon {{ "menu/delete", toastFg }}; + aiComposeCardBg: boxBg; aiComposeCardRadius: 22px; aiComposeCardPadding: margins(12px, 16px, 16px, 16px); -aiComposeCardDivider: shadowFg; aiComposeCardSectionSkip: 12px; -aiComposeCardTextSkip: 0px; aiComposeCardControlSkip: 8px; aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) { textFg: windowFg; @@ -1290,6 +1375,8 @@ aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) { maxHeight: 22px; } aiComposeEmojifyCheckbox: Checkbox(defaultBoxCheckbox) { + textFg: windowSubTextFg; + textFgActive: windowSubTextFg; width: 0px; } @@ -1312,6 +1399,17 @@ aiComposeCopyButton: IconButton(aiComposeExpandButton) { iconOver: aiComposeCopyIcon; } +aiComposeAuthorLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + minWidth: 0px; + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineActive; + } +} +aiComposeAuthorLabelTop: 8px; + aiComposeBoxButton: RoundButton(defaultActiveButton) { height: 42px; textTop: 12px; @@ -1337,3 +1435,4 @@ aiComposeBoxInfoButton: IconButton(boxTitleClose) { iconOver: icon {{ "menu/info", boxTitleCloseFgOver }}; ripple: defaultRippleAnimation; } +aiComposeShadowOpacity: 0.3; diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp index 0d51c9d322..ac61c9f0b0 100644 --- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp +++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp @@ -9,20 +9,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_compose_with_ai.h" #include "apiwrap.h" +#include "boxes/create_ai_tone_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/ui_integration.h" +#include "data/data_ai_compose_tones.h" #include "data/data_document.h" +#include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" -#include "data/data_session.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/sections/settings_premium.h" #include "spellcheck/platform/platform_language.h" @@ -46,11 +48,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" +#include "ui/widgets/menu/menu_action.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/tooltip.h" #include "styles/style_basic.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_widgets.h" #include @@ -214,20 +219,22 @@ enum class CardState { } [[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor( - const Main::AppConfig::AiComposeStyle &style) { + const Data::AiComposeTone &tone) { return { - .id = style.type, - .label = style.title, - .customEmojiData = Data::SerializeCustomEmojiId(style.emojiId), + .id = tone.isDefault ? tone.defaultType : QString::number(tone.id), + .label = tone.title, + .customEmojiData = tone.emojiId + ? Data::SerializeCustomEmojiId(tone.emojiId) + : QString(), }; } [[nodiscard]] std::vector ResolveStyleDescriptors( - const std::vector &styles) { + const std::vector &tones) { auto result = std::vector(); - result.reserve(styles.size()); - for (const auto &style : styles) { - result.push_back(ResolveStyleDescriptor(style)); + result.reserve(tones.size()); + for (const auto &tone : tones) { + result.push_back(ResolveStyleDescriptor(tone)); } return result; } @@ -249,6 +256,17 @@ enum class CardState { return result; } +[[nodiscard]] auto WithAddStyleTab(std::vector tabs) +-> std::vector { + tabs.push_back({ + .id = u"_add_style"_q, + .label = tr::lng_ai_compose_tone_create(tr::now), + .icon = &st::aiComposeAddStyleIcon, + .iconActive = &st::aiComposeAddStyleIconOver, + }); + return tabs; +} + [[nodiscard]] TextWithEntities LoadingTitleSparkle( not_null session) { const auto sparkles = ChatHelpers::GenerateLocalTgsSticker( @@ -364,6 +382,7 @@ public: [[nodiscard]] bool hasResult() const; [[nodiscard]] const TextWithEntities &result() const; [[nodiscard]] const std::vector &stylesData() const; + [[nodiscard]] const std::vector &tones() const; void setReadyChangedCallback(Fn callback); void setLoadingChangedCallback(Fn callback); void setPremiumFloodCallback(Fn callback); @@ -373,6 +392,8 @@ public: [[nodiscard]] bool hasStyleSelection() const; void setModeTabs(not_null tabs); void setStyleTabs(not_null*> stylesWrap); + void refreshTones(); + void selectToneById(uint64 id); void start(); protected: @@ -390,6 +411,7 @@ private: void resetState(CardState state); void applyResult(Api::ComposeWithAi::Result &&result); void showError(const QString &error = {}); + void setAuthorId(UserId authorId); void notifyLoadingChanged(); void notifyReadyChanged(); [[nodiscard]] QString currentTranslateStyle() const; @@ -400,12 +422,14 @@ private: const TextWithEntities _original; const LanguageId _detectedFrom; LanguageId _to; - const std::vector _stylesData; - const std::vector _translateStylesData; + std::vector _tones; + std::vector _stylesData; + std::vector _translateStylesData; QPointer _tabs; QPointer _styles; QPointer> _stylesWrap; const not_null _preview; + const not_null _authorLabel; Fn _readyChanged; Fn _loadingChanged; Fn _premiumFlood; @@ -414,6 +438,7 @@ private: ComposeAiMode _mode = ComposeAiMode::Style; int _styleIndex = -1; int _translateStyleIndex = 0; + UserId _authorId = UserId(0); bool _emojify = false; CardState _state = CardState::Waiting; mtpRequestId _requestId = 0; @@ -432,6 +457,7 @@ ComposeAiModeButton::ComposeAiModeButton( , _mode(mode) , _label(std::move(label)) { setCursor(style::cur_pointer); + setAccessibleName(_label); } void ComposeAiModeButton::setSelected(bool selected) { @@ -638,6 +664,7 @@ ComposeAiPreviewCard::ComposeAiPreviewCard( _copyCallback(); } }); + _copy->setAccessibleName(tr::lng_sr_ai_compose_copy_result(tr::now)); _emojify->checkedChanges( ) | rpl::on_next([=](bool checked) { if (_emojifyChanged) { @@ -863,9 +890,7 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) { ? _resultBody->st().style.lineHeight : _resultBody->st().style.font->height; if (!_copy->isHidden()) { - _resultBody->setSkipBlock( - _copy->width(), - lineHeight); + _resultBody->setSkipBlock(_copy->width(), lineHeight); } else { _resultBody->setSkipBlock(0, 0); } @@ -898,7 +923,9 @@ void ComposeAiPreviewCard::paintEvent(QPaintEvent *e) { st::aiComposeCardRadius); if (_dividerVisible) { p.setBrush(Qt::NoBrush); - p.setPen(st::aiComposeCardDivider); + auto color = st::windowSubTextFg->c; + color.setAlphaF(st::aiComposeShadowOpacity); + p.setPen(color); p.drawLine( st::aiComposeCardPadding.left(), _dividerTop, @@ -920,6 +947,9 @@ void ComposeAiPreviewCard::updateOriginalToggleIcon() { _originalToggle->setIconOverride( _originalExpanded ? &st::aiComposeCollapseIcon : nullptr, _originalExpanded ? &st::aiComposeCollapseIcon : nullptr); + _originalToggle->setAccessibleName(_originalExpanded + ? tr::lng_sr_ai_compose_collapse_original(tr::now) + : tr::lng_sr_ai_compose_expand_original(tr::now)); } // ComposeAiContent @@ -934,15 +964,18 @@ ComposeAiContent::ComposeAiContent( , _original(std::move(args.text)) , _detectedFrom(Platform::Language::Recognize(_original.text)) , _to(DefaultAiTranslateTo(_detectedFrom)) -, _stylesData(ResolveStyleDescriptors( - _session->appConfig().aiComposeStyles())) +, _tones(_session->data().aiComposeTones().list()) +, _stylesData(ResolveStyleDescriptors(_tones)) , _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData)) , _preview( Ui::CreateChild( this, _session, _original, - args.chatStyle)) { + args.chatStyle)) +, _authorLabel(Ui::CreateChild( + this, + st::aiComposeAuthorLabel)) { _preview->setResizeCallback([=] { refreshLayout(); }); _preview->setChooseCallback([=] { chooseLanguage(); }); _preview->setCopyCallback([=] { copyResult(); }); @@ -953,6 +986,26 @@ ComposeAiContent::ComposeAiContent( } }); _preview->setShow(_box->uiShow()); + _authorLabel->setVisible(false); + _authorLabel->heightValue( + ) | rpl::skip(1) | rpl::on_next([=] { + refreshLayout(); + }, lifetime()); + const auto show = _box->uiShow(); + _authorLabel->setClickHandlerFilter([=]( + const ClickHandlerPtr &handler, + Qt::MouseButton button) { + if (dynamic_cast(handler.get())) { + ActivateClickHandler(_authorLabel, handler, ClickContext{ + .button = button, + .other = QVariant::fromValue(ClickHandlerContext{ + .show = show, + }) + }); + return false; + } + return true; + }); } ComposeAiContent::~ComposeAiContent() { @@ -971,6 +1024,10 @@ const std::vector &ComposeAiContent::stylesData() const { return _stylesData; } +const std::vector &ComposeAiContent::tones() const { + return _tones; +} + void ComposeAiContent::setReadyChangedCallback(Fn callback) { _readyChanged = std::move(callback); } @@ -994,7 +1051,7 @@ void ComposeAiContent::setStyleTabs( _stylesWrap->setDuration(0); _styles = stylesWrap->entity(); _styles->setChangedCallback([=](int index) { - if (index >= 0 && index < int(_stylesData.size())) { + if (index >= 0 && index < int(_tones.size())) { const auto wasNoSelection = (_styleIndex < 0); _styleIndex = index; updateTitles(); @@ -1004,12 +1061,76 @@ void ComposeAiContent::setStyleTabs( _styleSelected(); } } + } else if (index == int(_tones.size())) { + _styles->setActive(_styleIndex); + _box->uiShow()->show(Box( + CreateAiToneBox, + _session, + crl::guard(this, [=](Data::AiComposeTone tone) { + selectToneById(tone.id); + }))); } }); _styles->setActive(_styleIndex); _stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant); } +void ComposeAiContent::refreshTones() { + auto previousKey = QString(); + auto hadSelection = false; + if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { + const auto &prev = _tones[_styleIndex]; + previousKey = prev.isDefault + ? prev.defaultType + : QString::number(prev.id); + hadSelection = true; + } + _tones = _session->data().aiComposeTones().list(); + _stylesData = ResolveStyleDescriptors(_tones); + _translateStylesData = ResolveTranslateStyleDescriptors( + _session, + _stylesData); + auto remapped = -1; + if (hadSelection) { + for (auto i = 0; i != int(_tones.size()); ++i) { + const auto &tone = _tones[i]; + const auto key = tone.isDefault + ? tone.defaultType + : QString::number(tone.id); + if (key == previousKey) { + remapped = i; + break; + } + } + } + _styleIndex = remapped; + if (_mode == ComposeAiMode::Style && hadSelection && _styleIndex < 0) { + request(); + } +} + +void ComposeAiContent::selectToneById(uint64 id) { + for (auto i = 0; i != int(_tones.size()); ++i) { + const auto &tone = _tones[i]; + if (!tone.isDefault && tone.id == id) { + const auto wasNoSelection = (_styleIndex < 0); + _styleIndex = i; + updateTitles(); + if (_styles) { + _styles->setActive(_styleIndex); + _styles->scrollToActive(); + } + if (_mode == ComposeAiMode::Style) { + request(); + if (wasNoSelection && _styleSelected) { + _styleSelected(); + } + } + return; + } + } +} + void ComposeAiContent::start() { updatePinnedTabs(anim::type::instant); updateTitles(); @@ -1019,7 +1140,16 @@ void ComposeAiContent::start() { int ComposeAiContent::resizeGetHeight(int newWidth) { _preview->resizeToWidth(newWidth); _preview->moveToLeft(0, 0, newWidth); - return _preview->height(); + auto y = _preview->height(); + if (!_authorLabel->isHidden()) { + _authorLabel->resizeToWidth(newWidth); + _authorLabel->moveToLeft( + 0, + y + st::aiComposeAuthorLabelTop, + newWidth); + y += st::aiComposeAuthorLabelTop + _authorLabel->height(); + } + return y; } void ComposeAiContent::refreshLayout() { @@ -1105,6 +1235,7 @@ void ComposeAiContent::setMode(ComposeAiMode mode) { _mode = mode; _state = CardState::Waiting; _preview->setState(CardState::Waiting); + setAuthorId(UserId(0)); notifyLoadingChanged(); if (_modeChanged) { _modeChanged(_mode); @@ -1173,13 +1304,21 @@ void ComposeAiContent::request() { .emojify = (_mode != ComposeAiMode::Fix) && _emojify, }; switch (_mode) { - case ComposeAiMode::Translate: + case ComposeAiMode::Translate: { request.translateToLang = _to.twoLetterCode(); - request.changeTone = currentTranslateStyle(); - break; + const auto style = currentTranslateStyle(); + if (!style.isEmpty()) { + request.setDefaultTone(style); + } + } break; case ComposeAiMode::Style: - if (_styleIndex >= 0) { - request.changeTone = _stylesData[_styleIndex].id; + if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) { + const auto &tone = _tones[_styleIndex]; + if (tone.isDefault) { + request.setDefaultTone(tone.defaultType); + } else { + request.setCustomTone(tone.id, tone.accessHash); + } } break; case ComposeAiMode::Fix: @@ -1211,9 +1350,43 @@ void ComposeAiContent::request() { }); } +void ComposeAiContent::setAuthorId(UserId authorId) { + if (_authorId == authorId) { + return; + } + _authorId = authorId; + if (const auto user = _session->data().userLoaded(authorId)) { + const auto name = user->shortName(); + auto mention = tr::marked(name); + mention.entities.push_back(EntityInText( + EntityType::MentionName, + 0, + name.size(), + TextUtilities::MentionNameDataFromFields({ + .selfId = _session->userId().bare, + .userId = authorId.bare, + .accessHash = user->accessHash(), + }))); + _authorLabel->setMarkedText( + tr::lng_ai_compose_author( + tr::now, + lt_user, + std::move(mention), + tr::marked), + Core::TextContext({ .session = _session })); + _authorLabel->setVisible(true); + } else { + _authorLabel->setMarkedText({}); + _authorLabel->setVisible(false); + _authorId = UserId(0); + } + refreshLayout(); +} + void ComposeAiContent::resetState(CardState state) { _state = state; _result = {}; + setAuthorId(UserId(0)); _preview->setState(state); notifyLoadingChanged(); updateTitles(); @@ -1234,6 +1407,13 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { notifyLoadingChanged(); if (_state == CardState::Ready) { _preview->setResultText(std::move(display)); + if (_mode == ComposeAiMode::Style + && _styleIndex >= 0 + && _styleIndex < int(_tones.size())) { + setAuthorId(_tones[_styleIndex].authorId); + } else { + setAuthorId(UserId(0)); + } } updateTitles(); notifyReadyChanged(); @@ -1242,6 +1422,7 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) { void ComposeAiContent::showError(const QString &error) { _state = CardState::Failed; + setAuthorId(UserId(0)); _preview->setState(CardState::Failed); notifyLoadingChanged(); updateTitles(); @@ -1264,6 +1445,9 @@ void ComposeAiContent::showError(const QString &error) { _premiumFlood(); } return; + } else if (error == u"INPUT_TEXT_TOO_LONG"_q) { + _box->showToast(tr::lng_ai_compose_error_too_long(tr::now)); + return; } _box->showToast(error.isEmpty() ? tr::lng_ai_compose_error(tr::now) @@ -1320,7 +1504,12 @@ bool ComposeAiContent::hasStyleSelection() const { return _styleIndex >= 0; } -[[nodiscard]] Fn SetupStyleTooltip( +struct StyleTooltipHandle { + QPointer tooltip; + Fn updateVisibility; +}; + +[[nodiscard]] StyleTooltipHandle SetupStyleTooltip( not_null box, not_null pinnedToTop, not_null stylesWrap, @@ -1393,7 +1582,7 @@ bool ComposeAiContent::hasStyleSelection() const { } }, tooltip->lifetime()); - return updateVisibility; + return { tooltip, updateVisibility }; } } // namespace @@ -1418,10 +1607,10 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { const auto session = args.session; box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); - }); + })->setAccessibleName(tr::lng_close(tr::now)); box->addTopButton(st::aiComposeBoxInfoButton, [=] { box->uiShow()->show(Box(Ui::AboutCocoonBox)); - }); + })->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now)); const auto body = box->verticalLayout(); const auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip); @@ -1433,26 +1622,112 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { const auto content = body->add( object_ptr(box, box, args), st::aiComposeContentMargin); - auto emojiFactory = session->data().customEmojiManager().factory( - Data::CustomEmojiSizeTag::Large); - const auto stylesWrap = pinnedToTop->add( - object_ptr>( + const auto contextMenu = box->lifetime().make_state< + base::unique_qptr>(); + const auto stylesWrapHolder = box->lifetime().make_state< + QPointer>>(); + const auto styleTooltipHolder = box->lifetime().make_state< + QPointer>(); + const auto styleTooltipUpdater = box->lifetime().make_state< + Fn>(); + + content->setModeTabs(tabs); + + const auto rebuildStylesWrap = [=] { + auto savedScroll = -1; + if (const auto old = stylesWrapHolder->data()) { + savedScroll = old->entity()->scrollLeft(); + delete old; + } + if (const auto old = styleTooltipHolder->data()) { + delete old; + } + auto emojiFactory = session->data().customEmojiManager().factory( + Data::CustomEmojiSizeTag::Large); + auto wrap = object_ptr>( pinnedToTop, object_ptr( pinnedToTop, - content->stylesData(), + WithAddStyleTab(content->stylesData()), std::move(emojiFactory)), - tabsSkip), - st::aiComposeContentMargin); - stylesWrap->hide(anim::type::instant); - content->setModeTabs(tabs); - content->setStyleTabs(stylesWrap); + tabsSkip); + const auto ptr = wrap.data(); + pinnedToTop->add(std::move(wrap), st::aiComposeContentMargin); + *stylesWrapHolder = ptr; + ptr->entity()->setContextMenuCallback([=](int index, QPoint globalPos) { + const auto &tones = content->tones(); + if (index < 0 || index >= int(tones.size())) { + return; + } + const auto &tone = tones[index]; + if (tone.isDefault) { + return; + } + *contextMenu = base::make_unique_q( + ptr->entity(), + st::popupMenuWithIcons); + const auto toneCopy = tone; + if (toneCopy.creator) { + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_edit(tr::now), + [=] { + box->uiShow()->show(Box( + EditAiToneBox, + session, + toneCopy, + crl::guard(content, [=](Data::AiComposeTone tone) { + content->selectToneById(tone.id); + }))); + }, + &st::menuIconEdit); + } + (*contextMenu)->addAction( + tr::lng_ai_compose_tone_share(tr::now), + [=] { + const auto url = session->createInternalLinkFull( + "addstyle/" + toneCopy.slug); + FastShareLink( + Main::MakeSessionShow(box->uiShow(), session), + url); + }, + &st::menuIconShare); + (*contextMenu)->addAction(base::make_unique_q( + (*contextMenu)->menu(), + st::menuWithIconsAttention, + Ui::Menu::CreateAction( + (*contextMenu)->menu().get(), + toneCopy.creator + ? tr::lng_ai_compose_tone_delete(tr::now) + : tr::lng_ai_compose_tone_remove(tr::now), + [=] { + ConfirmDeleteAiTone( + box->uiShow(), + session, + toneCopy); + }), + &st::menuIconDeleteAttention, + &st::menuIconDeleteAttention)); + (*contextMenu)->popup(globalPos); + }); + content->setStyleTabs(ptr); + if (savedScroll >= 0) { + ptr->entity()->setScrollLeft(savedScroll); + } + auto handle = SetupStyleTooltip( + box, + pinnedToTop, + ptr, + [=] { return content->mode(); }); + *styleTooltipHolder = handle.tooltip; + *styleTooltipUpdater = std::move(handle.updateVisibility); + }; + rebuildStylesWrap(); - const auto updateStyleTooltipVisibility = SetupStyleTooltip( - box, - pinnedToTop, - stylesWrap, - [=] { return content->mode(); }); + session->data().aiComposeTones().updated( + ) | rpl::on_next([=] { + content->refreshTones(); + rebuildStylesWrap(); + }, box->lifetime()); const auto sparkle = LoadingTitleSparkle(session); const auto loading = box->lifetime().make_state< @@ -1517,10 +1792,10 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { box->clearButtons(); box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); - }); + })->setAccessibleName(tr::lng_close(tr::now)); box->addTopButton(st::aiComposeBoxInfoButton, [=] { box->uiShow()->show(Box(Ui::AboutCocoonBox)); - }); + })->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now)); if (*premiumFlooded) { auto helper = Ui::Text::CustomEmojiHelper(); @@ -1570,6 +1845,7 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { btn->parentWidget(), st::aiComposeSendButton); send->setState({ .type = Ui::SendButton::Type::Send }); + send->setAccessibleName(tr::lng_send_button(tr::now)); send->show(); btn->geometryValue( ) | rpl::on_next([=](QRect geometry) { @@ -1614,14 +1890,14 @@ void ComposeAiBox(not_null box, ComposeAiBoxArgs &&args) { }); content->setModeChangedCallback([=](ComposeAiMode mode) { rebuildButtons(); - updateStyleTooltipVisibility(mode == ComposeAiMode::Style); + (*styleTooltipUpdater)(mode == ComposeAiMode::Style); }); content->setStyleSelectedCallback([=] { rebuildButtons(); if (!Core::App().settings().readPref(kAiComposeStyleTooltipHiddenPref)) { Core::App().settings().writePref(kAiComposeStyleTooltipHiddenPref, true); } - updateStyleTooltipVisibility(false); + (*styleTooltipUpdater)(false); }); rebuildButtons(); diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index fd0be2317a..765a6115fc 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -1828,10 +1828,11 @@ void ProxiesBoxController::ShowApplyConfirmation( } else { box->uiShow()->showBox(Ui::MakeConfirmBox({ .text = tr::lng_proxy_check_ip_warning(), - .confirmed = [=] { + .confirmed = [=](Fn close) { auto &proxy = Core::App().settings().proxy(); proxy.setCheckIpWarningShown(true); Local::writeSettings(); + close(); runCheck(); }, .confirmText = tr::lng_proxy_check_ip_proceed(), diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp new file mode 100644 index 0000000000..c7ae9415e3 --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.cpp @@ -0,0 +1,718 @@ +/* +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/create_ai_tone_box.h" + +#include "chat_helpers/compose/compose_show.h" +#include "chat_helpers/emoji_list_widget.h" +#include "chat_helpers/stickers_lottie.h" +#include "data/data_ai_compose_tones.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_forum_icons.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/view/media/history_view_sticker_player.h" +#include "lang/lang_keys.h" +#include "main/session/session_show.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/sections/settings_premium.h" +#include "ui/abstract_button.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/custom_emoji_toast_icon.h" +#include "ui/controls/warning_tooltip.h" +#include "ui/effects/animations.h" +#include "ui/layers/generic_box.h" +#include "ui/layers/show.h" +#include "ui/painter.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" + +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_dialogs.h" +#include "styles/style_layers.h" + +namespace { + +constexpr auto kAiComposeToneToastDuration = crl::time(4000); + +void ShowToneToast( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + bool created) { + const auto size = QSize( + st::aiComposeToneToastIconSize.width(), + st::aiComposeToneToastIconSize.height()); + show->showToast(Ui::Toast::Config{ + .title = (created + ? tr::lng_ai_compose_tone_created + : tr::lng_ai_compose_tone_updated)( + tr::now, + lt_title, + tone.title), + .text = tr::lng_ai_compose_tone_created_description( + tr::now, + Ui::Text::WithEntities), + .iconContent = Ui::MakeCustomEmojiToastIcon( + session, + tone.emojiId, + size), + .iconPadding = st::aiComposeToneToastIconPadding, + .duration = kAiComposeToneToastDuration, + }); +} + +void ChooseToneIconBox( + not_null box, + not_null controller, + Fn chosen) { + using namespace ChatHelpers; + + box->setTitle(tr::lng_ai_compose_tone_icon_title()); + box->setWidth(st::boxWideWidth); + box->setMaxHeight(st::editTopicMaxHeight); + box->setScrollStyle(st::reactPanelScroll); + + const auto manager = &controller->session().data().customEmojiManager(); + const auto icons = &controller->session().data().forumIcons(); + + auto factory = [=](DocumentId id, Fn repaint) + -> std::unique_ptr { + return manager->create( + id, + std::move(repaint), + Data::CustomEmojiManager::SizeTag::Large); + }; + + const auto top = box->setPinnedToTopContent( + object_ptr(box)); + + const auto body = box->verticalLayout(); + const auto selector = body->add( + object_ptr(body, EmojiListDescriptor{ + .show = controller->uiShow(), + .mode = EmojiListWidget::Mode::TopicIcon, + .paused = Window::PausedIn( + controller, + Window::GifPauseReason::Layer), + .customRecentList = DocumentListToRecent(icons->list()), + .customRecentFactory = std::move(factory), + .st = &st::reactPanelEmojiPan, + }), + st::reactPanelEmojiPan.padding); + + icons->requestDefaultIfUnknown(); + icons->defaultUpdates( + ) | rpl::on_next([=] { + selector->provideRecent(DocumentListToRecent(icons->list())); + }, selector->lifetime()); + + top->add(selector->createFooter()); + + const auto shadow = Ui::CreateChild(box.get()); + shadow->show(); + + rpl::combine( + top->heightValue(), + selector->widthValue() + ) | rpl::on_next([=](int topHeight, int width) { + shadow->setGeometry(0, topHeight, width, st::lineWidth); + }, shadow->lifetime()); + + selector->refreshEmoji(); + + selector->scrollToRequests( + ) | rpl::on_next([=](int y) { + box->scrollToY(y); + shadow->update(); + }, selector->lifetime()); + + rpl::combine( + box->heightValue(), + top->heightValue(), + rpl::mappers::_1 - rpl::mappers::_2 + ) | rpl::on_next([=](int height) { + selector->setMinimalHeight(selector->width(), height); + }, body->lifetime()); + + selector->customChosen( + ) | rpl::on_next([=](ChatHelpers::FileChosen data) { + chosen(data.document->id); + box->closeBox(); + }, selector->lifetime()); + + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +} // namespace + +not_null AddAiToneIconPreview( + not_null container, + not_null session, + rpl::producer emojiIdValue, + Fn emojiIdChosen) { + using StickerPlayer = HistoryView::StickerPlayer; + struct State { + DocumentId emojiId = 0; + std::shared_ptr player; + bool playerUsesTextColor = false; + }; + + const auto outer = st::aiToneIconPreviewSize; + const auto inner = st::aiToneIconPreviewInnerSize; + const auto top = st::aiToneIconPreviewTopSkip; + const auto bottom = st::aiToneIconPreviewBottomSkip; + const auto holder = container->add( + object_ptr( + container, + outer + top + bottom)); + const auto button = Ui::CreateChild(holder); + button->resize(outer, outer); + button->show(); + + holder->widthValue( + ) | rpl::on_next([=](int width) { + button->move((width - outer) / 2, top); + }, button->lifetime()); + + const auto state = button->lifetime().make_state(); + const auto emojiIdVar = button->lifetime().make_state< + rpl::variable>(std::move(emojiIdValue)); + + emojiIdVar->value( + ) | rpl::on_next([=](DocumentId id) { + state->emojiId = id; + }, button->lifetime()); + + emojiIdVar->value( + ) | rpl::map([=](DocumentId id) -> rpl::producer { + if (!id) { + return rpl::single((DocumentData*)nullptr); + } + return session->data().customEmojiManager().resolve( + id + ) | rpl::map([=](not_null document) { + return document.get(); + }) | rpl::map_error_to_done(); + }) | rpl::flatten_latest( + ) | rpl::map([=](DocumentData *document) + -> rpl::producer> { + if (!document) { + return rpl::single(std::shared_ptr()); + } + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + return rpl::single() | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::take(1) | rpl::map([=] { + auto result = std::shared_ptr(); + const auto sticker = document->sticker(); + const auto size = QSize(inner, inner); + if (sticker && sticker->isLottie()) { + result = std::make_shared( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::StickerSet, + size, + Lottie::Quality::High)); + } else if (sticker && sticker->isWebm()) { + result = std::make_shared( + media->owner()->location(), + media->bytes(), + size); + } else { + result = std::make_shared( + media->owner()->location(), + media->bytes(), + size); + } + result->setRepaintCallback([=] { button->update(); }); + state->playerUsesTextColor + = media->owner()->emojiUsesTextColor(); + return result; + }); + }) | rpl::flatten_latest( + ) | rpl::on_next([=](std::shared_ptr player) { + state->player = std::move(player); + button->update(); + }, button->lifetime()); + + button->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(button); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::aiToneIconPreviewBg); + p.drawEllipse(button->rect()); + if (state->player && state->player->ready()) { + const auto color = state->playerUsesTextColor + ? st::windowFg->c + : QColor(0, 0, 0, 0); + const auto frame = state->player->frame( + QSize(inner, inner), + color, + false, + crl::now(), + false).image; + const auto sz = frame.size() / style::DevicePixelRatio(); + p.drawImage( + QRect( + (outer - sz.width()) / 2, + (outer - sz.height()) / 2, + sz.width(), + sz.height()), + frame); + state->player->markFrameShown(); + } else if (!state->emojiId) { + st::aiToneIconPreviewPlaceholder.paintInCenter( + p, + button->rect()); + } + }, button->lifetime()); + + if (emojiIdChosen) { + button->setClickedCallback([=] { + const auto controller = ChatHelpers::ResolveWindowDefault()( + session); + if (!controller) { + return; + } + controller->uiShow()->showBox(Box( + ChooseToneIconBox, + controller, + crl::guard(button, [=](DocumentId id) { + emojiIdChosen(id); + }))); + }); + } else { + button->setAttribute(Qt::WA_TransparentForMouseEvents); + } + + return button; +} + +namespace { + +void SetupToneBox( + not_null box, + not_null session, + DocumentId initialEmojiId, + const QString &initialName, + const QString &initialPrompt, + bool initialDisplayAuthor, + rpl::producer title, + rpl::producer submitLabel, + Fn submit, + Fn requestDelete = nullptr) { + box->setStyle(st::aiComposeBox); + box->setNoContentMargin(true); + box->setWidth(st::boxWideWidth); + box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); }); + box->setTitle(std::move(title)); + + const auto container = box->verticalLayout(); + const auto emojiId = container->lifetime().make_state< + rpl::variable>(initialEmojiId); + + const auto iconButton = AddAiToneIconPreview( + container, + session, + emojiId->value(), + [=](DocumentId id) { *emojiId = id; }); + + const auto name = box->addRow( + object_ptr( + box, + st::aiToneNameField, + Ui::InputField::Mode::SingleLine, + rpl::producer(), + initialName), + st::aiToneFieldsMargin); + name->setMaxLength(session->appConfig().get( + u"aicompose_tone_title_length_max"_q, + 12)); + + Ui::AddSkip(container, st::aiToneFieldsSkip); + + const auto promptSt = box->lifetime().make_state( + st::aiTonePromptField); + { + const auto &placeholderStyle = st::aiTonePlaceholderLabel.style; + const auto fieldsMargin = st::aiToneFieldsMargin; + const auto contentWidth = st::boxWideWidth + - fieldsMargin.left() - fieldsMargin.right() + - promptSt->textMargins.left() - promptSt->textMargins.right(); + auto measure = Ui::Text::String{ contentWidth / 2 }; + measure.setText( + placeholderStyle, + tr::lng_ai_compose_tone_prompt_placeholder(tr::now)); + const auto desiredMin = measure.countHeight(contentWidth) + + promptSt->textMargins.top() + + promptSt->textMargins.bottom(); + if (promptSt->heightMin < desiredMin) { + promptSt->heightMin = desiredMin; + } + if (promptSt->heightMax < promptSt->heightMin) { + promptSt->heightMax = promptSt->heightMin; + } + } + + const auto prompt = box->addRow( + object_ptr( + box, + *promptSt, + Ui::InputField::Mode::MultiLine, + rpl::producer(), + initialPrompt), + st::aiToneFieldsMargin); + prompt->setSubmitSettings(Ui::InputField::SubmitSettings::None); + prompt->setMaxLength(session->appConfig().get( + u"aicompose_tone_prompt_length_max"_q, + 1024)); + + struct FieldDecor { + not_null bg; + not_null placeholder; + Ui::Animations::Simple anim; + bool hidden = false; + }; + const auto makeDecor = [=]( + not_null field, + rpl::producer placeholderText) { + const auto parent = field->parentWidget(); + const auto decor = field->lifetime().make_state(FieldDecor{ + .bg = Ui::CreateChild(parent), + .placeholder = Ui::CreateChild( + parent, + std::move(placeholderText), + st::aiTonePlaceholderLabel), + }); + decor->bg->setAttribute(Qt::WA_TransparentForMouseEvents); + decor->placeholder->setAttribute(Qt::WA_TransparentForMouseEvents); + decor->bg->paintRequest( + ) | rpl::on_next([bg = decor->bg] { + auto p = QPainter(bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::aiToneFieldBg); + const auto r = st::aiToneFieldRadius; + p.drawRoundedRect(bg->rect(), r, r); + }, decor->bg->lifetime()); + decor->bg->lower(); + decor->placeholder->raise(); + + const auto applyPosition = [=] { + const auto pad = st::aiToneFieldPadding; + const auto progress = decor->anim.value(decor->hidden ? 1. : 0.); + const auto shift = int(base::SafeRound( + progress * (-st::defaultInputField.placeholderShift))); + decor->placeholder->moveToLeft( + field->x() + pad.left() + shift, + field->y() + pad.top()); + decor->placeholder->setOpacity(1. - progress); + }; + field->geometryValue( + ) | rpl::on_next([=](QRect g) { + if (g.isEmpty()) { + return; + } + const auto pad = st::aiToneFieldPadding; + decor->bg->setGeometry(g); + decor->placeholder->resizeToWidth( + g.width() - pad.left() - pad.right()); + applyPosition(); + }, field->lifetime()); + + const auto animate = [=](bool hidden) { + if (decor->hidden == hidden) { + return; + } + decor->hidden = hidden; + decor->anim.start( + applyPosition, + hidden ? 0. : 1., + hidden ? 1. : 0., + st::defaultInputField.duration); + }; + field->changes( + ) | rpl::on_next([=] { + animate(!field->getLastText().isEmpty()); + }, field->lifetime()); + decor->hidden = !field->getLastText().isEmpty(); + applyPosition(); + return decor; + }; + makeDecor(name, tr::lng_ai_compose_tone_name_placeholder()); + const auto promptDecor = makeDecor( + prompt, + tr::lng_ai_compose_tone_prompt_placeholder()); + + const auto authorCheckbox = box->addRow( + object_ptr( + box, + tr::lng_ai_compose_tone_author(tr::now), + st::aiComposeEmojifyCheckbox, + std::make_unique( + st::defaultCheck, + initialDisplayAuthor)), + st::aiToneAuthorCheckboxMargin, + style::al_top); + + const auto deleteButton = requestDelete + ? box->addRow( + object_ptr( + box, + tr::lng_ai_compose_tone_delete(), + st::aiToneDeleteButton), + st::aiToneDeleteButtonMargin) + : nullptr; + if (deleteButton) { + deleteButton->setFullRadius(true); + deleteButton->setClickedCallback(std::move(requestDelete)); + box->widthValue( + ) | rpl::on_next([=](int width) { + const auto &margin = st::aiToneDeleteButtonMargin; + deleteButton->setFullWidth( + width - margin.left() - margin.right()); + }, deleteButton->lifetime()); + } + + rpl::combine( + prompt->topValue(), + promptDecor->placeholder->heightValue(), + box->getDelegate()->contentHeightMaxValue() + ) | rpl::on_next([=](int top, int phHeight, int contentHeight) { + const auto pad = st::aiToneFieldPadding; + const auto deleteBlock = deleteButton + ? (deleteButton->heightNoMargins() + + st::aiToneDeleteButtonMargin.top() + + st::aiToneDeleteButtonMargin.bottom()) + : 0; + prompt->setMaxHeight(contentHeight + - top + - st::aiToneFieldsMargin.bottom() + - authorCheckbox->heightNoMargins() + - st::aiToneAuthorCheckboxMargin.top() + - st::aiToneAuthorCheckboxMargin.bottom() + - deleteBlock); + prompt->setMinHeight(phHeight + pad.top() + pad.bottom()); + }, prompt->lifetime()); + + box->setFocusCallback([=] { + name->setFocusFast(); + }); + + const auto warning = box->lifetime().make_state(); + const auto save = [=] { + const auto nameText = name->getLastText().trimmed(); + const auto promptText = prompt->getLastText().trimmed(); + const auto showWarning = [=]( + not_null target, + rpl::producer text) { + warning->show({ + .parent = box, + .target = target, + .text = std::move(text), + }); + }; + if (!emojiId->current()) { + showWarning( + iconButton, + tr::lng_ai_compose_tone_warn_icon(tr::marked)); + return; + } else if (nameText.isEmpty()) { + name->showError(); + showWarning( + name, + tr::lng_ai_compose_tone_warn_name(tr::marked)); + return; + } else if (promptText.isEmpty()) { + prompt->showError(); + showWarning( + prompt, + tr::lng_ai_compose_tone_warn_prompt(tr::marked)); + return; + } + warning->hide(anim::type::normal); + submit( + emojiId->current(), + nameText, + promptText, + authorCheckbox->checked()); + }; + + const auto submitBtn = box->addButton(std::move(submitLabel), save); + submitBtn->setFullRadius(true); +} + +} // namespace + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved) { + SetupToneBox( + box, + session, + DocumentId(0), + QString(), + QString(), + false, + tr::lng_ai_compose_create_tone_title(), + tr::lng_ai_compose_tone_create(), + [=](DocumentId emojiId, + const QString &name, + const QString &prompt, + bool displayAuthor) { + session->data().aiComposeTones().create( + name, + prompt, + emojiId, + displayAuthor, + crl::guard(box, [=](Data::AiComposeTone tone) { + const auto show = box->uiShow(); + box->closeBox(); + ShowToneToast(show, session, tone, true); + if (saved) { + saved(tone); + } + }), + crl::guard(box, [=](const MTP::Error &error) { + if (error.type() == u"TONES_SAVED_TOO_MANY"_q) { + ShowAiComposeToneLimitError(box->uiShow(), session); + } else if (!MTP::IgnoreError(error)) { + box->showToast(error.type()); + } + })); + }, + nullptr); +} + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved) { + const auto toneCopy = tone; + SetupToneBox( + box, + session, + tone.emojiId, + tone.title, + tone.prompt, + tone.authorId != 0, + tr::lng_ai_compose_edit_tone_title(), + tr::lng_ai_compose_tone_save(), + [=](DocumentId emojiId, + const QString &name, + const QString &prompt, + bool displayAuthor) { + session->data().aiComposeTones().update( + toneCopy, + name, + prompt, + std::make_optional(emojiId), + std::make_optional(displayAuthor), + crl::guard(box, [=](Data::AiComposeTone updated) { + const auto show = box->uiShow(); + box->closeBox(); + ShowToneToast(show, session, updated, false); + if (saved) { + saved(updated); + } + })); + }, + [=] { + ConfirmDeleteAiTone( + box->uiShow(), + session, + toneCopy, + [=] { box->closeBox(); }); + }); +} + +void ConfirmDeleteAiTone( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + Fn done) { + if (!tone.creator) { + show->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_remove_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().save( + tone, + true, + done); + }, + .confirmText = tr::lng_box_remove(), + })); + return; + } + show->show(Ui::MakeConfirmBox({ + .text = tr::lng_ai_compose_tone_delete_sure(), + .confirmed = [=](Fn &&close) { + close(); + session->data().aiComposeTones().remove(tone, done); + }, + .confirmText = tr::lng_box_delete(), + .confirmStyle = &st::attentionBoxButton, + .title = tr::lng_ai_compose_tone_delete(), + })); +} + +void ShowAiComposeToneLimitError( + std::shared_ptr show, + not_null session) { + const auto limits = Data::PremiumLimits(session); + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + const auto defaultLimit = limits.aiComposeSavedTonesDefault(); + const auto premiumLimit = limits.aiComposeSavedTonesPremium(); + const auto current = premium ? premiumLimit : defaultLimit; + if (premium || !premiumPossible) { + show->showToast(tr::lng_ai_compose_tone_saved_limit_final( + tr::now, + lt_count, + current, + tr::rich)); + } else { + Settings::ShowPremiumPromoToast( + Main::MakeSessionShow(show, session), + ChatHelpers::ResolveWindowDefault(), + tr::lng_ai_compose_tone_saved_limit( + tr::now, + lt_count, + defaultLimit, + lt_link, + tr::bold(tr::lng_ai_compose_tone_saved_limit_link( + tr::now, + tr::link)), + lt_premium_count, + tr::bold(QString::number(premiumLimit)), + tr::rich), + u"ai_compose_tones"_q); + } +} diff --git a/Telegram/SourceFiles/boxes/create_ai_tone_box.h b/Telegram/SourceFiles/boxes/create_ai_tone_box.h new file mode 100644 index 0000000000..4760eeaacb --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_ai_tone_box.h @@ -0,0 +1,50 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +struct AiComposeTone; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class AbstractButton; +class GenericBox; +class Show; +class VerticalLayout; +} // namespace Ui + +not_null AddAiToneIconPreview( + not_null container, + not_null session, + rpl::producer emojiIdValue, + Fn emojiIdChosen = nullptr); + +void CreateAiToneBox( + not_null box, + not_null session, + Fn saved = nullptr); + +void EditAiToneBox( + not_null box, + not_null session, + const Data::AiComposeTone &tone, + Fn saved = nullptr); + +void ConfirmDeleteAiTone( + std::shared_ptr show, + not_null session, + const Data::AiComposeTone &tone, + Fn done = nullptr); + +void ShowAiComposeToneLimitError( + std::shared_ptr show, + not_null session); diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index bbd253a4d2..74093297b1 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "base/random.h" #include "base/unique_qptr.h" +#include "countries/countries_instance.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/tabbed_panel.h" @@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "history/view/media/menu/history_view_poll_menu.h" #include "history/view/history_view_schedule_box.h" +#include "info/channel_statistics/boosts/giveaway/select_countries_box.h" #include "lang/lang_keys.h" #include "layout/layout_document_generic_preview.h" #include "main/main_app_config.h" @@ -159,6 +161,10 @@ public: [[nodiscard]] rpl::producer<> backspaceInFront() const; [[nodiscard]] rpl::producer<> tabbed() const; + void handlePaste( + not_null field, + const QStringList &list); + private: class Option { public: @@ -201,6 +207,7 @@ private: void showAddIcon(bool show); [[nodiscard]] not_null field() const; + [[nodiscard]] not_null wrapWidget() const; [[nodiscard]] PollAnswer toPollAnswer(int index) const; @@ -233,12 +240,18 @@ private: void fixShadows(); void removeEmptyTail(); void addEmptyOption(); + void insertOption( + int beforeIndex, + const QString &text, + anim::type animated); + void initOptionField(not_null field); void checkLastOption(); void validateState(); void fixAfterErase(); void destroy(std::unique_ptr