Merge tag 'v5.1.2' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/boxes/send_files_box.cpp
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/data/data_search_controller.cpp
#	Telegram/SourceFiles/dialogs/dialogs_widget.cpp
#	Telegram/SourceFiles/dialogs/dialogs_widget.h
#	Telegram/SourceFiles/history/view/media/history_view_gif.cpp
#	Telegram/SourceFiles/mainwidget.cpp
#	Telegram/lib_ui
This commit is contained in:
AlexeyZavar 2024-06-04 03:27:24 +03:00
commit 01b8602437
304 changed files with 13815 additions and 3704 deletions

View file

@ -189,6 +189,8 @@ PRIVATE
api/api_common.h api/api_common.h
api/api_confirm_phone.cpp api/api_confirm_phone.cpp
api/api_confirm_phone.h api/api_confirm_phone.h
api/api_credits.cpp
api/api_credits.h
api/api_earn.cpp api/api_earn.cpp
api/api_earn.h api/api_earn.h
api/api_editing.cpp api/api_editing.cpp
@ -368,6 +370,8 @@ PRIVATE
boxes/ringtones_box.h boxes/ringtones_box.h
boxes/self_destruction_box.cpp boxes/self_destruction_box.cpp
boxes/self_destruction_box.h boxes/self_destruction_box.h
boxes/send_credits_box.cpp
boxes/send_credits_box.h
boxes/send_files_box.cpp boxes/send_files_box.cpp
boxes/send_files_box.h boxes/send_files_box.h
boxes/sessions_box.cpp boxes/sessions_box.cpp
@ -510,6 +514,8 @@ PRIVATE
core/launcher.h core/launcher.h
core/local_url_handlers.cpp core/local_url_handlers.cpp
core/local_url_handlers.h core/local_url_handlers.h
core/phone_click_handler.cpp
core/phone_click_handler.h
core/sandbox.cpp core/sandbox.cpp
core/sandbox.h core/sandbox.h
core/shortcuts.cpp core/shortcuts.cpp
@ -531,6 +537,8 @@ PRIVATE
data/business/data_business_info.h data/business/data_business_info.h
data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.cpp
data/business/data_shortcut_messages.h data/business/data_shortcut_messages.h
data/components/factchecks.cpp
data/components/factchecks.h
data/components/recent_peers.cpp data/components/recent_peers.cpp
data/components/recent_peers.h data/components/recent_peers.h
data/components/scheduled_messages.cpp data/components/scheduled_messages.cpp
@ -608,6 +616,8 @@ PRIVATE
data/data_groups.h data/data_groups.h
data/data_histories.cpp data/data_histories.cpp
data/data_histories.h data/data_histories.h
data/data_history_messages.cpp
data/data_history_messages.h
data/data_lastseen_status.h data/data_lastseen_status.h
data/data_location.cpp data/data_location.cpp
data/data_location.h data/data_location.h
@ -850,6 +860,8 @@ PRIVATE
history/view/history_view_about_view.h history/view/history_view_about_view.h
history/view/history_view_bottom_info.cpp history/view/history_view_bottom_info.cpp
history/view/history_view_bottom_info.h history/view/history_view_bottom_info.h
history/view/history_view_chat_preview.cpp
history/view/history_view_chat_preview.h
history/view/history_view_contact_status.cpp history/view/history_view_contact_status.cpp
history/view/history_view_contact_status.h history/view/history_view_contact_status.h
history/view/history_view_context_menu.cpp history/view/history_view_context_menu.cpp
@ -864,6 +876,8 @@ PRIVATE
history/view/history_view_emoji_interactions.h history/view/history_view_emoji_interactions.h
history/view/history_view_empty_list_bubble.cpp history/view/history_view_empty_list_bubble.cpp
history/view/history_view_empty_list_bubble.h history/view/history_view_empty_list_bubble.h
history/view/history_view_fake_items.cpp
history/view/history_view_fake_items.h
history/view/history_view_group_call_bar.cpp history/view/history_view_group_call_bar.cpp
history/view/history_view_group_call_bar.h history/view/history_view_group_call_bar.h
history/view/history_view_item_preview.h history/view/history_view_item_preview.h
@ -894,14 +908,14 @@ PRIVATE
history/view/history_view_send_action.h history/view/history_view_send_action.h
history/view/history_view_service_message.cpp history/view/history_view_service_message.cpp
history/view/history_view_service_message.h history/view/history_view_service_message.h
history/view/history_view_spoiler_click_handler.cpp
history/view/history_view_spoiler_click_handler.h
history/view/history_view_sponsored_click_handler.cpp history/view/history_view_sponsored_click_handler.cpp
history/view/history_view_sponsored_click_handler.h history/view/history_view_sponsored_click_handler.h
history/view/history_view_sticker_toast.cpp history/view/history_view_sticker_toast.cpp
history/view/history_view_sticker_toast.h history/view/history_view_sticker_toast.h
history/view/history_view_sublist_section.cpp history/view/history_view_sublist_section.cpp
history/view/history_view_sublist_section.h history/view/history_view_sublist_section.h
history/view/history_view_text_helper.cpp
history/view/history_view_text_helper.h
history/view/history_view_transcribe_button.cpp history/view/history_view_transcribe_button.cpp
history/view/history_view_transcribe_button.h history/view/history_view_transcribe_button.h
history/view/history_view_translate_bar.cpp history/view/history_view_translate_bar.cpp
@ -1278,6 +1292,8 @@ PRIVATE
payments/payments_checkout_process.h payments/payments_checkout_process.h
payments/payments_form.cpp payments/payments_form.cpp
payments/payments_form.h payments/payments_form.h
payments/payments_non_panel_process.cpp
payments/payments_non_panel_process.h
platform/linux/file_utilities_linux.cpp platform/linux/file_utilities_linux.cpp
platform/linux/file_utilities_linux.h platform/linux/file_utilities_linux.h
platform/linux/launcher_linux.cpp platform/linux/launcher_linux.cpp
@ -1423,6 +1439,10 @@ PRIVATE
settings/settings_codes.h settings/settings_codes.h
settings/settings_common_session.cpp settings/settings_common_session.cpp
settings/settings_common_session.h settings/settings_common_session.h
settings/settings_credits.cpp
settings/settings_credits.h
settings/settings_credits_graphics.cpp
settings/settings_credits_graphics.h
settings/settings_experimental.cpp settings/settings_experimental.cpp
settings/settings_experimental.h settings/settings_experimental.h
settings/settings_folders.cpp settings/settings_folders.cpp
@ -1518,6 +1538,8 @@ PRIVATE
ui/controls/silent_toggle.h ui/controls/silent_toggle.h
ui/controls/userpic_button.cpp ui/controls/userpic_button.cpp
ui/controls/userpic_button.h ui/controls/userpic_button.h
ui/effects/credits_graphics.cpp
ui/effects/credits_graphics.h
ui/effects/emoji_fly_animation.cpp ui/effects/emoji_fly_animation.cpp
ui/effects/emoji_fly_animation.h ui/effects/emoji_fly_animation.h
ui/effects/message_sending_animation_common.h ui/effects/message_sending_animation_common.h
@ -1565,6 +1587,8 @@ PRIVATE
window/section_widget.h window/section_widget.h
window/window_adaptive.cpp window/window_adaptive.cpp
window/window_adaptive.h window/window_adaptive.h
window/window_chat_preview.cpp
window/window_chat_preview.h
window/window_connecting_widget.cpp window/window_connecting_widget.cpp
window/window_connecting_widget.h window/window_connecting_widget.h
window/window_controller.cpp window/window_controller.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -251,6 +251,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters."; "lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters.";
"lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character."; "lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character.";
"lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters."; "lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters.";
"lng_caption_move_up" = "Move Caption Up";
"lng_caption_move_down" = "Move Caption Down";
"lng_file_size_limit_title" = "File Too Large"; "lng_file_size_limit_title" = "File Too Large";
"lng_file_size_limit#one" = "{count} Gb"; "lng_file_size_limit#one" = "{count} Gb";
@ -302,6 +304,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sure_ban_admin" = "This user is an admin. Are you sure you want to go ahead and restrict them?"; "lng_sure_ban_admin" = "This user is an admin. Are you sure you want to go ahead and restrict them?";
"lng_sure_enable_socks" = "Are you sure you want to enable this proxy?\n\nServer: {server}\nPort: {port}\n\nYou can change your proxy server later in the Settings (Connection Type)."; "lng_sure_enable_socks" = "Are you sure you want to enable this proxy?\n\nServer: {server}\nPort: {port}\n\nYou can change your proxy server later in the Settings (Connection Type).";
"lng_sure_enable" = "Enable"; "lng_sure_enable" = "Enable";
"lng_proxy_box_title" = "Enable proxy";
"lng_proxy_box_server" = "Server";
"lng_proxy_box_port" = "Port";
"lng_proxy_box_secret" = "Secret";
"lng_proxy_box_status" = "Status";
"lng_proxy_box_username" = "Username";
"lng_proxy_box_password" = "Password";
"lng_proxy_invalid" = "The proxy link is invalid."; "lng_proxy_invalid" = "The proxy link is invalid.";
"lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version."; "lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version.";
@ -561,6 +570,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reaction_invoice" = "{reaction} to your invoice"; "lng_reaction_invoice" = "{reaction} to your invoice";
"lng_reaction_gif" = "{reaction} to your GIF"; "lng_reaction_gif" = "{reaction} to your GIF";
"lng_effect_add_title" = "Add an animated effect";
"lng_effect_stickers_title" = "Effects from stickers";
"lng_effect_send" = "Send with Effect";
"lng_effect_none" = "No effects found.";
"lng_effect_premium" = "Subscribe to {link} to add this animated effect.";
"lng_effect_premium_link" = "Telegram Premium";
"lng_languages" = "Languages"; "lng_languages" = "Languages";
"lng_languages_none" = "No languages found."; "lng_languages_none" = "No languages found.";
"lng_languages_count#one" = "{count} language"; "lng_languages_count#one" = "{count} language";
@ -769,6 +785,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_power_chat" = "Animations in Chats"; "lng_settings_power_chat" = "Animations in Chats";
"lng_settings_power_chat_background" = "Background rotation"; "lng_settings_power_chat_background" = "Background rotation";
"lng_settings_power_chat_spoiler" = "Animated spoiler effect"; "lng_settings_power_chat_spoiler" = "Animated spoiler effect";
"lng_settings_power_chat_effects" = "Effects in messages";
"lng_settings_power_calls" = "Animations in Calls"; "lng_settings_power_calls" = "Animations in Calls";
"lng_settings_power_ui" = "Interface animations"; "lng_settings_power_ui" = "Interface animations";
"lng_settings_power_auto" = "Save Power on Low Battery"; "lng_settings_power_auto" = "Save Power on Low Battery";
@ -1053,6 +1070,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_sponsor" = "Proxy sponsor"; "lng_proxy_sponsor" = "Proxy sponsor";
"lng_proxy_sponsor_about" = "This channel is shown by your proxy server.\nTo remove this channel from your chats list,\ndisable the proxy in Telegram Settings."; "lng_proxy_sponsor_about" = "This channel is shown by your proxy server.\nTo remove this channel from your chats list,\ndisable the proxy in Telegram Settings.";
"lng_proxy_sponsor_warning" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic."; "lng_proxy_sponsor_warning" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic.";
"lng_proxy_add_from_clipboard" = "Add proxy from clipboard";
"lng_proxy_add_from_clipboard_good_toast" = "Proxy was added from clipboard.";
"lng_proxy_add_from_clipboard_failed_toast" = "This is not a proxy link.";
"lng_proxy_add_from_clipboard_existing_toast" = "This proxy is already in the list.";
"lng_badge_psa_default" = "PSA"; "lng_badge_psa_default" = "PSA";
"lng_about_psa_default" = "This message provides you with a public service announcement. To remove it from your chats list, right click it and select **Hide**."; "lng_about_psa_default" = "This message provides you with a public service announcement. To remove it from your chats list, right click it and select **Hide**.";
"lng_tooltip_psa_default" = "This message provides you with a public service announcement."; "lng_tooltip_psa_default" = "This message provides you with a public service announcement.";
@ -1522,8 +1543,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_link_invite" = "Invite link"; "lng_manage_peer_link_invite" = "Invite link";
"lng_manage_peer_link_expired" = "Expired link"; "lng_manage_peer_link_expired" = "Expired link";
"lng_manage_private_group_title" = "Private"; "lng_manage_private_group_title" = "Private";
"lng_manage_private_group_noforwards_title" = "Private restricted";
"lng_manage_public_group_title" = "Public"; "lng_manage_public_group_title" = "Public";
"lng_manage_private_peer_title" = "Private"; "lng_manage_private_peer_title" = "Private";
"lng_manage_private_peer_noforwards_title" = "Private restricted";
"lng_manage_public_peer_title" = "Public"; "lng_manage_public_peer_title" = "Public";
"lng_manage_peer_send_title" = "Who can send new messages?"; "lng_manage_peer_send_title" = "Who can send new messages?";
"lng_manage_peer_send_only_members" = "Only members"; "lng_manage_peer_send_only_members" = "Only members";
@ -2285,6 +2308,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_business_about_sponsored_link" = "Telegram Ad Platform {emoji}"; "lng_business_about_sponsored_link" = "Telegram Ad Platform {emoji}";
"lng_business_about_sponsored_url" = "https://ads.telegram.org"; "lng_business_about_sponsored_url" = "https://ads.telegram.org";
"lng_credits_summary_title" = "Telegram Stars";
"lng_credits_summary_about" = "Buy Stars to unlock content and services in miniapps on Telegram.";
"lng_credits_summary_options_subtitle" = "Choose package";
"lng_credits_summary_options_credits#one" = "{count} Star";
"lng_credits_summary_options_credits#other" = "{count} Stars";
"lng_credits_summary_options_more" = "More Options";
"lng_credits_summary_options_about" = "By proceeding and purchasing Stars, you agree with the {link}.";
"lng_credits_summary_options_about_link" = "Terms and Conditions";
"lng_credits_summary_history_tab_full" = "All Transactions";
"lng_credits_summary_history_tab_in" = "Incoming";
"lng_credits_summary_history_tab_out" = "Outgoing";
"lng_credits_summary_history_entry_inner_in" = "In-App Purchase";
"lng_credits_summary_balance" = "Balance";
"lng_credits_box_out_title" = "Confirm Your Purchase";
"lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?";
"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
"lng_credits_box_out_confirm#one" = "Confirm and Pay {emoji} {count} Star";
"lng_credits_box_out_confirm#other" = "Confirm and Pay {emoji} {count} Stars";
"lng_credits_box_out_about" = "Review the {link} for Stars.";
"lng_credits_summary_in_toast_title" = "Stars Acquired";
"lng_credits_summary_in_toast_about#one" = "**{count}** Star added to your balance.";
"lng_credits_summary_in_toast_about#other" = "**{count}** Stars added to your balance.";
"lng_credits_box_history_entry_peer" = "Recipient";
"lng_credits_box_history_entry_id" = "Transaction ID";
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
"lng_credits_box_history_entry_about" = "You can dispute this transaction {link}.";
"lng_credits_box_history_entry_about_link" = "here";
"lng_credits_small_balance_title#one" = "{count} Star Needed";
"lng_credits_small_balance_title#other" = "{count} Stars Needed";
"lng_credits_small_balance_about" = "Buy **Stars** and use them on **{bot}** and other miniapps.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_location_title" = "Location"; "lng_location_title" = "Location";
"lng_location_about" = "Display the location of your business on your account."; "lng_location_about" = "Display the location of your business on your account.";
"lng_location_address" = "Enter Address"; "lng_location_address" = "Enter Address";
@ -3177,6 +3232,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_reply_msg" = "Reply"; "lng_context_reply_msg" = "Reply";
"lng_context_quote_and_reply" = "Quote & Reply"; "lng_context_quote_and_reply" = "Quote & Reply";
"lng_context_edit_msg" = "Edit"; "lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit Fact Check";
"lng_context_forward_msg" = "Forward Message"; "lng_context_forward_msg" = "Forward Message";
"lng_context_send_now_msg" = "Send now"; "lng_context_send_now_msg" = "Send now";
"lng_context_reschedule" = "Reschedule"; "lng_context_reschedule" = "Reschedule";
@ -3248,6 +3305,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_spoiler_effect" = "Hide with Spoiler"; "lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler"; "lng_context_disable_spoiler" = "Remove Spoiler";
"lng_factcheck_title" = "Fact Check";
"lng_factcheck_placeholder" = "Add Facts or Context";
"lng_factcheck_whats_this" = "what's this?";
"lng_factcheck_about" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.";
"lng_factcheck_add_done" = "Fact check added.";
"lng_factcheck_edit_done" = "Fact check edited.";
"lng_factcheck_remove_done" = "Fact check removed.";
"lng_factcheck_bottom" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country ({country}) responsible for combatting misinformation.";
"lng_factcheck_links" = "Only **t.me/** links are allowed.";
"lng_translate_show_original" = "Show Original"; "lng_translate_show_original" = "Show Original";
"lng_translate_bar_to" = "Translate to {name}"; "lng_translate_bar_to" = "Translate to {name}";
"lng_translate_bar_to_other" = "Translate to {name}"; "lng_translate_bar_to_other" = "Translate to {name}";
@ -3368,6 +3435,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_add_contact" = "Create"; "lng_add_contact" = "Create";
"lng_add_contact_button" = "New contact"; "lng_add_contact_button" = "New contact";
"lng_contacts_header" = "Contacts"; "lng_contacts_header" = "Contacts";
"lng_menu_not_contact" = "This number is not on Telegram";
"lng_contacts_hidden_stories" = "Hidden Stories"; "lng_contacts_hidden_stories" = "Hidden Stories";
"lng_contacts_stories_status#one" = "{count} story"; "lng_contacts_stories_status#one" = "{count} story";
"lng_contacts_stories_status#other" = "{count} stories"; "lng_contacts_stories_status#other" = "{count} stories";
@ -5161,6 +5229,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_font_system" = "System font"; "lng_font_system" = "System font";
"lng_font_not_found" = "Font not found."; "lng_font_not_found" = "Font not found.";
"lng_search_tab_my_messages" = "My Messages";
"lng_search_tab_this_topic" = "This Topic";
"lng_search_tab_this_chat" = "This Chat";
"lng_search_tab_this_channel" = "This Channel";
"lng_search_tab_this_group" = "This Group";
"lng_search_tab_public_posts" = "Public Posts";
"lng_search_tab_no_results" = "No Results";
"lng_search_tab_no_results_text" = "There were no results for \"{query}\".";
"lng_search_tab_no_results_retry" = "Try another hashtag.";
"lng_search_tab_by_hashtag" = "Enter a hashtag to find messages containing it.";
"lng_contact_details_button" = "View Contact";
"lng_contact_details_title" = "Contact details";
"lng_contact_details_phone" = "Phone";
"lng_contact_details_phone_main" = "Main Phone";
"lng_contact_details_phone_home" = "Home Phone";
"lng_contact_details_phone_mobile" = "Mobile Phone";
"lng_contact_details_phone_work" = "Work Phone";
"lng_contact_details_phone_other" = "Other Phone";
"lng_contact_details_email" = "Email";
"lng_contact_details_address" = "Address";
"lng_contact_details_url" = "URL";
"lng_contact_details_note" = "Note";
"lng_contact_details_birthday" = "Birthday";
"lng_contact_details_organization" = "Organization";
// Wnd specific // Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program..."; "lng_wnd_choose_program_menu" = "Choose Default Program...";

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.0.2.0" /> Version="5.1.2.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,0,2,0 FILEVERSION 5,1,2,0
PRODUCTVERSION 5,0,2,0 PRODUCTVERSION 5,1,2,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "5.0.2.0" VALUE "FileVersion", "5.1.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.0.2.0" VALUE "ProductVersion", "5.1.2.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,0,2,0 FILEVERSION 5,1,2,0
PRODUCTVERSION 5,0,2,0 PRODUCTVERSION 5,1,2,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "5.0.2.0" VALUE "FileVersion", "5.1.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "5.0.2.0" VALUE "ProductVersion", "5.1.2.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "inline_bots/bot_attach_web_view.h" #include "inline_bots/bot_attach_web_view.h"
#include "payments/payments_checkout_process.h" #include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "mainwidget.h" #include "mainwidget.h"
#include "mainwindow.h" #include "mainwindow.h"
@ -335,7 +336,8 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
Payments::Mode::Payment, Payments::Mode::Payment,
crl::guard(controller, [=](auto) { crl::guard(controller, [=](auto) {
controller->widget()->activate(); controller->widget()->activate();
})); }),
Payments::ProcessNonPanelPaymentFormFactory(controller, item));
} break; } break;
case ButtonType::Url: { case ButtonType::Url: {

View file

@ -23,8 +23,10 @@ struct SendOptions {
PeerData *sendAs = nullptr; PeerData *sendAs = nullptr;
TimeId scheduled = 0; TimeId scheduled = 0;
BusinessShortcutId shortcutId = 0; BusinessShortcutId shortcutId = 0;
EffectId effectId = 0;
bool silent = false; bool silent = false;
bool handleSupportSwitch = false; bool handleSupportSwitch = false;
bool invertCaption = false;
bool hideViaBot = false; bool hideViaBot = false;
crl::time ttlSeconds = 0; crl::time ttlSeconds = 0;
}; };

View file

@ -97,8 +97,10 @@ void ConfirmPhone::resolve(
box->resendRequests( box->resendRequests(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_api.request(MTPauth_ResendCode( _api.request(MTPauth_ResendCode(
MTP_flags(0),
MTP_string(phone), MTP_string(phone),
MTP_string(phoneHash) MTP_string(phoneHash),
MTPstring() // reason
)).done([=] { )).done([=] {
if (boxWeak) { if (boxWeak) {
boxWeak->callDone(); boxWeak->callDone();

View file

@ -0,0 +1,222 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_credits.h"
#include "apiwrap.h"
#include "api/api_updates.h"
#include "base/unixtime.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL(
const MTPStarsTransaction &tl,
not_null<PeerData*> peer) {
using HistoryPeerTL = MTPDstarsTransactionPeer;
const auto photo = tl.data().vphoto()
? peer->owner().photoFromWeb(*tl.data().vphoto(), ImageLocation())
: nullptr;
return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()),
.description = qs(tl.data().vdescription().value_or_empty()),
.date = base::unixtime::parse(tl.data().vdate().v),
.photoId = photo ? photo->id : 0,
.credits = tl.data().vstars().v,
.bareId = tl.data().vpeer().match([](const HistoryPeerTL &p) {
return peerFromMTP(p.vpeer());
}, [](const auto &) {
return PeerId(0);
}).value,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
return Data::CreditsHistoryEntry::PeerType::PlayMarket;
}, [](const MTPDstarsTransactionPeerFragment &) {
return Data::CreditsHistoryEntry::PeerType::Fragment;
}, [](const MTPDstarsTransactionPeerAppStore &) {
return Data::CreditsHistoryEntry::PeerType::AppStore;
}, [](const MTPDstarsTransactionPeerUnsupported &) {
return Data::CreditsHistoryEntry::PeerType::Unsupported;
}, [](const MTPDstarsTransactionPeerPremiumBot &) {
return Data::CreditsHistoryEntry::PeerType::PremiumBot;
}),
.refunded = tl.data().is_refund(),
};
}
[[nodiscard]] Data::CreditsStatusSlice StatusFromTL(
const MTPpayments_StarsStatus &status,
not_null<PeerData*> peer) {
peer->owner().processUsers(status.data().vusers());
peer->owner().processChats(status.data().vchats());
return Data::CreditsStatusSlice{
.list = ranges::views::all(
status.data().vhistory().v
) | ranges::views::transform([&](const MTPStarsTransaction &tl) {
return HistoryFromTL(tl, peer);
}) | ranges::to_vector,
.balance = status.data().vbalance().v,
.allLoaded = !status.data().vnext_offset().has_value(),
.token = qs(status.data().vnext_offset().value_or_empty()),
};
}
} // namespace
CreditsTopupOptions::CreditsTopupOptions(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
using TLOption = MTPStarsTopupOption;
_api.request(MTPpayments_GetStarsTopupOptions(
)).done([=](const MTPVector<TLOption> &result) {
_options = ranges::views::all(
result.v
) | ranges::views::transform([](const TLOption &option) {
return Data::CreditTopupOption{
.credits = option.data().vstars().v,
.product = qs(
option.data().vstore_product().value_or_empty()),
.currency = qs(option.data().vcurrency()),
.amount = option.data().vamount().v,
.extended = option.data().is_extended(),
};
}) | ranges::to_vector;
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
CreditsStatus::CreditsStatus(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
void CreditsStatus::request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
if (_requestId) {
return;
}
using TLResult = MTPpayments_StarsStatus;
_requestId = _api.request(MTPpayments_GetStarsStatus(
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input
)).done([=](const TLResult &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
}).fail([=] {
_requestId = 0;
done({});
}).send();
}
CreditsHistory::CreditsHistory(not_null<PeerData*> peer, bool in, bool out)
: _peer(peer)
, _flags((in == out)
? HistoryTL::Flags(0)
: HistoryTL::Flags(0)
| (in ? HistoryTL::Flag::f_inbound : HistoryTL::Flags(0))
| (out ? HistoryTL::Flag::f_outbound : HistoryTL::Flags(0)))
, _api(&peer->session().api().instance()) {
}
void CreditsHistory::request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input,
MTP_string(token)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
}).fail([=] {
_requestId = 0;
done({});
}).send();
}
Data::CreditTopupOptions CreditsTopupOptions::options() const {
return _options;
}
rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session) {
const auto username = session->appConfig().get<QString>(
u"premium_bot_username"_q,
QString());
if (username.isEmpty()) {
return rpl::never<not_null<PeerData*>>();
}
if (const auto p = session->data().peerByUsername(username)) {
return rpl::single<not_null<PeerData*>>(p);
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto api = lifetime.make_state<MTP::Sender>(&session->mtp());
api->request(MTPcontacts_ResolveUsername(
MTP_string(username)
)).done([=](const MTPcontacts_ResolvedPeer &result) {
session->data().processUsers(result.data().vusers());
session->data().processChats(result.data().vchats());
const auto botPeer = session->data().peerLoaded(
peerFromMTP(result.data().vpeer()));
if (!botPeer) {
return consumer.put_done();
}
consumer.put_next(not_null{ botPeer });
}).send();
return lifetime;
};
}
void CreditsRefund(
not_null<PeerData*> peer,
const QString &entryId,
Fn<void()> done,
Fn<void(QString)> fail) {
const auto user = peer->asUser();
if (!user) {
return;
}
peer->session().api().request(MTPpayments_RefundStarsCharge(
user->inputUser,
MTP_string(entryId)
)).done([=](const MTPUpdates &result) {
peer->session().api().updates().applyUpdates(result);
done();
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
} // namespace Api

View file

@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/data_credits.h"
#include "mtproto/sender.h"
namespace Main {
class Session;
} // namespace Main
namespace Api {
class CreditsTopupOptions final {
public:
CreditsTopupOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::CreditTopupOptions options() const;
private:
const not_null<PeerData*> _peer;
Data::CreditTopupOptions _options;
MTP::Sender _api;
};
class CreditsStatus final {
public:
CreditsStatus(not_null<PeerData*> peer);
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
private:
const not_null<PeerData*> _peer;
mtpRequestId _requestId = 0;
MTP::Sender _api;
};
class CreditsHistory final {
public:
CreditsHistory(not_null<PeerData*> peer, bool in, bool out);
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
private:
using HistoryTL = MTPpayments_GetStarsTransactions;
const not_null<PeerData*> _peer;
const HistoryTL::Flags _flags;
mtpRequestId _requestId = 0;
MTP::Sender _api;
};
void CreditsRefund(
not_null<PeerData*> peer,
const QString &entryId,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session);
} // namespace Api

View file

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_histories.h" #include "data/data_histories.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_web_page.h" #include "data/data_web_page.h"
#include "history/view/controls/history_view_compose_media_edit_manager.h"
#include "history/history.h" #include "history/history.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -81,7 +82,8 @@ mtpRequestId EditMessage(
| ((!webpage.removed && !webpage.url.isEmpty()) | ((!webpage.removed && !webpage.url.isEmpty())
? MTPmessages_EditMessage::Flag::f_media ? MTPmessages_EditMessage::Flag::f_media
: emptyFlag) : emptyFlag)
| ((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
|| options.invertCaption)
? MTPmessages_EditMessage::Flag::f_invert_media ? MTPmessages_EditMessage::Flag::f_invert_media
: emptyFlag) : emptyFlag)
| (!sentEntities.v.isEmpty() | (!sentEntities.v.isEmpty()
@ -203,6 +205,7 @@ void RescheduleMessage(
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
SendOptions options) { SendOptions options) {
const auto empty = [] {}; const auto empty = [] {};
options.invertCaption = item->invertMedia();
EditMessage(item, options, empty, empty); EditMessage(item, options, empty, empty);
} }
@ -254,85 +257,85 @@ mtpRequestId EditTextMessage(
SendOptions options, SendOptions options,
Fn<void(mtpRequestId requestId)> done, Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail, Fn<void(const QString &error, mtpRequestId requestId)> fail,
std::optional<bool> spoilerMediaOverride) { bool spoilered) {
if (spoilerMediaOverride) { const auto media = item->media();
const auto spoiler = *spoilerMediaOverride; if (media
if (const auto media = item->media()) { && HistoryView::MediaEditManager::CanBeSpoilered(item)
auto takeInputMedia = Fn<std::optional<MTPInputMedia>()>(nullptr); && spoilered != media->hasSpoiler()) {
auto takeFileReference = Fn<QByteArray()>(nullptr); auto takeInputMedia = Fn<std::optional<MTPInputMedia>()>(nullptr);
if (const auto photo = media->photo()) { auto takeFileReference = Fn<QByteArray()>(nullptr);
using Flag = MTPDinputMediaPhoto::Flag; if (const auto photo = media->photo()) {
const auto flags = Flag() using Flag = MTPDinputMediaPhoto::Flag;
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag()) const auto flags = Flag()
| (spoiler ? Flag::f_spoiler : Flag()); | (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
takeInputMedia = [=] { | (spoilered ? Flag::f_spoiler : Flag());
return MTP_inputMediaPhoto( takeInputMedia = [=] {
MTP_flags(flags), return MTP_inputMediaPhoto(
photo->mtpInput(), MTP_flags(flags),
MTP_int(media->ttlSeconds())); photo->mtpInput(),
}; MTP_int(media->ttlSeconds()));
takeFileReference = [=] { return photo->fileReference(); };
} else if (const auto document = media->document()) {
using Flag = MTPDinputMediaDocument::Flag;
const auto flags = Flag()
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
| (spoiler ? Flag::f_spoiler : Flag());
takeInputMedia = [=] {
return MTP_inputMediaDocument(
MTP_flags(flags),
document->mtpInput(),
MTP_int(media->ttlSeconds()),
MTPstring()); // query
};
takeFileReference = [=] { return document->fileReference(); };
}
const auto usedFileReference = takeFileReference
? takeFileReference()
: QByteArray();
const auto origin = item->fullId();
const auto api = &item->history()->session().api();
const auto performRequest = [=](
const auto &repeatRequest,
mtpRequestId originalRequestId) -> mtpRequestId {
const auto handleReference = [=](
const QString &error,
mtpRequestId requestId) {
if (error.startsWith(u"FILE_REFERENCE_"_q)) {
api->refreshFileReference(origin, [=](const auto &) {
if (takeFileReference &&
(takeFileReference() != usedFileReference)) {
repeatRequest(
repeatRequest,
originalRequestId
? originalRequestId
: requestId);
} else {
fail(error, requestId);
}
});
} else {
fail(error, requestId);
}
};
const auto callback = [=](
Fn<void()> applyUpdates,
mtpRequestId requestId) {
applyUpdates();
done(originalRequestId ? originalRequestId : requestId);
};
const auto requestId = EditMessage(
item,
caption,
webpage,
options,
callback,
handleReference,
takeInputMedia ? takeInputMedia() : std::nullopt);
return originalRequestId ? originalRequestId : requestId;
}; };
return performRequest(performRequest, 0); takeFileReference = [=] { return photo->fileReference(); };
} else if (const auto document = media->document()) {
using Flag = MTPDinputMediaDocument::Flag;
const auto flags = Flag()
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
| (spoilered ? Flag::f_spoiler : Flag());
takeInputMedia = [=] {
return MTP_inputMediaDocument(
MTP_flags(flags),
document->mtpInput(),
MTP_int(media->ttlSeconds()),
MTPstring()); // query
};
takeFileReference = [=] { return document->fileReference(); };
} }
const auto usedFileReference = takeFileReference
? takeFileReference()
: QByteArray();
const auto origin = item->fullId();
const auto api = &item->history()->session().api();
const auto performRequest = [=](
const auto &repeatRequest,
mtpRequestId originalRequestId) -> mtpRequestId {
const auto handleReference = [=](
const QString &error,
mtpRequestId requestId) {
if (error.startsWith(u"FILE_REFERENCE_"_q)) {
api->refreshFileReference(origin, [=](const auto &) {
if (takeFileReference &&
(takeFileReference() != usedFileReference)) {
repeatRequest(
repeatRequest,
originalRequestId
? originalRequestId
: requestId);
} else {
fail(error, requestId);
}
});
} else {
fail(error, requestId);
}
};
const auto callback = [=](
Fn<void()> applyUpdates,
mtpRequestId requestId) {
applyUpdates();
done(originalRequestId ? originalRequestId : requestId);
};
const auto requestId = EditMessage(
item,
caption,
webpage,
options,
callback,
handleReference,
takeInputMedia ? takeInputMedia() : std::nullopt);
return originalRequestId ? originalRequestId : requestId;
};
return performRequest(performRequest, 0);
} }
const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) { const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {

View file

@ -56,6 +56,6 @@ mtpRequestId EditTextMessage(
SendOptions options, SendOptions options,
Fn<void(mtpRequestId requestId)> done, Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail, Fn<void(const QString &error, mtpRequestId requestId)> fail,
std::optional<bool> spoilerMediaOverride); bool spoilered);
} // namespace Api } // namespace Api

View file

@ -73,6 +73,9 @@ void Polls::create(
if (action.options.shortcutId) { if (action.options.shortcutId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
} }
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
const auto sendAs = action.options.sendAs; const auto sendAs = action.options.sendAs;
if (sendAs) { if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
@ -94,7 +97,8 @@ void Polls::create(
MTPVector<MTPMessageEntity>(), MTPVector<MTPMessageEntity>(),
MTP_int(action.options.scheduled), MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId) Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) { ), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) { if (clearCloudDraft) {
history->finishSavingCloudDraft( history->finishSavingCloudDraft(

View file

@ -133,6 +133,13 @@ void SendExistingMedia(
flags |= MessageFlag::ShortcutMessage; flags |= MessageFlag::ShortcutMessage;
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
} }
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
session->data().registerMessageRandomId(randomId, newId); session->data().registerMessageRandomId(randomId, newId);
@ -144,6 +151,7 @@ void SendExistingMedia(
.date = HistoryItem::NewMessageDate(action.options), .date = HistoryItem::NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
.effectId = action.options.effectId,
}, media, caption); }, media, caption);
const auto performRequest = [=](const auto &repeatRequest) -> void { const auto performRequest = [=](const auto &repeatRequest) -> void {
@ -165,7 +173,8 @@ void SendExistingMedia(
sentEntities, sentEntities,
MTP_int(action.options.scheduled), MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId) Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) { ), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) {
if (error.code() == 400 if (error.code() == 400
@ -306,6 +315,13 @@ bool SendDice(MessageToSend &message) {
flags |= MessageFlag::ShortcutMessage; flags |= MessageFlag::ShortcutMessage;
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
} }
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
session->data().registerMessageRandomId(randomId, newId); session->data().registerMessageRandomId(randomId, newId);
@ -317,6 +333,7 @@ bool SendDice(MessageToSend &message) {
.date = HistoryItem::NewMessageDate(action.options), .date = HistoryItem::NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaDice( }, TextWithEntities(), MTP_messageMediaDice(
MTP_int(0), MTP_int(0),
MTP_string(emoji))); MTP_string(emoji)));
@ -335,7 +352,8 @@ bool SendDice(MessageToSend &message) {
MTP_vector<MTPMessageEntity>(), MTP_vector<MTPMessageEntity>(),
MTP_int(action.options.scheduled), MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId) Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) { ), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId, newId); api->sendMessageFail(error, peer, randomId, newId);
@ -430,6 +448,9 @@ void SendConfirmedFile(
flags |= MessageFlag::MediaIsUnread; flags |= MessageFlag::MediaIsUnread;
} }
} }
if (file->to.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
}
const auto messageFromId = file->to.options.sendAs const auto messageFromId = file->to.options.sendAs
? file->to.options.sendAs->id ? file->to.options.sendAs->id
@ -493,6 +514,7 @@ void SendConfirmedFile(
edition.ttl = 0; edition.ttl = 0;
edition.mtpMedia = &media; edition.mtpMedia = &media;
edition.textWithEntities = caption; edition.textWithEntities = caption;
edition.invertMedia = file->to.options.invertCaption;
edition.useSameViews = true; edition.useSameViews = true;
edition.useSameForwards = true; edition.useSameForwards = true;
edition.useSameMarkup = true; edition.useSameMarkup = true;
@ -510,6 +532,7 @@ void SendConfirmedFile(
.shortcutId = file->to.options.shortcutId, .shortcutId = file->to.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
.groupedId = groupId, .groupedId = groupId,
.effectId = file->to.options.effectId,
}, caption, media); }, caption, media);
} }

View file

@ -178,7 +178,11 @@ EntitiesInText EntitiesFromMTP(
}); });
} }
}, [&](const MTPDmessageEntityPhone &d) { }, [&](const MTPDmessageEntityPhone &d) {
// Skipping phones. result.push_back({
EntityType::Phone,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityCashtag &d) { }, [&](const MTPDmessageEntityCashtag &d) {
result.push_back({ result.push_back({
EntityType::Cashtag, EntityType::Cashtag,
@ -217,6 +221,7 @@ EntitiesInText EntitiesFromMTP(
EntityType::Blockquote, EntityType::Blockquote,
d.voffset().v, d.voffset().v,
d.vlength().v, d.vlength().v,
d.is_collapsed() ? u"1"_q : QString(),
}); });
}); });
} }
@ -265,6 +270,9 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
case EntityType::Email: { case EntityType::Email: {
v.push_back(MTP_messageEntityEmail(offset, length)); v.push_back(MTP_messageEntityEmail(offset, length));
} break; } break;
case EntityType::Phone: {
v.push_back(MTP_messageEntityPhone(offset, length));
} break;
case EntityType::Hashtag: { case EntityType::Hashtag: {
v.push_back(MTP_messageEntityHashtag(offset, length)); v.push_back(MTP_messageEntityHashtag(offset, length));
} break; } break;
@ -311,7 +319,13 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(
MTP_string(entity.data()))); MTP_string(entity.data())));
} break; } break;
case EntityType::Blockquote: { case EntityType::Blockquote: {
v.push_back(MTP_messageEntityBlockquote(offset, length)); using Flag = MTPDmessageEntityBlockquote::Flag;
const auto collapsed = !entity.data().isEmpty();
v.push_back(
MTP_messageEntityBlockquote(
MTP_flags(collapsed ? Flag::f_collapsed : Flag()),
offset,
length));
} break; } break;
case EntityType::Spoiler: { case EntityType::Spoiler: {
v.push_back(MTP_messageEntitySpoiler(offset, length)); v.push_back(MTP_messageEntitySpoiler(offset, length));

View file

@ -1145,7 +1145,9 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTP_int(d.vttl_period().value_or_empty()), MTP_int(d.vttl_period().value_or_empty()),
MTPint()), // quick_reply_shortcut_id MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck()),
MessageFlags(), MessageFlags(),
NewMessageType::Unread); NewMessageType::Unread);
} break; } break;
@ -1180,7 +1182,9 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTP_int(d.vttl_period().value_or_empty()), MTP_int(d.vttl_period().value_or_empty()),
MTPint()), // quick_reply_shortcut_id MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck()),
MessageFlags(), MessageFlags(),
NewMessageType::Unread); NewMessageType::Unread);
} break; } break;
@ -2618,6 +2622,11 @@ void Updates::feedUpdate(const MTPUpdate &update) {
_session->data().stories().apply(data.vstealth_mode()); _session->data().stories().apply(data.vstealth_mode());
} break; } break;
case mtpc_updateStarsBalance: {
const auto &data = update.c_updateStarsBalance();
_session->setCredits(data.vbalance().v);
} break;
} }
} }

View file

@ -50,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h" #include "data/data_user.h"
#include "data/data_chat_filters.h" #include "data/data_chat_filters.h"
#include "data/data_histories.h" #include "data/data_histories.h"
#include "data/data_history_messages.h"
#include "core/core_cloud_password.h" #include "core/core_cloud_password.h"
#include "core/application.h" #include "core/application.h"
#include "base/unixtime.h" #include "base/unixtime.h"
@ -3117,6 +3118,46 @@ void ApiWrap::resolveJumpToHistoryDate(
} }
} }
void ApiWrap::requestHistory(
not_null<History*> history,
MsgId messageId,
SliceType slice) {
const auto peer = history->peer;
const auto key = HistoryRequest{
peer,
messageId,
slice,
};
if (_historyRequests.contains(key)) {
return;
}
const auto prepared = Api::PrepareHistoryRequest(peer, messageId, slice);
auto &histories = history->owner().histories();
const auto requestType = Data::Histories::RequestType::History;
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
return request(
std::move(prepared)
).done([=](const Api::HistoryRequestResult &result) {
_historyRequests.remove(key);
auto parsed = Api::ParseHistoryResult(
peer,
messageId,
slice,
result);
history->messages().addSlice(
std::move(parsed.messageIds),
parsed.noSkipRange,
parsed.fullCount);
finish();
}).fail([=] {
_historyRequests.remove(key);
finish();
}).send();
});
_historyRequests.emplace(key);
}
void ApiWrap::requestSharedMedia( void ApiWrap::requestSharedMedia(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId topicRootId, MsgId topicRootId,
@ -3388,6 +3429,9 @@ void ApiWrap::forwardMessages(
.date = HistoryItem::NewMessageDate(action.options), .date = HistoryItem::NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
// forwarded messages don't have effects
//.effectId = action.options.effectId,
}, item); }, item);
_session->data().registerMessageRandomId(randomId, newId); _session->data().registerMessageRandomId(randomId, newId);
if (!localIds) { if (!localIds) {
@ -3488,6 +3532,7 @@ void ApiWrap::sendSharedContact(
.date = HistoryItem::NewMessageDate(action.options), .date = HistoryItem::NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
.effectId = action.options.effectId,
}, TextWithEntities(), MTP_messageMediaContact( }, TextWithEntities(), MTP_messageMediaContact(
MTP_string(phone), MTP_string(phone),
MTP_string(firstName), MTP_string(firstName),
@ -3790,7 +3835,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
const auto anonymousPost = peer->amAnonymous(); const auto anonymousPost = peer->amAnonymous();
const auto silentPost = ShouldSendSilent(peer, action.options); const auto silentPost = ShouldSendSilent(peer, action.options);
FillMessagePostFlags(action, peer, flags); FillMessagePostFlags(action, peer, flags);
if (exactWebPage && !ignoreWebPage && message.webPage.invert) { if ((exactWebPage && !ignoreWebPage && message.webPage.invert)
|| action.options.invertCaption) {
flags |= MessageFlag::InvertMedia; flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media; sendFlags |= MTPmessages_SendMessage::Flag::f_invert_media;
mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media; mediaFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
@ -3836,6 +3882,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut;
mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
} }
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMessage::Flag::f_effect;
mediaFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
lastMessage = history->addNewLocalMessage({ lastMessage = history->addNewLocalMessage({
.id = newId.msg, .id = newId.msg,
.flags = flags, .flags = flags,
@ -3844,6 +3894,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
.date = HistoryItem::NewMessageDate(action.options), .date = HistoryItem::NewMessageDate(action.options),
.shortcutId = action.options.shortcutId, .shortcutId = action.options.shortcutId,
.postAuthor = messagePostAuthor, .postAuthor = messagePostAuthor,
.effectId = action.options.effectId,
}, sending, media); }, sending, media);
const auto done = [=]( const auto done = [=](
const MTPUpdates &result, const MTPUpdates &result,
@ -3891,7 +3942,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sentEntities, sentEntities,
MTP_int(action.options.scheduled), MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut mtpShortcut,
MTP_long(action.options.effectId)
), done, fail); ), done, fail);
} else { } else {
histories.sendPreparedMessage( histories.sendPreparedMessage(
@ -3908,7 +3960,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sentEntities, sentEntities,
MTP_int(action.options.scheduled), MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut mtpShortcut,
MTP_long(action.options.effectId)
), done, fail); ), done, fail);
} }
isFirst = false; isFirst = false;
@ -4191,7 +4244,9 @@ void ApiWrap::sendMediaWithRandomId(
| (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0)) | (!sentEntities.v.isEmpty() ? Flag::f_entities : Flag(0))
| (options.scheduled ? Flag::f_schedule_date : Flag(0)) | (options.scheduled ? Flag::f_schedule_date : Flag(0))
| (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)); | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0));
auto &histories = history->owner().histories(); auto &histories = history->owner().histories();
const auto peer = history->peer; const auto peer = history->peer;
@ -4211,7 +4266,8 @@ void ApiWrap::sendMediaWithRandomId(
sentEntities, sentEntities,
MTP_int(options.scheduled), MTP_int(options.scheduled),
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId) Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) { ), [=](const MTPUpdates &result, const MTP::Response &response) {
if (done) done(true); if (done) done(true);
if (updateRecentStickers) { if (updateRecentStickers) {
@ -4309,7 +4365,9 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
| (sendAs ? Flag::f_send_as : Flag(0)) | (sendAs ? Flag::f_send_as : Flag(0))
| (album->options.shortcutId | (album->options.shortcutId
? Flag::f_quick_reply_shortcut ? Flag::f_quick_reply_shortcut
: Flag(0)); : Flag(0))
| (album->options.effectId ? Flag::f_effect : Flag(0))
| (album->options.invertCaption ? Flag::f_invert_media : Flag(0));
auto &histories = history->owner().histories(); auto &histories = history->owner().histories();
const auto peer = history->peer; const auto peer = history->peer;
histories.sendPreparedMessage( histories.sendPreparedMessage(
@ -4323,7 +4381,8 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
MTP_vector<MTPInputSingleMedia>(medias), MTP_vector<MTPInputSingleMedia>(medias),
MTP_int(album->options.scheduled), MTP_int(album->options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()), (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, album->options.shortcutId) Data::ShortcutIdToMTP(_session, album->options.shortcutId),
MTP_long(album->options.effectId)
), [=](const MTPUpdates &result, const MTP::Response &response) { ), [=](const MTPUpdates &result, const MTP::Response &response) {
_sendingAlbums.remove(groupId); _sendingAlbums.remove(groupId);

View file

@ -274,6 +274,10 @@ public:
Fn<void(not_null<PeerData*>, MsgId)> callback); Fn<void(not_null<PeerData*>, MsgId)> callback);
using SliceType = Data::LoadDirection; using SliceType = Data::LoadDirection;
void requestHistory(
not_null<History*> history,
MsgId messageId,
SliceType slice);
void requestSharedMedia( void requestSharedMedia(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId topicRootId, MsgId topicRootId,
@ -511,7 +515,8 @@ private:
not_null<PeerData*> peer, not_null<PeerData*> peer,
bool justClear, bool justClear,
bool revoke); bool revoke);
void applyAffectedMessages(const MTPmessages_AffectedMessages &result) const; void applyAffectedMessages(
const MTPmessages_AffectedMessages &result) const;
void deleteAllFromParticipantSend( void deleteAllFromParticipantSend(
not_null<ChannelData*> channel, not_null<ChannelData*> channel,
@ -645,6 +650,17 @@ private:
}; };
base::flat_set<SharedMediaRequest> _sharedMediaRequests; base::flat_set<SharedMediaRequest> _sharedMediaRequests;
struct HistoryRequest {
not_null<PeerData*> peer;
MsgId aroundId = 0;
SliceType sliceType = {};
friend inline auto operator<=>(
const HistoryRequest&,
const HistoryRequest&) = default;
};
base::flat_set<HistoryRequest> _historyRequests;
std::unique_ptr<DialogsLoadState> _dialogsLoadState; std::unique_ptr<DialogsLoadState> _dialogsLoadState;
TimeId _dialogsLoadTill = 0; TimeId _dialogsLoadTill = 0;
rpl::variable<bool> _dialogsLoadMayBlockByDate = false; rpl::variable<bool> _dialogsLoadMayBlockByDate = false;

View file

@ -192,20 +192,27 @@ void ShowAddParticipantsError(
&& channel && channel
&& !channel->isMegagroup() && !channel->isMegagroup()
&& channel->canAddAdmins()) { && channel->canAddAdmins()) {
const auto makeAdmin = [=] { const auto makeAdmin = [=](Fn<void()> close) {
const auto user = forbidden.users.front(); const auto user = forbidden.users.front();
const auto weak = std::make_shared<QPointer<EditAdminBox>>(); const auto weak = std::make_shared<QPointer<EditAdminBox>>();
const auto close = [=](auto&&...) { const auto done = [=](auto&&...) {
if (*weak) { if (const auto strong = weak->data()) {
(*weak)->closeBox(); strong->uiShow()->showToast(
tr::lng_box_done(tr::now));
strong->closeBox();
}
};
const auto fail = [=] {
if (const auto strong = weak->data()) {
strong->closeBox();
} }
}; };
const auto saveCallback = SaveAdminCallback( const auto saveCallback = SaveAdminCallback(
show, show,
channel, channel,
user, user,
close, done,
close); fail);
auto box = Box<EditAdminBox>( auto box = Box<EditAdminBox>(
channel, channel,
user, user,
@ -214,6 +221,7 @@ void ShowAddParticipantsError(
box->setSaveCallback(saveCallback); box->setSaveCallback(saveCallback);
*weak = box.data(); *weak = box.data();
show->showBox(std::move(box)); show->showBox(std::move(box));
close();
}; };
show->showBox( show->showBox(
Ui::MakeConfirmBox({ Ui::MakeConfirmBox({

View file

@ -642,6 +642,10 @@ proxyDropdownUpPosition: point(-2px, 20px);
proxyAboutPadding: margins(22px, 7px, 22px, 14px); proxyAboutPadding: margins(22px, 7px, 22px, 14px);
proxyAboutSponsorPadding: margins(22px, 7px, 22px, 0px); proxyAboutSponsorPadding: margins(22px, 7px, 22px, 0px);
proxyApplyBoxLabel : FlatLabel(defaultFlatLabel) {
maxHeight: 30px;
}
markdownLinkFieldPadding: margins(22px, 0px, 22px, 10px); markdownLinkFieldPadding: margins(22px, 0px, 22px, 10px);
termsContent: FlatLabel(defaultFlatLabel) { termsContent: FlatLabel(defaultFlatLabel) {

View file

@ -7,32 +7,37 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "boxes/connection_box.h" #include "boxes/connection_box.h"
#include "ui/boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "base/qthelp_url.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "core/local_url_handlers.h"
#include "lang/lang_keys.h"
#include "main/main_account.h" #include "main/main_account.h"
#include "mtproto/facade.h" #include "mtproto/facade.h"
#include "ui/widgets/checkbox.h" #include "storage/localstorage.h"
#include "ui/basic_click_handlers.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h" #include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/password_input.h" #include "ui/widgets/fields/password_input.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/dropdown_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/toast/toast.h" #include "ui/vertical_list.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "boxes/abstract_box.h" // Ui::show(). #include "boxes/abstract_box.h" // Ui::show().
#include "window/window_session_controller.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h" #include "styles/style_chat_helpers.h"
@ -48,6 +53,22 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
using ProxyData = MTP::ProxyData; using ProxyData = MTP::ProxyData;
[[nodiscard]] ProxyData ProxyDataFromFields(
ProxyData::Type type,
const QMap<QString, QString> &fields) {
auto proxy = ProxyData();
proxy.type = type;
proxy.host = fields.value(u"server"_q);
proxy.port = fields.value(u"port"_q).toUInt();
if (type == ProxyData::Type::Socks5) {
proxy.user = fields.value(u"user"_q);
proxy.password = fields.value(u"pass"_q);
} else if (type == ProxyData::Type::Mtproto) {
proxy.password = fields.value(u"secret"_q);
}
return proxy;
};
class HostInput : public Ui::MaskedInputField { class HostInput : public Ui::MaskedInputField {
public: public:
HostInput( HostInput(
@ -203,6 +224,7 @@ protected:
private: private:
void setupContent(); void setupContent();
void setupTopButton();
void createNoRowsLabel(); void createNoRowsLabel();
void addNewProxy(); void addNewProxy();
void applyView(View &&view); void applyView(View &&view);
@ -600,9 +622,80 @@ void ProxiesBox::prepare() {
addButton(tr::lng_proxy_add(), [=] { addNewProxy(); }); addButton(tr::lng_proxy_add(), [=] { addNewProxy(); });
addButton(tr::lng_close(), [=] { closeBox(); }); addButton(tr::lng_close(), [=] { closeBox(); });
setupTopButton();
setupContent(); setupContent();
} }
void ProxiesBox::setupTopButton() {
const auto top = addTopButton(st::infoTopBarMenu);
const auto menu
= top->lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();
const auto callback = [=] {
const auto maybeUrl = QGuiApplication::clipboard()->text();
const auto local = Core::TryConvertUrlToLocal(maybeUrl);
const auto proxyString = u"proxy"_q;
const auto socksString = u"socks"_q;
const auto protocol = u"tg://"_q;
const auto command = base::StringViewMid(
local,
protocol.size(),
8192);
if (local.startsWith(protocol + proxyString)
|| local.startsWith(protocol + socksString)) {
using namespace qthelp;
const auto options = RegExOption::CaseInsensitive;
for (const auto &[expression, _] : Core::LocalUrlHandlers()) {
const auto midExpression = base::StringViewMid(
expression,
1);
const auto isSocks = midExpression.startsWith(
socksString);
if (!midExpression.startsWith(proxyString)
&& !isSocks) {
continue;
}
const auto match = regex_match(
expression,
command,
options);
if (!match) {
continue;
}
const auto type = isSocks
? ProxyData::Type::Socks5
: ProxyData::Type::Mtproto;
const auto fields = url_parse_params(
match->captured(1),
qthelp::UrlParamNameTransform::ToLower);
const auto proxy = ProxyDataFromFields(type, fields);
const auto contains = _controller->contains(proxy);
const auto toast = (contains
? tr::lng_proxy_add_from_clipboard_existing_toast
: tr::lng_proxy_add_from_clipboard_good_toast)(tr::now);
uiShow()->showToast(toast);
if (!contains) {
_controller->addNewItem(proxy);
}
break;
}
} else {
uiShow()->showToast(
tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
}
};
top->setClickedCallback([=] {
*menu = base::make_unique_q<Ui::PopupMenu>(top, st::defaultPopupMenu);
(*menu)->addAction(
tr::lng_proxy_add_from_clipboard(tr::now),
callback);
(*menu)->popup(QCursor::pos());
return true;
});
}
void ProxiesBox::setupContent() { void ProxiesBox::setupContent() {
const auto inner = setInnerWidget(object_ptr<Ui::VerticalLayout>(this)); const auto inner = setInnerWidget(object_ptr<Ui::VerticalLayout>(this));
@ -1094,70 +1187,84 @@ ProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)
} }
void ProxiesBoxController::ShowApplyConfirmation( void ProxiesBoxController::ShowApplyConfirmation(
Window::SessionController *controller,
Type type, Type type,
const QMap<QString, QString> &fields) { const QMap<QString, QString> &fields) {
const auto server = fields.value(u"server"_q); const auto proxy = ProxyDataFromFields(type, fields);
const auto port = fields.value(u"port"_q).toUInt(); if (!proxy) {
auto proxy = ProxyData(); auto box = Ui::MakeInformBox(
proxy.type = type; (proxy.status() == ProxyData::Status::Unsupported
proxy.host = server; ? tr::lng_proxy_unsupported(tr::now)
proxy.port = port; : tr::lng_proxy_invalid(tr::now)));
if (type == Type::Socks5) { if (controller) {
proxy.user = fields.value(u"user"_q); controller->uiShow()->showBox(std::move(box));
proxy.password = fields.value(u"pass"_q); } else {
} else if (type == Type::Mtproto) { Ui::show(std::move(box));
proxy.password = fields.value(u"secret"_q); }
return;
} }
if (proxy) { static const auto UrlStartRegExp = QRegularExpression(
static const auto UrlStartRegExp = QRegularExpression( "^https://",
"^https://", QRegularExpression::CaseInsensitiveOption);
QRegularExpression::CaseInsensitiveOption); static const auto UrlEndRegExp = QRegularExpression("/$");
static const auto UrlEndRegExp = QRegularExpression("/$"); const auto displayed = "https://" + proxy.host + "/";
const auto displayed = "https://" + server + "/"; const auto parsed = QUrl::fromUserInput(displayed);
const auto parsed = QUrl::fromUserInput(displayed); const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)
const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed) ? displayed
? displayed : parsed.isValid()
: parsed.isValid() ? QString::fromUtf8(parsed.toEncoded())
? QString::fromUtf8(parsed.toEncoded()) : UrlClickHandler::ShowEncoded(displayed);
: UrlClickHandler::ShowEncoded(displayed); const auto displayServer = QString(
const auto displayServer = QString( displayUrl
displayUrl ).replace(
).replace( UrlStartRegExp,
UrlStartRegExp, QString()
QString() ).replace(UrlEndRegExp, QString());
).replace(UrlEndRegExp, QString()); const auto box = [=](not_null<Ui::GenericBox*> box) {
const auto text = tr::lng_sure_enable_socks( box->setTitle(tr::lng_proxy_box_title());
tr::now, if (type == Type::Mtproto) {
lt_server, box->addRow(object_ptr<Ui::FlatLabel>(
displayServer, box,
lt_port, tr::lng_proxy_sponsor_warning(),
QString::number(port)) st::boxDividerLabel));
+ (proxy.type == Type::Mtproto Ui::AddSkip(box->verticalLayout());
? "\n\n" + tr::lng_proxy_sponsor_warning(tr::now) Ui::AddSkip(box->verticalLayout());
: QString()); }
auto callback = [=](Fn<void()> &&close) { const auto &stL = st::proxyApplyBoxLabel;
const auto &stSubL = st::boxDividerLabel;
const auto add = [&](const QString &s, tr::phrase<> phrase) {
if (!s.isEmpty()) {
box->addRow(object_ptr<Ui::FlatLabel>(box, s, stL));
box->addRow(object_ptr<Ui::FlatLabel>(box, phrase(), stSubL));
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
}
};
if (!displayServer.isEmpty()) {
add(displayServer, tr::lng_proxy_box_server);
}
add(QString::number(proxy.port), tr::lng_proxy_box_port);
if (type == Type::Socks5) {
add(proxy.user, tr::lng_proxy_box_username);
add(proxy.password, tr::lng_proxy_box_password);
} else if (type == Type::Mtproto) {
add(proxy.password, tr::lng_proxy_box_secret);
}
box->addButton(tr::lng_sure_enable(), [=] {
auto &proxies = Core::App().settings().proxy().list(); auto &proxies = Core::App().settings().proxy().list();
if (!ranges::contains(proxies, proxy)) { if (!ranges::contains(proxies, proxy)) {
proxies.push_back(proxy); proxies.push_back(proxy);
} }
Core::App().setCurrentProxy( Core::App().setCurrentProxy(proxy, ProxyData::Settings::Enabled);
proxy,
ProxyData::Settings::Enabled);
Local::writeSettings(); Local::writeSettings();
close(); box->closeBox();
}; });
Ui::show( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
Ui::MakeConfirmBox({ };
.text = text, if (controller) {
.confirmed = std::move(callback), controller->uiShow()->showBox(Box(box));
.confirmText = tr::lng_sure_enable(),
}),
Ui::LayerOption::KeepOther);
} else { } else {
Ui::show(Ui::MakeInformBox( Ui::show(Box(box));
(proxy.status() == ProxyData::Status::Unsupported
? tr::lng_proxy_unsupported(tr::now)
: tr::lng_proxy_invalid(tr::now))));
} }
} }
@ -1448,6 +1555,14 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::addNewItemBox() {
}); });
} }
bool ProxiesBoxController::contains(const ProxyData &proxy) const {
const auto j = ranges::find(
_list,
proxy,
[](const Item &item) { return item.data; });
return (j != end(_list));
}
void ProxiesBoxController::addNewItem(const ProxyData &proxy) { void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
auto &proxies = _settings.list(); auto &proxies = _settings.list();
proxies.push_back(proxy); proxies.push_back(proxy);

View file

@ -30,6 +30,10 @@ namespace Main {
class Account; class Account;
} // namespace Main } // namespace Main
namespace Window {
class SessionController;
} // namespace Window
class ProxiesBoxController { class ProxiesBoxController {
public: public:
using ProxyData = MTP::ProxyData; using ProxyData = MTP::ProxyData;
@ -38,6 +42,7 @@ public:
explicit ProxiesBoxController(not_null<Main::Account*> account); explicit ProxiesBoxController(not_null<Main::Account*> account);
static void ShowApplyConfirmation( static void ShowApplyConfirmation(
Window::SessionController *controller,
Type type, Type type,
const QMap<QString, QString> &fields); const QMap<QString, QString> &fields);
@ -77,6 +82,9 @@ public:
void setTryIPv6(bool enabled); void setTryIPv6(bool enabled);
rpl::producer<ProxyData::Settings> proxySettingsValue() const; rpl::producer<ProxyData::Settings> proxySettingsValue() const;
[[nodiscard]] bool contains(const ProxyData &proxy) const;
void addNewItem(const ProxyData &proxy);
rpl::producer<ItemView> views() const; rpl::producer<ItemView> views() const;
~ProxiesBoxController(); ~ProxiesBoxController();
@ -109,7 +117,6 @@ private:
void replaceItemValue( void replaceItemValue(
std::vector<Item>::iterator which, std::vector<Item>::iterator which,
const ProxyData &proxy); const ProxyData &proxy);
void addNewItem(const ProxyData &proxy);
const not_null<Main::Account*> _account; const not_null<Main::Account*> _account;
Core::SettingsProxy &_settings; Core::SettingsProxy &_settings;

View file

@ -910,12 +910,12 @@ CreatePollBox::CreatePollBox(
PollData::Flags chosen, PollData::Flags chosen,
PollData::Flags disabled, PollData::Flags disabled,
Api::SendType sendType, Api::SendType sendType,
SendMenu::Type sendMenuType) SendMenu::Details sendMenuDetails)
: _controller(controller) : _controller(controller)
, _chosen(chosen) , _chosen(chosen)
, _disabled(disabled) , _disabled(disabled)
, _sendType(sendType) , _sendType(sendType)
, _sendMenuType(sendMenuType) { , _sendMenuDetails([result = sendMenuDetails] { return result; }) {
} }
rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const { rpl::producer<CreatePollBox::Result> CreatePollBox::submitRequests() const {
@ -1044,7 +1044,7 @@ not_null<Ui::InputField*> CreatePollBox::setupSolution(
solution->setInstantReplaces(Ui::InstantReplaces::Default()); solution->setInstantReplaces(Ui::InstantReplaces::Default());
solution->setInstantReplacesEnabled( solution->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue()); Core::App().settings().replaceEmojiValue());
solution->setMarkdownReplacesEnabled(rpl::single(true)); solution->setMarkdownReplacesEnabled(true);
solution->setEditLinkCallback( solution->setEditLinkCallback(
DefaultEditLinkCallback(_controller->uiShow(), solution)); DefaultEditLinkCallback(_controller->uiShow(), solution));
solution->customTab(true); solution->customTab(true);
@ -1288,19 +1288,9 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
_submitRequests.fire({ collectResult(), sendOptions }); _submitRequests.fire({ collectResult(), sendOptions });
} }
}; };
const auto sendSilent = [=] { const auto sendAction = SendMenu::DefaultCallback(
send({ .silent = true }); _controller->uiShow(),
}; crl::guard(this, send));
const auto sendScheduled = [=] {
_controller->show(
HistoryView::PrepareScheduleBox(
this,
SendMenu::Type::Scheduled,
send));
};
const auto sendWhenOnline = [=] {
send(Api::DefaultSendWhenOnlineOptions());
};
options->scrollToWidget( options->scrollToWidget(
) | rpl::start_with_next([=](not_null<QWidget*> widget) { ) | rpl::start_with_next([=](not_null<QWidget*> widget) {
@ -1313,24 +1303,25 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
}, lifetime()); }, lifetime());
const auto isNormal = (_sendType == Api::SendType::Normal); const auto isNormal = (_sendType == Api::SendType::Normal);
const auto schedule = [=] {
sendAction(
{ .type = SendMenu::ActionType::Schedule },
_sendMenuDetails());
};
const auto submit = addButton( const auto submit = addButton(
isNormal (isNormal
? tr::lng_polls_create_button() ? tr::lng_polls_create_button()
: tr::lng_schedule_button(), : tr::lng_schedule_button()),
[=] { isNormal ? send({}) : sendScheduled(); }); [=] { isNormal ? send({}) : schedule(); });
const auto sendMenuType = [=] { const auto sendMenuDetails = [=] {
collectError(); collectError();
return (*error) return (*error) ? SendMenu::Details() : _sendMenuDetails();
? SendMenu::Type::Disabled
: _sendMenuType;
}; };
SendMenu::SetupMenuAndShortcuts( SendMenu::SetupMenuAndShortcuts(
submit.data(), submit.data(),
sendMenuType, _controller->uiShow(),
sendSilent, sendMenuDetails,
sendScheduled, sendAction);
sendWhenOnline);
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
return result; return result;

View file

@ -27,7 +27,7 @@ class SessionController;
} // namespace Window } // namespace Window
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
class CreatePollBox : public Ui::BoxContent { class CreatePollBox : public Ui::BoxContent {
@ -43,7 +43,7 @@ public:
PollData::Flags chosen, PollData::Flags chosen,
PollData::Flags disabled, PollData::Flags disabled,
Api::SendType sendType, Api::SendType sendType,
SendMenu::Type sendMenuType); SendMenu::Details sendMenuDetails);
[[nodiscard]] rpl::producer<Result> submitRequests() const; [[nodiscard]] rpl::producer<Result> submitRequests() const;
void submitFailed(const QString &error); void submitFailed(const QString &error);
@ -75,7 +75,7 @@ private:
const PollData::Flags _chosen = PollData::Flags(); const PollData::Flags _chosen = PollData::Flags();
const PollData::Flags _disabled = PollData::Flags(); const PollData::Flags _disabled = PollData::Flags();
const Api::SendType _sendType = Api::SendType(); const Api::SendType _sendType = Api::SendType();
const SendMenu::Type _sendMenuType; const Fn<SendMenu::Details()> _sendMenuDetails;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
Fn<void()> _setInnerFocus; Fn<void()> _setInnerFocus;
Fn<rpl::producer<bool>()> _dataIsValidValue; Fn<rpl::producer<bool>()> _dataIsValidValue;

View file

@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "mainwidget.h" // controller->content() -> QWidget* #include "mainwidget.h" // controller->content() -> QWidget*
#include "menu/menu_send.h"
#include "mtproto/mtproto_config.h" #include "mtproto/mtproto_config.h"
#include "platform/platform_specific.h" #include "platform/platform_specific.h"
#include "storage/localimageloader.h" // SendMediaType #include "storage/localimageloader.h" // SendMediaType
@ -175,7 +176,7 @@ void ChooseReplacement(
void EditPhotoImage( void EditPhotoImage(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
std::shared_ptr<Data::PhotoMedia> media, std::shared_ptr<Data::PhotoMedia> media,
bool wasSpoiler, bool spoilered,
Fn<void(Ui::PreparedList)> done) { Fn<void(Ui::PreparedList)> done) {
const auto large = media const auto large = media
? media->image(Data::PhotoSize::Large) ? media->image(Data::PhotoSize::Large)
@ -198,7 +199,7 @@ void EditPhotoImage(
using ImageInfo = Ui::PreparedFileInformation::Image; using ImageInfo = Ui::PreparedFileInformation::Image;
auto &file = list.files.front(); auto &file = list.files.front();
file.spoiler = wasSpoiler; file.spoiler = spoilered;
const auto image = std::get_if<ImageInfo>(&file.information->media); const auto image = std::get_if<ImageInfo>(&file.information->media);
image->modifications = mods; image->modifications = mods;
@ -225,25 +226,18 @@ void EditPhotoImage(
} // namespace } // namespace
EditCaptionBox::EditCaptionBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item)
: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) {
}
EditCaptionBox::EditCaptionBox( EditCaptionBox::EditCaptionBox(
QWidget*, QWidget*,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithTags &&text, TextWithTags &&text,
bool spoilered,
bool invertCaption,
Ui::PreparedList &&list, Ui::PreparedList &&list,
Fn<void()> saved) Fn<void()> saved)
: _controller(controller) : _controller(controller)
, _historyItem(item) , _historyItem(item)
, _isAllowedEditMedia(item->media() , _isAllowedEditMedia(item->media() && item->media()->allowsEditMedia())
? item->media()->allowsEditMedia()
: false)
, _albumType(ComputeAlbumType(item)) , _albumType(ComputeAlbumType(item))
, _controls(base::make_unique_q<Ui::VerticalLayout>(this)) , _controls(base::make_unique_q<Ui::VerticalLayout>(this))
, _scroll(base::make_unique_q<Ui::ScrollArea>(this, st::boxScroll)) , _scroll(base::make_unique_q<Ui::ScrollArea>(this, st::boxScroll))
@ -261,6 +255,8 @@ EditCaptionBox::EditCaptionBox(
Expects(item->media() != nullptr); Expects(item->media() != nullptr);
Expects(item->media()->allowsEditCaption()); Expects(item->media()->allowsEditCaption());
_mediaEditManager.start(item, spoilered, invertCaption);
_controller->session().data().itemRemoved( _controller->session().data().itemRemoved(
_historyItem->fullId() _historyItem->fullId()
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
@ -274,6 +270,8 @@ void EditCaptionBox::StartMediaReplace(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
FullMsgId itemId, FullMsgId itemId,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved) { Fn<void()> saved) {
const auto session = &controller->session(); const auto session = &controller->session();
const auto item = session->data().message(itemId); const auto item = session->data().message(itemId);
@ -285,6 +283,8 @@ void EditCaptionBox::StartMediaReplace(
controller, controller,
item, item,
std::move(text), std::move(text),
spoilered,
invertCaption,
std::move(list), std::move(list),
std::move(saved))); std::move(saved)));
}; };
@ -299,6 +299,8 @@ void EditCaptionBox::StartMediaReplace(
FullMsgId itemId, FullMsgId itemId,
Ui::PreparedList &&list, Ui::PreparedList &&list,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved) { Fn<void()> saved) {
const auto session = &controller->session(); const auto session = &controller->session();
const auto item = session->data().message(itemId); const auto item = session->data().message(itemId);
@ -332,6 +334,8 @@ void EditCaptionBox::StartMediaReplace(
controller, controller,
item, item,
std::move(text), std::move(text),
spoilered,
invertCaption,
std::move(list), std::move(list),
std::move(saved))); std::move(saved)));
} }
@ -342,14 +346,15 @@ void EditCaptionBox::StartPhotoEdit(
std::shared_ptr<Data::PhotoMedia> media, std::shared_ptr<Data::PhotoMedia> media,
FullMsgId itemId, FullMsgId itemId,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved) { Fn<void()> saved) {
const auto session = &controller->session(); const auto session = &controller->session();
const auto item = session->data().message(itemId); const auto item = session->data().message(itemId);
if (!item) { if (!item) {
return; return;
} }
const auto hasSpoiler = item->media() && item->media()->hasSpoiler(); EditPhotoImage(controller, media, spoilered, [=](
EditPhotoImage(controller, media, hasSpoiler, [=](
Ui::PreparedList &&list) mutable { Ui::PreparedList &&list) mutable {
const auto item = session->data().message(itemId); const auto item = session->data().message(itemId);
if (!item) { if (!item) {
@ -359,15 +364,48 @@ void EditCaptionBox::StartPhotoEdit(
controller, controller,
item, item,
std::move(text), std::move(text),
spoilered,
invertCaption,
std::move(list), std::move(list),
std::move(saved))); std::move(saved)));
}); });
} }
void EditCaptionBox::prepare() { void EditCaptionBox::prepare() {
addButton(tr::lng_settings_save(), [=] { save(); }); const auto button = addButton(tr::lng_settings_save(), [=] { save(); });
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
const auto details = crl::guard(this, [=] {
auto result = SendMenu::Details();
const auto allWithSpoilers = ranges::all_of(
_preparedList.files,
&Ui::PreparedFile::spoiler);
result.spoiler = !_preparedList.hasSpoilerMenu(!_asFile)
? SendMenu::SpoilerState::None
: allWithSpoilers
? SendMenu::SpoilerState::Enabled
: SendMenu::SpoilerState::Possible;
const auto canMoveCaption = _preparedList.canMoveCaption(
false,
!_asFile
) && _field && HasSendText(_field);
result.caption = !canMoveCaption
? SendMenu::CaptionState::None
: _mediaEditManager.invertCaption()
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
return result;
});
const auto callback = [=](SendMenu::Action action, const auto &) {
_mediaEditManager.apply(action);
rebuildPreview();
};
SendMenu::SetupMenuAndShortcuts(
button,
nullptr,
details,
crl::guard(this, callback));
updateBoxSize(); updateBoxSize();
setupField(); setupField();
@ -396,7 +434,6 @@ void EditCaptionBox::rebuildPreview() {
applyChanges(); applyChanges();
_previewHasSpoiler = nullptr;
if (_preparedList.files.empty()) { if (_preparedList.files.empty()) {
const auto media = _historyItem->media(); const auto media = _historyItem->media();
const auto photo = media->photo(); const auto photo = media->photo();
@ -430,7 +467,13 @@ void EditCaptionBox::rebuildPreview() {
_isPhoto = (media && media->isPhoto()); _isPhoto = (media && media->isPhoto());
const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType); const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);
if (media && (!withCheckbox || !_asFile)) { if (media && (!withCheckbox || !_asFile)) {
_previewHasSpoiler = [media] { return media->hasSpoiler(); }; media->spoileredChanges(
) | rpl::start_with_next([=](bool spoilered) {
_mediaEditManager.apply({ .type = spoilered
? SendMenu::ActionType::SpoilerOn
: SendMenu::ActionType::SpoilerOff
});
}, media->lifetime());
_content.reset(media); _content.reset(media);
} else { } else {
_content.reset(Ui::CreateChild<Ui::SingleFilePreview>( _content.reset(Ui::CreateChild<Ui::SingleFilePreview>(
@ -757,10 +800,7 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) {
} }
bool EditCaptionBox::hasSpoiler() const { bool EditCaptionBox::hasSpoiler() const {
return _preparedList.files.empty() return _mediaEditManager.spoilered();
? (_historyItem->media()
&& _historyItem->media()->hasSpoiler())
: _preparedList.files.front().spoiler;
} }
void EditCaptionBox::captionResized() { void EditCaptionBox::captionResized() {
@ -869,8 +909,8 @@ bool EditCaptionBox::validateLength(const QString &text) const {
} }
void EditCaptionBox::applyChanges() { void EditCaptionBox::applyChanges() {
if (!_preparedList.files.empty() && _previewHasSpoiler) { if (!_preparedList.files.empty()) {
_preparedList.files.front().spoiler = _previewHasSpoiler(); _preparedList.files.front().spoiler = _mediaEditManager.spoilered();
} }
} }
@ -899,6 +939,7 @@ void EditCaptionBox::save() {
auto options = Api::SendOptions(); auto options = Api::SendOptions();
options.scheduled = item->isScheduled() ? item->date() : 0; options.scheduled = item->isScheduled() ? item->date() : 0;
options.shortcutId = item->shortcutId(); options.shortcutId = item->shortcutId();
options.invertCaption = _mediaEditManager.invertCaption();
if (!_preparedList.files.empty()) { if (!_preparedList.files.empty()) {
if ((_albumType != Ui::AlbumType::None) if ((_albumType != Ui::AlbumType::None)

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "history/view/controls/history_view_compose_media_edit_manager.h"
#include "ui/layers/box_content.h" #include "ui/layers/box_content.h"
#include "ui/chat/attach/attach_prepare.h" #include "ui/chat/attach/attach_prepare.h"
@ -32,15 +33,13 @@ enum class AlbumType;
class EditCaptionBox final : public Ui::BoxContent { class EditCaptionBox final : public Ui::BoxContent {
public: public:
EditCaptionBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
EditCaptionBox( EditCaptionBox(
QWidget*, QWidget*,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item, not_null<HistoryItem*> item,
TextWithTags &&text, TextWithTags &&text,
bool spoilered,
bool invertCaption,
Ui::PreparedList &&list, Ui::PreparedList &&list,
Fn<void()> saved); Fn<void()> saved);
~EditCaptionBox(); ~EditCaptionBox();
@ -49,18 +48,24 @@ public:
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
FullMsgId itemId, FullMsgId itemId,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved); Fn<void()> saved);
static void StartMediaReplace( static void StartMediaReplace(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
FullMsgId itemId, FullMsgId itemId,
Ui::PreparedList &&list, Ui::PreparedList &&list,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved); Fn<void()> saved);
static void StartPhotoEdit( static void StartPhotoEdit(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
std::shared_ptr<Data::PhotoMedia> media, std::shared_ptr<Data::PhotoMedia> media,
FullMsgId itemId, FullMsgId itemId,
TextWithTags text, TextWithTags text,
bool spoilered,
bool invertCaption,
Fn<void()> saved); Fn<void()> saved);
protected: protected:
@ -111,7 +116,6 @@ private:
const base::unique_qptr<Ui::EmojiButton> _emojiToggle; const base::unique_qptr<Ui::EmojiButton> _emojiToggle;
base::unique_qptr<Ui::AbstractSinglePreview> _content; base::unique_qptr<Ui::AbstractSinglePreview> _content;
Fn<bool()> _previewHasSpoiler;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<QObject> _emojiFilter; base::unique_qptr<QObject> _emojiFilter;
@ -122,6 +126,7 @@ private:
std::shared_ptr<Data::PhotoMedia> _photoMedia; std::shared_ptr<Data::PhotoMedia> _photoMedia;
Ui::PreparedList _preparedList; Ui::PreparedList _preparedList;
HistoryView::MediaEditManager _mediaEditManager;
mtpRequestId _saveRequestId = 0; mtpRequestId _saveRequestId = 0;

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_boosts.h" #include "data/data_boosts.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_media_types.h" // Data::GiveawayStart. #include "data/data_media_types.h" // Data::GiveawayStart.
#include "data/data_peer_values.h" // Data::PeerPremiumValue. #include "data/data_peer_values.h" // Data::PeerPremiumValue.
#include "data/data_session.h" #include "data/data_session.h"
@ -1147,16 +1148,18 @@ void GiftCodeBox(
object_ptr<Ui::Premium::TopBar>( object_ptr<Ui::Premium::TopBar>(
box, box,
st::giveawayGiftCodeCover, st::giveawayGiftCodeCover,
nullptr, Ui::Premium::TopBarDescriptor{
rpl::conditional( .clickContextOther = nullptr,
state->used.value(), .title = rpl::conditional(
tr::lng_gift_link_used_title(), state->used.value(),
tr::lng_gift_link_title()), tr::lng_gift_link_used_title(),
rpl::conditional( tr::lng_gift_link_title()),
state->used.value(), .about = rpl::conditional(
tr::lng_gift_link_used_about(Ui::Text::RichLangValue), state->used.value(),
tr::lng_gift_link_about(Ui::Text::RichLangValue)), tr::lng_gift_link_used_about(Ui::Text::RichLangValue),
true)); tr::lng_gift_link_about(Ui::Text::RichLangValue)),
.light = true,
}));
const auto max = st::giveawayGiftCodeTopHeight; const auto max = st::giveawayGiftCodeTopHeight;
bar->setMaximumHeight(max); bar->setMaximumHeight(max);
@ -1283,13 +1286,15 @@ void GiftCodePendingBox(
object_ptr<Ui::Premium::TopBar>( object_ptr<Ui::Premium::TopBar>(
box, box,
st, st,
clickContext, Ui::Premium::TopBarDescriptor{
tr::lng_gift_link_title(), .clickContextOther = clickContext,
tr::lng_gift_link_pending_about( .title = tr::lng_gift_link_title(),
lt_user, .about = tr::lng_gift_link_pending_about(
rpl::single(Ui::Text::Link(resultToName)), lt_user,
Ui::Text::RichLangValue), rpl::single(Ui::Text::Link(resultToName)),
true)); Ui::Text::RichLangValue),
.light = true,
}));
const auto max = st::giveawayGiftCodeTopHeight; const auto max = st::giveawayGiftCodeTopHeight;
bar->setMaximumHeight(max); bar->setMaximumHeight(max);
@ -1629,3 +1634,50 @@ void ResolveGiveawayInfo(
messageId, messageId,
crl::guard(controller, show)); crl::guard(controller, show));
} }
void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry) {
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
if (entry.bareId) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer(),
controller,
PeerId(entry.bareId));
}
if (!entry.id.isEmpty()) {
constexpr auto kOneLineCount = 18;
const auto oneLine = entry.id.length() <= kOneLineCount;
auto label = object_ptr<Ui::FlatLabel>(
table,
rpl::single(
Ui::Text::Wrapped({ entry.id }, EntityType::Code, {})),
oneLine
? st::giveawayGiftCodeValue
: st::giveawayGiftCodeValueMultiline);
label->setClickHandlerFilter([=](const auto &...) {
TextUtilities::SetClipboardText(
TextForMimeData::Simple(entry.id));
controller->showToast(
tr::lng_credits_box_history_entry_id_copied(tr::now));
return false;
});
AddTableRow(
table,
tr::lng_credits_box_history_entry_id(),
std::move(label),
st::giveawayGiftCodeValueMargin);
}
if (!entry.date.isNull()) {
AddTableRow(
table,
tr::lng_gift_link_label_date(),
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
}
}

View file

@ -16,12 +16,14 @@ struct GiftCode;
} // namespace Api } // namespace Api
namespace Data { namespace Data {
struct CreditsHistoryEntry;
struct GiveawayStart; struct GiveawayStart;
struct GiveawayResults; struct GiveawayResults;
} // namespace Data } // namespace Data
namespace Ui { namespace Ui {
class GenericBox; class GenericBox;
class VerticalLayout;
} // namespace Ui } // namespace Ui
namespace Window { namespace Window {
@ -71,3 +73,8 @@ void ResolveGiveawayInfo(
MsgId messageId, MsgId messageId,
std::optional<Data::GiveawayStart> start, std::optional<Data::GiveawayStart> start,
std::optional<Data::GiveawayResults> results); std::optional<Data::GiveawayResults> results);
void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry);

View file

@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_widgets.h" #include "styles/style_widgets.h"
#include <xxhash.h> // XXH64. #include <xxhash.h> // XXH64.
#include <QtWidgets/QApplication>
[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) { [[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) {
return XXH64(d.data(), d.size() * sizeof(ushort), 0); return XXH64(d.data(), d.size() * sizeof(ushort), 0);
@ -1552,15 +1553,44 @@ void PeerListContent::handleMouseMove(QPoint globalPosition) {
&& *_lastMousePosition == globalPosition) { && *_lastMousePosition == globalPosition) {
return; return;
} }
if (_trackPressStart
&& ((*_trackPressStart - globalPosition).manhattanLength()
> QApplication::startDragDistance())) {
_trackPressStart = {};
_controller->rowTrackPressCancel();
}
if (!_controller->rowTrackPressSkipMouseSelection()) {
selectByMouse(globalPosition);
}
}
void PeerListContent::pressLeftToContextMenu(bool shown) {
if (shown) {
setContexted(_pressed);
setPressed(Selected());
} else {
setContexted(Selected());
}
}
bool PeerListContent::trackRowPressFromGlobal(QPoint globalPosition) {
selectByMouse(globalPosition); selectByMouse(globalPosition);
if (const auto row = getRow(_selected.index)) {
if (_controller->rowTrackPress(row)) {
_trackPressStart = globalPosition;
return true;
}
}
return false;
} }
void PeerListContent::mousePressEvent(QMouseEvent *e) { void PeerListContent::mousePressEvent(QMouseEvent *e) {
_pressButton = e->button(); _pressButton = e->button();
selectByMouse(e->globalPos()); selectByMouse(e->globalPos());
setPressed(_selected); setPressed(_selected);
if (auto row = getRow(_selected.index)) { _trackPressStart = {};
auto updateCallback = [this, row, hint = _selected.index] { if (const auto row = getRow(_selected.index)) {
const auto updateCallback = [this, row, hint = _selected.index] {
updateRow(row, hint); updateRow(row, hint);
}; };
if (_selected.element) { if (_selected.element) {
@ -1586,8 +1616,11 @@ void PeerListContent::mousePressEvent(QMouseEvent *e) {
row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback)); row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback));
} }
} }
if (_pressButton == Qt::LeftButton && _controller->rowTrackPress(row)) {
_trackPressStart = e->globalPos();
}
} }
if (anim::Disabled() && !_selected.element) { if (anim::Disabled() && !_trackPressStart && !_selected.element) {
mousePressReleased(e->button()); mousePressReleased(e->button());
} }
} }
@ -1597,6 +1630,9 @@ void PeerListContent::mouseReleaseEvent(QMouseEvent *e) {
} }
void PeerListContent::mousePressReleased(Qt::MouseButton button) { void PeerListContent::mousePressReleased(Qt::MouseButton button) {
_trackPressStart = {};
_controller->rowTrackPressCancel();
updateRow(_pressed.index); updateRow(_pressed.index);
updateRow(_selected.index); updateRow(_selected.index);

View file

@ -348,6 +348,9 @@ public:
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0; virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0; virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
virtual void peerListPressLeftToContextMenu(bool shown) = 0;
virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;
template <typename PeerDataRange> template <typename PeerDataRange>
void peerListAddSelectedPeers(PeerDataRange &&range) { void peerListAddSelectedPeers(PeerDataRange &&range) {
for (const auto peer : range) { for (const auto peer : range) {
@ -478,6 +481,15 @@ public:
} }
} }
virtual bool rowTrackPress(not_null<PeerListRow*> row) {
return false;
}
virtual void rowTrackPressCancel() {
}
virtual bool rowTrackPressSkipMouseSelection() {
return false;
}
virtual void loadMoreRows() { virtual void loadMoreRows() {
} }
virtual void itemDeselectedHook(not_null<PeerData*> peer) { virtual void itemDeselectedHook(not_null<PeerData*> peer) {
@ -655,6 +667,8 @@ public:
void refreshRows(); void refreshRows();
void mouseLeftGeometry(); void mouseLeftGeometry();
void pressLeftToContextMenu(bool shown);
bool trackRowPressFromGlobal(QPoint globalPosition);
void setSearchMode(PeerListSearchMode mode); void setSearchMode(PeerListSearchMode mode);
void changeCheckState( void changeCheckState(
@ -829,6 +843,7 @@ private:
bool _mouseSelection = false; bool _mouseSelection = false;
std::optional<QPoint> _lastMousePosition; std::optional<QPoint> _lastMousePosition;
Qt::MouseButton _pressButton = Qt::LeftButton; Qt::MouseButton _pressButton = Qt::LeftButton;
std::optional<QPoint> _trackPressStart;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests; rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
@ -992,6 +1007,13 @@ public:
bool highlightRow, bool highlightRow,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override; Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
void peerListPressLeftToContextMenu(bool shown) override {
_content->pressLeftToContextMenu(shown);
}
bool peerListTrackRowPressFromGlobal(QPoint globalPosition) override {
return _content->trackRowPressFromGlobal(globalPosition);
}
protected: protected:
not_null<PeerListContent*> content() const { not_null<PeerListContent*> content() const {
return _content; return _content;

View file

@ -42,16 +42,14 @@ namespace {
constexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL); constexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL);
struct DefaultIcon { using DefaultIcon = Data::TopicIconDescriptor;
QString title;
int32 colorId = 0;
};
class DefaultIconEmoji final : public Ui::Text::CustomEmoji { class DefaultIconEmoji final : public Ui::Text::CustomEmoji {
public: public:
DefaultIconEmoji( DefaultIconEmoji(
rpl::producer<DefaultIcon> value, rpl::producer<DefaultIcon> value,
Fn<void()> repaint); Fn<void()> repaint,
Data::CustomEmojiSizeTag tag);
int width() override; int width() override;
QString entityData() override; QString entityData() override;
@ -64,14 +62,17 @@ public:
private: private:
DefaultIcon _icon = {}; DefaultIcon _icon = {};
QImage _image; QImage _image;
Data::CustomEmojiSizeTag _tag = {};
rpl::lifetime _lifetime; rpl::lifetime _lifetime;
}; };
DefaultIconEmoji::DefaultIconEmoji( DefaultIconEmoji::DefaultIconEmoji(
rpl::producer<DefaultIcon> value, rpl::producer<DefaultIcon> value,
Fn<void()> repaint) { Fn<void()> repaint,
Data::CustomEmojiSizeTag tag)
: _tag(tag) {
std::move(value) | rpl::start_with_next([=](DefaultIcon value) { std::move(value) | rpl::start_with_next([=](DefaultIcon value) {
_icon = value; _icon = value;
_image = QImage(); _image = QImage();
@ -88,15 +89,22 @@ QString DefaultIconEmoji::entityData() {
} }
void DefaultIconEmoji::paint(QPainter &p, const Context &context) { void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
? st::normalForumTopicIcon
: st::defaultForumTopicIcon;
if (_image.isNull()) { if (_image.isNull()) {
_image = Data::ForumTopicIconFrame( _image = Data::IsForumGeneralIconTitle(_icon.title)
_icon.colorId, ? Data::ForumTopicGeneralIconFrame(
_icon.title, st.size,
st::defaultForumTopicIcon); Data::ParseForumGeneralIconColor(_icon.colorId))
: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
} }
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio(); const auto full = (_tag == Data::CustomEmojiSizeTag::Normal)
? Ui::Emoji::GetSizeNormal()
: Ui::Emoji::GetSizeLarge();
const auto esize = full / style::DevicePixelRatio();
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto skip = (customSize - st::defaultForumTopicIcon.size) / 2; const auto skip = (customSize - st.size) / 2;
p.drawImage(context.position + QPoint(skip, skip), _image); p.drawImage(context.position + QPoint(skip, skip), _image);
} }
@ -212,7 +220,7 @@ bool DefaultIconEmoji::readyInDefaultState() {
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
state->frame = Data::ForumTopicGeneralIconFrame( state->frame = Data::ForumTopicGeneralIconFrame(
st::largeForumTopicIcon.size, st::largeForumTopicIcon.size,
st::windowSubTextFg); st::windowSubTextFg->c);
result->update(); result->update();
}, result->lifetime()); }, result->lifetime());
@ -261,7 +269,8 @@ struct IconSelector {
if (id == kDefaultIconId) { if (id == kDefaultIconId) {
return std::make_unique<DefaultIconEmoji>( return std::make_unique<DefaultIconEmoji>(
rpl::duplicate(defaultIcon), rpl::duplicate(defaultIcon),
repaint); std::move(repaint),
tag);
} }
return manager->create(id, std::move(repaint), tag); return manager->create(id, std::move(repaint), tag);
}; };
@ -572,3 +581,13 @@ void EditForumTopicBox(
box->closeBox(); box->closeBox();
}); });
} }
std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
Data::TopicIconDescriptor descriptor,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag) {
return std::make_unique<DefaultIconEmoji>(
rpl::single(descriptor),
std::move(repaint),
tag);
}

View file

@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class History; class History;
namespace Data {
struct TopicIconDescriptor;
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Window { namespace Window {
class SessionController; class SessionController;
} // namespace Window } // namespace Window
@ -25,3 +30,8 @@ void EditForumTopicBox(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<History*> forum, not_null<History*> forum,
MsgId rootId); MsgId rootId);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
Data::TopicIconDescriptor descriptor,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag);

View file

@ -415,7 +415,12 @@ private:
std::deque<FnMut<void()>> _saveStagesQueue; std::deque<FnMut<void()>> _saveStagesQueue;
Saving _savingData; Saving _savingData;
const rpl::event_stream<Privacy> _privacyTypeUpdates; struct PrivacyAndForwards {
Privacy privacy;
bool noForwards = false;
};
const rpl::event_stream<PrivacyAndForwards> _privacyTypeUpdates;
const rpl::event_stream<ChannelData*> _linkedChatUpdates; const rpl::event_stream<ChannelData*> _linkedChatUpdates;
mtpRequestId _linkedChatsRequestId = 0; mtpRequestId _linkedChatsRequestId = 0;
@ -761,7 +766,7 @@ void Controller::refreshHistoryVisibility() {
void Controller::showEditPeerTypeBox( void Controller::showEditPeerTypeBox(
std::optional<rpl::producer<QString>> error) { std::optional<rpl::producer<QString>> error) {
const auto boxCallback = crl::guard(this, [=](EditPeerTypeData data) { const auto boxCallback = crl::guard(this, [=](EditPeerTypeData data) {
_privacyTypeUpdates.fire_copy(data.privacy); _privacyTypeUpdates.fire({ data.privacy, data.noForwards });
_typeDataSavedValue = data; _typeDataSavedValue = data;
refreshHistoryVisibility(); refreshHistoryVisibility();
}); });
@ -882,7 +887,8 @@ void Controller::fillPrivacyTypeButton() {
? tr::lng_manage_peer_group_type ? tr::lng_manage_peer_group_type
: tr::lng_manage_peer_channel_type)(), : tr::lng_manage_peer_channel_type)(),
_privacyTypeUpdates.events( _privacyTypeUpdates.events(
) | rpl::map([=](Privacy flag) { ) | rpl::map([=](PrivacyAndForwards data) {
const auto flag = data.privacy;
if (flag == Privacy::HasUsername) { if (flag == Privacy::HasUsername) {
_peer->session().api().usernames().requestToCache(_peer); _peer->session().api().usernames().requestToCache(_peer);
} }
@ -894,14 +900,21 @@ void Controller::fillPrivacyTypeButton() {
: tr::lng_manage_public_peer_title)() : tr::lng_manage_public_peer_title)()
: (hasLocation : (hasLocation
? tr::lng_manage_peer_link_invite ? tr::lng_manage_peer_link_invite
: isGroup : ((!data.noForwards) && isGroup)
? tr::lng_manage_private_group_title ? tr::lng_manage_private_group_title
: tr::lng_manage_private_peer_title)(); : ((!data.noForwards) && !isGroup)
? tr::lng_manage_private_peer_title
: isGroup
? tr::lng_manage_private_group_noforwards_title
: tr::lng_manage_private_peer_noforwards_title)();
}) | rpl::flatten_latest(), }) | rpl::flatten_latest(),
[=] { showEditPeerTypeBox(); }, [=] { showEditPeerTypeBox(); },
{ &st::menuIconCustomize }); { &st::menuIconCustomize });
_privacyTypeUpdates.fire_copy(_typeDataSavedValue->privacy); _privacyTypeUpdates.fire_copy({
_typeDataSavedValue->privacy,
_typeDataSavedValue->noForwards,
});
} }
void Controller::fillLinkedChatButton() { void Controller::fillLinkedChatButton() {

View file

@ -398,7 +398,7 @@ void PeerShortInfoCover::paintRadial(QPainter &p) {
QImage PeerShortInfoCover::currentVideoFrame() const { QImage PeerShortInfoCover::currentVideoFrame() const {
const auto size = QSize(_st.size, _st.size); const auto size = QSize(_st.size, _st.size);
const auto request = Media::Streaming::FrameRequest{ const auto request = Media::Streaming::FrameRequest{
.resize = size * style::DevicePixelRatio(), .resize = size,
.outer = size, .outer = size,
}; };
return (_videoInstance return (_videoInstance

View file

@ -286,7 +286,7 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
document, document,
media->videoThumbnailContent(), media->videoThumbnailContent(),
QString(), QString(),
true); Stickers::EffectType::PremiumSticker);
const auto update = [=] { const auto update = [=] {
if (!state->readyInvoked if (!state->readyInvoked

View file

@ -16,8 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/admin_log/history_admin_log_item.h" #include "history/admin_log/history_admin_log_item.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/reactions/history_view_reactions_strip.h" #include "history/view/reactions/history_view_reactions_strip.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_fake_items.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "boxes/premium_preview_box.h" #include "boxes/premium_preview_box.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -43,53 +44,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
PeerId GenerateUser(not_null<History*> history, const QString &name) {
Expects(history->peer->isUser());
const auto peerId = Data::FakePeerIdForJustName(name);
history->owner().processUser(MTP_user(
MTP_flags(MTPDuser::Flag::f_first_name | MTPDuser::Flag::f_min),
peerToBareMTPInt(peerId),
MTP_long(0),
MTP_string(tr::lng_settings_chat_message_reply_from(tr::now)),
MTPstring(), // last name
MTPstring(), // username
MTPstring(), // phone
MTPUserProfilePhoto(), // profile photo
MTPUserStatus(), // status
MTP_int(0), // bot info version
MTPVector<MTPRestrictionReason>(), // restrictions
MTPstring(), // bot placeholder
MTPstring(), // lang code
MTPEmojiStatus(),
MTPVector<MTPUsername>(),
MTPint(), // stories_max_id
MTPPeerColor(), // color
MTPPeerColor())); // profile_color
return peerId;
}
AdminLog::OwnedItem GenerateItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
PeerId from,
FullMsgId replyTo,
const QString &text) {
Expects(history->peer->isUser());
const auto item = history->addNewLocalMessage({
.id = history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::HasFromId
| MessageFlag::HasReplyInfo),
.from = from,
.replyTo = FullReplyTo{ .messageId = replyTo },
.date = base::unixtime::now(),
}, TextWithEntities{ .text = text }, MTP_messageMediaEmpty());
return AdminLog::OwnedItem(delegate, item);
}
void AddMessage( void AddMessage(
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
@ -135,15 +89,15 @@ void AddMessage(
const auto history = controller->session().data().history( const auto history = controller->session().data().history(
PeerData::kServiceNotificationsId); PeerData::kServiceNotificationsId);
state->reply = GenerateItem( state->reply = HistoryView::GenerateItem(
state->delegate.get(), state->delegate.get(),
history, history,
GenerateUser( HistoryView::GenerateUser(
history, history,
tr::lng_settings_chat_message_reply_from(tr::now)), tr::lng_settings_chat_message_reply_from(tr::now)),
FullMsgId(), FullMsgId(),
tr::lng_settings_chat_message_reply(tr::now)); tr::lng_settings_chat_message_reply(tr::now));
auto message = GenerateItem( auto message = HistoryView::GenerateItem(
state->delegate.get(), state->delegate.get(),
history, history,
history->peer->id, history->peer->id,

View file

@ -0,0 +1,250 @@
/*
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/send_credits_box.h"
#include "api/api_credits.h"
#include "apiwrap.h"
#include "core/ui_integration.h" // Core::MarkedTextContext.
#include "data/data_credits.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/history_item.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_form.h"
#include "settings/settings_credits_graphics.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg.
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "styles/style_boxes.h"
#include "styles/style_credits.h"
#include "styles/style_giveaway.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include "styles/style_settings.h"
namespace Ui {
void SendCreditsBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Payments::CreditsFormData> form,
Fn<void()> sent) {
if (!form) {
return;
}
struct State {
rpl::variable<bool> confirmButtonBusy = false;
};
const auto state = box->lifetime().make_state<State>();
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
const auto session = form->invoice.session;
const auto photoSize = st::defaultUserpicButton.photoSize;
const auto content = box->verticalLayout();
Ui::AddSkip(content, photoSize / 2);
{
const auto ministarsContainer = Ui::CreateChild<Ui::RpWidget>(box);
const auto fullHeight = photoSize * 2;
using MiniStars = Ui::Premium::ColoredMiniStars;
const auto ministars = box->lifetime().make_state<MiniStars>(
ministarsContainer,
false,
Ui::Premium::MiniStars::Type::BiStars);
ministars->setColorOverride(Ui::Premium::CreditsIconGradientStops());
ministarsContainer->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(ministarsContainer);
ministars->paint(p);
}, ministarsContainer->lifetime());
box->widthValue(
) | rpl::start_with_next([=](int width) {
ministarsContainer->resize(width, fullHeight);
const auto w = fullHeight / 3 * 2;
ministars->setCenter(QRect(
(width - w) / 2,
(fullHeight - w) / 2,
w,
w));
}, ministarsContainer->lifetime());
}
const auto bot = session->data().user(form->botId);
if (form->photo) {
box->addRow(object_ptr<Ui::CenterWrap<>>(
content,
Settings::HistoryEntryPhoto(content, form->photo, photoSize)));
} else {
const auto widget = box->addRow(
object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::UserpicButton>(
content,
bot,
st::defaultUserpicButton)));
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
}
Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_credits_box_out_title(),
st::settingsPremiumUserTitle)));
Ui::AddSkip(content);
box->addRow(object_ptr<Ui::CenterWrap<>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_credits_box_out_sure(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_text,
rpl::single(TextWithEntities{ form->title }),
lt_bot,
rpl::single(TextWithEntities{ bot->name() }),
Ui::Text::RichLangValue),
st::creditsBoxAbout)));
Ui::AddSkip(content);
Ui::AddSkip(content);
const auto button = box->addButton(rpl::single(QString()), [=] {
if (state->confirmButtonBusy.current()) {
return;
}
state->confirmButtonBusy = true;
session->api().request(
MTPpayments_SendStarsForm(
MTP_flags(0),
MTP_long(form->formId),
form->inputInvoice)
).done([=](auto result) {
state->confirmButtonBusy = false;
box->closeBox();
sent();
}).fail([=](const MTP::Error &error) {
state->confirmButtonBusy = false;
box->uiShow()->showToast(error.type());
}).send();
});
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
button,
st::giveawayGiftCodeStartButton.height / 2);
AddChildToWidgetCenter(button.data(), loadingAnimation);
loadingAnimation->showOn(state->confirmButtonBusy.value());
}
{
const auto emojiMargin = QMargins(
0,
-st::moderateBoxExpandInnerSkip,
0,
0);
const auto buttonEmoji = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::settingsPremiumIconStar,
emojiMargin,
true));
auto buttonText = tr::lng_credits_box_out_confirm(
lt_count,
rpl::single(form->invoice.amount) | tr::to_count(),
lt_emoji,
rpl::single(buttonEmoji),
Ui::Text::RichLangValue);
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(QString()),
st::defaultFlatLabel);
std::move(
buttonText
) | rpl::start_with_next([=](const TextWithEntities &text) {
buttonLabel->setMarkedText(
text,
Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = [=] { buttonLabel->update(); },
});
}, buttonLabel->lifetime());
buttonLabel->setTextColorOverride(
box->getDelegate()->style().button.textFg->c);
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
buttonLabel->moveToLeft(
(size.width() - buttonLabel->width()) / 2,
(size.height() - buttonLabel->height()) / 2);
}, buttonLabel->lifetime());
buttonLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
state->confirmButtonBusy.value(
) | rpl::start_with_next([=](bool busy) {
buttonLabel->setVisible(!busy);
}, buttonLabel->lifetime());
}
const auto buttonWidth = st::boxWidth
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
button->widthValue() | rpl::filter([=] {
return (button->widthNoMargins() != buttonWidth);
}) | rpl::start_with_next([=] {
button->resizeToWidth(buttonWidth);
}, button->lifetime());
{
const auto close = Ui::CreateChild<Ui::IconButton>(
box.get(),
st::boxTitleClose);
close->setClickedCallback([=] {
box->closeBox();
});
box->widthValue(
) | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0);
close->raise();
}, close->lifetime());
}
{
const auto balance = Settings::AddBalanceWidget(
content,
session->creditsValue(),
false);
const auto api = balance->lifetime().make_state<Api::CreditsStatus>(
session->user());
api->request({}, [=](Data::CreditsStatusSlice slice) {
session->setCredits(slice.balance);
});
rpl::combine(
balance->sizeValue(),
content->sizeValue()
) | rpl::start_with_next([=](const QSize &, const QSize &) {
balance->moveToLeft(
st::creditsHistoryRightSkip * 2,
st::creditsHistoryRightSkip);
balance->update();
}, balance->lifetime());
}
}
} // namespace Ui

View file

@ -0,0 +1,25 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class HistoryItem;
namespace Payments {
struct CreditsFormData;
} // namespace Payments
namespace Ui {
class GenericBox;
void SendCreditsBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Payments::CreditsFormData> data,
Fn<void()> sent);
} // namespace Ui

View file

@ -336,7 +336,7 @@ SendFilesBox::SendFilesBox(
const TextWithTags &caption, const TextWithTags &caption,
not_null<PeerData*> toPeer, not_null<PeerData*> toPeer,
Api::SendType sendType, Api::SendType sendType,
SendMenu::Type sendMenuType) SendMenu::Details sendMenuDetails)
: SendFilesBox(nullptr, { : SendFilesBox(nullptr, {
.show = controller->uiShow(), .show = controller->uiShow(),
.list = std::move(list), .list = std::move(list),
@ -345,7 +345,7 @@ SendFilesBox::SendFilesBox(
.limits = DefaultLimitsForPeer(toPeer), .limits = DefaultLimitsForPeer(toPeer),
.check = DefaultCheckForPeer(controller, toPeer), .check = DefaultCheckForPeer(controller, toPeer),
.sendType = sendType, .sendType = sendType,
.sendMenuType = sendMenuType, .sendMenuDetails = [=] { return sendMenuDetails; },
}) { }) {
} }
@ -358,7 +358,8 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
, _titleHeight(st::boxTitleHeight) , _titleHeight(st::boxTitleHeight)
, _list(std::move(descriptor.list)) , _list(std::move(descriptor.list))
, _limits(descriptor.limits) , _limits(descriptor.limits)
, _sendMenuType(descriptor.sendMenuType) , _sendMenuDetails(prepareSendMenuDetails(descriptor))
, _sendMenuCallback(prepareSendMenuCallback())
, _captionToPeer(descriptor.captionToPeer) , _captionToPeer(descriptor.captionToPeer)
, _check(std::move(descriptor.check)) , _check(std::move(descriptor.check))
, _confirmedCallback(std::move(descriptor.confirmed)) , _confirmedCallback(std::move(descriptor.confirmed))
@ -372,6 +373,50 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
enqueueNextPrepare(); enqueueNextPrepare();
} }
Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor) {
auto initial = descriptor.sendMenuDetails;
return crl::guard(this, [=] {
auto result = initial ? initial() : SendMenu::Details();
result.spoiler = !hasSpoilerMenu()
? SendMenu::SpoilerState::None
: allWithSpoilers()
? SendMenu::SpoilerState::Enabled
: SendMenu::SpoilerState::Possible;
const auto way = _sendWay.current();
const auto canMoveCaption = _list.canMoveCaption(
way.groupFiles() && way.sendImagesAsPhotos(),
way.sendImagesAsPhotos()
) && _caption && HasSendText(_caption);
result.caption = !canMoveCaption
? SendMenu::CaptionState::None
: _invertCaption
? SendMenu::CaptionState::Above
: SendMenu::CaptionState::Below;
return result;
});
}
auto SendFilesBox::prepareSendMenuCallback()
-> Fn<void(MenuAction, MenuDetails)> {
return crl::guard(this, [=](MenuAction action, MenuDetails details) {
using Type = SendMenu::ActionType;
switch (action.type) {
case Type::CaptionDown: _invertCaption = false; break;
case Type::CaptionUp: _invertCaption = true; break;
case Type::SpoilerOn: toggleSpoilers(true); break;
case Type::SpoilerOff: toggleSpoilers(false); break;
default:
SendMenu::DefaultCallback(
_show,
sendCallback())(
action,
details);
break;
}
});
}
void SendFilesBox::initPreview() { void SendFilesBox::initPreview() {
using namespace rpl::mappers; using namespace rpl::mappers;
@ -537,10 +582,9 @@ void SendFilesBox::refreshButtons() {
if (_sendType == Api::SendType::Normal) { if (_sendType == Api::SendType::Normal) {
SendMenu::SetupMenuAndShortcuts( SendMenu::SetupMenuAndShortcuts(
_send, _send,
[=] { return _sendMenuType; }, _show,
[=] { sendSilent(); }, _sendMenuDetails,
[=] { sendScheduled(); }, _sendMenuCallback);
[=] { sendWhenOnline(); });
} }
addButton(tr::lng_cancel(), [=] { closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); });
_addFile = addLeftButton( _addFile = addLeftButton(
@ -552,21 +596,14 @@ void SendFilesBox::refreshButtons() {
addMenuButton(); addMenuButton();
} }
bool SendFilesBox::hasSendMenu() const { bool SendFilesBox::hasSendMenu(const SendMenu::Details &details) const {
return (_sendMenuType != SendMenu::Type::Disabled); return (details.type != SendMenu::Type::Disabled)
|| (details.spoiler != SendMenu::SpoilerState::None)
|| (details.caption != SendMenu::CaptionState::None);
} }
bool SendFilesBox::hasSpoilerMenu() const { bool SendFilesBox::hasSpoilerMenu() const {
const auto allAreVideo = !ranges::any_of(_list.files, [](const auto &f) { return _list.hasSpoilerMenu(_sendWay.current().sendImagesAsPhotos());
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Video);
});
const auto allAreMedia = !ranges::any_of(_list.files, [](const auto &f) {
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Photo) && (f.type != Type::Video);
});
return allAreVideo
|| (allAreMedia && _sendWay.current().sendImagesAsPhotos());
} }
void SendFilesBox::applyBlockChanges() { void SendFilesBox::applyBlockChanges() {
@ -590,36 +627,23 @@ void SendFilesBox::toggleSpoilers(bool enabled) {
} }
void SendFilesBox::addMenuButton() { void SendFilesBox::addMenuButton() {
if (!hasSendMenu() && !hasSpoilerMenu()) { const auto details = _sendMenuDetails();
if (!hasSendMenu(details)) {
return; return;
} }
const auto top = addTopButton(_st.files.menu); const auto top = addTopButton(_st.files.menu);
top->setClickedCallback([=] { top->setClickedCallback([=] {
const auto &tabbed = _st.tabbed; const auto &tabbed = _st.tabbed;
const auto &icons = tabbed.icons;
_menu = base::make_unique_q<Ui::PopupMenu>(top, tabbed.menu); _menu = base::make_unique_q<Ui::PopupMenu>(top, tabbed.menu);
if (hasSpoilerMenu()) { const auto position = QCursor::pos();
const auto spoilered = allWithSpoilers(); SendMenu::FillSendMenu(
_menu->addAction( _menu.get(),
(spoilered _show,
? tr::lng_context_disable_spoiler(tr::now) _sendMenuDetails(),
: tr::lng_context_spoiler_effect(tr::now)), _sendMenuCallback,
[=] { toggleSpoilers(!spoilered); }, &_st.tabbed.icons,
spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); position);
if (hasSendMenu()) {
_menu->addSeparator(&tabbed.expandedSeparator);
}
}
if (hasSendMenu()) {
SendMenu::FillSendMenu(
_menu.get(),
_sendMenuType,
[=] { sendSilent(); },
[=] { sendScheduled(); },
[=] { sendWhenOnline(); },
&_st.tabbed.icons);
}
using ImageInfo = Ui::PreparedFileInformation::Image; using ImageInfo = Ui::PreparedFileInformation::Image;
if (_list.files.size() == 1 && std::get_if<ImageInfo>(&_list.files[0].information->media)) { if (_list.files.size() == 1 && std::get_if<ImageInfo>(&_list.files[0].information->media)) {
@ -650,10 +674,9 @@ void SendFilesBox::addMenuButton() {
}, },
&st::menuIconStickers); &st::menuIconStickers);
} }
_menu->popup(QCursor::pos()); _menu->popup(position);
return true; return true;
}); });
} }
void SendFilesBox::initSendWay() { void SendFilesBox::initSendWay() {
@ -695,9 +718,7 @@ void SendFilesBox::initSendWay() {
for (auto &block : _blocks) { for (auto &block : _blocks) {
block.setSendWay(value); block.setSendWay(value);
} }
if (!hasSendMenu()) { refreshButtons();
refreshButtons();
}
if (was != hidden()) { if (was != hidden()) {
updateBoxSize(); updateBoxSize();
updateControlsGeometry(); updateControlsGeometry();
@ -909,9 +930,7 @@ void SendFilesBox::pushBlock(int from, int till) {
} }
void SendFilesBox::refreshControls(bool initial) { void SendFilesBox::refreshControls(bool initial) {
if (initial || !hasSendMenu()) { refreshButtons();
refreshButtons();
}
refreshTitleText(); refreshTitleText();
updateSendWayControls(); updateSendWayControls();
updateCaptionPlaceholder(); updateCaptionPlaceholder();
@ -1477,7 +1496,12 @@ void SendFilesBox::send(
if ((_sendType == Api::SendType::Scheduled if ((_sendType == Api::SendType::Scheduled
|| _sendType == Api::SendType::ScheduledToUser) || _sendType == Api::SendType::ScheduledToUser)
&& !options.scheduled) { && !options.scheduled) {
return sendScheduled(); auto child = _sendMenuDetails();
child.spoiler = SendMenu::SpoilerState::None;
child.caption = SendMenu::CaptionState::None;
return SendMenu::DefaultCallback(_show, sendCallback())(
{ .type = SendMenu::ActionType::Schedule },
child);
} }
if (_preparing) { if (_preparing) {
_whenReadySend = [=] { _whenReadySend = [=] {
@ -1502,6 +1526,7 @@ void SendFilesBox::send(
auto caption = (_caption && !_caption->isHidden()) auto caption = (_caption && !_caption->isHidden())
? _caption->getTextWithAppliedMarkdown() ? _caption->getTextWithAppliedMarkdown()
: TextWithTags(); : TextWithTags();
options.invertCaption = _invertCaption;
if (!validateLength(caption.text)) { if (!validateLength(caption.text)) {
return; return;
} }
@ -1515,25 +1540,10 @@ void SendFilesBox::send(
closeBox(); closeBox();
} }
void SendFilesBox::sendSilent() { Fn<void(Api::SendOptions)> SendFilesBox::sendCallback() {
send({ .silent = true }); return crl::guard(this, [=](Api::SendOptions options) {
} send(options, false);
});
void SendFilesBox::sendScheduled() {
const auto type = (_sendType == Api::SendType::ScheduledToUser)
? SendMenu::Type::ScheduledToUser
: _sendMenuType;
const auto callback = [=](Api::SendOptions options) { send(options); };
auto box = HistoryView::PrepareScheduleBox(this, type, callback);
const auto weak = Ui::MakeWeak(box.data());
_show->showBox(std::move(box));
if (const auto strong = weak.data()) {
strong->setCloseByOutsideClick(false);
}
}
void SendFilesBox::sendWhenOnline() {
send(Api::DefaultSendWhenOnlineOptions());
} }
SendFilesBox::~SendFilesBox() = default; SendFilesBox::~SendFilesBox() = default;

View file

@ -47,7 +47,8 @@ class SessionController;
} // namespace Window } // namespace Window
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
struct Action;
} // namespace SendMenu } // namespace SendMenu
namespace HistoryView::Controls { namespace HistoryView::Controls {
@ -96,7 +97,7 @@ struct SendFilesBoxDescriptor {
SendFilesLimits limits = {}; SendFilesLimits limits = {};
SendFilesCheck check; SendFilesCheck check;
Api::SendType sendType = {}; Api::SendType sendType = {};
SendMenu::Type sendMenuType = {}; Fn<SendMenu::Details()> sendMenuDetails = nullptr;
const style::ComposeControls *stOverride = nullptr; const style::ComposeControls *stOverride = nullptr;
SendFilesConfirmed confirmed; SendFilesConfirmed confirmed;
Fn<void()> cancelled; Fn<void()> cancelled;
@ -115,7 +116,7 @@ public:
const TextWithTags &caption, const TextWithTags &caption,
not_null<PeerData*> toPeer, not_null<PeerData*> toPeer,
Api::SendType sendType, Api::SendType sendType,
SendMenu::Type sendMenuType); SendMenu::Details sendMenuDetails);
SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor); SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor);
void setConfirmedCallback(SendFilesConfirmed callback) { void setConfirmedCallback(SendFilesConfirmed callback) {
@ -136,6 +137,9 @@ protected:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;
private: private:
using MenuAction = SendMenu::Action;
using MenuDetails = SendMenu::Details;
class Block final { class Block final {
public: public:
Block( Block(
@ -173,7 +177,7 @@ private:
void initSendWay(); void initSendWay();
void initPreview(); void initPreview();
[[nodiscard]] bool hasSendMenu() const; [[nodiscard]] bool hasSendMenu(const MenuDetails &details) const;
[[nodiscard]] bool hasSpoilerMenu() const; [[nodiscard]] bool hasSpoilerMenu() const;
[[nodiscard]] bool allWithSpoilers(); [[nodiscard]] bool allWithSpoilers();
[[nodiscard]] bool checkWithWay( [[nodiscard]] bool checkWithWay(
@ -202,9 +206,7 @@ private:
void generatePreviewFrom(int fromBlock); void generatePreviewFrom(int fromBlock);
void send(Api::SendOptions options, bool ctrlShiftEnter = false); void send(Api::SendOptions options, bool ctrlShiftEnter = false);
void sendSilent(); [[nodiscard]] Fn<void(Api::SendOptions)> sendCallback();
void sendScheduled();
void sendWhenOnline();
void captionResized(); void captionResized();
void saveSendWaySettings(); void saveSendWaySettings();
@ -227,6 +229,11 @@ private:
void checkCharsLimitation(); void checkCharsLimitation();
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor);
[[nodiscard]] auto prepareSendMenuCallback()
-> Fn<void(MenuAction, MenuDetails)>;
const std::shared_ptr<ChatHelpers::Show> _show; const std::shared_ptr<ChatHelpers::Show> _show;
const style::ComposeControls &_st; const style::ComposeControls &_st;
const Api::SendType _sendType = Api::SendType(); const Api::SendType _sendType = Api::SendType();
@ -238,12 +245,14 @@ private:
std::optional<int> _removingIndex; std::optional<int> _removingIndex;
SendFilesLimits _limits = {}; SendFilesLimits _limits = {};
SendMenu::Type _sendMenuType = {}; Fn<MenuDetails()> _sendMenuDetails;
Fn<void(MenuAction, MenuDetails)> _sendMenuCallback;
PeerData *_captionToPeer = nullptr; PeerData *_captionToPeer = nullptr;
SendFilesCheck _check; SendFilesCheck _check;
SendFilesConfirmed _confirmedCallback; SendFilesConfirmed _confirmedCallback;
Fn<void()> _cancelledCallback; Fn<void()> _cancelledCallback;
bool _confirmed = false; bool _confirmed = false;
bool _invertCaption = false;
object_ptr<Ui::InputField> _caption = { nullptr }; object_ptr<Ui::InputField> _caption = { nullptr };
TextWithTags _prefilledCaptionText; TextWithTags _prefilledCaptionText;

View file

@ -478,15 +478,18 @@ void ShareBox::keyPressEvent(QKeyEvent *e) {
} }
} }
SendMenu::Type ShareBox::sendMenuType() const { SendMenu::Details ShareBox::sendMenuDetails() const {
const auto selected = _inner->selected(); const auto selected = _inner->selected();
return ranges::all_of( const auto type = ranges::all_of(
selected | ranges::views::transform(&Data::Thread::peer), selected | ranges::views::transform(&Data::Thread::peer),
HistoryView::CanScheduleUntilOnline) HistoryView::CanScheduleUntilOnline)
? SendMenu::Type::ScheduledToUser ? SendMenu::Type::ScheduledToUser
: (selected.size() == 1 && selected.front()->peer()->isSelf()) : (selected.size() == 1 && selected.front()->peer()->isSelf())
? SendMenu::Type::Reminder ? SendMenu::Type::Reminder
: SendMenu::Type::Scheduled; : SendMenu::Type::Scheduled;
// We can't support effect here because we don't have ChatHelpers::Show.
return { .type = type, .effectAllowed = false };
} }
void ShareBox::showMenu(not_null<Ui::RpWidget*> parent) { void ShareBox::showMenu(not_null<Ui::RpWidget*> parent) {
@ -523,15 +526,32 @@ void ShareBox::showMenu(not_null<Ui::RpWidget*> parent) {
_menu->addSeparator(); _menu->addSeparator();
} }
const auto result = SendMenu::FillSendMenu( using namespace SendMenu;
const auto sendAction = crl::guard(this, [=](Action action, Details) {
if (action.type == ActionType::Send) {
submit(action.options);
return;
}
uiShow()->showBox(
HistoryView::PrepareScheduleBox(
this,
nullptr, // ChatHelpers::Show for effect attachment.
sendMenuDetails(),
[=](Api::SendOptions options) { submit(options); },
action.options,
HistoryView::DefaultScheduleTime(),
_descriptor.scheduleBoxStyle));
});
_menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom);
const auto result = FillSendMenu(
_menu.get(), _menu.get(),
sendMenuType(), nullptr, // showForEffect.
[=] { submitSilent(); }, sendMenuDetails(),
[=] { submitScheduled(); }, sendAction);
[=] { submitWhenOnline(); }); if (result == SendMenu::FillMenuResult::Prepared) {
const auto success = (result == SendMenu::FillMenuResult::Success); _menu->popupPrepared();
if (_descriptor.forwardOptions.show || success) { } else if (_descriptor.forwardOptions.show
_menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom); && result != SendMenu::FillMenuResult::Failed) {
_menu->popup(QCursor::pos()); _menu->popup(QCursor::pos());
} }
} }
@ -612,25 +632,6 @@ void ShareBox::submit(Api::SendOptions options) {
} }
} }
void ShareBox::submitSilent() {
submit({ .silent = true });
}
void ShareBox::submitScheduled() {
const auto callback = [=](Api::SendOptions options) { submit(options); };
uiShow()->showBox(
HistoryView::PrepareScheduleBox(
this,
sendMenuType(),
callback,
HistoryView::DefaultScheduleTime(),
_descriptor.scheduleBoxStyle));
}
void ShareBox::submitWhenOnline() {
submit(Api::DefaultSendWhenOnlineOptions());
}
void ShareBox::copyLink() const { void ShareBox::copyLink() const {
if (const auto onstack = _descriptor.copyCallback) { if (const auto onstack = _descriptor.copyCallback) {
onstack(); onstack();

View file

@ -24,7 +24,7 @@ struct PeerList;
} // namespace style } // namespace style
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace Window { namespace Window {
@ -130,13 +130,10 @@ private:
void scrollAnimationCallback(); void scrollAnimationCallback();
void submit(Api::SendOptions options); void submit(Api::SendOptions options);
void submitSilent();
void submitScheduled();
void submitWhenOnline();
void copyLink() const; void copyLink() const;
bool searchByUsername(bool useCache = false); bool searchByUsername(bool useCache = false);
SendMenu::Type sendMenuType() const; [[nodiscard]] SendMenu::Details sendMenuDetails() const;
void scrollTo(Ui::ScrollToRequest request); void scrollTo(Ui::ScrollToRequest request);
void needSearchByUsername(); void needSearchByUsername();

View file

@ -79,7 +79,9 @@ using Data::StickersSet;
using Data::StickersPack; using Data::StickersPack;
using SetFlag = Data::StickersSetFlag; using SetFlag = Data::StickersSetFlag;
[[nodiscard]] std::optional<QColor> ComputeImageColor(const QImage &frame) { [[nodiscard]] std::optional<QColor> ComputeImageColor(
const style::icon &lockIcon,
const QImage &frame) {
if (frame.isNull() if (frame.isNull()
|| frame.format() != QImage::Format_ARGB32_Premultiplied) { || frame.format() != QImage::Format_ARGB32_Premultiplied) {
return {}; return {};
@ -89,7 +91,7 @@ using SetFlag = Data::StickersSetFlag;
auto sb = int64(); auto sb = int64();
auto sa = int64(); auto sa = int64();
const auto factor = frame.devicePixelRatio(); const auto factor = frame.devicePixelRatio();
const auto size = st::stickersPremiumLock.size() * factor; const auto size = lockIcon.size() * factor;
const auto width = std::min(frame.width(), size.width()); const auto width = std::min(frame.width(), size.width());
const auto height = std::min(frame.height(), size.height()); const auto height = std::min(frame.height(), size.height());
const auto skipx = (frame.width() - width) / 2; const auto skipx = (frame.width() - width) / 2;
@ -116,22 +118,30 @@ using SetFlag = Data::StickersSetFlag;
} }
[[nodiscard]] QColor ComputeLockColor(const QImage &frame) { [[nodiscard]] QColor ComputeLockColor(
return ComputeImageColor(frame).value_or(st::windowSubTextFg->c); const style::icon &lockIcon,
const QImage &frame) {
return ComputeImageColor(
lockIcon,
frame
).value_or(st::windowSubTextFg->c);
} }
void ValidatePremiumLockBg(QImage &image, const QImage &frame) { void ValidatePremiumLockBg(
const style::icon &lockIcon,
QImage &image,
const QImage &frame) {
if (!image.isNull()) { if (!image.isNull()) {
return; return;
} }
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto size = st::stickersPremiumLock.size(); const auto size = lockIcon.size();
image = QImage( image = QImage(
size * factor, size * factor,
QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor); image.setDevicePixelRatio(factor);
auto p = QPainter(&image); auto p = QPainter(&image);
const auto color = ComputeLockColor(frame); const auto color = ComputeLockColor(lockIcon, frame);
p.fillRect( p.fillRect(
QRect(QPoint(), size), QRect(QPoint(), size),
anim::color(color, st::windowSubTextFg, kGrayLockOpacity)); anim::color(color, st::windowSubTextFg, kGrayLockOpacity));
@ -140,12 +150,12 @@ void ValidatePremiumLockBg(QImage &image, const QImage &frame) {
image = Images::Circle(std::move(image)); image = Images::Circle(std::move(image));
} }
void ValidatePremiumStarFg(QImage &image) { void ValidatePremiumStarFg(const style::icon &lockIcon, QImage &image) {
if (!image.isNull()) { if (!image.isNull()) {
return; return;
} }
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto size = st::stickersPremiumLock.size(); const auto size = lockIcon.size();
image = QImage( image = QImage(
size * factor, size * factor,
QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
@ -182,7 +192,10 @@ void ValidatePremiumStarFg(QImage &image) {
} // namespace } // namespace
StickerPremiumMark::StickerPremiumMark(not_null<Main::Session*> session) { StickerPremiumMark::StickerPremiumMark(
not_null<Main::Session*> session,
const style::icon &lockIcon)
: _lockIcon(lockIcon) {
style::PaletteChanged( style::PaletteChanged(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
_lockGray = QImage(); _lockGray = QImage();
@ -208,16 +221,14 @@ void StickerPremiumMark::paint(
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto radius = st::roundRadiusSmall; const auto radius = st::roundRadiusSmall;
const auto point = position + QPoint( const auto point = position + QPoint(
(_premium (singleSize.width() - (bg.width() / factor) - radius),
? (singleSize.width() - (bg.width() / factor) - radius)
: (singleSize.width() - (bg.width() / factor)) / 2),
singleSize.height() - (bg.height() / factor) - radius); singleSize.height() - (bg.height() / factor) - radius);
p.drawImage(point, bg); p.drawImage(point, bg);
if (_premium) { if (_premium) {
validateStar(); validateStar();
p.drawImage(point, _star); p.drawImage(point, _star);
} else { } else {
st::stickersPremiumLock.paint(p, point, outerWidth); _lockIcon.paint(p, point, outerWidth);
} }
} }
@ -225,11 +236,11 @@ void StickerPremiumMark::validateLock(
const QImage &frame, const QImage &frame,
QImage &backCache) { QImage &backCache) {
auto &image = frame.isNull() ? _lockGray : backCache; auto &image = frame.isNull() ? _lockGray : backCache;
ValidatePremiumLockBg(image, frame); ValidatePremiumLockBg(_lockIcon, image, frame);
} }
void StickerPremiumMark::validateStar() { void StickerPremiumMark::validateStar() {
ValidatePremiumStarFg(_star); ValidatePremiumStarFg(_lockIcon, _star);
} }
class StickerSetBox::Inner final : public Ui::RpWidget { class StickerSetBox::Inner final : public Ui::RpWidget {
@ -708,7 +719,7 @@ StickerSetBox::Inner::Inner(
st::windowBgRipple, st::windowBgRipple,
st::windowBgOver, st::windowBgOver,
[=] { repaintItems(); })) [=] { repaintItems(); }))
, _premiumMark(_session) , _premiumMark(_session, st::stickersPremiumLock)
, _updateItemsTimer([=] { updateItems(); }) , _updateItemsTimer([=] { updateItems(); })
, _input(set) , _input(set)
, _padding((type == Data::StickersType::Emoji) , _padding((type == Data::StickersType::Emoji)
@ -1058,7 +1069,7 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
_menu = base::make_unique_q<Ui::PopupMenu>( _menu = base::make_unique_q<Ui::PopupMenu>(
this, this,
st::popupMenuWithIcons); st::popupMenuWithIcons);
const auto type = _show->sendMenuType(); const auto details = _show->sendMenuDetails();
if (setType() == Data::StickersType::Emoji) { if (setType() == Data::StickersType::Emoji) {
if (const auto t = PrepareTextFromEmoji(_pack[index]); !t.empty()) { if (const auto t = PrepareTextFromEmoji(_pack[index]); !t.empty()) {
_menu->addAction(tr::lng_mediaview_copy(tr::now), [=] { _menu->addAction(tr::lng_mediaview_copy(tr::now), [=] {
@ -1067,17 +1078,16 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
} }
}, &st::menuIconCopy); }, &st::menuIconCopy);
} }
} else if (type != SendMenu::Type::Disabled) { } else if (details.type != SendMenu::Type::Disabled) {
const auto document = _pack[index]; const auto document = _pack[index];
const auto sendSelected = [=](Api::SendOptions options) { const auto send = crl::guard(this, [=](Api::SendOptions options) {
chosen(index, document, options); chosen(index, document, options);
}; });
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
_menu.get(), _menu.get(),
type, _show,
SendMenu::DefaultSilentCallback(sendSelected), details,
SendMenu::DefaultScheduleCallback(_show, type, sendSelected), SendMenu::DefaultCallback(_show, send));
SendMenu::DefaultWhenOnlineCallback(sendSelected));
const auto show = _show; const auto show = _show;
const auto toggleFavedSticker = [=] { const auto toggleFavedSticker = [=] {

View file

@ -23,10 +23,6 @@ namespace Data {
class StickersSet; class StickersSet;
} // namespace Data } // namespace Data
namespace SendMenu {
enum class Type;
} // namespace SendMenu
namespace ChatHelpers { namespace ChatHelpers {
struct FileChosen; struct FileChosen;
class Show; class Show;
@ -34,7 +30,9 @@ class Show;
class StickerPremiumMark final { class StickerPremiumMark final {
public: public:
explicit StickerPremiumMark(not_null<Main::Session*> session); StickerPremiumMark(
not_null<Main::Session*> session,
const style::icon &lockIcon);
void paint( void paint(
QPainter &p, QPainter &p,
@ -48,6 +46,7 @@ private:
void validateLock(const QImage &frame, QImage &backCache); void validateLock(const QImage &frame, QImage &backCache);
void validateStar(); void validateStar();
const style::icon &_lockIcon;
QImage _lockGray; QImage _lockGray;
QImage _star; QImage _star;
bool _premium = false; bool _premium = false;

View file

@ -69,6 +69,8 @@ ComposeIcons {
menuWhenOnline: icon; menuWhenOnline: icon;
menuSpoiler: icon; menuSpoiler: icon;
menuSpoilerOff: icon; menuSpoilerOff: icon;
menuBelow: icon;
menuAbove: icon;
stripBubble: icon; stripBubble: icon;
stripExpandPanel: icon; stripExpandPanel: icon;
@ -489,6 +491,7 @@ hashtagClose: IconButton {
stickerPanWidthMin: 64px; stickerPanWidthMin: 64px;
stickerPanSize: size(stickerPanWidthMin, stickerPanWidthMin); stickerPanSize: size(stickerPanWidthMin, stickerPanWidthMin);
stickerEffectWidthMin: 48px;
stickerPanPadding: 11px; stickerPanPadding: 11px;
stickerPanDeleteIconBg: icon {{ "emoji/emoji_delete_bg", stickerPanDeleteBg }}; stickerPanDeleteIconBg: icon {{ "emoji/emoji_delete_bg", stickerPanDeleteBg }};
stickerPanDeleteIconFg: icon {{ "emoji/emoji_delete", stickerPanDeleteFg }}; stickerPanDeleteIconFg: icon {{ "emoji/emoji_delete", stickerPanDeleteFg }};
@ -605,6 +608,8 @@ defaultComposeIcons: ComposeIcons {
menuWhenOnline: menuIconWhenOnline; menuWhenOnline: menuIconWhenOnline;
menuSpoiler: menuIconSpoiler; menuSpoiler: menuIconSpoiler;
menuSpoilerOff: menuIconSpoilerOff; menuSpoilerOff: menuIconSpoilerOff;
menuBelow: menuIconBelow;
menuAbove: menuIconAbove;
stripBubble: icon{ stripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg }, { "chat/reactions_bubble_shadow", windowShadowFg },
@ -669,7 +674,7 @@ defaultEmojiPan: EmojiPan {
boxLabel: boxLabel; boxLabel: boxLabel;
icons: defaultComposeIcons; icons: defaultComposeIcons;
about: defaultEmojiPanAbout; about: defaultEmojiPanAbout;
aboutPadding: margins(12px, 2px, 12px, 2px); aboutPadding: margins(12px, 3px, 12px, 2px);
autocompleteBottomSkip: 0px; autocompleteBottomSkip: 0px;
} }
statusEmojiPan: EmojiPan(defaultEmojiPan) { statusEmojiPan: EmojiPan(defaultEmojiPan) {
@ -753,6 +758,7 @@ inlineResultsMinWidth: 48px;
inlineDurationMargin: 3px; inlineDurationMargin: 3px;
stickersPremiumLock: icon{{ "emoji/premium_lock", premiumButtonFg }}; stickersPremiumLock: icon{{ "emoji/premium_lock", premiumButtonFg }};
emojiPremiumLock: icon{{ "chat/mini_lock", premiumButtonFg }};
reactStripExtend: margins(21px, 49px, 39px, 0px); reactStripExtend: margins(21px, 49px, 39px, 0px);
reactStripHeight: 40px; reactStripHeight: 40px;
@ -924,7 +930,12 @@ historyPinnedBotButton: RoundButton(defaultActiveButton) {
textTop: 6px; textTop: 6px;
padding: margins(2px, 10px, 10px, 9px); padding: margins(2px, 10px, 10px, 9px);
} }
historyPinnedBotButtonMaxWidth: 150px; historyPinnedBotLabel: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
align: align(center);
maxHeight: 30px;
}
historyPinnedBotButtonMaxWidth: 120px;
historyToDownPosition: point(12px, 10px); historyToDownPosition: point(12px, 10px);
historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }};

View file

@ -22,7 +22,7 @@ class SessionController;
} // namespace Window } // namespace Window
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace ChatHelpers { namespace ChatHelpers {
@ -57,7 +57,7 @@ public:
[[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0; [[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0;
[[nodiscard]] virtual rpl::producer<bool> adjustShadowLeft() const; [[nodiscard]] virtual rpl::producer<bool> adjustShadowLeft() const;
[[nodiscard]] virtual SendMenu::Type sendMenuType() const = 0; [[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0;
virtual bool showMediaPreview( virtual bool showMediaPreview(
Data::FileOrigin origin, Data::FileOrigin origin,

View file

@ -136,6 +136,7 @@ struct EmojiListWidget::CustomEmojiInstance {
struct EmojiListWidget::RecentOne { struct EmojiListWidget::RecentOne {
Ui::Text::CustomEmoji *custom = nullptr; Ui::Text::CustomEmoji *custom = nullptr;
RecentEmojiId id; RecentEmojiId id;
mutable QImage premiumLock;
}; };
EmojiColorPicker::EmojiColorPicker( EmojiColorPicker::EmojiColorPicker(
@ -478,8 +479,12 @@ EmojiListWidget::EmojiListWidget(
, _localSetsManager( , _localSetsManager(
std::make_unique<LocalStickersManager>(&session())) std::make_unique<LocalStickersManager>(&session()))
, _customRecentFactory(std::move(descriptor.customRecentFactory)) , _customRecentFactory(std::move(descriptor.customRecentFactory))
, _freeEffects(std::move(descriptor.freeEffects))
, _customTextColor(std::move(descriptor.customTextColor)) , _customTextColor(std::move(descriptor.customTextColor))
, _overBg(st::emojiPanRadius, st().overBg) , _overBg(st::emojiPanRadius, st().overBg)
, _premiumMark(std::make_unique<StickerPremiumMark>(
&session(),
st::emojiPremiumLock))
, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) , _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg)
, _picker(this, st()) , _picker(this, st())
, _showPickerTimer([=] { showPicker(); }) , _showPickerTimer([=] { showPicker(); })
@ -583,9 +588,18 @@ void EmojiListWidget::setupSearch() {
InvokeQueued(this, [=] { InvokeQueued(this, [=] {
applyNextSearchQuery(); applyNextSearchQuery();
}); });
_searchQueries.fire_copy(_nextSearchQuery);
}, session, type); }, session, type);
} }
rpl::producer<std::vector<QString>> EmojiListWidget::searchQueries() const {
return _searchQueries.events();
}
rpl::producer<int> EmojiListWidget::recentShownCount() const {
return _recentShownCount.value();
}
void EmojiListWidget::applyNextSearchQuery() { void EmojiListWidget::applyNextSearchQuery() {
if (_searchQuery == _nextSearchQuery) { if (_searchQuery == _nextSearchQuery) {
return; return;
@ -607,6 +621,9 @@ void EmojiListWidget::applyNextSearchQuery() {
_searchCustomIds.clear(); _searchCustomIds.clear();
} }
resizeToWidth(width()); resizeToWidth(width());
_recentShownCount = searching
? _searchResults.size()
: _recent.size();
update(); update();
if (modeChanged) { if (modeChanged) {
visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom()); visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom());
@ -834,7 +851,8 @@ void EmojiListWidget::unloadCustomIn(const SectionInfo &info) {
object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() { object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {
Expects(_footer == nullptr); Expects(_footer == nullptr);
if (_mode == EmojiListMode::RecentReactions) { if (_mode == EmojiListMode::RecentReactions
|| _mode == EmojiListMode::MessageEffects) {
return { nullptr }; return { nullptr };
} }
@ -1018,9 +1036,10 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
const auto minimalLastHeight = std::max( const auto minimalLastHeight = std::max(
minimalHeight - padding.bottom(), minimalHeight - padding.bottom(),
0); 0);
return qMax( const auto result = countResult(minimalLastHeight);
minimalHeight, return result
countResult(minimalLastHeight) + padding.bottom()); ? qMax(minimalHeight, result + padding.bottom())
: 0;
} }
int EmojiListWidget::defaultMinimalHeight() const { int EmojiListWidget::defaultMinimalHeight() const {
@ -1104,7 +1123,7 @@ void EmojiListWidget::fillRecentFrom(const std::vector<DocumentId> &list) {
} }
base::unique_qptr<Ui::PopupMenu> EmojiListWidget::fillContextMenu( base::unique_qptr<Ui::PopupMenu> EmojiListWidget::fillContextMenu(
SendMenu::Type type) { const SendMenu::Details &details) {
if (v::is_null(_selected)) { if (v::is_null(_selected)) {
return nullptr; return nullptr;
} }
@ -1285,6 +1304,8 @@ void EmojiListWidget::paint(
QRect clip) { QRect clip) {
validateEmojiPaintContext(context); validateEmojiPaintContext(context);
_paintAsPremium = session().premium();
auto fromColumn = floorclamp( auto fromColumn = floorclamp(
clip.x() - _rowsLeft, clip.x() - _rowsLeft,
_singleSize.width(), _singleSize.width(),
@ -1449,16 +1470,44 @@ void EmojiListWidget::drawRecent(
QPoint position, QPoint position,
const RecentOne &recent) { const RecentOne &recent) {
_recentPainted = true; _recentPainted = true;
const auto locked = (_mode == Mode::MessageEffects)
&& !_paintAsPremium
&& v::is<RecentEmojiDocument>(recent.id.data)
&& !_freeEffects.contains(
v::get<RecentEmojiDocument>(recent.id.data).id);
auto lockedPainted = false;
if (locked) {
if (_premiumMarkFrameCache.isNull()) {
const auto ratio = style::DevicePixelRatio();
_premiumMarkFrameCache = QImage(
QSize(_customSingleSize, _customSingleSize) * ratio,
QImage::Format_ARGB32_Premultiplied);
_premiumMarkFrameCache.setDevicePixelRatio(ratio);
}
_premiumMarkFrameCache.fill(Qt::transparent);
}
if (const auto custom = recent.custom) { if (const auto custom = recent.custom) {
_emojiPaintContext->scale = context.progress; const auto exactPosition = position
_emojiPaintContext->position = position
+ _innerPosition + _innerPosition
+ _customPosition; + _customPosition;
_emojiPaintContext->scale = context.progress;
if (_mode == Mode::ChannelStatus) { if (_mode == Mode::ChannelStatus) {
_emojiPaintContext->internal.forceFirstFrame _emojiPaintContext->internal.forceFirstFrame
= (recent.id == _recent.front().id); = (recent.id == _recent.front().id);
} }
custom->paint(p, *_emojiPaintContext); if (locked) {
lockedPainted = custom->ready();
auto q = Painter(&_premiumMarkFrameCache);
_emojiPaintContext->position = QPoint();
custom->paint(q, *_emojiPaintContext);
q.end();
p.drawImage(exactPosition, _premiumMarkFrameCache);
} else {
_emojiPaintContext->position = exactPosition;
custom->paint(p, *_emojiPaintContext);
}
} else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) { } else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) {
if (_mode == Mode::EmojiStatus) { if (_mode == Mode::EmojiStatus) {
position += QPoint( position += QPoint(
@ -1472,6 +1521,16 @@ void EmojiListWidget::drawRecent(
} else { } else {
Unexpected("Empty custom emoji in EmojiListWidget::drawRecent."); Unexpected("Empty custom emoji in EmojiListWidget::drawRecent.");
} }
if (locked) {
_premiumMark->paint(
p,
lockedPainted ? _premiumMarkFrameCache : QImage(),
recent.premiumLock,
position,
_singleSize,
width());
}
} }
void EmojiListWidget::drawEmoji( void EmojiListWidget::drawEmoji(
@ -2131,7 +2190,7 @@ void EmojiListWidget::refreshRecent() {
} }
void EmojiListWidget::refreshCustom() { void EmojiListWidget::refreshCustom() {
if (_mode == Mode::RecentReactions) { if (_mode == Mode::RecentReactions || _mode == Mode::MessageEffects) {
return; return;
} }
auto old = base::take(_custom); auto old = base::take(_custom);

View file

@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/round_rect.h" #include "ui/round_rect.h"
#include "base/timer.h" #include "base/timer.h"
class StickerPremiumMark;
namespace style { namespace style {
struct EmojiPan; struct EmojiPan;
} // namespace style } // namespace style
@ -77,6 +79,7 @@ enum class EmojiListMode {
UserpicBuilder, UserpicBuilder,
BackgroundEmoji, BackgroundEmoji,
PeerTitle, PeerTitle,
MessageEffects,
}; };
struct EmojiListDescriptor { struct EmojiListDescriptor {
@ -88,6 +91,7 @@ struct EmojiListDescriptor {
Fn<std::unique_ptr<Ui::Text::CustomEmoji>( Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId, DocumentId,
Fn<void()>)> customRecentFactory; Fn<void()>)> customRecentFactory;
base::flat_set<DocumentId> freeEffects;
const style::EmojiPan *st = nullptr; const style::EmojiPan *st = nullptr;
ComposeFeatures features; ComposeFeatures features;
}; };
@ -144,7 +148,10 @@ public:
RectPart origin); RectPart origin);
base::unique_qptr<Ui::PopupMenu> fillContextMenu( base::unique_qptr<Ui::PopupMenu> fillContextMenu(
SendMenu::Type type) override; const SendMenu::Details &details) override;
[[nodiscard]] rpl::producer<std::vector<QString>> searchQueries() const;
[[nodiscard]] rpl::producer<int> recentShownCount() const;
protected: protected:
void visibleTopBottomUpdated( void visibleTopBottomUpdated(
@ -397,10 +404,13 @@ private:
int _counts[kEmojiSectionCount]; int _counts[kEmojiSectionCount];
std::vector<RecentOne> _recent; std::vector<RecentOne> _recent;
base::flat_set<DocumentId> _recentCustomIds; base::flat_set<DocumentId> _recentCustomIds;
base::flat_set<DocumentId> _freeEffects;
base::flat_set<uint64> _repaintsScheduled; base::flat_set<uint64> _repaintsScheduled;
rpl::variable<int> _recentShownCount;
std::unique_ptr<Ui::Text::CustomEmojiPaintContext> _emojiPaintContext; std::unique_ptr<Ui::Text::CustomEmojiPaintContext> _emojiPaintContext;
bool _recentPainted = false; bool _recentPainted = false;
bool _grabbingChosen = false; bool _grabbingChosen = false;
bool _paintAsPremium = false;
QVector<EmojiPtr> _emoji[kEmojiSectionCount]; QVector<EmojiPtr> _emoji[kEmojiSectionCount];
std::vector<CustomSet> _custom; std::vector<CustomSet> _custom;
base::flat_set<DocumentId> _restrictedCustomList; base::flat_set<DocumentId> _restrictedCustomList;
@ -414,10 +424,13 @@ private:
Ui::RoundRect _overBg; Ui::RoundRect _overBg;
QImage _searchExpandCache; QImage _searchExpandCache;
std::unique_ptr<StickerPremiumMark> _premiumMark;
QImage _premiumMarkFrameCache;
mutable std::unique_ptr<Ui::RippleAnimation> _colorAllRipple; mutable std::unique_ptr<Ui::RippleAnimation> _colorAllRipple;
bool _colorAllRippleForced = false; bool _colorAllRippleForced = false;
rpl::lifetime _colorAllRippleForcedLifetime; rpl::lifetime _colorAllRippleForcedLifetime;
rpl::event_stream<std::vector<QString>> _searchQueries;
std::vector<QString> _nextSearchQuery; std::vector<QString> _nextSearchQuery;
std::vector<QString> _searchQuery; std::vector<QString> _searchQuery;
base::flat_set<EmojiPtr> _searchEmoji; base::flat_set<EmojiPtr> _searchEmoji;

View file

@ -87,7 +87,7 @@ public:
Api::SendOptions options = {}) const; Api::SendOptions options = {}) const;
void setRecentInlineBotsInRows(int32 bots); void setRecentInlineBotsInRows(int32 bots);
void setSendMenuType(Fn<SendMenu::Type()> &&callback); void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
void rowsUpdated(); void rowsUpdated();
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const; rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
@ -155,7 +155,7 @@ private:
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient; const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
StickerPremiumMark _premiumMark; StickerPremiumMark _premiumMark;
Fn<SendMenu::Type()> _sendMenuType; Fn<SendMenu::Details()> _sendMenuDetails;
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen; rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen; rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
@ -835,8 +835,9 @@ bool FieldAutocomplete::chooseSelected(ChooseMethod method) const {
return _inner->chooseSelected(method); return _inner->chooseSelected(method);
} }
void FieldAutocomplete::setSendMenuType(Fn<SendMenu::Type()> &&callback) { void FieldAutocomplete::setSendMenuDetails(
_inner->setSendMenuType(std::move(callback)); Fn<SendMenu::Details()> &&callback) {
_inner->setSendMenuDetails(std::move(callback));
} }
bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
@ -890,7 +891,7 @@ FieldAutocomplete::Inner::Inner(
_st.pathBg, _st.pathBg,
_st.pathFg, _st.pathFg,
[=] { update(); })) [=] { update(); }))
, _premiumMark(_session) , _premiumMark(_session, st::stickersPremiumLock)
, _previewTimer([=] { showPreview(); }) { , _previewTimer([=] { showPreview(); }) {
_session->downloaderTaskFinished( _session->downloaderTaskFinished(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
@ -1364,24 +1365,22 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
return; return;
} }
const auto index = _sel; const auto index = _sel;
const auto type = _sendMenuType const auto details = _sendMenuDetails
? _sendMenuType() ? _sendMenuDetails()
: SendMenu::Type::Disabled; : SendMenu::Details();
const auto method = FieldAutocomplete::ChooseMethod::ByClick; const auto method = FieldAutocomplete::ChooseMethod::ByClick;
_menu = base::make_unique_q<Ui::PopupMenu>( _menu = base::make_unique_q<Ui::PopupMenu>(
this, this,
st::popupMenuWithIcons); st::popupMenuWithIcons);
const auto send = [=](Api::SendOptions options) { const auto send = crl::guard(this, [=](Api::SendOptions options) {
chooseAtIndex(method, index, options); chooseAtIndex(method, index, options);
}; });
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
_menu, _menu,
type, _show,
SendMenu::DefaultSilentCallback(send), details,
SendMenu::DefaultScheduleCallback(_show, type, send), SendMenu::DefaultCallback(_show, send));
SendMenu::DefaultWhenOnlineCallback(send));
if (!_menu->empty()) { if (!_menu->empty()) {
_menu->popup(QCursor::pos()); _menu->popup(QCursor::pos());
} }
@ -1604,9 +1603,9 @@ void FieldAutocomplete::Inner::showPreview() {
} }
} }
void FieldAutocomplete::Inner::setSendMenuType( void FieldAutocomplete::Inner::setSendMenuDetails(
Fn<SendMenu::Type()> &&callback) { Fn<SendMenu::Details()> &&callback) {
_sendMenuType = std::move(callback); _sendMenuDetails = std::move(callback);
} }
auto FieldAutocomplete::Inner::mentionChosen() const auto FieldAutocomplete::Inner::mentionChosen() const

View file

@ -42,7 +42,7 @@ class DocumentMedia;
} // namespace Data } // namespace Data
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace ChatHelpers { namespace ChatHelpers {
@ -123,7 +123,7 @@ public:
void setModerateKeyActivateCallback(Fn<bool(int)> callback) { void setModerateKeyActivateCallback(Fn<bool(int)> callback) {
_moderateKeyActivateCallback = std::move(callback); _moderateKeyActivateCallback = std::move(callback);
} }
void setSendMenuType(Fn<SendMenu::Type()> &&callback); void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
void hideFast(); void hideFast();
void showAnimated(); void showAnimated();

View file

@ -387,22 +387,31 @@ void GifsListWidget::mousePressEvent(QMouseEvent *e) {
} }
base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu( base::unique_qptr<Ui::PopupMenu> GifsListWidget::fillContextMenu(
SendMenu::Type type) { const SendMenu::Details &details) {
if (_selected < 0 || _pressed >= 0) { if (_selected < 0 || _pressed >= 0) {
return nullptr; return nullptr;
} }
auto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu); auto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu);
const auto send = [=, selected = _selected](Api::SendOptions options) { const auto selected = _selected;
const auto send = crl::guard(this, [=](Api::SendOptions options) {
selectInlineResult(selected, options, true); selectInlineResult(selected, options, true);
}; });
const auto item = _mosaic.maybeItemAt(_selected);
const auto isInlineResult = !item->getPhoto()
&& !item->getDocument()
&& item->getResult();
const auto icons = &st().icons; const auto icons = &st().icons;
auto copyDetails = details;
if (isInlineResult) {
// inline results don't have effects
copyDetails.effectAllowed = false;
}
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
menu, menu,
type, _show,
SendMenu::DefaultSilentCallback(send), copyDetails,
SendMenu::DefaultScheduleCallback(_show, type, send), SendMenu::DefaultCallback(_show, send),
SendMenu::DefaultWhenOnlineCallback(send),
icons); icons);
if (const auto item = _mosaic.maybeItemAt(_selected)) { if (const auto item = _mosaic.maybeItemAt(_selected)) {

View file

@ -40,7 +40,7 @@ class SessionController;
} // namespace Window } // namespace Window
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace Data { namespace Data {
@ -102,7 +102,7 @@ public:
rpl::producer<> cancelRequests() const; rpl::producer<> cancelRequests() const;
base::unique_qptr<Ui::PopupMenu> fillContextMenu( base::unique_qptr<Ui::PopupMenu> fillContextMenu(
SendMenu::Type type) override; const SendMenu::Details &details) override;
~GifsListWidget(); ~GifsListWidget();

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/shortcuts.h" #include "core/shortcuts.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
@ -60,60 +61,43 @@ constexpr auto kTypesDuration = 4 * crl::time(1000);
// For mention / custom emoji tags save and validate selfId, // For mention / custom emoji tags save and validate selfId,
// ignore tags for different users. // ignore tags for different users.
class FieldTagMimeProcessor final { [[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(
public: not_null<Main::Session*> session,
FieldTagMimeProcessor( Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
not_null<Main::Session*> _session, return [=](QStringView mimeTag) {
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji); const auto id = session->userId().bare;
auto all = TextUtilities::SplitTags(mimeTag);
QString operator()(QStringView mimeTag); auto premiumSkipped = (DocumentData*)nullptr;
for (auto i = all.begin(); i != all.end();) {
private: const auto tag = *i;
const not_null<Main::Session*> _session; if (TextUtilities::IsMentionLink(tag)
const Fn<bool(not_null<DocumentData*>)> _allowPremiumEmoji; && TextUtilities::MentionNameDataToFields(tag).selfId != id) {
};
FieldTagMimeProcessor::FieldTagMimeProcessor(
not_null<Main::Session*> session,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji)
: _session(session)
, _allowPremiumEmoji(allowPremiumEmoji) {
}
QString FieldTagMimeProcessor::operator()(QStringView mimeTag) {
const auto id = _session->userId().bare;
auto all = TextUtilities::SplitTags(mimeTag);
auto premiumSkipped = (DocumentData*)nullptr;
for (auto i = all.begin(); i != all.end();) {
const auto tag = *i;
if (TextUtilities::IsMentionLink(tag)
&& TextUtilities::MentionNameDataToFields(tag).selfId != id) {
i = all.erase(i);
continue;
} else if (Ui::InputField::IsCustomEmojiLink(tag)) {
const auto data = Ui::InputField::CustomEmojiEntityData(tag);
const auto emoji = Data::ParseCustomEmojiData(data);
if (!emoji) {
i = all.erase(i); i = all.erase(i);
continue; continue;
} else if (!_session->premium()) { } else if (Ui::InputField::IsCustomEmojiLink(tag)) {
const auto document = _session->data().document(emoji); const auto data = Ui::InputField::CustomEmojiEntityData(tag);
if (document->isPremiumEmoji()) { const auto emoji = Data::ParseCustomEmojiData(data);
if (!_allowPremiumEmoji if (!emoji) {
|| premiumSkipped i = all.erase(i);
|| !_session->premiumPossible() continue;
|| !_allowPremiumEmoji(document)) { } else if (!session->premium()) {
premiumSkipped = document; const auto document = session->data().document(emoji);
i = all.erase(i); if (document->isPremiumEmoji()) {
continue; if (!allowPremiumEmoji
|| premiumSkipped
|| !session->premiumPossible()
|| !allowPremiumEmoji(document)) {
premiumSkipped = document;
i = all.erase(i);
continue;
}
} }
} }
} }
++i;
} }
++i; return TextUtilities::JoinTag(all);
} };
return TextUtilities::JoinTag(all);
} }
//bool ValidateUrl(const QString &value) { //bool ValidateUrl(const QString &value) {
@ -132,7 +116,8 @@ void EditLinkBox(
const QString &startText, const QString &startText,
const QString &startLink, const QString &startLink,
Fn<void(QString, QString)> callback, Fn<void(QString, QString)> callback,
const style::InputField *fieldStyle) { const style::InputField *fieldStyle,
Fn<QString(QString)> validate) {
Expects(callback != nullptr); Expects(callback != nullptr);
const auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField; const auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField;
@ -177,7 +162,7 @@ void EditLinkBox(
const auto submit = [=] { const auto submit = [=] {
const auto linkText = text->getLastText(); const auto linkText = text->getLastText();
const auto linkUrl = qthelp::validate_url(url->getLastText()); const auto linkUrl = validate(url->getLastText());
if (linkText.isEmpty()) { if (linkText.isEmpty()) {
text->showError(); text->showError();
return; return;
@ -329,7 +314,8 @@ Fn<bool(
text, text,
link, link,
std::move(callback), std::move(callback),
fieldStyle)); fieldStyle,
qthelp::validate_url));
return true; return true;
}; };
} }
@ -352,7 +338,7 @@ void InitMessageFieldHandlers(
field->setInstantReplaces(Ui::InstantReplaces::Default()); field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled( field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue()); Core::App().settings().replaceEmojiValue());
field->setMarkdownReplacesEnabled(rpl::single(true)); field->setMarkdownReplacesEnabled(true);
if (show) { if (show) {
field->setEditLinkCallback( field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, fieldStyle)); DefaultEditLinkCallback(show, field, fieldStyle));
@ -360,6 +346,88 @@ void InitMessageFieldHandlers(
} }
} }
[[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
return url.startsWith(u"t.me/"_q) || url.startsWith(u"https://t.me/"_q);
}
[[nodiscard]] Fn<bool(
Ui::InputField::EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action)> FactcheckEditLinkCallback(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field) {
const auto weak = Ui::MakeWeak(field);
return [=](
EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action) {
const auto validate = [=](QString url) {
if (IsGoodFactcheckUrl(url)) {
const auto start = u"https://"_q;
return url.startsWith(start) ? url : (start + url);
}
show->showToast(
tr::lng_factcheck_links(tr::now, Ui::Text::RichLangValue));
return QString();
};
if (action == EditLinkAction::Check) {
return IsGoodFactcheckUrl(link);
}
auto callback = [=](const QString &text, const QString &link) {
if (const auto strong = weak.data()) {
strong->commitMarkdownLinkEdit(selection, text, link);
}
};
show->showBox(Box(
EditLinkBox,
show,
text,
link,
std::move(callback),
nullptr,
validate));
return true;
};
}
Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
std::shared_ptr<Main::SessionShow> show) {
Expects(show != nullptr);
return [=](not_null<Ui::InputField*> field) {
field->setTagMimeProcessor([](QStringView mimeTag) {
using Field = Ui::InputField;
auto all = TextUtilities::SplitTags(mimeTag);
for (auto i = all.begin(); i != all.end();) {
const auto tag = *i;
if (tag != Field::kTagBold
&& tag != Field::kTagItalic
&& (!Field::IsValidMarkdownLink(mimeTag)
|| TextUtilities::IsMentionLink(mimeTag))) {
i = all.erase(i);
continue;
}
++i;
}
return TextUtilities::JoinTag(all);
});
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
field->setMarkdownReplacesEnabled(rpl::single(
Ui::MarkdownEnabledState{
Ui::MarkdownEnabled{
{ Ui::InputField::kTagBold, Ui::InputField::kTagItalic }
}
}
));
field->setEditLinkCallback(FactcheckEditLinkCallback(show, field));
InitSpellchecker(show, field);
};
}
void InitMessageFieldHandlers( void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller, not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field, not_null<Ui::InputField*> field,
@ -431,10 +499,7 @@ bool HasSendText(not_null<const Ui::InputField*> field) {
const auto &text = field->getTextWithTags().text; const auto &text = field->getTextWithTags().text;
for (const auto &ch : text) { for (const auto &ch : text) {
const auto code = ch.unicode(); const auto code = ch.unicode();
if (code != ' ' if (!IsTrimmed(ch) && !IsReplacedBySpace(code)) {
&& code != '\n'
&& code != '\r'
&& !IsReplacedBySpace(code)) {
return true; return true;
} }
} }
@ -732,7 +797,8 @@ void MessageLinksParser::parse() {
|| (tag == Ui::InputField::kTagUnderline) || (tag == Ui::InputField::kTagUnderline)
|| (tag == Ui::InputField::kTagStrikeOut) || (tag == Ui::InputField::kTagStrikeOut)
|| (tag == Ui::InputField::kTagSpoiler) || (tag == Ui::InputField::kTagSpoiler)
|| (tag == Ui::InputField::kTagBlockquote); || (tag == Ui::InputField::kTagBlockquote)
|| (tag == Ui::InputField::kTagBlockquoteCollapsed);
}; };
_ranges.clear(); _ranges.clear();

View file

@ -77,6 +77,9 @@ void InitSpellchecker(
not_null<Ui::InputField*> field, not_null<Ui::InputField*> field,
bool skipDictionariesManager = false); bool skipDictionariesManager = false);
[[nodiscard]] Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
std::shared_ptr<Main::SessionShow> show);
bool HasSendText(not_null<const Ui::InputField*> field); bool HasSendText(not_null<const Ui::InputField*> field);
void InitMessageFieldFade( void InitMessageFieldFade(

View file

@ -269,10 +269,10 @@ std::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(
not_null<DocumentData*> document, not_null<DocumentData*> document,
QByteArray data, QByteArray data,
QString filepath, QString filepath,
bool premium) { EffectType type) {
// Shortened copy from stickers_lottie module. // Shortened copy from stickers_lottie module.
const auto baseKey = document->bigFileBaseCacheKey(); const auto baseKey = document->bigFileBaseCacheKey();
const auto tag = uint8(0); const auto tag = uint8(type);
const auto keyShift = ((tag << 4) & 0xF0) const auto keyShift = ((tag << 4) & 0xF0)
| (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F); | (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F);
const auto key = Storage::Cache::Key{ const auto key = Storage::Cache::Key{
@ -292,19 +292,24 @@ std::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(
std::move(data)); std::move(data));
}); });
}; };
const auto size = premium const auto size = (type == EffectType::PremiumSticker)
? HistoryView::Sticker::PremiumEffectSize(document) ? HistoryView::Sticker::PremiumEffectSize(document)
: HistoryView::Sticker::EmojiEffectSize(); : (type == EffectType::EmojiInteraction)
? HistoryView::Sticker::EmojiEffectSize()
: HistoryView::Sticker::MessageEffectSize();
const auto request = Lottie::FrameRequest{ const auto request = Lottie::FrameRequest{
size * style::DevicePixelRatio(), size * style::DevicePixelRatio(),
}; };
auto &weakProvider = _sharedProviders[document]; auto &weakProvider = _sharedProviders[{ document, type }];
auto shared = [&] { auto shared = [&] {
if (const auto result = weakProvider.lock()) { if (const auto result = weakProvider.lock()) {
return result; return result;
} }
const auto count = (type == EffectType::PremiumSticker)
? kPremiumCachesCount
: kEmojiCachesCount;
const auto result = Lottie::SinglePlayer::SharedProvider( const auto result = Lottie::SinglePlayer::SharedProvider(
premium ? kPremiumCachesCount : kEmojiCachesCount, count,
get, get,
put, put,
Lottie::ReadContent(data, filepath), Lottie::ReadContent(data, filepath),

View file

@ -50,6 +50,12 @@ struct LargeEmojiImage {
[[nodiscard]] static QSize Size(); [[nodiscard]] static QSize Size();
}; };
enum class EffectType : uint8 {
EmojiInteraction,
PremiumSticker,
MessageEffect,
};
class EmojiPack final { class EmojiPack final {
public: public:
using ViewElement = HistoryView::Element; using ViewElement = HistoryView::Element;
@ -95,11 +101,23 @@ public:
not_null<DocumentData*> document, not_null<DocumentData*> document,
QByteArray data, QByteArray data,
QString filepath, QString filepath,
bool premium); EffectType type);
private: private:
class ImageLoader; class ImageLoader;
struct ProviderKey {
not_null<DocumentData*> document;
Stickers::EffectType type = {};
friend inline auto operator<=>(
const ProviderKey &,
const ProviderKey &) = default;
friend inline bool operator==(
const ProviderKey &,
const ProviderKey &) = default;
};
void refresh(); void refresh();
void refreshDelayed(); void refreshDelayed();
void refreshAnimations(); void refreshAnimations();
@ -135,7 +153,7 @@ private:
mtpRequestId _animationsRequestId = 0; mtpRequestId _animationsRequestId = 0;
base::flat_map< base::flat_map<
not_null<DocumentData*>, ProviderKey,
std::weak_ptr<Lottie::FrameProvider>> _sharedProviders; std::weak_ptr<Lottie::FrameProvider>> _sharedProviders;
rpl::event_stream<> _refreshed; rpl::event_stream<> _refreshed;

View file

@ -195,8 +195,10 @@ StickersListWidget::StickersListWidget(
, _overBg(st::roundRadiusLarge, st().overBg) , _overBg(st::roundRadiusLarge, st().overBg)
, _api(&session().mtp()) , _api(&session().mtp())
, _localSetsManager(std::make_unique<LocalStickersManager>(&session())) , _localSetsManager(std::make_unique<LocalStickersManager>(&session()))
, _customRecentIds(std::move(descriptor.customRecentList))
, _section(Section::Stickers) , _section(Section::Stickers)
, _isMasks(_mode == Mode::Masks) , _isMasks(_mode == Mode::Masks)
, _isEffects(_mode == Mode::MessageEffects)
, _updateItemsTimer([=] { updateItems(); }) , _updateItemsTimer([=] { updateItems(); })
, _updateSetsTimer([=] { updateSets(); }) , _updateSetsTimer([=] { updateSets(); })
, _trendingAddBgOver( , _trendingAddBgOver(
@ -223,12 +225,16 @@ StickersListWidget::StickersListWidget(
, _installedWidth(st::stickersTrendingInstalled.font->width(_installedText)) , _installedWidth(st::stickersTrendingInstalled.font->width(_installedText))
, _settings(this, tr::lng_stickers_you_have(tr::now)) , _settings(this, tr::lng_stickers_you_have(tr::now))
, _previewTimer([=] { showPreview(); }) , _previewTimer([=] { showPreview(); })
, _premiumMark(std::make_unique<StickerPremiumMark>(&session())) , _premiumMark(std::make_unique<StickerPremiumMark>(
&session(),
st::stickersPremiumLock))
, _searchRequestTimer([=] { sendSearchRequest(); }) { , _searchRequestTimer([=] { sendSearchRequest(); }) {
setMouseTracking(true); setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent); if (st().bg->c.alpha() > 0) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
if (!_isMasks) { if (!_isMasks && !_isEffects) {
setupSearch(); setupSearch();
} }
@ -260,23 +266,30 @@ StickersListWidget::StickersListWidget(
refreshStickers(); refreshStickers();
}, lifetime()); }, lifetime());
session().data().stickers().recentUpdated( if (!_isEffects) {
_isMasks ? Data::StickersType::Masks : Data::StickersType::Stickers session().data().stickers().recentUpdated(_isMasks
) | rpl::start_with_next([=] { ? Data::StickersType::Masks
refreshRecent(); : Data::StickersType::Stickers
}, lifetime()); ) | rpl::start_with_next([=] {
refreshRecent();
}, lifetime());
}
positionValue( positionValue(
) | rpl::skip(1) | rpl::map_to( ) | rpl::skip(1) | rpl::map_to(
TabbedSelector::Action::Update TabbedSelector::Action::Update
) | rpl::start_to_stream(_choosingUpdated, lifetime()); ) | rpl::start_to_stream(_choosingUpdated, lifetime());
rpl::merge( if (_isEffects) {
Data::AmPremiumValue(&session()) | rpl::to_empty,
session().api().premium().cloudSetUpdated()
) | rpl::start_with_next([=] {
refreshStickers(); refreshStickers();
}, lifetime()); } else {
rpl::merge(
Data::AmPremiumValue(&session()) | rpl::to_empty,
session().api().premium().cloudSetUpdated()
) | rpl::start_with_next([=] {
refreshStickers();
}, lifetime());
}
} }
rpl::producer<FileChosen> StickersListWidget::chosen() const { rpl::producer<FileChosen> StickersListWidget::chosen() const {
@ -504,11 +517,14 @@ StickersListWidget::SectionInfo StickersListWidget::sectionInfoByOffset(int yOff
} }
int StickersListWidget::countDesiredHeight(int newWidth) { int StickersListWidget::countDesiredHeight(int newWidth) {
if (newWidth <= st::stickerPanWidthMin) { const auto minSize = _isEffects
? st::stickerEffectWidthMin
: st::stickerPanWidthMin;
if (newWidth < 2 * minSize) {
return 0; return 0;
} }
auto availableWidth = newWidth - (st::stickerPanPadding - st().margin.left()); auto availableWidth = newWidth - (st::stickerPanPadding - st().margin.left());
auto columnCount = availableWidth / st::stickerPanWidthMin; auto columnCount = availableWidth / minSize;
auto singleWidth = availableWidth / columnCount; auto singleWidth = availableWidth / columnCount;
auto fullWidth = (st().margin.left() + newWidth + st::emojiScroll.width); auto fullWidth = (st().margin.left() + newWidth + st::emojiScroll.width);
auto rowsRight = (fullWidth - columnCount * singleWidth) / 2; auto rowsRight = (fullWidth - columnCount * singleWidth) / 2;
@ -534,12 +550,12 @@ int StickersListWidget::countDesiredHeight(int newWidth) {
const auto minimalLastHeight = (_section == Section::Stickers) const auto minimalLastHeight = (_section == Section::Stickers)
? minimalHeight ? minimalHeight
: 0; : 0;
return qMax(minimalHeight, countResult(minimalLastHeight)) const auto result = qMax(minimalHeight, countResult(minimalLastHeight));
+ st::stickerPanPadding; return result ? (result + st::stickerPanPadding) : 0;
} }
void StickersListWidget::sendSearchRequest() { void StickersListWidget::sendSearchRequest() {
if (_searchRequestId || _searchNextQuery.isEmpty()) { if (_searchRequestId || _searchNextQuery.isEmpty() || _isEffects) {
return; return;
} }
@ -548,14 +564,12 @@ void StickersListWidget::sendSearchRequest() {
auto it = _searchCache.find(_searchQuery); auto it = _searchCache.find(_searchQuery);
if (it != _searchCache.cend()) { if (it != _searchCache.cend()) {
_search->setLoading(false); toggleSearchLoading(false);
return; return;
} }
toggleSearchLoading(true);
_search->setLoading(true);
if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) { if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) {
_search->setLoading(false); toggleSearchLoading(false);
_searchRequestId = 0; _searchRequestId = 0;
_searchCache.emplace(_searchQuery, std::vector<uint64>()); _searchCache.emplace(_searchQuery, std::vector<uint64>());
showSearchResults(); showSearchResults();
@ -571,7 +585,7 @@ void StickersListWidget::sendSearchRequest() {
searchResultsDone(result); searchResultsDone(result);
}).fail([=] { }).fail([=] {
// show error? // show error?
_search->setLoading(false); toggleSearchLoading(false);
_searchRequestId = 0; _searchRequestId = 0;
}).handleAllErrors().send(); }).handleAllErrors().send();
} }
@ -585,7 +599,10 @@ void StickersListWidget::searchForSets(
return; return;
} }
if (query == Ui::PremiumGroupFakeEmoticon()) { _filterStickersCornerEmoji.clear();
if (_isEffects) {
filterEffectsByEmoji(std::move(emoji));
} else if (query == Ui::PremiumGroupFakeEmoticon()) {
_filteredStickers = session().data().stickers().getPremiumList(0); _filteredStickers = session().data().stickers().getPremiumList(0);
} else { } else {
_filteredStickers = session().data().stickers().getListByEmoji( _filteredStickers = session().data().stickers().getListByEmoji(
@ -594,7 +611,7 @@ void StickersListWidget::searchForSets(
true); true);
} }
if (_searchQuery != cleaned) { if (_searchQuery != cleaned) {
_search->setLoading(false); toggleSearchLoading(false);
if (const auto requestId = base::take(_searchRequestId)) { if (const auto requestId = base::take(_searchRequestId)) {
_api.request(requestId).cancel(); _api.request(requestId).cancel();
} }
@ -610,13 +627,14 @@ void StickersListWidget::searchForSets(
} }
void StickersListWidget::cancelSetsSearch() { void StickersListWidget::cancelSetsSearch() {
_search->setLoading(false); toggleSearchLoading(false);
if (const auto requestId = base::take(_searchRequestId)) { if (const auto requestId = base::take(_searchRequestId)) {
_api.request(requestId).cancel(); _api.request(requestId).cancel();
} }
_searchRequestTimer.cancel(); _searchRequestTimer.cancel();
_searchQuery = _searchNextQuery = QString(); _searchQuery = _searchNextQuery = QString();
_filteredStickers.clear(); _filteredStickers.clear();
_filterStickersCornerEmoji.clear();
_searchCache.clear(); _searchCache.clear();
refreshSearchRows(nullptr); refreshSearchRows(nullptr);
} }
@ -647,8 +665,9 @@ void StickersListWidget::refreshSearchRows(
}); });
fillFilteredStickersRow(); fillFilteredStickersRow();
fillLocalSearchRows(_searchNextQuery); if (!_isEffects) {
fillLocalSearchRows(_searchNextQuery);
}
if (!cloudSets && _searchNextQuery.isEmpty()) { if (!cloudSets && _searchNextQuery.isEmpty()) {
showStickerSet(!_mySets.empty() showStickerSet(!_mySets.empty()
? _mySets[0].id ? _mySets[0].id
@ -657,17 +676,21 @@ void StickersListWidget::refreshSearchRows(
} }
setSection(Section::Search); setSection(Section::Search);
if (cloudSets) { if (!_isEffects && cloudSets) {
fillCloudSearchRows(*cloudSets); fillCloudSearchRows(*cloudSets);
} }
refreshIcons(ValidateIconAnimations::Scroll); refreshIcons(ValidateIconAnimations::Scroll);
_lastMousePosition = QCursor::pos(); _lastMousePosition = QCursor::pos();
resizeToWidth(width()); resizeToWidth(width());
_recentShownCount = _filteredStickers.size();
updateSelected(); updateSelected();
} }
rpl::producer<int> StickersListWidget::recentShownCount() const {
return _recentShownCount.value();
}
void StickersListWidget::fillLocalSearchRows(const QString &query) { void StickersListWidget::fillLocalSearchRows(const QString &query) {
const auto searchWordsList = TextUtilities::PrepareSearchWords(query); const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
if (searchWordsList.isEmpty()) { if (searchWordsList.isEmpty()) {
@ -727,7 +750,7 @@ void StickersListWidget::fillFilteredStickersRow() {
SearchEmojiSectionSetId(), SearchEmojiSectionSetId(),
nullptr, nullptr,
Data::StickersSetFlag::Special, Data::StickersSetFlag::Special,
QString(), // title _isEffects ? tr::lng_effect_stickers_title(tr::now) : QString(),
QString(), // shortName QString(), // shortName
_filteredStickers.size(), _filteredStickers.size(),
false, // externalLayout false, // externalLayout
@ -750,6 +773,12 @@ void StickersListWidget::addSearchRow(not_null<StickersSet*> set) {
std::move(elements)); std::move(elements));
} }
void StickersListWidget::toggleSearchLoading(bool loading) {
if (_search) {
_search->setLoading(loading);
}
}
void StickersListWidget::takeHeavyData( void StickersListWidget::takeHeavyData(
std::vector<Set> &to, std::vector<Set> &to,
std::vector<Set> &from) { std::vector<Set> &from) {
@ -831,7 +860,7 @@ auto StickersListWidget::shownSets() -> std::vector<Set> & {
void StickersListWidget::searchResultsDone( void StickersListWidget::searchResultsDone(
const MTPmessages_FoundStickerSets &result) { const MTPmessages_FoundStickerSets &result) {
_search->setLoading(false); toggleSearchLoading(false);
_searchRequestId = 0; _searchRequestId = 0;
if (result.type() == mtpc_messages_foundStickerSetsNotModified) { if (result.type() == mtpc_messages_foundStickerSetsNotModified) {
@ -878,7 +907,9 @@ QRect StickersListWidget::stickerRect(int section, int sel) {
void StickersListWidget::paintEvent(QPaintEvent *e) { void StickersListWidget::paintEvent(QPaintEvent *e) {
Painter p(this); Painter p(this);
auto clip = e->rect(); auto clip = e->rect();
p.fillRect(clip, st().bg); if (st().bg->c.alpha() > 0) {
p.fillRect(clip, st().bg);
}
paintStickers(p, clip); paintStickers(p, clip);
} }
@ -892,6 +923,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
toColumn = _columnCount - toColumn; toColumn = _columnCount - toColumn;
} }
_paintAsPremium = session().premium();
_pathGradient->startFrame(0, width(), width() / 2); _pathGradient->startFrame(0, width(), width() / 2);
auto &sets = shownSets(); auto &sets = shownSets();
@ -1465,7 +1497,26 @@ void StickersListWidget::paintSticker(
p.setOpacity(1.); p.setOpacity(1.);
} }
if (premium) { auto cornerPainted = false;
const auto corner = (set.id == Data::Stickers::RecentSetId)
? &_cornerEmoji
: (set.id == SearchEmojiSectionSetId())
? &_filterStickersCornerEmoji
: nullptr;
if (corner && !corner->empty() && _paintAsPremium) {
Assert(index < corner->size());
if (const auto emoji = (*corner)[index]) {
const auto size = Ui::Emoji::GetSizeNormal();
const auto ratio = style::DevicePixelRatio();
const auto radius = st::roundRadiusSmall;
const auto position = pos
+ QPoint(_singleSize.width(), _singleSize.height())
- QPoint(size / ratio + radius, size / ratio + radius);
Ui::Emoji::Draw(p, emoji, size, position.x(), position.y());
cornerPainted = true;
}
}
if (!cornerPainted && premium) {
_premiumMark->paint( _premiumMark->paint(
p, p,
lottieFrame, lottieFrame,
@ -1640,7 +1691,7 @@ void StickersListWidget::showStickerSetBox(not_null<DocumentData*> document) {
} }
base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu( base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
SendMenu::Type type) { const SendMenu::Details &details) {
auto selected = _selected; auto selected = _selected;
auto &sets = shownSets(); auto &sets = shownSets();
if (v::is_null(selected) || !v::is_null(_pressed)) { if (v::is_null(selected) || !v::is_null(_pressed)) {
@ -1659,7 +1710,7 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
auto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu); auto menu = base::make_unique_q<Ui::PopupMenu>(this, st().menu);
const auto document = set.stickers[sticker->index].document; const auto document = set.stickers[sticker->index].document;
const auto send = [=](Api::SendOptions options) { const auto send = crl::guard(this, [=](Api::SendOptions options) {
_chosen.fire({ _chosen.fire({
.document = document, .document = document,
.options = options, .options = options,
@ -1667,14 +1718,13 @@ base::unique_qptr<Ui::PopupMenu> StickersListWidget::fillContextMenu(
? Ui::MessageSendingAnimationFrom() ? Ui::MessageSendingAnimationFrom()
: messageSentAnimationInfo(section, index, document), : messageSentAnimationInfo(section, index, document),
}); });
}; });
const auto icons = &st().icons; const auto icons = &st().icons;
SendMenu::FillSendMenu( SendMenu::FillSendMenu(
menu, menu,
type, _show,
SendMenu::DefaultSilentCallback(send), details,
SendMenu::DefaultScheduleCallback(_show, type, send), SendMenu::DefaultCallback(_show, send),
SendMenu::DefaultWhenOnlineCallback(send),
icons); icons);
const auto show = _show; const auto show = _show;
@ -1948,6 +1998,11 @@ void StickersListWidget::setSection(Section section) {
} }
clearHeavyData(); clearHeavyData();
_section = section; _section = section;
_recentShownCount = (section == Section::Search)
? _filteredStickers.size()
: _mySets.empty()
? 0
: _mySets.front().stickers.size();
} }
void StickersListWidget::clearHeavyData() { void StickersListWidget::clearHeavyData() {
@ -1959,10 +2014,13 @@ void StickersListWidget::clearHeavyData() {
void StickersListWidget::refreshStickers() { void StickersListWidget::refreshStickers() {
clearSelection(); clearSelection();
refreshMySets(); if (_isEffects) {
refreshFeaturedSets(); refreshEffects();
refreshSearchSets(); } else {
refreshMySets();
refreshFeaturedSets();
refreshSearchSets();
}
resizeToWidth(width()); resizeToWidth(width());
if (_footer) { if (_footer) {
@ -1977,6 +2035,13 @@ void StickersListWidget::refreshStickers() {
visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom()); visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom());
} }
void StickersListWidget::refreshEffects() {
auto wasSets = base::take(_mySets);
_mySets.reserve(1);
refreshRecentStickers(false);
takeHeavyData(_mySets, wasSets);
}
void StickersListWidget::refreshMySets() { void StickersListWidget::refreshMySets() {
auto wasSets = base::take(_mySets); auto wasSets = base::take(_mySets);
_favedStickersMap.clear(); _favedStickersMap.clear();
@ -2138,7 +2203,26 @@ void StickersListWidget::refreshRecent() {
} }
} }
auto StickersListWidget::collectCustomRecents() -> std::vector<Sticker> {
_custom.clear();
_cornerEmoji.clear();
auto result = std::vector<Sticker>();
result.reserve(_customRecentIds.size());
for (const auto &descriptor : _customRecentIds) {
if (const auto document = descriptor.document; document->sticker()) {
result.push_back(Sticker{ document });
_custom.push_back(false);
_cornerEmoji.push_back(Ui::Emoji::Find(descriptor.cornerEmoji));
}
}
return result;
}
auto StickersListWidget::collectRecentStickers() -> std::vector<Sticker> { auto StickersListWidget::collectRecentStickers() -> std::vector<Sticker> {
if (_isEffects) {
return collectCustomRecents();
}
_custom.clear(); _custom.clear();
auto result = std::vector<Sticker>(); auto result = std::vector<Sticker>();
@ -2202,6 +2286,9 @@ void StickersListWidget::refreshRecentStickers(bool performResize) {
clearSelection(); clearSelection();
auto recentPack = collectRecentStickers(); auto recentPack = collectRecentStickers();
if (_section == Section::Stickers) {
_recentShownCount = recentPack.size();
}
auto recentIt = std::find_if(_mySets.begin(), _mySets.end(), [](auto &set) { auto recentIt = std::find_if(_mySets.begin(), _mySets.end(), [](auto &set) {
return set.id == Data::Stickers::RecentSetId; return set.id == Data::Stickers::RecentSetId;
}); });
@ -2212,7 +2299,9 @@ void StickersListWidget::refreshRecentStickers(bool performResize) {
Data::Stickers::RecentSetId, Data::Stickers::RecentSetId,
nullptr, nullptr,
(SetFlag::Official | SetFlag::Special), (SetFlag::Official | SetFlag::Special),
tr::lng_recent_stickers(tr::now), (_isEffects
? tr::lng_effect_stickers_title(tr::now)
: tr::lng_recent_stickers(tr::now)),
shortName, shortName,
recentPack.size(), recentPack.size(),
externalLayout, externalLayout,
@ -2463,7 +2552,9 @@ void StickersListWidget::updateSelected() {
} }
bool StickersListWidget::setHasTitle(const Set &set) const { bool StickersListWidget::setHasTitle(const Set &set) const {
if (set.id == Data::Stickers::FavedSetId if (_isEffects) {
return true;
} else if (set.id == Data::Stickers::FavedSetId
|| set.id == SearchEmojiSectionSetId()) { || set.id == SearchEmojiSectionSetId()) {
return false; return false;
} else if (set.id == Data::Stickers::RecentSetId) { } else if (set.id == Data::Stickers::RecentSetId) {
@ -2550,9 +2641,10 @@ void StickersListWidget::showStickerSet(uint64 setId) {
const auto guard = gsl::finally([&] { _showingSetById = false; }); const auto guard = gsl::finally([&] { _showingSetById = false; });
clearSelection(); clearSelection();
if (_search if (!_searchQuery.isEmpty() || !_searchNextQuery.isEmpty()) {
&& (!_searchQuery.isEmpty() || !_searchNextQuery.isEmpty())) { if (_search) {
_search->cancel(); _search->cancel();
}
cancelSetsSearch(); cancelSetsSearch();
} }
@ -2655,14 +2747,18 @@ void StickersListWidget::setupSearch() {
? TabbedSearchType::Greeting ? TabbedSearchType::Greeting
: TabbedSearchType::Stickers; : TabbedSearchType::Stickers;
_search = MakeSearch(this, st(), [=](std::vector<QString> &&query) { _search = MakeSearch(this, st(), [=](std::vector<QString> &&query) {
auto set = base::flat_set<EmojiPtr>(); applySearchQuery(std::move(query));
auto text = ranges::accumulate(query, QString(), []( }, session, type);
}
void StickersListWidget::applySearchQuery(std::vector<QString> &&query) {
auto set = base::flat_set<EmojiPtr>();
auto text = ranges::accumulate(query, QString(), [](
QString a, QString a,
QString b) { QString b) {
return a.isEmpty() ? b : (a + ' ' + b); return a.isEmpty() ? b : (a + ' ' + b);
}); });
searchForSets(std::move(text), SearchEmoji(query, set)); searchForSets(std::move(text), SearchEmoji(query, set));
}, session, type);
} }
void StickersListWidget::displaySet(uint64 setId) { void StickersListWidget::displaySet(uint64 setId) {
@ -2737,6 +2833,32 @@ bool StickersListWidget::mySetsEmpty() const {
return _mySets.empty(); return _mySets.empty();
} }
void StickersListWidget::filterEffectsByEmoji(
const std::vector<EmojiPtr> &emoji) {
_filteredStickers.clear();
_filterStickersCornerEmoji.clear();
if (_mySets.empty()
|| _mySets.front().id != Data::Stickers::RecentSetId
|| _mySets.front().stickers.empty()) {
return;
}
const auto &list = _mySets.front().stickers;
auto all = base::flat_set<EmojiPtr>();
for (const auto &one : emoji) {
all.emplace(one->original());
}
const auto count = int(list.size());
_filteredStickers.reserve(count);
_filterStickersCornerEmoji.reserve(count);
for (auto i = 0; i != count; ++i) {
Assert(i < _cornerEmoji.size());
if (all.contains(_cornerEmoji[i])) {
_filteredStickers.push_back(list[i].document);
_filterStickersCornerEmoji.push_back(_cornerEmoji[i]);
}
}
}
StickersListWidget::~StickersListWidget() = default; StickersListWidget::~StickersListWidget() = default;
object_ptr<Ui::BoxContent> MakeConfirmRemoveSetBox( object_ptr<Ui::BoxContent> MakeConfirmRemoveSetBox(

View file

@ -66,12 +66,19 @@ enum class StickersListMode {
Masks, Masks,
UserpicBuilder, UserpicBuilder,
ChatIntro, ChatIntro,
MessageEffects,
};
struct StickerCustomRecentDescriptor {
not_null<DocumentData*> document;
QString cornerEmoji;
}; };
struct StickersListDescriptor { struct StickersListDescriptor {
std::shared_ptr<Show> show; std::shared_ptr<Show> show;
StickersListMode mode = StickersListMode::Full; StickersListMode mode = StickersListMode::Full;
Fn<bool()> paused; Fn<bool()> paused;
std::vector<StickerCustomRecentDescriptor> customRecentList;
const style::EmojiPan *st = nullptr; const style::EmojiPan *st = nullptr;
ComposeFeatures features; ComposeFeatures features;
}; };
@ -116,10 +123,13 @@ public:
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer(); std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
base::unique_qptr<Ui::PopupMenu> fillContextMenu( base::unique_qptr<Ui::PopupMenu> fillContextMenu(
SendMenu::Type type) override; const SendMenu::Details &details) override;
bool mySetsEmpty() const; bool mySetsEmpty() const;
void applySearchQuery(std::vector<QString> &&query);
[[nodiscard]] rpl::producer<int> recentShownCount() const;
~StickersListWidget(); ~StickersListWidget();
protected: protected:
@ -239,8 +249,10 @@ private:
bool setHasTitle(const Set &set) const; bool setHasTitle(const Set &set) const;
bool stickerHasDeleteButton(const Set &set, int index) const; bool stickerHasDeleteButton(const Set &set, int index) const;
std::vector<Sticker> collectRecentStickers(); [[nodiscard]] std::vector<Sticker> collectRecentStickers();
[[nodiscard]] std::vector<Sticker> collectCustomRecents();
void refreshRecentStickers(bool resize = true); void refreshRecentStickers(bool resize = true);
void refreshEffects();
void refreshFavedStickers(); void refreshFavedStickers();
enum class GroupStickersPlace { enum class GroupStickersPlace {
Visible, Visible,
@ -252,12 +264,13 @@ private:
void updateSelected(); void updateSelected();
void setSelected(OverState newSelected); void setSelected(OverState newSelected);
void setPressed(OverState newPressed); void setPressed(OverState newPressed);
std::unique_ptr<Ui::RippleAnimation> createButtonRipple(int section); [[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
QPoint buttonRippleTopLeft(int section) const; int section);
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
std::vector<Set> &shownSets(); [[nodiscard]] std::vector<Set> &shownSets();
const std::vector<Set> &shownSets() const; [[nodiscard]] const std::vector<Set> &shownSets() const;
int featuredRowHeight() const; [[nodiscard]] int featuredRowHeight() const;
void checkVisibleFeatured(int visibleTop, int visibleBottom); void checkVisibleFeatured(int visibleTop, int visibleBottom);
void readVisibleFeatured(int visibleTop, int visibleBottom); void readVisibleFeatured(int visibleTop, int visibleBottom);
@ -315,6 +328,7 @@ private:
[[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const; [[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const;
[[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef(); [[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef();
void filterEffectsByEmoji(const std::vector<EmojiPtr> &emoji);
enum class AppendSkip { enum class AppendSkip {
None, None,
@ -347,6 +361,7 @@ private:
void fillLocalSearchRows(const QString &query); void fillLocalSearchRows(const QString &query);
void fillCloudSearchRows(const std::vector<uint64> &cloudSets); void fillCloudSearchRows(const std::vector<uint64> &cloudSets);
void addSearchRow(not_null<Data::StickersSet*> set); void addSearchRow(not_null<Data::StickersSet*> set);
void toggleSearchLoading(bool loading);
void showPreview(); void showPreview();
@ -364,14 +379,17 @@ private:
std::unique_ptr<LocalStickersManager> _localSetsManager; std::unique_ptr<LocalStickersManager> _localSetsManager;
ChannelData *_megagroupSet = nullptr; ChannelData *_megagroupSet = nullptr;
uint64 _megagroupSetIdRequested = 0; uint64 _megagroupSetIdRequested = 0;
std::vector<StickerCustomRecentDescriptor> _customRecentIds;
std::vector<Set> _mySets; std::vector<Set> _mySets;
std::vector<Set> _officialSets; std::vector<Set> _officialSets;
std::vector<Set> _searchSets; std::vector<Set> _searchSets;
int _featuredSetsCount = 0; int _featuredSetsCount = 0;
std::vector<bool> _custom; std::vector<bool> _custom;
std::vector<EmojiPtr> _cornerEmoji;
base::flat_set<not_null<DocumentData*>> _favedStickersMap; base::flat_set<not_null<DocumentData*>> _favedStickersMap;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer; std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
bool _paintAsPremium = false;
bool _showingSetById = false; bool _showingSetById = false;
crl::time _lastScrolledAt = 0; crl::time _lastScrolledAt = 0;
crl::time _lastFullUpdatedAt = 0; crl::time _lastFullUpdatedAt = 0;
@ -381,6 +399,7 @@ private:
Section _section = Section::Stickers; Section _section = Section::Stickers;
const bool _isMasks; const bool _isMasks;
const bool _isEffects;
base::Timer _updateItemsTimer; base::Timer _updateItemsTimer;
base::Timer _updateSetsTimer; base::Timer _updateSetsTimer;
@ -419,6 +438,8 @@ private:
std::unique_ptr<StickerPremiumMark> _premiumMark; std::unique_ptr<StickerPremiumMark> _premiumMark;
std::vector<not_null<DocumentData*>> _filteredStickers; std::vector<not_null<DocumentData*>> _filteredStickers;
std::vector<EmojiPtr> _filterStickersCornerEmoji;
rpl::variable<int> _recentShownCount;
std::map<QString, std::vector<uint64>> _searchCache; std::map<QString, std::vector<uint64>> _searchCache;
std::vector<std::pair<uint64, QStringList>> _searchIndex; std::vector<std::pair<uint64, QStringList>> _searchIndex;
base::Timer _searchRequestTimer; base::Timer _searchRequestTimer;

View file

@ -28,6 +28,9 @@ TabbedSection::TabbedSection(
not_null<Window::SessionController*> controller) not_null<Window::SessionController*> controller)
: Window::SectionWidget(parent, controller) : Window::SectionWidget(parent, controller)
, _selector(controller->tabbedSelector()) { , _selector(controller->tabbedSelector()) {
if (Ui::InFocusChain(_selector)) {
parent->window()->setFocus();
}
_selector->setParent(this); _selector->setParent(this);
_selector->setRoundRadius(0); _selector->setRoundRadius(0);
_selector->setGeometry(rect()); _selector->setGeometry(rect());

View file

@ -516,7 +516,8 @@ TabbedSelector::TabbedSelector(
emoji()->showSet(setId); emoji()->showSet(setId);
_showRequests.fire({}); _showRequests.fire({});
}, lifetime()); }, lifetime());
}
if (hasEmojiTab()) {
emoji()->refreshEmoji(); emoji()->refreshEmoji();
} }
//setAttribute(Qt::WA_AcceptTouchEvents); //setAttribute(Qt::WA_AcceptTouchEvents);
@ -1297,8 +1298,8 @@ void TabbedSelector::scrollToY(int y) {
} }
} }
void TabbedSelector::showMenuWithType(SendMenu::Type type) { void TabbedSelector::showMenuWithDetails(SendMenu::Details details) {
_menu = currentTab()->widget()->fillContextMenu(type); _menu = currentTab()->widget()->fillContextMenu(details);
if (_menu && !_menu->empty()) { if (_menu && !_menu->empty()) {
_menu->popup(QCursor::pos()); _menu->popup(QCursor::pos());
} }
@ -1460,9 +1461,7 @@ int TabbedSelector::Inner::resizeGetHeight(int newWidth) {
} }
int TabbedSelector::Inner::minimalHeight() const { int TabbedSelector::Inner::minimalHeight() const {
return (_minimalHeight > 0) return _minimalHeight.value_or(defaultMinimalHeight());
? _minimalHeight
: defaultMinimalHeight();
} }
int TabbedSelector::Inner::defaultMinimalHeight() const { int TabbedSelector::Inner::defaultMinimalHeight() const {

View file

@ -36,7 +36,7 @@ class TabbedSearch;
} // namespace Ui } // namespace Ui
namespace SendMenu { namespace SendMenu {
enum class Type; struct Details;
} // namespace SendMenu } // namespace SendMenu
namespace style { namespace style {
@ -178,7 +178,7 @@ public:
_beforeHidingCallback = std::move(callback); _beforeHidingCallback = std::move(callback);
} }
void showMenuWithType(SendMenu::Type type); void showMenuWithDetails(SendMenu::Details details);
void setDropDown(bool dropDown); void setDropDown(bool dropDown);
// Float player interface. // Float player interface.
@ -380,7 +380,7 @@ public:
virtual void beforeHiding() { virtual void beforeHiding() {
} }
[[nodiscard]] virtual base::unique_qptr<Ui::PopupMenu> fillContextMenu( [[nodiscard]] virtual base::unique_qptr<Ui::PopupMenu> fillContextMenu(
SendMenu::Type type) { const SendMenu::Details &details) {
return nullptr; return nullptr;
} }
@ -422,7 +422,7 @@ private:
int _visibleTop = 0; int _visibleTop = 0;
int _visibleBottom = 0; int _visibleBottom = 0;
int _minimalHeight = 0; std::optional<int> _minimalHeight;
rpl::event_stream<int> _scrollToRequests; rpl::event_stream<int> _scrollToRequests;
rpl::event_stream<bool> _disableScrollRequests; rpl::event_stream<bool> _disableScrollRequests;

View file

@ -383,7 +383,7 @@ void Application::run() {
} }
SetCrashAnnotationsGL(); SetCrashAnnotationsGL();
if (!Platform::IsMac() && Ui::GL::LastCrashCheckFailed()) { if (Ui::GL::LastCrashCheckFailed()) {
showOpenGLCrashNotification(); showOpenGLCrashNotification();
} }
@ -427,14 +427,12 @@ void Application::checkWindowAccount(not_null<Window::Controller*> window) {
void Application::showOpenGLCrashNotification() { void Application::showOpenGLCrashNotification() {
const auto enable = [=] { const auto enable = [=] {
Ui::GL::ForceDisable(false);
Ui::GL::CrashCheckFinish(); Ui::GL::CrashCheckFinish();
settings().setDisableOpenGL(false); settings().setDisableOpenGL(false);
Local::writeSettings(); Local::writeSettings();
Restart(); Restart();
}; };
const auto keepDisabled = [=](Fn<void()> close) { const auto keepDisabled = [=](Fn<void()> close) {
Ui::GL::ForceDisable(true);
Ui::GL::CrashCheckFinish(); Ui::GL::CrashCheckFinish();
settings().setDisableOpenGL(true); settings().setDisableOpenGL(true);
Local::writeSettings(); Local::writeSettings();
@ -792,6 +790,7 @@ void Application::badMtprotoConfigurationError() {
} }
void Application::startLocalStorage() { void Application::startLocalStorage() {
Ui::GL::DetectLastCheckCrash();
Local::start(); Local::start();
_saveSettingsTimer.emplace([=] { saveSettings(); }); _saveSettingsTimer.emplace([=] { saveSettings(); });
settings().saveDelayedRequests() | rpl::start_with_next([=] { settings().saveDelayedRequests() | rpl::start_with_next([=] {

View file

@ -52,6 +52,8 @@ struct ClickHandlerContext {
}; };
Q_DECLARE_METATYPE(ClickHandlerContext); Q_DECLARE_METATYPE(ClickHandlerContext);
class PhoneClickHandler;
class HiddenUrlClickHandler : public UrlClickHandler { class HiddenUrlClickHandler : public UrlClickHandler {
public: public:
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) { HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {

View file

@ -917,10 +917,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_recentEmojiPreload = std::move(recentEmojiPreload); _recentEmojiPreload = std::move(recentEmojiPreload);
_emojiVariants = std::move(emojiVariants); _emojiVariants = std::move(emojiVariants);
_disableOpenGL = (disableOpenGL == 1); _disableOpenGL = (disableOpenGL == 1);
if (!Platform::IsMac()) { Ui::GL::ForceDisable(_disableOpenGL);
Ui::GL::ForceDisable(_disableOpenGL
|| Ui::GL::LastCrashCheckFailed());
}
_groupCallNoiseSuppression = (groupCallNoiseSuppression == 1); _groupCallNoiseSuppression = (groupCallNoiseSuppression == 1);
const auto uncheckedWorkMode = static_cast<WorkMode>(workMode); const auto uncheckedWorkMode = static_cast<WorkMode>(workMode);
switch (uncheckedWorkMode) { switch (uncheckedWorkMode) {

View file

@ -7,10 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "core/file_utilities.h" #include "core/file_utilities.h"
#include "boxes/abstract_box.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "storage/storage_account.h" #include "storage/storage_account.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/base_platform_file_utilities.h" #include "base/platform/base_platform_file_utilities.h"
#include "platform/platform_file_utilities.h" #include "platform/platform_file_utilities.h"
#include "core/application.h" #include "core/application.h"
@ -159,10 +157,6 @@ void Launch(const QString &filepath) {
void ShowInFolder(const QString &filepath) { void ShowInFolder(const QString &filepath) {
crl::on_main([=] { crl::on_main([=] {
Ui::PreventDelayedActivation(); Ui::PreventDelayedActivation();
if (Platform::IsX11()) {
// Hide mediaview to make other apps visible.
Core::App().hideMediaView();
}
base::Platform::ShowInFolder(filepath); base::Platform::ShowInFolder(filepath);
}); });
} }

View file

@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/background_preview_box.h" #include "boxes/background_preview_box.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "ui/boxes/edit_birthday_box.h" #include "ui/boxes/edit_birthday_box.h"
#include "payments/payments_non_panel_process.h"
#include "boxes/share_box.h" #include "boxes/share_box.h"
#include "boxes/connection_box.h" #include "boxes/connection_box.h"
#include "boxes/edit_privacy_box.h" #include "boxes/edit_privacy_box.h"
@ -353,6 +354,7 @@ bool ApplySocksProxy(
match->captured(1), match->captured(1),
qthelp::UrlParamNameTransform::ToLower); qthelp::UrlParamNameTransform::ToLower);
ProxiesBoxController::ShowApplyConfirmation( ProxiesBoxController::ShowApplyConfirmation(
controller,
MTP::ProxyData::Type::Socks5, MTP::ProxyData::Type::Socks5,
params); params);
if (controller) { if (controller) {
@ -369,6 +371,7 @@ bool ApplyMtprotoProxy(
match->captured(1), match->captured(1),
qthelp::UrlParamNameTransform::ToLower); qthelp::UrlParamNameTransform::ToLower);
ProxiesBoxController::ShowApplyConfirmation( ProxiesBoxController::ShowApplyConfirmation(
controller,
MTP::ProxyData::Type::Mtproto, MTP::ProxyData::Type::Mtproto,
params); params);
if (controller) { if (controller) {
@ -1096,7 +1099,8 @@ bool ResolveInvoice(
Payments::CheckoutProcess::Start( Payments::CheckoutProcess::Start(
&controller->session(), &controller->session(),
slug, slug,
crl::guard(window, [=](auto) { window->activate(); })); crl::guard(window, [=](auto) { window->activate(); }),
Payments::ProcessNonPanelPaymentFormFactory(controller));
return true; return true;
} }

View file

@ -0,0 +1,335 @@
/*
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 "core/phone_click_handler.h"
#include "core/click_handler_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mtproto/sender.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h" // popupMenuExpandedSeparator.
#include "styles/style_menu_icons.h"
namespace {
[[nodiscard]] QString Trim(QString text) {
return text
.replace('+', QString())
.replace(' ', QString())
.replace('-', QString());
}
class ResolvePhoneAction final : public Ui::Menu::ItemBase {
public:
ResolvePhoneAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
const QString &phone,
not_null<Window::SessionController*> controller);
bool isEnabled() const override;
not_null<QAction*> action() const override;
void handleKeyPress(not_null<QKeyEvent*> e) override;
protected:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
int contentHeight() const override;
private:
void prepare();
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
rpl::variable<PeerData*> _peer;
rpl::variable<bool> _loaded;
Ui::PeerUserpicView _userpicView;
MTP::Sender _api;
Ui::Text::String _above;
Ui::Text::String _below;
int _aboveWidth = 0;
int _belowWidth = 0;
const int _height = 0;
};
ResolvePhoneAction::ResolvePhoneAction(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
const QString &phone,
not_null<Window::SessionController*> controller)
: ItemBase(parent, st)
, _dummyAction(new QAction(parent))
, _st(st)
, _api(&controller->session().mtp())
, _height(rect::m::sum::v(st::groupCallJoinAsPadding)
+ st::groupCallJoinAsPhotoSize) {
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setClickedCallback([=] {
if (const auto peer = _peer.current()) {
controller->showPeerInfo(peer);
}
});
const auto formattedPhone = Trim(phone);
const auto owner = &controller->session().data();
if (const auto peer = owner->userByPhone(formattedPhone)) {
_peer = peer;
_loaded.force_assign(true);
} else {
_api.request(MTPcontacts_ResolvePhone(
MTP_string(phone)
)).done([=](const MTPcontacts_ResolvedPeer &result) {
result.match([&](const MTPDcontacts_resolvedPeer &data) {
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
if (const auto peerId = peerFromMTP(data.vpeer())) {
_peer = owner->peer(peerId);
}
_loaded.force_assign(true);
});
}).fail([=](const MTP::Error &error) {
if (error.code() == 400) {
_peer.force_assign(nullptr);
_loaded.force_assign(true);
}
}).send();
}
paintRequest(
) | rpl::start_with_next([=] {
Painter p(this);
paint(p);
}, lifetime());
enableMouseSelecting();
prepare();
}
void ResolvePhoneAction::paint(Painter &p) {
const auto selected = isSelected() && _peer.current();
const auto height = contentHeight();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), height, _st.itemBg);
}
p.fillRect(0, 0, width(), height, selected ? _st.itemBgOver : _st.itemBg);
if (isEnabled()) {
paintRipple(p, 0, 0);
}
const auto &padding = st::groupCallJoinAsPadding;
const auto textLeft = padding.left()
+ st::groupCallJoinAsPhotoSize
+ padding.left();
if (const auto peer = _peer.current()) {
peer->paintUserpic(
p,
_userpicView,
padding.left(),
padding.top(),
st::groupCallJoinAsPhotoSize);
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_above.drawLeftElided(
p,
textLeft,
st::groupCallJoinAsTextTop,
width() - textLeft - padding.right(),
width());
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
_below.drawLeftElided(
p,
textLeft,
st::groupCallJoinAsNameTop,
_belowWidth,
width());
} else {
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
const auto w = width() - padding.left() - padding.right();
_below.draw(p, Ui::Text::PaintContext{
.position = QPoint(
(width() - w) / 2,
(height - _below.countHeight(w)) / 2),
.outerWidth = w,
.availableWidth = w,
.align = style::al_center,
.elisionLines = 2,
});
}
}
void ResolvePhoneAction::prepare() {
rpl::combine(
tr::lng_context_view_profile(),
_peer.value(
) | rpl::map([](PeerData *peer) {
return peer
? Info::Profile::NameValue(peer)
: rpl::single(QString());
}) | rpl::flatten_latest(),
tr::lng_menu_not_contact(),
_loaded.value(
) | rpl::map([](bool loaded) {
return loaded
? rpl::single(QString())
: tr::lng_contacts_loading();
}) | rpl::flatten_latest()
) | rpl::start_with_next([=](
QString text,
QString name,
QString no,
QString loading) {
const auto &padding = st::groupCallJoinAsPadding;
QWidget::setAttribute(
Qt::WA_TransparentForMouseEvents,
!_peer.current());
const auto above = name;
const auto below = !loading.isEmpty()
? loading
: name.isEmpty()
? no
: text;
const auto options = kDefaultTextOptions;
const auto tempWidth = [&] {
_below.setMarkedText(_st.itemStyle, { text }, options);
return _below.maxWidth();
}();
const auto textLeft = padding.left()
+ st::groupCallJoinAsPhotoSize
+ padding.left();
const auto w = std::clamp(
(textLeft + tempWidth + padding.right()),
_st.widthMin,
_st.widthMax);
if (!no.isEmpty()) {
_below = Ui::Text::String(w);
}
_above.setMarkedText(_st.itemStyle, { above }, options);
_below.setMarkedText(_st.itemStyle, { below }, options);
setMinWidth(w);
_aboveWidth = w - textLeft - padding.right();
_belowWidth = w
- ((loading.isEmpty() && name.isEmpty()) ? 0 : textLeft)
- padding.right();
update();
}, lifetime());
}
bool ResolvePhoneAction::isEnabled() const {
return true;
}
not_null<QAction*> ResolvePhoneAction::action() const {
return _dummyAction;
}
QPoint ResolvePhoneAction::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
QImage ResolvePhoneAction::prepareRippleMask() const {
return Ui::RippleAnimation::RectMask(size());
}
int ResolvePhoneAction::contentHeight() const {
return _height;
}
void ResolvePhoneAction::handleKeyPress(not_null<QKeyEvent*> e) {
if (!isSelected() || !_peer.current()) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
setClicked(Ui::Menu::TriggeredSource::Keyboard);
}
}
} // namespace
PhoneClickHandler::PhoneClickHandler(
not_null<Main::Session*> session,
QString text)
: _session(session)
, _text(text) {
}
void PhoneClickHandler::onClick(ClickContext context) const {
if (context.button != Qt::LeftButton) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto pos = QCursor::pos();
if (!controller) {
return;
}
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
controller->content(),
st::popupMenuWithIcons);
const auto phone = _text;
#if 0
const auto maybeContact = [&]() -> PeerData* {
const auto &chats = controller->session().data().contactsList();
for (const auto &row : chats->all()) {
if (const auto history = row->history()) {
if (const auto user = history->peer->asUser()) {
if (Trim(user->phone()) == Trim(phone)) {
return user;
}
}
}
}
return nullptr;
}();
#endif
menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] {
TextUtilities::SetClipboardText(
TextForMimeData::Simple(phone.trimmed()));
}, &st::menuIconCopy);
menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator);
menu->addAction(
base::make_unique_q<ResolvePhoneAction>(
menu,
menu->st().menu,
phone,
controller));
menu->popup(pos);
}
auto PhoneClickHandler::getTextEntity() const -> TextEntity {
return { EntityType::Phone };
}
QString PhoneClickHandler::tooltip() const {
return _text;
}

View file

@ -0,0 +1,30 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/basic_click_handlers.h"
namespace Main {
class Session;
} // namespace Main
class PhoneClickHandler : public ClickHandler {
public:
PhoneClickHandler(not_null<Main::Session*> session, QString text);
void onClick(ClickContext context) const override;
TextEntity getTextEntity() const override;
QString tooltip() const override;
private:
const not_null<Main::Session*> _session;
QString _text;
};

View file

@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "platform/platform_specific.h" #include "platform/platform_specific.h"
#include "boxes/url_auth_box.h" #include "boxes/url_auth_box.h"
#include "core/phone_click_handler.h"
#include "main/main_account.h" #include "main/main_account.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
@ -221,6 +222,8 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
return std::make_shared<MonospaceClickHandler>(data.text, data.type); return std::make_shared<MonospaceClickHandler>(data.text, data.type);
case EntityType::Pre: case EntityType::Pre:
return std::make_shared<MonospaceClickHandler>(data.text, data.type); return std::make_shared<MonospaceClickHandler>(data.text, data.type);
case EntityType::Phone:
return std::make_shared<PhoneClickHandler>(my->session, data.text);
} }
return Integration::createLinkHandler(data, context); return Integration::createLinkHandler(data, context);
} }

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 5000002; constexpr auto AppVersion = 5001002;
constexpr auto AppVersionStr = "5.0.2"; constexpr auto AppVersionStr = "5.1.2";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -87,7 +87,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTP_int(data.vttl_period().value_or_empty()), MTP_int(data.vttl_period().value_or_empty()),
MTP_int(shortcutId)); MTP_int(shortcutId),
MTP_long(data.veffect().value_or_empty()),
(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()));
}); });
} }

View file

@ -0,0 +1,218 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/factchecks.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/history_view_message.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/layers/show.h"
namespace Data {
namespace {
constexpr auto kRequestDelay = crl::time(1000);
} // namespace
Factchecks::Factchecks(not_null<Main::Session*> session)
: _session(session)
, _requestTimer([=] { request(); }) {
}
void Factchecks::requestFor(not_null<HistoryItem*> item) {
subscribeIfNotYet();
if (const auto factcheck = item->Get<HistoryMessageFactcheck>()) {
factcheck->requested = true;
}
if (!_requestTimer.isActive()) {
_requestTimer.callOnce(kRequestDelay);
}
const auto changed = !_pending.empty()
&& (_pending.front()->history() != item->history());
const auto added = _pending.emplace(item).second;
if (changed) {
request();
} else if (added && _pending.size() == 1) {
_requestTimer.callOnce(kRequestDelay);
}
}
void Factchecks::subscribeIfNotYet() {
if (_subscribed) {
return;
}
_subscribed = true;
_session->data().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
_pending.remove(item);
const auto i = ranges::find(_requested, item.get());
if (i != end(_requested)) {
*i = nullptr;
}
}, _lifetime);
}
void Factchecks::request() {
_requestTimer.cancel();
if (!_requested.empty() || _pending.empty()) {
return;
}
_session->api().request(base::take(_requestId)).cancel();
auto ids = QVector<MTPint>();
ids.reserve(_pending.size());
const auto history = _pending.front()->history();
for (auto i = begin(_pending); i != end(_pending);) {
const auto &item = *i;
if (item->history() == history) {
_requested.push_back(item);
ids.push_back(MTP_int(item->id.bare));
i = _pending.erase(i);
} else {
++i;
}
}
_requestId = _session->api().request(MTPmessages_GetFactCheck(
history->peer->input,
MTP_vector<MTPint>(std::move(ids))
)).done([=](const MTPVector<MTPFactCheck> &result) {
_requestId = 0;
const auto &list = result.v;
auto index = 0;
for (const auto &item : base::take(_requested)) {
if (!item) {
} else if (index >= list.size()) {
item->setFactcheck({});
} else {
item->setFactcheck(FromMTP(item, &list[index]));
}
++index;
}
if (!_pending.empty()) {
request();
}
}).fail([=] {
_requestId = 0;
for (const auto &item : base::take(_requested)) {
if (item) {
item->setFactcheck({});
}
}
if (!_pending.empty()) {
request();
}
}).send();
}
std::unique_ptr<HistoryView::WebPage> Factchecks::makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck) {
if (!factcheck->page) {
factcheck->page = view->history()->owner().webpage(
base::RandomValue<WebPageId>(),
tr::lng_factcheck_title(tr::now),
factcheck->data.text);
factcheck->page->type = WebPageType::Factcheck;
}
return std::make_unique<HistoryView::WebPage>(
view,
factcheck->page,
MediaWebPageFlags());
}
bool Factchecks::canEdit(not_null<HistoryItem*> item) const {
if (!canEdit()
|| !item->isRegular()
|| !item->history()->peer->isBroadcast()) {
return false;
}
const auto media = item->media();
if (!media || media->webpage() || media->photo()) {
return true;
} else if (const auto document = media->document()) {
return !document->isVideoMessage() && !document->sticker();
}
return false;
}
bool Factchecks::canEdit() const {
return _session->appConfig().get<bool>(u"can_edit_factcheck"_q, false);
}
int Factchecks::lengthLimit() const {
return _session->appConfig().get<int>(u"factcheck_length_limit"_q, 1024);
}
void Factchecks::save(
FullMsgId itemId,
TextWithEntities text,
Fn<void(QString)> done) {
const auto item = _session->data().message(itemId);
if (!item) {
return;
} else if (text.empty()) {
_session->api().request(MTPmessages_DeleteFactCheck(
item->history()->peer->input,
MTP_int(item->id.bare)
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
} else {
_session->api().request(MTPmessages_EditFactCheck(
item->history()->peer->input,
MTP_int(item->id.bare),
MTP_textWithEntities(
MTP_string(text.text),
Api::EntitiesToMTP(
_session,
text.entities,
Api::ConvertOption::SkipLocal))
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
done(QString());
}).fail([=](const MTP::Error &error) {
done(error.type());
}).send();
}
}
void Factchecks::save(
FullMsgId itemId,
const TextWithEntities &was,
TextWithEntities text,
std::shared_ptr<Ui::Show> show) {
const auto wasEmpty = was.empty();
const auto textEmpty = text.empty();
save(itemId, std::move(text), [=](QString error) {
show->showToast(!error.isEmpty()
? error
: textEmpty
? tr::lng_factcheck_remove_done(tr::now)
: wasEmpty
? tr::lng_factcheck_add_done(tr::now)
: tr::lng_factcheck_edit_done(tr::now));
});
}
} // namespace Data

View file

@ -0,0 +1,70 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
class HistoryItem;
struct HistoryMessageFactcheck;
namespace HistoryView {
class Message;
class WebPage;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class Show;
} // namespace Ui
namespace Data {
class Factchecks final {
public:
explicit Factchecks(not_null<Main::Session*> session);
void requestFor(not_null<HistoryItem*> item);
[[nodiscard]] std::unique_ptr<HistoryView::WebPage> makeMedia(
not_null<HistoryView::Message*> view,
not_null<HistoryMessageFactcheck*> factcheck);
[[nodiscard]] bool canEdit(not_null<HistoryItem*> item) const;
[[nodiscard]] int lengthLimit() const;
void save(
FullMsgId itemId,
TextWithEntities text,
Fn<void(QString)> done);
void save(
FullMsgId itemId,
const TextWithEntities &was,
TextWithEntities text,
std::shared_ptr<Ui::Show> show);
private:
[[nodiscard]] bool canEdit() const;
void subscribeIfNotYet();
void request();
const not_null<Main::Session*> _session;
base::Timer _requestTimer;
base::flat_set<not_null<HistoryItem*>> _pending;
std::vector<HistoryItem*> _requested;
mtpRequestId _requestId = 0;
bool _subscribed = false;
rpl::lifetime _lifetime;
};
} // namespace Data

View file

@ -91,7 +91,9 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTP_int(data.vttl_period().value_or_empty()), MTP_int(data.vttl_period().value_or_empty()),
MTPint()); // quick_reply_shortcut_id MTPint(), // quick_reply_shortcut_id
MTP_long(data.veffect().value_or_empty()), // effect
data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck());
}); });
} }
@ -227,6 +229,9 @@ void ScheduledMessages::sendNowSimpleMessage(
: MTPDmessage::Flag(0)) : MTPDmessage::Flag(0))
| ((localFlags & MessageFlag::Outgoing) | ((localFlags & MessageFlag::Outgoing)
? MTPDmessage::Flag::f_out ? MTPDmessage::Flag::f_out
: MTPDmessage::Flag(0))
| (local->effectId()
? MTPDmessage::Flag::f_effect
: MTPDmessage::Flag(0)); : MTPDmessage::Flag(0));
const auto views = 1; const auto views = 1;
const auto forwards = 0; const auto forwards = 0;
@ -259,7 +264,9 @@ void ScheduledMessages::sendNowSimpleMessage(
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTP_int(update.vttl_period().value_or_empty()), MTP_int(update.vttl_period().value_or_empty()),
MTPint()), // quick_reply_shortcut_id MTPint(), // quick_reply_shortcut_id
MTP_long(local->effectId()), // effect
MTPFactCheck()),
localFlags, localFlags,
NewMessageType::Unread); NewMessageType::Unread);

View file

@ -0,0 +1,51 @@
/*
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 CreditTopupOption final {
uint64 credits = 0;
QString product;
QString currency;
uint64 amount = 0;
bool extended = false;
};
using CreditTopupOptions = std::vector<CreditTopupOption>;
struct CreditsHistoryEntry final {
using PhotoId = uint64;
enum class PeerType {
Peer,
AppStore,
PlayMarket,
Fragment,
Unsupported,
PremiumBot,
};
QString id;
QString title;
QString description;
QDateTime date;
PhotoId photoId = 0;
uint64 credits = 0;
uint64 bareId = 0;
PeerType peerType;
bool refunded = false;
};
struct CreditsStatusSlice final {
using OffsetToken = QString;
std::vector<CreditsHistoryEntry> list;
uint64 balance = 0;
bool allLoaded = false;
OffsetToken token;
};
} // namespace Data

View file

@ -143,7 +143,7 @@ QImage ForumTopicIconFrame(
return background; return background;
} }
QImage ForumTopicGeneralIconFrame(int size, const style::color &color) { QImage ForumTopicGeneralIconFrame(int size, const QColor &color) {
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
auto svg = QSvgRenderer(ForumTopicIconPath(u"general"_q)); auto svg = QSvgRenderer(ForumTopicIconPath(u"general"_q));
auto result = QImage( auto result = QImage(
@ -172,6 +172,62 @@ TextWithEntities ForumTopicIconWithTitle(
: TextWithEntities{ title }; : TextWithEntities{ title };
} }
QString ForumGeneralIconTitle() {
return QChar(0) + u"general"_q;
}
bool IsForumGeneralIconTitle(const QString &title) {
return !title.isEmpty() && !title[0].unicode();
}
int32 ForumGeneralIconColor(const QColor &color) {
return int32(uint32(color.red()) << 16
| uint32(color.green()) << 8
| uint32(color.blue())
| (uint32(color.alpha() == 255 ? 0 : color.alpha()) << 24));
}
QColor ParseForumGeneralIconColor(int32 value) {
const auto alpha = uint32(value) >> 24;
return QColor(
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF,
alpha ? alpha : 255);
}
QString TopicIconEmojiEntity(TopicIconDescriptor descriptor) {
return IsForumGeneralIconTitle(descriptor.title)
? u"topic_general:"_q + QString::number(uint32(descriptor.colorId))
: (u"topic_icon:"_q
+ QString::number(uint32(descriptor.colorId))
+ ' '
+ ExtractNonEmojiLetter(descriptor.title));
}
TopicIconDescriptor ParseTopicIconEmojiEntity(QStringView entity) {
if (!entity.startsWith(u"topic_")) {
return {};
}
const auto general = u"topic_general:"_q;
const auto normal = u"topic_icon:"_q;
if (entity.startsWith(general)) {
return {
.title = ForumGeneralIconTitle(),
.colorId = int32(entity.mid(general.size()).toUInt()),
};
} else if (entity.startsWith(normal)) {
const auto parts = entity.mid(normal.size()).split(' ');
if (parts.size() == 2) {
return {
.title = parts[1].toString(),
.colorId = int32(parts[0].toUInt()),
};
}
}
return {};
}
ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId) ForumTopic::ForumTopic(not_null<Forum*> forum, MsgId rootId)
: Thread(&forum->history()->owner(), Type::ForumTopic) : Thread(&forum->history()->owner(), Type::ForumTopic)
, _forum(forum) , _forum(forum)
@ -640,7 +696,7 @@ void ForumTopic::validateGeneralIcon(
: context.selected : context.selected
? st::dialogsTextFgOver ? st::dialogsTextFgOver
: st::dialogsTextFg; : st::dialogsTextFg;
_defaultIcon = ForumTopicGeneralIconFrame(size, color); _defaultIcon = ForumTopicGeneralIconFrame(size, color->c);
_flags = (_flags & ~mask) | flags; _flags = (_flags & ~mask) | flags;
} }

View file

@ -48,12 +48,33 @@ class Forum;
const style::ForumTopicIcon &st); const style::ForumTopicIcon &st);
[[nodiscard]] QImage ForumTopicGeneralIconFrame( [[nodiscard]] QImage ForumTopicGeneralIconFrame(
int size, int size,
const style::color &color); const QColor &color);
[[nodiscard]] TextWithEntities ForumTopicIconWithTitle( [[nodiscard]] TextWithEntities ForumTopicIconWithTitle(
MsgId rootId, MsgId rootId,
DocumentId iconId, DocumentId iconId,
const QString &title); const QString &title);
[[nodiscard]] QString ForumGeneralIconTitle();
[[nodiscard]] bool IsForumGeneralIconTitle(const QString &title);
[[nodiscard]] int32 ForumGeneralIconColor(const QColor &color);
[[nodiscard]] QColor ParseForumGeneralIconColor(int32 value);
struct TopicIconDescriptor {
QString title;
int32 colorId = 0;
[[nodiscard]] bool empty() const {
return !colorId && title.isEmpty();
}
explicit operator bool() const {
return !empty();
}
};
[[nodiscard]] QString TopicIconEmojiEntity(TopicIconDescriptor descriptor);
[[nodiscard]] TopicIconDescriptor ParseTopicIconEmojiEntity(
QStringView entity);
class ForumTopic final : public Thread { class ForumTopic final : public Thread {
public: public:
static constexpr auto kGeneralId = 1; static constexpr auto kGeneralId = 1;

View file

@ -0,0 +1,219 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_history_messages.h"
#include "apiwrap.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_sparse_ids.h"
#include "history/history.h"
#include "main/main_session.h"
namespace Data {
void HistoryMessages::addNew(MsgId messageId) {
_chat.addNew(messageId);
}
void HistoryMessages::addExisting(MsgId messageId, MsgRange noSkipRange) {
_chat.addExisting(messageId, noSkipRange);
}
void HistoryMessages::addSlice(
std::vector<MsgId> &&messageIds,
MsgRange noSkipRange,
std::optional<int> count) {
_chat.addSlice(std::move(messageIds), noSkipRange, count);
}
void HistoryMessages::removeOne(MsgId messageId) {
_chat.removeOne(messageId);
_oneRemoved.fire_copy(messageId);
}
void HistoryMessages::removeAll() {
_chat.removeAll();
_allRemoved.fire({});
}
void HistoryMessages::invalidateBottom() {
_chat.invalidateBottom();
_bottomInvalidated.fire({});
}
Storage::SparseIdsListResult HistoryMessages::snapshot(
const Storage::SparseIdsListQuery &query) const {
return _chat.snapshot(query);
}
auto HistoryMessages::sliceUpdated() const
-> rpl::producer<Storage::SparseIdsSliceUpdate> {
return _chat.sliceUpdated();
}
rpl::producer<MsgId> HistoryMessages::oneRemoved() const {
return _oneRemoved.events();
}
rpl::producer<> HistoryMessages::allRemoved() const {
return _allRemoved.events();
}
rpl::producer<> HistoryMessages::bottomInvalidated() const {
return _bottomInvalidated.events();
}
rpl::producer<SparseIdsSlice> HistoryViewer(
not_null<History*> history,
MsgId aroundId,
int limitBefore,
int limitAfter) {
Expects(IsServerMsgId(aroundId) || (aroundId == 0));
Expects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0));
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto messages = &history->messages();
auto builder = lifetime.make_state<SparseIdsSliceBuilder>(
aroundId,
limitBefore,
limitAfter);
using RequestAroundInfo = SparseIdsSliceBuilder::AroundData;
builder->insufficientAround(
) | rpl::start_with_next([=](const RequestAroundInfo &info) {
if (!info.aroundId) {
// Ignore messages-count-only requests, because we perform
// them with non-zero limit of messages and end up adding
// a broken slice with several last messages from the chat
// with a non-skip range starting at zero.
return;
}
history->session().api().requestHistory(
history,
info.aroundId,
info.direction);
}, lifetime);
auto pushNextSnapshot = [=] {
consumer.put_next(builder->snapshot());
};
using SliceUpdate = Storage::SparseIdsSliceUpdate;
messages->sliceUpdated(
) | rpl::filter([=](const SliceUpdate &update) {
return builder->applyUpdate(update);
}) | rpl::start_with_next(pushNextSnapshot, lifetime);
messages->oneRemoved(
) | rpl::filter([=](MsgId messageId) {
return builder->removeOne(messageId);
}) | rpl::start_with_next(pushNextSnapshot, lifetime);
messages->allRemoved(
) | rpl::filter([=] {
return builder->removeAll();
}) | rpl::start_with_next(pushNextSnapshot, lifetime);
messages->bottomInvalidated(
) | rpl::filter([=] {
return builder->invalidateBottom();
}) | rpl::start_with_next(pushNextSnapshot, lifetime);
const auto snapshot = messages->snapshot({
aroundId,
limitBefore,
limitAfter,
});
if (snapshot.count || !snapshot.messageIds.empty()) {
if (builder->applyInitial(snapshot)) {
pushNextSnapshot();
}
}
builder->checkInsufficient();
return lifetime;
};
}
rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(
not_null<History*> history,
/*Universal*/MsgId universalAroundId,
int limitBefore,
int limitAfter) {
const auto migrateFrom = history->peer->migrateFrom();
auto createSimpleViewer = [=](
PeerId peerId,
MsgId topicRootId,
SparseIdsSlice::Key simpleKey,
int limitBefore,
int limitAfter) {
const auto chosen = (history->peer->id == peerId)
? history
: history->owner().history(peerId);
return HistoryViewer(chosen, simpleKey, limitBefore, limitAfter);
};
const auto peerId = history->peer->id;
const auto topicRootId = MsgId();
const auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0);
using Key = SparseIdsMergedSlice::Key;
return SparseIdsMergedSlice::CreateViewer(
Key(peerId, topicRootId, migratedPeerId, universalAroundId),
limitBefore,
limitAfter,
std::move(createSimpleViewer));
}
rpl::producer<MessagesSlice> HistoryMessagesViewer(
not_null<History*> history,
MessagePosition aroundId,
int limitBefore,
int limitAfter) {
const auto computeUnreadAroundId = [&] {
if (const auto migrated = history->migrateFrom()) {
if (const auto around = migrated->loadAroundId()) {
return MsgId(around - ServerMaxMsgId);
}
}
if (const auto around = history->loadAroundId()) {
return around;
}
return MsgId(ServerMaxMsgId - 1);
};
const auto messageId = (aroundId.fullId.msg == ShowAtUnreadMsgId)
? computeUnreadAroundId()
: (aroundId.fullId.msg == ShowAtTheEndMsgId)
? (ServerMaxMsgId - 1)
: (aroundId.fullId.peer == history->peer->id)
? aroundId.fullId.msg
: (aroundId.fullId.msg - ServerMaxMsgId);
return HistoryMergedViewer(
history,
messageId,
limitBefore,
limitAfter
) | rpl::map([=](SparseIdsMergedSlice &&slice) {
auto result = Data::MessagesSlice();
result.fullCount = slice.fullCount();
result.skippedAfter = slice.skippedAfter();
result.skippedBefore = slice.skippedBefore();
const auto count = slice.size();
result.ids.reserve(count);
if (const auto msgId = slice.nearest(messageId)) {
result.nearestToAround = *msgId;
}
for (auto i = 0; i != count; ++i) {
result.ids.push_back(slice[i]);
}
return result;
});
}
} // namespace Data

View file

@ -0,0 +1,67 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "storage/storage_sparse_ids_list.h"
class History;
class SparseIdsSlice;
class SparseIdsMergedSlice;
namespace Data {
struct MessagesSlice;
struct MessagePosition;
class HistoryMessages final {
public:
void addNew(MsgId messageId);
void addExisting(MsgId messageId, MsgRange noSkipRange);
void addSlice(
std::vector<MsgId> &&messageIds,
MsgRange noSkipRange,
std::optional<int> count);
void removeOne(MsgId messageId);
void removeAll();
void invalidateBottom();
[[nodiscard]] Storage::SparseIdsListResult snapshot(
const Storage::SparseIdsListQuery &query) const;
[[nodiscard]] auto sliceUpdated() const
-> rpl::producer<Storage::SparseIdsSliceUpdate>;
[[nodiscard]] rpl::producer<MsgId> oneRemoved() const;
[[nodiscard]] rpl::producer<> allRemoved() const;
[[nodiscard]] rpl::producer<> bottomInvalidated() const;
private:
Storage::SparseIdsList _chat;
rpl::event_stream<MsgId> _oneRemoved;
rpl::event_stream<> _allRemoved;
rpl::event_stream<> _bottomInvalidated;
};
[[nodiscard]] rpl::producer<SparseIdsSlice> HistoryViewer(
not_null<History*> history,
MsgId aroundId,
int limitBefore,
int limitAfter);
[[nodiscard]] rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer(
not_null<History*> history,
/*Universal*/MsgId universalAroundId,
int limitBefore,
int limitAfter);
[[nodiscard]] rpl::producer<MessagesSlice> HistoryMessagesViewer(
not_null<History*> history,
MessagePosition aroundId,
int limitBefore,
int limitAfter);
} // namespace Data

View file

@ -1224,14 +1224,17 @@ MediaContact::MediaContact(
UserId userId, UserId userId,
const QString &firstName, const QString &firstName,
const QString &lastName, const QString &lastName,
const QString &phoneNumber) const QString &phoneNumber,
: Media(parent) { const SharedContact::VcardItems &vcardItems)
: Media(parent)
, _contact(SharedContact{
.userId = userId,
.firstName = firstName,
.lastName = lastName,
.phoneNumber = phoneNumber,
.vcardItems = vcardItems,
}) {
parent->history()->owner().registerContactItem(userId, parent); parent->history()->owner().registerContactItem(userId, parent);
_contact.userId = userId;
_contact.firstName = firstName;
_contact.lastName = lastName;
_contact.phoneNumber = phoneNumber;
} }
MediaContact::~MediaContact() { MediaContact::~MediaContact() {
@ -1246,7 +1249,8 @@ std::unique_ptr<Media> MediaContact::clone(not_null<HistoryItem*> parent) {
_contact.userId, _contact.userId,
_contact.firstName, _contact.firstName,
_contact.lastName, _contact.lastName,
_contact.phoneNumber); _contact.phoneNumber,
_contact.vcardItems);
} }
const SharedContact *MediaContact::sharedContact() const { const SharedContact *MediaContact::sharedContact() const {
@ -1301,12 +1305,7 @@ std::unique_ptr<HistoryView::Media> MediaContact::createView(
not_null<HistoryView::Element*> message, not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) { HistoryView::Element *replacing) {
return std::make_unique<HistoryView::Contact>( return std::make_unique<HistoryView::Contact>(message, _contact);
message,
_contact.userId,
_contact.firstName,
_contact.lastName,
_contact.phoneNumber);
} }
MediaLocation::MediaLocation( MediaLocation::MediaLocation(

View file

@ -47,11 +47,30 @@ enum class CallFinishReason : char {
Hangup, Hangup,
}; };
struct SharedContact { struct SharedContact final {
UserId userId = 0; UserId userId = 0;
QString firstName; QString firstName;
QString lastName; QString lastName;
QString phoneNumber; QString phoneNumber;
enum class VcardItemType {
Phone,
PhoneMain,
PhoneHome,
PhoneMobile,
PhoneWork,
PhoneOther,
Email,
Address,
Url,
Note,
Birthday,
Organization,
Name,
};
using VcardItems = base::flat_map<VcardItemType, QString>;
VcardItems vcardItems;
}; };
struct Call { struct Call {
@ -308,7 +327,8 @@ public:
UserId userId, UserId userId,
const QString &firstName, const QString &firstName,
const QString &lastName, const QString &lastName,
const QString &phoneNumber); const QString &phoneNumber,
const SharedContact::VcardItems &vcardItems);
~MediaContact(); ~MediaContact();
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override; std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_peer_values.h" #include "data/data_peer_values.h"
#include "data/data_saved_sublist.h" #include "data/data_saved_sublist.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
@ -253,6 +254,9 @@ PossibleItemReactions::PossibleItemReactions(
: recent(other.recent | ranges::views::transform([](const auto &value) { : recent(other.recent | ranges::views::transform([](const auto &value) {
return *value; return *value;
}) | ranges::to_vector) }) | ranges::to_vector)
, stickers(other.stickers | ranges::views::transform([](const auto &value) {
return *value;
}) | ranges::to_vector)
, customAllowed(other.customAllowed) , customAllowed(other.customAllowed)
, tags(other.tags){ , tags(other.tags){
} }
@ -269,6 +273,7 @@ Reactions::Reactions(not_null<Session*> owner)
kRefreshFullListEach kRefreshFullListEach
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
refreshDefault(); refreshDefault();
requestEffects();
}, _lifetime); }, _lifetime);
_owner->session().changes().messageUpdates( _owner->session().changes().messageUpdates(
@ -348,6 +353,12 @@ void Reactions::refreshTags() {
requestTags(); requestTags();
} }
void Reactions::refreshEffects() {
if (_effects.empty()) {
requestEffects();
}
}
const std::vector<Reaction> &Reactions::list(Type type) const { const std::vector<Reaction> &Reactions::list(Type type) const {
switch (type) { switch (type) {
case Type::Active: return _active; case Type::Active: return _active;
@ -357,6 +368,7 @@ const std::vector<Reaction> &Reactions::list(Type type) const {
case Type::MyTags: case Type::MyTags:
return _myTags.find((SavedSublist*)nullptr)->second.tags; return _myTags.find((SavedSublist*)nullptr)->second.tags;
case Type::Tags: return _tags; case Type::Tags: return _tags;
case Type::Effects: return _effects;
} }
Unexpected("Type in Reactions::list."); Unexpected("Type in Reactions::list.");
} }
@ -557,25 +569,62 @@ rpl::producer<ReactionId> Reactions::myTagRenamed() const {
return _myTagRenamed.events(); return _myTagRenamed.events();
} }
rpl::producer<> Reactions::effectsUpdates() const {
return _effectsUpdated.events();
}
void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
if (!emoji.emoji().isEmpty()) {
preloadImageFor(emoji);
}
}
void Reactions::preloadEffectImageFor(EffectId id) {
if (id != kFakeEffectId) {
preloadImageFor({ DocumentId(id) });
}
}
void Reactions::preloadImageFor(const ReactionId &id) { void Reactions::preloadImageFor(const ReactionId &id) {
if (_images.contains(id) || id.emoji().isEmpty()) { if (_images.contains(id)) {
return; return;
} }
auto &set = _images.emplace(id).first->second; auto &set = _images.emplace(id).first->second;
const auto i = ranges::find(_available, id, &Reaction::id); set.effect = (id.custom() != 0);
const auto document = (i == end(_available)) auto &list = set.effect ? _effects : _available;
const auto i = ranges::find(list, id, &Reaction::id);
const auto document = (i == end(list))
? nullptr ? nullptr
: i->centerIcon : i->centerIcon
? i->centerIcon ? i->centerIcon
: i->selectAnimation.get(); : i->selectAnimation.get();
if (document) { if (document || (set.effect && i != end(list))) {
loadImage(set, document, !i->centerIcon); if (!set.effect || i->centerIcon) {
} else if (!_waitingForList) { loadImage(set, document, !i->centerIcon);
_waitingForList = true; } else {
generateImage(set, i->title);
}
if (set.effect) {
preloadEffect(*i);
}
} else if (set.effect && !_waitingForEffects) {
_waitingForEffects = true;
refreshEffects();
} else if (!set.effect && !_waitingForReactions) {
_waitingForReactions = true;
refreshDefault(); refreshDefault();
} }
} }
void Reactions::preloadEffect(const Reaction &effect) {
if (effect.aroundAnimation) {
effect.aroundAnimation->createMediaView()->checkStickerLarge();
} else {
const auto premium = effect.selectAnimation;
premium->loadVideoThumbnail(premium->stickerSetOrigin());
}
}
void Reactions::preloadAnimationsFor(const ReactionId &id) { void Reactions::preloadAnimationsFor(const ReactionId &id) {
const auto custom = id.custom(); const auto custom = id.custom();
const auto document = custom ? _owner->document(custom).get() : nullptr; const auto document = custom ? _owner->document(custom).get() : nullptr;
@ -602,14 +651,28 @@ void Reactions::preloadAnimationsFor(const ReactionId &id) {
preload(i->aroundAnimation); preload(i->aroundAnimation);
} }
QImage Reactions::resolveImageFor( QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) {
const ReactionId &emoji, Expects(!emoji.custom());
ImageSize size) {
const auto i = _images.find(emoji); return resolveImageFor(emoji);
}
QImage Reactions::resolveEffectImageFor(EffectId id) {
return (id == kFakeEffectId)
? QImage()
: resolveImageFor({ DocumentId(id) });
}
QImage Reactions::resolveImageFor(const ReactionId &id) {
auto i = _images.find(id);
if (i == end(_images)) { if (i == end(_images)) {
preloadImageFor(emoji); preloadImageFor(id);
i = _images.find(id);
Assert(i != end(_images));
} }
auto &set = (i != end(_images)) ? i->second : _images[emoji]; auto &set = i->second;
set.effect = (id.custom() != 0);
const auto resolve = [&](QImage &image, int size) { const auto resolve = [&](QImage &image, int size) {
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
const auto frameSize = set.fromSelectAnimation const auto frameSize = set.fromSelectAnimation
@ -639,21 +702,18 @@ QImage Reactions::resolveImageFor(
} }
image.setDevicePixelRatio(factor); image.setDevicePixelRatio(factor);
}; };
if (set.bottomInfo.isNull() && set.icon) { if (set.image.isNull() && set.icon) {
resolve(set.bottomInfo, st::reactionInfoImage); resolve(
resolve(set.inlineList, st::reactionInlineImage); set.image,
set.effect ? st::effectInfoImage : st::reactionInlineImage);
crl::async([icon = std::move(set.icon)]{}); crl::async([icon = std::move(set.icon)]{});
} }
switch (size) { return set.image;
case ImageSize::BottomInfo: return set.bottomInfo;
case ImageSize::InlineList: return set.inlineList;
}
Unexpected("ImageSize in Reactions::resolveImageFor.");
} }
void Reactions::resolveImages() { void Reactions::resolveReactionImages() {
for (auto &[id, set] : _images) { for (auto &[id, set] : _images) {
if (!set.bottomInfo.isNull() || set.icon || set.media) { if (set.effect || !set.image.isNull() || set.icon || set.media) {
continue; continue;
} }
const auto i = ranges::find(_available, id, &Reaction::id); const auto i = ranges::find(_available, id, &Reaction::id);
@ -671,14 +731,41 @@ void Reactions::resolveImages() {
} }
} }
void Reactions::resolveEffectImages() {
for (auto &[id, set] : _images) {
if (!set.effect || !set.image.isNull() || set.icon || set.media) {
continue;
}
const auto i = ranges::find(_effects, id, &Reaction::id);
const auto document = (i == end(_effects))
? nullptr
: i->centerIcon
? i->centerIcon
: nullptr;
if (document) {
loadImage(set, document, false);
} else if (i != end(_effects)) {
generateImage(set, i->title);
} else {
LOG(("API Error: Effect '%1' not found!"
).arg(ReactionIdToLog(id)));
}
if (i != end(_effects)) {
preloadEffect(*i);
}
}
}
void Reactions::loadImage( void Reactions::loadImage(
ImageSet &set, ImageSet &set,
not_null<DocumentData*> document, not_null<DocumentData*> document,
bool fromSelectAnimation) { bool fromSelectAnimation) {
if (!set.bottomInfo.isNull() || set.icon) { if (!set.image.isNull() || set.icon) {
return; return;
} else if (!set.media) { } else if (!set.media) {
set.fromSelectAnimation = fromSelectAnimation; if (!set.effect) {
set.fromSelectAnimation = fromSelectAnimation;
}
set.media = document->createMediaView(); set.media = document->createMediaView();
set.media->checkStickerLarge(); set.media->checkStickerLarge();
} }
@ -692,6 +779,26 @@ void Reactions::loadImage(
} }
} }
void Reactions::generateImage(ImageSet &set, const QString &emoji) {
Expects(set.effect);
const auto e = Ui::Emoji::Find(emoji);
Assert(e != nullptr);
const auto large = Ui::Emoji::GetSizeLarge();
const auto factor = style::DevicePixelRatio();
auto image = QImage(large, large, QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
{
QPainter p(&image);
Ui::Emoji::Draw(p, e, large, 0, 0);
}
const auto size = st::effectInfoImage;
set.image = image.scaled(size * factor, size * factor);
set.image.setDevicePixelRatio(factor);
}
void Reactions::setAnimatedIcon(ImageSet &set) { void Reactions::setAnimatedIcon(ImageSet &set) {
const auto size = style::ConvertScale(kSizeForDownscale); const auto size = style::ConvertScale(kSizeForDownscale);
set.icon = Ui::MakeAnimatedIcon({ set.icon = Ui::MakeAnimatedIcon({
@ -845,6 +952,25 @@ void Reactions::requestTags() {
} }
void Reactions::requestEffects() {
if (_effectsRequestId) {
return;
}
auto &api = _owner->session().api();
_effectsRequestId = api.request(MTPmessages_GetAvailableEffects(
MTP_int(_effectsHash)
)).done([=](const MTPmessages_AvailableEffects &result) {
_effectsRequestId = 0;
result.match([&](const MTPDmessages_availableEffects &data) {
updateEffects(data);
}, [&](const MTPDmessages_availableEffectsNotModified &) {
});
}).fail([=] {
_effectsRequestId = 0;
_effectsHash = 0;
}).send();
}
void Reactions::updateTop(const MTPDmessages_reactions &data) { void Reactions::updateTop(const MTPDmessages_reactions &data) {
_topHash = data.vhash().v; _topHash = data.vhash().v;
_topIds = ListFromMTP(data); _topIds = ListFromMTP(data);
@ -886,9 +1012,9 @@ void Reactions::updateDefault(const MTPDmessages_availableReactions &data) {
} }
} }
} }
if (_waitingForList) { if (_waitingForReactions) {
_waitingForList = false; _waitingForReactions = false;
resolveImages(); resolveReactionImages();
} }
defaultUpdated(); defaultUpdated();
} }
@ -944,6 +1070,32 @@ void Reactions::updateTags(const MTPDmessages_reactions &data) {
_tagsUpdated.fire({}); _tagsUpdated.fire({});
} }
void Reactions::updateEffects(const MTPDmessages_availableEffects &data) {
_effectsHash = data.vhash().v;
const auto &list = data.veffects().v;
const auto toCache = [&](DocumentData *document) {
if (document) {
_iconsCache.emplace(document, document->createMediaView());
}
};
for (const auto &document : data.vdocuments().v) {
toCache(_owner->processDocument(document));
}
_effects.clear();
_effects.reserve(list.size());
for (const auto &effect : list) {
if (const auto parsed = parse(effect)) {
_effects.push_back(*parsed);
}
}
if (_waitingForEffects) {
_waitingForEffects = false;
resolveEffectImages();
}
effectsUpdated();
}
void Reactions::recentUpdated() { void Reactions::recentUpdated() {
_topRefreshTimer.callOnce(kTopRequestDelay); _topRefreshTimer.callOnce(kTopRequestDelay);
_recentUpdated.fire({}); _recentUpdated.fire({});
@ -957,6 +1109,7 @@ void Reactions::defaultUpdated() {
} }
refreshMyTags(); refreshMyTags();
refreshTags(); refreshTags();
refreshEffects();
_defaultUpdated.fire({}); _defaultUpdated.fire({});
} }
@ -974,6 +1127,10 @@ void Reactions::tagsUpdated() {
_tagsUpdated.fire({}); _tagsUpdated.fire({});
} }
void Reactions::effectsUpdated() {
_effectsUpdated.fire({});
}
not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() { not_null<CustomEmojiManager::Listener*> Reactions::resolveListener() {
return static_cast<CustomEmojiManager::Listener*>(this); return static_cast<CustomEmojiManager::Listener*>(this);
} }
@ -1116,35 +1273,74 @@ void Reactions::resolve(const ReactionId &id) {
} }
std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) { std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
return entry.match([&](const MTPDavailableReaction &data) { const auto &data = entry.data();
const auto emoji = qs(data.vreaction()); const auto emoji = qs(data.vreaction());
const auto known = (Ui::Emoji::Find(emoji) != nullptr); const auto known = (Ui::Emoji::Find(emoji) != nullptr);
if (!known) { if (!known) {
LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji)); LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji));
} return std::nullopt;
return known }
? std::make_optional(Reaction{ return std::make_optional(Reaction{
.id = ReactionId{ emoji }, .id = ReactionId{ emoji },
.title = qs(data.vtitle()), .title = qs(data.vtitle()),
//.staticIcon = _owner->processDocument(data.vstatic_icon()), //.staticIcon = _owner->processDocument(data.vstatic_icon()),
.appearAnimation = _owner->processDocument( .appearAnimation = _owner->processDocument(
data.vappear_animation()), data.vappear_animation()),
.selectAnimation = _owner->processDocument( .selectAnimation = _owner->processDocument(
data.vselect_animation()), data.vselect_animation()),
//.activateAnimation = _owner->processDocument( //.activateAnimation = _owner->processDocument(
// data.vactivate_animation()), // data.vactivate_animation()),
//.activateEffects = _owner->processDocument( //.activateEffects = _owner->processDocument(
// data.veffect_animation()), // data.veffect_animation()),
.centerIcon = (data.vcenter_icon() .centerIcon = (data.vcenter_icon()
? _owner->processDocument(*data.vcenter_icon()).get() ? _owner->processDocument(*data.vcenter_icon()).get()
: nullptr), : nullptr),
.aroundAnimation = (data.varound_animation() .aroundAnimation = (data.varound_animation()
? _owner->processDocument( ? _owner->processDocument(*data.varound_animation()).get()
*data.varound_animation()).get() : nullptr),
: nullptr), .active = !data.is_inactive(),
.active = !data.is_inactive(), });
}) }
: std::nullopt;
std::optional<Reaction> Reactions::parse(const MTPAvailableEffect &entry) {
const auto &data = entry.data();
const auto emoji = qs(data.vemoticon());
const auto known = (Ui::Emoji::Find(emoji) != nullptr);
if (!known) {
LOG(("API Error: Unknown emoji in effects: %1").arg(emoji));
return std::nullopt;
}
const auto id = DocumentId(data.vid().v);
const auto stickerId = data.veffect_sticker_id().v;
const auto document = _owner->document(stickerId);
if (!document->sticker()) {
LOG(("API Error: Bad sticker in effects: %1").arg(stickerId));
return std::nullopt;
}
const auto aroundId = data.veffect_animation_id().value_or_empty();
const auto around = aroundId
? _owner->document(aroundId).get()
: nullptr;
if (around && !around->sticker()) {
LOG(("API Error: Bad sticker in effects around: %1").arg(aroundId));
return std::nullopt;
}
const auto iconId = data.vstatic_icon_id().value_or_empty();
const auto icon = iconId ? _owner->document(iconId).get() : nullptr;
if (icon && !icon->sticker()) {
LOG(("API Error: Bad sticker in effects icon: %1").arg(iconId));
return std::nullopt;
}
return std::make_optional(Reaction{
.id = ReactionId{ id },
.title = emoji,
.appearAnimation = document,
.selectAnimation = document,
.centerIcon = icon,
.aroundAnimation = around,
.active = true,
.effect = true,
.premium = data.is_premium_required(),
}); });
} }

View file

@ -37,10 +37,13 @@ struct Reaction {
DocumentData *aroundAnimation = nullptr; DocumentData *aroundAnimation = nullptr;
int count = 0; int count = 0;
bool active = false; bool active = false;
bool effect = false;
bool premium = false;
}; };
struct PossibleItemReactionsRef { struct PossibleItemReactionsRef {
std::vector<not_null<const Reaction*>> recent; std::vector<not_null<const Reaction*>> recent;
std::vector<not_null<const Reaction*>> stickers;
bool customAllowed = false; bool customAllowed = false;
bool tags = false; bool tags = false;
}; };
@ -50,6 +53,7 @@ struct PossibleItemReactions {
explicit PossibleItemReactions(const PossibleItemReactionsRef &other); explicit PossibleItemReactions(const PossibleItemReactionsRef &other);
std::vector<Reaction> recent; std::vector<Reaction> recent;
std::vector<Reaction> stickers;
bool customAllowed = false; bool customAllowed = false;
bool tags = false; bool tags = false;
}; };
@ -80,6 +84,7 @@ public:
void refreshMyTags(SavedSublist *sublist = nullptr); void refreshMyTags(SavedSublist *sublist = nullptr);
void refreshMyTagsDelayed(); void refreshMyTagsDelayed();
void refreshTags(); void refreshTags();
void refreshEffects();
enum class Type { enum class Type {
Active, Active,
@ -88,6 +93,7 @@ public:
All, All,
MyTags, MyTags,
Tags, Tags,
Effects,
}; };
[[nodiscard]] const std::vector<Reaction> &list(Type type) const; [[nodiscard]] const std::vector<Reaction> &list(Type type) const;
[[nodiscard]] const std::vector<MyTagInfo> &myTagsInfo() const; [[nodiscard]] const std::vector<MyTagInfo> &myTagsInfo() const;
@ -108,16 +114,19 @@ public:
[[nodiscard]] rpl::producer<> myTagsUpdates() const; [[nodiscard]] rpl::producer<> myTagsUpdates() const;
[[nodiscard]] rpl::producer<> tagsUpdates() const; [[nodiscard]] rpl::producer<> tagsUpdates() const;
[[nodiscard]] rpl::producer<ReactionId> myTagRenamed() const; [[nodiscard]] rpl::producer<ReactionId> myTagRenamed() const;
[[nodiscard]] rpl::producer<> effectsUpdates() const;
void preloadReactionImageFor(const ReactionId &emoji);
[[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji);
// This is used to reserve space for the effect in BottomInfo but not
// actually paint anything, used in case we want to paint icon ourselves.
static constexpr auto kFakeEffectId = EffectId(1);
void preloadEffectImageFor(EffectId id);
[[nodiscard]] QImage resolveEffectImageFor(EffectId id);
enum class ImageSize {
BottomInfo,
InlineList,
};
void preloadImageFor(const ReactionId &emoji);
void preloadAnimationsFor(const ReactionId &emoji); void preloadAnimationsFor(const ReactionId &emoji);
[[nodiscard]] QImage resolveImageFor(
const ReactionId &emoji,
ImageSize size);
void send(not_null<HistoryItem*> item, bool addToRecent); void send(not_null<HistoryItem*> item, bool addToRecent);
[[nodiscard]] bool sending(not_null<HistoryItem*> item) const; [[nodiscard]] bool sending(not_null<HistoryItem*> item) const;
@ -139,11 +148,11 @@ public:
private: private:
struct ImageSet { struct ImageSet {
QImage bottomInfo; QImage image;
QImage inlineList;
std::shared_ptr<DocumentMedia> media; std::shared_ptr<DocumentMedia> media;
std::unique_ptr<Ui::AnimatedIcon> icon; std::unique_ptr<Ui::AnimatedIcon> icon;
bool fromSelectAnimation = false; bool fromSelectAnimation = false;
bool effect = false;
}; };
struct TagsBySublist { struct TagsBySublist {
TagsBySublist() = default; TagsBySublist() = default;
@ -169,6 +178,7 @@ private:
void requestGeneric(); void requestGeneric();
void requestMyTags(SavedSublist *sublist = nullptr); void requestMyTags(SavedSublist *sublist = nullptr);
void requestTags(); void requestTags();
void requestEffects();
void updateTop(const MTPDmessages_reactions &data); void updateTop(const MTPDmessages_reactions &data);
void updateRecent(const MTPDmessages_reactions &data); void updateRecent(const MTPDmessages_reactions &data);
@ -178,11 +188,13 @@ private:
SavedSublist *sublist, SavedSublist *sublist,
const MTPDmessages_savedReactionTags &data); const MTPDmessages_savedReactionTags &data);
void updateTags(const MTPDmessages_reactions &data); void updateTags(const MTPDmessages_reactions &data);
void updateEffects(const MTPDmessages_availableEffects &data);
void recentUpdated(); void recentUpdated();
void defaultUpdated(); void defaultUpdated();
void myTagsUpdated(); void myTagsUpdated();
void tagsUpdated(); void tagsUpdated();
void effectsUpdated();
[[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id); [[nodiscard]] std::optional<Reaction> resolveById(const ReactionId &id);
[[nodiscard]] std::vector<Reaction> resolveByIds( [[nodiscard]] std::vector<Reaction> resolveByIds(
@ -203,13 +215,20 @@ private:
[[nodiscard]] std::optional<Reaction> parse( [[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableReaction &entry); const MTPAvailableReaction &entry);
[[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableEffect &entry);
void preloadEffect(const Reaction &effect);
void preloadImageFor(const ReactionId &id);
[[nodiscard]] QImage resolveImageFor(const ReactionId &id);
void loadImage( void loadImage(
ImageSet &set, ImageSet &set,
not_null<DocumentData*> document, not_null<DocumentData*> document,
bool fromSelectAnimation); bool fromSelectAnimation);
void generateImage(ImageSet &set, const QString &emoji);
void setAnimatedIcon(ImageSet &set); void setAnimatedIcon(ImageSet &set);
void resolveImages(); void resolveReactionImages();
void resolveEffectImages();
void downloadTaskFinished(); void downloadTaskFinished();
void repaintCollected(); void repaintCollected();
@ -233,6 +252,7 @@ private:
std::vector<ReactionId> _topIds; std::vector<ReactionId> _topIds;
base::flat_set<ReactionId> _unresolvedTop; base::flat_set<ReactionId> _unresolvedTop;
std::vector<not_null<DocumentData*>> _genericAnimations; std::vector<not_null<DocumentData*>> _genericAnimations;
std::vector<Reaction> _effects;
ReactionId _favoriteId; ReactionId _favoriteId;
ReactionId _unresolvedFavoriteId; ReactionId _unresolvedFavoriteId;
std::optional<Reaction> _favorite; std::optional<Reaction> _favorite;
@ -249,6 +269,7 @@ private:
rpl::event_stream<SavedSublist*> _myTagsUpdated; rpl::event_stream<SavedSublist*> _myTagsUpdated;
rpl::event_stream<> _tagsUpdated; rpl::event_stream<> _tagsUpdated;
rpl::event_stream<ReactionId> _myTagRenamed; rpl::event_stream<ReactionId> _myTagRenamed;
rpl::event_stream<> _effectsUpdated;
// We need &i->second stay valid while inserting new items. // We need &i->second stay valid while inserting new items.
// So we use std::map instead of base::flat_map here. // So we use std::map instead of base::flat_map here.
@ -271,9 +292,13 @@ private:
mtpRequestId _tagsRequestId = 0; mtpRequestId _tagsRequestId = 0;
uint64 _tagsHash = 0; uint64 _tagsHash = 0;
mtpRequestId _effectsRequestId = 0;
int32 _effectsHash = 0;
base::flat_map<ReactionId, ImageSet> _images; base::flat_map<ReactionId, ImageSet> _images;
rpl::lifetime _imagesLoadLifetime; rpl::lifetime _imagesLoadLifetime;
bool _waitingForList = false; bool _waitingForReactions = false;
bool _waitingForEffects = false;
base::flat_map<FullMsgId, mtpRequestId> _sentRequests; base::flat_map<FullMsgId, mtpRequestId> _sentRequests;

View file

@ -78,10 +78,6 @@ private:
[[nodiscard]] Histories &histories(); [[nodiscard]] Histories &histories();
void subscribeToUpdates(); void subscribeToUpdates();
[[nodiscard]] rpl::producer<MessagesSlice> sourceFromServer(
MessagePosition aroundId,
int limitBefore,
int limitAfter);
void appendClientSideMessages(MessagesSlice &slice); void appendClientSideMessages(MessagesSlice &slice);
[[nodiscard]] bool buildFromData(not_null<Viewer*> viewer); [[nodiscard]] bool buildFromData(not_null<Viewer*> viewer);

View file

@ -21,7 +21,8 @@ namespace {
constexpr auto kSharedMediaLimit = 100; constexpr auto kSharedMediaLimit = 100;
constexpr auto kFirstSharedMediaLimit = 0; constexpr auto kFirstSharedMediaLimit = 0;
constexpr auto kDefaultSearchTimeoutMs = crl::time(70); constexpr auto kHistoryLimit = 50;
constexpr auto kDefaultSearchTimeoutMs = crl::time(200);
} // namespace } // namespace
@ -199,6 +200,60 @@ SearchResult ParseSearchResult(
return result; return result;
} }
HistoryRequest PrepareHistoryRequest(
not_null<PeerData*> peer,
MsgId messageId,
Data::LoadDirection direction) {
const auto minId = 0;
const auto maxId = 0;
const auto limit = kHistoryLimit;
const auto offsetId = [&] {
switch (direction) {
case Data::LoadDirection::Before:
case Data::LoadDirection::Around: return messageId;
case Data::LoadDirection::After: return messageId + 1;
}
Unexpected("Direction in PrepareSearchRequest");
}();
const auto addOffset = [&] {
switch (direction) {
case Data::LoadDirection::Before: return 0;
case Data::LoadDirection::Around: return -limit / 2;
case Data::LoadDirection::After: return -limit;
}
Unexpected("Direction in PrepareSearchRequest");
}();
const auto hash = uint64(0);
const auto offsetDate = int32(0);
const auto mtpOffsetId = int(std::clamp(
offsetId.bare,
int64(0),
int64(0x3FFFFFFF)));
return MTPmessages_GetHistory(
peer->input,
MTP_int(mtpOffsetId),
MTP_int(offsetDate),
MTP_int(addOffset),
MTP_int(limit),
MTP_int(maxId),
MTP_int(minId),
MTP_long(hash));
}
HistoryResult ParseHistoryResult(
not_null<PeerData*> peer,
MsgId messageId,
Data::LoadDirection direction,
const HistoryRequestResult &data) {
return ParseSearchResult(
peer,
Storage::SharedMediaType::kCount,
messageId,
direction,
data);
}
SearchController::CacheEntry::CacheEntry( SearchController::CacheEntry::CacheEntry(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const Query &query) const Query &query)

View file

@ -32,7 +32,11 @@ struct SearchResult {
using SearchRequest = MTPmessages_Search; using SearchRequest = MTPmessages_Search;
using SearchRequestResult = MTPmessages_Messages; using SearchRequestResult = MTPmessages_Messages;
std::optional<SearchRequest> PrepareSearchRequest( using HistoryResult = SearchResult;
using HistoryRequest = MTPmessages_GetHistory;
using HistoryRequestResult = MTPmessages_Messages;
[[nodiscard]] std::optional<SearchRequest> PrepareSearchRequest(
not_null<PeerData*> peer, not_null<PeerData*> peer,
MsgId topicRootId, MsgId topicRootId,
Storage::SharedMediaType type, Storage::SharedMediaType type,
@ -40,13 +44,24 @@ std::optional<SearchRequest> PrepareSearchRequest(
MsgId messageId, MsgId messageId,
Data::LoadDirection direction); Data::LoadDirection direction);
SearchResult ParseSearchResult( [[nodiscard]] SearchResult ParseSearchResult(
not_null<PeerData*> peer, not_null<PeerData*> peer,
Storage::SharedMediaType type, Storage::SharedMediaType type,
MsgId messageId, MsgId messageId,
Data::LoadDirection direction, Data::LoadDirection direction,
const SearchRequestResult &data); const SearchRequestResult &data);
[[nodiscard]] HistoryRequest PrepareHistoryRequest(
not_null<PeerData*> peer,
MsgId messageId,
Data::LoadDirection direction);
[[nodiscard]] HistoryResult ParseHistoryResult(
not_null<PeerData*> peer,
MsgId messageId,
Data::LoadDirection direction,
const HistoryRequestResult &data);
class SearchController final { class SearchController final {
public: public:
using IdsList = Storage::SparseIdsList; using IdsList = Storage::SparseIdsList;

View file

@ -1830,11 +1830,16 @@ rpl::producer<not_null<HistoryItem*>> Session::itemDataChanges() const {
} }
void Session::requestItemTextRefresh(not_null<HistoryItem*> item) { void Session::requestItemTextRefresh(not_null<HistoryItem*> item) {
if (const auto i = _views.find(item); i != _views.end()) { const auto call = [&](not_null<HistoryItem*> item) {
for (const auto &view : i->second) { enumerateItemViews(item, [&](not_null<ViewElement*> view) {
view->itemTextUpdated(); view->itemTextUpdated();
} });
requestItemResize(item); requestItemResize(item);
};
if (const auto group = groups().find(item)) {
call(group->items.front());
} else {
call(item);
} }
} }
@ -4312,29 +4317,27 @@ void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) {
} }
void Session::sendWebPageGamePollNotifications() { void Session::sendWebPageGamePollNotifications() {
auto resize = std::vector<not_null<ViewElement*>>();
for (const auto &page : base::take(_webpagesUpdated)) { for (const auto &page : base::take(_webpagesUpdated)) {
_webpageUpdates.fire_copy(page); _webpageUpdates.fire_copy(page);
const auto i = _webpageViews.find(page); if (const auto i = _webpageViews.find(page)
if (i != _webpageViews.end()) { ; i != _webpageViews.end()) {
for (const auto &view : i->second) { resize.insert(end(resize), begin(i->second), end(i->second));
requestViewResize(view);
}
} }
} }
for (const auto &game : base::take(_gamesUpdated)) { for (const auto &game : base::take(_gamesUpdated)) {
if (const auto i = _gameViews.find(game); i != _gameViews.end()) { if (const auto i = _gameViews.find(game); i != _gameViews.end()) {
for (const auto &view : i->second) { resize.insert(end(resize), begin(i->second), end(i->second));
requestViewResize(view);
}
} }
} }
for (const auto &poll : base::take(_pollsUpdated)) { for (const auto &poll : base::take(_pollsUpdated)) {
if (const auto i = _pollViews.find(poll); i != _pollViews.end()) { if (const auto i = _pollViews.find(poll); i != _pollViews.end()) {
for (const auto &view : i->second) { resize.insert(end(resize), begin(i->second), end(i->second));
requestViewResize(view);
}
} }
} }
for (const auto &view : resize) {
requestViewResize(view);
}
} }
rpl::producer<not_null<WebPageData*>> Session::webPageUpdates() const { rpl::producer<not_null<WebPageData*>> Session::webPageUpdates() const {
@ -4636,7 +4639,9 @@ void Session::insertCheckedServiceNotification(
MTPMessageReactions(), MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(), MTPVector<MTPRestrictionReason>(),
MTPint(), // ttl_period MTPint(), // ttl_period
MTPint()), // quick_reply_shortcut_id MTPint(), // quick_reply_shortcut_id
MTPlong(), // effect
MTPFactCheck()),
localFlags, localFlags,
NewMessageType::Unread); NewMessageType::Unread);
} }

View file

@ -138,6 +138,7 @@ using PollId = uint64;
using WallPaperId = uint64; using WallPaperId = uint64;
using CallId = uint64; using CallId = uint64;
using BotAppId = uint64; using BotAppId = uint64;
using EffectId = uint64;
constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL);
@ -320,6 +321,8 @@ enum class MessageFlag : uint64 {
ReactionsAreTags = (1ULL << 43), ReactionsAreTags = (1ULL << 43),
ShortcutMessage = (1ULL << 44), ShortcutMessage = (1ULL << 44),
EffectWatched = (1ULL << 45),
}; };
inline constexpr bool is_flag_type(MessageFlag) { return true; } inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>; using MessageFlags = base::flags<MessageFlag>;

View file

@ -54,6 +54,8 @@ enum class WebPageType : uint8 {
VoiceChat, VoiceChat,
Livestream, Livestream,
Factcheck,
}; };
[[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type); [[nodiscard]] WebPageType ParseWebPageType(const MTPDwebPage &type);
[[nodiscard]] bool IgnoreIv(WebPageType type); [[nodiscard]] bool IgnoreIv(WebPageType type);

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "boxes/peers/edit_forum_topic_box.h" // MakeTopicIconEmoji.
#include "chat_helpers/stickers_emoji_pack.h" #include "chat_helpers/stickers_emoji_pack.h"
#include "main/main_app_config.h" #include "main/main_app_config.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_document_media.h" #include "data/data_document_media.h"
#include "data/data_file_origin.h" #include "data/data_file_origin.h"
#include "data/data_forum_topic.h" // ParseTopicIconEmojiEntity.
#include "data/data_peer.h" #include "data/data_peer.h"
#include "data/data_message_reactions.h" #include "data/data_message_reactions.h"
#include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers.h"
@ -539,6 +541,8 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
const auto size = EmojiSizeFromTag(tag) / ratio; const auto size = EmojiSizeFromTag(tag) / ratio;
return userpic(data, std::move(update), size); return userpic(data, std::move(update), size);
} else if (const auto parsed = Data::ParseTopicIconEmojiEntity(data)) {
return MakeTopicIconEmoji(parsed, std::move(update), tag);
} }
const auto parsed = ParseCustomEmojiData(data); const auto parsed = ParseCustomEmojiData(data);
return parsed return parsed
@ -594,13 +598,21 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::userpic(
if (v.size() != 5 && v.size() != 1) { if (v.size() != 5 && v.size() != 1) {
return nullptr; return nullptr;
} }
const auto id = PeerId(v[0].toULongLong()); auto image = std::shared_ptr<Ui::DynamicImage>();
if (v[0] == u"self"_q) {
image = Ui::MakeSavedMessagesThumbnail();
} else if (v[0] == u"replies"_q) {
image = Ui::MakeRepliesThumbnail();
} else {
const auto id = PeerId(v[0].toULongLong());
image = Ui::MakeUserpicThumbnail(_owner->peer(id));
}
const auto padding = (v.size() == 5) const auto padding = (v.size() == 5)
? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt()) ? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt())
: QMargins(); : QMargins();
return std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>( return std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>(
data.toString(), data.toString(),
Ui::MakeUserpicThumbnail(_owner->peer(id)), std::move(image),
std::move(update), std::move(update),
padding, padding,
size); size);
@ -988,10 +1000,16 @@ QString CustomEmojiManager::registerInternalEmoji(
[[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData( [[nodiscard]] QString CustomEmojiManager::peerUserpicEmojiData(
not_null<PeerData*> peer, not_null<PeerData*> peer,
QMargins padding) { QMargins padding,
return UserpicEmojiPrefix() bool respectSavedRepliesEtc) {
+ QString::number(peer->id.value) const auto id = !respectSavedRepliesEtc
+ InternalPadding(padding); ? QString::number(peer->id.value)
: peer->isSelf()
? u"self"_q
: peer->isRepliesChat()
? u"replies"_q
: QString::number(peer->id.value);
return UserpicEmojiPrefix() + id + InternalPadding(padding);
} }
int FrameSizeFromTag(SizeTag tag) { int FrameSizeFromTag(SizeTag tag) {

View file

@ -94,7 +94,8 @@ public:
[[nodiscard]] QString peerUserpicEmojiData( [[nodiscard]] QString peerUserpicEmojiData(
not_null<PeerData*> peer, not_null<PeerData*> peer,
QMargins padding = {}); QMargins padding = {},
bool respectSavedRepliesEtc = false);
[[nodiscard]] uint64 coloredSetId() const; [[nodiscard]] uint64 coloredSetId() const;

View file

@ -46,6 +46,11 @@ defaultForumTopicIcon: ForumTopicIcon {
font: font(bold 11px); font: font(bold 11px);
textTop: 2px; textTop: 2px;
} }
normalForumTopicIcon: ForumTopicIcon {
size: 19px;
font: font(bold 10px);
textTop: 2px;
}
largeForumTopicIcon: ForumTopicIcon { largeForumTopicIcon: ForumTopicIcon {
size: 26px; size: 26px;
font: font(bold 13px); font: font(bold 13px);
@ -517,7 +522,7 @@ forumTopicRow: DialogRow(defaultDialogRow) {
photoSize: 20px; photoSize: 20px;
nameLeft: 39px; nameLeft: 39px;
nameTop: 7px; nameTop: 7px;
textLeft: 68px; textLeft: 39px;
textTop: 29px; textTop: 29px;
unreadMarkDiameter: 8px; unreadMarkDiameter: 8px;
} }
@ -649,7 +654,7 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {
barRadius: 2px; barRadius: 2px;
barFg: transparent; barFg: transparent;
barSnapToLabel: true; barSnapToLabel: true;
strictSkip: 34px; strictSkip: 18px;
labelTop: 7px; labelTop: 7px;
labelStyle: semiboldTextStyle; labelStyle: semiboldTextStyle;
labelFg: windowSubTextFg; labelFg: windowSubTextFg;
@ -659,7 +664,7 @@ dialogsSearchTabs: SettingsSlider(defaultSettingsSlider) {
rippleBgActive: lightButtonBgOver; rippleBgActive: lightButtonBgOver;
ripple: defaultRippleAnimation; ripple: defaultRippleAnimation;
} }
dialogsSearchTabsPadding: 8px;
dialogsStoriesList: DialogsStoriesList { dialogsStoriesList: DialogsStoriesList {
small: dialogsStories; small: dialogsStories;

File diff suppressed because it is too large Load diff

View file

@ -7,14 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/flags.h"
#include "base/object_ptr.h"
#include "base/timer.h"
#include "dialogs/dialogs_key.h" #include "dialogs/dialogs_key.h"
#include "data/data_messages.h" #include "data/data_messages.h"
#include "ui/dragging_scroll_manager.h" #include "ui/dragging_scroll_manager.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "ui/userpic_view.h" #include "ui/userpic_view.h"
#include "base/flags.h"
#include "base/object_ptr.h"
namespace style { namespace style {
struct DialogRow; struct DialogRow;
@ -59,6 +60,7 @@ class Row;
class FakeRow; class FakeRow;
class IndexedList; class IndexedList;
class SearchTags; class SearchTags;
class SearchEmpty;
struct ChosenRow { struct ChosenRow {
Key key; Key key;
@ -118,9 +120,15 @@ public:
void clearFilter(); void clearFilter();
void refresh(bool toTop = false); void refresh(bool toTop = false);
void refreshEmptyLabel(); void refreshEmpty();
void resizeEmptyLabel(); void resizeEmpty();
[[nodiscard]] bool isUserpicPress() const;
[[nodiscard]] bool isUserpicPressOnWide() const;
void cancelChatPreview();
bool scheduleChatPreview(QPoint positionOverride);
bool showChatPreview();
void chatPreviewShown(bool shown, RowDescriptor row = {});
bool chooseRow( bool chooseRow(
Qt::KeyboardModifiers modifiers = {}, Qt::KeyboardModifiers modifiers = {},
MsgId pressedTopicRootId = {}); MsgId pressedTopicRootId = {});
@ -134,19 +142,12 @@ public:
[[nodiscard]] not_null<const style::DialogRow*> st() const { [[nodiscard]] not_null<const style::DialogRow*> st() const {
return _st; return _st;
} }
[[nodiscard]] bool waitingForSearch() const {
return _waitingForSearch;
}
[[nodiscard]] bool hasFilteredResults() const; [[nodiscard]] bool hasFilteredResults() const;
void searchInChat( void applySearchState(SearchState state);
Key key,
PeerData *from,
std::vector<Data::ReactionId> tags);
[[nodiscard]] auto searchTagsChanges() const [[nodiscard]] auto searchTagsChanges() const
-> rpl::producer<std::vector<Data::ReactionId>>; -> rpl::producer<std::vector<Data::ReactionId>>;
void applyFilterUpdate(QString newFilter, bool force = false);
void onHashtagFilterUpdate(QStringView newFilter); void onHashtagFilterUpdate(QStringView newFilter);
void appendToFiltered(Key key); void appendToFiltered(Key key);
@ -156,6 +157,7 @@ public:
void setLoadMoreFilteredCallback(Fn<void()> callback); void setLoadMoreFilteredCallback(Fn<void()> callback);
[[nodiscard]] rpl::producer<> listBottomReached() const; [[nodiscard]] rpl::producer<> listBottomReached() const;
[[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const; [[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const;
[[nodiscard]] rpl::producer<> cancelSearchRequests() const;
[[nodiscard]] rpl::producer<ChosenRow> chosenRow() const; [[nodiscard]] rpl::producer<ChosenRow> chosenRow() const;
[[nodiscard]] rpl::producer<> updated() const; [[nodiscard]] rpl::producer<> updated() const;
@ -163,7 +165,6 @@ public:
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const; [[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const; [[nodiscard]] rpl::producer<Ui::ScrollToRequest> dialogMoved() const;
[[nodiscard]] rpl::producer<> searchMessages() const; [[nodiscard]] rpl::producer<> searchMessages() const;
[[nodiscard]] rpl::producer<> cancelSearchInChatRequests() const;
[[nodiscard]] rpl::producer<QString> completeHashtagRequests() const; [[nodiscard]] rpl::producer<QString> completeHashtagRequests() const;
[[nodiscard]] rpl::producer<> refreshHashtagsRequests() const; [[nodiscard]] rpl::producer<> refreshHashtagsRequests() const;
@ -174,6 +175,11 @@ public:
void parentGeometryChanged(); void parentGeometryChanged();
bool processTouchEvent(not_null<QTouchEvent*> e);
[[nodiscard]] rpl::producer<> touchCancelRequests() const {
return _touchCancelRequests.events();
}
protected: protected:
void visibleTopBottomUpdated( void visibleTopBottomUpdated(
int visibleTop, int visibleTop,
@ -253,6 +259,7 @@ private:
QPoint globalPosition, QPoint globalPosition,
Qt::MouseButton button, Qt::MouseButton button,
Qt::KeyboardModifiers modifiers); Qt::KeyboardModifiers modifiers);
void processGlobalForceClick(QPoint globalPosition);
void clearIrrelevantState(); void clearIrrelevantState();
void selectByMouse(QPoint globalPosition); void selectByMouse(QPoint globalPosition);
void preloadRowsData(); void preloadRowsData();
@ -332,6 +339,7 @@ private:
[[nodiscard]] int filteredIndex(int y) const; [[nodiscard]] int filteredIndex(int y) const;
[[nodiscard]] int filteredHeight(int till = -1) const; [[nodiscard]] int filteredHeight(int till = -1) const;
[[nodiscard]] int peerSearchOffset() const; [[nodiscard]] int peerSearchOffset() const;
[[nodiscard]] int searchTagsOffset() const;
[[nodiscard]] int searchInChatOffset() const; [[nodiscard]] int searchInChatOffset() const;
[[nodiscard]] int searchedOffset() const; [[nodiscard]] int searchedOffset() const;
[[nodiscard]] int searchInChatSkip() const; [[nodiscard]] int searchInChatSkip() const;
@ -347,6 +355,9 @@ private:
Painter &p, Painter &p,
not_null<const PeerSearchResult*> result, not_null<const PeerSearchResult*> result,
const Ui::PaintContext &context); const Ui::PaintContext &context);
void paintSearchTags(
Painter &p,
const Ui::PaintContext &context) const;
void paintSearchInChat( void paintSearchInChat(
Painter &p, Painter &p,
const Ui::PaintContext &context) const; const Ui::PaintContext &context) const;
@ -388,21 +399,24 @@ private:
void clearSearchResults(bool clearPeerSearchResults = true); void clearSearchResults(bool clearPeerSearchResults = true);
void updateSelectedRow(Key key = Key()); void updateSelectedRow(Key key = Key());
void trackSearchResultsHistory(not_null<History*> history); void trackSearchResultsHistory(not_null<History*> history);
void trackSearchResultsForum(Data::Forum *forum);
[[nodiscard]] QBrush currentBg() const; [[nodiscard]] QBrush currentBg() const;
[[nodiscard]] RowDescriptor computeChatPreviewRow() const;
[[nodiscard]] const std::vector<Key> &pinnedChatsOrder() const; [[nodiscard]] const std::vector<Key> &pinnedChatsOrder() const;
void checkReorderPinnedStart(QPoint localPosition); void checkReorderPinnedStart(QPoint localPosition);
void startReorderPinned(QPoint localPosition);
int updateReorderIndexGetCount(); int updateReorderIndexGetCount();
bool updateReorderPinned(QPoint localPosition); bool updateReorderPinned(QPoint localPosition);
void finishReorderPinned(); void finishReorderPinned();
bool finishReorderOnRelease();
void stopReorderPinned(); void stopReorderPinned();
int countPinnedIndex(Row *ofRow); int countPinnedIndex(Row *ofRow);
void savePinnedOrder(); void savePinnedOrder();
bool pinnedShiftAnimationCallback(crl::time now); bool pinnedShiftAnimationCallback(crl::time now);
void handleChatListEntryRefreshes(); void handleChatListEntryRefreshes();
void moveCancelSearchButtons(); void moveCancelSearchButtons();
void dragPinnedFromTouch();
void saveChatsFilterScrollState(FilterId filterId); void saveChatsFilterScrollState(FilterId filterId);
void restoreChatsFilterScrollState(FilterId filterId); void restoreChatsFilterScrollState(FilterId filterId);
@ -459,7 +473,6 @@ private:
int _filteredSelected = -1; int _filteredSelected = -1;
int _filteredPressed = -1; int _filteredPressed = -1;
bool _waitingForSearch = false;
EmptyState _emptyState = EmptyState::None; EmptyState _emptyState = EmptyState::None;
QString _peerSearchQuery; QString _peerSearchQuery;
@ -477,22 +490,21 @@ private:
WidgetState _state = WidgetState::Default; WidgetState _state = WidgetState::Default;
object_ptr<Ui::RpWidget> _loadingAnimation = { nullptr };
object_ptr<SearchEmpty> _searchEmpty = { nullptr };
SearchState _searchEmptyState;
object_ptr<Ui::FlatLabel> _empty = { nullptr }; object_ptr<Ui::FlatLabel> _empty = { nullptr };
object_ptr<Ui::IconButton> _cancelSearchInChat;
object_ptr<Ui::IconButton> _cancelSearchFromUser; object_ptr<Ui::IconButton> _cancelSearchFromUser;
rpl::event_stream<> _cancelSearch;
Ui::DraggingScrollManager _draggingScroll; Ui::DraggingScrollManager _draggingScroll;
Key _searchInChat; SearchState _searchState;
History *_searchInMigrated = nullptr; History *_searchInMigrated = nullptr;
PeerData *_searchFromPeer = nullptr;
PeerData *_searchFromShown = nullptr; PeerData *_searchFromShown = nullptr;
mutable Ui::PeerUserpicView _searchInChatUserpic;
mutable Ui::PeerUserpicView _searchFromUserUserpic; mutable Ui::PeerUserpicView _searchFromUserUserpic;
Ui::Text::String _searchInChatText;
Ui::Text::String _searchFromUserText; Ui::Text::String _searchFromUserText;
std::unique_ptr<SearchTags> _searchTags; std::unique_ptr<SearchTags> _searchTags;
std::vector<Data::ReactionId> _searchTagsSelected;
int _searchTagsLeft = 0; int _searchTagsLeft = 0;
RowDescriptor _menuRow; RowDescriptor _menuRow;
@ -514,11 +526,20 @@ private:
rpl::event_stream<QString> _completeHashtagRequests; rpl::event_stream<QString> _completeHashtagRequests;
rpl::event_stream<> _refreshHashtagsRequests; rpl::event_stream<> _refreshHashtagsRequests;
RowDescriptor _chatPreviewRow;
bool _chatPreviewScheduled = false;
std::optional<QPoint> _chatPreviewTouchGlobal;
base::Timer _touchDragPinnedTimer;
std::optional<QPoint> _touchDragStartGlobal;
std::optional<QPoint> _touchDragNowGlobal;
rpl::event_stream<> _touchCancelRequests;
rpl::variable<ChildListShown> _childListShown; rpl::variable<ChildListShown> _childListShown;
float64 _narrowRatio = 0.; float64 _narrowRatio = 0.;
bool _geometryInited = false; bool _geometryInited = false;
bool _savedSublists = false; bool _savedSublists = false;
bool _searchLoading = false;
base::unique_qptr<Ui::PopupMenu> _menu; base::unique_qptr<Ui::PopupMenu> _menu;

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_folder.h" #include "data/data_folder.h"
#include "data/data_forum_topic.h" #include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h" #include "data/data_saved_sublist.h"
#include "dialogs/ui/chat_search_tabs.h"
#include "history/history.h" #include "history/history.h"
namespace Dialogs { namespace Dialogs {
@ -84,4 +85,22 @@ PeerData *Key::peer() const {
return nullptr; return nullptr;
} }
[[nodiscard]] bool SearchState::empty() const {
return !inChat
&& tags.empty()
&& QStringView(query).trimmed().isEmpty();
}
ChatSearchTab SearchState::defaultTabForMe() const {
return inChat.topic()
? ChatSearchTab::ThisTopic
: (inChat.history() || inChat.sublist())
? ChatSearchTab::ThisPeer
: ChatSearchTab::MyMessages;
}
bool SearchState::filterChatsList() const {
return !inChat && (tab == ChatSearchTab::MyMessages);
}
} // namespace Dialogs } // namespace Dialogs

View file

@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "base/qt/qt_compare.h"
#include "data/data_message_reaction_id.h"
class History; class History;
class PeerData; class PeerData;
@ -15,11 +18,13 @@ class Thread;
class Folder; class Folder;
class ForumTopic; class ForumTopic;
class SavedSublist; class SavedSublist;
struct ReactionId;
} // namespace Data } // namespace Data
namespace Dialogs { namespace Dialogs {
class Entry; class Entry;
enum class ChatSearchTab : uchar;
class Key { class Key {
public: public:
@ -120,4 +125,27 @@ struct EntryState {
= default; = default;
}; };
struct SearchState {
Key inChat;
PeerData *fromPeer = nullptr;
std::vector<Data::ReactionId> tags;
ChatSearchTab tab = {};
QString query;
[[nodiscard]] bool empty() const;
[[nodiscard]] ChatSearchTab defaultTabForMe() const;
[[nodiscard]] bool filterChatsList() const;
explicit operator bool() const {
return !empty();
}
friend inline auto operator<=>(
const SearchState&,
const SearchState&) noexcept = default;
friend inline bool operator==(
const SearchState&,
const SearchState&) = default;
};
} // namespace Dialogs } // namespace Dialogs

View file

@ -168,7 +168,7 @@ void SearchTags::fill(
.selected = ranges::contains(selected, id), .selected = ranges::contains(selected, id),
}); });
if (!customId) { if (!customId) {
_owner->reactions().preloadImageFor(id); _owner->reactions().preloadReactionImageFor(id);
} }
}; };
if (!premium) { if (!premium) {
@ -335,9 +335,7 @@ void SearchTags::paint(
paintBackground(p, geometry, tag); paintBackground(p, geometry, tag);
paintText(p, geometry, tag); paintText(p, geometry, tag);
if (!tag.custom && !tag.promo && tag.image.isNull()) { if (!tag.custom && !tag.promo && tag.image.isNull()) {
tag.image = _owner->reactions().resolveImageFor( tag.image = _owner->reactions().resolveReactionImageFor(tag.id);
tag.id,
::Data::Reactions::ImageSize::InlineList);
} }
const auto inner = geometry.marginsRemoved(padding); const auto inner = geometry.marginsRemoved(padding);
const auto image = QRect( const auto image = QRect(

Some files were not shown because too many files have changed in this diff Show more