diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 1fd37cb054193b..d622736643783a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -160,7 +160,7 @@ jobs: - name: Upload. if: (github.event_name == 'release') - uses: svenstaro/upload-release-action@v1-release + uses: svenstaro/upload-release-action@2.3.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ./Telegram.tar.xz diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 8857dc74f8341e..46fafdbad66cc2 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -130,9 +130,9 @@ jobs: if [ -n "${{ matrix.defines }}" ]; then DEFINE="-D ${{ matrix.defines }}=ON" echo Define from matrix: $DEFINE - echo "ARTIFACT_NAME=Telegram_${{ matrix.defines }}" >> $GITHUB_ENV + echo "ARTIFACT_NAME=Telegram_${{ matrix.arch }}_${{ matrix.defines }}" >> $GITHUB_ENV else - echo "ARTIFACT_NAME=Telegram" >> $GITHUB_ENV + echo "ARTIFACT_NAME=Telegram_${{ matrix.arch }}" >> $GITHUB_ENV fi echo "TDESKTOP_BUILD_DEFINE=$DEFINE" >> $GITHUB_ENV diff --git a/.gitmodules b/.gitmodules index 074f7c5c732971..6aa07142a54f6e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -85,15 +85,9 @@ [submodule "Telegram/ThirdParty/jemalloc"] path = Telegram/ThirdParty/jemalloc url = https://github.com/jemalloc/jemalloc -[submodule "Telegram/ThirdParty/kwayland"] - path = Telegram/ThirdParty/kwayland - url = https://github.com/KDE/kwayland.git [submodule "Telegram/ThirdParty/dispatch"] path = Telegram/ThirdParty/dispatch url = https://github.com/apple/swift-corelibs-libdispatch -[submodule "Telegram/ThirdParty/extra-cmake-modules"] - path = Telegram/ThirdParty/extra-cmake-modules - url = https://github.com/KDE/extra-cmake-modules.git [submodule "Telegram/ThirdParty/plasma-wayland-protocols"] path = Telegram/ThirdParty/plasma-wayland-protocols url = https://github.com/KDE/plasma-wayland-protocols.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 15fda19810a1bf..9694f98ccb06d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,12 @@ # For license and copyright information please follow this link: # https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -cmake_minimum_required(VERSION 3.16) +if (APPLE) + # target_precompile_headers with COMPILE_LANGUAGE restriction. + cmake_minimum_required(VERSION 3.23) +else() + cmake_minimum_required(VERSION 3.16) +endif() cmake_policy(SET CMP0076 NEW) cmake_policy(SET CMP0091 NEW) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index ec84262db4277f..0573f7f260b30d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -97,7 +97,7 @@ PRIVATE desktop-app::external_xxhash ) -target_precompile_headers(Telegram PRIVATE ${src_loc}/stdafx.h) +target_precompile_headers(Telegram PRIVATE $<$:${src_loc}/stdafx.h>) nice_target_sources(Telegram ${src_loc} PRIVATE ${style_files} @@ -141,6 +141,8 @@ PRIVATE api/api_peer_photo.h api/api_polls.cpp api/api_polls.h + api/api_premium.cpp + api/api_premium.h api/api_report.cpp api/api_report.h api/api_ringtones.cpp @@ -159,6 +161,8 @@ PRIVATE api/api_text_entities.h api/api_toggling_media.cpp api/api_toggling_media.h + api/api_transcribes.cpp + api/api_transcribes.h api/api_unread_things.cpp api/api_unread_things.h api/api_updates.cpp @@ -260,6 +264,10 @@ PRIVATE boxes/phone_banned_box.h boxes/pin_messages_box.cpp boxes/pin_messages_box.h + boxes/premium_limits_box.cpp + boxes/premium_limits_box.h + boxes/premium_preview_box.cpp + boxes/premium_preview_box.h boxes/reactions_settings_box.cpp boxes/reactions_settings_box.h boxes/report_messages_box.cpp @@ -475,13 +483,17 @@ PRIVATE data/data_media_rotation.h data/data_media_types.cpp data/data_media_types.h - data/data_messages.cpp - data/data_messages.h + # data/data_messages.cpp + # data/data_messages.h data/data_message_reactions.cpp data/data_message_reactions.h data/data_msg_id.h data/data_peer.cpp data/data_peer.h + data/data_peer_bot_command.cpp + data/data_peer_bot_command.h + data/data_peer_bot_commands.cpp + data/data_peer_bot_commands.h data/data_peer_id.cpp data/data_peer_id.h data/data_peer_values.cpp @@ -492,6 +504,8 @@ PRIVATE data/data_photo_media.h data/data_poll.cpp data/data_poll.h + data/data_premium_limits.cpp + data/data_premium_limits.h data/data_pts_waiter.cpp data/data_pts_waiter.h data/data_replies_list.cpp @@ -548,6 +562,8 @@ PRIVATE dialogs/ui/dialogs_layout.h dialogs/ui/dialogs_message_view.cpp dialogs/ui/dialogs_message_view.h + dialogs/ui/dialogs_video_userpic.cpp + dialogs/ui/dialogs_video_userpic.h editor/color_picker.cpp editor/color_picker.h editor/controllers/controllers.h @@ -692,6 +708,10 @@ PRIVATE history/view/history_view_service_message.h history/view/history_view_spoiler_click_handler.cpp history/view/history_view_spoiler_click_handler.h + history/view/history_view_sticker_toast.cpp + history/view/history_view_sticker_toast.h + history/view/history_view_transcribe_button.cpp + history/view/history_view_transcribe_button.h history/view/history_view_top_bar_widget.cpp history/view/history_view_top_bar_widget.h history/view/history_view_view_button.cpp @@ -722,6 +742,8 @@ PRIVATE history/history_service.h history/history_unread_things.cpp history/history_unread_things.h + history/history_view_highlight_manager.cpp + history/history_view_highlight_manager.h history/history_widget.cpp history/history_widget.h info/info_content_widget.cpp @@ -981,8 +1003,6 @@ PRIVATE platform/linux/linux_wayland_integration_dummy.cpp platform/linux/linux_wayland_integration.cpp platform/linux/linux_wayland_integration.h - platform/linux/linux_xdp_file_dialog.cpp - platform/linux/linux_xdp_file_dialog.h platform/linux/linux_xdp_open_with_dialog.cpp platform/linux/linux_xdp_open_with_dialog.h platform/linux/file_utilities_linux.cpp @@ -1122,6 +1142,8 @@ PRIVATE settings/settings_main.h settings/settings_notifications.cpp settings/settings_notifications.h + settings/settings_premium.cpp + settings/settings_premium.h settings/settings_privacy_controllers.cpp settings/settings_privacy_controllers.h settings/settings_privacy_security.cpp @@ -1189,13 +1211,9 @@ PRIVATE ui/chat/choose_send_as.h ui/chat/choose_theme_controller.cpp ui/chat/choose_theme_controller.h - ui/effects/fireworks_animation.cpp - ui/effects/fireworks_animation.h ui/effects/message_sending_animation_common.h ui/effects/message_sending_animation_controller.cpp ui/effects/message_sending_animation_controller.h - ui/effects/round_checkbox.cpp - ui/effects/round_checkbox.h ui/effects/send_action_animations.cpp ui/effects/send_action_animations.h ui/image/image.cpp @@ -1306,8 +1324,6 @@ endif() if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION) remove_target_sources(Telegram ${src_loc} - platform/linux/linux_xdp_file_dialog.cpp - platform/linux/linux_xdp_file_dialog.h platform/linux/linux_xdp_open_with_dialog.cpp platform/linux/linux_xdp_open_with_dialog.h platform/linux/notifications_manager_linux.cpp @@ -1436,10 +1452,19 @@ else() endif() if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) + if (DESKTOP_APP_QT6) + qt6_generate_wayland_protocol_client_sources(Telegram + FILES + ${third_party_loc}/plasma-wayland-protocols/src/protocols/plasma-shell.xml + ) + else() + message(FATAL_ERROR "This piece of cmake code is not ported to Qt 5") + endif() + target_link_libraries(Telegram PRIVATE desktop-app::lib_waylandshells - desktop-app::external_kwayland + desktop-app::external_wayland_client ) endif() endif() diff --git a/Telegram/Resources/icons/chat/reactions_premium_bg.png b/Telegram/Resources/icons/chat/reactions_premium_bg.png new file mode 100644 index 00000000000000..fbac75ad6d809a Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_bg.png differ diff --git a/Telegram/Resources/icons/chat/reactions_premium_bg@2x.png b/Telegram/Resources/icons/chat/reactions_premium_bg@2x.png new file mode 100644 index 00000000000000..20cc69e9c56215 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_bg@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_premium_bg@3x.png b/Telegram/Resources/icons/chat/reactions_premium_bg@3x.png new file mode 100644 index 00000000000000..791ae612c618f3 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_bg@3x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_premium_star.png b/Telegram/Resources/icons/chat/reactions_premium_star.png new file mode 100644 index 00000000000000..6e14fb0084c8ae Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_star.png differ diff --git a/Telegram/Resources/icons/chat/reactions_premium_star@2x.png b/Telegram/Resources/icons/chat/reactions_premium_star@2x.png new file mode 100644 index 00000000000000..c2d4aea54dbe08 Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_star@2x.png differ diff --git a/Telegram/Resources/icons/chat/reactions_premium_star@3x.png b/Telegram/Resources/icons/chat/reactions_premium_star@3x.png new file mode 100644 index 00000000000000..e46f5c98ea693d Binary files /dev/null and b/Telegram/Resources/icons/chat/reactions_premium_star@3x.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text.png b/Telegram/Resources/icons/chat/voice_to_text.png new file mode 100644 index 00000000000000..b677a7c5e54976 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text@2x.png b/Telegram/Resources/icons/chat/voice_to_text@2x.png new file mode 100644 index 00000000000000..224b40bb369d33 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text@2x.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text@3x.png b/Telegram/Resources/icons/chat/voice_to_text@3x.png new file mode 100644 index 00000000000000..731ba5aad67076 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text@3x.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text_collapse.png b/Telegram/Resources/icons/chat/voice_to_text_collapse.png new file mode 100644 index 00000000000000..752f8be2fe5860 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text_collapse.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text_collapse@2x.png b/Telegram/Resources/icons/chat/voice_to_text_collapse@2x.png new file mode 100644 index 00000000000000..da161b7338abc8 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text_collapse@2x.png differ diff --git a/Telegram/Resources/icons/chat/voice_to_text_collapse@3x.png b/Telegram/Resources/icons/chat/voice_to_text_collapse@3x.png new file mode 100644 index 00000000000000..204ffcbac71506 Binary files /dev/null and b/Telegram/Resources/icons/chat/voice_to_text_collapse@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_premium.png b/Telegram/Resources/icons/dialogs/dialogs_premium.png new file mode 100644 index 00000000000000..199597443f6d78 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_premium.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_premium@2x.png b/Telegram/Resources/icons/dialogs/dialogs_premium@2x.png new file mode 100644 index 00000000000000..7caa2f465ec06d Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_premium@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_premium@3x.png b/Telegram/Resources/icons/dialogs/dialogs_premium@3x.png new file mode 100644 index 00000000000000..7996adc28a28a3 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_premium@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_check.png b/Telegram/Resources/icons/dialogs/dialogs_verified_check.png index 14dfa4ee68d4c0..dc9c4b0da56a22 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_check.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_check.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_check@2x.png b/Telegram/Resources/icons/dialogs/dialogs_verified_check@2x.png index 1caf9a6853fb27..677770111ab2e3 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_check@2x.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_check@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_check@3x.png b/Telegram/Resources/icons/dialogs/dialogs_verified_check@3x.png index f2b4cea40f7029..ceffe4197bddda 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_check@3x.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_check@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_star.png b/Telegram/Resources/icons/dialogs/dialogs_verified_star.png index 334e70cdf53a3d..24adac8ca8bfc2 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_star.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_star.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_star@2x.png b/Telegram/Resources/icons/dialogs/dialogs_verified_star@2x.png index 9bfd666d5c160e..98d33bd2282aa5 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_star@2x.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_star@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_verified_star@3x.png b/Telegram/Resources/icons/dialogs/dialogs_verified_star@3x.png index bcfd5eeb77a643..55734f48e6a56b 100644 Binary files a/Telegram/Resources/icons/dialogs/dialogs_verified_star@3x.png and b/Telegram/Resources/icons/dialogs/dialogs_verified_star@3x.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_settings.png b/Telegram/Resources/icons/emoji/emoji_settings.png index ed8de446d6f77f..fa77adf096ea03 100644 Binary files a/Telegram/Resources/icons/emoji/emoji_settings.png and b/Telegram/Resources/icons/emoji/emoji_settings.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_settings@2x.png b/Telegram/Resources/icons/emoji/emoji_settings@2x.png index ded23bc2ff2dea..591d1164a3c1af 100644 Binary files a/Telegram/Resources/icons/emoji/emoji_settings@2x.png and b/Telegram/Resources/icons/emoji/emoji_settings@2x.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_settings@3x.png b/Telegram/Resources/icons/emoji/emoji_settings@3x.png index 7b16e6b5b92d95..70c9bfaa2bbae6 100644 Binary files a/Telegram/Resources/icons/emoji/emoji_settings@3x.png and b/Telegram/Resources/icons/emoji/emoji_settings@3x.png differ diff --git a/Telegram/Resources/icons/emoji/premium_lock.png b/Telegram/Resources/icons/emoji/premium_lock.png new file mode 100644 index 00000000000000..0498a400ee86ee Binary files /dev/null and b/Telegram/Resources/icons/emoji/premium_lock.png differ diff --git a/Telegram/Resources/icons/emoji/premium_lock@2x.png b/Telegram/Resources/icons/emoji/premium_lock@2x.png new file mode 100644 index 00000000000000..78390b73412af9 Binary files /dev/null and b/Telegram/Resources/icons/emoji/premium_lock@2x.png differ diff --git a/Telegram/Resources/icons/emoji/premium_lock@3x.png b/Telegram/Resources/icons/emoji/premium_lock@3x.png new file mode 100644 index 00000000000000..81b8e06001552d Binary files /dev/null and b/Telegram/Resources/icons/emoji/premium_lock@3x.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_premium.png b/Telegram/Resources/icons/emoji/stickers_premium.png new file mode 100644 index 00000000000000..f2fc24545c7312 Binary files /dev/null and b/Telegram/Resources/icons/emoji/stickers_premium.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_premium@2x.png b/Telegram/Resources/icons/emoji/stickers_premium@2x.png new file mode 100644 index 00000000000000..71b77efe222c83 Binary files /dev/null and b/Telegram/Resources/icons/emoji/stickers_premium@2x.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_premium@3x.png b/Telegram/Resources/icons/emoji/stickers_premium@3x.png new file mode 100644 index 00000000000000..9676bc9c553049 Binary files /dev/null and b/Telegram/Resources/icons/emoji/stickers_premium@3x.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_search.png b/Telegram/Resources/icons/emoji/stickers_search.png index 1c78d9ce49c5cb..9645d1c9b5eac2 100644 Binary files a/Telegram/Resources/icons/emoji/stickers_search.png and b/Telegram/Resources/icons/emoji/stickers_search.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_search@2x.png b/Telegram/Resources/icons/emoji/stickers_search@2x.png index 25bf885bfe6fe5..fedddd5a50d357 100644 Binary files a/Telegram/Resources/icons/emoji/stickers_search@2x.png and b/Telegram/Resources/icons/emoji/stickers_search@2x.png differ diff --git a/Telegram/Resources/icons/emoji/stickers_search@3x.png b/Telegram/Resources/icons/emoji/stickers_search@3x.png index 5ec974dacdcf76..f3d5cef32e5870 100644 Binary files a/Telegram/Resources/icons/emoji/stickers_search@3x.png and b/Telegram/Resources/icons/emoji/stickers_search@3x.png differ diff --git a/Telegram/Resources/icons/limits/accounts.png b/Telegram/Resources/icons/limits/accounts.png new file mode 100644 index 00000000000000..84205fe9bacb56 Binary files /dev/null and b/Telegram/Resources/icons/limits/accounts.png differ diff --git a/Telegram/Resources/icons/limits/accounts@2x.png b/Telegram/Resources/icons/limits/accounts@2x.png new file mode 100644 index 00000000000000..7b5dc52061e2c1 Binary files /dev/null and b/Telegram/Resources/icons/limits/accounts@2x.png differ diff --git a/Telegram/Resources/icons/limits/accounts@3x.png b/Telegram/Resources/icons/limits/accounts@3x.png new file mode 100644 index 00000000000000..43210dd1c32e9b Binary files /dev/null and b/Telegram/Resources/icons/limits/accounts@3x.png differ diff --git a/Telegram/Resources/icons/limits/chats.png b/Telegram/Resources/icons/limits/chats.png new file mode 100644 index 00000000000000..2c3815a8ed4419 Binary files /dev/null and b/Telegram/Resources/icons/limits/chats.png differ diff --git a/Telegram/Resources/icons/limits/chats@2x.png b/Telegram/Resources/icons/limits/chats@2x.png new file mode 100644 index 00000000000000..414acb193834eb Binary files /dev/null and b/Telegram/Resources/icons/limits/chats@2x.png differ diff --git a/Telegram/Resources/icons/limits/chats@3x.png b/Telegram/Resources/icons/limits/chats@3x.png new file mode 100644 index 00000000000000..c814dd1994b293 Binary files /dev/null and b/Telegram/Resources/icons/limits/chats@3x.png differ diff --git a/Telegram/Resources/icons/limits/files.png b/Telegram/Resources/icons/limits/files.png new file mode 100644 index 00000000000000..48ed2f5d935360 Binary files /dev/null and b/Telegram/Resources/icons/limits/files.png differ diff --git a/Telegram/Resources/icons/limits/files@2x.png b/Telegram/Resources/icons/limits/files@2x.png new file mode 100644 index 00000000000000..368d56ca98bd0c Binary files /dev/null and b/Telegram/Resources/icons/limits/files@2x.png differ diff --git a/Telegram/Resources/icons/limits/files@3x.png b/Telegram/Resources/icons/limits/files@3x.png new file mode 100644 index 00000000000000..8b204778c90b48 Binary files /dev/null and b/Telegram/Resources/icons/limits/files@3x.png differ diff --git a/Telegram/Resources/icons/limits/folders.png b/Telegram/Resources/icons/limits/folders.png new file mode 100644 index 00000000000000..6c94dab3184e42 Binary files /dev/null and b/Telegram/Resources/icons/limits/folders.png differ diff --git a/Telegram/Resources/icons/limits/folders@2x.png b/Telegram/Resources/icons/limits/folders@2x.png new file mode 100644 index 00000000000000..6372f5a9a750e5 Binary files /dev/null and b/Telegram/Resources/icons/limits/folders@2x.png differ diff --git a/Telegram/Resources/icons/limits/folders@3x.png b/Telegram/Resources/icons/limits/folders@3x.png new file mode 100644 index 00000000000000..0c515302e021f9 Binary files /dev/null and b/Telegram/Resources/icons/limits/folders@3x.png differ diff --git a/Telegram/Resources/icons/limits/groups.png b/Telegram/Resources/icons/limits/groups.png new file mode 100644 index 00000000000000..06948fb75be2c8 Binary files /dev/null and b/Telegram/Resources/icons/limits/groups.png differ diff --git a/Telegram/Resources/icons/limits/groups@2x.png b/Telegram/Resources/icons/limits/groups@2x.png new file mode 100644 index 00000000000000..b4176982e73147 Binary files /dev/null and b/Telegram/Resources/icons/limits/groups@2x.png differ diff --git a/Telegram/Resources/icons/limits/groups@3x.png b/Telegram/Resources/icons/limits/groups@3x.png new file mode 100644 index 00000000000000..ba35f53981d6ab Binary files /dev/null and b/Telegram/Resources/icons/limits/groups@3x.png differ diff --git a/Telegram/Resources/icons/limits/links.png b/Telegram/Resources/icons/limits/links.png new file mode 100644 index 00000000000000..96d5761299b82b Binary files /dev/null and b/Telegram/Resources/icons/limits/links.png differ diff --git a/Telegram/Resources/icons/limits/links@2x.png b/Telegram/Resources/icons/limits/links@2x.png new file mode 100644 index 00000000000000..90ac07fddcf0d5 Binary files /dev/null and b/Telegram/Resources/icons/limits/links@2x.png differ diff --git a/Telegram/Resources/icons/limits/links@3x.png b/Telegram/Resources/icons/limits/links@3x.png new file mode 100644 index 00000000000000..926a5ad409217c Binary files /dev/null and b/Telegram/Resources/icons/limits/links@3x.png differ diff --git a/Telegram/Resources/icons/limits/pins.png b/Telegram/Resources/icons/limits/pins.png new file mode 100644 index 00000000000000..572cffebfbcb81 Binary files /dev/null and b/Telegram/Resources/icons/limits/pins.png differ diff --git a/Telegram/Resources/icons/limits/pins@2x.png b/Telegram/Resources/icons/limits/pins@2x.png new file mode 100644 index 00000000000000..a1f4bbd4024320 Binary files /dev/null and b/Telegram/Resources/icons/limits/pins@2x.png differ diff --git a/Telegram/Resources/icons/limits/pins@3x.png b/Telegram/Resources/icons/limits/pins@3x.png new file mode 100644 index 00000000000000..a7bfe571a883e3 Binary files /dev/null and b/Telegram/Resources/icons/limits/pins@3x.png differ diff --git a/Telegram/Resources/icons/profile_admin_star.png b/Telegram/Resources/icons/profile_admin_star.png deleted file mode 100644 index bee28eba7f6d2d..00000000000000 Binary files a/Telegram/Resources/icons/profile_admin_star.png and /dev/null differ diff --git a/Telegram/Resources/icons/profile_admin_star@2x.png b/Telegram/Resources/icons/profile_admin_star@2x.png deleted file mode 100644 index 4756363b41e833..00000000000000 Binary files a/Telegram/Resources/icons/profile_admin_star@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/profile_admin_star@3x.png b/Telegram/Resources/icons/profile_admin_star@3x.png deleted file mode 100644 index 376b48673f746e..00000000000000 Binary files a/Telegram/Resources/icons/profile_admin_star@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/profile_premium.png b/Telegram/Resources/icons/profile_premium.png new file mode 100644 index 00000000000000..b62c5573980904 Binary files /dev/null and b/Telegram/Resources/icons/profile_premium.png differ diff --git a/Telegram/Resources/icons/profile_premium@2x.png b/Telegram/Resources/icons/profile_premium@2x.png new file mode 100644 index 00000000000000..5c72af41506a52 Binary files /dev/null and b/Telegram/Resources/icons/profile_premium@2x.png differ diff --git a/Telegram/Resources/icons/profile_premium@3x.png b/Telegram/Resources/icons/profile_premium@3x.png new file mode 100644 index 00000000000000..f309d0aaccd962 Binary files /dev/null and b/Telegram/Resources/icons/profile_premium@3x.png differ diff --git a/Telegram/Resources/icons/profile_verified_check.png b/Telegram/Resources/icons/profile_verified_check.png index fafa4023a3addf..45ce8c9ecee049 100644 Binary files a/Telegram/Resources/icons/profile_verified_check.png and b/Telegram/Resources/icons/profile_verified_check.png differ diff --git a/Telegram/Resources/icons/profile_verified_check@2x.png b/Telegram/Resources/icons/profile_verified_check@2x.png index 790cc94cdf5e59..b9180a6b1e3904 100644 Binary files a/Telegram/Resources/icons/profile_verified_check@2x.png and b/Telegram/Resources/icons/profile_verified_check@2x.png differ diff --git a/Telegram/Resources/icons/profile_verified_check@3x.png b/Telegram/Resources/icons/profile_verified_check@3x.png index 511c349f2f29ee..3b61b0f783852a 100644 Binary files a/Telegram/Resources/icons/profile_verified_check@3x.png and b/Telegram/Resources/icons/profile_verified_check@3x.png differ diff --git a/Telegram/Resources/icons/profile_verified_star.png b/Telegram/Resources/icons/profile_verified_star.png index ef20dc84b003a3..bc19c2dcbe2629 100644 Binary files a/Telegram/Resources/icons/profile_verified_star.png and b/Telegram/Resources/icons/profile_verified_star.png differ diff --git a/Telegram/Resources/icons/profile_verified_star@2x.png b/Telegram/Resources/icons/profile_verified_star@2x.png index 967762a593bd67..8db01d0be3bee5 100644 Binary files a/Telegram/Resources/icons/profile_verified_star@2x.png and b/Telegram/Resources/icons/profile_verified_star@2x.png differ diff --git a/Telegram/Resources/icons/profile_verified_star@3x.png b/Telegram/Resources/icons/profile_verified_star@3x.png index 8ada01147bfcbf..c999dabaabf4e7 100644 Binary files a/Telegram/Resources/icons/profile_verified_star@3x.png and b/Telegram/Resources/icons/profile_verified_star@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/channels_off.png b/Telegram/Resources/icons/settings/premium/channels_off.png new file mode 100644 index 00000000000000..108a1573f5eb82 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/channels_off.png differ diff --git a/Telegram/Resources/icons/settings/premium/channels_off@2x.png b/Telegram/Resources/icons/settings/premium/channels_off@2x.png new file mode 100644 index 00000000000000..18ec1bb7705ac8 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/channels_off@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/channels_off@3x.png b/Telegram/Resources/icons/settings/premium/channels_off@3x.png new file mode 100644 index 00000000000000..22e083d65aaeee Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/channels_off@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/double.png b/Telegram/Resources/icons/settings/premium/double.png new file mode 100644 index 00000000000000..388994de898fb8 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/double.png differ diff --git a/Telegram/Resources/icons/settings/premium/double@2x.png b/Telegram/Resources/icons/settings/premium/double@2x.png new file mode 100644 index 00000000000000..62ef97fc851a38 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/double@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/double@3x.png b/Telegram/Resources/icons/settings/premium/double@3x.png new file mode 100644 index 00000000000000..2de7119b266c51 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/double@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/files.png b/Telegram/Resources/icons/settings/premium/files.png new file mode 100644 index 00000000000000..f82c4e495103d9 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/files.png differ diff --git a/Telegram/Resources/icons/settings/premium/files@2x.png b/Telegram/Resources/icons/settings/premium/files@2x.png new file mode 100644 index 00000000000000..cffe0090925519 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/files@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/files@3x.png b/Telegram/Resources/icons/settings/premium/files@3x.png new file mode 100644 index 00000000000000..4c20f6d87e39ca Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/files@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/like.png b/Telegram/Resources/icons/settings/premium/like.png new file mode 100644 index 00000000000000..77e9f63a1b24ab Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/like.png differ diff --git a/Telegram/Resources/icons/settings/premium/like@2x.png b/Telegram/Resources/icons/settings/premium/like@2x.png new file mode 100644 index 00000000000000..183760ba79acad Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/like@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/like@3x.png b/Telegram/Resources/icons/settings/premium/like@3x.png new file mode 100644 index 00000000000000..0e5e12899398b4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/like@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/play.png b/Telegram/Resources/icons/settings/premium/play.png new file mode 100644 index 00000000000000..0a3d576978d2d4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/play.png differ diff --git a/Telegram/Resources/icons/settings/premium/play@2x.png b/Telegram/Resources/icons/settings/premium/play@2x.png new file mode 100644 index 00000000000000..ef9bca6e1cd5cd Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/play@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/play@3x.png b/Telegram/Resources/icons/settings/premium/play@3x.png new file mode 100644 index 00000000000000..4bc9dcb1c0b587 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/play@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/speed.png b/Telegram/Resources/icons/settings/premium/speed.png new file mode 100644 index 00000000000000..29ab2bb05da344 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/speed.png differ diff --git a/Telegram/Resources/icons/settings/premium/speed@2x.png b/Telegram/Resources/icons/settings/premium/speed@2x.png new file mode 100644 index 00000000000000..69689a452693c6 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/speed@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/speed@3x.png b/Telegram/Resources/icons/settings/premium/speed@3x.png new file mode 100644 index 00000000000000..f0be41b0b75108 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/speed@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/star.png b/Telegram/Resources/icons/settings/premium/star.png new file mode 100644 index 00000000000000..82fa9b74c85b79 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/star.png differ diff --git a/Telegram/Resources/icons/settings/premium/star@2x.png b/Telegram/Resources/icons/settings/premium/star@2x.png new file mode 100644 index 00000000000000..24e485e355916c Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/star@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/star@3x.png b/Telegram/Resources/icons/settings/premium/star@3x.png new file mode 100644 index 00000000000000..024947d929b66f Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/star@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/voice.png b/Telegram/Resources/icons/settings/premium/voice.png new file mode 100644 index 00000000000000..7d83225f96e7ec Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/voice.png differ diff --git a/Telegram/Resources/icons/settings/premium/voice@2x.png b/Telegram/Resources/icons/settings/premium/voice@2x.png new file mode 100644 index 00000000000000..650696908c66d3 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/voice@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/voice@3x.png b/Telegram/Resources/icons/settings/premium/voice@3x.png new file mode 100644 index 00000000000000..bffe3be39e026e Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/voice@3x.png differ diff --git a/Telegram/Resources/icons/settings/star.svg b/Telegram/Resources/icons/settings/star.svg new file mode 100644 index 00000000000000..1549c272f606f7 --- /dev/null +++ b/Telegram/Resources/icons/settings/star.svg @@ -0,0 +1,9 @@ + + +Star + + + + + + diff --git a/Telegram/Resources/icons/settings/starmini.svg b/Telegram/Resources/icons/settings/starmini.svg new file mode 100644 index 00000000000000..13e53a95de99a7 --- /dev/null +++ b/Telegram/Resources/icons/settings/starmini.svg @@ -0,0 +1,7 @@ + + +ministar + + + + diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 49b2b203446869..cb6faa44e1da8c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -164,15 +164,84 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_scam_badge" = "SCAM"; "lng_fake_badge" = "FAKE"; +"lng_channels_limit_title" = "Too Many Communities"; +"lng_channels_limit1#one" = "You are a member of **{count}** groups and channels."; +"lng_channels_limit1#other" = "You are a member of **{count}** groups and channels."; +"lng_channels_limit2#one" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to doulbe the limit to **{count}** groups and channels."; +"lng_channels_limit2#other" = "Please leave some before joining a new one - or upgrade to **Telegram Premium** to doulbe the limit to **{count}** groups and channels."; +"lng_channels_limit2_final" = "Please leave some before joining a new one."; +"lng_channels_leave_title" = "Least active communities"; +"lng_channels_leave_status" = "{type}, inactive {time}"; +"lng_channels_leave#one" = "Leave {count} community"; +"lng_channels_leave#other" = "Leave {count} communities"; +"lng_channels_leave_done" = "You've left the selected communities."; + +"lng_links_limit_title" = "Too Many Public Links"; +"lng_links_limit1#one" = "You have reserved **{count}** public link."; +"lng_links_limit1#other" = "You have reserved **{count}** public links."; +"lng_links_limit2#one" = "Try revoking the link from an older group or channel, or upgrade to **Telegram Premium** to double the limit to **{count}** public link."; +"lng_links_limit2#other" = "Try revoking the link from an older group or channel, or upgrade to **Telegram Premium** to double the limit to **{count}** public links."; +"lng_links_limit2_final" = "Try revoking the link from an older group or channel"; +"lng_links_revoke_title" = "Your public communities"; + +"lng_filter_chats_limit_title" = "Limit Reached"; +"lng_filter_chats_limit1#one" = "Sorry, you can't add more than **{count}** chat to a folder."; +"lng_filter_chats_limit1#other" = "Sorry, you can't add more than **{count}** chats to a folder."; +"lng_filter_chats_limit2#one" = "You can increase this limit to **{count}** by upgrading to **Telegram Premium**."; +"lng_filter_chats_limit2#other" = "You can increase this limit to **{count}** by upgrading to **Telegram Premium**."; + +"lng_filters_limit_title" = "Limit Reached"; +"lng_filters_limit1#one" = "You have reached the limit of **{count}** folder."; +"lng_filters_limit1#other" = "You have reached the limit of **{count}** folders."; +"lng_filters_limit2#one" = "You can double the limit to **{count}** folder by subscribing to **Telegram Premium**."; +"lng_filters_limit2#other" = "You can double the limit to **{count}** folders by subscribing to **Telegram Premium**."; + +"lng_filter_pin_limit_title" = "Limit Reached"; +"lng_filter_pin_limit1#one" = "Sorry, you can't pin more than **{count}** chat to the top."; +"lng_filter_pin_limit1#other" = "Sorry, you can't pin more than **{count}** chats to the top."; +"lng_filter_pin_limit2#one" = "Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **{count}** chat."; +"lng_filter_pin_limit2#other" = "Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **{count}** chats."; + +"lng_fave_sticker_limit_title#one" = "The Limit of {count} Stickers Reached"; +"lng_fave_sticker_limit_title#other" = "The Limit of {count} Stickers Reached"; +"lng_fave_sticker_limit_more#one" = "An older sticker was replaced with this one.\nYou can {link} to {count} sticker."; +"lng_fave_sticker_limit_more#other" = "An older sticker was replaced with this one.\nYou can {link} to {count} stickers."; +"lng_fave_sticker_limit_link" = "increase the limit"; + +"lng_saved_gif_limit_title#one" = "The Limit of {count} GIF Reached"; +"lng_saved_gif_limit_title#other" = "The Limit of {count} GIFs Reached"; +"lng_saved_gif_limit_more#one" = "An older GIF was replaced with this one.\nYou can {link} to {count} GIF."; +"lng_saved_gif_limit_more#other" = "An older GIF was replaced with this one.\nYou can {link} to {count} GIFs."; +"lng_saved_gif_limit_link" = "increase the limit"; + +"lng_caption_limit_title" = "Limit Reached"; +"lng_caption_limit1#one" = "Sorry, you can't use more than **{count}** character in media captions."; +"lng_caption_limit1#other" = "Sorry, you can't use more than **{count}** characters in media captions."; +"lng_caption_limit2#one" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** character."; +"lng_caption_limit2#other" = "Make the caption shorter or subscribe to **Telegram Premium** to double the limit to **{count}** characters."; +"lng_caption_limit_reached#one" = "You've reached the media caption limit. Please make the caption shorter by {count} character."; +"lng_caption_limit_reached#other" = "You've reached the media caption limit. Please make the caption shorter by {count} characters."; + +"lng_file_size_limit_title" = "File Too Large"; +"lng_file_size_limit#one" = "{count} Gb"; +"lng_file_size_limit#other" = "{count} Gb"; +"lng_file_size_limit1" = "The document can't be sent, because it is larger than {size}."; +"lng_file_size_limit2" = "You can double this limit to {size} per document by subscribing to **Telegram Premium**."; + +"lng_limits_increase" = "Increase Limit"; + +"lng_sticker_premium_title" = "With Effects"; +"lng_sticker_premium_text" = "This pack contains premium stickers like this one."; +"lng_sticker_premium_view" = "View"; +"lng_reaction_premium_info" = "Click on the reaction to preview the animation."; +"lng_reaction_premium_no_group" = "Some reactions are restricted in this group."; +"lng_reaction_premium_no_channel" = "Some reactions are restricted in this channel."; + "lng_flood_error" = "Too many tries. Please try again later."; "lng_gif_error" = "An error has occurred while reading GIF animation :("; "lng_edit_error" = "You cannot edit this message"; -"lng_join_channel_error" = "Sorry, you have joined too many channels and supergroups. Please leave some before joining this one."; -"lng_migrate_error" = "This action will convert the group to a supergroup. Unfortunately, you are a member of too many supergroups and channels. Please leave some of the channels or groups you don't need before proceeding."; "lng_error_phone_flood" = "Sorry, you have deleted and re-created your account too many times recently. Please wait for a few days before signing up again."; "lng_error_start_minimized_passcoded" = "You have set a local passcode, so Telegram Desktop can't be launched minimised; it will ask you to enter your passcode before it can start working."; -"lng_error_pinned_max#one" = "Sorry, you can pin no more than {count} chat to the top."; -"lng_error_pinned_max#other" = "Sorry, you can pin no more than {count} chats to the top."; "lng_error_public_groups_denied" = "Unfortunately, you were banned from participating in public groups.\n{more_info}"; "lng_error_cant_add_member" = "Sorry, you can't add the bot to this group. Ask a group admin to do it."; "lng_error_cant_add_bot" = "Sorry, this bot can't be added to groups."; @@ -1031,6 +1100,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_view_discussion" = "View discussion"; "lng_profile_join_channel" = "Join Channel"; "lng_profile_join_group" = "Join Group"; +"lng_profile_apply_to_join_group" = "Apply to Join Group"; "lng_profile_delete_and_exit" = "Leave"; "lng_profile_kick" = "Remove"; "lng_profile_delete_removed" = "Delete"; @@ -1159,6 +1229,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_public_group_title" = "Public"; "lng_manage_private_peer_title" = "Private"; "lng_manage_public_peer_title" = "Public"; +"lng_manage_peer_send_title" = "Who can send new messages?"; +"lng_manage_peer_send_only_members" = "Only members"; +"lng_manage_peer_send_only_members_about" = "Turn this on if you expect users to join your group before being able to send messages."; +"lng_manage_peer_send_approve_members" = "Approve new members"; +"lng_manage_peer_send_approve_members_about" = "Turn this on if you want users to join the group only after they are approved by an admin."; "lng_manage_peer_no_forwards_title" = "Saving content"; "lng_manage_peer_no_forwards" = "Restrict saving content"; "lng_manage_peer_no_forwards_about" = "Members won't be able to forward messages from this group or save media files."; @@ -1382,6 +1457,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_game_you_scored_no_game#other" = "You scored {count}"; "lng_action_payment_done" = "You have just successfully transferred {amount} to {user}"; "lng_action_payment_done_for" = "You have just successfully transferred {amount} to {user} for {invoice}"; +"lng_action_payment_init_recurring_for" = "You have just successfully transferred {amount} to {user} for {invoice} and allowed future recurring payments"; +"lng_action_payment_init_recurring" = "You have just successfully transferred {amount} to {user} and allowed future recurring payments"; +"lng_action_payment_used_recurring" = "You were charged {amount} via recurring payment"; "lng_action_took_screenshot" = "{from} took a screenshot!"; "lng_action_you_took_screenshot" = "You took a screenshot!"; "lng_action_bot_allowed_from_domain" = "You allowed this bot to message you when you logged in on {domain}."; @@ -1551,6 +1629,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_signed_author" = "Author: {user}"; "lng_in_reply_to" = "In reply to"; "lng_sponsored" = "sponsored"; +"lng_recommended" = "recommended"; "lng_edited" = "edited"; "lng_edited_date" = "Edited: {date}"; "lng_sent_date" = "Sent: {date}"; @@ -1617,6 +1696,88 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_stickers" = "Group stickers"; "lng_group_stickers_description" = "You can choose a sticker set which will be available for every member while in the group chat."; "lng_group_stickers_add" = "Choose sticker set"; +"lng_premium_stickers" = "Premium stickers"; + +"lng_premium" = "Premium"; +"lng_premium_free" = "Free"; +"lng_premium_more_about" = "More About Telegram Premium"; +"lng_premium_unlock_reactions" = "Unlock Premium Reactions"; +"lng_premium_unlock_stickers" = "Unlock Premium Stickers"; + +"lng_premium_summary_title" = "Telegram Premium"; +"lng_premium_summary_top_about" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**."; +"lng_premium_summary_title_subscribed" = "You are all set!"; +"lng_premium_summary_subtitle_double_limits" = "Doubled Limits"; +"lng_premium_summary_about_double_limits" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more."; +"lng_premium_summary_subtitle_more_upload" = "4Gb Upload Size"; +"lng_premium_summary_about_more_upload" = "Increased upload size from 2Gb to 4Gb to per document, unlimited storage overall."; +"lng_premium_summary_subtitle_faster_download" = "Faster Download Speed"; +"lng_premium_summary_about_faster_download" = "No more limits on the speed with which media and documents are downloaded."; +"lng_premium_summary_subtitle_voice_to_text" = "Voice-to-Text Conversion"; +"lng_premium_summary_about_voice_to_text" = "Ability to read the transcript of any incoming voice message."; +"lng_premium_summary_subtitle_no_ads" = "No Ads"; +"lng_premium_summary_about_no_ads" = "No more ads in public channels where Telegram sometimes shows ads."; +"lng_premium_summary_subtitle_unique_reactions" = "Unique Reactions"; +"lng_premium_summary_about_unique_reactions" = "Additional animated reactions on messages available only to the Premium subscribers."; +"lng_premium_summary_subtitle_premium_stickers" = "Premium Stickers"; +"lng_premium_summary_about_premium_stickers" = "Exclusive enlarged stickers featuring additional effects, updated monthly."; +"lng_premium_summary_subtitle_advanced_chat_management" = "Advanced Chat Management"; +"lng_premium_summary_about_advanced_chat_management" = "Tools to set default folder, auto-archive and hide new chats from non-contacts."; +"lng_premium_summary_subtitle_profile_badge" = "Profile Badge"; +"lng_premium_summary_about_profile_badge" = "A badge next to your name showing that you are helping support Telegram."; +"lng_premium_summary_subtitle_animated_userpics" = "Animated Profile Pictures"; +"lng_premium_summary_about_animated_userpics" = "Video avatars animated in chat lists and chats to allow for additional self-expression."; +"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"; + +"lng_premium_success" = "You've successfully subscribed to Telegram Premium!"; +"lng_premium_unavailable" = "This feature requires subscription to **Telegram Premium**.\n\nUnfortunately, **Telegram Premium** is not available in your region."; + +// Doubled Limits. +"lng_premium_double_limits_subtitle_channels" = "Groups and Channels"; +"lng_premium_double_limits_about_channels#one" = "Join up to {count} channels and large groups"; +"lng_premium_double_limits_about_channels#other" = "Join up to {count} channels and large groups"; + +"lng_premium_double_limits_subtitle_pins" = "Pinned Chats"; +"lng_premium_double_limits_about_pins#one" = "Pin up to {count} chats in your main chat list"; +"lng_premium_double_limits_about_pins#other" = "Pin up to {count} chats in your main chat list"; + +"lng_premium_double_limits_subtitle_links" = "Public Links"; +"lng_premium_double_limits_about_links#one" = "Reserve up to {count} t.me/name links"; +"lng_premium_double_limits_about_links#other" = "Reserve up to {count} t.me/name links"; + +"lng_premium_double_limits_subtitle_gifs" = "Saved GIFs"; +"lng_premium_double_limits_about_gifs#one" = "Save up to {count} GIFs in your Favorite GIFs"; +"lng_premium_double_limits_about_gifs#other" = "Save up to {count} GIFs in your Favorite GIFs"; + +"lng_premium_double_limits_subtitle_stickers" = "Favorite Stickers"; +"lng_premium_double_limits_about_stickers#one" = "Save up to {count} stickers in your Favorite stickers"; +"lng_premium_double_limits_about_stickers#other" = "Save up to {count} stickers in your Favorite stickers"; + +"lng_premium_double_limits_subtitle_bio" = "Bio"; +"lng_premium_double_limits_about_bio" = "Add more symbols and use links in your bio"; + +"lng_premium_double_limits_subtitle_captions" = "Captions"; +"lng_premium_double_limits_about_captions" = "Use longer description for your photos and videos"; + +"lng_premium_double_limits_subtitle_folders" = "Folders"; +"lng_premium_double_limits_about_folders#one" = "Organize your chats into {count} folders"; +"lng_premium_double_limits_about_folders#other" = "Organize your chats into {count} folders"; + +"lng_premium_double_limits_subtitle_folder_chats" = "Chats per Folder"; +"lng_premium_double_limits_about_folder_chats#one" = "Add up to {count} chats into each of your folders"; +"lng_premium_double_limits_about_folder_chats#other" = "Add up to {count} chats into each of your folders"; + +"lng_premium_double_limits_subtitle_accounts" = "Connected Accounts"; +"lng_premium_double_limits_about_accounts#one" = "Connect {count} account with different mobile numbers"; +"lng_premium_double_limits_about_accounts#other" = "Connect {count} accounts with different mobile numbers"; +// + +"lng_accounts_limit_title" = "Limit Reached"; +"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected accounts."; +"lng_accounts_limit1#other" = "You have reached the limit of **{count}** connected accounts."; +"lng_accounts_limit2" = "You can free one space by subscribing to **Telegram Premium** with one of these connected accounts:"; "lng_group_about_header" = "You have created a group."; "lng_group_about_text" = "Groups can have:"; @@ -1830,8 +1991,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_sure_add_text_channel" = "Are you sure you want to add this bot as an admin in the channel {group}?"; "lng_bot_sure_add" = "Add as admin"; "lng_bot_no_webview" = "Unfortunately, you can't open such menu with current system configuration."; -"lng_bot_remove_from_menu" = "Remove from menu"; +"lng_bot_remove_from_menu" = "Remove From Menu"; +"lng_bot_remove_from_menu_sure" = "Remove {bot} from the attachment menu?"; "lng_bot_remove_from_menu_done" = "Bot removed from the menu."; +"lng_bot_settings" = "Settings"; +"lng_bot_open" = "Open Bot"; +"lng_bot_reload_page" = "Reload Page"; "lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachments menu so you can access it from any chat."; "lng_bot_add_to_menu_done" = "Bot added to the menu."; "lng_bot_menu_not_supported" = "This bot isn't supported in the attach menu."; @@ -1990,7 +2155,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_downloads_delete_in_cloud" = "They will be deleted from your disk, but will remain accessible in the cloud."; "lng_send_image_empty" = "Could not send an empty file: {name}"; -"lng_send_image_too_large" = "Could not send a file, because it is larger than 1500 MB: {name}"; "lng_send_images_selected#one" = "{count} image selected"; "lng_send_images_selected#other" = "{count} images selected"; "lng_send_photos#one" = "Send {count} photo"; @@ -2282,6 +2446,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_payments_precheckout_failed" = "The bot couldn't process your payment. Your card has not been billed."; "lng_payments_already_paid" = "You have already paid for this item."; +"lng_payments_terms_title" = "Terms of Service"; +"lng_payments_terms_text" = "Subscribe and accept terms of service of {bot}?"; +"lng_payments_terms_agree" = "I agree to {link}"; +"lng_payments_terms_link" = "Terms of Service"; +"lng_payments_terms_accept" = "Accept"; + "lng_call_status_incoming" = "is calling you..."; "lng_call_status_connecting" = "connecting..."; "lng_call_status_exchanging" = "exchanging encryption keys..."; @@ -2561,6 +2731,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_audio_player_reverse" = "Reverse order"; "lng_audio_player_shuffle" = "Shuffle"; +"lng_audio_transcribe_long" = "This voice message is too long."; "lng_rights_edit_admin" = "Manage permissions"; "lng_rights_edit_admin_header" = "What can this admin do?"; @@ -3211,6 +3382,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_voice_chat_channel" = "Live stream"; "lng_view_button_request_join" = "Request to Join"; +"lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_title" = "What are sponsored messages?"; "lng_sponsored_info_description1" = "Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.\n\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can’t spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:"; "lng_sponsored_info_description2" = "Sponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\n\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate – together."; diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 16728048e5d207..a1235624b6eeb4 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -29,6 +29,8 @@ ../../art/recording/recording_info_video_landscape.svg ../../art/recording/recording_info_video_portrait.svg ../../icons/settings/dino.svg + ../../icons/settings/star.svg + ../../icons/settings/starmini.svg ../../icons/calls/hands.lottie diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 2ebe8fd7208f4f..96e0b2fc8f900e 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -110,7 +110,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#3ff6ecb0 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; +user#3ff6ecb0 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -150,7 +150,7 @@ messageMediaPhoto#695150d7 flags:# photo:flags.0?Photo ttl_seconds:flags.2?int = messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#9cb070d7 flags:# document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#9cb070d7 flags:# nopremium:flags.3?true document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; @@ -173,8 +173,8 @@ messageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageActi messageActionPinMessage#94bd38ed = MessageAction; messageActionHistoryClear#9fbab604 = MessageAction; messageActionGameScore#92a72876 game_id:long score:int = MessageAction; -messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; -messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction; +messageActionPaymentSentMe#8f31b327 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; +messageActionPaymentSent#96163f56 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string = MessageAction; messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; messageActionCustomAction#fae69f56 message:string = MessageAction; @@ -390,6 +390,7 @@ updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; updateSavedRingtones#74d8be99 = Update; +updateTranscribedAudio#84cd5a flags:# pending:flags.0?true peer:Peer msg_id:int transcription_id:long text:string = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -434,7 +435,7 @@ encryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = En inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat; encryptedFileEmpty#c21f497e = EncryptedFile; -encryptedFile#4a70994c id:long access_hash:long size:int dc_id:int key_fingerprint:int = EncryptedFile; +encryptedFile#a8008cd8 id:long access_hash:long size:long dc_id:int key_fingerprint:int = EncryptedFile; inputEncryptedFileEmpty#1837c364 = InputEncryptedFile; inputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile; @@ -454,7 +455,7 @@ inputDocumentEmpty#72f0eaae = InputDocument; inputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument; documentEmpty#36f8c871 id:long = Document; -document#1e87342b flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:int thumbs:flags.0?Vector video_thumbs:flags.1?Vector dc_id:int attributes:Vector = Document; +document#8fd4c4d8 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:long thumbs:flags.0?Vector video_thumbs:flags.1?Vector dc_id:int attributes:Vector = Document; help.support#17c6b5f6 phone_number:string user:User = help.Support; @@ -562,6 +563,7 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int title:flags.8?string = ExportedChatInvite; +chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; @@ -581,7 +583,7 @@ messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; -botInfo#e4169b5d user_id:long description:string commands:Vector menu_button:BotMenuButton = BotInfo; +botInfo#8f300b57 flags:# user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton = BotInfo; keyboardButton#a2fa4880 text:string = KeyboardButton; keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; @@ -823,7 +825,7 @@ dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#cd886e0 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector = Invoice; +invoice#3e85a91b flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector recurring_terms_url:flags.9?string = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -843,7 +845,7 @@ inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; -payments.paymentForm#1694761b flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; +payments.paymentForm#b0133b37 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; @@ -972,7 +974,7 @@ dialogPeerFolder#514519e2 folder_id:int = DialogPeer; messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; messages.foundStickerSets#8af09dd2 hash:long sets:Vector = messages.FoundStickerSets; -fileHash#6242c773 offset:int limit:int hash:bytes = FileHash; +fileHash#f39b035c offset:long limit:int hash:bytes = FileHash; inputClientProxy#75588b3f address:string port:int = InputClientProxy; @@ -983,7 +985,7 @@ inputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash inputSecureFile#5367e5be id:long access_hash:long = InputSecureFile; secureFileEmpty#64199744 = SecureFile; -secureFile#e0277a62 id:long access_hash:long size:int dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; +secureFile#7d09c27e id:long access_hash:long size:long dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; secureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData; @@ -1110,7 +1112,7 @@ codeSettings#8a6469c2 flags:# allow_flashcall:flags.0?true current_number:flags. wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; -autoDownloadSettings#e04232f3 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:int file_size_max:int video_upload_maxbitrate:int = AutoDownloadSettings; +autoDownloadSettings#8efab953 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int = AutoDownloadSettings; account.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings; @@ -1182,6 +1184,7 @@ bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector = 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 include_peers:Vector exclude_peers:Vector = DialogFilter; +dialogFilterDefault#363293ae = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; @@ -1321,7 +1324,7 @@ messageReactions#4f2b9479 flags:# min:flags.0?true can_see_list:flags.2?true res messages.messageReactionsList#31bd492d flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; -availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; +availableReaction#c077ec01 flags:# inactive:flags.0?true premium:flags.2?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; @@ -1341,7 +1344,7 @@ attachMenuBotIconColor#4576f3f0 name:string color:int = AttachMenuBotIconColor; attachMenuBotIcon#b2a7386b flags:# name:string icon:Document colors:flags.0?Vector = AttachMenuBotIcon; -attachMenuBot#e93cb772 flags:# inactive:flags.0?true bot_id:long short_name:string icons:Vector = AttachMenuBot; +attachMenuBot#c8aa2cd2 flags:# inactive:flags.0?true has_settings:flags.1?true bot_id:long short_name:string peer_types:Vector icons:Vector = AttachMenuBot; attachMenuBotsNotModified#f1d88a5c = AttachMenuBots; attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector = AttachMenuBots; @@ -1369,6 +1372,21 @@ notificationSoundRingtone#ff6c8049 id:long = NotificationSound; account.savedRingtone#b7263f6d = account.SavedRingtone; account.savedRingtoneConverted#1f307eb7 document:Document = account.SavedRingtone; +attachMenuPeerTypeSameBotPM#7d6be90e = AttachMenuPeerType; +attachMenuPeerTypeBotPM#c32bfa1a = AttachMenuPeerType; +attachMenuPeerTypePM#f146d31f = AttachMenuPeerType; +attachMenuPeerTypeChat#509113f = AttachMenuPeerType; +attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; + +inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; +inputInvoiceSlug#c326caef slug:string = InputInvoice; + +payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; + +messages.transcribedAudio#93752c52 flags:# pending:flags.0?true transcription_id:long text:string = messages.TranscribedAudio; + +help.premiumPromo#8a4f3c29 status_text:string status_entities:Vector video_sections:Vector videos:Vector currency:string monthly_amount:long users:Vector = help.PremiumPromo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1439,7 +1457,7 @@ account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; account.sendVerifyEmailCode#7011509f email:string = account.SentEmailCode; account.verifyEmail#ecba39db email:string code:string = Bool; -account.initTakeoutSession#f05b4804 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?int = account.Takeout; +account.initTakeoutSession#8ef3eab0 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?long = account.Takeout; account.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool; account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.resendPasswordEmail#7a7f2a15 = Bool; @@ -1551,7 +1569,7 @@ messages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = B messages.migrateChat#a2875319 chat_id:long = Updates; messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector = Bool; -messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document; +messages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document; 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; @@ -1664,11 +1682,13 @@ messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = mes messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; messages.toggleBotInAttachMenu#1aee33af bot:InputUser enabled:Bool = Bool; -messages.requestWebView#fa04dff flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON reply_to_msg_id:flags.0?int = WebViewResult; -messages.prolongWebView#d22ad148 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to_msg_id:flags.0?int = Bool; +messages.requestWebView#91b15831 flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON reply_to_msg_id:flags.0?int send_as:flags.13?InputPeer = WebViewResult; +messages.prolongWebView#ea5fbcce flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to_msg_id:flags.0?int send_as:flags.13?InputPeer = Bool; messages.requestSimpleWebView#6abb2f73 flags:# bot:InputUser url:string theme_params:flags.0?DataJSON = SimpleWebViewResult; messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; +messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio; +messages.rateTranscribedAudio#7f1d072f peer:InputPeer msg_id:int transcription_id:long good:Bool = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1680,13 +1700,13 @@ photos.deletePhotos#87cf7f2f id:Vector = Vector; photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; -upload.getFile#b15a9afc flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:int limit:int = upload.File; +upload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; -upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile; +upload.getCdnFile#395f69da file_token:bytes offset:long limit:int = upload.CdnFile; upload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector; -upload.getCdnFileHashes#4da54231 file_token:bytes offset:int = Vector; -upload.getFileHashes#c7025931 location:InputFileLocation offset:int = Vector; +upload.getCdnFileHashes#91dc3f31 file_token:bytes offset:long = Vector; +upload.getFileHashes#9156982a location:InputFileLocation offset:long = Vector; help.getConfig#c4f9186b = Config; help.getNearestDc#1fb33026 = NearestDc; @@ -1710,6 +1730,7 @@ help.getPromoData#c0977421 = help.PromoData; help.hidePromoData#1e251c95 peer:InputPeer = Bool; help.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool; help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; +help.getPremiumPromo#b81b93d4 = help.PremiumPromo; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; @@ -1750,6 +1771,8 @@ channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bo channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; +channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; +channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1761,13 +1784,19 @@ bots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton; bots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool; bots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool; -payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm; +payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; -payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; -payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult; +payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; +payments.sendPaymentForm#2d03522f flags:# form_id:long invoice:InputInvoice requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult; payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; +payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice; +payments.assignAppStoreTransaction#fec13c6 flags:# restore:flags.0?true transaction_id:string receipt:bytes = Updates; +payments.assignPlayMarketTransaction#4faa4aed purchase_token:string = Updates; +payments.restorePlayMarketReceipt#d164e36a receipt:bytes = Updates; +payments.canPurchasePremium#aa6a90c8 = Bool; +payments.requestRecurringPayment#146e958d user_id:InputUser recurring_init_charge:string invoice_media:InputMedia = Updates; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true videos:flags.4?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -1824,4 +1853,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 142 +// LAYER 143 diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index f05a836f5951b5..9d2f3375051b43 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="3.7.6.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 8a9b1d54bb5456..fe58d53606800b 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 3,7,5,0 - PRODUCTVERSION 3,7,5,0 + FILEVERSION 3,7,6,0 + PRODUCTVERSION 3,7,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "3.7.5.0" + VALUE "FileVersion", "3.7.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2022" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "3.7.5.0" + VALUE "ProductVersion", "3.7.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index fecb0fe885b1cc..041d317a9a5c9e 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 3,7,5,0 - PRODUCTVERSION 3,7,5,0 + FILEVERSION 3,7,6,0 + PRODUCTVERSION 3,7,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "3.7.5.0" + VALUE "FileVersion", "3.7.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2022" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "3.7.5.0" + VALUE "ProductVersion", "3.7.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 143166b78ae3b1..778a3db6b1c938 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "base/unixtime.h" #include "core/changelogs.h" #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_keys.h" namespace Api { diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 80bec3cf856f10..0727582985271e 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -23,6 +23,7 @@ For license and copyright information please follow this link: #include "history/history_item.h" #include "history/history_item_components.h" #include "main/main_session.h" +#include "window/window_session_controller.h" #include "ui/toast/toast.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" @@ -31,6 +32,7 @@ namespace Api { namespace { void SendBotCallbackData( + not_null controller, not_null item, int row, int column, @@ -73,6 +75,8 @@ void SendBotCallbackData( if (withPassword) { flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_password; } + const auto weak = base::make_weak(controller.get()); + const auto show = std::make_shared(controller); button->requestId = api->request(MTPmessages_GetBotCallbackAnswer( MTP_flags(flags), history->peer->input, @@ -100,12 +104,12 @@ void SendBotCallbackData( if (!message.isEmpty()) { if (showAlert) { - Ui::show(Ui::MakeInformBox(message)); + show->showBox(Ui::MakeInformBox(message)); } else { if (withPassword) { - Ui::hideLayer(); + show->hideLayer(); } - Ui::Toast::Show(message); + Ui::Toast::Show(show->toastParent(), message); } } else if (!link.isEmpty()) { if (!isGame) { @@ -116,12 +120,18 @@ void SendBotCallbackData( session, link, item->fullId()); - BotGameUrlClickHandler(bot, scoreLink).onClick({}); + BotGameUrlClickHandler(bot, scoreLink).onClick({ + Qt::LeftButton, + QVariant::fromValue(ClickHandlerContext{ + .itemId = item->fullId(), + .sessionWindow = weak, + }), + }); session->sendProgressManager().update( history, Api::SendProgressType::PlayGame); } else if (withPassword) { - Ui::hideLayer(); + show->hideLayer(); } }).fail([=](const MTP::Error &error) { const auto item = owner->message(fullId); @@ -147,13 +157,15 @@ void SendBotCallbackData( } // namespace void SendBotCallbackData( + not_null controller, not_null item, int row, int column) { - SendBotCallbackData(item, row, column, std::nullopt); + SendBotCallbackData(controller, item, row, column, std::nullopt); } void SendBotCallbackDataWithPassword( + not_null controller, not_null item, int row, int column) { @@ -177,7 +189,9 @@ void SendBotCallbackDataWithPassword( return; } api->cloudPassword().reload(); - SendBotCallbackData(item, row, column, std::nullopt, [=](const QString &error) { + const auto weak = base::make_weak(controller.get()); + const auto show = std::make_shared(controller); + SendBotCallbackData(controller, item, row, column, std::nullopt, [=](const QString &error) { auto box = PrePasswordErrorBox( error, session, @@ -185,7 +199,7 @@ void SendBotCallbackDataWithPassword( tr::now, Ui::Text::WithEntities)); if (box) { - Ui::show(std::move(box)); + show->showBox(std::move(box), Ui::LayerOption::CloseOther); } else { auto lifetime = std::make_shared(); button->requestId = -1; @@ -219,14 +233,20 @@ void SendBotCallbackDataWithPassword( return; } if (const auto item = owner->message(fullId)) { - SendBotCallbackData(item, row, column, result, [=](const QString &error) { + const auto strongController = weak.get(); + if (!strongController) { + return; + } + SendBotCallbackData(strongController, item, row, column, result, [=](const QString &error) { if (*box) { (*box)->handleCustomCheckError(error); } }); } }; - *box = Ui::show(Box(session, fields)); + auto object = Box(session, fields); + *box = Ui::MakeWeak(object.data()); + show->showBox(std::move(object), Ui::LayerOption::CloseOther); }, *lifetime); } }); diff --git a/Telegram/SourceFiles/api/api_bot.h b/Telegram/SourceFiles/api/api_bot.h index 06a420a41e3842..45a40ca082b3a9 100644 --- a/Telegram/SourceFiles/api/api_bot.h +++ b/Telegram/SourceFiles/api/api_bot.h @@ -9,14 +9,20 @@ For license and copyright information please follow this link: class HistoryItem; +namespace Window { +class SessionController; +} // namespace Window + namespace Api { void SendBotCallbackData( + not_null controller, not_null item, int row, int column); void SendBotCallbackDataWithPassword( + not_null controller, not_null item, int row, int column); diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 68b0dbebe05380..5cb157b5c206c1 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -21,9 +21,7 @@ void SaveNewFilterPinned( nullptr, filterId); auto &filters = session->data().chatsFilters(); - const auto &filter = filters.applyUpdatedPinned( - filterId, - order); + const auto &filter = filters.applyUpdatedPinned(filterId, order); session->api().request(MTPmessages_UpdateDialogFilter( MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter), MTP_int(filterId), diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index f2ec676292eefd..fdf14757dd16f4 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "apiwrap.h" #include "window/window_session_controller.h" +#include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/empty_userpic.h" #include "core/application.h" @@ -20,7 +21,7 @@ For license and copyright information please follow this link: #include "data/data_file_origin.h" #include "ui/boxes/confirm_box.h" #include "ui/toasts/common_toasts.h" -#include "boxes/abstract_box.h" +#include "boxes/premium_limits_box.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -70,12 +71,16 @@ void SubmitChatInvite( "(ApiWrap::importChatInvite)").arg(result.type())); }); }).fail([=](const MTP::Error &error) { + const auto &type = error.type(); + const auto strongController = weak.get(); if (!strongController) { return; + } else if (type == u"CHANNELS_TOO_MUCH"_q) { + strongController->show( + Box(ChannelsLimitBox, &strongController->session())); } - const auto &type = error.type(); strongController->hideLayer(); Ui::ShowMultilineToast({ .parentOverride = Window::Show(strongController).toastParent(), @@ -84,8 +89,6 @@ void SubmitChatInvite( return isGroup ? tr::lng_group_request_sent(tr::now) : tr::lng_group_request_sent_channel(tr::now); - } else if (type == u"CHANNELS_TOO_MUCH"_q) { - return tr::lng_join_channel_error(tr::now); } else if (type == u"USERS_TOO_MUCH"_q) { return tr::lng_group_invite_no_room(tr::now); } else { diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index 3aad8c14e5a934..e6d8182f331654 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -123,9 +123,9 @@ void InviteLinks::performCreate( )).done([=](const MTPExportedChatInvite &result) { const auto callbacks = _createCallbacks.take(peer); const auto link = prepend(peer, peer->session().user(), result); - if (callbacks) { + if (link && callbacks) { for (const auto &callback : *callbacks) { - callback(link); + callback(*link); } } }).fail([=] { @@ -155,15 +155,18 @@ auto InviteLinks::lookupMyPermanent(const Links &links) const -> const Link* { auto InviteLinks::prepend( not_null peer, not_null admin, - const MTPExportedChatInvite &invite) -> Link { + const MTPExportedChatInvite &invite) -> std::optional { const auto link = parse(peer, invite); + if (!link) { + return link; + } if (admin->isSelf()) { - prependMyToFirstSlice(peer, admin, link); + prependMyToFirstSlice(peer, admin, *link); } _updates.fire(Update{ .peer = peer, .admin = admin, - .now = link + .now = *link }); return link; } @@ -281,6 +284,9 @@ void InviteLinks::performEdit( result.match([&](const auto &data) { _api->session().data().processUsers(data.vusers()); const auto link = parse(peer, data.vinvite()); + if (!link) { + return; + } auto i = _firstSlices.find(peer); if (i != end(_firstSlices)) { const auto j = ranges::find( @@ -288,18 +294,18 @@ void InviteLinks::performEdit( key.link, &Link::link); if (j != end(i->second.links)) { - if (link.revoked && !j->revoked) { + if (link->revoked && !j->revoked) { i->second.links.erase(j); if (i->second.count > 0) { --i->second.count; } } else { - *j = link; + *j = *link; } } } for (const auto &callback : *callbacks) { - callback(link); + callback(*link); } _updates.fire(Update{ .peer = peer, @@ -617,7 +623,11 @@ void InviteLinks::setMyPermanent( not_null peer, const MTPExportedChatInvite &invite) { auto link = parse(peer, invite); - if (!link.permanent) { + if (!link) { + LOG(("API Error: " + "InviteLinks::setPermanent called with non-link.")); + return; + } else if (!link->permanent) { LOG(("API Error: " "InviteLinks::setPermanent called with non-permanent link.")); return; @@ -632,13 +642,13 @@ void InviteLinks::setMyPermanent( .admin = peer->session().user(), }; if (const auto permanent = lookupMyPermanent(links)) { - if (permanent->link == link.link) { - if (permanent->usage != link.usage) { - permanent->usage = link.usage; + if (permanent->link == link->link) { + if (permanent->usage != link->usage) { + permanent->usage = link->usage; _updates.fire(Update{ .peer = peer, .admin = peer->session().user(), - .was = link.link, + .was = link->link, .now = *permanent }); } @@ -652,9 +662,9 @@ void InviteLinks::setMyPermanent( --links.count; } } - links.links.insert(begin(links.links), link); + links.links.insert(begin(links.links), *link); - editPermanentLink(peer, link.link); + editPermanentLink(peer, link->link); notify(peer); if (updateOldPermanent.now) { @@ -722,9 +732,10 @@ auto InviteLinks::parseSlice( peer->session().data().processUsers(data.vusers()); result.count = data.vcount().v; for (const auto &invite : data.vinvites().v) { - const auto link = parse(peer, invite); - if (!permanent || link.link != permanent->link) { - result.links.push_back(link); + if (const auto link = parse(peer, invite)) { + if (!permanent || link->link != permanent->link) { + result.links.push_back(*link); + } } } }); @@ -733,9 +744,9 @@ auto InviteLinks::parseSlice( auto InviteLinks::parse( not_null peer, - const MTPExportedChatInvite &invite) const -> Link { + const MTPExportedChatInvite &invite) const -> std::optional { return invite.match([&](const MTPDchatInviteExported &data) { - return Link{ + return std::optional(Link{ .link = qs(data.vlink()), .label = qs(data.vtitle().value_or_empty()), .admin = peer->session().data().user(data.vadmin_id()), @@ -748,7 +759,9 @@ auto InviteLinks::parse( .requestApproval = data.is_request_needed(), .permanent = data.is_permanent(), .revoked = data.is_revoked(), - }; + }); + }, [&](const MTPDchatInvitePublicJoinRequests &data) { + return std::optional(); }); } diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 586a38a1efa15b..14f5107968a4f6 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -157,13 +157,13 @@ class InviteLinks final { [[nodiscard]] Links parseSlice( not_null peer, const MTPmessages_ExportedChatInvites &slice) const; - [[nodiscard]] Link parse( + [[nodiscard]] std::optional parse( not_null peer, const MTPExportedChatInvite &invite) const; [[nodiscard]] Link *lookupMyPermanent(not_null peer); [[nodiscard]] Link *lookupMyPermanent(Links &links); [[nodiscard]] const Link *lookupMyPermanent(const Links &links) const; - Link prepend( + std::optional prepend( not_null peer, not_null admin, const MTPExportedChatInvite &invite); diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 46b8cb980ff993..be7fcadacc0bfd 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -28,6 +28,8 @@ For license and copyright information please follow this link: namespace Api { namespace { +constexpr auto kSharedMediaLimit = 100; + SendMediaReady PreparePeerPhoto( MTP::DcId dcId, PeerId peerId, @@ -75,7 +77,7 @@ SendMediaReady PreparePeerPhoto( MTP_int(dcId)); QString file, filename; - int32 filesize = 0; + int64 filesize = 0; QByteArray data; return SendMediaReady( @@ -240,4 +242,51 @@ void PeerPhoto::ready(const FullMsgId &msgId, const MTPInputFile &file) { } } +void PeerPhoto::requestUserPhotos( + not_null user, + UserPhotoId afterId) { + if (_userPhotosRequests.contains(user)) { + return; + } + + const auto requestId = _api.request(MTPphotos_GetUserPhotos( + user->inputUser, + MTP_int(0), + MTP_long(afterId), + MTP_int(kSharedMediaLimit) + )).done([this, user](const MTPphotos_Photos &result) { + _userPhotosRequests.remove(user); + + const auto fullCount = result.match([](const MTPDphotos_photos &d) { + return int(d.vphotos().v.size()); + }, [](const MTPDphotos_photosSlice &d) { + return d.vcount().v; + }); + + auto photoIds = result.match([&](const auto &data) { + auto &owner = _session->data(); + owner.processUsers(data.vusers()); + + auto photoIds = std::vector(); + photoIds.reserve(data.vphotos().v.size()); + + for (const auto &photo : data.vphotos().v) { + if (const auto photoData = owner.processPhoto(photo)) { + photoIds.push_back(photoData->id); + } + } + return photoIds; + }); + + _session->storage().add(Storage::UserPhotosAddSlice( + peerToUser(user->id), + std::move(photoIds), + fullCount + )); + }).fail([this, user] { + _userPhotosRequests.remove(user); + }).send(); + _userPhotosRequests.emplace(user, requestId); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h index f0050e18fe5a33..ca7b641196b11c 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.h +++ b/Telegram/SourceFiles/api/api_peer_photo.h @@ -11,6 +11,7 @@ For license and copyright information please follow this link: class ApiWrap; class PeerData; +class UserData; namespace Main { class Session; @@ -20,12 +21,15 @@ namespace Api { class PeerPhoto final { public: + using UserPhotoId = PhotoId; explicit PeerPhoto(not_null api); void upload(not_null peer, QImage &&image); void clear(not_null photo); void set(not_null peer, not_null photo); + void requestUserPhotos(not_null user, UserPhotoId afterId); + private: void ready(const FullMsgId &msgId, const MTPInputFile &file); @@ -34,6 +38,8 @@ class PeerPhoto final { base::flat_map> _uploads; + base::flat_map, mtpRequestId> _userPhotosRequests; + }; } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp new file mode 100644 index 00000000000000..2114e914c751f2 --- /dev/null +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -0,0 +1,140 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "api/api_premium.h" + +#include "api/api_text_entities.h" +#include "main/main_session.h" +#include "data/data_peer_values.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_peer.h" +#include "apiwrap.h" + +namespace Api { + +Premium::Premium(not_null api) +: _session(&api->session()) +, _api(&api->instance()) { + crl::on_main(_session, [=] { + // You can't use _session->user() in the constructor, + // only queued, because it is not constructed yet. + Data::AmPremiumValue( + _session + ) | rpl::start_with_next([=] { + reload(); + }, _session->lifetime()); + }); +} + +rpl::producer Premium::statusTextValue() const { + return _statusTextUpdates.events_starting_with_copy( + _statusText.value_or(TextWithEntities())); +} + +auto Premium::videos() const +-> const base::flat_map> & { + return _videos; +} + +rpl::producer<> Premium::videosUpdated() const { + return _videosUpdated.events(); +} + +auto Premium::stickers() const +-> const std::vector> & { + return _stickers; +} + +rpl::producer<> Premium::stickersUpdated() const { + return _stickersUpdated.events(); +} + +int64 Premium::monthlyAmount() const { + return _monthlyAmount; +} + +QString Premium::monthlyCurrency() const { + return _monthlyCurrency; +} + +void Premium::reload() { + reloadPromo(); + reloadStickers(); +} + +void Premium::reloadPromo() { + if (_promoRequestId) { + return; + } + _promoRequestId = _api.request(MTPhelp_GetPremiumPromo( + )).done([=](const MTPhelp_PremiumPromo &result) { + _promoRequestId = 0; + result.match([&](const MTPDhelp_premiumPromo &data) { + _session->data().processUsers(data.vusers()); + _monthlyAmount = data.vmonthly_amount().v; + _monthlyCurrency = qs(data.vcurrency()); + auto text = TextWithEntities{ + qs(data.vstatus_text()), + EntitiesFromMTP(_session, data.vstatus_entities().v), + }; + _statusText = text; + _statusTextUpdates.fire(std::move(text)); + auto videos = base::flat_map>(); + const auto count = int(std::min( + data.vvideo_sections().v.size(), + data.vvideos().v.size())); + videos.reserve(count); + for (auto i = 0; i != count; ++i) { + const auto document = _session->data().processDocument( + data.vvideos().v[i]); + if ((!document->isVideoFile() && !document->isGifv()) + || !document->supportsStreaming()) { + document->forceIsStreamedAnimation(); + } + videos.emplace( + qs(data.vvideo_sections().v[i]), + document); + } + if (_videos != videos) { + _videos = std::move(videos); + _videosUpdated.fire({}); + } + }); + }).fail([=] { + _promoRequestId = 0; + }).send(); +} + +void Premium::reloadStickers() { + if (_stickersRequestId) { + return; + } + _stickersRequestId = _api.request(MTPmessages_GetStickers( + MTP_string("\xe2\xad\x90\xef\xb8\x8f\xe2\xad\x90\xef\xb8\x8f"), + MTP_long(_stickersHash) + )).done([=](const MTPmessages_Stickers &result) { + _stickersRequestId = 0; + result.match([&](const MTPDmessages_stickersNotModified &) { + }, [&](const MTPDmessages_stickers &data) { + _stickersHash = data.vhash().v; + const auto owner = &_session->data(); + _stickers.clear(); + for (const auto &sticker : data.vstickers().v) { + const auto document = owner->processDocument(sticker); + if (document->isPremiumSticker()) { + _stickers.push_back(document); + } + } + _stickersUpdated.fire({}); + }); + }).fail([=] { + _stickersRequestId = 0; + }).send(); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h new file mode 100644 index 00000000000000..b89d92b45aa4b9 --- /dev/null +++ b/Telegram/SourceFiles/api/api_premium.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 "mtproto/sender.h" + +class ApiWrap; + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +class Premium final { +public: + explicit Premium(not_null api); + + void reload(); + [[nodiscard]] rpl::producer statusTextValue() const; + + [[nodiscard]] auto videos() const + -> const base::flat_map> &; + [[nodiscard]] rpl::producer<> videosUpdated() const; + + [[nodiscard]] auto stickers() const + -> const std::vector> &; + [[nodiscard]] rpl::producer<> stickersUpdated() const; + + [[nodiscard]] int64 monthlyAmount() const; + [[nodiscard]] QString monthlyCurrency() const; + +private: + void reloadPromo(); + void reloadStickers(); + + const not_null _session; + MTP::Sender _api; + + mtpRequestId _promoRequestId = 0; + std::optional _statusText; + rpl::event_stream _statusTextUpdates; + + base::flat_map> _videos; + rpl::event_stream<> _videosUpdated; + + mtpRequestId _stickersRequestId = 0; + uint64 _stickersHash = 0; + std::vector> _stickers; + rpl::event_stream<> _stickersUpdated; + + int64 _monthlyAmount = 0; + QString _monthlyCurrency; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index 0315de90d9edd2..c7954a883bfc1f 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -41,7 +41,7 @@ SendMediaReady PrepareRingtoneDocument( MTP_bytes(), MTP_int(base::unixtime::now()), MTP_string(filemime), - MTP_int(content.size()), + MTP_long(content.size()), MTP_vector(), MTPVector(), MTP_int(dcId), @@ -191,8 +191,8 @@ void Ringtones::remove(DocumentId id) { } } -int Ringtones::maxSize() const { - return int(base::SafeRound(_session->account().appConfig().get( +int64 Ringtones::maxSize() const { + return int64(base::SafeRound(_session->account().appConfig().get( "ringtone_size_max", 100 * 1024))); } diff --git a/Telegram/SourceFiles/api/api_ringtones.h b/Telegram/SourceFiles/api/api_ringtones.h index 2707c92ad1429f..08918a89e1f891 100644 --- a/Telegram/SourceFiles/api/api_ringtones.h +++ b/Telegram/SourceFiles/api/api_ringtones.h @@ -38,7 +38,7 @@ class Ringtones final { [[nodiscard]] rpl::producer uploadFails() const; [[nodiscard]] rpl::producer uploadDones() const; - [[nodiscard]] int maxSize() const; + [[nodiscard]] int64 maxSize() const; [[nodiscard]] int maxSavedCount() const; [[nodiscard]] int maxDuration() const; diff --git a/Telegram/SourceFiles/api/api_toggling_media.cpp b/Telegram/SourceFiles/api/api_toggling_media.cpp index dbfcfde3494d3d..b67e3002de4b5a 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.cpp +++ b/Telegram/SourceFiles/api/api_toggling_media.cpp @@ -12,6 +12,7 @@ For license and copyright information please follow this link: #include "data/data_file_origin.h" #include "data/data_session.h" #include "data/stickers/data_stickers.h" +#include "window/window_session_controller.h" #include "main/main_session.h" namespace Api { @@ -47,28 +48,35 @@ void ToggleExistingMedia( } // namespace void ToggleFavedSticker( + not_null controller, not_null document, Data::FileOrigin origin) { ToggleFavedSticker( + controller, document, std::move(origin), !document->owner().stickers().isFaved(document)); } void ToggleFavedSticker( + not_null controller, not_null document, Data::FileOrigin origin, bool faved) { if (faved && !document->sticker()) { return; } + const auto weak = base::make_weak(controller.get()); + auto done = [=] { + document->owner().stickers().setFaved(weak.get(), document, faved); + }; ToggleExistingMedia( document, std::move(origin), [=, d = document] { return MTPmessages_FaveSticker(d->mtpInput(), MTP_bool(!faved)); }, - [=] { document->owner().stickers().setFaved(document, faved); }); + std::move(done)); } void ToggleRecentSticker( @@ -96,15 +104,17 @@ void ToggleRecentSticker( } void ToggleSavedGif( + Window::SessionController *controller, not_null document, Data::FileOrigin origin, bool saved) { if (saved && !document->isGifv()) { return; } + const auto weak = base::make_weak(controller); auto done = [=] { if (saved) { - document->owner().stickers().addSavedGif(document); + document->owner().stickers().addSavedGif(weak.get(), document); } }; ToggleExistingMedia( diff --git a/Telegram/SourceFiles/api/api_toggling_media.h b/Telegram/SourceFiles/api/api_toggling_media.h index fc1c84e1755da9..a028824d5bb2f9 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.h +++ b/Telegram/SourceFiles/api/api_toggling_media.h @@ -7,13 +7,19 @@ For license and copyright information please follow this link: */ #pragma once +namespace Window { +class SessionController; +} // namespace Window + namespace Api { void ToggleFavedSticker( + not_null controller, not_null document, Data::FileOrigin origin); void ToggleFavedSticker( + not_null controller, not_null document, Data::FileOrigin origin, bool faved); @@ -24,6 +30,7 @@ void ToggleRecentSticker( bool saved); void ToggleSavedGif( + Window::SessionController *controller, not_null document, Data::FileOrigin origin, bool saved); diff --git a/Telegram/SourceFiles/api/api_transcribes.cpp b/Telegram/SourceFiles/api/api_transcribes.cpp new file mode 100644 index 00000000000000..30c7fdee5bbd25 --- /dev/null +++ b/Telegram/SourceFiles/api/api_transcribes.cpp @@ -0,0 +1,99 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "api/api_transcribes.h" + +#include "history/history_item.h" +#include "history/history.h" +#include "main/main_session.h" +#include "data/data_session.h" +#include "data/data_peer.h" +#include "apiwrap.h" + +namespace Api { + +Transcribes::Transcribes(not_null api) +: _session(&api->session()) +, _api(&api->instance()) { +} + +void Transcribes::toggle(not_null item) { + const auto id = item->fullId(); + auto i = _map.find(id); + if (i == _map.end()) { + load(item); + //_session->data().requestItemRepaint(item); + _session->data().requestItemResize(item); + } else if (!i->second.requestId) { + i->second.shown = !i->second.shown; + _session->data().requestItemResize(item); + } +} + +const Transcribes::Entry &Transcribes::entry( + not_null item) const { + static auto empty = Entry(); + const auto i = _map.find(item->fullId()); + return (i != _map.end()) ? i->second : empty; +} + +void Transcribes::apply(const MTPDupdateTranscribedAudio &update) { + const auto id = update.vtranscription_id().v; + const auto i = _ids.find(id); + if (i == _ids.end()) { + return; + } + const auto j = _map.find(i->second); + if (j == _map.end()) { + return; + } + const auto text = qs(update.vtext()); + j->second.result = text; + j->second.pending = update.is_pending(); + if (const auto item = _session->data().message(i->second)) { + _session->data().requestItemResize(item); + } +} + +void Transcribes::load(not_null item) { + if (!item->isHistoryEntry() || item->isLocal()) { + return; + } + const auto id = item->fullId(); + const auto requestId = _api.request(MTPmessages_TranscribeAudio( + item->history()->peer->input, + MTP_int(item->id) + )).done([=](const MTPmessages_TranscribedAudio &result) { + result.match([&](const MTPDmessages_transcribedAudio &data) { + auto &entry = _map[id]; + entry.requestId = 0; + entry.pending = data.is_pending(); + entry.result = qs(data.vtext()); + _ids.emplace(data.vtranscription_id().v, id); + if (const auto item = _session->data().message(id)) { + _session->data().requestItemResize(item); + } + }); + }).fail([=](const MTP::Error &error) { + auto &entry = _map[id]; + entry.requestId = 0; + entry.pending = false; + entry.failed = true; + if (error.type() == qstr("MSG_VOICE_TOO_LONG")) { + entry.toolong = true; + } else if (const auto item = _session->data().message(id)) { + _session->data().requestItemResize(item); + } + }).send(); + auto &entry = _map.emplace(id).first->second; + entry.requestId = requestId; + entry.shown = true; + entry.failed = false; + entry.pending = false; +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_transcribes.h b/Telegram/SourceFiles/api/api_transcribes.h new file mode 100644 index 00000000000000..a63fb97afd5386 --- /dev/null +++ b/Telegram/SourceFiles/api/api_transcribes.h @@ -0,0 +1,49 @@ +/* +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 "mtproto/sender.h" + +class ApiWrap; + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +class Transcribes final { +public: + explicit Transcribes(not_null api); + + struct Entry { + QString result; + bool shown = false; + bool failed = false; + bool toolong = false; + bool pending = false; + mtpRequestId requestId = 0; + }; + + void toggle(not_null item); + [[nodiscard]] const Entry &entry(not_null item) const; + + void apply(const MTPDupdateTranscribedAudio &update); + +private: + void load(not_null item); + + const not_null _session; + MTP::Sender _api; + + base::flat_map _map; + base::flat_map _ids; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 804c3831107e6c..9cb3b2bf0f50b3 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -13,6 +13,7 @@ For license and copyright information please follow this link: #include "api/api_text_entities.h" #include "api/api_user_privacy.h" #include "api/api_unread_things.h" +#include "api/api_transcribes.h" #include "main/main_session.h" #include "main/main_account.h" #include "mtproto/mtp_instance.h" @@ -1986,16 +1987,29 @@ void Updates::feedUpdate(const MTPUpdate &update) { const auto &d = update.c_updateBotCommands(); if (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer()))) { const auto botId = UserId(d.vbot_id().v); + const auto commands = Data::BotCommands{ + .userId = UserId(d.vbot_id().v), + .commands = ranges::views::all( + d.vcommands().v + ) | ranges::views::transform( + Data::BotCommandFromTL + ) | ranges::to_vector, + }; + if (const auto user = peer->asUser()) { if (user->isBot() && user->id == peerFromUser(botId)) { - if (Data::UpdateBotCommands(user->botInfo->commands, d.vcommands())) { + const auto equal = ranges::equal( + user->botInfo->commands, + commands.commands); + user->botInfo->commands = commands.commands; + if (!equal) { session().data().botCommandsChanged(user); } } } else if (const auto chat = peer->asChat()) { - chat->setBotCommands(botId, d.vcommands()); + chat->setBotCommands({ commands }); } else if (const auto megagroup = peer->asMegagroup()) { - if (megagroup->mgInfo->updateBotCommands(botId, d.vcommands())) { + if (megagroup->mgInfo->setBotCommands({ commands })) { session().data().botCommandsChanged(megagroup); } } @@ -2015,7 +2029,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { const auto &d = update.c_updateBotMenuButton(); if (const auto bot = session().data().userLoaded(d.vbot_id())) { if (const auto info = bot->botInfo.get(); info && info->inited) { - if (Data::ApplyBotMenuButton(info, d.vbutton())) { + if (Data::ApplyBotMenuButton(info, &d.vbutton())) { session().data().botCommandsChanged(bot); } } @@ -2387,6 +2401,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().api().ringtones().applyUpdate(); } break; + case mtpc_updateTranscribedAudio: { + const auto &data = update.c_updateTranscribedAudio(); + _session->api().transcribes().apply(data); + } + } } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index bb0b8eba2af53b..47314e066681cf 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -28,6 +28,8 @@ For license and copyright information please follow this link: #include "api/api_confirm_phone.h" #include "api/api_unread_things.h" #include "api/api_ringtones.h" +#include "api/api_transcribes.h" +#include "api/api_premium.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -70,6 +72,7 @@ For license and copyright information please follow this link: #include "ui/boxes/confirm_box.h" #include "boxes/stickers_box.h" #include "boxes/sticker_set_box.h" +#include "boxes/premium_limits_box.h" #include "window/notifications_manager.h" #include "window/window_lock_widgets.h" #include "window/window_session_controller.h" @@ -81,12 +84,12 @@ For license and copyright information please follow this link: #include "ui/chat/attach/attach_prepare.h" #include "ui/toasts/common_toasts.h" #include "support/support_helper.h" +#include "settings/settings_premium.h" #include "storage/localimageloader.h" #include "storage/download_manager_mtproto.h" #include "storage/file_upload.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" -#include "storage/storage_user_photos.h" #include "storage/storage_media_prepare.h" #include "storage/storage_account.h" #include "facades.h" @@ -99,7 +102,6 @@ constexpr auto kSaveCloudDraftTimeout = 1000; constexpr auto kTopPromotionInterval = TimeId(60 * 60); constexpr auto kTopPromotionMinDelay = TimeId(10); constexpr auto kSmallDelayMs = 5; -constexpr auto kSharedMediaLimit = 100; constexpr auto kReadFeaturedSetsTimeout = crl::time(1000); constexpr auto kFileLoaderQueueStopTimeout = crl::time(5000); constexpr auto kStickersByEmojiInvalidateTimeout = crl::time(6 * 1000); @@ -143,7 +145,9 @@ ApiWrap::ApiWrap(not_null session) , _polls(std::make_unique(this)) , _chatParticipants(std::make_unique(this)) , _unreadThings(std::make_unique(this)) -, _ringtones(std::make_unique(this)) { +, _ringtones(std::make_unique(this)) +, _transcribes(std::make_unique(this)) +, _premium(std::make_unique(this)) { crl::on_main(session, [=] { // You can't use _session->lifetime() in the constructor, // only queued, because it is not constructed yet. @@ -450,6 +454,8 @@ void ApiWrap::sendMessageFail( ? tr::lng_error_noforwards_channel(tr::now) : tr::lng_error_noforwards_group(tr::now) }, .duration = kJoinErrorDuration }); + } else if (error.type() == qstr("PREMIUM_ACCOUNT_REQUIRED")) { + Settings::ShowPremium(&session(), "premium_stickers"); } if (const auto item = _session->data().message(itemId)) { Assert(randomId != 0); @@ -992,46 +998,41 @@ void ApiWrap::requestFullPeer(not_null peer) { } return request(MTPusers_GetFullUser( user->inputUser - )).done([=](const MTPusers_UserFull &result, mtpRequestId requestId) { + )).done([=](const MTPusers_UserFull &result) { result.match([&](const MTPDusers_userFull &data) { _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); }); - gotUserFull(user, result, requestId); + gotUserFull(user, result); }).fail(failHandler).send(); } else if (const auto chat = peer->asChat()) { return request(MTPmessages_GetFullChat( chat->inputChat - )).done([=]( - const MTPmessages_ChatFull &result, - mtpRequestId requestId) { - gotChatFull(peer, result, requestId); + )).done([=](const MTPmessages_ChatFull &result) { + gotChatFull(peer, result); }).fail(failHandler).send(); } else if (const auto channel = peer->asChannel()) { return request(MTPchannels_GetFullChannel( channel->inputChannel - )).done([=]( - const MTPmessages_ChatFull &result, - mtpRequestId requestId) { - gotChatFull(peer, result, requestId); + )).done([=](const MTPmessages_ChatFull &result) { + gotChatFull(peer, result); migrateDone(channel, channel); }).fail(failHandler).send(); } Unexpected("Peer type in requestFullPeer."); }(); - _fullPeerRequests.insert(peer, requestId); + _fullPeerRequests.emplace(peer, requestId); } void ApiWrap::processFullPeer( not_null peer, const MTPmessages_ChatFull &result) { - gotChatFull(peer, result, mtpRequestId(0)); + gotChatFull(peer, result); } void ApiWrap::gotChatFull( not_null peer, - const MTPmessages_ChatFull &result, - mtpRequestId req) { + const MTPmessages_ChatFull &result) { const auto &d = result.c_messages_chatFull(); _session->data().applyMaximumChatVersions(d.vchats()); @@ -1054,12 +1055,7 @@ void ApiWrap::gotChatFull( } }); - if (req) { - const auto i = _fullPeerRequests.find(peer); - if (i != _fullPeerRequests.cend() && i.value() == req) { - _fullPeerRequests.erase(i); - } - } + _fullPeerRequests.remove(peer); _session->changes().peerUpdated( peer, Data::PeerUpdate::Flag::FullInfo); @@ -1067,8 +1063,7 @@ void ApiWrap::gotChatFull( void ApiWrap::gotUserFull( not_null user, - const MTPusers_UserFull &result, - mtpRequestId req) { + const MTPusers_UserFull &result) { result.match([&](const MTPDusers_userFull &data) { data.vfull_user().match([&](const MTPDuserFull &fields) { if (user == _session->user() && !_session->validateSelf(fields.vid().v)) { @@ -1081,12 +1076,7 @@ void ApiWrap::gotUserFull( Data::ApplyUserUpdate(user, fields); }); }); - if (req) { - const auto i = _fullPeerRequests.find(user); - if (i != _fullPeerRequests.cend() && i.value() == req) { - _fullPeerRequests.erase(i); - } - } + _fullPeerRequests.remove(user); _session->changes().peerUpdated( user, Data::PeerUpdate::Flag::FullInfo); @@ -1127,7 +1117,7 @@ void ApiWrap::requestPeer(not_null peer) { } Unexpected("Peer type in requestPeer."); }(); - _peerRequests.insert(peer, requestId); + _peerRequests.emplace(peer, requestId); } void ApiWrap::requestPeerSettings(not_null peer) { @@ -1224,7 +1214,7 @@ void ApiWrap::migrateDone( void ApiWrap::migrateFail(not_null peer, const QString &error) { if (error == u"CHANNELS_TOO_MUCH"_q) { - Ui::show(Ui::MakeInformBox(tr::lng_migrate_error())); + Ui::show(Box(ChannelsLimitBox, _session)); } if (auto handlers = _migrateCallbacks.take(peer)) { for (auto &handler : *handlers) { @@ -1655,6 +1645,8 @@ void ApiWrap::joinChannel(not_null channel) { if (type == qstr("CHANNEL_PRIVATE") && channel->invitePeekExpires()) { channel->privateErrorReceived(); + } else if (type == qstr("CHANNELS_TOO_MUCH")) { + Ui::show(Box(ChannelsLimitBox, _session)); } else { const auto text = [&] { if (type == qstr("INVITE_REQUEST_SENT")) { @@ -1667,8 +1659,6 @@ void ApiWrap::joinChannel(not_null channel) { return channel->isMegagroup() ? tr::lng_group_not_accessible(tr::now) : tr::lng_channel_not_accessible(tr::now); - } else if (type == qstr("CHANNELS_TOO_MUCH")) { - return tr::lng_join_channel_error(tr::now); } else if (type == qstr("USERS_TOO_MUCH")) { return tr::lng_group_full(tr::now); } @@ -2046,15 +2036,16 @@ void ApiWrap::saveDraftsToCloud() { history->finishSavingCloudDraft( UnixtimeFromMsgId(response.outerMsgId)); + const auto requestId = response.requestId; if (const auto cloudDraft = history->cloudDraft()) { - if (cloudDraft->saveRequestId == response.requestId) { + if (cloudDraft->saveRequestId == requestId) { cloudDraft->saveRequestId = 0; history->draftSavedToCloud(); } } auto i = _draftsSaveRequestIds.find(history); if (i != _draftsSaveRequestIds.cend() - && i->second == response.requestId) { + && i->second == requestId) { _draftsSaveRequestIds.erase(history); checkQuitPreventFinished(); } @@ -2062,14 +2053,15 @@ void ApiWrap::saveDraftsToCloud() { history->finishSavingCloudDraft( UnixtimeFromMsgId(response.outerMsgId)); + const auto requestId = response.requestId; if (const auto cloudDraft = history->cloudDraft()) { - if (cloudDraft->saveRequestId == response.requestId) { + if (cloudDraft->saveRequestId == requestId) { history->clearCloudDraft(); } } auto i = _draftsSaveRequestIds.find(history); if (i != _draftsSaveRequestIds.cend() - && i->second == response.requestId) { + && i->second == requestId) { _draftsSaveRequestIds.erase(history); checkQuitPreventFinished(); } @@ -2443,6 +2435,8 @@ void ApiWrap::refreshFileReference( MTP_long(0))); }, [&](Data::FileOriginRingtones data) { request(MTPaccount_GetSavedRingtones(MTP_long(0))); + }, [&](Data::FileOriginPremiumPreviews data) { + request(MTPhelp_GetPremiumPromo()); }, [&](v::null_t) { fail(); }); @@ -2909,10 +2903,16 @@ void ApiWrap::requestSharedMedia( histories.sendRequest(history, requestType, [=](Fn finish) { return request( std::move(*prepared) - ).done([=](const MTPmessages_Messages &result) { + ).done([=](const Api::SearchRequestResult &result) { const auto key = std::make_tuple(peer, type, messageId, slice); _sharedMediaRequests.remove(key); - sharedMediaDone(peer, type, messageId, slice, result); + auto parsed = Api::ParseSearchResult( + peer, + type, + messageId, + slice, + result); + sharedMediaDone(peer, type, std::move(parsed)); finish(); }).fail([=] { _sharedMediaRequests.remove(key); @@ -2925,15 +2925,7 @@ void ApiWrap::requestSharedMedia( void ApiWrap::sharedMediaDone( not_null peer, SharedMediaType type, - MsgId messageId, - SliceType slice, - const MTPmessages_Messages &result) { - auto parsed = Api::ParseSearchResult( - peer, - type, - messageId, - slice, - result); + Api::SearchResult &&parsed) { _session->storage().add(Storage::SharedMediaAddSlice( peer->id, type, @@ -2946,67 +2938,6 @@ void ApiWrap::sharedMediaDone( } } -void ApiWrap::requestUserPhotos( - not_null user, - PhotoId afterId) { - if (_userPhotosRequests.contains(user)) { - return; - } - - auto limit = kSharedMediaLimit; - - auto requestId = request(MTPphotos_GetUserPhotos( - user->inputUser, - MTP_int(0), - MTP_long(afterId), - MTP_int(limit) - )).done([this, user, afterId](const MTPphotos_Photos &result) { - _userPhotosRequests.remove(user); - userPhotosDone(user, afterId, result); - }).fail([this, user] { - _userPhotosRequests.remove(user); - }).send(); - _userPhotosRequests.emplace(user, requestId); -} - -void ApiWrap::userPhotosDone( - not_null user, - PhotoId photoId, - const MTPphotos_Photos &result) { - auto fullCount = 0; - auto &photos = *[&] { - switch (result.type()) { - case mtpc_photos_photos: { - auto &d = result.c_photos_photos(); - _session->data().processUsers(d.vusers()); - fullCount = d.vphotos().v.size(); - return &d.vphotos().v; - } break; - - case mtpc_photos_photosSlice: { - auto &d = result.c_photos_photosSlice(); - _session->data().processUsers(d.vusers()); - fullCount = d.vcount().v; - return &d.vphotos().v; - } break; - } - Unexpected("photos.Photos type in userPhotosDone()"); - }(); - - auto photoIds = std::vector(); - photoIds.reserve(photos.size()); - for (auto &photo : photos) { - if (auto photoData = _session->data().processPhoto(photo)) { - photoIds.push_back(photoData->id); - } - } - _session->storage().add(Storage::UserPhotosAddSlice( - peerToUser(user->id), - std::move(photoIds), - fullCount - )); -} - void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled) { _session->data().histories().readInbox(action.history); @@ -4098,3 +4029,11 @@ Api::UnreadThings &ApiWrap::unreadThings() { Api::Ringtones &ApiWrap::ringtones() { return *_ringtones; } + +Api::Transcribes &ApiWrap::transcribes() { + return *_transcribes; +} + +Api::Premium &ApiWrap::premium() { + return *_premium; +} diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 467a2828c595b1..be4b9f1c4e7c6d 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -53,6 +53,8 @@ struct PreparedList; namespace Api { +struct SearchResult; + class Updates; class Authorizations; class AttachedStickers; @@ -70,6 +72,8 @@ class Polls; class ChatParticipants; class UnreadThings; class Ringtones; +class Transcribes; +class Premium; namespace details { @@ -264,10 +268,6 @@ class ApiWrap final : public MTP::Sender { not_null peer, Storage::SharedMediaType type); - void requestUserPhotos( - not_null user, - PhotoId afterId); - void readFeaturedSetDelayed(uint64 setId); rpl::producer sendActions() const { @@ -361,6 +361,8 @@ class ApiWrap final : public MTP::Sender { [[nodiscard]] Api::ChatParticipants &chatParticipants(); [[nodiscard]] Api::UnreadThings &unreadThings(); [[nodiscard]] Api::Ringtones &ringtones(); + [[nodiscard]] Api::Transcribes &transcribes(); + [[nodiscard]] Api::Premium &premium(); void updatePrivacyLastSeens(); @@ -420,12 +422,10 @@ class ApiWrap final : public MTP::Sender { void gotChatFull( not_null peer, - const MTPmessages_ChatFull &result, - mtpRequestId req); + const MTPmessages_ChatFull &result); void gotUserFull( not_null user, - const MTPusers_UserFull &result, - mtpRequestId req); + const MTPusers_UserFull &result); void resolveWebPages(); void gotWebPages( ChannelData *channel, @@ -452,14 +452,7 @@ class ApiWrap final : public MTP::Sender { void sharedMediaDone( not_null peer, SharedMediaType type, - MsgId messageId, - SliceType slice, - const MTPmessages_Messages &result); - - void userPhotosDone( - not_null user, - PhotoId photoId, - const MTPphotos_Photos &result); + Api::SearchResult &&parsed); void sendSharedContact( const QString &phone, @@ -527,7 +520,7 @@ class ApiWrap final : public MTP::Sender { MessageDataRequests> _channelMessageDataRequests; SingleQueuedInvokation _messageDataResolveDelayed; - using PeerRequests = QMap; + using PeerRequests = base::flat_map; PeerRequests _fullPeerRequests; PeerRequests _peerRequests; base::flat_set> _requestedPeerSettings; @@ -575,8 +568,6 @@ class ApiWrap final : public MTP::Sender { MsgId, SliceType>> _sharedMediaRequests; - base::flat_map, mtpRequestId> _userPhotosRequests; - std::unique_ptr _dialogsLoadState; TimeId _dialogsLoadTill = 0; rpl::variable _dialogsLoadMayBlockByDate = false; @@ -644,6 +635,8 @@ class ApiWrap final : public MTP::Sender { const std::unique_ptr _chatParticipants; const std::unique_ptr _unreadThings; const std::unique_ptr _ringtones; + const std::unique_ptr _transcribes; + const std::unique_ptr _premium; mtpRequestId _wallPaperRequestId = 0; QString _wallPaperSlug; diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index f9727afd75652a..b2d39c3741a85f 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -11,11 +11,13 @@ For license and copyright information please follow this link: #include "base/random.h" #include "ui/boxes/confirm_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/premium_limits_box.h" #include "boxes/peers/add_participants_box.h" #include "boxes/peers/edit_peer_common.h" #include "boxes/peers/edit_participant_box.h" #include "boxes/peers/edit_participants_box.h" #include "core/application.h" +#include "core/core_settings.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "countries/countries_instance.h" // Countries::ExtractPhoneCode. #include "window/window_session_controller.h" @@ -105,14 +107,6 @@ void ChatCreateDone( } // namespace -style::InputField CreateBioFieldStyle() { - auto result = st::newGroupDescription; - result.textMargins.setRight( - st::boxTextFont->spacew - + st::boxTextFont->width(QString::number(kMaxBioLength))); - return result; -} - TextWithEntities PeerFloodErrorText( not_null session, PeerFloodType type) { @@ -206,48 +200,6 @@ void ShowAddParticipantsError( Ui::show(Ui::MakeInformBox(text), Ui::LayerOption::KeepOther); } -class RevokePublicLinkBox::Inner : public TWidget { -public: - Inner( - QWidget *parent, - not_null session, - Fn revokeCallback); - -protected: - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void paintEvent(QPaintEvent *e) override; - -private: - struct ChatRow { - ChatRow(not_null peer) : peer(peer) { - } - - not_null peer; - mutable std::shared_ptr userpic; - Ui::Text::String name, status; - }; - void paintChat(Painter &p, const ChatRow &row, bool selected) const; - void updateSelected(); - - const not_null _session; - MTP::Sender _api; - - PeerData *_selected = nullptr; - PeerData *_pressed = nullptr; - - std::vector _rows; - - int _rowsTop = 0; - int _rowHeight = 0; - int _revokeWidth = 0; - - Fn _revokeCallback; - mtpRequestId _revokeRequestId = 0; - -}; - AddContactBox::AddContactBox( QWidget*, not_null session) @@ -446,7 +398,7 @@ void AddContactBox::save() { if (user->isContact() || user->session().supportMode()) { Ui::showPeerHistory(user, ShowAtTheEndMsgId); } - Ui::hideLayer(); + getDelegate()->hideLayer(); } else if (isBoxShown()) { hideChildren(); _retrying = true; @@ -642,7 +594,7 @@ void GroupInfoBox::createGroup( auto image = _photo->takeResultImage(); const auto navigation = _navigation; - Ui::hideLayer(); // Destroys 'this'. + getDelegate()->hideLayer(); // Destroys 'this'. ChatCreateDone(navigation, std::move(image), result); }).fail([=](const MTP::Error &error) { const auto &type = error.type(); @@ -782,7 +734,7 @@ void GroupInfoBox::createChannel( Ui::LayerOption::CloseOther); } else if (type == u"CHANNELS_TOO_MUCH"_q) { controller->show( - Ui::MakeInformBox(tr::lng_cant_do_this()), + Box(ChannelsLimitBox, &controller->session()), Ui::LayerOption::CloseOther); // TODO } }).send(); @@ -1104,7 +1056,9 @@ void SetupChannelBox::mousePressEvent(QMouseEvent *e) { return; } else if (!_channel->inviteLink().isEmpty()) { QGuiApplication::clipboard()->setText(_channel->inviteLink()); - Ui::Toast::Show(tr::lng_create_channel_link_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_create_channel_link_copied(tr::now)); } else if (_channel->isFullLoaded() && !_creatingInviteLink) { _creatingInviteLink = true; _channel->session().api().inviteLinks().create(_channel); @@ -1242,9 +1196,7 @@ void SetupChannelBox::privacyChanged(Privacy value) { check(); }); Ui::show( - Box( - &_channel->session(), - callback), + Box(PublicLinksLimitBox, _navigation, callback), Ui::LayerOption::KeepOther); return; } @@ -1304,7 +1256,7 @@ void SetupChannelBox::updateFail(UsernameResult result) { void SetupChannelBox::checkFail(UsernameResult result) { if (result == UsernameResult::NA) { - Ui::hideLayer(); + getDelegate()->hideLayer(); } else if (result == UsernameResult::ChatsTooMuch) { if (_existing) { showRevokePublicLinkBoxForEdit(); @@ -1336,15 +1288,13 @@ void SetupChannelBox::showRevokePublicLinkBoxForEdit() { }; closeBox(); Ui::show( - Box( - &channel->session(), - callback), + Box(PublicLinksLimitBox, navigation, callback), Ui::LayerOption::KeepOther); } void SetupChannelBox::firstCheckFail(UsernameResult result) { if (result == UsernameResult::NA) { - Ui::hideLayer(); + getDelegate()->hideLayer(); } else if (result == UsernameResult::ChatsTooMuch) { if (_existing) { showRevokePublicLinkBoxForEdit(); @@ -1504,251 +1454,3 @@ void EditNameBox::saveSelfFail(const QString &error) { _first->setFocus(); } } - -RevokePublicLinkBox::Inner::Inner( - QWidget *parent, - not_null session, - Fn revokeCallback) -: TWidget(parent) -, _session(session) -, _api(&_session->mtp()) -, _rowHeight(st::contactsPadding.top() - + st::contactsPhotoSize - + st::contactsPadding.bottom()) -, _revokeWidth(st::normalFont->width( - tr::lng_channels_too_much_public_revoke(tr::now))) -, _revokeCallback(std::move(revokeCallback)) { - setMouseTracking(true); - - resize(width(), 5 * _rowHeight); - - _api.request(MTPchannels_GetAdminedPublicChannels( - MTP_flags(0) - )).done([=](const MTPmessages_Chats &result) { - const auto &chats = result.match([](const auto &data) { - return data.vchats().v; - }); - for (const auto &chat : chats) { - if (const auto peer = _session->data().processChat(chat)) { - if (!peer->isChannel() || peer->userName().isEmpty()) { - continue; - } - - auto row = ChatRow(peer); - row.peer = peer; - row.name.setText( - st::contactsNameStyle, - peer->name, - Ui::NameTextOptions()); - row.status.setMarkedText( - st::defaultTextStyle, - _session->createInternalLink( - Ui::Text::Link(peer->userName()))); - _rows.push_back(std::move(row)); - } - } - resize(width(), _rows.size() * _rowHeight); - update(); - }).send(); -} - -RevokePublicLinkBox::RevokePublicLinkBox( - QWidget*, - not_null session, - Fn revokeCallback) -: _session(session) -, _aboutRevoke( - this, - tr::lng_channels_too_much_public_about(tr::now), - st::aboutRevokePublicLabel) -, _revokeCallback(std::move(revokeCallback)) { -} - -void RevokePublicLinkBox::prepare() { - _innerTop = st::boxPadding.top() - + _aboutRevoke->height() - + st::boxPadding.top(); - _inner = setInnerWidget(object_ptr(this, _session, [=] { - const auto callback = _revokeCallback; - closeBox(); - if (callback) { - callback(); - } - }), st::boxScroll, _innerTop); - - addButton(tr::lng_cancel(), [=] { closeBox(); }); - - _session->downloaderTaskFinished( - ) | rpl::start_with_next([=] { - update(); - }, lifetime()); - - _inner->resizeToWidth(st::boxWideWidth); - setDimensions(st::boxWideWidth, _innerTop + _inner->height()); -} - -void RevokePublicLinkBox::Inner::mouseMoveEvent(QMouseEvent *e) { - updateSelected(); -} - -void RevokePublicLinkBox::Inner::updateSelected() { - const auto point = mapFromGlobal(QCursor::pos()); - PeerData *selected = nullptr; - auto top = _rowsTop; - for (const auto &row : _rows) { - const auto revokeLink = style::rtlrect( - width() - - st::contactsPadding.right() - - st::contactsCheckPosition.x() - - _revokeWidth, - top - + st::contactsPadding.top() - + (st::contactsPhotoSize - st::normalFont->height) / 2, - _revokeWidth, - st::normalFont->height, - width()); - if (revokeLink.contains(point)) { - selected = row.peer; - break; - } - top += _rowHeight; - } - if (selected != _selected) { - _selected = selected; - setCursor((_selected || _pressed) - ? style::cur_pointer - : style::cur_default); - update(); - } -} - -void RevokePublicLinkBox::Inner::mousePressEvent(QMouseEvent *e) { - if (_pressed != _selected) { - _pressed = _selected; - update(); - } -} - -void RevokePublicLinkBox::Inner::mouseReleaseEvent(QMouseEvent *e) { - const auto pressed = base::take(_pressed); - setCursor((_selected || _pressed) - ? style::cur_pointer - : style::cur_default); - if (pressed && pressed == _selected) { - const auto textMethod = pressed->isMegagroup() - ? tr::lng_channels_too_much_public_revoke_confirm_group - : tr::lng_channels_too_much_public_revoke_confirm_channel; - const auto text = textMethod( - tr::now, - lt_link, - _session->createInternalLink(pressed->userName()), - lt_group, - pressed->name); - const auto confirmText = tr::lng_channels_too_much_public_revoke( - tr::now); - auto callback = crl::guard(this, [=](Fn &&close) { - if (_revokeRequestId) { - return; - } - _revokeRequestId = _api.request(MTPchannels_UpdateUsername( - pressed->asChannel()->inputChannel, - MTP_string() - )).done([=, close = std::move(close)] { - close(); - if (const auto callback = _revokeCallback) { - callback(); - } - }).send(); - }); - Ui::show( - Ui::MakeConfirmBox({ - .text = text, - .confirmed = std::move(callback), - .confirmText = confirmText, - }), - Ui::LayerOption::KeepOther); - } -} - -void RevokePublicLinkBox::Inner::paintEvent(QPaintEvent *e) { - Painter p(this); - p.translate(0, _rowsTop); - for (const auto &row : _rows) { - paintChat(p, row, (row.peer == _selected)); - p.translate(0, _rowHeight); - } -} - -void RevokePublicLinkBox::resizeEvent(QResizeEvent *e) { - BoxContent::resizeEvent(e); - - _aboutRevoke->moveToLeft(st::boxPadding.left(), st::boxPadding.top()); -} - -void RevokePublicLinkBox::Inner::paintChat( - Painter &p, - const ChatRow &row, - bool selected) const { - const auto peer = row.peer; - peer->paintUserpicLeft( - p, - row.userpic, - st::contactsPadding.left(), - st::contactsPadding.top(), - width(), - st::contactsPhotoSize); - - p.setPen(st::contactsNameFg); - - const auto namex = st::contactsPadding.left() - + st::contactsPhotoSize - + st::contactsPadding.left(); - auto namew = width() - - namex - - st::contactsPadding.right() - - (_revokeWidth + st::contactsCheckPosition.x() * 2); - - const auto badgeStyle = Ui::PeerBadgeStyle{ - &st::dialogsVerifiedIcon, - &st::attentionButtonFg - }; - namew -= Ui::DrawPeerBadgeGetWidth( - peer, - p, - QRect( - namex, - st::contactsPadding.top() + st::contactsNameTop, - row.name.maxWidth(), - st::contactsNameStyle.font->height), - namew, - width(), - badgeStyle); - row.name.drawLeftElided( - p, - namex, - st::contactsPadding.top() + st::contactsNameTop, - namew, - width()); - - p.setFont(selected ? st::linkOverFont : st::linkFont); - p.setPen(selected - ? st::defaultLinkButton.overColor - : st::defaultLinkButton.color); - p.drawTextRight( - st::contactsPadding.right() + st::contactsCheckPosition.x(), - st::contactsPadding.top() - + (st::contactsPhotoSize - st::normalFont->height) / 2, - width(), - tr::lng_channels_too_much_public_revoke(tr::now), - _revokeWidth); - - p.setPen(st::contactsStatusFg); - p.setTextPalette(st::revokePublicLinkStatusPalette); - row.status.drawLeftElided( - p, - namex, - st::contactsPadding.top() + st::contactsStatusTop, - namew, - width()); - p.restoreTextPalette(); -} diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index 35bd39bf2c998a..34bb6b6d6702b4 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -35,16 +35,12 @@ class LinkButton; class UserpicButton; } // namespace Ui -constexpr auto kMaxBioLength = 70; - enum class PeerFloodType { Send, InviteGroup, InviteChannel, }; -[[nodiscard]] style::InputField CreateBioFieldStyle(); - [[nodiscard]] TextWithEntities PeerFloodErrorText( not_null session, PeerFloodType type); @@ -249,28 +245,3 @@ class EditNameBox : public Ui::BoxContent { QString _sentName; }; - -class RevokePublicLinkBox final : public Ui::BoxContent { -public: - RevokePublicLinkBox( - QWidget*, - not_null session, - Fn revokeCallback); - -protected: - void prepare() override; - - void resizeEvent(QResizeEvent *e) override; - -private: - const not_null _session; - - object_ptr _aboutRevoke; - - class Inner; - QPointer _inner; - - int _innerTop = 0; - Fn _revokeCallback; - -}; diff --git a/Telegram/SourceFiles/boxes/auto_download_box.cpp b/Telegram/SourceFiles/boxes/auto_download_box.cpp index 5e8262a5a4d3da..6f2cf6f3cff971 100644 --- a/Telegram/SourceFiles/boxes/auto_download_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_download_box.cpp @@ -31,12 +31,12 @@ constexpr auto kDefaultAutoPlayLimit = 50 * kMegabyte; using Type = Data::AutoDownload::Type; -not_null AddSizeLimitSlider( +not_null AddSizeLimitSlider( not_null container, - const base::flat_map &values, - int defaultValue) { + const base::flat_map &values, + int64 defaultValue) { using namespace Settings; - using Pair = base::flat_map::value_type; + using Pair = base::flat_map::value_type; const auto limits = Ui::CreateChild>( container.get()); @@ -44,13 +44,13 @@ not_null AddSizeLimitSlider( values, std::less<>(), [](Pair pair) { return pair.second; })->second; - const auto initialLimit = currentLimit ? currentLimit : defaultValue; - const auto result = Ui::CreateChild(container.get(), initialLimit); + const auto startLimit = currentLimit ? currentLimit : defaultValue; + const auto result = Ui::CreateChild(container.get(), startLimit); AddButtonWithLabel( container, tr::lng_media_size_limit(), limits->events_starting_with_copy( - initialLimit + startLimit ) | rpl::map([](int value) { return tr::lng_media_size_up_to( tr::now, @@ -92,7 +92,7 @@ void AutoDownloadBox::setupContent() { using namespace Settings; using namespace Data::AutoDownload; using Type = Data::AutoDownload::Type; - using Pair = base::flat_map::value_type; + using Pair = base::flat_map::value_type; setTitle(tr::lng_profile_settings_section()); @@ -105,7 +105,7 @@ void AutoDownloadBox::setupContent() { std::move(wrap))); const auto add = [&]( - not_null*> values, + not_null*> values, Type type, rpl::producer label) { const auto value = settings->bytesLimit(_source, type); @@ -124,7 +124,7 @@ void AutoDownloadBox::setupContent() { AddSubsectionTitle(content, tr::lng_media_auto_title()); - const auto downloadValues = Ui::CreateChild>( + const auto downloadValues = Ui::CreateChild>( content); add(downloadValues, Type::Photo, tr::lng_media_photo_title()); add(downloadValues, Type::File, tr::lng_media_file_title()); @@ -137,7 +137,7 @@ void AutoDownloadBox::setupContent() { AddSkip(content); AddSubsectionTitle(content, tr::lng_media_auto_play()); - const auto autoPlayValues = Ui::CreateChild>( + const auto autoPlayValues = Ui::CreateChild>( content); add( autoPlayValues, diff --git a/Telegram/SourceFiles/boxes/auto_lock_box.cpp b/Telegram/SourceFiles/boxes/auto_lock_box.cpp index 49383558eb603f..70c09ec9effc41 100644 --- a/Telegram/SourceFiles/boxes/auto_lock_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_lock_box.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "boxes/auto_lock_box.h" #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_keys.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/time_input.h" diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 1ca6faf39b6674..d942e60d718748 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -258,7 +258,9 @@ void BackgroundPreviewBox::apply() { void BackgroundPreviewBox::share() { QGuiApplication::clipboard()->setText( _paper.shareUrl(&_controller->session())); - Ui::Toast::Show(tr::lng_background_link_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_background_link_copied(tr::now)); } void BackgroundPreviewBox::paintEvent(QPaintEvent *e) { diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 8c1456e842f1b5..dd8719298045d9 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1077,3 +1077,41 @@ ringtonesBoxButton: SettingsButton(defaultSettingsButton) { iconLeft: 25px; } ringtonesBoxSkip: 7px; + +premiumBubblePadding: margins(14px, 0px, 14px, 0px); +premiumBubblePenWidth: 6; +premiumBubbleHeight: 40px; +premiumBubbleSkip: 8px; +premiumBubbleWidthLimit: 80px; +premiumBubbleTextSkip: 3px; +premiumBubbleSlideDuration: 1000; +premiumBubbleTailSize: size(21px, 7px); +premiumBubbleFont: font(19px); +premiumLineTextSkip: 11px; +premiumInfographicPadding: margins(0px, 10px, 0px, 15px); + +premiumIconChats: icon {{ "limits/chats", settingsIconFg }}; +premiumIconFiles: icon {{ "limits/files", settingsIconFg }}; +premiumIconFolders: icon {{ "limits/folders", settingsIconFg }}; +premiumIconGroups: icon {{ "limits/groups", settingsIconFg }}; +premiumIconLinks: icon {{ "limits/links", settingsIconFg }}; +premiumIconPins: icon {{ "limits/pins", settingsIconFg }}; +premiumIconAccounts: icon {{ "limits/accounts", settingsIconFg }}; + +premiumAccountsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { + imageRadius: 27px; + imageSmallRadius: 23px; + check: RoundCheckbox(defaultRoundCheckbox) { + size: 0px; + } +} +premiumAccountsLabelSize: size(22px, 15px); +premiumAccountsLabelPadding: margins(2px, 2px, 2px, 2px); +premiumAccountsLabelRadius: 6; +premiumAccountsNameTop: 13px; +premiumAccountsPadding: margins(0px, 20px, 0px, 14px); +premiumAccountsHeight: 105px; + +gradientButtonGlareDuration: 700; +gradientButtonGlareTimeout: 2000; +gradientButtonGlareWidth: 100px; diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index d5efcde408b0eb..0471998dea6c5c 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "boxes/choose_filter_box.h" #include "apiwrap.h" +#include "boxes/premium_limits_box.h" #include "core/application.h" // primaryWindow #include "data/data_chat_filters.h" #include "data/data_session.h" @@ -56,6 +57,8 @@ void ChangeFilterById( FilterId filterId, not_null history, bool add) { + Expects(filterId != 0); + const auto list = history->owner().chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); if (i != end(list)) { @@ -98,7 +101,7 @@ ChooseFilterValidator::ChooseFilterValidator(not_null history) bool ChooseFilterValidator::canAdd() const { for (const auto &filter : _history->owner().chatsFilters().list()) { - if (!filter.contains(_history)) { + if (filter.id() && !filter.contains(_history)) { return true; } } @@ -106,6 +109,8 @@ bool ChooseFilterValidator::canAdd() const { } bool ChooseFilterValidator::canRemove(FilterId filterId) const { + Expects(filterId != 0); + const auto list = _history->owner().chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); if (i != end(list)) { @@ -116,6 +121,21 @@ bool ChooseFilterValidator::canRemove(FilterId filterId) const { return false; } +ChooseFilterValidator::LimitData ChooseFilterValidator::limitReached( + FilterId filterId) const { + Expects(filterId != 0); + + const auto list = _history->owner().chatsFilters().list(); + const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); + const auto limit = _history->owner().pinnedChatsLimit(nullptr, filterId); + return { + .reached = (i != end(list)) + && !ranges::contains(i->always(), _history) + && (i->always().size() >= limit), + .count = int(i->always().size()), + }; +} + void ChooseFilterValidator::add(FilterId filterId) const { ChangeFilterById(filterId, _history, true); } @@ -125,21 +145,30 @@ void ChooseFilterValidator::remove(FilterId filterId) const { } void FillChooseFilterMenu( + not_null controller, not_null menu, not_null history) { + const auto weak = base::make_weak(controller.get()); const auto validator = ChooseFilterValidator(history); for (const auto &filter : history->owner().chatsFilters().list()) { const auto id = filter.id(); + if (!id) { + continue; + } + const auto contains = filter.contains(history); const auto action = menu->addAction(filter.title(), [=] { if (filter.contains(history)) { if (validator.canRemove(id)) { validator.remove(id); } - } else { - if (validator.canAdd()) { - validator.add(id); - } + } else if (const auto r = validator.limitReached(id); r.reached) { + controller->show(Box( + FilterChatsLimitBox, + &controller->session(), + r.count)); + } else if (validator.canAdd()) { + validator.add(id); } }, contains ? &st::mediaPlayerMenuCheck : nullptr); action->setEnabled(contains diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index 03dac402caf6d3..8e32b267c87cde 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -11,14 +11,23 @@ namespace Ui { class PopupMenu; } // namespace Ui +namespace Window { +class SessionController; +} // namespace Window + class History; class ChooseFilterValidator final { public: ChooseFilterValidator(not_null history); + struct LimitData { + const bool reached = false; + const int count = 0; + }; [[nodiscard]] bool canAdd() const; [[nodiscard]] bool canRemove(FilterId filterId) const; + [[nodiscard]] LimitData limitReached(FilterId filterId) const; void add(FilterId filterId) const; void remove(FilterId filterId) const; @@ -29,5 +38,6 @@ class ChooseFilterValidator final { }; void FillChooseFilterMenu( + not_null controller, not_null menu, not_null history); diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 8dd8db93b70f27..ab8dd8ee48c205 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -1264,6 +1264,7 @@ object_ptr ProxiesBoxController::CreateOwningBox( object_ptr ProxiesBoxController::create() { auto result = Box(this, _settings); + _toastParent = Ui::BoxShow(result.data()).toastParent(); for (const auto &item : _list) { updateView(item); } @@ -1549,7 +1550,9 @@ void ProxiesBoxController::share(const ProxyData &proxy) { + ((proxy.type == Type::Mtproto && !proxy.password.isEmpty()) ? "&secret=" + proxy.password : ""); QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show(tr::lng_username_copied(tr::now)); + if (_toastParent) { + Ui::Toast::Show(_toastParent, tr::lng_username_copied(tr::now)); + } } ProxiesBoxController::~ProxiesBoxController() { diff --git a/Telegram/SourceFiles/boxes/connection_box.h b/Telegram/SourceFiles/boxes/connection_box.h index 26244fe50bc42f..566a43421f1639 100644 --- a/Telegram/SourceFiles/boxes/connection_box.h +++ b/Telegram/SourceFiles/boxes/connection_box.h @@ -117,6 +117,7 @@ class ProxiesBoxController { rpl::event_stream _views; base::Timer _saveTimer; rpl::event_stream _proxySettingsChanges; + QPointer _toastParent; ProxyData _lastSelectedProxy; bool _lastSelectedProxyUsed = false; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index d46f73a964f536..8c4cd47a525113 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -18,7 +18,6 @@ For license and copyright information please follow this link: #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" -#include "ui/toast/toast.h" #include "ui/text/text_utilities.h" #include "main/main_session.h" #include "core/application.h" @@ -776,7 +775,7 @@ void CreatePollBox::setInnerFocus() { } void CreatePollBox::submitFailed(const QString &error) { - Ui::Toast::Show(error); + Ui::Toast::Show(Ui::BoxShow(this).toastParent(), error); } not_null CreatePollBox::setupQuestion( @@ -990,8 +989,11 @@ object_ptr CreatePollBox::setupContent() { multiple->events( ) | rpl::filter([=](not_null e) { return (e->type() == QEvent::MouseButtonPress) && quiz->checked(); - }) | rpl::start_with_next([=] { - Ui::Toast::Show(tr::lng_polls_create_one_answer(tr::now)); + }) | rpl::start_with_next([ + toastParent = Ui::BoxShow(this).toastParent()] { + Ui::Toast::Show( + toastParent, + tr::lng_polls_create_one_answer(tr::now)); }, multiple->lifetime()); } @@ -1068,8 +1070,10 @@ object_ptr CreatePollBox::setupContent() { *error &= ~Error::Solution; } }; - const auto showError = [](tr::phrase<> text) { - Ui::Toast::Show(text(tr::now)); + const auto showError = [ + toastParent = Ui::BoxShow(this).toastParent()]( + tr::phrase<> text) { + Ui::Toast::Show(toastParent, text(tr::now)); }; const auto send = [=](Api::SendOptions sendOptions) { collectError(); diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index b828281c4352ae..294a265eb52d16 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "api/api_chat_participants.h" #include "api/api_messages_search.h" #include "base/unixtime.h" +#include "core/application.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_histories.h" @@ -26,8 +27,6 @@ For license and copyright information please follow this link: #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/labels.h" -#include "window/window_session_controller.h" -#include "facades.h" // Ui::showChatsList #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -493,11 +492,7 @@ void DeleteMessagesBox::deleteAndClear() { if (justClear) { session->api().clearHistory(peer, revoke); } else { - for (const auto &controller : session->windows()) { - if (controller->activeChatCurrent().peer() == peer) { - Ui::showChatsList(session); - } - } + Core::App().closeChatFromWindows(peer); // Don't delete old history by default, // because Android app doesn't. // diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp index add88f3a42e495..72cfd29c0d2eb7 100644 --- a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp +++ b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp @@ -12,6 +12,8 @@ For license and copyright information please follow this link: #include "base/event_filter.h" #include "chat_helpers/spellchecker_common.h" #include "core/application.h" +#include "core/core_settings.h" +#include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_session.h" #include "mainwidget.h" diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index afde2a5a44bcca..33df2cd86feef1 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "api/api_text_entities.h" #include "apiwrap.h" #include "base/event_filter.h" +#include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/tabbed_panel.h" @@ -22,6 +23,8 @@ For license and copyright information please follow this link: #include "data/data_document.h" #include "data/data_photo_media.h" #include "data/data_session.h" +#include "data/data_user.h" +#include "data/data_premium_limits.h" #include "editor/photo_editor_layer_widget.h" #include "history/history_drag_area.h" #include "history/history_item.h" @@ -32,6 +35,7 @@ For license and copyright information please follow this link: #include "platform/platform_specific.h" #include "storage/localimageloader.h" // SendMediaType #include "storage/storage_media_prepare.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/attach/attach_item_single_file_preview.h" #include "ui/chat/attach/attach_item_single_media_preview.h" #include "ui/chat/attach/attach_single_file_preview.h" @@ -56,13 +60,14 @@ For license and copyright information please follow this link: namespace { -auto ListFromMimeData(not_null data) { +auto ListFromMimeData(not_null data, bool premium) { using Error = Ui::PreparedList::Error; auto result = data->hasUrls() ? Storage::PrepareMediaList( // When we edit media, we need only 1 file. data->urls().mid(0, 1), - st::sendMediaPreviewSize) + st::sendMediaPreviewSize, + premium) : Ui::PreparedList(Error::EmptyFile, QString()); if (result.error == Error::None) { return result; @@ -238,8 +243,6 @@ void EditCaptionBox::rebuildPreview() { void EditCaptionBox::setupField() { const auto show = std::make_shared(_controller); const auto session = &_controller->session(); - _field->setMaxLength( - _controller->session().serverConfig().captionLengthMax); _field->setSubmitSettings( Core::App().settings().sendSubmitWay()); _field->setInstantReplaces(Ui::InstantReplaces::Default()); @@ -322,9 +325,10 @@ void EditCaptionBox::setupControls() { } void EditCaptionBox::setupEditEventHandler() { + const auto toastParent = Ui::BoxShow(this).toastParent(); const auto callback = [=](FileDialog::OpenResult &&result) { - auto showError = [](tr::phrase<> t) { - Ui::Toast::Show(t(tr::now)); + auto showError = [toastParent](tr::phrase<> t) { + Ui::Toast::Show(toastParent, t(tr::now)); }; const auto checkResult = [=](const Ui::PreparedList &list) { @@ -343,11 +347,13 @@ void EditCaptionBox::setupEditEventHandler() { } return true; }; + const auto premium = _controller->session().premium(); auto list = Storage::PreparedFileFromFilesDialog( std::move(result), checkResult, showError, - st::sendMediaPreviewSize); + st::sendMediaPreviewSize, + premium); if (list) { setPreparedList(std::move(*list)); @@ -522,7 +528,8 @@ void EditCaptionBox::updateEmojiPanelGeometry() { } bool EditCaptionBox::fileFromClipboard(not_null data) { - return setPreparedList(ListFromMimeData(data)); + const auto premium = _controller->session().premium(); + return setPreparedList(ListFromMimeData(data, premium)); } bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { @@ -544,7 +551,9 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { } } if (invalidForAlbum) { - Ui::Toast::Show(tr::lng_edit_media_album_error(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_edit_media_album_error(tr::now)); return false; } _preparedList = std::move(list); @@ -645,6 +654,17 @@ void EditCaptionBox::setInnerFocus() { _field->setFocusFast(); } +bool EditCaptionBox::validateLength(const QString &text) const { + const auto session = &_controller->session(); + const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); + const auto remove = int(text.size()) - limit; + if (remove <= 0) { + return true; + } + _controller->show(Box(CaptionLimitReachedBox, session, remove)); + return false; +} + void EditCaptionBox::save() { if (_saveRequestId) { return; @@ -659,6 +679,9 @@ void EditCaptionBox::save() { } const auto textWithTags = _field->getTextWithAppliedMarkdown(); + if (!validateLength(textWithTags.text)) { + return; + } const auto sending = TextWithEntities{ textWithTags.text, TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index ce9b68acc9767d..554c9bd8b0051e 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -62,6 +62,7 @@ class EditCaptionBox final : public Ui::BoxContent { void setupDragArea(); + bool validateLength(const QString &text) const; void save(); bool fileFromClipboard(not_null data); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 127cb07bb1a991..27acc6bf128820 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -18,6 +18,7 @@ For license and copyright information please follow this link: #include "ui/filter_icon_panel.h" #include "data/data_chat_filters.h" #include "data/data_peer.h" +#include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_session.h" #include "core/application.h" #include "core/core_settings.h" @@ -504,6 +505,12 @@ void EditFilterBox( box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit()); box->setCloseByOutsideClick(false); + Data::AmPremiumValue( + &window->session() + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + using State = rpl::variable; const auto data = box->lifetime().make_state(filter); @@ -668,6 +675,8 @@ void EditFilterBox( void EditExistingFilter( not_null window, FilterId id) { + Expects(id != 0); + const auto session = &window->session(); const auto &list = session->data().chatsFilters().list(); const auto i = ranges::find(list, id, &Data::ChatFilter::id); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index be1a11885399d6..60c7cc82b17921 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -9,17 +9,20 @@ For license and copyright information please follow this link: #include "history/history.h" #include "window/window_session_controller.h" +#include "boxes/premium_limits_box.h" #include "lang/lang_keys.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" +#include "main/main_app_config.h" +#include "main/main_account.h" +#include "main/main_session.h" #include "base/object_ptr.h" +#include "data/data_user.h" #include "styles/style_window.h" #include "styles/style_boxes.h" namespace { -constexpr auto kMaxExceptions = 100; - using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; @@ -93,6 +96,22 @@ class TypeController final : public PeerListController { return PeerId(FakeChatId(static_cast(flag))).value; } +[[nodiscard]] int Limit( + not_null session, + const QString &key, + double fallback) { + return session->account().appConfig().get(key, fallback); +} + +[[nodiscard]] int Limit(not_null session) { + const auto premium = session->premium(); + return Limit(session, + (premium + ? "dialog_filters_chats_limit_premium" + : "dialog_filters_chats_limit_default"), + premium ? 200 : 100); +} + TypeRow::TypeRow(Flag flag) : PeerListRow(TypeId(flag)) { } @@ -302,18 +321,35 @@ EditFilterChatsListController::EditFilterChatsListController( , _title(std::move(title)) , _peers(peers) , _options(options) -, _selected(selected) { +, _selected(selected) +, _limit(Limit(session)) { } Main::Session &EditFilterChatsListController::session() const { return *_session; } +int EditFilterChatsListController::selectedTypesCount() const { + Expects(_typesDelegate != nullptr); + + auto result = 0; + for (auto i = 0; i != _typesDelegate->peerListFullRowsCount(); ++i) { + if (_typesDelegate->peerListRowAt(i)->checked()) { + ++result; + } + } + return result; +} + void EditFilterChatsListController::rowClicked(not_null row) { - const auto count = delegate()->peerListSelectedRowsCount(); - if (count < kMaxExceptions || row->checked()) { + const auto count = delegate()->peerListSelectedRowsCount() + - selectedTypesCount(); + if (count < _limit || row->checked()) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); + } else { + delegate()->peerListShowBox( + Box(FilterChatsLimitBox, _session, count)); } } @@ -363,7 +399,7 @@ object_ptr EditFilterChatsListController::prepareTypesList() { container->add(object_ptr( container, st::membersMarginTop)); - const auto delegate = container->lifetime().make_state< + _typesDelegate = container->lifetime().make_state< PeerListContentDelegateSimple >(); const auto controller = container->lifetime().make_state( @@ -374,11 +410,11 @@ object_ptr EditFilterChatsListController::prepareTypesList() { const auto content = result->add(object_ptr( container, controller)); - delegate->setContent(content); - controller->setDelegate(delegate); + _typesDelegate->setContent(content); + controller->setDelegate(_typesDelegate); for (const auto flag : kAllTypes) { if (_selected & flag) { - if (const auto row = delegate->peerListFindRow(TypeId(flag))) { + if (const auto row = _typesDelegate->peerListFindRow(TypeId(flag))) { content->changeCheckState(row, true, anim::type::instant); this->delegate()->peerListSetForeignRowChecked( row, @@ -408,8 +444,8 @@ object_ptr EditFilterChatsListController::prepareTypesList() { }, _lifetime); _deselectOption = [=](PeerListRowId itemId) { - if (const auto row = delegate->peerListFindRow(itemId)) { - delegate->peerListSetRowChecked(row, false); + if (const auto row = _typesDelegate->peerListFindRow(itemId)) { + _typesDelegate->peerListSetRowChecked(row, false); } }; @@ -424,13 +460,8 @@ auto EditFilterChatsListController::createRow(not_null history) } void EditFilterChatsListController::updateTitle() { - auto types = 0; - for (const auto flag : kAllTypes) { - if (_selected & flag) { - ++types; - } - } - const auto count = delegate()->peerListSelectedRowsCount() - types; - const auto additional = qsl("%1 / %2").arg(count).arg(kMaxExceptions); + const auto count = delegate()->peerListSelectedRowsCount() + - selectedTypesCount(); + const auto additional = qsl("%1 / %2").arg(count).arg(_limit); delegate()->peerListSetAdditionalTitle(rpl::single(additional)); } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index dd39baf9cc2354..5302480c3a55df 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -62,6 +62,7 @@ class EditFilterChatsListController final : public ChatsListBoxController { bool handleDeselectForeignRow(PeerListRowId itemId) override; private: + int selectedTypesCount() const; void prepareViewHook() override; std::unique_ptr createRow(not_null history) override; [[nodiscard]] object_ptr prepareTypesList(); @@ -73,9 +74,12 @@ class EditFilterChatsListController final : public ChatsListBoxController { base::flat_set> _peers; Flags _options; Flags _selected; + int _limit = 0; Fn _deselectOption; + PeerListContentDelegate *_typesDelegate = nullptr; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/boxes/max_invite_box.cpp b/Telegram/SourceFiles/boxes/max_invite_box.cpp index 88be91f046fd32..c9408aaec683ed 100644 --- a/Telegram/SourceFiles/boxes/max_invite_box.cpp +++ b/Telegram/SourceFiles/boxes/max_invite_box.cpp @@ -90,7 +90,9 @@ void MaxInviteBox::mousePressEvent(QMouseEvent *e) { if (_linkOver) { if (!_channel->inviteLink().isEmpty()) { QGuiApplication::clipboard()->setText(_channel->inviteLink()); - Ui::Toast::Show(tr::lng_create_channel_link_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_create_channel_link_copied(tr::now)); } else if (_channel->isFullLoaded() && !_creatingInviteLink) { _creatingInviteLink = true; _channel->session().api().inviteLinks().create(_channel); diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 4edf1738f36756..d17c6bac15982a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -480,10 +480,12 @@ std::unique_ptr ContactsBoxController::createRow( ChooseRecipientBoxController::ChooseRecipientBoxController( not_null session, - FnMut)> callback) + FnMut)> callback, + Fn)> filter) : ChatsListBoxController(session) , _session(session) -, _callback(std::move(callback)) { +, _callback(std::move(callback)) +, _filter(std::move(filter)) { } Main::Session &ChooseRecipientBoxController::session() const { @@ -506,7 +508,9 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { auto ChooseRecipientBoxController::createRow( not_null history) -> std::unique_ptr { const auto peer = history->peer; - const auto skip = (peer->isBroadcast() && !peer->canWrite()) - || peer->isRepliesChat(); + const auto skip = _filter + ? !_filter(peer) + : ((peer->isBroadcast() && !peer->canWrite()) + || peer->isRepliesChat()); return skip ? nullptr : std::make_unique(history); } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 3422c5ad6a1167..f8d58090229597 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -173,7 +173,8 @@ class ChooseRecipientBoxController public: ChooseRecipientBoxController( not_null session, - FnMut)> callback); + FnMut)> callback, + Fn)> filter = nullptr); Main::Session &session() const override; void rowClicked(not_null row) override; @@ -189,5 +190,6 @@ class ChooseRecipientBoxController private: const not_null _session; FnMut)> _callback; + Fn)> _filter; }; diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp index 6509064f846605..014c10036d5024 100644 --- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp @@ -24,6 +24,7 @@ For license and copyright information please follow this link: #include "base/random.h" #include "base/weak_ptr.h" #include "api/api_chat_participants.h" +#include "window/window_session_controller.h" #include "apiwrap.h" #include "facades.h" #include "styles/style_boxes.h" @@ -91,8 +92,6 @@ void ShareBotGame( ).send(); return history->sendRequestId; }); - Ui::hideLayer(); - Ui::showPeerHistory(chat, ShowAtUnreadMsgId); } Controller::Controller( @@ -144,6 +143,7 @@ void Controller::addRow(not_null peer) { } // namespace void AddBotToGroupBoxController::Start( + not_null controller, not_null bot, Scope scope, const QString &token, @@ -151,8 +151,9 @@ void AddBotToGroupBoxController::Start( auto initBox = [=](not_null box) { box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }; - Ui::show(Box( + controller->show(Box( std::make_unique( + controller, bot, scope, token, @@ -161,6 +162,7 @@ void AddBotToGroupBoxController::Start( } AddBotToGroupBoxController::AddBotToGroupBoxController( + not_null controller, not_null bot, Scope scope, const QString &token, @@ -168,6 +170,7 @@ AddBotToGroupBoxController::AddBotToGroupBoxController( : ChatsListBoxController((scope == Scope::ShareGame) ? std::make_unique(&bot->session()) : nullptr) +, _controller(controller) , _bot(bot) , _scope(scope) , _token(token) @@ -192,8 +195,15 @@ void AddBotToGroupBoxController::rowClicked(not_null row) { } void AddBotToGroupBoxController::shareBotGame(not_null chat) { - auto send = crl::guard(this, [bot = _bot, chat, token = _token] { + auto send = crl::guard(this, [ + bot = _bot, + controller = _controller, + chat, + token = _token] { ShareBotGame(bot, chat, token); + using Way = Window::SectionShow::Way; + controller->hideLayer(); + controller->showPeerHistory(chat, Way::ClearStack, ShowAtUnreadMsgId); }); auto confirmText = [chat] { if (chat->isUser()) { @@ -201,7 +211,7 @@ void AddBotToGroupBoxController::shareBotGame(not_null chat) { } return tr::lng_bot_sure_share_game_group(tr::now, lt_group, chat->name); }(); - Ui::show( + _controller->show( Ui::MakeConfirmBox({ .text = confirmText, .confirmed = std::move(send), @@ -240,7 +250,7 @@ void AddBotToGroupBoxController::requestExistingRights( void AddBotToGroupBoxController::addBotToGroup(not_null chat) { if (const auto megagroup = chat->asMegagroup()) { if (!megagroup->canAddMembers()) { - Ui::show( + _controller->show( Ui::MakeInformBox(tr::lng_error_cant_add_member()), Ui::LayerOption::KeepOther); return; @@ -261,9 +271,11 @@ void AddBotToGroupBoxController::addBotToGroup(not_null chat) { return; } const auto bot = _bot; + const auto controller = _controller; const auto close = [=](auto&&...) { - Ui::hideLayer(); - Ui::showPeerHistory(chat, ShowAtUnreadMsgId); + using Way = Window::SectionShow::Way; + controller->hideLayer(); + controller->showPeerHistory(chat, Way::ClearStack, ShowAtUnreadMsgId); }; const auto rights = requestedAddAdmin ? _requestedRights @@ -300,12 +312,16 @@ void AddBotToGroupBoxController::addBotToGroup(not_null chat) { _token, _existingRights.value_or(ChatAdminRights()) }); box->setSaveCallback(saveCallback); - Ui::show(std::move(box), Ui::LayerOption::KeepOther); + controller->show(std::move(box), Ui::LayerOption::KeepOther); } else { - Ui::show( + auto callback = crl::guard(this, [=] { + AddBotToGroup(bot, chat, _token); + controller->hideLayer(); + }); + controller->show( Ui::MakeConfirmBox({ tr::lng_bot_sure_invite(tr::now, lt_group, chat->name), - crl::guard(this, [=] { AddBotToGroup(bot, chat, _token); }), + std::move(callback), }), Ui::LayerOption::KeepOther); } @@ -469,6 +485,5 @@ void AddBotToGroup( } else { chat->session().api().chatParticipants().add(chat, { 1, bot }); } - Ui::hideLayer(); Ui::showPeerHistory(chat, ShowAtUnreadMsgId); } diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h index 51f6cbbfeb2056..f048ee0bd1edb6 100644 --- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h +++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.h @@ -22,12 +22,14 @@ class AddBotToGroupBoxController All, }; static void Start( + not_null controller, not_null bot, Scope scope = Scope::All, const QString &token = QString(), ChatAdminRights requestedRights = {}); AddBotToGroupBoxController( + not_null controller, not_null bot, Scope scope, const QString &token, @@ -56,6 +58,7 @@ class AddBotToGroupBoxController void addBotToGroup(not_null chat); void requestExistingRights(not_null channel); + const not_null _controller; const not_null _bot; const Scope _scope = Scope::None; const QString _token; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 96e09b80983c58..c8d39858e96c9e 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -199,10 +199,18 @@ void AddParticipantsBoxController::addInviteLinkButton() { tr::lng_profile_add_via_link(), st::inviteViaLinkButton), style::margins(0, st::membersMarginTop, 0, 0)); - object_ptr( + + const auto icon = Ui::CreateChild( button->entity(), st::inviteViaLinkIcon, - st::inviteViaLinkIconPosition); + QPoint()); + button->entity()->heightValue( + ) | rpl::start_with_next([=](int height) { + icon->moveToLeft( + st::inviteViaLinkIconPosition.x(), + (height - st::inviteViaLinkIcon.height()) / 2); + }, icon->lifetime()); + button->entity()->setClickedCallback([=] { showBox(Box(_peer)); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 342433935e9464..bcf9c0efeeb1a9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -384,6 +384,7 @@ void EditAdminBox::prepare() { return; } else if (_addAsAdmin && !_addAsAdmin->checked()) { AddBotToGroup(user(), peer(), _addingBot->token); + getDelegate()->hideLayer(); return; } else if (_addingBot && !_addingBot->existing) { const auto phrase = peer()->isBroadcast() diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index ff2a36f7e1a5d1..fcaf8dbf433b24 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -30,6 +30,7 @@ For license and copyright information please follow this link: #include "base/unixtime.h" #include "ui/widgets/popup_menu.h" #include "ui/ui_utility.h" +#include "info/profile/info_profile_values.h" #include "window/window_session_controller.h" #include "history/history.h" #include "styles/style_menu_icons.h" @@ -1930,8 +1931,19 @@ auto ParticipantsBoxController::computeType( : (user && _additional.adminRights(user).has_value()) ? Rights::Admin : Rights::Normal; - // result.canRemove = _additional.canRemoveParticipant(participant); result.adminRank = user ? _additional.adminRank(user) : QString(); + using Badge = Info::Profile::Badge; + result.badge = !user + ? Badge::None + : user->isScam() + ? Badge::Scam + : user->isFake() + ? Badge::Fake + : user->isVerified() + ? Badge::Verified + : (user->isPremium() && participant->session().premiumPossible()) + ? Badge::Premium + : Badge::None; return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index d9bc84af769a9e..df5ee031605a15 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -53,7 +53,6 @@ For license and copyright information please follow this link: #include "window/window_session_controller.h" #include "info/profile/info_profile_icon.h" #include "api/api_invite_links.h" -#include "facades.h" // Ui::showChatsList #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_info.h" @@ -269,6 +268,8 @@ class Controller : public base::has_weak_ptr { std::optional hiddenPreHistory; std::optional signatures; std::optional noForwards; + std::optional joinToWrite; + std::optional requestToJoin; std::optional linkedChat; }; @@ -306,6 +307,8 @@ class Controller : public base::has_weak_ptr { [[nodiscard]] bool validateHistoryVisibility(Saving &to) const; [[nodiscard]] bool validateSignatures(Saving &to) const; [[nodiscard]] bool validateForwards(Saving &to) const; + [[nodiscard]] bool validateJoinToWrite(Saving &to) const; + [[nodiscard]] bool validateRequestToJoin(Saving &to) const; void save(); void saveUsername(); @@ -315,6 +318,8 @@ class Controller : public base::has_weak_ptr { void saveHistoryVisibility(); void saveSignatures(); void saveForwards(); + void saveJoinToWrite(); + void saveRequestToJoin(); void savePhoto(); void pushSaveStage(FnMut &&lambda); void continueSave(); @@ -329,14 +334,12 @@ class Controller : public base::has_weak_ptr { void subscribeToMigration(); void migrate(not_null channel); - std::optional _privacySavedValue; std::optional _linkedChatSavedValue; ChannelData *_linkedChatOriginalValue = nullptr; bool _channelHasLocationOriginalValue = false; std::optional _historyVisibilitySavedValue; - std::optional _usernameSavedValue; + std::optional _typeDataSavedValue; std::optional _signaturesSavedValue; - std::optional _noForwardsSavedValue; const not_null _navigation; const not_null _box; @@ -602,8 +605,10 @@ void Controller::refreshHistoryVisibility() { if (!_controls.historyVisibilityWrap) { return; } + const auto withUsername = _typeDataSavedValue + && (_typeDataSavedValue->privacy == Privacy::HasUsername); _controls.historyVisibilityWrap->toggle( - (_privacySavedValue != Privacy::HasUsername + (withUsername && !_channelHasLocationOriginalValue && (!_linkedChatSavedValue || !*_linkedChatSavedValue)), anim::type::instant); @@ -611,22 +616,20 @@ void Controller::refreshHistoryVisibility() { void Controller::showEditPeerTypeBox( std::optional> error) { - const auto boxCallback = crl::guard(this, [=]( - Privacy checked, QString publicLink, bool noForwards) { - _privacyTypeUpdates.fire(std::move(checked)); - _privacySavedValue = checked; - _usernameSavedValue = publicLink; - _noForwardsSavedValue = noForwards; + const auto boxCallback = crl::guard(this, [=](EditPeerTypeData data) { + _privacyTypeUpdates.fire_copy(data.privacy); + _typeDataSavedValue = data; refreshHistoryVisibility(); }); + _typeDataSavedValue->hasLinkedChat + = (_linkedChatSavedValue.value_or(nullptr) != nullptr); _navigation->parentController()->show( Box( + _navigation, _peer, _channelHasLocationOriginalValue, boxCallback, - _privacySavedValue, - _usernameSavedValue, - _noForwardsSavedValue, + _typeDataSavedValue, error), Ui::LayerOption::KeepOther); } @@ -698,12 +701,20 @@ void Controller::fillPrivacyTypeButton() { // Create Privacy Button. const auto hasLocation = _peer->isChannel() && _peer->asChannel()->hasLocation(); - _privacySavedValue = (_peer->isChannel() - && _peer->asChannel()->hasUsername()) - ? Privacy::HasUsername - : Privacy::NoUsername; - _noForwardsSavedValue = !_peer->allowsForwarding(); - + _typeDataSavedValue = EditPeerTypeData{ + .privacy = ((_peer->isChannel() + && _peer->asChannel()->hasUsername()) + ? Privacy::HasUsername + : Privacy::NoUsername), + .username = (_peer->isChannel() + ? _peer->asChannel()->username + : QString()), + .noForwards = !_peer->allowsForwarding(), + .joinToWrite = (_peer->isMegagroup() + && _peer->asChannel()->joinToWrite()), + .requestToJoin = (_peer->isMegagroup() + && _peer->asChannel()->requestToJoin()), + }; const auto isGroup = (_peer->isChat() || _peer->isMegagroup()); const auto icon = isGroup ? &st::settingsIconGroup @@ -732,7 +743,7 @@ void Controller::fillPrivacyTypeButton() { [=] { showEditPeerTypeBox(); }, { icon, Settings::kIconLightBlue }); - _privacyTypeUpdates.fire_copy(*_privacySavedValue); + _privacyTypeUpdates.fire_copy(_typeDataSavedValue->privacy); } void Controller::fillLinkedChatButton() { @@ -1060,9 +1071,9 @@ void Controller::fillManageSection() { }, { &st::infoRoundedIconInviteLinks, Settings::kIconLightOrange }); - if (_privacySavedValue) { + if (_typeDataSavedValue) { _privacyTypeUpdates.events_starting_with_copy( - *_privacySavedValue + _typeDataSavedValue->privacy ) | rpl::start_with_next([=](Privacy flag) { wrap->toggle( flag != Privacy::HasUsername, @@ -1217,24 +1228,22 @@ std::optional Controller::validate() const { && validateDescription(result) && validateHistoryVisibility(result) && validateSignatures(result) - && validateForwards(result)) { + && validateForwards(result) + && validateJoinToWrite(result) + && validateRequestToJoin(result)) { return result; } return {}; } bool Controller::validateUsername(Saving &to) const { - if (!_privacySavedValue) { + if (!_typeDataSavedValue) { return true; - } else if (_privacySavedValue != Privacy::HasUsername) { + } else if (_typeDataSavedValue->privacy != Privacy::HasUsername) { to.username = QString(); return true; } - const auto username = _usernameSavedValue.value_or( - _peer->isChannel() - ? _peer->asChannel()->username - : QString() - ); + const auto username = _typeDataSavedValue->username; if (username.isEmpty()) { return false; } @@ -1276,7 +1285,8 @@ bool Controller::validateHistoryVisibility(Saving &to) const { if (!_controls.historyVisibilityWrap || !_controls.historyVisibilityWrap->toggled() || _channelHasLocationOriginalValue - || (_privacySavedValue == Privacy::HasUsername)) { + || (_typeDataSavedValue + && _typeDataSavedValue->privacy == Privacy::HasUsername)) { return true; } to.hiddenPreHistory @@ -1293,10 +1303,26 @@ bool Controller::validateSignatures(Saving &to) const { } bool Controller::validateForwards(Saving &to) const { - if (!_noForwardsSavedValue.has_value()) { + if (!_typeDataSavedValue) { return true; } - to.noForwards = _noForwardsSavedValue; + to.noForwards = _typeDataSavedValue->noForwards; + return true; +} + +bool Controller::validateJoinToWrite(Saving &to) const { + if (!_typeDataSavedValue) { + return true; + } + to.joinToWrite = _typeDataSavedValue->joinToWrite; + return true; +} + +bool Controller::validateRequestToJoin(Saving &to) const { + if (!_typeDataSavedValue) { + return true; + } + to.requestToJoin = _typeDataSavedValue->requestToJoin; return true; } @@ -1315,6 +1341,8 @@ void Controller::save() { pushSaveStage([=] { saveHistoryVisibility(); }); pushSaveStage([=] { saveSignatures(); }); pushSaveStage([=] { saveForwards(); }); + pushSaveStage([=] { saveJoinToWrite(); }); + pushSaveStage([=] { saveRequestToJoin(); }); pushSaveStage([=] { savePhoto(); }); continueSave(); } @@ -1591,6 +1619,50 @@ void Controller::saveForwards() { }).send(); } +void Controller::saveJoinToWrite() { + const auto joinToWrite = _peer->isMegagroup() + && _peer->asChannel()->joinToWrite(); + if (!_savingData.joinToWrite + || *_savingData.joinToWrite == joinToWrite) { + return continueSave(); + } + _api.request(MTPchannels_ToggleJoinToSend( + _peer->asChannel()->inputChannel, + MTP_bool(*_savingData.joinToWrite) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + continueSave(); + }).fail([=](const MTP::Error &error) { + if (error.type() == qstr("CHAT_NOT_MODIFIED")) { + continueSave(); + } else { + cancelSave(); + } + }).send(); +} + +void Controller::saveRequestToJoin() { + const auto requestToJoin = _peer->isMegagroup() + && _peer->asChannel()->requestToJoin(); + if (!_savingData.requestToJoin + || *_savingData.requestToJoin == requestToJoin) { + return continueSave(); + } + _api.request(MTPchannels_ToggleJoinRequest( + _peer->asChannel()->inputChannel, + MTP_bool(*_savingData.requestToJoin) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + continueSave(); + }).fail([=](const MTP::Error &error) { + if (error.type() == qstr("CHAT_NOT_MODIFIED")) { + continueSave(); + } else { + cancelSave(); + } + }).send(); +} + void Controller::savePhoto() { auto image = _controls.photo ? _controls.photo->takeResultImage() @@ -1629,8 +1701,8 @@ void Controller::deleteChannel() { const auto session = &_peer->session(); - Ui::hideLayer(); - Ui::showChatsList(session); + _navigation->parentController()->hideLayer(); + Core::App().closeChatFromWindows(_peer); if (chat) { session->api().deleteConversation(chat, false); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 496680089676a7..34004a09cd21a7 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -66,7 +66,9 @@ void ShowPeerInfoSync(not_null peer) { // we can safely use activeWindow. if (const auto window = Core::App().activeWindow()) { if (const auto controller = window->sessionController()) { - controller->showPeerInfo(peer); + if (&controller->session() == &peer->session()) { + controller->showPeerInfo(peer); + } } } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index e6d6ad2e7fddf4..7609d14f7341bb 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "boxes/add_contact_box.h" #include "ui/boxes/confirm_box.h" +#include "boxes/premium_limits_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_common.h" @@ -48,20 +49,19 @@ namespace { class Controller : public base::has_weak_ptr { public: Controller( + Window::SessionNavigation *navigation, std::shared_ptr show, not_null container, not_null peer, bool useLocationPhrases, - std::optional privacySavedValue, - std::optional usernameSavedValue, - std::optional noForwardsSavedValue); + std::optional dataSavedValue); void createContent(); [[nodiscard]] QString getUsernameInput() const; void setFocusUsername(); [[nodiscard]] rpl::producer getTitle() const { - return !_privacySavedValue + return !_dataSavedValue ? tr::lng_create_invite_link_title() : _isGroup ? tr::lng_manage_peer_group_type() @@ -79,6 +79,12 @@ class Controller : public base::has_weak_ptr { [[nodiscard]] bool noForwards() const { return _controls.noForwards->toggled(); } + [[nodiscard]] bool joinToWrite() const { + return _controls.joinToWrite && _controls.joinToWrite->toggled(); + } + [[nodiscard]] bool requestToJoin() const { + return _controls.requestToJoin && _controls.requestToJoin->toggled(); + } void showError(rpl::producer text) { _controls.usernameInput->showError(); @@ -93,10 +99,13 @@ class Controller : public base::has_weak_ptr { base::unique_qptr usernameResult; const style::FlatLabel *usernameResultStyle = nullptr; - Ui::SlideWrap *inviteLinkWrap = nullptr; + Ui::SlideWrap<> *inviteLinkWrap = nullptr; Ui::FlatLabel *inviteLink = nullptr; + Ui::SlideWrap *whoSendWrap = nullptr; Ui::SettingsButton *noForwards = nullptr; + Ui::SettingsButton *joinToWrite = nullptr; + Ui::SettingsButton *requestToJoin = nullptr; }; Controls _controls; @@ -124,15 +133,14 @@ class Controller : public base::has_weak_ptr { const QString &text, rpl::producer about); + Window::SessionNavigation *_navigation = nullptr; std::shared_ptr _show; not_null _peer; bool _linkOnly = false; MTP::Sender _api; - std::optional _privacySavedValue; - std::optional _usernameSavedValue; - std::optional _noForwardsSavedValue; + std::optional _dataSavedValue; bool _useLocationPhrases = false; bool _isGroup = false; @@ -149,24 +157,23 @@ class Controller : public base::has_weak_ptr { }; Controller::Controller( + Window::SessionNavigation *navigation, std::shared_ptr show, not_null container, not_null peer, bool useLocationPhrases, - std::optional privacySavedValue, - std::optional usernameSavedValue, - std::optional noForwardsSavedValue) -: _show(show) + std::optional dataSavedValue) +: _navigation(navigation) +, _show(show) , _peer(peer) -, _linkOnly(!privacySavedValue.has_value()) +, _linkOnly(!dataSavedValue.has_value()) , _api(&_peer->session().mtp()) -, _privacySavedValue(privacySavedValue) -, _usernameSavedValue(usernameSavedValue) -, _noForwardsSavedValue(noForwardsSavedValue) +, _dataSavedValue(dataSavedValue) , _useLocationPhrases(useLocationPhrases) , _isGroup(_peer->isChat() || _peer->isMegagroup()) -, _goodUsername(!_usernameSavedValue.value_or( - _peer->isChannel() ? _peer->asChannel()->username : QString()).isEmpty()) +, _goodUsername(_dataSavedValue + ? !_dataSavedValue->username.isEmpty() + : (_peer->isChannel() && !_peer->asChannel()->username.isEmpty())) , _wrap(container) , _checkUsernameTimer([=] { checkUsernameAvailability(); }) { _peer->updateFull(); @@ -175,7 +182,12 @@ Controller::Controller( void Controller::createContent() { _controls = Controls(); - fillPrivaciesButtons(_wrap, _privacySavedValue); + fillPrivaciesButtons( + _wrap, + (_dataSavedValue + ? _dataSavedValue->privacy + : std::optional())); + // Skip. if (!_linkOnly) { _wrap->add(object_ptr(_wrap)); @@ -204,6 +216,63 @@ void Controller::createContent() { AddDividerText(_wrap.get(), tr::lng_group_invite_manage_about()); if (!_linkOnly) { + if (_peer->isMegagroup()) { + _controls.whoSendWrap = _wrap->add( + object_ptr>( + _wrap.get(), + object_ptr(_wrap.get()))); + const auto wrap = _controls.whoSendWrap->entity(); + + AddSkip(wrap); + if (_dataSavedValue->hasLinkedChat) { + AddSubsectionTitle(wrap, tr::lng_manage_peer_send_title()); + + _controls.joinToWrite = wrap->add(EditPeerInfoBox::CreateButton( + wrap, + tr::lng_manage_peer_send_only_members(), + rpl::single(QString()), + [=] {}, + st::manageGroupTopButtonWithText, + {} + )); + _controls.joinToWrite->toggleOn( + rpl::single(_dataSavedValue->joinToWrite) + )->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + _dataSavedValue->joinToWrite = toggled; + }, wrap->lifetime()); + } + auto joinToWrite = _controls.joinToWrite + ? _controls.joinToWrite->toggledValue() + : rpl::single(true); + + const auto requestToJoinWrap = wrap->add( + object_ptr>( + wrap, + EditPeerInfoBox::CreateButton( + wrap, + tr::lng_manage_peer_send_approve_members(), + rpl::single(QString()), + [=] {}, + st::peerPermissionsButton, + {})))->setDuration(0); + requestToJoinWrap->toggleOn(rpl::duplicate(joinToWrite)); + _controls.requestToJoin = requestToJoinWrap->entity(); + _controls.requestToJoin->toggleOn( + rpl::single(_dataSavedValue->requestToJoin) + )->toggledValue( + ) | rpl::start_with_next([=](bool toggled) { + _dataSavedValue->requestToJoin = toggled; + }, wrap->lifetime()); + + AddSkip(wrap); + AddDividerText( + wrap, + rpl::conditional( + std::move(joinToWrite), + tr::lng_manage_peer_send_approve_members_about(), + tr::lng_manage_peer_send_only_members_about())); + } AddSkip(_wrap.get()); AddSubsectionTitle( _wrap.get(), @@ -216,10 +285,10 @@ void Controller::createContent() { st::peerPermissionsButton, {})); _controls.noForwards->toggleOn( - rpl::single(_noForwardsSavedValue.value_or(false)) + rpl::single(_dataSavedValue->noForwards) )->toggledValue( ) | rpl::start_with_next([=](bool toggled) { - _noForwardsSavedValue = toggled; + _dataSavedValue->noForwards = toggled; }, _wrap->lifetime()); AddSkip(_wrap.get()); AddDividerText( @@ -234,8 +303,9 @@ void Controller::createContent() { if (_controls.privacy->value() == Privacy::NoUsername) { checkUsernameAvailability(); } - const auto forShowing = _privacySavedValue.value_or( - Privacy::NoUsername); + const auto forShowing = _dataSavedValue + ? _dataSavedValue->privacy + : Privacy::NoUsername; _controls.inviteLinkWrap->toggle( (forShowing != Privacy::HasUsername), anim::type::instant); @@ -332,8 +402,9 @@ object_ptr Controller::createUsernameEdit() { Expects(_wrap != nullptr); const auto channel = _peer->asChannel(); - const auto username = _usernameSavedValue.value_or( - channel ? channel->username : QString()); + const auto username = (!_dataSavedValue || !channel) + ? QString() + : channel->username; auto result = object_ptr>( _wrap, @@ -401,6 +472,15 @@ void Controller::privacyChanged(Privacy value) { (value == Privacy::HasUsername), anim::type::instant); }; + const auto toggleWhoSendWrap = [&] { + if (!_controls.whoSendWrap) { + return; + } + _controls.whoSendWrap->toggle( + (value == Privacy::HasUsername + || (_dataSavedValue && _dataSavedValue->hasLinkedChat)), + anim::type::instant); + }; const auto refreshVisibilities = [&] { // Now first we need to hide that was shown. // Otherwise box will change own Y position. @@ -408,10 +488,12 @@ void Controller::privacyChanged(Privacy value) { if (value == Privacy::HasUsername) { toggleInviteLink(); toggleEditUsername(); + toggleWhoSendWrap(); _controls.usernameResult = nullptr; checkUsernameAvailability(); } else { + toggleWhoSendWrap(); toggleEditUsername(); toggleInviteLink(); } @@ -497,9 +579,7 @@ void Controller::askUsernameRevoke() { checkUsernameAvailability(); }); _show->showBox( - Box( - &_peer->session(), - std::move(revokeCallback)), + Box(PublicLinksLimitBox, _navigation, revokeCallback), Ui::LayerOption::KeepOther); } @@ -572,7 +652,7 @@ object_ptr Controller::createInviteLinkBlock() { const auto container = result->entity(); using namespace Settings; - if (_privacySavedValue) { + if (_dataSavedValue) { AddSkip(container); AddSubsectionTitle(container, tr::lng_create_permanent_link_title()); @@ -599,26 +679,24 @@ object_ptr Controller::createInviteLinkBlock() { EditPeerTypeBox::EditPeerTypeBox( QWidget*, + Window::SessionNavigation *navigation, not_null peer, bool useLocationPhrases, - std::optional> savedCallback, - std::optional privacySaved, - std::optional usernameSaved, - std::optional noForwardsValue, + std::optional> savedCallback, + std::optional dataSaved, std::optional> usernameError) -: _peer(peer) +: _navigation(navigation) +, _peer(peer) , _useLocationPhrases(useLocationPhrases) , _savedCallback(std::move(savedCallback)) -, _privacySavedValue(privacySaved) -, _usernameSavedValue(usernameSaved) -, _noForwardsValue(noForwardsValue) +, _dataSavedValue(dataSaved) , _usernameError(usernameError) { } EditPeerTypeBox::EditPeerTypeBox( QWidget*, not_null peer) -: EditPeerTypeBox(nullptr, peer, {}, {}, {}, {}, {}, {}) { +: EditPeerTypeBox(nullptr, nullptr, peer, {}, {}, {}) { } void EditPeerTypeBox::setInnerFocus() { @@ -632,13 +710,12 @@ void EditPeerTypeBox::prepare() { const auto controller = Ui::CreateChild( this, + _navigation, std::make_shared(this), content.data(), _peer, _useLocationPhrases, - _privacySavedValue, - _usernameSavedValue, - _noForwardsValue); + _dataSavedValue); _focusRequests.events( ) | rpl::start_with_next( [=] { @@ -662,12 +739,15 @@ void EditPeerTypeBox::prepare() { } auto local = std::move(*_savedCallback); - local( - v, - (v == Privacy::HasUsername + local(EditPeerTypeData{ + .privacy = v, + .username = (v == Privacy::HasUsername ? controller->getUsernameInput() : QString()), - controller->noForwards()); // We don't need username with private type. + .noForwards = controller->noForwards(), + .joinToWrite = controller->joinToWrite(), + .requestToJoin = controller->requestToJoin(), + }); // We don't need username with private type. closeBox(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h index 3f544e1f97f99e..d69c79e92bd92c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.h @@ -18,6 +18,10 @@ class VerticalLayout; class SettingsButton; } // namespace Ui +namespace Window { +class SessionNavigation; +} // namespace Window + enum class Privacy { HasUsername, NoUsername, @@ -29,16 +33,24 @@ enum class UsernameState { NotAvailable, }; +struct EditPeerTypeData { + Privacy privacy = Privacy::NoUsername; + QString username; + bool hasLinkedChat = false; + bool noForwards = false; + bool joinToWrite = false; + bool requestToJoin = false; +}; + class EditPeerTypeBox : public Ui::BoxContent { public: EditPeerTypeBox( QWidget*, + Window::SessionNavigation *navigation, not_null peer, bool useLocationPhrases, - std::optional> savedCallback, - std::optional privacySaved, - std::optional usernameSaved, - std::optional noForwardsSaved, + std::optional> savedCallback, + std::optional dataSaved, std::optional> usernameError = {}); // For invite link only. @@ -51,13 +63,12 @@ class EditPeerTypeBox : public Ui::BoxContent { void setInnerFocus() override; private: - not_null _peer; + Window::SessionNavigation *_navigation = nullptr; + const not_null _peer; bool _useLocationPhrases = false; - std::optional> _savedCallback; + std::optional> _savedCallback; - std::optional _privacySavedValue; - std::optional _usernameSavedValue; - std::optional _noForwardsValue; + std::optional _dataSavedValue; std::optional> _usernameError; rpl::event_stream<> _focusRequests; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index 5ca433f792e6cd..17ccc4969de14b 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -115,7 +115,9 @@ void Preload( : Data::FileOriginUserPhoto(peerToUser(peer->id), photo->id); state->photoPreloads.push_back(photo->createMediaView()); if (photo->hasVideo()) { - state->photoPreloads.back()->videoWanted(origin); + state->photoPreloads.back()->videoWanted( + Data::PhotoSize::Large, + origin); } else { state->photoPreloads.back()->wanted( Data::PhotoSize::Large, diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp new file mode 100644 index 00000000000000..5b8bc827f669de --- /dev/null +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -0,0 +1,1042 @@ +/* +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/premium_limits_box.h" + +#include "ui/boxes/confirm_box.h" +#include "ui/controls/peer_list_dummy.h" +#include "ui/effects/premium_graphics.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/text/text_utilities.h" +#include "ui/toasts/common_toasts.h" +#include "main/main_session.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "boxes/peer_list_controllers.h" +#include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfoBox +#include "window/window_session_controller.h" +#include "data/data_chat_filters.h" +#include "data/data_user.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "data/data_folder.h" +#include "data/data_premium_limits.h" +#include "lang/lang_keys.h" +#include "settings/settings_common.h" +#include "settings/settings_premium.h" +#include "base/unixtime.h" +#include "apiwrap.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" +#include "styles/style_info.h" +#include "styles/style_settings.h" + +namespace { + +struct InfographicDescriptor { + float64 defaultLimit = 0; + float64 current = 0; + float64 premiumLimit = 0; + const style::icon *icon; + std::optional> phrase; +}; + +[[nodiscard]] rpl::producer<> BoxShowFinishes(not_null box) { + const auto singleShot = box->lifetime().make_state(); + const auto showFinishes = singleShot->make_state>(); + + box->setShowFinishedCallback([=] { + showFinishes->fire({}); + singleShot->destroy(); + box->setShowFinishedCallback(nullptr); + }); + + return showFinishes->events(); +} + +void AddSubsectionTitle( + not_null container, + rpl::producer text) { + const auto &subtitlePadding = st::settingsButton.padding; + Settings::AddSubsectionTitle( + container, + std::move(text), + { 0, subtitlePadding.top(), 0, -subtitlePadding.bottom() }); +} + +class InactiveController final : public PeerListController { +public: + explicit InactiveController(not_null session); + ~InactiveController(); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + +private: + void appendRow(not_null peer, TimeId date); + [[nodiscard]] std::unique_ptr createRow( + not_null peer, + TimeId date) const; + + const not_null _session; + mtpRequestId _requestId = 0; + +}; + +class PublicsController final : public PeerListController { +public: + PublicsController( + not_null navigation, + Fn closeBox); + ~PublicsController(); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + +private: + void appendRow(not_null peer); + [[nodiscard]] std::unique_ptr createRow( + not_null peer) const; + + const not_null _navigation; + Fn _closeBox; + mtpRequestId _requestId = 0; + +}; + +class InactiveDelegate final : public PeerListContentDelegate { +public: + void peerListSetTitle(rpl::producer title) override; + void peerListSetAdditionalTitle(rpl::producer title) override; + bool peerListIsRowChecked(not_null row) override; + int peerListSelectedRowsCount() override; + void peerListScrollToTop() override; + void peerListAddSelectedPeerInBunch( + not_null peer) override; + void peerListAddSelectedRowInBunch( + not_null row) override; + void peerListFinishSelectedRowsBunch() override; + void peerListSetDescription( + object_ptr description) override; + void peerListShowBox( + object_ptr content, + Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; + void peerListHideLayer() override; + not_null peerListToastParent() override; + void peerListSetRowChecked( + not_null row, + bool checked) override; + + [[nodiscard]] rpl::producer selectedCountChanges() const; + [[nodiscard]] const base::flat_set &selected() const; + +private: + base::flat_set _selectedIds; + rpl::event_stream _selectedCountChanges; + +}; + +void InactiveDelegate::peerListSetTitle(rpl::producer title) { +} + +void InactiveDelegate::peerListSetAdditionalTitle( + rpl::producer title) { +} + +bool InactiveDelegate::peerListIsRowChecked(not_null row) { + return _selectedIds.contains(row->id()); +} + +int InactiveDelegate::peerListSelectedRowsCount() { + return int(_selectedIds.size()); +} + +void InactiveDelegate::peerListScrollToTop() { +} + +void InactiveDelegate::peerListAddSelectedPeerInBunch( + not_null peer) { + _selectedIds.emplace(PeerListRowId(peer->id.value)); + _selectedCountChanges.fire(int(_selectedIds.size())); +} + +void InactiveDelegate::peerListAddSelectedRowInBunch( + not_null row) { + _selectedIds.emplace(row->id()); + _selectedCountChanges.fire(int(_selectedIds.size())); +} + +void InactiveDelegate::peerListSetRowChecked( + not_null row, + bool checked) { + if (checked) { + _selectedIds.emplace(row->id()); + } else { + _selectedIds.remove(row->id()); + } + _selectedCountChanges.fire(int(_selectedIds.size())); + PeerListContentDelegate::peerListSetRowChecked(row, checked); +} + +void InactiveDelegate::peerListFinishSelectedRowsBunch() { +} + +void InactiveDelegate::peerListSetDescription( + object_ptr description) { + description.destroy(); +} + +void InactiveDelegate::peerListShowBox( + object_ptr content, + Ui::LayerOptions options) { +} + +void InactiveDelegate::peerListHideLayer() { +} + +not_null InactiveDelegate::peerListToastParent() { + Unexpected("...InactiveDelegate::peerListToastParent"); +} + +rpl::producer InactiveDelegate::selectedCountChanges() const { + return _selectedCountChanges.events(); +} + +const base::flat_set &InactiveDelegate::selected() const { + return _selectedIds; +} + +InactiveController::InactiveController(not_null session) +: _session(session) { +} + +InactiveController::~InactiveController() { + if (_requestId) { + _session->api().request(_requestId).cancel(); + } +} + +Main::Session &InactiveController::session() const { + return *_session; +} + +void InactiveController::prepare() { + _requestId = _session->api().request(MTPchannels_GetInactiveChannels( + )).done([=](const MTPmessages_InactiveChats &result) { + _requestId = 0; + result.match([&](const MTPDmessages_inactiveChats &data) { + _session->data().processUsers(data.vusers()); + const auto &list = data.vchats().v; + const auto &dates = data.vdates().v; + for (auto i = 0, count = int(list.size()); i != count; ++i) { + const auto peer = _session->data().processChat(list[i]); + const auto date = (i < dates.size()) ? dates[i].v : TimeId(); + appendRow(peer, date); + } + delegate()->peerListRefreshRows(); + }); + }).send(); +} + +void InactiveController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked(row, !row->checked()); +} + +void InactiveController::appendRow( + not_null participant, + TimeId date) { + if (!delegate()->peerListFindRow(participant->id.value)) { + delegate()->peerListAppendRow(createRow(participant, date)); + } +} + +std::unique_ptr InactiveController::createRow( + not_null peer, + TimeId date) const { + auto result = std::make_unique(peer); + const auto active = base::unixtime::parse(date).date(); + const auto now = QDate::currentDate(); + const auto time = [&] { + const auto days = active.daysTo(now); + if (now < active) { + return QString(); + } else if (active == now) { + const auto unixtime = base::unixtime::now(); + const auto delta = int64(unixtime) - int64(date); + if (delta <= 0) { + return QString(); + } else if (delta >= 3600) { + return tr::lng_hours(tr::now, lt_count, delta / 3600); + } else if (delta >= 60) { + return tr::lng_minutes(tr::now, lt_count, delta / 60); + } else { + return tr::lng_seconds(tr::now, lt_count, delta); + } + } else if (days >= 365) { + return tr::lng_years(tr::now, lt_count, days / 365); + } else if (days >= 31) { + return tr::lng_months(tr::now, lt_count, days / 31); + } else if (days >= 7) { + return tr::lng_weeks(tr::now, lt_count, days / 7); + } else { + return tr::lng_days(tr::now, lt_count, days); + } + }(); + result->setCustomStatus(tr::lng_channels_leave_status( + tr::now, + lt_type, + (peer->isBroadcast() + ? tr::lng_channel_status(tr::now) + : tr::lng_group_status(tr::now)), + lt_time, + time)); + return result; +} + +PublicsController::PublicsController( + not_null navigation, + Fn closeBox) +: _navigation(navigation) +, _closeBox(std::move(closeBox)) { +} + +PublicsController::~PublicsController() { + if (_requestId) { + _navigation->session().api().request(_requestId).cancel(); + } +} + +Main::Session &PublicsController::session() const { + return _navigation->session(); +} + +void PublicsController::prepare() { + _requestId = _navigation->session().api().request( + MTPchannels_GetAdminedPublicChannels(MTP_flags(0)) + ).done([=](const MTPmessages_Chats &result) { + _requestId = 0; + + const auto &chats = result.match([](const auto &data) { + return data.vchats().v; + }); + auto &owner = _navigation->session().data(); + for (const auto &chat : chats) { + if (const auto peer = owner.processChat(chat)) { + if (!peer->isChannel() || peer->userName().isEmpty()) { + continue; + } + appendRow(peer); + } + delegate()->peerListRefreshRows(); + } + }).send(); +} + +void PublicsController::rowClicked(not_null row) { + _navigation->parentController()->show( + PrepareShortInfoBox(row->peer(), _navigation)); +} + +void PublicsController::rowRightActionClicked(not_null row) { + const auto peer = row->peer(); + const auto textMethod = peer->isMegagroup() + ? tr::lng_channels_too_much_public_revoke_confirm_group + : tr::lng_channels_too_much_public_revoke_confirm_channel; + const auto text = textMethod( + tr::now, + lt_link, + peer->session().createInternalLink(peer->userName()), + lt_group, + peer->name); + const auto confirmText = tr::lng_channels_too_much_public_revoke( + tr::now); + const auto closeBox = _closeBox; + const auto once = std::make_shared(false); + auto callback = crl::guard(_navigation, [=](Fn &&close) { + if (*once) { + return; + } + *once = true; + peer->session().api().request(MTPchannels_UpdateUsername( + peer->asChannel()->inputChannel, + MTP_string() + )).done([=, close = std::move(close)] { + closeBox(); + close(); + }).send(); + }); + _navigation->parentController()->show( + Ui::MakeConfirmBox({ + .text = text, + .confirmed = std::move(callback), + .confirmText = confirmText, + }), + Ui::LayerOption::KeepOther); +} + +void PublicsController::appendRow(not_null participant) { + if (!delegate()->peerListFindRow(participant->id.value)) { + delegate()->peerListAppendRow(createRow(participant)); + } +} + +std::unique_ptr PublicsController::createRow( + not_null peer) const { + auto result = std::make_unique(peer); + result->setActionLink(tr::lng_channels_too_much_public_revoke(tr::now)); + result->setCustomStatus( + _navigation->session().createInternalLink(peer->userName())); + return result; +} + +void SimpleLimitBox( + not_null box, + not_null session, + bool premiumPossible, + rpl::producer title, + rpl::producer text, + const QString &refAddition, + const InfographicDescriptor &descriptor, + bool fixed = false) { + box->setWidth(st::boxWideWidth); + + const auto top = fixed + ? box->setPinnedToTopContent(object_ptr(box)) + : box->verticalLayout(); + + Settings::AddSkip(top, st::premiumInfographicPadding.top()); + Ui::Premium::AddBubbleRow( + top, + BoxShowFinishes(box), + descriptor.defaultLimit, + descriptor.current, + descriptor.premiumLimit, + premiumPossible, + descriptor.phrase, + descriptor.icon); + Settings::AddSkip(top, st::premiumLineTextSkip); + if (premiumPossible) { + Ui::Premium::AddLimitRow( + top, + descriptor.premiumLimit, + descriptor.phrase); + Settings::AddSkip(top, st::premiumInfographicPadding.bottom()); + } + + box->setTitle(std::move(title)); + + auto padding = st::boxPadding; + padding.setTop(padding.bottom()); + top->add( + object_ptr( + box, + std::move(text), + st::aboutRevokePublicLabel), + padding); + + if (session->premium() || !premiumPossible) { + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + } else { + box->addButton(tr::lng_limits_increase(), [=] { + Settings::ShowPremium(session, LimitsPremiumRef(refAddition)); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + } + + if (fixed) { + Settings::AddSkip(top, st::settingsButton.padding.bottom()); + Settings::AddDivider(top); + } +} + +void SimpleLimitBox( + not_null box, + not_null session, + rpl::producer title, + rpl::producer text, + const QString &refAddition, + const InfographicDescriptor &descriptor, + bool fixed = false) { + SimpleLimitBox( + box, + session, + session->premiumPossible(), + std::move(title), + std::move(text), + refAddition, + descriptor, + fixed); +} + +[[nodiscard]] int PinsCount(not_null list) { + return list->pinned()->order().size(); +} + +void SimplePinsLimitBox( + not_null box, + not_null session, + const QString &refAddition, + float64 defaultLimit, + float64 premiumLimit, + float64 currentCount) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto current = std::clamp(currentCount, defaultLimit, premiumLimit); + + auto text = rpl::combine( + tr::lng_filter_pin_limit1( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? rpl::single(TextWithEntities()) + : tr::lng_filter_pin_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + SimpleLimitBox( + box, + session, + tr::lng_filter_pin_limit_title(), + std::move(text), + refAddition, + { defaultLimit, current, premiumLimit, &st::premiumIconPins }); +} + +} // namespace + +void ChannelsLimitBox( + not_null box, + not_null session) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.channelsDefault()); + const auto premiumLimit = float64(limits.channelsPremium()); + const auto current = (premium ? premiumLimit : defaultLimit); + + auto text = rpl::combine( + tr::lng_channels_limit1( + lt_count, + rpl::single(current), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? tr::lng_channels_limit2_final(Ui::Text::RichLangValue) + : tr::lng_channels_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + session, + tr::lng_channels_limit_title(), + std::move(text), + "channels", + { defaultLimit, current, premiumLimit, &st::premiumIconGroups }, + true); + + AddSubsectionTitle(box->verticalLayout(), tr::lng_channels_leave_title()); + + const auto delegate = box->lifetime().make_state(); + const auto controller = box->lifetime().make_state( + session); + + const auto content = box->addRow( + object_ptr(box, controller), + {}); + delegate->setContent(content); + controller->setDelegate(delegate); + + const auto count = 100; + const auto placeholder = box->addRow( + object_ptr(box, count, st::defaultPeerList), + {}); + + using namespace rpl::mappers; + content->heightValue( + ) | rpl::filter(_1 > 0) | rpl::start_with_next([=] { + delete placeholder; + }, placeholder->lifetime()); + + delegate->selectedCountChanges( + ) | rpl::start_with_next([=](int count) { + const auto leave = [=](const base::flat_set &ids) { + for (const auto rowId : ids) { + const auto id = peerToChannel(PeerId(rowId)); + if (const auto channel = session->data().channelLoaded(id)) { + session->api().leaveChannel(channel); + } + } + Ui::ShowMultilineToast({ + .parentOverride = Ui::BoxShow(box).toastParent(), + .text = { tr::lng_channels_leave_done(tr::now) }, + }); + box->closeBox(); + }; + box->clearButtons(); + if (count) { + box->addButton( + tr::lng_channels_leave(lt_count, rpl::single(count * 1.)), + [=] { leave(delegate->selected()); }); + } else if (premium) { + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + } else { + box->addButton(tr::lng_limits_increase(), [=] { + Settings::ShowPremium(session, LimitsPremiumRef("channels")); + }); + } + }, box->lifetime()); +} + +void PublicLinksLimitBox( + not_null box, + not_null navigation, + Fn retry) { + const auto session = &navigation->session(); + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.channelsPublicDefault()); + const auto premiumLimit = float64(limits.channelsPublicPremium()); + const auto current = (premium ? premiumLimit : defaultLimit); + + auto text = rpl::combine( + tr::lng_links_limit1( + lt_count, + rpl::single(current), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? tr::lng_links_limit2_final(Ui::Text::RichLangValue) + : tr::lng_links_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + session, + tr::lng_links_limit_title(), + std::move(text), + "channels_public", + { defaultLimit, current, premiumLimit, &st::premiumIconLinks }, + true); + + AddSubsectionTitle(box->verticalLayout(), tr::lng_links_revoke_title()); + + const auto delegate = box->lifetime().make_state(); + const auto controller = box->lifetime().make_state( + navigation, + crl::guard(box, [=] { box->closeBox(); retry(); })); + + const auto content = box->addRow( + object_ptr(box, controller), + {}); + delegate->setContent(content); + controller->setDelegate(delegate); + + const auto count = defaultLimit; + const auto placeholder = box->addRow( + object_ptr(box, count, st::defaultPeerList), + {}); + + using namespace rpl::mappers; + content->heightValue( + ) | rpl::filter(_1 > 0) | rpl::start_with_next([=] { + delete placeholder; + }, placeholder->lifetime()); +} + +void FilterChatsLimitBox( + not_null box, + not_null session, + int currentCount) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.dialogFiltersChatsDefault()); + const auto premiumLimit = float64(limits.dialogFiltersChatsPremium()); + const auto current = std::clamp( + float64(currentCount), + defaultLimit, + premiumLimit); + + auto text = rpl::combine( + tr::lng_filter_chats_limit1( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? rpl::single(TextWithEntities()) + : tr::lng_filter_chats_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + session, + tr::lng_filter_chats_limit_title(), + std::move(text), + "dialog_filters_chats", + { defaultLimit, current, premiumLimit, &st::premiumIconChats }); +} + +void FiltersLimitBox( + not_null box, + not_null session) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.dialogFiltersDefault()); + const auto premiumLimit = float64(limits.dialogFiltersPremium()); + const auto current = float64(ranges::count_if( + session->data().chatsFilters().list(), + [](const Data::ChatFilter &f) { return f.id() != FilterId(); })); + + auto text = rpl::combine( + tr::lng_filters_limit1( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? rpl::single(TextWithEntities()) + : tr::lng_filters_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + SimpleLimitBox( + box, + session, + tr::lng_filters_limit_title(), + std::move(text), + "dialog_filters", + { defaultLimit, current, premiumLimit, &st::premiumIconFolders }); +} + +void FilterPinsLimitBox( + not_null box, + not_null session, + FilterId filterId) { + const auto limits = Data::PremiumLimits(session); + SimplePinsLimitBox( + box, + session, + "dialog_filters_pinned", + limits.dialogFiltersChatsDefault(), + limits.dialogFiltersChatsPremium(), + PinsCount(session->data().chatsFilters().chatsList(filterId))); +} + +void FolderPinsLimitBox( + not_null box, + not_null session) { + const auto limits = Data::PremiumLimits(session); + SimplePinsLimitBox( + box, + session, + "dialogs_folder_pinned", + limits.dialogsFolderPinnedDefault(), + limits.dialogsFolderPinnedPremium(), + PinsCount(session->data().folder(Data::Folder::kId)->chatsList())); +} + +void PinsLimitBox( + not_null box, + not_null session) { + const auto limits = Data::PremiumLimits(session); + SimplePinsLimitBox( + box, + session, + "dialog_pinned", + limits.dialogsPinnedDefault(), + limits.dialogsPinnedPremium(), + PinsCount(session->data().chatsList())); +} + +void CaptionLimitBox( + not_null box, + not_null session, + int remove) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.captionLengthDefault()); + const auto premiumLimit = float64(limits.captionLengthPremium()); + const auto currentLimit = premium ? premiumLimit : defaultLimit; + const auto current = std::clamp( + remove + currentLimit, + defaultLimit, + premiumLimit); + + auto text = rpl::combine( + tr::lng_caption_limit1( + lt_count, + rpl::single(currentLimit), + Ui::Text::RichLangValue), + (!premiumPossible + ? rpl::single(TextWithEntities()) + : tr::lng_caption_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + session, + tr::lng_caption_limit_title(), + std::move(text), + "caption_length", + { defaultLimit, current, premiumLimit, &st::premiumIconChats }); +} + +void CaptionLimitReachedBox( + not_null box, + not_null session, + int remove) { + Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ + .text = tr::lng_caption_limit_reached(tr::now, lt_count, remove), + .inform = true, + }); + if (!session->premium()) { + box->addLeftButton(tr::lng_limits_increase(), [=] { + box->getDelegate()->showBox( + Box(CaptionLimitBox, session, remove), + Ui::LayerOption::KeepOther, + anim::type::normal); + box->closeBox(); + }); + } +} + +void FileSizeLimitBox( + not_null box, + not_null session, + uint64 fileSizeBytes) { + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.uploadMaxDefault()); + const auto premiumLimit = float64(limits.uploadMaxPremium()); + + const auto defaultGb = float64(int(defaultLimit + 999) / 2000); + const auto premiumGb = float64(int(premiumLimit + 999) / 2000); + + const auto tooLarge = (fileSizeBytes > premiumLimit * 512ULL * 1024); + const auto showLimit = tooLarge ? premiumGb : defaultGb; + const auto premiumPossible = !tooLarge && session->premiumPossible(); + + const auto current = (fileSizeBytes && premiumPossible) + ? std::clamp( + float64(((fileSizeBytes / uint64(1024 * 1024)) + 499) / 1000), + defaultGb, + premiumGb) + : showLimit; + const auto gb = [](int count) { + return tr::lng_file_size_limit(tr::now, lt_count, count); + }; + + auto text = rpl::combine( + tr::lng_file_size_limit1( + lt_size, + rpl::single(Ui::Text::Bold(gb(showLimit))), + Ui::Text::RichLangValue), + (!premiumPossible + ? rpl::single(TextWithEntities()) + : tr::lng_file_size_limit2( + lt_size, + rpl::single(Ui::Text::Bold(gb(premiumGb))), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + session, + premiumPossible, + tr::lng_file_size_limit_title(), + std::move(text), + "upload_max_fileparts", + { + defaultGb, + current, + (tooLarge ? showLimit * 2 : premiumGb), + &st::premiumIconFiles, + tr::lng_file_size_limit + }); +} + +void AccountsLimitBox( + not_null box, + not_null session) { + const auto defaultLimit = Main::Domain::kMaxAccounts; + const auto premiumLimit = Main::Domain::kPremiumMaxAccounts; + + using Args = Ui::Premium::AccountsRowArgs; + const auto accounts = session->domain().orderedAccounts(); + auto promotePossible = ranges::views::all( + accounts + ) | ranges::views::filter([&](not_null account) { + return account->sessionExists() + && !account->session().premium() + && account->session().premiumPossible(); + }) | ranges::views::transform([&](not_null account) { + const auto user = account->session().user(); + return Args::Entry{ user->name, PaintUserpicCallback(user, false) }; + }) | ranges::views::take(defaultLimit) | ranges::to_vector; + + const auto premiumPossible = !promotePossible.empty(); + const auto current = int(accounts.size()); + + auto text = rpl::combine( + tr::lng_accounts_limit1( + lt_count, + rpl::single(current), + Ui::Text::RichLangValue), + ((!premiumPossible || current > premiumLimit) + ? rpl::single(TextWithEntities()) + : tr::lng_accounts_limit2(Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + + box->setWidth(st::boxWideWidth); + + const auto top = box->verticalLayout(); + const auto group = std::make_shared(0); + + Settings::AddSkip(top, st::premiumInfographicPadding.top()); + Ui::Premium::AddBubbleRow( + top, + BoxShowFinishes(box), + 0, + current, + (!premiumPossible + ? (current * 2) + : (current > defaultLimit) + ? (current + 1) + : (defaultLimit * 2)), + premiumPossible, + std::nullopt, + &st::premiumIconAccounts); + Settings::AddSkip(top, st::premiumLineTextSkip); + if (premiumPossible) { + Ui::Premium::AddLimitRow( + top, + (QString::number(std::max(current, defaultLimit) + 1) + + ((current + 1 == premiumLimit) ? "" : "+")), + QString::number(defaultLimit)); + Settings::AddSkip(top, st::premiumInfographicPadding.bottom()); + } + box->setTitle(tr::lng_accounts_limit_title()); + + auto padding = st::boxPadding; + padding.setTop(padding.bottom()); + top->add( + object_ptr( + box, + std::move(text), + st::aboutRevokePublicLabel), + padding); + + if (!premiumPossible || current > premiumLimit) { + box->addButton(tr::lng_box_ok(), [=] { + box->closeBox(); + }); + return; + } + auto switchingLifetime = std::make_shared(); + box->addButton(tr::lng_continue(), [=]() mutable { + const auto ref = QString(); + + const auto wasAccount = &session->account(); + const auto nowAccount = accounts[group->value()]; + if (wasAccount == nowAccount) { + Settings::ShowPremium(session, ref); + return; + } + + if (*switchingLifetime) { + return; + } + *switchingLifetime = session->domain().activeSessionChanges( + ) | rpl::start_with_next([=](Main::Session *session) mutable { + if (session) { + Settings::ShowPremium(session, ref); + } + if (switchingLifetime) { + base::take(switchingLifetime)->destroy(); + } + }); + session->domain().activate(nowAccount); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + auto args = Args{ + .group = group, + .st = st::premiumAccountsCheckbox, + .stName = st::shareBoxListItem.nameStyle, + .stNameFg = st::shareBoxListItem.nameFg, + .entries = std::move(promotePossible), + }; + if (!args.entries.empty()) { + box->addSkip(st::premiumAccountsPadding.top()); + Ui::Premium::AddAccountsRow(box->verticalLayout(), std::move(args)); + box->addSkip(st::premiumAccountsPadding.bottom()); + } +} + +QString LimitsPremiumRef(const QString &addition) { + return "double_limits__" + addition; +} diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h new file mode 100644 index 00000000000000..de2a7db73e4a04 --- /dev/null +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -0,0 +1,60 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/layers/generic_box.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionNavigation; +} // namespace Window + +void ChannelsLimitBox( + not_null box, + not_null session); +void PublicLinksLimitBox( + not_null box, + not_null navigation, + Fn retry); +void FilterChatsLimitBox( + not_null box, + not_null session, + int currentCount); +void FiltersLimitBox( + not_null box, + not_null session); +void FilterPinsLimitBox( + not_null box, + not_null session, + FilterId filterId); +void FolderPinsLimitBox( + not_null box, + not_null session); +void PinsLimitBox( + not_null box, + not_null session); +void CaptionLimitBox( + not_null box, + not_null session, + int remove); +void CaptionLimitReachedBox( + not_null box, + not_null session, + int remove); +void FileSizeLimitBox( + not_null box, + not_null session, + uint64 fileSizeBytes); +void AccountsLimitBox( + not_null box, + not_null session); + +[[nodiscard]] QString LimitsPremiumRef(const QString &addition); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp new file mode 100644 index 00000000000000..337800d9cc16d1 --- /dev/null +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -0,0 +1,1783 @@ +/* +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/premium_preview_box.h" + +#include "chat_helpers/stickers_lottie.h" +#include "chat_helpers/stickers_emoji_pack.h" +#include "data/data_file_origin.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_message_reactions.h" +#include "data/data_document_media.h" +#include "data/data_streaming.h" +#include "data/data_peer_values.h" +#include "data/data_premium_limits.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "main/main_domain.h" // kMaxAccounts +#include "ui/chat/chat_theme.h" +#include "ui/chat/chat_style.h" +#include "ui/layers/generic_box.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/gradient.h" +#include "ui/text/text.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/boxes/confirm_box.h" +#include "settings/settings_premium.h" +#include "lottie/lottie_single_player.h" +#include "history/view/media/history_view_sticker.h" +#include "history/view/history_view_element.h" +#include "media/streaming/media_streaming_instance.h" +#include "media/streaming/media_streaming_player.h" +#include "window/window_session_controller.h" +#include "api/api_premium.h" +#include "apiwrap.h" +#include "styles/style_layers.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_settings.h" + +#include + +namespace { + +constexpr auto kPremiumShift = 21. / 240; +constexpr auto kShiftDuration = crl::time(200); +constexpr auto kReactionsPerRow = 5; +constexpr auto kDisabledOpacity = 0.5; +constexpr auto kPreviewsCount = int(PremiumPreview::kCount); +constexpr auto kToggleStickerTimeout = 2 * crl::time(1000); +constexpr auto kStarOpacityOff = 0.1; +constexpr auto kStarOpacityOn = 1.; +constexpr auto kStarPeriod = 3 * crl::time(1000); + +struct Descriptor { + PremiumPreview section = PremiumPreview::Stickers; + DocumentData *requestedSticker = nullptr; + base::flat_map disabled; + bool fromSettings = false; + Fn hiddenCallback; +}; + +bool operator==(const Descriptor &a, const Descriptor &b) { + return (a.section == b.section) + && (a.requestedSticker == b.requestedSticker) + && (a.disabled == b.disabled) + && (a.fromSettings == b.fromSettings); +} + +[[nodiscard]] int ComputeX(int column, int columns) { + const auto skip = st::premiumReactionWidthSkip; + const auto fullWidth = columns * skip; + const auto left = (st::boxWideWidth - fullWidth) / 2; + return left + column * skip + (skip / 2); +} + +[[nodiscard]] int ComputeY(int row, int rows) { + const auto middle = (rows > 3) + ? (st::premiumReactionInfoTop / 2) + : st::premiumReactionsMiddle; + const auto skip = st::premiumReactionHeightSkip; + const auto fullHeight = rows * skip; + const auto top = middle - (fullHeight / 2); + return top + row * skip + (skip / 2); +} + +struct Preload { + Descriptor descriptor; + std::shared_ptr media; + base::weak_ptr controller; +}; + +[[nodiscard]] std::vector &Preloads() { + static auto result = std::vector(); + return result; +} + +void PreloadSticker(const std::shared_ptr &media) { + const auto origin = media->owner()->stickerSetOrigin(); + media->automaticLoad(origin, nullptr); + media->videoThumbnailWanted(origin); +} + +[[nodiscard]] rpl::producer SectionTitle(PremiumPreview section) { + switch (section) { + case PremiumPreview::MoreUpload: + return tr::lng_premium_summary_subtitle_more_upload(); + case PremiumPreview::FasterDownload: + return tr::lng_premium_summary_subtitle_faster_download(); + case PremiumPreview::VoiceToText: + return tr::lng_premium_summary_subtitle_voice_to_text(); + case PremiumPreview::NoAds: + return tr::lng_premium_summary_subtitle_no_ads(); + case PremiumPreview::Reactions: + return tr::lng_premium_summary_subtitle_unique_reactions(); + case PremiumPreview::Stickers: + return tr::lng_premium_summary_subtitle_premium_stickers(); + case PremiumPreview::AdvancedChatManagement: + return tr::lng_premium_summary_subtitle_advanced_chat_management(); + case PremiumPreview::ProfileBadge: + return tr::lng_premium_summary_subtitle_profile_badge(); + case PremiumPreview::AnimatedUserpics: + return tr::lng_premium_summary_subtitle_animated_userpics(); + } + Unexpected("PremiumPreview in SectionTitle."); +} + +[[nodiscard]] rpl::producer SectionAbout(PremiumPreview section) { + switch (section) { + case PremiumPreview::MoreUpload: + return tr::lng_premium_summary_about_more_upload(); + case PremiumPreview::FasterDownload: + return tr::lng_premium_summary_about_faster_download(); + case PremiumPreview::VoiceToText: + return tr::lng_premium_summary_about_voice_to_text(); + case PremiumPreview::NoAds: + return tr::lng_premium_summary_about_no_ads(); + case PremiumPreview::Reactions: + return tr::lng_premium_summary_about_unique_reactions(); + case PremiumPreview::Stickers: + return tr::lng_premium_summary_about_premium_stickers(); + case PremiumPreview::AdvancedChatManagement: + return tr::lng_premium_summary_about_advanced_chat_management(); + case PremiumPreview::ProfileBadge: + return tr::lng_premium_summary_about_profile_badge(); + case PremiumPreview::AnimatedUserpics: + return tr::lng_premium_summary_about_animated_userpics(); + } + Unexpected("PremiumPreview in SectionTitle."); +} + +[[nodiscard]] object_ptr ChatBackPreview( + QWidget *parent, + int height, + const QImage &back) { + auto result = object_ptr(parent, height); + const auto raw = result.data(); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + p.drawImage(0, 0, back); + }, raw->lifetime()); + + return result; +} + +[[nodiscard]] not_null StickerPreview( + not_null parent, + not_null controller, + const std::shared_ptr &media, + Fn readyCallback = nullptr) { + using namespace HistoryView; + + PreloadSticker(media); + + const auto document = media->owner(); + const auto lottieSize = Sticker::Size(document); + const auto effectSize = Sticker::PremiumEffectSize(document); + const auto result = Ui::CreateChild(parent.get()); + result->show(); + + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect( + QPoint( + (size.width() - effectSize.width()) / 2, + (size.height() - effectSize.height()) / 2), + effectSize)); + }, result->lifetime()); + auto &lifetime = result->lifetime(); + + struct State { + std::unique_ptr lottie; + std::unique_ptr effect; + std::unique_ptr pathGradient; + bool readyInvoked = false; + }; + const auto state = lifetime.make_state(); + const auto createLottieIfReady = [=] { + if (state->lottie) { + return; + } + const auto document = media->owner(); + const auto sticker = document->sticker(); + if (!sticker || !sticker->isLottie() || !media->loaded()) { + return; + } else if (media->videoThumbnailContent().isEmpty()) { + return; + } + + const auto factor = style::DevicePixelRatio(); + state->lottie = ChatHelpers::LottiePlayerFromDocument( + media.get(), + nullptr, + ChatHelpers::StickerLottieSize::MessageHistory, + lottieSize * factor, + Lottie::Quality::High); + state->effect = document->session().emojiStickersPack().effectPlayer( + document, + media->videoThumbnailContent(), + QString(), + true); + + const auto update = [=] { + if (!state->readyInvoked + && readyCallback + && state->lottie->ready() + && state->effect->ready()) { + state->readyInvoked = true; + readyCallback(); + } + result->update(); + }; + auto &lifetime = result->lifetime(); + state->lottie->updates() | rpl::start_with_next(update, lifetime); + state->effect->updates() | rpl::start_with_next(update, lifetime); + }; + createLottieIfReady(); + if (!state->lottie || !state->effect) { + controller->session().downloaderTaskFinished( + ) | rpl::take_while([=] { + createLottieIfReady(); + return !state->lottie || !state->effect; + }) | rpl::start(result->lifetime()); + } + state->pathGradient = MakePathShiftGradient( + controller->chatStyle(), + [=] { result->update(); }); + + result->paintRequest( + ) | rpl::start_with_next([=] { + createLottieIfReady(); + + auto p = QPainter(result); + + const auto left = effectSize.width() + - int(lottieSize.width() * (1. + kPremiumShift)); + const auto top = (effectSize.height() - lottieSize.height()) / 2; + const auto r = QRect(QPoint(left, top), lottieSize); + if (!state->lottie + || !state->lottie->ready() + || !state->effect->ready()) { + p.setBrush(controller->chatStyle()->msgServiceBg()); + ChatHelpers::PaintStickerThumbnailPath( + p, + media.get(), + r, + state->pathGradient.get()); + return; + } + + const auto factor = style::DevicePixelRatio(); + const auto frame = state->lottie->frameInfo({ lottieSize * factor }); + const auto effect = state->effect->frameInfo( + { effectSize * factor }); + //const auto framesCount = !frame.image.isNull() + // ? state->lottie->framesCount() + // : 1; + //const auto effectsCount = !effect.image.isNull() + // ? state->effect->framesCount() + // : 1; + + p.drawImage(r, frame.image); + p.drawImage( + QRect(QPoint(), effect.image.size() / factor), + effect.image); + + if (!frame.image.isNull()/* + && ((frame.index % effectsCount) <= effect.index)*/) { + state->lottie->markFrameShown(); + } + if (!effect.image.isNull()/* + && ((effect.index % framesCount) <= frame.index)*/) { + state->effect->markFrameShown(); + } + }, lifetime); + + return result; +} + +[[nodiscard]] not_null StickersPreview( + not_null parent, + not_null controller, + Fn readyCallback) { + const auto result = Ui::CreateChild(parent.get()); + result->show(); + + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect(QPoint(), size)); + }, result->lifetime()); + auto &lifetime = result->lifetime(); + + struct State { + std::vector> medias; + Ui::RpWidget *previous = nullptr; + Ui::RpWidget *current = nullptr; + Ui::RpWidget *next = nullptr; + Ui::Animations::Simple slide; + base::Timer toggleTimer; + bool toggleTimerPending = false; + Fn singleReadyCallback; + bool readyInvoked = false; + bool timerFired = false; + bool nextReady = false; + int index = 0; + }; + const auto premium = &controller->session().api().premium(); + const auto state = lifetime.make_state(); + const auto create = [=](std::shared_ptr media) { + const auto outer = Ui::CreateChild(result); + outer->show(); + + result->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + outer->resize(size); + }, outer->lifetime()); + + [[maybe_unused]] const auto sticker = StickerPreview( + outer, + controller, + media, + state->singleReadyCallback); + + return outer; + }; + const auto createNext = [=] { + state->nextReady = false; + state->next = create(state->medias[state->index]); + state->next->move(0, state->current->height()); + }; + const auto check = [=] { + if (!state->timerFired || !state->nextReady) { + return; + } + const auto animationCallback = [=] { + const auto top = int(base::SafeRound(state->slide.value(0.))); + state->previous->move(0, top - state->current->height()); + state->current->move(0, top); + if (!state->slide.animating()) { + delete base::take(state->previous); + state->timerFired = false; + state->toggleTimer.callOnce(kToggleStickerTimeout); + } + }; + state->timerFired = false; + ++state->index; + state->index %= state->medias.size(); + delete std::exchange(state->previous, state->current); + state->current = state->next; + createNext(); + state->slide.stop(); + state->slide.start( + animationCallback, + state->current->height(), + 0, + st::premiumSlideDuration, + anim::sineInOut); + }; + state->toggleTimer.setCallback([=] { + state->timerFired = true; + check(); + }); + state->singleReadyCallback = [=] { + if (!state->readyInvoked && readyCallback) { + state->readyInvoked = true; + readyCallback(); + } + if (!state->next) { + createNext(); + if (result->isHidden()) { + state->toggleTimerPending = true; + } else { + state->toggleTimer.callOnce(kToggleStickerTimeout); + } + } else { + state->nextReady = true; + check(); + } + }; + + result->shownValue( + ) | rpl::filter([=](bool shown) { + return shown && state->toggleTimerPending; + }) | rpl::start_with_next([=] { + state->toggleTimerPending = false; + state->toggleTimer.callOnce(kToggleStickerTimeout); + }, result->lifetime()); + + const auto fill = [=] { + const auto &list = premium->stickers(); + for (const auto &document : list) { + state->medias.push_back(document->createMediaView()); + } + if (!state->medias.empty()) { + state->current = create(state->medias.front()); + state->index = 1 % state->medias.size(); + state->current->move(0, 0); + } + }; + + fill(); + if (state->medias.empty()) { + premium->stickersUpdated( + ) | rpl::take(1) | rpl::start_with_next(fill, lifetime); + } + + return result; +} + +struct VideoPreviewDocument { + DocumentData *document = nullptr; + RectPart align = RectPart::Bottom; +}; + +[[nodiscard]] bool VideoAlignToTop(PremiumPreview section) { + return (section == PremiumPreview::MoreUpload) + || (section == PremiumPreview::NoAds); +} + +[[nodiscard]] DocumentData *LookupVideo( + not_null session, + PremiumPreview 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::AdvancedChatManagement: + return "advanced_chat_management"; + case PremiumPreview::ProfileBadge: return "profile_badge"; + case PremiumPreview::AnimatedUserpics: return "animated_userpics"; + } + return ""; + }(); + const auto &videos = session->api().premium().videos(); + const auto i = videos.find(name); + return (i != end(videos)) ? i->second.get() : nullptr; +} + +[[nodiscard]] QPainterPath GenerateFrame( + int left, + int top, + int width, + int height, + bool alignToBottom) { + const auto radius = style::ConvertScaleExact(20.); + const auto thickness = style::ConvertScaleExact(6.); + const auto skip = thickness / 2.; + auto path = QPainterPath(); + if (alignToBottom) { + path.moveTo(left - skip, top + height); + path.lineTo(left - skip, top - skip + radius); + path.arcTo( + left - skip, + top - skip, + radius * 2, + radius * 2, + 180, + -90); + path.lineTo(left + width + skip - radius, top - skip); + path.arcTo( + left + width + skip - 2 * radius, + top - skip, + radius * 2, + radius * 2, + 90, + -90); + path.lineTo(left + width + skip, top + height); + } else { + path.moveTo(left - skip, top); + path.lineTo(left - skip, top + height + skip - radius); + path.arcTo( + left - skip, + top + height + skip - 2 * radius, + radius * 2, + radius * 2, + 180, + 90); + path.lineTo(left + width + skip - radius, top + height + skip); + path.arcTo( + left + width + skip - 2 * radius, + top + height + skip - 2 * radius, + radius * 2, + radius * 2, + 270, + 90); + path.lineTo(left + width + skip, top); + } + return path; +} + +[[nodiscard]] not_null VideoPreview( + not_null parent, + not_null controller, + not_null document, + bool alignToBottom, + Fn readyCallback) { + const auto result = Ui::CreateChild(parent.get()); + result->show(); + + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + result->setGeometry(parent->rect()); + }, result->lifetime()); + auto &lifetime = result->lifetime(); + + auto shared = document->owner().streaming().sharedDocument( + document, + Data::FileOriginPremiumPreviews()); + if (!shared) { + return result; + } + + struct State { + State( + std::shared_ptr shared, + Fn waitingCallback) + : instance(shared, std::move(waitingCallback)) + , star(u":/gui/icons/settings/star.svg"_q) { + } + QImage blurred; + Media::Streaming::Instance instance; + std::shared_ptr media; + Ui::Animations::Basic loading; + QPainterPath frame; + QSvgRenderer star; + bool readyInvoked = false; + }; + const auto state = lifetime.make_state(std::move(shared), [] {}); + state->media = document->createMediaView(); + if (const auto image = state->media->thumbnailInline()) { + if (image->width() > 0) { + const auto width = st::premiumVideoWidth; + const auto height = std::max( + int(base::SafeRound( + float64(width) * image->height() / image->width())), + 1); + using Option = Images::Option; + const auto corners = alignToBottom + ? (Option::RoundSkipBottomLeft + | Option::RoundSkipBottomRight) + : (Option::RoundSkipTopLeft + | Option::RoundSkipTopRight); + state->blurred = Images::Prepare( + image->original(), + QSize(width, height) * style::DevicePixelRatio(), + { .options = (Option::Blur | Option::RoundLarge | corners) }); + } + } + const auto width = st::premiumVideoWidth; + const auto height = state->blurred.height() + ? (state->blurred.height() / state->blurred.devicePixelRatio()) + : width; + const auto left = (st::boxWideWidth - width) / 2; + const auto top = alignToBottom ? (st::premiumPreviewHeight - height) : 0; + state->frame = GenerateFrame(left, top, width, height, alignToBottom); + const auto check = [=] { + if (state->instance.playerLocked()) { + return; + } else if (state->instance.paused()) { + state->instance.resume(); + } + if (!state->instance.active() && !state->instance.failed()) { + auto options = Media::Streaming::PlaybackOptions(); + options.waitForMarkAsShown = true; + options.mode = ::Media::Streaming::Mode::Video; + options.loop = true; + state->instance.play(options); + } + }; + state->instance.player().updates( + ) | rpl::start_with_next_error([=](Media::Streaming::Update &&update) { + if (v::is(update.data) + || v::is(update.data)) { + if (!state->readyInvoked && readyCallback) { + state->readyInvoked = true; + readyCallback(); + } + result->update(); + } + }, [=](::Media::Streaming::Error &&error) { + result->update(); + }, state->instance.lifetime()); + + state->loading.init([=] { + if (!anim::Disabled()) { + result->update(); + } + }); + + result->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(result); + const auto paintFrame = [&](QColor color, float64 thickness) { + auto hq = PainterHighQualityEnabler(p); + auto pen = QPen(color); + pen.setWidthF(style::ConvertScaleExact(thickness)); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawPath(state->frame); + }; + + check(); + const auto corners = alignToBottom + ? (RectPart::TopLeft | RectPart::TopRight) + : (RectPart::BottomLeft | RectPart::BottomRight); + const auto ready = state->instance.player().ready() + && !state->instance.player().videoSize().isEmpty(); + const auto size = QSize(width, height) * style::DevicePixelRatio(); + const auto frame = !ready + ? state->blurred + : state->instance.frame({ + .resize = size, + .outer = size, + .radius = ImageRoundRadius::Large, + .corners = corners, + }); + paintFrame(QColor(0, 0, 0, 128), 12.); + p.drawImage(QRect(left, top, width, height), frame); + paintFrame(Qt::black, 6.6); + if (ready) { + state->loading.stop(); + state->instance.markFrameShown(); + } else { + if (!state->loading.animating()) { + state->loading.start(); + } + const auto progress = anim::Disabled() + ? 1. + : ((crl::now() % kStarPeriod) / float64(kStarPeriod)); + const auto ratio = anim::Disabled() + ? 1. + : (1. + cos(progress * 2 * M_PI)) / 2.; + const auto opacity = kStarOpacityOff + + (kStarOpacityOn - kStarOpacityOff) * ratio; + p.setOpacity(opacity); + + const auto starSize = st::premiumVideoStarSize; + state->star.render(&p, QRectF( + QPointF( + left + (width - starSize.width()) / 2., + top + (height - starSize.height()) / 2.), + starSize)); + } + }, lifetime); + + return result; +} + +[[nodiscard]] not_null GenericPreview( + not_null parent, + not_null controller, + PremiumPreview section, + Fn readyCallback) { + const auto result = Ui::CreateChild(parent.get()); + result->show(); + + parent->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + result->setGeometry(QRect(QPoint(), size)); + }, result->lifetime()); + auto &lifetime = result->lifetime(); + + struct State { + std::vector> medias; + Ui::RpWidget *single = nullptr; + }; + const auto session = &controller->session(); + const auto state = lifetime.make_state(); + const auto create = [=] { + const auto document = LookupVideo(session, section); + if (!document) { + return; + } + state->single = VideoPreview( + result, + controller, + document, + !VideoAlignToTop(section), + readyCallback); + }; + create(); + if (!state->single) { + session->api().premium().videosUpdated( + ) | rpl::take(1) | rpl::start_with_next(create, lifetime); + } + + return result; +} + + +class ReactionPreview final { +public: + ReactionPreview( + not_null controller, + const Data::Reaction &reaction, + ReactionDisableType type, + Fn update, + QPoint position); + + [[nodiscard]] bool playsEffect() const; + void paint(Painter &p); + void paintEffect(QPainter &p); + + void setOver(bool over); + void startAnimations(); + void cancelAnimations(); + [[nodiscard]] bool ready() const; + [[nodiscard]] bool disabled() const; + [[nodiscard]] QRect geometry() const; + +private: + void checkReady(); + + const not_null _controller; + const Fn _update; + const QPoint _position; + Ui::Animations::Simple _scale; + std::shared_ptr _centerMedia; + std::shared_ptr _aroundMedia; + std::unique_ptr _center; + std::unique_ptr _around; + std::unique_ptr _pathGradient; + QImage _cache1; + QImage _cache2; + bool _over = false; + bool _disabled = false; + bool _playRequested = false; + bool _aroundPlaying = false; + bool _centerPlaying = false; + rpl::lifetime _lifetime; + +}; + +ReactionPreview::ReactionPreview( + not_null controller, + const Data::Reaction &reaction, + ReactionDisableType type, + Fn update, + QPoint position) +: _controller(controller) +, _update(std::move(update)) +, _position(position) +, _centerMedia(reaction.centerIcon->createMediaView()) +, _aroundMedia(reaction.aroundAnimation->createMediaView()) +, _pathGradient( + HistoryView::MakePathShiftGradient( + controller->chatStyle(), + _update)) +, _disabled(type != ReactionDisableType::None) { + _centerMedia->checkStickerLarge(); + _aroundMedia->checkStickerLarge(); + checkReady(); + if (!_center || !_around) { + _controller->session().downloaderTaskFinished( + ) | rpl::take_while([=] { + checkReady(); + return !_center || !_around; + }) | rpl::start(_lifetime); + } +} + +QRect ReactionPreview::geometry() const { + const auto xsize = st::premiumReactionWidthSkip; + const auto ysize = st::premiumReactionHeightSkip; + return { _position - QPoint(xsize / 2, ysize / 2), QSize(xsize, ysize) }; +} + +void ReactionPreview::checkReady() { + const auto make = [&]( + const std::shared_ptr &media, + int size) { + const auto bytes = media->bytes(); + const auto filepath = media->owner()->filepath(); + auto result = ChatHelpers::LottiePlayerFromDocument( + media.get(), + nullptr, + ChatHelpers::StickerLottieSize::PremiumReactionPreview, + QSize(size, size) * style::DevicePixelRatio(), + Lottie::Quality::Default); + result->updates() | rpl::start_with_next(_update, _lifetime); + return result; + }; + if (!_center && _centerMedia->loaded()) { + _center = make(_centerMedia, st::premiumReactionSize); + } + if (!_around && _aroundMedia->loaded()) { + _around = make(_aroundMedia, st::premiumReactionAround); + } +} + +void ReactionPreview::setOver(bool over) { + if (_over == over || _disabled) { + return; + } + _over = over; + const auto from = st::premiumReactionScale; + _scale.start( + _update, + over ? from : 1., + over ? 1. : from, + st::slideWrapDuration); +} + +void ReactionPreview::startAnimations() { + if (_disabled) { + return; + } + _playRequested = true; + if (!_center || !_center->ready() || !_around || !_around->ready()) { + return; + } + _update(); +} + +void ReactionPreview::cancelAnimations() { + _playRequested = false; +} + +bool ReactionPreview::ready() const { + return _center && _center->ready(); +} + +bool ReactionPreview::disabled() const { + return _disabled; +} + +void ReactionPreview::paint(Painter &p) { + const auto center = st::premiumReactionSize; + const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale); + const auto inner = QRect( + -center / 2, + -center / 2, + center, + center + ).translated(_position); + auto hq = PainterHighQualityEnabler(p); + const auto centerReady = _center && _center->ready(); + const auto staticCenter = centerReady && !_centerPlaying; + const auto use1 = staticCenter && scale == 1.; + const auto use2 = staticCenter && scale == st::premiumReactionScale; + const auto useScale = (!use1 && !use2 && scale != 1.); + if (useScale) { + p.save(); + p.translate(inner.center()); + p.scale(scale, scale); + p.translate(-inner.center()); + } + if (_disabled) { + p.setOpacity(kDisabledOpacity); + } + checkReady(); + if (centerReady) { + if (use1 || use2) { + auto &cache = use1 ? _cache1 : _cache2; + const auto use = int(std::round(center * scale)); + const auto rect = QRect(-use / 2, -use / 2, use, use).translated( + _position); + if (cache.isNull()) { + cache = _center->frame().scaledToWidth( + use * style::DevicePixelRatio(), + Qt::SmoothTransformation); + } + p.drawImage(rect, cache); + } else { + p.drawImage(inner, _center->frame()); + } + if (_centerPlaying) { + const auto almost = (_center->frameIndex() + 1) + == _center->framesCount(); + const auto marked = _center->markFrameShown(); + if (almost && marked) { + _centerPlaying = false; + } + } + if (_around + && _around->ready() + && !_aroundPlaying + && !_centerPlaying + && _playRequested) { + _aroundPlaying = _centerPlaying = true; + _playRequested = false; + } + } else { + p.setBrush(_controller->chatStyle()->msgServiceBg()); + ChatHelpers::PaintStickerThumbnailPath( + p, + _centerMedia.get(), + inner, + _pathGradient.get()); + } + if (useScale) { + p.restore(); + } else if (_disabled) { + p.setOpacity(1.); + } +} + +bool ReactionPreview::playsEffect() const { + return _aroundPlaying; +} + +void ReactionPreview::paintEffect(QPainter &p) { + if (!_aroundPlaying) { + return; + } + const auto size = st::premiumReactionAround; + const auto outer = QRect(-size/2, -size/2, size, size).translated( + _position); + const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale); + auto hq = PainterHighQualityEnabler(p); + if (scale != 1.) { + p.save(); + p.translate(outer.center()); + p.scale(scale, scale); + p.translate(-outer.center()); + } + p.drawImage(outer, _around->frame()); + if (scale != 1.) { + p.restore(); + } + if (_aroundPlaying) { + const auto almost = (_around->frameIndex() + 1) + == _around->framesCount(); + const auto marked = _around->markFrameShown(); + if (almost && marked) { + _aroundPlaying = false; + } + } +} + +[[nodiscard]] not_null ReactionsPreview( + not_null parent, + not_null controller, + const base::flat_map &disabled, + Fn readyCallback) { + struct State { + std::vector> entries; + Ui::Text::String bottom; + int selected = -1; + bool readyInvoked = false; + }; + const auto result = Ui::CreateChild(parent.get()); + result->show(); + + auto &lifetime = result->lifetime(); + const auto state = lifetime.make_state(); + + result->setMouseTracking(true); + + parent->sizeValue( + ) | rpl::start_with_next([=] { + result->setGeometry(parent->rect()); + }, result->lifetime()); + + using namespace HistoryView; + const auto list = controller->session().data().reactions().list( + Data::Reactions::Type::Active); + const auto count = ranges::count(list, true, &Data::Reaction::premium); + const auto rows = (count + kReactionsPerRow - 1) / kReactionsPerRow; + const auto inrowmax = (count + rows - 1) / rows; + const auto inrowless = (inrowmax * rows - count); + const auto inrowmore = rows - inrowless; + const auto inmaxrows = inrowmore * inrowmax; + auto index = 0; + auto disableType = ReactionDisableType::None; + for (const auto &reaction : list) { + if (!reaction.premium) { + continue; + } + const auto inrow = (index < inmaxrows) ? inrowmax : (inrowmax - 1); + const auto row = (index < inmaxrows) + ? (index / inrow) + : (inrowmore + ((index - inmaxrows) / inrow)); + const auto column = (index < inmaxrows) + ? (index % inrow) + : ((index - inmaxrows) % inrow); + ++index; + if (!reaction.centerIcon || !reaction.aroundAnimation) { + continue; + } + const auto i = disabled.find(reaction.emoji); + const auto disable = (i != end(disabled)) + ? i->second + : ReactionDisableType::None; + if (disable != ReactionDisableType::None) { + disableType = disable; + } + state->entries.push_back(std::make_unique( + controller, + reaction, + disable, + [=] { result->update(); }, + QPoint(ComputeX(column, inrow), ComputeY(row, rows)))); + } + + const auto bottom1 = tr::lng_reaction_premium_info(tr::now); + const auto bottom2 = (disableType == ReactionDisableType::None) + ? QString() + : (disableType == ReactionDisableType::Group) + ? tr::lng_reaction_premium_no_group(tr::now) + : tr::lng_reaction_premium_no_channel(tr::now); + state->bottom.setText( + st::defaultTextStyle, + (bottom1 + '\n' + bottom2).trimmed()); + + result->paintRequest( + ) | rpl::start_with_next([=] { + auto p = Painter(result); + auto effects = std::vector>(); + auto ready = 0; + for (const auto &entry : state->entries) { + entry->paint(p); + if (entry->ready()) { + ++ready; + } + if (entry->playsEffect()) { + effects.push_back([&] { + entry->paintEffect(p); + }); + } + } + if (!state->readyInvoked + && readyCallback + && ready > 0 + && ready == state->entries.size()) { + state->readyInvoked = true; + readyCallback(); + + } + const auto padding = st::boxRowPadding; + const auto available = parent->width() + - padding.left() + - padding.right(); + const auto top = st::premiumReactionInfoTop + + ((state->bottom.maxWidth() > available) + ? st::normalFont->height + : 0); + p.setPen(st::premiumButtonFg); + state->bottom.draw( + p, + padding.left(), + top, + available, + style::al_top); + for (const auto &paint : effects) { + paint(); + } + }, lifetime); + + const auto lookup = [=](QPoint point) { + auto index = 0; + for (const auto &entry : state->entries) { + if (entry->geometry().contains(point) && !entry->disabled()) { + return index; + } + ++index; + } + return -1; + }; + const auto select = [=](int index) { + const auto wasInside = (state->selected >= 0); + const auto nowInside = (index >= 0); + if (state->selected != index) { + if (wasInside) { + state->entries[state->selected]->setOver(false); + } + if (nowInside) { + state->entries[index]->setOver(true); + } + state->selected = index; + } + if (wasInside != nowInside) { + result->setCursor(nowInside + ? style::cur_pointer + : style::cur_default); + } + }; + result->events( + ) | rpl::start_with_next([=](not_null event) { + if (event->type() == QEvent::MouseButtonPress) { + const auto point = static_cast(event.get())->pos(); + if (state->selected >= 0) { + state->entries[state->selected]->cancelAnimations(); + } + if (const auto index = lookup(point); index >= 0) { + state->entries[index]->startAnimations(); + } + } else if (event->type() == QEvent::MouseMove) { + const auto point = static_cast(event.get())->pos(); + select(lookup(point)); + } else if (event->type() == QEvent::Leave) { + select(-1); + } + }, lifetime); + + return result; +} + +[[nodiscard]] not_null GenerateDefaultPreview( + not_null parent, + not_null controller, + PremiumPreview section, + Fn readyCallback) { + switch (section) { + case PremiumPreview::Reactions: + return ReactionsPreview(parent, controller, {}, readyCallback); + case PremiumPreview::Stickers: + return StickersPreview(parent, controller, readyCallback); + default: + return GenericPreview(parent, controller, section, readyCallback); + } +} + +[[nodiscard]] object_ptr CreateGradientButton( + QWidget *parent, + QGradientStops stops) { + return object_ptr(parent, std::move(stops)); +} + +[[nodiscard]] object_ptr CreatePremiumButton( + QWidget *parent) { + return CreateGradientButton(parent, Ui::Premium::ButtonGradientStops()); +} + +[[nodiscard]] object_ptr CreateUnlockButton( + QWidget *parent, + rpl::producer text) { + auto result = CreatePremiumButton(parent); + const auto &st = st::premiumPreviewBox.button; + result->resize(result->width(), st.height); + + const auto label = Ui::CreateChild( + result.data(), + std::move(text), + st::premiumPreviewButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + rpl::combine( + result->widthValue(), + label->widthValue() + ) | rpl::start_with_next([=](int outer, int width) { + label->moveToLeft( + (outer - width) / 2, + st::premiumPreviewBox.button.textTop, + outer); + }, label->lifetime()); + + return result; +} + +[[nodiscard]] object_ptr CreateSwitch( + not_null parent, + not_null*> selected) { + const auto padding = st::premiumDotPadding; + const auto width = padding.left() + st::premiumDot + padding.right(); + const auto height = padding.top() + st::premiumDot + padding.bottom(); + const auto stops = Ui::Premium::ButtonGradientStops(); + auto result = object_ptr(parent.get(), height); + const auto raw = result.data(); + for (auto i = 0; i != kPreviewsCount; ++i) { + const auto section = PremiumPreview(i); + const auto button = Ui::CreateChild(raw); + parent->widthValue( + ) | rpl::start_with_next([=](int outer) { + const auto full = width * kPreviewsCount; + const auto left = (outer - full) / 2 + (i * width); + button->setGeometry(left, 0, width, height); + }, button->lifetime()); + button->setClickedCallback([=] { + *selected = section; + }); + button->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(button); + auto hq = PainterHighQualityEnabler(p); + p.setBrush((selected->current() == section) + ? anim::gradient_color_at( + stops, + float64(i) / (kPreviewsCount - 1)) + : st::windowBgRipple->c); + p.setPen(Qt::NoPen); + p.drawEllipse( + button->rect().marginsRemoved(st::premiumDotPadding)); + }, button->lifetime()); + selected->changes( + ) | rpl::start_with_next([=] { + button->update(); + }, button->lifetime()); + } + return result; +} + +void PreviewBox( + not_null box, + not_null controller, + const Descriptor &descriptor, + const std::shared_ptr &media, + const QImage &back) { + const auto single = st::boxWideWidth; + const auto size = QSize(single, st::premiumPreviewHeight); + box->setWidth(size.width()); + box->setNoContentMargin(true); + + const auto outer = box->addRow( + ChatBackPreview(box, size.height(), back), + {}); + + struct Hiding { + not_null widget; + int leftFrom = 0; + int leftTill = 0; + }; + struct State { + int leftFrom = 0; + Ui::RpWidget *content = nullptr; + Ui::RpWidget *stickersPreload = nullptr; + bool stickersPreloadReady = false; + Ui::RpWidget *reactionsPreload = nullptr; + bool reactionsPreloadReady = false; + bool preloadScheduled = false; + bool showFinished = false; + Ui::Animations::Simple animation; + Fn preload; + std::vector hiding; + rpl::variable selected; + }; + const auto state = outer->lifetime().make_state(); + state->selected = descriptor.section; + + const auto move = [=](int delta) { + using Type = PremiumPreview; + const auto count = int(Type::kCount); + const auto now = state->selected.current(); + state->selected = Type((int(now) + count + delta) % count); + }; + + const auto buttonsParent = box->verticalLayout().get(); + const auto close = Ui::CreateChild( + buttonsParent, + st::settingsPremiumTopBarClose); + close->setClickedCallback([=] { box->closeBox(); }); + + const auto left = Ui::CreateChild( + buttonsParent, + st::settingsPremiumMoveLeft); + left->setClickedCallback([=] { move(-1); }); + + const auto right = Ui::CreateChild( + buttonsParent, + st::settingsPremiumMoveRight); + right->setClickedCallback([=] { move(1); }); + + buttonsParent->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto outerHeight = st::premiumPreviewHeight; + close->moveToRight(0, 0, width); + left->moveToLeft(0, (outerHeight - left->height()) / 2, width); + right->moveToRight(0, (outerHeight - right->height()) / 2, width); + }, close->lifetime()); + + state->preload = [=] { + if (!state->showFinished) { + state->preloadScheduled = true; + return; + } + const auto now = state->selected.current(); + if (now != PremiumPreview::Stickers && !state->stickersPreload) { + const auto ready = [=] { + if (state->stickersPreload) { + state->stickersPreloadReady = true; + } else { + state->preload(); + } + }; + state->stickersPreload = GenerateDefaultPreview( + outer, + controller, + PremiumPreview::Stickers, + ready); + state->stickersPreload->hide(); + } + if (now != PremiumPreview::Reactions && !state->reactionsPreload) { + const auto ready = [=] { + if (state->reactionsPreload) { + state->reactionsPreloadReady = true; + } else { + state->preload(); + } + }; + state->reactionsPreload = GenerateDefaultPreview( + outer, + controller, + PremiumPreview::Reactions, + ready); + state->reactionsPreload->hide(); + } + }; + + switch (descriptor.section) { + case PremiumPreview::Stickers: + state->content = media + ? StickerPreview(outer, controller, media, state->preload) + : StickersPreview(outer, controller, state->preload); + break; + case PremiumPreview::Reactions: + state->content = ReactionsPreview( + outer, + controller, + descriptor.disabled, + state->preload); + break; + default: + state->content = GenericPreview( + outer, + controller, + descriptor.section, + state->preload); + break; + } + + state->selected.value( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](PremiumPreview was, PremiumPreview now) { + const auto animationCallback = [=] { + if (!state->animation.animating()) { + for (const auto &hiding : base::take(state->hiding)) { + delete hiding.widget; + } + state->leftFrom = 0; + state->content->move(0, 0); + } else { + const auto progress = state->animation.value(1.); + state->content->move( + anim::interpolate(state->leftFrom, 0, progress), + 0); + for (const auto &hiding : state->hiding) { + hiding.widget->move(anim::interpolate( + hiding.leftFrom, + hiding.leftTill, + progress), 0); + } + } + }; + animationCallback(); + const auto toLeft = int(now) > int(was); + auto start = state->content->x() + (toLeft ? single : -single); + for (const auto &hiding : state->hiding) { + const auto left = hiding.widget->x(); + if (toLeft && left + single > start) { + start = left + single; + } else if (!toLeft && left - single < start) { + start = left - single; + } + } + for (auto &hiding : state->hiding) { + hiding.leftFrom = hiding.widget->x(); + hiding.leftTill = hiding.leftFrom - start; + } + state->hiding.push_back({ + .widget = state->content, + .leftFrom = state->content->x(), + .leftTill = state->content->x() - start, + }); + state->leftFrom = start; + if (now == PremiumPreview::Stickers && state->stickersPreload) { + state->content = base::take(state->stickersPreload); + state->content->show(); + if (base::take(state->stickersPreloadReady)) { + state->preload(); + } + } else if (now == PremiumPreview::Reactions + && state->reactionsPreload) { + state->content = base::take(state->reactionsPreload); + state->content->show(); + if (base::take(state->reactionsPreloadReady)) { + state->preload(); + } + } else { + state->content = GenerateDefaultPreview( + outer, + controller, + now, + state->preload); + } + state->animation.stop(); + state->animation.start( + animationCallback, + 0., + 1., + st::premiumSlideDuration, + anim::sineInOut); + }, outer->lifetime()); + + auto title = state->selected.value( + ) | rpl::map([=](PremiumPreview section) { + return SectionTitle(section); + }) | rpl::flatten_latest(); + + auto text = state->selected.value( + ) | rpl::map([=](PremiumPreview section) { + return SectionAbout(section); + }) | rpl::flatten_latest(); + + const auto padding = st::premiumPreviewAboutPadding; + const auto available = size.width() - padding.left() - padding.right(); + auto titleLabel = object_ptr( + box, + std::move(title), + st::premiumPreviewAboutTitle); + titleLabel->resizeToWidth(available); + box->addRow( + object_ptr>( + box, + std::move(titleLabel)), + st::premiumPreviewAboutTitlePadding); + auto textLabel = object_ptr( + box, + std::move(text), + st::premiumPreviewAbout); + textLabel->resizeToWidth(available); + box->addRow( + object_ptr>(box, std::move(textLabel)), + padding); + box->addRow( + CreateSwitch(box->verticalLayout(), &state->selected), + st::premiumDotsMargin); + const auto showFinished = [=] { + state->showFinished = true; + if (base::take(state->preloadScheduled)) { + state->preload(); + } + }; + if (descriptor.fromSettings && controller->session().premium()) { + box->setShowFinishedCallback(showFinished); + box->addButton(tr::lng_close(), [=] { box->closeBox(); }); + } else { + box->setStyle(st::premiumPreviewBox); + const auto buttonPadding = st::premiumPreviewBox.buttonPadding; + const auto width = size.width() + - buttonPadding.left() + - buttonPadding.right(); + const auto computeRef = [=] { + return Settings::LookupPremiumRef(state->selected.current()); + }; + auto unlock = state->selected.value( + ) | rpl::map([=](PremiumPreview section) { + return (section == PremiumPreview::Reactions) + ? tr::lng_premium_unlock_reactions() + : (section == PremiumPreview::Stickers) + ? tr::lng_premium_unlock_stickers() + : tr::lng_premium_more_about(); + }) | rpl::flatten_latest(); + auto button = descriptor.fromSettings + ? object_ptr::fromRaw( + Settings::CreateSubscribeButton(controller, box, computeRef)) + : CreateUnlockButton(box, std::move(unlock)); + button->resizeToWidth(width); + if (!descriptor.fromSettings) { + button->setClickedCallback([=] { + Settings::ShowPremium( + controller, + Settings::LookupPremiumRef(state->selected.current())); + }); + } + box->setShowFinishedCallback([=, raw = button.data()]{ + showFinished(); + raw->startGlareAnimation(); + }); + box->addButton(std::move(button)); + } + + if (descriptor.fromSettings) { + Data::AmPremiumValue( + &controller->session() + ) | rpl::skip(1) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + } + + box->events( + ) | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::KeyPress) { + const auto key = static_cast(e.get())->key(); + if (key == Qt::Key_Left) { + move(-1); + } else if (key == Qt::Key_Right) { + move(1); + } + } + }, box->lifetime()); + + if (const auto &hidden = descriptor.hiddenCallback) { + box->boxClosing() | rpl::start_with_next(hidden, box->lifetime()); + } +} + +void Show( + not_null controller, + const Descriptor &descriptor, + const std::shared_ptr &media, + QImage back) { + controller->show(Box(PreviewBox, controller, descriptor, media, back)); +} + +void Show(not_null controller, QImage back) { + auto &list = Preloads(); + for (auto i = begin(list); i != end(list);) { + const auto already = i->controller.get(); + if (!already) { + i = list.erase(i); + } else if (already == controller) { + Show(controller, i->descriptor, i->media, back); + i = list.erase(i); + return; + } else { + ++i; + } + } +} + +void Show( + not_null controller, + Descriptor &&descriptor) { + if (!controller->session().premiumPossible()) { + controller->show(Box(PremiumUnavailableBox)); + return; + } + auto &list = Preloads(); + for (auto i = begin(list); i != end(list);) { + const auto already = i->controller.get(); + if (!already) { + i = list.erase(i); + } else if (already == controller) { + if (i->descriptor == descriptor) { + return; + } + i->descriptor = descriptor; + i->media = descriptor.requestedSticker + ? descriptor.requestedSticker->createMediaView() + : nullptr; + if (const auto &media = i->media) { + PreloadSticker(media); + } + return; + } else { + ++i; + } + } + + const auto weak = base::make_weak(controller.get()); + list.push_back({ + .descriptor = descriptor, + .media = (descriptor.requestedSticker + ? descriptor.requestedSticker->createMediaView() + : nullptr), + .controller = weak, + }); + if (const auto &media = list.back().media) { + PreloadSticker(media); + } + + const auto fill = QSize(st::boxWideWidth, st::boxWideWidth); + const auto stops = Ui::Premium::LimitGradientStops(); + crl::async([=] { + const auto factor = style::DevicePixelRatio(); + auto cropped = QImage( + fill * factor, + QImage::Format_ARGB32_Premultiplied); + cropped.setDevicePixelRatio(factor); + auto p = QPainter(&cropped); + auto gradient = QLinearGradient(0, fill.height(), fill.width(), 0); + gradient.setStops(stops); + p.fillRect(QRect(QPoint(), fill), gradient); + p.end(); + + const auto result = Images::Round( + std::move(cropped), + Images::CornersMask(st::boxRadius), + RectPart::TopLeft | RectPart::TopRight); + crl::on_main([=] { + if (const auto strong = weak.get()) { + Show(strong, result); + } + }); + }); +} + +} // namespace + +void ShowStickerPreviewBox( + not_null controller, + not_null document) { + Show(controller, Descriptor{ + .section = PremiumPreview::Stickers, + .requestedSticker = document, + }); +} + +void ShowPremiumPreviewBox( + not_null controller, + PremiumPreview section, + const base::flat_map &disabled) { + Show(controller, Descriptor{ + .section = section, + .disabled = disabled, + }); +} + +void ShowPremiumPreviewToBuy( + not_null controller, + PremiumPreview section, + Fn hiddenCallback) { + Show(controller, Descriptor{ + .section = section, + .fromSettings = true, + .hiddenCallback = std::move(hiddenCallback), + }); +} + +void PremiumUnavailableBox(not_null box) { + Ui::ConfirmBox(box, { + .text = tr::lng_premium_unavailable( + tr::now, + Ui::Text::RichLangValue), + .inform = true, + }); +} + +void DoubledLimitsPreviewBox( + not_null box, + not_null session) { + const auto limits = Data::PremiumLimits(session); + auto entries = std::vector(); + { + const auto premium = limits.channelsPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_channels(), + tr::lng_premium_double_limits_about_channels( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.channelsDefault(), + premium, + }); + } + { + const auto premium = limits.dialogsFolderPinnedPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_pins(), + tr::lng_premium_double_limits_about_pins( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.dialogsFolderPinnedDefault(), + premium, + }); + } + { + const auto premium = limits.channelsPublicPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_links(), + tr::lng_premium_double_limits_about_links( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.channelsPublicDefault(), + premium, + }); + } + { + const auto premium = limits.gifsPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_gifs(), + tr::lng_premium_double_limits_about_gifs( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.gifsDefault(), + premium, + }); + } + { + const auto premium = limits.stickersFavedPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_stickers(), + tr::lng_premium_double_limits_about_stickers( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.stickersFavedDefault(), + premium, + }); + } + { + const auto premium = limits.captionLengthPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_bio(), + tr::lng_premium_double_limits_about_bio( + Ui::Text::RichLangValue), + limits.captionLengthDefault(), + premium, + }); + } + { + const auto premium = limits.captionLengthPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_captions(), + tr::lng_premium_double_limits_about_captions( + Ui::Text::RichLangValue), + limits.captionLengthDefault(), + premium, + }); + } + { + const auto premium = limits.dialogFiltersPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_folders(), + tr::lng_premium_double_limits_about_folders( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.dialogFiltersDefault(), + premium, + }); + } + { + const auto premium = limits.dialogFiltersChatsPremium(); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_folder_chats(), + tr::lng_premium_double_limits_about_folder_chats( + lt_count, + rpl::single(float64(premium)), + Ui::Text::RichLangValue), + limits.dialogFiltersChatsDefault(), + premium, + }); + } + const auto nextMax = session->domain().maxAccounts() + 1; + const auto till = (nextMax >= Main::Domain::kPremiumMaxAccounts) + ? QString::number(Main::Domain::kPremiumMaxAccounts) + : (QString::number(nextMax) + QChar('+')); + entries.push_back(Ui::Premium::ListEntry{ + tr::lng_premium_double_limits_subtitle_accounts(), + tr::lng_premium_double_limits_about_accounts( + lt_count, + rpl::single(float64(Main::Domain::kPremiumMaxAccounts)), + Ui::Text::RichLangValue), + Main::Domain::kMaxAccounts, + Main::Domain::kPremiumMaxAccounts, + till, + }); + Ui::Premium::ShowListBox(box, std::move(entries)); +} diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h new file mode 100644 index 00000000000000..0cd6fa341ee972 --- /dev/null +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -0,0 +1,61 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class DocumentData; + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Main { +class Session; +} // namespace Main + +void ShowStickerPreviewBox( + not_null controller, + not_null document); + +void DoubledLimitsPreviewBox( + not_null box, + not_null session); + +enum class PremiumPreview { + MoreUpload, + FasterDownload, + VoiceToText, + NoAds, + Reactions, + Stickers, + AdvancedChatManagement, + ProfileBadge, + AnimatedUserpics, + + kCount, +}; +enum class ReactionDisableType { + None, + Group, + Channel, +}; + +void ShowPremiumPreviewBox( + not_null controller, + PremiumPreview section, + const base::flat_map &disabled = {}); + +void ShowPremiumPreviewToBuy( + not_null controller, + PremiumPreview section, + Fn hiddenCallback); + +void PremiumUnavailableBox(not_null box); diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index 8c0bbe5503b73c..166c4797e512d7 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -8,6 +8,7 @@ For license and copyright information please follow this link: #include "boxes/reactions_settings_box.h" #include "base/unixtime.h" +#include "data/data_user.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_message_reactions.h" @@ -19,12 +20,15 @@ For license and copyright information please follow this link: #include "history/view/history_view_react_button.h" // DefaultIconFactory #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" +#include "boxes/premium_preview_box.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "settings/settings_premium.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/scroll_content_shadow.h" #include "ui/layers/generic_box.h" +#include "ui/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" @@ -418,12 +422,18 @@ void ReactionsSettingsBox( }; auto firstCheckedButton = (Ui::RpWidget*)(nullptr); + const auto premiumPossible = controller->session().premiumPossible(); for (const auto &r : reactions.list(Data::Reactions::Type::Active)) { const auto button = Settings::AddButton( container, rpl::single(base::duplicate(r.title)), st::settingsButton); + const auto premium = r.premium; + if (premium && !premiumPossible) { + continue; + } + const auto iconSize = st::settingsReactionSize; AddReactionLottieIcon( button, @@ -443,6 +453,12 @@ void ReactionsSettingsBox( &button->lifetime()); button->setClickedCallback([=, emoji = r.emoji] { + if (premium && !controller->session().premium()) { + ShowPremiumPreviewBox( + controller, + PremiumPreview::Reactions); + return; + } checkButton(button); state->selectedEmoji = emoji; }); diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp index 1b504973adcca7..23186daffe86f3 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.cpp +++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp @@ -15,6 +15,7 @@ For license and copyright information please follow this link: #include "base/timer_rpl.h" #include "base/unixtime.h" #include "core/application.h" +#include "core/core_settings.h" #include "core/file_utilities.h" #include "core/mime_type.h" #include "data/data_document.h" diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index a76121c0ae9745..13f697c2bd41af 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -25,6 +25,8 @@ For license and copyright information please follow this link: #include "core/file_utilities.h" #include "core/mime_type.h" #include "base/event_filter.h" +#include "boxes/premium_limits_box.h" +#include "ui/boxes/confirm_box.h" #include "ui/effects/animations.h" #include "ui/effects/scroll_content_shadow.h" #include "ui/widgets/checkbox.h" @@ -44,6 +46,8 @@ For license and copyright information please follow this link: #include "ui/controls/emoji_button.h" #include "lottie/lottie_single_player.h" #include "data/data_document.h" +#include "data/data_user.h" +#include "data/data_premium_limits.h" #include "media/clip/media_clip_reader.h" #include "api/api_common.h" #include "window/window_session_controller.h" @@ -59,6 +63,8 @@ For license and copyright information please follow this link: namespace { +constexpr auto kMaxMessageLength = 4096; + using Ui::SendFilesWay; inline bool CanAddUrls(const QList &urls) { @@ -68,16 +74,19 @@ inline bool CanAddUrls(const QList &urls) { void FileDialogCallback( FileDialog::OpenResult &&result, Fn checkResult, - Fn callback) { - auto showError = [](tr::phrase<> text) { - Ui::Toast::Show(text(tr::now)); + Fn callback, + bool premium, + not_null toastParent) { + auto showError = [=](tr::phrase<> text) { + Ui::Toast::Show(toastParent, text(tr::now)); }; auto list = Storage::PreparedFileFromFilesDialog( std::move(result), checkResult, showError, - st::sendMediaPreviewSize); + st::sendMediaPreviewSize, + premium); if (!list) { return; @@ -387,20 +396,24 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem) { } void SendFilesBox::openDialogToAddFileToAlbum() { + const auto toastParent = Ui::BoxShow(this).toastParent(); const auto checkResult = [=](const Ui::PreparedList &list) { if (_sendLimit != SendLimit::One) { return true; } else if (!_list.canBeSentInSlowmodeWith(list)) { - Ui::Toast::Show(tr::lng_slowmode_no_many(tr::now)); + Ui::Toast::Show(toastParent, tr::lng_slowmode_no_many(tr::now)); return false; } return true; }; const auto callback = [=](FileDialog::OpenResult &&result) { + const auto premium = _controller->session().premium(); FileDialogCallback( std::move(result), checkResult, - [=](Ui::PreparedList list) { addFiles(std::move(list)); }); + [=](Ui::PreparedList list) { addFiles(std::move(list)); }, + premium, + toastParent); }; FileDialog::GetOpenPaths( @@ -540,6 +553,7 @@ void SendFilesBox::pushBlock(int from, int till) { }); }, widget->lifetime()); + const auto toastParent = Ui::BoxShow(this).toastParent(); block.itemReplaceRequest( ) | rpl::start_with_next([=](int index) { const auto replace = [=](Ui::PreparedList list) { @@ -560,16 +574,21 @@ void SendFilesBox::pushBlock(int from, int till) { _list.files.push_back(std::move(removing)); std::swap(_list.files[index], _list.files.back()); if (!result) { - Ui::Toast::Show(tr::lng_slowmode_no_many(tr::now)); + Ui::Toast::Show( + toastParent, + tr::lng_slowmode_no_many(tr::now)); return false; } return true; }; const auto callback = [=](FileDialog::OpenResult &&result) { + const auto premium = _controller->session().premium(); FileDialogCallback( std::move(result), checkResult, - replace); + replace, + premium, + toastParent); }; FileDialog::GetOpenPath( @@ -653,8 +672,7 @@ void SendFilesBox::updateSendWayControlsVisibility() { } void SendFilesBox::setupCaption() { - _caption->setMaxLength( - _controller->session().serverConfig().captionLengthMax); + _caption->setMaxLength(kMaxMessageLength); _caption->setSubmitSettings( Core::App().settings().sendSubmitWay()); connect(_caption, &Ui::InputField::resized, [=] { @@ -765,10 +783,14 @@ bool SendFilesBox::canAddFiles(not_null data) const { } bool SendFilesBox::addFiles(not_null data) { + const auto premium = _controller->session().premium(); auto list = [&] { const auto urls = data->hasUrls() ? data->urls() : QList(); auto result = CanAddUrls(urls) - ? Storage::PrepareMediaList(urls, st::sendMediaPreviewSize) + ? Storage::PrepareMediaList( + urls, + st::sendMediaPreviewSize, + premium) : Ui::PreparedList( Ui::PreparedList::Error::EmptyFile, QString()); @@ -964,6 +986,20 @@ void SendFilesBox::saveSendWaySettings() { } } +bool SendFilesBox::validateLength(const QString &text) const { + const auto session = &_controller->session(); + const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); + const auto remove = int(text.size()) - limit; + const auto way = _sendWay.current(); + if (remove <= 0 + || !_list.canAddCaption( + way.groupFiles() && way.sendImagesAsPhotos())) { + return true; + } + _controller->show(Box(CaptionLimitReachedBox, session, remove)); + return false; +} + void SendFilesBox::send( Api::SendOptions options, bool ctrlShiftEnter) { @@ -992,6 +1028,9 @@ void SendFilesBox::send( auto caption = (_caption && !_caption->isHidden()) ? _caption->getTextWithAppliedMarkdown() : TextWithTags(); + if (!validateLength(caption.text)) { + return; + } _confirmedCallback( std::move(_list), _sendWay.current(), diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 774f3ae828b5e5..e081c96621f964 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -120,6 +120,7 @@ class SendFilesBox : public Ui::BoxContent { void initSendWay(); void initPreview(); + bool validateLength(const QString &text) const; void refreshControls(); void setupSendWayControls(); void setupCaption(); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index a80b8586263c08..5d5ad9eaa663cd 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -47,6 +47,7 @@ For license and copyright information please follow this link: #include "data/data_changes.h" #include "main/main_session.h" #include "core/application.h" +#include "core/core_settings.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" @@ -1283,7 +1284,7 @@ void FastShareMessage( } if (item->hasDirectLink()) { using namespace HistoryView; - CopyPostLink(session, item->fullId(), Context::History); + CopyPostLink(controller, item->fullId(), Context::History); } else if (const auto bot = item->getMessageBot()) { if (const auto media = item->media()) { if (const auto game = media->game()) { diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index d125f346dd2b24..27393a5336bcc1 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -55,12 +55,54 @@ namespace { constexpr auto kStickersPanelPerRow = 5; constexpr auto kMinRepaintDelay = crl::time(33); constexpr auto kMinAfterScrollDelay = crl::time(33); +constexpr auto kGrayLockOpacity = 0.3; using Data::StickersSet; using Data::StickersPack; using Data::StickersByEmojiMap; using SetFlag = Data::StickersSetFlag; +[[nodiscard]] std::optional ComputeImageColor(const QImage &frame) { + if (frame.isNull() + || frame.format() != QImage::Format_ARGB32_Premultiplied) { + return {}; + } + auto sr = int64(); + auto sg = int64(); + auto sb = int64(); + auto sa = int64(); + const auto factor = frame.devicePixelRatio(); + const auto size = st::stickersPremiumLock.size() * factor; + const auto width = std::min(frame.width(), size.width()); + const auto height = std::min(frame.height(), size.height()); + const auto skipx = (frame.width() - width) / 2; + const auto radius = st::roundRadiusSmall; + const auto skipy = std::max(frame.height() - height - radius, 0); + const auto perline = frame.bytesPerLine(); + const auto addperline = perline - (width * 4); + auto bits = static_cast(frame.bits()) + + perline * skipy + + sizeof(uint32) * skipx; + for (auto y = 0; y != height; ++y) { + for (auto x = 0; x != width; ++x) { + sb += int(*bits++); + sg += int(*bits++); + sr += int(*bits++); + sa += int(*bits++); + } + bits += addperline; + } + if (!sa) { + return {}; + } + return QColor(sr * 255 / sa, sg * 255 / sa, sb * 255 / sa, 255); + +} + +[[nodiscard]] QColor ComputeLockColor(const QImage &frame) { + return ComputeImageColor(frame).value_or(st::windowSubTextFg->c); +} + } // namespace class StickerSetBox::Inner final : public Ui::RpWidget { @@ -106,6 +148,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { Lottie::Animation *lottie = nullptr; Media::Clip::ReaderPointer webm; Ui::Animations::Simple overAnimation; + mutable QImage premiumLock; }; void visibleTopBottomUpdated(int visibleTop, int visibleBottom) override; @@ -138,6 +181,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { not_null getLottiePlayer(); void showPreview(); + const QImage &validatePremiumLock(int index, const QImage &frame) const; void updateItems(); void repaintItems(crl::time now = 0); @@ -158,6 +202,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { ImageWithLocation _setThumbnail; const std::unique_ptr _pathGradient; + mutable QImage _premiumLockGray; int _visibleTop = 0; int _visibleBottom = 0; @@ -227,7 +272,9 @@ void StickerSetBox::prepare() { _inner->setInstalled( ) | rpl::start_with_next([=](uint64 setId) { if (_inner->isMasksSet()) { - Ui::Toast::Show(tr::lng_masks_installed(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_masks_installed(tr::now)); } else { auto &stickers = _controller->session().data().stickers(); stickers.notifyStickerSetInstalled(setId); @@ -244,9 +291,11 @@ void StickerSetBox::prepare() { ) | rpl::start_with_next([=](uint64 setId) { const auto isMasks = _inner->isMasksSet(); - Ui::Toast::Show(isMasks - ? tr::lng_masks_has_been_archived(tr::now) - : tr::lng_stickers_has_been_archived(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + isMasks + ? tr::lng_masks_has_been_archived(tr::now) + : tr::lng_stickers_has_been_archived(tr::now)); auto &order = isMasks ? _controller->session().data().stickers().maskSetsOrderRef() @@ -315,7 +364,9 @@ void StickerSetBox::updateButtons() { const auto top = addTopButton(st::infoTopBarMenu); const auto share = [=] { copyStickersLink(); - Ui::Toast::Show(tr::lng_stickers_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_stickers_copied(tr::now)); closeBox(); }; const auto menu = @@ -339,7 +390,9 @@ void StickerSetBox::updateButtons() { } else { auto share = [=] { copyStickersLink(); - Ui::Toast::Show(tr::lng_stickers_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_stickers_copied(tr::now)); }; auto shareText = isMasks ? tr::lng_stickers_share_masks() @@ -416,6 +469,11 @@ StickerSetBox::Inner::Inner( updateItems(); }, lifetime()); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _premiumLockGray = QImage(); + }, lifetime()); + setMouseTracking(true); } @@ -425,18 +483,25 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { _elements.clear(); _selected = -1; setCursor(style::cur_default); + const auto owner = &_controller->session().data(); + const auto premiumPossible = _controller->session().premiumPossible(); set.match([&](const MTPDmessages_stickerSet &data) { const auto &v = data.vdocuments().v; _pack.reserve(v.size()); _elements.reserve(v.size()); for (const auto &item : v) { - const auto document = _controller->session().data().processDocument(item); + const auto document = owner->processDocument(item); const auto sticker = document->sticker(); if (!sticker) { continue; } _pack.push_back(document); - _elements.push_back({ document, document->createMediaView() }); + if (!document->isPremiumSticker() || premiumPossible) { + _elements.push_back({ + document, + document->createMediaView(), + }); + } } for (const auto &pack : data.vpacks().v) { pack.match([&](const MTPDstickerPack &pack) { @@ -699,8 +764,10 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { SendMenu::DefaultSilentCallback(sendSelected), SendMenu::DefaultScheduleCallback(this, type, sendSelected)); + const auto controller = _controller; const auto toggleFavedSticker = [=] { Api::ToggleFavedSticker( + controller, document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0)); }; @@ -754,6 +821,15 @@ void StickerSetBox::Inner::showPreview() { } } +const QImage &StickerSetBox::Inner::validatePremiumLock( + int index, + const QImage &frame) const { + auto &element = _elements[index]; + auto &image = frame.isNull() ? _premiumLockGray : element.premiumLock; + ValidatePremiumLockBg(image, frame); + return image; +} + not_null StickerSetBox::Inner::getLottiePlayer() { if (!_lottiePlayer) { _lottiePlayer = std::make_unique( @@ -937,6 +1013,8 @@ void StickerSetBox::Inner::paintSticker( const auto document = element.document; const auto &media = element.documentMedia; const auto sticker = document->sticker(); + const auto locked = document->isPremiumSticker() + && !_controller->session().premium(); media->checkStickerSmall(); if (media->loaded()) { @@ -953,12 +1031,12 @@ void StickerSetBox::Inner::paintSticker( const auto ppos = position + QPoint( (st::stickersSize.width() - size.width()) / 2, (st::stickersSize.height() - size.height()) / 2); - + auto lottieFrame = QImage(); if (element.lottie && element.lottie->ready()) { - const auto frame = element.lottie->frame(); + lottieFrame = element.lottie->frame(); p.drawImage( - QRect(ppos, frame.size() / cIntRetinaFactor()), - frame); + QRect(ppos, lottieFrame.size() / cIntRetinaFactor()), + lottieFrame); _lottiePlayer->unpause(element.lottie); } else if (element.webm && element.webm->started()) { @@ -978,6 +1056,20 @@ void StickerSetBox::Inner::paintSticker( QRect(ppos, size), _pathGradient.get()); } + if (locked) { + validatePremiumLock(index, lottieFrame); + const auto &bg = lottieFrame.isNull() + ? _premiumLockGray + : element.premiumLock; + const auto factor = style::DevicePixelRatio(); + const auto radius = st::roundRadiusSmall; + const auto point = position + QPoint( + (st::stickersSize.width() - (bg.width() / factor)) / 2, + st::stickersSize.height() - (bg.height() / factor) - radius); + p.drawImage(point, bg); + + st::stickersPremiumLock.paint(p, point, width()); + } } bool StickerSetBox::Inner::loaded() const { @@ -1039,8 +1131,8 @@ void StickerSetBox::Inner::archiveStickers() { if (result.type() == mtpc_messages_stickerSetInstallResultSuccess) { _setArchived.fire_copy(_setId); } - }).fail([] { - Ui::Toast::Show(Lang::Hard::ServerError()); + }).fail([toastParent = Window::Show(_controller).toastParent()] { + Ui::Toast::Show(toastParent, Lang::Hard::ServerError()); }).send(); } @@ -1064,3 +1156,23 @@ void StickerSetBox::Inner::repaintItems(crl::time now) { } StickerSetBox::Inner::~Inner() = default; + +void ValidatePremiumLockBg(QImage &image, const QImage &frame) { + if (!image.isNull()) { + return; + } + const auto factor = style::DevicePixelRatio(); + const auto size = st::stickersPremiumLock.size(); + image = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + auto p = QPainter(&image); + const auto color = ComputeLockColor(frame); + p.fillRect( + QRect(QPoint(), size), + anim::color(color, st::windowSubTextFg, kGrayLockOpacity)); + p.end(); + + image = Images::Circle(std::move(image)); +} diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index a4be4c68064f08..a53bcafdcf68ea 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -53,3 +53,5 @@ class StickerSetBox final : public Ui::BoxContent { QPointer _inner; }; + +void ValidatePremiumLockBg(QImage &image, const QImage &frame); diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 3a4eaca8b03963..ec91c766f46de2 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -242,11 +242,11 @@ class StickersBox::Inner : public Ui::RpWidget { [[nodiscard]] Data::StickersSetFlags fillSetFlags( not_null set) const; void rebuildMegagroupSet(); - void fixupMegagroupSetAddress(); void handleMegagroupSetAddressChange(); void setMegagroupSelectedSet(const StickerSetIdentifier &set); int countMaxNameWidth() const; + [[nodiscard]] bool skipPremium() const; const not_null _controller; MTP::Sender _api; @@ -278,8 +278,6 @@ class StickersBox::Inner : public Ui::RpWidget { QString _undoText; int _undoWidth = 0; - int _buttonHeight = 0; - QPoint _mouse; bool _inDragArea = false; SelectedRow _selected; @@ -2193,6 +2191,10 @@ bool StickersBox::Inner::appendSet(not_null set) { return true; } +bool StickersBox::Inner::skipPremium() const { + return !_controller->session().premiumPossible(); +} + int StickersBox::Inner::countMaxNameWidth() const { int namex = st::contactsPadding.left() + st::contactsPhotoSize + st::contactsPadding.left(); if (!_megagroupSet && _isInstalled) { @@ -2317,23 +2319,39 @@ void StickersBox::Inner::fillSetCover( } int StickersBox::Inner::fillSetCount(not_null set) const { + const auto skipPremium = this->skipPremium(); int result = set->stickers.isEmpty() ? set->count : set->stickers.size(); + if (skipPremium && !set->stickers.isEmpty()) { + result -= ranges::count( + set->stickers, + true, + &DocumentData::isPremiumSticker); + } auto added = 0; if (set->id == Data::Stickers::CloudRecentSetId) { const auto &sets = session().data().stickers().sets(); + const auto &recent = session().data().stickers().getRecentPack(); auto customIt = sets.find(Data::Stickers::CustomSetId); if (customIt != sets.cend()) { - added = customIt->second->stickers.size(); - const auto &recent = session().data().stickers().getRecentPack(); + auto &custom = customIt->second->stickers; + added = custom.size(); + if (skipPremium) { + added -= ranges::count( + custom, + true, + &DocumentData::isPremiumSticker); + } for (const auto &sticker : recent) { - if (customIt->second->stickers.indexOf(sticker.first) < 0) { + if (skipPremium && sticker.first->isPremiumSticker()) { + continue; + } else if (customIt->second->stickers.indexOf(sticker.first) < 0) { ++added; } } } else { - added = session().data().stickers().getRecentPack().size(); + added = recent.size(); } } return result + added; diff --git a/Telegram/SourceFiles/boxes/username_box.cpp b/Telegram/SourceFiles/boxes/username_box.cpp index d3b3bf140eae20..7562167288cb1e 100644 --- a/Telegram/SourceFiles/boxes/username_box.cpp +++ b/Telegram/SourceFiles/boxes/username_box.cpp @@ -254,7 +254,9 @@ void UsernameBox::changed() { void UsernameBox::linkClick() { QGuiApplication::clipboard()->setText( _session->createInternalLinkFull(getName())); - Ui::Toast::Show(tr::lng_username_copied(tr::now)); + Ui::Toast::Show( + Ui::BoxShow(this).toastParent(), + tr::lng_username_copied(tr::now)); } void UsernameBox::updateFail(const QString &error) { diff --git a/Telegram/SourceFiles/calls/calls_controller.h b/Telegram/SourceFiles/calls/calls_controller.h index cab175e25e2b2b..136f968ace1066 100644 --- a/Telegram/SourceFiles/calls/calls_controller.h +++ b/Telegram/SourceFiles/calls/calls_controller.h @@ -7,7 +7,7 @@ For license and copyright information please follow this link: */ #pragma once -#include "TgVoip.h" +#include namespace Calls { diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index b69f84f42456bb..ec6d0fca0579df 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "calls/group/calls_group_rtmp.h" #include "mtproto/mtproto_dh_utils.h" #include "core/application.h" +#include "core/core_settings.h" #include "main/main_session.h" #include "main/main_account.h" #include "apiwrap.h" @@ -202,14 +203,15 @@ void Instance::startOutgoingCall(not_null user, bool video) { } void Instance::startOrJoinGroupCall( + std::shared_ptr show, not_null peer, const StartGroupCallArgs &args) { using JoinConfirm = StartGroupCallArgs::JoinConfirm; if (args.rtmpNeeded) { _startWithRtmp->start(peer, [=](object_ptr box) { - Ui::show(std::move(box), Ui::LayerOption::KeepOther); + show->showBox(std::move(box), Ui::LayerOption::KeepOther); }, [=](QString text) { - Ui::Toast::Show(text); + Ui::Toast::Show(show->toastParent(), text); }, [=](Group::JoinInfo info) { createGroupCall( std::move(info), @@ -225,9 +227,9 @@ void Instance::startOrJoinGroupCall( ? Group::ChooseJoinAsProcess::Context::CreateScheduled : Group::ChooseJoinAsProcess::Context::Create; _chooseJoinAs->start(peer, context, [=](object_ptr box) { - Ui::show(std::move(box), Ui::LayerOption::KeepOther); + show->showBox(std::move(box), Ui::LayerOption::KeepOther); }, [=](QString text) { - Ui::Toast::Show(text); + Ui::Toast::Show(show->toastParent(), text); }, [=](Group::JoinInfo info) { const auto call = info.peer->groupCall(); info.joinHash = args.joinHash; @@ -717,9 +719,9 @@ void Instance::requestPermissionOrFail(Platform::PermissionType type, Fn } Ui::show(Ui::MakeConfirmBox({ .text = tr::lng_no_mic_permission(), - .confirmed = crl::guard(this, [=] { + .confirmed = crl::guard(this, [=](Fn &&close) { Platform::OpenSystemSettingsForPermission(type); - Ui::hideLayer(); + close(); }), .confirmText = tr::lng_menu_settings(), })); diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 7bfe569708b640..1bdfaa61849a53 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -25,6 +25,10 @@ namespace Main { class Session; } // namespace Main +namespace Ui { +class Show; +} // namespace Ui + namespace Calls::Group { struct JoinInfo; class Panel; @@ -64,6 +68,7 @@ class Instance final : public base::has_weak_ptr { void startOutgoingCall(not_null user, bool video); void startOrJoinGroupCall( + std::shared_ptr show, not_null peer, const StartGroupCallArgs &args); void handleUpdate( diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index 3af604b0d75c60..342ab059a17cba 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -227,23 +227,27 @@ class Mute final : public Ui::IconButton { TopBar::TopBar( QWidget *parent, - const base::weak_ptr &call) -: TopBar(parent, call, nullptr) { + const base::weak_ptr &call, + std::shared_ptr show) +: TopBar(parent, show, call, nullptr) { } TopBar::TopBar( QWidget *parent, - const base::weak_ptr &call) -: TopBar(parent, nullptr, call) { + const base::weak_ptr &call, + std::shared_ptr show) +: TopBar(parent, show, nullptr, call) { } TopBar::TopBar( QWidget *parent, + std::shared_ptr show, const base::weak_ptr &call, const base::weak_ptr &groupCall) : RpWidget(parent) , _call(call) , _groupCall(groupCall) +, _show(show) , _userpics(call ? nullptr : std::make_unique( @@ -279,7 +283,9 @@ void TopBar::initControls() { call->setMuted(!call->muted()); } else if (const auto group = _groupCall.get()) { if (group->mutedByAdmin()) { - Ui::Toast::Show(tr::lng_group_call_force_muted_sub(tr::now)); + Ui::Toast::Show( + _show->toastParent(), + tr::lng_group_call_force_muted_sub(tr::now)); } else { group->setMuted((group->muted() == MuteState::Muted) ? MuteState::Active @@ -394,7 +400,9 @@ void TopBar::initControls() { if (const auto call = _call.get()) { if (Logs::DebugEnabled() && (_info->clickModifiers() & Qt::ControlModifier)) { - Ui::show(Box(_call)); + _show->showBox( + Box(_call), + Ui::LayerOption::CloseOther); } else { Core::App().calls().showInfoPanel(call); } @@ -409,11 +417,13 @@ void TopBar::initControls() { if (!group->peer()->canManageGroupCall()) { group->hangup(); } else { - Ui::show(Box( - Group::LeaveBox, - group, - false, - Group::BoxContext::MainWindow)); + _show->showBox( + Box( + Group::LeaveBox, + group, + false, + Group::BoxContext::MainWindow), + Ui::LayerOption::CloseOther); } } }); diff --git a/Telegram/SourceFiles/calls/calls_top_bar.h b/Telegram/SourceFiles/calls/calls_top_bar.h index 79f65830c734d6..0ea23a81827b85 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.h +++ b/Telegram/SourceFiles/calls/calls_top_bar.h @@ -22,6 +22,7 @@ class LabelSimple; class FlatLabel; struct GroupCallUser; class GroupCallUserpics; +class Show; } // namespace Ui namespace Main { @@ -39,8 +40,14 @@ enum class BarState; class TopBar : public Ui::RpWidget { public: - TopBar(QWidget *parent, const base::weak_ptr &call); - TopBar(QWidget *parent, const base::weak_ptr &call); + TopBar( + QWidget *parent, + const base::weak_ptr &call, + std::shared_ptr show); + TopBar( + QWidget *parent, + const base::weak_ptr &call, + std::shared_ptr show); ~TopBar(); void initBlobsUnder( @@ -56,6 +63,7 @@ class TopBar : public Ui::RpWidget { TopBar( QWidget *parent, + std::shared_ptr show, const base::weak_ptr &call, const base::weak_ptr &groupCall); @@ -72,6 +80,7 @@ class TopBar : public Ui::RpWidget { const base::weak_ptr _call; const base::weak_ptr _groupCall; + const std::shared_ptr _show; bool _muted = false; std::vector _users; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index c1ee3c23481ae1..a01f529f16e64e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -2515,7 +2515,7 @@ void GroupCall::broadcastPartStart(std::shared_ptr task) { : (videoQuality == Quality::Medium) ? 1 : 0)), - MTP_int(0), + MTP_long(0), MTP_int(128 * 1024) )).done([=]( const MTPupload_File &result, diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index 5086768adb3698..a0957c74d2ab53 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1198,7 +1198,7 @@ base::unique_qptr Members::Controller::createRowContextMenu( if (const auto window = Core::App().separateWindowForPeer( participantPeer)) { return window->sessionController(); - } else if (const auto window = Core::App().activeWindow()) { + } else if (const auto window = Core::App().primaryWindow()) { if (const auto controller = window->sessionController()) { if (&controller->session() == session) { return controller; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 3b6442175ebf5d..5cb34b58e264d2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -37,6 +37,7 @@ For license and copyright information please follow this link: #include "ui/special_buttons.h" #include "info/profile/info_profile_values.h" // Info::Profile::Value. #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_keys.h" #include "data/data_channel.h" #include "data/data_chat.h" diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 226583a22bb0ff..9b027a450da2e2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -39,6 +39,7 @@ For license and copyright information please follow this link: #include "ui/toast/toast.h" #include "data/data_changes.h" #include "core/application.h" +#include "core/core_settings.h" #include "ui/boxes/single_choice_box.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_media_devices.h" @@ -593,7 +594,7 @@ void SettingsBox( }); const auto showToast = crl::guard(box, [=](QString text) { Ui::ShowMultilineToast({ - .parentOverride = box->getDelegate()->outerContainer(), + .parentOverride = Ui::BoxShow(box).toastParent(), .text = { text }, }); }); @@ -636,7 +637,7 @@ void SettingsBox( QGuiApplication::clipboard()->setText(link); if (weakBox) { Ui::ShowMultilineToast({ - .parentOverride = box->getDelegate()->outerContainer(), + .parentOverride = Ui::BoxShow(box).toastParent(), .text = { tr::lng_create_channel_link_copied(tr::now) }, }); } diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index ebf4ff8c8dbd82..7aa4832d4e8db1 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -94,8 +94,9 @@ stickersTrendingUnread: icon { { "emoji/stickers_add_unread", emojiIconFg }, { "emoji/stickers_add_dot", dialogsUnreadBg } }; -stickersRecent: icon {{ "emoji/stickers_recent", emojiIconFg }}; +stickersRecent: icon {{ "emoji/emoji_recent", emojiIconFg }}; stickersSearch: icon {{ "emoji/stickers_search", emojiIconFg }}; +stickersPremium: icon {{ "emoji/stickers_premium", emojiIconFg }}; stickersSettingsUnreadSize: 6px; stickersSettingsUnreadPosition: point(6px, 10px); @@ -277,3 +278,56 @@ manageEmojiStatusTop: 25px; inlineRadialSize: 44px; inlineFileSize: 44px; + +premiumPreviewBox: Box(defaultBox) { + buttonPadding: margins(18px, 18px, 18px, 18px); + buttonHeight: 44px; + button: RoundButton(defaultActiveButton) { + height: 44px; + textTop: 12px; + font: font(13px semibold); + } +} +premiumPreviewDoubledLimitsBox: Box(premiumPreviewBox) { + buttonPadding: margins(12px, 12px, 12px, 12px); +} +premiumPreviewAboutTitlePadding: margins(18px, 19px, 18px, 0px); +premiumPreviewAboutTitle: FlatLabel(defaultFlatLabel) { + minWidth: 240px; + textFg: windowBoldFg; + align: align(top); + style: TextStyle(defaultTextStyle) { + font: font(17px semibold); + linkFont: font(17px semibold); + linkFontOver: font(17px semibold); + } +} +premiumPreviewAbout: FlatLabel(defaultFlatLabel) { + minWidth: 240px; + textFg: membersAboutLimitFg; + align: align(top); +} +premiumPreviewAboutPadding: margins(18px, 8px, 18px, 8px); +premiumPreviewButtonLabel: FlatLabel(defaultFlatLabel) { + textFg: premiumButtonFg; + style: semiboldTextStyle; +} +premiumSlideDuration: 200; +premiumVideoStarSize: size(77px, 73px); + +stickersPremiumLock: icon{{ "emoji/premium_lock", premiumButtonFg }}; + +premiumPreviewHeight: 312px; + +premiumReactionWidthSkip: 64px; +premiumReactionHeightSkip: 75px; +premiumReactionSize: 108px; +premiumReactionAround: 164px; +premiumReactionsMiddle: 148px; +premiumReactionScale: 0.70; +premiumReactionInfoTop: 260px; + +premiumDot: 6px; +premiumDotPadding: margins(4px, 4px, 4px, 4px); +premiumDotsMargin: margins(0px, 5px, 0px, 6px); +premiumVideoWidth: 182px; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp index a532de970c6651..982556729fa5f5 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "emoji_suggestions_helper.h" #include "lang/lang_instance.h" #include "lang/lang_cloud_manager.h" +#include "lang/lang_keys.h" #include "core/application.h" #include "base/platform/base_platform_info.h" #include "ui/emoji_config.h" diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp index 7aaa3800f1ef20..19784fc4201862 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp @@ -17,8 +17,8 @@ For license and copyright information please follow this link: #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "core/application.h" +#include "lang/lang_keys.h" #include "main/main_account.h" -#include "mainwidget.h" #include "storage/storage_cloud_blob.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -120,7 +120,7 @@ void SetGlobalLoader(base::unique_qptr loader) { GlobalLoaderValues.fire(GlobalLoader.get()); } -int GetDownloadSize(int id) { +int64 GetDownloadSize(int id) { return ranges::find(kSets, id, &Set::id)->size; } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index a9f4eb19e41150..903d0b7747f405 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -480,7 +480,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { bool hasUsername = _filter.indexOf('@') > 0; base::flat_map< not_null, - not_null*>> bots; + not_null*>> bots; int32 cnt = 0; if (_chat) { if (_chat->noParticipantInfo()) { @@ -527,7 +527,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { if (cnt) { const auto make = [&]( not_null user, - const BotCommand &command) { + const Data::BotCommand &command) { return BotCommandRow{ user, command.command, diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index a7e99f2f20979c..f0d301417ad0c7 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -52,6 +52,7 @@ constexpr auto kMinAfterScrollDelay = crl::time(33); void AddGifAction( Fn &&, const style::icon*)> callback, + Window::SessionController *controller, not_null document) { if (!document->isGifv()) { return; @@ -64,6 +65,7 @@ void AddGifAction( : tr::lng_context_save_gif)(tr::now); callback(text, [=] { Api::ToggleSavedGif( + controller, document, Data::FileOriginSavedGifs(), !saved); @@ -396,7 +398,7 @@ void GifsListWidget::fillContextMenu( const style::icon *icon) { menu->addAction(text, std::move(done), icon); }; - AddGifAction(std::move(callback), document); + AddGifAction(std::move(callback), controller(), document); } }; } diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index 8bc6e3d23040ff..14bb998e0714eb 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -42,6 +42,7 @@ namespace ChatHelpers { void AddGifAction( Fn &&, const style::icon*)> callback, + Window::SessionController *controller, not_null document); class GifsListWidget diff --git a/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp b/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp index dfc0e70a34281b..c6e1186c9fc01b 100644 --- a/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp +++ b/Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp @@ -200,7 +200,7 @@ DictLoader::DictLoader( int id, MTP::DedicatedLoader::Location location, const QString &folder, - int size, + int64 size, Fn destroyCallback) : BlobLoader(parent, session, id, location, folder, size) , _destroyCallback(std::move(destroyCallback)) { @@ -233,7 +233,7 @@ std::vector Dictionaries() { return kDictionaries | ranges::to_vector; } -int GetDownloadSize(int id) { +int64 GetDownloadSize(int id) { return ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size; } diff --git a/Telegram/SourceFiles/chat_helpers/spellchecker_common.h b/Telegram/SourceFiles/chat_helpers/spellchecker_common.h index 4d535c363e405a..f0e02879aeb596 100644 --- a/Telegram/SourceFiles/chat_helpers/spellchecker_common.h +++ b/Telegram/SourceFiles/chat_helpers/spellchecker_common.h @@ -21,7 +21,7 @@ namespace Spellchecker { struct Dict : public Storage::CloudBlob::Blob { }; -int GetDownloadSize(int id); +int64 GetDownloadSize(int id); MTP::DedicatedLoader::Location GetDownloadLocation(int id); [[nodiscard]] QString DictionariesPath(); @@ -48,7 +48,7 @@ class DictLoader : public Storage::CloudBlob::BlobLoader { int id, MTP::DedicatedLoader::Location location, const QString &folder, - int size, + int64 size, Fn destroyCallback); void destroy() override; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp index d663f4290a3c6e..82846c1f90bbf6 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp @@ -20,6 +20,9 @@ For license and copyright information please follow this link: #include "core/core_settings.h" #include "core/application.h" #include "base/call_delayed.h" +#include "chat_helpers/stickers_lottie.h" +#include "history/view/media/history_view_sticker.h" +#include "lottie/lottie_single_player.h" #include "apiwrap.h" #include "styles/style_chat.h" @@ -29,6 +32,8 @@ namespace Stickers { namespace { constexpr auto kRefreshTimeout = 7200 * crl::time(1000); +constexpr auto kEmojiCachesCount = 4; +constexpr auto kPremiumCachesCount = 8; [[nodiscard]] std::optional IndexFromEmoticon(const QString &emoticon) { if (emoticon.size() < 2) { @@ -201,6 +206,57 @@ auto EmojiPack::animationsForEmoji(EmojiPtr emoji) const return (i != end(_animations)) ? i->second : empty; } +std::unique_ptr EmojiPack::effectPlayer( + not_null document, + QByteArray data, + QString filepath, + bool premium) { + // Shortened copy from stickers_lottie module. + const auto baseKey = document->bigFileBaseCacheKey(); + const auto tag = uint8(0); + const auto keyShift = ((tag << 4) & 0xF0) + | (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F); + const auto key = Storage::Cache::Key{ + baseKey.high, + baseKey.low + keyShift + }; + const auto get = [=](int i, FnMut handler) { + document->owner().cacheBigFile().get( + { key.high, key.low + i }, + std::move(handler)); + }; + const auto weak = base::make_weak(&document->session()); + const auto put = [=](int i, QByteArray &&cached) { + crl::on_main(weak, [=, data = std::move(cached)]() mutable { + weak->data().cacheBigFile().put( + { key.high, key.low + i }, + std::move(data)); + }); + }; + const auto size = premium + ? HistoryView::Sticker::PremiumEffectSize(document) + : HistoryView::Sticker::EmojiEffectSize(); + const auto request = Lottie::FrameRequest{ + size * style::DevicePixelRatio(), + }; + auto &weakProvider = _sharedProviders[document]; + auto shared = [&] { + if (const auto result = weakProvider.lock()) { + return result; + } + const auto result = Lottie::SinglePlayer::SharedProvider( + premium ? kPremiumCachesCount : kEmojiCachesCount, + get, + put, + Lottie::ReadContent(data, filepath), + request, + Lottie::Quality::High); + weakProvider = result; + return result; + }(); + return std::make_unique(std::move(shared), request); +} + void EmojiPack::refresh() { if (_requestId) { return; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h index 8f62a5d2af0574..3170ca1100eed3 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h @@ -21,6 +21,8 @@ class Session; } // namespace Main namespace Lottie { +class SinglePlayer; +class FrameProvider; struct ColorReplacements; } // namespace Lottie @@ -70,6 +72,12 @@ class EmojiPack final { [[nodiscard]] auto animationsForEmoji(EmojiPtr emoji) const -> const base::flat_map> &; + [[nodiscard]] std::unique_ptr effectPlayer( + not_null document, + QByteArray data, + QString filepath, + bool premium); + private: class ImageLoader; @@ -103,6 +111,10 @@ class EmojiPack final { base::flat_map>> _animations; mtpRequestId _animationsRequestId = 0; + base::flat_map< + not_null, + std::weak_ptr> _sharedProviders; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 58e6024b837ad0..110662359ae9c8 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -14,6 +14,7 @@ For license and copyright information please follow this link: #include "data/data_file_origin.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_peer_values.h" #include "menu/menu_send.h" // SendMenu::FillSendMenu #include "chat_helpers/stickers_lottie.h" #include "ui/widgets/buttons.h" @@ -21,6 +22,7 @@ For license and copyright information please follow this link: #include "ui/effects/animations.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/path_shift_gradient.h" +#include "ui/effects/premium_graphics.h" #include "ui/image/image.h" #include "ui/cached_round_corners.h" #include "lottie/lottie_multi_player.h" @@ -185,6 +187,7 @@ class StickersListWidget::Footer final : public TabbedSelector::InnerFooter { bool paused) const; void paintSelectionBar(Painter &p) const; void paintLeftRightFading(Painter &p) const; + void validatePremiumIcon() const; void initSearch(); void toggleSearch(bool visible); @@ -205,6 +208,7 @@ class StickersListWidget::Footer final : public TabbedSelector::InnerFooter { bool _iconsDragging = false; Ui::Animations::Basic _iconsAnimation; QPoint _iconsMousePos, _iconsMouseDown; + mutable QImage _premiumIcon; int _iconsLeft = 0; int _iconsRight = 0; int _iconsTop = 0; @@ -232,6 +236,7 @@ struct StickersListWidget::Sticker { Media::Clip::ReaderPointer webm; QPixmap savedFrame; QSize savedFrameFor; + QImage premiumLock; void ensureMediaCreated(); }; @@ -267,11 +272,14 @@ struct StickersListWidget::Set { }; auto StickersListWidget::PrepareStickers( - const QVector &pack) + const QVector &pack, + bool skipPremium) -> std::vector { return ranges::views::all( pack - ) | ranges::views::transform([](DocumentData *document) { + ) | ranges::views::filter([&](DocumentData *document) { + return !skipPremium || !document->isPremiumSticker(); + }) | ranges::views::transform([](DocumentData *document) { return Sticker{ document }; }) | ranges::to_vector; } @@ -328,6 +336,36 @@ StickersListWidget::Footer::Footer( ) | rpl::start_with_next([=] { update(); }, lifetime()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _premiumIcon = QImage(); + }, lifetime()); +} + +void StickersListWidget::Footer::validatePremiumIcon() const { + if (!_premiumIcon.isNull()) { + return; + } + const auto size = st::stickersPremium.size(); + const auto mask = st::stickersPremium.instance(Qt::white); + const auto factor = style::DevicePixelRatio(); + _premiumIcon = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + _premiumIcon.setDevicePixelRatio(factor); + + QPainter p(&_premiumIcon); + auto gradient = QLinearGradient( + QPoint(0, size.height()), + QPoint(size.width(), 0)); + gradient.setStops({ + { 0., st::stickerPanPremium1->c }, + { 1., st::stickerPanPremium2->c }, + }); + p.fillRect(QRect(QPoint(), size), gradient); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.drawImage(QRect(QPoint(), size), mask); } void StickersListWidget::Footer::clearHeavyData() { @@ -1038,6 +1076,13 @@ void StickersListWidget::Footer::paintSetIcon( _iconsTop + (st::emojiFooterHeight - size) / 2, width(), st::stickerGroupCategorySize); + } else if (icon.setId == Data::Stickers::PremiumSetId) { + validatePremiumIcon(); + const auto size = st::stickersPremium.size(); + p.drawImage( + info.left + (st::stickerIconWidth - size.width()) / 2, + _iconsTop + (st::emojiFooterHeight - size.height()) / 2, + _premiumIcon); } else { const auto paintedIcon = [&] { if (icon.setId == Data::Stickers::FeaturedSetId) { @@ -1048,7 +1093,7 @@ void StickersListWidget::Footer::paintSetIcon( //} else if (setId == Stickers::FavedSetId) { // return &st::stickersFaved; } - return &st::stickersRecent; + return &st::emojiRecent; }(); paintedIcon->paint( p, @@ -1138,6 +1183,17 @@ StickersListWidget::StickersListWidget( ) | rpl::skip(1) | rpl::map_to( TabbedSelector::Action::Update ) | rpl::start_to_stream(_choosingUpdated, lifetime()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _premiumLockGray = QImage(); + }, lifetime()); + + Data::AmPremiumValue( + &session() + ) | rpl::start_with_next([=](bool premium) { + refreshStickers(); + }, lifetime()); } Main::Session &StickersListWidget::session() const { @@ -1170,7 +1226,7 @@ object_ptr StickersListWidget::createFooter() { _footer->openSettingsRequests( ) | rpl::start_with_next([=] { const auto onlyFeatured = _footer->hasOnlyFeaturedSets(); - Ui::show(Box( + controller()->show(Box( controller(), (onlyFeatured ? StickersBox::Section::Featured @@ -1579,6 +1635,10 @@ void StickersListWidget::fillCloudSearchRows( } void StickersListWidget::addSearchRow(not_null set) { + const auto skipPremium = !session().premiumPossible(); + auto elements = PrepareStickers( + set->stickers.empty() ? set->covers : set->stickers, + skipPremium); _searchSets.emplace_back( set->id, set, @@ -1587,9 +1647,7 @@ void StickersListWidget::addSearchRow(not_null set) { set->shortName, set->count, !SetInMyList(set->flags), - PrepareStickers(set->stickers.empty() - ? set->covers - : set->stickers)); + std::move(elements)); } void StickersListWidget::takeHeavyData( @@ -2238,6 +2296,7 @@ void StickersListWidget::paintSticker( return; } + const auto locked = document->isPremiumSticker() && !session().premium(); const auto isLottie = document->sticker()->isLottie(); const auto isWebm = document->sticker()->isWebm(); if (isLottie @@ -2264,15 +2323,18 @@ void StickersListWidget::paintSticker( (_singleSize.width() - size.width()) / 2, (_singleSize.height() - size.height()) / 2); + auto lottieFrame = QImage(); if (sticker.lottie && sticker.lottie->ready()) { auto request = Lottie::FrameRequest(); request.box = boundingBoxSize() * cIntRetinaFactor(); - const auto frame = sticker.lottie->frame(request); + lottieFrame = sticker.lottie->frame(request); p.drawImage( - QRect(ppos, frame.size() / cIntRetinaFactor()), - frame); + QRect(ppos, lottieFrame.size() / cIntRetinaFactor()), + lottieFrame); if (sticker.savedFrame.isNull()) { - sticker.savedFrame = QPixmap::fromImage(frame, Qt::ColorOnly); + sticker.savedFrame = QPixmap::fromImage( + lottieFrame, + Qt::ColorOnly); sticker.savedFrame.setDevicePixelRatio(cRetinaFactor()); sticker.savedFrameFor = _singleSize; } @@ -2302,7 +2364,12 @@ void StickersListWidget::paintSticker( sticker.savedFrame = pixmap; sticker.savedFrameFor = _singleSize; } + if (locked) { + lottieFrame = pixmap.toImage().convertToFormat( + QImage::Format_ARGB32_Premultiplied); + } } else { + p.setOpacity(1.); PaintStickerThumbnailPath( p, media.get(), @@ -2319,6 +2386,31 @@ void StickersListWidget::paintSticker( st::stickerPanDeleteIconFg.paint(p, xPos, width()); p.setOpacity(1.); } + + if (locked) { + validatePremiumLock(set, index, lottieFrame); + const auto &bg = lottieFrame.isNull() + ? _premiumLockGray + : sticker.premiumLock; + const auto factor = style::DevicePixelRatio(); + const auto radius = st::roundRadiusSmall; + const auto point = pos + QPoint( + (_singleSize.width() - (bg.width() / factor)) / 2, + _singleSize.height() - (bg.height() / factor) - radius); + p.drawImage(point, bg); + + st::stickersPremiumLock.paint(p, point, width()); + } +} + +const QImage &StickersListWidget::validatePremiumLock( + Set &set, + int index, + const QImage &frame) { + auto &sticker = set.stickers[index]; + auto &image = frame.isNull() ? _premiumLockGray : sticker.premiumLock; + ValidatePremiumLockBg(image, frame); + return image; } int StickersListWidget::stickersRight() const { @@ -2499,8 +2591,10 @@ void StickersListWidget::fillContextMenu( SendMenu::DefaultSilentCallback(send), SendMenu::DefaultScheduleCallback(this, type, send)); + const auto window = controller(); const auto toggleFavedSticker = [=] { Api::ToggleFavedSticker( + window, document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0)); }; @@ -2673,8 +2767,9 @@ void StickersListWidget::removeFavedSticker(int section, int index) { clearSelection(); const auto &sticker = _mySets[section].stickers[index]; const auto document = sticker.document; - session().data().stickers().setFaved(document, false); + session().data().stickers().setFaved(controller(), document, false); Api::ToggleFavedSticker( + controller(), document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0), false); @@ -2787,13 +2882,28 @@ void StickersListWidget::refreshMySets() { _favedStickersMap.clear(); _mySets.reserve(defaultSetsOrder().size() + 3); + refreshPremiumStickers(); refreshFavedStickers(); refreshRecentStickers(false); refreshMegagroupStickers(GroupStickersPlace::Visible); + + _premiumsIndex = -1; + for (auto i = 0, count = int(_mySets.size()); i != count; ++i) { + if (_mySets[i].id == Data::Stickers::PremiumSetId) { + _premiumsIndex = i; + } + } + for (const auto setId : defaultSetsOrder()) { const auto externalLayout = false; appendSet(_mySets, setId, externalLayout, AppendSkip::Archived); } + + if (_premiumsIndex >= 0 && _mySets[_premiumsIndex].stickers.empty()) { + _mySets.erase(_mySets.begin() + _premiumsIndex); + _premiumsIndex = -1; + } + refreshMegagroupStickers(GroupStickersPlace::Hidden); takeHeavyData(_mySets, wasSets); @@ -2834,13 +2944,15 @@ void StickersListWidget::refreshSearchSets() { refreshSearchIndex(); const auto &sets = session().data().stickers().sets(); + const auto skipPremium = !session().premiumPossible(); for (auto &entry : _searchSets) { if (const auto it = sets.find(entry.id); it != sets.end()) { const auto set = it->second.get(); entry.flags = set->flags; - if (!set->stickers.empty()) { + auto elements = PrepareStickers(set->stickers, skipPremium); + if (!elements.empty()) { entry.lottiePlayer = nullptr; - entry.stickers = PrepareStickers(set->stickers); + entry.stickers = std::move(elements); } if (!SetInMyList(entry.flags)) { _installedLocallySets.remove(entry.id); @@ -2915,6 +3027,15 @@ bool StickersListWidget::appendSet( return false; } } + const auto skipPremium = !session().premiumPossible(); + auto elements = PrepareStickers( + ((set->stickers.empty() && externalLayout) + ? set->covers + : set->stickers), + skipPremium); + if (elements.empty()) { + return false; + } to.emplace_back( set->id, set, @@ -2923,9 +3044,16 @@ bool StickersListWidget::appendSet( set->shortName, set->count, externalLayout, - PrepareStickers((set->stickers.empty() && externalLayout) - ? set->covers - : set->stickers)); + std::move(elements)); + if (!externalLayout && _premiumsIndex >= 0 && session().premium()) { + for (const auto &sticker : to.back().stickers) { + const auto document = sticker.document; + if (document->isPremiumSticker()) { + to[_premiumsIndex].stickers.push_back(Sticker{ document }); + } + ++to[_premiumsIndex].count; + } + } return true; } @@ -3036,6 +3164,25 @@ void StickersListWidget::refreshRecentStickers(bool performResize) { } } +void StickersListWidget::refreshPremiumStickers() { + if (_isMasks) { + return; + } + clearSelection(); + const auto externalLayout = false; + const auto shortName = QString(); + const auto count = 0; + _mySets.insert(_mySets.begin(), Set{ + Data::Stickers::PremiumSetId, + nullptr, + (SetFlag::Official | SetFlag::Special), + tr::lng_premium_stickers(tr::now), + shortName, + count, + externalLayout + }); +} + void StickersListWidget::refreshFavedStickers() { if (_isMasks) { return; @@ -3043,12 +3190,17 @@ void StickersListWidget::refreshFavedStickers() { clearSelection(); const auto &sets = session().data().stickers().sets(); const auto it = sets.find(Data::Stickers::FavedSetId); - if (it == sets.cend() || it->second->stickers.isEmpty()) { + if (it == sets.cend()) { return; } + const auto skipPremium = !session().premiumPossible(); const auto set = it->second.get(); const auto externalLayout = false; const auto shortName = QString(); + auto elements = PrepareStickers(set->stickers, skipPremium); + if (elements.empty()) { + return; + } _mySets.insert(_mySets.begin(), Set{ Data::Stickers::FavedSetId, nullptr, @@ -3057,7 +3209,7 @@ void StickersListWidget::refreshFavedStickers() { shortName, set->count, externalLayout, - PrepareStickers(set->stickers) + std::move(elements) }); _favedStickersMap = base::flat_set> { set->stickers.begin(), @@ -3119,15 +3271,19 @@ void StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) { } else if (isShownHere(hidden)) { const auto shortName = QString(); const auto externalLayout = false; - _mySets.emplace_back( - Data::Stickers::MegagroupSetId, - set, - SetFlag::Special, - tr::lng_group_stickers(tr::now), - shortName, - set->count, - externalLayout, - PrepareStickers(set->stickers)); + const auto skipPremium = !session().premiumPossible(); + auto elements = PrepareStickers(set->stickers, skipPremium); + if (!elements.empty()) { + _mySets.emplace_back( + Data::Stickers::MegagroupSetId, + set, + SetFlag::Special, + tr::lng_group_stickers(tr::now), + shortName, + set->count, + externalLayout, + std::move(elements)); + } } return; } else if (!isShownHere(hidden) || _megagroupSetIdRequested == set.id) { @@ -3171,6 +3327,10 @@ std::vector StickersListWidget::fillIcons() { result.emplace_back(Data::Stickers::RecentSetId); } } + if (i != _mySets.size() && _mySets[i].id == Data::Stickers::PremiumSetId) { + ++i; + result.emplace_back(Data::Stickers::PremiumSetId); + } for (auto l = _mySets.size(); i != l; ++i) { if (_mySets[i].id == Data::Stickers::MegagroupSetId) { result.emplace_back(Data::Stickers::MegagroupSetId); @@ -3525,13 +3685,14 @@ void StickersListWidget::removeMegagroupSet(bool locally) { _removingSetId = Data::Stickers::MegagroupSetId; controller()->show(Ui::MakeConfirmBox({ .text = tr::lng_stickers_remove_group_set(), - .confirmed = crl::guard(this, [this, group = _megagroupSet] { + .confirmed = crl::guard(this, [this, group = _megagroupSet]( + Fn &&close) { Expects(group->mgInfo != nullptr); if (group->mgInfo->stickerSet) { session().api().setGroupStickerSet(group, {}); } - Ui::hideLayer(); + close(); _removingSetId = 0; _checkForHide.fire({}); }), diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index 8c744f650b1dc6..2748cf7ceca93e 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -192,7 +192,8 @@ class StickersListWidget final : public TabbedSelector::Inner { }; static std::vector PrepareStickers( - const QVector &pack); + const QVector &pack, + bool skipPremium); void preloadMoreOfficial(); QSize boundingBoxSize() const; @@ -220,6 +221,7 @@ class StickersListWidget final : public TabbedSelector::Inner { bool stickerHasDeleteButton(const Set &set, int index) const; std::vector collectRecentStickers(); void refreshRecentStickers(bool resize = true); + void refreshPremiumStickers(); void refreshFavedStickers(); enum class GroupStickersPlace { Visible, @@ -329,6 +331,10 @@ class StickersListWidget final : public TabbedSelector::Inner { void addSearchRow(not_null set); void showPreview(); + const QImage &validatePremiumLock( + Set &set, + int index, + const QImage &frame); Ui::MessageSendingAnimationFrom messageSentAnimationInfo( int section, @@ -341,6 +347,7 @@ class StickersListWidget final : public TabbedSelector::Inner { std::vector _mySets; std::vector _officialSets; std::vector _searchSets; + int _premiumsIndex = -1; int _featuredSetsCount = 0; base::flat_set _installedLocallySets; std::vector _custom; @@ -388,6 +395,8 @@ class StickersListWidget final : public TabbedSelector::Inner { base::Timer _previewTimer; bool _previewShown = false; + QImage _premiumLockGray; + std::map> _searchCache; std::vector> _searchIndex; base::Timer _searchRequestTimer; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index 8f93889339b1ad..b4f16f34be4204 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -235,7 +235,8 @@ bool PaintStickerThumbnailPath( QPainter &p, not_null media, QRect target, - QLinearGradient *gradient) { + QLinearGradient *gradient, + bool mirrorHorizontal) { const auto &path = media->thumbnailPath(); const auto dimensions = media->owner()->dimensions; if (path.isEmpty() || dimensions.isEmpty() || target.isEmpty()) { @@ -254,6 +255,12 @@ bool PaintStickerThumbnailPath( 0); p.setBrush(*gradient); } + if (mirrorHorizontal) { + const auto c = QPointF(target.width() / 2., target.height() / 2.); + p.translate(c); + p.scale(-1., 1.); + p.translate(-c); + } p.scale( target.width() / float64(dimensions.width()), target.height() / float64(dimensions.height())); @@ -266,14 +273,25 @@ bool PaintStickerThumbnailPath( QPainter &p, not_null media, QRect target, - not_null gradient) { + not_null gradient, + bool mirrorHorizontal) { return gradient->paint([&](const Ui::PathShiftGradient::Background &bg) { if (const auto color = std::get_if(&bg)) { p.setBrush(*color); - return PaintStickerThumbnailPath(p, media, target); + return PaintStickerThumbnailPath( + p, + media, + target, + nullptr, + mirrorHorizontal); } const auto gradient = v::get(bg); - return PaintStickerThumbnailPath(p, media, target, gradient); + return PaintStickerThumbnailPath( + p, + media, + target, + gradient, + mirrorHorizontal); }); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h index 0e95a806ac2f63..c5fd606a0073ba 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h @@ -60,6 +60,11 @@ enum class StickerLottieSize : uchar { EmojiInteractionReserved1, EmojiInteractionReserved2, EmojiInteractionReserved3, + EmojiInteractionReserved4, + EmojiInteractionReserved5, + EmojiInteractionReserved6, + EmojiInteractionReserved7, + PremiumReactionPreview, }; [[nodiscard]] std::unique_ptr LottiePlayerFromDocument( @@ -105,13 +110,15 @@ bool PaintStickerThumbnailPath( QPainter &p, not_null media, QRect target, - QLinearGradient *gradient = nullptr); + QLinearGradient *gradient = nullptr, + bool mirrorHorizontal = false); bool PaintStickerThumbnailPath( QPainter &p, not_null media, QRect target, - not_null gradient); + not_null gradient, + bool mirrorHorizontal = false); [[nodiscard]] QSize ComputeStickerSize( not_null document, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 4fd46b716a24b5..31c78a302898a8 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -394,11 +394,12 @@ TabbedSelector::TabbedSelector( _showRequests.fire({}); }, lifetime()); - session().data().stickers().updated( + rpl::merge( + session().premiumPossibleValue() | rpl::to_empty, + session().data().stickers().updated() ) | rpl::start_with_next([=] { refreshStickers(); }, lifetime()); - refreshStickers(); } //setAttribute(Qt::WA_AcceptTouchEvents); setAttribute(Qt::WA_OpaquePaintEvent, false); diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 68a8816c1eb459..512ee948efe4de 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -17,7 +17,6 @@ enum { LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway) LocalEncryptSaltSize = 32, // 256 bit - AnimationTimerDelta = 7, RecentInlineBotsLimit = 10, AutoSearchTimeout = 900, // 0.9 secs diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 241fbda120c63a..e0b6d3d581b68a 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -18,6 +18,7 @@ For license and copyright information please follow this link: #include "base/concurrent_timer.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" +#include "core/core_settings.h" #include "core/update_checker.h" #include "core/shortcuts.h" #include "core/sandbox.h" @@ -85,8 +86,8 @@ For license and copyright information please follow this link: #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "boxes/connection_box.h" +#include "boxes/premium_limits_box.h" #include "ui/boxes/confirm_box.h" -#include "boxes/share_box.h" #include #include @@ -285,6 +286,31 @@ void Application::run() { _primaryWindow->showAccount(account); }, _primaryWindow->widget()->lifetime()); + ( + _domain->activeValue( + ) | rpl::to_empty | rpl::filter([=] { + return _domain->started(); + }) | rpl::take(1) + ) | rpl::then( + _domain->accountsChanges() + ) | rpl::map([=] { + return (_domain->accounts().size() > Main::Domain::kMaxAccounts) + ? _domain->activeChanges() + : rpl::never>(); + }) | rpl::flatten_latest( + ) | rpl::start_with_next([=](not_null account) { + const auto ordered = _domain->orderedAccounts(); + const auto it = ranges::find(ordered, account); + if (it != end(ordered)) { + const auto index = std::distance(begin(ordered), it); + if ((index + 1) > _domain->maxAccounts()) { + _primaryWindow->show(Box( + AccountsLimitBox, + &account->session())); + } + } + }, _primaryWindow->widget()->lifetime()); + QCoreApplication::instance()->installEventFilter(this); appDeactivatedValue( @@ -298,7 +324,7 @@ void Application::run() { DEBUG_LOG(("Application Info: window created...")); - // Depend on activeWindow() for now :( + // Depend on primaryWindow() for now :( startShortcuts(); startDomain(); style::SetSquareUserpics(settings().fork().squareUserpics()); @@ -329,12 +355,16 @@ void Application::run() { showOpenGLCrashNotification(); } - _primaryWindow->openInMediaViewRequests( + _openInMediaViewRequests.events( ) | rpl::start_with_next([=](Media::View::OpenRequest &&request) { if (_mediaView) { _mediaView->show(std::move(request)); } - }, _primaryWindow->lifetime()); + }, _lifetime); + _primaryWindow->openInMediaViewRequests( + ) | rpl::start_to_stream( + _openInMediaViewRequests, + _primaryWindow->lifetime()); { const auto countries = std::make_shared( @@ -414,27 +444,35 @@ void Application::startSystemDarkModeViewer() { }, _lifetime); } +void Application::enumerateWindows(Fn)> callback) const { + if (_primaryWindow) { + callback(_primaryWindow.get()); + } + for (const auto &window : ranges::views::values(_secondaryWindows)) { + callback(window.get()); + } +} + +void Application::processSecondaryWindow( + not_null window) { + window->openInMediaViewRequests( + ) | rpl::start_to_stream(_openInMediaViewRequests, window->lifetime()); +} + void Application::startTray() { using WindowRaw = not_null; - const auto enumerate = [=](Fn c) { - if (_primaryWindow) { - c(_primaryWindow.get()); - } - for (const auto &window : ranges::views::values(_secondaryWindows)) { - c(window.get()); - } - }; _tray->create(); _tray->aboutToShowRequests( ) | rpl::start_with_next([=] { - enumerate([&](WindowRaw w) { w->updateIsActive(); }); + enumerateWindows([&](WindowRaw w) { w->updateIsActive(); }); _tray->updateMenuText(); }, _primaryWindow->widget()->lifetime()); _tray->showFromTrayRequests( ) | rpl::start_with_next([=] { const auto last = _lastActiveWindow; - enumerate([&](WindowRaw w) { w->widget()->showFromTray(); }); + enumerateWindows([&](WindowRaw w) { w->widget()->showFromTray(); }); if (last) { last->widget()->showFromTray(); } @@ -442,7 +480,7 @@ void Application::startTray() { _tray->hideToTrayRequests( ) | rpl::start_with_next([=] { - enumerate([&](WindowRaw w) { w->widget()->minimizeToTray(); }); + enumerateWindows([&](WindowRaw w) { w->widget()->minimizeToTray(); }); }, _primaryWindow->widget()->lifetime()); } @@ -744,7 +782,7 @@ void Application::checkLocalTime() { base::ConcurrentTimerEnvironment::Adjust(); base::unixtime::http_invalidate(); } - if (const auto session = maybeActiveSession()) { + if (const auto session = maybePrimarySession()) { session->updates().checkLastUpdate(adjusted); } } @@ -760,6 +798,12 @@ void Application::handleAppDeactivated() { if (_primaryWindow) { _primaryWindow->updateIsActiveBlur(); } + const auto session = _lastActiveWindow + ? _lastActiveWindow->maybeSession() + : nullptr; + if (session) { + session->updates().updateOnline(); + } Ui::Tooltip::Hide(); } @@ -817,7 +861,7 @@ Main::Account &Application::activeAccount() const { return _domain->active(); } -Main::Session *Application::maybeActiveSession() const { +Main::Session *Application::maybePrimarySession() const { return _domain->started() ? activeAccount().maybeSession() : nullptr; } @@ -988,18 +1032,18 @@ void Application::preventOrInvoke(Fn &&callback) { void Application::lockByPasscode() { preventOrInvoke([=] { - if (_primaryWindow) { + enumerateWindows([&](not_null w) { _passcodeLock = true; - _primaryWindow->setupPasscodeLock(); - } + w->setupPasscodeLock(); + }); }); } void Application::unlockPasscode() { clearPasscodeLock(); - if (_primaryWindow) { - _primaryWindow->clearPasscodeLock(); - } + enumerateWindows([&](not_null w) { + w->clearPasscodeLock(); + }); } void Application::clearPasscodeLock() { @@ -1013,7 +1057,7 @@ bool Application::passcodeLocked() const { void Application::updateNonIdle() { _lastNonIdleTime = crl::now(); - if (const auto session = maybeActiveSession()) { + if (const auto session = maybePrimarySession()) { session->updates().checkIdleFinish(_lastNonIdleTime); } } @@ -1087,11 +1131,9 @@ bool Application::hasActiveWindow(not_null session) const { return false; } else if (_calls->hasActivePanel(session)) { return true; - } else if (const auto controller = _primaryWindow->sessionController()) { - if (&controller->session() == session - && _primaryWindow->widget()->isActive()) { - return true; - } + } else if (const auto window = _lastActiveWindow) { + return (window->account().maybeSession() == session) + && window->widget()->isActive(); } return false; } @@ -1137,6 +1179,7 @@ Window::Controller *Application::ensureSeparateWindowForPeer( peer->owner().history(peer), std::make_unique(peer, showAtMsgId) ).first->second.get(); + processSecondaryWindow(result); result->widget()->show(); result->finishFirstShow(); return activate(result); @@ -1146,6 +1189,66 @@ Window::Controller *Application::activeWindow() const { return _lastActiveWindow; } +void Application::closeWindow(not_null window) { + for (auto i = begin(_secondaryWindows); i != end(_secondaryWindows);) { + if (i->second.get() == window) { + if (_lastActiveWindow == window) { + _lastActiveWindow = _primaryWindow.get(); + } + i = _secondaryWindows.erase(i); + } else { + ++i; + } + } +} + +void Application::closeChatFromWindows(not_null peer) { + for (const auto &[history, window] : _secondaryWindows) { + if (!window) { + continue; + } + if (history->peer == peer) { + closeWindow(window.get()); + } else if (const auto session = window->sessionController()) { + if (session->activeChatCurrent().peer() == peer) { + session->showPeerHistory( + window->singlePeer()->id, + Window::SectionShow::Way::ClearStack); + } + } + } + if (_primaryWindow && _primaryWindow->sessionController()) { + const auto primary = _primaryWindow->sessionController(); + if ((primary->activeChatCurrent().peer() == peer) + && (&primary->session() == &peer->session())) { + // showChatsList + primary->showPeerHistory( + PeerId(0), + Window::SectionShow::Way::ClearStack); + } + } +} + +void Application::windowActivated(not_null window) { + const auto was = _lastActiveWindow; + const auto now = window; + _lastActiveWindow = window; + + const auto wasSession = was ? was->maybeSession() : nullptr; + const auto nowSession = now->maybeSession(); + if (wasSession != nowSession) { + if (wasSession) { + wasSession->updates().updateOnline(); + } + if (nowSession) { + nowSession->updates().updateOnline(); + } + } + if (_mediaView && !_mediaView->isHidden()) { + _mediaView->activate(); + } +} + bool Application::closeActiveWindow() { if (hideMediaView()) { return true; @@ -1174,11 +1277,12 @@ bool Application::minimizeActiveWindow() { } QWidget *Application::getFileDialogParent() { - return (_mediaView && !_mediaView->isHidden()) - ? static_cast(_mediaView->widget()) - : activeWindow() - ? static_cast(activeWindow()->widget()) - : nullptr; + if (const auto view = _mediaView.get(); view && !view->isHidden()) { + return view->widget(); + } else if (const auto active = activeWindow()) { + return active->widget(); + } + return nullptr; } void Application::notifyFileDialogShown(bool shown) { @@ -1187,12 +1291,6 @@ void Application::notifyFileDialogShown(bool shown) { } } -void Application::checkMediaViewActivation() { - if (_mediaView && !_mediaView->isHidden()) { - _mediaView->activate(); - } -} - QPoint Application::getPointForCallPanelCenter() const { if (const auto window = activeWindow()) { return window->getPointForCallPanelCenter(); diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index 056ada5bdf017d..6958cf222ca274 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -7,15 +7,11 @@ For license and copyright information please follow this link: */ #pragma once -#include "core/core_settings.h" #include "mtproto/mtproto_auth_key.h" #include "mtproto/mtproto_proxy_data.h" #include "base/timer.h" -class MainWindow; -class MainWidget; -class FileUploader; -class Translator; +class History; namespace Platform { class Integration; @@ -69,6 +65,7 @@ class Instance; } // namespace Audio namespace View { class OverlayWidget; +struct OpenRequest; } // namespace View namespace Player { class FloatController; @@ -103,6 +100,7 @@ namespace Core { class Launcher; struct LocalUrlHandler; +class Settings; class Tray; enum class LaunchState { @@ -162,15 +160,17 @@ class Application final : public QObject { Window::Controller *ensureSeparateWindowForPeer( not_null peer, MsgId showAtMsgId); + void closeWindow(not_null window); + void windowActivated(not_null window); bool closeActiveWindow(); bool minimizeActiveWindow(); [[nodiscard]] QWidget *getFileDialogParent(); void notifyFileDialogShown(bool shown); void checkSystemDarkMode(); [[nodiscard]] bool isActiveForTrayMenu() const; + void closeChatFromWindows(not_null peer); // Media view interface. - void checkMediaViewActivation(); bool hideMediaView(); [[nodiscard]] QPoint getPointForCallPanelCenter() const; @@ -207,7 +207,7 @@ class Application final : public QObject { [[nodiscard]] bool exportPreventsQuit(); // Main::Session component. - Main::Session *maybeActiveSession() const; + Main::Session *maybePrimarySession() const; [[nodiscard]] int unreadBadge() const; [[nodiscard]] bool unreadBadgeMuted() const; [[nodiscard]] rpl::producer<> unreadBadgeChanges() const; @@ -323,6 +323,10 @@ class Application final : public QObject { void startSystemDarkModeViewer(); void startTray(); + void enumerateWindows( + Fn)> callback) const; + void processSecondaryWindow(not_null window); + friend void QuitAttempt(); void quitDelayed(); [[nodiscard]] bool readyToQuit(); @@ -402,6 +406,8 @@ class Application final : public QObject { }; base::flat_map, LeaveFilter> _leaveFilters; + rpl::event_stream _openInMediaViewRequests; + rpl::lifetime _lifetime; crl::time _lastNonIdleTime = 0; diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index 288594a7d122c0..90aa2575c73fbb 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -129,6 +129,15 @@ std::map BetaLogs() { "- Improve some more sections design.\n" "- Update the OpenAL library to 1.22.0.\n" + }, + { + 3007006, + "- Settings > Advanced > Experimental adds an option " + "to open chats in separate windows.\n" + + "- Fix possible crash in video chat reconnection.\n" + + "- Fix possible crash after account switch.\n" } }; }; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 67f7a84ea27b35..caffdce7ca7f67 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -25,11 +25,11 @@ For license and copyright information please follow this link: #include "data/data_user.h" #include "data/data_session.h" #include "window/window_session_controller.h" -#include "boxes/abstract_box.h" // Ui::hideLayer(). -#include "facades.h" namespace { +// Possible context owners: media viewer, profile, history widget. + void SearchByHashtag(ClickContext context, const QString &tag) { const auto my = context.other.value(); const auto controller = my.sessionWindow.get(); @@ -117,15 +117,18 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { : parsedUrl.isValid() ? QString::fromUtf8(parsedUrl.toEncoded()) : ShowEncoded(displayed); - Ui::show( - Ui::MakeConfirmBox({ - .text = (tr::lng_open_this_link(tr::now) - + qsl("\n\n") - + displayUrl), - .confirmed = [=] { Ui::hideLayer(); open(); }, - .confirmText = tr::lng_open_link(), - }), - Ui::LayerOption::KeepOther); + const auto my = context.value(); + if (const auto controller = my.sessionWindow.get()) { + controller->show( + Ui::MakeConfirmBox({ + .text = (tr::lng_open_this_link(tr::now) + + qsl("\n\n") + + displayUrl), + .confirmed = [=](Fn hide) { hide(); open(); }, + .confirmText = tr::lng_open_link(), + }), + Ui::LayerOption::KeepOther); + } } else { open(); } @@ -148,16 +151,22 @@ void BotGameUrlClickHandler::onClick(ClickContext context) const { || _bot->session().local().isBotTrustedOpenGame(_bot->id)) { open(); } else { - const auto callback = [=, bot = _bot] { - Ui::hideLayer(); - bot->session().local().markBotTrustedOpenGame(bot->id); - open(); - }; - Ui::show(Ui::MakeConfirmBox({ - .text = tr::lng_allow_bot_pass(tr::now, lt_bot_name, _bot->name), - .confirmed = callback, - .confirmText = tr::lng_allow_bot(), - })); + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + const auto callback = [=, bot = _bot](Fn close) { + close(); + bot->session().local().markBotTrustedOpenGame(bot->id); + open(); + }; + controller->show(Ui::MakeConfirmBox({ + .text = tr::lng_allow_bot_pass( + tr::now, + lt_bot_name, + _bot->name), + .confirmed = callback, + .confirmText = tr::lng_allow_bot(), + })); + } } } @@ -172,9 +181,10 @@ QString MentionClickHandler::copyToClipboardContextItemText() const { void MentionClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { - if (const auto m = App::main()) { // multi good + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { using Info = Window::SessionNavigation::PeerByLinkInfo; - m->controller()->showPeerByLink(Info{ + controller->showPeerByLink(Info{ .usernameOrId = _tag.mid(1), .resolveType = Window::ResolveType::Mention, }); @@ -189,8 +199,11 @@ auto MentionClickHandler::getTextEntity() const -> TextEntity { void MentionNameClickHandler::onClick(ClickContext context) const { const auto button = context.button; if (button == Qt::LeftButton || button == Qt::MiddleButton) { - if (auto user = _session->data().userLoaded(_userId)) { - Ui::showPeerProfile(user); + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + if (auto user = _session->data().userLoaded(_userId)) { + controller->showPeerInfo(user); + } } } } @@ -295,14 +308,17 @@ void MonospaceClickHandler::onClick(ClickContext context) const { const auto hasCopyRestriction = item && (!item->history()->peer->allowsForwarding() || item->forbidsForward()); + const auto toastParent = Window::Show(controller).toastParent(); if (hasCopyRestriction) { - Ui::Toast::Show(item->history()->peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now)); + Ui::Toast::Show( + toastParent, + item->history()->peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return; } + Ui::Toast::Show(toastParent, tr::lng_text_copied(tr::now)); } - Ui::Toast::Show(tr::lng_text_copied(tr::now)); TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed())); } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 73154ecc7a65a2..b4d352325f7d88 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -36,6 +36,7 @@ struct ClickHandlerContext { Fn elementDelegate; base::weak_ptr sessionWindow; bool skipBotAutoLogin = false; + bool botStartAutoSubmit = false; // Is filled from peer info. PeerData *peer = nullptr; }; diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 4f71408f5eb9bf..4be8521a959747 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -46,8 +46,6 @@ enum class OrderMode; namespace Core { struct WindowPosition { - WindowPosition() = default; - int32 moncrc = 0; int maximized = 0; int scale = 0; diff --git a/Telegram/SourceFiles/core/crash_report_window.cpp b/Telegram/SourceFiles/core/crash_report_window.cpp index 9f9f8c703d0c8b..60826a86a07678 100644 --- a/Telegram/SourceFiles/core/crash_report_window.cpp +++ b/Telegram/SourceFiles/core/crash_report_window.cpp @@ -22,7 +22,6 @@ For license and copyright information please follow this link: #include #include #include -#include namespace { @@ -44,10 +43,7 @@ PreLaunchWindow::PreLaunchWindow(QString title) { p.setColor(QPalette::Window, QColor(255, 255, 255)); setPalette(p); - const auto dpi = screen()->handle()->logicalDpi().second; - auto font = this->font(); - font.setPixelSize(base::SafeRound(std::floor(font.pointSizeF() * dpi / 72. * 100. + 0.5) / 100.)); - _size = QFontMetrics(font).height(); + _size = QFontMetrics(font()).height(); int paddingVertical = (_size / 2); int paddingHorizontal = _size; diff --git a/Telegram/SourceFiles/core/crash_reports.cpp b/Telegram/SourceFiles/core/crash_reports.cpp index 0c5d401316125b..bc626697350325 100644 --- a/Telegram/SourceFiles/core/crash_reports.cpp +++ b/Telegram/SourceFiles/core/crash_reports.cpp @@ -22,7 +22,7 @@ For license and copyright information please follow this link: #pragma warning(push) #pragma warning(disable:4091) -#include "client/windows/handler/exception_handler.h" +#include #pragma warning(pop) #elif defined Q_OS_UNIX // Q_OS_WIN @@ -36,14 +36,14 @@ For license and copyright information please follow this link: #include #ifdef MAC_USE_BREAKPAD -#include "client/mac/handler/exception_handler.h" +#include #else // MAC_USE_BREAKPAD -#include "client/crashpad_client.h" +#include #endif // else for MAC_USE_BREAKPAD #else // Q_OS_MAC -#include "client/linux/handler/exception_handler.h" +#include #endif // Q_OS_MAC diff --git a/Telegram/SourceFiles/core/file_location.cpp b/Telegram/SourceFiles/core/file_location.cpp index f9c927f7f17607..8d2eef45834fe5 100644 --- a/Telegram/SourceFiles/core/file_location.cpp +++ b/Telegram/SourceFiles/core/file_location.cpp @@ -16,6 +16,7 @@ namespace Core { namespace { const auto kInMediaCacheLocation = u"*media_cache*"_q; +constexpr auto kMaxFileSize = 4000 * int64(1024 * 1024); } // namespace @@ -55,13 +56,13 @@ FileLocation::FileLocation(const QFileInfo &info) : fname(info.filePath()) { void FileLocation::resolveFromInfo(const QFileInfo &info) { if (info.exists()) { const auto s = info.size(); - if (s > INT_MAX) { + if (s > kMaxFileSize) { fname = QString(); _bookmark = nullptr; size = 0; } else { modified = info.lastModified(); - size = qint32(s); + size = s; } } else { fname = QString(); @@ -88,12 +89,12 @@ bool FileLocation::check() const { if (!f.isReadable()) return false; quint64 s = f.size(); - if (s > INT_MAX) { + if (s > kMaxFileSize) { DEBUG_LOG(("File location check: Wrong size %1").arg(s)); return false; } - if (qint32(s) != size) { + if (s != size) { DEBUG_LOG(("File location check: Wrong size %1 when should be %2").arg(s).arg(size)); return false; } diff --git a/Telegram/SourceFiles/core/file_location.h b/Telegram/SourceFiles/core/file_location.h index f1c62930b010da..a7d89a5d3ca1eb 100644 --- a/Telegram/SourceFiles/core/file_location.h +++ b/Telegram/SourceFiles/core/file_location.h @@ -55,7 +55,7 @@ class FileLocation { QString fname; QDateTime modified; - qint32 size; + qint64 size = 0; private: void resolveFromInfo(const QFileInfo &info); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index c4082ba35491e3..65191822fbfc2b 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -38,14 +38,17 @@ For license and copyright information please follow this link: #include "window/window_session_controller.h" #include "window/window_controller.h" #include "window/themes/window_theme_editor_box.h" // GenerateSlug. +#include "payments/payments_checkout_process.h" #include "settings/settings_common.h" #include "settings/settings_folders.h" #include "settings/settings_main.h" #include "settings/settings_privacy_security.h" #include "settings/settings_chat.h" +#include "settings/settings_premium.h" #include "mainwidget.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "inline_bots/bot_attach_web_view.h" #include "history/history.h" #include "base/qt/qt_common_adapters.h" #include "apiwrap.h" @@ -363,7 +366,7 @@ bool ResolveUsernameOrPhone( startToken = gameParam; resolveType = ResolveType::ShareGame; } - const auto fromMessageId = context.value().itemId; + const auto myContext = context.value(); using Navigation = Window::SessionNavigation; controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = domain, @@ -381,10 +384,13 @@ bool ResolveUsernameOrPhone( .resolveType = resolveType, .startToken = startToken, .startAdminRights = adminRights, + .startAutoSubmit = myContext.botStartAutoSubmit, .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) ? params.value(u"startattach"_q) : std::optional()), + .attachBotChooseTypes = InlineBots::ParseChooseTypes( + params.value(u"choose"_q)), .voicechatHash = (params.contains(u"livestream"_q) ? std::make_optional(params.value(u"livestream"_q)) : params.contains(u"videochat"_q) @@ -392,7 +398,7 @@ bool ResolveUsernameOrPhone( : params.contains(u"voicechat"_q) ? std::make_optional(params.value(u"voicechat"_q)) : std::nullopt), - .clickFromMessageId = fromMessageId, + .clickFromMessageId = myContext.itemId, }); controller->window().activate(); return true; @@ -447,7 +453,7 @@ bool ResolveSettings( } controller->window().activate(); const auto section = match->captured(1).mid(1).toLower(); - + const auto type = [&]() -> std::optional<::Settings::Type> { if (section == qstr("language")) { ShowLanguagesBox(); @@ -466,7 +472,7 @@ bool ResolveSettings( } return ::Settings::Main::Id(); }(); - + if (type.has_value()) { controller->showSettings(*type); controller->window().activate(); @@ -559,7 +565,9 @@ bool ShowInviteLink( return false; } QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show(tr::lng_group_invite_copied(tr::now)); + Ui::Toast::Show( + Window::Show(controller).toastParent(), + tr::lng_group_invite_copied(tr::now)); return true; } @@ -573,13 +581,15 @@ bool OpenExternalLink( } void ExportTestChatTheme( - not_null session, + not_null controller, not_null theme) { + const auto session = &controller->session(); + const auto show = std::make_shared(controller); const auto inputSettings = [&](Data::CloudThemeType type) -> std::optional { const auto i = theme->settings.find(type); if (i == end(theme->settings)) { - Ui::Toast::Show("Something went wrong :("); + Ui::Toast::Show(show->toastParent(), "Something went wrong :("); return std::nullopt; } const auto &fields = i->second; @@ -587,7 +597,7 @@ void ExportTestChatTheme( || !fields.paper->isPattern() || fields.paper->backgroundColors().empty() || !fields.paper->hasShareUrl()) { - Ui::Toast::Show("Something went wrong :("); + Ui::Toast::Show(show->toastParent(), "Something went wrong :("); return std::nullopt; } const auto &bg = fields.paper->backgroundColors(); @@ -595,7 +605,9 @@ void ExportTestChatTheme( const auto from = url.indexOf("bg/"); const auto till = url.indexOf("?"); if (from < 0 || till <= from) { - Ui::Toast::Show("Bad WallPaper link: " + url); + Ui::Toast::Show( + show->toastParent(), + "Bad WallPaper link: " + url); return std::nullopt; } @@ -677,9 +689,15 @@ void ExportTestChatTheme( const auto slug = Data::CloudTheme::Parse(session, result, true).slug; QGuiApplication::clipboard()->setText( session->createInternalLinkFull("addtheme/" + slug)); - Ui::Toast::Show(tr::lng_background_link_copied(tr::now)); + if (show->valid()) { + Ui::Toast::Show( + show->toastParent(), + tr::lng_background_link_copied(tr::now)); + } }).fail([=](const MTP::Error &error) { - Ui::Toast::Show("Error: " + error.type()); + if (show->valid()) { + Ui::Toast::Show(show->toastParent(), "Error: " + error.type()); + } }).send(); } @@ -700,7 +718,7 @@ bool ResolveTestChatTheme( params); if (theme) { if (!params["export"].isEmpty()) { - ExportTestChatTheme(&controller->session(), &*theme); + ExportTestChatTheme(controller, &*theme); } const auto recache = [&](Data::CloudThemeType type) { [[maybe_unused]] auto value = theme->settings.contains(type) @@ -714,6 +732,46 @@ bool ResolveTestChatTheme( return true; } +bool ResolveInvoice( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto params = url_parse_params( + match->captured(1), + qthelp::UrlParamNameTransform::ToLower); + const auto slug = params.value(qsl("slug")); + if (slug.isEmpty()) { + return false; + } + const auto window = &controller->window(); + Payments::CheckoutProcess::Start( + &controller->session(), + slug, + crl::guard(window, [=](auto) { window->activate(); })); + return true; +} + +bool ResolvePremiumOffer( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto params = url_parse_params( + match->captured(1).mid(1), + qthelp::UrlParamNameTransform::ToLower); + const auto refAddition = params.value(qsl("ref")); + const auto ref = "deeplink" + + (refAddition.isEmpty() ? QString() : '_' + refAddition); + ::Settings::ShowPremium(controller, ref); + controller->window().activate(); + return true; +} + } // namespace const std::vector &LocalUrlHandlers() { @@ -778,6 +836,14 @@ const std::vector &LocalUrlHandlers() { qsl("^test_chat_theme/?\\?(.+)(#|$)"), ResolveTestChatTheme, }, + { + qsl("invoice/?\\?(.+)(#|$)"), + ResolveInvoice, + }, + { + qsl("premium_offer/?(\\?.+)?(#|$)"), + ResolvePremiumOffer, + }, { qsl("^([^\\?]+)(\\?|#|$)"), HandleUnknown @@ -844,6 +910,8 @@ QString TryConvertUrlToLocal(QString url) { return qsl("tg://socks?") + socksMatch->captured(1); } else if (auto proxyMatch = regex_match(qsl("^proxy/?\\?(.+)(#|$)"), query, matchOptions)) { return qsl("tg://proxy?") + proxyMatch->captured(1); + } else if (auto invoiceMatch = regex_match(qsl("^(invoice/|\\$)([a-zA-Z0-9_\\-]+)(\\?|#|$)"), query, matchOptions)) { + return qsl("tg://invoice?slug=") + invoiceMatch->captured(2); } else if (auto bgMatch = regex_match(qsl("^bg/([a-zA-Z0-9\\.\\_\\-\\~]+)(\\?(.+)?)?$"), query, matchOptions)) { const auto params = bgMatch->captured(3); const auto bg = bgMatch->captured(1); diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index c6e6d3c3ea716b..9e9b5a4762efa5 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -31,7 +31,6 @@ For license and copyright information please follow this link: #include #include #include -#include namespace Core { namespace { @@ -219,11 +218,7 @@ void Sandbox::launchApplication() { } void Sandbox::setupScreenScale() { - constexpr auto processDpi = [](const QDpi &dpi) { - return (dpi.first + dpi.second) * 0.5; - }; - const auto dpi = processDpi( - Sandbox::primaryScreen()->handle()->logicalDpi()); + const auto dpi = Sandbox::primaryScreen()->logicalDotsPerInch(); LOG(("Primary screen DPI: %1").arg(dpi)); if (dpi <= 108) { cSetScreenScale(100); // 100%: 96 DPI (0-108) @@ -366,7 +361,6 @@ void Sandbox::singleInstanceChecked() { LOG(("App Info: Detected another instance")); } - Ui::DisableCustomScaling(); refreshGlobalProxy(); if (!Logs::started() || !Logs::instanceChecked()) { new NotStartedWindow(); diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 2d6f7903ad02de..eec50c170698f7 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -23,7 +23,7 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "main/main_domain.h" #include "info/info_memento.h" -#include "info/settings/info_settings_widget.h" +#include "info/info_controller.h" #include "window/window_session_controller.h" #include "settings/settings_advanced.h" #include "settings/settings_intro.h" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 55a1364f9e3d28..e623e7f34ce9ab 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -24,7 +24,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 3007005; -constexpr auto AppVersionStr = "3.7.5"; +constexpr auto AppVersion = 3007006; +constexpr auto AppVersionStr = "3.7.6"; constexpr auto AppBetaVersion = true; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_auto_download.cpp b/Telegram/SourceFiles/data/data_auto_download.cpp index 15e164b93cc737..c0ea8505518769 100644 --- a/Telegram/SourceFiles/data/data_auto_download.cpp +++ b/Telegram/SourceFiles/data/data_auto_download.cpp @@ -17,8 +17,8 @@ namespace Data { namespace AutoDownload { namespace { -constexpr auto kDefaultMaxSize = 8 * 1024 * 1024; -constexpr auto kDefaultAutoPlaySize = 50 * 1024 * 1024; +constexpr auto kDefaultMaxSize = 8 * int64(1024 * 1024); +constexpr auto kDefaultAutoPlaySize = 50 * int64(1024 * 1024); constexpr auto kVersion1 = char(1); constexpr auto kVersion = char(2); @@ -82,26 +82,29 @@ Type AutoPlayTypeFromDocument(not_null document) { } // namespace -void Single::setBytesLimit(int bytesLimit) { +void Single::setBytesLimit(int64 bytesLimit) { Expects(bytesLimit >= 0 && bytesLimit <= kMaxBytesLimit); - _limit = bytesLimit; + _limit = int32(uint32(bytesLimit)); + + Ensures(hasValue()); } bool Single::hasValue() const { - return (_limit >= 0); + return (_limit != -1); } -bool Single::shouldDownload(int fileSize) const { +bool Single::shouldDownload(int64 fileSize) const { Expects(hasValue()); - return (_limit > 0) && (fileSize <= _limit); + const auto realLimit = bytesLimit(); + return (realLimit > 0) && (fileSize <= realLimit); } -int Single::bytesLimit() const { +int64 Single::bytesLimit() const { Expects(hasValue()); - return _limit; + return uint32(_limit); } qint32 Single::serialize() const { @@ -109,7 +112,8 @@ qint32 Single::serialize() const { } bool Single::setFromSerialized(qint32 serialized) { - if (serialized < -1 || serialized > kMaxBytesLimit) { + auto realLimit = quint32(serialized); + if (serialized != -1 && int64(realLimit) > kMaxBytesLimit) { return false; } _limit = serialized; @@ -127,7 +131,7 @@ Single &Set::single(Type type) { return const_cast(static_cast(this)->single(type)); } -void Set::setBytesLimit(Type type, int bytesLimit) { +void Set::setBytesLimit(Type type, int64 bytesLimit) { single(type).setBytesLimit(bytesLimit); } @@ -135,11 +139,11 @@ bool Set::hasValue(Type type) const { return single(type).hasValue(); } -bool Set::shouldDownload(Type type, int fileSize) const { +bool Set::shouldDownload(Type type, int64 fileSize) const { return single(type).shouldDownload(fileSize); } -int Set::bytesLimit(Type type) const { +int64 Set::bytesLimit(Type type) const { return single(type).bytesLimit(); } @@ -174,11 +178,11 @@ const Set &Full::setOrDefault(Source source, Type type) const { return result; } -void Full::setBytesLimit(Source source, Type type, int bytesLimit) { +void Full::setBytesLimit(Source source, Type type, int64 bytesLimit) { set(source).setBytesLimit(type, bytesLimit); } -bool Full::shouldDownload(Source source, Type type, int fileSize) const { +bool Full::shouldDownload(Source source, Type type, int64 fileSize) const { if (ranges::find(kStreamedTypes, type) != end(kStreamedTypes)) { // With streaming we disable autodownload and hide them in Settings. return false; @@ -186,7 +190,7 @@ bool Full::shouldDownload(Source source, Type type, int fileSize) const { return setOrDefault(source, type).shouldDownload(type, fileSize); } -int Full::bytesLimit(Source source, Type type) const { +int64 Full::bytesLimit(Source source, Type type) const { return setOrDefault(source, type).bytesLimit(type); } @@ -313,7 +317,7 @@ bool ShouldAutoPlay( not_null peer, not_null photo) { const auto source = SourceFromPeer(peer); - const auto size = photo->videoByteSize(); + const auto size = photo->videoByteSize(PhotoSize::Large); return photo->hasVideo() && (data.shouldDownload(source, Type::AutoPlayGIF, size) || data.shouldDownload(source, Type::AutoPlayVideo, size) diff --git a/Telegram/SourceFiles/data/data_auto_download.h b/Telegram/SourceFiles/data/data_auto_download.h index 410451317f6dee..a3275452fd9089 100644 --- a/Telegram/SourceFiles/data/data_auto_download.h +++ b/Telegram/SourceFiles/data/data_auto_download.h @@ -12,7 +12,7 @@ For license and copyright information please follow this link: namespace Data { namespace AutoDownload { -constexpr auto kMaxBytesLimit = 4000 * 512 * 1024; +constexpr auto kMaxBytesLimit = 8000 * int64(512 * 1024); enum class Source { User = 0x00, @@ -47,27 +47,27 @@ constexpr auto kTypesCount = 7; class Single { public: - void setBytesLimit(int bytesLimit); + void setBytesLimit(int64 bytesLimit); bool hasValue() const; - bool shouldDownload(int fileSize) const; - int bytesLimit() const; + bool shouldDownload(int64 fileSize) const; + int64 bytesLimit() const; qint32 serialize() const; bool setFromSerialized(qint32 serialized); private: - int _limit = -1; + int _limit = -1; // FileSize: Right now any file size fits 32 bit. }; class Set { public: - void setBytesLimit(Type type, int bytesLimit); + void setBytesLimit(Type type, int64 bytesLimit); bool hasValue(Type type) const; - bool shouldDownload(Type type, int fileSize) const; - int bytesLimit(Type type) const; + bool shouldDownload(Type type, int64 fileSize) const; + int64 bytesLimit(Type type) const; qint32 serialize(Type type) const; bool setFromSerialized(Type type, qint32 serialized); @@ -82,13 +82,13 @@ class Set { class Full { public: - void setBytesLimit(Source source, Type type, int bytesLimit); + void setBytesLimit(Source source, Type type, int64 bytesLimit); [[nodiscard]] bool shouldDownload( Source source, Type type, - int fileSize) const; - [[nodiscard]] int bytesLimit(Source source, Type type) const; + int64 fileSize) const; + [[nodiscard]] int64 bytesLimit(Source source, Type type) const; [[nodiscard]] QByteArray serialize() const; bool setFromSerialized(const QByteArray &serialized); diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index c74ffde88b4c93..85221a847dc83c 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -18,6 +18,7 @@ For license and copyright information please follow this link: #include "data/data_histories.h" #include "data/data_group_call.h" #include "data/data_message_reactions.h" +#include "data/data_peer_bot_command.h" #include "main/main_session.h" #include "main/session/send_as_peers.h" #include "base/unixtime.h" @@ -49,14 +50,9 @@ void MegagroupInfo::setLocation(const ChannelLocation &location) { _location = location; } -bool MegagroupInfo::updateBotCommands(const MTPVector &data) { - return Data::UpdateBotCommands(_botCommands, data); -} - -bool MegagroupInfo::updateBotCommands( - UserId botId, - const MTPVector &data) { - return Data::UpdateBotCommands(_botCommands, botId, data); +Data::ChatBotCommands::Changed MegagroupInfo::setBotCommands( + const std::vector &list) { + return _botCommands.update(list); } ChannelData::ChannelData(not_null owner, PeerId id) @@ -92,7 +88,10 @@ ChannelData::ChannelData(not_null owner, PeerId id) void ChannelData::setPhoto(const MTPChatPhoto &photo) { photo.match([&](const MTPDchatPhoto & data) { - updateUserpic(data.vphoto_id().v, data.vdc_id().v); + updateUserpic( + data.vphoto_id().v, + data.vdc_id().v, + data.is_has_video()); }, [&](const MTPDchatPhotoEmpty &) { clearUserpic(); }); @@ -467,7 +466,8 @@ bool ChannelData::canPublish() const { bool ChannelData::canWrite() const { // Duplicated in Data::CanWriteValue(). - const auto allowed = amIn() || (flags() & Flag::HasLink); + const auto allowed = amIn() + || ((flags() & Flag::HasLink) && !(flags() & Flag::JoinToWrite)); return allowed && (canPublish() || (!isBroadcast() && !amRestricted(Restriction::SendMessages))); @@ -903,7 +903,13 @@ void ApplyChannelUpdate( SetTopPinnedMessageId(channel, pinned->v); } if (channel->isMegagroup()) { - if (channel->mgInfo->updateBotCommands(update.vbot_info())) { + auto commands = ranges::views::all( + update.vbot_info().v + ) | ranges::views::transform( + Data::BotCommandsFromTL + ) | ranges::to_vector; + + if (channel->mgInfo->setBotCommands(std::move(commands))) { channel->owner().botCommandsChanged(channel); } const auto stickerSet = update.vstickerset(); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 5496dd56c8fd71..c895626a8a2200 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -11,6 +11,7 @@ For license and copyright information please follow this link: #include "data/data_pts_waiter.h" #include "data/data_location.h" #include "data/data_chat_participant_status.h" +#include "data/data_peer_bot_commands.h" struct ChannelLocation { QString address; @@ -52,6 +53,8 @@ enum class ChannelDataFlag { HasLink = (1 << 18), SlowmodeEnabled = (1 << 19), NoForwards = (1 << 20), + JoinToWrite = (1 << 21), + RequestToJoin = (1 << 22), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -83,12 +86,9 @@ class MegagroupInfo { const ChannelLocation *getLocation() const; void setLocation(const ChannelLocation &location); - bool updateBotCommands(const MTPVector &data); - bool updateBotCommands( - UserId botId, - const MTPVector &data); - [[nodiscard]] auto botCommands() const - -> const base::flat_map> & { + Data::ChatBotCommands::Changed setBotCommands( + const std::vector &commands); + [[nodiscard]] const Data::ChatBotCommands &botCommands() const { return _botCommands; } @@ -118,7 +118,7 @@ class MegagroupInfo { private: ChatData *_migratedFrom = nullptr; ChannelLocation _location; - base::flat_map> _botCommands; + Data::ChatBotCommands _botCommands; }; @@ -255,6 +255,12 @@ class ChannelData : public PeerData { [[nodiscard]] bool amCreator() const { return flags() & Flag::Creator; } + [[nodiscard]] bool joinToWrite() const { + return flags() & Flag::JoinToWrite; + } + [[nodiscard]] bool requestToJoin() const { + return flags() & Flag::RequestToJoin; + } [[nodiscard]] auto adminRights() const { return _adminRights.current(); diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index 090dda7ded0b0a..f5f272d9945094 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -39,7 +39,10 @@ ChatData::ChatData(not_null owner, PeerId id) void ChatData::setPhoto(const MTPChatPhoto &photo) { photo.match([&](const MTPDchatPhoto &data) { - updateUserpic(data.vphoto_id().v, data.vdc_id().v); + updateUserpic( + data.vphoto_id().v, + data.vdc_id().v, + data.is_has_video()); }, [&](const MTPDchatPhotoEmpty &) { clearUserpic(); }); @@ -254,16 +257,8 @@ PeerId ChatData::groupCallDefaultJoinAs() const { return _callDefaultJoinAs; } -void ChatData::setBotCommands(const MTPVector &data) { - if (Data::UpdateBotCommands(_botCommands, data)) { - owner().botCommandsChanged(this); - } -} - -void ChatData::setBotCommands( - UserId botId, - const MTPVector &data) { - if (Data::UpdateBotCommands(_botCommands, botId, data)) { +void ChatData::setBotCommands(const std::vector &list) { + if (_botCommands.update(list)) { owner().botCommandsChanged(this); } } @@ -454,9 +449,12 @@ void ApplyChatUpdate(not_null chat, const MTPDchatFull &update) { chat->setMessagesTTL(update.vttl_period().value_or_empty()); if (const auto info = update.vbot_info()) { - chat->setBotCommands(*info); + auto &&commands = ranges::views::all( + info->v + ) | ranges::views::transform(Data::BotCommandsFromTL); + chat->setBotCommands(std::move(commands) | ranges::to_vector); } else { - chat->setBotCommands(MTP_vector()); + chat->setBotCommands({}); } using Flag = ChatDataFlag; const auto mask = Flag::CanSetUsername; diff --git a/Telegram/SourceFiles/data/data_chat.h b/Telegram/SourceFiles/data/data_chat.h index 66df6e0a7eb9e3..f75c519bbac453 100644 --- a/Telegram/SourceFiles/data/data_chat.h +++ b/Telegram/SourceFiles/data/data_chat.h @@ -9,6 +9,7 @@ For license and copyright information please follow this link: #include "data/data_peer.h" #include "data/data_chat_participant_status.h" +#include "data/data_peer_bot_commands.h" enum class ChatAdminRight; @@ -148,12 +149,8 @@ class ChatData : public PeerData { void setGroupCallDefaultJoinAs(PeerId peerId); [[nodiscard]] PeerId groupCallDefaultJoinAs() const; - void setBotCommands(const MTPVector &data); - void setBotCommands( - UserId botId, - const MTPVector &data); - [[nodiscard]] auto botCommands() const - -> const base::flat_map> & { + void setBotCommands(const std::vector &commands); + [[nodiscard]] const Data::ChatBotCommands &botCommands() const { return _botCommands; } @@ -201,7 +198,7 @@ class ChatData : public PeerData { std::unique_ptr _call; PeerId _callDefaultJoinAs = 0; - base::flat_map> _botCommands; + Data::ChatBotCommands _botCommands; ChannelData *_migratedTo = nullptr; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 68812aaa83ebaa..d77b47c02e43c8 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -16,9 +16,12 @@ For license and copyright information please follow this link: #include "data/data_folder.h" #include "data/data_histories.h" #include "dialogs/dialogs_main_list.h" +#include "history/history.h" #include "history/history_unread_things.h" #include "ui/ui_utility.h" #include "main/main_session.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "apiwrap.h" namespace Data { @@ -104,6 +107,8 @@ ChatFilter ChatFilter::FromTL( std::move(list), std::move(pinned), { never.begin(), never.end() }); + }, [](const MTPDdialogFilterDefault &d) { + return ChatFilter(); }); } @@ -217,6 +222,7 @@ bool ChatFilter::contains(not_null history) const { } ChatFilters::ChatFilters(not_null owner) : _owner(owner) { + _list.emplace_back(); crl::on_main(&owner->session(), [=] { load(); }); } @@ -225,10 +231,15 @@ ChatFilters::~ChatFilters() = default; not_null ChatFilters::chatsList(FilterId filterId) { auto &pointer = _chatsLists[filterId]; if (!pointer) { + auto limit = rpl::single(rpl::empty_value()) | rpl::then( + _owner->session().account().appConfig().refreshed() + ) | rpl::map([=] { + return _owner->pinnedChatsLimit(nullptr, filterId); + }); pointer = std::make_unique( &_owner->session(), filterId, - rpl::single(ChatFilter::kPinnedLimit)); + _owner->maxPinnedChatsLimitValue(nullptr, filterId)); } return pointer.get(); } @@ -287,6 +298,9 @@ void ChatFilters::received(const QVector &list) { applyRemove(position); changed = true; } + if (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) { + _list.insert(begin(_list), ChatFilter()); + } if (changed || !_loaded) { _loaded = true; _listChanged.fire({}); @@ -344,6 +358,16 @@ void ChatFilters::remove(FilterId id) { _listChanged.fire({}); } +void ChatFilters::moveAllToFront() { + const auto i = ranges::find(_list, FilterId(), &ChatFilter::id); + if (!_list.empty() && i == begin(_list)) { + return; + } else if (i != end(_list)) { + _list.erase(i); + } + _list.insert(begin(_list), ChatFilter()); +} + void ChatFilters::applyRemove(int position) { Expects(position >= 0 && position < _list.size()); @@ -450,6 +474,7 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( const auto i = ranges::find(_list, id, &ChatFilter::id); Assert(i != end(_list)); + const auto limit = _owner->pinnedChatsLimit(nullptr, id); auto always = i->always(); auto pinned = std::vector>(); pinned.reserve(dialogs.size()); @@ -457,7 +482,7 @@ const ChatFilter &ChatFilters::applyUpdatedPinned( if (const auto history = row.history()) { if (always.contains(history)) { pinned.push_back(history); - } else if (always.size() < ChatFilter::kPinnedLimit) { + } else if (always.size() < limit) { always.insert(history); pinned.push_back(history); } @@ -509,6 +534,32 @@ const std::vector &ChatFilters::list() const { return _list; } +FilterId ChatFilters::defaultId() const { + return lookupId(0); +} + +FilterId ChatFilters::lookupId(int index) const { + Expects(index >= 0 && index < _list.size()); + + if (_owner->session().user()->isPremium() || !_list.front().id()) { + return _list[index].id(); + } + const auto i = ranges::find(_list, FilterId(0), &ChatFilter::id); + return !index + ? FilterId() + : (index <= int(i - begin(_list))) + ? _list[index - 1].id() + : _list[index].id(); +} + +bool ChatFilters::loaded() const { + return _loaded; +} + +bool ChatFilters::has() const { + return _list.size() > 1; +} + rpl::producer<> ChatFilters::changed() const { return _listChanged.events(); } diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 43cb0d58177dbf..b4637e0c2d4ac3 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -35,8 +35,6 @@ class ChatFilter final { friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; - static constexpr int kPinnedLimit = 100; - ChatFilter() = default; ChatFilter( FilterId id, @@ -101,8 +99,14 @@ class ChatFilters final { void apply(const MTPUpdate &update); void set(ChatFilter filter); void remove(FilterId id); + void moveAllToFront(); [[nodiscard]] const std::vector &list() const; [[nodiscard]] rpl::producer<> changed() const; + [[nodiscard]] bool loaded() const; + [[nodiscard]] bool has() const; + + [[nodiscard]] FilterId defaultId() const; + [[nodiscard]] FilterId lookupId(int index) const; bool loadNextExceptions(bool chatsListLoaded); diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 3fabde796e8757..5556f10295a18e 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -465,7 +465,8 @@ bool DocumentData::checkWallPaperProperties() { void DocumentData::updateThumbnails( const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, - const ImageWithLocation &videoThumbnail) { + const ImageWithLocation &videoThumbnail, + bool isPremiumSticker) { if (!inlineThumbnail.bytes.isEmpty() && _inlineThumbnailBytes.isEmpty()) { _inlineThumbnailBytes = inlineThumbnail.bytes; @@ -475,6 +476,11 @@ void DocumentData::updateThumbnails( _flags &= ~Flag::InlineThumbnailIsPath; } } + if (isPremiumSticker) { + _flags |= Flag::PremiumSticker; + } else { + _flags &= ~Flag::PremiumSticker; + } Data::UpdateCloudFile( _thumbnail, thumbnail, @@ -511,6 +517,10 @@ bool DocumentData::isPatternWallPaperSVG() const { return isWallPaper() && hasMimeType(qstr("application/x-tgwallpattern")); } +bool DocumentData::isPremiumSticker() const { + return (_flags & Flag::PremiumSticker); +} + bool DocumentData::hasThumbnail() const { return _thumbnail.location.valid(); } @@ -746,7 +756,7 @@ float64 DocumentData::progress() const { if (uploading()) { if (uploadingData->size > 0) { const auto result = float64(uploadingData->offset) - / uploadingData->size; + / float64(uploadingData->size); return std::clamp(result, 0., 1.); } return 0.; @@ -754,7 +764,7 @@ float64 DocumentData::progress() const { return loading() ? _loader->currentProgress() : 0.; } -int DocumentData::loadOffset() const { +int64 DocumentData::loadOffset() const { return loading() ? _loader->currentOffset() : 0; } @@ -1343,6 +1353,12 @@ LocationType DocumentData::locationType() const { : DocumentFileLocation; } +void DocumentData::forceIsStreamedAnimation() { + type = AnimatedDocument; + _additional = nullptr; + setMaybeSupportsStreaming(true); +} + bool DocumentData::isVoiceMessage() const { return (type == VoiceDocument); } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 9b648527c64c62..c660687da04bc2 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -78,7 +78,6 @@ struct StickerData : public DocumentAdditionalData { struct SongData : public DocumentAdditionalData { int32 duration = 0; QString title, performer; - }; struct VoiceData : public DocumentAdditionalData { @@ -117,7 +116,7 @@ class DocumentData final { void cancel(); [[nodiscard]] bool cancelled() const; [[nodiscard]] float64 progress() const; - [[nodiscard]] int loadOffset() const; + [[nodiscard]] int64 loadOffset() const; [[nodiscard]] bool uploading() const; [[nodiscard]] bool loadedInMediaCache() const; void setLoadedInMediaCache(bool loaded); @@ -151,6 +150,7 @@ class DocumentData final { [[nodiscard]] VoiceData *voice(); [[nodiscard]] const VoiceData *voice() const; + void forceIsStreamedAnimation(); [[nodiscard]] bool isVoiceMessage() const; [[nodiscard]] bool isVideoMessage() const; [[nodiscard]] bool isSong() const; @@ -172,6 +172,7 @@ class DocumentData final { [[nodiscard]] bool isPatternWallPaper() const; [[nodiscard]] bool isPatternWallPaperPNG() const; [[nodiscard]] bool isPatternWallPaperSVG() const; + [[nodiscard]] bool isPremiumSticker() const; [[nodiscard]] bool hasThumbnail() const; [[nodiscard]] bool thumbnailLoading() const; @@ -190,7 +191,8 @@ class DocumentData final { void updateThumbnails( const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, - const ImageWithLocation &videoThumbnail); + const ImageWithLocation &videoThumbnail, + bool isPremiumSticker); [[nodiscard]] QByteArray inlineThumbnailBytes() const { return _inlineThumbnailBytes; @@ -259,11 +261,10 @@ class DocumentData final { [[nodiscard]] bool inappPlaybackFailed() const; DocumentId id = 0; - DocumentType type = FileDocument; + int64 size = 0; QSize dimensions; int32 date = 0; - int32 size = 0; - + DocumentType type = FileDocument; FileStatus status = FileReady; std::unique_ptr uploadingData; @@ -279,6 +280,7 @@ class DocumentData final { HasAttachedStickers = 0x040, InlineThumbnailIsPath = 0x080, ForceToCache = 0x100, + PremiumSticker = 0x200, }; using Flags = base::flags; friend constexpr bool is_flag_type(Flag) { return true; }; diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 058b6b02f9642d..83bf70d3193c41 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -254,6 +254,7 @@ void DocumentMedia::videoThumbnailWanted(Data::FileOrigin origin) { void DocumentMedia::setVideoThumbnail(QByteArray content) { _videoThumbnailBytes = std::move(content); + _videoThumbnailBytes.detach(); } void DocumentMedia::checkStickerLarge() { diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index f2d248e71ba4df..c1cdf47bab1178 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -39,7 +39,7 @@ namespace Data { namespace { constexpr auto kClearLoadingTimeout = 5 * crl::time(1000); -constexpr auto kMaxFileSize = 2000 * 1024 * 1024; +constexpr auto kMaxFileSize = 4000 * int64(1024 * 1024); constexpr auto kMaxResolvePerAttempt = 100; constexpr auto ByItem = [](const auto &entry) { @@ -295,7 +295,7 @@ void DownloadManager::addLoaded( .download = id, .started = started, .path = path, - .size = int32(size), + .size = size, .itemId = item->fullId(), .peerAccessHash = PeerAccessHash(item->history()->peer), .object = std::make_unique(object), @@ -743,6 +743,7 @@ void DownloadManager::generateEntry( InlineImageLocation(), // inlineThumbnail ImageWithLocation(), // thumbnail ImageWithLocation(), // videoThumbnail + false, // isPremiumSticker 0, // dc id.size); document->setLocation(Core::FileLocation(info)); @@ -943,7 +944,7 @@ Fn()> DownloadManager::serializator( const auto constant = sizeof(quint64) // download.objectId + sizeof(qint32) // download.type + sizeof(qint64) // started - + sizeof(qint32) // size + + sizeof(quint32) // size + sizeof(quint64) // itemId.peer + sizeof(qint64) // itemId.msg + sizeof(quint64); // peerAccessHash @@ -962,7 +963,8 @@ Fn()> DownloadManager::serializator( << quint64(id.download.objectId) << qint32(id.download.type) << qint64(id.started) - << qint32(id.size) + // FileSize: Right now any file size fits 32 bit. + << quint32(id.size) << quint64(id.itemId.peer.value) << qint64(id.itemId.msg.bare) << quint64(id.peerAccessHash) @@ -995,7 +997,8 @@ std::vector DownloadManager::deserialize( auto downloadObjectId = quint64(); auto uncheckedDownloadType = qint32(); auto started = qint64(); - auto size = qint32(); + // FileSize: Right now any file size fits 32 bit. + auto size = quint32(); auto itemIdPeer = quint64(); auto itemIdMsg = qint64(); auto peerAccessHash = quint64(); @@ -1025,7 +1028,7 @@ std::vector DownloadManager::deserialize( }, .started = started, .path = path, - .size = size, + .size = int64(size), .itemId = { PeerId(itemIdPeer), MsgId(itemIdMsg) }, .peerAccessHash = peerAccessHash, }); diff --git a/Telegram/SourceFiles/data/data_download_manager.h b/Telegram/SourceFiles/data/data_download_manager.h index e20ac639b4b600..bb031c62e4c136 100644 --- a/Telegram/SourceFiles/data/data_download_manager.h +++ b/Telegram/SourceFiles/data/data_download_manager.h @@ -58,7 +58,7 @@ struct DownloadedId { DownloadId download; DownloadDate started = 0; QString path; - int32 size = 0; + int64 size = 0; FullMsgId itemId; uint64 peerAccessHash = 0; @@ -69,8 +69,8 @@ struct DownloadingId { DownloadObject object; DownloadDate started = 0; QString path; - int ready = 0; - int total = 0; + int64 ready = 0; + int64 total = 0; bool hiddenByView = false; bool done = false; }; diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index c7324ed3cf7dc5..9f0426dfe753ec 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -155,6 +155,11 @@ struct FileReferenceAccumulator { }, [](const MTPDaccount_savedRingtonesNotModified &data) { }); } + void push(const MTPhelp_PremiumPromo &data) { + data.match([&](const MTPDhelp_premiumPromo &data) { + push(data.vvideos()); + }); + } UpdatedFileReferences result; }; @@ -208,6 +213,10 @@ UpdatedFileReferences GetFileReferences( return GetFileReferencesHelper(data); } +UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) { + return GetFileReferencesHelper(data); +} + UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) { return GetFileReferencesHelper(data); } diff --git a/Telegram/SourceFiles/data/data_file_origin.h b/Telegram/SourceFiles/data/data_file_origin.h index 84b382fb1b7cd6..ad258ac192c389 100644 --- a/Telegram/SourceFiles/data/data_file_origin.h +++ b/Telegram/SourceFiles/data/data_file_origin.h @@ -102,6 +102,12 @@ struct FileOriginRingtones { } }; +struct FileOriginPremiumPreviews { + inline bool operator<(const FileOriginPremiumPreviews &) const { + return false; + } +}; + struct FileOrigin { using Variant = std::variant< v::null_t, @@ -112,7 +118,8 @@ struct FileOrigin { FileOriginSavedGifs, FileOriginWallpaper, FileOriginTheme, - FileOriginRingtones>; + FileOriginRingtones, + FileOriginPremiumPreviews>; FileOrigin() = default; FileOrigin(FileOriginMessage data) : data(data) { @@ -131,6 +138,8 @@ struct FileOrigin { } FileOrigin(FileOriginRingtones data) : data(data) { } + FileOrigin(FileOriginPremiumPreviews data) : data(data) { + } explicit operator bool() const { return !v::is_null(data); @@ -178,6 +187,7 @@ UpdatedFileReferences GetFileReferences(const MTPWallPaper &data); UpdatedFileReferences GetFileReferences(const MTPTheme &data); UpdatedFileReferences GetFileReferences( const MTPaccount_SavedRingtones &data); +UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data); // Admin Log Event. UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data); diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 890f51b5ce122e..85d41f5ac23c4a 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -17,6 +17,7 @@ For license and copyright information please follow this link: #include "lang/lang_keys.h" #include "storage/storage_facade.h" #include "core/application.h" +#include "core/core_settings.h" #include "main/main_account.h" #include "main/main_session.h" #include "mtproto/mtproto_config.h" @@ -38,7 +39,7 @@ Folder::Folder(not_null owner, FolderId id) , _chatsList( &owner->session(), FilterId(), - owner->session().serverConfig().pinnedDialogsInFolderMax.value()) + owner->maxPinnedChatsLimitValue(this, FilterId())) , _name(tr::lng_archived_name(tr::now)) , _chatListNameSortKey(owner->nameSortKey(_name)) { indexNameParts(); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index c867df8ac7ab6b..baa4f702eec9f2 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -35,6 +35,7 @@ For license and copyright information please follow this link: #include "ui/toast/toast.h" #include "ui/emoji_config.h" #include "api/api_sending.h" +#include "api/api_transcribes.h" #include "storage/storage_shared_media.h" #include "storage/localstorage.h" #include "chat_helpers/stickers_dice_pack.h" // Stickers::DicePacks::IsSlot. @@ -52,8 +53,11 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "main/main_session_settings.h" #include "core/application.h" +#include "core/click_handler_types.h" // ClickHandlerContext #include "lang/lang_keys.h" #include "storage/file_upload.h" +#include "window/window_session_controller.h" // Window::Show +#include "apiwrap.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" @@ -635,10 +639,12 @@ std::unique_ptr MediaPhoto::createView( MediaFile::MediaFile( not_null parent, - not_null document) + not_null document, + bool skipPremiumEffect) : Media(parent) , _document(document) -, _emoji(document->sticker() ? document->sticker()->alt : QString()) { +, _emoji(document->sticker() ? document->sticker()->alt : QString()) +, _skipPremiumEffect(skipPremiumEffect) { parent->history()->owner().registerDocumentItem(_document, parent); if (!_emoji.isEmpty()) { @@ -658,7 +664,10 @@ MediaFile::~MediaFile() { } std::unique_ptr MediaFile::clone(not_null parent) { - return std::make_unique(parent, _document); + return std::make_unique( + parent, + _document, + !_document->session().premium()); } DocumentData *MediaFile::document() const { @@ -851,9 +860,27 @@ TextForMimeData MediaFile::clipboardText() const { } return tr::lng_in_dlg_file(tr::now) + addName; }(); - return WithCaptionClipboardText( - attachType, - parent()->clipboardText()); + auto caption = parent()->clipboardText(); + + if (_document->isVoiceMessage()) { + const auto &entry = _document->session().api().transcribes().entry( + parent()); + if (!entry.requestId + && entry.shown + && !entry.toolong + && !entry.failed + && (entry.pending || !entry.result.isEmpty())) { + const auto text = "{{\n" + + entry.result + + (entry.result.isEmpty() ? "" : " ") + + (entry.pending ? "[...]" : "") + + "\n}}" + + (caption.rich.text.isEmpty() ? "" : "\n"); + caption = TextForMimeData{ text, { text } }.append(std::move(caption)); + } + } + + return WithCaptionClipboardText(attachType, std::move(caption)); } bool MediaFile::allowsEditCaption() const { @@ -953,6 +980,7 @@ std::unique_ptr MediaFile::createView( std::make_unique( message, _document, + _skipPremiumEffect, replacing)); } else if (_document->isAnimation() || _document->isVideoFile() @@ -1640,6 +1668,7 @@ ClickHandlerPtr MediaDice::makeHandler() const { ClickHandlerPtr MediaDice::MakeHandler( not_null history, const QString &emoji) { + // TODO support multi-windows. static auto ShownToast = base::weak_ptr(); static const auto HideExisting = [] { if (const auto toast = ShownToast.get()) { @@ -1647,7 +1676,7 @@ ClickHandlerPtr MediaDice::MakeHandler( ShownToast = nullptr; } }; - return std::make_shared([=] { + return std::make_shared([=](ClickContext context) { auto config = Ui::Toast::Config{ .text = { tr::lng_about_random(tr::now, lt_emoji, emoji) }, .st = &st::historyDiceToast, @@ -1677,7 +1706,15 @@ ClickHandlerPtr MediaDice::MakeHandler( } HideExisting(); - ShownToast = Ui::Toast::Show(config); + const auto my = context.other.value(); + const auto weak = my.sessionWindow; + if (const auto strong = weak.get()) { + ShownToast = Ui::Toast::Show( + Window::Show(strong).toastParent(), + config); + } else { + ShownToast = Ui::Toast::Show(config); + } }); } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 34110473f2e555..7c4c83b7aae49f 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -185,7 +185,8 @@ class MediaFile final : public Media { public: MediaFile( not_null parent, - not_null document); + not_null document, + bool skipPremiumEffect); ~MediaFile(); std::unique_ptr clone(not_null parent) override; @@ -218,6 +219,7 @@ class MediaFile final : public Media { private: not_null _document; QString _emoji; + bool _skipPremiumEffect = false; }; diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 958b85bcf676fe..a429e8fa8b9e3d 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -19,6 +19,8 @@ For license and copyright information please follow this link: #include "data/data_document.h" #include "data/data_document_media.h" #include "lottie/lottie_icon.h" +#include "storage/localimageloader.h" +#include "ui/image/image_location_factory.h" #include "base/timer_rpl.h" #include "apiwrap.h" #include "styles/style_chat.h" @@ -354,6 +356,7 @@ std::optional Reactions::parse(const MTPAvailableReaction &entry) { *data.varound_animation()).get() : nullptr), .active = !data.is_inactive(), + .premium = data.is_premium(), }) : std::nullopt; }); diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 0a587ea151e0be..f1d553d827ebc2 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -29,6 +29,7 @@ struct Reaction { DocumentData *centerIcon = nullptr; DocumentData *aroundAnimation = nullptr; bool active = false; + bool premium = false; }; class Reactions final { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index ac7ec651846606..01fe66a84ba9ad 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -88,89 +88,18 @@ PeerId FakePeerIdForJustName(const QString &name) { return peerFromUser(kShift + std::abs(base)); } -bool UpdateBotCommands( - std::vector &commands, - const MTPVector &data) { - const auto &v = data.v; - commands.reserve(v.size()); - auto result = false; - auto index = 0; - for (const auto &command : v) { - command.match([&](const MTPDbotCommand &data) { - const auto command = qs(data.vcommand()); - const auto description = qs(data.vdescription()); - if (commands.size() <= index) { - commands.push_back({ - .command = command, - .description = description, - }); - result = true; - } else { - auto &entry = commands[index]; - if (entry.command != command - || entry.description != description) { - entry.command = command; - entry.description = description; - result = true; - } - } - ++index; - }); - } - if (index < commands.size()) { - result = true; - } - commands.resize(index); - return result; -} - -bool UpdateBotCommands( - base::flat_map> &commands, - UserId botId, - const MTPVector &data) { - return data.v.isEmpty() - ? commands.remove(botId) - : UpdateBotCommands(commands[botId], data); -} - -bool UpdateBotCommands( - base::flat_map> &commands, - const MTPVector &data) { - auto result = false; - auto filled = base::flat_set(); - filled.reserve(data.v.size()); - for (const auto &item : data.v) { - item.match([&](const MTPDbotInfo &data) { - const auto id = UserId(data.vuser_id().v); - if (!filled.emplace(id).second) { - LOG(("API Error: Two BotInfo for a single bot.")); - return; - } else if (UpdateBotCommands(commands, id, data.vcommands())) { - result = true; - } - }); - } - for (auto i = begin(commands); i != end(commands);) { - if (filled.contains(i->first)) { - ++i; - } else { - i = commands.erase(i); - result = true; - } - } - return result; -} - bool ApplyBotMenuButton( not_null info, - const MTPBotMenuButton &button) { + const MTPBotMenuButton *button) { auto text = QString(); auto url = QString(); - button.match([&](const MTPDbotMenuButton &data) { - text = qs(data.vtext()); - url = qs(data.vurl()); - }, [&](const auto &) { - }); + if (button) { + button->match([&](const MTPDbotMenuButton &data) { + text = qs(data.vtext()); + url = qs(data.vurl()); + }, [&](const auto &) { + }); + } const auto changed = (info->botMenuButtonText != text) || (info->botMenuButtonUrl != url); @@ -296,8 +225,12 @@ ClickHandlerPtr PeerData::createOpenLink() { return std::make_shared(this); } -void PeerData::setUserpic(PhotoId photoId, const ImageLocation &location) { +void PeerData::setUserpic( + PhotoId photoId, + const ImageLocation &location, + bool hasVideo) { _userpicPhotoId = photoId; + _userpicHasVideo = hasVideo; _userpic.set(&session(), ImageWithLocation{ .location = location }); } @@ -468,7 +401,10 @@ Data::FileOrigin PeerData::userpicPhotoOrigin() const { : Data::FileOrigin(); } -void PeerData::updateUserpic(PhotoId photoId, MTP::DcId dcId) { +void PeerData::updateUserpic( + PhotoId photoId, + MTP::DcId dcId, + bool hasVideo) { setUserpicChecked( photoId, ImageLocation( @@ -480,19 +416,27 @@ void PeerData::updateUserpic(PhotoId photoId, MTP::DcId dcId) { input, MTP_long(photoId))) }, kUserpicSize, - kUserpicSize)); + kUserpicSize), + hasVideo); } void PeerData::clearUserpic() { - setUserpicChecked(PhotoId(), ImageLocation()); + setUserpicChecked(PhotoId(), ImageLocation(), false); } void PeerData::setUserpicChecked( PhotoId photoId, - const ImageLocation &location) { - if (_userpicPhotoId != photoId || _userpic.location() != location) { - setUserpic(photoId, location); + const ImageLocation &location, + bool hasVideo) { + if (_userpicPhotoId != photoId + || _userpic.location() != location + || _userpicHasVideo != hasVideo) { + const auto known = !userpicPhotoUnknown(); + setUserpic(photoId, location, hasVideo); session().changes().peerUpdated(this, UpdateFlag::Photo); + if (known && isPremium() && userpicPhotoUnknown()) { + updateFull(); + } } } @@ -838,6 +782,13 @@ bool PeerData::isVerified() const { return false; } +bool PeerData::isPremium() const { + if (const auto user = asUser()) { + return user->isPremium(); + } + return false; +} + bool PeerData::isScam() const { if (const auto user = asUser()) { return user->isScam(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 02d186834146c8..9725b8ad0c69ae 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -20,11 +20,6 @@ class ChannelData; enum class ChatRestriction; -struct BotCommand { - QString command; - QString description; -}; - namespace Ui { class EmptyUserpic; } // namespace Ui @@ -100,19 +95,9 @@ struct UnavailableReason { } }; -bool UpdateBotCommands( - std::vector &commands, - const MTPVector &data); -bool UpdateBotCommands( - base::flat_map> &commands, - UserId botId, - const MTPVector &data); -bool UpdateBotCommands( - base::flat_map> &commands, - const MTPVector &data); bool ApplyBotMenuButton( not_null info, - const MTPBotMenuButton &button); + const MTPBotMenuButton *button); } // namespace Data @@ -172,6 +157,7 @@ class PeerData { } [[nodiscard]] bool isSelf() const; [[nodiscard]] bool isVerified() const; + [[nodiscard]] bool isPremium() const; [[nodiscard]] bool isScam() const; [[nodiscard]] bool isFake() const; [[nodiscard]] bool isMegagroup() const; @@ -266,7 +252,10 @@ class PeerData { return _nameFirstLetters; } - void setUserpic(PhotoId photoId, const ImageLocation &location); + void setUserpic( + PhotoId photoId, + const ImageLocation &location, + bool hasVideo); void setUserpicPhoto(const MTPPhoto &data); void paintUserpic( Painter &p, @@ -320,6 +309,9 @@ class PeerData { [[nodiscard]] PhotoId userpicPhotoId() const { return userpicPhotoUnknown() ? 0 : _userpicPhotoId; } + [[nodiscard]] bool userpicHasVideo() const { + return _userpicHasVideo; + } [[nodiscard]] Data::FileOrigin userpicOrigin() const; [[nodiscard]] Data::FileOrigin userpicPhotoOrigin() const; @@ -426,7 +418,7 @@ class PeerData { const QString &newName, const QString &newNameOrPhone, const QString &newUsername); - void updateUserpic(PhotoId photoId, MTP::DcId dcId); + void updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo); void clearUserpic(); private: @@ -435,12 +427,17 @@ class PeerData { [[nodiscard]] virtual auto unavailableReasons() const -> const std::vector &; - void setUserpicChecked(PhotoId photoId, const ImageLocation &location); + void setUserpicChecked( + PhotoId photoId, + const ImageLocation &location, + bool hasVideo); const not_null _owner; mutable Data::CloudImage _userpic; PhotoId _userpicPhotoId = kUnknownPhotoId; + bool _userpicHasVideo = false; + mutable std::unique_ptr _userpicEmpty; Ui::Text::String _nameText; diff --git a/Telegram/SourceFiles/data/data_peer_bot_command.cpp b/Telegram/SourceFiles/data/data_peer_bot_command.cpp new file mode 100644 index 00000000000000..9aef25403f276f --- /dev/null +++ b/Telegram/SourceFiles/data/data_peer_bot_command.cpp @@ -0,0 +1,21 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_peer_bot_command.h" + +namespace Data { + +BotCommand BotCommandFromTL(const MTPBotCommand &result) { + return result.match([](const MTPDbotCommand &data) { + return BotCommand { + .command = qs(data.vcommand().v), + .description = qs(data.vdescription().v), + }; + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_bot_command.h b/Telegram/SourceFiles/data/data_peer_bot_command.h new file mode 100644 index 00000000000000..6b87c1fd42b539 --- /dev/null +++ b/Telegram/SourceFiles/data/data_peer_bot_command.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { + +struct BotCommand final { + QString command; + QString description; + + inline bool operator==(const BotCommand &other) const { + return (command == other.command) + && (description == other.description); + } + inline bool operator!=(const BotCommand &other) const { + return !(*this == other); + } +}; + +[[nodiscard]] BotCommand BotCommandFromTL(const MTPBotCommand &result); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_bot_commands.cpp b/Telegram/SourceFiles/data/data_peer_bot_commands.cpp new file mode 100644 index 00000000000000..8adc31a9e31056 --- /dev/null +++ b/Telegram/SourceFiles/data/data_peer_bot_commands.cpp @@ -0,0 +1,48 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_peer_bot_commands.h" + +namespace Data { + +ChatBotCommands::Changed ChatBotCommands::update( + const std::vector &list) { + auto changed = false; + if (list.empty()) { + changed = (list.empty() != empty()); + clear(); + } else { + for (const auto &commands : list) { + auto &value = operator[](commands.userId); + changed |= commands.commands.empty() + ? remove(commands.userId) + : !ranges::equal(value, commands.commands); + value = commands.commands; + } + } + return changed; +} + +BotCommands BotCommandsFromTL(const MTPBotInfo &result) { + return result.match([](const MTPDbotInfo &data) { + const auto userId = data.vuser_id() + ? UserId(*data.vuser_id()) + : UserId(); + if (!data.vcommands()) { + return BotCommands{ .userId = userId }; + } + auto commands = ranges::views::all( + data.vcommands()->v + ) | ranges::views::transform(BotCommandFromTL) | ranges::to_vector; + return BotCommands{ + .userId = userId, + .commands = std::move(commands), + }; + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_bot_commands.h b/Telegram/SourceFiles/data/data_peer_bot_commands.h new file mode 100644 index 00000000000000..e0730b4e43045a --- /dev/null +++ b/Telegram/SourceFiles/data/data_peer_bot_commands.h @@ -0,0 +1,32 @@ +/* +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_peer_bot_command.h" + +namespace Data { + +struct BotCommands final { + UserId userId; + std::vector commands; +}; + +struct ChatBotCommands final : public base::flat_map< + UserId, + std::vector> { +public: + using Changed = bool; + + using base::flat_map>::flat_map; + + Changed update(const std::vector &list); +}; + +[[nodiscard]] BotCommands BotCommandsFromTL(const MTPBotInfo &result); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 8a66837d741fae..b1c3fc81b62fb0 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -205,6 +205,7 @@ rpl::producer CanWriteValue(ChannelData *channel) { using Flag = ChannelDataFlag; const auto mask = 0 | Flag::Left + | Flag::JoinToWrite | Flag::HasLink | Flag::Forbidden | Flag::Creator @@ -227,7 +228,7 @@ rpl::producer CanWriteValue(ChannelData *channel) { bool defaultSendMessagesRestriction) { const auto notAmInFlags = Flag::Left | Flag::Forbidden; const auto allowed = !(flags & notAmInFlags) - || (flags & Flag::HasLink); + || ((flags & Flag::HasLink) && !(flags & Flag::JoinToWrite)); return allowed && (postMessagesRight || (flags & Flag::Creator) || (!(flags & Flag::Broadcast) @@ -322,6 +323,23 @@ rpl::producer CanManageGroupCallValue(not_null peer) { return rpl::single(false); } +rpl::producer PeerPremiumValue(not_null peer) { + const auto user = peer->asUser(); + if (!user) { + return rpl::single(false); + } + return user->flagsValue( + ) | rpl::filter([=](UserData::Flags::Change change) { + return (change.diff & UserDataFlag::Premium); + }) | rpl::map([=] { + return user->isPremium(); + }); +} + +rpl::producer AmPremiumValue(not_null session) { + return PeerPremiumValue(session->user()); +} + TimeId SortByOnlineValue(not_null user, TimeId now) { if (user->isServiceUser() || user->isBot()) { return -1; diff --git a/Telegram/SourceFiles/data/data_peer_values.h b/Telegram/SourceFiles/data/data_peer_values.h index 5e35e0aa850816..6245b382ef7686 100644 --- a/Telegram/SourceFiles/data/data_peer_values.h +++ b/Telegram/SourceFiles/data/data_peer_values.h @@ -104,12 +104,19 @@ inline auto PeerFullFlagValue( [[nodiscard]] rpl::producer CanWriteValue(ChatData *chat); [[nodiscard]] rpl::producer CanWriteValue(ChannelData *channel); [[nodiscard]] rpl::producer CanWriteValue(not_null peer); -[[nodiscard]] rpl::producer CanPinMessagesValue(not_null peer); -[[nodiscard]] rpl::producer CanManageGroupCallValue(not_null peer); +[[nodiscard]] rpl::producer CanPinMessagesValue( + not_null peer); +[[nodiscard]] rpl::producer CanManageGroupCallValue( + not_null peer); +[[nodiscard]] rpl::producer PeerPremiumValue(not_null peer); +[[nodiscard]] rpl::producer AmPremiumValue( + not_null session); [[nodiscard]] TimeId SortByOnlineValue(not_null user, TimeId now); [[nodiscard]] crl::time OnlineChangeTimeout(TimeId online, TimeId now); -[[nodiscard]] crl::time OnlineChangeTimeout(not_null user, TimeId now); +[[nodiscard]] crl::time OnlineChangeTimeout( + not_null user, + TimeId now); [[nodiscard]] QString OnlineText(TimeId online, TimeId now); [[nodiscard]] QString OnlineText(not_null user, TimeId now); [[nodiscard]] QString OnlineTextFull(not_null user, TimeId now); diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index 7eec4041a6d26d..6690246cbb6d9f 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -51,7 +51,7 @@ PhotoData::~PhotoData() { for (auto &image : _images) { base::take(image.loader).reset(); } - base::take(_video.loader).reset(); + base::take(_videoSizes); } Data::Session &PhotoData::owner() const { @@ -369,7 +369,8 @@ void PhotoData::updateImages( const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime) { if (!inlineThumbnailBytes.isEmpty() && _inlineThumbnailBytes.isEmpty()) { @@ -399,15 +400,28 @@ void PhotoData::updateImages( update(PhotoSize::Thumbnail, thumbnail); update(PhotoSize::Large, large); - if (video.location.valid()) { - _videoStartTime = videoStartTime; + if (!videoLarge.location.valid()) { + _videoSizes = nullptr; + } else { + if (!_videoSizes) { + _videoSizes = std::make_unique(); + } + _videoSizes->startTime = videoStartTime; + constexpr auto large = PhotoSize::Large; + constexpr auto small = PhotoSize::Small; + Data::UpdateCloudFile( + _videoSizes->large, + videoLarge, + owner().cache(), + Data::kAnimationCacheTag, + [&](Data::FileOrigin origin) { loadVideo(large, origin); }); + Data::UpdateCloudFile( + _videoSizes->small, + videoSmall, + owner().cache(), + Data::kAnimationCacheTag, + [&](Data::FileOrigin origin) { loadVideo(small, origin); }); } - Data::UpdateCloudFile( - _video, - video, - owner().cache(), - Data::kAnimationCacheTag, - [&](Data::FileOrigin origin) { loadVideo(origin); }); } [[nodiscard]] bool PhotoData::hasAttachedStickers() const { @@ -426,34 +440,59 @@ int PhotoData::height() const { return _images[PhotoSizeIndex(PhotoSize::Large)].location.height(); } +Data::CloudFile &PhotoData::videoFile(PhotoSize size) { + Expects(_videoSizes != nullptr); + + return (size == PhotoSize::Small && hasVideoSmall()) + ? _videoSizes->small + : _videoSizes->large; +} + +const Data::CloudFile &PhotoData::videoFile(PhotoSize size) const { + Expects(_videoSizes != nullptr); + + return (size == PhotoSize::Small && hasVideoSmall()) + ? _videoSizes->small + : _videoSizes->large; +} + + bool PhotoData::hasVideo() const { - return _video.location.valid(); + return _videoSizes != nullptr; +} + +bool PhotoData::hasVideoSmall() const { + return hasVideo() && _videoSizes->small.location.valid(); } -bool PhotoData::videoLoading() const { - return _video.loader != nullptr; +bool PhotoData::videoLoading(Data::PhotoSize size) const { + return _videoSizes && videoFile(size).loader != nullptr; } -bool PhotoData::videoFailed() const { - return (_video.flags & Data::CloudFile::Flag::Failed); +bool PhotoData::videoFailed(Data::PhotoSize size) const { + return _videoSizes + && (videoFile(size).flags & Data::CloudFile::Flag::Failed); } -void PhotoData::loadVideo(Data::FileOrigin origin) { +void PhotoData::loadVideo(Data::PhotoSize size, Data::FileOrigin origin) { + if (!_videoSizes) { + return; + } const auto autoLoading = false; const auto finalCheck = [=] { if (const auto active = activeMediaView()) { - return active->videoContent().isEmpty(); + return active->videoContent(size).isEmpty(); } return true; }; const auto done = [=](QByteArray result) { if (const auto active = activeMediaView()) { - active->setVideo(std::move(result)); + active->setVideo(size, std::move(result)); } }; Data::LoadCloudFile( &session(), - _video, + videoFile(size), origin, LoadFromCloudOrLocal, autoLoading, @@ -462,12 +501,27 @@ void PhotoData::loadVideo(Data::FileOrigin origin) { done); } -const ImageLocation &PhotoData::videoLocation() const { - return _video.location; +const ImageLocation &PhotoData::videoLocation(Data::PhotoSize size) const { + static const auto empty = ImageLocation(); + return _videoSizes ? videoFile(size).location : empty; +} + +int PhotoData::videoByteSize(Data::PhotoSize size) const { + return _videoSizes ? videoFile(size).byteSize : 0; +} + +crl::time PhotoData::videoStartPosition() const { + return _videoSizes ? _videoSizes->startTime : crl::time(0); +} + +void PhotoData::setVideoPlaybackFailed() { + if (_videoSizes) { + _videoSizes->playbackFailed = true; + } } -int PhotoData::videoByteSize() const { - return _video.byteSize; +bool PhotoData::videoPlaybackFailed() const { + return _videoSizes && _videoSizes->playbackFailed; } bool PhotoData::videoCanBePlayed() const { @@ -481,17 +535,19 @@ auto PhotoData::createStreamingLoader( if (!hasVideo()) { return nullptr; } + constexpr auto large = PhotoSize::Large; if (!forceRemoteLoader) { const auto media = activeMediaView(); - if (media && !media->videoContent().isEmpty()) { - return Media::Streaming::MakeBytesLoader(media->videoContent()); + const auto bytes = media ? media->videoContent(large) : QByteArray(); + if (media && !bytes.isEmpty()) { + return Media::Streaming::MakeBytesLoader(bytes); } } - return v::is(videoLocation().file().data) + return v::is(videoLocation(large).file().data) ? std::make_unique( &session().downloader(), - v::get(videoLocation().file().data), - videoByteSize(), + v::get(videoLocation(large).file().data), + videoByteSize(large), origin) : nullptr; } diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index 4e6127965619cb..aeec93009a37d0 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -93,7 +93,8 @@ class PhotoData final { const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime); [[nodiscard]] int validSizeIndex(Data::PhotoSize size) const; [[nodiscard]] int existingSizeIndex(Data::PhotoSize size) const; @@ -126,20 +127,16 @@ class PhotoData final { [[nodiscard]] int imageByteSize(Data::PhotoSize size) const; [[nodiscard]] bool hasVideo() const; - [[nodiscard]] bool videoLoading() const; - [[nodiscard]] bool videoFailed() const; - void loadVideo(Data::FileOrigin origin); - [[nodiscard]] const ImageLocation &videoLocation() const; - [[nodiscard]] int videoByteSize() const; - [[nodiscard]] crl::time videoStartPosition() const { - return _videoStartTime; - } - void setVideoPlaybackFailed() { - _videoPlaybackFailed = true; - } - [[nodiscard]] bool videoPlaybackFailed() const { - return _videoPlaybackFailed; - } + [[nodiscard]] bool hasVideoSmall() const; + [[nodiscard]] bool videoLoading(Data::PhotoSize size) const; + [[nodiscard]] bool videoFailed(Data::PhotoSize size) const; + void loadVideo(Data::PhotoSize size, Data::FileOrigin origin); + [[nodiscard]] const ImageLocation &videoLocation( + Data::PhotoSize size) const; + [[nodiscard]] int videoByteSize(Data::PhotoSize size) const; + [[nodiscard]] crl::time videoStartPosition() const; + void setVideoPlaybackFailed(); + [[nodiscard]] bool videoPlaybackFailed() const; [[nodiscard]] bool videoCanBePlayed() const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, @@ -162,11 +159,19 @@ class PhotoData final { std::unique_ptr uploadingData; private: + [[nodiscard]] Data::CloudFile &videoFile(Data::PhotoSize size); + [[nodiscard]] const Data::CloudFile &videoFile( + Data::PhotoSize size) const; + + struct VideoSizes { + Data::CloudFile small; + Data::CloudFile large; + crl::time startTime = 0; + bool playbackFailed = false; + }; QByteArray _inlineThumbnailBytes; std::array _images; - Data::CloudFile _video; - crl::time _videoStartTime = 0; - bool _videoPlaybackFailed = false; + std::unique_ptr _videoSizes; int32 _dc = 0; uint64 _access = 0; diff --git a/Telegram/SourceFiles/data/data_photo_media.cpp b/Telegram/SourceFiles/data/data_photo_media.cpp index 532ccf8e46e078..312b672f5215a2 100644 --- a/Telegram/SourceFiles/data/data_photo_media.cpp +++ b/Telegram/SourceFiles/data/data_photo_media.cpp @@ -117,23 +117,25 @@ void PhotoMedia::set( _owner->session().notifyDownloaderTaskFinished(); } -QByteArray PhotoMedia::videoContent() const { - return _videoBytes; +QByteArray PhotoMedia::videoContent(PhotoSize size) const { + const auto small = (size == PhotoSize::Small) && _owner->hasVideoSmall(); + return small ? _videoBytesSmall : _videoBytesLarge; } -QSize PhotoMedia::videoSize() const { - const auto &location = _owner->videoLocation(); +QSize PhotoMedia::videoSize(PhotoSize size) const { + const auto &location = _owner->videoLocation(size); return { location.width(), location.height() }; } -void PhotoMedia::videoWanted(Data::FileOrigin origin) { - if (_videoBytes.isEmpty()) { - _owner->loadVideo(origin); +void PhotoMedia::videoWanted(PhotoSize size, Data::FileOrigin origin) { + if (videoContent(size).isEmpty()) { + _owner->loadVideo(size, origin); } } -void PhotoMedia::setVideo(QByteArray content) { - _videoBytes = std::move(content); +void PhotoMedia::setVideo(PhotoSize size, QByteArray content) { + const auto small = (size == PhotoSize::Small) && _owner->hasVideoSmall(); + (small ? _videoBytesSmall : _videoBytesLarge) = std::move(content); } bool PhotoMedia::loaded() const { @@ -191,16 +193,17 @@ void PhotoMedia::collectLocalData(not_null local) { } bool PhotoMedia::saveToFile(const QString &path) { - if (const auto video = videoContent(); !video.isEmpty()) { + constexpr auto large = PhotoSize::Large; + if (const auto video = videoContent(large); !video.isEmpty()) { QFile f(path); return f.open(QIODevice::WriteOnly) && (f.write(video) == video.size()); - } else if (const auto photo = imageBytes(Data::PhotoSize::Large) + } else if (const auto photo = imageBytes(large) ; !photo.isEmpty()) { QFile f(path); return f.open(QIODevice::WriteOnly) && (f.write(photo) == photo.size()); - } else if (const auto fallback = image(Data::PhotoSize::Large)->original() + } else if (const auto fallback = image(large)->original() ; !fallback.isNull()) { return fallback.save(path, "JPG"); } diff --git a/Telegram/SourceFiles/data/data_photo_media.h b/Telegram/SourceFiles/data/data_photo_media.h index 99d4d428768adb..4e97aa9118417f 100644 --- a/Telegram/SourceFiles/data/data_photo_media.h +++ b/Telegram/SourceFiles/data/data_photo_media.h @@ -33,10 +33,10 @@ class PhotoMedia final { QImage image, QByteArray bytes); - [[nodiscard]] QByteArray videoContent() const; - [[nodiscard]] QSize videoSize() const; - void videoWanted(Data::FileOrigin origin); - void setVideo(QByteArray content); + [[nodiscard]] QByteArray videoContent(PhotoSize size) const; + [[nodiscard]] QSize videoSize(PhotoSize size) const; + void videoWanted(PhotoSize size, Data::FileOrigin origin); + void setVideo(PhotoSize size, QByteArray content); [[nodiscard]] bool loaded() const; [[nodiscard]] float64 progress() const; @@ -64,7 +64,8 @@ class PhotoMedia final { const not_null _owner; mutable std::unique_ptr _inlineThumbnail; std::array _images; - QByteArray _videoBytes; + QByteArray _videoBytesSmall; + QByteArray _videoBytesLarge; }; diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp new file mode 100644 index 00000000000000..8dd43625068cae --- /dev/null +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -0,0 +1,163 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_premium_limits.h" + +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" + +namespace Data { + +PremiumLimits::PremiumLimits(not_null session) +: _session(session) { +} + +int PremiumLimits::channelsDefault() const { + return appConfigLimit("channels_limit_default", 500.); +} +int PremiumLimits::channelsPremium() const { + return appConfigLimit("channels_limit_premium", 1000.); +} +int PremiumLimits::channelsCurrent() const { + return isPremium() + ? channelsPremium() + : channelsDefault(); +} + +int PremiumLimits::gifsDefault() const { + return appConfigLimit("saved_gifs_limit_default", 200.); +} +int PremiumLimits::gifsPremium() const { + return appConfigLimit("saved_gifs_limit_premium", 400.); +} +int PremiumLimits::gifsCurrent() const { + return isPremium() + ? gifsPremium() + : gifsDefault(); +} + +int PremiumLimits::stickersFavedDefault() const { + return appConfigLimit("stickers_faved_limit_default", 5.); +} +int PremiumLimits::stickersFavedPremium() const { + return appConfigLimit("stickers_faved_limit_premium", 10.); +} +int PremiumLimits::stickersFavedCurrent() const { + return isPremium() + ? stickersFavedPremium() + : stickersFavedDefault(); +} + +int PremiumLimits::dialogFiltersDefault() const { + return appConfigLimit("dialog_filters_limit_default", 10.); +} +int PremiumLimits::dialogFiltersPremium() const { + return appConfigLimit("dialog_filters_limit_premium", 20.); +} +int PremiumLimits::dialogFiltersCurrent() const { + return isPremium() + ? dialogFiltersPremium() + : dialogFiltersDefault(); +} + +int PremiumLimits::dialogFiltersChatsDefault() const { + return appConfigLimit("dialog_filters_chats_limit_default", 100.); +} +int PremiumLimits::dialogFiltersChatsPremium() const { + return appConfigLimit("dialog_filters_chats_limit_premium", 200.); +} +int PremiumLimits::dialogFiltersChatsCurrent() const { + return isPremium() + ? dialogFiltersChatsPremium() + : dialogFiltersChatsDefault(); +} + +int PremiumLimits::dialogsPinnedDefault() const { + return appConfigLimit("dialogs_pinned_limit_default", 5.); +} +int PremiumLimits::dialogsPinnedPremium() const { + return appConfigLimit("dialogs_pinned_limit_premium", 10.); +} +int PremiumLimits::dialogsPinnedCurrent() const { + return isPremium() + ? dialogsPinnedPremium() + : dialogsPinnedDefault(); +} + +int PremiumLimits::dialogsFolderPinnedDefault() const { + return appConfigLimit("dialogs_folder_pinned_limit_default", 100.); +} +int PremiumLimits::dialogsFolderPinnedPremium() const { + return appConfigLimit("dialogs_folder_pinned_limit_premium", 200.); +} +int PremiumLimits::dialogsFolderPinnedCurrent() const { + return isPremium() + ? dialogsFolderPinnedPremium() + : dialogsFolderPinnedDefault(); +} + +int PremiumLimits::channelsPublicDefault() const { + return appConfigLimit("channels_public_limit_default", 10.); +} +int PremiumLimits::channelsPublicPremium() const { + return appConfigLimit("channels_public_limit_premium", 20.); +} +int PremiumLimits::channelsPublicCurrent() const { + return isPremium() + ? channelsPublicPremium() + : channelsPublicDefault(); +} + +int PremiumLimits::captionLengthDefault() const { + return appConfigLimit("caption_length_limit_default", 1024.); +} +int PremiumLimits::captionLengthPremium() const { + return appConfigLimit("caption_length_limit_premium", 2048.); +} +int PremiumLimits::captionLengthCurrent() const { + return isPremium() + ? captionLengthPremium() + : captionLengthDefault(); +} + +int PremiumLimits::uploadMaxDefault() const { + return appConfigLimit("upload_max_fileparts_default", 4000.); +} +int PremiumLimits::uploadMaxPremium() const { + return appConfigLimit("upload_max_fileparts_premium", 8000.); +} +int PremiumLimits::uploadMaxCurrent() const { + return isPremium() + ? uploadMaxPremium() + : uploadMaxDefault(); +} + +int PremiumLimits::aboutLengthDefault() const { + return appConfigLimit("about_length_limit_default", 70.); +} +int PremiumLimits::aboutLengthPremium() const { + return appConfigLimit("about_length_limit_premium", 140.); +} +int PremiumLimits::aboutLengthCurrent() const { + return isPremium() + ? aboutLengthPremium() + : aboutLengthDefault(); +} + +int PremiumLimits::appConfigLimit( + const QString &key, + float64 fallback) const { + return int(base::SafeRound( + _session->account().appConfig().get(key, fallback))); +} + +bool PremiumLimits::isPremium() const { + return _session->premium(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h new file mode 100644 index 00000000000000..331515f807acf4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -0,0 +1,74 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class PremiumLimits final { +public: + PremiumLimits(not_null session); + + [[nodiscard]] int channelsDefault() const; + [[nodiscard]] int channelsPremium() const; + [[nodiscard]] int channelsCurrent() const; + + [[nodiscard]] int gifsDefault() const; + [[nodiscard]] int gifsPremium() const; + [[nodiscard]] int gifsCurrent() const; + + [[nodiscard]] int stickersFavedDefault() const; + [[nodiscard]] int stickersFavedPremium() const; + [[nodiscard]] int stickersFavedCurrent() const; + + [[nodiscard]] int dialogFiltersDefault() const; + [[nodiscard]] int dialogFiltersPremium() const; + [[nodiscard]] int dialogFiltersCurrent() const; + + [[nodiscard]] int dialogFiltersChatsDefault() const; + [[nodiscard]] int dialogFiltersChatsPremium() const; + [[nodiscard]] int dialogFiltersChatsCurrent() const; + + [[nodiscard]] int dialogsPinnedDefault() const; + [[nodiscard]] int dialogsPinnedPremium() const; + [[nodiscard]] int dialogsPinnedCurrent() const; + + [[nodiscard]] int dialogsFolderPinnedDefault() const; + [[nodiscard]] int dialogsFolderPinnedPremium() const; + [[nodiscard]] int dialogsFolderPinnedCurrent() const; + + [[nodiscard]] int channelsPublicDefault() const; + [[nodiscard]] int channelsPublicPremium() const; + [[nodiscard]] int channelsPublicCurrent() const; + + [[nodiscard]] int captionLengthDefault() const; + [[nodiscard]] int captionLengthPremium() const; + [[nodiscard]] int captionLengthCurrent() const; + + [[nodiscard]] int uploadMaxDefault() const; + [[nodiscard]] int uploadMaxPremium() const; + [[nodiscard]] int uploadMaxCurrent() const; + + [[nodiscard]] int aboutLengthDefault() const; + [[nodiscard]] int aboutLengthPremium() const; + [[nodiscard]] int aboutLengthCurrent() const; + +private: + [[nodiscard]] int appConfigLimit( + const QString &key, + float64 fallback) const; + [[nodiscard]] bool isPremium() const; + + const not_null _session; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index 0448042bb5a589..48cfa926cae14a 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -93,6 +93,11 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); } // namespace +bool IsScheduledMsgId(MsgId id) { + return (id > ServerMaxMsgId) + && (id < ServerMaxMsgId + ScheduledMsgIdsRange); +} + ScheduledMessages::ScheduledMessages(not_null owner) : _session(&owner->session()) , _clearTimer([=] { clearOldRequests(); }) { diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.h b/Telegram/SourceFiles/data/data_scheduled_messages.h index b1a339770352bc..7d546ad1bc2030 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.h +++ b/Telegram/SourceFiles/data/data_scheduled_messages.h @@ -21,6 +21,8 @@ namespace Data { class Session; struct MessagesSlice; +[[nodiscard]] bool IsScheduledMsgId(MsgId id); + class ScheduledMessages final { public: explicit ScheduledMessages(not_null owner); @@ -32,6 +34,7 @@ class ScheduledMessages final { [[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const; [[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const; [[nodiscard]] int count(not_null history) const; + [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; void checkEntitiesAndUpdate(const MTPDmessage &data); void apply(const MTPDupdateNewScheduledMessage &update); @@ -81,8 +84,6 @@ class ScheduledMessages final { [[nodiscard]] uint64 countListHash(const List &list) const; void clearOldRequests(); - [[nodiscard]] MsgId localMessageId(MsgId remoteId) const; - const not_null _session; base::Timer _clearTimer; diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 76c2182f020297..6dc598e497f77e 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -20,11 +20,12 @@ namespace Api { namespace { constexpr auto kSharedMediaLimit = 100; +constexpr auto kFirstSharedMediaLimit = 0; constexpr auto kDefaultSearchTimeoutMs = crl::time(200); } // namespace -std::optional PrepareSearchRequest( +std::optional PrepareSearchRequest( not_null peer, Storage::SharedMediaType type, const QString &query, @@ -66,7 +67,7 @@ std::optional PrepareSearchRequest( const auto minId = 0; const auto maxId = 0; - const auto limit = messageId ? kSharedMediaLimit : 0; + const auto limit = messageId ? kSharedMediaLimit : kFirstSharedMediaLimit; const auto offsetId = [&] { switch (direction) { case Data::LoadDirection::Before: @@ -111,7 +112,7 @@ SearchResult ParseSearchResult( Storage::SharedMediaType type, MsgId messageId, Data::LoadDirection direction, - const MTPmessages_Messages &data) { + const SearchRequestResult &data) { auto result = SearchResult(); result.noSkipRange = MsgRange{ messageId, messageId }; @@ -382,7 +383,7 @@ void SearchController::requestMore( auto requestId = histories.sendRequest(history, type, [=](Fn finish) { return _session->api().request( std::move(*prepared) - ).done([=](const MTPmessages_Messages &result) { + ).done([=](const SearchRequestResult &result) { listData->requests.remove(key); auto parsed = ParseSearchResult( listData->peer, diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index ba98c1af42b94f..27f0904d29b980 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -7,7 +7,6 @@ For license and copyright information please follow this link: */ #pragma once -#include "mtproto/sender.h" #include "data/data_sparse_ids.h" #include "storage/storage_sparse_ids_list.h" #include "storage/storage_shared_media.h" @@ -30,7 +29,10 @@ struct SearchResult { int fullCount = 0; }; -std::optional PrepareSearchRequest( +using SearchRequest = MTPmessages_Search; +using SearchRequestResult = MTPmessages_Messages; + +std::optional PrepareSearchRequest( not_null peer, Storage::SharedMediaType type, const QString &query, @@ -42,7 +44,7 @@ SearchResult ParseSearchResult( Storage::SharedMediaType type, MsgId messageId, Data::LoadDirection direction, - const MTPmessages_Messages &data); + const SearchRequestResult &data); class SearchController final { public: diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 7d7bb82cadc270..fec3b1b932195a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -10,10 +10,12 @@ For license and copyright information please follow this link: #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" +#include "main/main_app_config.h" #include "apiwrap.h" #include "mainwidget.h" #include "api/api_text_entities.h" #include "core/application.h" +#include "core/core_settings.h" #include "core/mime_type.h" // Core::IsMimeSticker #include "core/crash_reports.h" // CrashReports::SetAnnotation #include "ui/image/image.h" @@ -61,6 +63,8 @@ For license and copyright information please follow this link: #include "data/data_streaming.h" #include "data/data_media_rotation.h" #include "data/data_histories.h" +#include "data/data_peer_values.h" +#include "data/data_premium_limits.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" #include "base/call_delayed.h" @@ -225,7 +229,7 @@ Session::Session(not_null session) , _chatsList( session, FilterId(), - session->serverConfig().pinnedDialogsCountMax.value()) + maxPinnedChatsLimitValue(nullptr, FilterId())) , _contactsList(Dialogs::SortMode::Name) , _contactsNoChatsList(Dialogs::SortMode::Name) , _ttlCheckTimer([=] { checkTTLs(); }) @@ -266,12 +270,26 @@ Session::Session(not_null session) _chatsFilters->changed( ) | rpl::start_with_next([=] { - const auto enabled = !_chatsFilters->list().empty(); + const auto enabled = _chatsFilters->has(); if (enabled != session->settings().dialogsFiltersEnabled()) { session->settings().setDialogsFiltersEnabled(enabled); session->saveSettingsDelayed(); } }, _lifetime); + + crl::on_main(_session, [=] { + AmPremiumValue( + _session + ) | rpl::start_with_next([=] { + for (const auto &[document, items] : _documentItems) { + if (document->isVoiceMessage()) { + for (const auto &item : items) { + requestItemResize(item); + } + } + } + }, _lifetime); + }); } void Session::clear() { @@ -414,6 +432,7 @@ not_null Session::processUser(const MTPUser &data) { | Flag::Scam | Flag::Fake | Flag::BotInlineGeo + | Flag::Premium | Flag::Support | (!minimal ? Flag::Contact @@ -425,6 +444,7 @@ not_null Session::processUser(const MTPUser &data) { | (data.is_scam() ? Flag::Scam : Flag()) | (data.is_fake() ? Flag::Fake : Flag()) | (data.is_bot_inline_geo() ? Flag::BotInlineGeo : Flag()) + | (data.is_premium() ? Flag::Premium : Flag()) | (data.is_support() ? Flag::Support : Flag()) | (!minimal ? (data.is_contact() ? Flag::Contact : Flag()) @@ -741,7 +761,9 @@ not_null Session::processChat(const MTPChat &data) { | Flag::CallNotEmpty | Flag::Forbidden | (!minimal ? (Flag::Left | Flag::Creator) : Flag()) - | Flag::NoForwards; + | Flag::NoForwards + | Flag::JoinToWrite + | Flag::RequestToJoin; const auto flagsSet = (data.is_broadcast() ? Flag::Broadcast : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) | (data.is_scam() ? Flag::Scam : Flag()) @@ -762,7 +784,9 @@ not_null Session::processChat(const MTPChat &data) { ? (data.is_left() ? Flag::Left : Flag()) | (data.is_creator() ? Flag::Creator : Flag()) : Flag()) - | (data.is_noforwards() ? Flag::NoForwards : Flag()); + | (data.is_noforwards() ? Flag::NoForwards : Flag()) + | (data.is_join_to_send() ? Flag::JoinToWrite : Flag()) + | (data.is_join_request() ? Flag::RequestToJoin : Flag()); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setName( @@ -1167,7 +1191,6 @@ void Session::setupPeerNameViewer() { const auto &oldLetters = update.oldFirstLetters; _contactsNoChatsList.peerNameChanged(peer, oldLetters); _contactsList.peerNameChanged(peer, oldLetters); - }, _lifetime); } @@ -1826,20 +1849,44 @@ int Session::pinnedCanPin( FilterId filterId, not_null history) const { if (!filterId) { - const auto limit = pinnedChatsLimit(folder); + const auto limit = pinnedChatsLimit(folder, filterId); return pinnedChatsOrder(folder, FilterId()).size() < limit; } const auto &list = chatsFilters().list(); const auto i = ranges::find(list, filterId, &Data::ChatFilter::id); return (i == end(list)) || (i->always().contains(history)) - || (i->always().size() < Data::ChatFilter::kPinnedLimit); + || (i->always().size() < pinnedChatsLimit(folder, filterId)); +} + +int Session::pinnedChatsLimit( + Data::Folder *folder, + FilterId filterId) const { + const auto limits = Data::PremiumLimits(_session); + return filterId + ? limits.dialogFiltersChatsCurrent() + : folder + ? limits.dialogsFolderPinnedCurrent() + : limits.dialogsPinnedCurrent(); } -int Session::pinnedChatsLimit(Data::Folder *folder) const { - return folder - ? session().serverConfig().pinnedDialogsInFolderMax.current() - : session().serverConfig().pinnedDialogsCountMax.current(); +rpl::producer Session::maxPinnedChatsLimitValue( + Data::Folder *folder, + FilterId filterId) const { + // Premium limit from appconfig. + // We always use premium limit in the MainList limit producer, + // 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 rpl::single(rpl::empty_value()) | rpl::then( + _session->account().appConfig().refreshed() + ) | rpl::map([=] { + const auto limits = Data::PremiumLimits(_session); + return filterId + ? limits.dialogFiltersChatsPremium() + : folder + ? limits.dialogsFolderPinnedPremium() + : limits.dialogsPinnedPremium(); + }); } const std::vector &Session::pinnedChatsOrder( @@ -2422,6 +2469,7 @@ not_null Session::processPhoto( thumbnail, large, ImageWithLocation{}, + ImageWithLocation{}, crl::time(0)); }, [&](const MTPDphotoEmpty &data) { return photo(data.vid().v); @@ -2439,7 +2487,8 @@ not_null Session::photo( const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime) { const auto result = photo(id); photoApplyFields( @@ -2453,7 +2502,8 @@ not_null Session::photo( small, thumbnail, large, - video, + videoSmall, + videoLarge, videoStartTime); return result; } @@ -2504,6 +2554,7 @@ PhotoData *Session::photoFromWeb( ImageWithLocation{ .location = thumbnailLocation }, ImageWithLocation{ .location = large }, ImageWithLocation{}, + ImageWithLocation{}, crl::time(0)); } @@ -2556,9 +2607,10 @@ void Session::photoApplyFields( ? ImageWithLocation() : Images::FromPhotoSize(_session, data, *i); }; - const auto findVideoSize = [&]() -> std::optional { + const auto findVideoSize = [&](PhotoSize size) + -> std::optional { const auto sizes = data.vvideo_sizes(); - if (!sizes || sizes->v.isEmpty()) { + if (!sizes) { return std::nullopt; } const auto area = [](const MTPVideoSize &size) { @@ -2566,18 +2618,28 @@ void Session::photoApplyFields( return data.vsize().v ? (data.vw().v * data.vh().v) : 0; }); }; - const auto result = *ranges::max_element( - sizes->v, - std::greater<>(), - area); - return (area(result) > 0) ? std::make_optional(result) : std::nullopt; + const auto type = [](const MTPVideoSize &size) { + return size.match([](const MTPDvideoSize &data) { + return data.vtype().v.isEmpty() + ? char(0) + : data.vtype().v.front(); + }); + }; + const auto result = (size == PhotoSize::Small) + ? ranges::find(sizes->v, 'p', type) + : ranges::max_element(sizes->v, std::less<>(), area); + if (result == sizes->v.end() || area(*result) <= 0) { + return std::nullopt; + } + return std::make_optional(*result); }; const auto useProgressive = (progressive != sizes.end()); const auto large = useProgressive ? Images::FromPhotoSize(_session, data, *progressive) : image(LargeLevels); if (large.location.valid()) { - const auto video = findVideoSize(); + const auto videoSmall = findVideoSize(PhotoSize::Small); + const auto videoLarge = findVideoSize(PhotoSize::Large); photoApplyFields( photo, data.vaccess_hash().v, @@ -2593,12 +2655,15 @@ void Session::photoApplyFields( ? Images::FromProgressiveSize(_session, *progressive, 1) : image(ThumbnailLevels)), large, - (video - ? Images::FromVideoSize(_session, data, *video) + (videoSmall + ? Images::FromVideoSize(_session, data, *videoSmall) : ImageWithLocation()), - (video - ? VideoStartTime( - *video->match([](const auto &data) { return &data; })) + (videoLarge + ? Images::FromVideoSize(_session, data, *videoLarge) + : ImageWithLocation()), + (videoLarge + ? VideoStartTime(*videoLarge->match( + [](const auto &data) { return &data; })) : 0)); } } @@ -2614,7 +2679,8 @@ void Session::photoApplyFields( const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime) { if (!date) { return; @@ -2627,7 +2693,8 @@ void Session::photoApplyFields( small, thumbnail, large, - video, + videoSmall, + videoLarge, videoStartTime); } @@ -2668,7 +2735,8 @@ not_null Session::processDocument( qs(data.vmime_type()), InlineImageLocation(), thumbnail, - ImageWithLocation(), + ImageWithLocation(), // videoThumbnail + false, // isPremiumSticker data.vdc_id().v, data.vsize().v); }, [&](const MTPDdocumentEmpty &data) { @@ -2686,8 +2754,9 @@ not_null Session::document( const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, const ImageWithLocation &videoThumbnail, + bool isPremiumSticker, int32 dc, - int32 size) { + int64 size) { const auto result = document(id); documentApplyFields( result, @@ -2699,6 +2768,7 @@ not_null Session::document( inlineThumbnail, thumbnail, videoThumbnail, + isPremiumSticker, dc, size); return result; @@ -2767,8 +2837,9 @@ DocumentData *Session::documentFromWeb( InlineImageLocation(), ImageWithLocation{ .location = thumbnailLocation }, ImageWithLocation{ .location = videoThumbnailLocation }, + false, // isPremiumSticker session().mainDcId(), - int32(0)); // data.vsize().v + int64(0)); // data.vsize().v result->setWebLocation(WebFileLocation( data.vurl().v, data.vaccess_hash().v)); @@ -2789,8 +2860,9 @@ DocumentData *Session::documentFromWeb( InlineImageLocation(), ImageWithLocation{ .location = thumbnailLocation }, ImageWithLocation{ .location = videoThumbnailLocation }, + false, // isPremiumSticker session().mainDcId(), - int32(0)); // data.vsize().v + int64(0)); // data.vsize().v result->setContentUrl(qs(data.vurl())); return result; } @@ -2816,6 +2888,8 @@ void Session::documentApplyFields( const auto videoThumbnail = videoThumbnailSize ? Images::FromVideoSize(_session, data, *videoThumbnailSize) : ImageWithLocation(); + const auto isPremiumSticker = videoThumbnailSize + && (videoThumbnailSize->c_videoSize().vtype().v == "f"); documentApplyFields( document, data.vaccess_hash().v, @@ -2826,6 +2900,7 @@ void Session::documentApplyFields( inlineThumbnail, prepared, videoThumbnail, + isPremiumSticker, data.vdc_id().v, data.vsize().v); } @@ -2840,8 +2915,9 @@ void Session::documentApplyFields( const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, const ImageWithLocation &videoThumbnail, + bool isPremiumSticker, int32 dc, - int32 size) { + int64 size) { if (!date) { return; } @@ -2850,7 +2926,8 @@ void Session::documentApplyFields( document->updateThumbnails( inlineThumbnail, thumbnail, - videoThumbnail); + videoThumbnail, + isPremiumSticker); document->size = size; document->setattributes(attributes); @@ -3719,6 +3796,9 @@ void Session::refreshChatListEntry(Dialogs::Key key) { } for (const auto &filter : _chatsFilters->list()) { const auto id = filter.id(); + if (!id) { + continue; + } const auto filterList = chatsFilters().chatsList(id); auto event = ChatListEntryRefresh{ .key = key, .filterId = id }; if (filter.contains(history)) { @@ -3756,7 +3836,7 @@ void Session::removeChatListEntry(Dialogs::Key key) { Assert(entry->folderKnown()); for (const auto &filter : _chatsFilters->list()) { const auto id = filter.id(); - if (entry->inChatList(id)) { + if (id && entry->inChatList(id)) { entry->removeFromChatList(id, chatsFilters().chatsList(id)); _chatListEntryRefreshes.fire(ChatListEntryRefresh{ .key = key, diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 45ccb1592ad017..631eec61afac46 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -329,7 +329,12 @@ class Session final { Data::Folder *folder, FilterId filterId, not_null history) const; - int pinnedChatsLimit(Data::Folder *folder) const; + int pinnedChatsLimit( + Data::Folder *folder, + FilterId filterId) const; + rpl::producer maxPinnedChatsLimitValue( + Data::Folder *folder, + FilterId filterId) const; const std::vector &pinnedChatsOrder( Data::Folder *folder, FilterId filterId) const; @@ -477,7 +482,8 @@ class Session final { const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime); void photoConvert( not_null original, @@ -502,8 +508,9 @@ class Session final { const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, const ImageWithLocation &videoThumbnail, + bool isPremiumSticker, int32 dc, - int32 size); + int64 size); void documentConvert( not_null original, const MTPDocument &data); @@ -734,7 +741,8 @@ class Session final { const ImageWithLocation &small, const ImageWithLocation &thumbnail, const ImageWithLocation &large, - const ImageWithLocation &video, + const ImageWithLocation &videoSmall, + const ImageWithLocation &videoLarge, crl::time videoStartTime); void documentApplyFields( @@ -753,8 +761,9 @@ class Session final { const InlineImageLocation &inlineThumbnail, const ImageWithLocation &thumbnail, const ImageWithLocation &videoThumbnail, + bool isPremiumSticker, int32 dc, - int32 size); + int64 size); DocumentData *documentFromWeb( const MTPDwebDocument &data, const ImageLocation &thumbnailLocation, diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 53480ceb908119..e715250df8b954 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -9,23 +9,15 @@ For license and copyright information please follow this link: #include #include "main/main_session.h" -#include "main/main_domain.h" -#include "core/application.h" #include "apiwrap.h" #include "storage/storage_facade.h" -#include "storage/storage_shared_media.h" #include "history/history.h" #include "history/history_item.h" #include "data/data_document.h" #include "data/data_media_types.h" #include "data/data_photo.h" #include "data/data_scheduled_messages.h" -#include "data/data_sparse_ids.h" #include "data/data_session.h" -#include "info/info_memento.h" -#include "info/info_controller.h" -#include "window/window_session_controller.h" -#include "mainwindow.h" #include "core/crash_reports.h" namespace { @@ -92,23 +84,6 @@ std::optional SharedMediaOverviewType( return std::nullopt; } -void SharedMediaShowOverview( - Storage::SharedMediaType type, - not_null history) { - if (SharedMediaOverviewType(type)) { - const auto &windows = history->session().windows(); - if (windows.empty()) { - Core::App().domain().activate(&history->session().account()); - if (windows.empty()) { - return; - } - } - windows.front()->showSection(std::make_shared( - history->peer, - Info::Section(type))); - } -} - bool SharedMediaAllowSearch(Storage::SharedMediaType type) { switch (type) { case Type::MusicFile: diff --git a/Telegram/SourceFiles/data/data_shared_media.h b/Telegram/SourceFiles/data/data_shared_media.h index 445dc2e2e7080a..f24c35693dcab6 100644 --- a/Telegram/SourceFiles/data/data_shared_media.h +++ b/Telegram/SourceFiles/data/data_shared_media.h @@ -11,17 +11,12 @@ For license and copyright information please follow this link: #include "base/weak_ptr.h" #include "data/data_sparse_ids.h" -class History; - namespace Main { class Session; } // namespace Main -std::optional SharedMediaOverviewType( +[[nodiscard]] std::optional SharedMediaOverviewType( Storage::SharedMediaType type); -void SharedMediaShowOverview( - Storage::SharedMediaType type, - not_null history); bool SharedMediaAllowSearch(Storage::SharedMediaType type); rpl::producer SharedMediaViewer( diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index 8399febad4a83f..9b490701f2d91e 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -152,7 +152,7 @@ void SponsoredMessages::append( }); const auto randomId = data.vrandom_id().v; const auto hash = qs(data.vchat_invite_hash().value_or_empty()); - const auto makeFrom = []( + const auto makeFrom = [&]( not_null peer, bool exactPost = false) { const auto channel = peer->asChannel(); @@ -165,6 +165,7 @@ void SponsoredMessages::append( .isPublic = (channel && channel->isPublic()), .isBot = (peer->isUser() && peer->asUser()->isBot()), .isExactPost = exactPost, + .isRecommended = data.is_recommended(), .userpic = { .location = peer->userpicLocation() }, }; }; diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h index 6159e40864cbb7..be791a1c08c6dc 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.h +++ b/Telegram/SourceFiles/data/data_sponsored_messages.h @@ -30,6 +30,7 @@ struct SponsoredFrom { bool isPublic = false; bool isBot = false; bool isExactPost = false; + bool isRecommended = false; ImageWithLocation userpic; }; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index cfd8855cf412c0..f8d869848c4480 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -37,10 +37,10 @@ using Options = base::flags