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 &current() 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 &params) {
-	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 &params) {
-	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 &params);
 	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 &parameters) {
-				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 &parameters) {
-			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 &current = 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 &params);
+	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 &params) {
+	_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