diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8e99d7162d..68bab389ca 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -64,7 +64,7 @@ jobs: - name: First set up. run: | sudo chown -R `whoami`:admin /usr/local/share - brew install automake ninja pkg-config + brew install automake ninja pkg-config nasm meson # Disable spotlight. sudo mdutil -a -i off diff --git a/.gitmodules b/.gitmodules index 9cbb4a41a3..b489d4b7ec 100644 --- a/.gitmodules +++ b/.gitmodules @@ -100,3 +100,6 @@ [submodule "Telegram/ThirdParty/libprisma"] path = Telegram/ThirdParty/libprisma url = https://github.com/desktop-app/libprisma.git +[submodule "Telegram/ThirdParty/xdg-desktop-portal"] + path = Telegram/ThirdParty/xdg-desktop-portal + url = https://github.com/flatpak/xdg-desktop-portal.git diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7b41428e19..ee1f569e41 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -248,6 +248,8 @@ PRIVATE boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp boxes/filters/edit_filter_chats_list.h + boxes/filters/edit_filter_chats_preview.cpp + boxes/filters/edit_filter_chats_preview.h boxes/filters/edit_filter_links.cpp boxes/filters/edit_filter_links.h boxes/peers/add_bot_to_chat_box.cpp @@ -514,6 +516,14 @@ PRIVATE core/version.h countries/countries_manager.cpp countries/countries_manager.h + data/business/data_business_chatbots.cpp + data/business/data_business_chatbots.h + data/business/data_business_common.cpp + data/business/data_business_common.h + data/business/data_business_info.cpp + data/business/data_business_info.h + data/business/data_shortcut_messages.cpp + data/business/data_shortcut_messages.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -1345,6 +1355,22 @@ PRIVATE profile/profile_block_widget.h profile/profile_cover_drop_area.cpp profile/profile_cover_drop_area.h + settings/business/settings_away_message.cpp + settings/business/settings_away_message.h + settings/business/settings_shortcut_messages.cpp + settings/business/settings_shortcut_messages.h + settings/business/settings_chatbots.cpp + settings/business/settings_chatbots.h + settings/business/settings_greeting.cpp + settings/business/settings_greeting.h + settings/business/settings_location.cpp + settings/business/settings_location.h + settings/business/settings_quick_replies.cpp + settings/business/settings_quick_replies.h + settings/business/settings_recipients_helper.cpp + settings/business/settings_recipients_helper.h + settings/business/settings_working_hours.cpp + settings/business/settings_working_hours.h settings/cloud_password/settings_cloud_password_common.cpp settings/cloud_password/settings_cloud_password_common.h settings/cloud_password/settings_cloud_password_email.cpp @@ -1363,6 +1389,8 @@ PRIVATE settings/settings_advanced.h settings/settings_blocked_peers.cpp settings/settings_blocked_peers.h + settings/settings_business.cpp + settings/settings_business.h settings/settings_chat.cpp settings/settings_chat.h settings/settings_calls.cpp @@ -1722,7 +1750,7 @@ else() ) include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake) - generate_dbus(Telegram org.freedesktop.portal. XdpInhibit ${src_loc}/platform/linux/org.freedesktop.portal.Inhibit.xml) + generate_dbus(Telegram org.freedesktop.portal. XdpBackground ${third_party_loc}/xdg-desktop-portal/data/org.freedesktop.portal.Background.xml) if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) target_link_libraries(Telegram @@ -1796,6 +1824,7 @@ set_target_properties(Telegram PROPERTIES XCODE_ATTRIBUTE_ALWAYS_SEARCH_USER_PATHS NO XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY libc++ XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS --deep + XCODE_ATTRIBUTE_CLANG_DEBUG_INFORMATION_LEVEL $,default,line-tables-only> ) set(entitlement_sources "${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Telegram.entitlements" diff --git a/Telegram/Resources/animations/greeting.tgs b/Telegram/Resources/animations/greeting.tgs new file mode 100644 index 0000000000..dd1ab78d28 Binary files /dev/null and b/Telegram/Resources/animations/greeting.tgs differ diff --git a/Telegram/Resources/animations/hours.tgs b/Telegram/Resources/animations/hours.tgs new file mode 100644 index 0000000000..d49a48c326 Binary files /dev/null and b/Telegram/Resources/animations/hours.tgs differ diff --git a/Telegram/Resources/animations/location.tgs b/Telegram/Resources/animations/location.tgs new file mode 100644 index 0000000000..32ba54f16c Binary files /dev/null and b/Telegram/Resources/animations/location.tgs differ diff --git a/Telegram/Resources/animations/phone.tgs b/Telegram/Resources/animations/phone.tgs new file mode 100644 index 0000000000..7541526afb Binary files /dev/null and b/Telegram/Resources/animations/phone.tgs differ diff --git a/Telegram/Resources/animations/robot.tgs b/Telegram/Resources/animations/robot.tgs new file mode 100644 index 0000000000..0076344f4c Binary files /dev/null and b/Telegram/Resources/animations/robot.tgs differ diff --git a/Telegram/Resources/animations/sleep.tgs b/Telegram/Resources/animations/sleep.tgs new file mode 100644 index 0000000000..b766d6e438 Binary files /dev/null and b/Telegram/Resources/animations/sleep.tgs differ diff --git a/Telegram/Resources/animations/writing.tgs b/Telegram/Resources/animations/writing.tgs new file mode 100644 index 0000000000..47caac05a2 Binary files /dev/null and b/Telegram/Resources/animations/writing.tgs differ diff --git a/Telegram/Resources/art/business_logo.png b/Telegram/Resources/art/business_logo.png new file mode 100644 index 0000000000..25c357e50b Binary files /dev/null and b/Telegram/Resources/art/business_logo.png differ diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index 79b680cc2c..102f5f3a5d 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -559,3 +559,26 @@ div.toast_shown { opacity: 0; user-select: none; } + +.bot_buttons_table { + border-spacing: 0px 2px; + width: 100%; +} +.bot_button { + border-radius: 8px; + text-align: center; + vertical-align: middle; + background-color: #168acd40; +} +.bot_button_row { + display: table; + table-layout: fixed; + padding: 0px; + width:100%; +} +.bot_button_row div { + display: table-cell; +} +.bot_button_column_separator { + width: 2px +} diff --git a/Telegram/Resources/export_html/js/script.js b/Telegram/Resources/export_html/js/script.js index 8d25f53022..2842322020 100644 --- a/Telegram/Resources/export_html/js/script.js +++ b/Telegram/Resources/export_html/js/script.js @@ -62,6 +62,12 @@ function ShowNotAvailableEmoji() { return false; } +function ShowTextCopied(content) { + navigator.clipboard.writeText(content); + ShowToast("Text copied to clipboard."); + return false; +} + function ShowSpoiler(target) { if (target.classList.contains("hidden")) { target.classList.toggle("hidden"); diff --git a/Telegram/Resources/icons/chat/large_away.png b/Telegram/Resources/icons/chat/large_away.png new file mode 100644 index 0000000000..b0a943e0c0 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_away.png differ diff --git a/Telegram/Resources/icons/chat/large_away@2x.png b/Telegram/Resources/icons/chat/large_away@2x.png new file mode 100644 index 0000000000..ca2b6adb61 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_away@2x.png differ diff --git a/Telegram/Resources/icons/chat/large_away@3x.png b/Telegram/Resources/icons/chat/large_away@3x.png new file mode 100644 index 0000000000..064090f768 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_away@3x.png differ diff --git a/Telegram/Resources/icons/chat/large_greeting.png b/Telegram/Resources/icons/chat/large_greeting.png new file mode 100644 index 0000000000..0b1cb033e5 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_greeting.png differ diff --git a/Telegram/Resources/icons/chat/large_greeting@2x.png b/Telegram/Resources/icons/chat/large_greeting@2x.png new file mode 100644 index 0000000000..66fd705ad5 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_greeting@2x.png differ diff --git a/Telegram/Resources/icons/chat/large_greeting@3x.png b/Telegram/Resources/icons/chat/large_greeting@3x.png new file mode 100644 index 0000000000..cd08060ff3 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_greeting@3x.png differ diff --git a/Telegram/Resources/icons/chat/large_quickreply.png b/Telegram/Resources/icons/chat/large_quickreply.png new file mode 100644 index 0000000000..084b399ea3 Binary files /dev/null and b/Telegram/Resources/icons/chat/large_quickreply.png differ diff --git a/Telegram/Resources/icons/chat/large_quickreply@2x.png b/Telegram/Resources/icons/chat/large_quickreply@2x.png new file mode 100644 index 0000000000..5ec1ffe4fe Binary files /dev/null and b/Telegram/Resources/icons/chat/large_quickreply@2x.png differ diff --git a/Telegram/Resources/icons/chat/large_quickreply@3x.png b/Telegram/Resources/icons/chat/large_quickreply@3x.png new file mode 100644 index 0000000000..8bc18f9ade Binary files /dev/null and b/Telegram/Resources/icons/chat/large_quickreply@3x.png differ diff --git a/Telegram/Resources/icons/folders/folder_existing_chats.png b/Telegram/Resources/icons/folders/folder_existing_chats.png new file mode 100644 index 0000000000..e54a104259 Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_existing_chats.png differ diff --git a/Telegram/Resources/icons/folders/folder_existing_chats@2x.png b/Telegram/Resources/icons/folders/folder_existing_chats@2x.png new file mode 100644 index 0000000000..e3a73f7e1f Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_existing_chats@2x.png differ diff --git a/Telegram/Resources/icons/folders/folder_existing_chats@3x.png b/Telegram/Resources/icons/folders/folder_existing_chats@3x.png new file mode 100644 index 0000000000..2b37db754e Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_existing_chats@3x.png differ diff --git a/Telegram/Resources/icons/folders/folder_new_chats.png b/Telegram/Resources/icons/folders/folder_new_chats.png new file mode 100644 index 0000000000..03c8380d44 Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_new_chats.png differ diff --git a/Telegram/Resources/icons/folders/folder_new_chats@2x.png b/Telegram/Resources/icons/folders/folder_new_chats@2x.png new file mode 100644 index 0000000000..f91df76357 Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_new_chats@2x.png differ diff --git a/Telegram/Resources/icons/folders/folder_new_chats@3x.png b/Telegram/Resources/icons/folders/folder_new_chats@3x.png new file mode 100644 index 0000000000..d379a9e4b0 Binary files /dev/null and b/Telegram/Resources/icons/folders/folder_new_chats@3x.png differ diff --git a/Telegram/Resources/icons/menu/shop.png b/Telegram/Resources/icons/menu/shop.png new file mode 100644 index 0000000000..80dfbc6f1c Binary files /dev/null and b/Telegram/Resources/icons/menu/shop.png differ diff --git a/Telegram/Resources/icons/menu/shop@2x.png b/Telegram/Resources/icons/menu/shop@2x.png new file mode 100644 index 0000000000..38625e754d Binary files /dev/null and b/Telegram/Resources/icons/menu/shop@2x.png differ diff --git a/Telegram/Resources/icons/menu/shop@3x.png b/Telegram/Resources/icons/menu/shop@3x.png new file mode 100644 index 0000000000..0da0b229c5 Binary files /dev/null and b/Telegram/Resources/icons/menu/shop@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_away.png b/Telegram/Resources/icons/settings/premium/business/business_away.png new file mode 100644 index 0000000000..b6fe3bedef Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_away.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_away@2x.png b/Telegram/Resources/icons/settings/premium/business/business_away@2x.png new file mode 100644 index 0000000000..be8035562f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_away@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_away@3x.png b/Telegram/Resources/icons/settings/premium/business/business_away@3x.png new file mode 100644 index 0000000000..59afa2748f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_away@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots.png new file mode 100644 index 0000000000..aaa60e6a4e Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_chatbots.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png new file mode 100644 index 0000000000..e48940aaa1 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_chatbots@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png b/Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png new file mode 100644 index 0000000000..2db7e80ea5 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_chatbots@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours.png b/Telegram/Resources/icons/settings/premium/business/business_hours.png new file mode 100644 index 0000000000..c6f0d03e3f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_hours.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours@2x.png b/Telegram/Resources/icons/settings/premium/business/business_hours@2x.png new file mode 100644 index 0000000000..ce09849202 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_hours@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_hours@3x.png b/Telegram/Resources/icons/settings/premium/business/business_hours@3x.png new file mode 100644 index 0000000000..0aee12c236 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_hours@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_location.png b/Telegram/Resources/icons/settings/premium/business/business_location.png new file mode 100644 index 0000000000..ae7020aad4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_location.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_location@2x.png b/Telegram/Resources/icons/settings/premium/business/business_location@2x.png new file mode 100644 index 0000000000..49b7ee3c81 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_location@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_location@3x.png b/Telegram/Resources/icons/settings/premium/business/business_location@3x.png new file mode 100644 index 0000000000..a442c069d4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_location@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick.png b/Telegram/Resources/icons/settings/premium/business/business_quick.png new file mode 100644 index 0000000000..a9e7e1d1f6 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_quick.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick@2x.png b/Telegram/Resources/icons/settings/premium/business/business_quick@2x.png new file mode 100644 index 0000000000..3f4c1852e0 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_quick@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/business/business_quick@3x.png b/Telegram/Resources/icons/settings/premium/business/business_quick@3x.png new file mode 100644 index 0000000000..d927c704fc Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/business/business_quick@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/market.png b/Telegram/Resources/icons/settings/premium/market.png new file mode 100644 index 0000000000..3c2b9cd2da Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/market.png differ diff --git a/Telegram/Resources/icons/settings/premium/market@2x.png b/Telegram/Resources/icons/settings/premium/market@2x.png new file mode 100644 index 0000000000..1ae9e950a0 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/market@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/market@3x.png b/Telegram/Resources/icons/settings/premium/market@3x.png new file mode 100644 index 0000000000..5d132577d0 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/market@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/status.png b/Telegram/Resources/icons/settings/premium/status.png index fe15411457..712c91ba07 100644 Binary files a/Telegram/Resources/icons/settings/premium/status.png and b/Telegram/Resources/icons/settings/premium/status.png differ diff --git a/Telegram/Resources/icons/settings/premium/status@2x.png b/Telegram/Resources/icons/settings/premium/status@2x.png index d05da77b42..f0e395b4ea 100644 Binary files a/Telegram/Resources/icons/settings/premium/status@2x.png and b/Telegram/Resources/icons/settings/premium/status@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/status@3x.png b/Telegram/Resources/icons/settings/premium/status@3x.png index 01052baef0..9127319e96 100644 Binary files a/Telegram/Resources/icons/settings/premium/status@3x.png and b/Telegram/Resources/icons/settings/premium/status@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f2dd1fc894..0048795c92 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1313,6 +1313,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_info_link_label" = "Link"; "lng_info_location_label" = "Location"; "lng_info_about_label" = "About"; +"lng_info_work_open" = "Open"; +"lng_info_work_closed" = "Closed"; +"lng_info_hours_label" = "Business hours"; +"lng_info_hours_closed" = "closed"; +"lng_info_hours_opens_in_minutes#one" = "opens in {count} minute"; +"lng_info_hours_opens_in_minutes#other" = "opens in {count} minutes"; +"lng_info_hours_opens_in_hours#one" = "opens in {count} hour"; +"lng_info_hours_opens_in_hours#other" = "opens in {count} hours"; +"lng_info_hours_opens_in_days#one" = "opens in {count} day"; +"lng_info_hours_opens_in_days#other" = "opens in {count} days"; +"lng_info_hours_open_full" = "open 24 hours"; +"lng_info_hours_next_day" = "{time} (next day)"; +"lng_info_hours_local_time" = "local time"; +"lng_info_hours_my_time" = "my time"; "lng_info_user_title" = "User Info"; "lng_info_bot_title" = "Bot Info"; "lng_info_group_title" = "Group Info"; @@ -2056,6 +2070,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_animated_userpics" = "Video avatars animated in chat lists and chats to allow for additional self-expression."; "lng_premium_summary_subtitle_translation" = "Real-Time Translation"; "lng_premium_summary_about_translation" = "Real-time translation of channels and chats into other languages."; +"lng_premium_summary_subtitle_business" = "Telegram Business"; +"lng_premium_summary_about_business" = "Upgrade your account with business features such as location, opening hours and quick replies."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -2154,6 +2170,125 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_gifts_terms" = "By gifting Telegram Premium, you agree to the Telegram {link} and {policy}."; "lng_premium_gifts_terms_policy" = "Privacy Policy"; +"lng_business_title" = "Telegram Business"; +"lng_business_about" = "Turn your account to a business page with these additional features."; +"lng_business_unlocked" = "You have now unlocked these additional business features."; +"lng_business_subtitle_location" = "Location"; +"lng_business_about_location" = "Display the location of your business on your account."; +"lng_business_subtitle_opening_hours" = "Opening Hours"; +"lng_business_about_opening_hours" = "Show to your customers when you are open for business."; +"lng_business_subtitle_quick_replies" = "Quick Replies"; +"lng_business_about_quick_replies" = "Set up shortcuts up to 20 messages each to respond to customers faster."; +"lng_business_subtitle_greeting_messages" = "Greeting Messages"; +"lng_business_about_greeting_messages" = "Create greetings that will be automatically sent to new customers."; +"lng_business_subtitle_away_messages" = "Away Messages"; +"lng_business_about_away_messages" = "Define messages that are automatically sent when you are off."; +"lng_business_subtitle_chatbots" = "Chatbots"; +"lng_business_about_chatbots" = "Add any third party chatbots that will process customer interactions."; + +"lng_location_title" = "Location"; +"lng_location_about" = "Display the location of your business on your account."; +"lng_location_address" = "Enter Address"; +"lng_location_fallback" = "You can set your location on the map from your mobile device."; + +"lng_hours_title" = "Business Hours"; +"lng_hours_about" = "Turn this on to show your opening hours schedule to your customers."; +"lng_hours_show" = "Show Business Hours"; +"lng_hours_time_zone" = "Time Zone"; +"lng_hours_monday" = "Monday"; +"lng_hours_tuesday" = "Tuesday"; +"lng_hours_wednesday" = "Wednesday"; +"lng_hours_thursday" = "Thursday"; +"lng_hours_friday" = "Friday"; +"lng_hours_saturday" = "Saturday"; +"lng_hours_sunday" = "Sunday"; +"lng_hours_closed" = "Closed"; +"lng_hours_open_full" = "Open 24 hours"; +"lng_hours_next_day" = "{time} (Next day)"; +"lng_hours_on_next_day" = "Next day {time}"; +"lng_hours_time_zone_title" = "Choose Time Zone"; +"lng_hours_add_button" = "Add a Set of Hours"; +"lng_hours_opening" = "Opening Time"; +"lng_hours_closing" = "Closing Time"; +"lng_hours_remove" = "Remove"; +"lng_hours_about_day" = "Specify your working hours during the day."; + +"lng_replies_title" = "Quick Replies"; +"lng_replies_about" = "Set up shortcuts with rich text and media to respond to messages faster."; +"lng_replies_add" = "Add Quick Reply"; +"lng_replies_add_title" = "New Quick Reply"; +"lng_replies_add_shortcut" = "Add a shortcut for your reply."; +"lng_replies_add_placeholder" = "Shortcut"; +"lng_replies_add_exists" = "This shortcut already exists."; +"lng_replies_empty_title" = "New Quick Reply"; +"lng_replies_empty_about" = "Enter a message below that will be sent in chat when you type {shortcut}.\n\nYou can access Quick Replies in any chat by typing /."; +"lng_replies_remove_title" = "Remove Shortcut"; +"lng_replies_remove_text" = "You didn't create a quick reply message. Do you want to remove the shortcut?"; +"lng_replies_edit_title" = "Edit Shortcut"; +"lng_replies_edit_about" = "Edit the name for this shortcut."; +"lng_replies_message_placeholder" = "Add a Quick Reply"; +"lng_replies_delete_sure" = "Are you sure you want to delete this quick reply with all its messages?"; +"lng_replies_error_occupied" = "This shortcut is already used."; +"lng_replies_edit_button" = "Edit Quick Replies"; + +"lng_greeting_title" = "Greeting Message"; +"lng_greeting_about" = "Greet customers when they message you the first time or after a period of no activity."; +"lng_greeting_enable" = "Send Greeting Message"; +"lng_greeting_create" = "Create a Greeting Message"; +"lng_greeting_recipients" = "Recipients"; +"lng_greeting_select" = "Select chats or entire chat categories for sending a greeting message."; +"lng_greeting_period_title" = "Period of no activity"; +"lng_greeting_period_about" = "Choose how many days should pass after your last interaction with a recipient to send them a greeting in response to their message."; +"lng_greeting_empty_title" = "New Greeting Message"; +"lng_greeting_empty_about" = "Create greetings that will be automatically sent to new customers."; +"lng_greeting_message_placeholder" = "Add a Greeting"; +"lng_greeting_limit_reached" = "You have too many quick replies. Remove one to add a greeting message."; +"lng_greeting_recipients_empty" = "Please choose at least one recipient."; + +"lng_away_title" = "Away Message"; +"lng_away_about" = "Automatically reply with a message when you are away."; +"lng_away_enable" = "Send Away Message"; +"lng_away_create" = "Create an Away Message"; +"lng_away_schedule" = "Schedule"; +"lng_away_schedule_always" = "Send Always"; +"lng_away_schedule_outside" = "Outside of Business Hours"; +"lng_away_schedule_custom" = "Custom Schedule"; +"lng_away_custom_start" = "Start Time"; +"lng_away_custom_end" = "End Time"; +"lng_away_offline_only" = "Only if Offline"; +"lng_away_offline_only_about" = "Don't send the away message if you've recently been online."; +"lng_away_recipients" = "Recipients"; +"lng_away_select" = "Select chats or entire chat categories for sending an away message."; +"lng_away_empty_title" = "New Away Message"; +"lng_away_empty_about" = "Add messages that will be automatically sent when you are off."; +"lng_away_message_placeholder" = "Add an Away Message"; +"lng_away_limit_reached" = "You have too many quick replies. Remove one to add an away message."; + +"lng_business_edit_messages" = "Edit messages"; +"lng_business_limit_reached#one" = "Limit of {count} message reached."; +"lng_business_limit_reached#other" = "Limit of {count} messages reached."; + +"lng_chatbots_title" = "Chatbots"; +"lng_chatbots_about" = "Add a bot to your account to help you automatically process and respond to the messages you receive. {link}"; +"lng_chatbots_about_link" = "Learn more..."; +"lng_chatbots_placeholder" = "Enter bot URL or username"; +"lng_chatbots_add_about" = "Enter the link to the Telegram bot that you want to automatically process your chats."; +"lng_chatbots_access_title" = "Chats accessible for the bot"; +"lng_chatbots_all_except" = "All 1-to-1 Chats Except..."; +"lng_chatbots_selected" = "Only Selected Chats"; +"lng_chatbots_excluded_title" = "Excluded chats"; +"lng_chatbots_exclude_button" = "Exclude Chats"; +"lng_chatbots_included_title" = "Included chats"; +"lng_chatbots_include_button" = "Select Chats"; +"lng_chatbots_exclude_about" = "Select chats or entire chat categories which the bot will not have access to."; +"lng_chatbots_permissions_title" = "Bot permissions"; +"lng_chatbots_reply" = "Reply to Messages"; +"lng_chatbots_reply_about" = "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."; +"lng_chatbots_remove" = "Remove Bot"; +"lng_chatbots_not_found" = "Chatbot not found."; +"lng_chatbots_add" = "Add"; +"lng_chatbots_info_url" = "https://telegram.org/privacy"; + "lng_boost_channel_button" = "Boost Channel"; "lng_boost_group_button" = "Boost Group"; "lng_boost_again_button" = "Boost Again"; @@ -2917,6 +3052,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_translate_selected" = "Translate Selected Text"; "lng_context_read_hidden" = "read"; "lng_context_read_show" = "show when"; +"lng_context_edit_shortcut" = "Edit Shortcut"; +"lng_context_delete_shortcut" = "Delete Quick Reply"; "lng_add_tag_about" = "Tag this message with an emoji for quick search."; "lng_subscribe_tag_about" = "Organize your Saved Messages with tags. {link}"; @@ -4305,6 +4442,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_type_non_contacts" = "Non-Contacts"; "lng_filters_type_groups" = "Groups"; "lng_filters_type_channels" = "Channels"; +"lng_filters_type_new" = "New Chats"; +"lng_filters_type_existing" = "Existing Chats"; "lng_filters_type_bots" = "Bots"; "lng_filters_type_no_archived" = "Archived"; "lng_filters_type_no_muted" = "Muted"; @@ -4724,6 +4863,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boosts_prepaid_giveaway_status#one" = "{count} subscription {duration}"; "lng_boosts_prepaid_giveaway_status#other" = "{count} subscriptions {duration}"; +"lng_contact_add" = "Add"; +"lng_contact_send_message" = "message"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index a129237ca0..12666b6fd3 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -14,5 +14,12 @@ ../../animations/voice_ttl_idle.tgs ../../animations/voice_ttl_start.tgs ../../animations/palette.tgs + ../../animations/sleep.tgs + ../../animations/greeting.tgs + ../../animations/location.tgs + ../../animations/robot.tgs + ../../animations/writing.tgs + ../../animations/hours.tgs + ../../animations/phone.tgs diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 38e9f48e4e..b6caf76426 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -3,6 +3,7 @@ ../../art/background.tgv ../../art/bg_thumbnail.png ../../art/bg_initial.jpg + ../../art/business_logo.png ../../art/logo_256.png ../../art/logo_256_no_margin.png ../../art/themeimage.jpg diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 54c27edef5..b416185325 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.15.2.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 5b93e930d7..67536dd5db 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,0,0 - PRODUCTVERSION 4,15,0,0 + FILEVERSION 4,15,2,0 + PRODUCTVERSION 4,15,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.15.0.0" + VALUE "FileVersion", "4.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.15.0.0" + VALUE "ProductVersion", "4.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index af1902b73c..40604a7791 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,15,0,0 - PRODUCTVERSION 4,15,0,0 + FILEVERSION 4,15,2,0 + PRODUCTVERSION 4,15,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "4.15.0.0" + VALUE "FileVersion", "4.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.15.0.0" + VALUE "ProductVersion", "4.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 155666a5d4..fe0d489b01 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -22,6 +22,7 @@ inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); struct SendOptions { PeerData *sendAs = nullptr; TimeId scheduled = 0; + BusinessShortcutId shortcutId = 0; bool silent = false; bool handleSupportSwitch = false; bool hideViaBot = false; diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 005effad99..84f0cbfffc 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_media.h" #include "api/api_text_entities.h" #include "ui/boxes/confirm_box.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_histories.h" #include "data/data_scheduled_messages.h" #include "data/data_session.h" @@ -88,10 +89,15 @@ mtpRequestId EditMessage( : emptyFlag) | (options.scheduled ? MTPmessages_EditMessage::Flag::f_schedule_date + : emptyFlag) + | (item->isBusinessShortcut() + ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id : emptyFlag); const auto id = item->isScheduled() ? session->data().scheduledMessages().lookupId(item) + : item->isBusinessShortcut() + ? session->data().shortcutMessages().lookupId(item) : item->id; return api->request(MTPmessages_EditMessage( MTP_flags(flags), @@ -101,7 +107,8 @@ mtpRequestId EditMessage( inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())), MTPReplyMarkup(), sentEntities, - MTP_int(options.scheduled) + MTP_int(options.scheduled), + MTP_int(item->shortcutId()) )).done([=]( const MTPUpdates &result, [[maybe_unused]] mtpRequestId requestId) { diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index b4464019d0..482f23987c 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_updates.h" #include "apiwrap.h" #include "base/random.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_changes.h" #include "data/data_histories.h" #include "data/data_poll.h" @@ -69,6 +70,9 @@ void Polls::create( if (action.options.scheduled) { sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto sendAs = action.options.sendAs; if (sendAs) { sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; @@ -89,7 +93,8 @@ void Polls::create( MTPReplyMarkup(), MTPVector(), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( @@ -184,7 +189,8 @@ void Polls::close(not_null item) { PollDataToInputMedia(poll, true), MTPReplyMarkup(), MTPVector(), - MTP_int(0) // schedule_date + MTP_int(0), // schedule_date + MTPint() // quick_reply_shortcut_id )).done([=](const MTPUpdates &result) { _pollCloseRequestIds.erase(itemId); _session->updates().applyUpdates(result); diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 0e5d7c5855..495b9467e9 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "base/random.h" #include "base/unixtime.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_document.h" #include "data/data_photo.h" #include "data/data_channel.h" // ChannelData::addsSignature. @@ -84,20 +85,21 @@ void SendExistingMedia( ? (*localMessageId) : session->data().nextLocalMessageId()); const auto randomId = base::RandomValue(); + const auto &action = message.action; auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); - if (message.action.replyTo) { + if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); - const auto silentPost = ShouldSendSilent(peer, message.action.options); - InnerFillMessagePostFlags(message.action.options, peer, flags); + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - const auto sendAs = message.action.options.sendAs; + const auto sendAs = action.options.sendAs; const auto messageFromId = sendAs ? sendAs->id : anonymousPost @@ -124,32 +126,34 @@ void SendExistingMedia( } const auto captionText = caption.text; - if (message.action.options.scheduled) { + if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - message.action.replyTo, - HistoryItem::NewMessageDate(message.action.options.scheduled), - messageFromId, - messagePostAuthor, - media, - caption, - HistoryMessageMarkupData()); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, media, caption); const auto performRequest = [=](const auto &repeatRequest) -> void { auto &histories = history->owner().histories(); + const auto session = &history->session(); const auto usedFileReference = media->fileReference(); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), @@ -160,8 +164,9 @@ void SendExistingMedia( MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -180,7 +185,7 @@ void SendExistingMedia( }; performRequest(performRequest); - api->finishForwarding(message.action); + api->finishForwarding(action); } } // namespace @@ -259,7 +264,10 @@ bool SendDice(MessageToSend &message) { message.textWithTags = TextWithTags(); message.action.clearDraft = false; message.action.generateLocal = true; - api->sendAction(message.action); + + + const auto &action = message.action; + api->sendAction(action); const auto newId = FullMsgId( peer->id, @@ -269,17 +277,17 @@ bool SendDice(MessageToSend &message) { auto &histories = history->owner().histories(); auto flags = NewMessageFlags(peer); auto sendFlags = MTPmessages_SendMedia::Flags(0); - if (message.action.replyTo) { + if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); - const auto silentPost = ShouldSendSilent(peer, message.action.options); - InnerFillMessagePostFlags(message.action.options, peer, flags); + const auto silentPost = ShouldSendSilent(peer, action.options); + InnerFillMessagePostFlags(action.options, peer, flags); if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - const auto sendAs = message.action.options.sendAs; + const auto sendAs = action.options.sendAs; const auto messageFromId = sendAs ? sendAs->id : anonymousPost @@ -292,28 +300,31 @@ bool SendDice(MessageToSend &message) { ? session->user()->name() : QString(); - if (message.action.options.scheduled) { + if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } session->data().registerMessageRandomId(randomId, newId); - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - message.action.replyTo, - HistoryItem::NewMessageDate(message.action.options.scheduled), - messageFromId, - messagePostAuthor, - TextWithEntities(), - MTP_messageMediaDice(MTP_int(0), MTP_string(emoji)), - HistoryMessageMarkupData()); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, TextWithEntities(), MTP_messageMediaDice( + MTP_int(0), + MTP_string(emoji))); histories.sendPreparedMessage( history, - message.action.replyTo, + action.replyTo, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), @@ -324,13 +335,14 @@ bool SendDice(MessageToSend &message) { MTP_long(randomId), MTPReplyMarkup(), MTP_vector(), - MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); }); - api->finishForwarding(message.action); + api->finishForwarding(action); return true; } @@ -406,7 +418,13 @@ void SendConfirmedFile( if (file->to.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - // Scheduled messages have no the 'edited' badge. + // Scheduled messages have no 'edited' badge. + flags |= MessageFlag::HideEdited; + } + if (file->to.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + + // Shortcut messages have no 'edited' badge. flags |= MessageFlag::HideEdited; } if (file->type == SendMediaType::Audio) { @@ -415,8 +433,7 @@ void SendConfirmedFile( } } - const auto messageFromId = - file->to.options.sendAs + const auto messageFromId = file->to.options.sendAs ? file->to.options.sendAs->id : anonymousPost ? PeerId() @@ -486,19 +503,16 @@ void SendConfirmedFile( edition.savePreviousMedia = true; itemToEdit->applyEdition(std::move(edition)); } else { - const auto viaBotId = UserId(); - history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - file->to.replyTo, - HistoryItem::NewMessageDate(file->to.options.scheduled), - messageFromId, - messagePostAuthor, - caption, - media, - HistoryMessageMarkupData(), - groupId); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = file->to.replyTo, + .date = HistoryItem::NewMessageDate(file->to.options), + .shortcutId = file->to.options.shortcutId, + .postAuthor = messagePostAuthor, + .groupedId = groupId, + }, caption, media); } if (isEditing) { diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index d10af3e429..8b54f8b82b 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtp_instance.h" #include "mtproto/mtproto_config.h" #include "mtproto/mtproto_dc_options.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_saved_messages.h" @@ -1141,7 +1142,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), MTPMessageReactions(), MTPVector(), - MTP_int(d.vttl_period().value_or_empty())), + MTP_int(d.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id MessageFlags(), NewMessageType::Unread); } break; @@ -1174,7 +1176,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), MTPMessageReactions(), MTPVector(), - MTP_int(d.vttl_period().value_or_empty())), + MTP_int(d.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id MessageFlags(), NewMessageType::Unread); } break; @@ -1565,6 +1568,8 @@ void Updates::feedUpdate(const MTPUpdate &update) { if (const auto local = owner.message(id)) { if (local->isScheduled()) { session().data().scheduledMessages().apply(d, local); + } else if (local->isBusinessShortcut()) { + session().data().shortcutMessages().apply(d, local); } else { const auto existing = session().data().message( id.peer, @@ -1780,6 +1785,31 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().scheduledMessages().apply(d); } break; + case mtpc_updateQuickReplies: { + const auto &d = update.c_updateQuickReplies(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateNewQuickReply: { + const auto &d = update.c_updateNewQuickReply(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateDeleteQuickReply: { + const auto &d = update.c_updateDeleteQuickReply(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateQuickReplyMessage: { + const auto &d = update.c_updateQuickReplyMessage(); + session().data().shortcutMessages().apply(d); + } break; + + case mtpc_updateDeleteQuickReplyMessages: { + const auto &d = update.c_updateDeleteQuickReplyMessages(); + session().data().shortcutMessages().apply(d); + } break; + case mtpc_updateWebPage: { auto &d = update.c_updateWebPage(); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a722b99dda..58dd1c4ca5 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_user_names.h" #include "api/api_websites.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_web_page.h" @@ -968,6 +969,7 @@ void ApiWrap::requestMoreDialogsIfNeeded() { } } requestContacts(); + _session->data().shortcutMessages().preloadShortcuts(); } void ApiWrap::updateDialogsOffset( @@ -2480,6 +2482,14 @@ void ApiWrap::refreshFileReference( request(MTPmessages_GetScheduledMessages( item->history()->peer->input, MTP_vector(1, MTP_int(realId)))); + } else if (item->isBusinessShortcut()) { + const auto &shortcuts = _session->data().shortcutMessages(); + const auto realId = shortcuts.lookupId(item); + request(MTPmessages_GetQuickReplyMessages( + MTP_flags(MTPmessages_GetQuickReplyMessages::Flag::f_id), + MTP_int(item->shortcutId()), + MTP_vector(1, MTP_int(realId)), + MTP_long(0))); } else if (const auto channel = item->history()->peer->asChannel()) { request(MTPchannels_GetMessages( channel->inputChannel, @@ -3155,6 +3165,7 @@ void ApiWrap::sharedMediaDone( if (topicRootId && !topic) { return; } + const auto hasMessages = !parsed.messageIds.empty(); _session->storage().add(Storage::SharedMediaAddSlice( peer->id, topicRootId, @@ -3163,7 +3174,7 @@ void ApiWrap::sharedMediaDone( parsed.noSkipRange, parsed.fullCount )); - if (type == SharedMediaType::Pinned && !parsed.messageIds.empty()) { + if (type == SharedMediaType::Pinned && hasMessages) { peer->owner().history(peer)->setHasPinnedMessages(true); if (topic) { topic->setHasPinnedMessages(true); @@ -3172,7 +3183,9 @@ void ApiWrap::sharedMediaDone( } void ApiWrap::sendAction(const SendAction &action) { - if (!action.options.scheduled && !action.replaceMediaOf) { + if (!action.options.scheduled + && !action.options.shortcutId + && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) @@ -3207,11 +3220,13 @@ void ApiWrap::finishForwarding(const SendAction &action) { } _session->data().sendHistoryChangeNotifications(); - _session->changes().historyUpdated( - history, - (action.options.scheduled - ? Data::HistoryUpdate::Flag::ScheduledSent - : Data::HistoryUpdate::Flag::MessageSent)); + if (!action.options.shortcutId) { + _session->changes().historyUpdated( + history, + (action.options.scheduled + ? Data::HistoryUpdate::Flag::ScheduledSent + : Data::HistoryUpdate::Flag::MessageSent)); + } } void ApiWrap::forwardMessages( @@ -3240,7 +3255,7 @@ void ApiWrap::forwardMessages( const auto history = action.history; const auto peer = history->peer; - if (!action.options.scheduled) { + if (!action.options.scheduled && !action.options.shortcutId) { histories.readInbox(history); } const auto anonymousPost = peer->amAnonymous(); @@ -3258,6 +3273,10 @@ void ApiWrap::forwardMessages( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (draft.options != Data::ForwardOptions::PreserveInfo) { sendFlags |= SendFlag::f_drop_author; } @@ -3296,7 +3315,8 @@ void ApiWrap::forwardMessages( peer->input, MTP_int(topMsgId), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, action.options.shortcutId) )).done([=](const MTPUpdates &result) { applyUpdates(result); if (shared && !--shared->requestsLeft) { @@ -3340,14 +3360,15 @@ void ApiWrap::forwardMessages( const auto messagePostAuthor = peer->isBroadcast() ? self->name() : QString(); - history->addNewLocalMessage( - newId.msg, - flags, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - item, - topMsgId); + history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = { .topicRootId = topMsgId }, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, item); _session->data().registerMessageRandomId(randomId, newId); if (!localIds) { localIds = std::make_shared>(); @@ -3428,6 +3449,9 @@ void ApiWrap::sendSharedContact( if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + } const auto messageFromId = action.options.sendAs ? action.options.sendAs->id : anonymousPost @@ -3436,23 +3460,20 @@ void ApiWrap::sendSharedContact( const auto messagePostAuthor = peer->isBroadcast() ? _session->user()->name() : QString(); - const auto viaBotId = UserId(); - const auto item = history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - action.replyTo, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - TextWithEntities(), - MTP_messageMediaContact( - MTP_string(phone), - MTP_string(firstName), - MTP_string(lastName), - MTP_string(), // vcard - MTP_long(userId.bare)), - HistoryMessageMarkupData()); + const auto item = history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, TextWithEntities(), MTP_messageMediaContact( + MTP_string(phone), + MTP_string(firstName), + MTP_string(lastName), + MTP_string(), // vcard + MTP_long(userId.bare))); const auto media = MTP_inputMediaContact( MTP_string(phone), @@ -3635,6 +3656,17 @@ void ApiWrap::cancelLocalItem(not_null item) { } } +void ApiWrap::sendShortcutMessages( + not_null peer, + BusinessShortcutId id) { + request(MTPmessages_SendQuickReplyMessages( + peer->input, + MTP_int(id) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).send(); +} + void ApiWrap::sendMessage(MessageToSend &&message) { const auto history = message.action.history; const auto peer = history->peer; @@ -3776,18 +3808,20 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_schedule_date; mediaFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; } - const auto viaBotId = UserId(); - lastMessage = history->addNewLocalMessage( - newId.msg, - flags, - viaBotId, - action.replyTo, - HistoryItem::NewMessageDate(action.options.scheduled), - messageFromId, - messagePostAuthor, - sending, - media, - HistoryMessageMarkupData()); + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= MTPmessages_SendMessage::Flag::f_quick_reply_shortcut; + mediaFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } + lastMessage = history->addNewLocalMessage({ + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .postAuthor = messagePostAuthor, + }, sending, media); const auto done = [=]( const MTPUpdates &result, const MTP::Response &response) { @@ -3813,6 +3847,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { UnixtimeFromMsgId(response.outerMsgId)); } }; + const auto mtpShortcut = Data::ShortcutIdToMTP( + _session, + action.options.shortcutId); if (exactWebPage && !ignoreWebPage && (manualWebPage || sending.empty())) { @@ -3829,8 +3866,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTP_long(randomId), MTPReplyMarkup(), sentEntities, - MTP_int(message.action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + mtpShortcut ), done, fail); } else { histories.sendPreparedMessage( @@ -3846,7 +3884,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { MTPReplyMarkup(), sentEntities, MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + mtpShortcut ), done, fail); } isFirst = false; @@ -3937,6 +3976,10 @@ void ApiWrap::sendInlineResult( flags |= MessageFlag::IsOrWasScheduled; sendFlags |= SendFlag::f_schedule_date; } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + sendFlags |= SendFlag::f_quick_reply_shortcut; + } if (action.options.hideViaBot) { sendFlags |= SendFlag::f_hide_via; } @@ -3955,15 +3998,18 @@ void ApiWrap::sendInlineResult( _session->data().registerMessageRandomId(randomId, newId); - data->addToHistory( - history, - flags, - newId.msg, - messageFromId, - HistoryItem::NewMessageDate(action.options.scheduled), - (bot && !action.options.hideViaBot) ? peerToUser(bot->id) : 0, - action.replyTo, - messagePostAuthor); + data->addToHistory(history, { + .id = newId.msg, + .flags = flags, + .from = messageFromId, + .replyTo = action.replyTo, + .date = HistoryItem::NewMessageDate(action.options), + .shortcutId = action.options.shortcutId, + .viaBotId = ((bot && !action.options.hideViaBot) + ? peerToUser(bot->id) + : UserId()), + .postAuthor = messagePostAuthor, + }); history->clearCloudDraft(topicRootId); history->startSavingCloudDraft(topicRootId); @@ -3981,7 +4027,8 @@ void ApiWrap::sendInlineResult( MTP_long(data->getQueryId()), MTP_string(data->getId()), MTP_int(action.options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, action.options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, @@ -4118,7 +4165,8 @@ void ApiWrap::sendMediaWithRandomId( : Flag(0)) | (!sentEntities.v.isEmpty() ? Flag::f_entities : 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)); auto &histories = history->owner().histories(); const auto peer = history->peer; @@ -4137,7 +4185,8 @@ void ApiWrap::sendMediaWithRandomId( MTPReplyMarkup(), sentEntities, MTP_int(options.scheduled), - (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()) + (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4233,7 +4282,10 @@ void ApiWrap::sendAlbumIfReady(not_null album) { ? Flag::f_silent : Flag(0)) | (album->options.scheduled ? Flag::f_schedule_date : Flag(0)) - | (sendAs ? Flag::f_send_as : Flag(0)); + | (sendAs ? Flag::f_send_as : Flag(0)) + | (album->options.shortcutId + ? Flag::f_quick_reply_shortcut + : Flag(0)); auto &histories = history->owner().histories(); const auto peer = history->peer; histories.sendPreparedMessage( @@ -4246,7 +4298,8 @@ void ApiWrap::sendAlbumIfReady(not_null album) { Data::Histories::ReplyToPlaceholder(), MTP_vector(medias), MTP_int(album->options.scheduled), - (sendAs ? sendAs->input : MTP_inputPeerEmpty()) + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, album->options.shortcutId) ), [=](const MTPUpdates &result, const MTP::Response &response) { _sendingAlbums.remove(groupId); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 615960126c..58165adec4 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -337,6 +337,9 @@ public: void cancelLocalItem(not_null item); + void sendShortcutMessages( + not_null peer, + BusinessShortcutId id); void sendMessage(MessageToSend &&message); void sendBotStart( not_null bot, diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 9819c855cb..d3715e8843 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -1018,7 +1018,7 @@ void SetupChannelBox::prepare() { cancel); connect(_link, &Ui::MaskedInputField::changed, [=] { handleChange(); }); - _link->setVisible(_privacyGroup->value() == Privacy::Public); + _link->setVisible(_privacyGroup->current() == Privacy::Public); _privacyGroup->setChangedCallback([=](Privacy value) { privacyChanged(value); @@ -1063,7 +1063,7 @@ void SetupChannelBox::updateMaxHeight() { : 0) + st::newGroupPadding.bottom(); if (!_channel->isMegagroup() - || _privacyGroup->value() == Privacy::Public) { + || _privacyGroup->current() == Privacy::Public) { newHeight += st::newGroupLinkPadding.top() + _link->height() + st::newGroupLinkPadding.bottom(); @@ -1264,7 +1264,7 @@ void SetupChannelBox::save() { }; if (_saveRequestId) { return; - } else if (_privacyGroup->value() == Privacy::Private) { + } else if (_privacyGroup->current() == Privacy::Private) { closeBox(); } else { const auto link = _link->text().trimmed(); diff --git a/Telegram/SourceFiles/boxes/auto_lock_box.cpp b/Telegram/SourceFiles/boxes/auto_lock_box.cpp index 70c09ec9ef..cc7cdb4271 100644 --- a/Telegram/SourceFiles/boxes/auto_lock_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_lock_box.cpp @@ -81,9 +81,9 @@ void AutoLockBox::prepare() { const auto timeInput = Ui::CreateChild( this, - (group->value() == kCustom) + (group->current() == kCustom ? TimeString(currentTime) - : kDefaultCustom.utf8(), + : kDefaultCustom.utf8()), st::autolockTimeField, st::autolockDateField, st::scheduleTimeSeparator, @@ -115,7 +115,9 @@ void AutoLockBox::prepare() { }); rpl::merge( - boxClosing() | rpl::filter([=] { return group->value() == kCustom; }), + boxClosing() | rpl::filter( + [=] { return group->current() == kCustom; } + ), timeInput->submitRequests() ) | rpl::start_with_next([=] { if (const auto result = collect()) { diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 6dadab562f..d445ce1f51 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -31,8 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_message.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "apiwrap.h" #include "data/data_session.h" @@ -42,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_resolver.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" +#include "data/data_premium_limits.h" #include "settings/settings_premium.h" #include "storage/file_upload.h" #include "storage/localimageloader.h" @@ -82,11 +81,11 @@ constexpr auto kMaxWallPaperSlugLength = 255; const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto item = history->makeMessage( - history->owner().nextLocalMessageId(), - flags, - base::unixtime::now(), - PreparedServiceText{ { text } }); + const auto item = history->makeMessage({ + .id = history->owner().nextLocalMessageId(), + .flags = flags, + .date = base::unixtime::now(), + }, PreparedServiceText{ { text } }); return AdminLog::OwnedItem(delegate, item); } @@ -97,24 +96,16 @@ constexpr auto kMaxWallPaperSlugLength = 255; bool out) { Expects(history->peer->isUser()); - const auto flags = MessageFlag::FakeHistoryItem - | MessageFlag::HasFromId - | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - base::unixtime::now(), - out ? history->session().userId() : peerToUser(history->peer->id), - QString(), - TextWithEntities{ text }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | (out ? MessageFlag::Outgoing : MessageFlag(0))), + .from = (out + ? history->session().userId() + : peerToUser(history->peer->id)), + .date = base::unixtime::now(), + }, TextWithEntities{ text }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } @@ -699,16 +690,10 @@ void BackgroundPreviewBox::checkLevelForChannel() { if (!weak) { return std::optional(); } - const auto appConfig = &_forPeer->session().account().appConfig(); - const auto defaultRequired = appConfig->get( - "channel_wallpaper_level_min", - 9); - const auto customRequired = appConfig->get( - "channel_custom_wallpaper_level_min", - 10); + const auto limits = Data::LevelLimits(&_forPeer->session()); const auto required = _paperEmojiId.isEmpty() - ? customRequired - : defaultRequired; + ? limits.channelCustomWallpaperLevelMin() + : limits.channelWallpaperLevelMin(); if (level >= required) { applyForPeer(false); return std::optional(); @@ -791,7 +776,7 @@ void BackgroundPreviewBox::applyForPeer() { } else { ShowPremiumPreviewBox( _controller->uiShow(), - PremiumPreview::Wallpapers); + PremiumFeature::Wallpapers); } }); const auto cancel = CreateChild( diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 4403f56faa..cf9d91f8cb 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -717,7 +717,7 @@ void ProxiesBox::refreshProxyForCalls() { return; } _proxyForCalls->toggle( - (_proxySettings->value() == ProxyData::Settings::Enabled + (_proxySettings->current() == ProxyData::Settings::Enabled && _currentProxySupportsCallsId != 0), anim::type::normal); } @@ -864,7 +864,7 @@ void ProxyBox::refreshButtons() { addButton(tr::lng_settings_save(), [=] { save(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); - const auto type = _type->value(); + const auto type = _type->current(); if (type == Type::Socks5 || type == Type::Mtproto) { addLeftButton(tr::lng_proxy_share(), [=] { share(); }); } @@ -885,7 +885,7 @@ void ProxyBox::share() { ProxyData ProxyBox::collectData() { auto result = ProxyData(); - result.type = _type->value(); + result.type = _type->current(); result.host = _host->getLastText().trimmed(); result.port = _port->getLastText().trimmed().toInt(); result.user = (result.type == Type::Mtproto) @@ -1053,7 +1053,7 @@ void ProxyBox::setupControls(const ProxyData &data) { handleType(type); refreshButtons(); }); - handleType(_type->value()); + handleType(_type->current()); } void ProxyBox::addLabel( diff --git a/Telegram/SourceFiles/boxes/download_path_box.cpp b/Telegram/SourceFiles/boxes/download_path_box.cpp index 69179e9f45..f61c0e6e7d 100644 --- a/Telegram/SourceFiles/boxes/download_path_box.cpp +++ b/Telegram/SourceFiles/boxes/download_path_box.cpp @@ -44,7 +44,9 @@ void DownloadPathBox::prepare() { setTitle(tr::lng_download_path_header()); - _group->setChangedCallback([this](Directory value) { radioChanged(value); }); + _group->setChangedCallback([this](Directory value) { + radioChanged(value); + }); _pathLink->addClickHandler([=] { editPath(); }); if (!_path.isEmpty() && _path != FileDialog::Tmp()) { @@ -54,7 +56,7 @@ void DownloadPathBox::prepare() { } void DownloadPathBox::updateControlsVisibility() { - auto custom = (_group->value() == Directory::Custom); + auto custom = (_group->current() == Directory::Custom); _pathLink->setVisible(custom); auto newHeight = st::boxOptionListPadding.top() + (_default ? _default->getMargins().top() + _default->heightNoMargins() : 0) + st::boxOptionListSkip + _temp->heightNoMargins() + st::boxOptionListSkip + _dir->heightNoMargins(); @@ -122,7 +124,7 @@ void DownloadPathBox::editPath() { void DownloadPathBox::save() { #ifndef OS_WIN_STORE - auto value = _group->value(); + auto value = _group->current(); auto computePath = [this, value] { if (value == Directory::Custom) { return _path; diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 8e94beeb9c..5ffdfb6f36 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -682,7 +682,7 @@ void EditCaptionBox::setupEmojiPanel() { && !_controller->session().premium()) { ShowPremiumPreviewBox( _controller, - PremiumPreview::AnimatedEmoji); + PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_field.get(), data.document); } @@ -895,6 +895,7 @@ void EditCaptionBox::save() { auto options = Api::SendOptions(); options.scheduled = item->isScheduled() ? item->date() : 0; + options.shortcutId = item->shortcutId(); if (!_preparedList.files.empty()) { if ((_albumType != Ui::AlbumType::None) diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index f09b104484..367742971a 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -10,28 +10,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_global_privacy.h" #include "ui/layers/generic_box.h" #include "ui/widgets/checkbox.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "history/history.h" #include "boxes/peer_list_controllers.h" -#include "settings/settings_common.h" #include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "calls/calls_instance.h" -#include "base/binary_guard.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "main/main_session.h" #include "data/data_user.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_peer_values.h" #include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_layers.h" @@ -67,6 +63,32 @@ void CreateRadiobuttonLock( }, lock->lifetime()); } +void AddPremiumRequiredRow( + not_null widget, + not_null session, + Fn clickedCallback, + Fn setDefaultOption, + const style::Checkbox &st) { + const auto row = Ui::CreateChild(widget.get()); + + widget->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + row->resize(s); + }, row->lifetime()); + row->setClickedCallback(std::move(clickedCallback)); + + CreateRadiobuttonLock(row, st); + + Data::AmPremiumValue( + session + ) | rpl::start_with_next([=](bool premium) { + row->setVisible(!premium); + if (!premium) { + setDefaultOption(); + } + }, row->lifetime()); +} + } // namespace class PrivacyExceptionsBoxController : public ChatsListBoxController { @@ -363,10 +385,29 @@ void EditPrivacyBox::setupContent() { content, _controller->optionsTitleKey(), { 0, st::settingsPrivacySkipTop, 0, 0 }); - addOptionRow(Option::Everyone); - addOptionRow(Option::Contacts); - addOptionRow(Option::CloseFriends); - addOptionRow(Option::Nobody); + + const auto options = { + Option::Everyone, + Option::Contacts, + Option::CloseFriends, + Option::Nobody, + }; + for (const auto &option : options) { + if (const auto row = addOptionRow(option)) { + const auto premiumCallback = _controller->premiumClickedCallback( + option, + _window); + if (premiumCallback) { + AddPremiumRequiredRow( + row, + &_window->session(), + premiumCallback, + [=] { group->setValue(Option::Everyone); }, + st::messagePrivacyCheck); + } + } + } + const auto warning = addLabelOrDivider( content, _controller->warning(), @@ -541,7 +582,7 @@ void EditMessagesPrivacyBox( box->addButton(tr::lng_settings_save(), [=] { if (controller->session().premium()) { privacy->updateNewRequirePremium( - group->value() == kOptionPremium); + group->current() == kOptionPremium); box->closeBox(); } else { showToast(); diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index c715ef4734..cfdc14ad7f 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -88,6 +88,12 @@ public: virtual void saveAdditional() { } + [[nodiscard]] virtual Fn premiumClickedCallback( + Option option, + not_null controller) { + return nullptr; + } + virtual ~EditPrivacyController() = default; protected: diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 37b946d637..ce23d235ff 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/filters/edit_filter_box.h" #include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" #include "boxes/filters/edit_filter_links.h" #include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -26,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_peer.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_user.h" #include "core/application.h" @@ -56,60 +58,6 @@ using Flags = Data::ChatFilter::Flags; using ExceptionPeersRef = const base::flat_set> &; using ExceptionPeersGetter = ExceptionPeersRef(Data::ChatFilter::*)() const; -constexpr auto kAllTypes = { - Flag::Contacts, - Flag::NonContacts, - Flag::Groups, - Flag::Channels, - Flag::Bots, - Flag::NoMuted, - Flag::NoRead, - Flag::NoArchived, -}; - -class FilterChatsPreview final : public Ui::RpWidget { -public: - FilterChatsPreview( - not_null parent, - Flags flags, - const base::flat_set> &peers); - - [[nodiscard]] rpl::producer flagRemoved() const; - [[nodiscard]] rpl::producer> peerRemoved() const; - - void updateData( - Flags flags, - const base::flat_set> &peers); - - int resizeGetHeight(int newWidth) override; - -private: - using Button = base::unique_qptr; - struct FlagButton { - Flag flag = Flag(); - Button button; - }; - struct PeerButton { - not_null history; - Ui::PeerUserpicView userpic; - Ui::Text::String name; - Button button; - }; - - void paintEvent(QPaintEvent *e) override; - - void refresh(); - void removeFlag(Flag flag); - void removePeer(not_null history); - - std::vector _removeFlag; - std::vector _removePeer; - - rpl::event_stream _flagRemoved; - rpl::event_stream> _peerRemoved; - -}; - struct NameEditing { not_null field; bool custom = false; @@ -167,167 +115,6 @@ not_null SetupChatsPreview( return preview; } -FilterChatsPreview::FilterChatsPreview( - not_null parent, - Flags flags, - const base::flat_set> &peers) -: RpWidget(parent) { - updateData(flags, peers); -} - -void FilterChatsPreview::refresh() { - resizeToWidth(width()); -} - -void FilterChatsPreview::updateData( - Flags flags, - const base::flat_set> &peers) { - _removeFlag.clear(); - _removePeer.clear(); - const auto makeButton = [&](Fn handler) { - auto result = base::make_unique_q( - this, - st::windowFilterSmallRemove); - result->setClickedCallback(std::move(handler)); - return result; - }; - for (const auto flag : kAllTypes) { - if (flags & flag) { - _removeFlag.push_back({ - flag, - makeButton([=] { removeFlag(flag); }) }); - } - } - for (const auto &history : peers) { - _removePeer.push_back(PeerButton{ - .history = history, - .button = makeButton([=] { removePeer(history); }) - }); - } - refresh(); -} - -int FilterChatsPreview::resizeGetHeight(int newWidth) { - const auto right = st::windowFilterSmallRemoveRight; - const auto add = (st::windowFilterSmallItem.height - - st::windowFilterSmallRemove.height) / 2; - auto top = 0; - const auto moveNextButton = [&](not_null button) { - button->moveToRight(right, top + add, newWidth); - top += st::windowFilterSmallItem.height; - }; - for (const auto &[flag, button] : _removeFlag) { - moveNextButton(button.get()); - } - for (const auto &[history, userpic, name, button] : _removePeer) { - moveNextButton(button.get()); - } - return top; -} - -void FilterChatsPreview::paintEvent(QPaintEvent *e) { - auto p = Painter(this); - auto top = 0; - const auto &st = st::windowFilterSmallItem; - const auto iconLeft = st.photoPosition.x(); - const auto iconTop = st.photoPosition.y(); - const auto nameLeft = st.namePosition.x(); - p.setFont(st::windowFilterSmallItem.nameStyle.font); - const auto nameTop = st.namePosition.y(); - for (const auto &[flag, button] : _removeFlag) { - PaintFilterChatsTypeIcon( - p, - flag, - iconLeft, - top + iconTop, - width(), - st.photoSize); - - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - FilterChatsTypeName(flag)); - top += st.height; - } - for (auto &[history, userpic, name, button] : _removePeer) { - const auto savedMessages = history->peer->isSelf(); - const auto repliesMessages = history->peer->isRepliesChat(); - if (savedMessages || repliesMessages) { - if (savedMessages) { - Ui::EmptyUserpic::PaintSavedMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } else { - Ui::EmptyUserpic::PaintRepliesMessages( - p, - iconLeft, - top + iconTop, - width(), - st.photoSize); - } - p.setPen(st::contactsNameFg); - p.drawTextLeft( - nameLeft, - top + nameTop, - width(), - (savedMessages - ? tr::lng_saved_messages(tr::now) - : tr::lng_replies_messages(tr::now))); - } else { - history->peer->paintUserpicLeft( - p, - userpic, - iconLeft, - top + iconTop, - width(), - st.photoSize); - p.setPen(st::contactsNameFg); - if (name.isEmpty()) { - name.setText( - st::msgNameStyle, - history->peer->name(), - Ui::NameTextOptions()); - } - name.drawLeftElided( - p, - nameLeft, - top + nameTop, - button->x() - nameLeft, - width()); - } - top += st.height; - } -} - -void FilterChatsPreview::removeFlag(Flag flag) { - const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); - Assert(i != end(_removeFlag)); - _removeFlag.erase(i); - refresh(); - _flagRemoved.fire_copy(flag); -} - -void FilterChatsPreview::removePeer(not_null history) { - const auto i = ranges::find(_removePeer, history, &PeerButton::history); - Assert(i != end(_removePeer)); - _removePeer.erase(i); - refresh(); - _peerRemoved.fire_copy(history); -} - -rpl::producer FilterChatsPreview::flagRemoved() const { - return _flagRemoved.events(); -} - -rpl::producer> FilterChatsPreview::peerRemoved() const { - return _peerRemoved.events(); -} - void EditExceptions( not_null window, not_null context, @@ -338,6 +125,12 @@ void EditExceptions( const auto include = (options & Flag::Contacts) != Flags(0); const auto rules = data->current(); const auto session = &window->session(); + const auto limit = Data::PremiumLimits( + session + ).dialogFiltersChatsCurrent(); + const auto showLimitReached = [=] { + window->show(Box(FilterChatsLimitBox, session, limit, include)); + }; auto controller = std::make_unique( session, (include @@ -346,9 +139,8 @@ void EditExceptions( options, rules.flags() & options, include ? rules.always() : rules.never(), - [=](int count) { - return Box(FilterChatsLimitBox, session, count, include); - }); + limit, + showLimitReached); const auto rawController = controller.get(); auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 989a9867b3..0ee2bace04 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -28,6 +28,8 @@ using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, Flag::Contacts, Flag::NonContacts, Flag::Groups, @@ -119,7 +121,7 @@ PaintRoundImageCallback TypeRow::generatePaintUserpicCallback( } Flag TypeRow::flag() const { - return static_cast(id() & 0xFF); + return static_cast(id() & 0xFFFF); } ExceptionRow::ExceptionRow(not_null history) : Row(history) { @@ -219,6 +221,8 @@ auto TypeController::rowSelectionChanges() const [[nodiscard]] QString FilterChatsTypeName(Flag flag) { switch (flag) { + case Flag::NewChats: return tr::lng_filters_type_new(tr::now); + case Flag::ExistingChats: return tr::lng_filters_type_existing(tr::now); case Flag::Contacts: return tr::lng_filters_type_contacts(tr::now); case Flag::NonContacts: return tr::lng_filters_type_non_contacts(tr::now); @@ -241,6 +245,8 @@ void PaintFilterChatsTypeIcon( int size) { const auto &color1 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg; + case Flag::ExistingChats: return st::historyPeer8UserpicBg; case Flag::Contacts: return st::historyPeer4UserpicBg; case Flag::NonContacts: return st::historyPeer7UserpicBg; case Flag::Groups: return st::historyPeer2UserpicBg; @@ -254,6 +260,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &color2 = [&]() -> const style::color& { switch (flag) { + case Flag::NewChats: return st::historyPeer5UserpicBg2; + case Flag::ExistingChats: return st::historyPeer8UserpicBg2; case Flag::Contacts: return st::historyPeer4UserpicBg2; case Flag::NonContacts: return st::historyPeer7UserpicBg2; case Flag::Groups: return st::historyPeer2UserpicBg2; @@ -267,6 +275,8 @@ void PaintFilterChatsTypeIcon( }(); const auto &icon = [&]() -> const style::icon& { switch (flag) { + case Flag::NewChats: return st::windowFilterTypeNewChats; + case Flag::ExistingChats: return st::windowFilterTypeExistingChats; case Flag::Contacts: return st::windowFilterTypeContacts; case Flag::NonContacts: return st::windowFilterTypeNonContacts; case Flag::Groups: return st::windowFilterTypeGroups; @@ -323,17 +333,17 @@ EditFilterChatsListController::EditFilterChatsListController( Flags options, Flags selected, const base::flat_set> &peers, - LimitBoxFactory limitBox) + int limit, + Fn showLimitReached) : ChatsListBoxController(session) , _session(session) -, _limitBox(std::move(limitBox)) +, _showLimitReached(std::move(showLimitReached)) , _title(std::move(title)) , _peers(peers) , _options(options & ~Flag::Chatlist) , _selected(selected) -, _limit(Data::PremiumLimits(session).dialogFiltersChatsCurrent()) +, _limit(limit) , _chatlist(options & Flag::Chatlist) { - Expects(_limitBox != nullptr); } Main::Session &EditFilterChatsListController::session() const { @@ -361,8 +371,8 @@ void EditFilterChatsListController::rowClicked(not_null row) { if (count < _limit || row->checked()) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); - } else { - delegate()->peerListUiShow()->showBox(_limitBox(count)); + } else if (const auto copy = _showLimitReached) { + copy(); } } @@ -469,6 +479,10 @@ object_ptr EditFilterChatsListController::prepareTypesList() { auto EditFilterChatsListController::createRow(not_null history) -> std::unique_ptr { + const auto business = _options & (Flag::NewChats | Flag::ExistingChats); + if (business && (history->peer->isSelf() || !history->peer->isUser())) { + return nullptr; + } return history->inChatList() ? std::make_unique(history) : nullptr; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index a9dfd3fa28..26e0529c32 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -43,7 +43,6 @@ class EditFilterChatsListController final : public ChatsListBoxController { public: using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; - using LimitBoxFactory = Fn(int)>; EditFilterChatsListController( not_null session, @@ -51,7 +50,8 @@ public: Flags options, Flags selected, const base::flat_set> &peers, - LimitBoxFactory limitBox); + int limit, + Fn showLimitReached); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -72,7 +72,7 @@ private: void updateTitle(); const not_null _session; - const LimitBoxFactory _limitBox; + const Fn _showLimitReached; rpl::producer _title; base::flat_set> _peers; Flags _options; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp new file mode 100644 index 0000000000..3e2efb87e8 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.cpp @@ -0,0 +1,199 @@ +/* +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/filters/edit_filter_chats_preview.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "data/data_peer.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/text/text_options.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "styles/style_chat.h" +#include "styles/style_window.h" + +namespace { + +using Flag = Data::ChatFilter::Flag; + +constexpr auto kAllTypes = { + Flag::NewChats, + Flag::ExistingChats, + Flag::Contacts, + Flag::NonContacts, + Flag::Groups, + Flag::Channels, + Flag::Bots, + Flag::NoMuted, + Flag::NoRead, + Flag::NoArchived, +}; + +} // namespace + +FilterChatsPreview::FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers) +: RpWidget(parent) { + updateData(flags, peers); +} + +void FilterChatsPreview::refresh() { + resizeToWidth(width()); +} + +void FilterChatsPreview::updateData( + Flags flags, + const base::flat_set> &peers) { + _removeFlag.clear(); + _removePeer.clear(); + const auto makeButton = [&](Fn handler) { + auto result = base::make_unique_q( + this, + st::windowFilterSmallRemove); + result->setClickedCallback(std::move(handler)); + result->show(); + return result; + }; + for (const auto flag : kAllTypes) { + if (flags & flag) { + _removeFlag.push_back({ + flag, + makeButton([=] { removeFlag(flag); }) }); + } + } + for (const auto &history : peers) { + _removePeer.push_back(PeerButton{ + .history = history, + .button = makeButton([=] { removePeer(history); }) + }); + } + refresh(); +} + +int FilterChatsPreview::resizeGetHeight(int newWidth) { + const auto right = st::windowFilterSmallRemoveRight; + const auto add = (st::windowFilterSmallItem.height + - st::windowFilterSmallRemove.height) / 2; + auto top = 0; + const auto moveNextButton = [&](not_null button) { + button->moveToRight(right, top + add, newWidth); + top += st::windowFilterSmallItem.height; + }; + for (const auto &[flag, button] : _removeFlag) { + moveNextButton(button.get()); + } + for (const auto &[history, userpic, name, button] : _removePeer) { + moveNextButton(button.get()); + } + return top; +} + +void FilterChatsPreview::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + auto top = 0; + const auto &st = st::windowFilterSmallItem; + const auto iconLeft = st.photoPosition.x(); + const auto iconTop = st.photoPosition.y(); + const auto nameLeft = st.namePosition.x(); + p.setFont(st::windowFilterSmallItem.nameStyle.font); + const auto nameTop = st.namePosition.y(); + for (const auto &[flag, button] : _removeFlag) { + PaintFilterChatsTypeIcon( + p, + flag, + iconLeft, + top + iconTop, + width(), + st.photoSize); + + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + FilterChatsTypeName(flag)); + top += st.height; + } + for (auto &[history, userpic, name, button] : _removePeer) { + const auto savedMessages = history->peer->isSelf(); + const auto repliesMessages = history->peer->isRepliesChat(); + if (savedMessages || repliesMessages) { + if (savedMessages) { + Ui::EmptyUserpic::PaintSavedMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } else { + Ui::EmptyUserpic::PaintRepliesMessages( + p, + iconLeft, + top + iconTop, + width(), + st.photoSize); + } + p.setPen(st::contactsNameFg); + p.drawTextLeft( + nameLeft, + top + nameTop, + width(), + (savedMessages + ? tr::lng_saved_messages(tr::now) + : tr::lng_replies_messages(tr::now))); + } else { + history->peer->paintUserpicLeft( + p, + userpic, + iconLeft, + top + iconTop, + width(), + st.photoSize); + p.setPen(st::contactsNameFg); + if (name.isEmpty()) { + name.setText( + st::msgNameStyle, + history->peer->name(), + Ui::NameTextOptions()); + } + name.drawLeftElided( + p, + nameLeft, + top + nameTop, + button->x() - nameLeft, + width()); + } + top += st.height; + } +} + +void FilterChatsPreview::removeFlag(Flag flag) { + const auto i = ranges::find(_removeFlag, flag, &FlagButton::flag); + Assert(i != end(_removeFlag)); + _removeFlag.erase(i); + refresh(); + _flagRemoved.fire_copy(flag); +} + +void FilterChatsPreview::removePeer(not_null history) { + const auto i = ranges::find(_removePeer, history, &PeerButton::history); + Assert(i != end(_removePeer)); + _removePeer.erase(i); + refresh(); + _peerRemoved.fire_copy(history); +} + +rpl::producer FilterChatsPreview::flagRemoved() const { + return _flagRemoved.events(); +} + +rpl::producer> FilterChatsPreview::peerRemoved() const { + return _peerRemoved.events(); +} diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h new file mode 100644 index 0000000000..c795bc4932 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_preview.h @@ -0,0 +1,64 @@ +/* +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_chat_filters.h" +#include "ui/rp_widget.h" +#include "ui/userpic_view.h" + +class History; + +namespace Ui { +class IconButton; +} // namespace Ui + +class FilterChatsPreview final : public Ui::RpWidget { +public: + using Flag = Data::ChatFilter::Flag; + using Flags = Data::ChatFilter::Flags; + + FilterChatsPreview( + not_null parent, + Flags flags, + const base::flat_set> &peers); + + [[nodiscard]] rpl::producer flagRemoved() const; + [[nodiscard]] rpl::producer> peerRemoved() const; + + void updateData( + Flags flags, + const base::flat_set> &peers); + + int resizeGetHeight(int newWidth) override; + +private: + using Button = base::unique_qptr; + struct FlagButton { + Flag flag = Flag(); + Button button; + }; + struct PeerButton { + not_null history; + Ui::PeerUserpicView userpic; + Ui::Text::String name; + Button button; + }; + + void paintEvent(QPaintEvent *e) override; + + void refresh(); + void removeFlag(Flag flag); + void removePeer(not_null history); + + std::vector _removeFlag; + std::vector _removePeer; + + rpl::event_stream _flagRemoved; + rpl::event_stream> _peerRemoved; + +}; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index d60cc030d1..5640c11b5a 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -982,8 +982,7 @@ bool GoodForExportFilterLink( not_null window, const Data::ChatFilter &filter) { using Flag = Data::ChatFilter::Flag; - const auto listflags = Flag::Chatlist | Flag::HasMyLinks; - if (!filter.never().empty() || (filter.flags() & ~listflags)) { + if (!filter.never().empty() || (filter.flags() & Flag::RulesMask)) { window->showToast(tr::lng_filters_link_cant(tr::now)); return false; } diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 7e0e6b5dc5..41f9cc097e 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -386,7 +386,7 @@ void GiftBox( state->buttonText.events(), Ui::Premium::GiftGradientStops(), [=] { - const auto value = group->value(); + const auto value = group->current(); return (value < options.size() && value >= 0) ? options[value].botUrl : QString(); @@ -587,7 +587,7 @@ void GiftsBox( const auto content = box->addRow( object_ptr(box), {}); - auto buttonCallback = [=](PremiumPreview section) { + auto buttonCallback = [=](PremiumFeature section) { stars->setPaused(true); const auto previewBoxShown = [=]( not_null previewBox) { @@ -665,7 +665,7 @@ void GiftsBox( } auto invoice = api->invoice( users.size(), - api->monthsFromPreset(group->value())); + api->monthsFromPreset(group->current())); invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ users }; state->confirmButtonBusy = true; diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index aa69a6fee8..98c49900a2 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1216,7 +1216,7 @@ void LanguageBox::setupTop(not_null container) { if (checked && !premium) { ShowPremiumPreviewToBuy( _controller, - PremiumPreview::RealTimeTranslation); + PremiumFeature::RealTimeTranslation); _translateChatTurnOff.fire(false); } return premium diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index 1f24ecd76c..f8a7ea6e87 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -260,11 +260,11 @@ void Controller::choose(not_null chat) { const auto init = [=](not_null box) { auto above = object_ptr(box); - Settings::AddDividerTextWithLottie( - above, - box->showFinishes(), - About(channel, chat), - u"discussion"_q); + Settings::AddDividerTextWithLottie(above, { + .lottie = u"discussion"_q, + .showFinished = box->showFinishes(), + .about = About(channel, chat), + }); if (!chat) { Assert(channel->isBroadcast()); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 51d969d0eb..6268db65bd 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -216,6 +216,9 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { : peer()->isMegagroup() ? ChatAdminRightsInfo{ (Flag::ChangeInfo | Flag::DeleteMessages + | Flag::PostStories + | Flag::EditStories + | Flag::DeleteStories | Flag::BanUsers | Flag::InviteByLinkOrAdd | Flag::ManageTopics @@ -225,6 +228,9 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::PostMessages | Flag::EditMessages | Flag::DeleteMessages + | Flag::PostStories + | Flag::EditStories + | Flag::DeleteStories | Flag::InviteByLinkOrAdd | Flag::ManageCall) }; } @@ -840,7 +846,7 @@ void EditRestrictedBox::createUntilGroup() { void EditRestrictedBox::createUntilVariants() { auto addVariant = [&](int value, const QString &text) { - if (!canSave() && _untilGroup->value() != value) { + if (!canSave() && _untilGroup->current() != value) { return; } _untilVariants.emplace_back( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 2627d23423..2e24d2804c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/background_box.h" #include "boxes/stickers_box.h" #include "chat_helpers/compose/compose_show.h" +#include "core/ui_integration.h" // Core::MarkedTextContext. #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "data/data_changes.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_emoji_statuses.h" #include "data/data_file_origin.h" #include "data/data_peer.h" +#include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "history/view/history_view_element.h" @@ -34,8 +36,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "lottie/lottie_single_player.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_common.h" #include "settings/settings_premium.h" @@ -43,10 +43,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/path_shift_gradient.h" +#include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/vertical_list.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" @@ -146,6 +148,28 @@ private: }; +class LevelBadge final : public Ui::RpWidget { +public: + LevelBadge( + not_null parent, + uint32 level, + not_null session); + + void setMinimal(bool value); + +private: + void paintEvent(QPaintEvent *e) override; + + void updateText(); + + const uint32 _level; + const TextWithEntities _icon; + const Core::MarkedTextContext _context; + Ui::Text::String _text; + bool _minimal = false; + +}; + ColorSample::ColorSample( not_null parent, std::shared_ptr style, @@ -295,47 +319,36 @@ PreviewWrap::PreviewWrap( , _delegate(std::make_unique(box, _style.get(), [=] { update(); })) -, _replyToItem(_history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem +, _replyToItem(_history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::Post), - UserId(), // via - FullReplyTo(), - base::unixtime::now(), // date - _fake->id, - QString(), // postAuthor - TextWithEntities{ _peer->isSelf() - ? tr::lng_settings_color_reply(tr::now) - : tr::lng_settings_color_reply_channel(tr::now), - }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - uint64(0))) -, _replyItem(_history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + .from = _fake->id, + .date = base::unixtime::now(), +}, TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_reply(tr::now) + : tr::lng_settings_color_reply_channel(tr::now), +}, MTP_messageMediaEmpty())) +, _replyItem(_history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::HasReplyInfo | MessageFlag::Post), - UserId(), // via - FullReplyTo{ .messageId = _replyToItem->fullId() }, - base::unixtime::now(), // date - _fake->id, - QString(), // postAuthor - TextWithEntities{ _peer->isSelf() - ? tr::lng_settings_color_text(tr::now) - : tr::lng_settings_color_text_channel(tr::now), - }, - MTP_messageMediaWebPage( + .from = _fake->id, + .replyTo = FullReplyTo{.messageId = _replyToItem->fullId() }, + .date = base::unixtime::now(), +}, TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_text(tr::now) + : tr::lng_settings_color_text_channel(tr::now), +}, MTP_messageMediaWebPage( + MTP_flags(0), + MTP_webPagePending( MTP_flags(0), - MTP_webPagePending( - MTP_flags(0), - MTP_long(_webpage->id), - MTPstring(), - MTP_int(0))), - HistoryMessageMarkupData(), - uint64(0))) + MTP_long(_webpage->id), + MTPstring(), + MTP_int(0))))) , _element(_replyItem->createView(_delegate.get())) , _position(0, st::msgMargin.bottom()) { _style->apply(_theme.get()); @@ -437,6 +450,108 @@ HistoryView::Context PreviewDelegate::elementContext() { return HistoryView::Context::AdminLog; } +LevelBadge::LevelBadge( + not_null parent, + uint32 level, + not_null session) +: Ui::RpWidget(parent) +, _level(level) +, _icon(Ui::Text::SingleCustomEmoji( + session->data().customEmojiManager().registerInternalEmoji( + st::settingsLevelBadgeLock, + QMargins(0, st::settingsLevelBadgeLockSkip, 0, 0), + false))) +, _context({ .session = session }) { + updateText(); +} + +void LevelBadge::updateText() { + auto text = _icon; + text.append(' '); + if (!_minimal) { + text.append(tr::lng_boost_level( + tr::now, + lt_count, + _level, + Ui::Text::WithEntities)); + } else { + text.append(QString::number(_level)); + } + const auto &st = st::settingsPremiumNewBadge.style; + _text.setMarkedText( + st, + text, + kMarkupTextOptions, + _context); + const auto &padding = st::settingsColorSamplePadding; + QWidget::resize( + _text.maxWidth() + rect::m::sum::h(padding), + st.font->height + rect::m::sum::v(padding)); +} + +void LevelBadge::setMinimal(bool value) { + if ((value != _minimal) && value) { + _minimal = value; + updateText(); + update(); + } +} + +void LevelBadge::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + auto hq = PainterHighQualityEnabler(p); + + const auto radius = height() / 2; + p.setPen(Qt::NoPen); + auto gradient = QLinearGradient(QPointF(0, 0), QPointF(width(), 0)); + gradient.setStops(Ui::Premium::ButtonGradientStops()); + p.setBrush(gradient); + p.drawRoundedRect(rect(), radius, radius); + + p.setPen(st::premiumButtonFg); + p.setBrush(Qt::NoBrush); + + const auto context = Ui::Text::PaintContext{ + .position = rect::m::pos::tl(st::settingsColorSamplePadding), + .outerWidth = width(), + .availableWidth = width(), + }; + _text.draw(p, context); +} + +void AddLevelBadge( + int level, + not_null button, + Ui::RpWidget *right, + not_null channel, + const QMargins &padding, + rpl::producer text) { + if (channel->levelHint() >= level) { + return; + } + const auto badge = Ui::CreateChild( + button.get(), + level, + &channel->session()); + badge->show(); + const auto sampleLeft = st::settingsColorSamplePadding.left(); + const auto badgeLeft = padding.left() + sampleLeft; + rpl::combine( + button->sizeValue(), + std::move(text) + ) | rpl::start_with_next([=](const QSize &s, const QString &) { + if (s.isNull()) { + return; + } + badge->moveToLeft( + button->fullTextWidth() + badgeLeft, + (s.height() - badge->height()) / 2); + const auto rightEdge = right ? right->pos().x() : button->width(); + badge->setMinimal((rect::right(badge) + sampleLeft) > rightEdge); + badge->setVisible((rect::right(badge) + sampleLeft) < rightEdge); + }, badge->lifetime()); +} + struct SetValues { uint8 colorIndex = 0; DocumentId backgroundEmojiId = 0; @@ -541,16 +656,13 @@ void Apply( : peerColors->requiredChannelLevelFor( peer->id, values.colorIndex); + const auto limits = Data::LevelLimits(&peer->session()); const auto iconRequired = values.backgroundEmojiId - ? session->account().appConfig().get( - "channel_bg_icon_level_min", - 5) + ? limits.channelBgIconLevelMin() : 0; const auto statusRequired = (values.statusChanged && values.statusId) - ? session->account().appConfig().get( - "channel_emoji_status_level_min", - 8) + ? limits.channelEmojiStatusLevelMin() : 0; const auto required = std::max({ colorRequired, @@ -726,6 +838,7 @@ struct ButtonWithEmoji { not_null parent, std::shared_ptr show, std::shared_ptr style, + not_null peer, rpl::producer colorIndexValue, rpl::producer emojiIdValue, Fn emojiIdChosen) { @@ -827,21 +940,33 @@ struct ButtonWithEmoji { } }); + if (const auto channel = peer->asChannel()) { + AddLevelBadge( + Data::LevelLimits(&channel->session()).channelBgIconLevelMin(), + raw, + right, + channel, + button.st->padding, + tr::lng_settings_color_emoji()); + } + return result; } [[nodiscard]] object_ptr CreateEmojiStatusButton( not_null parent, std::shared_ptr show, + not_null channel, rpl::producer statusIdValue, Fn statusIdChosen, bool group) { const auto button = ButtonStyleWithRightEmoji(parent); + const auto &phrase = group + ? tr::lng_edit_channel_status_group + : tr::lng_edit_channel_status; auto result = Settings::CreateButtonWithIcon( parent, - (group - ? tr::lng_edit_channel_status_group() - : tr::lng_edit_channel_status()), + phrase(), *button.st, { &st::menuBlueIconEmojiStatus }); const auto raw = result.data(); @@ -926,6 +1051,17 @@ struct ButtonWithEmoji { } }); + const auto limits = Data::LevelLimits(&channel->session()); + AddLevelBadge( + (group + ? limits.groupEmojiStatusLevelMin() + : limits.channelEmojiStatusLevelMin()), + raw, + right, + channel, + button.st->padding, + phrase()); + return result; } @@ -1036,6 +1172,14 @@ struct ButtonWithEmoji { } }, right->lifetime()); + AddLevelBadge( + Data::LevelLimits(&channel->session()).groupEmojiStickersLevelMin(), + raw, + right, + channel, + button.st->padding, + tr::lng_group_emoji()); + return result; } @@ -1068,38 +1212,15 @@ void EditPeerColorBox( state->index = peer->colorIndex(); state->emojiId = peer->backgroundEmojiId(); state->statusId = peer->emojiStatusId(); - if (group) { - const auto divider = Ui::CreateChild( - box.get()); - const auto verticalLayout = box->verticalLayout()->add( - object_ptr(box.get())); - - auto icon = CreateLottieIcon( - verticalLayout, - { - .name = u"palette"_q, - .sizeOverride = { - st::settingsCloudPasswordIconSize, - st::settingsCloudPasswordIconSize, - }, - }, - st::peerAppearanceIconPadding); - box->setShowFinishedCallback([animate = std::move(icon.animate)] { - animate(anim::repeat::once); + Settings::AddDividerTextWithLottie(box->verticalLayout(), { + .lottie = u"palette"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_boost_group_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, }); - verticalLayout->add(std::move(icon.widget)); - verticalLayout->add( - object_ptr( - verticalLayout, - tr::lng_boost_group_about(), - st::peerAppearanceCoverLabel), - st::peerAppearanceCoverLabelMargin); - - verticalLayout->geometryValue( - ) | rpl::start_with_next([=](const QRect &r) { - divider->setGeometry(r); - }, divider->lifetime()); } else { box->addRow(object_ptr( box, @@ -1135,6 +1256,7 @@ void EditPeerColorBox( container, show, style, + peer, state->index.value(), state->emojiId.value(), [=](DocumentId id) { state->emojiId = id; })); @@ -1150,20 +1272,35 @@ void EditPeerColorBox( if (const auto channel = peer->asChannel()) { Ui::AddSkip(container, st::settingsColorSampleSkip); - Settings::AddButtonWithIcon( + const auto &phrase = group + ? tr::lng_edit_channel_wallpaper_group + : tr::lng_edit_channel_wallpaper; + const auto button = Settings::AddButtonWithIcon( container, - (group - ? tr::lng_edit_channel_wallpaper_group() - : tr::lng_edit_channel_wallpaper()), + phrase(), st::peerAppearanceButton, { &st::menuBlueIconWallpaper } - )->setClickedCallback([=] { + ); + button->setClickedCallback([=] { const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto strong = show->resolveWindow(usage)) { show->show(Box(strong, channel)); } }); + { + const auto limits = Data::LevelLimits(&channel->session()); + AddLevelBadge( + group + ? limits.groupCustomWallpaperLevelMin() + : limits.channelCustomWallpaperLevelMin(), + button, + nullptr, + channel, + st::peerAppearanceButton.padding, + phrase()); + } + Ui::AddSkip(container, st::settingsColorSampleSkip); Ui::AddDividerText( container, @@ -1201,6 +1338,7 @@ void EditPeerColorBox( container->add(CreateEmojiStatusButton( container, show, + channel, state->statusId.value(), [=](DocumentId id, TimeId until) { state->statusId = id; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp index 6970664166..9086dfce8f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_history_visibility_box.cpp @@ -25,7 +25,7 @@ void EditPeerHistoryVisibilityBox( box->setTitle(tr::lng_manage_history_visibility_title()); box->addButton(tr::lng_settings_save(), [=] { - savedCallback(historyVisibility->value()); + savedCallback(historyVisibility->current()); box->closeBox(); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index 46fb2619c0..6cafe4d2c6 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -76,7 +76,7 @@ public: } [[nodiscard]] Privacy getPrivacy() const { - return _controls.privacy->value(); + return _controls.privacy->current(); } [[nodiscard]] bool noForwards() const { @@ -238,7 +238,7 @@ void Controller::createContent() { }, wrap->lifetime()); } else { _controls.whoSendWrap->toggle( - (_controls.privacy->value() == Privacy::HasUsername), + (_controls.privacy->current() == Privacy::HasUsername), anim::type::instant); } auto joinToWrite = _controls.joinToWrite @@ -299,7 +299,7 @@ void Controller::createContent() { if (_linkOnly) { _controls.inviteLinkWrap->show(anim::type::instant); } else { - if (_controls.privacy->value() == Privacy::NoUsername) { + if (_controls.privacy->current() == Privacy::NoUsername) { checkUsernameAvailability(); } const auto forShowing = _dataSavedValue @@ -474,7 +474,7 @@ object_ptr Controller::createUsernameEdit() { &Ui::UsernameInput::changed, [this] { usernameChanged(); }); - const auto shown = (_controls.privacy->value() == Privacy::HasUsername); + const auto shown = (_controls.privacy->current() == Privacy::HasUsername); result->toggle(shown, anim::type::instant); return result; @@ -539,7 +539,7 @@ void Controller::checkUsernameAvailability() { if (!_controls.usernameInput) { return; } - const auto initial = (_controls.privacy->value() != Privacy::HasUsername); + const auto initial = (_controls.privacy->current() != Privacy::HasUsername); const auto checking = initial ? u".bad."_q : getUsernameInput(); @@ -573,11 +573,11 @@ void Controller::checkUsernameAvailability() { _controls.privacy->setValue(Privacy::NoUsername); } else if (type == u"CHANNELS_ADMIN_PUBLIC_TOO_MUCH"_q) { _usernameState = UsernameState::TooMany; - if (_controls.privacy->value() == Privacy::HasUsername) { + if (_controls.privacy->current() == Privacy::HasUsername) { askUsernameRevoke(); } } else if (initial) { - if (_controls.privacy->value() == Privacy::HasUsername) { + if (_controls.privacy->current() == Privacy::HasUsername) { showUsernameEmpty(); setFocusUsername(); } diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp index ef99b3a904..f57573d996 100644 --- a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/event_filter.h" #include "base/unixtime.h" +#include "data/data_premium_limits.h" #include "boxes/peer_list_box.h" #include "data/data_channel.h" #include "data/data_cloud_themes.h" @@ -424,14 +425,9 @@ Ui::BoostCounters ParseBoostCounters( } Ui::BoostFeatures LookupBoostFeatures(not_null channel) { - const auto group = channel->isMegagroup(); - const auto appConfig = &channel->session().account().appConfig(); - const auto get = [&](const QString &key, int fallback, bool ok = true) { - return ok ? appConfig->get(key, fallback) : 0; - }; - auto nameColorsByLevel = base::flat_map(); auto linkStylesByLevel = base::flat_map(); + const auto group = channel->isMegagroup(); const auto peerColors = &channel->session().api().peerColors(); const auto &list = group ? peerColors->requiredLevelsGroup() @@ -447,22 +443,23 @@ Ui::BoostFeatures LookupBoostFeatures(not_null channel) { if (themes.empty()) { channel->owner().cloudThemes().refreshChatThemes(); } + const auto levelLimits = Data::LevelLimits(&channel->session()); return Ui::BoostFeatures{ .nameColorsByLevel = std::move(nameColorsByLevel), .linkStylesByLevel = std::move(linkStylesByLevel), - .linkLogoLevel = get(u"channel_bg_icon_level_min"_q, 4, !group), - .transcribeLevel = get(u"group_transcribe_level_min"_q, 6, group), - .emojiPackLevel = get(u"group_emoji_stickers_level_min"_q, 4, group), - .emojiStatusLevel = get(group - ? u"group_emoji_status_level_min"_q - : u"channel_emoji_status_level_min"_q, 8), - .wallpaperLevel = get(group - ? u"group_wallpaper_level_min"_q - : u"channel_wallpaper_level_min"_q, 9), + .linkLogoLevel = group ? 0 : levelLimits.channelBgIconLevelMin(), + .transcribeLevel = group ? levelLimits.groupTranscribeLevelMin() : 0, + .emojiPackLevel = group ? levelLimits.groupEmojiStickersLevelMin() : 0, + .emojiStatusLevel = group + ? levelLimits.groupEmojiStatusLevelMin() + : levelLimits.channelEmojiStatusLevelMin(), + .wallpaperLevel = group + ? levelLimits.groupWallpaperLevelMin() + : levelLimits.channelWallpaperLevelMin(), .wallpapersCount = themes.empty() ? 8 : int(themes.size()), - .customWallpaperLevel = get(group - ? u"channel_custom_wallpaper_level_min"_q - : u"group_custom_wallpaper_level_min"_q, 10), + .customWallpaperLevel = group + ? levelLimits.groupCustomWallpaperLevelMin() + : levelLimits.channelCustomWallpaperLevelMin(), }; } diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index ad8d313df0..2a9c0d4b10 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -1142,7 +1142,7 @@ void AccountsLimitBox( const auto ref = QString(); const auto wasAccount = &session->account(); - const auto nowAccount = accounts[group->value()]; + const auto nowAccount = accounts[group->current()]; if (wasAccount == nowAccount) { Settings::ShowPremium(session, ref); return; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 93df20cfa8..76df0c2d7b 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/painter.h" #include "ui/vertical_list.h" +#include "settings/settings_business.h" #include "settings/settings_premium.h" #include "lottie/lottie_single_player.h" #include "history/view/media/history_view_sticker.h" @@ -59,7 +60,7 @@ constexpr auto kStarPeriod = 3 * crl::time(1000); using Data::ReactionId; struct Descriptor { - PremiumPreview section = PremiumPreview::Stickers; + PremiumFeature section = PremiumFeature::Stickers; DocumentData *requestedSticker = nullptr; bool fromSettings = false; Fn hiddenCallback; @@ -90,88 +91,118 @@ void PreloadSticker(const std::shared_ptr &media) { media->videoThumbnailWanted(origin); } -[[nodiscard]] rpl::producer SectionTitle(PremiumPreview section) { +[[nodiscard]] rpl::producer SectionTitle(PremiumFeature section) { switch (section) { - case PremiumPreview::Wallpapers: + case PremiumFeature::Wallpapers: return tr::lng_premium_summary_subtitle_wallpapers(); - case PremiumPreview::Stories: + case PremiumFeature::Stories: return tr::lng_premium_summary_subtitle_stories(); - case PremiumPreview::DoubleLimits: + case PremiumFeature::DoubleLimits: return tr::lng_premium_summary_subtitle_double_limits(); - case PremiumPreview::MoreUpload: + case PremiumFeature::MoreUpload: return tr::lng_premium_summary_subtitle_more_upload(); - case PremiumPreview::FasterDownload: + case PremiumFeature::FasterDownload: return tr::lng_premium_summary_subtitle_faster_download(); - case PremiumPreview::VoiceToText: + case PremiumFeature::VoiceToText: return tr::lng_premium_summary_subtitle_voice_to_text(); - case PremiumPreview::NoAds: + case PremiumFeature::NoAds: return tr::lng_premium_summary_subtitle_no_ads(); - case PremiumPreview::EmojiStatus: + case PremiumFeature::EmojiStatus: return tr::lng_premium_summary_subtitle_emoji_status(); - case PremiumPreview::InfiniteReactions: + case PremiumFeature::InfiniteReactions: return tr::lng_premium_summary_subtitle_infinite_reactions(); - case PremiumPreview::TagsForMessages: + case PremiumFeature::TagsForMessages: return tr::lng_premium_summary_subtitle_tags_for_messages(); - case PremiumPreview::LastSeen: + case PremiumFeature::LastSeen: return tr::lng_premium_summary_subtitle_last_seen(); - case PremiumPreview::MessagePrivacy: + case PremiumFeature::MessagePrivacy: return tr::lng_premium_summary_subtitle_message_privacy(); - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return tr::lng_premium_summary_subtitle_premium_stickers(); - case PremiumPreview::AnimatedEmoji: + case PremiumFeature::AnimatedEmoji: return tr::lng_premium_summary_subtitle_animated_emoji(); - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::AdvancedChatManagement: return tr::lng_premium_summary_subtitle_advanced_chat_management(); - case PremiumPreview::ProfileBadge: + case PremiumFeature::ProfileBadge: return tr::lng_premium_summary_subtitle_profile_badge(); - case PremiumPreview::AnimatedUserpics: + case PremiumFeature::AnimatedUserpics: return tr::lng_premium_summary_subtitle_animated_userpics(); - case PremiumPreview::RealTimeTranslation: + case PremiumFeature::RealTimeTranslation: return tr::lng_premium_summary_subtitle_translation(); + case PremiumFeature::Business: + return tr::lng_premium_summary_subtitle_business(); + + case PremiumFeature::BusinessLocation: + return tr::lng_business_subtitle_location(); + case PremiumFeature::BusinessHours: + return tr::lng_business_subtitle_opening_hours(); + case PremiumFeature::QuickReplies: + return tr::lng_business_subtitle_quick_replies(); + case PremiumFeature::GreetingMessage: + return tr::lng_business_subtitle_greeting_messages(); + case PremiumFeature::AwayMessage: + return tr::lng_business_subtitle_away_messages(); + case PremiumFeature::BusinessBots: + return tr::lng_business_subtitle_chatbots(); } - Unexpected("PremiumPreview in SectionTitle."); + Unexpected("PremiumFeature in SectionTitle."); } -[[nodiscard]] rpl::producer SectionAbout(PremiumPreview section) { +[[nodiscard]] rpl::producer SectionAbout(PremiumFeature section) { switch (section) { - case PremiumPreview::Wallpapers: + case PremiumFeature::Wallpapers: return tr::lng_premium_summary_about_wallpapers(); - case PremiumPreview::Stories: + case PremiumFeature::Stories: return tr::lng_premium_summary_about_stories(); - case PremiumPreview::DoubleLimits: + case PremiumFeature::DoubleLimits: return tr::lng_premium_summary_about_double_limits(); - case PremiumPreview::MoreUpload: + case PremiumFeature::MoreUpload: return tr::lng_premium_summary_about_more_upload(); - case PremiumPreview::FasterDownload: + case PremiumFeature::FasterDownload: return tr::lng_premium_summary_about_faster_download(); - case PremiumPreview::VoiceToText: + case PremiumFeature::VoiceToText: return tr::lng_premium_summary_about_voice_to_text(); - case PremiumPreview::NoAds: + case PremiumFeature::NoAds: return tr::lng_premium_summary_about_no_ads(); - case PremiumPreview::EmojiStatus: + case PremiumFeature::EmojiStatus: return tr::lng_premium_summary_about_emoji_status(); - case PremiumPreview::InfiniteReactions: + case PremiumFeature::InfiniteReactions: return tr::lng_premium_summary_about_infinite_reactions(); - case PremiumPreview::TagsForMessages: + case PremiumFeature::TagsForMessages: return tr::lng_premium_summary_about_tags_for_messages(); - case PremiumPreview::LastSeen: + case PremiumFeature::LastSeen: return tr::lng_premium_summary_about_last_seen(); - case PremiumPreview::MessagePrivacy: + case PremiumFeature::MessagePrivacy: return tr::lng_premium_summary_about_message_privacy(); - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return tr::lng_premium_summary_about_premium_stickers(); - case PremiumPreview::AnimatedEmoji: + case PremiumFeature::AnimatedEmoji: return tr::lng_premium_summary_about_animated_emoji(); - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::AdvancedChatManagement: return tr::lng_premium_summary_about_advanced_chat_management(); - case PremiumPreview::ProfileBadge: + case PremiumFeature::ProfileBadge: return tr::lng_premium_summary_about_profile_badge(); - case PremiumPreview::AnimatedUserpics: + case PremiumFeature::AnimatedUserpics: return tr::lng_premium_summary_about_animated_userpics(); - case PremiumPreview::RealTimeTranslation: + case PremiumFeature::RealTimeTranslation: return tr::lng_premium_summary_about_translation(); + case PremiumFeature::Business: + return tr::lng_premium_summary_about_business(); + + case PremiumFeature::BusinessLocation: + return tr::lng_business_about_location(); + case PremiumFeature::BusinessHours: + return tr::lng_business_about_opening_hours(); + case PremiumFeature::QuickReplies: + return tr::lng_business_about_quick_replies(); + case PremiumFeature::GreetingMessage: + return tr::lng_business_about_greeting_messages(); + case PremiumFeature::AwayMessage: + return tr::lng_business_about_away_messages(); + case PremiumFeature::BusinessBots: + return tr::lng_business_about_chatbots(); } - Unexpected("PremiumPreview in SectionTitle."); + Unexpected("PremiumFeature in SectionTitle."); } [[nodiscard]] object_ptr ChatBackPreview( @@ -463,33 +494,40 @@ struct VideoPreviewDocument { RectPart align = RectPart::Bottom; }; -[[nodiscard]] bool VideoAlignToTop(PremiumPreview section) { - return (section == PremiumPreview::MoreUpload) - || (section == PremiumPreview::NoAds) - || (section == PremiumPreview::AnimatedEmoji); +[[nodiscard]] bool VideoAlignToTop(PremiumFeature section) { + return (section == PremiumFeature::MoreUpload) + || (section == PremiumFeature::NoAds) + || (section == PremiumFeature::AnimatedEmoji); } [[nodiscard]] DocumentData *LookupVideo( not_null session, - PremiumPreview section) { + PremiumFeature section) { const auto name = [&] { switch (section) { - case PremiumPreview::MoreUpload: return "more_upload"; - case PremiumPreview::FasterDownload: return "faster_download"; - case PremiumPreview::VoiceToText: return "voice_to_text"; - case PremiumPreview::NoAds: return "no_ads"; - case PremiumPreview::AnimatedEmoji: return "animated_emoji"; - case PremiumPreview::AdvancedChatManagement: + case PremiumFeature::MoreUpload: return "more_upload"; + case PremiumFeature::FasterDownload: return "faster_download"; + case PremiumFeature::VoiceToText: return "voice_to_text"; + case PremiumFeature::NoAds: return "no_ads"; + case PremiumFeature::AnimatedEmoji: return "animated_emoji"; + case PremiumFeature::AdvancedChatManagement: return "advanced_chat_management"; - case PremiumPreview::EmojiStatus: return "emoji_status"; - case PremiumPreview::InfiniteReactions: return "infinite_reactions"; - case PremiumPreview::TagsForMessages: return "saved_tags"; - case PremiumPreview::ProfileBadge: return "profile_badge"; - case PremiumPreview::AnimatedUserpics: return "animated_userpics"; - case PremiumPreview::RealTimeTranslation: return "translations"; - case PremiumPreview::Wallpapers: return "wallpapers"; - case PremiumPreview::LastSeen: return "last_seen"; - case PremiumPreview::MessagePrivacy: return "message_privacy"; + case PremiumFeature::EmojiStatus: return "emoji_status"; + case PremiumFeature::InfiniteReactions: return "infinite_reactions"; + case PremiumFeature::TagsForMessages: return "saved_tags"; + case PremiumFeature::ProfileBadge: return "profile_badge"; + case PremiumFeature::AnimatedUserpics: return "animated_userpics"; + case PremiumFeature::RealTimeTranslation: return "translations"; + case PremiumFeature::Wallpapers: return "wallpapers"; + case PremiumFeature::LastSeen: return "last_seen"; + case PremiumFeature::MessagePrivacy: return "message_privacy"; + + case PremiumFeature::BusinessLocation: return "business_location"; + case PremiumFeature::BusinessHours: return "business_hours"; + case PremiumFeature::QuickReplies: return "quick_replies"; + case PremiumFeature::GreetingMessage: return "greeting_message"; + case PremiumFeature::AwayMessage: return "away_message"; + case PremiumFeature::BusinessBots: return "business_bots"; } return ""; }(); @@ -716,7 +754,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenericPreview( not_null parent, std::shared_ptr show, - PremiumPreview section, + PremiumFeature section, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); @@ -757,10 +795,10 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, std::shared_ptr show, - PremiumPreview section, + PremiumFeature section, Fn readyCallback) { switch (section) { - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: return StickersPreview(parent, std::move(show), readyCallback); default: return GenericPreview( @@ -784,8 +822,8 @@ struct VideoPreviewDocument { [[nodiscard]] object_ptr CreateSwitch( not_null parent, - not_null*> selected, - std::vector order) { + not_null*> selected, + std::vector order) { const auto padding = st::premiumDotPadding; const auto width = padding.left() + st::premiumDot + padding.right(); const auto height = padding.top() + st::premiumDot + padding.bottom(); @@ -856,14 +894,20 @@ void PreviewBox( Ui::Animations::Simple animation; Fn preload; std::vector hiding; - rpl::variable selected; - std::vector order; + rpl::variable selected; + std::vector order; }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; - state->order = Settings::PremiumPreviewOrder(&show->session()); + auto premiumOrder = Settings::PremiumFeaturesOrder(&show->session()); + auto businessOrder = Settings::BusinessFeaturesOrder(&show->session()); + state->order = ranges::contains(businessOrder, descriptor.section) + ? std::move(businessOrder) + : ranges::contains(businessOrder, descriptor.section) + ? std::move(premiumOrder) + : std::vector{ descriptor.section }; - const auto index = [=](PremiumPreview section) { + const auto index = [=](PremiumFeature section) { const auto it = ranges::find(state->order, section); return (it == end(state->order)) ? 0 @@ -906,7 +950,7 @@ void PreviewBox( return; } const auto now = state->selected.current(); - if (now != PremiumPreview::Stickers && !state->stickersPreload) { + if (now != PremiumFeature::Stickers && !state->stickersPreload) { const auto ready = [=] { if (state->stickersPreload) { state->stickersPreloadReady = true; @@ -917,14 +961,14 @@ void PreviewBox( state->stickersPreload = GenerateDefaultPreview( outer, show, - PremiumPreview::Stickers, + PremiumFeature::Stickers, ready); state->stickersPreload->hide(); } }; switch (descriptor.section) { - case PremiumPreview::Stickers: + case PremiumFeature::Stickers: state->content = media ? StickerPreview(outer, show, media, state->preload) : StickersPreview(outer, show, state->preload); @@ -940,7 +984,7 @@ void PreviewBox( state->selected.value( ) | rpl::combine_previous( - ) | rpl::start_with_next([=](PremiumPreview was, PremiumPreview now) { + ) | rpl::start_with_next([=](PremiumFeature was, PremiumFeature now) { const auto animationCallback = [=] { if (!state->animation.animating()) { for (const auto &hiding : base::take(state->hiding)) { @@ -982,7 +1026,7 @@ void PreviewBox( .leftTill = state->content->x() - start, }); state->leftFrom = start; - if (now == PremiumPreview::Stickers && state->stickersPreload) { + if (now == PremiumFeature::Stickers && state->stickersPreload) { state->content = base::take(state->stickersPreload); state->content->show(); if (base::take(state->stickersPreloadReady)) { @@ -1053,14 +1097,14 @@ void PreviewBox( return Settings::LookupPremiumRef(state->selected.current()); }; auto unlock = state->selected.value( - ) | rpl::map([=](PremiumPreview section) { - return (section == PremiumPreview::InfiniteReactions) + ) | rpl::map([=](PremiumFeature section) { + return (section == PremiumFeature::InfiniteReactions) ? tr::lng_premium_unlock_reactions() - : (section == PremiumPreview::Stickers) + : (section == PremiumFeature::Stickers) ? tr::lng_premium_unlock_stickers() - : (section == PremiumPreview::AnimatedEmoji) + : (section == PremiumFeature::AnimatedEmoji) ? tr::lng_premium_unlock_emoji() - : (section == PremiumPreview::EmojiStatus) + : (section == PremiumFeature::EmojiStatus) ? tr::lng_premium_unlock_status() : tr::lng_premium_more_about(); }) | rpl::flatten_latest(); @@ -1207,18 +1251,25 @@ void Show( descriptor.shownCallback(raw); } return; - } else if (descriptor.section == PremiumPreview::DoubleLimits) { + } else if (descriptor.section == PremiumFeature::DoubleLimits) { show->showBox(Box([=](not_null box) { DoubledLimitsPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; - } else if (descriptor.section == PremiumPreview::Stories) { + } else if (descriptor.section == PremiumFeature::Stories) { show->showBox(Box([=](not_null box) { UpgradedStoriesPreviewBox(box, &show->session()); DecorateListPromoBox(box, show, descriptor); })); return; + } else if (descriptor.section == PremiumFeature::Business) { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (window) { + Settings::ShowBusiness(window); + } + return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { @@ -1286,21 +1337,21 @@ void ShowStickerPreviewBox( std::shared_ptr show, not_null document) { Show(std::move(show), Descriptor{ - .section = PremiumPreview::Stickers, + .section = PremiumFeature::Stickers, .requestedSticker = document, }); } void ShowPremiumPreviewBox( not_null controller, - PremiumPreview section, + PremiumFeature section, Fn)> shown) { ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); } void ShowPremiumPreviewBox( std::shared_ptr show, - PremiumPreview section, + PremiumFeature section, Fn)> shown, bool hideSubscriptionButton) { Show(std::move(show), Descriptor{ @@ -1312,7 +1363,7 @@ void ShowPremiumPreviewBox( void ShowPremiumPreviewToBuy( not_null controller, - PremiumPreview section, + PremiumFeature section, Fn hiddenCallback) { Show(controller->uiShow(), Descriptor{ .section = section, diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index b7fb7a40eb..80400a6eed 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -45,7 +45,8 @@ void UpgradedStoriesPreviewBox( not_null box, not_null session); -enum class PremiumPreview { +enum class PremiumFeature { + // Premium features. Stories, DoubleLimits, MoreUpload, @@ -64,24 +65,33 @@ enum class PremiumPreview { TagsForMessages, LastSeen, MessagePrivacy, + Business, + + // Business features. + BusinessLocation, + BusinessHours, + QuickReplies, + GreetingMessage, + AwayMessage, + BusinessBots, kCount, }; void ShowPremiumPreviewBox( not_null controller, - PremiumPreview section, + PremiumFeature section, Fn)> shown = nullptr); void ShowPremiumPreviewBox( std::shared_ptr show, - PremiumPreview section, + PremiumFeature section, Fn)> shown = nullptr, bool hideSubscriptionButton = false); void ShowPremiumPreviewToBuy( not_null controller, - PremiumPreview section, + PremiumFeature section, Fn hiddenCallback = nullptr); void PremiumUnavailableBox(not_null box); diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 4de09e882d..e336244039 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -77,20 +77,15 @@ AdminLog::OwnedItem GenerateItem( const QString &text) { Expects(history->peer->isUser()); - const auto item = history->addNewLocalMessage( - history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + const auto item = history->addNewLocalMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | MessageFlag::HasReplyInfo), - UserId(), // via - FullReplyTo{ .messageId = replyTo }, - base::unixtime::now(), // date - from, - QString(), // postAuthor - TextWithEntities{ .text = text }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - uint64(0)); // groupedId + .from = from, + .replyTo = FullReplyTo{ .messageId = replyTo }, + .date = base::unixtime::now(), + }, TextWithEntities{ .text = text }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp index 74e8f510b7..d8097222e9 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.cpp +++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp @@ -327,7 +327,7 @@ void RingtonesBox( box->setWidth(st::boxWideWidth); box->addButton(tr::lng_settings_save(), [=] { - const auto value = state->group->value(); + const auto value = state->group->current(); auto sound = (value == kDefaultValue) ? Data::NotifySound() : (value == kNoSoundValue) diff --git a/Telegram/SourceFiles/boxes/self_destruction_box.cpp b/Telegram/SourceFiles/boxes/self_destruction_box.cpp index 03d8a6155a..0ab869ae5b 100644 --- a/Telegram/SourceFiles/boxes/self_destruction_box.cpp +++ b/Telegram/SourceFiles/boxes/self_destruction_box.cpp @@ -95,7 +95,7 @@ void SelfDestructionBox::showContent() { clearButtons(); addButton(tr::lng_settings_save(), [=] { - const auto value = _ttlGroup->value(); + const auto value = _ttlGroup->current(); switch (_type) { case Type::Account: _session->api().selfDestruct().updateAccountTTL(value); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index cd0c59ebfb..6e0c9066e6 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1172,7 +1172,7 @@ void SendFilesBox::setupEmojiPanel() { _captionToPeer, data.document) : (_limits & SendFilesAllow::EmojiWithoutPremium))) { - ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumFeature::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); } diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index dff268c262..4581fac40d 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/share_message_phrase_factory.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_channel.h" #include "data/data_game.h" #include "data/data_histories.h" @@ -1543,11 +1544,15 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( const auto threadHistory = thread->owningHistory(); histories.sendRequest(threadHistory, requestType, [=]( Fn finish) { - auto &api = threadHistory->session().api(); + const auto session = &threadHistory->session(); + auto &api = session->api(); const auto sendFlags = commonSendFlags | (topMsgId ? Flag::f_top_msg_id : Flag(0)) | (ShouldSendSilent(peer, options) ? Flag::f_silent + : Flag(0)) + | (options.shortcutId + ? Flag::f_quick_reply_shortcut : Flag(0)); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( @@ -1558,7 +1563,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( peer->input, MTP_int(topMsgId), MTP_int(options.scheduled), - MTP_inputPeerEmpty() // send_as + MTP_inputPeerEmpty(), // send_as + Data::ShortcutIdToMTP(session, options.shortcutId) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 8a88acfba5..7d72ebf5a8 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_file_origin.h" #include "data/data_document_media.h" +#include "data/data_premium_limits.h" #include "data/stickers/data_stickers.h" #include "core/application.h" #include "lang/lang_keys.h" @@ -40,8 +41,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/unread_badge_paint.h" #include "media/clip/media_clip_reader.h" -#include "main/main_account.h" -#include "main/main_app_config.h" #include "main/main_session.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -2050,10 +2049,8 @@ void StickersBox::Inner::checkGroupLevel(Fn done) { return std::optional(); } _checkingGroupLevel = false; - const auto appConfig = &peer->session().account().appConfig(); - const auto required = appConfig->get( - "group_emoji_stickers_level_min", - 4); + const auto required = Data::LevelLimits( + &peer->session()).groupEmojiStickersLevelMin(); if (level >= required) { save(); return std::optional(); diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 8ee43eeda5..05fdaaff2d 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "apiwrap.h" #include "platform/platform_specific.h" +#include "base/event_filter.h" #include "base/platform/base_platform_info.h" #include "base/power_save_blocker.h" #include "media/streaming/media_streaming_utility.h" @@ -147,17 +148,18 @@ void Panel::initWindow() { window()->setTitle(_user->name()); window()->setTitleStyle(st::callTitle); - window()->events( - ) | rpl::start_with_next([=](not_null e) { - if (e->type() == QEvent::Close) { - handleClose(); + base::install_event_filter(window().get(), [=](not_null e) { + if (e->type() == QEvent::Close && handleClose()) { + e->ignore(); + return base::EventFilterResult::Cancel; } else if (e->type() == QEvent::KeyPress) { if ((static_cast(e.get())->key() == Qt::Key_Escape) && window()->isFullScreen()) { window()->showNormal(); } } - }, window()->lifetime()); + return base::EventFilterResult::Continue; + }); window()->setBodyTitleArea([=](QPoint widgetPoint) { using Flag = Ui::WindowTitleHitTestFlag; @@ -828,10 +830,12 @@ void Panel::paint(QRect clip) { } } -void Panel::handleClose() { +bool Panel::handleClose() const { if (_call) { - _call->hangup(); + window()->hide(); + return true; } + return false; } not_null Panel::window() const { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index dc715584ae..c98537eb98 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -106,7 +106,7 @@ private: void initLayout(); void initGeometry(); - void handleClose(); + [[nodiscard]] bool handleClose() const; void updateControlsGeometry(); void updateHangupGeometry(); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index a39019a93d..a622c37eaf 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -608,7 +608,7 @@ defaultComposeIcons: ComposeIcons { stripBubble: icon{ { "chat/reactions_bubble_shadow", windowShadowFg }, - { "chat/reactions_bubble", windowBg }, + { "chat/reactions_bubble", emojiPanBg }, }; stripExpandPanel: icon{ { "chat/reactions_round_big", windowBgRipple }, diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h index ba6f43b4e7..5466b34e97 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -23,6 +23,7 @@ struct ComposeFeatures { bool autocompleteHashtags = true; bool autocompleteMentions = true; bool autocompleteCommands = true; + bool commonTabbedPanel = true; }; } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 382d850e3b..d5aba296be 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/field_autocomplete.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_channel.h" @@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_account.h" #include "core/application.h" #include "core/core_settings.h" +#include "lang/lang_keys.h" #include "lottie/lottie_single_player.h" #include "media/clip/media_clip_reader.h" #include "ui/widgets/popup_menu.h" @@ -636,6 +638,30 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } } } + const auto shortcuts = (_user && !_user->isBot()) + ? _user->owner().shortcutMessages().shortcuts().list + : base::flat_map(); + if (!hasUsername && brows.empty() && !shortcuts.empty()) { + const auto self = _user->session().user(); + for (const auto &[id, shortcut] : shortcuts) { + if (shortcut.count < 1) { + continue; + } else if (!listAllSuggestions) { + if (!shortcut.name.startsWith(_filter, Qt::CaseInsensitive)) { + continue; + } + } + brows.push_back(BotCommandRow{ + self, + shortcut.name, + tr::lng_forum_messages(tr::now, lt_count, shortcut.count), + self->activeUserpicView() + }); + } + if (!brows.empty()) { + brows.insert(begin(brows), BotCommandRow{ self }); // Edit. + } + } } rowsUpdated( std::move(mrows), @@ -1073,6 +1099,15 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { } else { auto &row = _brows->at(i); const auto user = row.user; + if (user->isSelf() && row.command.isEmpty()) { + p.setPen(st::windowActiveTextFg); + p.setFont(st::semiboldFont); + p.drawText( + QRect(0, i * st::mentionHeight, width(), st::mentionHeight), + tr::lng_replies_edit_button(tr::now), + style::al_center); + continue; + } auto toHighlight = row.command; int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); @@ -1140,7 +1175,13 @@ void FieldAutocomplete::Inner::clearSel(bool hidden) { _overDelete = false; _mouseSelection = false; _lastMousePosition = std::nullopt; - setSel((_mrows->empty() && _brows->empty() && _hrows->empty()) ? -1 : 0); + setSel((_mrows->empty() && _brows->empty() && _hrows->empty()) + ? -1 + : (_brows->size() > 1 + && _brows->front().user->isSelf() + && _brows->front().command.isEmpty()) + ? 1 + : 0); if (hidden) { _down = -1; _previewShown = false; @@ -1246,8 +1287,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex( const auto commandString = QString("/%1%2").arg( command, insertUsername ? ('@' + PrimaryUsername(user)) : QString()); - - _botCommandChosen.fire({ commandString, method }); + _botCommandChosen.fire({ user, commandString, method }); return true; } } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 2606dc1f20..5c9e291f3a 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -96,6 +96,7 @@ public: ChooseMethod method = ChooseMethod::ByEnter; }; struct BotCommandChosen { + not_null user; QString command; ChooseMethod method = ChooseMethod::ByEnter; }; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index fdef095481..ed29357449 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -727,7 +727,8 @@ void MessageLinksParser::parse() { || (tag == Ui::InputField::kTagItalic) || (tag == Ui::InputField::kTagUnderline) || (tag == Ui::InputField::kTagStrikeOut) - || (tag == Ui::InputField::kTagSpoiler); + || (tag == Ui::InputField::kTagSpoiler) + || (tag == Ui::InputField::kTagBlockquote); }; _ranges.clear(); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index 4534818d34..6659343d2d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -114,10 +114,7 @@ std::optional SetIdEmojiSection(uint64 id) { rpl::producer> GifSectionsValue( not_null session) { const auto config = &session->account().appConfig(); - return rpl::single( - rpl::empty_value() - ) | rpl::then( - config->refreshed() + return config->value( ) | rpl::map([=] { return config->get>( u"gif_search_emojies"_q, diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index d6a7ac8e17..60a27fcca5 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -669,7 +669,7 @@ bool ShowSearchTagsPromo( if (!controller) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages); + ShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages); return true; } @@ -1048,7 +1048,7 @@ const std::vector &InternalUrlHandlers() { { u"about_tags"_q, ShowSearchTagsPromo - } + }, }; return Result; } diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 03d6dc7622..01763e1e25 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -54,8 +54,13 @@ const auto kBadPrefix = u"http://"_q; [[nodiscard]] QString UrlWithAutoLoginToken( const QString &url, QUrl parsed, - const QString &domain) { - const auto &active = Core::App().activeAccount(); + const QString &domain, + QVariant context) { + const auto my = context.value(); + const auto window = my.sessionWindow.get(); + const auto &active = window + ? window->session().account() + : Core::App().activeAccount(); const auto token = active.mtp().configValues().autologinToken; const auto domains = active.appConfig().get>( "autologin_domains", @@ -246,7 +251,8 @@ bool UiIntegration::handleUrlClick( const auto domain = DomainForAutoLogin(parsed); const auto skip = context.value().skipBotAutoLogin; if (skip || !BotAutoLogin(url, domain, context)) { - File::OpenUrl(UrlWithAutoLoginToken(url, std::move(parsed), domain)); + File::OpenUrl( + UrlWithAutoLoginToken(url, std::move(parsed), domain, context)); } return true; } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index d9af5939e0..350612c553 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 4015000; -constexpr auto AppVersionStr = "4.15"; +constexpr auto AppVersion = 4015002; +constexpr auto AppVersionStr = "4.15.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp new file mode 100644 index 0000000000..5c8c9f8957 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -0,0 +1,106 @@ +/* +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/business/data_business_chatbots.h" + +#include "apiwrap.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + +namespace Data { + +Chatbots::Chatbots(not_null owner) +: _owner(owner) { +} + +Chatbots::~Chatbots() = default; + +void Chatbots::preload() { + if (_loaded || _requestId) { + return; + } + _requestId = _owner->session().api().request( + MTPaccount_GetConnectedBots() + ).done([=](const MTPaccount_ConnectedBots &result) { + _requestId = 0; + _loaded = true; + + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + const auto &list = data.vconnected_bots().v; + if (!list.isEmpty()) { + const auto &bot = list.front().data(); + const auto botId = bot.vbot_id().v; + _settings = ChatbotsSettings{ + .bot = _owner->session().data().user(botId), + .recipients = FromMTP(_owner, bot.vrecipients()), + .repliesAllowed = bot.is_can_reply(), + }; + } else { + _settings.force_assign(ChatbotsSettings()); + } + }).fail([=](const MTP::Error &error) { + _requestId = 0; + LOG(("API Error: Could not get connected bots %1 (%2)" + ).arg(error.code() + ).arg(error.type())); + }).send(); +} + +bool Chatbots::loaded() const { + return _loaded; +} + +const ChatbotsSettings &Chatbots::current() const { + return _settings.current(); +} + +rpl::producer Chatbots::changes() const { + return _settings.changes(); +} + +rpl::producer Chatbots::value() const { + return _settings.value(); +} + +void Chatbots::save( + ChatbotsSettings settings, + Fn done, + Fn fail) { + const auto was = _settings.current(); + if (was == settings) { + return; + } else if (was.bot || settings.bot) { + using Flag = MTPaccount_UpdateConnectedBot::Flag; + const auto api = &_owner->session().api(); + api->request(MTPaccount_UpdateConnectedBot( + MTP_flags(!settings.bot + ? Flag::f_deleted + : settings.repliesAllowed + ? Flag::f_can_reply + : Flag()), + (settings.bot ? settings.bot : was.bot)->inputUser, + ToMTP(settings.recipients) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + if (done) { + done(); + } + }).fail([=](const MTP::Error &error) { + _settings = was; + if (fail) { + fail(error.type()); + } + }).send(); + } + _settings = settings; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.h b/Telegram/SourceFiles/data/business/data_business_chatbots.h new file mode 100644 index 0000000000..6328b487d4 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.h @@ -0,0 +1,53 @@ +/* +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/business/data_business_common.h" + +class UserData; + +namespace Data { + +class Session; + +struct ChatbotsSettings { + UserData *bot = nullptr; + BusinessRecipients recipients; + bool repliesAllowed = false; + + friend inline bool operator==( + const ChatbotsSettings &, + const ChatbotsSettings &) = default; +}; + +class Chatbots final { +public: + explicit Chatbots(not_null owner); + ~Chatbots(); + + void preload(); + [[nodiscard]] bool loaded() const; + [[nodiscard]] const ChatbotsSettings ¤t() const; + [[nodiscard]] rpl::producer changes() const; + [[nodiscard]] rpl::producer value() const; + + void save( + ChatbotsSettings settings, + Fn done, + Fn fail); + +private: + const not_null _owner; + + rpl::variable _settings; + mtpRequestId _requestId = 0; + bool _loaded = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp new file mode 100644 index 0000000000..06cb17e91e --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -0,0 +1,288 @@ +/* +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/business/data_business_common.h" + +#include "data/data_session.h" +#include "data/data_user.h" + +namespace Data { +namespace { + +constexpr auto kDay = WorkingInterval::kDay; +constexpr auto kWeek = WorkingInterval::kWeek; +constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax; + +[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) { + auto &list = intervals.list; + ranges::sort(list, ranges::less(), &WorkingInterval::start); + for (auto i = 0, count = int(list.size()); i != count; ++i) { + if (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) { + list[i - 1] = list[i - 1].united(list[i]); + list[i] = {}; + } + if (!list[i]) { + list.erase(list.begin() + i); + --i; + --count; + } + } + return intervals; +} + +[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) { + auto &list = intervals.list; + auto after = WorkingInterval{ kWeek, kWeek + kDay }; + while (!list.empty()) { + if (const auto tail = list.back().intersected(after)) { + list.back().end = tail.start; + if (!list.back()) { + list.pop_back(); + } + list.insert(begin(list), tail.shifted(-kWeek)); + } else { + break; + } + } + return intervals; +} + +} // namespace + +MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data) { + using Flag = MTPDinputBusinessRecipients::Flag; + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + const auto flags = Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) + | (chats.list.empty() ? Flag() : Flag::f_users) + | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); + const auto &users = data.allButExcluded + ? data.excluded + : data.included; + return MTP_inputBusinessRecipients( + MTP_flags(flags), + MTP_vector_from_range(users.list + | ranges::views::transform(&UserData::inputUser))); +} + +BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessRecipients &recipients) { + using Type = BusinessChatType; + + const auto &data = recipients.data(); + auto result = BusinessRecipients{ + .allButExcluded = data.is_exclude_selected(), + }; + auto &chats = result.allButExcluded + ? result.excluded + : result.included; + chats.types = Type() + | (data.is_new_chats() ? Type::NewChats : Type()) + | (data.is_existing_chats() ? Type::ExistingChats : Type()) + | (data.is_contacts() ? Type::Contacts : Type()) + | (data.is_non_contacts() ? Type::NonContacts : Type()); + if (const auto users = data.vusers()) { + for (const auto &userId : users->v) { + chats.list.push_back(owner->user(UserId(userId.v))); + } + } + return result; +} + +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional &hours, + const tl::conditional &location) { + auto result = BusinessDetails(); + if (hours) { + const auto &data = hours->data(); + result.hours.timezoneId = qs(data.vtimezone_id()); + result.hours.intervals.list = ranges::views::all( + data.vweekly_open().v + ) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) { + const auto &data = open.data(); + return WorkingInterval{ + data.vstart_minute().v * 60, + data.vend_minute().v * 60, + }; + }) | ranges::to_vector; + } + if (location) { + const auto &data = location->data(); + result.location.address = qs(data.vaddress()); + if (const auto point = data.vgeo_point()) { + point->match([&](const MTPDgeoPoint &data) { + result.location.point = LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } + } + return result; +} + +[[nodiscard]] AwaySettings FromMTP( + not_null owner, + const tl::conditional &message) { + if (!message) { + return AwaySettings(); + } + const auto &data = message->data(); + auto result = AwaySettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .shortcutId = data.vshortcut_id().v, + .offlineOnly = data.is_offline_only(), + }; + data.vschedule().match([&]( + const MTPDbusinessAwayMessageScheduleAlways &) { + result.schedule.type = AwayScheduleType::Always; + }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { + result.schedule.type = AwayScheduleType::OutsideWorkingHours; + }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { + result.schedule.type = AwayScheduleType::Custom; + result.schedule.customInterval = WorkingInterval{ + data.vstart_date().v, + data.vend_date().v, + }; + }); + return result; +} + +[[nodiscard]] GreetingSettings FromMTP( + not_null owner, + const tl::conditional &message) { + if (!message) { + return GreetingSettings(); + } + const auto &data = message->data(); + return GreetingSettings{ + .recipients = FromMTP(owner, data.vrecipients()), + .noActivityDays = data.vno_activity_days().v, + .shortcutId = data.vshortcut_id().v, + }; +} + +WorkingIntervals WorkingIntervals::normalized() const { + return SortAndMerge(MoveTailToFront(SortAndMerge(*this))); +} + +WorkingIntervals ExtractDayIntervals( + const WorkingIntervals &intervals, + int dayIndex) { + Expects(dayIndex >= 0 && dayIndex < 7); + + auto result = WorkingIntervals(); + auto &list = result.list; + for (const auto &interval : intervals.list) { + const auto now = interval.intersected( + { (dayIndex - 1) * kDay, (dayIndex + 2) * kDay }); + const auto after = interval.intersected( + { (dayIndex + 6) * kDay, (dayIndex + 9) * kDay }); + const auto before = interval.intersected( + { (dayIndex - 8) * kDay, (dayIndex - 5) * kDay }); + if (now) { + list.push_back(now.shifted(-dayIndex * kDay)); + } + if (after) { + list.push_back(after.shifted(-(dayIndex + 7) * kDay)); + } + if (before) { + list.push_back(before.shifted(-(dayIndex - 7) * kDay)); + } + } + result = result.normalized(); + + const auto outside = [&](WorkingInterval interval) { + return (interval.end <= 0) || (interval.start >= kDay); + }; + list.erase(ranges::remove_if(list, outside), end(list)); + + if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) { + list.back() = { 0, kDay }; + } else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) { + list.back() = list.back().intersected({ 0, kDay }); + } + if (!list.empty() && list.front().start <= 0) { + if (list.front().start < 0 + && list.front().end <= kInNextDayMax + && list.front().start > -kDay) { + list.erase(begin(list)); + } else { + list.front() = list.front().intersected({ 0, kDay }); + if (!list.front()) { + list.erase(begin(list)); + } + } + } + + return result; +} + +bool IsFullOpen(const WorkingIntervals &extractedDay) { + return extractedDay // 00:00-23:59 or 00:00-00:00 (next day) + && (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 } + || extractedDay.list.front() == WorkingInterval{ 0, kDay }); +} + +WorkingIntervals RemoveDayIntervals( + const WorkingIntervals &intervals, + int dayIndex) { + auto result = intervals.normalized(); + auto &list = result.list; + const auto day = WorkingInterval{ 0, kDay }; + const auto shifted = day.shifted(dayIndex * kDay); + auto before = WorkingInterval{ 0, shifted.start }; + auto after = WorkingInterval{ shifted.end, kWeek }; + for (auto i = 0, count = int(list.size()); i != count; ++i) { + if (list[i].end <= shifted.start || list[i].start >= shifted.end) { + continue; + } else if (list[i].end <= shifted.start + kInNextDayMax + && (list[i].start < shifted.start + || (!dayIndex // This 'Sunday' finishing on next day <= 6:00. + && list[i].start == shifted.start + && list.back().end >= kWeek))) { + continue; + } else if (const auto first = list[i].intersected(before)) { + list[i] = first; + if (const auto second = list[i].intersected(after)) { + list.push_back(second); + } + } else if (const auto second = list[i].intersected(after)) { + list[i] = second; + } else { + list.erase(list.begin() + i); + --i; + --count; + } + } + return result.normalized(); +} + +WorkingIntervals ReplaceDayIntervals( + const WorkingIntervals &intervals, + int dayIndex, + WorkingIntervals replacement) { + auto result = RemoveDayIntervals(intervals, dayIndex); + const auto first = result.list.insert( + end(result.list), + begin(replacement.list), + end(replacement.list)); + for (auto &interval : ranges::make_subrange(first, end(result.list))) { + interval = interval.shifted(dayIndex * kDay); + } + return result.normalized(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h new file mode 100644 index 0000000000..af34294210 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -0,0 +1,245 @@ +/* +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/flags.h" +#include "data/data_location.h" + +class UserData; + +namespace Data { + +class Session; + +enum class BusinessChatType { + NewChats = (1 << 0), + ExistingChats = (1 << 1), + Contacts = (1 << 2), + NonContacts = (1 << 3), +}; +inline constexpr bool is_flag_type(BusinessChatType) { return true; } + +using BusinessChatTypes = base::flags; + +struct BusinessChats { + BusinessChatTypes types; + std::vector> list; + + [[nodiscard]] bool empty() const { + return !types && list.empty(); + } + + friend inline bool operator==( + const BusinessChats &a, + const BusinessChats &b) = default; +}; + +struct BusinessRecipients { + BusinessChats included; + BusinessChats excluded; + bool allButExcluded = false; + + friend inline bool operator==( + const BusinessRecipients &a, + const BusinessRecipients &b) = default; +}; + +[[nodiscard]] MTPInputBusinessRecipients ToMTP( + const BusinessRecipients &data); +[[nodiscard]] BusinessRecipients FromMTP( + not_null owner, + const MTPBusinessRecipients &recipients); + +struct Timezone { + QString id; + QString name; + TimeId utcOffset = 0; + + friend inline bool operator==( + const Timezone &a, + const Timezone &b) = default; +}; + +struct Timezones { + std::vector list; + + friend inline bool operator==( + const Timezones &a, + const Timezones &b) = default; +};; + +struct WorkingInterval { + static constexpr auto kDay = 24 * 3600; + static constexpr auto kWeek = 7 * kDay; + static constexpr auto kInNextDayMax = 6 * 3600; + + TimeId start = 0; + TimeId end = 0; + + explicit operator bool() const { + return start < end; + } + + [[nodiscard]] WorkingInterval shifted(TimeId offset) const { + return { start + offset, end + offset }; + } + [[nodiscard]] WorkingInterval united(WorkingInterval other) const { + if (!*this) { + return other; + } else if (!other) { + return *this; + } + return { + std::min(start, other.start), + std::max(end, other.end), + }; + } + [[nodiscard]] WorkingInterval intersected(WorkingInterval other) const { + const auto result = WorkingInterval{ + std::max(start, other.start), + std::min(end, other.end), + }; + return result ? result : WorkingInterval(); + } + + friend inline bool operator==( + const WorkingInterval &a, + const WorkingInterval &b) = default; +}; + +struct WorkingIntervals { + std::vector list; + + [[nodiscard]] WorkingIntervals normalized() const; + + explicit operator bool() const { + for (const auto &interval : list) { + if (interval) { + return true; + } + } + return false; + } + friend inline bool operator==( + const WorkingIntervals &a, + const WorkingIntervals &b) = default; +}; + +struct WorkingHours { + WorkingIntervals intervals; + QString timezoneId; + + [[nodiscard]] WorkingHours normalized() const { + return { intervals.normalized(), timezoneId }; + } + + explicit operator bool() const { + return !timezoneId.isEmpty() && !intervals.list.empty(); + } + + friend inline bool operator==( + const WorkingHours &a, + const WorkingHours &b) = default; +}; + +[[nodiscard]] WorkingIntervals ExtractDayIntervals( + const WorkingIntervals &intervals, + int dayIndex); +[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay); +[[nodiscard]] WorkingIntervals RemoveDayIntervals( + const WorkingIntervals &intervals, + int dayIndex); +[[nodiscard]] WorkingIntervals ReplaceDayIntervals( + const WorkingIntervals &intervals, + int dayIndex, + WorkingIntervals replacement); + +struct BusinessLocation { + QString address; + std::optional point; + + explicit operator bool() const { + return !address.isEmpty(); + } + + friend inline bool operator==( + const BusinessLocation &a, + const BusinessLocation &b) = default; +}; + +struct BusinessDetails { + WorkingHours hours; + BusinessLocation location; + + explicit operator bool() const { + return hours || location; + } + + friend inline bool operator==( + const BusinessDetails &a, + const BusinessDetails &b) = default; +}; + +[[nodiscard]] BusinessDetails FromMTP( + const tl::conditional &hours, + const tl::conditional &location); + +enum class AwayScheduleType : uchar { + Never = 0, + Always = 1, + OutsideWorkingHours = 2, + Custom = 3, +}; + +struct AwaySchedule { + AwayScheduleType type = AwayScheduleType::Never; + WorkingInterval customInterval; + + friend inline bool operator==( + const AwaySchedule &a, + const AwaySchedule &b) = default; +}; + +struct AwaySettings { + BusinessRecipients recipients; + AwaySchedule schedule; + BusinessShortcutId shortcutId = 0; + bool offlineOnly = false; + + explicit operator bool() const { + return schedule.type != AwayScheduleType::Never; + } + + friend inline bool operator==( + const AwaySettings &a, + const AwaySettings &b) = default; +}; + +[[nodiscard]] AwaySettings FromMTP( + not_null owner, + const tl::conditional &message); + +struct GreetingSettings { + BusinessRecipients recipients; + int noActivityDays = 0; + BusinessShortcutId shortcutId = 0; + + explicit operator bool() const { + return noActivityDays > 0; + } + + friend inline bool operator==( + const GreetingSettings &a, + const GreetingSettings &b) = default; +}; + +[[nodiscard]] GreetingSettings FromMTP( + not_null owner, + const tl::conditional &message); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp new file mode 100644 index 0000000000..151e36dcba --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -0,0 +1,247 @@ +/* +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/business/data_business_info.h" + +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/business/data_business_common.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) { + const auto list = data.intervals.normalized().list; + const auto proj = [](const WorkingInterval &data) { + return MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen( + MTP_int(data.start / 60), + MTP_int(data.end / 60))); + }; + return MTP_businessWorkHours( + MTP_flags(0), + MTP_string(data.timezoneId), + MTP_vector_from_range(list | ranges::views::transform(proj))); +} + +[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP( + const AwaySchedule &data) { + Expects(data.type != AwayScheduleType::Never); + + return (data.type == AwayScheduleType::Always) + ? MTP_businessAwayMessageScheduleAlways() + : (data.type == AwayScheduleType::OutsideWorkingHours) + ? MTP_businessAwayMessageScheduleOutsideWorkHours() + : MTP_businessAwayMessageScheduleCustom( + MTP_int(data.customInterval.start), + MTP_int(data.customInterval.end)); +} + +[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) { + using Flag = MTPDinputBusinessAwayMessage::Flag; + return MTP_inputBusinessAwayMessage( + MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()), + MTP_int(data.shortcutId), + ToMTP(data.schedule), + ToMTP(data.recipients)); +} + +[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP( + const GreetingSettings &data) { + return MTP_inputBusinessGreetingMessage( + MTP_int(data.shortcutId), + ToMTP(data.recipients), + MTP_int(data.noActivityDays)); +} + +} // namespace + +BusinessInfo::BusinessInfo(not_null owner) +: _owner(owner) { +} + +BusinessInfo::~BusinessInfo() = default; + +void BusinessInfo::saveWorkingHours( + WorkingHours data, + Fn fail) { + const auto session = &_owner->session(); + auto details = session->user()->businessDetails(); + const auto &was = details.hours; + if (was == data) { + return; + } + + using Flag = MTPaccount_UpdateBusinessWorkHours::Flag; + session->api().request(MTPaccount_UpdateBusinessWorkHours( + MTP_flags(data ? Flag::f_business_work_hours : Flag()), + ToMTP(data) + )).fail([=](const MTP::Error &error) { + auto details = session->user()->businessDetails(); + details.hours = was; + session->user()->setBusinessDetails(std::move(details)); + if (fail) { + fail(error.type()); + } + }).send(); + + details.hours = std::move(data); + session->user()->setBusinessDetails(std::move(details)); +} + +void BusinessInfo::applyAwaySettings(AwaySettings data) { + if (_awaySettings == data) { + return; + } + _awaySettings = data; + _awaySettingsChanged.fire({}); +} + +void BusinessInfo::saveAwaySettings( + AwaySettings data, + Fn fail) { + const auto &was = _awaySettings; + if (was == data) { + return; + } else if (!data || data.shortcutId) { + using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; + const auto session = &_owner->session(); + session->api().request(MTPaccount_UpdateBusinessAwayMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessAwayMessage() + )).fail([=](const MTP::Error &error) { + _awaySettings = was; + _awaySettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); + } + _awaySettings = std::move(data); + _awaySettingsChanged.fire({}); +} + +bool BusinessInfo::awaySettingsLoaded() const { + return _awaySettings.has_value(); +} + +AwaySettings BusinessInfo::awaySettings() const { + return _awaySettings.value_or(AwaySettings()); +} + +rpl::producer<> BusinessInfo::awaySettingsChanged() const { + return _awaySettingsChanged.events(); +} + +void BusinessInfo::applyGreetingSettings(GreetingSettings data) { + if (_greetingSettings == data) { + return; + } + _greetingSettings = data; + _greetingSettingsChanged.fire({}); +} + +void BusinessInfo::saveGreetingSettings( + GreetingSettings data, + Fn fail) { + const auto &was = _greetingSettings; + if (was == data) { + return; + } else if (!data || data.shortcutId) { + using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag; + _owner->session().api().request( + MTPaccount_UpdateBusinessGreetingMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessGreetingMessage()) + ).fail([=](const MTP::Error &error) { + _greetingSettings = was; + _greetingSettingsChanged.fire({}); + if (fail) { + fail(error.type()); + } + }).send(); + } + _greetingSettings = std::move(data); + _greetingSettingsChanged.fire({}); +} + +bool BusinessInfo::greetingSettingsLoaded() const { + return _greetingSettings.has_value(); +} + +GreetingSettings BusinessInfo::greetingSettings() const { + return _greetingSettings.value_or(GreetingSettings()); +} + +rpl::producer<> BusinessInfo::greetingSettingsChanged() const { + return _greetingSettingsChanged.events(); +} + +void BusinessInfo::preload() { + preloadTimezones(); +} + +void BusinessInfo::preloadTimezones() { + if (!_timezones.current().list.empty() || _timezonesRequestId) { + return; + } + _timezonesRequestId = _owner->session().api().request( + MTPhelp_GetTimezonesList(MTP_int(_timezonesHash)) + ).done([=](const MTPhelp_TimezonesList &result) { + result.match([&](const MTPDhelp_timezonesList &data) { + _timezonesHash = data.vhash().v; + const auto proj = [](const MTPtimezone &result) { + return Timezone{ + .id = qs(result.data().vid()), + .name = qs(result.data().vname()), + .utcOffset = result.data().vutc_offset().v, + }; + }; + _timezones = Timezones{ + .list = ranges::views::all( + data.vtimezones().v + ) | ranges::views::transform( + proj + ) | ranges::to_vector, + }; + }, [](const MTPDhelp_timezonesListNotModified &) { + }); + }).send(); +} + +rpl::producer BusinessInfo::timezonesValue() const { + const_cast(this)->preloadTimezones(); + return _timezones.value(); +} + +bool BusinessInfo::timezonesLoaded() const { + return !_timezones.current().list.empty(); +} + +QString FindClosestTimezoneId(const std::vector &list) { + const auto local = QDateTime::currentDateTime(); + const auto utc = QDateTime(local.date(), local.time(), Qt::UTC); + const auto shift = base::unixtime::now() - (TimeId)::time(nullptr); + const auto delta = int(utc.toSecsSinceEpoch()) + - int(local.toSecsSinceEpoch()) + - shift; + const auto proj = [&](const Timezone &value) { + auto distance = value.utcOffset - delta; + while (distance > 12 * 3600) { + distance -= 24 * 3600; + } + while (distance < -12 * 3600) { + distance += 24 * 3600; + } + return std::abs(distance); + }; + return ranges::min_element(list, ranges::less(), proj)->id; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h new file mode 100644 index 0000000000..933d869107 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -0,0 +1,62 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/business/data_business_common.h" + +namespace Data { + +class Session; + +class BusinessInfo final { +public: + explicit BusinessInfo(not_null owner); + ~BusinessInfo(); + + void preload(); + + void saveWorkingHours(WorkingHours data, Fn fail); + + void saveAwaySettings(AwaySettings data, Fn fail); + void applyAwaySettings(AwaySettings data); + [[nodiscard]] AwaySettings awaySettings() const; + [[nodiscard]] bool awaySettingsLoaded() const; + [[nodiscard]] rpl::producer<> awaySettingsChanged() const; + + void saveGreetingSettings( + GreetingSettings data, + Fn fail); + void applyGreetingSettings(GreetingSettings data); + [[nodiscard]] GreetingSettings greetingSettings() const; + [[nodiscard]] bool greetingSettingsLoaded() const; + [[nodiscard]] rpl::producer<> greetingSettingsChanged() const; + + void preloadTimezones(); + [[nodiscard]] bool timezonesLoaded() const; + [[nodiscard]] rpl::producer timezonesValue() const; + +private: + const not_null _owner; + + rpl::variable _timezones; + + std::optional _awaySettings; + rpl::event_stream<> _awaySettingsChanged; + + std::optional _greetingSettings; + rpl::event_stream<> _greetingSettingsChanged; + + mtpRequestId _timezonesRequestId = 0; + int32 _timezonesHash = 0; + +}; + +[[nodiscard]] QString FindClosestTimezoneId( + const std::vector &list); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp new file mode 100644 index 0000000000..75b27a25ba --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -0,0 +1,778 @@ +/* +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/business/data_shortcut_messages.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "api/api_text_entities.h" +#include "main/main_session.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/history_item_helpers.h" +#include "apiwrap.h" + +namespace Data { +namespace { + +constexpr auto kRequestTimeLimit = 60 * crl::time(1000); + +[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) { + Expects(IsServerMsgId(id)); + + return ScheduledMaxMsgId + id + 1; +} + +[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) { + Expects(IsShortcutMsgId(id)); + + return (id - ScheduledMaxMsgId - 1); +} + +[[nodiscard]] bool TooEarlyForRequest(crl::time received) { + return (received > 0) && (received + kRequestTimeLimit > crl::now()); +} + +[[nodiscard]] MTPMessage PrepareMessage( + BusinessShortcutId shortcutId, + const MTPMessage &message) { + return message.match([&](const MTPDmessageEmpty &data) { + return MTP_messageEmpty( + data.vflags(), + data.vid(), + data.vpeer_id() ? *data.vpeer_id() : MTPPeer()); + }, [&](const MTPDmessageService &data) { + return MTP_messageService( + data.vflags(), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + data.vpeer_id(), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vaction(), + MTP_int(data.vttl_period().value_or_empty())); + }, [&](const MTPDmessage &data) { + return MTP_message( + MTP_flags(data.vflags().v + | MTPDmessage::Flag::f_quick_reply_shortcut_id), + data.vid(), + data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), + MTPint(), // from_boosts_applied + data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), + data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(), + MTP_long(data.vvia_bot_id().value_or_empty()), + data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), + data.vdate(), + data.vmessage(), + data.vmedia() ? *data.vmedia() : MTPMessageMedia(), + data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(), + (data.ventities() + ? *data.ventities() + : MTPVector()), + MTP_int(data.vviews().value_or_empty()), + MTP_int(data.vforwards().value_or_empty()), + data.vreplies() ? *data.vreplies() : MTPMessageReplies(), + MTP_int(data.vedit_date().value_or_empty()), + MTP_bytes(data.vpost_author().value_or_empty()), + MTP_long(data.vgrouped_id().value_or_empty()), + MTPMessageReactions(), + MTPVector(), + MTP_int(data.vttl_period().value_or_empty()), + MTP_int(shortcutId)); + }); +} + +} // namespace + +bool IsShortcutMsgId(MsgId id) { + return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId); +} + +ShortcutMessages::ShortcutMessages(not_null owner) +: _session(&owner->session()) +, _history(owner->history(_session->userPeerId())) +, _clearTimer([=] { clearOldRequests(); }) { + owner->itemRemoved( + ) | rpl::filter([](not_null item) { + return item->isBusinessShortcut(); + }) | rpl::start_with_next([=](not_null item) { + remove(item); + }, _lifetime); +} + +ShortcutMessages::~ShortcutMessages() { + for (const auto &request : _requests) { + _session->api().request(request.second.requestId).cancel(); + } +} + +void ShortcutMessages::clearOldRequests() { + const auto now = crl::now(); + while (true) { + const auto i = ranges::find_if(_requests, [&](const auto &value) { + const auto &request = value.second; + return !request.requestId + && (request.lastReceived + kRequestTimeLimit <= now); + }); + if (i == end(_requests)) { + break; + } + _requests.erase(i); + } +} + +void ShortcutMessages::updateShortcuts(const QVector &list) { + auto shortcuts = parseShortcuts(list); + auto changes = std::vector(); + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcuts.list.contains(id)) { + continue; + } + auto foundId = BusinessShortcutId(); + for (auto &[realId, real] : shortcuts.list) { + if (real.name == shortcut.name) { + foundId = realId; + break; + } + } + if (foundId) { + mergeMessagesFromTo(id, foundId); + changes.push_back({ .oldId = id, .newId = foundId }); + } else { + shortcuts.list.emplace(id, shortcut); + } + } + const auto changed = !_shortcutsLoaded + || (shortcuts != _shortcuts); + if (changed) { + _shortcuts = std::move(shortcuts); + _shortcutsLoaded = true; + for (const auto &change : changes) { + _shortcutIdChanges.fire_copy(change); + } + _shortcutsChanged.fire({}); + } else { + Assert(changes.empty()); + } +} + +void ShortcutMessages::mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId) { + auto &to = _data[toId]; + const auto i = _data.find(fromId); + if (i == end(_data)) { + return; + } + + auto &from = i->second; + auto destroy = base::flat_set>(); + for (auto &item : from.items) { + if (item->isSending() || item->hasFailed()) { + item->setRealShortcutId(toId); + to.items.push_back(std::move(item)); + } else { + destroy.emplace(item.get()); + } + } + for (const auto &item : destroy) { + item->destroy(); + } + _data.remove(fromId); + + cancelRequest(fromId); + + _updates.fire_copy(toId); + if (!destroy.empty()) { + cancelRequest(toId); + request(toId); + } +} + +Shortcuts ShortcutMessages::parseShortcuts( + const QVector &list) const { + auto result = Shortcuts(); + for (const auto &reply : list) { + const auto shortcut = parseShortcut(reply); + result.list.emplace(shortcut.id, shortcut); + } + return result; +} + +Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const { + const auto &data = reply.data(); + return Shortcut{ + .id = BusinessShortcutId(data.vshortcut_id().v), + .count = data.vcount().v, + .name = qs(data.vshortcut()), + .topMessageId = localMessageId(data.vtop_message().v), + }; +} + +MsgId ShortcutMessages::localMessageId(MsgId remoteId) const { + return RemoteToLocalMsgId(remoteId); +} + +MsgId ShortcutMessages::lookupId(not_null item) const { + Expects(item->isBusinessShortcut()); + Expects(!item->isSending()); + Expects(!item->hasFailed()); + + return LocalToRemoteMsgId(item->id); +} + +int ShortcutMessages::count(BusinessShortcutId shortcutId) const { + const auto i = _data.find(shortcutId); + return (i != end(_data)) ? i->second.items.size() : 0; +} + +void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) { + updateShortcuts(update.vquick_replies().v); + scheduleShortcutsReload(); +} + +void ShortcutMessages::scheduleShortcutsReload() { + const auto hasUnknownMessages = [&] { + const auto selfId = _session->userPeerId(); + for (const auto &[id, shortcut] : _shortcuts.list) { + if (!_session->data().message({ selfId, shortcut.topMessageId })) { + return true; + } + } + return false; + }; + if (hasUnknownMessages()) { + _shortcutsLoaded = false; + const auto cancelledId = base::take(_shortcutsRequestId); + _session->api().request(cancelledId).cancel(); + crl::on_main(_session, [=] { + if (cancelledId || hasUnknownMessages()) { + preloadShortcuts(); + } + }); + } +} + +void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) { + const auto &reply = update.vquick_reply(); + auto foundId = BusinessShortcutId(); + const auto shortcut = parseShortcut(reply); + for (auto &[id, existing] : _shortcuts.list) { + if (id == shortcut.id) { + foundId = id; + break; + } else if (existing.name == shortcut.name) { + foundId = id; + break; + } + } + if (foundId == shortcut.id) { + auto &already = _shortcuts.list[shortcut.id]; + if (already != shortcut) { + already = shortcut; + _shortcutsChanged.fire({}); + } + return; + } else if (foundId) { + _shortcuts.list.emplace(shortcut.id, shortcut); + mergeMessagesFromTo(foundId, shortcut.id); + _shortcuts.list.remove(foundId); + _shortcutIdChanges.fire({ foundId, shortcut.id }); + _shortcutsChanged.fire({}); + } +} + +void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) { + const auto &message = update.vmessage(); + const auto shortcutId = BusinessShortcutIdFromMessage(message); + if (!shortcutId) { + return; + } + const auto loaded = _data.contains(shortcutId); + auto &list = _data[shortcutId]; + append(shortcutId, list, message); + sort(list); + _updates.fire_copy(shortcutId); + updateCount(shortcutId); + if (!loaded) { + request(shortcutId); + } +} + +void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + const auto j = _shortcuts.list.find(shortcutId); + if (j == end(_shortcuts.list)) { + return; + } + const auto count = (i != end(_data)) + ? int(i->second.itemById.size()) + : 0; + if (j->second.count != count) { + _shortcuts.list[shortcutId].count = count; + _shortcutsChanged.fire({}); + } +} + +void ShortcutMessages::apply( + const MTPDupdateDeleteQuickReplyMessages &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + for (const auto &id : update.vmessages().v) { + const auto &list = i->second; + const auto j = list.itemById.find(id.v); + if (j != end(list.itemById)) { + j->second->destroy(); + i = _data.find(shortcutId); + if (i == end(_data)) { + break; + } + } + } + _updates.fire_copy(shortcutId); + updateCount(shortcutId); + + cancelRequest(shortcutId); + request(shortcutId); +} + +void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) { + const auto shortcutId = update.vshortcut_id().v; + if (!shortcutId) { + return; + } + auto i = _data.find(shortcutId); + while (i != end(_data) && !i->second.itemById.empty()) { + i->second.itemById.back().second->destroy(); + i = _data.find(shortcutId); + } + _updates.fire_copy(shortcutId); + if (_data.contains(shortcutId)) { + updateCount(shortcutId); + } else { + _shortcuts.list.remove(shortcutId); + _shortcutIdChanges.fire({ shortcutId, 0 }); + } +} + +void ShortcutMessages::apply( + const MTPDupdateMessageID &update, + not_null local) { + const auto id = update.vid().v; + const auto i = _data.find(local->shortcutId()); + Assert(i != end(_data)); + auto &list = i->second; + const auto j = list.itemById.find(id); + if (j != end(list.itemById) || !IsServerMsgId(id)) { + local->destroy(); + } else { + Assert(!list.itemById.contains(local->id)); + local->setRealId(localMessageId(id)); + list.itemById.emplace(id, local); + } +} + +void ShortcutMessages::appendSending(not_null item) { + Expects(item->isSending()); + Expects(item->isBusinessShortcut()); + + const auto shortcutId = item->shortcutId(); + auto &list = _data[shortcutId]; + list.items.emplace_back(item); + sort(list); + _updates.fire_copy(shortcutId); +} + +void ShortcutMessages::removeSending(not_null item) { + Expects(item->isSending() || item->hasFailed()); + Expects(item->isBusinessShortcut()); + + item->destroy(); +} + +rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) { + request(shortcutId); + + return _updates.events( + ) | rpl::filter([=](BusinessShortcutId value) { + return (value == shortcutId); + }) | rpl::to_empty; +} + +Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) { + auto result = Data::MessagesSlice(); + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + const auto i = _requests.find(shortcutId); + if (i == end(_requests)) { + return result; + } + result.fullCount = result.skippedAfter = result.skippedBefore = 0; + return result; + } + const auto &list = i->second.items; + result.skippedAfter = result.skippedBefore = 0; + result.fullCount = int(list.size()); + result.ids = ranges::views::all( + list + ) | ranges::views::transform( + &HistoryItem::fullId + ) | ranges::to_vector; + return result; +} + +void ShortcutMessages::preloadShortcuts() { + if (_shortcutsLoaded || _shortcutsRequestId) { + return; + } + const auto owner = &_session->data(); + _shortcutsRequestId = owner->session().api().request( + MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash)) + ).done([=](const MTPmessages_QuickReplies &result) { + result.match([&](const MTPDmessages_quickReplies &data) { + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + owner->processMessages( + data.vmessages(), + NewMessageType::Existing); + updateShortcuts(data.vquick_replies().v); + }, [&](const MTPDmessages_quickRepliesNotModified &) { + if (!_shortcutsLoaded) { + _shortcutsLoaded = true; + _shortcutsChanged.fire({}); + } + }); + }).send(); +} + +const Shortcuts &ShortcutMessages::shortcuts() const { + return _shortcuts; +} + +bool ShortcutMessages::shortcutsLoaded() const { + return _shortcutsLoaded; +} + +rpl::producer<> ShortcutMessages::shortcutsChanged() const { + return _shortcutsChanged.events(); +} + +auto ShortcutMessages::shortcutIdChanged() const +-> rpl::producer { + return _shortcutIdChanges.events(); +} + +BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) { + Expects(_shortcutsLoaded); + + for (auto &[id, shortcut] : _shortcuts.list) { + if (shortcut.name == name) { + return id; + } + } + const auto result = --_localShortcutId; + _shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name }); + return result; +} + +Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const { + const auto i = _shortcuts.list.find(id); + + Ensures(i != end(_shortcuts.list)); + return i->second; +} + +BusinessShortcutId ShortcutMessages::lookupShortcutId( + const QString &name) const { + for (const auto &[id, shortcut] : _shortcuts.list) { + if (!shortcut.name.compare(name, Qt::CaseInsensitive)) { + return id; + } + } + return {}; +} + +void ShortcutMessages::editShortcut( + BusinessShortcutId id, + QString name, + Fn done, + Fn fail) { + name = name.trimmed(); + if (name.isEmpty()) { + fail(QString()); + return; + } + const auto finish = [=] { + const auto i = _shortcuts.list.find(id); + if (i != end(_shortcuts.list)) { + i->second.name = name; + _shortcutsChanged.fire({}); + } + done(); + }; + for (const auto &[existingId, shortcut] : _shortcuts.list) { + if (shortcut.name == name) { + if (existingId == id) { + //done(); + //return; + break; + } else if (_data[existingId].items.empty() && !shortcut.count) { + removeShortcut(existingId); + break; + } else { + fail(u"SHORTCUT_OCCUPIED"_q); + return; + } + } + } + _session->api().request(MTPmessages_EditQuickReplyShortcut( + MTP_int(id), + MTP_string(name) + )).done(finish).fail([=](const MTP::Error &error) { + const auto type = error.type(); + if (type == u"SHORTCUT_ID_INVALID"_q) { + // Not on the server (yet). + finish(); + } else { + fail(type); + } + }).send(); +} + +void ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) { + auto i = _data.find(shortcutId); + while (i != end(_data)) { + if (i->second.items.empty()) { + _data.erase(i); + } else { + i->second.items.front()->destroy(); + } + i = _data.find(shortcutId); + } + _shortcuts.list.remove(shortcutId); + _shortcutIdChanges.fire({ shortcutId, 0 }); + + _session->api().request(MTPmessages_DeleteQuickReplyShortcut( + MTP_int(shortcutId) + )).send(); +} + +void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) { + const auto j = _requests.find(shortcutId); + if (j != end(_requests)) { + _session->api().request(j->second.requestId).cancel(); + _requests.erase(j); + } +} + +void ShortcutMessages::request(BusinessShortcutId shortcutId) { + auto &request = _requests[shortcutId]; + if (request.requestId || TooEarlyForRequest(request.lastReceived)) { + return; + } + const auto i = _data.find(shortcutId); + const auto hash = (i != end(_data)) + ? countListHash(i->second) + : uint64(0); + request.requestId = _session->api().request( + MTPmessages_GetQuickReplyMessages( + MTP_flags(0), + MTP_int(shortcutId), + MTPVector(), + MTP_long(hash)) + ).done([=](const MTPmessages_Messages &result) { + parse(shortcutId, result); + }).fail([=] { + _requests.remove(shortcutId); + }).send(); +} + +void ShortcutMessages::parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list) { + auto &request = _requests[shortcutId]; + request.lastReceived = crl::now(); + request.requestId = 0; + if (!_clearTimer.isActive()) { + _clearTimer.callOnce(kRequestTimeLimit * 2); + } + + list.match([&](const MTPDmessages_messagesNotModified &data) { + }, [&](const auto &data) { + _session->data().processUsers(data.vusers()); + _session->data().processChats(data.vchats()); + + const auto &messages = data.vmessages().v; + if (messages.isEmpty()) { + clearNotSending(shortcutId); + return; + } + auto received = base::flat_set>(); + auto clear = base::flat_set>(); + auto &list = _data.emplace(shortcutId, List()).first->second; + for (const auto &message : messages) { + if (const auto item = append(shortcutId, list, message)) { + received.emplace(item); + } + } + for (const auto &owned : list.items) { + const auto item = owned.get(); + if (!item->isSending() && !received.contains(item)) { + clear.emplace(item); + } + } + updated(shortcutId, received, clear); + }); +} + +HistoryItem *ShortcutMessages::append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message) { + const auto id = message.match([&](const auto &data) { + return data.vid().v; + }); + const auto i = list.itemById.find(id); + if (i != end(list.itemById)) { + const auto existing = i->second; + message.match([&](const MTPDmessage &data) { + if (data.is_edit_hide()) { + existing->applyEdition(HistoryMessageEdition(_session, data)); + } else { + existing->updateSentContent({ + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()) + }, data.vmedia()); + existing->updateReplyMarkup( + HistoryMessageMarkupData(data.vreply_markup())); + existing->updateForwardedInfo(data.vfwd_from()); + } + existing->updateDate(data.vdate().v); + _history->owner().requestItemTextRefresh(existing); + }, [&](const auto &data) {}); + return existing; + } + + if (!IsServerMsgId(id)) { + LOG(("API Error: Bad id in quick reply messages: %1.").arg(id)); + return nullptr; + } + const auto item = _session->data().addNewMessage( + localMessageId(id), + PrepareMessage(shortcutId, message), + MessageFlags(), // localFlags + NewMessageType::Existing); + if (!item + || item->history() != _history + || item->shortcutId() != shortcutId) { + LOG(("API Error: Bad data received in quick reply messages.")); + return nullptr; + } + list.items.emplace_back(item); + list.itemById.emplace(id, item); + return item; +} + +void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) { + const auto i = _data.find(shortcutId); + if (i == end(_data)) { + return; + } + auto clear = base::flat_set>(); + for (const auto &owned : i->second.items) { + if (!owned->isSending() && !owned->hasFailed()) { + clear.emplace(owned.get()); + } + } + updated(shortcutId, {}, clear); +} + +void ShortcutMessages::updated( + BusinessShortcutId shortcutId, + const base::flat_set> &added, + const base::flat_set> &clear) { + if (!clear.empty()) { + for (const auto &item : clear) { + item->destroy(); + } + } + const auto i = _data.find(shortcutId); + if (i != end(_data)) { + sort(i->second); + } + if (!added.empty() || !clear.empty()) { + _updates.fire_copy(shortcutId); + } +} + +void ShortcutMessages::sort(List &list) { + ranges::sort(list.items, ranges::less(), &HistoryItem::position); +} + +void ShortcutMessages::remove(not_null item) { + const auto shortcutId = item->shortcutId(); + const auto i = _data.find(shortcutId); + Assert(i != end(_data)); + auto &list = i->second; + + if (!item->isSending() && !item->hasFailed()) { + list.itemById.remove(lookupId(item)); + } + const auto k = ranges::find(list.items, item, &OwnedItem::get); + Assert(k != list.items.end()); + k->release(); + list.items.erase(k); + + if (list.items.empty()) { + _data.erase(i); + } + _updates.fire_copy(shortcutId); + updateCount(shortcutId); +} + +uint64 ShortcutMessages::countListHash(const List &list) const { + using namespace Api; + + auto hash = HashInit(); + auto &&serverside = ranges::views::all( + list.items + ) | ranges::views::filter([](const OwnedItem &item) { + return !item->isSending() && !item->hasFailed(); + }) | ranges::views::reverse; + for (const auto &item : serverside) { + HashUpdate(hash, lookupId(item.get()).bare); + if (const auto edited = item->Get()) { + HashUpdate(hash, edited->date); + } else { + HashUpdate(hash, TimeId(0)); + } + } + return HashFinalize(hash); +} + +MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null session, + BusinessShortcutId id) { + return id + ? MTP_inputQuickReplyShortcut(MTP_string( + session->data().shortcutMessages().lookupShortcut(id).name)) + : MTPInputQuickReplyShortcut(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.h b/Telegram/SourceFiles/data/business/data_shortcut_messages.h new file mode 100644 index 0000000000..76a1df56d4 --- /dev/null +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.h @@ -0,0 +1,154 @@ +/* +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 "history/history_item.h" +#include "base/timer.h" + +class History; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +struct MessagesSlice; + +struct Shortcut { + BusinessShortcutId id = 0; + int count = 0; + QString name; + MsgId topMessageId = 0; + + friend inline bool operator==( + const Shortcut &a, + const Shortcut &b) = default; +}; + +struct ShortcutIdChange { + BusinessShortcutId oldId = 0; + BusinessShortcutId newId = 0; +}; + +struct Shortcuts { + base::flat_map list; + + friend inline bool operator==( + const Shortcuts &a, + const Shortcuts &b) = default; +}; + +[[nodiscard]] bool IsShortcutMsgId(MsgId id); + +class ShortcutMessages final { +public: + explicit ShortcutMessages(not_null owner); + ~ShortcutMessages(); + + [[nodiscard]] MsgId lookupId(not_null item) const; + [[nodiscard]] int count(BusinessShortcutId shortcutId) const; + [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; + + void apply(const MTPDupdateQuickReplies &update); + void apply(const MTPDupdateNewQuickReply &update); + void apply(const MTPDupdateQuickReplyMessage &update); + void apply(const MTPDupdateDeleteQuickReplyMessages &update); + void apply(const MTPDupdateDeleteQuickReply &update); + void apply( + const MTPDupdateMessageID &update, + not_null local); + + void appendSending(not_null item); + void removeSending(not_null item); + + [[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId); + [[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId); + + void preloadShortcuts(); + [[nodiscard]] const Shortcuts &shortcuts() const; + [[nodiscard]] bool shortcutsLoaded() const; + [[nodiscard]] rpl::producer<> shortcutsChanged() const; + [[nodiscard]] rpl::producer shortcutIdChanged() const; + [[nodiscard]] BusinessShortcutId emplaceShortcut(QString name); + [[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const; + [[nodiscard]] BusinessShortcutId lookupShortcutId( + const QString &name) const; + void editShortcut( + BusinessShortcutId id, + QString name, + Fn done, + Fn fail); + void removeShortcut(BusinessShortcutId shortcutId); + +private: + using OwnedItem = std::unique_ptr; + struct List { + std::vector items; + base::flat_map> itemById; + }; + struct Request { + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; + + void request(BusinessShortcutId shortcutId); + void parse( + BusinessShortcutId shortcutId, + const MTPmessages_Messages &list); + HistoryItem *append( + BusinessShortcutId shortcutId, + List &list, + const MTPMessage &message); + void clearNotSending(BusinessShortcutId shortcutId); + void updated( + BusinessShortcutId shortcutId, + const base::flat_set> &added, + const base::flat_set> &clear); + void sort(List &list); + void remove(not_null item); + [[nodiscard]] uint64 countListHash(const List &list) const; + void clearOldRequests(); + void cancelRequest(BusinessShortcutId shortcutId); + void updateCount(BusinessShortcutId shortcutId); + + void scheduleShortcutsReload(); + void mergeMessagesFromTo( + BusinessShortcutId fromId, + BusinessShortcutId toId); + void updateShortcuts(const QVector &list); + [[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const; + [[nodiscard]] Shortcuts parseShortcuts( + const QVector &list) const; + + const not_null _session; + const not_null _history; + + base::Timer _clearTimer; + base::flat_map _data; + base::flat_map _requests; + rpl::event_stream _updates; + + Shortcuts _shortcuts; + rpl::event_stream<> _shortcutsChanged; + rpl::event_stream _shortcutIdChanges; + BusinessShortcutId _localShortcutId = 0; + uint64 _shortcutsHash = 0; + mtpRequestId _shortcutsRequestId = 0; + bool _shortcutsLoaded = false; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null session, + BusinessShortcutId id); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2c29ef8fca..bd85b02faa 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -73,42 +73,43 @@ struct PeerUpdate { TranslationDisabled = (1ULL << 13), Color = (1ULL << 14), BackgroundEmoji = (1ULL << 15), + StoriesState = (1ULL << 16), // For users - CanShareContact = (1ULL << 16), - IsContact = (1ULL << 17), - PhoneNumber = (1ULL << 18), - OnlineStatus = (1ULL << 19), - BotCommands = (1ULL << 20), - BotCanBeInvited = (1ULL << 21), - BotStartToken = (1ULL << 22), - CommonChats = (1ULL << 23), - HasCalls = (1ULL << 24), - SupportInfo = (1ULL << 25), - IsBot = (1ULL << 26), - EmojiStatus = (1ULL << 27), - StoriesState = (1ULL << 28), + CanShareContact = (1ULL << 17), + IsContact = (1ULL << 18), + PhoneNumber = (1ULL << 19), + OnlineStatus = (1ULL << 20), + BotCommands = (1ULL << 21), + BotCanBeInvited = (1ULL << 22), + BotStartToken = (1ULL << 23), + CommonChats = (1ULL << 24), + HasCalls = (1ULL << 25), + SupportInfo = (1ULL << 26), + IsBot = (1ULL << 27), + EmojiStatus = (1ULL << 28), + BusinessDetails = (1ULL << 29), // For chats and channels - InviteLinks = (1ULL << 29), - Members = (1ULL << 30), - Admins = (1ULL << 31), - BannedUsers = (1ULL << 32), - Rights = (1ULL << 33), - PendingRequests = (1ULL << 34), - Reactions = (1ULL << 35), + InviteLinks = (1ULL << 30), + Members = (1ULL << 31), + Admins = (1ULL << 32), + BannedUsers = (1ULL << 33), + Rights = (1ULL << 34), + PendingRequests = (1ULL << 35), + Reactions = (1ULL << 36), // For channels - ChannelAmIn = (1ULL << 36), - StickersSet = (1ULL << 37), - EmojiSet = (1ULL << 38), - ChannelLinkedChat = (1ULL << 39), - ChannelLocation = (1ULL << 40), - Slowmode = (1ULL << 41), - GroupCall = (1ULL << 42), + ChannelAmIn = (1ULL << 37), + StickersSet = (1ULL << 38), + EmojiSet = (1ULL << 39), + ChannelLinkedChat = (1ULL << 40), + ChannelLocation = (1ULL << 41), + Slowmode = (1ULL << 42), + GroupCall = (1ULL << 43), // For iteration - LastUsedBit = (1ULL << 42), + LastUsedBit = (1ULL << 43), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 20fadb41de..1addfcadb2 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -167,6 +167,7 @@ ChatFilter ChatFilter::withTitle(const QString &title) const { ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { auto result = *this; + result._flags &= Flag::RulesMask; if (chatlist) { result._flags |= Flag::Chatlist; if (hasMyLinks) { @@ -174,8 +175,6 @@ ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { } else { result._flags &= ~Flag::HasMyLinks; } - } else { - result._flags &= ~(Flag::Chatlist | Flag::HasMyLinks); } return result; } @@ -201,6 +200,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), + MTPint(), // color MTP_vector(pinned), MTP_vector(include)); } @@ -226,6 +226,7 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), + MTPint(), // color MTP_vector(pinned), MTP_vector(include), MTP_vector(never)); @@ -366,8 +367,8 @@ void ChatFilters::load(bool force) { auto &api = _owner->session().api(); api.request(_loadRequestId).cancel(); _loadRequestId = api.request(MTPmessages_GetDialogFilters( - )).done([=](const MTPVector &result) { - received(result.v); + )).done([=](const MTPmessages_DialogFilters &result) { + received(result.data().vfilters().v); _loadRequestId = 0; }).fail([=] { _loadRequestId = 0; @@ -610,7 +611,7 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) { const auto id = filter.id(); const auto exceptionsChanged = filter.always() != updated.always(); - const auto rulesMask = ~(Flag::Chatlist | Flag::HasMyLinks); + const auto rulesMask = Flag() | Flag::RulesMask; const auto rulesChanged = exceptionsChanged || ((filter.flags() & rulesMask) != (updated.flags() & rulesMask)) || (filter.never() != updated.never()); diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 987d55ebe8..7b5a964768 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -36,9 +36,13 @@ public: NoMuted = (1 << 5), NoRead = (1 << 6), NoArchived = (1 << 7), + RulesMask = ((1 << 8) - 1), Chatlist = (1 << 8), HasMyLinks = (1 << 9), + + NewChats = (1 << 10), // Telegram Business exceptions. + ExistingChats = (1 << 11), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index cd736e9e7f..95c78092de 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -879,29 +879,20 @@ not_null DownloadManager::generateItem( const auto session = document ? &document->session() : &photo->session(); - const auto fromId = previousItem - ? previousItem->from()->id - : session->userPeerId(); const auto history = previousItem ? previousItem->history() : session->data().history(session->user()); - const auto flags = MessageFlag::FakeHistoryItem; - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto date = base::unixtime::now(); + ; const auto caption = TextWithEntities(); const auto make = [&](const auto media) { - return history->makeMessage( - history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - date, - fromId, - QString(), - media, - caption, - HistoryMessageMarkupData()); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem, + .from = (previousItem + ? previousItem->from()->id + : session->userPeerId()), + .date = base::unixtime::now(), + }, media, caption); }; const auto result = document ? make(document) : make(photo); _generated.emplace(result); diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 3f5e22a231..4fdd9159c0 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -96,6 +96,18 @@ public: [[nodiscard]] static constexpr DraftKey ScheduledEdit() { return kScheduledDraftIndex + kEditDraftShift; } + [[nodiscard]] static constexpr DraftKey Shortcut( + BusinessShortcutId shortcutId) { + return (shortcutId < 0 || shortcutId >= ServerMaxMsgId) + ? None() + : (kShortcutDraftShift + shortcutId); + } + [[nodiscard]] static constexpr DraftKey ShortcutEdit( + BusinessShortcutId shortcutId) { + return (shortcutId < 0 || shortcutId >= ServerMaxMsgId) + ? None() + : (kShortcutDraftShift + kEditDraftShift + shortcutId); + } [[nodiscard]] static constexpr DraftKey FromSerialized(qint64 value) { return value; @@ -156,6 +168,7 @@ private: static constexpr auto kScheduledDraftIndex = -3; static constexpr auto kEditDraftShift = ServerMaxMsgId.bare; static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare; + static constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare; static constexpr auto kEditDraftShiftOld = 0x3FFF'FFFF; int64 _value = 0; diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 9f62316525..5d3c11e0bf 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "api/api_text_entities.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -837,6 +838,7 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { remove.reserve(ids.size()); base::flat_map, QVector> idsByPeer; base::flat_map, QVector> scheduledIdsByPeer; + base::flat_map> quickIdsByShortcut; for (const auto &itemId : ids) { if (const auto item = _owner->message(itemId)) { const auto history = item->history(); @@ -850,6 +852,16 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { _owner->scheduledMessages().removeSending(item); } continue; + } else if (item->isBusinessShortcut()) { + const auto wasOnServer = !item->isSending() + && !item->hasFailed(); + if (wasOnServer) { + quickIdsByShortcut[item->shortcutId()].push_back(MTP_int( + _owner->shortcutMessages().lookupId(item))); + } else { + _owner->shortcutMessages().removeSending(item); + } + continue; } remove.push_back(item); if (item->isRegular()) { @@ -869,6 +881,15 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { peer->session().api().applyUpdates(result); }).send(); } + for (const auto &[shortcutId, ids] : quickIdsByShortcut) { + const auto api = &_owner->session().api(); + api->request(MTPmessages_DeleteQuickReplyMessages( + MTP_int(shortcutId), + MTP_vector(ids) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + } for (const auto item : remove) { const auto history = item->history(); diff --git a/Telegram/SourceFiles/data/data_location.h b/Telegram/SourceFiles/data/data_location.h index 7d9a59b4a7..a5e0090db8 100644 --- a/Telegram/SourceFiles/data/data_location.h +++ b/Telegram/SourceFiles/data/data_location.h @@ -26,7 +26,6 @@ public: [[nodiscard]] size_t hash() const; -private: friend inline bool operator==( const LocationPoint &a, const LocationPoint &b) { @@ -39,6 +38,7 @@ private: return (a._lat < b._lat) || ((a._lat == b._lat) && (a._lon < b._lon)); } +private: float64 _lat = 0; float64 _lon = 0; uint64 _access = 0; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 7361373df3..3e43cd47af 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1253,7 +1253,7 @@ const SharedContact *MediaContact::sharedContact() const { } TextWithEntities MediaContact::notificationText() const { - return tr::lng_in_dlg_contact(tr::now, Ui::Text::WithEntities); + return Ui::Text::Colorized(tr::lng_in_dlg_contact(tr::now)); } QString MediaContact::pinnedTextSubstring() const { diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index d2790a2881..b3ecb27b0b 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -54,6 +54,7 @@ Q_DECLARE_METATYPE(MsgId); } using StoryId = int32; +using BusinessShortcutId = int32; struct FullStoryId { PeerId peer = 0; @@ -77,7 +78,8 @@ constexpr auto ServerMaxStoryId = StoryId(1 << 30); constexpr auto StoryMsgIds = int64(ServerMaxStoryId); constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); -constexpr auto ScheduledMsgIdsRange = (1LL << 32); +constexpr auto ScheduledMaxMsgId = MsgId(ServerMaxMsgId + (1LL << 32)); +constexpr auto ShortcutMaxMsgId = MsgId(ScheduledMaxMsgId + (1LL << 32)); constexpr auto ShowAtUnreadMsgId = MsgId(0); constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 443474040c..ecca861493 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -217,4 +217,80 @@ bool PremiumLimits::isPremium() const { return _session->premium(); } +LevelLimits::LevelLimits(not_null session) +: _session(session) { +} + +int LevelLimits::channelColorLevelMin() const { + return _session->account().appConfig().get( + u"channel_color_level_min"_q, + 5); +} + +int LevelLimits::channelBgIconLevelMin() const { + return _session->account().appConfig().get( + u"channel_bg_icon_level_min"_q, + 4); +} + +int LevelLimits::channelProfileBgIconLevelMin() const { + return _session->account().appConfig().get( + u"channel_profile_bg_icon_level_min"_q, + 7); +} + +int LevelLimits::channelEmojiStatusLevelMin() const { + return _session->account().appConfig().get( + u"channel_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::channelWallpaperLevelMin() const { + return _session->account().appConfig().get( + u"channel_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::channelCustomWallpaperLevelMin() const { + return _session->account().appConfig().get( + u"channel_custom_wallpaper_level_min"_q, + 10); +} + +int LevelLimits::groupTranscribeLevelMin() const { + return _session->account().appConfig().get( + u"group_transcribe_level_min"_q, + 6); +} + +int LevelLimits::groupEmojiStickersLevelMin() const { + return _session->account().appConfig().get( + u"group_emoji_stickers_level_min"_q, + 4); +} + +int LevelLimits::groupProfileBgIconLevelMin() const { + return _session->account().appConfig().get( + u"group_profile_bg_icon_level_min"_q, + 5); +} + +int LevelLimits::groupEmojiStatusLevelMin() const { + return _session->account().appConfig().get( + u"group_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::groupWallpaperLevelMin() const { + return _session->account().appConfig().get( + u"group_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::groupCustomWallpaperLevelMin() const { + return _session->account().appConfig().get( + u"group_custom_wallpaper_level_min"_q, + 10); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index bc3c86d9ff..5c50d7a12a 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -91,4 +91,26 @@ private: }; +class LevelLimits final { +public: + LevelLimits(not_null session); + + [[nodiscard]] int channelColorLevelMin() const; + [[nodiscard]] int channelBgIconLevelMin() const; + [[nodiscard]] int channelProfileBgIconLevelMin() const; + [[nodiscard]] int channelEmojiStatusLevelMin() const; + [[nodiscard]] int channelWallpaperLevelMin() const; + [[nodiscard]] int channelCustomWallpaperLevelMin() const; + [[nodiscard]] int groupTranscribeLevelMin() const; + [[nodiscard]] int groupEmojiStickersLevelMin() const; + [[nodiscard]] int groupProfileBgIconLevelMin() const; + [[nodiscard]] int groupEmojiStatusLevelMin() const; + [[nodiscard]] int groupWallpaperLevelMin() const; + [[nodiscard]] int groupCustomWallpaperLevelMin() const; + +private: + const not_null _session; + +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 6c2a1a9d7c..0cd53700cb 100644 --- a/Telegram/SourceFiles/data/data_replies_list.cpp +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -38,11 +38,11 @@ constexpr auto kMaxMessagesToDeleteMyTopic = 10; not_null history, TimeId date, const QString &text) { - return history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::FakeHistoryItem, - date, - PreparedServiceText{ { .text = text } }); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem, + .date = date, + }, PreparedServiceText{ { .text = text } }); } [[nodiscard]] bool IsCreating(not_null history, MsgId rootId) { diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 9ea40996e5..3fe58f5305 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -88,15 +88,15 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.vgrouped_id().value_or_empty()), MTPMessageReactions(), MTPVector(), - MTP_int(data.vttl_period().value_or_empty())); + MTP_int(data.vttl_period().value_or_empty()), + MTPint()); // quick_reply_shortcut_id }); } } // namespace bool IsScheduledMsgId(MsgId id) { - return (id > ServerMaxMsgId) - && (id < ServerMaxMsgId + ScheduledMsgIdsRange); + return (id > ServerMaxMsgId) && (id < ScheduledMaxMsgId); } ScheduledMessages::ScheduledMessages(not_null owner) @@ -238,7 +238,8 @@ void ScheduledMessages::sendNowSimpleMessage( MTPlong(), MTPMessageReactions(), MTPVector(), - MTP_int(update.vttl_period().value_or_empty())), + MTP_int(update.vttl_period().value_or_empty()), + MTPint()), // quick_reply_shortcut_id localFlags, NewMessageType::Unread); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index f7cd3d44db..c686d86cfc 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -37,6 +37,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "passport/passport_form_controller.h" #include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name +#include "data/business/data_business_chatbots.h" +#include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" #include "data/stickers/data_stickers.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" @@ -245,10 +248,8 @@ Session::Session(not_null session) _session->local().cacheBigFilePath(), _session->local().cacheBigFileSettings())) , _groupFreeTranscribeLevel(session->account().appConfig().value( -) | rpl::map([=] { - return session->account().appConfig().get( - u"group_transcribe_level_min"_q, - 6); +) | rpl::map([limits = Data::LevelLimits(session)] { + return limits.groupTranscribeLevelMin(); })) , _chatsList( session, @@ -262,21 +263,24 @@ Session::Session(not_null session) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) , _chatsFilters(std::make_unique(this)) -, _scheduledMessages(std::make_unique(this)) , _cloudThemes(std::make_unique(session)) , _sendActionManager(std::make_unique()) , _streaming(std::make_unique(this)) , _mediaRotation(std::make_unique()) , _histories(std::make_unique(this)) , _stickers(std::make_unique(this)) -, _sponsoredMessages(std::make_unique(this)) , _reactions(std::make_unique(this)) , _emojiStatuses(std::make_unique(this)) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) , _customEmojiManager(std::make_unique(this)) , _stories(std::make_unique(this)) -, _savedMessages(std::make_unique(this)) { +, _savedMessages(std::make_unique(this)) +, _chatbots(std::make_unique(this)) +, _businessInfo(std::make_unique(this)) +, _scheduledMessages(std::make_unique(this)) +, _shortcutMessages(std::make_unique(this)) +, _sponsoredMessages(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -404,6 +408,7 @@ void Session::clear() { _histories->unloadAll(); _scheduledMessages = nullptr; + _shortcutMessages = nullptr; _sponsoredMessages = nullptr; _dependentMessages.clear(); base::take(_messages); @@ -2195,8 +2200,7 @@ rpl::producer Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([folder, limits = Data::PremiumLimits(_session)] { return folder ? limits.dialogsFolderPinnedPremium() : limits.dialogsPinnedPremium(); @@ -2210,8 +2214,7 @@ rpl::producer Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.dialogFiltersChatsPremium(); }); } @@ -2219,8 +2222,7 @@ rpl::producer Session::maxPinnedChatsLimitValue( rpl::producer Session::maxPinnedChatsLimitValue( not_null forum) const { return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.topicsPinnedCurrent(); }); } @@ -2232,8 +2234,7 @@ rpl::producer Session::maxPinnedChatsLimitValue( // because it slices the list to that limit. We don't want to slice // premium-ly added chats from the pinned list because of sync issues. return _session->account().appConfig().value( - ) | rpl::map([=] { - const auto limits = Data::PremiumLimits(_session); + ) | rpl::map([limits = Data::PremiumLimits(_session)] { return limits.savedSublistsPinnedPremium(); }); } @@ -4543,7 +4544,8 @@ void Session::insertCheckedServiceNotification( MTPlong(), MTPMessageReactions(), MTPVector(), - MTPint()), // ttl_period + MTPint(), // ttl_period + MTPint()), // quick_reply_shortcut_id localFlags, NewMessageType::Unread); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index d391d1d318..74204af26e 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -44,6 +44,7 @@ class Folder; class LocationPoint; class WallPaper; class ScheduledMessages; +class ShortcutMessages; class SendActionManager; class SponsoredMessages; class Reactions; @@ -62,6 +63,8 @@ class NotifySettings; class CustomEmojiManager; class Stories; class SavedMessages; +class Chatbots; +class BusinessInfo; struct ReactionId; struct RepliesReadTillUpdate { @@ -100,6 +103,9 @@ public: [[nodiscard]] ScheduledMessages &scheduledMessages() const { return *_scheduledMessages; } + [[nodiscard]] ShortcutMessages &shortcutMessages() const { + return *_shortcutMessages; + } [[nodiscard]] SendActionManager &sendActionManager() const { return *_sendActionManager; } @@ -142,6 +148,12 @@ public: [[nodiscard]] SavedMessages &savedMessages() const { return *_savedMessages; } + [[nodiscard]] Chatbots &chatbots() const { + return *_chatbots; + } + [[nodiscard]] BusinessInfo &businessInfo() const { + return *_businessInfo; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -1050,14 +1062,12 @@ private: Groups _groups; const std::unique_ptr _chatsFilters; - std::unique_ptr _scheduledMessages; const std::unique_ptr _cloudThemes; const std::unique_ptr _sendActionManager; const std::unique_ptr _streaming; const std::unique_ptr _mediaRotation; const std::unique_ptr _histories; const std::unique_ptr _stickers; - std::unique_ptr _sponsoredMessages; const std::unique_ptr _reactions; const std::unique_ptr _emojiStatuses; const std::unique_ptr _forumIcons; @@ -1065,8 +1075,13 @@ private: const std::unique_ptr _customEmojiManager; const std::unique_ptr _stories; const std::unique_ptr _savedMessages; + const std::unique_ptr _chatbots; + const std::unique_ptr _businessInfo; + std::unique_ptr _scheduledMessages; + std::unique_ptr _shortcutMessages; + std::unique_ptr _sponsoredMessages; - MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; + MsgId _nonHistoryEntryId = ShortcutMaxMsgId; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 8d5489d70f..6b762e2dbe 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -27,8 +27,7 @@ using SparseUnsortedIdsSlice = AbstractSparseIds>; class SparseIdsMergedSlice { public: using UniversalMsgId = MsgId; - static constexpr MsgId kScheduledTopicId - = ServerMaxMsgId + ScheduledMsgIdsRange; + static constexpr MsgId kScheduledTopicId = ScheduledMaxMsgId; struct Key { Key( diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index ccc8e12361..a22e54fcba 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -84,7 +84,7 @@ bool SponsoredMessages::append(not_null history) { entryIt->itemFullId = FullMsgId( history->peer->id, _session->data().nextLocalMessageId()); - entryIt->item.reset(history->addNewLocalMessage( + entryIt->item.reset(history->addSponsoredMessage( entryIt->itemFullId.msg, entryIt->sponsored.from, entryIt->sponsored.textWithEntities)); diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index a858899499..c9a14745af 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -974,7 +974,7 @@ std::shared_ptr Stories::resolveItem(not_null story) { } const auto history = _owner->history(story->peer()); auto result = std::shared_ptr( - history->makeMessage(story).get(), + history->makeMessage(StoryIdToMsgId(story->id()), story).get(), HistoryItem::Destroyer()); i->second = result; return result; diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 997d4282eb..ab792cd305 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -145,6 +145,15 @@ TimeId DateFromMessage(const MTPmessage &message) { }); } +BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message) { + return message.match([](const MTPDmessage &data) { + return data.vquick_reply_shortcut_id().value_or_empty(); + }, [](const auto &) { + return BusinessShortcutId(); + }); +} + bool GoodStickerDimensions(int width, int height) { // Show all .webp (except very large ones) as stickers, // allow to open them in media viewer to see details. diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 3b511cba01..ca11c8969a 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -109,10 +109,13 @@ using FilterId = int32; using MessageIdsList = std::vector; -PeerId PeerFromMessage(const MTPmessage &message); -MTPDmessage::Flags FlagsFromMessage(const MTPmessage &message); -MsgId IdFromMessage(const MTPmessage &message); -TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] PeerId PeerFromMessage(const MTPmessage &message); +[[nodiscard]] MTPDmessage::Flags FlagsFromMessage( + const MTPmessage &message); +[[nodiscard]] MsgId IdFromMessage(const MTPmessage &message); +[[nodiscard]] TimeId DateFromMessage(const MTPmessage &message); +[[nodiscard]] BusinessShortcutId BusinessShortcutIdFromMessage( + const MTPmessage &message); [[nodiscard]] inline MTPint MTP_int(MsgId id) noexcept { return MTP_int(id.bare); @@ -315,6 +318,8 @@ enum class MessageFlag : uint64 { Sponsored = (1ULL << 42), ReactionsAreTags = (1ULL << 43), + + ShortcutMessage = (1ULL << 44), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 543adf9b5e..a804e514af 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "storage/storage_user_photos.h" #include "main/main_session.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -67,6 +69,8 @@ UserData::UserData(not_null owner, PeerId id) , _flags((id == owner->session().userPeerId()) ? Flag::Self : Flag(0)) { } +UserData::~UserData() = default; + bool UserData::canShareThisContact() const { return canShareThisContactFast() || !owner().findContactPhone(peerToUser(id)).isEmpty(); @@ -179,6 +183,23 @@ void UserData::setStoriesState(StoriesState state) { } } +const Data::BusinessDetails &UserData::businessDetails() const { + static const auto empty = Data::BusinessDetails(); + return _businessDetails ? *_businessDetails : empty; +} + +void UserData::setBusinessDetails(Data::BusinessDetails details) { + details.hours = details.hours.normalized(); + if ((!details && !_businessDetails) + || (details && _businessDetails && details == *_businessDetails)) { + return; + } + _businessDetails = details + ? std::make_unique(std::move(details)) + : nullptr; + session().changes().peerUpdated(this, UpdateFlag::BusinessDetails); +} + void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) { bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty(); @@ -586,6 +607,16 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setWallPaper({}); } + user->setBusinessDetails(FromMTP( + update.vbusiness_work_hours(), + update.vbusiness_location())); + if (user->isSelf()) { + user->owner().businessInfo().applyAwaySettings( + FromMTP(&user->owner(), update.vbusiness_away_message())); + user->owner().businessInfo().applyGreetingSettings( + FromMTP(&user->owner(), update.vbusiness_greeting_message())); + } + user->owner().stories().apply(user, update.vstories()); user->fullUpdated(); diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index f6ba397496..cf9eafef70 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { struct BotCommand; +struct BusinessDetails; } // namespace Data struct BotInfo { @@ -84,6 +85,8 @@ public: using Flags = Data::Flags; UserData(not_null owner, PeerId id); + ~UserData(); + void setPhoto(const MTPUserProfilePhoto &photo); void setName( @@ -192,6 +195,9 @@ public: [[nodiscard]] bool hasUnreadStories() const; void setStoriesState(StoriesState state); + [[nodiscard]] const Data::BusinessDetails &businessDetails() const; + void setBusinessDetails(Data::BusinessDetails details); + private: auto unavailableReasons() const -> const std::vector & override; @@ -201,6 +207,7 @@ private: Data::UsernamesInfo _username; + std::unique_ptr _businessDetails; std::vector _unavailableReasons; QString _phone; QString _privateForwardName; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 2396b216f5..c43dc9f159 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -108,6 +108,7 @@ struct EntryState { Replies, SavedSublist, ContextMenu, + ShortcutMessages, }; Key key; diff --git a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp index a3d1264ffa..276d4e5b69 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp @@ -47,7 +47,7 @@ namespace { if (const auto controller = my.sessionWindow.get()) { ShowPremiumPreviewBox( controller, - PremiumPreview::TagsForMessages); + PremiumFeature::TagsForMessages); } }); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 95e3f374f3..326d265ac7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -2987,7 +2987,9 @@ void Widget::updateControlsGeometry() { if (_connecting) { _connecting->setBottomSkip(bottomSkip); } - controller()->setConnectingBottomSkip(bottomSkip); + if (_layout != Layout::Child) { + controller()->setConnectingBottomSkip(bottomSkip); + } const auto wasScrollTop = _scroll->scrollTop(); const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0) diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index f7928f006b..1f56f9ec19 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -160,6 +160,30 @@ std::vector> ButtonRowsFromTL( } // namespace +QByteArray HistoryMessageMarkupButton::TypeToString( + const HistoryMessageMarkupButton &button) { + using Type = HistoryMessageMarkupButton::Type; + switch (button.type) { + case Type::Default: return "default"; + case Type::Url: return "url"; + case Type::Callback: return "callback"; + case Type::CallbackWithPassword: return "callback_with_password"; + case Type::RequestPhone: return "request_phone"; + case Type::RequestLocation: return "request_location"; + case Type::RequestPoll: return "request_poll"; + case Type::RequestPeer: return "request_peer"; + case Type::SwitchInline: return "switch_inline"; + case Type::SwitchInlineSame: return "switch_inline_same"; + case Type::Game: return "game"; + case Type::Buy: return "buy"; + case Type::Auth: return "auth"; + case Type::UserProfile: return "user_profile"; + case Type::WebView: return "web_view"; + case Type::SimpleWebView: return "simple_web_view"; + } + Unexpected("Type in HistoryMessageMarkupButton::Type."); +} + uint8 PeerColorIndex(BareId bareId) { const uint8 map[] = { 0, 7, 4, 1, 6, 3, 5 }; return map[bareId % base::array_size(map)]; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 5f2c2da393..76585991ca 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -690,6 +690,8 @@ struct HistoryMessageMarkupButton { SimpleWebView, }; + static QByteArray TypeToString(const HistoryMessageMarkupButton &); + Type type; QString text; QByteArray data; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 1aa595089f..af5a48a09a 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1443,6 +1443,55 @@ auto HtmlWriter::Wrap::pushMessage( block.append(text); block.append(popTag()); } + if (!message.inlineButtonRows.empty()) { + using Type = HistoryMessageMarkupButton::Type; + const auto endline = u" | "_q; + block.append(pushTag("table", { { "class", "bot_buttons_table" } })); + block.append(pushTag("tbody")); + for (const auto &row : message.inlineButtonRows) { + block.append(pushTag("tr")); + block.append(pushTag("td", { { "class", "bot_button_row" } })); + for (const auto &button : row) { + using Attribute = std::pair; + const auto content = (!button.data.isEmpty() + ? (u"Data: "_q + button.data + endline) + : QString()) + + (!button.forwardText.isEmpty() + ? (u"Forward text: "_q + button.forwardText + endline) + : QString()) + + (u"Type: "_q + + HistoryMessageMarkupButton::TypeToString(button)); + const auto link = (button.type == Type::Url) + ? button.data + : QByteArray(); + const auto onclick = (button.type != Type::Url) + ? ("return ShowTextCopied('" + content + "');").toUtf8() + : QByteArray(); + block.append(pushTag("div", { { "class", "bot_button" } })); + block.append(pushTag("a", { + link.isEmpty() ? Attribute() : Attribute{ "href", link }, + onclick.isEmpty() + ? Attribute() + : Attribute{ "onclick", onclick }, + })); + block.append(pushTag("div")); + block.append(button.text.toUtf8()); + block.append(popTag()); + block.append(popTag()); + block.append(popTag()); + + if (&button != &row.back()) { + block.append(pushTag("div", { + { "class", "bot_button_column_separator" } + })); + block.append(popTag()); + } + } + block.append(popTag()); + block.append(popTag()); + } + block.append(popTag()); + } if (!message.signature.isEmpty()) { block.append(pushDiv("signature details")); block.append(SerializeString(message.signature)); diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index dde36a5a38..5bd7f2c376 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -784,29 +784,6 @@ QByteArray SerializeMessage( pushBare("text_entities", SerializeText(context, message.text, true)); if (!message.inlineButtonRows.empty()) { - const auto typeString = []( - const HistoryMessageMarkupButton &entry) -> QByteArray { - using Type = HistoryMessageMarkupButton::Type; - switch (entry.type) { - case Type::Default: return "default"; - case Type::Url: return "url"; - case Type::Callback: return "callback"; - case Type::CallbackWithPassword: return "callback_with_password"; - case Type::RequestPhone: return "request_phone"; - case Type::RequestLocation: return "request_location"; - case Type::RequestPoll: return "request_poll"; - case Type::RequestPeer: return "request_peer"; - case Type::SwitchInline: return "switch_inline"; - case Type::SwitchInlineSame: return "switch_inline_same"; - case Type::Game: return "game"; - case Type::Buy: return "buy"; - case Type::Auth: return "auth"; - case Type::UserProfile: return "user_profile"; - case Type::WebView: return "web_view"; - case Type::SimpleWebView: return "simple_web_view"; - } - Unexpected("Type in HistoryMessageMarkupButton::Type."); - }; const auto serializeRow = [&]( const std::vector &row) { context.nesting.push_back(Context::kArray); @@ -817,7 +794,8 @@ QByteArray SerializeMessage( auto pairs = std::vector>(); pairs.push_back({ "type", - SerializeString(typeString(entry)), + SerializeString( + HistoryMessageMarkupButton::TypeToString(entry)), }); if (!entry.text.isEmpty()) { pairs.push_back({ diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index bbc91c2dcd..8f546260a7 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -78,7 +78,7 @@ void ChooseFormatBox( addFormatOption( tr::lng_export_option_html_and_json(tr::now), Format::HtmlAndJson); - box->addButton(tr::lng_settings_save(), [=] { done(group->value()); }); + box->addButton(tr::lng_settings_save(), [=] { done(group->current()); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 0642a15688..14f9718fc7 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -137,7 +137,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTP_long(0), // grouped_id MTPMessageReactions(), MTPVector(), - MTPint()); // ttl_period + MTPint(), // ttl_period + MTPint()); // quick_reply_shortcut_id }); } @@ -800,13 +801,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); addPart( - history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id), - photo), + history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message), photo), 0, realId); }; @@ -825,23 +825,12 @@ void GenerateItems( }; const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { - const auto bodyFlags = MessageFlag::HasFromId - | MessageFlag::AdminLogEntry; - const auto bodyReplyTo = FullReplyTo(); - const auto bodyViaBotId = UserId(); - const auto bodyGroupedId = uint64(); - return history->makeMessage( - history->nextNonHistoryEntryId(), - bodyFlags, - bodyReplyTo, - bodyViaBotId, - date, - peerToUser(from->id), - QString(), - std::move(text), - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - bodyGroupedId); + return history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(text), MTP_messageMediaEmpty()); }; const auto addSimpleTextMessage = [&](TextWithEntities &&text) { @@ -1144,12 +1133,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(setLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1188,12 +1177,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(setLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1269,12 +1258,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(chatLink); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); } }; @@ -1365,12 +1354,12 @@ void GenerateItems( auto message = PreparedServiceText{ text }; message.links.push_back(fromLink); message.links.push_back(link); - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id))); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); }; const auto createParticipantMute = [&](const LogMute &data) { @@ -1440,13 +1429,12 @@ void GenerateItems( if (additional) { message.links.push_back(std::move(additional)); } - addPart(history->makeMessage( - history->nextNonHistoryEntryId(), - MessageFlag::AdminLogEntry, - date, - std::move(message), - peerToUser(from->id), - nullptr)); + addPart(history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::AdminLogEntry, + .from = from->id, + .date = date, + }, std::move(message))); }; const auto createParticipantJoinByInvite = [&]( diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 52a9803d39..bdfa6d1d03 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_translation.h" #include "history/history_unread_things.h" #include "dialogs/ui/dialogs_layout.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -76,6 +77,12 @@ constexpr auto kSkipCloudDraftsFor = TimeId(2); using UpdateFlag = Data::HistoryUpdate::Flag; +[[nodiscard]] HistoryItemCommonFields WithLocalFlag( + HistoryItemCommonFields fields) { + fields.flags |= MessageFlag::Local; + return fields; +} + } // namespace History::History(not_null owner, PeerId peerId) @@ -451,17 +458,17 @@ std::vector> History::createItems( not_null History::addNewMessage( MsgId id, - const MTPMessage &msg, + const MTPMessage &message, MessageFlags localFlags, NewMessageType type) { - const auto detachExistingItem = (type == NewMessageType::Unread); - const auto item = createItem(id, msg, localFlags, detachExistingItem); + const auto detachExisting = (type == NewMessageType::Unread); + const auto item = createItem(id, message, localFlags, detachExisting); if (type == NewMessageType::Existing || item->mainView()) { return item; } const auto unread = (type == NewMessageType::Unread); if (unread && item->isHistoryEntry()) { - applyMessageChanges(item, msg); + applyMessageChanges(item, message); } return addNewItem(item, unread); } @@ -593,6 +600,9 @@ not_null History::addNewItem( if (item->isScheduled()) { owner().scheduledMessages().appendSending(item); return item; + } else if (item->isBusinessShortcut()) { + owner().shortcutMessages().appendSending(item); + return item; } else if (!item->isHistoryEntry()) { return item; } @@ -643,139 +653,54 @@ void History::checkForLoadedAtTop(not_null added) { } not_null History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &text, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId) { + const MTPMessageMedia &media) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - text, - media, - std::move(markup), - groupedId), + makeMessage(WithLocalFlag(std::move(fields)), text, media), true); } not_null History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null forwardOriginal, - MsgId topicRootId) { + HistoryItemCommonFields &&fields, + not_null forwardOriginal) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - date, - from, - postAuthor, - forwardOriginal, - topicRootId), + makeMessage(WithLocalFlag(std::move(fields)), forwardOriginal), true); } not_null History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + const TextWithEntities &caption) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - document, - caption, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), document, caption), true); } not_null History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) { + const TextWithEntities &caption) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - photo, - caption, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), photo, caption), true); } not_null History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null game, - HistoryMessageMarkupData &&markup) { + HistoryItemCommonFields &&fields, + not_null game) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - replyTo, - viaBotId, - date, - from, - postAuthor, - game, - std::move(markup)), + makeMessage(WithLocalFlag(std::move(fields)), game), true); } -not_null History::addNewLocalMessage( +not_null History::addSponsoredMessage( MsgId id, Data::SponsoredFrom from, const TextWithEntities &textWithEntities) { return addNewItem( - makeMessage( - id, - from, - textWithEntities, - nullptr), + makeMessage(id, from, textWithEntities, nullptr), true); } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index adeb63fc26..29620dc2e9 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -20,6 +20,7 @@ class History; class HistoryBlock; class HistoryTranslation; class HistoryItem; +struct HistoryItemCommonFields; struct HistoryMessageMarkupData; class HistoryMainElementDelegateMixin; struct LanguageId; @@ -127,11 +128,23 @@ public: void applyGroupAdminChanges(const base::flat_set &changes); template - not_null makeMessage(Args &&...args) { + not_null makeMessage(MsgId id, Args &&...args) { return static_cast( insertItem( std::make_unique( this, + id, + std::forward(args)...)).get()); + } + template + not_null makeMessage( + HistoryItemCommonFields &&fields, + Args &&...args) { + return static_cast( + insertItem( + std::make_unique( + this, + std::move(fields), std::forward(args)...)).get()); } @@ -143,62 +156,30 @@ public: not_null addNewMessage( MsgId id, - const MTPMessage &msg, + const MTPMessage &message, MessageFlags localFlags, NewMessageType type); + not_null addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &text, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId = 0); + const MTPMessageMedia &media); not_null addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null forwardOriginal, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null forwardOriginal); not_null addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); - not_null addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null game, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null addNewLocalMessage( + HistoryItemCommonFields &&fields, + not_null game); + + not_null addSponsoredMessage( MsgId id, Data::SponsoredFrom from, const TextWithEntities &textWithEntities); // sponsored diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index eedcab8935..b3e4639e1d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_inner_widget.h" +#include "chat_helpers/stickers_emoji_pack.h" #include "core/file_utilities.h" #include "core/click_handler_types.h" #include "history/history_item_helpers.h" @@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_options.h" +#include "ui/text/text_isolated_emoji.h" #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "ui/controls/delete_message_context_action.h" @@ -163,7 +165,7 @@ void FillSponsoredMessagesMenu( menu->addSeparator(&st::expandedMenuSeparator); } menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] { - ShowPremiumPreviewBox(controller, PremiumPreview::NoAds); + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); }, &st::menuIconCancel); } @@ -2249,6 +2251,17 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }; + if (const auto item = _dragStateItem) { + const auto emojiStickers = &session->emojiStickersPack(); + if (const auto view = item->mainView()) { + if (const auto isolated = view->isolatedEmoji()) { + if (const auto sticker = emojiStickers->stickerForEmoji(isolated)) { + addDocumentActions(sticker.document, item); + } + } + } + } + const auto asGroup = !Element::Moused() || (Element::Moused() != Element::Hovered()) || (Element::Moused()->pointState( diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 1ccc07cf9a..1ebead4c82 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -131,6 +131,14 @@ template return false; } +[[nodiscard]] HistoryItemCommonFields ForwardedFields( + HistoryItemCommonFields fields, + not_null history, + not_null original) { + fields.flags |= NewForwardedFlags(history->peer, fields.from, original); + return fields; +} + } // namespace void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) { @@ -353,12 +361,13 @@ HistoryItem::HistoryItem( MsgId id, const MTPDmessage &data, MessageFlags localFlags) -: HistoryItem( - history, - id, - FlagsFromMTP(id, data.vflags().v, localFlags), - data.vdate().v, - data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { +: HistoryItem(history, { + .id = id, + .flags = FlagsFromMTP(id, data.vflags().v, localFlags), + .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), + .date = data.vdate().v, + .shortcutId = data.vquick_reply_shortcut_id().value_or_empty(), +}) { _boostsApplied = data.vfrom_boosts_applied().value_or_empty(); const auto media = data.vmedia(); @@ -445,12 +454,12 @@ HistoryItem::HistoryItem( MsgId id, const MTPDmessageService &data, MessageFlags localFlags) -: HistoryItem( - history, - id, - FlagsFromMTP(id, data.vflags().v, localFlags), - data.vdate().v, - data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { +: HistoryItem(history, { + .id = id, + .flags = FlagsFromMTP(id, data.vflags().v, localFlags), + .from = data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0), + .date = data.vdate().v, +}) { if (data.vaction().type() != mtpc_messageActionPhoneCall) { createServiceFromMtp(data); } else { @@ -470,9 +479,7 @@ HistoryItem::HistoryItem( MessageFlags localFlags) : HistoryItem( history, - id, - localFlags, - TimeId(0), + { .id = id, .flags = localFlags }, PreparedServiceText{ tr::lng_message_empty( tr::now, Ui::Text::WithEntities) }) { @@ -480,13 +487,10 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - TimeId date, + HistoryItemCommonFields &&fields, PreparedServiceText &&message, - PeerId from, PhotoData *photo) -: HistoryItem(history, id, flags, date, from) { +: HistoryItem(history, fields) { setServiceText(std::move(message)); if (photo) { _media = std::make_unique( @@ -498,25 +502,16 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null original, - MsgId topicRootId) -: HistoryItem( - history, - id, - (NewForwardedFlags(history->peer, from, original) | flags), - date, - from) { + HistoryItemCommonFields &&fields, + not_null original) +: HistoryItem(history, ForwardedFields(fields, history, original)) { const auto peer = history->peer; auto config = CreateConfig(); const auto originalMedia = original->media(); const auto dropForwardInfo = original->computeDropForwardedInfo(); + const auto topicRootId = fields.replyTo.topicRootId; config.reply.messageId = config.reply.topMessageId = topicRootId; config.reply.topicPost = (topicRootId != 0) ? 1 : 0; if (const auto originalReply = original->Get()) { @@ -559,8 +554,8 @@ HistoryItem::HistoryItem( ? original->author()->id : PeerId(); } - if (flags & MessageFlag::HasPostAuthor) { - config.postAuthor = postAuthor; + if (_flags & MessageFlag::HasPostAuthor) { + config.postAuthor = fields.postAuthor; } if (const auto fwdViaBot = original->viaBot()) { config.viaBotId = peerToUser(fwdViaBot->id); @@ -610,63 +605,28 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &textWithEntities, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const MTPMessageMedia &media) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); setMedia(media); setText(textWithEntities); - if (groupedId) { + if (fields.groupedId) { setGroupId(MessageGroupId::FromRaw( history->peer->id, - groupedId, - flags & MessageFlag::IsOrWasScheduled)); + fields.groupedId, + _flags & MessageFlag::IsOrWasScheduled)); } } HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const TextWithEntities &caption) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); const auto skipPremiumEffect = !history->session().premium(); const auto spoiler = false; @@ -681,28 +641,11 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + const TextWithEntities &caption) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); const auto spoiler = false; _media = std::make_unique(this, photo, spoiler); @@ -711,27 +654,10 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null game, - HistoryMessageMarkupData &&markup) -: HistoryItem( - history, - id, - flags, - date, - (flags & MessageFlag::HasFromId) ? from : 0) { - createComponentsHelper( - flags, - replyTo, - viaBotId, - postAuthor, - std::move(markup)); + HistoryItemCommonFields &&fields, + not_null game) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); _media = std::make_unique(this, game); setTextValue({}); @@ -743,18 +669,15 @@ HistoryItem::HistoryItem( Data::SponsoredFrom from, const TextWithEntities &textWithEntities, HistoryItem *injectedAfter) -: HistoryItem( - history, - id, - ((history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0)) - //| (from.peer ? MessageFlag::HasFromId : MessageFlag(0)) - | MessageFlag::Local), - HistoryItem::NewMessageDate(injectedAfter - ? injectedAfter->date() - : 0), - /*from.peer ? from.peer->id : */PeerId(0)) { - _flags |= MessageFlag::Sponsored; - +: HistoryItem(history, { + .id = id, + .flags = (MessageFlag::Local + | MessageFlag::Sponsored + | (history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0))), + .date = HistoryItem::NewMessageDate(injectedAfter + ? injectedAfter->date() + : 0), +}) { const auto webPageType = !from.externalLink.isEmpty() ? WebPageType::None : from.isExactPost @@ -797,15 +720,15 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from) -: id(id) + const HistoryItemCommonFields &fields) +: id(fields.id) , _history(history) -, _from(from ? history->owner().peer(from) : history->peer) -, _flags(FinalizeMessageFlags(history, flags)) -, _date(date) { +, _from((fields.flags & MessageFlag::HasFromId && fields.from) + ? history->owner().peer(fields.from) + : history->peer) +, _flags(FinalizeMessageFlags(history, fields.flags)) +, _date(fields.date) +, _shortcutId(fields.shortcutId) { if (isHistoryEntry() && IsClientMsgId(id)) { _history->registerClientSideMessage(this); } @@ -813,15 +736,18 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null history, + MsgId id, not_null story) -: id(StoryIdToMsgId(story->id())) -, _history(history) -, _from(history->peer) -, _flags(MessageFlag::Local - | MessageFlag::Outgoing - | MessageFlag::FakeHistoryItem - | MessageFlag::StoryItem) -, _date(story->date()) { +: HistoryItem(history, { + .id = id, + .flags = (MessageFlag::Local + | MessageFlag::Outgoing + | MessageFlag::HasFromId + | MessageFlag::FakeHistoryItem + | MessageFlag::StoryItem), + .from = history->peer->id, + .date = story->date(), +}) { setStoryFields(story); } @@ -846,6 +772,11 @@ TimeId HistoryItem::NewMessageDate(TimeId scheduled) { return scheduled ? scheduled : base::unixtime::now(); } +TimeId HistoryItem::NewMessageDate( + const Api::SendOptions &options) { + return options.shortcutId ? 1 : NewMessageDate(options.scheduled); +} + HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { if (const auto pinned = Get()) { return pinned; @@ -1655,6 +1586,18 @@ bool HistoryItem::isUserpicSuggestion() const { return (_flags & MessageFlag::IsUserpicSuggestion); } +BusinessShortcutId HistoryItem::shortcutId() const { + return _shortcutId; +} + +bool HistoryItem::isBusinessShortcut() const { + return _shortcutId != 0; +} + +void HistoryItem::setRealShortcutId(BusinessShortcutId id) { + _shortcutId = id; +} + void HistoryItem::destroy() { _history->destroyMessage(this); } @@ -2150,6 +2093,9 @@ void HistoryItem::setRealId(MsgId newId) { const auto oldId = std::exchange(id, newId); _flags &= ~(MessageFlag::BeingSent | MessageFlag::Local); + if (isBusinessShortcut()) { + _date = 0; + } if (isRegular()) { _history->unregisterClientSideMessage(this); } @@ -2214,7 +2160,7 @@ bool HistoryItem::allowsEdit(TimeId now) const { } bool HistoryItem::canBeEdited() const { - if ((!isRegular() && !isScheduled()) + if ((!isRegular() && !isScheduled() && !isBusinessShortcut()) || Has() || Has()) { return false; @@ -2267,7 +2213,9 @@ bool HistoryItem::canDelete() const { return false; } else if (topicRootId() == id) { return false; - } else if (!isHistoryEntry() && !isScheduled()) { + } else if (!isHistoryEntry() + && !isScheduled() + && !isBusinessShortcut()) { return false; } auto channel = _history->peer->asChannel(); @@ -3617,15 +3565,11 @@ TextWithEntities HistoryItem::withLocalEntities( return textWithEntities; } -void HistoryItem::createComponentsHelper( - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) { +void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { + const auto &replyTo = fields.replyTo; auto config = CreateConfig(); - config.viaBotId = viaBotId; - if (flags & MessageFlag::HasReplyInfo) { + config.viaBotId = fields.viaBotId; + if (fields.flags & MessageFlag::HasReplyInfo) { config.reply.messageId = replyTo.messageId.msg; config.reply.storyId = replyTo.storyId.story; config.reply.externalPeerId = replyTo.storyId @@ -3664,9 +3608,13 @@ void HistoryItem::createComponentsHelper( config.reply.quoteOffset = replyTo.quoteOffset; config.reply.quote = std::move(replyTo.quote); } - config.markup = std::move(markup); - if (flags & MessageFlag::HasPostAuthor) config.postAuthor = postAuthor; - if (flags & MessageFlag::HasViews) config.viewsCount = 1; + config.markup = std::move(fields.markup); + if (fields.flags & MessageFlag::HasPostAuthor) { + config.postAuthor = fields.postAuthor; + } + if (fields.flags & MessageFlag::HasViews) { + config.viewsCount = 1; + } createComponents(std::move(config)); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 689bca9b8a..b81a0057c4 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HiddenSenderInfo; class History; + struct HistoryMessageReply; struct HistoryMessageViews; struct HistoryMessageMarkupData; @@ -28,6 +29,10 @@ struct PreparedServiceText; class ReplyKeyboard; struct LanguageId; +namespace Api { +struct SendOptions; +} // namespace Api + namespace base { template class enum_mask; @@ -86,6 +91,19 @@ class Service; class ServiceMessagePainter; } // namespace HistoryView +struct HistoryItemCommonFields { + MsgId id = 0; + MessageFlags flags = 0; + PeerId from = 0; + FullReplyTo replyTo; + TimeId date = 0; + BusinessShortcutId shortcutId = 0; + UserId viaBotId = 0; + QString postAuthor; + uint64 groupedId = 0; + HistoryMessageMarkupData markup; +}; + class HistoryItem final : public RuntimeComposer { public: [[nodiscard]] static std::unique_ptr CreateMedia( @@ -114,73 +132,39 @@ public: Data::SponsoredFrom from, const TextWithEntities &textWithEntities, HistoryItem *injectedAfter); + HistoryItem( // Story wrap. + not_null history, + MsgId id, + not_null story); HistoryItem( // Local message. not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, const TextWithEntities &textWithEntities, - const MTPMessageMedia &media, - HistoryMessageMarkupData &&markup, - uint64 groupedId); + const MTPMessageMedia &media); HistoryItem( // Local service message. not_null history, - MsgId id, - MessageFlags flags, - TimeId date, + HistoryItemCommonFields &&fields, PreparedServiceText &&message, - PeerId from = 0, PhotoData *photo = nullptr); HistoryItem( // Local forwarded. not_null history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null original, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null original); HistoryItem( // Local photo. not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local document. not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local game. not_null history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null game, - HistoryMessageMarkupData &&markup); - HistoryItem(not_null history, not_null story); + HistoryItemCommonFields &&fields, + not_null game); ~HistoryItem(); struct Destroyer { @@ -210,6 +194,9 @@ public: [[nodiscard]] bool isSponsored() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; + [[nodiscard]] BusinessShortcutId shortcutId() const; + [[nodiscard]] bool isBusinessShortcut() const; + void setRealShortcutId(BusinessShortcutId id); void addLogEntryOriginal( WebPageId localId, @@ -468,6 +455,8 @@ public: [[nodiscard]] TimeId date() const; [[nodiscard]] static TimeId NewMessageDate(TimeId scheduled); + [[nodiscard]] static TimeId NewMessageDate( + const Api::SendOptions &options); [[nodiscard]] Data::Media *media() const { return _media.get(); @@ -549,17 +538,9 @@ private: HistoryItem( not_null history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from); + const HistoryItemCommonFields &fields); - void createComponentsHelper( - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - const QString &postAuthor, - HistoryMessageMarkupData &&markup); + void createComponentsHelper(HistoryItemCommonFields &&fields); void createComponents(CreateConfig &&config); void setupForwardedComponent(const CreateConfig &config); @@ -663,6 +644,7 @@ private: TimeId _date = 0; TimeId _ttlDestroyAt = 0; int _boostsApplied = 0; + BusinessShortcutId _shortcutId = 0; HistoryView::Element *_mainView = nullptr; MessageGroupId _groupId = MessageGroupId(); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 05e99d2a21..aeef375578 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "media/audio/media_audio.h" #include "media/player/media_player_instance.h" +#include "data/business/data_shortcut_messages.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_media_types.h" @@ -301,9 +302,11 @@ ReplyFields ReplyFieldsFromMTP( if (const auto id = data.vreply_to_msg_id().value_or_empty()) { result.messageId = data.is_reply_to_scheduled() ? owner->scheduledMessages().localMessageId(id) + : item->shortcutId() + ? owner->shortcutMessages().localMessageId(id) : id; result.topMessageId - = data.vreply_to_top_id().value_or(id); + = data.vreply_to_top_id().value_or(result.messageId.bare); result.topicPost = data.is_forum_topic() ? 1 : 0; } if (const auto header = data.vreply_from()) { diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 6fdc3de55b..25bbcc789d 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -366,7 +366,7 @@ ClickHandlerPtr HideSponsoredClickHandler() { return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { - ShowPremiumPreviewBox(controller, PremiumPreview::NoAds); + ShowPremiumPreviewBox(controller, PremiumFeature::NoAds); } }); } @@ -390,6 +390,9 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag()) | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag()) | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag()) + | ((flags & MTP::f_quick_reply_shortcut_id) + ? Flag::ShortcutMessage + : Flag()) | ((flags & MTP::f_from_scheduled) ? Flag::IsOrWasScheduled : Flag()) @@ -603,11 +606,11 @@ not_null GenerateJoinedMessage( TimeId inviteDate, not_null inviter, bool viaRequest) { - return history->makeMessage( - history->owner().nextLocalMessageId(), - MessageFlag::Local | MessageFlag::ShowSimilarChannels, - inviteDate, - GenerateJoinedText(history, inviter, viaRequest)); + return history->makeMessage({ + .id = history->owner().nextLocalMessageId(), + .flags = MessageFlag::Local | MessageFlag::ShowSimilarChannels, + .date = inviteDate, + }, GenerateJoinedText(history, inviter, viaRequest)); } std::optional PeerHasThisCall( @@ -662,6 +665,7 @@ std::optional PeerHasThisCall( MessageFlags flags) { if (!(flags & MessageFlag::FakeHistoryItem) && !(flags & MessageFlag::IsOrWasScheduled) + && !(flags & MessageFlag::ShortcutMessage) && !(flags & MessageFlag::AdminLogEntry)) { flags |= MessageFlag::HistoryEntry; if (history->peer->isSelf()) { @@ -794,7 +798,7 @@ void ShowTrialTranscribesToast(int left, TimeId until) { } const auto filter = [=](const auto &...) { if (const auto controller = window->sessionController()) { - ShowPremiumPreviewBox(controller, PremiumPreview::VoiceToText); + ShowPremiumPreviewBox(controller, PremiumFeature::VoiceToText); window->activate(); } return false; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a8652e72f4..a0e154edce 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "boxes/edit_caption_box.h" #include "boxes/premium_limits_box.h" +#include "boxes/premium_preview_box.h" #include "boxes/peers/edit_peer_permissions_box.h" // ShowAboutGigagroup. #include "boxes/peers/edit_peer_requests_box.h" #include "core/file_utilities.h" @@ -52,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" #include "base/call_delayed.h" +#include "data/business/data_shortcut_messages.h" #include "data/notify/data_notify_settings.h" #include "data/data_changes.h" #include "data/data_drafts.h" @@ -121,6 +123,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwidget.h" #include "mainwindow.h" +#include "settings/business/settings_quick_replies.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" #include "storage/file_upload.h" @@ -384,6 +387,11 @@ HistoryWidget::HistoryWidget( checkFieldAutocomplete(); }, Qt::QueuedConnection); + controller->session().data().shortcutMessages().shortcutsChanged( + ) | rpl::start_with_next([=] { + checkFieldAutocomplete(); + }, lifetime()); + _fieldBarCancel->hide(); _topBar->hide(); @@ -439,7 +447,27 @@ HistoryWidget::HistoryWidget( _fieldAutocomplete->botCommandChosen( ) | rpl::start_with_next([=](FieldAutocomplete::BotCommandChosen data) { - insertHashtagOrBotCommand(data.command, data.method); + using Method = FieldAutocomplete::ChooseMethod; + const auto messages = &data.user->owner().shortcutMessages(); + const auto shortcut = data.user->isSelf(); + const auto command = data.command.mid(1); + const auto byTab = (data.method == Method::ByTab); + const auto shortcutId = (_peer && shortcut && !byTab) + ? messages->lookupShortcutId(command) + : BusinessShortcutId(); + if (shortcut && command.isEmpty()) { + controller->showSettings(Settings::QuickRepliesId()); + } else if (!shortcutId) { + insertHashtagOrBotCommand(data.command, data.method); + } else if (!_peer->session().premium()) { + ShowPremiumPreviewToBuy( + controller, + PremiumFeature::QuickReplies); + } else { + session().api().sendShortcutMessages(_peer, shortcutId); + session().api().finishForwarding(prepareSendAction({})); + setFieldText(_field->getTextWithTagsPart(_field->textCursor().position())); + } }, lifetime()); _fieldAutocomplete->setModerateKeyActivateCallback([=](int key) { @@ -1431,9 +1459,14 @@ AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { } else if (result.query[0] == '@' && cRecentInlineBots().isEmpty()) { session().local().readRecentHashtagsAndBots(); - } else if (result.query[0] == '/' - && ((_peer->isUser() && !_peer->asUser()->isBot()) || _editMsgId)) { - return AutocompleteQuery(); + } else if (result.query[0] == '/') { + if (_editMsgId) { + return {}; + } else if (_peer->isUser() + && !_peer->asUser()->isBot() + && _peer->owner().shortcutMessages().shortcuts().list.empty()) { + return {}; + } } return result; } @@ -1974,6 +2007,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { updateControlsVisibility(); updateControlsGeometry(); refreshTopBarActiveChat(); + checkCharsLimitation(); if (_editMsgId) { updateReplyEditTexts(); if (!_replyEditMsg) { @@ -3893,17 +3927,12 @@ void HistoryWidget::saveEditMsg() { return; } const auto webPageDraft = _preview->draft(); - auto left = prepareTextForEditMsg(); - auto sending = TextWithEntities(); + const auto sending = prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) + if (sending.text.isEmpty() && (webPageDraft.removed || webPageDraft.url.isEmpty() || !webPageDraft.manual) @@ -3912,11 +3941,22 @@ void HistoryWidget::saveEditMsg() { controller()->show( Box(item, suggestModerateActions)); return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); +#ifndef _DEBUG + return; +#else + if (!base::IsCtrlPressed()) { + return; + } +#endif + } } const auto weak = Ui::MakeWeak(this); @@ -7352,13 +7392,16 @@ void HistoryWidget::checkCharsLimitation() { return; } const auto item = session().data().message(_history->peer, _editMsgId); - if (!item || !item->media() || !item->media()->allowsEditCaption()) { + if (!item) { _charsLimitation = nullptr; return; } - const auto limits = Data::PremiumLimits(&session()); - const auto left = prepareTextForEditMsg(); - const auto remove = left.text.size() - limits.captionLengthCurrent(); + const auto hasMediaWithCaption = item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; if (remove > 0) { if (!_charsLimitation) { _charsLimitation = base::make_unique_q( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 505950697a..c7da2180c6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -114,6 +114,9 @@ using ForwardPanel = Controls::ForwardPanel; } // namespace +const ChatHelpers::PauseReason kDefaultPanelsLevel + = ChatHelpers::PauseReason::TabbedPanel; + class FieldHeader final : public Ui::RpWidget { public: FieldHeader( @@ -760,7 +763,10 @@ MessageToEdit FieldHeader::queryToEdit() { } return { .fullId = item->fullId(), - .options = { .scheduled = item->isScheduled() ? item->date() : 0 }, + .options = { + .scheduled = item->isScheduled() ? item->date() : 0, + .shortcutId = item->shortcutId(), + }, }; } @@ -788,21 +794,24 @@ ComposeControls::ComposeControls( : st::defaultComposeControls) , _features(descriptor.features) , _parent(parent) +, _panelsParent(descriptor.panelsParent + ? descriptor.panelsParent + : _parent.get()) , _show(std::move(descriptor.show)) , _session(&_show->session()) , _regularWindow(descriptor.regularWindow) -, _ownedSelector(_regularWindow +, _ownedSelector((_regularWindow && _features.commonTabbedPanel) ? nullptr : std::make_unique( - _parent, + _panelsParent, ChatHelpers::TabbedSelectorDescriptor{ .show = _show, .st = _st.tabbed, - .level = Window::GifPauseReason::TabbedPanel, + .level = descriptor.panelsLevel, .mode = ChatHelpers::TabbedSelector::Mode::Full, .features = _features, })) -, _selector(_regularWindow +, _selector((_regularWindow && _features.commonTabbedPanel) ? _regularWindow->tabbedSelector() : not_null(_ownedSelector.get())) , _mode(descriptor.mode) @@ -876,6 +885,12 @@ void ComposeControls::updateTopicRootId(MsgId topicRootId) { _header->updateTopicRootId(_topicRootId); } +void ComposeControls::updateShortcutId(BusinessShortcutId shortcutId) { + unregisterDraftSources(); + _shortcutId = shortcutId; + registerDraftSource(); +} + void ComposeControls::setHistory(SetHistoryArgs &&args) { _showSlowmodeError = std::move(args.showSlowmodeError); _sendActionFactory = std::move(args.sendActionFactory); @@ -1592,7 +1607,7 @@ void ComposeControls::initField() { && Data::AllowEmojiWithoutPremium(_history->peer, emoji); }; const auto suggestions = Ui::Emoji::SuggestionsController::Init( - _parent, + _panelsParent, _field, _session, { @@ -1828,6 +1843,10 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { return (type == DraftType::Edit) ? Key::ScheduledEdit() : Key::Scheduled(); + case Section::ShortcutMessages: + return (type == DraftType::Edit) + ? Key::ShortcutEdit(_shortcutId) + : Key::Shortcut(_shortcutId); } return Key::None(); } @@ -2053,7 +2072,9 @@ rpl::producer ComposeControls::sendActionUpdates() const { } void ComposeControls::initTabbedSelector() { - if (!_regularWindow || _regularWindow->hasTabbedSelectorOwnership()) { + if (!_regularWindow + || !_features.commonTabbedPanel + || _regularWindow->hasTabbedSelectorOwnership()) { createTabbedPanel(); } else { setTabbedPanel(nullptr); @@ -2326,6 +2347,7 @@ void SetupRestrictionView( }); state->label = makeLabel(value.text, st->premiumRequired.label); } + state->updateGeometries(); }, widget->lifetime()); widget->sizeValue( @@ -2686,7 +2708,7 @@ void ComposeControls::updateAttachBotsMenu() { return; } _attachBotsMenu = InlineBots::MakeAttachBotsMenu( - _parent, + _panelsParent, _regularWindow, _history->peer, _sendActionFactory, @@ -2729,7 +2751,7 @@ void ComposeControls::escape() { bool ComposeControls::pushTabbedSelectorToThirdSection( not_null thread, const Window::SectionShow ¶ms) { - if (!_tabbedPanel || !_regularWindow) { + if (!_tabbedPanel || !_regularWindow || !_features.commonTabbedPanel) { return true; //} else if (!_canSendMessages) { // Core::App().settings().setTabbedReplacedWithInfo(true); @@ -2764,7 +2786,7 @@ void ComposeControls::createTabbedPanel() { .nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(), }; setTabbedPanel(std::make_unique( - _parent, + _panelsParent, std::move(descriptor))); _tabbedPanel->setDesiredHeightValues( st::emojiPanHeightRatio, @@ -2787,7 +2809,7 @@ void ComposeControls::setTabbedPanel( } void ComposeControls::toggleTabbedSelectorMode() { - if (!_history || !_regularWindow) { + if (!_history || !_regularWindow || !_features.commonTabbedPanel) { return; } if (_tabbedPanel) { @@ -3150,6 +3172,10 @@ not_null ComposeControls::likeAnimationTarget() const { return _like; } +int ComposeControls::fieldCharacterCount() const { + return Ui::FieldCharacterCount(_field); +} + bool ComposeControls::preventsClose(Fn &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); @@ -3244,7 +3270,7 @@ void ComposeControls::applyInlineBotQuery( } if (!_inlineResults) { _inlineResults = std::make_unique( - _parent, + _panelsParent, _regularWindow); _inlineResults->setResultSelectedCallback([=]( InlineBots::ResultSelected result) { @@ -3319,13 +3345,16 @@ void ComposeControls::checkCharsLimitation() { return; } const auto item = _history->owner().message(_header->editMsgId()); - if (!item || !item->media() || !item->media()->allowsEditCaption()) { + if (!item) { _charsLimitation = nullptr; return; } - const auto limits = Data::PremiumLimits(&session()); - const auto left = prepareTextForEditMsg(); - const auto remove = left.text.size() - limits.captionLengthCurrent(); + const auto hasMediaWithCaption = item->media() + && item->media()->allowsEditCaption(); + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = Ui::FieldCharacterCount(_field) - maxCaptionSize; if (remove > 0) { if (!_charsLimitation) { using namespace Controls; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index b1bc012ca3..d21ea9884f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -37,6 +37,7 @@ class TabbedSelector; struct FileChosen; struct PhotoChosen; class Show; +enum class PauseReason; } // namespace ChatHelpers namespace Data { @@ -95,6 +96,8 @@ enum class ComposeControlsMode { Scheduled, }; +extern const ChatHelpers::PauseReason kDefaultPanelsLevel; + struct ComposeControlsDescriptor { const style::ComposeControls *stOverride = nullptr; std::shared_ptr show; @@ -104,6 +107,8 @@ struct ComposeControlsDescriptor { Window::SessionController *regularWindow = nullptr; rpl::producer stickerOrEmojiChosen; rpl::producer customPlaceholder; + QWidget *panelsParent = nullptr; + ChatHelpers::PauseReason panelsLevel = kDefaultPanelsLevel; QString voiceCustomCancelText; bool voiceLockFromBottom = false; ChatHelpers::ComposeFeatures features; @@ -137,6 +142,7 @@ public: [[nodiscard]] Main::Session &session() const; void setHistory(SetHistoryArgs &&args); void updateTopicRootId(MsgId topicRootId); + void updateShortcutId(BusinessShortcutId shortcutId); void setCurrentDialogsEntryState(Dialogs::EntryState state); [[nodiscard]] PeerData *sendAsPeer() const; @@ -228,6 +234,7 @@ public: [[nodiscard]] rpl::producer hasSendTextValue() const; [[nodiscard]] rpl::producer fieldMenuShownValue() const; [[nodiscard]] not_null likeAnimationTarget() const; + [[nodiscard]] int fieldCharacterCount() const; [[nodiscard]] TextWithEntities prepareTextForEditMsg() const; @@ -342,6 +349,7 @@ private: const style::ComposeControls &_st; const ChatHelpers::ComposeFeatures _features; const not_null _parent; + const not_null _panelsParent; const std::shared_ptr _show; const not_null _session; @@ -352,6 +360,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + BusinessShortcutId _shortcutId = 0; Fn _showSlowmodeError; Fn _sendActionFactory; rpl::variable _slowmodeSecondsLeft; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 5ba81cd1ec..a0d3d6066d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -257,38 +257,32 @@ rpl::producer PreviewWrap::showLinkSelector( was->destroy(); } using Flag = MTPDmessageMediaWebPage::Flag; - _draftItem = _history->addNewLocalMessage( - _history->nextNonHistoryEntryId(), - (MessageFlag::FakeHistoryItem + _draftItem = _history->addNewLocalMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeHistoryItem | MessageFlag::Outgoing | MessageFlag::HasFromId | (webpage.invert ? MessageFlag::InvertMedia : MessageFlag())), - UserId(), // via - FullReplyTo(), - base::unixtime::now(), // date - _history->session().userPeerId(), - QString(), // postAuthor - HighlightParsedLinks({ - message.text, - TextUtilities::ConvertTextTagsToEntities(message.tags), - }, links), - MTP_messageMediaWebPage( - MTP_flags(Flag() - | (webpage.forceLargeMedia - ? Flag::f_force_large_media - : Flag()) - | (webpage.forceSmallMedia - ? Flag::f_force_small_media - : Flag())), - MTP_webPagePending( - MTP_flags(webpage.url.isEmpty() - ? MTPDwebPagePending::Flag() - : MTPDwebPagePending::Flag::f_url), - MTP_long(webpage.id), - MTP_string(webpage.url), - MTP_int(0))), - HistoryMessageMarkupData(), - uint64(0)); // groupedId + .from = _history->session().userPeerId(), + .date = base::unixtime::now(), + }, HighlightParsedLinks({ + message.text, + TextUtilities::ConvertTextTagsToEntities(message.tags), + }, links), MTP_messageMediaWebPage( + MTP_flags(Flag() + | (webpage.forceLargeMedia + ? Flag::f_force_large_media + : Flag()) + | (webpage.forceSmallMedia + ? Flag::f_force_small_media + : Flag())), + MTP_webPagePending( + MTP_flags(webpage.url.isEmpty() + ? MTPDwebPagePending::Flag() + : MTPDwebPagePending::Flag::f_url), + MTP_long(webpage.id), + MTP_string(webpage.url), + MTP_int(0)))); _element = _draftItem->createView(_delegate.get()); _selectType = TextSelectType::Letters; _symbol = _selectionStartSymbol = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 51ae055f3c..c017d8842a 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -188,54 +188,39 @@ bool AboutView::refresh() { } AdminLog::OwnedItem AboutView::makeAboutBot(not_null info) { - const auto flags = MessageFlag::FakeAboutView - | MessageFlag::FakeHistoryItem - | MessageFlag::Local; - const auto postAuthor = QString(); - const auto date = TimeId(0); - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(0); - const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( info->description, Ui::ItemTextBotNoMonoOptions().flags); - const auto make = [&](auto &&a, auto &&b, auto &&...other) { - return _history->makeMessage( - _history->nextNonHistoryEntryId(), - flags, - replyTo, - viaBotId, - date, - _history->peer->id, - postAuthor, - std::forward(a), - std::forward(b), - HistoryMessageMarkupData(), - std::forward(other)...); + const auto make = [&](auto &&...args) { + return _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, std::forward(args)...); }; const auto item = info->document ? make(info->document, textWithEntities) : info->photo ? make(info->photo, textWithEntities) - : make(textWithEntities, MTP_messageMediaEmpty(), groupedId); + : make(textWithEntities, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(_delegate, item); } AdminLog::OwnedItem AboutView::makePremiumRequired() { - const auto flags = MessageFlag::FakeAboutView - | MessageFlag::FakeHistoryItem - | MessageFlag::Local; - const auto date = TimeId(0); - const auto item = _history->makeMessage( - _history->nextNonHistoryEntryId(), - flags, - date, - PreparedServiceText{ tr::lng_send_non_premium_text( - tr::now, - lt_user, - Ui::Text::Bold(_history->peer->shortName()), - Ui::Text::RichLangValue) }, - peerToUser(_history->peer->id)); + const auto item = _history->makeMessage({ + .id = _history->nextNonHistoryEntryId(), + .flags = (MessageFlag::FakeAboutView + | MessageFlag::FakeHistoryItem + | MessageFlag::Local), + .from = _history->peer->id, + }, PreparedServiceText{ tr::lng_send_non_premium_text( + tr::now, + lt_user, + Ui::Text::Bold(_history->peer->shortName()), + Ui::Text::RichLangValue), + }); auto result = AdminLog::OwnedItem(_delegate, item); result->overrideMedia(std::make_unique( result.get(), diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 3e4eb947dd..7a176cf610 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -447,7 +447,7 @@ void BottomInfo::paintReactions( } QSize BottomInfo::countCurrentSize(int newWidth) { - if (newWidth >= maxWidth()) { + if (newWidth >= maxWidth() || (_data.flags & Data::Flag::Shortcut)) { return optimalSize(); } const auto dateHeight = (_data.flags & Data::Flag::Sponsored) @@ -519,7 +519,8 @@ void BottomInfo::layoutRepliesText() { if (!_data.replies || !*_data.replies || (_data.flags & Data::Flag::RepliesContext) - || (_data.flags & Data::Flag::Sending)) { + || (_data.flags & Data::Flag::Sending) + || (_data.flags & Data::Flag::Shortcut)) { _replies.clear(); return; } @@ -559,6 +560,9 @@ void BottomInfo::layoutReactionsText() { } QSize BottomInfo::countOptimalSize() { + if (_data.flags & Data::Flag::Shortcut) { + return { st::historyShortcutStateSpace, st::msgDateFont->height }; + } auto width = 0; if (!AyuFeatures::MessageShot::isTakingShot() && _data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) { width += st::historySendStateSpace; @@ -664,6 +668,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null message) { if (item->isPinned() && message->context() != Context::Pinned) { result.flags |= Flag::Pinned; } + if (message->context() == Context::ShortcutMessages) { + result.flags |= Flag::Shortcut; + } if (const auto msgsigned = item->Get()) { if (!msgsigned->isAnonymousRank) { result.author = msgsigned->postAuthor; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index d593063ef2..efdab33347 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -44,6 +44,7 @@ public: Sponsored = 0x10, Pinned = 0x20, Imported = 0x40, + Shortcut = 0x80, //Unread, // We don't want to pass and update it in Date for now. }; friend inline constexpr bool is_flag_type(Flag) { return true; }; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index ab1ca2ee99..ac5cd3fc92 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -107,7 +107,9 @@ bool HasEditMessageAction( || item->hasFailed() || item->isEditingMedia() || !request.selectedItems.empty() - || (context != Context::History && context != Context::Replies)) { + || (context != Context::History + && context != Context::Replies + && context != Context::ShortcutMessages)) { return false; } const auto peer = item->history()->peer; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 8a8b09cc04..2b1050d8a1 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -475,7 +475,7 @@ Element::Element( Flag serviceFlag) : _delegate(delegate) , _data(data) -, _dateTime(IsItemScheduledUntilOnline(data) +, _dateTime((IsItemScheduledUntilOnline(data) || data->shortcutId()) ? QDateTime() : ItemDateTime(data)) , _text(st::msgMinWidth) diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 0f15a0fef2..5a6ea07d69 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -59,6 +59,7 @@ enum class Context : char { ContactPreview, SavedSublist, TTLViewer, + ShortcutMessages, }; enum class OnlyEmojiAndSpaces : char { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 1f2d4b0228..f434fd74e1 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1734,7 +1734,7 @@ void ListWidget::elementHandleViaClick(not_null bot) { } bool ListWidget::elementIsChatWide() { - return _isChatWide; + return _overrideIsChatWide.value_or(_isChatWide); } not_null ListWidget::elementPathShiftGradient() { @@ -1792,7 +1792,7 @@ void ListWidget::updateItemsGeometry() { if (view->isHidden()) { view->setDisplayDate(false); } else { - view->setDisplayDate(true); + view->setDisplayDate(_context != Context::ShortcutMessages); view->setAttachToPrevious(false); return i; } @@ -1937,6 +1937,9 @@ int ListWidget::resizeGetHeight(int newWidth) { _itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0; + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } return _itemsTop + _itemsHeight + st::historyPaddingBottom; } @@ -2048,7 +2051,8 @@ void ListWidget::checkActivation() { } void ListWidget::paintEvent(QPaintEvent *e) { - if (_controller->contentOverlapped(this, e)) { + if ((_context != Context::ShortcutMessages) + && _controller->contentOverlapped(this, e)) { return; } @@ -2162,6 +2166,21 @@ void ListWidget::paintEvent(QPaintEvent *e) { context.translate(0, top); p.translate(0, -top); + paintUserpics(p, context, clip); + paintDates(p, context, clip); + + _reactionsManager->paint(p, context); + _emojiInteractions->paint(p); +} + +void ListWidget::paintUserpics( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip) { + if (_context == Context::ShortcutMessages) { + return; + } + const auto session = &controller()->session(); enumerateUserpics([&](not_null view, int userpicTop) { // stop the enumeration if the userpic is below the painted rect if (userpicTop >= clip.top() + clip.height()) { @@ -2206,6 +2225,15 @@ void ListWidget::paintEvent(QPaintEvent *e) { } return true; }); +} + +void ListWidget::paintDates( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip) { + if (_context == Context::ShortcutMessages) { + return; + } auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); @@ -2252,9 +2280,6 @@ void ListWidget::paintEvent(QPaintEvent *e) { } return true; }); - - _reactionsManager->paint(p, context); - _emojiInteractions->paint(p); } void ListWidget::maybeMarkReactionsRead(not_null item) { @@ -3737,7 +3762,8 @@ void ListWidget::refreshAttachmentsFromTill(int from, int till) { } else { const auto viewDate = view->dateTime(); const auto nextDate = next->dateTime(); - next->setDisplayDate(nextDate.date() != viewDate.date()); + next->setDisplayDate(_context != Context::ShortcutMessages + && nextDate.date() != viewDate.date()); auto attached = next->computeIsAttachToPrevious(view); next->setAttachToPrevious(attached, view); view->setAttachToNext(attached, next); @@ -3932,6 +3958,13 @@ void ListWidget::replyNextMessage(FullMsgId fullId, bool next) { void ListWidget::setEmptyInfoWidget(base::unique_qptr &&w) { _emptyInfo = std::move(w); + if (_emptyInfo) { + _emptyInfo->setVisible(isEmpty()); + } +} + +void ListWidget::overrideIsChatWide(bool isWide) { + _overrideIsChatWide = isWide; } ListWidget::~ListWidget() { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index ebba2f5780..12e09accf0 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -343,6 +343,7 @@ public: QString elementAuthorRank(not_null view) override; void setEmptyInfoWidget(base::unique_qptr &&w); + void overrideIsChatWide(bool isWide); ~ListWidget(); @@ -600,6 +601,15 @@ private: void showPremiumStickerTooltip( not_null view); + void paintUserpics( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip); + void paintDates( + Painter &p, + const Ui::ChatPaintContext &context, + QRect clip); + // This function finds all history items that are displayed and calls template method // for each found message (in given direction) in the passed history with passed top offset. // @@ -631,11 +641,11 @@ private: const not_null _delegate; const not_null _controller; const std::unique_ptr _emojiInteractions; + const Context _context; Data::MessagePosition _aroundPosition; Data::MessagePosition _shownAtPosition; Data::MessagePosition _initialAroundPosition; - Context _context; int _aroundIndex = -1; int _idsLimit = kMinimalIdsLimit; Data::MessagesSlice _slice; @@ -716,6 +726,7 @@ private: bool _refreshingViewer = false; bool _showFinished = false; bool _resizePending = false; + std::optional _overrideIsChatWide; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 3001349a8d..722e7800bc 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2086,6 +2086,7 @@ bool Message::hasFromPhoto() const { return !item->out() && !item->history()->peer->isUser(); } break; case Context::ContactPreview: + case Context::ShortcutMessages: return false; } Unexpected("Context in Message::hasFromPhoto."); @@ -3055,7 +3056,7 @@ void Message::refreshReactions() { = ExtractController(context)) { ShowPremiumPreviewBox( controller, - PremiumPreview::TagsForMessages); + PremiumFeature::TagsForMessages); } return; } @@ -3293,6 +3294,7 @@ bool Message::hasFromName() const { return false; } break; case Context::ContactPreview: + case Context::ShortcutMessages: return false; } Unexpected("Context in Message::hasFromName."); @@ -3331,6 +3333,9 @@ bool Message::hasOutLayout() const { const auto item = data(); if (item->history()->peer->isSelf()) { if (const auto forwarded = item->Get()) { + if (context() == Context::ShortcutMessages) { + return true; + } return (context() == Context::SavedSublist) && (!forwarded->forwardOfForward() ? (forwarded->originalSender diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 4d3d16d9ad..3f39d58908 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -975,7 +975,8 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage() == action.replyTo) { + if (_composeControls->replyingToMessage().messageId + == action.replyTo.messageId) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } @@ -1178,29 +1179,29 @@ void RepliesWidget::edit( return; } const auto webpage = _composeControls->webPageDraft(); - auto sending = TextWithEntities(); - auto left = _composeControls->prepareTextForEditMsg(); + const auto sending = _composeControls->prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) - && !hasMediaWithCaption) { + if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { controller()->show(Box(item, false)); } else { doSetInnerFocus(); } return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = _composeControls->fieldCharacterCount() + - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } } lifetime().add([=] { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 56798905b7..d3f2dc4826 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -571,7 +571,7 @@ Api::SendAction ScheduledWidget::prepareSendAction( void ScheduledWidget::send() { const auto textWithTags = _composeControls->getTextWithAppliedMarkdown(); - if (textWithTags.text.isEmpty()) { + if (textWithTags.text.isEmpty() && !_composeControls->readyToForward()) { return; } @@ -600,6 +600,7 @@ void ScheduledWidget::send(Api::SendOptions options) { session().api().sendMessage(std::move(message)); + _composeControls->cancelForward(); _composeControls->clear(); //_saveDraftText = true; //_saveDraftStart = crl::now(); @@ -642,29 +643,29 @@ void ScheduledWidget::edit( return; } const auto webpage = _composeControls->webPageDraft(); - auto sending = TextWithEntities(); - auto left = _composeControls->prepareTextForEditMsg(); + const auto sending = _composeControls->prepareTextForEditMsg(); - const auto originalLeftSize = left.text.size(); const auto hasMediaWithCaption = item && item->media() && item->media()->allowsEditCaption(); - const auto maxCaptionSize = !hasMediaWithCaption - ? MaxMessageSize - : Data::PremiumLimits(&session()).captionLengthCurrent(); - if (!TextUtilities::CutPart(sending, left, maxCaptionSize) - && !hasMediaWithCaption) { + if (sending.text.isEmpty() && !hasMediaWithCaption) { if (item) { controller()->show(Box(item, false)); } else { _composeControls->focus(); } return; - } else if (!left.text.isEmpty()) { - const auto remove = originalLeftSize - maxCaptionSize; - controller()->showToast( - tr::lng_edit_limit_reached(tr::now, lt_count, remove)); - return; + } else { + const auto maxCaptionSize = !hasMediaWithCaption + ? MaxMessageSize + : Data::PremiumLimits(&session()).captionLengthCurrent(); + const auto remove = _composeControls->fieldCharacterCount() + - maxCaptionSize; + if (remove > 0) { + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); + return; + } } lifetime().add([=] { diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp index c076069062..76598533f4 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp @@ -238,7 +238,7 @@ void StickerToast::showWithTitle(const QString &title) { && (i->second->flags & Data::StickersSetFlag::Installed)) { ShowPremiumPreviewBox( _controller, - PremiumPreview::AnimatedEmoji); + PremiumFeature::AnimatedEmoji); } else { _controller->show(Box( _controller->uiShow(), diff --git a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp index dccca0f3a7..1688b8f6c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_transcribe_button.cpp @@ -272,7 +272,7 @@ ClickHandlerPtr TranscribeButton::link() { if (const auto controller = my.sessionWindow.get()) { ShowPremiumPreviewBox( controller, - PremiumPreview::VoiceToText); + PremiumFeature::VoiceToText); } } else { const auto max = session->api().transcribes().trialsMaxLengthMs(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp index 7b0ac35b95..1e6654abde 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.cpp @@ -7,34 +7,51 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_contact.h" -#include "core/click_handler_types.h" // ClickHandlerContext -#include "lang/lang_keys.h" -#include "layout/layout_selection.h" -#include "mainwindow.h" #include "boxes/add_contact_box.h" -#include "history/history_item_components.h" -#include "history/history_item.h" -#include "history/history.h" -#include "history/view/history_view_element.h" -#include "history/view/history_view_cursor_state.h" -#include "window/window_session_controller.h" -#include "ui/empty_userpic.h" -#include "ui/chat/chat_style.h" -#include "ui/text/format_values.h" // Ui::FormatPhone -#include "ui/text/text_options.h" -#include "ui/painter.h" +#include "core/click_handler_types.h" // ClickHandlerContext #include "data/data_session.h" #include "data/data_user.h" -#include "data/data_media_types.h" -#include "data/data_cloud_file.h" +#include "history/history.h" +#include "history/history_item_components.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_reply.h" +#include "history/view/media/history_view_media_common.h" +#include "lang/lang_keys.h" #include "main/main_session.h" +#include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "ui/chat/chat_style.h" +#include "ui/empty_userpic.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" // Ui::FormatPhone +#include "ui/text/text_options.h" +#include "window/window_session_controller.h" namespace HistoryView { namespace { -ClickHandlerPtr SendMessageClickHandler(PeerData *peer) { - return std::make_shared([peer](ClickContext context) { +class ContactClickHandler : public LambdaClickHandler { +public: + using LambdaClickHandler::LambdaClickHandler; + + void setDragText(const QString &t) { + _dragText = t; + } + + QString dragText() const override { + return _dragText; + } + +private: + QString _dragText; + +}; + +ClickHandlerPtr SendMessageClickHandler(not_null peer) { + const auto clickHandlerPtr = std::make_shared([peer]( + ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (controller->session().uniqueId() @@ -46,30 +63,44 @@ ClickHandlerPtr SendMessageClickHandler(PeerData *peer) { Window::SectionShow::Way::Forward); } }); + if (const auto user = peer->asUser()) { + clickHandlerPtr->setDragText(user->phone().isEmpty() + ? peer->name() + : Ui::FormatPhone(user->phone())); + } + return clickHandlerPtr; } ClickHandlerPtr AddContactClickHandler(not_null item) { const auto session = &item->history()->session(); - const auto fullId = item->fullId(); - return std::make_shared([=](ClickContext context) { + const auto sharedContact = [=, fullId = item->fullId()] { + if (const auto item = session->data().message(fullId)) { + if (const auto media = item->media()) { + return media->sharedContact(); + } + } + return (const Data::SharedContact *)nullptr; + }; + const auto clickHandlerPtr = std::make_shared([=]( + ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { if (controller->session().uniqueId() != session->uniqueId()) { return; } - if (const auto item = session->data().message(fullId)) { - if (const auto media = item->media()) { - if (const auto contact = media->sharedContact()) { - controller->show(Box( - session, - contact->firstName, - contact->lastName, - contact->phoneNumber)); - } - } + if (const auto contact = sharedContact()) { + controller->show(Box( + session, + contact->firstName, + contact->lastName, + contact->phoneNumber)); } } }); + if (const auto contact = sharedContact()) { + clickHandlerPtr->setDragText(Ui::FormatPhone(contact->phoneNumber)); + } + return clickHandlerPtr; } } // namespace @@ -81,17 +112,32 @@ Contact::Contact( const QString &last, const QString &phone) : Media(parent) -, _userId(userId) -, _fname(first) -, _lname(last) -, _phone(Ui::FormatPhone(phone)) { +, _st(st::historyPagePreview) +, _pixh(st::contactsPhotoSize) +, _userId(userId) { history()->owner().registerContactView(userId, parent); - _name.setText( - st::semiboldTextStyle, - tr::lng_full_name(tr::now, lt_first_name, first, lt_last_name, last).trimmed(), - Ui::NameTextOptions()); - _phonew = st::normalFont->width(_phone); + _nameLine.setText( + st::webPageTitleStyle, + tr::lng_full_name( + tr::now, + lt_first_name, + first, + lt_last_name, + last).trimmed(), + Ui::WebpageTextTitleOptions()); + + _phoneLine.setText( + st::webPageDescriptionStyle, + Ui::FormatPhone(phone), + Ui::WebpageTextTitleOptions()); + +#if 0 // No info. + _infoLine.setText( + st::webPageDescriptionStyle, + phone, + Ui::WebpageTextTitleOptions()); +#endif } Contact::~Contact() { @@ -111,121 +157,297 @@ void Contact::updateSharedContactUserId(UserId userId) { } QSize Contact::countOptimalSize() { - const auto item = _parent->data(); - auto maxWidth = st::msgFileMinWidth; - _contact = _userId - ? item->history()->owner().userLoaded(_userId) + ? _parent->data()->history()->owner().userLoaded(_userId) : nullptr; if (_contact) { _contact->loadUserpic(); } else { - const auto full = _name.toString(); + const auto full = _nameLine.toString(); _photoEmpty = std::make_unique( Ui::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId ? peerFromUser(_userId) : Data::FakePeerIdForJustName(full))), full); } - if (_contact && _contact->isContact()) { - _linkl = SendMessageClickHandler(_contact); - _link = tr::lng_profile_send_message(tr::now).toUpper(); - } else if (_userId) { - _linkl = AddContactClickHandler(_parent->data()); - _link = tr::lng_profile_add_contact(tr::now).toUpper(); - } - _linkw = _link.isEmpty() ? 0 : st::semiboldFont->width(_link); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - - const auto tleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto tright = st.padding.right(); - if (_userId) { - accumulate_max(maxWidth, tleft + _phonew + tright); + _buttons.clear(); + if (_contact) { + const auto message = tr::lng_contact_send_message(tr::now).toUpper(); + _buttons.push_back({ + message, + st::semiboldFont->width(message), + SendMessageClickHandler(_contact), + }); + if (!_contact->isContact()) { + const auto add = tr::lng_contact_add(tr::now).toUpper(); + _buttons.push_back({ + add, + st::semiboldFont->width(add), + AddContactClickHandler(_parent->data()), + }); + } + _mainButton.link = _buttons.front().link; } else { - accumulate_max(maxWidth, tleft + _phonew + _parent->skipBlockWidth() + st::msgPadding.right()); +#if 0 // Can't view contact. + const auto view = tr::lng_profile_add_contact(tr::now).toUpper(); + _buttons.push_back({ + view, + st::semiboldFont->width(view), + AddContactClickHandler(_parent->data()), + }); +#endif + _mainButton.link = nullptr; } - accumulate_max(maxWidth, tleft + _name.maxWidth() + tright); - accumulate_min(maxWidth, st::msgMaxWidth); - auto minHeight = st.padding.top() + st.thumbSize + st.padding.bottom(); - if (_parent->bottomInfoIsWide()) { - minHeight += st::msgDateFont->height - st::msgDateDelta.y(); + const auto padding = inBubblePadding() + innerMargin(); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineHeight = UnitedLineHeight(); + + auto maxWidth = _parent->skipBlockWidth(); + auto minHeight = 0; + + auto textMinHeight = 0; + if (!_nameLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _nameLine.maxWidth()); + textMinHeight += 1 * lineHeight; } - if (!isBubbleTop()) { - minHeight -= st::msgFileTopMinus; + if (!_phoneLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _phoneLine.maxWidth()); + textMinHeight += 1 * lineHeight; } + if (!_infoLine.isEmpty()) { + accumulate_max(maxWidth, lineLeft + _infoLine.maxWidth()); + textMinHeight += std::min(_infoLine.minHeight(), 1 * lineHeight); + } + minHeight = std::max(textMinHeight, st::contactsPhotoSize); + + if (!_buttons.empty()) { + auto buttonsWidth = rect::m::sum::h(st::historyPageButtonPadding); + for (const auto &button : _buttons) { + buttonsWidth += button.width; + } + accumulate_max(maxWidth, buttonsWidth); + } + maxWidth += rect::m::sum::h(padding); + minHeight += rect::m::sum::v(padding); + return { maxWidth, minHeight }; } void Contact::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; - auto paintw = width(); + if (width() < rect::m::sum::h(st::msgPadding) + 1) { + return; + } + const auto st = context.st; const auto stm = context.messageStyle(); - accumulate_min(paintw, maxWidth()); + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + auto tshift = inner.top(); - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto nametop = st.nameTop - topMinus; - const auto nameright = st.padding.right(); - const auto statustop = st.statusTop - topMinus; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (_userId) { - QRect rthumb(style::rtlrect(st.padding.left(), st.padding.top() - topMinus, st.thumbSize, st.thumbSize, paintw)); - if (_contact) { - const auto was = !_userpic.null(); - _contact->paintUserpic(p, _userpic, rthumb.x(), rthumb.y(), st.thumbSize); - if (!was && !_userpic.null()) { - history()->owner().registerHeavyViewPart(_parent); + const auto selected = context.selected(); + const auto view = parent(); + const auto colorIndex = _contact + ? _contact->colorIndex() + : Data::DecideColorIndex( + Data::FakePeerIdForJustName(_nameLine.toString())); + const auto cache = context.outbg + ? stm->replyCache[st->colorPatternIndex(colorIndex)].get() + : st->coloredReplyCache(selected, colorIndex).get(); + const auto backgroundEmojiId = _contact + ? _contact->backgroundEmojiId() + : DocumentId(); + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + true, + colorIndex + 1)] + : nullptr; + Ui::Text::ValidateQuotePaintCache(*cache, _st); + Ui::Text::FillQuotePaint(p, outer, *cache, _st); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + view); + if (!backgroundEmojiCache->frames[0].isNull()) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto r = outer + - QMargins(0, 0, 0, rect::bottom(outer) - end); + FillBackgroundEmoji(p, r, false, *backgroundEmojiCache); + } + } + + if (_mainButton.ripple) { + _mainButton.ripple->paint( + p, + outer.x(), + outer.y(), + width(), + &cache->bg); + if (_mainButton.ripple->empty()) { + _mainButton.ripple = nullptr; + } + } + + { + const auto left = inner.left(); + const auto top = tshift; + if (_userId) { + if (_contact) { + const auto was = !_userpic.null(); + _contact->paintUserpic(p, _userpic, left, top, _pixh); + if (!was && !_userpic.null()) { + history()->owner().registerHeavyViewPart(_parent); + } + } else { + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); + _photoEmpty->paintCircle(p, left, top, _pixh, _pixh); } if (context.selected()) { - PainterHighQualityEnabler hq(p); + auto hq = PainterHighQualityEnabler(p); p.setBrush(p.textPalette().selectOverlay); p.setPen(Qt::NoPen); - p.drawEllipse(rthumb); + p.drawEllipse(left, top, _pixh, _pixh); } - - bool over = ClickHandler::showAsActive(_linkl); - p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); - p.setPen(stm->msgFileThumbLinkFg); - p.drawTextLeft(nameleft, linktop, paintw, _link, _linkw); - } else { - _photoEmpty->paintCircle(p, st.padding.left(), st.padding.top() - topMinus, paintw, st.thumbSize); } - const auto namewidth = paintw - nameleft - nameright; - p.setFont(st::semiboldFont); - p.setPen(stm->historyFileNameFg); - _name.drawLeftElided(p, nameleft, nametop, namewidth, paintw); + const auto lineHeight = UnitedLineHeight(); + const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left(); + const auto lineWidth = rect::right(inner) - lineLeft; - p.setFont(st::normalFont); - p.setPen(stm->mediaFg); - p.drawTextLeft(nameleft, statustop, paintw, _phone); + { + p.setPen(cache->icon); + p.setTextPalette(context.outbg + ? stm->semiboldPalette + : st->coloredTextPalette(selected, colorIndex)); + + const auto endskip = _nameLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _nameLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + context.selection); + tshift += lineHeight; + + p.setTextPalette(stm->textPalette); + } + p.setPen(stm->historyTextFg); + { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _phoneLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _phoneLine.drawLeftElided( + p, + lineLeft, + tshift, + lineWidth, + width(), + 1, + style::al_left, + 0, + -1, + endskip, + false, + toTitleSelection(context.selection)); + tshift += 1 * lineHeight; + } + if (!_infoLine.isEmpty()) { + tshift += st::lineWidth * 3; // Additional skip. + const auto endskip = _infoLine.hasSkipBlock() + ? _parent->skipBlockWidth() + : 0; + _parent->prepareCustomEmojiPaint(p, context, _infoLine); + _infoLine.draw(p, { + .position = { lineLeft, tshift }, + .outerWidth = width(), + .availableWidth = lineWidth, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .selection = toDescriptionSelection(context.selection), + .elisionHeight = (1 * lineHeight), + .elisionRemoveFromEnd = endskip, + }); + tshift += (1 * lineHeight); + } + + if (!_buttons.empty()) { + p.setFont(st::semiboldFont); + p.setPen(cache->icon); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto line = st::historyPageButtonLine; + auto color = cache->icon; + color.setAlphaF(color.alphaF() * 0.3); + const auto top = end + st::historyPageButtonPadding.top(); + const auto buttonWidth = inner.width() / float64(_buttons.size()); + p.fillRect(inner.x(), end, inner.width(), line, color); + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + const auto left = inner.x() + i * buttonWidth; + if (button.ripple) { + button.ripple->paint(p, left, end, buttonWidth, &cache->bg); + if (button.ripple->empty()) { + _buttons[i].ripple = nullptr; + } + } + p.drawText( + left + (buttonWidth - button.width) / 2, + top + st::semiboldFont->ascent, + button.text); + } + } } TextState Contact::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - if (_userId) { - const auto &st = _userId ? st::msgFileThumbLayout : st::msgFileLayout; - const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus; - const auto nameleft = st.padding.left() + st.thumbSize + st.thumbSkip; - const auto linkshift = st::msgDateFont->height / 2; - const auto linktop = st.linkTop - topMinus - linkshift; - if (style::rtlrect(nameleft, linktop, _linkw, st::semiboldFont->height, width()).contains(point)) { - result.link = _linkl; - return result; + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + + _lastPoint = point; + + if (_buttons.size() > 1) { + const auto end = rect::bottom(inner) + _st.padding.bottom(); + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto left = inner.x() + i * bWidth; + if (QRectF(left, end, bWidth, bHeight).contains(point)) { + result.link = _buttons[i].link; + return result; + } } } - if (QRect(0, 0, width(), height()).contains(point) && _contact) { - result.link = _contact->openLink(); + if (outer.contains(point)) { + result.link = _mainButton.link; return result; } return result; @@ -239,4 +461,100 @@ bool Contact::hasHeavyPart() const { return !_userpic.null(); } +void Contact::clickHandlerPressedChanged( + const ClickHandlerPtr &p, + bool pressed) { + const auto full = Rect(currentSize()); + const auto outer = full - inBubblePadding(); + const auto inner = outer - innerMargin(); + const auto end = rect::bottom(inner) + _st.padding.bottom(); + if ((_lastPoint.y() < end) || (_buttons.size() <= 1)) { + if (p != _mainButton.link) { + return; + } + if (pressed) { + if (!_mainButton.ripple) { + const auto owner = &parent()->history()->owner(); + _mainButton.ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + outer.size(), + _st.radius), + [=] { owner->requestViewRepaint(parent()); }); + } + _mainButton.ripple->add(_lastPoint - outer.topLeft()); + } else if (_mainButton.ripple) { + _mainButton.ripple->lastStop(); + } + return; + } else if (_buttons.empty()) { + return; + } + const auto bWidth = inner.width() / float64(_buttons.size()); + const auto bHeight = rect::bottom(outer) - end; + for (auto i = 0; i < _buttons.size(); i++) { + const auto &button = _buttons[i]; + if (p != button.link) { + continue; + } + if (pressed) { + if (!button.ripple) { + const auto owner = &parent()->history()->owner(); + + _buttons[i].ripple = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::MaskByDrawer( + QSize(bWidth, bHeight), + false, + [=](QPainter &p) { + p.drawRect(0, 0, bWidth, bHeight); + }), + [=] { owner->requestViewRepaint(parent()); }); + } + button.ripple->add(_lastPoint + - QPoint(inner.x() + i * bWidth, end)); + } else if (button.ripple) { + button.ripple->lastStop(); + } + } +} + +QMargins Contact::inBubblePadding() const { + return { + st::msgPadding.left(), + isBubbleTop() ? st::msgPadding.left() : 0, + st::msgPadding.right(), + isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0 + }; +} + +QMargins Contact::innerMargin() const { + const auto button = _buttons.empty() ? 0 : st::historyPageButtonHeight; + return _st.padding + QMargins(0, 0, 0, button); +} + +int Contact::bottomInfoPadding() const { + if (!isBubbleBottom()) { + return 0; + } + + auto result = st::msgDateFont->height; + + // We use padding greater than st::msgPadding.bottom() in the + // bottom of the bubble so that the left line looks pretty. + // but if we have bottom skip because of the info display + // we don't need that additional padding so we replace it + // back with st::msgPadding.bottom() instead of left(). + result += st::msgPadding.bottom() - st::msgPadding.left(); + return result; +} + +TextSelection Contact::toTitleSelection(TextSelection selection) const { + return UnshiftItemSelection(selection, _nameLine); +} + +TextSelection Contact::toDescriptionSelection(TextSelection selection) const { + return UnshiftItemSelection(toTitleSelection(selection), _phoneLine); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_contact.h b/Telegram/SourceFiles/history/view/media/history_view_contact.h index ecca595f2e..2dd665b946 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_contact.h +++ b/Telegram/SourceFiles/history/view/media/history_view_contact.h @@ -12,11 +12,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class EmptyUserpic; +class RippleAnimation; } // namespace Ui namespace HistoryView { -class Contact : public Media { +class Contact final : public Media { public: Contact( not_null parent, @@ -29,7 +30,8 @@ public: void draw(Painter &p, const PaintContext &context) const override; TextState textState(QPoint point, StateRequest request) const override; - bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { + bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override { return true; } bool dragItemByHandler(const ClickHandlerPtr &p) const override { @@ -43,16 +45,6 @@ public: return false; } - const QString &fname() const { - return _fname; - } - const QString &lname() const { - return _lname; - } - const QString &phone() const { - return _phone; - } - // Should be called only by Data::Session. void updateSharedContactUserId(UserId userId) override; @@ -62,18 +54,40 @@ public: private: QSize countOptimalSize() override; + void clickHandlerPressedChanged( + const ClickHandlerPtr &p, bool pressed) override; + + [[nodiscard]] QMargins inBubblePadding() const; + [[nodiscard]] QMargins innerMargin() const; + [[nodiscard]] int bottomInfoPadding() const; + + [[nodiscard]] TextSelection toTitleSelection( + TextSelection selection) const; + [[nodiscard]] TextSelection toDescriptionSelection( + TextSelection selection) const; + + const style::QuoteStyle &_st; + const int _pixh; + UserId _userId = 0; UserData *_contact = nullptr; - int _phonew = 0; - QString _fname, _lname, _phone; - Ui::Text::String _name; + Ui::Text::String _nameLine; + Ui::Text::String _phoneLine; + Ui::Text::String _infoLine; + + struct Button { + QString text; + int width = 0; + ClickHandlerPtr link; + mutable std::unique_ptr ripple; + }; + std::vector