diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 8e99d7162..68bab389c 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 9cbb4a41a..b489d4b7e 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 7b41428e1..ee1f569e4 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 $<IF:$<CONFIG:Debug>,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 000000000..dd1ab78d2 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 000000000..d49a48c32 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 000000000..32ba54f16 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 000000000..7541526af 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 000000000..0076344f4 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 000000000..b766d6e43 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 000000000..47caac05a 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 000000000..25c357e50 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 79b680cc2..102f5f3a5 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 8d25f5302..284232202 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 000000000..b0a943e0c 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 000000000..ca2b6adb6 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 000000000..064090f76 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 000000000..0b1cb033e 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 000000000..66fd705ad 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 000000000..cd08060ff 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 000000000..084b399ea 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 000000000..5ec1ffe4f 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 000000000..8bc18f9ad 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 000000000..e54a10425 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 000000000..e3a73f7e1 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 000000000..2b37db754 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 000000000..03c8380d4 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 000000000..f91df7635 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 000000000..d379a9e4b 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 000000000..80dfbc6f1 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 000000000..38625e754 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 000000000..0da0b229c 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 000000000..b6fe3bede 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 000000000..be8035562 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 000000000..59afa2748 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 000000000..aaa60e6a4 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 000000000..e48940aaa 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 000000000..2db7e80ea 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 000000000..c6f0d03e3 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 000000000..ce0984920 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 000000000..0aee12c23 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 000000000..ae7020aad 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 000000000..49b7ee3c8 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 000000000..a442c069d 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 000000000..a9e7e1d1f 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 000000000..3f4c1852e 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 000000000..d927c704f 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 000000000..3c2b9cd2d 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 000000000..1ae9e950a 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 000000000..5d132577d 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 fe1541145..712c91ba0 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 d05da77b4..f0e395b4e 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 01052baef..9127319e9 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 f2dd1fc89..0048795c9 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 a129237ca..12666b6fd 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -14,5 +14,12 @@ <file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file> <file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file> <file alias="palette.tgs">../../animations/palette.tgs</file> + <file alias="sleep.tgs">../../animations/sleep.tgs</file> + <file alias="greeting.tgs">../../animations/greeting.tgs</file> + <file alias="location.tgs">../../animations/location.tgs</file> + <file alias="robot.tgs">../../animations/robot.tgs</file> + <file alias="writing.tgs">../../animations/writing.tgs</file> + <file alias="hours.tgs">../../animations/hours.tgs</file> + <file alias="phone.tgs">../../animations/phone.tgs</file> </qresource> </RCC> diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 38e9f48e4..b6caf7642 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -3,6 +3,7 @@ <file alias="art/background.tgv">../../art/background.tgv</file> <file alias="art/bg_thumbnail.png">../../art/bg_thumbnail.png</file> <file alias="art/bg_initial.jpg">../../art/bg_initial.jpg</file> + <file alias="art/business_logo.png">../../art/business_logo.png</file> <file alias="art/logo_256.png">../../art/logo_256.png</file> <file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file> <file alias="art/themeimage.jpg">../../art/themeimage.jpg</file> diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 54c27edef..b41618532 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="4.15.0.0" /> + Version="4.15.2.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 5b93e930d..67536dd5d 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 af1902b73..40604a779 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 155666a5d..fe0d489b0 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 005effad9..84f0cbfff 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 b4464019d..482f23987 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<MTPMessageEntity>(), 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<HistoryItem*> item) { PollDataToInputMedia(poll, true), MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), - 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 0e5d7c585..495b9467e 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<uint64>(); + 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<MTPmessages_SendMedia>( 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<MTPmessages_SendMedia>( MTP_flags(sendFlags), @@ -324,13 +335,14 @@ bool SendDice(MessageToSend &message) { MTP_long(randomId), MTPReplyMarkup(), MTP_vector<MTPMessageEntity>(), - 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 d10af3e42..8b54f8b82 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<MTPRestrictionReason>(), - 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<MTPRestrictionReason>(), - 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 a722b99dd..58dd1c4ca 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<MTPint>(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<MTPint>(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<base::flat_map<uint64, FullMsgId>>(); @@ -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<HistoryItem*> item) { } } +void ApiWrap::sendShortcutMessages( + not_null<PeerData*> 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<SendingAlbum*> 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<SendingAlbum*> album) { Data::Histories::ReplyToPlaceholder(), MTP_vector<MTPInputSingleMedia>(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 615960126..58165adec 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -337,6 +337,9 @@ public: void cancelLocalItem(not_null<HistoryItem*> item); + void sendShortcutMessages( + not_null<PeerData*> peer, + BusinessShortcutId id); void sendMessage(MessageToSend &&message); void sendBotStart( not_null<UserData*> bot, diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 9819c855c..d3715e884 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 70c09ec9e..cc7cdb427 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<Ui::TimeInput>( 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 6dadab562..d445ce1f5 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<Ui::AskBoostReason>(); } - const auto appConfig = &_forPeer->session().account().appConfig(); - const auto defaultRequired = appConfig->get<int>( - "channel_wallpaper_level_min", - 9); - const auto customRequired = appConfig->get<int>( - "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<Ui::AskBoostReason>(); @@ -791,7 +776,7 @@ void BackgroundPreviewBox::applyForPeer() { } else { ShowPremiumPreviewBox( _controller->uiShow(), - PremiumPreview::Wallpapers); + PremiumFeature::Wallpapers); } }); const auto cancel = CreateChild<RoundButton>( diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 4403f56fa..cf9d91f8c 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 69179e9f4..f61c0e6e7 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 8e94beeb9..5ffdfb6f3 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 f09b10448..367742971 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<Ui::RpWidget*> widget, + not_null<Main::Session*> session, + Fn<void()> clickedCallback, + Fn<void()> setDefaultOption, + const style::Checkbox &st) { + const auto row = Ui::CreateChild<Ui::AbstractButton>(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 c715ef473..cfdc14ad7 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<void()> premiumClickedCallback( + Option option, + not_null<Window::SessionController*> 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 37b946d63..ce23d235f 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<not_null<History*>> &; 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<QWidget*> parent, - Flags flags, - const base::flat_set<not_null<History*>> &peers); - - [[nodiscard]] rpl::producer<Flag> flagRemoved() const; - [[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const; - - void updateData( - Flags flags, - const base::flat_set<not_null<History*>> &peers); - - int resizeGetHeight(int newWidth) override; - -private: - using Button = base::unique_qptr<Ui::IconButton>; - struct FlagButton { - Flag flag = Flag(); - Button button; - }; - struct PeerButton { - not_null<History*> 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*> history); - - std::vector<FlagButton> _removeFlag; - std::vector<PeerButton> _removePeer; - - rpl::event_stream<Flag> _flagRemoved; - rpl::event_stream<not_null<History*>> _peerRemoved; - -}; - struct NameEditing { not_null<Ui::InputField*> field; bool custom = false; @@ -167,167 +115,6 @@ not_null<FilterChatsPreview*> SetupChatsPreview( return preview; } -FilterChatsPreview::FilterChatsPreview( - not_null<QWidget*> parent, - Flags flags, - const base::flat_set<not_null<History*>> &peers) -: RpWidget(parent) { - updateData(flags, peers); -} - -void FilterChatsPreview::refresh() { - resizeToWidth(width()); -} - -void FilterChatsPreview::updateData( - Flags flags, - const base::flat_set<not_null<History*>> &peers) { - _removeFlag.clear(); - _removePeer.clear(); - const auto makeButton = [&](Fn<void()> handler) { - auto result = base::make_unique_q<Ui::IconButton>( - 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<Ui::IconButton*> 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*> history) { - const auto i = ranges::find(_removePeer, history, &PeerButton::history); - Assert(i != end(_removePeer)); - _removePeer.erase(i); - refresh(); - _peerRemoved.fire_copy(history); -} - -rpl::producer<Flag> FilterChatsPreview::flagRemoved() const { - return _flagRemoved.events(); -} - -rpl::producer<not_null<History*>> FilterChatsPreview::peerRemoved() const { - return _peerRemoved.events(); -} - void EditExceptions( not_null<Window::SessionController*> window, not_null<QObject*> 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<EditFilterChatsListController>( 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<PeerListBox*> 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 989a9867b..0ee2bace0 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<Flag>(id() & 0xFF); + return static_cast<Flag>(id() & 0xFFFF); } ExceptionRow::ExceptionRow(not_null<History*> 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<not_null<History*>> &peers, - LimitBoxFactory limitBox) + int limit, + Fn<void()> 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<PeerListRow*> 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<Ui::RpWidget> EditFilterChatsListController::prepareTypesList() { auto EditFilterChatsListController::createRow(not_null<History*> history) -> std::unique_ptr<Row> { + const auto business = _options & (Flag::NewChats | Flag::ExistingChats); + if (business && (history->peer->isSelf() || !history->peer->isUser())) { + return nullptr; + } return history->inChatList() ? std::make_unique<ExceptionRow>(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 a9dfd3fa2..26e0529c3 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<object_ptr<Ui::BoxContent>(int)>; EditFilterChatsListController( not_null<Main::Session*> session, @@ -51,7 +50,8 @@ public: Flags options, Flags selected, const base::flat_set<not_null<History*>> &peers, - LimitBoxFactory limitBox); + int limit, + Fn<void()> showLimitReached); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -72,7 +72,7 @@ private: void updateTitle(); const not_null<Main::Session*> _session; - const LimitBoxFactory _limitBox; + const Fn<void()> _showLimitReached; rpl::producer<QString> _title; base::flat_set<not_null<History*>> _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 000000000..3e2efb87e --- /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<QWidget*> parent, + Flags flags, + const base::flat_set<not_null<History*>> &peers) +: RpWidget(parent) { + updateData(flags, peers); +} + +void FilterChatsPreview::refresh() { + resizeToWidth(width()); +} + +void FilterChatsPreview::updateData( + Flags flags, + const base::flat_set<not_null<History*>> &peers) { + _removeFlag.clear(); + _removePeer.clear(); + const auto makeButton = [&](Fn<void()> handler) { + auto result = base::make_unique_q<Ui::IconButton>( + 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<Ui::IconButton*> 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*> history) { + const auto i = ranges::find(_removePeer, history, &PeerButton::history); + Assert(i != end(_removePeer)); + _removePeer.erase(i); + refresh(); + _peerRemoved.fire_copy(history); +} + +rpl::producer<Flag> FilterChatsPreview::flagRemoved() const { + return _flagRemoved.events(); +} + +rpl::producer<not_null<History*>> 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 000000000..c795bc493 --- /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<QWidget*> parent, + Flags flags, + const base::flat_set<not_null<History*>> &peers); + + [[nodiscard]] rpl::producer<Flag> flagRemoved() const; + [[nodiscard]] rpl::producer<not_null<History*>> peerRemoved() const; + + void updateData( + Flags flags, + const base::flat_set<not_null<History*>> &peers); + + int resizeGetHeight(int newWidth) override; + +private: + using Button = base::unique_qptr<Ui::IconButton>; + struct FlagButton { + Flag flag = Flag(); + Button button; + }; + struct PeerButton { + not_null<History*> 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*> history); + + std::vector<FlagButton> _removeFlag; + std::vector<PeerButton> _removePeer; + + rpl::event_stream<Flag> _flagRemoved; + rpl::event_stream<not_null<History*>> _peerRemoved; + +}; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index d60cc030d..5640c11b5 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::SessionController*> 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 7e0e6b5dc..41f9cc097 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<Ui::VerticalLayout>(box), {}); - auto buttonCallback = [=](PremiumPreview section) { + auto buttonCallback = [=](PremiumFeature section) { stars->setPaused(true); const auto previewBoxShown = [=]( not_null<Ui::BoxContent*> 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 aa69a6fee..98c49900a 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1216,7 +1216,7 @@ void LanguageBox::setupTop(not_null<Ui::VerticalLayout*> 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 1f24ecd76..f8a7ea6e8 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<ChatData*> chat) { const auto init = [=](not_null<ListBox*> box) { auto above = object_ptr<Ui::VerticalLayout>(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 51d969d0e..6268db65b 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 2627d2342..2e24d2804 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<QWidget*> parent, + uint32 level, + not_null<Main::Session*> 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<QWidget*> parent, std::shared_ptr<Ui::ChatStyle> style, @@ -295,47 +319,36 @@ PreviewWrap::PreviewWrap( , _delegate(std::make_unique<PreviewDelegate>(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<QWidget*> parent, + uint32 level, + not_null<Main::Session*> 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<Ui::SettingsButton*> button, + Ui::RpWidget *right, + not_null<ChannelData*> channel, + const QMargins &padding, + rpl::producer<QString> text) { + if (channel->levelHint() >= level) { + return; + } + const auto badge = Ui::CreateChild<LevelBadge>( + 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<int>( - "channel_bg_icon_level_min", - 5) + ? limits.channelBgIconLevelMin() : 0; const auto statusRequired = (values.statusChanged && values.statusId) - ? session->account().appConfig().get<int>( - "channel_emoji_status_level_min", - 8) + ? limits.channelEmojiStatusLevelMin() : 0; const auto required = std::max({ colorRequired, @@ -726,6 +838,7 @@ struct ButtonWithEmoji { not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<Ui::ChatStyle> style, + not_null<PeerData*> peer, rpl::producer<uint8> colorIndexValue, rpl::producer<DocumentId> emojiIdValue, Fn<void(DocumentId)> 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<Ui::SettingsButton> CreateEmojiStatusButton( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, + not_null<ChannelData*> channel, rpl::producer<DocumentId> statusIdValue, Fn<void(DocumentId,TimeId)> 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<Ui::BoxContentDivider>( - box.get()); - const auto verticalLayout = box->verticalLayout()->add( - object_ptr<Ui::VerticalLayout>(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<Ui::FlatLabel>( - 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<PreviewWrap>( 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<BackgroundBox>(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 697066416..9086dfce8 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 46fb2619c..6cafe4d2c 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<Ui::RpWidget> 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 ef99b3a90..f57573d99 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<ChannelData*> 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<int>(key, fallback) : 0; - }; - auto nameColorsByLevel = base::flat_map<int, int>(); auto linkStylesByLevel = base::flat_map<int, int>(); + 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<ChannelData*> 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 ad8d313df..2a9c0d4b1 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 93df20cfa..76df0c2d7 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<void()> hiddenCallback; @@ -90,88 +91,118 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { media->videoThumbnailWanted(origin); } -[[nodiscard]] rpl::producer<QString> SectionTitle(PremiumPreview section) { +[[nodiscard]] rpl::producer<QString> 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<QString> SectionAbout(PremiumPreview section) { +[[nodiscard]] rpl::producer<QString> 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<Ui::RpWidget> 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<Main::Session*> 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<Ui::RpWidget*> GenericPreview( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void()> readyCallback) { const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get()); result->show(); @@ -757,10 +795,10 @@ struct VideoPreviewDocument { [[nodiscard]] not_null<Ui::RpWidget*> GenerateDefaultPreview( not_null<Ui::RpWidget*> parent, std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void()> 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<Ui::RpWidget> CreateSwitch( not_null<Ui::RpWidget*> parent, - not_null<rpl::variable<PremiumPreview>*> selected, - std::vector<PremiumPreview> order) { + not_null<rpl::variable<PremiumFeature>*> selected, + std::vector<PremiumFeature> 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<void()> preload; std::vector<Hiding> hiding; - rpl::variable<PremiumPreview> selected; - std::vector<PremiumPreview> order; + rpl::variable<PremiumFeature> selected; + std::vector<PremiumFeature> order; }; const auto state = outer->lifetime().make_state<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<Ui::GenericBox*> 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<Ui::GenericBox*> 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<ChatHelpers::Show> show, not_null<DocumentData*> document) { Show(std::move(show), Descriptor{ - .section = PremiumPreview::Stickers, + .section = PremiumFeature::Stickers, .requestedSticker = document, }); } void ShowPremiumPreviewBox( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown) { ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); } void ShowPremiumPreviewBox( std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown, bool hideSubscriptionButton) { Show(std::move(show), Descriptor{ @@ -1312,7 +1363,7 @@ void ShowPremiumPreviewBox( void ShowPremiumPreviewToBuy( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void()> 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 b7fb7a40e..80400a6ee 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<Ui::GenericBox*> box, not_null<Main::Session*> 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<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr); void ShowPremiumPreviewBox( std::shared_ptr<ChatHelpers::Show> show, - PremiumPreview section, + PremiumFeature section, Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr, bool hideSubscriptionButton = false); void ShowPremiumPreviewToBuy( not_null<Window::SessionController*> controller, - PremiumPreview section, + PremiumFeature section, Fn<void()> hiddenCallback = nullptr); void PremiumUnavailableBox(not_null<Ui::GenericBox*> box); diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 4de09e882..e33624403 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 74e8f510b..d8097222e 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 03d8a6155..0ab869ae5 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 cd0c59ebf..6e0c9066e 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 dff268c26..4581fac40 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<void()> 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 8a88acfba..7d72ebf5a 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<void()> done) { return std::optional<Ui::AskBoostReason>(); } _checkingGroupLevel = false; - const auto appConfig = &peer->session().account().appConfig(); - const auto required = appConfig->get<int>( - "group_emoji_stickers_level_min", - 4); + const auto required = Data::LevelLimits( + &peer->session()).groupEmojiStickersLevelMin(); if (level >= required) { save(); return std::optional<Ui::AskBoostReason>(); diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 8ee43eeda..05fdaaff2 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<QEvent*> e) { - if (e->type() == QEvent::Close) { - handleClose(); + base::install_event_filter(window().get(), [=](not_null<QEvent*> e) { + if (e->type() == QEvent::Close && handleClose()) { + e->ignore(); + return base::EventFilterResult::Cancel; } else if (e->type() == QEvent::KeyPress) { if ((static_cast<QKeyEvent*>(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<Ui::RpWindow*> Panel::window() const { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index dc715584a..c98537eb9 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 a39019a93..a622c37ea 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 ba6f43b4e..5466b34e9 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 382d850e3..d5aba296b 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<BusinessShortcutId, Data::Shortcut>(); + 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 2606dc1f2..5c9e291f3 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<UserData*> 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 fdef09548..ed2935744 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 4534818d3..6659343d2 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<EmojiSection> SetIdEmojiSection(uint64 id) { rpl::producer<std::vector<GifSection>> GifSectionsValue( not_null<Main::Session*> 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<std::vector<QString>>( u"gif_search_emojies"_q, diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index d6a7ac8e1..60a27fcca 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<LocalUrlHandler> &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 03d6dc762..01763e1e2 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<ClickHandlerContext>(); + 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<std::vector<QString>>( "autologin_domains", @@ -246,7 +251,8 @@ bool UiIntegration::handleUrlClick( const auto domain = DomainForAutoLogin(parsed); const auto skip = context.value<ClickHandlerContext>().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 d9af5939e..350612c55 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 000000000..5c8c9f895 --- /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<Session*> 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<ChatbotsSettings> Chatbots::changes() const { + return _settings.changes(); +} + +rpl::producer<ChatbotsSettings> Chatbots::value() const { + return _settings.value(); +} + +void Chatbots::save( + ChatbotsSettings settings, + Fn<void()> done, + Fn<void(QString)> 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 000000000..6328b487d --- /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<Session*> owner); + ~Chatbots(); + + void preload(); + [[nodiscard]] bool loaded() const; + [[nodiscard]] const ChatbotsSettings ¤t() const; + [[nodiscard]] rpl::producer<ChatbotsSettings> changes() const; + [[nodiscard]] rpl::producer<ChatbotsSettings> value() const; + + void save( + ChatbotsSettings settings, + Fn<void()> done, + Fn<void(QString)> fail); + +private: + const not_null<Session*> _owner; + + rpl::variable<ChatbotsSettings> _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 000000000..06cb17e91 --- /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<Session*> 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<MTPBusinessWorkHours> &hours, + const tl::conditional<MTPBusinessLocation> &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<Session*> owner, + const tl::conditional<MTPBusinessAwayMessage> &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<Session*> owner, + const tl::conditional<MTPBusinessGreetingMessage> &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 000000000..af3429421 --- /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<BusinessChatType>; + +struct BusinessChats { + BusinessChatTypes types; + std::vector<not_null<UserData*>> 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<Session*> 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<Timezone> 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<WorkingInterval> 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<LocationPoint> 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<MTPBusinessWorkHours> &hours, + const tl::conditional<MTPBusinessLocation> &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<Session*> owner, + const tl::conditional<MTPBusinessAwayMessage> &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<Session*> owner, + const tl::conditional<MTPBusinessGreetingMessage> &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 000000000..151e36dcb --- /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<Session*> owner) +: _owner(owner) { +} + +BusinessInfo::~BusinessInfo() = default; + +void BusinessInfo::saveWorkingHours( + WorkingHours data, + Fn<void(QString)> 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<void(QString)> 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<void(QString)> 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<Timezones> BusinessInfo::timezonesValue() const { + const_cast<BusinessInfo*>(this)->preloadTimezones(); + return _timezones.value(); +} + +bool BusinessInfo::timezonesLoaded() const { + return !_timezones.current().list.empty(); +} + +QString FindClosestTimezoneId(const std::vector<Timezone> &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 000000000..933d86910 --- /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<Session*> owner); + ~BusinessInfo(); + + void preload(); + + void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail); + + void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail); + void applyAwaySettings(AwaySettings data); + [[nodiscard]] AwaySettings awaySettings() const; + [[nodiscard]] bool awaySettingsLoaded() const; + [[nodiscard]] rpl::producer<> awaySettingsChanged() const; + + void saveGreetingSettings( + GreetingSettings data, + Fn<void(QString)> 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<Timezones> timezonesValue() const; + +private: + const not_null<Session*> _owner; + + rpl::variable<Timezones> _timezones; + + std::optional<AwaySettings> _awaySettings; + rpl::event_stream<> _awaySettingsChanged; + + std::optional<GreetingSettings> _greetingSettings; + rpl::event_stream<> _greetingSettingsChanged; + + mtpRequestId _timezonesRequestId = 0; + int32 _timezonesHash = 0; + +}; + +[[nodiscard]] QString FindClosestTimezoneId( + const std::vector<Timezone> &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 000000000..75b27a25b --- /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<MTPMessageEntity>()), + 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<MTPRestrictionReason>(), + 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<Session*> owner) +: _session(&owner->session()) +, _history(owner->history(_session->userPeerId())) +, _clearTimer([=] { clearOldRequests(); }) { + owner->itemRemoved( + ) | rpl::filter([](not_null<const HistoryItem*> item) { + return item->isBusinessShortcut(); + }) | rpl::start_with_next([=](not_null<const HistoryItem*> 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<MTPQuickReply> &list) { + auto shortcuts = parseShortcuts(list); + auto changes = std::vector<ShortcutIdChange>(); + 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<not_null<HistoryItem*>>(); + 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<MTPQuickReply> &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<const HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> 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<ShortcutIdChange> { + 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<void()> done, + Fn<void(QString)> 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<MTPint>(), + 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<not_null<HistoryItem*>>(); + auto clear = base::flat_set<not_null<HistoryItem*>>(); + 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<not_null<HistoryItem*>>(); + 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<not_null<HistoryItem*>> &added, + const base::flat_set<not_null<HistoryItem*>> &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<const HistoryItem*> 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<HistoryMessageEdited>()) { + HashUpdate(hash, edited->date); + } else { + HashUpdate(hash, TimeId(0)); + } + } + return HashFinalize(hash); +} + +MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null<Main::Session*> 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 000000000..76a1df56d --- /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<BusinessShortcutId, Shortcut> 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<Session*> owner); + ~ShortcutMessages(); + + [[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> 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<HistoryItem*> local); + + void appendSending(not_null<HistoryItem*> item); + void removeSending(not_null<HistoryItem*> 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<ShortcutIdChange> 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<void()> done, + Fn<void(QString)> fail); + void removeShortcut(BusinessShortcutId shortcutId); + +private: + using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>; + struct List { + std::vector<OwnedItem> items; + base::flat_map<MsgId, not_null<HistoryItem*>> 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<not_null<HistoryItem*>> &added, + const base::flat_set<not_null<HistoryItem*>> &clear); + void sort(List &list); + void remove(not_null<const HistoryItem*> 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<MTPQuickReply> &list); + [[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const; + [[nodiscard]] Shortcuts parseShortcuts( + const QVector<MTPQuickReply> &list) const; + + const not_null<Main::Session*> _session; + const not_null<History*> _history; + + base::Timer _clearTimer; + base::flat_map<BusinessShortcutId, List> _data; + base::flat_map<BusinessShortcutId, Request> _requests; + rpl::event_stream<BusinessShortcutId> _updates; + + Shortcuts _shortcuts; + rpl::event_stream<> _shortcutsChanged; + rpl::event_stream<ShortcutIdChange> _shortcutIdChanges; + BusinessShortcutId _localShortcutId = 0; + uint64 _shortcutsHash = 0; + mtpRequestId _shortcutsRequestId = 0; + bool _shortcutsLoaded = false; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP( + not_null<Main::Session*> session, + BusinessShortcutId id); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2c29ef8fc..bd85b02fa 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<Flag>; 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 20fadb41d..1addfcadb 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<MTPInputPeer>(pinned), MTP_vector<MTPInputPeer>(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<MTPInputPeer>(pinned), MTP_vector<MTPInputPeer>(include), MTP_vector<MTPInputPeer>(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<MTPDialogFilter> &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 987d55ebe..7b5a96476 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<Flag>; diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index cd736e9e7..95c78092d 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -879,29 +879,20 @@ not_null<HistoryItem*> 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 3f5e22a23..4fdd9159c 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 9f6231652..5d3c11e0b 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<not_null<History*>, QVector<MTPint>> idsByPeer; base::flat_map<not_null<PeerData*>, QVector<MTPint>> scheduledIdsByPeer; + base::flat_map<BusinessShortcutId, QVector<MTPint>> 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<MTPint>(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 7d9a59b4a..a5e0090db 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 7361373df..3e43cd47a 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 d2790a288..b3ecb27b0 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 443474040..ecca86149 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<Main::Session*> session) +: _session(session) { +} + +int LevelLimits::channelColorLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_color_level_min"_q, + 5); +} + +int LevelLimits::channelBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_bg_icon_level_min"_q, + 4); +} + +int LevelLimits::channelProfileBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_profile_bg_icon_level_min"_q, + 7); +} + +int LevelLimits::channelEmojiStatusLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::channelWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::channelCustomWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"channel_custom_wallpaper_level_min"_q, + 10); +} + +int LevelLimits::groupTranscribeLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_transcribe_level_min"_q, + 6); +} + +int LevelLimits::groupEmojiStickersLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_emoji_stickers_level_min"_q, + 4); +} + +int LevelLimits::groupProfileBgIconLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_profile_bg_icon_level_min"_q, + 5); +} + +int LevelLimits::groupEmojiStatusLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_emoji_status_level_min"_q, + 8); +} + +int LevelLimits::groupWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + u"group_wallpaper_level_min"_q, + 9); +} + +int LevelLimits::groupCustomWallpaperLevelMin() const { + return _session->account().appConfig().get<int>( + 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 bc3c86d9f..5c50d7a12 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<Main::Session*> 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<Main::Session*> _session; + +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 6c2a1a9d7..0cd53700c 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*> 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*> history, MsgId rootId) { diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 9ea40996e..3fe58f530 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<MTPRestrictionReason>(), - 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<Session*> owner) @@ -238,7 +238,8 @@ void ScheduledMessages::sendNowSimpleMessage( MTPlong(), MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - 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 f7cd3d44d..c686d86cf 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<Main::Session*> session) _session->local().cacheBigFilePath(), _session->local().cacheBigFileSettings())) , _groupFreeTranscribeLevel(session->account().appConfig().value( -) | rpl::map([=] { - return session->account().appConfig().get<int>( - 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<Main::Session*> session) , _watchForOfflineTimer([=] { checkLocalUsersWentOffline(); }) , _groups(this) , _chatsFilters(std::make_unique<ChatFilters>(this)) -, _scheduledMessages(std::make_unique<ScheduledMessages>(this)) , _cloudThemes(std::make_unique<CloudThemes>(session)) , _sendActionManager(std::make_unique<SendActionManager>()) , _streaming(std::make_unique<Streaming>(this)) , _mediaRotation(std::make_unique<MediaRotation>()) , _histories(std::make_unique<Histories>(this)) , _stickers(std::make_unique<Stickers>(this)) -, _sponsoredMessages(std::make_unique<SponsoredMessages>(this)) , _reactions(std::make_unique<Reactions>(this)) , _emojiStatuses(std::make_unique<EmojiStatuses>(this)) , _forumIcons(std::make_unique<ForumIcons>(this)) , _notifySettings(std::make_unique<NotifySettings>(this)) , _customEmojiManager(std::make_unique<CustomEmojiManager>(this)) , _stories(std::make_unique<Stories>(this)) -, _savedMessages(std::make_unique<SavedMessages>(this)) { +, _savedMessages(std::make_unique<SavedMessages>(this)) +, _chatbots(std::make_unique<Chatbots>(this)) +, _businessInfo(std::make_unique<BusinessInfo>(this)) +, _scheduledMessages(std::make_unique<ScheduledMessages>(this)) +, _shortcutMessages(std::make_unique<ShortcutMessages>(this)) +, _sponsoredMessages(std::make_unique<SponsoredMessages>(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<int> 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<int> 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<int> Session::maxPinnedChatsLimitValue( rpl::producer<int> Session::maxPinnedChatsLimitValue( not_null<Data::Forum*> 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<int> 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<MTPRestrictionReason>(), - 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 d391d1d31..74204af26 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<ChatFilters> _chatsFilters; - std::unique_ptr<ScheduledMessages> _scheduledMessages; const std::unique_ptr<CloudThemes> _cloudThemes; const std::unique_ptr<SendActionManager> _sendActionManager; const std::unique_ptr<Streaming> _streaming; const std::unique_ptr<MediaRotation> _mediaRotation; const std::unique_ptr<Histories> _histories; const std::unique_ptr<Stickers> _stickers; - std::unique_ptr<SponsoredMessages> _sponsoredMessages; const std::unique_ptr<Reactions> _reactions; const std::unique_ptr<EmojiStatuses> _emojiStatuses; const std::unique_ptr<ForumIcons> _forumIcons; @@ -1065,8 +1075,13 @@ private: const std::unique_ptr<CustomEmojiManager> _customEmojiManager; const std::unique_ptr<Stories> _stories; const std::unique_ptr<SavedMessages> _savedMessages; + const std::unique_ptr<Chatbots> _chatbots; + const std::unique_ptr<BusinessInfo> _businessInfo; + std::unique_ptr<ScheduledMessages> _scheduledMessages; + std::unique_ptr<ShortcutMessages> _shortcutMessages; + std::unique_ptr<SponsoredMessages> _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 8d5489d70..6b762e2db 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -27,8 +27,7 @@ using SparseUnsortedIdsSlice = AbstractSparseIds<std::vector<MsgId>>; 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 ccc8e1236..a22e54fcb 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*> 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 a85889949..c9a14745a 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -974,7 +974,7 @@ std::shared_ptr<HistoryItem> Stories::resolveItem(not_null<Story*> story) { } const auto history = _owner->history(story->peer()); auto result = std::shared_ptr<HistoryItem>( - 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 997d4282e..ab792cd30 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 3b511cba0..ca11c8969 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<FullMsgId>; -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<MessageFlag>; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 543adf9b5..a804e514a 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<Data::Session*> 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<Data::BusinessDetails>(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<UserData*> 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 f6ba39749..cf9eafef7 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<UserDataFlags>; UserData(not_null<Data::Session*> 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<Data::UnavailableReason> & override; @@ -201,6 +207,7 @@ private: Data::UsernamesInfo _username; + std::unique_ptr<Data::BusinessDetails> _businessDetails; std::vector<Data::UnavailableReason> _unavailableReasons; QString _phone; QString _privateForwardName; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 2396b216f..c43dc9f15 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 a3d1264ff..276d4e5b6 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 95e3f374f..326d265ac 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 f7928f006..1f56f9ec1 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<std::vector<HistoryMessageMarkupButton>> 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 5f2c2da39..76585991c 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 1aa595089..af5a48a09 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<QByteArray, QByteArray>; + 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 dde36a5a3..5bd7f2c37 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<HistoryMessageMarkupButton> &row) { context.nesting.push_back(Context::kArray); @@ -817,7 +794,8 @@ QByteArray SerializeMessage( auto pairs = std::vector<std::pair<QByteArray, QByteArray>>(); 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 bbc91c2dc..8f546260a 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 0642a1568..14f9718fc 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<MTPRestrictionReason>(), - 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 52a9803d3..bdfa6d1d0 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<Data::Session*> owner, PeerId peerId) @@ -451,17 +458,17 @@ std::vector<not_null<HistoryItem*>> History::createItems( not_null<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> added) { } not_null<HistoryItem*> 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<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> forwardOriginal, - MsgId topicRootId) { + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> forwardOriginal) { return addNewItem( - makeMessage( - id, - flags | MessageFlag::Local, - date, - from, - postAuthor, - forwardOriginal, - topicRootId), + makeMessage(WithLocalFlag(std::move(fields)), forwardOriginal), true); } not_null<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> 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<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> 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<HistoryItem*> History::addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup) { + HistoryItemCommonFields &&fields, + not_null<GameData*> 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<HistoryItem*> History::addNewLocalMessage( +not_null<HistoryItem*> 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 adeb63fc2..29620dc2e 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<UserId> &changes); template <typename ...Args> - not_null<HistoryItem*> makeMessage(Args &&...args) { + not_null<HistoryItem*> makeMessage(MsgId id, Args &&...args) { return static_cast<HistoryItem*>( insertItem( std::make_unique<HistoryItem>( this, + id, + std::forward<Args>(args)...)).get()); + } + template <typename ...Args> + not_null<HistoryItem*> makeMessage( + HistoryItemCommonFields &&fields, + Args &&...args) { + return static_cast<HistoryItem*>( + insertItem( + std::make_unique<HistoryItem>( + this, + std::move(fields), std::forward<Args>(args)...)).get()); } @@ -143,62 +156,30 @@ public: not_null<HistoryItem*> addNewMessage( MsgId id, - const MTPMessage &msg, + const MTPMessage &message, MessageFlags localFlags, NewMessageType type); + not_null<HistoryItem*> 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<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> forwardOriginal, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> forwardOriginal); not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); - not_null<HistoryItem*> addNewLocalMessage( - MsgId id, - MessageFlags flags, - UserId viaBotId, - FullReplyTo replyTo, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); not_null<HistoryItem*> addNewLocalMessage( + HistoryItemCommonFields &&fields, + not_null<GameData*> game); + + not_null<HistoryItem*> 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 eedcab893..b3e4639e1 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 1ccc07cf9..1ebead4c8 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -131,6 +131,14 @@ template <typename T> return false; } +[[nodiscard]] HistoryItemCommonFields ForwardedFields( + HistoryItemCommonFields fields, + not_null<History*> history, + not_null<HistoryItem*> 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*> 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<Data::MediaPhoto>( @@ -498,25 +502,16 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> original, - MsgId topicRootId) -: HistoryItem( - history, - id, - (NewForwardedFlags(history->peer, from, original) | flags), - date, - from) { + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> 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<HistoryMessageReply>()) { @@ -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*> 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*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> 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*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> 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<Data::MediaPhoto>(this, photo, spoiler); @@ -711,27 +654,10 @@ HistoryItem::HistoryItem( HistoryItem::HistoryItem( not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> 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<GameData*> game) +: HistoryItem(history, fields) { + createComponentsHelper(std::move(fields)); _media = std::make_unique<Data::MediaGame>(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*> 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*> history, + MsgId id, not_null<Data::Story*> 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<HistoryServicePinned>()) { 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<HistoryMessageVia>() || Has<HistoryMessageForwarded>()) { 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 689bca9b8..b81a0057c 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 <typename Enum> 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<HistoryItem> { public: [[nodiscard]] static std::unique_ptr<Data::Media> CreateMedia( @@ -114,73 +132,39 @@ public: Data::SponsoredFrom from, const TextWithEntities &textWithEntities, HistoryItem *injectedAfter); + HistoryItem( // Story wrap. + not_null<History*> history, + MsgId id, + not_null<Data::Story*> story); HistoryItem( // Local message. not_null<History*> 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*> history, - MsgId id, - MessageFlags flags, - TimeId date, + HistoryItemCommonFields &&fields, PreparedServiceText &&message, - PeerId from = 0, PhotoData *photo = nullptr); HistoryItem( // Local forwarded. not_null<History*> history, - MsgId id, - MessageFlags flags, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<HistoryItem*> original, - MsgId topicRootId); + HistoryItemCommonFields &&fields, + not_null<HistoryItem*> original); HistoryItem( // Local photo. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<PhotoData*> photo, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local document. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, + HistoryItemCommonFields &&fields, not_null<DocumentData*> document, - const TextWithEntities &caption, - HistoryMessageMarkupData &&markup); + const TextWithEntities &caption); HistoryItem( // Local game. not_null<History*> history, - MsgId id, - MessageFlags flags, - FullReplyTo replyTo, - UserId viaBotId, - TimeId date, - PeerId from, - const QString &postAuthor, - not_null<GameData*> game, - HistoryMessageMarkupData &&markup); - HistoryItem(not_null<History*> history, not_null<Data::Story*> story); + HistoryItemCommonFields &&fields, + not_null<GameData*> 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*> 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 05e99d2a2..aeef37557 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 6fdc3de55..25bbcc789 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<LambdaClickHandler>([=](ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); 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<HistoryItem*> GenerateJoinedMessage( TimeId inviteDate, not_null<UserData*> 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<bool> PeerHasThisCall( @@ -662,6 +665,7 @@ std::optional<bool> 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 a8652e72f..a0e154edc 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<DeleteMessagesBox>(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<CharactersLimitLabel>( 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 505950697..c7da2180c 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<ChatHelpers::TabbedSelector>( - _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<SendActionUpdate> 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<Data::Thread*> 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<TabbedPanel>( - _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<Ui::RpWidget*> ComposeControls::likeAnimationTarget() const { return _like; } +int ComposeControls::fieldCharacterCount() const { + return Ui::FieldCharacterCount(_field); +} + bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); @@ -3244,7 +3270,7 @@ void ComposeControls::applyInlineBotQuery( } if (!_inlineResults) { _inlineResults = std::make_unique<InlineBots::Layout::Widget>( - _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 b1bc012ca..d21ea9884 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<ChatHelpers::Show> show; @@ -104,6 +107,8 @@ struct ComposeControlsDescriptor { Window::SessionController *regularWindow = nullptr; rpl::producer<ChatHelpers::FileChosen> stickerOrEmojiChosen; rpl::producer<QString> 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<bool> hasSendTextValue() const; [[nodiscard]] rpl::producer<bool> fieldMenuShownValue() const; [[nodiscard]] not_null<Ui::RpWidget*> 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<QWidget*> _parent; + const not_null<QWidget*> _panelsParent; const std::shared_ptr<ChatHelpers::Show> _show; const not_null<Main::Session*> _session; @@ -352,6 +360,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + BusinessShortcutId _shortcutId = 0; Fn<bool()> _showSlowmodeError; Fn<Api::SendAction()> _sendActionFactory; rpl::variable<int> _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 5ba81cd1e..a0d3d6066 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<QString> 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 51ae055f3..c017d8842 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<BotInfo*> 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<decltype(a)>(a), - std::forward<decltype(b)>(b), - HistoryMessageMarkupData(), - std::forward<decltype(other)>(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<decltype(args)>(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<ServiceBox>( 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 3e4eb947d..7a176cf61 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*> 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<HistoryMessageSigned>()) { 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 d593063ef..efdab3334 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 ab1ca2ee9..ac5cd3fc9 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 8a8b09cc0..2b1050d8a 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 0f15a0fef..5a6ea07d6 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 1f2d4b022..f434fd74e 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<UserData*> bot) { } bool ListWidget::elementIsChatWide() { - return _isChatWide; + return _overrideIsChatWide.value_or(_isChatWide); } not_null<Ui::PathShiftGradient*> 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<Element*> 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<HistoryItem*> 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<Ui::RpWidget> &&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 ebba2f578..12e09accf 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<const Element*> view) override; void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w); + void overrideIsChatWide(bool isWide); ~ListWidget(); @@ -600,6 +601,15 @@ private: void showPremiumStickerTooltip( not_null<const HistoryView::Element*> 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<ListDelegate*> _delegate; const not_null<Window::SessionController*> _controller; const std::unique_ptr<EmojiInteractions> _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<bool> _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 3001349a8..722e7800b 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<HistoryMessageForwarded>()) { + 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 4d3d16d9a..3f39d5890 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<DeleteMessagesBox>(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 56798905b..d3f2dc482 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<DeleteMessagesBox>(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 c07606906..76598533f 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<StickerSetBox>( _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 dccca0f3a..1688b8f6c 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 7b0ac35b9..1e6654abd 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<LambdaClickHandler>([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<PeerData*> peer) { + const auto clickHandlerPtr = std::make_shared<ContactClickHandler>([peer]( + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); 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<HistoryItem*> item) { const auto session = &item->history()->session(); - const auto fullId = item->fullId(); - return std::make_shared<LambdaClickHandler>([=](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<ContactClickHandler>([=]( + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); 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<AddContactBox>( - session, - contact->firstName, - contact->lastName, - contact->phoneNumber)); - } - } + if (const auto contact = sharedContact()) { + controller->show(Box<AddContactBox>( + 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>( 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<Ui::RippleAnimation>( + 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<Ui::RippleAnimation>( + 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 ecca595f2..2dd665b94 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<Element*> 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<Ui::RippleAnimation> ripple; + }; + std::vector<Button> _buttons; + Button _mainButton; + std::unique_ptr<Ui::EmptyUserpic> _photoEmpty; mutable Ui::PeerUserpicView _userpic; - - ClickHandlerPtr _linkl; - int _linkw = 0; - QString _link; + mutable QPoint _lastPoint; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index e02bd8f1a..bb754dc0a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -236,7 +236,7 @@ QSize Photo::countCurrentSize(int newWidth) { const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize); const auto minWidth = std::clamp( _parent->minWidthForMedia(), - (_parent->hasBubble() + qMin(thumbMaxWidth, _parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), thumbMaxWidth); diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 7e96e068f..82ca69e8f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -803,9 +803,11 @@ void Poll::paintInlineFooter( p, left, top, - std::min( - _totalVotesLabel.maxWidth(), - paintw - _parent->bottomInfoFirstLineWidth()), + _parent->data()->reactions().empty() + ? std::min( + _totalVotesLabel.maxWidth(), + paintw - _parent->bottomInfoFirstLineWidth()) + : _totalVotesLabel.maxWidth(), width()); } diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp index 6c5583723..01462aa3b 100644 --- a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp @@ -630,9 +630,9 @@ void CreateGiveawayBox( const auto createCallback = [=](GiveawayType type) { return [=] { - const auto was = membersGroup->value(); + const auto was = membersGroup->current(); membersGroup->setValue(type); - const auto now = membersGroup->value(); + const auto now = membersGroup->current(); if (was == now) { base::call_delayed( st::defaultRippleAnimation.hideDuration, @@ -990,7 +990,7 @@ void CreateGiveawayBox( if (state->confirmButtonBusy.current()) { return; } - const auto type = typeGroup->value(); + const auto type = typeGroup->current(); const auto isSpecific = (type == GiveawayType::SpecificUsers); const auto isRandom = (type == GiveawayType::Random); if (!isSpecific && !isRandom) { @@ -1003,7 +1003,7 @@ void CreateGiveawayBox( prepaid ? prepaid->months : state->apiOptions.monthsFromPreset( - durationGroup->value())); + durationGroup->current())); if (isSpecific) { if (state->selectedToAward.empty()) { return; @@ -1029,7 +1029,7 @@ void CreateGiveawayBox( .countries = state->countriesValue.current(), .additionalPrize = state->additionalPrize.current(), .untilDate = state->dateValue.current(), - .onlyNewSubscribers = (membersGroup->value() + .onlyNewSubscribers = (membersGroup->current() == GiveawayType::OnlyNewMembers), .showWinners = state->showWinners.current(), }; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 6efd528f6..9203981dc 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -10,13 +10,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/downloads/info_downloads_inner_widget.h" #include "info/info_controller.h" #include "info/info_memento.h" +#include "ui/boxes/confirm_box.h" #include "ui/search_field_controller.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/scroll_area.h" #include "data/data_download_manager.h" #include "data/data_user.h" #include "core/application.h" #include "lang/lang_keys.h" #include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" namespace Info::Downloads { @@ -102,6 +106,31 @@ void Widget::selectionAction(SelectionAction action) { _inner->selectionAction(action); } +void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto window = controller()->parentController(); + const auto deleteAll = [=] { + auto &manager = Core::App().downloadManager(); + const auto phrase = tr::lng_downloads_delete_sure_all(tr::now); + const auto added = manager.loadedHasNonCloudFile() + ? QString() + : tr::lng_downloads_delete_in_cloud(tr::now); + const auto deleteSure = [=, &manager](Fn<void()> close) { + Ui::PostponeCall(this, close); + manager.deleteAll(); + }; + window->show(Ui::MakeConfirmBox({ + .text = phrase + (added.isEmpty() ? QString() : "\n\n" + added), + .confirmed = deleteSure, + .confirmText = tr::lng_box_delete(tr::now), + .confirmStyle = &st::attentionBoxButton, + })); + }; + addAction( + tr::lng_context_delete_all_files(tr::now), + deleteAll, + &st::menuIconDelete); +} + rpl::producer<QString> Widget::title() { return tr::lng_downloads_section(); } diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.h b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h index 3da1a4f79..f79a31466 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.h @@ -57,6 +57,8 @@ public: rpl::producer<SelectedItems> selectedListValue() const override; void selectionAction(SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; + rpl::producer<QString> title() override; private: diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index d53d12f94..19162c400 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1008,3 +1008,21 @@ similarChannelsLockAbout: FlatLabel(defaultFlatLabel) { minWidth: 128px; } similarChannelsLockAboutPadding: margins(12px, 12px, 12px, 12px); + +infoHoursState: FlatLabel(infoLabeled) { + minWidth: 0px; +} +infoHoursValue: FlatLabel(infoHoursState) { + textFg: windowSubTextFg; + align: align(topright); +} +infoHoursDayLabel: infoHoursState; +infoHoursOuter: RoundButton(defaultActiveButton) { + textBg: transparent; + textBgOver: transparent; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +infoHoursOuterMargin: margins(8px, 4px, 8px, 4px); +infoHoursDaySkip: 6px; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index 8cf7a38d5..b2908c3ac 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_forum.h" #include "main/main_session.h" +#include "window/window_peer_menu.h" #include "styles/style_info.h" #include "styles/style_profile.h" #include "styles/style_layers.h" @@ -203,13 +204,25 @@ void ContentWidget::applyAdditionalScroll(int additionalScroll) { } } +void ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) { + if (_maxVisibleHeight != maxVisibleHeight) { + _maxVisibleHeight = maxVisibleHeight; + update(); + } +} + rpl::producer<int> ContentWidget::desiredHeightValue() const { using namespace rpl::mappers; return rpl::combine( _innerWrap->entity()->desiredHeightValue(), _scrollTopSkip.value(), _scrollBottomSkip.value() - ) | rpl::map(_1 + _2 + _3); + //) | rpl::map(_1 + _2 + _3); + ) | rpl::map([=](int desired, int, int) { + return desired + + _scrollTopSkip.current() + + _scrollBottomSkip.current(); + }); } rpl::producer<bool> ContentWidget::desiredShadowVisibility() const { @@ -256,6 +269,24 @@ QRect ContentWidget::floatPlayerAvailableRect() const { return mapToGlobal(_scroll->geometry()); } +void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto peer = _controller->key().peer(); + const auto topic = _controller->key().topic(); + if (!peer && !topic) { + return; + } + + Window::FillDialogsEntryMenu( + _controller->parentController(), + Dialogs::EntryState{ + .key = (topic + ? Dialogs::Key{ topic } + : Dialogs::Key{ peer->owner().history(peer) }), + .section = Dialogs::EntryState::Section::Profile, + }, + addAction); +} + rpl::producer<SelectedItems> ContentWidget::selectedListValue() const { return rpl::single(SelectedItems(Storage::SharedMediaType::Photo)); } @@ -328,6 +359,10 @@ rpl::producer<bool> ContentWidget::desiredBottomShadowVisibility() const { }); } +not_null<Ui::ScrollArea*> ContentWidget::scroll() const { + return _scroll.data(); +} + Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index a7782d4f9..7f7cd0f2f 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -28,6 +28,10 @@ template <typename Widget> class PaddingWrap; } // namespace Ui +namespace Ui::Menu { +struct MenuCallback; +} // namespace Ui::Menu + namespace Info::Settings { struct Tag; } // namespace Info::Settings @@ -81,6 +85,7 @@ public: const QRect &newGeometry, int topDelta); void applyAdditionalScroll(int additionalScroll); + void applyMaxVisibleHeight(int maxVisibleHeight); int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const; [[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const { @@ -94,7 +99,14 @@ public: virtual rpl::producer<SelectedItems> selectedListValue() const; virtual void selectionAction(SelectionAction action) { } + virtual void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction); + [[nodiscard]] virtual bool closeByOutsideClick() const { + return true; + } + virtual void checkBeforeClose(Fn<void()> close) { + close(); + } [[nodiscard]] virtual rpl::producer<QString> title() = 0; [[nodiscard]] virtual rpl::producer<QString> subtitle() { return nullptr; @@ -115,9 +127,13 @@ protected: doSetInnerWidget(std::move(inner))); } - not_null<Controller*> controller() const { + [[nodiscard]] not_null<Controller*> controller() const { return _controller; } + [[nodiscard]] not_null<Ui::ScrollArea*> scroll() const; + [[nodiscard]] int maxVisibleHeight() const { + return _maxVisibleHeight; + } void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; @@ -151,6 +167,7 @@ private: base::unique_qptr<Ui::RpWidget> _searchWrap = nullptr; QPointer<Ui::InputField> _searchField; int _innerDesiredHeight = 0; + int _maxVisibleHeight = 0; bool _isStackBottom = false; // Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent(). diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index cbf5117c3..3404cf932 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -30,7 +30,7 @@ LayerWidget::LayerWidget( not_null<Window::SessionController*> controller, not_null<Memento*> memento) : _controller(controller) -, _content(this, controller, Wrap::Layer, memento) { +, _contentWrap(this, controller, Wrap::Layer, memento) { setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -39,7 +39,7 @@ LayerWidget::LayerWidget( not_null<Window::SessionController*> controller, not_null<MoveMemento*> memento) : _controller(controller) -, _content(memento->takeContent(this, Wrap::Layer)) { +, _contentWrap(memento->takeContent(this, Wrap::Layer)) { setupHeightConsumers(); controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate()); } @@ -64,17 +64,17 @@ void LayerWidget::floatPlayerToggleGifsPaused(bool paused) { auto LayerWidget::floatPlayerGetSection(Window::Column column) -> not_null<::Media::Player::FloatSectionDelegate*> { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - return _content; + return _contentWrap; } void LayerWidget::floatPlayerEnumerateSections(Fn<void( not_null<::Media::Player::FloatSectionDelegate*> widget, Window::Column widgetColumn)> callback) { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - callback(_content, Window::Column::Second); + callback(_contentWrap, Window::Column::Second); } bool LayerWidget::floatPlayerIsVisible(not_null<HistoryItem*> item) { @@ -87,9 +87,9 @@ void LayerWidget::floatPlayerDoubleClickEvent( } void LayerWidget::setupHeightConsumers() { - Expects(_content != nullptr); + Expects(_contentWrap != nullptr); - _content->scrollTillBottomChanges( + _contentWrap->scrollTillBottomChanges( ) | rpl::filter([this] { if (!_inResize) { return true; @@ -100,10 +100,10 @@ void LayerWidget::setupHeightConsumers() { resizeToWidth(width()); }, lifetime()); - _content->grabbingForExpanding( + _contentWrap->grabbingForExpanding( ) | rpl::start_with_next([=](bool grabbing) { if (grabbing) { - _savedHeight = _contentHeight; + _savedHeight = _contentWrapHeight; _savedHeightAnimation = base::take(_heightAnimation); setContentHeight(_desiredHeight); } else { @@ -112,7 +112,7 @@ void LayerWidget::setupHeightConsumers() { } }, lifetime()); - _content->desiredHeightValue( + _contentWrap->desiredHeightValue( ) | rpl::start_with_next([this](int height) { if (!height) { // New content arrived. @@ -128,32 +128,31 @@ void LayerWidget::setupHeightConsumers() { _heightAnimated = true; _heightAnimation.start([=] { setContentHeight(_heightAnimation.value(_desiredHeight)); - }, _contentHeight, _desiredHeight, st::slideDuration); + }, _contentWrapHeight, _desiredHeight, st::slideDuration); resizeToWidth(width()); } }, lifetime()); } void LayerWidget::setContentHeight(int height) { - if (_contentHeight == height) { + if (_contentWrapHeight == height) { return; } - - _contentHeight = height; + _contentWrapHeight = height; if (_inResize) { _pendingResize = true; - } else if (_content) { + } else if (_contentWrap) { resizeToWidth(width()); } } void LayerWidget::showFinished() { floatPlayerShowVisible(); - _content->showFast(); + _contentWrap->showFast(); } void LayerWidget::parentResized() { - if (!_content) { + if (!_contentWrap) { return; } @@ -163,7 +162,7 @@ void LayerWidget::parentResized() { Ui::FocusPersister persister(this); restoreFloatPlayerDelegate(); - auto memento = std::make_shared<MoveMemento>(std::move(_content)); + auto memento = std::make_shared<MoveMemento>(std::move(_contentWrap)); // We want to call hideSpecialLayer synchronously to avoid glitches, // but we can't destroy LayerStackWidget from its' resizeEvent, @@ -209,7 +208,7 @@ bool LayerWidget::takeToThirdSection() { // //Ui::FocusPersister persister(this); //auto localCopy = _controller; - //auto memento = MoveMemento(std::move(_content)); + //auto memento = MoveMemento(std::move(_contentWrap)); //localCopy->hideSpecialLayer(anim::type::instant); //// When creating third section in response to the window @@ -235,7 +234,7 @@ bool LayerWidget::takeToThirdSection() { bool LayerWidget::showSectionInternal( not_null<Window::SectionMemento*> memento, const Window::SectionShow ¶ms) { - if (_content && _content->showInternal(memento, params)) { + if (_contentWrap && _contentWrap->showInternal(memento, params)) { if (params.activation != anim::activation::background) { _controller->parentController()->hideLayer(); } @@ -245,7 +244,7 @@ bool LayerWidget::showSectionInternal( } bool LayerWidget::closeByOutsideClick() const { - return _content ? _content->closeByOutsideClick() : true; + return _contentWrap ? _contentWrap->closeByOutsideClick() : true; } int LayerWidget::MinimalSupportedWidth() { @@ -254,7 +253,7 @@ int LayerWidget::MinimalSupportedWidth() { } int LayerWidget::resizeGetHeight(int newWidth) { - if (!parentWidget() || !_content || !newWidth) { + if (!parentWidget() || !_contentWrap || !newWidth) { return 0; } constexpr auto kMaxAttempts = 16; @@ -266,7 +265,7 @@ int LayerWidget::resizeGetHeight(int newWidth) { if (!_pendingResize) { const auto oldGeometry = geometry(); if (newGeometry != oldGeometry) { - _content->forceContentRepaint(); + _contentWrap->forceContentRepaint(); } if (newGeometry.topLeft() != oldGeometry.topLeft()) { move(newGeometry.topLeft()); @@ -291,9 +290,10 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto newBottom = newTop; const auto bottomRadius = st::boxRadius; - // Top rounding is included in _contentHeight. - auto desiredHeight = _contentHeight + bottomRadius; - accumulate_min(desiredHeight, windowHeight - newTop - newBottom); + const auto maxVisibleHeight = windowHeight - newTop; + // Top rounding is included in _contentWrapHeight. + auto desiredHeight = _contentWrapHeight + bottomRadius; + accumulate_min(desiredHeight, maxVisibleHeight - newBottom); // First resize content to new width and get the new desired height. const auto contentLeft = 0; @@ -301,34 +301,35 @@ QRect LayerWidget::countGeometry(int newWidth) { const auto contentBottom = bottomRadius; const auto contentWidth = newWidth; auto contentHeight = desiredHeight - contentTop - contentBottom; - const auto scrollTillBottom = _content->scrollTillBottom(contentHeight); + const auto scrollTillBottom = _contentWrap->scrollTillBottom( + contentHeight); auto additionalScroll = std::min(scrollTillBottom, newBottom); - const auto expanding = (_desiredHeight > _contentHeight); + const auto expanding = (_desiredHeight > _contentWrapHeight); desiredHeight += additionalScroll; contentHeight += additionalScroll; - _tillBottom = (newTop + desiredHeight >= windowHeight); + _tillBottom = (desiredHeight >= maxVisibleHeight); if (_tillBottom) { additionalScroll += contentBottom; } - _contentTillBottom = _tillBottom && !_content->scrollBottomSkip(); + _contentTillBottom = _tillBottom && !_contentWrap->scrollBottomSkip(); if (_contentTillBottom) { contentHeight += contentBottom; } - _content->updateGeometry({ + _contentWrap->updateGeometry({ contentLeft, contentTop, contentWidth, contentHeight, - }, expanding, additionalScroll); + }, expanding, additionalScroll, maxVisibleHeight); return QRect(newLeft, newTop, newWidth, desiredHeight); } void LayerWidget::doSetInnerFocus() { - if (_content) { - _content->setInnerFocus(); + if (_contentWrap) { + _contentWrap->setInnerFocus(); } } @@ -341,7 +342,7 @@ void LayerWidget::paintEvent(QPaintEvent *e) { if (!_tillBottom) { const auto bottom = QRect{ 0, height() - radius, width(), radius }; if (clip.intersects(bottom)) { - if (const auto rounding = _content->bottomSkipRounding()) { + if (const auto rounding = _contentWrap->bottomSkipRounding()) { rounding->paint(p, rect(), RectPart::FullBottom); } else { Ui::FillRoundRect(p, bottom, st::boxBg, { @@ -350,11 +351,11 @@ void LayerWidget::paintEvent(QPaintEvent *e) { } } } else if (!_contentTillBottom) { - const auto rounding = _content->bottomSkipRounding(); + const auto rounding = _contentWrap->bottomSkipRounding(); const auto &color = rounding ? rounding->color() : st::boxBg; p.fillRect(0, height() - radius, width(), radius, color); } - if (_content->animatingShow()) { + if (_contentWrap->animatingShow()) { const auto top = QRect{ 0, 0, width(), radius }; if (clip.intersects(top)) { Ui::FillRoundRect(p, top, st::boxBg, { diff --git a/Telegram/SourceFiles/info/info_layer_widget.h b/Telegram/SourceFiles/info/info_layer_widget.h index c223b9d82..df8bc2726 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.h +++ b/Telegram/SourceFiles/info/info_layer_widget.h @@ -73,10 +73,10 @@ private: [[nodiscard]] QRect countGeometry(int newWidth); not_null<Window::SessionController*> _controller; - object_ptr<WrapWidget> _content; + object_ptr<WrapWidget> _contentWrap; int _desiredHeight = 0; - int _contentHeight = 0; + int _contentWrapHeight = 0; int _savedHeight = 0; Ui::Animations::Simple _heightAnimation; Ui::Animations::Simple _savedHeightAnimation; diff --git a/Telegram/SourceFiles/info/info_section_widget.cpp b/Telegram/SourceFiles/info/info_section_widget.cpp index 084b2f223..0a7b7da67 100644 --- a/Telegram/SourceFiles/info/info_section_widget.cpp +++ b/Telegram/SourceFiles/info/info_section_widget.cpp @@ -50,11 +50,15 @@ void SectionWidget::init() { return (_content != nullptr); }) | rpl::start_with_next([=](QSize size, int) { const auto expanding = false; - const auto additionalScroll = st::boxRadius; const auto full = !_content->scrollBottomSkip(); + const auto additionalScroll = (full ? st::boxRadius : 0); const auto height = size.height() - (full ? 0 : st::boxRadius); const auto wrapGeometry = QRect{ 0, 0, size.width(), height }; - _content->updateGeometry(wrapGeometry, expanding, additionalScroll); + _content->updateGeometry( + wrapGeometry, + expanding, + additionalScroll, + size.height()); }, lifetime()); _connecting = std::make_unique<Window::ConnectionState>( diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index a8b57bc55..d09652a85 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" -#include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" @@ -731,25 +730,7 @@ bool TopBar::computeCanToggleStoryPin() const { } Ui::StringWithNumbers TopBar::generateSelectedText() const { - using Type = Storage::SharedMediaType; - const auto phrase = [&] { - switch (_selectedItems.type) { - case Type::Photo: return tr::lng_media_selected_photo; - case Type::GIF: return tr::lng_media_selected_gif; - case Type::Video: return tr::lng_media_selected_video; - case Type::File: return tr::lng_media_selected_file; - case Type::MusicFile: return tr::lng_media_selected_song; - case Type::Link: return tr::lng_media_selected_link; - case Type::RoundVoiceFile: return tr::lng_media_selected_audio; - case Type::PhotoVideo: return tr::lng_stories_row_count; - } - Unexpected("Type in TopBar::generateSelectedText()"); - }(); - return phrase( - tr::now, - lt_count, - _selectedItems.list.size(), - Ui::StringWithNumbers::FromString); + return _selectedItems.title(_selectedItems.list.size()); } bool TopBar::selectionMode() const { diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 7018d52fe..7b677be15 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_main.h" #include "settings/settings_premium.h" #include "ui/effects/ripple_animation.h" // MaskByDrawer. +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" @@ -31,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/shortcuts.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" -#include "window/window_peer_menu.h" #include "boxes/peer_list_box.h" #include "ui/boxes/confirm_box.h" #include "main/main_session.h" @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "mainwidget.h" #include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_info.h" #include "styles/style_profile.h" @@ -68,6 +69,26 @@ const style::InfoTopBar &TopBarStyle(Wrap wrap) { && section.settingsType()->hasCustomTopBar(); } +[[nodiscard]] Fn<Ui::StringWithNumbers(int)> SelectedTitleForMedia( + Section::MediaType type) { + return [type](int count) { + using Type = Storage::SharedMediaType; + return [&] { + switch (type) { + case Type::Photo: return tr::lng_media_selected_photo; + case Type::GIF: return tr::lng_media_selected_gif; + case Type::Video: return tr::lng_media_selected_video; + case Type::File: return tr::lng_media_selected_file; + case Type::MusicFile: return tr::lng_media_selected_song; + case Type::Link: return tr::lng_media_selected_link; + case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + case Type::PhotoVideo: return tr::lng_stories_row_count; + } + Unexpected("Type in TopBar::generateSelectedText()"); + }()(tr::now, lt_count, count, Ui::StringWithNumbers::FromString); + }; +} + } // namespace struct WrapWidget::StackItem { @@ -75,6 +96,10 @@ struct WrapWidget::StackItem { // std::shared_ptr<ContentMemento> anotherTab; }; +SelectedItems::SelectedItems(Section::MediaType mediaType) +: title(SelectedTitleForMedia(mediaType)) { +} + WrapWidget::WrapWidget( QWidget *parent, not_null<Window::SessionController*> window, @@ -343,17 +368,24 @@ void WrapWidget::createTopBar() { _controller->searchEnabledByContent(), _controller->takeSearchStartsFocused()); } + _topBar->lower(); + _topBar->resizeToWidth(width()); + _topBar->finishAnimating(); + _topBar->show(); +} + +void WrapWidget::setupTopBarMenuToggle() { + Expects(_content != nullptr); + + if (!_topBar) { + return; + } const auto section = _controller->section(); if (section.type() == Section::Type::Profile - && (wrapValue != Wrap::Side || hasStackHistory())) { + && (wrap() != Wrap::Side || hasStackHistory())) { addTopBarMenuButton(); addProfileCallsButton(); - } else if (section.type() == Section::Type::Settings - && (section.settingsType() - == ::Settings::CloudPasswordEmailConfirmId() - || section.settingsType() == ::Settings::Main::Id() - || section.settingsType() == ::Settings::Chat::Id() - || section.settingsType() == ::Settings::Ayu::Id())) { + } else if (section.type() == Section::Type::Settings) { addTopBarMenuButton(); } else if (section.type() == Section::Type::Downloads) { auto &manager = Core::App().downloadManager(); @@ -379,25 +411,23 @@ void WrapWidget::createTopBar() { } }, _topBar->lifetime()); } - - _topBar->lower(); - _topBar->resizeToWidth(width()); - _topBar->finishAnimating(); - _topBar->show(); } void WrapWidget::checkBeforeClose(Fn<void()> close) { - _controller->parentController()->hideLayer(); - close(); + _content->checkBeforeClose(crl::guard(this, [=] { + _controller->parentController()->hideLayer(); + close(); + })); } void WrapWidget::addTopBarMenuButton() { Expects(_topBar != nullptr); + Expects(_content != nullptr); { const auto guard = gsl::finally([&] { _topBarMenu = nullptr; }); showTopBarMenu(true); - if (_topBarMenu->empty()) { + if (!_topBarMenu) { return; } } @@ -414,7 +444,7 @@ void WrapWidget::addTopBarMenuButton() { } bool WrapWidget::closeByOutsideClick() const { - return true; + return _content->closeByOutsideClick(); } void WrapWidget::addProfileCallsButton() { @@ -466,65 +496,19 @@ void WrapWidget::showTopBarMenu(bool check) { } }); - const auto addAction = Ui::Menu::CreateAddActionCallback(_topBarMenu); - if (key().isDownloads()) { - addAction( - tr::lng_context_delete_all_files(tr::now), - [=] { deleteAllDownloads(); }, - &st::menuIconDelete); - } else if (const auto peer = key().peer()) { - const auto topic = key().topic(); - Window::FillDialogsEntryMenu( - _controller->parentController(), - Dialogs::EntryState{ - .key = (topic - ? Dialogs::Key{ topic } - : Dialogs::Key{ peer->owner().history(peer) }), - .section = Dialogs::EntryState::Section::Profile, - }, - addAction); - } else if (const auto self = key().settingsSelf()) { - const auto showOther = [=](::Settings::Type type) { - const auto controller = _controller.get(); - _topBarMenu = nullptr; - controller->showSettings(type); - }; - ::Settings::FillMenu( - _controller->parentController(), - _controller->section().settingsType(), - showOther, - addAction); - } else { + _content->fillTopBarMenu(Ui::Menu::CreateAddActionCallback(_topBarMenu)); + if (_topBarMenu->empty()) { _topBarMenu = nullptr; return; + } else if (check) { + return; } _topBarMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - if (check) { - return; - } _topBarMenuToggle->setForceRippled(true); _topBarMenu->popup(_topBarMenuToggle->mapToGlobal( st::infoLayerTopBarMenuPosition)); } -void WrapWidget::deleteAllDownloads() { - auto &manager = Core::App().downloadManager(); - const auto phrase = tr::lng_downloads_delete_sure_all(tr::now); - const auto added = manager.loadedHasNonCloudFile() - ? QString() - : tr::lng_downloads_delete_in_cloud(tr::now); - const auto deleteSure = [=, &manager](Fn<void()> close) { - Ui::PostponeCall(this, close); - manager.deleteAll(); - }; - _controller->parentController()->show(Ui::MakeConfirmBox({ - .text = phrase + (added.isEmpty() ? QString() : "\n\n" + added), - .confirmed = deleteSure, - .confirmText = tr::lng_box_delete(tr::now), - .confirmStyle = &st::attentionBoxButton, - })); -} - bool WrapWidget::requireTopBarSearch() const { if (!_topBar || !_controller->searchFieldController()) { return false; @@ -599,6 +583,7 @@ void WrapWidget::showContent(object_ptr<ContentWidget> content) { } void WrapWidget::finishShowContent() { + setupTopBarMenuToggle(); updateContentGeometry(); _content->setIsStackBottom(!hasStackHistory()); if (_topBar) { @@ -614,7 +599,12 @@ void WrapWidget::finishShowContent() { _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); _desiredBottomShadowVisibilities.fire( _content->desiredBottomShadowVisibility()); - _selectedLists.fire(_content->selectedListValue()); + if (auto selection = _content->selectedListValue()) { + _selectedLists.fire(std::move(selection)); + } else { + _selectedLists.fire(rpl::single( + SelectedItems(Storage::SharedMediaType::Photo))); + } _scrollTillBottomChanges.fire(_content->scrollTillBottomChanges()); _topShadow->raise(); _topShadow->finishAnimating(); @@ -888,8 +878,12 @@ void WrapWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) { if (hasStackHistory() || wrap() != Wrap::Layer) { checkBeforeClose([=] { _controller->showBackFromStack(); }); - return; + } else { + checkBeforeClose([=] { + _controller->parentController()->hideSpecialLayer(); + }); } + return; } SectionWidget::keyPressEvent(e); } @@ -939,13 +933,17 @@ object_ptr<Ui::RpWidget> WrapWidget::createTopBarSurrogate( void WrapWidget::updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll) { + int additionalScroll, + int maxVisibleHeight) { auto scrollChanged = (_additionalScroll != additionalScroll); auto geometryChanged = (geometry() != newGeometry); auto shrinkingContent = (additionalScroll < _additionalScroll); _additionalScroll = additionalScroll; + _maxVisibleHeight = maxVisibleHeight; _expanding = expanding; + _content->applyMaxVisibleHeight(maxVisibleHeight); + if (geometryChanged) { if (shrinkingContent) { setGeometry(newGeometry); diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index d16108114..db76d4b43 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -20,6 +20,7 @@ class PlainShadow; class PopupMenu; class IconButton; class RoundRect; +struct StringWithNumbers; } // namespace Ui namespace Window { @@ -61,11 +62,10 @@ struct SelectedItem { }; struct SelectedItems { - explicit SelectedItems(Storage::SharedMediaType type) - : type(type) { - } + SelectedItems() = default; + explicit SelectedItems(Storage::SharedMediaType type); - Storage::SharedMediaType type; + Fn<Ui::StringWithNumbers(int)> title; std::vector<SelectedItem> list; }; @@ -124,7 +124,8 @@ public: void updateGeometry( QRect newGeometry, bool expanding, - int additionalScroll); + int additionalScroll, + int maxVisibleHeight); [[nodiscard]] int scrollBottomSkip() const; [[nodiscard]] int scrollTillBottom(int forHeight) const; [[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const; @@ -171,6 +172,7 @@ private: not_null<ContentMemento*> memento, const Window::SectionShow ¶ms); void setupTop(); + void setupTopBarMenuToggle(); void createTopBar(); void highlightTopBar(); void setupShortcuts(); @@ -201,12 +203,12 @@ private: void addTopBarMenuButton(); void addProfileCallsButton(); void showTopBarMenu(bool check); - void deleteAllDownloads(); rpl::variable<Wrap> _wrap; std::unique_ptr<Controller> _controller; object_ptr<ContentWidget> _content = { nullptr }; int _additionalScroll = 0; + int _maxVisibleHeight = 0; bool _expanding = false; rpl::variable<bool> _grabbingForExpanding = false; object_ptr<TopBar> _topBar = { nullptr }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 6f62370b2..1734d0475 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "base/options.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "data/data_folder.h" @@ -77,6 +81,8 @@ namespace Info { namespace Profile { namespace { +constexpr auto kDay = Data::WorkingInterval::kDay; + base::options::toggle ShowPeerIdBelowAbout({ .id = kOptionShowPeerIdBelowAbout, .name = "Show Peer IDs in Profile", @@ -171,6 +177,439 @@ base::options::toggle ShowPeerIdBelowAbout({ }); } +[[nodiscard]] bool AreNonTrivialHours(const Data::WorkingHours &hours) { + if (!hours) { + return false; + } + const auto &intervals = hours.intervals.list; + for (auto i = 0; i != 7; ++i) { + const auto day = Data::WorkingInterval{ i * kDay, (i + 1) * kDay }; + for (const auto &interval : intervals) { + const auto intersection = interval.intersected(day); + if (intersection && intersection != day) { + return true; + } + } + } + return false; +} + +[[nodiscard]] TimeId OpensIn( + const Data::WorkingIntervals &intervals, + TimeId now) { + using namespace Data; + + while (now < 0) { + now += WorkingInterval::kWeek; + } + while (now > WorkingInterval::kWeek) { + now -= WorkingInterval::kWeek; + } + auto closest = WorkingInterval::kWeek; + for (const auto &interval : intervals.list) { + if (interval.start <= now && interval.end > now) { + return TimeId(0); + } else if (interval.start > now && interval.start - now < closest) { + closest = interval.start - now; + } else if (interval.start < now) { + const auto next = interval.start + WorkingInterval::kWeek - now; + if (next < closest) { + closest = next; + } + } + } + return closest; +} + +[[nodiscard]] rpl::producer<QString> OpensInText( + rpl::producer<TimeId> in, + rpl::producer<bool> hoursExpanded, + rpl::producer<QString> fallback) { + return rpl::combine( + std::move(in), + std::move(hoursExpanded), + std::move(fallback) + ) | rpl::map([](TimeId in, bool hoursExpanded, QString fallback) { + return (!in || hoursExpanded) + ? std::move(fallback) + : (in >= 86400) + ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400) + : (in >= 3600) + ? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, in / 3600) + : tr::lng_info_hours_opens_in_minutes( + tr::now, + lt_count, + std::max(in / 60, 1)); + }); +} + +[[nodiscard]] QString FormatDayTime(TimeId time) { + const auto wrap = [](TimeId value) { + const auto hours = value / 3600; + const auto minutes = (value % 3600) / 60; + return QString::number(hours).rightJustified(2, u'0') + + ':' + + QString::number(minutes).rightJustified(2, u'0'); + }; + return (time > kDay) + ? tr::lng_info_hours_next_day(tr::now, lt_time, wrap(time - kDay)) + : wrap(time == kDay ? 0 : time); +} + +[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) { + auto result = QStringList(); + result.reserve(data.list.size()); + for (const auto &interval : data.list) { + const auto start = FormatDayTime(interval.start); + const auto end = FormatDayTime(interval.end); + result.push_back(start + u" - "_q + end); + } + return result.join('\n'); +} + +[[nodiscard]] QString FormatDayHours( + const Data::WorkingHours &hours, + const Data::WorkingIntervals &mine, + bool my, + int day) { + using namespace Data; + + const auto local = ExtractDayIntervals(hours.intervals, day); + if (IsFullOpen(local)) { + return tr::lng_info_hours_open_full(tr::now); + } + const auto use = my ? ExtractDayIntervals(mine, day) : local; + if (!use) { + return tr::lng_info_hours_closed(tr::now); + } + return JoinIntervals(use); +} + +[[nodiscard]] Data::WorkingIntervals ShiftedIntervals( + Data::WorkingIntervals intervals, + int delta) { + auto &list = intervals.list; + if (!delta || list.empty()) { + return { std::move(list) }; + } + for (auto &interval : list) { + interval.start += delta; + interval.end += delta; + } + while (list.front().start < 0) { + constexpr auto kWeek = Data::WorkingInterval::kWeek; + const auto first = list.front(); + if (first.end > 0) { + list.push_back({ first.start + kWeek, kWeek }); + list.front().start = 0; + } else { + list.push_back(first.shifted(kWeek)); + list.erase(list.begin()); + } + } + return intervals.normalized(); +} + +[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateWorkingHours( + not_null<QWidget*> parent, + not_null<UserData*> user) { + using namespace Data; + + auto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>( + parent, + object_ptr<Ui::RoundButton>( + parent, + rpl::single(QString()), + st::infoHoursOuter), + st::infoProfileLabeledPadding - st::infoHoursOuterMargin); + const auto button = result->entity(); + const auto inner = Ui::CreateChild<Ui::VerticalLayout>(button); + button->widthValue() | rpl::start_with_next([=](int width) { + const auto margin = st::infoHoursOuterMargin; + inner->resizeToWidth(width - margin.left() - margin.right()); + inner->move(margin.left(), margin.top()); + }, inner->lifetime()); + inner->heightValue() | rpl::start_with_next([=](int height) { + const auto margin = st::infoHoursOuterMargin; + height += margin.top() + margin.bottom(); + button->resize(button->width(), height); + }, inner->lifetime()); + + const auto info = &user->owner().businessInfo(); + + struct State { + rpl::variable<WorkingHours> hours; + rpl::variable<TimeId> time; + rpl::variable<int> day; + rpl::variable<int> timezoneDelta; + + rpl::variable<WorkingIntervals> mine; + rpl::variable<WorkingIntervals> mineByDays; + rpl::variable<TimeId> opensIn; + rpl::variable<bool> opened; + rpl::variable<bool> expanded; + rpl::variable<bool> nonTrivial; + rpl::variable<bool> myTimezone; + + rpl::event_stream<> recounts; + }; + const auto state = inner->lifetime().make_state<State>(); + + auto recounts = state->recounts.events_starting_with_copy(rpl::empty); + const auto recount = [=] { + state->recounts.fire({}); + }; + + state->hours = user->session().changes().peerFlagsValue( + user, + PeerUpdate::Flag::BusinessDetails + ) | rpl::map([=] { + return user->businessDetails().hours; + }); + state->nonTrivial = state->hours.value() | rpl::map(AreNonTrivialHours); + + const auto seconds = QTime::currentTime().msecsSinceStartOfDay() / 1000; + const auto inMinute = seconds % 60; + const auto firstTick = inMinute ? (61 - inMinute) : 1; + state->time = rpl::single(rpl::empty) | rpl::then( + base::timer_once(firstTick * crl::time(1000)) + ) | rpl::then( + base::timer_each(60 * crl::time(1000)) + ) | rpl::map([] { + const auto local = QDateTime::currentDateTime(); + const auto day = local.date().dayOfWeek() - 1; + const auto seconds = local.time().msecsSinceStartOfDay() / 1000; + return day * kDay + seconds; + }); + + state->day = state->time.value() | rpl::map([](TimeId time) { + return time / kDay; + }); + state->timezoneDelta = rpl::combine( + state->hours.value(), + info->timezonesValue() + ) | rpl::filter([]( + const WorkingHours &hours, + const Timezones &timezones) { + return ranges::contains( + timezones.list, + hours.timezoneId, + &Timezone::id); + }) | rpl::map([](WorkingHours &&hours, const Timezones &timezones) { + const auto &list = timezones.list; + const auto closest = FindClosestTimezoneId(list); + const auto i = ranges::find(list, closest, &Timezone::id); + const auto j = ranges::find(list, hours.timezoneId, &Timezone::id); + Assert(i != end(list)); + Assert(j != end(list)); + return i->utcOffset - j->utcOffset; + }); + + state->mine = rpl::combine( + state->hours.value(), + state->timezoneDelta.value() + ) | rpl::map([](WorkingHours &&hours, int delta) { + return ShiftedIntervals(hours.intervals, delta); + }); + + state->opensIn = rpl::combine( + state->mine.value(), + state->time.value() + ) | rpl::map([](const WorkingIntervals &mine, TimeId time) { + return OpensIn(mine, time); + }); + state->opened = state->opensIn.value() | rpl::map(rpl::mappers::_1 == 0); + + state->mineByDays = rpl::combine( + state->hours.value(), + state->timezoneDelta.value() + ) | rpl::map([](WorkingHours &&hours, int delta) { + auto full = std::array<bool, 7>(); + auto withoutFullDays = hours.intervals; + for (auto i = 0; i != 7; ++i) { + if (IsFullOpen(ExtractDayIntervals(hours.intervals, i))) { + full[i] = true; + withoutFullDays = ReplaceDayIntervals( + withoutFullDays, + i, + Data::WorkingIntervals()); + } + } + auto result = ShiftedIntervals(withoutFullDays, delta); + for (auto i = 0; i != 7; ++i) { + if (full[i]) { + result = ReplaceDayIntervals( + result, + i, + Data::WorkingIntervals{ { { 0, kDay } } }); + } + } + return result; + }); + + const auto dayHoursText = [=](int day) { + return rpl::combine( + state->hours.value(), + state->mineByDays.value(), + state->myTimezone.value() + ) | rpl::map([=]( + const WorkingHours &hours, + const WorkingIntervals &mine, + bool my) { + return FormatDayHours(hours, mine, my, day); + }); + }; + const auto dayHoursTextValue = [=](rpl::producer<int> day) { + return std::move(day) + | rpl::map(dayHoursText) + | rpl::flatten_latest(); + }; + + const auto openedWrap = inner->add(object_ptr<Ui::RpWidget>(inner)); + const auto opened = Ui::CreateChild<Ui::FlatLabel>( + openedWrap, + rpl::conditional( + state->opened.value(), + tr::lng_info_work_open(), + tr::lng_info_work_closed() + ) | rpl::after_next(recount), + st::infoHoursState); + opened->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto timing = Ui::CreateChild<Ui::FlatLabel>( + openedWrap, + OpensInText( + state->opensIn.value(), + state->expanded.value(), + dayHoursTextValue(state->day.value()) + ) | rpl::after_next(recount), + st::infoHoursValue); + timing->setAttribute(Qt::WA_TransparentForMouseEvents); + state->opened.value() | rpl::start_with_next([=](bool value) { + opened->setTextColorOverride(value + ? st::boxTextFgGood->c + : st::boxTextFgError->c); + }, opened->lifetime()); + + rpl::combine( + openedWrap->widthValue(), + opened->heightValue(), + timing->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + opened->moveToLeft(0, 0, width); + timing->moveToRight(0, 0, width); + + const auto margins = opened->getMargins(); + const auto added = margins.top() + margins.bottom(); + openedWrap->resize(width, std::max(h1, size.height()) - added); + }, openedWrap->lifetime()); + + const auto labelWrap = inner->add(object_ptr<Ui::RpWidget>(inner)); + const auto label = Ui::CreateChild<Ui::FlatLabel>( + labelWrap, + tr::lng_info_hours_label(), + st::infoLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto link = Ui::CreateChild<Ui::LinkButton>( + labelWrap, + QString()); + rpl::combine( + state->nonTrivial.value(), + state->hours.value(), + state->mine.value(), + state->myTimezone.value() + ) | rpl::map([=]( + bool complex, + const WorkingHours &hours, + const WorkingIntervals &mine, + bool my) { + return (!complex || hours.intervals == mine) + ? rpl::single(QString()) + : my + ? tr::lng_info_hours_my_time() + : tr::lng_info_hours_local_time(); + }) | rpl::flatten_latest( + ) | rpl::start_with_next([=](const QString &text) { + link->setText(text); + }, link->lifetime()); + link->setClickedCallback([=] { + state->myTimezone = !state->myTimezone.current(); + state->expanded = true; + }); + + rpl::combine( + labelWrap->widthValue(), + label->heightValue(), + link->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + label->moveToLeft(0, 0, width); + link->moveToRight(0, 0, width); + + const auto margins = label->getMargins(); + const auto added = margins.top() + margins.bottom(); + labelWrap->resize(width, std::max(h1, size.height()) - added); + }, labelWrap->lifetime()); + + const auto other = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + other->toggleOn(state->expanded.value(), anim::type::normal); + other->finishAnimating(); + const auto days = other->entity(); + + for (auto i = 1; i != 7; ++i) { + const auto dayWrap = days->add( + object_ptr<Ui::RpWidget>(other), + QMargins(0, st::infoHoursDaySkip, 0, 0)); + auto label = state->day.value() | rpl::map([=](int day) { + switch ((day + i) % 7) { + case 0: return tr::lng_hours_monday(); + case 1: return tr::lng_hours_tuesday(); + case 2: return tr::lng_hours_wednesday(); + case 3: return tr::lng_hours_thursday(); + case 4: return tr::lng_hours_friday(); + case 5: return tr::lng_hours_saturday(); + case 6: return tr::lng_hours_sunday(); + } + Unexpected("Index in working hours."); + }) | rpl::flatten_latest(); + const auto dayLabel = Ui::CreateChild<Ui::FlatLabel>( + dayWrap, + std::move(label), + st::infoHoursDayLabel); + dayLabel->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto dayHours = Ui::CreateChild<Ui::FlatLabel>( + dayWrap, + dayHoursTextValue(state->day.value() + | rpl::map((rpl::mappers::_1 + i) % 7)), + st::infoHoursValue); + dayHours->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + dayWrap->widthValue(), + dayLabel->heightValue(), + dayHours->sizeValue() + ) | rpl::start_with_next([=](int width, int h1, QSize size) { + dayLabel->moveToLeft(0, 0, width); + dayHours->moveToRight(0, 0, width); + + const auto margins = dayLabel->getMargins(); + const auto added = margins.top() + margins.bottom(); + dayWrap->resize(width, std::max(h1, size.height()) - added); + }, dayWrap->lifetime()); + } + + button->setClickedCallback([=] { + state->expanded = !state->expanded.current(); + }); + + result->toggleOn(state->hours.value( + ) | rpl::map([](const WorkingHours &data) { + return bool(data); + })); + + return result; +} + template <typename Text, typename ToggleOn, typename Callback> auto AddActionButton( not_null<Ui::VerticalLayout*> parent, @@ -577,6 +1016,28 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { } return false; }); + } else { + tracker.track(result->add(CreateWorkingHours(result, user))); + + auto locationText = user->session().changes().peerFlagsValue( + user, + Data::PeerUpdate::Flag::BusinessDetails + ) | rpl::map([=] { + const auto &details = user->businessDetails(); + if (!details.location) { + return TextWithEntities(); + } else if (!details.location.point) { + return TextWithEntities{ details.location.address }; + } + return Ui::Text::Link( + TextUtilities::SingleLine(details.location.address), + LocationClickHandler::Url(*details.location.point)); + }); + addInfoOneLine( + tr::lng_info_location_label(), + std::move(locationText), + QString() + ).text->setLinksTrusted(); } if (settings->showPeerId != 0) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index 8881dbc99..c4cce455f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -159,6 +159,10 @@ void EmojiStatusPanel::show(Descriptor &&descriptor) { _panel->toggleAnimated(); } +bool EmojiStatusPanel::hasFocus() const { + return _panel && Ui::InFocusChain(_panel.get()); +} + void EmojiStatusPanel::repaint() { _panel->selector()->update(); } @@ -281,7 +285,7 @@ bool EmojiStatusPanel::filter( if (_chooseFilter) { return _chooseFilter(chosenId); } else if (chosenId && !controller->session().premium()) { - ShowPremiumPreviewBox(controller, PremiumPreview::EmojiStatus); + ShowPremiumPreviewBox(controller, PremiumFeature::EmojiStatus); return false; } return true; diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h index a373c904c..9777cfcfa 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h @@ -46,6 +46,7 @@ public: not_null<Window::SessionController*> controller, not_null<QWidget*> button, Data::CustomEmojiSizeTag animationSizeTag = {}); + [[nodiscard]] bool hasFocus() const; struct Descriptor { not_null<Window::SessionController*> controller; diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp index cd6d6bd03..bbc0ca717 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.cpp +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.cpp @@ -44,7 +44,14 @@ Widget::Widget( , _self(controller->key().settingsSelf()) , _type(controller->section().settingsType()) , _inner([&] { - auto inner = _type->create(this, controller->parentController()); + auto inner = _type->create( + this, + controller->parentController(), + scroll(), + controller->wrapValue( + ) | rpl::map([](Wrap wrap) { return (wrap == Wrap::Layer) + ? ::Settings::Container::Layer + : ::Settings::Container::Section; })); if (inner->hasFlexibleTopBar()) { auto filler = setInnerWidget(object_ptr<Ui::RpWidget>(this)); filler->resize(1, 1); @@ -114,17 +121,17 @@ Widget::Widget( } if (_pinnedToBottom) { - const auto processHeight = [=](int bottomHeight, int height) { - setScrollBottomSkip(bottomHeight); + const auto processHeight = [=] { + setScrollBottomSkip(_pinnedToBottom->height()); _pinnedToBottom->moveToLeft( _pinnedToBottom->x(), - height - bottomHeight); + height() - _pinnedToBottom->height()); }; _inner->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { _pinnedToBottom->resizeToWidth(s.width()); - processHeight(_pinnedToBottom->height(), height()); + //processHeight(); }, _pinnedToBottom->lifetime()); rpl::combine( @@ -225,10 +232,24 @@ rpl::producer<bool> Widget::desiredShadowVisibility() const { : rpl::single(true); } +bool Widget::closeByOutsideClick() const { + return _inner->closeByOutsideClick();; +} + +void Widget::checkBeforeClose(Fn<void()> close) { + _inner->checkBeforeClose(std::move(close)); +} + rpl::producer<QString> Widget::title() { return _inner->title(); } +void Widget::paintEvent(QPaintEvent *e) { + if (!_inner->paintOuter(this, maxVisibleHeight(), e->rect())) { + ContentWidget::paintEvent(e); + } +} + std::shared_ptr<ContentMemento> Widget::doCreateMemento() { auto result = std::make_shared<Memento>(self(), _type); saveState(result.get()); @@ -239,6 +260,18 @@ void Widget::enableBackButton() { _flexibleScroll.backButtonEnables.fire({}); } +rpl::producer<SelectedItems> Widget::selectedListValue() const { + return _inner->selectedListValue(); +} + +void Widget::selectionAction(SelectionAction action) { + _inner->selectionAction(action); +} + +void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + _inner->fillTopBarMenu(addAction); +} + void Widget::saveState(not_null<Memento*> memento) { memento->setScrollTop(scrollTopSave()); } diff --git a/Telegram/SourceFiles/info/settings/info_settings_widget.h b/Telegram/SourceFiles/info/settings/info_settings_widget.h index e6715d68c..23b4e7c93 100644 --- a/Telegram/SourceFiles/info/settings/info_settings_widget.h +++ b/Telegram/SourceFiles/info/settings/info_settings_widget.h @@ -76,14 +76,22 @@ public: rpl::producer<bool> desiredShadowVisibility() const override; + bool closeByOutsideClick() const override; + void checkBeforeClose(Fn<void()> close) override; rpl::producer<QString> title() override; void enableBackButton() override; + rpl::producer<SelectedItems> selectedListValue() const override; + void selectionAction(SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; + private: void saveState(not_null<Memento*> memento); void restoreState(not_null<Memento*> memento); + void paintEvent(QPaintEvent *e) override; + std::shared_ptr<ContentMemento> doCreateMemento() override; not_null<UserData*> _self; diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 9aba7d65b..056aa224e 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -566,9 +566,16 @@ bool AttachWebView::botHandleLocalUri(QString uri, bool keepOpen) { if (!keepOpen) { botClose(); } - crl::on_main([=, shownUrl = _lastShownUrl] { + crl::on_main([=, shownUrl = _lastShownUrl, bot = _bot] { + if (bot->session().windows().empty()) { + Core::App().domain().activate(&bot->session().account()); + } + const auto window = !bot->session().windows().empty() + ? bot->session().windows().front().get() + : nullptr; const auto variant = QVariant::fromValue(ClickHandlerContext{ .attachBotWebviewUrl = shownUrl, + .sessionWindow = window, }); UrlClickHandler::Open(local, variant); }); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 339b40eaf..5931c22c4 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_document_media.h" #include "history/history.h" +#include "history/history_item.h" #include "history/history_item_reply_markup.h" #include "inline_bots/inline_bot_layout_item.h" #include "inline_bots/inline_bot_send_data.h" @@ -376,30 +377,15 @@ bool Result::hasThumbDisplay() const { void Result::addToHistory( not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor) const { - flags |= MessageFlag::FromInlineBot; - - auto markup = _replyMarkup ? *_replyMarkup : HistoryMessageMarkupData(); - if (!markup.isNull()) { - flags |= MessageFlag::HasReplyMarkup; + HistoryItemCommonFields &&fields) const { + fields.flags |= MessageFlag::FromInlineBot; + if (_replyMarkup) { + fields.markup = *_replyMarkup; + if (!fields.markup.isNull()) { + fields.flags |= MessageFlag::HasReplyMarkup; + } } - sendData->addToHistory( - this, - history, - flags, - msgId, - fromId, - date, - viaBotId, - replyTo, - postAuthor, - std::move(markup)); + sendData->addToHistory(this, history, std::move(fields)); } QString Result::getErrorOnSend(not_null<History*> history) const { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index 296deb613..cc7e090ee 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -16,6 +16,7 @@ class FileLoader; class History; class UserData; struct HistoryMessageMarkupData; +struct HistoryItemCommonFields; namespace Data { class LocationPoint; @@ -64,13 +65,7 @@ public: void addToHistory( not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor) const; + HistoryItemCommonFields &&fields) const; QString getErrorOnSend(not_null<History*> history) const; // interface for Layout:: usage diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp index a8135979b..0b8e9d8b7 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.cpp @@ -31,29 +31,15 @@ QString SendData::getLayoutDescription(const Result *owner) const { void SendDataCommon::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { - auto fields = getSentMessageFields(); - if (replyTo) { - flags |= MessageFlag::HasReplyInfo; + HistoryItemCommonFields &&fields) const { + auto distinct = getSentMessageFields(); + if (fields.replyTo) { + fields.flags |= MessageFlag::HasReplyInfo; } history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, - std::move(fields.text), - std::move(fields.media), - std::move(markup)); + std::move(fields), + std::move(distinct.text), + std::move(distinct.media)); } QString SendDataCommon::getErrorOnSend( @@ -113,25 +99,11 @@ QString SendContact::getLayoutDescription(const Result *owner) const { void SendPhoto::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { + HistoryItemCommonFields &&fields) const { history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, + std::move(fields), _photo, - { _message, _entities }, - std::move(markup)); + { _message, _entities }); } QString SendPhoto::getErrorOnSend( @@ -144,25 +116,11 @@ QString SendPhoto::getErrorOnSend( void SendFile::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { + HistoryItemCommonFields &&fields) const { history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, + std::move(fields), _document, - { _message, _entities }, - std::move(markup)); + { _message, _entities }); } QString SendFile::getErrorOnSend( @@ -175,24 +133,8 @@ QString SendFile::getErrorOnSend( void SendGame::addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const { - history->addNewLocalMessage( - msgId, - flags, - viaBotId, - replyTo, - date, - fromId, - postAuthor, - _game, - std::move(markup)); + HistoryItemCommonFields &&fields) const { + history->addNewLocalMessage(std::move(fields), _game); } QString SendGame::getErrorOnSend( diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h index 84c25624f..502cc006e 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_send_data.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_location_manager.h" -struct HistoryMessageMarkupData; +struct HistoryItemCommonFields; namespace Main { class Session; @@ -43,14 +43,7 @@ public: virtual void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const = 0; + HistoryItemCommonFields &&fields) const = 0; virtual QString getErrorOnSend( const Result *owner, not_null<History*> history) const = 0; @@ -85,14 +78,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -253,14 +239,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -294,14 +273,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, @@ -329,14 +301,7 @@ public: void addToHistory( const Result *owner, not_null<History*> history, - MessageFlags flags, - MsgId msgId, - PeerId fromId, - TimeId date, - UserId viaBotId, - FullReplyTo replyTo, - const QString &postAuthor, - HistoryMessageMarkupData &&markup) const override; + HistoryItemCommonFields &&fields) const override; QString getErrorOnSend( const Result *owner, diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 7ac420956..5e8261674 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -32,13 +32,18 @@ namespace Intro { namespace details { namespace { -bool AllowPhoneAttempt(const QString &phone) { +[[nodiscard]] bool AllowPhoneAttempt(const QString &phone) { const auto digits = ranges::count_if( phone, [](QChar ch) { return ch.isNumber(); }); return (digits > 1); } +[[nodiscard]] QString DigitsOnly(QString value) { + static const auto RegExp = QRegularExpression("[^0-9]"); + return value.replace(RegExp, QString()); +} + } // namespace PhoneWidget::PhoneWidget( @@ -168,16 +173,12 @@ void PhoneWidget::submit() { cancelNearestDcRequest(); // Check if such account is authorized already. - const auto digitsOnly = [](QString value) { - static const auto RegExp = QRegularExpression("[^0-9]"); - return value.replace(RegExp, QString()); - }; - const auto phoneDigits = digitsOnly(phone); + const auto phoneDigits = DigitsOnly(phone); for (const auto &[index, existing] : Core::App().domain().accounts()) { const auto raw = existing.get(); if (const auto session = raw->maybeSession()) { if (raw->mtp().environment() == account().mtp().environment() - && digitsOnly(session->user()->phone()) == phoneDigits) { + && DigitsOnly(session->user()->phone()) == phoneDigits) { crl::on_main(raw, [=] { Core::App().domain().activate(raw); }); @@ -231,7 +232,7 @@ void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) { result.match([&](const MTPDauth_sentCode &data) { fillSentCodeData(data); - getData()->phone = _sentPhone; + getData()->phone = DigitsOnly(_sentPhone); getData()->phoneHash = qba(data.vphone_code_hash()); const auto next = data.vnext_type(); if (next && next->type() == mtpc_auth_codeTypeCall) { diff --git a/Telegram/SourceFiles/intro/intro_step.cpp b/Telegram/SourceFiles/intro/intro_step.cpp index 0e3d3f4bb..2319b6aa3 100644 --- a/Telegram/SourceFiles/intro/intro_step.cpp +++ b/Telegram/SourceFiles/intro/intro_step.cpp @@ -197,8 +197,8 @@ void Step::finish(const MTPUser &user, QImage &&photo) { } api().request(MTPmessages_GetDialogFilters( - )).done([=](const MTPVector<MTPDialogFilter> &result) { - createSession(user, photo, result.v); + )).done([=](const MTPmessages_DialogFilters &result) { + createSession(user, photo, result.data().vfilters().v); }).fail([=] { createSession(user, photo, QVector<MTPDialogFilter>()); }).send(); diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index ca5298972..d0ebc4f36 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -83,7 +83,8 @@ Session::Session( not_null<Account*> account, const MTPUser &user, std::unique_ptr<SessionSettings> settings) -: _account(account) +: _userId(user.c_user().vid()) +, _account(account) , _settings(std::move(settings)) , _changes(std::make_unique<Data::Changes>(this)) , _api(std::make_unique<ApiWrap>(this)) @@ -93,7 +94,6 @@ Session::Session( , _uploader(std::make_unique<Storage::Uploader>(_api.get())) , _storage(std::make_unique<Storage::Facade>()) , _data(std::make_unique<Data::Session>(this)) -, _userId(user.c_user().vid()) , _user(_data->processUser(user)) , _emojiStickersPack(std::make_unique<Stickers::EmojiPack>(this)) , _diceStickersPacks(std::make_unique<Stickers::DicePacks>(this)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index cca061d08..635f453d5 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -199,6 +199,7 @@ private: void parseColorIndices(const MTPDhelp_peerColors &data); + const UserId _userId; const not_null<Account*> _account; const std::unique_ptr<SessionSettings> _settings; @@ -212,7 +213,6 @@ private: // _data depends on _downloader / _uploader. const std::unique_ptr<Data::Session> _data; - const UserId _userId; const not_null<UserData*> _user; // _emojiStickersPack depends on _data. diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index f401f6b37..da07a1516 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -131,23 +131,12 @@ private: not_null<History*> history) { Expects(history->peer->isUser()); - const auto flags = MessageFlag::FakeHistoryItem - | MessageFlag::HasFromId; - 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(), - peerToUser(history->peer->id), - QString(), - TextWithEntities(), - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId, + .from = history->peer->id, + .date = base::unixtime::now(), + }, TextWithEntities(), MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index deb291cc1..76e353c00 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "boxes/share_box.h" #include "chat_helpers/compose/compose_show.h" +#include "data/business/data_shortcut_messages.h" #include "data/data_chat_participant_status.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" @@ -119,6 +120,7 @@ namespace Media::Stories { message.action.clearDraft = false; api->sendMessage(std::move(message)); } + const auto session = &thread->session(); const auto threadPeer = thread->peer(); const auto threadHistory = thread->owningHistory(); const auto randomId = base::RandomValue<uint64>(); @@ -132,6 +134,12 @@ namespace Media::Stories { if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } + if (options.scheduled) { + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + if (options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } const auto done = [=] { if (!--state->requests) { if (show->valid()) { @@ -154,7 +162,8 @@ namespace Media::Stories { MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), MTP_int(action.options.scheduled), - MTP_inputPeerEmpty() + MTP_inputPeerEmpty(), + Data::ShortcutIdToMTP(session, action.options.shortcutId) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp index 404ff1aea..80d4f20a7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -352,7 +352,7 @@ struct Feature { data->requested = false; const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto window = show->resolveWindow(usage)) { - ShowPremiumPreviewBox(window, PremiumPreview::Stories); + ShowPremiumPreviewBox(window, PremiumFeature::Stories); window->window().activate(); } } else if (now.mode.cooldownTill > now.now) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 5a0165630..858559ad2 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1258,7 +1258,7 @@ void OverlayWidget::showPremiumDownloadPromo() { const auto filter = [=](const auto &...) { const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (const auto window = uiShow()->resolveWindow(usage)) { - ShowPremiumPreviewBox(window, PremiumPreview::Stories); + ShowPremiumPreviewBox(window, PremiumFeature::Stories); window->window().activate(); } return false; diff --git a/Telegram/SourceFiles/menu/menu_mute.cpp b/Telegram/SourceFiles/menu/menu_mute.cpp index dd58f6fc0..661ef0672 100644 --- a/Telegram/SourceFiles/menu/menu_mute.cpp +++ b/Telegram/SourceFiles/menu/menu_mute.cpp @@ -111,6 +111,7 @@ MuteItem::MuteItem( isMuted ? 1. : 0., st::defaultPopupMenu.showDuration); }, lifetime()); + _animation.stop(); setClickedCallback([=] { descriptor.updateMutePeriod(_isMuted ? 0 : kMuteForeverValue); @@ -123,7 +124,7 @@ void MuteItem::paintEvent(QPaintEvent *e) { const auto progress = _animation.value(_isMuted ? 1. : 0.); const auto color = anim::color( st::menuIconAttentionColor, - st::settingsIconBg2, + st::boxTextFgGood, progress); p.setPen(color); diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index b058b9bb8..ea0a53f30 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/121.0.6167.85 Safari/537.36"); + "Chrome/122.0.0.0 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 1f17c1a7d..af421910b 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -114,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#1e4c8a69 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int = Message; +message#a66c7efc flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -227,7 +227,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#b9b12c6c flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories = UserFull; +userFull#22ff3e85 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector<PremiumGiftOption> wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -402,6 +402,12 @@ updateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update; updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update; updateSavedReactionTags#39c67432 = Update; +updateSmsJob#f16269d4 job_id:string = Update; +updateQuickReplies#f9470ab2 quick_replies:Vector<QuickReply> = Update; +updateNewQuickReply#f53da717 quick_reply:QuickReply = Update; +updateDeleteQuickReply#53e6f1ec shortcut_id:int = Update; +updateQuickReplyMessage#3e050d0f message:Message = Update; +updateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector<int> = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1228,9 +1234,9 @@ bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector<BankCardOpenUrl> = payments.BankCardData; -dialogFilter#7438f7e8 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter; +dialogFilter#5fb5523b flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter; dialogFilterDefault#363293ae = DialogFilter; -dialogFilterChatlist#d64a04a8 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> = DialogFilter; +dialogFilterChatlist#9fe28ea4 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; @@ -1653,6 +1659,53 @@ messages.savedReactionTags#3259950a tags:Vector<SavedReactionTag> hash:long = me outboxReadDate#3bb842ac date:int = OutboxReadDate; +smsjobs.eligibleToJoin#dc8b44cf terms_url:string monthly_sent_sms:int = smsjobs.EligibilityToJoin; + +smsjobs.status#2aee9191 flags:# allow_international:flags.0?true recent_sent:int recent_since:int recent_remains:int total_sent:int total_since:int last_gift_slug:flags.1?string terms_url:string = smsjobs.Status; + +smsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob; + +businessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen; + +businessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector<BusinessWeeklyOpen> = BusinessWorkHours; + +businessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation; + +inputBusinessRecipients#6f8b32aa flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<InputUser> = InputBusinessRecipients; + +businessRecipients#21108ff7 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector<long> = BusinessRecipients; + +businessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule; +businessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule; +businessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule; + +inputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRecipients no_activity_days:int = InputBusinessGreetingMessage; + +businessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage; + +inputBusinessAwayMessage#832175e0 flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage; + +businessAwayMessage#ef156a5c flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage; + +timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; + +help.timezonesListNotModified#970708cc = help.TimezonesList; +help.timezonesList#7b74ed71 timezones:Vector<Timezone> hash:int = help.TimezonesList; + +quickReply#697102b shortcut_id:int shortcut:string top_message:int count:int = QuickReply; + +inputQuickReplyShortcut#24596d41 shortcut:string = InputQuickReplyShortcut; +inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; + +messages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies; +messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; + +connectedBot#e7e999e7 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessRecipients = ConnectedBot; + +account.connectedBots#17d7f87b connected_bots:Vector<ConnectedBot> users:Vector<User> = account.ConnectedBots; + +messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector<DialogFilter> = messages.DialogFilters; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1778,6 +1831,12 @@ account.updateColor#7cefa15d flags:# for_profile:flags.1?true color:flags.2?int account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; +account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool; +account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; +account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; +account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; +account.updateConnectedBot#9c2d527d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessRecipients = Updates; +account.getConnectedBots#4ea4c80f = account.ConnectedBots; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -1819,9 +1878,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#280d096f flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.sendMedia#72ccc23d flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.forwardMessages#c661bbc4 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMessage#dff8042c flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMedia#7bd66041 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector<int> reason:ReportReason message:string = Bool; @@ -1864,9 +1923,9 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#f7bc68ba flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendInlineBotResult#3ebee86a flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#48f71778 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates; +messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool; messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; @@ -1899,7 +1958,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#456e8987 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMultiMedia#c964709 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector<MessageRange>; @@ -1926,7 +1985,7 @@ messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates; messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates; messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList; messages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector<InputStickerSet> = Bool; -messages.getDialogFilters#f19ed96d = Vector<DialogFilter>; +messages.getDialogFilters#efd48c89 = messages.DialogFilters; messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool; @@ -2008,6 +2067,15 @@ messages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool; messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions; messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; +messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; +messages.reorderQuickReplies#60331907 order:Vector<int> = Bool; +messages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool; +messages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool; +messages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool; +messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages; +messages.sendQuickReplyMessages#33153ad4 peer:InputPeer shortcut_id:int = Updates; +messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates; +messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2052,6 +2120,7 @@ help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; help.getPremiumPromo#b81b93d4 = help.PremiumPromo; help.getPeerColors#da80f42f hash:int = help.PeerColors; help.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors; +help.getTimezonesList#49b30240 hash:int = help.TimezonesList; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages; @@ -2252,4 +2321,12 @@ premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = p premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList; -// LAYER 174 +smsjobs.isEligibleToJoin#edc39d0 = smsjobs.EligibilityToJoin; +smsjobs.join#a74ece2d = Bool; +smsjobs.leave#9898ad73 = Bool; +smsjobs.updateSettings#93fa0bf flags:# allow_international:flags.0?true = Bool; +smsjobs.getStatus#10a698e8 = smsjobs.Status; +smsjobs.getSmsJob#778d902f job_id:string = SmsJob; +smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; + +// LAYER 176 diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index c3715e977..bac92c2c8 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -140,7 +140,7 @@ void RequestTypeBox::setupControls( _height = y; _submit = [=] { - const auto value = group->hasValue() ? group->value() : -1; + const auto value = group->hasValue() ? group->current() : -1; if (value >= 0) { submit(value); } diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index 1087e09fb..fef646b20 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -180,7 +180,7 @@ gi::ref_ptr<Application> MakeApplication() { return result; } -class LinuxIntegration final : public Integration { +class LinuxIntegration final : public Integration, public base::has_weak_ptr { public: LinuxIntegration(); @@ -200,13 +200,6 @@ private: LinuxIntegration::LinuxIntegration() : _application(MakeApplication()) -, _inhibitProxy( - XdpInhibit::InhibitProxy::new_for_bus_sync( - Gio::BusType::SESSION_, - Gio::DBusProxyFlags::DO_NOT_AUTO_START_AT_CONSTRUCTION_, - base::Platform::XDP::kService, - base::Platform::XDP::kObjectPath, - nullptr)) , _darkModeWatcher( "org.freedesktop.appearance", "color-scheme", @@ -230,7 +223,18 @@ LinuxIntegration::LinuxIntegration() } void LinuxIntegration::init() { - initInhibit(); + XdpInhibit::InhibitProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) { + _inhibitProxy = XdpInhibit::InhibitProxy::new_for_bus_finish( + res, + nullptr); + + initInhibit(); + })); } void LinuxIntegration::initInhibit() { @@ -248,7 +252,8 @@ void LinuxIntegration::initInhibit() { const auto sessionHandleToken = "tdesktop" + std::to_string(base::RandomValue<uint>()); - const auto sessionHandle = "/org/freedesktop/portal/desktop/session/" + const auto sessionHandle = base::Platform::XDP::kObjectPath + + std::string("/session/") + uniqueName + '/' + sessionHandleToken; diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp index e75bcb421..bd82cb17f 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp @@ -13,130 +13,115 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include <fcntl.h> -#include <glibmm.h> -#include <giomm.h> +#include <xdpopenuri/xdpopenuri.hpp> +#include <xdprequest/xdprequest.hpp> namespace Platform { namespace File { namespace internal { namespace { -constexpr auto kXDPOpenURIInterface = "org.freedesktop.portal.OpenURI"; -constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"; +using namespace gi::repository; using base::Platform::XdgActivationToken; } // namespace bool ShowXDPOpenWithDialog(const QString &filepath) { - try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); + auto proxy = XdpOpenURI::OpenURIProxy::new_for_bus_sync( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + nullptr); - const auto version = connection->call_sync( - base::Platform::XDP::kObjectPath, - kPropertiesInterface, - "Get", - Glib::create_variant(std::tuple{ - Glib::ustring(kXDPOpenURIInterface), - Glib::ustring("version"), - }), - base::Platform::XDP::kService - ).get_child(0).get_dynamic<Glib::Variant<uint>>().get(); - - if (version < 3) { - return false; - } - - const auto filepathUtf8 = filepath.toUtf8(); - - const auto fd = open( - filepathUtf8.constData(), - O_RDONLY); - - if (fd == -1) { - return false; - } - - const auto fdGuard = gsl::finally([&] { ::close(fd); }); - - const auto handleToken = Glib::ustring("tdesktop") - + std::to_string(base::RandomValue<uint>()); - - auto uniqueName = connection->get_unique_name(); - uniqueName.erase(0, 1); - uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - - const auto requestPath = Glib::ustring( - "/org/freedesktop/portal/desktop/request/") - + uniqueName - + '/' - + handleToken; - - const auto loop = Glib::MainLoop::create(); - - const auto signalId = connection->signal_subscribe( - [&]( - const Glib::RefPtr<Gio::DBus::Connection> &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { - loop->quit(); - }, - base::Platform::XDP::kService, - base::Platform::XDP::kRequestInterface, - "Response", - requestPath); - - const auto signalGuard = gsl::finally([&] { - if (signalId != 0) { - connection->signal_unsubscribe(signalId); - } - }); - - auto outFdList = Glib::RefPtr<Gio::UnixFDList>(); - - connection->call_sync( - base::Platform::XDP::kObjectPath, - kXDPOpenURIInterface, - "OpenFile", - Glib::create_variant(std::tuple{ - base::Platform::XDP::ParentWindowID(), - Glib::DBusHandle(), - std::map<Glib::ustring, Glib::VariantBase>{ - { - "handle_token", - Glib::create_variant(handleToken) - }, - { - "activation_token", - Glib::create_variant( - Glib::ustring(XdgActivationToken().toStdString())) - }, - { - "ask", - Glib::create_variant(true) - }, - }, - }), - Gio::UnixFDList::create(std::vector<int>{ fd }), - outFdList, - base::Platform::XDP::kService); - - if (signalId != 0) { - QWidget window; - window.setAttribute(Qt::WA_DontShowOnScreen); - window.setWindowModality(Qt::ApplicationModal); - window.show(); - loop->run(); - } - - return true; - } catch (...) { + if (!proxy) { + return false; } - return false; + auto interface = XdpOpenURI::OpenURI(proxy); + if (interface.get_version() < 3) { + return false; + } + + const auto fd = open( + QFile::encodeName(filepath).constData(), + O_RDONLY); + + if (fd == -1) { + return false; + } + + const auto fdGuard = gsl::finally([&] { close(fd); }); + + const auto handleToken = "tdesktop" + + std::to_string(base::RandomValue<uint>()); + + std::string uniqueName = proxy.get_connection().get_unique_name(); + uniqueName.erase(0, 1); + uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); + + auto request = XdpRequest::Request( + XdpRequest::RequestProxy::new_sync( + proxy.get_connection(), + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath + + std::string("/request/") + + uniqueName + + '/' + + handleToken, + nullptr, + nullptr)); + + if (!request) { + return false; + } + + auto loop = GLib::MainLoop::new_(); + + const auto signalId = request.signal_response().connect([=]( + XdpRequest::Request, + guint, + GLib::Variant) mutable { + loop.quit(); + }); + + const auto signalGuard = gsl::finally([&] { + request.disconnect(signalId); + }); + + auto result = interface.call_open_file_sync( + std::string(base::Platform::XDP::ParentWindowID()), + GLib::Variant::new_handle(0), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(handleToken))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("activation_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string( + XdgActivationToken().toStdString()))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("ask"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(true))), + }), + Gio::UnixFDList::new_from_array((std::array{ fd }).data(), 1), + nullptr); + + if (!result) { + return false; + } + + QWidget window; + window.setAttribute(Qt::WA_DontShowOnScreen); + window.setWindowModality(Qt::ApplicationModal); + window.show(); + loop.run(); + + return true; } } // namespace internal diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index cb768ff33..ae7fb1e99 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -43,8 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <QtWidgets/QLineEdit> #include <QtWidgets/QTextEdit> -#include <glibmm.h> -#include <giomm.h> +#include <gio/gio.hpp> namespace Platform { namespace { @@ -236,6 +235,8 @@ void MainWindow::updateUnityCounter() { #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) qApp->setBadgeNumber(Core::App().unreadBadge()); #else // Qt >= 6.6.0 + using namespace gi::repository; + static const auto djbStringHash = [](const std::string &string) { uint hash = 5381; for (const auto &curChar : string) { @@ -244,40 +245,36 @@ void MainWindow::updateUnityCounter() { return hash; }; - const auto launcherUrl = Glib::ustring( - "application://" - + QGuiApplication::desktopFileName().toStdString() - + ".desktop"); + const auto launcherUrl = "application://" + + QGuiApplication::desktopFileName().toStdString() + + ".desktop"; + const auto counterSlice = std::min(Core::App().unreadBadge(), 9999); - std::map<Glib::ustring, Glib::VariantBase> dbusUnityProperties; - if (counterSlice > 0) { - // According to the spec, it should be of 'x' D-Bus signature, - // which corresponds to signed 64-bit integer - // https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry - dbusUnityProperties["count"] = Glib::create_variant( - int64(counterSlice)); - dbusUnityProperties["count-visible"] = Glib::create_variant(true); - } else { - dbusUnityProperties["count-visible"] = Glib::create_variant(false); + auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr); + if (!connection) { + return; } - try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); - - connection->emit_signal( - "/com/canonical/unity/launcherentry/" - + std::to_string(djbStringHash(launcherUrl)), - "com.canonical.Unity.LauncherEntry", - "Update", - {}, - Glib::create_variant(std::tuple{ - launcherUrl, - dbusUnityProperties, - })); - } catch (...) { - } + connection.emit_signal( + {}, + "/com/canonical/unity/launcherentry/" + + std::to_string(djbStringHash(launcherUrl)), + "com.canonical.Unity.LauncherEntry", + "Update", + GLib::Variant::new_tuple({ + GLib::Variant::new_string(launcherUrl), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("count"), + GLib::Variant::new_variant( + GLib::Variant::new_int64(counterSlice))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("count-visible"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(counterSlice))), + }), + })); #endif // Qt < 6.6.0 } diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index c94400ff1..0473edf72 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -78,24 +78,24 @@ std::unique_ptr<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() { Gio::DBus::BusType::SESSION); const auto activatable = [&] { - try { - return ranges::contains( - base::Platform::DBus::ListActivatableNames(connection), - kService, - &Glib::ustring::raw); - } catch (...) { + const auto names = base::Platform::DBus::ListActivatableNames( + connection->gobj()); + + if (!names) { // avoid service restart loop in sandboxed environments return true; } + + return ranges::contains(*names, kService); }(); return std::make_unique<base::Platform::DBus::ServiceWatcher>( - connection, + connection->gobj(), kService, [=]( - const Glib::ustring &service, - const Glib::ustring &oldOwner, - const Glib::ustring &newOwner) { + const std::string &service, + const std::string &oldOwner, + const std::string &newOwner) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { if (activatable && newOwner.empty()) { Core::App().notifications().clearAll(); @@ -115,27 +115,28 @@ void StartServiceAsync(Fn<void()> callback) { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); - base::Platform::DBus::StartServiceByNameAsync( - connection, + namespace DBus = base::Platform::DBus; + DBus::StartServiceByNameAsync( + connection->gobj(), kService, - [=](Fn<base::Platform::DBus::StartReply()> result) { + [=](Fn<DBus::Result<DBus::StartReply>()> result) { Core::Sandbox::Instance().customEnterFromEventLoop([&] { Noexcept([&] { - try { - result(); // get the error if any - } catch (const Glib::Error &e) { + // get the error if any + if (const auto ret = result(); !ret) { static const auto NotSupportedErrors = { "org.freedesktop.DBus.Error.ServiceUnknown", }; - const auto errorName = - Gio::DBus::ErrorUtils::get_remote_error(e) - .raw(); - - if (!ranges::contains( + if (ranges::none_of( NotSupportedErrors, - errorName)) { - throw; + [&](const auto &error) { + return strstr( + ret.error()->what(), + error); + })) { + throw std::runtime_error( + ret.error()->what()); } } }); @@ -156,25 +157,20 @@ bool GetServiceRegistered() { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::SESSION); - const auto hasOwner = [&] { - try { - return base::Platform::DBus::NameHasOwner( - connection, - kService); - } catch (...) { - return false; - } - }(); + const auto hasOwner = base::Platform::DBus::NameHasOwner( + connection->gobj(), + kService + ).value_or(false); static const auto activatable = [&] { - try { - return ranges::contains( - base::Platform::DBus::ListActivatableNames(connection), - kService, - &Glib::ustring::raw); - } catch (...) { + const auto names = base::Platform::DBus::ListActivatableNames( + connection->gobj()); + + if (!names) { return false; } + + return ranges::contains(*names, kService); }(); return hasOwner || activatable; diff --git a/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml b/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml deleted file mode 100644 index e91bd22d3..000000000 --- a/Telegram/SourceFiles/platform/linux/org.freedesktop.portal.Inhibit.xml +++ /dev/null @@ -1,186 +0,0 @@ -<?xml version="1.0"?> -<!-- - Copyright (C) 2016 Red Hat, Inc. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library. If not, see <http://www.gnu.org/licenses/>. - - Author: Matthias Clasen <mclasen@redhat.com> ---> - -<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> - <!-- - org.freedesktop.portal.Inhibit: - @short_description: Portal for inhibiting session transitions - - This simple interface lets sandboxed applications inhibit the user - session from ending, suspending, idling or getting switched away. - - This documentation describes version 3 of this interface. - --> - <interface name="org.freedesktop.portal.Inhibit"> - <!-- - Inhibit: - @window: Identifier for the window - @flags: Flags identifying what is inhibited - @options: Vardict with optional further information - @handle: Object path for the #org.freedesktop.portal.Request object representing this call - - Inhibits a session status changes. To remove the inhibition, - call org.freedesktop.portal.Request.Close() on the returned - handle. - - The flags determine what changes are inhibited: - <simplelist> - <member>1: Logout</member> - <member>2: User Switch</member> - <member>4: Suspend</member> - <member>8: Idle</member> - </simplelist> - - Supported keys in the @options vardict include: - <variablelist> - <varlistentry> - <term>handle_token s</term> - <listitem><para> - A string that will be used as the last element of the @handle. Must be a valid - object path element. See the #org.freedesktop.portal.Request documentation for - more information about the @handle. - </para></listitem> - </varlistentry> - <varlistentry> - <term>reason s</term> - <listitem><para>User-visible reason for the inhibition.</para></listitem> - </varlistentry> - </variablelist> - --> - <method name="Inhibit"> - <arg type="s" name="window" direction="in"/> - <arg type="u" name="flags" direction="in"/> - <arg type="a{sv}" name="options" direction="in"/> - <arg type="o" name="handle" direction="out"/> - </method> - - <!-- - CreateMonitor: - @window: the parent window - @options: Vardict with optional further information - @handle: Object path for the #org.freedesktop.portal.Request object representing this call - - Creates a monitoring session. While this session is - active, the caller will receive StateChanged signals - with updates on the session state. - - A successfully created session can at any time be closed using - org.freedesktop.portal.Session::Close, or may at any time be closed - by the portal implementation, which will be signalled via - #org.freedesktop.portal.Session::Closed. - - Supported keys in the @options vardict include: - <variablelist> - <varlistentry> - <term>handle_token s</term> - <listitem><para> - A string that will be used as the last element of the @handle. Must be a valid - object path element. See the #org.freedesktop.portal.Request documentation for - more information about the @handle. - </para></listitem> - </varlistentry> - <varlistentry> - <term>session_handle_token s</term> - <listitem><para> - A string that will be used as the last element of the session handle. Must be a valid - object path element. See the #org.freedesktop.portal.Session documentation for - more information about the session handle. - </para></listitem> - </varlistentry> - </variablelist> - - The following results get returned via the #org.freedesktop.portal.Request::Response signal: - <variablelist> - <varlistentry> - <term>session_handle o</term> - <listitem><para> - The session handle. An object path for the - #org.freedesktop.portal.Session object representing the created - session. - </para></listitem> - </varlistentry> - </variablelist> - - This method was added in version 2 of this interface. - --> - <method name="CreateMonitor"> - <arg type="s" name="window" direction="in"/> - <arg type="a{sv}" name="options" direction="in"/> - <arg type="o" name="handle" direction="out"/> - </method> - - <!-- - StateChanged: - @session_handle: Object path for the #org.freedesktop.portal.Session object - @state: Vardict with information about the session state - - The StateChanged signal is sent to active monitoring sessions when - the session state changes. - - When the session state changes to 'Query End', clients with active monitoring - sessions are expected to respond by calling - org.freedesktop.portal.Inhibit.QueryEndResponse() within a second - of receiving the StateChanged signal. They may call org.freedesktop.portal.Inhibit.Inhibit() - first to inhibit logout, to prevent the session from proceeding to the Ending state. - - The following information may get returned in the @state vardict: - <variablelist> - <varlistentry> - <term>screensaver-active b</term> - <listitem><para> - Whether the screensaver is active. - </para></listitem> - </varlistentry> - <varlistentry> - <term>session-state u</term> - <listitem><para> - The state of the session. This member is new in version 3. - </para> - <simplelist> - <member>1: Running</member> - <member>2: Query End</member> - <member>3: Ending</member> - </simplelist> - </listitem> - </varlistentry> - </variablelist> - --> - <signal name="StateChanged"> - <arg type="o" name="session_handle" direction="out"/> - <arg type="a{sv}" name="state" direction="out"/> - </signal> - - <!-- - QueryEndResponse: - @session_handle: Object path for the #org.freedesktop.portal.Session object - - Acknowledges that the caller received the #org.freedesktop.portal.Inhibit::StateChanged - signal. This method should be called within one second or receiving a StateChanged - signal with the 'Query End' state. - - Since version 3. - --> - <method name="QueryEndResponse"> - <arg type="o" name="session_handle" direction="in"/> - </method> - - <property name="version" type="u" access="read"/> - </interface> -</node> diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 2299bea98..1318bda4b 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -38,6 +38,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <glibmm.h> #include <giomm.h> +#include <xdgdbus/xdgdbus.hpp> +#include <xdpbackground/xdpbackground.hpp> +#include <xdprequest/xdprequest.hpp> + #include <sys/stat.h> #include <sys/types.h> #include <sys/un.h> @@ -48,141 +52,157 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <iostream> +namespace { + +using namespace gi::repository; +namespace Gio = gi::repository::Gio; using namespace Platform; using Platform::internal::WaylandIntegration; -namespace Platform { -namespace { - void PortalAutostart(bool enabled, Fn<void(bool)> done) { - if (cExeName().isEmpty()) { + const auto executable = ExecutablePathForShortcuts(); + if (executable.isEmpty()) { if (done) { done(false); } return; } - const auto connection = [&] { - try { - return Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION); - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); + XdpBackground::BackgroundProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath, + [=](GObject::Object, Gio::AsyncResult res) { + auto proxy = XdpBackground::BackgroundProxy::new_for_bus_finish( + res); + + if (!proxy) { + if (done) { + LOG(("Portal Autostart Error: %1").arg( + proxy.error().what())); + done(false); + } + return; } - return Glib::RefPtr<Gio::DBus::Connection>(); - } - }(); - if (!connection) { - if (done) { - done(false); - } - return; - } + auto interface = XdpBackground::Background(*proxy); - const auto handleToken = Glib::ustring("tdesktop") - + std::to_string(base::RandomValue<uint>()); + const auto handleToken = "tdesktop" + + std::to_string(base::RandomValue<uint>()); - std::vector<Glib::ustring> commandline; - commandline.push_back(cExeName().toStdString()); - if (Core::Launcher::Instance().customWorkingDir()) { - commandline.push_back("-workdir"); - commandline.push_back(cWorkingDir().toStdString()); - } - commandline.push_back("-autostart"); + auto uniqueName = std::string( + proxy->get_connection().get_unique_name()); + uniqueName.erase(0, 1); + uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); - std::map<Glib::ustring, Glib::VariantBase> options; - options["handle_token"] = Glib::create_variant(handleToken); - options["reason"] = Glib::create_variant( - Glib::ustring( - tr::lng_settings_auto_start(tr::now).toStdString())); - options["autostart"] = Glib::create_variant(enabled); - options["commandline"] = Glib::create_variant(commandline); - options["dbus-activatable"] = Glib::create_variant(false); + const auto window = std::make_shared<QWidget>(); + window->setAttribute(Qt::WA_DontShowOnScreen); + window->setWindowModality(Qt::ApplicationModal); + window->show(); - auto uniqueName = connection->get_unique_name(); - uniqueName.erase(0, 1); - uniqueName.replace(uniqueName.find('.'), 1, 1, '_'); + XdpRequest::RequestProxy::new_( + proxy->get_connection(), + Gio::DBusProxyFlags::NONE_, + base::Platform::XDP::kService, + base::Platform::XDP::kObjectPath + + std::string("/request/") + + uniqueName + + '/' + + handleToken, + nullptr, + [=](GObject::Object, Gio::AsyncResult res) mutable { + auto requestProxy = XdpRequest::RequestProxy::new_finish( + res); - const auto requestPath = Glib::ustring( - "/org/freedesktop/portal/desktop/request/") - + uniqueName - + '/' - + handleToken; - - const auto window = std::make_shared<QWidget>(); - window->setAttribute(Qt::WA_DontShowOnScreen); - window->setWindowModality(Qt::ApplicationModal); - window->show(); - - const auto signalId = std::make_shared<uint>(); - *signalId = connection->signal_subscribe( - [=]( - const Glib::RefPtr<Gio::DBus::Connection> &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - (void)window; // don't destroy until finish - - try { - const auto response = parameters.get_child( - 0 - ).get_dynamic<uint>(); - - if (response) { + if (!requestProxy) { if (done) { - LOG(("Portal Autostart Error: Request denied")); + LOG(("Portal Autostart Error: %1").arg( + requestProxy.error().what())); done(false); } - } else if (done) { - done(enabled); - } - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); - done(false); - } - } - - if (*signalId) { - connection->signal_unsubscribe(*signalId); - } - }); - }, - base::Platform::XDP::kService, - base::Platform::XDP::kRequestInterface, - "Response", - requestPath); - - connection->call( - base::Platform::XDP::kObjectPath, - "org.freedesktop.portal.Background", - "RequestBackground", - Glib::create_variant(std::tuple{ - base::Platform::XDP::ParentWindowID(), - options, - }), - [=](const Glib::RefPtr<Gio::AsyncResult> &result) { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - try { - connection->call_finish(result); - } catch (const std::exception &e) { - if (done) { - LOG(("Portal Autostart Error: %1").arg(e.what())); - done(false); + return; } - if (*signalId) { - connection->signal_unsubscribe(*signalId); + auto request = XdpRequest::Request(*requestProxy); + const auto signalId = std::make_shared<ulong>(); + *signalId = request.signal_response().connect([=]( + XdpRequest::Request, + guint response, + GLib::Variant) mutable { + auto &sandbox = Core::Sandbox::Instance(); + sandbox.customEnterFromEventLoop([&] { + (void)window; // don't destroy until finish + + if (response) { + if (done) { + LOG(("Portal Autostart Error: " + "Request denied")); + done(false); + } + } else if (done) { + done(enabled); + } + + request.disconnect(*signalId); + }); + }); + + + std::vector<std::string> commandline; + commandline.push_back(executable.toStdString()); + if (Core::Launcher::Instance().customWorkingDir()) { + commandline.push_back("-workdir"); + commandline.push_back(cWorkingDir().toStdString()); } - } - }); - }, - base::Platform::XDP::kService); + commandline.push_back("-autostart"); + + interface.call_request_background( + std::string(base::Platform::XDP::ParentWindowID()), + GLib::Variant::new_array({ + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("handle_token"), + GLib::Variant::new_variant( + GLib::Variant::new_string(handleToken))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("reason"), + GLib::Variant::new_variant( + GLib::Variant::new_string( + tr::lng_settings_auto_start(tr::now) + .toStdString()))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("autostart"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(enabled))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("commandline"), + GLib::Variant::new_variant( + GLib::Variant::new_strv(commandline))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("dbus-activatable"), + GLib::Variant::new_variant( + GLib::Variant::new_boolean(false))), + }), + [=](GObject::Object, Gio::AsyncResult res) mutable { + auto &sandbox = Core::Sandbox::Instance(); + sandbox.customEnterFromEventLoop([&] { + const auto result = + interface.call_request_background_finish( + res); + + if (!result) { + if (done) { + LOG(("Portal Autostart Error: %1") + .arg(result.error().what())); + done(false); + } + + request.disconnect(*signalId); + } + }); + }); + }); + }); } bool GenerateDesktopFile( @@ -218,77 +238,89 @@ bool GenerateDesktopFile( return false; } - try { - const auto target = Glib::KeyFile::create(); - target->load_from_data( - sourceText, - Glib::KeyFile::Flags::KEEP_COMMENTS - | Glib::KeyFile::Flags::KEEP_TRANSLATIONS); + auto target = GLib::KeyFile::new_(); + const auto loaded = target.load_from_data( + sourceText, + -1, + GLib::KeyFileFlags::KEEP_COMMENTS_ + | GLib::KeyFileFlags::KEEP_TRANSLATIONS_); + + if (!loaded) { + if (!silent) { + LOG(("App Error: %1").arg(loaded.error().what())); + } + return false; + } - for (const auto &group : target->get_groups()) { - if (onlyMainGroup && group != "Desktop Entry") { - target->remove_group(group); - continue; + for (const auto &group : target.get_groups(nullptr)) { + if (onlyMainGroup && group != "Desktop Entry") { + const auto removed = target.remove_group(group); + if (!removed) { + if (!silent) { + LOG(("App Error: %1").arg(removed.error().what())); + } + return false; } + continue; + } - if (target->has_key(group, "TryExec")) { - target->set_string( + if (target.has_key(group, "TryExec", nullptr)) { + target.set_string( + group, + "TryExec", + KShell::joinArgs({ executable }).replace( + '\\', + qstr("\\\\")).toStdString()); + } + + if (target.has_key(group, "Exec", nullptr)) { + if (group == "Desktop Entry" && !args.isEmpty()) { + QStringList exec; + exec.append(executable); + if (Core::Launcher::Instance().customWorkingDir()) { + exec.append(u"-workdir"_q); + exec.append(cWorkingDir()); + } + exec.append(args); + target.set_string( group, - "TryExec", - KShell::joinArgs({ executable }).replace( + "Exec", + KShell::joinArgs(exec).replace( '\\', qstr("\\\\")).toStdString()); - } + } else { + auto exec = KShell::splitArgs( + QString::fromStdString( + target.get_string(group, "Exec", nullptr) + ).replace( + qstr("\\\\"), + qstr("\\"))); - if (target->has_key(group, "Exec")) { - if (group == "Desktop Entry" && !args.isEmpty()) { - QStringList exec; - exec.append(executable); + if (!exec.isEmpty()) { + exec[0] = executable; if (Core::Launcher::Instance().customWorkingDir()) { - exec.append(u"-workdir"_q); - exec.append(cWorkingDir()); + exec.insert(1, u"-workdir"_q); + exec.insert(2, cWorkingDir()); } - exec.append(args); - target->set_string( + target.set_string( group, "Exec", KShell::joinArgs(exec).replace( '\\', qstr("\\\\")).toStdString()); - } else { - auto exec = KShell::splitArgs( - QString::fromStdString( - target->get_string(group, "Exec") - ).replace( - qstr("\\\\"), - qstr("\\"))); - - if (!exec.isEmpty()) { - exec[0] = executable; - if (Core::Launcher::Instance().customWorkingDir()) { - exec.insert(1, u"-workdir"_q); - exec.insert(2, cWorkingDir()); - } - target->set_string( - group, - "Exec", - KShell::joinArgs(exec).replace( - '\\', - qstr("\\\\")).toStdString()); - } } } } + } - if (!args.isEmpty() - && target->has_key("Desktop Entry", "DBusActivatable")) { - target->remove_key("Desktop Entry", "DBusActivatable"); - } + if (!args.isEmpty()) { + target.remove_key("Desktop Entry", "DBusActivatable"); + } - target->save_to_file(targetFile.toStdString()); - } catch (const std::exception &e) { + const auto saved = target.save_to_file(targetFile.toStdString()); + if (!saved) { if (!silent) { - LOG(("App Error: %1").arg(e.what())); + LOG(("App Error: %1").arg(saved.error().what())); } return false; } @@ -357,10 +389,10 @@ bool GenerateServiceFile(bool silent = false) { DEBUG_LOG(("App Info: placing D-Bus service file to %1").arg(targetPath)); if (!QDir(targetPath).exists()) QDir().mkpath(targetPath); - const auto target = Glib::KeyFile::create(); + auto target = GLib::KeyFile::new_(); constexpr auto group = "D-BUS Service"; - target->set_string( + target.set_string( group, "Name", QGuiApplication::desktopFileName().toStdString()); @@ -371,21 +403,21 @@ bool GenerateServiceFile(bool silent = false) { exec.append(u"-workdir"_q); exec.append(cWorkingDir()); } - target->set_string( + target.set_string( group, "Exec", KShell::joinArgs(exec).toStdString()); - try { - target->save_to_file(targetFile.toStdString()); - } catch (const std::exception &e) { + const auto saved = target.save_to_file(targetFile.toStdString()); + if (!saved) { if (!silent) { - LOG(("App Error: %1").arg(e.what())); + LOG(("App Error: %1").arg(saved.error().what())); } return false; } - if (!Core::UpdaterDisabled() && !Core::Launcher::Instance().customWorkingDir()) { + if (!Core::UpdaterDisabled() + && !Core::Launcher::Instance().customWorkingDir()) { DEBUG_LOG(("App Info: removing old D-Bus service files")); char md5Hash[33] = { 0 }; @@ -397,19 +429,21 @@ bool GenerateServiceFile(bool silent = false) { md5Hash)); } - try { - Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::SESSION - )->call( - base::Platform::DBus::kObjectPath, - base::Platform::DBus::kInterface, - "ReloadConfig", - {}, - {}, - base::Platform::DBus::kService - ); - } catch (...) { - } + XdgDBus::DBusProxy::new_for_bus( + Gio::BusType::SESSION_, + Gio::DBusProxyFlags::NONE_, + base::Platform::DBus::kService, + base::Platform::DBus::kObjectPath, + [=](GObject::Object, Gio::AsyncResult res) { + auto interface = XdgDBus::DBus( + XdgDBus::DBusProxy::new_for_bus_finish(res, nullptr)); + + if (!interface) { + return; + } + + interface.call_reload_config(nullptr); + }); return true; } @@ -447,6 +481,8 @@ void InstallLauncher() { } // namespace +namespace Platform { + void SetApplicationIcon(const QIcon &icon) { QApplication::setWindowIcon(icon); } @@ -648,11 +684,11 @@ void start() { qputenv("PULSE_PROP_application.name", AppName.utf8()); qputenv("PULSE_PROP_application.icon_name", base::IconName().toLatin1()); - Glib::set_prgname(cExeName().toStdString()); - Glib::set_application_name(AppName.data()); + GLib::set_prgname(cExeName().toStdString()); + GLib::set_application_name(AppName.data()); Glib::init(); - Gio::init(); + ::Gio::init(); Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-%4"_q.arg( QDir::tempPath(), diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp new file mode 100644 index 000000000..a5fef26a2 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -0,0 +1,375 @@ +/* +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 "settings/business/settings_away_message.h" + +#include "base/unixtime.h" +#include "core/application.h" +#include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" +#include "ui/boxes/choose_date_time.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class AwayMessage : public BusinessSection<AwayMessage> { +public: + AwayMessage( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~AwayMessage(); + + [[nodiscard]] bool closeByOutsideClick() const override; + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + rpl::variable<bool> _canHave; + rpl::event_stream<> _deactivateOnAttempt; + rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<Data::AwaySchedule> _schedule; + rpl::variable<bool> _offlineOnly; + rpl::variable<bool> _enabled; + +}; + +[[nodiscard]] TimeId StartTimeMin() { + // Telegram was launched in August 2013 :) + return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1), QTime(0, 0))); +} + +[[nodiscard]] TimeId EndTimeMin() { + return StartTimeMin() + 3600; +} + +[[nodiscard]] bool BadCustomInterval(const Data::WorkingInterval &interval) { + return !interval + || (interval.start < StartTimeMin()) + || (interval.end < EndTimeMin()); +} + +struct AwayScheduleSelectorDescriptor { + not_null<Window::SessionController*> controller; + not_null<rpl::variable<Data::AwaySchedule>*> data; +}; +void AddAwayScheduleSelector( + not_null<Ui::VerticalLayout*> container, + AwayScheduleSelectorDescriptor &&descriptor) { + using Type = Data::AwayScheduleType; + using namespace rpl::mappers; + + const auto controller = descriptor.controller; + const auto data = descriptor.data; + + Ui::AddSubsectionTitle(container, tr::lng_away_schedule()); + const auto group = std::make_shared<Ui::RadioenumGroup<Type>>( + data->current().type); + + const auto add = [&](Type type, const QString &label) { + container->add( + object_ptr<Ui::Radioenum<Type>>( + container, + group, + type, + label), + st::boxRowPadding + st::settingsAwaySchedulePadding); + }; + add(Type::Always, tr::lng_away_schedule_always(tr::now)); + add(Type::OutsideWorkingHours, tr::lng_away_schedule_outside(tr::now)); + add(Type::Custom, tr::lng_away_schedule_custom(tr::now)); + + const auto customWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto customInner = customWrap->entity(); + customWrap->toggleOn(group->value() | rpl::map(_1 == Type::Custom)); + + group->changes() | rpl::start_with_next([=](Type value) { + auto copy = data->current(); + copy.type = value; + *data = copy; + }, customWrap->lifetime()); + + const auto chooseDate = [=]( + rpl::producer<QString> title, + TimeId now, + Fn<TimeId()> min, + Fn<TimeId()> max, + Fn<void(TimeId)> done) { + using namespace Ui; + const auto box = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto save = [=](TimeId time) { + done(time); + if (const auto strong = box->data()) { + strong->closeBox(); + } + }; + *box = controller->show(Box(ChooseDateTimeBox, ChooseDateTimeBoxArgs{ + .title = std::move(title), + .submit = tr::lng_settings_save(), + .done = save, + .min = min, + .time = now, + .max = max, + })); + }; + + Ui::AddSkip(customInner); + Ui::AddDivider(customInner); + Ui::AddSkip(customInner); + + auto startLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.start)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_start(), + std::move(startLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_start(), + data->current().customInterval.start, + StartTimeMin, + [=] { return data->current().customInterval.end - 1; }, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.start = time; + *data = copy; + }); + }); + + auto endLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.end)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_end(), + std::move(endLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_end(), + data->current().customInterval.end, + [=] { return data->current().customInterval.start + 1; }, + nullptr, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.end = time; + *data = copy; + }); + }); +} + +AwayMessage::AwayMessage( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +AwayMessage::~AwayMessage() { + if (!Core::Quitting()) { + save(); + } +} + +bool AwayMessage::closeByOutsideClick() const { + return false; +} + +rpl::producer<QString> AwayMessage::title() { + return tr::lng_away_title(); +} + +void AwayMessage::setupContent( + not_null<Window::SessionController*> controller) { + using namespace Data; + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->awaySettings(); + const auto disabled = (current.schedule.type == AwayScheduleType::Never); + + _recipients = disabled + ? Data::BusinessRecipients{ .allButExcluded = true } + : current.recipients; + auto initialSchedule = disabled ? AwaySchedule{ + .type = AwayScheduleType::Always, + } : current.schedule; + if (BadCustomInterval(initialSchedule.customInterval)) { + const auto now = base::unixtime::now(); + initialSchedule.customInterval = WorkingInterval{ + .start = now, + .end = now + 24 * 60 * 60, + }; + } + _schedule = initialSchedule; + + AddDividerTextWithLottie(content, { + .lottie = u"sleep"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_away_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto session = &controller->session(); + _canHave = rpl::combine( + ShortcutsCountValue(session), + ShortcutsLimitValue(session), + ShortcutExistsValue(session, u"away"_q), + (_1 < _2) || _3); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_away_enable(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single( + !disabled + ) | rpl::then(rpl::merge( + _canHave.value() | rpl::filter(!_1), + _deactivateOnAttempt.events() | rpl::map_to(false) + ))); + + _enabled = enabled->toggledValue(); + _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { + if (!_canHave.current()) { + controller->showToast({ + .text = { tr::lng_away_limit_reached(tr::now) }, + .adaptive = true, + }); + _deactivateOnAttempt.fire({}); + } + }, lifetime()); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + + const auto createWrap = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = AddButtonWithLabel( + createInner, + rpl::conditional( + ShortcutExistsValue(session, u"away"_q), + tr::lng_business_edit_messages(), + tr::lng_away_create()), + ShortcutMessagesCountValue( + session, + u"away"_q + ) | rpl::map([=](int count) { + return count + ? tr::lng_forum_messages(tr::now, lt_count, count) + : QString(); + }), + st::settingsButtonLightNoIcon); + create->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("away"); + showOther(ShortcutMessagesId(id)); + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); + AddAwayScheduleSelector(inner, { + .controller = controller, + .data = &_schedule, + }); + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + const auto offlineOnly = inner->add( + object_ptr<Ui::SettingsButton>( + inner, + tr::lng_away_offline_only(), + st::settingsButtonNoIcon) + )->toggleOn(rpl::single(current.offlineOnly)); + _offlineOnly = offlineOnly->toggledValue(); + + Ui::AddSkip(inner); + Ui::AddDividerText(inner, tr::lng_away_offline_only_about()); + + AddBusinessRecipientsSelector(inner, { + .controller = controller, + .title = tr::lng_away_recipients(), + .data = &_recipients, + }); + + Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + Ui::ResizeFitChild(this, content); +} + +void AwayMessage::save() { + const auto show = controller()->uiShow(); + const auto session = &controller()->session(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); + } else if (error != u"SHORTCUT_INVALID"_q) { + show->showToast(error); + } + }; + session->data().businessInfo().saveAwaySettings( + _enabled.current() ? Data::AwaySettings{ + .recipients = _recipients.current(), + .schedule = _schedule.current(), + .shortcutId = LookupShortcutId(session, u"away"_q), + .offlineOnly = _offlineOnly.current(), + } : Data::AwaySettings(), + fail); +} + +} // namespace + +Type AwayMessageId() { + return AwayMessage::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.h b/Telegram/SourceFiles/settings/business/settings_away_message.h new file mode 100644 index 000000000..e9037b4f6 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_away_message.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type AwayMessageId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp new file mode 100644 index 000000000..656d49958 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -0,0 +1,501 @@ +/* +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 "settings/business/settings_chatbots.h" + +#include "apiwrap.h" +#include "boxes/peers/prepare_short_info_box.h" +#include "boxes/peer_list_box.h" +#include "core/application.h" +#include "data/business/data_business_chatbots.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kDebounceTimeout = crl::time(400); + +enum class LookupState { + Empty, + Loading, + Ready, +}; + +struct BotState { + UserData *bot = nullptr; + LookupState state = LookupState::Empty; +}; + +class Chatbots : public BusinessSection<Chatbots> { +public: + Chatbots( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~Chatbots(); + + [[nodiscard]] bool closeByOutsideClick() const override; + [[nodiscard]] rpl::producer<QString> title() override; + + const Ui::RoundRect *bottomSkipRounding() const override { + return &_bottomSkipRounding; + } + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + Ui::RoundRect _bottomSkipRounding; + + rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<QString> _usernameValue; + rpl::variable<BotState> _botValue; + rpl::variable<bool> _repliesAllowed = true; + +}; + +class PreviewController final : public PeerListController { +public: + PreviewController(not_null<PeerData*> peer, Fn<void()> resetBot); + + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null<PeerListRow*> row) override; + void rowRightActionClicked(not_null<PeerListRow*> row) override; + Main::Session &session() const override; + +private: + const not_null<PeerData*> _peer; + const Fn<void()> _resetBot; + rpl::lifetime _lifetime; + +}; + +class PreviewRow final : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + void rightActionAddRipple( + QPoint point, + Fn<void()> updateCallback) override; + void rightActionStopLastRipple() override; + +private: + std::unique_ptr<Ui::RippleAnimation> _actionRipple; + +}; + +QSize PreviewRow::rightActionSize() const { + return QSize( + st::settingsChatbotsDeleteIcon.width(), + st::settingsChatbotsDeleteIcon.height()) * 2; +} + +QMargins PreviewRow::rightActionMargins() const { + const auto itemHeight = st::peerListSingleRow.item.height; + const auto skip = (itemHeight - rightActionSize().height()) / 2; + return QMargins(0, skip, skip, 0); +} + +void PreviewRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionRipple) { + _actionRipple->paint( + p, + x, + y, + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + const auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize()); + (actionSelected + ? st::settingsChatbotsDeleteIconOver + : st::settingsChatbotsDeleteIcon).paintInCenter(p, rect); +} + +void PreviewRow::rightActionAddRipple( + QPoint point, + Fn<void()> updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize()); + _actionRipple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point); +} + +void PreviewRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +PreviewController::PreviewController( + not_null<PeerData*> peer, + Fn<void()> resetBot) +: _peer(peer) +, _resetBot(std::move(resetBot)) { +} + +void PreviewController::prepare() { + delegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer)); + delegate()->peerListRefreshRows(); +} + +void PreviewController::loadMoreRows() { +} + +void PreviewController::rowClicked(not_null<PeerListRow*> row) { +} + +void PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) { + _resetBot(); +} + +Main::Session &PreviewController::session() const { + return _peer->session(); +} + +[[nodiscard]] rpl::producer<QString> DebouncedValue( + not_null<Ui::InputField*> field) { + return [=](auto consumer) { + + auto result = rpl::lifetime(); + struct State { + base::Timer timer; + QString lastText; + }; + const auto state = result.make_state<State>(); + const auto push = [=] { + state->timer.cancel(); + consumer.put_next_copy(state->lastText); + }; + state->timer.setCallback(push); + state->lastText = field->getLastText(); + consumer.put_next_copy(field->getLastText()); + field->changes() | rpl::start_with_next([=] { + const auto &text = field->getLastText(); + const auto was = std::exchange(state->lastText, text); + if (std::abs(int(text.size()) - int(was.size())) == 1) { + state->timer.callOnce(kDebounceTimeout); + } else { + push(); + } + }, result); + return result; + }; +} + +[[nodiscard]] QString ExtractUsername(QString text) { + text = text.trimmed(); + static const auto expression = QRegularExpression( + "^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)"); + const auto match = expression.match(text); + return match.hasMatch() ? match.captured(3) : text; +} + +[[nodiscard]] rpl::producer<BotState> LookupBot( + not_null<Main::Session*> session, + rpl::producer<QString> usernameChanges) { + using Cache = base::flat_map<QString, UserData*>; + const auto cache = std::make_shared<Cache>(); + return std::move( + usernameChanges + ) | rpl::map([=](const QString &username) -> rpl::producer<BotState> { + const auto extracted = ExtractUsername(username); + const auto owner = &session->data(); + static const auto expression = QRegularExpression( + "^[a-zA-Z0-9_\\.]+$"); + if (!expression.match(extracted).hasMatch()) { + return rpl::single(BotState()); + } else if (const auto peer = owner->peerByUsername(extracted)) { + if (const auto user = peer->asUser(); user && user->isBot()) { + return rpl::single(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + } + return rpl::single(BotState{ + .state = LookupState::Ready, + }); + } else if (const auto i = cache->find(extracted); i != end(*cache)) { + return rpl::single(BotState{ + .bot = i->second, + .state = LookupState::Ready, + }); + } + + return [=](auto consumer) { + auto result = rpl::lifetime(); + + const auto requestId = result.make_state<mtpRequestId>(); + *requestId = session->api().request(MTPcontacts_ResolveUsername( + MTP_string(extracted) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + const auto &data = result.data(); + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + const auto peerId = peerFromMTP(data.vpeer()); + const auto peer = session->data().peer(peerId); + if (const auto user = peer->asUser()) { + if (user->isBot()) { + cache->emplace(extracted, user); + consumer.put_next(BotState{ + .bot = user, + .state = LookupState::Ready, + }); + return; + } + } + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).fail([=] { + cache->emplace(extracted, nullptr); + consumer.put_next(BotState{ .state = LookupState::Ready }); + }).send(); + + result.add([=] { + session->api().request(*requestId).cancel(); + }); + return result; + }; + }) | rpl::flatten_latest(); +} + +[[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview( + not_null<Ui::RpWidget*> parent, + rpl::producer<BotState> state, + Fn<void()> resetBot) { + auto result = object_ptr<Ui::SlideWrap<>>( + parent.get(), + object_ptr<Ui::RpWidget>(parent.get())); + const auto raw = result.data(); + const auto inner = raw->entity(); + raw->hide(anim::type::instant); + + const auto child = inner->lifetime().make_state<Ui::RpWidget*>(nullptr); + std::move(state) | rpl::filter([=](BotState state) { + return state.state != LookupState::Loading; + }) | rpl::start_with_next([=](BotState state) { + raw->toggle(state.state == LookupState::Ready, anim::type::normal); + if (state.bot) { + const auto delegate = parent->lifetime().make_state< + PeerListContentDelegateSimple + >(); + const auto controller = parent->lifetime().make_state< + PreviewController + >(state.bot, resetBot); + controller->setStyleOverrides(&st::peerListSingleRow); + const auto content = Ui::CreateChild<PeerListContent>( + inner, + controller); + delegate->setContent(content); + controller->setDelegate(delegate); + delete base::take(*child); + *child = content; + } else if (state.state == LookupState::Ready) { + const auto content = Ui::CreateChild<Ui::RpWidget>(inner); + const auto label = Ui::CreateChild<Ui::FlatLabel>( + content, + tr::lng_chatbots_not_found(), + st::settingsChatbotsNotFound); + content->resize( + inner->width(), + st::peerListSingleRow.item.height); + rpl::combine( + content->sizeValue(), + label->sizeValue() + ) | rpl::start_with_next([=](QSize size, QSize inner) { + label->move( + (size.width() - inner.width()) / 2, + (size.height() - inner.height()) / 2); + }, label->lifetime()); + delete base::take(*child); + *child = content; + } else { + return; + } + (*child)->show(); + + inner->widthValue() | rpl::start_with_next([=](int width) { + (*child)->resizeToWidth(width); + }, (*child)->lifetime()); + + (*child)->heightValue() | rpl::start_with_next([=](int height) { + inner->resize(inner->width(), height + st::contactSkip); + }, inner->lifetime()); + }, inner->lifetime()); + + raw->finishAnimating(); + return result; +} + +Chatbots::Chatbots( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { + setupContent(controller); +} + +Chatbots::~Chatbots() { + if (!Core::Quitting()) { + save(); + } +} + +bool Chatbots::closeByOutsideClick() const { + return false; +} + +rpl::producer<QString> Chatbots::title() { + return tr::lng_chatbots_title(); +} + +void Chatbots::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + const auto current = controller->session().data().chatbots().current(); + + _recipients = current.recipients; + _repliesAllowed = current.repliesAllowed; + + AddDividerTextWithLottie(content, { + .lottie = u"robot"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_chatbots_about( + lt_link, + tr::lng_chatbots_about_link( + ) | Ui::Text::ToLink(tr::lng_chatbots_info_url(tr::now)), + Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto username = content->add( + object_ptr<Ui::InputField>( + content, + st::settingsChatbotsUsername, + tr::lng_chatbots_placeholder(), + (current.bot + ? current.bot->session().createInternalLink( + current.bot->username()) + : QString())), + st::settingsChatbotsUsernameMargins); + + _usernameValue = DebouncedValue(username); + _botValue = rpl::single(BotState{ + current.bot, + current.bot ? LookupState::Ready : LookupState::Empty + }) | rpl::then( + LookupBot(&controller->session(), _usernameValue.changes()) + ); + + const auto resetBot = [=] { + username->setText(QString()); + username->setFocus(); + }; + content->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>( + content, + MakeBotPreview(content, _botValue.value(), resetBot))); + + Ui::AddDividerText( + content, + tr::lng_chatbots_add_about(), + st::peerAppearanceDividerTextMargin); + + AddBusinessRecipientsSelector(content, { + .controller = controller, + .title = tr::lng_chatbots_access_title(), + .data = &_recipients, + }); + + Ui::AddSkip(content, st::settingsChatbotsAccessSkip); + Ui::AddDividerText( + content, + tr::lng_chatbots_exclude_about(), + st::peerAppearanceDividerTextMargin); + + Ui::AddSkip(content); + Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title()); + content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_chatbots_reply(), + st::settingsButtonNoIcon + ))->toggleOn(_repliesAllowed.value())->toggledChanges( + ) | rpl::start_with_next([=](bool value) { + _repliesAllowed = value; + }, content->lifetime()); + Ui::AddSkip(content); + + Ui::AddDividerText( + content, + tr::lng_chatbots_reply_about(), + st::settingsChatbotsBottomTextMargin, + RectPart::Top); + + Ui::ResizeFitChild(this, content); +} + +void Chatbots::save() { + const auto show = controller()->uiShow(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); + } + }; + controller()->session().data().chatbots().save({ + .bot = _botValue.current().bot, + .recipients = _recipients.current(), + .repliesAllowed = _repliesAllowed.current(), + }, [=] { + }, fail); +} + +} // namespace + +Type ChatbotsId() { + return Chatbots::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.h b/Telegram/SourceFiles/settings/business/settings_chatbots.h new file mode 100644 index 000000000..06fab806c --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ChatbotsId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp new file mode 100644 index 000000000..9809c1b71 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -0,0 +1,291 @@ +/* +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 "settings/business/settings_greeting.h" + +#include "base/event_filter.h" +#include "core/application.h" +#include "data/business/data_business_info.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_shortcut_messages.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/boxes/time_picker_box.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/box_content_divider.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/vertical_drum_picker.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kDefaultNoActivityDays = 7; + +class Greeting : public BusinessSection<Greeting> { +public: + Greeting( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~Greeting(); + + [[nodiscard]] bool closeByOutsideClick() const override; + [[nodiscard]] rpl::producer<QString> title() override; + + const Ui::RoundRect *bottomSkipRounding() const override { + return &_bottomSkipRounding; + } + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + Ui::RoundRect _bottomSkipRounding; + + rpl::variable<Data::BusinessRecipients> _recipients; + rpl::variable<bool> _canHave; + rpl::event_stream<> _deactivateOnAttempt; + rpl::variable<int> _noActivityDays; + rpl::variable<bool> _enabled; + +}; + +Greeting::Greeting( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { + setupContent(controller); +} + +void EditPeriodBox( + not_null<Ui::GenericBox*> box, + int days, + Fn<void(int)> save) { + auto values = std::vector{ 7, 14, 21, 28 }; + if (!ranges::contains(values, days)) { + values.push_back(days); + ranges::sort(values); + } + + const auto phrases = ranges::views::all( + values + ) | ranges::views::transform([](int days) { + return tr::lng_days(tr::now, lt_count, days); + }) | ranges::to_vector; + const auto take = TimePickerBox(box, values, phrases, days); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(take()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +Greeting::~Greeting() { + if (!Core::Quitting()) { + save(); + } +} + +bool Greeting::closeByOutsideClick() const { + return false; +} + +rpl::producer<QString> Greeting::title() { + return tr::lng_greeting_title(); +} + +void Greeting::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->greetingSettings(); + const auto disabled = !current.noActivityDays; + + _recipients = disabled + ? Data::BusinessRecipients{ .allButExcluded = true } + : current.recipients; + _noActivityDays = disabled + ? kDefaultNoActivityDays + : current.noActivityDays; + + AddDividerTextWithLottie(content, { + .lottie = u"greeting"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_greeting_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto session = &controller->session(); + _canHave = rpl::combine( + ShortcutsCountValue(session), + ShortcutsLimitValue(session), + ShortcutExistsValue(session, u"hello"_q), + (_1 < _2) || _3); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_greeting_enable(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single( + !disabled + ) | rpl::then(rpl::merge( + _canHave.value() | rpl::filter(!_1), + _deactivateOnAttempt.events() | rpl::map_to(false) + ))); + + _enabled = enabled->toggledValue(); + _enabled.value() | rpl::filter(_1) | rpl::start_with_next([=] { + if (!_canHave.current()) { + controller->showToast({ + .text = { tr::lng_greeting_limit_reached(tr::now) }, + .adaptive = true, + }); + _deactivateOnAttempt.fire({}); + } + }, lifetime()); + + Ui::AddSkip(content); + + content->add( + object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>( + content, + object_ptr<Ui::BoxContentDivider>( + content, + st::boxDividerHeight, + st::boxDividerBg, + RectPart::Top)) + )->setDuration(0)->toggleOn(enabled->toggledValue() | rpl::map(!_1)); + content->add( + object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>( + content, + object_ptr<Ui::BoxContentDivider>( + content)) + )->setDuration(0)->toggleOn(enabled->toggledValue()); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + const auto createWrap = inner->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + inner, + object_ptr<Ui::VerticalLayout>(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = AddButtonWithLabel( + createInner, + rpl::conditional( + ShortcutExistsValue(session, u"hello"_q), + tr::lng_business_edit_messages(), + tr::lng_greeting_create()), + ShortcutMessagesCountValue( + session, + u"hello"_q + ) | rpl::map([=](int count) { + return count + ? tr::lng_forum_messages(tr::now, lt_count, count) + : QString(); + }), + st::settingsButtonLightNoIcon); + create->setClickedCallback([=] { + const auto owner = &controller->session().data(); + const auto id = owner->shortcutMessages().emplaceShortcut("hello"); + showOther(ShortcutMessagesId(id)); + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); + AddBusinessRecipientsSelector(inner, { + .controller = controller, + .title = tr::lng_greeting_recipients(), + .data = &_recipients, + }); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + AddButtonWithLabel( + inner, + tr::lng_greeting_period_title(), + _noActivityDays.value( + ) | rpl::map( + [](int days) { return tr::lng_days(tr::now, lt_count, days); } + ), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + controller->show(Box( + EditPeriodBox, + _noActivityDays.current(), + [=](int days) { _noActivityDays = days; })); + }); + + Ui::AddSkip(inner); + Ui::AddDividerText( + inner, + tr::lng_greeting_period_about(), + st::settingsChatbotsBottomTextMargin, + RectPart::Top); + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + Ui::ResizeFitChild(this, content); +} + +void Greeting::save() { + const auto show = controller()->uiShow(); + const auto session = &controller()->session(); + const auto fail = [=](QString error) { + if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) { + show->showToast(tr::lng_greeting_recipients_empty(tr::now)); + } else if (error != u"SHORTCUT_INVALID"_q) { + show->showToast(error); + } + }; + session->data().businessInfo().saveGreetingSettings( + _enabled.current() ? Data::GreetingSettings{ + .recipients = _recipients.current(), + .noActivityDays = _noActivityDays.current(), + .shortcutId = LookupShortcutId(session, u"hello"_q), + } : Data::GreetingSettings(), + fail); +} + +} // namespace + +Type GreetingId() { + return Greeting::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.h b/Telegram/SourceFiles/settings/business/settings_greeting.h new file mode 100644 index 000000000..2bb9afd59 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_greeting.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type GreetingId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_location.cpp b/Telegram/SourceFiles/settings/business/settings_location.cpp new file mode 100644 index 000000000..4a2f14e73 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_location.cpp @@ -0,0 +1,127 @@ +/* +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 "settings/business/settings_location.h" + +#include "core/application.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class Location : public BusinessSection<Location> { +public: + Location( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~Location(); + + [[nodiscard]] rpl::producer<QString> title() override; + + const Ui::RoundRect *bottomSkipRounding() const override { + return mapSupported() ? nullptr : &_bottomSkipRounding; + } + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + [[nodiscard]] bool mapSupported() const; + + Ui::RoundRect _bottomSkipRounding; + +}; + +Location::Location( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) +, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { + setupContent(controller); +} + +Location::~Location() { + if (!Core::Quitting()) { + save(); + } +} + +rpl::producer<QString> Location::title() { + return tr::lng_location_title(); +} + +void Location::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + +#if 0 // #TODO location choosing + AddDividerTextWithLottie(content, { + .lottie = u"location"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_location_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + const auto address = content->add( + object_ptr<Ui::InputField>( + content, + st::settingsLocationAddress, + Ui::InputField::Mode::MultiLine, + tr::lng_location_address(), + QString()), + st::settingsChatbotsUsernameMargins); + + showFinishes() | rpl::start_with_next([=] { + address->setFocus(); + }, address->lifetime()); +#endif + + if (!mapSupported()) { + AddDividerTextWithLottie(content, { + .lottie = u"phone"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_location_fallback(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + .parts = RectPart::Top, + }); + } else { + + } + + Ui::ResizeFitChild(this, content); +} + +void Location::save() { +} + +bool Location::mapSupported() const { + return false; +} + +} // namespace + +Type LocationId() { + return Location::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_location.h b/Telegram/SourceFiles/settings/business/settings_location.h new file mode 100644 index 000000000..31e033253 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_location.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type LocationId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp new file mode 100644 index 000000000..d451feb1b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.cpp @@ -0,0 +1,231 @@ +/* +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 "settings/business/settings_quick_replies.h" + +#include "boxes/premium_preview_box.h" +#include "core/application.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "settings/business/settings_shortcut_messages.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +class QuickReplies : public BusinessSection<QuickReplies> { +public: + QuickReplies( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~QuickReplies(); + + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + + rpl::variable<int> _count; + +}; + +QuickReplies::QuickReplies( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +QuickReplies::~QuickReplies() = default; + +rpl::producer<QString> QuickReplies::title() { + return tr::lng_replies_title(); +} + +void QuickReplies::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + AddDividerTextWithLottie(content, { + .lottie = u"writing"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_replies_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + Ui::AddSkip(content); + + const auto addWrap = content->add( + object_ptr<Ui::VerticalLayout>(content)); + + const auto owner = &controller->session().data(); + const auto messages = &owner->shortcutMessages(); + + rpl::combine( + _count.value(), + ShortcutsLimitValue(&controller->session()) + ) | rpl::start_with_next([=](int count, int limit) { + while (addWrap->count()) { + delete addWrap->widgetAt(0); + } + if (count < limit) { + const auto add = addWrap->add(object_ptr<Ui::SettingsButton>( + addWrap, + tr::lng_replies_add(), + st::settingsButtonNoIcon + )); + + add->setClickedCallback([=] { + if (!controller->session().premium()) { + ShowPremiumPreviewToBuy( + controller, + PremiumFeature::QuickReplies); + return; + } + const auto submit = [=](QString name, Fn<void()> close) { + const auto id = messages->emplaceShortcut(name); + showOther(ShortcutMessagesId(id)); + close(); + }; + controller->show( + Box(EditShortcutNameBox, QString(), crl::guard(this, submit))); + }); + if (count > 0) { + AddSkip(addWrap); + AddDivider(addWrap); + AddSkip(addWrap); + } + } + if (const auto width = content->width()) { + content->resizeToWidth(width); + } + }, lifetime()); + + const auto inner = content->add( + object_ptr<Ui::VerticalLayout>(content)); + rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::start_with_next([=] { + auto old = inner->count(); + + const auto &shortcuts = messages->shortcuts(); + for (const auto &[_, shortcut] + : shortcuts.list | ranges::views::reverse) { + if (!shortcut.count) { + continue; + } + const auto name = shortcut.name; + AddButtonWithLabel( + inner, + rpl::single('/' + name), + tr::lng_forum_messages( + lt_count, + rpl::single(1. * shortcut.count)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto id = messages->emplaceShortcut(name); + showOther(ShortcutMessagesId(id)); + }); + if (old) { + delete inner->widgetAt(0); + --old; + } + } + while (old--) { + delete inner->widgetAt(0); + } + _count = inner->count(); + }, content->lifetime()); + + Ui::ResizeFitChild(this, content); +} + +[[nodiscard]] bool ValidShortcutName(const QString &name) { + if (name.isEmpty() || name.size() > 32) { + return false; + } + for (const auto &ch : name) { + if (!ch.isLetterOrNumber() + && (ch != '_') + && (ch != 0x200c) + && (ch != 0x00b7) + && (ch < 0x0d80 || ch > 0x0dff)) { + return false; + } + } + return true; +} + +} // namespace + +Type QuickRepliesId() { + return QuickReplies::Id(); +} + +void EditShortcutNameBox( + not_null<Ui::GenericBox*> box, + QString name, + Fn<void(QString, Fn<void()>)> submit) { + name = name.trimmed(); + const auto editing = !name.isEmpty(); + box->setTitle(editing + ? tr::lng_replies_edit_title() + : tr::lng_replies_add_title()); + box->addRow(object_ptr<Ui::FlatLabel>( + box, + (editing + ? tr::lng_replies_edit_about() + : tr::lng_replies_add_shortcut()), + st::settingsAddReplyLabel)); + const auto field = box->addRow(object_ptr<Ui::InputField>( + box, + st::settingsAddReplyField, + tr::lng_replies_add_placeholder(), + name)); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + field->selectAll(); + + const auto callback = [=] { + const auto name = field->getLastText().trimmed(); + if (!ValidShortcutName(name)) { + field->showError(); + } else { + submit(name, [weak = Ui::MakeWeak(box)] { + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + } + }; + field->submits( + ) | rpl::start_with_next(callback, field->lifetime()); + box->addButton(tr::lng_settings_save(), callback); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_quick_replies.h b/Telegram/SourceFiles/settings/business/settings_quick_replies.h new file mode 100644 index 000000000..4765c4f59 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_quick_replies.h @@ -0,0 +1,25 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "settings/settings_type.h" + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Settings { + +[[nodiscard]] Type QuickRepliesId(); + +void EditShortcutNameBox( + not_null<Ui::GenericBox*> box, + QString name, + Fn<void(QString, Fn<void()>)> submit); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp new file mode 100644 index 000000000..160b1d83b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.cpp @@ -0,0 +1,410 @@ +/* +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 "settings/business/settings_recipients_helper.h" + +#include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_chats_preview.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kAllExcept = 0; +constexpr auto kSelectedOnly = 1; + +using Flag = Data::ChatFilter::Flag; +using Flags = Data::ChatFilter::Flags; + +[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) { + using Type = Data::BusinessChatType; + return ((types & Type::Contacts) ? Flag::Contacts : Flag()) + | ((types & Type::NonContacts) ? Flag::NonContacts : Flag()) + | ((types & Type::NewChats) ? Flag::NewChats : Flag()) + | ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag()); +} + +[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) { + using Type = Data::BusinessChatType; + return ((flags & Flag::Contacts) ? Type::Contacts : Type()) + | ((flags & Flag::NonContacts) ? Type::NonContacts : Type()) + | ((flags & Flag::NewChats) ? Type::NewChats : Type()) + | ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type()); +} + +} // namespace + +void EditBusinessChats( + not_null<Window::SessionController*> window, + BusinessChatsDescriptor &&descriptor) { + const auto session = &window->session(); + const auto options = Flag::ExistingChats + | Flag::NewChats + | Flag::Contacts + | Flag::NonContacts; + auto &&peers = descriptor.current.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + auto controller = std::make_unique<EditFilterChatsListController>( + session, + (descriptor.include + ? tr::lng_filters_include_title() + : tr::lng_filters_exclude_title()), + options, + TypesToFlags(descriptor.current.types) & options, + base::flat_set<not_null<History*>>(begin(peers), end(peers)), + 100, + nullptr); + const auto rawController = controller.get(); + const auto save = descriptor.save; + auto initBox = [=](not_null<PeerListBox*> box) { + box->setCloseByOutsideClick(false); + box->addButton(tr::lng_settings_save(), crl::guard(box, [=] { + const auto peers = box->collectSelectedRows(); + auto &&users = ranges::views::all( + peers + ) | ranges::views::transform([=](not_null<PeerData*> peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + save(Data::BusinessChats{ + .types = FlagsToTypes(rawController->chosenOptions()), + .list = std::move(users), + }); + box->closeBox(); + })); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + window->show( + Box<PeerListBox>(std::move(controller), std::move(initBox))); +} + +not_null<FilterChatsPreview*> SetupBusinessChatsPreview( + not_null<Ui::VerticalLayout*> container, + not_null<rpl::variable<Data::BusinessChats>*> data) { + const auto rules = data->current(); + + const auto locked = std::make_shared<bool>(); + auto &&peers = data->current().list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + const auto preview = container->add(object_ptr<FilterChatsPreview>( + container, + TypesToFlags(data->current().types), + base::flat_set<not_null<History*>>(begin(peers), end(peers)))); + + preview->flagRemoved( + ) | rpl::start_with_next([=](Flag flag) { + *locked = true; + *data = Data::BusinessChats{ + data->current().types & ~FlagsToTypes(flag), + data->current().list + }; + *locked = false; + }, preview->lifetime()); + + preview->peerRemoved( + ) | rpl::start_with_next([=](not_null<History*> history) { + auto list = data->current().list; + list.erase( + ranges::remove(list, not_null(history->peer->asUser())), + end(list)); + + *locked = true; + *data = Data::BusinessChats{ + data->current().types, + std::move(list) + }; + *locked = false; + }, preview->lifetime()); + + data->changes( + ) | rpl::filter([=] { + return !*locked; + }) | rpl::start_with_next([=](const Data::BusinessChats &rules) { + auto &&peers = rules.list | ranges::views::transform([=]( + not_null<UserData*> user) { + return user->owner().history(user); + }); + preview->updateData( + TypesToFlags(rules.types), + base::flat_set<not_null<History*>>(begin(peers), end(peers))); + }, preview->lifetime()); + + return preview; +} + +void AddBusinessRecipientsSelector( + not_null<Ui::VerticalLayout*> container, + BusinessRecipientsSelectorDescriptor &&descriptor) { + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, std::move(descriptor.title)); + + auto &lifetime = container->lifetime(); + const auto controller = descriptor.controller; + const auto data = descriptor.data; + const auto change = [=](Fn<void(Data::BusinessRecipients&)> modify) { + auto now = data->current(); + modify(now); + *data = std::move(now); + }; + const auto ¤t = data->current(); + const auto all = current.allButExcluded || current.included.empty(); + const auto group = std::make_shared<Ui::RadiobuttonGroup>( + all ? kAllExcept : kSelectedOnly); + container->add( + object_ptr<Ui::Radiobutton>( + container, + group, + kAllExcept, + tr::lng_chatbots_all_except(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + container->add( + object_ptr<Ui::Radiobutton>( + container, + group, + kSelectedOnly, + tr::lng_chatbots_selected(tr::now), + st::settingsChatbotsAccess), + st::settingsChatbotsAccessMargins); + + Ui::AddSkip(container, st::settingsChatbotsAccessSkip); + Ui::AddDivider(container); + + const auto excludeWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container)) + )->setDuration(0); + const auto excludeInner = excludeWrap->entity(); + + Ui::AddSkip(excludeInner); + Ui::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title()); + const auto excludeAdd = AddButtonWithIcon( + excludeInner, + tr::lng_chatbots_exclude_button(), + st::settingsChatbotsAdd, + { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); + excludeAdd->setClickedCallback([=] { + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.excluded = std::move(value); + }); + }; + EditBusinessChats(controller, { + .current = data->current().excluded, + .save = crl::guard(excludeAdd, save), + .include = false, + }); + }); + + const auto excluded = lifetime.make_state< + rpl::variable<Data::BusinessChats> + >(data->current().excluded); + data->changes( + ) | rpl::start_with_next([=](const Data::BusinessRecipients &value) { + *excluded = value.excluded; + }, lifetime); + excluded->changes( + ) | rpl::start_with_next([=](Data::BusinessChats &&value) { + auto now = data->current(); + now.excluded = std::move(value); + *data = std::move(now); + }, lifetime); + + SetupBusinessChatsPreview(excludeInner, excluded); + + excludeWrap->toggleOn(data->value( + ) | rpl::map([](const Data::BusinessRecipients &value) { + return value.allButExcluded; + })); + excludeWrap->finishAnimating(); + + const auto includeWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container)) + )->setDuration(0); + const auto includeInner = includeWrap->entity(); + + Ui::AddSkip(includeInner); + Ui::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title()); + const auto includeAdd = AddButtonWithIcon( + includeInner, + tr::lng_chatbots_include_button(), + st::settingsChatbotsAdd, + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + includeAdd->setClickedCallback([=] { + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + }; + EditBusinessChats(controller, { + .current = data->current().included , + .save = crl::guard(includeAdd, save), + .include = true, + }); + }); + + const auto included = lifetime.make_state< + rpl::variable<Data::BusinessChats> + >(data->current().included); + data->changes( + ) | rpl::start_with_next([=](const Data::BusinessRecipients &value) { + *included = value.included; + }, lifetime); + included->changes( + ) | rpl::start_with_next([=](Data::BusinessChats &&value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + }, lifetime); + + SetupBusinessChatsPreview(includeInner, included); + included->value( + ) | rpl::start_with_next([=](const Data::BusinessChats &value) { + if (value.empty() && group->current() == kSelectedOnly) { + group->setValue(kAllExcept); + } + }, lifetime); + + includeWrap->toggleOn(data->value( + ) | rpl::map([](const Data::BusinessRecipients &value) { + return !value.allButExcluded; + })); + includeWrap->finishAnimating(); + + group->setChangedCallback([=](int value) { + if (value == kSelectedOnly && data->current().included.empty()) { + group->setValue(kAllExcept); + const auto save = [=](Data::BusinessChats value) { + change([&](Data::BusinessRecipients &data) { + data.included = std::move(value); + }); + group->setValue(kSelectedOnly); + }; + EditBusinessChats(controller, { + .save = crl::guard(includeAdd, save), + .include = true, + }); + return; + } + change([&](Data::BusinessRecipients &data) { + data.allButExcluded = (value == kAllExcept); + }); + }); +} + +int ShortcutsCount(not_null<Main::Session*> session) { + const auto &shortcuts = session->data().shortcutMessages().shortcuts(); + auto result = 0; + for (const auto &[_, shortcut] : shortcuts.list) { + if (shortcut.count > 0) { + ++result; + } + } + return result; +} + +rpl::producer<int> ShortcutsCountValue(not_null<Main::Session*> session) { + const auto messages = &session->data().shortcutMessages(); + return rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::map([=] { + return ShortcutsCount(session); + }); +} + +int ShortcutMessagesCount( + not_null<Main::Session*> session, + const QString &name) { + const auto &shortcuts = session->data().shortcutMessages().shortcuts(); + for (const auto &[_, shortcut] : shortcuts.list) { + if (shortcut.name == name) { + return shortcut.count; + } + } + return 0; +} + +rpl::producer<int> ShortcutMessagesCountValue( + not_null<Main::Session*> session, + const QString &name) { + const auto messages = &session->data().shortcutMessages(); + return rpl::single(rpl::empty) | rpl::then( + messages->shortcutsChanged() + ) | rpl::map([=] { + return ShortcutMessagesCount(session, name); + }); +} + +bool ShortcutExists(not_null<Main::Session*> session, const QString &name) { + return ShortcutMessagesCount(session, name) > 0; +} + +rpl::producer<bool> ShortcutExistsValue( + not_null<Main::Session*> session, + const QString &name) { + return ShortcutMessagesCountValue(session, name) + | rpl::map(rpl::mappers::_1 > 0); +} + +int ShortcutsLimit(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->get<int>("quick_replies_limit", 100); +} + +rpl::producer<int> ShortcutsLimitValue(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->value() | rpl::map([=] { + return ShortcutsLimit(session); + }); +} + +int ShortcutMessagesLimit(not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->get<int>("quick_reply_messages_limit", 20); +} + +rpl::producer<int> ShortcutMessagesLimitValue( + not_null<Main::Session*> session) { + const auto appConfig = &session->account().appConfig(); + return appConfig->value() | rpl::map([=] { + return ShortcutMessagesLimit(session); + }); +} + +BusinessShortcutId LookupShortcutId( + not_null<Main::Session*> session, + const QString &name) { + const auto messages = &session->data().shortcutMessages(); + for (const auto &[id, shortcut] : messages->shortcuts().list) { + if (shortcut.name == name) { + return id; + } + } + return {}; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_recipients_helper.h b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h new file mode 100644 index 000000000..f4432ea3b --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_recipients_helper.h @@ -0,0 +1,100 @@ +/* +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" +#include "settings/settings_common_session.h" + +class FilterChatsPreview; + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +template <typename SectionType> +class BusinessSection : public Section<SectionType> { +public: + BusinessSection( + QWidget *parent, + not_null<Window::SessionController*> controller) + : Section<SectionType>(parent) + , _controller(controller) { + } + + [[nodiscard]] not_null<Window::SessionController*> controller() const { + return _controller; + } + [[nodiscard]] rpl::producer<> showFinishes() const { + return _showFinished.events(); + } + +private: + void showFinished() override { + _showFinished.fire({}); + } + + const not_null<Window::SessionController*> _controller; + rpl::event_stream<> _showFinished; + +}; + +struct BusinessChatsDescriptor { + Data::BusinessChats current; + Fn<void(const Data::BusinessChats&)> save; + bool include = false; +}; +void EditBusinessChats( + not_null<Window::SessionController*> window, + BusinessChatsDescriptor &&descriptor); + +not_null<FilterChatsPreview*> SetupBusinessChatsPreview( + not_null<Ui::VerticalLayout*> container, + not_null<rpl::variable<Data::BusinessChats>*> data); + +struct BusinessRecipientsSelectorDescriptor { + not_null<Window::SessionController*> controller; + rpl::producer<QString> title; + not_null<rpl::variable<Data::BusinessRecipients>*> data; +}; +void AddBusinessRecipientsSelector( + not_null<Ui::VerticalLayout*> container, + BusinessRecipientsSelectorDescriptor &&descriptor); + +[[nodiscard]] int ShortcutsCount(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutsCountValue( + not_null<Main::Session*> session); +[[nodiscard]] int ShortcutMessagesCount( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] rpl::producer<int> ShortcutMessagesCountValue( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] bool ShortcutExists( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] rpl::producer<bool> ShortcutExistsValue( + not_null<Main::Session*> session, + const QString &name); +[[nodiscard]] int ShortcutsLimit(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutsLimitValue( + not_null<Main::Session*> session); +[[nodiscard]] int ShortcutMessagesLimit(not_null<Main::Session*> session); +[[nodiscard]] rpl::producer<int> ShortcutMessagesLimitValue( + not_null<Main::Session*> session); + +[[nodiscard]] BusinessShortcutId LookupShortcutId( + not_null<Main::Session*> session, + const QString &name); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp new file mode 100644 index 000000000..e1fcec944 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -0,0 +1,1608 @@ +/* +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 "settings/business/settings_shortcut_messages.h" + +#include "api/api_editing.h" +#include "api/api_sending.h" +#include "apiwrap.h" +#include "base/call_delayed.h" +#include "boxes/delete_messages_box.h" +#include "boxes/premium_limits_box.h" +#include "boxes/premium_preview_box.h" +#include "boxes/send_files_box.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/file_utilities.h" +#include "core/mime_type.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_message_reaction_id.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/view/controls/compose_controls_common.h" +#include "history/view/controls/history_view_compose_controls.h" +#include "history/view/history_view_corner_buttons.h" +#include "history/view/history_view_empty_list_bubble.h" +#include "history/view/history_view_list_widget.h" +#include "history/view/history_view_service_message.h" +#include "history/view/history_view_sticker_toast.h" +#include "history/history.h" +#include "history/history_item.h" +#include "info/info_wrap_widget.h" +#include "inline_bots/inline_bot_result.h" +#include "lang/lang_keys.h" +#include "lang/lang_numbers_animation.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "menu/menu_send.h" +#include "settings/business/settings_quick_replies.h" +#include "settings/business/settings_recipients_helper.h" +#include "storage/localimageloader.h" +#include "storage/storage_account.h" +#include "storage/storage_media_prepare.h" +#include "storage/storage_shared_media.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/attach/attach_send_files_way.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/controls/jump_down_button.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/scroll_area.h" +#include "ui/painter.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_chat.h" +#include "styles/style_menu_icons.h" +#include "styles/style_layers.h" + +namespace Settings { +namespace { + +using namespace HistoryView; + +class ShortcutMessages + : public AbstractSection + , private ListDelegate + , private CornerButtonsDelegate { +public: + ShortcutMessages( + QWidget *parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue, + BusinessShortcutId shortcutId); + ~ShortcutMessages(); + + [[nodiscard]] static Type Id(BusinessShortcutId shortcutId); + + [[nodiscard]] Type id() const final override { + return Id(_shortcutId.current()); + } + + [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] rpl::producer<> sectionShowBack() override; + void setInnerFocus() override; + + rpl::producer<Info::SelectedItems> selectedListValue() override; + void selectionAction(Info::SelectionAction action) override; + void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override; + + bool paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) override; + +private: + void outerResized(); + void updateComposeControlsPosition(); + + // ListDelegate interface. + Context listContext() override; + bool listScrollTo(int top, bool syntetic = true) override; + void listCancelRequest() override; + void listDeleteRequest() override; + void listTryProcessKeyInput(not_null<QKeyEvent*> e) override; + rpl::producer<Data::MessagesSlice> listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) override; + bool listAllowsMultiSelect() override; + bool listIsItemGoodForSelection(not_null<HistoryItem*> item) override; + bool listIsLessInOrder( + not_null<HistoryItem*> first, + not_null<HistoryItem*> second) override; + void listSelectionChanged(SelectedItems &&items) override; + void listMarkReadTill(not_null<HistoryItem*> item) override; + void listMarkContentsRead( + const base::flat_set<not_null<HistoryItem*>> &items) override; + MessagesBarData listMessagesBar( + const std::vector<not_null<Element*>> &elements) override; + void listContentRefreshed() override; + void listUpdateDateLink( + ClickHandlerPtr &link, + not_null<Element*> view) override; + bool listElementHideReply(not_null<const Element*> view) override; + bool listElementShownUnread(not_null<const Element*> view) override; + bool listIsGoodForAroundPosition( + not_null<const Element *> view) override; + void listSendBotCommand( + const QString &command, + const FullMsgId &context) override; + void listSearch( + const QString &query, + const FullMsgId &context) override; + void listHandleViaClick(not_null<UserData*> bot) override; + not_null<Ui::ChatTheme*> listChatTheme() override; + CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; + CopyRestrictionType listCopyMediaRestrictionType( + not_null<HistoryItem*> item) override; + CopyRestrictionType listSelectRestrictionType() override; + auto listAllowedReactionsValue() + -> rpl::producer<Data::AllowedReactions> override; + void listShowPremiumToast(not_null<DocumentData*> document) override; + void listOpenPhoto( + not_null<PhotoData*> photo, + FullMsgId context) override; + void listOpenDocument( + not_null<DocumentData*> document, + FullMsgId context, + bool showInMediaView) override; + void listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) override; + QString listElementAuthorRank(not_null<const Element*> view) override; + History *listTranslateHistory() override; + void listAddTranslatedItems( + not_null<TranslateTracker*> tracker) override; + + // CornerButtonsDelegate delegate. + void cornerButtonsShowAtPosition( + Data::MessagePosition position) override; + Data::Thread *cornerButtonsThread() override; + FullMsgId cornerButtonsCurrentId() override; + bool cornerButtonsIgnoreVisibility() override; + std::optional<bool> cornerButtonsDownShown() override; + bool cornerButtonsUnreadMayBeShown() override; + bool cornerButtonsHas(CornerButtonType type) override; + + QPointer<Ui::RpWidget> createPinnedToBottom( + not_null<Ui::RpWidget*> parent) override; + void setupComposeControls(); + void processScroll(); + void updateInnerVisibleArea(); + + void checkReplyReturns(); + void confirmDeleteSelected(); + void clearSelected(); + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional<bool> overrideSendImagesAsPhotos = std::nullopt, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + const QStringList &files, + const QString &insertTextOnCancel); + bool confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + not_null<const QMimeData*> data, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Ui::PreparedList &list) const; + bool showSendingFilesError( + const Ui::PreparedList &list, + std::optional<bool> compress) const; + void sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter); + + void sendExistingDocument(not_null<DocumentData*> document); + bool sendExistingDocument( + not_null<DocumentData*> document, + Api::SendOptions options, + std::optional<MsgId> localId); + void sendExistingPhoto(not_null<PhotoData*> photo); + bool sendExistingPhoto( + not_null<PhotoData*> photo, + Api::SendOptions options); + void sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot); + void sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot, + Api::SendOptions options, + std::optional<MsgId> localMessageId); + + [[nodiscard]] Api::SendAction prepareSendAction( + Api::SendOptions options) const; + void send(); + void send(Api::SendOptions options); + void sendVoice(Controls::VoiceToSend &&data); + void edit( + not_null<HistoryItem*> item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId); + void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos); + [[nodiscard]] SendMenu::Type sendMenuType() const; + [[nodiscard]] FullReplyTo replyTo() const; + void doSetInnerFocus(); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId = {}); + void showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms); + void showAtEnd(); + void finishSending(); + void refreshEmptyText(); + bool showPremiumRequired() const; + + const not_null<Window::SessionController*> _controller; + const not_null<Main::Session*> _session; + const not_null<Ui::ScrollArea*> _scroll; + const not_null<History*> _history; + rpl::variable<BusinessShortcutId> _shortcutId; + rpl::variable<QString> _shortcut; + rpl::variable<Container> _container; + rpl::variable<int> _count; + std::shared_ptr<Ui::ChatStyle> _style; + std::shared_ptr<Ui::ChatTheme> _theme; + QPointer<ListWidget> _inner; + std::unique_ptr<Ui::RpWidget> _controlsWrap; + std::unique_ptr<ComposeControls> _composeControls; + rpl::event_stream<> _showBackRequests; + bool _skipScrollEvent = false; + + QSize _inOuterResize; + QSize _pendingOuterResize; + + const style::icon *_emptyIcon = nullptr; + Ui::Text::String _emptyText; + int _emptyTextWidth = 0; + int _emptyTextHeight = 0; + + rpl::variable<Info::SelectedItems> _selectedItems + = Info::SelectedItems(Storage::SharedMediaType::kCount); + + std::unique_ptr<StickerToast> _stickerToast; + + FullMsgId _lastShownAt; + CornerButtons _cornerButtons; + + Data::MessagesSlice _lastSlice; + bool _choosingAttach = false; + +}; + +struct Factory final : AbstractSectionFactory { + explicit Factory(BusinessShortcutId shortcutId) + : shortcutId(shortcutId) { + } + + object_ptr<AbstractSection> create( + not_null<QWidget*> parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue + ) const final override { + return object_ptr<ShortcutMessages>( + parent, + controller, + scroll, + std::move(containerValue), + shortcutId); + } + + const BusinessShortcutId shortcutId = {}; +}; + +[[nodiscard]] bool IsAway(const QString &shortcut) { + return (shortcut == u"away"_q); +} + +[[nodiscard]] bool IsGreeting(const QString &shortcut) { + return (shortcut == u"hello"_q); +} + +ShortcutMessages::ShortcutMessages( + QWidget *parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue, + BusinessShortcutId shortcutId) +: AbstractSection(parent) +, _controller(controller) +, _session(&controller->session()) +, _scroll(scroll) +, _history(_session->data().history(_session->user()->id)) +, _shortcutId(shortcutId) +, _shortcut( + _session->data().shortcutMessages().lookupShortcut(shortcutId).name) +, _container(std::move(containerValue)) +, _cornerButtons( + _scroll, + controller->chatStyle(), + static_cast<HistoryView::CornerButtonsDelegate*>(this)) { + const auto messages = &_session->data().shortcutMessages(); + + messages->shortcutIdChanged( + ) | rpl::start_with_next([=](Data::ShortcutIdChange change) { + if (change.oldId == _shortcutId.current()) { + if (change.newId) { + _shortcutId = change.newId; + } else { + _showBackRequests.fire({}); + } + } + }, lifetime()); + messages->shortcutsChanged( + ) | rpl::start_with_next([=] { + _shortcut = messages->lookupShortcut(_shortcutId.current()).name; + }, lifetime()); + + controller->chatStyle()->paletteChanged( + ) | rpl::start_with_next([=] { + _scroll->updateBars(); + }, _scroll->lifetime()); + + _style = std::make_shared<Ui::ChatStyle>(_session->colorIndicesValue()); + _theme = std::shared_ptr<Ui::ChatTheme>( + Window::Theme::DefaultChatThemeOn(lifetime())); + + _inner = Ui::CreateChild<ListWidget>( + this, + controller, + static_cast<ListDelegate*>(this)); + _inner->overrideIsChatWide(false); + + _scroll->sizeValue() | rpl::filter([](QSize size) { + return !size.isEmpty(); + }) | rpl::start_with_next([=] { + outerResized(); + }, lifetime()); + + _scroll->scrolls( + ) | rpl::start_with_next([=] { + processScroll(); + }, lifetime()); + + _shortcut.value() | rpl::start_with_next([=] { + refreshEmptyText(); + _inner->update(); + }, lifetime()); + + _inner->editMessageRequested( + ) | rpl::start_with_next([=](auto fullId) { + if (const auto item = _session->data().message(fullId)) { + const auto media = item->media(); + if (!media || media->webpage() || media->allowsEditCaption()) { + _composeControls->editMessage(fullId); + } + } + }, _inner->lifetime()); + + _inner->heightValue() | rpl::start_with_next([=](int height) { + resize(width(), height); + }, lifetime()); +} + +ShortcutMessages::~ShortcutMessages() = default; + +void ShortcutMessages::refreshEmptyText() { + const auto &shortcut = _shortcut.current(); + const auto away = IsAway(shortcut); + const auto greeting = !away && IsGreeting(shortcut); + auto text = away + ? tr::lng_away_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_away_empty_about(tr::now)) + : greeting + ? tr::lng_greeting_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_greeting_empty_about(tr::now)) + : tr::lng_replies_empty_title( + tr::now, + Ui::Text::Bold + ).append("\n\n").append(tr::lng_replies_empty_about( + tr::now, + lt_shortcut, + Ui::Text::Bold('/' + shortcut), + Ui::Text::WithEntities)); + _emptyIcon = away + ? &st::awayEmptyIcon + : greeting + ? &st::greetingEmptyIcon + : &st::repliesEmptyIcon; + const auto padding = st::repliesEmptyPadding; + const auto minWidth = st::repliesEmptyWidth / 4; + const auto maxWidth = std::max( + minWidth + 1, + st::repliesEmptyWidth - padding.left() - padding.right()); + _emptyText = Ui::Text::String( + st::messageTextStyle, + text, + kMarkupTextOptions, + minWidth); + const auto countHeight = [&](int width) { + return _emptyText.countHeight(width); + }; + _emptyTextWidth = Ui::FindNiceTooltipWidth( + minWidth, + maxWidth, + countHeight); + _emptyTextHeight = countHeight(_emptyTextWidth); +} + +Type ShortcutMessages::Id(BusinessShortcutId shortcutId) { + return std::make_shared<Factory>(shortcutId); +} + +rpl::producer<QString> ShortcutMessages::title() { + return _shortcut.value() | rpl::map([=](const QString &shortcut) { + return IsAway(shortcut) + ? tr::lng_away_title() + : IsGreeting(shortcut) + ? tr::lng_greeting_title() + : rpl::single('/' + shortcut); + }) | rpl::flatten_latest(); +} + +void ShortcutMessages::processScroll() { + if (_skipScrollEvent) { + return; + } + updateInnerVisibleArea(); +} + +void ShortcutMessages::updateInnerVisibleArea() { + if (!_inner->animatedScrolling()) { + checkReplyReturns(); + } + const auto scrollTop = _scroll->scrollTop(); + _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); +} + +rpl::producer<> ShortcutMessages::sectionShowBack() { + return _showBackRequests.events(); +} + +void ShortcutMessages::setInnerFocus() { + _composeControls->focus(); +} + +rpl::producer<Info::SelectedItems> ShortcutMessages::selectedListValue() { + return _selectedItems.value(); +} + +void ShortcutMessages::selectionAction(Info::SelectionAction action) { + switch (action) { + case Info::SelectionAction::Clear: clearSelected(); return; + case Info::SelectionAction::Delete: confirmDeleteSelected(); return; + } + Unexpected("Action in ShortcutMessages::selectionAction."); +} + +void ShortcutMessages::fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + const auto owner = &_controller->session().data(); + const auto messages = &owner->shortcutMessages(); + + addAction(tr::lng_context_edit_shortcut(tr::now), [=] { + if (!_controller->session().premium()) { + ShowPremiumPreviewToBuy( + _controller, + PremiumFeature::QuickReplies); + return; + } + const auto submit = [=](QString name, Fn<void()> close) { + const auto id = _shortcutId.current(); + const auto error = [=](QString text) { + if (!text.isEmpty()) { + _controller->showToast((text == u"SHORTCUT_OCCUPIED"_q) + ? tr::lng_replies_error_occupied(tr::now) + : text); + } + }; + messages->editShortcut(id, name, close, crl::guard(this, error)); + }; + const auto name = _shortcut.current(); + _controller->show( + Box(EditShortcutNameBox, name, crl::guard(this, submit))); + }, &st::menuIconEdit); + + const auto justDelete = crl::guard(this, [=] { + messages->removeShortcut(_shortcutId.current()); + }); + const auto confirmDeleteShortcut = [=] { + const auto slice = messages->list(_shortcutId.current()); + if (slice.fullCount == 0) { + justDelete(); + } else { + const auto confirmed = [=](Fn<void()> close) { + justDelete(); + close(); + }; + _controller->show(Ui::MakeConfirmBox({ + .text = { tr::lng_replies_delete_sure() }, + .confirmed = confirmed, + .confirmText = tr::lng_box_delete(), + .confirmStyle = &st::attentionBoxButton, + })); + } + }; + addAction({ + .text = tr::lng_context_delete_shortcut(tr::now), + .handler = crl::guard(this, confirmDeleteShortcut), + .icon = &st::menuIconDeleteAttention, + .isAttention = true, + }); +} + +bool ShortcutMessages::paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) { + Window::SectionWidget::PaintBackground( + _theme.get(), + outer, + std::max(outer->height(), maxVisibleHeight), + 0, + clip); + return true; +} + +void ShortcutMessages::outerResized() { + const auto outer = _scroll->size(); + if (!_inOuterResize.isEmpty()) { + _pendingOuterResize = (_inOuterResize != outer) + ? outer + : QSize(); + return; + } + _inOuterResize = outer; + + do { + const auto newScrollTop = _scroll->isHidden() + ? std::nullopt + : _scroll->scrollTop() + ? base::make_optional(_scroll->scrollTop()) + : 0; + _skipScrollEvent = true; + const auto minHeight = (_container.current() == Container::Layer) + ? st::boxWidth + : _inOuterResize.height(); + _inner->resizeToWidth(_inOuterResize.width(), minHeight); + _skipScrollEvent = false; + + if (!_scroll->isHidden() && newScrollTop) { + _scroll->scrollToY(*newScrollTop); + } + _inOuterResize = base::take(_pendingOuterResize); + } while (!_inOuterResize.isEmpty()); + + if (!_scroll->isHidden()) { + updateInnerVisibleArea(); + } + updateComposeControlsPosition(); + _cornerButtons.updatePositions(); +} + +void ShortcutMessages::updateComposeControlsPosition() { + const auto bottom = _scroll->parentWidget()->height(); + const auto controlsHeight = _composeControls->heightCurrent(); + _composeControls->move(0, bottom - controlsHeight + st::boxRadius); + _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); +} + +void ShortcutMessages::setupComposeControls() { + _shortcutId.value() | rpl::start_with_next([=](BusinessShortcutId id) { + _composeControls->updateShortcutId(id); + }, lifetime()); + + const auto state = Dialogs::EntryState{ + .key = Dialogs::Key{ _history }, + .section = Dialogs::EntryState::Section::ShortcutMessages, + .currentReplyTo = replyTo(), + }; + _composeControls->setCurrentDialogsEntryState(state); + + auto writeRestriction = rpl::combine( + _count.value(), + ShortcutMessagesLimitValue(_session) + ) | rpl::map([=](int count, int limit) { + return (count >= limit) + ? Controls::WriteRestriction{ + .text = tr::lng_business_limit_reached( + tr::now, + lt_count, + limit), + .type = Controls::WriteRestrictionType::Rights, + } : Controls::WriteRestriction(); + }); + _composeControls->setHistory({ + .history = _history.get(), + .writeRestriction = std::move(writeRestriction), + }); + + _composeControls->cancelRequests( + ) | rpl::start_with_next([=] { + listCancelRequest(); + }, lifetime()); + + _composeControls->sendRequests( + ) | rpl::start_with_next([=] { + send(); + }, lifetime()); + + _composeControls->sendVoiceRequests( + ) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) { + sendVoice(std::move(data)); + }, lifetime()); + + _composeControls->sendCommandRequests( + ) | rpl::start_with_next([=](const QString &command) { + listSendBotCommand(command, FullMsgId()); + }, lifetime()); + + const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0); + _composeControls->editRequests( + ) | rpl::start_with_next([=](auto data) { + if (const auto item = _session->data().message(data.fullId)) { + if (item->isBusinessShortcut()) { + edit(item, data.options, saveEditMsgRequestId); + } + } + }, lifetime()); + + _composeControls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=](std::optional<bool> overrideCompress) { + _choosingAttach = true; + base::call_delayed(st::historyAttach.ripple.hideDuration, this, [=] { + _choosingAttach = false; + chooseAttach(overrideCompress); + }); + }, lifetime()); + + _composeControls->fileChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + _controller->hideLayer(anim::type::normal); + sendExistingDocument(data.document); + }, lifetime()); + + _composeControls->photoChosen( + ) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) { + sendExistingPhoto(chosen.photo); + }, lifetime()); + + _composeControls->inlineResultChosen( + ) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) { + sendInlineResult(chosen.result, chosen.bot); + }, lifetime()); + + _composeControls->jumpToItemRequests( + ) | rpl::start_with_next([=](FullReplyTo to) { + if (const auto item = _session->data().message(to.messageId)) { + showAtPosition(item->position()); + } + }, lifetime()); + + _composeControls->scrollKeyEvents( + ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) { + _scroll->keyPressEvent(e); + }, lifetime()); + + _composeControls->editLastMessageRequests( + ) | rpl::start_with_next([=](not_null<QKeyEvent*> e) { + if (!_inner->lastMessageEditRequestNotify()) { + _scroll->keyPressEvent(e); + } + }, lifetime()); + + _composeControls->setMimeDataHook([=]( + not_null<const QMimeData*> data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return Core::CanSendFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + return confirmSendingFiles( + data, + std::nullopt, + Core::ReadMimeText(data)); + } + Unexpected("action in MimeData hook."); + }); + + _composeControls->lockShowStarts( + ) | rpl::start_with_next([=] { + _cornerButtons.updateJumpDownVisibility(); + _cornerButtons.updateUnreadThingsVisibility(); + }, lifetime()); + + _composeControls->viewportEvents( + ) | rpl::start_with_next([=](not_null<QEvent*> e) { + _scroll->viewportEvent(e); + }, lifetime()); + + _controlsWrap->widthValue() | rpl::start_with_next([=](int width) { + _composeControls->resizeToWidth(width); + }, _controlsWrap->lifetime()); + + _composeControls->height( + ) | rpl::start_with_next([=](int height) { + const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop()); + _controlsWrap->resize(width(), height - st::boxRadius); + updateComposeControlsPosition(); + if (wasMax) { + listScrollTo(_scroll->scrollTopMax()); + } + }, lifetime()); +} + +QPointer<Ui::RpWidget> ShortcutMessages::createPinnedToBottom( + not_null<Ui::RpWidget*> parent) { + auto placeholder = rpl::deferred([=] { + return _shortcutId.value(); + }) | rpl::map([=](BusinessShortcutId id) { + return _session->data().shortcutMessages().lookupShortcut(id).name; + }) | rpl::map([=](const QString &shortcut) { + return (shortcut == u"away"_q) + ? tr::lng_away_message_placeholder() + : (shortcut == u"hello"_q) + ? tr::lng_greeting_message_placeholder() + : tr::lng_replies_message_placeholder(); + }) | rpl::flatten_latest(); + + _controlsWrap = std::make_unique<Ui::RpWidget>(parent); + _composeControls = std::make_unique<ComposeControls>( + dynamic_cast<Ui::RpWidget*>(_scroll->parentWidget()), + ComposeControlsDescriptor{ + .stOverride = &st::repliesComposeControls, + .show = _controller->uiShow(), + .unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) { + listShowPremiumToast(emoji); + }, + .mode = HistoryView::ComposeControlsMode::Normal, + .sendMenuType = SendMenu::Type::Disabled, + .regularWindow = _controller, + .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), + .customPlaceholder = std::move(placeholder), + .panelsLevel = Window::GifPauseReason::Layer, + .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), + .voiceLockFromBottom = true, + .features = { + .sendAs = false, + .ttlInfo = false, + .botCommandSend = false, + .silentBroadcastToggle = false, + .attachBotsMenu = false, + .megagroupSet = false, + .commonTabbedPanel = false, + }, + }); + + setupComposeControls(); + + showAtEnd(); + + return _controlsWrap.get(); +} + +Context ShortcutMessages::listContext() { + return Context::ShortcutMessages; +} + +bool ShortcutMessages::listScrollTo(int top, bool syntetic) { + top = std::clamp(top, 0, _scroll->scrollTopMax()); + if (_scroll->scrollTop() == top) { + updateInnerVisibleArea(); + return false; + } + _scroll->scrollToY(top); + return true; +} + +void ShortcutMessages::listCancelRequest() { + if (_inner && !_inner->getSelectedItems().empty()) { + clearSelected(); + return; + } else if (_composeControls->handleCancelRequest()) { + return; + } + _showBackRequests.fire({}); +} + +void ShortcutMessages::listDeleteRequest() { + confirmDeleteSelected(); +} + +void ShortcutMessages::listTryProcessKeyInput(not_null<QKeyEvent*> e) { + _composeControls->tryProcessKeyInput(e); +} + +rpl::producer<Data::MessagesSlice> ShortcutMessages::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto messages = &_session->data().shortcutMessages(); + return _shortcutId.value( + ) | rpl::map([=](BusinessShortcutId shortcutId) { + return rpl::single(rpl::empty) | rpl::then( + messages->updates(shortcutId) + ) | rpl::map([=] { + return messages->list(shortcutId); + }); + }) | rpl::flatten_latest( + ) | rpl::after_next([=](const Data::MessagesSlice &slice) { + _count = slice.fullCount.value_or( + messages->count(_shortcutId.current())); + }); +} + +bool ShortcutMessages::listAllowsMultiSelect() { + return true; +} + +bool ShortcutMessages::listIsItemGoodForSelection( + not_null<HistoryItem*> item) { + return !item->isSending() && !item->hasFailed(); +} + +bool ShortcutMessages::listIsLessInOrder( + not_null<HistoryItem*> first, + not_null<HistoryItem*> second) { + return first->position() < second->position(); +} + +void ShortcutMessages::listSelectionChanged(SelectedItems &&items) { + auto value = Info::SelectedItems(); + value.title = [](int count) { + return tr::lng_forum_messages( + tr::now, + lt_count, + count, + Ui::StringWithNumbers::FromString); + }; + value.list = items | ranges::views::transform([](SelectedItem item) { + auto result = Info::SelectedItem(GlobalMsgId{ item.msgId }); + result.canDelete = item.canDelete; + return result; + }) | ranges::to_vector; + _selectedItems = std::move(value); + + if (items.empty()) { + doSetInnerFocus(); + } +} + +void ShortcutMessages::listMarkReadTill(not_null<HistoryItem*> item) { +} + +void ShortcutMessages::listMarkContentsRead( + const base::flat_set<not_null<HistoryItem*>> &items) { +} + +MessagesBarData ShortcutMessages::listMessagesBar( + const std::vector<not_null<Element*>> &elements) { + return {}; +} + +void ShortcutMessages::listContentRefreshed() { +} + +void ShortcutMessages::listUpdateDateLink( + ClickHandlerPtr &link, + not_null<Element*> view) { +} + +bool ShortcutMessages::listElementHideReply(not_null<const Element*> view) { + return false; +} + +bool ShortcutMessages::listElementShownUnread(not_null<const Element*> view) { + return true; +} + +bool ShortcutMessages::listIsGoodForAroundPosition( + not_null<const Element*> view) { + return true; +} + +void ShortcutMessages::listSendBotCommand( + const QString &command, + const FullMsgId &context) { +} + +void ShortcutMessages::listSearch( + const QString &query, + const FullMsgId &context) { + const auto inChat = _history->peer->isUser() + ? Dialogs::Key() + : Dialogs::Key(_history); + _controller->searchMessages(query, inChat); +} + +void ShortcutMessages::listHandleViaClick(not_null<UserData*> bot) { + _composeControls->setText({ '@' + bot->username() + ' ' }); +} + +not_null<Ui::ChatTheme*> ShortcutMessages::listChatTheme() { + return _theme.get(); +} + +CopyRestrictionType ShortcutMessages::listCopyRestrictionType( + HistoryItem *item) { + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listCopyMediaRestrictionType( + not_null<HistoryItem*> item) { + if (const auto media = item->media()) { + if (const auto invoice = media->invoice()) { + if (invoice->extendedMedia) { + return CopyMediaRestrictionTypeFor(_history->peer, item); + } + } + } + return CopyRestrictionType::None; +} + +CopyRestrictionType ShortcutMessages::listSelectRestrictionType() { + return CopyRestrictionType::None; +} + +auto ShortcutMessages::listAllowedReactionsValue() +-> rpl::producer<Data::AllowedReactions> { + return rpl::single(Data::AllowedReactions()); +} + +void ShortcutMessages::listShowPremiumToast( + not_null<DocumentData*> document) { + if (!_stickerToast) { + _stickerToast = std::make_unique<HistoryView::StickerToast>( + _controller, + this, + [=] { _stickerToast = nullptr; }); + } + _stickerToast->showFor(document); +} + +void ShortcutMessages::listOpenPhoto( + not_null<PhotoData*> photo, + FullMsgId context) { + _controller->openPhoto(photo, { context }); +} + +void ShortcutMessages::listOpenDocument( + not_null<DocumentData*> document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, showInMediaView, { context }); +} + +void ShortcutMessages::listPaintEmpty( + Painter &p, + const Ui::ChatPaintContext &context) { + Expects(_emptyIcon != nullptr); + + const auto width = st::repliesEmptyWidth; + const auto padding = st::repliesEmptyPadding; + const auto height = padding.top() + + _emptyIcon->height() + + st::repliesEmptySkip + + _emptyTextHeight + + padding.bottom(); + const auto r = QRect( + (this->width() - width) / 2, + (this->height() - height) / 3, + width, + height); + HistoryView::ServiceMessagePainter::PaintBubble(p, context.st, r); + + _emptyIcon->paint( + p, + r.x() + (r.width() - _emptyIcon->width()) / 2, + r.y() + padding.top(), + this->width()); + p.setPen(st::msgServiceFg); + _emptyText.draw( + p, + r.x() + (r.width() - _emptyTextWidth) / 2, + r.y() + padding.top() + _emptyIcon->height() + st::repliesEmptySkip, + _emptyTextWidth, + style::al_top); +} + +QString ShortcutMessages::listElementAuthorRank( + not_null<const Element*> view) { + return {}; +} + +History *ShortcutMessages::listTranslateHistory() { + return nullptr; +} + +void ShortcutMessages::listAddTranslatedItems( + not_null<TranslateTracker*> tracker) { +} + +void ShortcutMessages::cornerButtonsShowAtPosition( + Data::MessagePosition position) { + showAtPosition(position); +} + +Data::Thread *ShortcutMessages::cornerButtonsThread() { + return _history; +} + +FullMsgId ShortcutMessages::cornerButtonsCurrentId() { + return _lastShownAt; +} + +bool ShortcutMessages::cornerButtonsIgnoreVisibility() { + return false;// animatingShow(); +} + +std::optional<bool> ShortcutMessages::cornerButtonsDownShown() { + if (_composeControls->isLockPresent() + || _composeControls->isTTLButtonShown()) { + return false; + } + const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; + if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { + return true; + } else if (_inner->loadedAtBottomKnown()) { + return !_inner->loadedAtBottom(); + } + return std::nullopt; +} + +bool ShortcutMessages::cornerButtonsUnreadMayBeShown() { + return _inner->loadedAtBottomKnown() + && !_composeControls->isLockPresent() + && !_composeControls->isTTLButtonShown(); +} + +bool ShortcutMessages::cornerButtonsHas(CornerButtonType type) { + return (type == CornerButtonType::Down); +} + +void ShortcutMessages::checkReplyReturns() { + const auto currentTop = _scroll->scrollTop(); + const auto shortcutId = _shortcutId.current(); + while (const auto replyReturn = _cornerButtons.replyReturn()) { + const auto position = replyReturn->position(); + const auto scrollTop = _inner->scrollTopForPosition(position); + const auto below = scrollTop + ? (currentTop >= std::min(*scrollTop, _scroll->scrollTopMax())) + : _inner->isBelowPosition(position); + if (replyReturn->shortcutId() != shortcutId || below) { + _cornerButtons.calculateNextReplyReturn(); + } else { + break; + } + } +} + +void ShortcutMessages::confirmDeleteSelected() { + ConfirmDeleteSelectedItems(_inner); +} + +void ShortcutMessages::clearSelected() { + _inner->cancelSelection(); +} + +void ShortcutMessages::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + _session->api().sendFile(fileContent, type, prepareSendAction({})); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list) const { + return showSendingFilesError(list, std::nullopt); +} + +bool ShortcutMessages::showSendingFilesError( + const Ui::PreparedList &list, + std::optional<bool> compress) const { + if (showPremiumRequired()) { + return true; + } + const auto text = [&] { + using Error = Ui::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return u"(toolarge)"_q; + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } else if (text == u"(toolarge)"_q) { + const auto fileSize = list.files.back().size; + _controller->show( + Box(FileSizeLimitBox, _session, fileSize, nullptr)); + return true; + } + + _controller->showToast(text); + return true; +} + +Api::SendAction ShortcutMessages::prepareSendAction( + Api::SendOptions options) const { + auto result = Api::SendAction(_history, options); + result.replyTo = replyTo(); + result.options.shortcutId = _shortcutId.current(); + result.options.sendAs = _composeControls->sendAsPeer(); + return result; +} + +void ShortcutMessages::send() { + if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { + return; + } + send({}); +} + +void ShortcutMessages::sendVoice(ComposeControls::VoiceToSend &&data) { + if (showPremiumRequired()) { + return; + } + auto action = prepareSendAction(data.options); + _session->api().sendVoiceMessage( + data.bytes, + data.waveform, + data.duration, + std::move(action)); + + _composeControls->cancelReplyMessage(); + _composeControls->clearListenState(); + finishSending(); +} + +void ShortcutMessages::send(Api::SendOptions options) { + if (showPremiumRequired()) { + return; + } + _cornerButtons.clearReplyReturns(); + + auto message = Api::MessageToSend(prepareSendAction(options)); + message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); + message.webPage = _composeControls->webPageDraft(); + + _session->api().sendMessage(std::move(message)); + + _composeControls->clear(); + + finishSending(); +} + +void ShortcutMessages::edit( + not_null<HistoryItem*> item, + Api::SendOptions options, + mtpRequestId *const saveEditMsgRequestId) { + if (*saveEditMsgRequestId) { + return; + } + const auto webpage = _composeControls->webPageDraft(); + auto sending = TextWithEntities(); + auto left = _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 (item) { + _controller->show(Box<DeleteMessagesBox>(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; + } + + lifetime().add([=] { + if (!*saveEditMsgRequestId) { + return; + } + _session->api().request(base::take(*saveEditMsgRequestId)).cancel(); + }); + + const auto done = [=](mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + _composeControls->cancelEditMessage(); + } + }; + + const auto fail = [=](const QString &error, mtpRequestId requestId) { + if (requestId == *saveEditMsgRequestId) { + *saveEditMsgRequestId = 0; + } + + if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { + _controller->showToast(tr::lng_edit_error(tr::now)); + } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { + _composeControls->cancelEditMessage(); + } else if (error == u"MESSAGE_EMPTY"_q) { + doSetInnerFocus(); + } else { + _controller->showToast(tr::lng_edit_error(tr::now)); + } + update(); + return true; + }; + + *saveEditMsgRequestId = Api::EditTextMessage( + item, + sending, + webpage, + options, + crl::guard(this, done), + crl::guard(this, fail)); + + _composeControls->hidePanelsAnimated(); + doSetInnerFocus(); +} + +bool ShortcutMessages::confirmSendingFiles( + not_null<const QMimeData*> data, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + const auto hasImage = data->hasImage(); + const auto premium = _controller->session().user()->isPremium(); + + if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { + auto list = Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize, + premium); + if (list.error != Ui::PreparedList::Error::NonLocalUrl) { + if (list.error == Ui::PreparedList::Error::None + || !hasImage) { + const auto emptyTextOnCancel = QString(); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list), emptyTextOnCancel); + return true; + } + } + } + + if (auto read = Core::ReadMimeImage(data)) { + confirmSendingFiles( + std::move(read.image), + std::move(read.content), + overrideSendImagesAsPhotos, + insertTextOnCancel); + return true; + } + return false; +} + +bool ShortcutMessages::confirmSendingFiles( + Ui::PreparedList &&list, + const QString &insertTextOnCancel) { + if (_composeControls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { + return false; + } + + auto box = Box<SendFilesBox>( + _controller, + std::move(list), + _composeControls->getTextWithAppliedMarkdown(), + _history->peer, + Api::SendType::Normal, + SendMenu::Type::Disabled); + + box->setConfirmedCallback(crl::guard(this, [=]( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + sendingFilesConfirmed( + std::move(list), + way, + std::move(caption), + options, + ctrlShiftEnter); + })); + box->setCancelledCallback(_composeControls->restoreTextCallback( + insertTextOnCancel)); + + //ActivateWindow(_controller); + _controller->show(std::move(box)); + + return true; +} + +bool ShortcutMessages::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + std::optional<bool> overrideSendImagesAsPhotos, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + return confirmSendingFiles(std::move(list), insertTextOnCancel); +} + +void ShortcutMessages::sendingFilesConfirmed( + Ui::PreparedList &&list, + Ui::SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + Expects(list.filesToProcess.empty()); + + if (showSendingFilesError(list, way.sendImagesAsPhotos())) { + return; + } + auto groups = DivideByGroups( + std::move(list), + way, + _history->peer->slowmodeApplied()); + const auto type = way.sendImagesAsPhotos() + ? SendMediaType::Photo + : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + if ((groups.size() != 1 || !groups.front().sentWithCaption()) + && !caption.text.isEmpty()) { + auto message = Api::MessageToSend(action); + message.textWithTags = base::take(caption); + _session->api().sendMessage(std::move(message)); + } + for (auto &group : groups) { + const auto album = (group.type != Ui::AlbumType::None) + ? std::make_shared<SendingAlbum>() + : nullptr; + _session->api().sendFiles( + std::move(group.list), + type, + base::take(caption), + album, + action); + } + if (_composeControls->replyingToMessage() == action.replyTo) { + _composeControls->cancelReplyMessage(); + } + finishSending(); +} + +void ShortcutMessages::chooseAttach( + std::optional<bool> overrideSendImagesAsPhotos) { + if (showPremiumRequired()) { + return; + } + _choosingAttach = false; + + const auto filter = (overrideSendImagesAsPhotos == true) + ? FileDialog::ImagesOrAllFilter() + : FileDialog::AllOrImagesFilter(); + FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto read = Images::Read({ + .content = result.remoteContent, + }); + if (!read.image.isNull() && !read.animated) { + confirmSendingFiles( + std::move(read.image), + std::move(result.remoteContent), + overrideSendImagesAsPhotos); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + const auto premium = _controller->session().user()->isPremium(); + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize, + premium); + list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos; + confirmSendingFiles(std::move(list)); + } + }), nullptr); +} + +void ShortcutMessages::finishSending() { + _composeControls->hidePanelsAnimated(); + //if (_previewData && _previewData->pendingTill) previewCancel(); + doSetInnerFocus(); + showAtEnd(); +} + +void ShortcutMessages::showAtEnd() { + showAtPosition(Data::MaxMessagePosition); +} + +void ShortcutMessages::doSetInnerFocus() { + if (!_inner->getSelectedText().rich.text.isEmpty() + || !_inner->getSelectedItems().empty() + || !_composeControls->focus()) { + _inner->setFocus(); + } +} + +void ShortcutMessages::sendExistingDocument( + not_null<DocumentData*> document) { + sendExistingDocument(document, {}, std::nullopt); +} + +bool ShortcutMessages::sendExistingDocument( + not_null<DocumentData*> document, + Api::SendOptions options, + std::optional<MsgId> localId) { + if (showPremiumRequired()) { + return false; + } + + Api::SendExistingDocument( + Api::MessageToSend(prepareSendAction(options)), + document, + localId); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendExistingPhoto(not_null<PhotoData*> photo) { + sendExistingPhoto(photo, {}); +} + +bool ShortcutMessages::sendExistingPhoto( + not_null<PhotoData*> photo, + Api::SendOptions options) { + if (showPremiumRequired()) { + return false; + } + Api::SendExistingPhoto( + Api::MessageToSend(prepareSendAction(options)), + photo); + + _composeControls->cancelReplyMessage(); + finishSending(); + return true; +} + +void ShortcutMessages::sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot) { + if (showPremiumRequired()) { + return; + } + const auto errorText = result->getErrorOnSend(_history); + if (!errorText.isEmpty()) { + _controller->showToast(errorText); + return; + } + sendInlineResult(result, bot, {}, std::nullopt); + //const auto callback = [=](Api::SendOptions options) { + // sendInlineResult(result, bot, options); + //}; + //Ui::show( + // PrepareScheduleBox(this, sendMenuType(), callback), + // Ui::LayerOption::KeepOther); +} + +void ShortcutMessages::sendInlineResult( + not_null<InlineBots::Result*> result, + not_null<UserData*> bot, + Api::SendOptions options, + std::optional<MsgId> localMessageId) { + if (showPremiumRequired()) { + return; + } + auto action = prepareSendAction(options); + action.generateLocal = true; + _session->api().sendInlineResult(bot, result, action, localMessageId); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + bot->session().local().writeRecentHashtagsAndBots(); + } + finishSending(); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId) { + showAtPosition(position, originItemId, {}); +} + +void ShortcutMessages::showAtPosition( + Data::MessagePosition position, + FullMsgId originItemId, + const Window::SectionShow ¶ms) { + _lastShownAt = position.fullId; + _inner->showAtPosition( + position, + params, + _cornerButtons.doneJumpFrom(position.fullId, originItemId, true)); +} + +FullReplyTo ShortcutMessages::replyTo() const { + return _composeControls->replyingToMessage(); +} + +bool ShortcutMessages::showPremiumRequired() const { + if (!_controller->session().premium()) { + ShowPremiumPreviewToBuy(_controller, PremiumFeature::QuickReplies); + return true; + } + return false; +} + +} // namespace + +Type ShortcutMessagesId(int shortcutId) { + return ShortcutMessages::Id(shortcutId); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h new file mode 100644 index 000000000..325b12602 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type ShortcutMessagesId(int shortcutId); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.cpp b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp new file mode 100644 index 000000000..fe80e5c73 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.cpp @@ -0,0 +1,745 @@ +/* +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 "settings/business/settings_working_hours.h" + +#include "base/event_filter.h" +#include "base/unixtime.h" +#include "core/application.h" +#include "data/business/data_business_info.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/business/settings_recipients_helper.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/vertical_drum_picker.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +constexpr auto kDay = Data::WorkingInterval::kDay; +constexpr auto kWeek = Data::WorkingInterval::kWeek; +constexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax; + +class WorkingHours : public BusinessSection<WorkingHours> { +public: + WorkingHours( + QWidget *parent, + not_null<Window::SessionController*> controller); + ~WorkingHours(); + + [[nodiscard]] bool closeByOutsideClick() const override; + [[nodiscard]] rpl::producer<QString> title() override; + +private: + void setupContent(not_null<Window::SessionController*> controller); + void save(); + + rpl::variable<Data::WorkingHours> _hours; + rpl::variable<bool> _enabled; + +}; + +[[nodiscard]] QString TimezoneFullName(const Data::Timezone &data) { + const auto abs = std::abs(data.utcOffset); + const auto hours = abs / 3600; + const auto minutes = (abs % 3600) / 60; + const auto sign = (data.utcOffset < 0) ? '-' : '+'; + const auto prefix = u"(UTC"_q + + sign + + QString::number(hours) + + u":"_q + + QString::number(minutes).rightJustified(2, u'0') + + u")"_q; + return prefix + ' ' + data.name; +} + +[[nodiscard]] QString FormatDayTime( + TimeId time, + bool showEndAsNextDay = false) { + const auto wrap = [](TimeId value) { + const auto hours = value / 3600; + const auto minutes = (value % 3600) / 60; + return QString::number(hours).rightJustified(2, u'0') + + ':' + + QString::number(minutes).rightJustified(2, u'0'); + }; + return (time > kDay || (showEndAsNextDay && time == kDay)) + ? tr::lng_hours_next_day(tr::now, lt_time, wrap(time - kDay)) + : wrap(time == kDay ? 0 : time); +} + +[[nodiscard]] QString FormatTimeHour(TimeId time) { + const auto wrap = [](TimeId value) { + return QString::number(value / 3600).rightJustified(2, u'0'); + }; + if (time < kDay) { + return wrap(time); + } + const auto wrapped = wrap(time - kDay); + const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped); + const auto i = result.indexOf(wrapped); + return (i >= 0) ? (result.left(i) + wrapped) : result; +} + +[[nodiscard]] QString FormatTimeMinute(TimeId time) { + const auto wrap = [](TimeId value) { + return QString::number((value / 60) % 60).rightJustified(2, u'0'); + }; + if (time < kDay) { + return wrap(time); + } + const auto wrapped = wrap(time - kDay); + const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped); + const auto i = result.indexOf(wrapped); + return (i >= 0) + ? (wrapped + result.right(result.size() - i - wrapped.size())) + : result; +} + +[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) { + auto result = QStringList(); + result.reserve(data.list.size()); + for (const auto &interval : data.list) { + const auto start = FormatDayTime(interval.start); + const auto end = FormatDayTime(interval.end); + result.push_back(start + u" - "_q + end); + } + return result.join(u", "_q); +} + +void EditTimeBox( + not_null<Ui::GenericBox*> box, + TimeId low, + TimeId high, + TimeId value, + Fn<void(TimeId)> save) { + Expects(low <= high); + + const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>( + box, + st::settingsWorkingHoursPicker)); + + const auto font = st::boxTextFont; + const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; + const auto picker = [=]( + int count, + int startIndex, + Fn<void(QPainter &p, QRectF rect, int index)> paint) { + auto paintCallback = [=]( + QPainter &p, + int index, + float64 y, + float64 distanceFromCenter, + int outerWidth) { + const auto r = QRectF(0, y, outerWidth, itemHeight); + const auto progress = std::abs(distanceFromCenter); + const auto revProgress = 1. - progress; + p.save(); + p.translate(r.center()); + constexpr auto kMinYScale = 0.2; + const auto yScale = kMinYScale + + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); + p.scale(1., yScale); + p.translate(-r.center()); + p.setOpacity(revProgress); + p.setFont(font); + p.setPen(st::defaultFlatLabel.textFg); + paint(p, r, index); + p.restore(); + }; + return Ui::CreateChild<Ui::VerticalDrumPicker>( + content, + std::move(paintCallback), + count, + itemHeight, + startIndex); + }; + + const auto hoursCount = (high - low + 3600) / 3600; + const auto hoursStartIndex = (value / 3600) - (low / 3600); + const auto hoursPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + FormatTimeHour(((low / 3600) + index) * 3600), + style::al_right); + }; + const auto hours = picker(hoursCount, hoursStartIndex, hoursPaint); + const auto minutes = content->lifetime().make_state< + rpl::variable<Ui::VerticalDrumPicker*> + >(nullptr); + + // hours->value() is valid only after size is set. + const auto separator = u":"_q; + const auto separatorWidth = st::boxTextFont->width(separator); + rpl::combine( + content->sizeValue(), + minutes->value() + ) | rpl::start_with_next([=](QSize s, Ui::VerticalDrumPicker *minutes) { + const auto half = (s.width() - separatorWidth) / 2; + hours->setGeometry(0, 0, half, s.height()); + if (minutes) { + minutes->setGeometry(half + separatorWidth, 0, half, s.height()); + } + }, content->lifetime()); + + Ui::SendPendingMoveResizeEvents(hours); + + const auto minutesStart = content->lifetime().make_state<TimeId>(); + hours->value() | rpl::start_with_next([=](int hoursIndex) { + const auto start = std::max(low, (hoursIndex + (low / 3600)) * 3600); + const auto end = std::min(high, ((start / 3600) * 60 + 59) * 60); + const auto minutesCount = (end - start + 60) / 60; + const auto minutesStartIndex = minutes->current() + ? std::clamp( + ((((*minutesStart) / 60 + minutes->current()->index()) % 60) + - ((start / 60) % 60)), + 0, + (minutesCount - 1)) + : std::clamp((value / 60) - (start / 60), 0, minutesCount - 1); + *minutesStart = start; + + const auto minutesPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + FormatTimeMinute(((start / 60) + index) * 60), + style::al_left); + }; + const auto updated = picker( + minutesCount, + minutesStartIndex, + minutesPaint); + delete minutes->current(); + *minutes = updated; + minutes->current()->show(); + }, hours->lifetime()); + + content->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(content); + + p.fillRect(r, Qt::transparent); + + const auto lineRect = QRect( + 0, + content->height() / 2, + content->width(), + st::defaultInputField.borderActive); + p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); + p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); + p.drawText(QRectF(content->rect()), separator, style::al_center); + }, content->lifetime()); + + base::install_event_filter(box, [=](not_null<QEvent*> e) { + if (e->type() == QEvent::KeyPress) { + hours->handleKeyEvent(static_cast<QKeyEvent*>(e.get())); + } + return base::EventFilterResult::Continue; + }); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(std::clamp( + ((*minutesStart) / 60 + minutes->current()->index()) * 60, + low, + high)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void EditDayBox( + not_null<Ui::GenericBox*> box, + rpl::producer<QString> title, + Data::WorkingIntervals intervals, + Fn<void(Data::WorkingIntervals)> save) { + box->setTitle(std::move(title)); + box->setWidth(st::boxWideWidth); + struct State { + rpl::variable<Data::WorkingIntervals> data; + }; + const auto state = box->lifetime().make_state<State>(State{ + .data = std::move(intervals), + }); + + const auto container = box->verticalLayout(); + const auto rows = container->add( + object_ptr<Ui::VerticalLayout>(container)); + const auto makeRow = [=]( + Data::WorkingInterval interval, + TimeId min, + TimeId max) { + auto result = object_ptr<Ui::VerticalLayout>(rows); + const auto raw = result.data(); + AddDivider(raw); + AddSkip(raw); + AddButtonWithLabel( + raw, + tr::lng_hours_opening(), + rpl::single(FormatDayTime(interval.start, true)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto max = std::max(min, interval.end - 60); + const auto now = std::clamp(interval.start, min, max); + const auto save = crl::guard(box, [=](TimeId value) { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + i->start = value; + state->data = now.normalized(); + } + }); + box->getDelegate()->show(Box(EditTimeBox, min, max, now, save)); + }); + AddButtonWithLabel( + raw, + tr::lng_hours_closing(), + rpl::single(FormatDayTime(interval.end, true)), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto min = std::min(max, interval.start + 60); + const auto now = std::clamp(interval.end, min, max); + const auto save = crl::guard(box, [=](TimeId value) { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + i->end = value; + state->data = now.normalized(); + } + }); + box->getDelegate()->show(Box(EditTimeBox, min, max, now, save)); + }); + raw->add(object_ptr<Ui::SettingsButton>( + raw, + tr::lng_hours_remove(), + st::settingsAttentionButton + ))->setClickedCallback([=] { + auto now = state->data.current(); + const auto i = ranges::find(now.list, interval); + if (i != end(now.list)) { + now.list.erase(i); + state->data = std::move(now); + } + }); + AddSkip(raw); + + return result; + }; + + const auto addWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + AddDivider(addWrap->entity()); + AddSkip(addWrap->entity()); + const auto add = addWrap->entity()->add( + object_ptr<Ui::SettingsButton>( + container, + tr::lng_hours_add_button(), + st::settingsButtonLightNoIcon)); + add->setClickedCallback([=] { + auto now = state->data.current(); + if (now.list.empty()) { + now.list.push_back({ 8 * 3600, 20 * 3600 }); + } else if (const auto last = now.list.back().end; last + 60 < kDay) { + const auto from = std::max( + std::min(last + 30 * 60, kDay - 30 * 60), + last + 60); + now.list.push_back({ from, from + 4 * 3600 }); + } + state->data = std::move(now); + }); + + state->data.value( + ) | rpl::start_with_next([=](const Data::WorkingIntervals &data) { + const auto count = int(data.list.size()); + for (auto i = 0; i != count; ++i) { + const auto min = (i == 0) ? 0 : (data.list[i - 1].end + 60); + const auto max = (i == count - 1) + ? (kDay + kInNextDayMax) + : (data.list[i + 1].start - 60); + rows->insert(i, makeRow(data.list[i], min, max)); + if (rows->count() > i + 1) { + delete rows->widgetAt(i + 1); + } + } + while (rows->count() > count) { + delete rows->widgetAt(count); + } + rows->resizeToWidth(st::boxWideWidth); + addWrap->toggle(data.list.empty() + || data.list.back().end + 60 < kDay, anim::type::instant); + add->clearState(); + }, add->lifetime()); + addWrap->finishAnimating(); + + AddSkip(container); + AddDividerText(container, tr::lng_hours_about_day()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + save(state->data.current()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void ChooseTimezoneBox( + not_null<Ui::GenericBox*> box, + std::vector<Data::Timezone> list, + QString id, + Fn<void(QString)> save) { + Expects(!list.empty()); + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_hours_time_zone_title()); + + const auto height = st::boxWideWidth; + box->setMaxHeight(height); + + ranges::sort(list, ranges::less(), [](const Data::Timezone &value) { + return std::pair(value.utcOffset, value.name); + }); + + if (!ranges::contains(list, id, &Data::Timezone::id)) { + id = Data::FindClosestTimezoneId(list); + } + const auto i = ranges::find(list, id, &Data::Timezone::id); + const auto value = int(i - begin(list)); + const auto group = std::make_shared<Ui::RadiobuttonGroup>(value); + const auto radioPadding = st::defaultCheckbox.margin; + const auto max = std::max(radioPadding.top(), radioPadding.bottom()); + auto index = 0; + auto padding = st::boxRowPadding + QMargins(0, max, 0, max); + auto selected = (Ui::Radiobutton*)nullptr; + for (const auto &entry : list) { + const auto button = box->addRow( + object_ptr<Ui::Radiobutton>( + box, + group, + index++, + TimezoneFullName(entry)), + padding); + if (index == value + 1) { + selected = button; + } + padding = st::boxRowPadding + QMargins(0, 0, 0, max); + } + if (selected) { + box->verticalLayout()->resizeToWidth(st::boxWideWidth); + const auto y = selected->y() - (height - selected->height()) / 2; + box->setInitScrollCallback([=] { + box->scrollToY(y); + }); + } + group->setChangedCallback([=](int index) { + const auto weak = Ui::MakeWeak(box); + save(list[index].id); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); +} + +void AddWeekButton( + not_null<Ui::VerticalLayout*> container, + not_null<Window::SessionController*> controller, + int index, + not_null<rpl::variable<Data::WorkingHours>*> data) { + auto label = [&] { + switch (index) { + case 0: return tr::lng_hours_monday(); + case 1: return tr::lng_hours_tuesday(); + case 2: return tr::lng_hours_wednesday(); + case 3: return tr::lng_hours_thursday(); + case 4: return tr::lng_hours_friday(); + case 5: return tr::lng_hours_saturday(); + case 6: return tr::lng_hours_sunday(); + } + Unexpected("Index in AddWeekButton."); + }(); + const auto &st = st::settingsWorkingHoursWeek; + const auto button = AddButtonWithIcon( + container, + rpl::duplicate(label), + st); + button->setClickedCallback([=] { + const auto done = [=](Data::WorkingIntervals intervals) { + auto now = data->current(); + now.intervals = ReplaceDayIntervals( + now.intervals, + index, + std::move(intervals)); + *data = now.normalized(); + }; + controller->show(Box( + EditDayBox, + rpl::duplicate(label), + ExtractDayIntervals(data->current().intervals, index), + crl::guard(button, done))); + }); + + const auto toggleButton = Ui::CreateChild<Ui::SettingsButton>( + container.get(), + nullptr, + st); + const auto checkView = button->lifetime().make_state<Ui::ToggleView>( + st.toggle, + false, + [=] { toggleButton->update(); }); + + auto status = data->value( + ) | rpl::map([=](const Data::WorkingHours &data) -> rpl::producer<QString> { + using namespace Data; + + const auto intervals = ExtractDayIntervals(data.intervals, index); + const auto empty = intervals.list.empty(); + if (checkView->checked() == empty) { + checkView->setChecked(!empty, anim::type::instant); + } + if (!intervals) { + return tr::lng_hours_closed(); + } else if (IsFullOpen(intervals)) { + return tr::lng_hours_open_full(); + } + return rpl::single(JoinIntervals(intervals)); + }) | rpl::flatten_latest(); + const auto details = Ui::CreateChild<Ui::FlatLabel>( + button.get(), + std::move(status), + st::settingsWorkingHoursDetails); + details->show(); + details->moveToLeft( + st.padding.left(), + st.padding.top() + st.height - details->height()); + details->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto separator = Ui::CreateChild<Ui::RpWidget>(container.get()); + separator->paintRequest( + ) | rpl::start_with_next([=, bg = st.textBgOver] { + auto p = QPainter(separator); + p.fillRect(separator->rect(), bg); + }, separator->lifetime()); + const auto separatorHeight = st.height - 2 * st.toggle.border; + button->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + const auto w = st::rightsButtonToggleWidth; + toggleButton->setGeometry( + r.x() + r.width() - w, + r.y(), + w, + r.height()); + separator->setGeometry( + toggleButton->x() - st::lineWidth, + r.y() + (r.height() - separatorHeight) / 2, + st::lineWidth, + separatorHeight); + }, toggleButton->lifetime()); + + const auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton); + checkWidget->resize(checkView->getSize()); + checkWidget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(checkWidget); + checkView->paint(p, 0, 0, checkWidget->width()); + }, checkWidget->lifetime()); + toggleButton->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + checkWidget->moveToRight( + st.toggleSkip, + (s.height() - checkWidget->height()) / 2); + }, toggleButton->lifetime()); + + toggleButton->setClickedCallback([=] { + const auto enabled = !checkView->checked(); + checkView->setChecked(enabled, anim::type::normal); + auto now = data->current(); + now.intervals = ReplaceDayIntervals( + now.intervals, + index, + (enabled + ? Data::WorkingIntervals{ { { 0, kDay } } } + : Data::WorkingIntervals())); + *data = now.normalized(); + }); +} + +WorkingHours::WorkingHours( + QWidget *parent, + not_null<Window::SessionController*> controller) +: BusinessSection(parent, controller) { + setupContent(controller); +} + +WorkingHours::~WorkingHours() { + if (!Core::Quitting()) { + save(); + } +} + +bool WorkingHours::closeByOutsideClick() const { + return false; +} + +rpl::producer<QString> WorkingHours::title() { + return tr::lng_hours_title(); +} + +void WorkingHours::setupContent( + not_null<Window::SessionController*> controller) { + using namespace rpl::mappers; + + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + struct State { + rpl::variable<Data::Timezones> timezones; + bool timezoneEditPending = false; + }; + const auto info = &controller->session().data().businessInfo(); + const auto state = content->lifetime().make_state<State>(State{ + .timezones = info->timezonesValue(), + }); + _hours = controller->session().user()->businessDetails().hours; + + AddDividerTextWithLottie(content, { + .lottie = u"hours"_q, + .lottieSize = st::settingsCloudPasswordIconSize, + .lottieMargins = st::peerAppearanceIconPadding, + .showFinished = showFinishes(), + .about = tr::lng_hours_about(Ui::Text::WithEntities), + .aboutMargins = st::peerAppearanceCoverLabelMargin, + }); + + Ui::AddSkip(content); + const auto enabled = content->add(object_ptr<Ui::SettingsButton>( + content, + tr::lng_hours_show(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single(bool(_hours.current()))); + + _enabled = enabled->toggledValue(); + + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + content, + object_ptr<Ui::VerticalLayout>(content))); + const auto inner = wrap->entity(); + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + for (auto i = 0; i != 7; ++i) { + AddWeekButton(inner, controller, i, &_hours); + } + + Ui::AddSkip(inner); + Ui::AddDivider(inner); + Ui::AddSkip(inner); + + state->timezones.value( + ) | rpl::filter([=](const Data::Timezones &value) { + return !value.list.empty(); + }) | rpl::start_with_next([=](const Data::Timezones &value) { + const auto now = _hours.current().timezoneId; + if (!ranges::contains(value.list, now, &Data::Timezone::id)) { + auto copy = _hours.current(); + copy.timezoneId = Data::FindClosestTimezoneId(value.list); + _hours = std::move(copy); + } + }, inner->lifetime()); + + auto timezoneLabel = rpl::combine( + _hours.value(), + state->timezones.value() + ) | rpl::map([]( + const Data::WorkingHours &hours, + const Data::Timezones &timezones) { + const auto i = ranges::find( + timezones.list, + hours.timezoneId, + &Data::Timezone::id); + return (i != end(timezones.list)) ? TimezoneFullName(*i) : QString(); + }); + const auto editTimezone = [=](const std::vector<Data::Timezone> &list) { + const auto was = _hours.current().timezoneId; + controller->show(Box(ChooseTimezoneBox, list, was, [=](QString id) { + if (id != was) { + auto copy = _hours.current(); + copy.timezoneId = id; + _hours = std::move(copy); + } + })); + }; + AddButtonWithLabel( + inner, + tr::lng_hours_time_zone(), + std::move(timezoneLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + const auto &list = state->timezones.current().list; + if (!list.empty()) { + editTimezone(list); + } else { + state->timezoneEditPending = true; + } + }); + + if (state->timezones.current().list.empty()) { + state->timezones.value( + ) | rpl::filter([](const Data::Timezones &value) { + return !value.list.empty(); + }) | rpl::start_with_next([=](const Data::Timezones &value) { + if (state->timezoneEditPending) { + state->timezoneEditPending = false; + editTimezone(value.list); + } + }, inner->lifetime()); + } + + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + + Ui::ResizeFitChild(this, content); +} + +void WorkingHours::save() { + const auto show = controller()->uiShow(); + controller()->session().data().businessInfo().saveWorkingHours( + _enabled.current() ? _hours.current() : Data::WorkingHours(), + [=](QString error) { show->showToast(error); }); +} + +} // namespace + +Type WorkingHoursId() { + return WorkingHours::Id(); +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/business/settings_working_hours.h b/Telegram/SourceFiles/settings/business/settings_working_hours.h new file mode 100644 index 000000000..213ef1488 --- /dev/null +++ b/Telegram/SourceFiles/settings/business/settings_working_hours.h @@ -0,0 +1,16 @@ +/* +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 "settings/settings_type.h" + +namespace Settings { + +[[nodiscard]] Type WorkingHoursId(); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp index 60596c5bf..b1879387e 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_email_confirm.cpp @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/cloud_password/settings_cloud_password_email_confirm.h" +#include "apiwrap.h" #include "api/api_cloud_password.h" #include "base/unixtime.h" #include "core/core_cloud_password.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/cloud_password/settings_cloud_password_email.h" #include "settings/cloud_password/settings_cloud_password_hint.h" @@ -20,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/boxes/confirm_box.h" #include "ui/text/format_values.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/buttons.h" #include "ui/widgets/sent_code_field.h" #include "ui/wrap/padding_wrap.h" @@ -27,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" /* @@ -55,6 +59,10 @@ public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer<QString> title() override; + + void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; + void setupContent(); protected: @@ -69,6 +77,20 @@ rpl::producer<QString> EmailConfirm::title() { return tr::lng_settings_cloud_password_email_title(); } +void EmailConfirm::fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + const auto api = &controller()->session().api(); + if (const auto state = api->cloudPassword().stateCurrent()) { + if (state->unconfirmedPattern.isEmpty()) { + return; + } + } + addAction( + tr::lng_settings_password_abort(tr::now), + [=] { api->cloudPassword().clearUnconfirmedPassword(); }, + &st::menuIconCancel); +} + rpl::producer<std::vector<Type>> EmailConfirm::removeTypes() { return rpl::single(std::vector<Type>{ CloudPasswordStartId(), diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp index d2f8f9569..2d273895e 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp @@ -121,12 +121,12 @@ void Manage::setupContent() { showOther(type); }; - AddDividerTextWithLottie( - content, - showFinishes(), - tr::lng_settings_cloud_password_manage_about1( + AddDividerTextWithLottie(content, { + .lottie = u"cloud_password/intro"_q, + .showFinished = showFinishes(), + .about = tr::lng_settings_cloud_password_manage_about1( TextWithEntities::Simple), - u"cloud_password/intro"_q); + }); Ui::AddSkip(content); AddButtonWithIcon( diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index f45d68c0b..9dcb8e191 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -94,6 +94,7 @@ settingsPremiumIconTranslations: icon {{ "settings/premium/translations", settin settingsPremiumIconTags: icon {{ "settings/premium/tags", settingsIconFg }}; settingsPremiumIconLastSeen: icon {{ "settings/premium/lastseen", settingsIconFg }}; settingsPremiumIconPrivacy: icon {{ "settings/premium/privacy", settingsIconFg }}; +settingsPremiumIconBusiness: icon {{ "settings/premium/market", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; @@ -103,6 +104,13 @@ settingsStoriesIconDownload: icon {{ "menu/download", premiumButtonBg1 }}; settingsStoriesIconCaption: icon {{ "settings/premium/stories_caption", premiumButtonBg1 }}; settingsStoriesIconLinks: icon {{ "menu/links_profile", premiumButtonBg1 }}; +settingsBusinessIconLocation: icon {{ "settings/premium/business/business_location", settingsIconFg }}; +settingsBusinessIconHours: icon {{ "settings/premium/business/business_hours", settingsIconFg }}; +settingsBusinessIconReplies: icon {{ "settings/premium/business/business_quick", settingsIconFg }}; +settingsBusinessIconGreeting: icon {{ "settings/premium/status", settingsIconFg }}; +settingsBusinessIconAway: icon {{ "settings/premium/business/business_away", settingsIconFg }}; +settingsBusinessIconChatbots: icon {{ "settings/premium/business/business_chatbots", settingsIconFg }}; + settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) { style: TextStyle(semiboldTextStyle) { font: font(10px semibold); @@ -562,6 +570,8 @@ settingsColorButton: SettingsButton(settingsButton) { settingsColorRadioMargin: 17px; settingsColorRadioSkip: 13px; settingsColorRadioStroke: 2px; +settingsLevelBadgeLock: icon {{ "chat/mini_lock", premiumButtonFg }}; +settingsLevelBadgeLockSkip: 4px; messagePrivacyTopSkip: 8px; messagePrivacyRadioSkip: 6px; @@ -579,9 +589,52 @@ peerAppearanceButton: SettingsButton(settingsButtonLight) { padding: margins(60px, 8px, 22px, 8px); iconLeft: 20px; } -peerAppearanceCoverLabel: FlatLabel(boxDividerLabel) { - align: align(top); -} peerAppearanceCoverLabelMargin: margins(22px, 0px, 22px, 17px); peerAppearanceIconPadding: margins(0px, 15px, 0px, 5px); peerAppearanceDividerTextMargin: margins(22px, 8px, 22px, 11px); + +settingsChatbotsUsername: InputField(defaultMultiSelectSearchField) { +} +settingsChatbotsAccess: Checkbox(defaultCheckbox) { + textPosition: point(18px, 2px); +} +settingsLocationAddress: InputField(defaultMultiSelectSearchField) { +} +settingsChatbotsUsernameMargins: margins(20px, 8px, 20px, 8px); +settingsChatbotsAccessMargins: margins(22px, 5px, 22px, 9px); +settingsChatbotsAccessSkip: 4px; +settingsChatbotsBottomTextMargin: margins(22px, 8px, 22px, 3px); +settingsChatbotsAdd: SettingsButton(settingsButton) { + iconLeft: 22px; +} +settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) { + height: 40px; + padding: margins(22px, 4px, 22px, 4px); +} +settingsWorkingHoursDetails: settingsNotificationTypeDetails; +settingsWorkingHoursPicker: 200px; +settingsWorkingHoursPickerItemHeight: 40px; + +settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px); + +settingsAddReplyLabel: FlatLabel(defaultFlatLabel) { + minWidth: 256px; +} +settingsAddReplyField: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 10px, 0px, 2px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + + heightMin: 36px; +} +settingsChatbotsNotFound: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + align: align(top); +} +settingsChatbotsDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; +settingsChatbotsDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }}; diff --git a/Telegram/SourceFiles/settings/settings_advanced.cpp b/Telegram/SourceFiles/settings/settings_advanced.cpp index 1d6cdda03..4f4b8a9fb 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced.cpp @@ -978,10 +978,6 @@ rpl::producer<QString> Advanced::title() { return tr::lng_settings_advanced(); } -rpl::producer<Type> Advanced::sectionShowOther() { - return _showOther.events(); -} - void Advanced::setupContent(not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); @@ -1033,9 +1029,7 @@ void Advanced::setupContent(not_null<Window::SessionController*> controller) { AddSkip(content); AddDivider(content); AddSkip(content); - SetupExport(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupExport(controller, content, showOtherMethod()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_advanced.h b/Telegram/SourceFiles/settings/settings_advanced.h index fce804253..1d46797b4 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.h +++ b/Telegram/SourceFiles/settings/settings_advanced.h @@ -54,13 +54,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.cpp b/Telegram/SourceFiles/settings/settings_business.cpp new file mode 100644 index 000000000..c9123c528 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_business.cpp @@ -0,0 +1,680 @@ +/* +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 "settings/settings_business.h" + +#include "boxes/premium_preview_box.h" +#include "core/click_handler_types.h" +#include "data/business/data_business_info.h" +#include "data/business/data_business_chatbots.h" +#include "data/business/data_shortcut_messages.h" +#include "data/data_changes.h" +#include "data/data_peer_values.h" // AmPremiumValue. +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/info_wrap_widget.h" // Info::Wrap. +#include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/business/settings_away_message.h" +#include "settings/business/settings_chatbots.h" +#include "settings/business/settings_greeting.h" +#include "settings/business/settings_location.h" +#include "settings/business/settings_quick_replies.h" +#include "settings/business/settings_working_hours.h" +#include "settings/settings_common_session.h" +#include "settings/settings_premium.h" +#include "ui/effects/gradient.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. +#include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "apiwrap.h" +#include "api/api_premium.h" +#include "styles/style_premium.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +struct Entry { + const style::icon *icon; + rpl::producer<QString> title; + rpl::producer<QString> description; + PremiumFeature feature = PremiumFeature::BusinessLocation; +}; + +using Order = std::vector<QString>; + +[[nodiscard]] Order FallbackOrder() { + return Order{ + u"greeting_message"_q, + u"away_message"_q, + u"quick_replies"_q, + u"business_hours"_q, + u"business_location"_q, + u"business_bots"_q, + }; +} + +[[nodiscard]] base::flat_map<QString, Entry> EntryMap() { + return base::flat_map<QString, Entry>{ + { + u"business_location"_q, + Entry{ + &st::settingsBusinessIconLocation, + tr::lng_business_subtitle_location(), + tr::lng_business_about_location(), + PremiumFeature::BusinessLocation, + }, + }, + { + u"business_hours"_q, + Entry{ + &st::settingsBusinessIconHours, + tr::lng_business_subtitle_opening_hours(), + tr::lng_business_about_opening_hours(), + PremiumFeature::BusinessHours, + }, + }, + { + u"quick_replies"_q, + Entry{ + &st::settingsBusinessIconReplies, + tr::lng_business_subtitle_quick_replies(), + tr::lng_business_about_quick_replies(), + PremiumFeature::QuickReplies, + }, + }, + { + u"greeting_message"_q, + Entry{ + &st::settingsBusinessIconGreeting, + tr::lng_business_subtitle_greeting_messages(), + tr::lng_business_about_greeting_messages(), + PremiumFeature::GreetingMessage, + }, + }, + { + u"away_message"_q, + Entry{ + &st::settingsBusinessIconAway, + tr::lng_business_subtitle_away_messages(), + tr::lng_business_about_away_messages(), + PremiumFeature::AwayMessage, + }, + }, + { + u"business_bots"_q, + Entry{ + &st::settingsBusinessIconChatbots, + tr::lng_business_subtitle_chatbots(), + tr::lng_business_about_chatbots(), + PremiumFeature::BusinessBots, + }, + }, + }; +} + +void AddBusinessSummary( + not_null<Ui::VerticalLayout*> content, + not_null<Window::SessionController*> controller, + Fn<void(PremiumFeature)> buttonCallback) { + const auto &stDefault = st::settingsButton; + const auto &stLabel = st::defaultFlatLabel; + const auto iconSize = st::settingsPremiumIconDouble.size(); + const auto &titlePadding = st::settingsPremiumRowTitlePadding; + const auto &descriptionPadding = st::settingsPremiumRowAboutPadding; + + auto entryMap = EntryMap(); + auto iconContainers = std::vector<Ui::AbstractButton*>(); + iconContainers.reserve(int(entryMap.size())); + + const auto addRow = [&](Entry &entry) { + const auto labelAscent = stLabel.style.font->ascent; + const auto button = Ui::CreateChild<Ui::SettingsButton>( + content.get(), + rpl::single(QString())); + + const auto label = content->add( + object_ptr<Ui::FlatLabel>( + content, + std::move(entry.title) | rpl::map(Ui::Text::Bold), + stLabel), + titlePadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + const auto description = content->add( + object_ptr<Ui::FlatLabel>( + content, + std::move(entry.description), + st::boxDividerLabel), + descriptionPadding); + description->setAttribute(Qt::WA_TransparentForMouseEvents); + + const auto dummy = Ui::CreateChild<Ui::AbstractButton>(content.get()); + dummy->setAttribute(Qt::WA_TransparentForMouseEvents); + + content->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + dummy->resize(s.width(), iconSize.height()); + }, dummy->lifetime()); + + label->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + dummy->moveToLeft(0, r.y() + (r.height() - labelAscent)); + }, dummy->lifetime()); + + rpl::combine( + content->widthValue(), + label->heightValue(), + description->heightValue() + ) | rpl::start_with_next([=, + topPadding = titlePadding, + bottomPadding = descriptionPadding]( + int width, + int topHeight, + int bottomHeight) { + button->resize( + width, + topPadding.top() + + topHeight + + topPadding.bottom() + + bottomPadding.top() + + bottomHeight + + bottomPadding.bottom()); + }, button->lifetime()); + label->topValue( + ) | rpl::start_with_next([=, padding = titlePadding.top()](int top) { + button->moveToLeft(0, top - padding); + }, button->lifetime()); + const auto arrow = Ui::CreateChild<Ui::IconButton>( + button, + st::backButton); + arrow->setIconOverride( + &st::settingsPremiumArrow, + &st::settingsPremiumArrowOver); + arrow->setAttribute(Qt::WA_TransparentForMouseEvents); + button->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto &point = st::settingsPremiumArrowShift; + arrow->moveToRight( + -point.x(), + point.y() + (s.height() - arrow->height()) / 2); + }, arrow->lifetime()); + + const auto feature = entry.feature; + button->setClickedCallback([=] { buttonCallback(feature); }); + + iconContainers.push_back(dummy); + }; + + auto icons = std::vector<const style::icon *>(); + icons.reserve(int(entryMap.size())); + { + const auto &account = controller->session().account(); + const auto mtpOrder = account.appConfig().get<Order>( + "business_promo_order", + FallbackOrder()); + const auto processEntry = [&](Entry &entry) { + icons.push_back(entry.icon); + addRow(entry); + }; + + for (const auto &key : mtpOrder) { + auto it = entryMap.find(key); + if (it == end(entryMap)) { + continue; + } + processEntry(it->second); + } + } + + content->resizeToWidth(content->height()); + + // Icons. + Assert(iconContainers.size() > 2); + const auto from = iconContainers.front()->y(); + const auto to = iconContainers.back()->y() + iconSize.height(); + auto gradient = QLinearGradient(0, 0, 0, to - from); + gradient.setStops(Ui::Premium::FullHeightGradientStops()); + for (auto i = 0; i < int(icons.size()); i++) { + const auto &iconContainer = iconContainers[i]; + + const auto pointTop = iconContainer->y() - from; + const auto pointBottom = pointTop + iconContainer->height(); + const auto ratioTop = pointTop / float64(to - from); + const auto ratioBottom = pointBottom / float64(to - from); + + auto resultGradient = QLinearGradient( + QPointF(), + QPointF(0, pointBottom - pointTop)); + + resultGradient.setColorAt( + .0, + anim::gradient_color_at(gradient, ratioTop)); + resultGradient.setColorAt( + .1, + anim::gradient_color_at(gradient, ratioBottom)); + + const auto brush = QBrush(resultGradient); + AddButtonIcon( + iconContainer, + stDefault, + { .icon = icons[i], .backgroundBrush = brush }); + } + + Ui::AddSkip(content, descriptionPadding.bottom()); +} + +class Business : public Section<Business> { +public: + Business( + QWidget *parent, + not_null<Window::SessionController*> controller); + + [[nodiscard]] rpl::producer<QString> title() override; + + [[nodiscard]] QPointer<Ui::RpWidget> createPinnedToTop( + not_null<QWidget*> parent) override; + [[nodiscard]] QPointer<Ui::RpWidget> createPinnedToBottom( + not_null<Ui::RpWidget*> parent) override; + + void showFinished() override; + + [[nodiscard]] bool hasFlexibleTopBar() const override; + + void setStepDataReference(std::any &data) override; + + [[nodiscard]] rpl::producer<> sectionShowBack() override final; + +private: + void setupContent(); + + const not_null<Window::SessionController*> _controller; + + QPointer<Ui::GradientButton> _subscribe; + base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back; + base::unique_qptr<Ui::IconButton> _close; + rpl::variable<bool> _backToggles; + rpl::variable<Info::Wrap> _wrap; + Fn<void(bool)> _setPaused; + std::shared_ptr<Ui::RadiobuttonGroup> _radioGroup; + + rpl::event_stream<> _showBack; + rpl::event_stream<> _showFinished; + rpl::variable<QString> _buttonText; + + PremiumFeature _waitingToShow = PremiumFeature::Business; + +}; + +Business::Business( + QWidget *parent, + not_null<Window::SessionController*> controller) +: Section(parent) +, _controller(controller) +, _radioGroup(std::make_shared<Ui::RadiobuttonGroup>()) { + setupContent(); + _controller->session().api().premium().reload(); +} + +rpl::producer<QString> Business::title() { + return tr::lng_premium_summary_title(); +} + +bool Business::hasFlexibleTopBar() const { + return true; +} + +rpl::producer<> Business::sectionShowBack() { + return _showBack.events(); +} + +void Business::setStepDataReference(std::any &data) { + using namespace Info::Settings; + const auto my = std::any_cast<SectionCustomTopBarData>(&data); + if (my) { + _backToggles = std::move( + my->backButtonEnables + ) | rpl::map_to(true); + _wrap = std::move(my->wrapValue); + } +} + +void Business::setupContent() { + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + const auto owner = &_controller->session().data(); + owner->chatbots().preload(); + owner->businessInfo().preload(); + owner->shortcutMessages().preloadShortcuts(); + + Ui::AddSkip(content, st::settingsFromFileTop); + + const auto showFeature = [=](PremiumFeature feature) { + showOther([&] { + switch (feature) { + case PremiumFeature::AwayMessage: return AwayMessageId(); + case PremiumFeature::BusinessHours: return WorkingHoursId(); + case PremiumFeature::BusinessLocation: return LocationId(); + case PremiumFeature::GreetingMessage: return GreetingId(); + case PremiumFeature::QuickReplies: return QuickRepliesId(); + case PremiumFeature::BusinessBots: return ChatbotsId(); + } + Unexpected("Feature in showFeature."); + }()); + }; + const auto isReady = [=](PremiumFeature feature) { + switch (feature) { + case PremiumFeature::AwayMessage: + return owner->businessInfo().awaySettingsLoaded() + && owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::BusinessHours: + return owner->session().user()->isFullLoaded() + && owner->businessInfo().timezonesLoaded(); + case PremiumFeature::BusinessLocation: + return owner->session().user()->isFullLoaded(); + case PremiumFeature::GreetingMessage: + return owner->businessInfo().greetingSettingsLoaded() + && owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::QuickReplies: + return owner->shortcutMessages().shortcutsLoaded(); + case PremiumFeature::BusinessBots: + return owner->chatbots().loaded(); + } + Unexpected("Feature in isReady."); + }; + const auto check = [=] { + if (_waitingToShow != PremiumFeature::Business + && isReady(_waitingToShow)) { + showFeature( + std::exchange(_waitingToShow, PremiumFeature::Business)); + } + }; + + rpl::merge( + owner->businessInfo().awaySettingsChanged(), + owner->businessInfo().greetingSettingsChanged(), + owner->businessInfo().timezonesValue() | rpl::to_empty, + owner->shortcutMessages().shortcutsChanged(), + owner->chatbots().changes() | rpl::to_empty, + owner->session().changes().peerUpdates( + owner->session().user(), + Data::PeerUpdate::Flag::FullInfo) | rpl::to_empty + ) | rpl::start_with_next(check, content->lifetime()); + + AddBusinessSummary(content, _controller, [=](PremiumFeature feature) { + if (!_controller->session().premium()) { + _setPaused(true); + const auto hidden = crl::guard(this, [=] { _setPaused(false); }); + + ShowPremiumPreviewToBuy(_controller, feature, hidden); + return; + } else if (!isReady(feature)) { + _waitingToShow = feature; + } else { + showFeature(feature); + } + }); + + Ui::ResizeFitChild(this, content); +} + +QPointer<Ui::RpWidget> Business::createPinnedToTop( + not_null<QWidget*> parent) { + auto title = tr::lng_business_title(); + auto about = [&]() -> rpl::producer<TextWithEntities> { + return rpl::conditional( + Data::AmPremiumValue(&_controller->session()), + tr::lng_business_unlocked(), + tr::lng_business_about() + ) | Ui::Text::ToWithEntities(); + }(); + + const auto content = [&]() -> Ui::Premium::TopBarAbstract* { + const auto weak = base::make_weak(_controller); + const auto clickContextOther = [=] { + return QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + .botStartAutoSubmit = true, + }); + }; + return Ui::CreateChild<Ui::Premium::TopBar>( + parent.get(), + st::defaultPremiumCover, + Ui::Premium::TopBarDescriptor{ + .clickContextOther = clickContextOther, + .logo = u"dollar"_q, + .title = std::move(title), + .about = std::move(about), + }); + }(); + _setPaused = [=](bool paused) { + content->setPaused(paused); + if (_subscribe) { + _subscribe->setGlarePaused(paused); + } + }; + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + content->setRoundEdges(wrap == Info::Wrap::Layer); + }, content->lifetime()); + + const auto calculateMaximumHeight = [=] { + return st::settingsPremiumTopHeight; + }; + + content->setMaximumHeight(calculateMaximumHeight()); + content->setMinimumHeight(st::settingsPremiumTopHeight);// st::infoLayerTopBarHeight); + + content->resize(content->width(), content->maximumHeight()); + //content->additionalHeight( + //) | rpl::start_with_next([=](int additionalHeight) { + // const auto wasMax = (content->height() == content->maximumHeight()); + // content->setMaximumHeight(calculateMaximumHeight() + // + additionalHeight); + // if (wasMax) { + // content->resize(content->width(), content->maximumHeight()); + // } + //}, content->lifetime()); + + _wrap.value( + ) | rpl::start_with_next([=](Info::Wrap wrap) { + const auto isLayer = (wrap == Info::Wrap::Layer); + _back = base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>( + content, + object_ptr<Ui::IconButton>( + content, + (isLayer + ? st::settingsPremiumLayerTopBarBack + : st::settingsPremiumTopBarBack)), + st::infoTopBarScale); + _back->setDuration(0); + _back->toggleOn(isLayer + ? _backToggles.value() | rpl::type_erased() + : rpl::single(true)); + _back->entity()->addClickHandler([=] { + _showBack.fire({}); + }); + _back->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + const auto &st = isLayer ? st::infoLayerTopBar : st::infoTopBar; + content->setTextPosition( + toggled ? st.back.width : st.titlePosition.x(), + st.titlePosition.y()); + }, _back->lifetime()); + + if (!isLayer) { + _close = nullptr; + } else { + _close = base::make_unique_q<Ui::IconButton>( + content, + st::settingsPremiumTopBarClose); + _close->addClickHandler([=] { + _controller->parentController()->hideLayer(); + _controller->parentController()->hideSpecialLayer(); + }); + content->widthValue( + ) | rpl::start_with_next([=] { + _close->moveToRight(0, 0); + }, _close->lifetime()); + } + }, content->lifetime()); + + return Ui::MakeWeak(not_null<Ui::RpWidget*>{ content }); +} + +void Business::showFinished() { + _showFinished.fire({}); +} + +QPointer<Ui::RpWidget> Business::createPinnedToBottom( + not_null<Ui::RpWidget*> parent) { + const auto content = Ui::CreateChild<Ui::RpWidget>(parent.get()); + + const auto session = &_controller->session(); + + auto buttonText = _buttonText.value(); + + _subscribe = CreateSubscribeButton({ + _controller, + content, + [] { return u"business"_q; }, + std::move(buttonText), + std::nullopt, + [=, options = session->api().premium().subscriptionOptions()] { + const auto value = _radioGroup->current(); + return (value < options.size() && value >= 0) + ? options[value].botUrl + : QString(); + }, + }); + { + const auto callback = [=](int value) { + const auto options = + _controller->session().api().premium().subscriptionOptions(); + if (options.empty()) { + return; + } + Assert(value < options.size() && value >= 0); + auto text = tr::lng_premium_subscribe_button( + tr::now, + lt_cost, + options[value].costPerMonth); + _buttonText = std::move(text); + }; + _radioGroup->setChangedCallback(callback); + callback(0); + } + + _showFinished.events( + ) | rpl::take(1) | rpl::start_with_next([=] { + _subscribe->startGlareAnimation(); + }, _subscribe->lifetime()); + + content->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto padding = st::settingsPremiumButtonPadding; + _subscribe->resizeToWidth(width - padding.left() - padding.right()); + }, _subscribe->lifetime()); + + rpl::combine( + _subscribe->heightValue(), + Data::AmPremiumValue(session), + session->premiumPossibleValue() + ) | rpl::start_with_next([=]( + int buttonHeight, + bool premium, + bool premiumPossible) { + const auto padding = st::settingsPremiumButtonPadding; + const auto finalHeight = !premiumPossible + ? 0 + : !premium + ? (padding.top() + buttonHeight + padding.bottom()) + : 0; + content->resize(content->width(), finalHeight); + _subscribe->moveToLeft(padding.left(), padding.top()); + _subscribe->setVisible(!premium && premiumPossible); + }, _subscribe->lifetime()); + + return Ui::MakeWeak(not_null<Ui::RpWidget*>{ content }); +} + +} // namespace + +template <> +struct SectionFactory<Business> : AbstractSectionFactory { + object_ptr<AbstractSection> create( + not_null<QWidget*> parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue + ) const final override { + return object_ptr<Business>(parent, controller); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() { + static const auto result = std::make_shared<SectionFactory>(); + return result; + } +}; + +Type BusinessId() { + return Business::Id(); +} + +void ShowBusiness(not_null<Window::SessionController*> controller) { + if (!controller->session().premiumPossible()) { + controller->show(Box(PremiumUnavailableBox)); + return; + } + controller->showSettings(Settings::BusinessId()); +} + +std::vector<PremiumFeature> BusinessFeaturesOrder( + not_null<::Main::Session*> session) { + const auto mtpOrder = session->account().appConfig().get<Order>( + "business_promo_order", + FallbackOrder()); + return ranges::views::all( + mtpOrder + ) | ranges::views::transform([](const QString &s) { + if (s == u"greeting_message"_q) { + return PremiumFeature::GreetingMessage; + } else if (s == u"away_message"_q) { + return PremiumFeature::AwayMessage; + } else if (s == u"quick_replies"_q) { + return PremiumFeature::QuickReplies; + } else if (s == u"business_hours"_q) { + return PremiumFeature::BusinessHours; + } else if (s == u"business_location"_q) { + return PremiumFeature::BusinessLocation; + } else if (s == u"business_bots"_q) { + return PremiumFeature::BusinessBots; + } + return PremiumFeature::kCount; + }) | ranges::views::filter([](PremiumFeature feature) { + return (feature != PremiumFeature::kCount); + }) | ranges::to_vector; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_business.h b/Telegram/SourceFiles/settings/settings_business.h new file mode 100644 index 000000000..6bd077af0 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_business.h @@ -0,0 +1,31 @@ +/* +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 "settings/settings_type.h" + +enum class PremiumFeature; + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings { + +[[nodiscard]] Type BusinessId(); + +void ShowBusiness(not_null<Window::SessionController*> controller); + +[[nodiscard]] std::vector<PremiumFeature> BusinessFeaturesOrder( + not_null<::Main::Session*> session); + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 0b6a70674..de7c34e3e 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -607,7 +607,7 @@ void ChooseMediaDeviceBox( button->finishAnimating(); button->clicks( ) | rpl::filter([=] { - return (group->value() == index); + return (group->current() == index); }) | rpl::start_with_next([=] { choose(id); }, button->lifetime()); diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index e2e47abf4..5ccf30405 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "history/view/history_view_quick_action.h" #include "lang/lang_keys.h" #include "export/export_manager.h" @@ -1810,7 +1811,8 @@ void SetupSupport( } Chat::Chat(QWidget *parent, not_null<Window::SessionController*> controller) -: Section(parent) { +: Section(parent) +, _controller(controller) { setupContent(controller); } @@ -1818,6 +1820,14 @@ rpl::producer<QString> Chat::title() { return tr::lng_settings_section_chat_settings(); } +void Chat::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto window = &_controller->window(); + addAction( + tr::lng_settings_bg_theme_create(tr::now), + [=] { window->show(Box(Window::Theme::CreateBox, window)); }, + &st::menuIconChangeColors); +} + void Chat::setupContent(not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); diff --git a/Telegram/SourceFiles/settings/settings_chat.h b/Telegram/SourceFiles/settings/settings_chat.h index 91cd93101..70724ea2b 100644 --- a/Telegram/SourceFiles/settings/settings_chat.h +++ b/Telegram/SourceFiles/settings/settings_chat.h @@ -44,9 +44,14 @@ public: [[nodiscard]] rpl::producer<QString> title() override; + void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; + private: void setupContent(not_null<Window::SessionController*> controller); + const not_null<Window::SessionController*> _controller; + }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index f9de195fc..448061167 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -170,39 +170,46 @@ not_null<Button*> AddButtonWithLabel( } void AddDividerTextWithLottie( - not_null<Ui::VerticalLayout*> parent, - rpl::producer<> showFinished, - rpl::producer<TextWithEntities> text, - const QString &lottie) { - const auto divider = Ui::CreateChild<Ui::BoxContentDivider>(parent.get()); - const auto verticalLayout = parent->add( - object_ptr<Ui::VerticalLayout>(parent.get())); - + not_null<Ui::VerticalLayout*> container, + DividerWithLottieDescriptor &&descriptor) { + const auto divider = Ui::CreateChild<Ui::BoxContentDivider>( + container.get(), + 0, + st::boxDividerBg, + descriptor.parts); + const auto verticalLayout = container->add( + object_ptr<Ui::VerticalLayout>(container.get())); + const auto size = descriptor.lottieSize.value_or( + st::settingsFilterIconSize); auto icon = CreateLottieIcon( verticalLayout, { - .name = lottie, - .sizeOverride = { - st::settingsFilterIconSize, - st::settingsFilterIconSize, - }, + .name = descriptor.lottie, + .sizeOverride = { size, size }, }, - st::settingsFilterIconPadding); - std::move( - showFinished - ) | rpl::start_with_next([animate = std::move(icon.animate)] { - animate(anim::repeat::once); - }, verticalLayout->lifetime()); + descriptor.lottieMargins.value_or(st::settingsFilterIconPadding)); + if (descriptor.showFinished) { + const auto repeat = descriptor.lottieRepeat.value_or( + anim::repeat::once); + std::move( + descriptor.showFinished + ) | rpl::start_with_next([animate = std::move(icon.animate), repeat] { + animate(repeat); + }, verticalLayout->lifetime()); + } verticalLayout->add(std::move(icon.widget)); - verticalLayout->add( - object_ptr<Ui::CenterWrap<>>( - verticalLayout, - object_ptr<Ui::FlatLabel>( + if (descriptor.about) { + verticalLayout->add( + object_ptr<Ui::CenterWrap<>>( verticalLayout, - std::move(text), - st::settingsFilterDividerLabel)), - st::settingsFilterDividerLabelPadding); + object_ptr<Ui::FlatLabel>( + verticalLayout, + std::move(descriptor.about), + st::settingsFilterDividerLabel)), + descriptor.aboutMargins.value_or( + st::settingsFilterDividerLabelPadding)); + } verticalLayout->geometryValue( ) | rpl::start_with_next([=](const QRect &r) { diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 838edcc64..c36927978 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/text_variant.h" #include "ui/rp_widget.h" #include "ui/round_rect.h" #include "base/object_ptr.h" @@ -16,6 +17,11 @@ namespace anim { enum class repeat : uchar; } // namespace anim +namespace Info { +struct SelectedItems; +enum class SelectionAction; +} // namespace Info + namespace Main { class Session; } // namespace Main @@ -64,6 +70,12 @@ public: [[nodiscard]] virtual rpl::producer<std::vector<Type>> removeFromStack() { return nullptr; } + [[nodiscard]] virtual bool closeByOutsideClick() const { + return true; + } + virtual void checkBeforeClose(Fn<void()> close) { + close(); + } [[nodiscard]] virtual rpl::producer<QString> title() = 0; virtual void sectionSaveChanges(FnMut<void()> done) { done(); @@ -89,6 +101,23 @@ public: } virtual void setStepDataReference(std::any &data) { } + + [[nodiscard]] virtual auto selectedListValue() + -> rpl::producer<Info::SelectedItems> { + return nullptr; + } + virtual void selectionAction(Info::SelectionAction action) { + } + virtual void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) { + } + + virtual bool paintOuter( + not_null<QWidget*> outer, + int maxVisibleHeight, + QRect clip) { + return false; + } }; enum class IconType { @@ -151,11 +180,20 @@ void CreateRightLabel( rpl::producer<QString> label, const style::SettingsButton &st, rpl::producer<QString> buttonText); + +struct DividerWithLottieDescriptor { + QString lottie; + std::optional<anim::repeat> lottieRepeat; + std::optional<int> lottieSize; + std::optional<QMargins> lottieMargins; + rpl::producer<> showFinished; + rpl::producer<TextWithEntities> about; + std::optional<QMargins> aboutMargins; + RectParts parts = RectPart::Top | RectPart::Bottom; +}; void AddDividerTextWithLottie( - not_null<Ui::VerticalLayout*> parent, - rpl::producer<> showFinished, - rpl::producer<TextWithEntities> text, - const QString &lottie); + not_null<Ui::VerticalLayout*> container, + DividerWithLottieDescriptor &&descriptor); struct LottieIcon { object_ptr<Ui::RpWidget> widget; diff --git a/Telegram/SourceFiles/settings/settings_common_session.cpp b/Telegram/SourceFiles/settings/settings_common_session.cpp index 56c976e05..8fac1a399 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.cpp +++ b/Telegram/SourceFiles/settings/settings_common_session.cpp @@ -32,63 +32,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <QAction> -// AyuGram includes -#include "ayu/ui/settings/settings_ayu.h" - - namespace Settings { -void FillMenu( - not_null<Window::SessionController*> controller, - Type type, - Fn<void(Type)> showOther, - Ui::Menu::MenuCallback addAction) { - const auto window = &controller->window(); - if (type == Chat::Id()) { - addAction( - tr::lng_settings_bg_theme_create(tr::now), - [=] { window->show(Box(Window::Theme::CreateBox, window)); }, - &st::menuIconChangeColors); - } else if (type == CloudPasswordEmailConfirmId()) { - const auto api = &controller->session().api(); - if (const auto state = api->cloudPassword().stateCurrent()) { - if (state->unconfirmedPattern.isEmpty()) { - return; - } - } - addAction( - tr::lng_settings_password_abort(tr::now), - [=] { api->cloudPassword().clearUnconfirmedPassword(); }, - &st::menuIconCancel); - } else if (type == Ayu::Id()) { - addAction( - tr::ayu_RegisterURLScheme(tr::now), - [=] { Core::Application::RegisterUrlScheme(); }, - &st::menuIconLinks); - addAction( - tr::lng_restart_button(tr::now), - [=] { Core::Restart(); }, - &st::menuIconRestore); - } else { - const auto &list = Core::App().domain().accounts(); - if (list.size() < Core::App().domain().maxAccounts()) { - addAction(tr::lng_menu_add_account(tr::now), [=] { - Core::App().domain().addActivated(MTP::Environment{}); - }, &st::menuIconAddAccount); - } - if (!controller->session().supportMode()) { - addAction( - tr::lng_settings_information(tr::now), - [=] { showOther(Information::Id()); }, - &st::menuIconInfo); - } - addAction({ - .text = tr::lng_settings_logout(tr::now), - .handler = [=] { window->showLogoutConfirmation(); }, - .icon = &st::menuIconLeaveAttention, - .isAttention = true, - }); - } +bool HasMenu(Type type) { + return (type == ::Settings::CloudPasswordEmailConfirmId()) + || (type == Main::Id()) + || (type == Chat::Id()); } } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common_session.h b/Telegram/SourceFiles/settings/settings_common_session.h index 911e22897..8bea347a0 100644 --- a/Telegram/SourceFiles/settings/settings_common_session.h +++ b/Telegram/SourceFiles/settings/settings_common_session.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "settings/settings_type.h" +namespace Ui { +class ScrollArea; +} // namespace Ui + namespace Ui::Menu { struct MenuCallback; } // namespace Ui::Menu @@ -22,12 +26,19 @@ class SessionController; namespace Settings { +enum class Container { + Section, + Layer, +}; + class AbstractSection; struct AbstractSectionFactory { [[nodiscard]] virtual object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller) const = 0; + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue) const = 0; [[nodiscard]] virtual bool hasCustomTopBar() const { return false; } @@ -39,7 +50,9 @@ template <typename SectionType> struct SectionFactory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<SectionType>(parent, controller); } @@ -48,6 +61,7 @@ struct SectionFactory : AbstractSectionFactory { static const auto result = std::make_shared<SectionFactory>(); return result; } + }; template <typename SectionType> @@ -61,12 +75,24 @@ public: [[nodiscard]] Type id() const final override { return Id(); } + + [[nodiscard]] rpl::producer<Type> sectionShowOther() final override { + return _showOtherRequests.events(); + } + void showOther(Type type) { + _showOtherRequests.fire_copy(type); + } + [[nodiscard]] Fn<void(Type)> showOtherMethod() { + return crl::guard(this, [=](Type type) { + showOther(type); + }); + } + +private: + rpl::event_stream<Type> _showOtherRequests; + }; -void FillMenu( - not_null<Window::SessionController*> controller, - Type type, - Fn<void(Type)> showOther, - Ui::Menu::MenuCallback addAction); +bool HasMenu(Type type); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_global_ttl.cpp b/Telegram/SourceFiles/settings/settings_global_ttl.cpp index 8e2024622..99dea5b2d 100644 --- a/Telegram/SourceFiles/settings/settings_global_ttl.cpp +++ b/Telegram/SourceFiles/settings/settings_global_ttl.cpp @@ -294,7 +294,7 @@ void GlobalTTL::rebuildButtons(TimeId currentTTL) const { rpl::single(ttlText)), st::settingsButtonNoIcon)); button->setClickedCallback([=] { - if (_group->value() == ttl) { + if (_group->current() == ttl) { return; } if (!ttl) { @@ -357,7 +357,7 @@ void GlobalTTL::setupContent() { show->showBox(Box(TTLMenu::TTLBox, TTLMenu::Args{ .show = show, - .startTtl = _group->value(), + .startTtl = _group->current(), .callback = [=](TimeId ttl, Fn<void()>) { showSure(ttl, true); }, .hideDisable = true, })); diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 1f0a3f883..faea47813 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -482,7 +482,8 @@ void SetupBio( } changed->fire(*current != text); const auto limit = self->isPremium() ? premiumLimit : defaultLimit; - const auto countLeft = limit - int(text.size()); + const auto countLeft = limit + - bio->lastTextSizeWithoutSurrogatePairsCount(); countdown->setText(QString::number(countLeft)); countdown->setTextColorOverride( countLeft < 0 ? st::boxTextFgError->c : std::optional<QColor>()); diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp index d06726cb4..9a766f16d 100644 --- a/Telegram/SourceFiles/settings/settings_local_passcode.cpp +++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp @@ -383,7 +383,6 @@ public: [[nodiscard]] rpl::producer<QString> title() override; void showFinished() override; - [[nodiscard]] rpl::producer<Type> sectionShowOther() override; [[nodiscard]] rpl::producer<> sectionShowBack() override; [[nodiscard]] rpl::producer<std::vector<Type>> removeFromStack() override; @@ -399,7 +398,6 @@ private: rpl::variable<bool> _isBottomFillerShown; rpl::event_stream<> _showFinished; - rpl::event_stream<Type> _showOther; rpl::event_stream<> _showBack; }; @@ -445,7 +443,7 @@ void LocalPasscodeManage::setupContent() { st::settingsButton, { &st::menuIconLock } )->addClickHandler([=] { - _showOther.fire(LocalPasscodeChange::Id()); + showOther(LocalPasscodeChange::Id()); }); auto autolockLabel = state->autoLockBoxClosing.events_starting_with( @@ -542,10 +540,6 @@ void LocalPasscodeManage::showFinished() { _showFinished.fire({}); } -rpl::producer<Type> LocalPasscodeManage::sectionShowOther() { - return _showOther.events(); -} - rpl::producer<> LocalPasscodeManage::sectionShowBack() { return _showBack.events(); } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 0ad8c0db3..6d92c3d25 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "core/application.h" +#include "settings/settings_business.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" #include "settings/settings_information.h" @@ -27,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/padding_wrap.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/labels.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" @@ -48,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" +#include "main/main_domain.h" #include "main/main_app_config.h" #include "apiwrap.h" #include "api/api_peer_photo.h" @@ -175,7 +179,16 @@ Cover::Cover( }, _name->lifetime()); } -Cover::~Cover() = default; +Cover::~Cover() { + if (_emojiStatusPanel.hasFocus()) { + // Panel will try to return focus to the layer widget, the problem is + // we are destroying the layer widget probably right now and focusing + // it will lead to a crash, because it destroys its children (how we + // got here) after it clears focus out of itself. So if you return + // the focus inside a child destructor, it won't be cleared at all. + window()->setFocus(); + } +} void Cover::setupChildGeometry() { using namespace rpl::mappers; @@ -433,6 +446,19 @@ void SetupPremium( controller->setPremiumRef("settings"); showOther(PremiumId()); }); + const auto button = AddButtonWithIcon( + container, + tr::lng_business_title(), + st::settingsButton, + { .icon = &st::menuIconShop }); + button->addClickHandler([=] { + showOther(BusinessId()); + }); + constexpr auto kNewExpiresAt = int(1711958400); + if (base::unixtime::now() < kNewExpiresAt) { + Ui::NewBadge::AddToRight(button); + } + if (controller->session().premiumCanBuy()) { const auto button = AddButtonWithIcon( container, @@ -443,10 +469,6 @@ void SetupPremium( button->addClickHandler([=] { controller->showGiftPremiumsBox(u"gift"_q); }); - constexpr auto kNewExpiresAt = int(1735689600); - if (base::unixtime::now() < kNewExpiresAt) { - Ui::NewBadge::AddToRight(button); - } } Ui::AddSkip(container); } @@ -691,6 +713,28 @@ rpl::producer<QString> Main::title() { return tr::lng_menu_settings(); } +void Main::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { + const auto &list = Core::App().domain().accounts(); + if (list.size() < Core::App().domain().maxAccounts()) { + addAction(tr::lng_menu_add_account(tr::now), [=] { + Core::App().domain().addActivated(MTP::Environment{}); + }, &st::menuIconAddAccount); + } + if (!_controller->session().supportMode()) { + addAction( + tr::lng_settings_information(tr::now), + [=] { showOther(Information::Id()); }, + &st::menuIconInfo); + } + const auto window = &_controller->window(); + addAction({ + .text = tr::lng_settings_logout(tr::now), + .handler = [=] { window->showLogoutConfirmation(); }, + .icon = &st::menuIconLeaveAttention, + .isAttention = true, + }); +} + void Main::keyPressEvent(QKeyEvent *e) { crl::on_main(this, [=, text = e->text()]{ CodesFeedString(_controller, text); @@ -706,18 +750,14 @@ void Main::setupContent(not_null<Window::SessionController*> controller) { controller, controller->session().user())); - SetupSections(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupSections(controller, content, showOtherMethod()); if (HasInterfaceScale()) { Ui::AddDivider(content); Ui::AddSkip(content); SetupInterfaceScale(&controller->window(), content); Ui::AddSkip(content); } - SetupPremium(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupPremium(controller, content, showOtherMethod()); SetupHelp(controller, content); Ui::ResizeFitChild(this, content); @@ -730,8 +770,4 @@ void Main::setupContent(not_null<Window::SessionController*> controller) { controller->session().data().cloudThemes().refresh(); } -rpl::producer<Type> Main::sectionShowOther() { - return _showOther.events(); -} - } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index e3d26d02b..7040fb9fa 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -38,7 +38,8 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; + void fillTopBarMenu( + const Ui::Menu::MenuCallback &addAction) override; protected: void keyPressEvent(QKeyEvent *e) override; @@ -47,7 +48,6 @@ private: void setupContent(not_null<Window::SessionController*> controller); const not_null<Window::SessionController*> _controller; - rpl::event_stream<Type> _showOther; }; diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index 4074129f1..2bdb2a518 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -1307,17 +1307,11 @@ rpl::producer<QString> Notifications::title() { return tr::lng_settings_section_notify(); } -rpl::producer<Type> Notifications::sectionShowOther() { - return _showOther.events(); -} - void Notifications::setupContent( not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); - SetupNotifications(controller, content, [=](Type type) { - _showOther.fire_copy(type); - }); + SetupNotifications(controller, content, showOtherMethod()); Ui::ResizeFitChild(this, content); } diff --git a/Telegram/SourceFiles/settings/settings_notifications.h b/Telegram/SourceFiles/settings/settings_notifications.h index 1911a48af..f6df4d2e5 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.h +++ b/Telegram/SourceFiles/settings/settings_notifications.h @@ -19,13 +19,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_notifications_type.cpp b/Telegram/SourceFiles/settings/settings_notifications_type.cpp index 52114830f..802a82ee6 100644 --- a/Telegram/SourceFiles/settings/settings_notifications_type.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications_type.cpp @@ -43,7 +43,9 @@ struct Factory : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<NotificationsType>(parent, controller, type); } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 69a70b46e..ca03445fd 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -173,7 +173,7 @@ struct Entry { const style::icon *icon; rpl::producer<QString> title; rpl::producer<QString> description; - PremiumPreview section = PremiumPreview::DoubleLimits; + PremiumFeature section = PremiumFeature::DoubleLimits; bool newBadge = false; }; @@ -201,6 +201,7 @@ using Order = std::vector<QString>; u"infinite_reactions"_q, u"animated_userpics"_q, u"premium_stickers"_q, + u"business"_q, }; } @@ -212,7 +213,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconTags, tr::lng_premium_summary_subtitle_tags_for_messages(), tr::lng_premium_summary_about_tags_for_messages(), - PremiumPreview::TagsForMessages, + PremiumFeature::TagsForMessages, true, }, }, @@ -222,7 +223,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconLastSeen, tr::lng_premium_summary_subtitle_last_seen(), tr::lng_premium_summary_about_last_seen(), - PremiumPreview::LastSeen, + PremiumFeature::LastSeen, true, }, }, @@ -232,7 +233,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconPrivacy, tr::lng_premium_summary_subtitle_message_privacy(), tr::lng_premium_summary_about_message_privacy(), - PremiumPreview::MessagePrivacy, + PremiumFeature::MessagePrivacy, true, }, }, @@ -242,7 +243,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconWallpapers, tr::lng_premium_summary_subtitle_wallpapers(), tr::lng_premium_summary_about_wallpapers(), - PremiumPreview::Wallpapers, + PremiumFeature::Wallpapers, }, }, { @@ -251,7 +252,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStories, tr::lng_premium_summary_subtitle_stories(), tr::lng_premium_summary_about_stories(), - PremiumPreview::Stories, + PremiumFeature::Stories, }, }, { @@ -260,7 +261,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconDouble, tr::lng_premium_summary_subtitle_double_limits(), tr::lng_premium_summary_about_double_limits(), - PremiumPreview::DoubleLimits, + PremiumFeature::DoubleLimits, }, }, { @@ -269,7 +270,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconFiles, tr::lng_premium_summary_subtitle_more_upload(), tr::lng_premium_summary_about_more_upload(), - PremiumPreview::MoreUpload, + PremiumFeature::MoreUpload, }, }, { @@ -278,7 +279,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconSpeed, tr::lng_premium_summary_subtitle_faster_download(), tr::lng_premium_summary_about_faster_download(), - PremiumPreview::FasterDownload, + PremiumFeature::FasterDownload, }, }, { @@ -287,7 +288,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconVoice, tr::lng_premium_summary_subtitle_voice_to_text(), tr::lng_premium_summary_about_voice_to_text(), - PremiumPreview::VoiceToText, + PremiumFeature::VoiceToText, }, }, { @@ -296,7 +297,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconChannelsOff, tr::lng_premium_summary_subtitle_no_ads(), tr::lng_premium_summary_about_no_ads(), - PremiumPreview::NoAds, + PremiumFeature::NoAds, }, }, { @@ -305,7 +306,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStatus, tr::lng_premium_summary_subtitle_emoji_status(), tr::lng_premium_summary_about_emoji_status(), - PremiumPreview::EmojiStatus, + PremiumFeature::EmojiStatus, }, }, { @@ -314,7 +315,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconLike, tr::lng_premium_summary_subtitle_infinite_reactions(), tr::lng_premium_summary_about_infinite_reactions(), - PremiumPreview::InfiniteReactions, + PremiumFeature::InfiniteReactions, }, }, { @@ -323,7 +324,7 @@ using Order = std::vector<QString>; &st::settingsIconStickers, tr::lng_premium_summary_subtitle_premium_stickers(), tr::lng_premium_summary_about_premium_stickers(), - PremiumPreview::Stickers, + PremiumFeature::Stickers, }, }, { @@ -332,7 +333,7 @@ using Order = std::vector<QString>; &st::settingsIconEmoji, tr::lng_premium_summary_subtitle_animated_emoji(), tr::lng_premium_summary_about_animated_emoji(), - PremiumPreview::AnimatedEmoji, + PremiumFeature::AnimatedEmoji, }, }, { @@ -341,7 +342,7 @@ using Order = std::vector<QString>; &st::settingsIconChat, tr::lng_premium_summary_subtitle_advanced_chat_management(), tr::lng_premium_summary_about_advanced_chat_management(), - PremiumPreview::AdvancedChatManagement, + PremiumFeature::AdvancedChatManagement, }, }, { @@ -350,7 +351,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconStar, tr::lng_premium_summary_subtitle_profile_badge(), tr::lng_premium_summary_about_profile_badge(), - PremiumPreview::ProfileBadge, + PremiumFeature::ProfileBadge, }, }, { @@ -359,7 +360,7 @@ using Order = std::vector<QString>; &st::settingsPremiumIconPlay, tr::lng_premium_summary_subtitle_animated_userpics(), tr::lng_premium_summary_about_animated_userpics(), - PremiumPreview::AnimatedUserpics, + PremiumFeature::AnimatedUserpics, }, }, { @@ -368,7 +369,17 @@ using Order = std::vector<QString>; &st::settingsPremiumIconTranslations, tr::lng_premium_summary_subtitle_translation(), tr::lng_premium_summary_about_translation(), - PremiumPreview::RealTimeTranslation, + PremiumFeature::RealTimeTranslation, + }, + }, + { + u"business"_q, + Entry{ + &st::settingsPremiumIconBusiness, + tr::lng_premium_summary_subtitle_business(), + tr::lng_premium_summary_about_business(), + PremiumFeature::Business, + true, }, }, }; @@ -964,7 +975,7 @@ void Premium::setupContent() { setupSubscriptionOptions(content); - auto buttonCallback = [=](PremiumPreview section) { + auto buttonCallback = [=](PremiumFeature section) { _setPaused(true); const auto hidden = crl::guard(this, [=] { _setPaused(false); }); @@ -1189,7 +1200,7 @@ QPointer<Ui::RpWidget> Premium::createPinnedToBottom( std::move(buttonText), std::nullopt, [=, options = session->api().premium().subscriptionOptions()] { - const auto value = _radioGroup->value(); + const auto value = _radioGroup->current(); return (value < options.size() && value >= 0) ? options[value].botUrl : QString(); @@ -1266,7 +1277,9 @@ template <> struct SectionFactory<Premium> : AbstractSectionFactory { object_ptr<AbstractSection> create( not_null<QWidget*> parent, - not_null<Window::SessionController*> controller + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue ) const final override { return object_ptr<Premium>(parent, controller); } @@ -1347,7 +1360,7 @@ void StartPremiumPayment( } } -QString LookupPremiumRef(PremiumPreview section) { +QString LookupPremiumRef(PremiumFeature section) { for (const auto &[ref, entry] : EntryMap()) { if (entry.section == section) { return ref; @@ -1534,7 +1547,7 @@ not_null<Ui::GradientButton*> CreateSubscribeButton( return result; } -[[nodiscard]] std::vector<PremiumPreview> PremiumPreviewOrder( +std::vector<PremiumFeature> PremiumFeaturesOrder( not_null<Main::Session*> session) { const auto mtpOrder = session->account().appConfig().get<Order>( "premium_promo_order", @@ -1543,41 +1556,41 @@ not_null<Ui::GradientButton*> CreateSubscribeButton( mtpOrder ) | ranges::views::transform([](const QString &s) { if (s == u"more_upload"_q) { - return PremiumPreview::MoreUpload; + return PremiumFeature::MoreUpload; } else if (s == u"faster_download"_q) { - return PremiumPreview::FasterDownload; + return PremiumFeature::FasterDownload; } else if (s == u"voice_to_text"_q) { - return PremiumPreview::VoiceToText; + return PremiumFeature::VoiceToText; } else if (s == u"no_ads"_q) { - return PremiumPreview::NoAds; + return PremiumFeature::NoAds; } else if (s == u"emoji_status"_q) { - return PremiumPreview::EmojiStatus; + return PremiumFeature::EmojiStatus; } else if (s == u"infinite_reactions"_q) { - return PremiumPreview::InfiniteReactions; + return PremiumFeature::InfiniteReactions; } else if (s == u"saved_tags"_q) { - return PremiumPreview::TagsForMessages; + return PremiumFeature::TagsForMessages; } else if (s == u"last_seen"_q) { - return PremiumPreview::LastSeen; + return PremiumFeature::LastSeen; } else if (s == u"message_privacy"_q) { - return PremiumPreview::MessagePrivacy; + return PremiumFeature::MessagePrivacy; } else if (s == u"premium_stickers"_q) { - return PremiumPreview::Stickers; + return PremiumFeature::Stickers; } else if (s == u"animated_emoji"_q) { - return PremiumPreview::AnimatedEmoji; + return PremiumFeature::AnimatedEmoji; } else if (s == u"advanced_chat_management"_q) { - return PremiumPreview::AdvancedChatManagement; + return PremiumFeature::AdvancedChatManagement; } else if (s == u"profile_badge"_q) { - return PremiumPreview::ProfileBadge; + return PremiumFeature::ProfileBadge; } else if (s == u"animated_userpics"_q) { - return PremiumPreview::AnimatedUserpics; + return PremiumFeature::AnimatedUserpics; } else if (s == u"translations"_q) { - return PremiumPreview::RealTimeTranslation; + return PremiumFeature::RealTimeTranslation; } else if (s == u"wallpapers"_q) { - return PremiumPreview::Wallpapers; + return PremiumFeature::Wallpapers; } - return PremiumPreview::kCount; - }) | ranges::views::filter([](PremiumPreview type) { - return (type != PremiumPreview::kCount); + return PremiumFeature::kCount; + }) | ranges::views::filter([](PremiumFeature type) { + return (type != PremiumFeature::kCount); }) | ranges::to_vector; } @@ -1585,7 +1598,7 @@ void AddSummaryPremium( not_null<Ui::VerticalLayout*> content, not_null<Window::SessionController*> controller, const QString &ref, - Fn<void(PremiumPreview)> buttonCallback) { + Fn<void(PremiumFeature)> buttonCallback) { const auto &stDefault = st::settingsButton; const auto &stLabel = st::defaultFlatLabel; const auto iconSize = st::settingsPremiumIconDouble.size(); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index a8603aca6..6dd571812 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" -enum class PremiumPreview; +enum class PremiumFeature; namespace style { struct RoundButton; @@ -57,7 +57,7 @@ void StartPremiumPayment( not_null<Window::SessionController*> controller, const QString &ref); -[[nodiscard]] QString LookupPremiumRef(PremiumPreview section); +[[nodiscard]] QString LookupPremiumRef(PremiumFeature section); void ShowPremiumPromoToast( std::shared_ptr<ChatHelpers::Show> show, @@ -91,14 +91,14 @@ struct SubscribeButtonArgs final { [[nodiscard]] not_null<Ui::GradientButton*> CreateSubscribeButton( SubscribeButtonArgs &&args); -[[nodiscard]] std::vector<PremiumPreview> PremiumPreviewOrder( +[[nodiscard]] std::vector<PremiumFeature> PremiumFeaturesOrder( not_null<::Main::Session*> session); void AddSummaryPremium( not_null<Ui::VerticalLayout*> content, not_null<Window::SessionController*> controller, const QString &ref, - Fn<void(PremiumPreview)> buttonCallback); + Fn<void(PremiumFeature)> buttonCallback); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index 6620c4bbb..5f7cb56b1 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -19,7 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/prepare_short_info_box.h" #include "calls/calls_instance.h" #include "core/application.h" -#include "core/core_settings.h" #include "data/data_changes.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. @@ -31,34 +30,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "editor/photo_editor_layer_widget.h" #include "history/admin_log/history_admin_log_item.h" #include "history/history.h" -#include "history/history_item.h" #include "history/history_item_components.h" -#include "history/view/history_view_element.h" #include "history/view/history_view_message.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_premium.h" #include "settings/settings_privacy_security.h" #include "ui/boxes/confirm_box.h" -#include "ui/cached_round_corners.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" -#include "ui/image/image_prepare.h" -#include "ui/image/image_prepare.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/text/format_values.h" // Ui::FormatPhone #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" -#include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/vertical_layout.h" #include "window/section_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" -#include "styles/style_boxes.h" #include "styles/style_settings.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" @@ -203,7 +195,8 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPlong(), // grouped_id MTPMessageReactions(), MTPVector<MTPRestrictionReason>(), - MTPint() // ttl_period + MTPint(), // ttl_period + MTPint() // quick_reply_shortcut_id ).match([&](const MTPDmessage &data) { return history->makeMessage( history->nextNonHistoryEntryId(), @@ -605,7 +598,7 @@ object_ptr<Ui::RpWidget> PhoneNumberPrivacyController::setupMiddleWidget( _saveAdditional = [=] { controller->session().api().userPrivacy().save( Api::UserPrivacy::Key::AddedByPhone, - Api::UserPrivacy::Rule{ .option = group->value() }); + Api::UserPrivacy::Rule{ .option = group->current() }); }; return widget; @@ -1377,6 +1370,78 @@ auto VoicesPrivacyController::exceptionsDescription() const return tr::lng_edit_privacy_voices_exceptions(); } +object_ptr<Ui::RpWidget> VoicesPrivacyController::setupBelowWidget( + not_null<Window::SessionController*> controller, + not_null<QWidget*> parent, + rpl::producer<Option> option) { + using namespace rpl::mappers; + + auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + parent, + object_ptr<Ui::VerticalLayout>(parent)); + result->toggleOn( + Data::AmPremiumValue(&controller->session()) | rpl::map(!_1), + anim::type::instant); + + const auto content = result->entity(); + + Ui::AddSkip(content); + Settings::AddButtonWithIcon( + content, + tr::lng_messages_privacy_premium_button(), + st::messagePrivacySubscribe, + { .icon = &st::menuBlueIconPremium } + )->setClickedCallback([=] { + Settings::ShowPremium( + controller, + u"voice_restrictions_require_premium"_q); + }); + Ui::AddSkip(content); + Ui::AddDividerText(content, tr::lng_messages_privacy_premium_about()); + + return result; +} + +Fn<void()> VoicesPrivacyController::premiumClickedCallback( + Option option, + not_null<Window::SessionController*> controller) { + if (option == Option::Everyone) { + return nullptr; + } + const auto showToast = [=] { + auto link = Ui::Text::Link( + Ui::Text::Semibold( + tr::lng_settings_privacy_premium_link(tr::now))); + _toastInstance = controller->showToast({ + .text = tr::lng_settings_privacy_premium( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .st = &st::defaultMultilineToast, + .duration = Ui::Toast::kDefaultDuration * 2, + .multiline = true, + .filter = crl::guard(&controller->session(), [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = _toastInstance.get()) { + strong->hideAnimated(); + _toastInstance = nullptr; + Settings::ShowPremium( + controller, + u"voice_restrictions_require_premium"_q); + return true; + } + } + return false; + }), + }); + }; + + return showToast; +} + UserPrivacy::Key AboutPrivacyController::key() const { return Key::About; } diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 2c214bfec..25920a1f0 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -18,6 +18,9 @@ class SessionController; namespace Ui { class ChatStyle; +namespace Toast { +class Instance; +} // namespace Toast } // namespace Ui namespace Settings { @@ -280,8 +283,16 @@ public: rpl::producer<QString> exceptionBoxTitle( Exception exception) const override; rpl::producer<QString> exceptionsDescription() const override; + object_ptr<Ui::RpWidget> setupBelowWidget( + not_null<Window::SessionController*> controller, + not_null<QWidget*> parent, + rpl::producer<Option> option) override; + Fn<void()> premiumClickedCallback( + Option option, + not_null<Window::SessionController*> controller) override; private: + base::weak_ptr<Ui::Toast::Instance> _toastInstance; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index feeb83a66..6d16cac01 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_privacy_security.h" #include "api/api_authorizations.h" -#include "api/api_blocked_peers.h" #include "api/api_cloud_password.h" #include "api/api_self_destruct.h" #include "api/api_sensitive_content.h" @@ -24,31 +23,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_privacy_controllers.h" #include "settings/settings_websites.h" #include "base/timer_rpl.h" -#include "boxes/edit_privacy_box.h" #include "boxes/passcode_box.h" -#include "boxes/auto_lock_box.h" #include "boxes/sessions_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/self_destruction_box.h" #include "core/application.h" #include "core/core_settings.h" #include "ui/chat/chat_style.h" +#include "ui/effects/premium_top_bar.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" -#include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/fade_wrap.h" #include "ui/widgets/shadow.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" -#include "ui/layers/generic_box.h" #include "ui/vertical_list.h" +#include "ui/rect.h" #include "calls/calls_instance.h" -#include "core/core_cloud_password.h" #include "core/update_checker.h" -#include "base/platform/base_platform_last_input.h" #include "lang/lang_keys.h" #include "data/data_session.h" #include "data/data_chat.h" @@ -62,9 +55,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" #include "styles/style_menu_icons.h" #include "styles/style_layers.h" -#include "styles/style_boxes.h" #include <QtGui/QGuiApplication> +#include <QtSvg/QSvgRenderer> namespace Settings { namespace { @@ -73,6 +66,53 @@ constexpr auto kUpdateTimeout = 60 * crl::time(1000); using Privacy = Api::UserPrivacy; +[[nodiscard]] QImage PremiumStar() { + const auto factor = style::DevicePixelRatio(); + const auto size = Size(st::settingsButtonNoIcon.style.font->ascent); + auto image = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto star = QSvgRenderer(Ui::Premium::ColorizedSvg()); + star.render(&p, Rect(size)); + } + return image; +} + +void AddPremiumStar( + not_null<Ui::SettingsButton*> button, + not_null<Main::Session*> session, + rpl::producer<QString> label, + const QMargins &padding) { + const auto badge = Ui::CreateChild<Ui::RpWidget>(button.get()); + badge->showOn(Data::AmPremiumValue(session)); + const auto sampleLeft = st::settingsColorSamplePadding.left(); + const auto badgeLeft = padding.left() + sampleLeft; + + auto star = PremiumStar(); + badge->resize(star.size() / style::DevicePixelRatio()); + badge->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(badge); + p.drawImage(0, 0, star); + }, badge->lifetime()); + + rpl::combine( + button->sizeValue(), + std::move(label) + ) | rpl::start_with_next([=](const QSize &s, const QString &) { + if (s.isNull()) { + return; + } + badge->moveToLeft( + button->fullTextWidth() + badgeLeft, + (s.height() - badge->height()) / 2); + }, badge->lifetime()); +} + QString PrivacyBase(Privacy::Key key, Privacy::Option option) { using Key = Privacy::Key; using Option = Privacy::Option; @@ -124,6 +164,7 @@ rpl::producer<QString> PrivacyString( }); } +#if 0 // Dead code. void AddPremiumPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, @@ -137,6 +178,9 @@ void AddPremiumPrivacyButton( container, rpl::duplicate(label), st)); + + AddPremiumStar(button, session, rpl::duplicate(label), st.padding); + struct State { State(QWidget *parent) : widget(parent) { widget.setAttribute(Qt::WA_TransparentForMouseEvents); @@ -240,24 +284,29 @@ void AddPremiumPrivacyButton( }); }); } +#endif void AddMessagesPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container) { const auto session = &controller->session(); const auto privacy = &session->api().globalPrivacy(); - AddButtonWithLabel( + auto label = rpl::conditional( + privacy->newRequirePremium(), + tr::lng_edit_privacy_premium(), + tr::lng_edit_privacy_everyone()); + const auto &st = st::settingsButtonNoIcon; + const auto button = AddButtonWithLabel( container, tr::lng_settings_messages_privacy(), - rpl::conditional( - privacy->newRequirePremium(), - tr::lng_edit_privacy_premium(), - tr::lng_edit_privacy_everyone()), - st::settingsButtonNoIcon, - {} - )->addClickHandler([=] { + rpl::duplicate(label), + st, + {}); + button->addClickHandler([=] { controller->show(Box(EditMessagesPrivacyBox, controller)); }); + + AddPremiumStar(button, session, rpl::duplicate(label), st.padding); } rpl::producer<int> BlockedPeersCount(not_null<::Main::Session*> session) { @@ -281,7 +330,7 @@ void SetupPrivacy( rpl::producer<QString> label, Key key, auto controllerFactory) { - AddPrivacyButton( + return AddPrivacyButton( controller, container, std::move(label), @@ -319,12 +368,15 @@ void SetupPrivacy( tr::lng_settings_groups_invite(), Key::Invites, [] { return std::make_unique<GroupsInvitePrivacyController>(); }); - AddPremiumPrivacyButton( - controller, - container, - tr::lng_settings_voices_privacy(), - Key::Voices, - [=] { return std::make_unique<VoicesPrivacyController>(session); }); + { + const auto &phrase = tr::lng_settings_voices_privacy; + const auto &st = st::settingsButtonNoIcon; + auto callback = [=] { + return std::make_unique<VoicesPrivacyController>(session); + }; + const auto voices = add(phrase(), Key::Voices, std::move(callback)); + AddPremiumStar(voices, session, phrase(), st.padding); + } AddMessagesPrivacyButton(controller, container); session->api().userPrivacy().reload(Api::UserPrivacy::Key::AddedByPhone); @@ -826,7 +878,7 @@ object_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox() { }); } -void AddPrivacyButton( +not_null<Ui::SettingsButton*> AddPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, rpl::producer<QString> label, @@ -836,13 +888,14 @@ void AddPrivacyButton( const style::SettingsButton *stOverride) { const auto shower = Ui::CreateChild<rpl::lifetime>(container.get()); const auto session = &controller->session(); - AddButtonWithLabel( + const auto button = AddButtonWithLabel( container, std::move(label), PrivacyString(session, key), stOverride ? *stOverride : st::settingsButtonNoIcon, std::move(descriptor) - )->addClickHandler([=] { + ); + button->addClickHandler([=] { *shower = session->api().userPrivacy().value( key ) | rpl::take( @@ -854,6 +907,7 @@ void AddPrivacyButton( value)); }); }); + return button; } void SetupArchiveAndMute( @@ -914,10 +968,6 @@ rpl::producer<QString> PrivacySecurity::title() { return tr::lng_settings_section_privacy(); } -rpl::producer<Type> PrivacySecurity::sectionShowOther() { - return _showOther.events(); -} - void PrivacySecurity::setupContent( not_null<Window::SessionController*> controller) { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); @@ -928,9 +978,7 @@ void PrivacySecurity::setupContent( return rpl::duplicate(updateOnTick); }; - SetupSecurity(controller, content, trigger(), [=](Type type) { - _showOther.fire_copy(type); - }); + SetupSecurity(controller, content, trigger(), showOtherMethod()); SetupPrivacy(controller, content, trigger()); #if !defined OS_MAC_STORE && !defined OS_WIN_STORE SetupSensitiveContent(controller, content, trigger()); diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.h b/Telegram/SourceFiles/settings/settings_privacy_security.h index dfa7f1299..01922b01e 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.h +++ b/Telegram/SourceFiles/settings/settings_privacy_security.h @@ -26,7 +26,7 @@ object_ptr<Ui::BoxContent> EditCloudPasswordBox( void RemoveCloudPassword(not_null<Window::SessionController*> session); object_ptr<Ui::BoxContent> CloudPasswordAppOutdatedBox(); -void AddPrivacyButton( +not_null<Ui::SettingsButton*> AddPrivacyButton( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, rpl::producer<QString> label, @@ -47,13 +47,9 @@ public: [[nodiscard]] rpl::producer<QString> title() override; - rpl::producer<Type> sectionShowOther() override; - private: void setupContent(not_null<Window::SessionController*> controller); - rpl::event_stream<Type> _showOther; - }; } // namespace Settings diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 300b832b0..6fa1e72b8 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -518,6 +518,7 @@ FileLoadTask::FileLoadTask( , _caption(caption) , _spoiler(spoiler) { Expects(to.options.scheduled + || to.options.shortcutId || !to.replaceMediaOf || IsServerMsgId(to.replaceMediaOf)); @@ -902,11 +903,11 @@ void FileLoadTask::process(Args &&args) { attributes.push_back(MTP_documentAttributeImageSize(MTP_int(w), MTP_int(h))); if (ValidateThumbDimensions(w, h)) { - isSticker = (_type == SendMediaType::File) - && Core::IsMimeSticker(filemime) + isSticker = Core::IsMimeSticker(filemime) && (filesize < Storage::kMaxStickerBytesSize) && (Core::IsMimeStickerAnimated(filemime) - || GoodStickerDimensions(w, h)); + || (_type == SendMediaType::File + && GoodStickerDimensions(w, h))); if (isSticker) { attributes.push_back(MTP_documentAttributeSticker( MTP_flags(0), diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 75d42760d..45ad83edd 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -273,24 +273,14 @@ AdminLog::OwnedItem GenerateCommentItem( if (data.comment.isEmpty()) { return nullptr; } - const auto flags = MessageFlag::HasFromId - | MessageFlag::Outgoing - | MessageFlag::FakeHistoryItem; - 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(), - history->session().userId(), - QString(), - TextWithEntities{ data.comment }, - MTP_messageMediaEmpty(), - HistoryMessageMarkupData(), - groupedId); + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::HasFromId + | MessageFlag::Outgoing + | MessageFlag::FakeHistoryItem), + .from = history->session().userPeerId(), + .date = base::unixtime::now(), + }, TextWithEntities{ data.comment }, MTP_messageMediaEmpty()); return AdminLog::OwnedItem(delegate, item); } @@ -298,29 +288,19 @@ AdminLog::OwnedItem GenerateContactItem( not_null<HistoryView::ElementDelegate*> delegate, not_null<History*> history, const Contact &data) { - const auto replyTo = FullReplyTo(); - const auto viaBotId = UserId(); - const auto postAuthor = QString(); - const auto groupedId = uint64(); - const auto item = history->makeMessage( - history->nextNonHistoryEntryId(), - (MessageFlag::HasFromId + const auto item = history->makeMessage({ + .id = history->nextNonHistoryEntryId(), + .flags = (MessageFlag::HasFromId | MessageFlag::Outgoing | MessageFlag::FakeHistoryItem), - replyTo, - viaBotId, - base::unixtime::now(), - history->session().userPeerId(), - postAuthor, - TextWithEntities(), - MTP_messageMediaContact( - MTP_string(data.phone), - MTP_string(data.firstName), - MTP_string(data.lastName), - MTP_string(), // vcard - MTP_long(0)), // user_id - HistoryMessageMarkupData(), - groupedId); + .from = history->session().userPeerId(), + .date = base::unixtime::now(), + }, TextWithEntities(), MTP_messageMediaContact( + MTP_string(data.phone), + MTP_string(data.firstName), + MTP_string(data.lastName), + MTP_string(), // vcard + MTP_long(0))); // user_id return AdminLog::OwnedItem(delegate, item); } diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index f4676dcc5..ff4c92c4e 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -201,57 +201,12 @@ void AddFeaturesList( lt_count, rpl::single(float64(i)))), st::boostLevelBadgePadding); - add( - tr::lng_feature_stories(lt_count, rpl::single(float64(i)), proj), - st::boostFeatureStories); - if (!group) { - add(tr::lng_feature_reactions( - lt_count, - rpl::single(float64(i)), - proj - ), st::boostFeatureCustomReactions); - if (const auto j = features.nameColorsByLevel.find(i) - ; j != end(features.nameColorsByLevel)) { - nameColors += j->second; - } - if (nameColors > 0) { - add(tr::lng_feature_name_color_channel( - lt_count, - rpl::single(float64(nameColors)), - proj - ), st::boostFeatureName); - } - if (const auto j = features.linkStylesByLevel.find(i) - ; j != end(features.linkStylesByLevel)) { - linkStyles += j->second; - } - if (linkStyles > 0) { - add(tr::lng_feature_link_style_channel( - lt_count, - rpl::single(float64(linkStyles)), - proj - ), st::boostFeatureLink); - } - if (i >= features.linkLogoLevel) { - add( - tr::lng_feature_link_emoji(proj), - st::boostFeatureCustomLink); - } - } - if (group && i >= features.emojiPackLevel) { + if (i >= features.customWallpaperLevel) { add( - tr::lng_feature_custom_emoji_pack(proj), - st::boostFeatureCustomEmoji); - } - if (group && i >= features.transcribeLevel) { - add( - tr::lng_feature_transcribe(proj), - st::boostFeatureTranscribe); - } - if (i >= features.emojiStatusLevel) { - add( - tr::lng_feature_emoji_status(proj), - st::boostFeatureEmojiStatus); + (group + ? tr::lng_feature_custom_background_group + : tr::lng_feature_custom_background_channel)(proj), + st::boostFeatureCustomBackground); } if (i >= features.wallpaperLevel) { add( @@ -263,13 +218,58 @@ void AddFeaturesList( proj), st::boostFeatureBackground); } - if (i >= features.customWallpaperLevel) { + if (i >= features.emojiStatusLevel) { add( - (group - ? tr::lng_feature_custom_background_group - : tr::lng_feature_custom_background_channel)(proj), - st::boostFeatureCustomBackground); + tr::lng_feature_emoji_status(proj), + st::boostFeatureEmojiStatus); } + if (group && i >= features.transcribeLevel) { + add( + tr::lng_feature_transcribe(proj), + st::boostFeatureTranscribe); + } + if (group && i >= features.emojiPackLevel) { + add( + tr::lng_feature_custom_emoji_pack(proj), + st::boostFeatureCustomEmoji); + } + if (!group) { + if (const auto j = features.linkStylesByLevel.find(i) + ; j != end(features.linkStylesByLevel)) { + linkStyles += j->second; + } + if (i >= features.linkLogoLevel) { + add( + tr::lng_feature_link_emoji(proj), + st::boostFeatureCustomLink); + } + if (linkStyles > 0) { + add(tr::lng_feature_link_style_channel( + lt_count, + rpl::single(float64(linkStyles)), + proj + ), st::boostFeatureLink); + } + if (const auto j = features.nameColorsByLevel.find(i) + ; j != end(features.nameColorsByLevel)) { + nameColors += j->second; + } + if (nameColors > 0) { + add(tr::lng_feature_name_color_channel( + lt_count, + rpl::single(float64(nameColors)), + proj + ), st::boostFeatureName); + } + add(tr::lng_feature_reactions( + lt_count, + rpl::single(float64(i)), + proj + ), st::boostFeatureCustomReactions); + } + add( + tr::lng_feature_stories(lt_count, rpl::single(float64(i)), proj), + st::boostFeatureStories); } } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 264c76b91..6ee83977c 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -301,6 +301,7 @@ historySentInvertedIcon: icon {{ "history_sent", historyIconFgInverted, point(2p historyReceivedIcon: icon {{ "history_received", historyOutIconFg, point(2px, 4px) }}; historyReceivedSelectedIcon: icon {{ "history_received", historyOutIconFgSelected, point(2px, 4px) }}; historyReceivedInvertedIcon: icon {{ "history_received", historyIconFgInverted, point(2px, 4px) }}; +historyShortcutStateSpace: 18px; historyViewsSpace: 8px; historyViewsWidth: 20px; @@ -1043,6 +1044,16 @@ premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; premiumRequiredCircle: 60px; +repliesEmptyIcon: icon{{ "chat/large_quickreply", msgServiceFg }}; +greetingEmptyIcon: icon{{ "chat/large_greeting", msgServiceFg }}; +awayEmptyIcon: icon{{ "chat/large_away", msgServiceFg }}; +repliesEmptyWidth: 264px; +repliesEmptySkip: 16px; +repliesEmptyPadding: margins(10px, 20px, 10px, 16px); +repliesComposeControls: ComposeControls(defaultComposeControls) { + tabbedHeightMin: 220px; +} + boostMessageIcon: icon {{ "stories/boost_mini", windowFg }}; boostMessageIconPadding: margins(0px, 2px, 0px, 0px); boostsMessageIcon: icon {{ "stories/boosts_mini", windowFg }}; diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index e46760ee1..3d3cb00ef 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -1063,7 +1063,7 @@ void AddAccountsRow( }); const auto index = int(state->accounts.size()) - 1; state->accounts[index].checkbox.setChecked( - index == group->value(), + index == group->current(), anim::type::instant); widget->paintRequest( @@ -1303,7 +1303,7 @@ void AddGiftOptions( int nowIndex = 0; Ui::Animations::Simple animation; }; - const auto wasGroupValue = group->value(); + const auto wasGroupValue = group->current(); const auto animation = parent->lifetime().make_state<Animation>(); animation->nowIndex = wasGroupValue; @@ -1324,7 +1324,7 @@ void AddGiftOptions( const auto &stCheckbox = st::defaultBoxCheckbox; auto radioView = std::make_unique<GradientRadioView>( st::defaultRadio, - (group->hasValue() && group->value() == index)); + (group->hasValue() && group->current() == index)); const auto radioViewRaw = radioView.get(); const auto radio = Ui::CreateChild<Ui::Radiobutton>( row, @@ -1468,7 +1468,7 @@ void AddGiftOptions( row->setClickedCallback([=, duration = st::defaultCheck.duration] { group->setValue(index); - animation->nowIndex = group->value(); + animation->nowIndex = group->current(); animation->animation.stop(); animation->animation.start( [=] { parent->update(); }, diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 4094b584d..5ef11ba50 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -25,6 +25,21 @@ constexpr auto kBodyAnimationPart = 0.90; constexpr auto kTitleAdditionalScale = 0.15; constexpr auto kMinAcceptableContrast = 4.5; // 1.14; +[[nodiscard]] QImage ScaleTo(QImage image) { + using namespace style; + const auto size = image.size(); + const auto scale = DevicePixelRatio() * Scale() / 300.; + const auto scaled = QSize( + int(base::SafeRound(size.width() * scale)), + int(base::SafeRound(size.height() * scale))); + image = image.scaled( + scaled, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + image.setDevicePixelRatio(DevicePixelRatio()); + return image; +} + } // namespace QString Svg() { @@ -157,27 +172,41 @@ TopBar::TopBar( rpl::producer<TextWithEntities> about, bool light, bool optimizeMinistars) +: TopBar(parent, st, { + .clickContextOther = std::move(clickContextOther), + .title = std::move(title), + .about = std::move(about), + .light = light, + .optimizeMinistars = optimizeMinistars, +}) { +} + +TopBar::TopBar( + not_null<QWidget*> parent, + const style::PremiumCover &st, + TopBarDescriptor &&descriptor) : TopBarAbstract(parent, st) -, _light(light) +, _light(descriptor.light) +, _logo(descriptor.logo) , _titleFont(st.titleFont) , _titlePadding(st.titlePadding) -, _about(this, std::move(about), st.about) -, _ministars(this, optimizeMinistars) { +, _about(this, std::move(descriptor.about), st.about) +, _ministars(this, descriptor.optimizeMinistars) { std::move( - title + descriptor.title ) | rpl::start_with_next([=](QString text) { _titlePath = QPainterPath(); _titlePath.addText(0, _titleFont->ascent, _titleFont, text); update(); }, lifetime()); - if (clickContextOther) { + if (const auto other = descriptor.clickContextOther) { _about->setClickHandlerFilter([=]( const ClickHandlerPtr &handler, Qt::MouseButton button) { ActivateClickHandler(_about, handler, { button, - clickContextOther() + other() }); return false; }); @@ -188,7 +217,10 @@ TopBar::TopBar( ) | rpl::start_with_next([=] { TopBarAbstract::computeIsDark(); - if (!_light && !TopBarAbstract::isDark()) { + if (_logo == u"dollar"_q) { + _dollar = ScaleTo(QImage(u":/gui/art/business_logo.png"_q)); + _ministars.setColorOverride(st::premiumButtonFg->c); + } else if (!_light && !TopBarAbstract::isDark()) { _star.load(Svg()); _ministars.setColorOverride(st::premiumButtonFg->c); } else { @@ -232,8 +264,11 @@ rpl::producer<int> TopBar::additionalHeight() const { } void TopBar::resizeEvent(QResizeEvent *e) { - const auto progress = (e->size().height() - minimumHeight()) - / float64(maximumHeight() - minimumHeight()); + const auto max = maximumHeight(); + const auto min = minimumHeight(); + const auto progress = (max > min) + ? ((e->size().height() - min) / float64(max - min)) + : 1.; _progress.top = 1. - std::clamp( (1. - progress) / kBodyAnimationPart, @@ -291,7 +326,12 @@ void TopBar::paintEvent(QPaintEvent *e) { } p.resetTransform(); - _star.render(&p, _starRect); + if (!_dollar.isNull()) { + auto hq = PainterHighQualityEnabler(p); + p.drawImage(_starRect, _dollar); + } else { + _star.render(&p, _starRect); + } const auto color = _light ? st::settingsPremiumUserTitle.textFg diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 9ccc4fa30..4ac3cc8be 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -64,6 +64,15 @@ private: }; +struct TopBarDescriptor { + Fn<QVariant()> clickContextOther; + QString logo; + rpl::producer<QString> title; + rpl::producer<TextWithEntities> about; + bool light = false; + bool optimizeMinistars = true; +}; + class TopBar final : public TopBarAbstract { public: TopBar( @@ -74,6 +83,10 @@ public: rpl::producer<TextWithEntities> about, bool light = false, bool optimizeMinistars = true); + TopBar( + not_null<QWidget*> parent, + const style::PremiumCover &st, + TopBarDescriptor &&descriptor); ~TopBar(); void setPaused(bool paused) override; @@ -87,11 +100,13 @@ protected: private: const bool _light = false; + const QString _logo; const style::font &_titleFont; const style::margins &_titlePadding; object_ptr<FlatLabel> _about; ColoredMiniStars _ministars; QSvgRenderer _star; + QImage _dollar; struct { float64 top = 0.; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index b0e0dda13..60fa215bf 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -137,6 +137,7 @@ menuIconAntispam: icon {{ "menu/antispam", menuIconColor }}; menuIconChatDiscuss: icon {{ "menu/chat_discuss", menuIconColor }}; menuIconBotCommands: icon {{ "menu/bot_commands", menuIconColor }}; menuIconPremium: icon {{ "menu/premium", menuIconColor }}; +menuIconShop: icon {{ "menu/shop", menuIconColor }}; menuIconIpAddress: icon {{ "menu/ip_address", menuIconColor }}; menuIconAddress: icon {{ "menu/payment_address", menuIconColor }}; menuIconShowAll: icon {{ "menu/all_media", menuIconColor }}; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index c5dd7763b..e792e6560 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -388,7 +388,9 @@ QString FormatPhone(const QString &phone) { if (phone.at(0) == '0') { return phone; } - return Countries::Instance().format({ .phone = phone }).formatted; + return Countries::Instance().format({ + .phone = (phone.at(0) == '+') ? phone.mid(1) : phone, + }).formatted; } QString FormatTTL(float64 ttl) { diff --git a/Telegram/SourceFiles/ui/vertical_list.cpp b/Telegram/SourceFiles/ui/vertical_list.cpp index b1acce232..11347aa61 100644 --- a/Telegram/SourceFiles/ui/vertical_list.cpp +++ b/Telegram/SourceFiles/ui/vertical_list.cpp @@ -31,24 +31,28 @@ void AddDivider(not_null<Ui::VerticalLayout*> container) { void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, - const style::margins &margins) { + const style::margins &margins, + RectParts parts) { AddDividerText( container, std::move(text) | Ui::Text::ToWithEntities(), - margins); + margins, + parts); } void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<TextWithEntities> text, - const style::margins &margins) { + const style::margins &margins, + RectParts parts) { container->add(object_ptr<Ui::DividerLabel>( container, object_ptr<Ui::FlatLabel>( container, std::move(text), st::boxDividerLabel), - margins)); + margins, + parts)); } not_null<Ui::FlatLabel*> AddSubsectionTitle( diff --git a/Telegram/SourceFiles/ui/vertical_list.h b/Telegram/SourceFiles/ui/vertical_list.h index 87deef178..7ab743bd3 100644 --- a/Telegram/SourceFiles/ui/vertical_list.h +++ b/Telegram/SourceFiles/ui/vertical_list.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/rect_part.h" + namespace style { struct FlatLabel; } // namespace style @@ -26,11 +28,13 @@ void AddDivider(not_null<Ui::VerticalLayout*> container); void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, - const style::margins &margins = st::defaultBoxDividerLabelPadding); + const style::margins &margins = st::defaultBoxDividerLabelPadding, + RectParts parts = RectPart::Top | RectPart::Bottom); void AddDividerText( not_null<Ui::VerticalLayout*> container, rpl::producer<TextWithEntities> text, - const style::margins &margins = st::defaultBoxDividerLabelPadding); + const style::margins &margins = st::defaultBoxDividerLabelPadding, + RectParts parts = RectPart::Top | RectPart::Bottom); not_null<Ui::FlatLabel*> AddSubsectionTitle( not_null<Ui::VerticalLayout*> container, rpl::producer<QString> text, diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp index d0cc71ca3..73f2ecf8c 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp @@ -82,15 +82,18 @@ VerticalDrumPicker::VerticalDrumPicker( ) | rpl::start_with_next([=](const QSize &s) { _itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight); _itemsVisible.centerOffset = _itemsVisible.count / 2; - if (_pendingStartIndex && _itemsVisible.count) { - _index = normalizedIndex(base::take(_pendingStartIndex) + if ((_pendingStartIndex >= 0) && _itemsVisible.count) { + _index = normalizedIndex(_pendingStartIndex - _itemsVisible.centerOffset); + _pendingStartIndex = -1; } if (!_loopData.looped) { _loopData.minIndex = -_itemsVisible.centerOffset; _loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset; } + + _changes.fire({}); }, lifetime()); paintRequest( @@ -143,7 +146,9 @@ void VerticalDrumPicker::increaseShift(float64 by) { index++; index = normalizedIndex(index); } - if (!_loopData.looped && (index <= _loopData.minIndex)) { + if (_loopData.minIndex == _loopData.maxIndex) { + _shift = 0.; + } else if (!_loopData.looped && (index <= _loopData.minIndex)) { _shift = std::min(0., shift); _index = _loopData.minIndex; } else if (!_loopData.looped && (index >= _loopData.maxIndex)) { @@ -153,6 +158,7 @@ void VerticalDrumPicker::increaseShift(float64 by) { _shift = shift; _index = index; } + _changes.fire({}); update(); } @@ -269,4 +275,14 @@ int VerticalDrumPicker::index() const { return normalizedIndex(_index + _itemsVisible.centerOffset); } +rpl::producer<int> VerticalDrumPicker::changes() const { + return _changes.events() | rpl::map([=] { return index(); }); +} + +rpl::producer<int> VerticalDrumPicker::value() const { + return rpl::single(index()) + | rpl::then(changes()) + | rpl::distinct_until_changed(); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h index 802f72fba..63c0cbc2a 100644 --- a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h @@ -52,6 +52,8 @@ public: bool looped = false); [[nodiscard]] int index() const; + [[nodiscard]] rpl::producer<int> changes() const; + [[nodiscard]] rpl::producer<int> value() const; void handleWheelEvent(not_null<QWheelEvent*> e); void handleMouseEvent(not_null<QMouseEvent*> e); @@ -75,7 +77,7 @@ private: PaintItemCallback _paintCallback; - int _pendingStartIndex = 0; + int _pendingStartIndex = -1; struct { int count = 0; @@ -84,6 +86,7 @@ private: int _index = 0; float64 _shift = 0.; + rpl::event_stream<> _changes; struct { const bool looped; diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index ddc2a1e86..3c805946e 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -539,7 +539,7 @@ bool ShowReactPremiumError( if (controller->session().premium()) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::TagsForMessages); + ShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages); return true; } else if (controller->session().premium() || ranges::contains(item->chosenReactions(), id) @@ -548,7 +548,7 @@ bool ShowReactPremiumError( } else if (!id.custom()) { return false; } - ShowPremiumPreviewBox(controller, PremiumPreview::InfiniteReactions); + ShowPremiumPreviewBox(controller, PremiumFeature::InfiniteReactions); return true; } diff --git a/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp b/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp index 2f26dc348..8b28ebe00 100644 --- a/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp +++ b/Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp @@ -519,7 +519,7 @@ bool CloudList::insertTillLimit( void CloudList::insert(int index, const Data::CloudTheme &theme) { const auto id = theme.id; const auto value = groupValueForId(id); - const auto checked = _group->hasValue() && (_group->value() == value); + const auto checked = _group->hasValue() && (_group->current() == value); auto check = std::make_unique<CloudListCheck>(checked); const auto raw = check.get(); auto button = std::make_unique<Ui::Radiobutton>( diff --git a/Telegram/SourceFiles/window/window.style b/Telegram/SourceFiles/window/window.style index 38621c69b..8afc8d243 100644 --- a/Telegram/SourceFiles/window/window.style +++ b/Telegram/SourceFiles/window/window.style @@ -303,6 +303,8 @@ windowFilterTypeBots: icon {{ "folders/folders_type_bots", historyPeerUserpicFg windowFilterTypeNoMuted: icon {{ "folders/folders_type_muted", historyPeerUserpicFg }}; windowFilterTypeNoArchived: icon {{ "folders/folders_type_archived", historyPeerUserpicFg }}; windowFilterTypeNoRead: icon {{ "folders/folders_type_read", historyPeerUserpicFg }}; +windowFilterTypeNewChats: icon {{ "folders/folder_new_chats", historyPeerUserpicFg }}; +windowFilterTypeExistingChats: icon {{ "folders/folder_existing_chats", historyPeerUserpicFg }}; windowFilterChatsSectionSubtitleHeight: 28px; windowFilterChatsSectionSubtitle: FlatLabel(defaultFlatLabel) { style: TextStyle(defaultTextStyle) { diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 117aeba8b..28a468f33 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -1096,7 +1096,7 @@ void MainMenu::chooseEmojiStatus() { if (const auto widget = _badge->widget()) { _emojiStatusPanel->show(_controller, widget, _badge->sizeTag()); } else { - ShowPremiumPreviewBox(_controller, PremiumPreview::EmojiStatus); + ShowPremiumPreviewBox(_controller, PremiumFeature::EmojiStatus); } } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index bcba1a434..44ddebc9d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -132,7 +132,8 @@ void ShareBotGame( MTPReplyMarkup(), MTPVector<MTPMessageEntity>(), MTP_int(0), // schedule_date - MTPInputPeer() // send_as + MTPInputPeer(), // send_as + MTPInputQuickReplyShortcut() ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { history->session().api().sendMessageFail(error, history->peer); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 4bad973ff..a36febec5 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1435,8 +1435,10 @@ void SessionController::setupPremiumToast() { session().mtp().requestConfig(); return premium; }) | rpl::start_with_next([=] { - MainWindowShow(this).showToast( - { tr::lng_premium_success(tr::now) }); + MainWindowShow(this).showToast({ + .text = { tr::lng_premium_success(tr::now) }, + .adaptive = true, + }); }, _lifetime); } diff --git a/Telegram/ThirdParty/fcitx5-qt b/Telegram/ThirdParty/fcitx5-qt index 413747e76..cc77e32c0 160000 --- a/Telegram/ThirdParty/fcitx5-qt +++ b/Telegram/ThirdParty/fcitx5-qt @@ -1 +1 @@ -Subproject commit 413747e761b13bacc5ebd01e20810c64c2f3b6dc +Subproject commit cc77e32c0ab675a663a7c019b3bb8cfcc60c5ec3 diff --git a/Telegram/ThirdParty/xdg-desktop-portal b/Telegram/ThirdParty/xdg-desktop-portal new file mode 160000 index 000000000..fa8d41a2f --- /dev/null +++ b/Telegram/ThirdParty/xdg-desktop-portal @@ -0,0 +1 @@ +Subproject commit fa8d41a2f9a5d30a1e41568b6fb53b046dce14dc diff --git a/Telegram/build/prepare/linux.sh b/Telegram/build/prepare/linux.sh index d3ee5b5be..e4d2f920e 100755 --- a/Telegram/build/prepare/linux.sh +++ b/Telegram/build/prepare/linux.sh @@ -1,3 +1,5 @@ +#!/bin/bash + set -e FullExecPath=$PWD pushd `dirname $0` > /dev/null diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index f53417ea3..727e8521b 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -202,6 +202,12 @@ def removeDir(folder): return 'if exist ' + folder + ' rmdir /Q /S ' + folder + '\nif exist ' + folder + ' exit /b 1' return 'rm -rf ' + folder +def setVar(key, multilineValue): + singlelineValue = ' '.join(multilineValue.replace('\n', '').split()); + if win: + return 'SET "' + key + '=' + singlelineValue + '"'; + return key + '="' + singlelineValue + '"'; + def filterByPlatform(commands): commands = commands.split('\n') result = '' @@ -418,7 +424,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 94be868240 + git checkout bed08b53a3 """) stage('msys64', """ @@ -690,10 +696,9 @@ mac: """) stage('dav1d', """ -win: git clone -b 1.2.1 --depth 1 https://code.videolan.org/videolan/dav1d.git cd dav1d - +win: if "%X8664%" equ "x64" ( SET "TARGET=x86_64" ) else ( @@ -709,7 +714,7 @@ win: echo system = 'windows' >> %FILE% echo cpu_family = '%TARGET%' >> %FILE% echo cpu = '%TARGET%' >> %FILE% - echo endian = 'little'>> %FILE% + echo endian = 'little' >> %FILE% depends:python/Scripts/activate.bat %THIRDPARTY_DIR%\\python\\Scripts\\activate.bat @@ -723,12 +728,55 @@ release: win: copy %LIBS_DIR%\\local\\lib\\libdav1d.a %LIBS_DIR%\\local\\lib\\dav1d.lib deactivate +mac: + buildOneArch() { + arch=$1 + folder=`pwd`/$2 + + TARGET="\'${arch}\'" + MIN="\'${MIN_VER}\'" + FILE=cross-file.txt + echo "[binaries]" > $FILE + echo "c = ['clang', '-arch', ${TARGET}]" >> $FILE + echo "cpp = ['clang++', '-arch', ${TARGET}]" >> $FILE + echo "ar = 'ar'" >> $FILE + echo "strip = 'strip'" >> $FILE + echo "[built-in options]" >> $FILE + echo "c_args = [${MIN}]" >> $FILE + echo "cpp_args = [${MIN}]" >> $FILE + echo "c_link_args = [${MIN}]" >> $FILE + echo "cpp_link_args = [${MIN}]" >> $FILE + echo "[host_machine]" >> $FILE + echo "system = 'darwin'" >> $FILE + echo "subsystem = 'macos'" >> $FILE + echo "cpu_family = ${TARGET}" >> $FILE + echo "cpu = ${TARGET}" >> $FILE + echo "endian = 'little'" >> $FILE + + meson setup \\ + --cross-file $FILE \\ + --prefix ${USED_PREFIX} \\ + --default-library=static \\ + --buildtype=minsize \\ + -Denable_tools=false \\ + -Denable_tests=false \\ + ${folder} + meson compile -C ${folder} + meson install -C ${folder} + + mv ${USED_PREFIX}/lib/libdav1d.a ${folder}/libdav1d.a + } + + buildOneArch arm64 build.arm64 + buildOneArch x86_64 build + + lipo -create build.arm64/libdav1d.a build/libdav1d.a -output ${USED_PREFIX}/lib/libdav1d.a """) stage('libavif', """ -win: git clone -b v0.11.1 --depth 1 https://github.com/AOMediaCodec/libavif.git cd libavif +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -743,12 +791,22 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D AVIF_ENABLE_WERROR=OFF \\ + -D AVIF_CODEC_DAV1D=ON + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libde265', """ -win: git clone --depth 1 -b v1.0.12 https://github.com/strukturag/libde265.git cd libde265 +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -768,12 +826,63 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D DISABLE_SSE=ON \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D ENABLE_DECODER=ON \\ + -D ENABLE_ENCODER=OFF + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel +""") + +stage('libwebp', """ + git clone -b v1.3.2 https://github.com/webmproject/libwebp.git + cd libwebp +win: + nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all + nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all + copy out\\release-static\\$X8664\\lib\\libwebp.lib out\\release-static\\$X8664\\lib\\webp.lib + copy out\\release-static\\$X8664\\lib\\libwebpdemux.lib out\\release-static\\$X8664\\lib\\webpdemux.lib + copy out\\release-static\\$X8664\\lib\\libwebpmux.lib out\\release-static\\$X8664\\lib\\webpmux.lib +mac: + buildOneArch() { + arch=$1 + folder=$2 + + CFLAGS=$UNGUARDED cmake -B $folder -G Ninja . \\ + -D CMAKE_BUILD_TYPE=Release \\ + -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_OSX_ARCHITECTURES=$arch \\ + -D WEBP_BUILD_ANIM_UTILS=OFF \\ + -D WEBP_BUILD_CWEBP=OFF \\ + -D WEBP_BUILD_DWEBP=OFF \\ + -D WEBP_BUILD_GIF2WEBP=OFF \\ + -D WEBP_BUILD_IMG2WEBP=OFF \\ + -D WEBP_BUILD_VWEBP=OFF \\ + -D WEBP_BUILD_WEBPMUX=OFF \\ + -D WEBP_BUILD_WEBPINFO=OFF \\ + -D WEBP_BUILD_EXTRAS=OFF + cmake --build $folder $MAKE_THREADS_CNT + } + buildOneArch arm64 build.arm64 + buildOneArch x86_64 build + + lipo -create build.arm64/libsharpyuv.a build/libsharpyuv.a -output build/libsharpyuv.a + lipo -create build.arm64/libwebp.a build/libwebp.a -output build/libwebp.a + lipo -create build.arm64/libwebpdemux.a build/libwebpdemux.a -output build/libwebpdemux.a + lipo -create build.arm64/libwebpmux.a build/libwebpmux.a -output build/libwebpmux.a + cmake --install build """) stage('libheif', """ -win: git clone --depth 1 -b v1.16.2 https://github.com/strukturag/libheif.git cd libheif +win: %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' libheif/CMakeLists.txt cmake . ^ @@ -797,12 +906,55 @@ win: release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + -D BUILD_SHARED_LIBS=OFF \\ + -D ENABLE_PLUGIN_LOADING=OFF \\ + -D WITH_AOM_ENCODER=OFF \\ + -D WITH_AOM_DECODER=OFF \\ + -D WITH_X265=OFF \\ + -D WITH_SvtEnc=OFF \\ + -D WITH_RAV1E=OFF \\ + -D WITH_DAV1D=ON \\ + -D WITH_LIBDE265=ON \\ + -D LIBDE265_INCLUDE_DIR=$USED_PREFIX/include/ \\ + -D LIBDE265_LIBRARY=$USED_PREFIX/lib/libde265.a \\ + -D LIBSHARPYUV_INCLUDE_DIR=$USED_PREFIX/include/webp/ \\ + -D LIBSHARPYUV_LIBRARY=$USED_PREFIX/lib/libsharpyuv.a \\ + -D WITH_EXAMPLES=OFF + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libjxl', """ -win: git clone -b v0.8.2 --depth 1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git cd libjxl +""" + setVar("cmake_defines", """ + -DBUILD_SHARED_LIBS=OFF + -DBUILD_TESTING=OFF + -DJPEGXL_ENABLE_FUZZERS=OFF + -DJPEGXL_ENABLE_DEVTOOLS=OFF + -DJPEGXL_ENABLE_TOOLS=OFF + -DJPEGXL_ENABLE_DOXYGEN=OFF + -DJPEGXL_ENABLE_MANPAGES=OFF + -DJPEGXL_ENABLE_EXAMPLES=OFF + -DJPEGXL_ENABLE_JNI=OFF + -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF + -DJPEGXL_ENABLE_SJPEG=OFF + -DJPEGXL_ENABLE_OPENEXR=OFF + -DJPEGXL_ENABLE_SKCMS=ON + -DJPEGXL_BUNDLE_SKCMS=ON + -DJPEGXL_ENABLE_VIEWERS=OFF + -DJPEGXL_ENABLE_TCMALLOC=OFF + -DJPEGXL_ENABLE_PLUGINS=OFF + -DJPEGXL_ENABLE_COVERAGE=OFF + -DJPEGXL_ENABLE_PROFILER=OFF + -DJPEGXL_WARNINGS_AS_ERRORS=OFF +""") + """ +win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -813,31 +965,20 @@ win: -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ - -DBUILD_SHARED_LIBS=OFF ^ - -DBUILD_TESTING=OFF ^ - -DJPEGXL_ENABLE_FUZZERS=OFF ^ - -DJPEGXL_ENABLE_DEVTOOLS=OFF ^ - -DJPEGXL_ENABLE_TOOLS=OFF ^ - -DJPEGXL_ENABLE_DOXYGEN=OFF ^ - -DJPEGXL_ENABLE_MANPAGES=OFF ^ - -DJPEGXL_ENABLE_EXAMPLES=OFF ^ - -DJPEGXL_ENABLE_JNI=OFF ^ - -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=OFF ^ - -DJPEGXL_ENABLE_SJPEG=OFF ^ - -DJPEGXL_ENABLE_OPENEXR=OFF ^ - -DJPEGXL_ENABLE_SKCMS=ON ^ - -DJPEGXL_BUNDLE_SKCMS=ON ^ - -DJPEGXL_ENABLE_VIEWERS=OFF ^ - -DJPEGXL_ENABLE_TCMALLOC=OFF ^ - -DJPEGXL_ENABLE_PLUGINS=OFF ^ - -DJPEGXL_ENABLE_COVERAGE=OFF ^ - -DJPEGXL_ENABLE_PROFILER=OFF ^ - -DJPEGXL_WARNINGS_AS_ERRORS=OFF + %cmake_defines% cmake --build . --config Debug cmake --install . --config Debug release: cmake --build . --config Release cmake --install . --config Release +mac: + cmake . \\ + -D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \\ + -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ + -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ + ${cmake_defines} + cmake --build . --config MinSizeRel $MAKE_THREADS_CNT + cmake --install . --config MinSizeRel """) stage('libvpx', """ @@ -855,9 +996,9 @@ win: SET MSYS2_PATH_TYPE=inherit if "%X8664%" equ "x64" ( - SET "TARGET=x86_64-win64-vs17" + SET "TOOLCHAIN=x86_64-win64-vs17" ) else ( - SET "TARGET=x86-win32-vs17" + SET "TOOLCHAIN=x86-win32-vs17" ) depends:patches/build_libvpx_win.sh @@ -906,46 +1047,6 @@ depends:yasm/yasm make install """) -stage('libwebp', """ - git clone -b v1.3.2 https://github.com/webmproject/libwebp.git - cd libwebp -win: - nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all - nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all - copy out\\release-static\\$X8664\\lib\\libwebp.lib out\\release-static\\$X8664\\lib\\webp.lib - copy out\\release-static\\$X8664\\lib\\libwebpdemux.lib out\\release-static\\$X8664\\lib\\webpdemux.lib - copy out\\release-static\\$X8664\\lib\\libwebpmux.lib out\\release-static\\$X8664\\lib\\webpmux.lib -mac: - buildOneArch() { - arch=$1 - folder=$2 - - CFLAGS=$UNGUARDED cmake -B $folder -G Ninja . \\ - -D CMAKE_BUILD_TYPE=Release \\ - -D CMAKE_INSTALL_PREFIX=$USED_PREFIX \\ - -D CMAKE_OSX_DEPLOYMENT_TARGET:STRING=$MACOSX_DEPLOYMENT_TARGET \\ - -D CMAKE_OSX_ARCHITECTURES=$arch \\ - -D WEBP_BUILD_ANIM_UTILS=OFF \\ - -D WEBP_BUILD_CWEBP=OFF \\ - -D WEBP_BUILD_DWEBP=OFF \\ - -D WEBP_BUILD_GIF2WEBP=OFF \\ - -D WEBP_BUILD_IMG2WEBP=OFF \\ - -D WEBP_BUILD_VWEBP=OFF \\ - -D WEBP_BUILD_WEBPMUX=OFF \\ - -D WEBP_BUILD_WEBPINFO=OFF \\ - -D WEBP_BUILD_EXTRAS=OFF - cmake --build $folder $MAKE_THREADS_CNT - } - buildOneArch arm64 build.arm64 - buildOneArch x86_64 build - - lipo -create build.arm64/libsharpyuv.a build/libsharpyuv.a -output build/libsharpyuv.a - lipo -create build.arm64/libwebp.a build/libwebp.a -output build/libwebp.a - lipo -create build.arm64/libwebpdemux.a build/libwebpdemux.a -output build/libwebpdemux.a - lipo -create build.arm64/libwebpmux.a build/libwebpmux.a -output build/libwebpmux.a - cmake --install build -""") - stage('nv-codec-headers', """ win: git clone https://github.com/FFmpeg/nv-codec-headers.git @@ -1300,11 +1401,9 @@ release: if buildQt5: stage('qt_5_15_12', """ - git clone https://github.com/qt/qt5.git qt_5_15_12 + git clone -b v5.15.12-lts-lgpl https://github.com/qt/qt5.git qt_5_15_12 cd qt_5_15_12 perl init-repository --module-subset=qtbase,qtimageformats,qtsvg - git checkout v5.15.12-lts-lgpl - git submodule update qtbase qtimageformats qtsvg depends:patches/qtbase_5.15.12/*.patch cd qtbase win: @@ -1413,6 +1512,7 @@ mac: -system-webp \ -I "$USED_PREFIX/include" \ -no-feature-futimens \ + -no-feature-brotli \ -nomake examples \ -nomake tests \ -platform macx-clang -- \ diff --git a/Telegram/build/setup.iss b/Telegram/build/setup.iss index c7e70f43a..e146e9070 100644 --- a/Telegram/build/setup.iss +++ b/Telegram/build/setup.iss @@ -33,6 +33,7 @@ VersionInfoVersion={#MyAppVersion}.0 CloseApplications=force DisableDirPage=no DisableProgramGroupPage=no +WizardStyle=modern #if MyBuildTarget == "win64" ArchitecturesAllowed="x64 arm64" diff --git a/Telegram/build/version b/Telegram/build/version index cb2635869..e540466fc 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4015000 +AppVersion 4015002 AppVersionStrMajor 4.15 -AppVersionStrSmall 4.15 -AppVersionStr 4.15.0 +AppVersionStrSmall 4.15.2 +AppVersionStr 4.15.2 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.15 +AppVersionOriginal 4.15.2 diff --git a/Telegram/lib_base b/Telegram/lib_base index 888a19075..5b9556fdd 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 888a19075b569eda3d18a977543320823b984ae0 +Subproject commit 5b9556fddb9a67e514d0bed2c123e18cbe1663b7 diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index 96543c171..9b52030bf 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit 96543c1716d3790ef12bdec6b113958427710441 +Subproject commit 9b52030bfcd7e90e3e550231a3783ad1982fda78 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 4fce8b197..fbf9dd547 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 4fce8b1971721da739619acf36da0fe79d614a23 +Subproject commit fbf9dd54787df90c98cf230cb53323527e0b0639 diff --git a/changelog.txt b/changelog.txt index 4453ccb54..f1b710187 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,17 @@ +4.15.2 (12.03.24) + +- Telegram Business: Greeting Message. +- Telegram Business: Away Message. +- Telegram Business: Quick Replies. +- Telegram Business: Working Hours. +- Close the ongoing call window without hanging up the call. +- Fast scroll through chats list with Ctrl or Shift pressed. +- Several bugfixes. + +4.15.1 (08.03.24) + +- Telegram Business features. + 4.15 (18.02.24) - Stories from groups. diff --git a/cmake b/cmake index a46279fcf..5a61112d6 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit a46279fcfe69ebcc806bb31679ccece5f7c07508 +Subproject commit 5a61112d6d025b56573ad48bcc1331ac65c4a927