diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 014b25725..f73fc436f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,6 @@ blank_issues_enabled: false contact_links: - - name: API issue + - name: Platform-wide issue url: https://bugs.telegram.org about: Any bug report or feature request affecting more than only Telegram Desktop. - name: Issue of other client diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 257010128..4402a95f0 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -61,13 +61,10 @@ jobs: steps: - name: Prepare directories. run: | - mkdir %userprofile%\TBuild + mkdir %userprofile%\TBuild\Libraries mklink /d %GITHUB_WORKSPACE%\TBuild %userprofile%\TBuild echo TBUILD=%GITHUB_WORKSPACE%\TBuild>>%GITHUB_ENV% - mkdir %userprofile%\TBuild Libraries - mklink /d %userprofile%\TBuild\Libraries %GITHUB_WORKSPACE%\Libraries - - name: Get repository name. shell: bash run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV @@ -101,7 +98,7 @@ jobs: id: cache-libs uses: actions/cache@v3.0.11 with: - path: Libraries + path: ${{ env.TBUILD }}\Libraries key: ${{ runner.OS }}-${{ matrix.arch }}-libs-${{ env.CACHE_KEY }} restore-keys: ${{ runner.OS }}-${{ matrix.arch }}-libs- @@ -109,7 +106,9 @@ jobs: env: GYP_MSVS_OVERRIDE_PATH: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\' GYP_MSVS_VERSION: 2022 - run: '%TBUILD%\%REPO_NAME%\Telegram\build\prepare\win.bat skip-release silent' + run: | + cd %TBUILD% + %REPO_NAME%\Telegram\build\prepare\win.bat skip-release silent - name: Read defines. shell: bash @@ -133,6 +132,7 @@ jobs: - name: Free up some disk space. run: | + cd %TBUILD% del /S Libraries\*.pdb del /S Libraries\*.pch del /S Libraries\*.obj diff --git a/CMakeLists.txt b/CMakeLists.txt index 2979473c4..7ec62b158 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,9 +19,11 @@ include(cmake/validate_special_target.cmake) include(cmake/version.cmake) desktop_app_parse_version(Telegram/build/version) -set(project_langs ASM C CXX) +set(project_langs C CXX) if (APPLE) list(APPEND project_langs OBJC OBJCXX) +elseif (LINUX) + list(APPEND project_langs ASM) endif() project(Telegram @@ -57,7 +59,7 @@ include(cmake/options.cmake) if (NOT DESKTOP_APP_USE_PACKAGED) if (WIN32) - set(qt_version 5.15.9) + set(qt_version 5.15.10) elseif (APPLE) set(qt_version 6.3.2) endif() diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 898416cbb..56e22c90c 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -211,6 +211,8 @@ PRIVATE api/api_user_privacy.h api/api_views.cpp api/api_views.h + api/api_websites.cpp + api/api_websites.h api/api_who_reacted.cpp api/api_who_reacted.h boxes/filters/edit_filter_box.cpp @@ -1041,6 +1043,8 @@ PRIVATE media/stories/media_stories_sibling.h media/stories/media_stories_slider.cpp media/stories/media_stories_slider.h + media/stories/media_stories_stealth.cpp + media/stories/media_stories_stealth.h media/stories/media_stories_view.cpp media/stories/media_stories_view.h media/streaming/media_streaming_audio_track.cpp @@ -1312,6 +1316,8 @@ PRIVATE settings/settings_scale_preview.cpp settings/settings_scale_preview.h settings/settings_type.h + settings/settings_websites.cpp + settings/settings_websites.h storage/details/storage_file_utilities.cpp storage/details/storage_file_utilities.h storage/details/storage_settings_scheme.cpp diff --git a/Telegram/Resources/icons/chat/input_like.png b/Telegram/Resources/icons/chat/input_like.png new file mode 100644 index 000000000..c7ccead7e Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like.png differ diff --git a/Telegram/Resources/icons/chat/input_like@2x.png b/Telegram/Resources/icons/chat/input_like@2x.png new file mode 100644 index 000000000..965ae814b Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_like@3x.png b/Telegram/Resources/icons/chat/input_like@3x.png new file mode 100644 index 000000000..cb2285df9 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_like@3x.png differ diff --git a/Telegram/Resources/icons/chat/input_liked.png b/Telegram/Resources/icons/chat/input_liked.png new file mode 100644 index 000000000..5a267ca1d Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked.png differ diff --git a/Telegram/Resources/icons/chat/input_liked@2x.png b/Telegram/Resources/icons/chat/input_liked@2x.png new file mode 100644 index 000000000..600b472be Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_liked@3x.png b/Telegram/Resources/icons/chat/input_liked@3x.png new file mode 100644 index 000000000..5d6c546e6 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_liked@3x.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_skin.png b/Telegram/Resources/icons/emoji/emoji_skin.png new file mode 100644 index 000000000..776617ebe Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_skin@2x.png b/Telegram/Resources/icons/emoji/emoji_skin@2x.png new file mode 100644 index 000000000..a53f5c18e Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin@2x.png differ diff --git a/Telegram/Resources/icons/emoji/emoji_skin@3x.png b/Telegram/Resources/icons/emoji/emoji_skin@3x.png new file mode 100644 index 000000000..fd756d0a9 Binary files /dev/null and b/Telegram/Resources/icons/emoji/emoji_skin@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/download_locked.png b/Telegram/Resources/icons/mediaview/download_locked.png new file mode 100644 index 000000000..206d5893d Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked.png differ diff --git a/Telegram/Resources/icons/mediaview/download_locked@2x.png b/Telegram/Resources/icons/mediaview/download_locked@2x.png new file mode 100644 index 000000000..46bac9086 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/download_locked@3x.png b/Telegram/Resources/icons/mediaview/download_locked@3x.png new file mode 100644 index 000000000..57752e830 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/download_locked@3x.png differ diff --git a/Telegram/Resources/icons/menu/antispam.png b/Telegram/Resources/icons/menu/antispam.png new file mode 100644 index 000000000..201dd2fbb Binary files /dev/null and b/Telegram/Resources/icons/menu/antispam.png differ diff --git a/Telegram/Resources/icons/menu/antispam@2x.png b/Telegram/Resources/icons/menu/antispam@2x.png new file mode 100644 index 000000000..f465352f2 Binary files /dev/null and b/Telegram/Resources/icons/menu/antispam@2x.png differ diff --git a/Telegram/Resources/icons/menu/antispam@3x.png b/Telegram/Resources/icons/menu/antispam@3x.png new file mode 100644 index 000000000..43e7f8778 Binary files /dev/null and b/Telegram/Resources/icons/menu/antispam@3x.png differ diff --git a/Telegram/Resources/icons/menu/bot_commands.png b/Telegram/Resources/icons/menu/bot_commands.png new file mode 100644 index 000000000..198b475be Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_commands.png differ diff --git a/Telegram/Resources/icons/menu/bot_commands@2x.png b/Telegram/Resources/icons/menu/bot_commands@2x.png new file mode 100644 index 000000000..2ab6754f8 Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_commands@2x.png differ diff --git a/Telegram/Resources/icons/menu/bot_commands@3x.png b/Telegram/Resources/icons/menu/bot_commands@3x.png new file mode 100644 index 000000000..1d0974ca4 Binary files /dev/null and b/Telegram/Resources/icons/menu/bot_commands@3x.png differ diff --git a/Telegram/Resources/icons/menu/calls_receive.png b/Telegram/Resources/icons/menu/calls_receive.png new file mode 100644 index 000000000..cfe1f6699 Binary files /dev/null and b/Telegram/Resources/icons/menu/calls_receive.png differ diff --git a/Telegram/Resources/icons/menu/calls_receive@2x.png b/Telegram/Resources/icons/menu/calls_receive@2x.png new file mode 100644 index 000000000..83e8e622d Binary files /dev/null and b/Telegram/Resources/icons/menu/calls_receive@2x.png differ diff --git a/Telegram/Resources/icons/menu/calls_receive@3x.png b/Telegram/Resources/icons/menu/calls_receive@3x.png new file mode 100644 index 000000000..a74e596a7 Binary files /dev/null and b/Telegram/Resources/icons/menu/calls_receive@3x.png differ diff --git a/Telegram/Resources/icons/menu/chat_discuss.png b/Telegram/Resources/icons/menu/chat_discuss.png new file mode 100644 index 000000000..014033429 Binary files /dev/null and b/Telegram/Resources/icons/menu/chat_discuss.png differ diff --git a/Telegram/Resources/icons/menu/chat_discuss@2x.png b/Telegram/Resources/icons/menu/chat_discuss@2x.png new file mode 100644 index 000000000..2b266a33d Binary files /dev/null and b/Telegram/Resources/icons/menu/chat_discuss@2x.png differ diff --git a/Telegram/Resources/icons/menu/chat_discuss@3x.png b/Telegram/Resources/icons/menu/chat_discuss@3x.png new file mode 100644 index 000000000..2a54806d8 Binary files /dev/null and b/Telegram/Resources/icons/menu/chat_discuss@3x.png differ diff --git a/Telegram/Resources/icons/menu/devices.png b/Telegram/Resources/icons/menu/devices.png new file mode 100644 index 000000000..192140249 Binary files /dev/null and b/Telegram/Resources/icons/menu/devices.png differ diff --git a/Telegram/Resources/icons/menu/devices@2x.png b/Telegram/Resources/icons/menu/devices@2x.png new file mode 100644 index 000000000..6ac88a975 Binary files /dev/null and b/Telegram/Resources/icons/menu/devices@2x.png differ diff --git a/Telegram/Resources/icons/menu/devices@3x.png b/Telegram/Resources/icons/menu/devices@3x.png new file mode 100644 index 000000000..a5b443200 Binary files /dev/null and b/Telegram/Resources/icons/menu/devices@3x.png differ diff --git a/Telegram/Resources/icons/menu/dock_bounce.png b/Telegram/Resources/icons/menu/dock_bounce.png new file mode 100644 index 000000000..b538f11e0 Binary files /dev/null and b/Telegram/Resources/icons/menu/dock_bounce.png differ diff --git a/Telegram/Resources/icons/menu/dock_bounce@2x.png b/Telegram/Resources/icons/menu/dock_bounce@2x.png new file mode 100644 index 000000000..232aaa82c Binary files /dev/null and b/Telegram/Resources/icons/menu/dock_bounce@2x.png differ diff --git a/Telegram/Resources/icons/menu/dock_bounce@3x.png b/Telegram/Resources/icons/menu/dock_bounce@3x.png new file mode 100644 index 000000000..5bc6d3dd3 Binary files /dev/null and b/Telegram/Resources/icons/menu/dock_bounce@3x.png differ diff --git a/Telegram/Resources/icons/menu/download_locked.png b/Telegram/Resources/icons/menu/download_locked.png new file mode 100644 index 000000000..824ca3707 Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked.png differ diff --git a/Telegram/Resources/icons/menu/download_locked@2x.png b/Telegram/Resources/icons/menu/download_locked@2x.png new file mode 100644 index 000000000..1e2dde2ce Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked@2x.png differ diff --git a/Telegram/Resources/icons/menu/download_locked@3x.png b/Telegram/Resources/icons/menu/download_locked@3x.png new file mode 100644 index 000000000..69d05e38d Binary files /dev/null and b/Telegram/Resources/icons/menu/download_locked@3x.png differ diff --git a/Telegram/Resources/icons/menu/experimental.png b/Telegram/Resources/icons/menu/experimental.png new file mode 100644 index 000000000..f84b745a9 Binary files /dev/null and b/Telegram/Resources/icons/menu/experimental.png differ diff --git a/Telegram/Resources/icons/menu/experimental@2x.png b/Telegram/Resources/icons/menu/experimental@2x.png new file mode 100644 index 000000000..e56b0c3ed Binary files /dev/null and b/Telegram/Resources/icons/menu/experimental@2x.png differ diff --git a/Telegram/Resources/icons/menu/experimental@3x.png b/Telegram/Resources/icons/menu/experimental@3x.png new file mode 100644 index 000000000..d2c1efdd2 Binary files /dev/null and b/Telegram/Resources/icons/menu/experimental@3x.png differ diff --git a/Telegram/Resources/icons/menu/faq.png b/Telegram/Resources/icons/menu/faq.png new file mode 100644 index 000000000..e280515e6 Binary files /dev/null and b/Telegram/Resources/icons/menu/faq.png differ diff --git a/Telegram/Resources/icons/menu/faq@2x.png b/Telegram/Resources/icons/menu/faq@2x.png new file mode 100644 index 000000000..c44914078 Binary files /dev/null and b/Telegram/Resources/icons/menu/faq@2x.png differ diff --git a/Telegram/Resources/icons/menu/faq@3x.png b/Telegram/Resources/icons/menu/faq@3x.png new file mode 100644 index 000000000..853065e0b Binary files /dev/null and b/Telegram/Resources/icons/menu/faq@3x.png differ diff --git a/Telegram/Resources/icons/menu/group_log.png b/Telegram/Resources/icons/menu/group_log.png new file mode 100644 index 000000000..5452f7975 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_log.png differ diff --git a/Telegram/Resources/icons/menu/group_log@2x.png b/Telegram/Resources/icons/menu/group_log@2x.png new file mode 100644 index 000000000..b372a0cf6 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_log@2x.png differ diff --git a/Telegram/Resources/icons/menu/group_log@3x.png b/Telegram/Resources/icons/menu/group_log@3x.png new file mode 100644 index 000000000..678b9dc62 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_log@3x.png differ diff --git a/Telegram/Resources/icons/menu/group_reactions.png b/Telegram/Resources/icons/menu/group_reactions.png new file mode 100644 index 000000000..c941a5955 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_reactions.png differ diff --git a/Telegram/Resources/icons/menu/group_reactions@2x.png b/Telegram/Resources/icons/menu/group_reactions@2x.png new file mode 100644 index 000000000..406d42273 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_reactions@2x.png differ diff --git a/Telegram/Resources/icons/menu/group_reactions@3x.png b/Telegram/Resources/icons/menu/group_reactions@3x.png new file mode 100644 index 000000000..caf8ae470 Binary files /dev/null and b/Telegram/Resources/icons/menu/group_reactions@3x.png differ diff --git a/Telegram/Resources/icons/menu/groups_create.png b/Telegram/Resources/icons/menu/groups_create.png new file mode 100644 index 000000000..5d6bb4c00 Binary files /dev/null and b/Telegram/Resources/icons/menu/groups_create.png differ diff --git a/Telegram/Resources/icons/menu/groups_create@2x.png b/Telegram/Resources/icons/menu/groups_create@2x.png new file mode 100644 index 000000000..cf110c030 Binary files /dev/null and b/Telegram/Resources/icons/menu/groups_create@2x.png differ diff --git a/Telegram/Resources/icons/menu/groups_create@3x.png b/Telegram/Resources/icons/menu/groups_create@3x.png new file mode 100644 index 000000000..b45349322 Binary files /dev/null and b/Telegram/Resources/icons/menu/groups_create@3x.png differ diff --git a/Telegram/Resources/icons/menu/hide_members.png b/Telegram/Resources/icons/menu/hide_members.png new file mode 100644 index 000000000..5e81f7655 Binary files /dev/null and b/Telegram/Resources/icons/menu/hide_members.png differ diff --git a/Telegram/Resources/icons/menu/hide_members@2x.png b/Telegram/Resources/icons/menu/hide_members@2x.png new file mode 100644 index 000000000..e10056514 Binary files /dev/null and b/Telegram/Resources/icons/menu/hide_members@2x.png differ diff --git a/Telegram/Resources/icons/menu/hide_members@3x.png b/Telegram/Resources/icons/menu/hide_members@3x.png new file mode 100644 index 000000000..011d3c425 Binary files /dev/null and b/Telegram/Resources/icons/menu/hide_members@3x.png differ diff --git a/Telegram/Resources/icons/menu/info_notifications.png b/Telegram/Resources/icons/menu/info_notifications.png new file mode 100644 index 000000000..3686dba72 Binary files /dev/null and b/Telegram/Resources/icons/menu/info_notifications.png differ diff --git a/Telegram/Resources/icons/menu/info_notifications@2x.png b/Telegram/Resources/icons/menu/info_notifications@2x.png new file mode 100644 index 000000000..ed724c7f2 Binary files /dev/null and b/Telegram/Resources/icons/menu/info_notifications@2x.png differ diff --git a/Telegram/Resources/icons/menu/info_notifications@3x.png b/Telegram/Resources/icons/menu/info_notifications@3x.png new file mode 100644 index 000000000..de98cf241 Binary files /dev/null and b/Telegram/Resources/icons/menu/info_notifications@3x.png differ diff --git a/Telegram/Resources/icons/menu/ip_address.png b/Telegram/Resources/icons/menu/ip_address.png new file mode 100644 index 000000000..3aa87b0aa Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address.png differ diff --git a/Telegram/Resources/icons/menu/ip_address@2x.png b/Telegram/Resources/icons/menu/ip_address@2x.png new file mode 100644 index 000000000..1184e10b0 Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address@2x.png differ diff --git a/Telegram/Resources/icons/menu/ip_address@3x.png b/Telegram/Resources/icons/menu/ip_address@3x.png new file mode 100644 index 000000000..3cee2b09d Binary files /dev/null and b/Telegram/Resources/icons/menu/ip_address@3x.png differ diff --git a/Telegram/Resources/icons/menu/links_profile.png b/Telegram/Resources/icons/menu/links_profile.png new file mode 100644 index 000000000..da8497860 Binary files /dev/null and b/Telegram/Resources/icons/menu/links_profile.png differ diff --git a/Telegram/Resources/icons/menu/links_profile@2x.png b/Telegram/Resources/icons/menu/links_profile@2x.png new file mode 100644 index 000000000..26dfa285e Binary files /dev/null and b/Telegram/Resources/icons/menu/links_profile@2x.png differ diff --git a/Telegram/Resources/icons/menu/links_profile@3x.png b/Telegram/Resources/icons/menu/links_profile@3x.png new file mode 100644 index 000000000..fef135b0d Binary files /dev/null and b/Telegram/Resources/icons/menu/links_profile@3x.png differ diff --git a/Telegram/Resources/icons/menu/lock.png b/Telegram/Resources/icons/menu/lock.png new file mode 100644 index 000000000..7e7c58bbb Binary files /dev/null and b/Telegram/Resources/icons/menu/lock.png differ diff --git a/Telegram/Resources/icons/menu/lock@2x.png b/Telegram/Resources/icons/menu/lock@2x.png new file mode 100644 index 000000000..2887113a8 Binary files /dev/null and b/Telegram/Resources/icons/menu/lock@2x.png differ diff --git a/Telegram/Resources/icons/menu/lock@3x.png b/Telegram/Resources/icons/menu/lock@3x.png new file mode 100644 index 000000000..f960d6a5d Binary files /dev/null and b/Telegram/Resources/icons/menu/lock@3x.png differ diff --git a/Telegram/Resources/icons/menu/network.png b/Telegram/Resources/icons/menu/network.png new file mode 100644 index 000000000..c1f4b9829 Binary files /dev/null and b/Telegram/Resources/icons/menu/network.png differ diff --git a/Telegram/Resources/icons/menu/network@2x.png b/Telegram/Resources/icons/menu/network@2x.png new file mode 100644 index 000000000..76f22a6ae Binary files /dev/null and b/Telegram/Resources/icons/menu/network@2x.png differ diff --git a/Telegram/Resources/icons/menu/network@3x.png b/Telegram/Resources/icons/menu/network@3x.png new file mode 100644 index 000000000..0f0e8a6fa Binary files /dev/null and b/Telegram/Resources/icons/menu/network@3x.png differ diff --git a/Telegram/Resources/icons/menu/payment_address.png b/Telegram/Resources/icons/menu/payment_address.png new file mode 100644 index 000000000..a7cc0eb69 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address.png differ diff --git a/Telegram/Resources/icons/menu/payment_address@2x.png b/Telegram/Resources/icons/menu/payment_address@2x.png new file mode 100644 index 000000000..12daa492c Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address@2x.png differ diff --git a/Telegram/Resources/icons/menu/payment_address@3x.png b/Telegram/Resources/icons/menu/payment_address@3x.png new file mode 100644 index 000000000..3172912c6 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_address@3x.png differ diff --git a/Telegram/Resources/icons/menu/payment_email.png b/Telegram/Resources/icons/menu/payment_email.png new file mode 100644 index 000000000..5a1c58863 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_email.png differ diff --git a/Telegram/Resources/icons/menu/payment_email@2x.png b/Telegram/Resources/icons/menu/payment_email@2x.png new file mode 100644 index 000000000..a8af89a41 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_email@2x.png differ diff --git a/Telegram/Resources/icons/menu/payment_email@3x.png b/Telegram/Resources/icons/menu/payment_email@3x.png new file mode 100644 index 000000000..b47586e27 Binary files /dev/null and b/Telegram/Resources/icons/menu/payment_email@3x.png differ diff --git a/Telegram/Resources/icons/menu/power_usage.png b/Telegram/Resources/icons/menu/power_usage.png new file mode 100644 index 000000000..a6a8015c6 Binary files /dev/null and b/Telegram/Resources/icons/menu/power_usage.png differ diff --git a/Telegram/Resources/icons/menu/power_usage@2x.png b/Telegram/Resources/icons/menu/power_usage@2x.png new file mode 100644 index 000000000..cf2e46f6c Binary files /dev/null and b/Telegram/Resources/icons/menu/power_usage@2x.png differ diff --git a/Telegram/Resources/icons/menu/power_usage@3x.png b/Telegram/Resources/icons/menu/power_usage@3x.png new file mode 100644 index 000000000..cb81ed700 Binary files /dev/null and b/Telegram/Resources/icons/menu/power_usage@3x.png differ diff --git a/Telegram/Resources/icons/menu/premium.png b/Telegram/Resources/icons/menu/premium.png new file mode 100644 index 000000000..56e0e0e94 Binary files /dev/null and b/Telegram/Resources/icons/menu/premium.png differ diff --git a/Telegram/Resources/icons/menu/premium@2x.png b/Telegram/Resources/icons/menu/premium@2x.png new file mode 100644 index 000000000..64913421b Binary files /dev/null and b/Telegram/Resources/icons/menu/premium@2x.png differ diff --git a/Telegram/Resources/icons/menu/premium@3x.png b/Telegram/Resources/icons/menu/premium@3x.png new file mode 100644 index 000000000..0ba0e8b9f Binary files /dev/null and b/Telegram/Resources/icons/menu/premium@3x.png differ diff --git a/Telegram/Resources/icons/menu/recovery_email.png b/Telegram/Resources/icons/menu/recovery_email.png new file mode 100644 index 000000000..a9d149fed Binary files /dev/null and b/Telegram/Resources/icons/menu/recovery_email.png differ diff --git a/Telegram/Resources/icons/menu/recovery_email@2x.png b/Telegram/Resources/icons/menu/recovery_email@2x.png new file mode 100644 index 000000000..dd378b846 Binary files /dev/null and b/Telegram/Resources/icons/menu/recovery_email@2x.png differ diff --git a/Telegram/Resources/icons/menu/recovery_email@3x.png b/Telegram/Resources/icons/menu/recovery_email@3x.png new file mode 100644 index 000000000..1372dd22c Binary files /dev/null and b/Telegram/Resources/icons/menu/recovery_email@3x.png differ diff --git a/Telegram/Resources/icons/menu/remove.png b/Telegram/Resources/icons/menu/remove.png new file mode 100644 index 000000000..e8c9534cd Binary files /dev/null and b/Telegram/Resources/icons/menu/remove.png differ diff --git a/Telegram/Resources/icons/menu/remove@2x.png b/Telegram/Resources/icons/menu/remove@2x.png new file mode 100644 index 000000000..b064e3efe Binary files /dev/null and b/Telegram/Resources/icons/menu/remove@2x.png differ diff --git a/Telegram/Resources/icons/menu/remove@3x.png b/Telegram/Resources/icons/menu/remove@3x.png new file mode 100644 index 000000000..90b1b337d Binary files /dev/null and b/Telegram/Resources/icons/menu/remove@3x.png differ diff --git a/Telegram/Resources/icons/menu/signed.png b/Telegram/Resources/icons/menu/signed.png new file mode 100644 index 000000000..ab59a070d Binary files /dev/null and b/Telegram/Resources/icons/menu/signed.png differ diff --git a/Telegram/Resources/icons/menu/signed@2x.png b/Telegram/Resources/icons/menu/signed@2x.png new file mode 100644 index 000000000..e8a3faaf1 Binary files /dev/null and b/Telegram/Resources/icons/menu/signed@2x.png differ diff --git a/Telegram/Resources/icons/menu/signed@3x.png b/Telegram/Resources/icons/menu/signed@3x.png new file mode 100644 index 000000000..2a9e1011f Binary files /dev/null and b/Telegram/Resources/icons/menu/signed@3x.png differ diff --git a/Telegram/Resources/icons/menu/stealth.png b/Telegram/Resources/icons/menu/stealth.png new file mode 100644 index 000000000..af12353a9 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth.png differ diff --git a/Telegram/Resources/icons/menu/stealth@2x.png b/Telegram/Resources/icons/menu/stealth@2x.png new file mode 100644 index 000000000..50706a61f Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth@2x.png differ diff --git a/Telegram/Resources/icons/menu/stealth@3x.png b/Telegram/Resources/icons/menu/stealth@3x.png new file mode 100644 index 000000000..c659d25b8 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth@3x.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked.png b/Telegram/Resources/icons/menu/stealth_locked.png new file mode 100644 index 000000000..dbc0cb5e3 Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked@2x.png b/Telegram/Resources/icons/menu/stealth_locked@2x.png new file mode 100644 index 000000000..979e23d5c Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked@2x.png differ diff --git a/Telegram/Resources/icons/menu/stealth_locked@3x.png b/Telegram/Resources/icons/menu/stealth_locked@3x.png new file mode 100644 index 000000000..07278760a Binary files /dev/null and b/Telegram/Resources/icons/menu/stealth_locked@3x.png differ diff --git a/Telegram/Resources/icons/menu/stop_poll.png b/Telegram/Resources/icons/menu/stop_poll.png deleted file mode 100644 index 55846def1..000000000 Binary files a/Telegram/Resources/icons/menu/stop_poll.png and /dev/null differ diff --git a/Telegram/Resources/icons/menu/stop_poll@2x.png b/Telegram/Resources/icons/menu/stop_poll@2x.png deleted file mode 100644 index 85fb5f827..000000000 Binary files a/Telegram/Resources/icons/menu/stop_poll@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/menu/stop_poll@3x.png b/Telegram/Resources/icons/menu/stop_poll@3x.png deleted file mode 100644 index 179ea1b89..000000000 Binary files a/Telegram/Resources/icons/menu/stop_poll@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/menu/storage.png b/Telegram/Resources/icons/menu/storage.png new file mode 100644 index 000000000..ce6ffd0a3 Binary files /dev/null and b/Telegram/Resources/icons/menu/storage.png differ diff --git a/Telegram/Resources/icons/menu/storage@2x.png b/Telegram/Resources/icons/menu/storage@2x.png new file mode 100644 index 000000000..c38320ba7 Binary files /dev/null and b/Telegram/Resources/icons/menu/storage@2x.png differ diff --git a/Telegram/Resources/icons/menu/storage@3x.png b/Telegram/Resources/icons/menu/storage@3x.png new file mode 100644 index 000000000..532f2ec0f Binary files /dev/null and b/Telegram/Resources/icons/menu/storage@3x.png differ diff --git a/Telegram/Resources/icons/menu/timer.png b/Telegram/Resources/icons/menu/timer.png new file mode 100644 index 000000000..5fe0383b4 Binary files /dev/null and b/Telegram/Resources/icons/menu/timer.png differ diff --git a/Telegram/Resources/icons/menu/timer@2x.png b/Telegram/Resources/icons/menu/timer@2x.png new file mode 100644 index 000000000..28d3d1480 Binary files /dev/null and b/Telegram/Resources/icons/menu/timer@2x.png differ diff --git a/Telegram/Resources/icons/menu/timer@3x.png b/Telegram/Resources/icons/menu/timer@3x.png new file mode 100644 index 000000000..173ad913d Binary files /dev/null and b/Telegram/Resources/icons/menu/timer@3x.png differ diff --git a/Telegram/Resources/icons/menu/topics.png b/Telegram/Resources/icons/menu/topics.png new file mode 100644 index 000000000..3e5eed67b Binary files /dev/null and b/Telegram/Resources/icons/menu/topics.png differ diff --git a/Telegram/Resources/icons/menu/topics@2x.png b/Telegram/Resources/icons/menu/topics@2x.png new file mode 100644 index 000000000..789d9cf60 Binary files /dev/null and b/Telegram/Resources/icons/menu/topics@2x.png differ diff --git a/Telegram/Resources/icons/menu/topics@3x.png b/Telegram/Resources/icons/menu/topics@3x.png new file mode 100644 index 000000000..f5c03b336 Binary files /dev/null and b/Telegram/Resources/icons/menu/topics@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_caption.png b/Telegram/Resources/icons/settings/premium/stories_caption.png new file mode 100644 index 000000000..d73bc9068 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_caption.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_caption@2x.png b/Telegram/Resources/icons/settings/premium/stories_caption@2x.png new file mode 100644 index 000000000..e67594df0 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_caption@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_caption@3x.png b/Telegram/Resources/icons/settings/premium/stories_caption@3x.png new file mode 100644 index 000000000..373884ace Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_caption@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_order.png b/Telegram/Resources/icons/settings/premium/stories_order.png new file mode 100644 index 000000000..3051561df Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_order.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_order@2x.png b/Telegram/Resources/icons/settings/premium/stories_order@2x.png new file mode 100644 index 000000000..4b4f64ceb Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_order@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/stories_order@3x.png b/Telegram/Resources/icons/settings/premium/stories_order@3x.png new file mode 100644 index 000000000..d550ddcef Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/stories_order@3x.png differ diff --git a/Telegram/Resources/icons/settings/premium/timer.png b/Telegram/Resources/icons/settings/premium/timer.png new file mode 100644 index 000000000..5fe0383b4 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/timer.png differ diff --git a/Telegram/Resources/icons/settings/premium/timer@2x.png b/Telegram/Resources/icons/settings/premium/timer@2x.png new file mode 100644 index 000000000..28d3d1480 Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/timer@2x.png differ diff --git a/Telegram/Resources/icons/settings/premium/timer@3x.png b/Telegram/Resources/icons/settings/premium/timer@3x.png new file mode 100644 index 000000000..173ad913d Binary files /dev/null and b/Telegram/Resources/icons/settings/premium/timer@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/stories_next.png b/Telegram/Resources/icons/stories/next.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next.png rename to Telegram/Resources/icons/stories/next.png diff --git a/Telegram/Resources/icons/mediaview/stories_next@2x.png b/Telegram/Resources/icons/stories/next@2x.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next@2x.png rename to Telegram/Resources/icons/stories/next@2x.png diff --git a/Telegram/Resources/icons/mediaview/stories_next@3x.png b/Telegram/Resources/icons/stories/next@3x.png similarity index 100% rename from Telegram/Resources/icons/mediaview/stories_next@3x.png rename to Telegram/Resources/icons/stories/next@3x.png diff --git a/Telegram/Resources/icons/stories/stealth_25m.png b/Telegram/Resources/icons/stories/stealth_25m.png new file mode 100644 index 000000000..77fb7fdbb Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m.png differ diff --git a/Telegram/Resources/icons/stories/stealth_25m@2x.png b/Telegram/Resources/icons/stories/stealth_25m@2x.png new file mode 100644 index 000000000..6ec9a01f7 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_25m@3x.png b/Telegram/Resources/icons/stories/stealth_25m@3x.png new file mode 100644 index 000000000..e1601cd0e Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_25m@3x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m.png b/Telegram/Resources/icons/stories/stealth_5m.png new file mode 100644 index 000000000..51f1cdfc3 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m@2x.png b/Telegram/Resources/icons/stories/stealth_5m@2x.png new file mode 100644 index 000000000..dd0804fea Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_5m@3x.png b/Telegram/Resources/icons/stories/stealth_5m@3x.png new file mode 100644 index 000000000..c635dec9f Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_5m@3x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo.png b/Telegram/Resources/icons/stories/stealth_logo.png new file mode 100644 index 000000000..10a249195 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo@2x.png b/Telegram/Resources/icons/stories/stealth_logo@2x.png new file mode 100644 index 000000000..4300b18f1 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo@2x.png differ diff --git a/Telegram/Resources/icons/stories/stealth_logo@3x.png b/Telegram/Resources/icons/stories/stealth_logo@3x.png new file mode 100644 index 000000000..161618d20 Binary files /dev/null and b/Telegram/Resources/icons/stories/stealth_logo@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c9de1df7c..de72609e7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -272,6 +272,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_error_noforwards_channel" = "Sorry, forwarding from this channel is disabled by admins."; "lng_error_nocopy_group" = "Sorry, copying from this group is disabled by admins."; "lng_error_nocopy_channel" = "Sorry, copying from this channel is disabled by admins."; +"lng_error_nocopy_story" = "Sorry, copying of this story is disabled by the author."; "lng_sure_add_admin_invite" = "This user is not a member of this group. Add them to the group and promote them to admin?"; "lng_sure_add_admin_invite_channel" = "This user is not a subscriber of this channel. Add them to the channel and promote them to admin?"; "lng_sure_add_admin_unremove" = "This user is currently restricted or removed. Are you sure you want to promote them?"; @@ -599,6 +600,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_username_label" = "Username"; "lng_settings_phone_label" = "Phone number"; "lng_settings_username_add" = "Add username"; +"lng_settings_username_about" = "Username lets people contact you on Telegram without needing your phone number."; +"lng_settings_add_account_about" = "You can add up to four accounts with different phone numbers."; "lng_settings_peer_to_peer_about" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but may slightly decrease audio quality."; "lng_settings_advanced" = "Advanced"; "lng_settings_stickers_emoji" = "Stickers and emoji"; @@ -611,12 +614,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_theme_accent_title" = "Choose accent color"; "lng_settings_data_storage" = "Data and storage"; "lng_settings_information" = "Edit profile"; +"lng_settings_my_account" = "My Account"; "lng_settings_security" = "Security"; "lng_settings_passcode_title" = "Local passcode"; "lng_settings_sessions_title" = "Active sessions"; +"lng_settings_sessions_about" = "Review the list of devices where you are logged into your Telegram account."; +"lng_settings_archive_title" = "Archive Settings"; "lng_settings_new_unknown" = "New chats from unknown users"; "lng_settings_auto_archive" = "Archive and Mute"; "lng_settings_auto_archive_about" = "Automatically archive and mute new chats, groups and channels from non-contacts."; +"lng_settings_unmuted_chats" = "Unmuted chats"; +"lng_settings_always_in_archive" = "Always keep archived"; +"lng_settings_unmuted_chats_about" = "Keep archived chats in the Archive even if they are unmuted and get a new message."; +"lng_settings_chats_from_folders" = "Chats from folders"; +"lng_settings_chats_from_folders_about" = "Keep archived chats from folders in the Archive even if they are unmuted and get a new message."; "lng_settings_destroy_title" = "Delete my account"; "lng_settings_version_info" = "Version and updates"; "lng_settings_system_integration" = "System integration"; @@ -635,6 +646,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices."; "lng_settings_security_bots" = "Bots and websites"; "lng_settings_clear_payment_info" = "Clear Payment and Shipping Info"; +"lng_settings_logged_in" = "Connected websites"; +"lng_settings_logged_in_title" = "Logged in with Telegram"; +"lng_settings_logged_in_description" = "You can log in on websites that support signing in with Telegram."; +"lng_settings_disconnect_all" = "Disconnect all websites"; +"lng_settings_disconnect_title" = "Disconnect website"; +"lng_settings_disconnect_sure" = "Are you sure you want to disconnect {domain}?"; +"lng_settings_disconnect_block" = "Block {name}"; +"lng_settings_disconnect_all_title" = "Disconnect websites"; +"lng_settings_disconnect_all_sure" = "Are you sure you want to disconnect all websites where you logged in with Telegram?"; +"lng_settings_disconnect" = "Disconnect"; +"lng_settings_connected_title" = "Connected websites"; "lng_settings_power_menu" = "Battery and Animations"; "lng_settings_power_title" = "Power Usage"; @@ -944,6 +966,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_sessions_terminate" = "Terminate Session"; "lng_sessions_application" = "Application"; "lng_sessions_system" = "System version"; +"lng_sessions_browser" = "Browser"; "lng_sessions_ip" = "IP address"; "lng_sessions_location" = "Location"; "lng_sessions_location_about" = "This location is based only on the IP address and may not always be accurate."; @@ -1789,6 +1812,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_emoji_set_active" = "Current set"; "lng_emoji_set_download" = "Download {size}"; "lng_emoji_set_loading" = "{percent}, {progress}"; +"lng_emoji_color_all" = "Choose color for all emoji"; +"lng_emoji_copy" = "Copy emoji"; +"lng_emoji_view_pack" = "View pack"; +"lng_emoji_remove_recent" = "Remove from recents"; +"lng_emoji_reset_recent" = "Reset recents"; +"lng_emoji_reset_recent_sure" = "Do you want to reset recent emoji?"; +"lng_emoji_reset_recent_button" = "Reset"; "lng_recent_stickers" = "Frequently used"; "lng_faved_stickers_add" = "Add to Favorites"; @@ -1827,6 +1857,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_subtitle_gift#other" = "{user} has gifted you a {count}-months subscription for Telegram Premium."; "lng_premium_summary_subtitle_gift_me#one" = "You gifted {user} a {count}-month subscription for Telegram Premium."; "lng_premium_summary_subtitle_gift_me#other" = "You gifted {user} a {count}-months subscription for Telegram Premium."; +"lng_premium_summary_subtitle_stories" = "Upgraded Stories"; +"lng_premium_summary_about_stories" = "Priority order, stealth mode, permanent views history and more."; "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"; @@ -1857,9 +1889,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_summary_new_badge" = "NEW"; + "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."; +// Upgraded Stories. +"lng_premium_stories_subtitle_order" = "Priority Order"; +"lng_premium_stories_about_order" = "Get more views as your stories are always displayed first."; + +"lng_premium_stories_subtitle_stealth" = "Stealth Mode"; +"lng_premium_stories_about_stealth" = "Hide the fact that you viewed other people's stories."; + +"lng_premium_stories_subtitle_views" = "Permanent Views History"; +"lng_premium_stories_about_views" = "Check who opens your stories – even after they expire."; + +"lng_premium_stories_subtitle_expiration" = "Expiration Durations*"; +"lng_premium_stories_about_expiration" = "Set custom expiration durations like 6 or 48 hours for your stories."; + +"lng_premium_stories_subtitle_download" = "Download Stories"; +"lng_premium_stories_about_download" = "Save other people's unprotected stories to your disk."; + +"lng_premium_stories_subtitle_caption" = "Longer Captions*"; +"lng_premium_stories_about_caption" = "Add ten times longer captions to your stories – up to 2048 symbols."; + +"lng_premium_stories_subtitle_links" = "Links and Formatting*"; +"lng_premium_stories_about_links" = "Add links and formatting in captions of your stories."; + +"lng_premium_stories_about_mobile" = "* Available when posting stories from Telegram apps for iOS and Android."; + // 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"; @@ -2249,6 +2307,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_archive_to_menu" = "Move to main menu"; "lng_context_archive_to_list" = "Move to chats list"; "lng_context_archive_to_menu_info" = "Archive moved to the main menu!\nYou can return it from the context menu of the archive button."; +"lng_context_archive_settings" = "Archive settings"; "lng_context_mute" = "Mute"; "lng_context_unmute" = "Unmute"; @@ -3876,6 +3935,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_archive_done" = "This story is hidden from your profile."; "lng_stories_archive_done_many#one" = "{count} story is hidden from your profile."; "lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile."; +"lng_stories_save_promo" = "Subscribe to {link} to download other people's unprotected stories to disk."; + +"lng_stealth_mode_menu_item" = "Stealth Mode"; +"lng_stealth_mode_title" = "Stealth Mode"; +"lng_stealth_mode_unlock_about" = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them."; +"lng_stealth_mode_about" = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them."; +"lng_stealth_mode_past_title" = "Hide Recent Views"; +"lng_stealth_mode_past_about" = "Hide my views in the last 5 minutes."; +"lng_stealth_mode_next_title" = "Hide Next Views"; +"lng_stealth_mode_next_about" = "Hide my views in the next 25 minutes."; +"lng_stealth_mode_unlock" = "Unlock Stealth Mode"; +"lng_stealth_mode_enable" = "Enable Stealth Mode"; +"lng_stealth_mode_cooldown_in" = "Available in {left}"; +"lng_stealth_mode_cooldown_tip" = "Please wait until the **Stealth Mode** is ready to use again."; +"lng_stealth_mode_enabled_tip_title" = "Stealth Mode On"; +"lng_stealth_mode_enabled_tip" = "The creators of stories you viewed in the last **5 minutes** or will view in the next **25 minutes** won't see you in the viewers' lists."; +"lng_stealth_mode_countdown" = "Stealth Mode active – {left}"; +"lng_stealth_mode_already_title" = "You are in Stealth Mode"; +"lng_stealth_mode_already_about" = "The creators of stories you will view in the next **{left}** won't see you in the viewers' lists."; "lng_stories_link_invalid" = "This link is broken or has expired."; diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 0fabf4cf8..15007bceb 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.9.3.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 79fe54aee..f13710dae 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,8,11,0 - PRODUCTVERSION 4,8,11,0 + FILEVERSION 4,9,3,0 + PRODUCTVERSION 4,9,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.8.11.0" + VALUE "FileVersion", "4.9.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.8.11.0" + VALUE "ProductVersion", "4.9.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 83e9ddab5..197deffe1 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,8,11,0 - PRODUCTVERSION 4,8,11,0 + FILEVERSION 4,9,3,0 + PRODUCTVERSION 4,9,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "4.8.11.0" + VALUE "FileVersion", "4.9.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.8.11.0" + VALUE "ProductVersion", "4.9.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index 6d82a1d11..78e463c11 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -72,26 +72,9 @@ Authorizations::Entry ParseEntry(const MTPDauthorization &data) { appName, appVer.isEmpty() ? QString() : (' ' + appVer)); result.ip = qs(data.vip()); - if (!result.hash) { - result.active = tr::lng_status_online(tr::now); - } else { - const auto now = QDateTime::currentDateTime(); - const auto lastTime = base::unixtime::parse(result.activeTime); - const auto nowDate = now.date(); - const auto lastDate = lastTime.date(); - if (lastDate == nowDate) { - result.active = QLocale().toString( - lastTime.time(), - QLocale::ShortFormat); - } else if (lastDate.year() == nowDate.year() - && lastDate.weekNumber() == nowDate.weekNumber()) { - result.active = langDayOfWeek(lastDate); - } else { - result.active = QLocale().toString( - lastDate, - QLocale::ShortFormat); - } - } + result.active = result.hash + ? Authorizations::ActiveDateString(result.activeTime) + : tr::lng_status_online(tr::now); result.location = country; return result; @@ -129,16 +112,15 @@ void Authorizations::reload() { )).done([=](const MTPaccount_Authorizations &result) { _requestId = 0; _lastReceived = crl::now(); - result.match([&](const MTPDaccount_authorizations &auths) { - _ttlDays = auths.vauthorization_ttl_days().v; - _list = ( - auths.vauthorizations().v - ) | ranges::views::transform([](const MTPAuthorization &d) { - return ParseEntry(d.c_authorization()); - }) | ranges::to; - refreshCallsDisabledHereFromCloud(); - _listChanges.fire({}); - }); + const auto &data = result.data(); + _ttlDays = data.vauthorization_ttl_days().v; + _list = ranges::views::all( + data.vauthorizations().v + ) | ranges::views::transform([](const MTPAuthorization &auth) { + return ParseEntry(auth.data()); + }) | ranges::to; + refreshCallsDisabledHereFromCloud(); + _listChanges.fire({}); }).fail([=] { _requestId = 0; }).send(); @@ -190,19 +172,21 @@ Authorizations::List Authorizations::list() const { return _list; } -auto Authorizations::listChanges() const +auto Authorizations::listValue() const -> rpl::producer { return rpl::single( list() ) | rpl::then( - _listChanges.events() | rpl::map([=] { return list(); })); + _listChanges.events() | rpl::map([=] { return list(); }) + ); } -rpl::producer Authorizations::totalChanges() const { +rpl::producer Authorizations::totalValue() const { return rpl::single( total() ) | rpl::then( - _listChanges.events() | rpl::map([=] { return total(); })); + _listChanges.events() | rpl::map([=] { return total(); }) + ); } void Authorizations::updateTTL(int days) { @@ -254,6 +238,19 @@ rpl::producer Authorizations::callsDisabledHereChanges() const { return _callsDisabledHere.changes(); } +QString Authorizations::ActiveDateString(TimeId active) { + const auto now = QDateTime::currentDateTime(); + const auto lastTime = base::unixtime::parse(active); + const auto nowDate = now.date(); + const auto lastDate = lastTime.date(); + return (lastDate == nowDate) + ? QLocale().toString(lastTime.time(), QLocale::ShortFormat) + : (lastDate.year() == nowDate.year() + && lastDate.weekNumber() == nowDate.weekNumber()) + ? langDayOfWeek(lastDate) + : QLocale().toString(lastDate, QLocale::ShortFormat); +} + int Authorizations::total() const { return ranges::count_if( _list, diff --git a/Telegram/SourceFiles/api/api_authorizations.h b/Telegram/SourceFiles/api/api_authorizations.h index 96819edf1..5e2a41c9f 100644 --- a/Telegram/SourceFiles/api/api_authorizations.h +++ b/Telegram/SourceFiles/api/api_authorizations.h @@ -38,9 +38,9 @@ public: [[nodiscard]] crl::time lastReceivedTime(); [[nodiscard]] List list() const; - [[nodiscard]] rpl::producer listChanges() const; + [[nodiscard]] rpl::producer listValue() const; [[nodiscard]] int total() const; - [[nodiscard]] rpl::producer totalChanges() const; + [[nodiscard]] rpl::producer totalValue() const; void updateTTL(int days); [[nodiscard]] rpl::producer ttlDays() const; @@ -53,6 +53,8 @@ public: [[nodiscard]] rpl::producer callsDisabledHereValue() const; [[nodiscard]] rpl::producer callsDisabledHereChanges() const; + [[nodiscard]] static QString ActiveDateString(TimeId active); + private: void refreshCallsDisabledHereFromCloud(); diff --git a/Telegram/SourceFiles/api/api_blocked_peers.cpp b/Telegram/SourceFiles/api/api_blocked_peers.cpp index bf3a061e8..9dfc886b1 100644 --- a/Telegram/SourceFiles/api/api_blocked_peers.cpp +++ b/Telegram/SourceFiles/api/api_blocked_peers.cpp @@ -79,6 +79,7 @@ void BlockedPeers::block(not_null peer) { Data::PeerUpdate::Flag::IsBlocked); } else if (_blockRequests.find(peer) == end(_blockRequests)) { const auto requestId = _api.request(MTPcontacts_Block( + MTP_flags(0), peer->input )).done([=] { _blockRequests.erase(peer); @@ -111,6 +112,7 @@ void BlockedPeers::unblock( return; } const auto requestId = _api.request(MTPcontacts_Unblock( + MTP_flags(0), peer->input )).done([=] { _blockRequests.erase(peer); @@ -163,6 +165,7 @@ void BlockedPeers::request(int offset, Fn onDone) { return; } _requestId = _api.request(MTPcontacts_GetBlocked( + MTP_flags(0), MTP_int(offset), MTP_int(offset ? kBlockedPerPage : kBlockedFirstSlice) )).done([=](const MTPcontacts_Blocked &result) { diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index aeb7bd0a6..0ee748f2b 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -439,6 +439,7 @@ void ChatParticipants::requestAdmins(not_null channel) { MTP_int(channel->session().serverConfig().chatSizeMax), MTP_long(participantsHash) )).done([=](const MTPchannels_ChannelParticipants &result) { + channel->mgInfo->adminsLoaded = true; _adminsRequests.remove(channel); result.match([&](const MTPDchannels_channelParticipants &data) { channel->owner().processUsers(data.vusers()); @@ -448,6 +449,7 @@ void ChatParticipants::requestAdmins(not_null channel) { "channels.channelParticipantsNotModified received!")); }); }).fail([=] { + channel->mgInfo->adminsLoaded = true; _adminsRequests.remove(channel); }).send(); diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index 89e1d57da..219d335f1 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -84,19 +84,29 @@ void GlobalPrivacy::dismissArchiveAndMuteSuggestion() { u"AUTOARCHIVE_POPULAR"_q); } -void GlobalPrivacy::update(bool archiveAndMute) { +void GlobalPrivacy::updateArchiveAndMute(bool value) { + update(value, unarchiveOnNewMessageCurrent()); +} + +void GlobalPrivacy::updateUnarchiveOnNewMessage( + UnarchiveOnNewMessage value) { + update(archiveAndMuteCurrent(), value); +} + +void GlobalPrivacy::update( + bool archiveAndMute, + UnarchiveOnNewMessage unarchiveOnNewMessage) { using Flag = MTPDglobalPrivacySettings::Flag; - const auto unarchive = unarchiveOnNewMessageCurrent(); _api.request(_requestId).cancel(); const auto flags = Flag() | (archiveAndMute ? Flag::f_archive_and_mute_new_noncontact_peers : Flag()) - | (unarchive == UnarchiveOnNewMessage::AnyUnmuted + | (unarchiveOnNewMessage == UnarchiveOnNewMessage::None ? Flag::f_keep_archived_unmuted : Flag()) - | (unarchive != UnarchiveOnNewMessage::None + | (unarchiveOnNewMessage != UnarchiveOnNewMessage::AnyUnmuted ? Flag::f_keep_archived_folders : Flag()); _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( @@ -108,16 +118,17 @@ void GlobalPrivacy::update(bool archiveAndMute) { _requestId = 0; }).send(); _archiveAndMute = archiveAndMute; + _unarchiveOnNewMessage = unarchiveOnNewMessage; } void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) { data.match([&](const MTPDglobalPrivacySettings &data) { _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers(); _unarchiveOnNewMessage = data.is_keep_archived_unmuted() - ? UnarchiveOnNewMessage::AnyUnmuted + ? UnarchiveOnNewMessage::None : data.is_keep_archived_folders() ? UnarchiveOnNewMessage::NotInFoldersUnmuted - : UnarchiveOnNewMessage::None; + : UnarchiveOnNewMessage::AnyUnmuted; }); } diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index f569951ca..9e4b8e121 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -28,7 +28,8 @@ public: explicit GlobalPrivacy(not_null api); void reload(Fn callback = nullptr); - void update(bool archiveAndMute); + void updateArchiveAndMute(bool value); + void updateUnarchiveOnNewMessage(UnarchiveOnNewMessage value); [[nodiscard]] bool archiveAndMuteCurrent() const; [[nodiscard]] rpl::producer archiveAndMute() const; @@ -43,6 +44,10 @@ public: private: void apply(const MTPGlobalPrivacySettings &data); + void update( + bool archiveAndMute, + UnarchiveOnNewMessage unarchiveOnNewMessage); + const not_null _session; MTP::Sender _api; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 31462da39..58f5e08f4 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2017,7 +2017,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updatePeerBlocked: { const auto &d = update.c_updatePeerBlocked(); if (const auto peer = session().data().peerLoaded(peerFromMTP(d.vpeer_id()))) { - peer->setIsBlocked(mtpIsTrue(d.vblocked())); + peer->setIsBlocked(d.is_blocked()); } } break; @@ -2558,6 +2558,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->data().stories().apply(update.c_updateReadStories()); } break; + case mtpc_updateStoriesStealthMode: { + const auto &data = update.c_updateStoriesStealthMode(); + _session->data().stories().apply(data.vstealth_mode()); + } break; + } } diff --git a/Telegram/SourceFiles/api/api_websites.cpp b/Telegram/SourceFiles/api/api_websites.cpp new file mode 100644 index 000000000..855056675 --- /dev/null +++ b/Telegram/SourceFiles/api/api_websites.cpp @@ -0,0 +1,138 @@ +/* +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_websites.h" + +#include "api/api_authorizations.h" +#include "api/api_blocked_peers.h" +#include "apiwrap.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" + +namespace Api { +namespace { + +constexpr auto TestApiId = 17349; +constexpr auto SnapApiId = 611335; +constexpr auto DesktopApiId = 2040; + +Websites::Entry ParseEntry( + not_null owner, + const MTPDwebAuthorization &data) { + auto result = Websites::Entry{ + .hash = data.vhash().v, + .bot = owner->user(data.vbot_id()), + .platform = qs(data.vplatform()), + .domain = qs(data.vdomain()), + .browser = qs(data.vbrowser()), + .ip = qs(data.vip()), + .location = qs(data.vregion()), + }; + result.activeTime = data.vdate_active().v + ? data.vdate_active().v + : data.vdate_created().v; + result.active = Authorizations::ActiveDateString(result.activeTime); + return result; +} + +} // namespace + +Websites::Websites(not_null api) +: _session(&api->session()) +, _api(&api->instance()) { +} + +void Websites::reload() { + if (_requestId) { + return; + } + + _requestId = _api.request(MTPaccount_GetWebAuthorizations( + )).done([=](const MTPaccount_WebAuthorizations &result) { + _requestId = 0; + _lastReceived = crl::now(); + const auto owner = &_session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + _list = ranges::views::all( + data.vauthorizations().v + ) | ranges::views::transform([&](const MTPwebAuthorization &auth) { + return ParseEntry(owner, auth.data()); + }) | ranges::to; + _listChanges.fire({}); + }).fail([=] { + _requestId = 0; + }).send(); +} + +void Websites::cancelCurrentRequest() { + _api.request(base::take(_requestId)).cancel(); +} + +void Websites::requestTerminate( + Fn &&done, + Fn &&fail, + std::optional hash, + UserData *botToBlock) { + const auto send = [&](auto request) { + _api.request( + std::move(request) + ).done([=, done = std::move(done)](const MTPBool &result) { + done(result); + if (hash) { + _list.erase( + ranges::remove(_list, *hash, &Entry::hash), + end(_list)); + } else { + _list.clear(); + } + _listChanges.fire({}); + }).fail( + std::move(fail) + ).send(); + }; + if (hash) { + send(MTPaccount_ResetWebAuthorization(MTP_long(*hash))); + if (botToBlock) { + botToBlock->session().api().blockedPeers().block(botToBlock); + } + } else { + send(MTPaccount_ResetWebAuthorizations()); + } +} + +Websites::List Websites::list() const { + return _list; +} + +auto Websites::listValue() const +-> rpl::producer { + return rpl::single( + list() + ) | rpl::then( + _listChanges.events() | rpl::map([=] { return list(); }) + ); +} + +rpl::producer Websites::totalValue() const { + return rpl::single( + total() + ) | rpl::then( + _listChanges.events() | rpl::map([=] { return total(); }) + ); +} + +int Websites::total() const { + return _list.size(); +} + +crl::time Websites::lastReceivedTime() { + return _lastReceived; +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_websites.h b/Telegram/SourceFiles/api/api_websites.h new file mode 100644 index 000000000..1551ae4d4 --- /dev/null +++ b/Telegram/SourceFiles/api/api_websites.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 Websites final { +public: + explicit Websites(not_null api); + + struct Entry { + uint64 hash = 0; + + not_null bot; + TimeId activeTime = 0; + QString active, platform, domain, browser, ip, location; + }; + using List = std::vector; + + void reload(); + void cancelCurrentRequest(); + void requestTerminate( + Fn &&done, + Fn &&fail, + std::optional hash = std::nullopt, + UserData *botToBlock = nullptr); + + [[nodiscard]] crl::time lastReceivedTime(); + + [[nodiscard]] List list() const; + [[nodiscard]] rpl::producer listValue() const; + [[nodiscard]] int total() const; + [[nodiscard]] rpl::producer totalValue() const; + +private: + not_null _session; + + MTP::Sender _api; + mtpRequestId _requestId = 0; + + List _list; + rpl::event_stream<> _listChanges; + + crl::time _lastReceived = 0; + rpl::lifetime _lifetime; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 5772b06c5..fec04ea1d 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_transcribes.h" #include "api/api_premium.h" #include "api/api_user_names.h" +#include "api/api_websites.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -178,7 +179,8 @@ ApiWrap::ApiWrap(not_null session) , _ringtones(std::make_unique(this)) , _transcribes(std::make_unique(this)) , _premium(std::make_unique(this)) -, _usernames(std::make_unique(this)) { +, _usernames(std::make_unique(this)) +, _websites(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. @@ -4330,3 +4332,7 @@ Api::Premium &ApiWrap::premium() { Api::Usernames &ApiWrap::usernames() { return *_usernames; } + +Api::Websites &ApiWrap::websites() { + return *_websites; +} diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 5815fd3c0..954500f76 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -80,6 +80,7 @@ class Ringtones; class Transcribes; class Premium; class Usernames; +class Websites; namespace details { @@ -383,6 +384,7 @@ public: [[nodiscard]] Api::Transcribes &transcribes(); [[nodiscard]] Api::Premium &premium(); [[nodiscard]] Api::Usernames &usernames(); + [[nodiscard]] Api::Websites &websites(); void updatePrivacyLastSeens(); @@ -693,6 +695,7 @@ private: const std::unique_ptr _transcribes; const std::unique_ptr _premium; const std::unique_ptr _usernames; + const std::unique_ptr _websites; mtpRequestId _wallPaperRequestId = 0; QString _wallPaperSlug; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 00862eada..b952e5a6e 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -53,12 +53,12 @@ boxPhotoCaptionSkip: 8px; defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }}; defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }}; defaultUserpicButton: UserpicButton { - size: size(76px, 76px); - photoSize: 76px; + size: size(72px, 72px); + photoSize: 72px; photoPosition: point(-1px, -1px); changeButton: defaultActiveButton; changeIcon: defaultChangeUserpicIcon; - changeIconPosition: point(23px, 25px); + changeIconPosition: point(21px, 23px); duration: 500; uploadHeight: 24px; uploadBg: msgDateImgBgOver; diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 416b30ff1..181dccb1e 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -70,8 +70,6 @@ void ChangeFilterById( MTP_int(filter.id()), filter.tl() )).done([=, chat = history->peer->name(), name = filter.title()] { - // Since only the primary window has dialogs list, - // We can safely show toast there. const auto account = &history->session().account(); if (const auto controller = Core::App().windowFor(account)) { controller->showToast((add @@ -120,17 +118,19 @@ bool ChooseFilterValidator::canRemove(FilterId filterId) const { } ChooseFilterValidator::LimitData ChooseFilterValidator::limitReached( - FilterId filterId) const { + FilterId filterId, + bool always) 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(filterId); + const auto &chatsList = always ? i->always() : i->never(); return { .reached = (i != end(list)) - && !ranges::contains(i->always(), _history) - && (i->always().size() >= limit), - .count = int(i->always().size()), + && !ranges::contains(chatsList, _history) + && (chatsList.size() >= limit), + .count = int(chatsList.size()), }; } @@ -156,18 +156,21 @@ void FillChooseFilterMenu( 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 (const auto r = validator.limitReached(id); r.reached) { + const auto toAdd = !filter.contains(history); + const auto r = validator.limitReached(id, toAdd); + if (r.reached) { controller->show(Box( FilterChatsLimitBox, &controller->session(), r.count, - true)); - } else if (validator.canAdd()) { - validator.add(id); + toAdd)); + return; + } else if (toAdd ? validator.canAdd() : validator.canRemove(id)) { + if (toAdd) { + validator.add(id); + } else { + validator.remove(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 8e32b267c..e6c5ad335 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -27,7 +27,9 @@ public: [[nodiscard]] bool canAdd() const; [[nodiscard]] bool canRemove(FilterId filterId) const; - [[nodiscard]] LimitData limitReached(FilterId filterId) const; + [[nodiscard]] LimitData limitReached( + FilterId filterId, + bool always) const; void add(FilterId filterId) const; void remove(FilterId filterId) const; diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index c9779b1f1..90a989ce7 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -517,7 +517,8 @@ void EditCaptionBox::setInitialText() { _field->setTextCursor(cursor); _checkChangedTimer.setCallback([=] { - if (_field->getTextWithAppliedMarkdown() == _initialText) { + if (_field->getTextWithAppliedMarkdown() == _initialText + && _preparedList.files.empty()) { setCloseByOutsideClick(true); } }); @@ -738,6 +739,7 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { const auto wasSpoiler = hasSpoiler(); _preparedList = std::move(list); _preparedList.files.front().spoiler = wasSpoiler; + setCloseByOutsideClick(false); rebuildPreview(); return true; } diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 51be4cd11..e5c601881 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -291,24 +291,17 @@ void EditPrivacyBox::setupContent() { return Settings::ExceptionUsersCount(exceptions(exception)); })); auto text = _controller->exceptionButtonTextKey(exception); - const auto always = (exception == Exception::Always); const auto button = content->add( object_ptr>( content, CreateButton( content, rpl::duplicate(text), - st::settingsButton, - { - (always - ? &st::settingsIconPlus - : &st::settingsIconMinus), - always ? kIconGreen : kIconRed, - }))); + st::settingsButtonNoIcon))); CreateRightLabel( button->entity(), std::move(label), - st::settingsButton, + st::settingsButtonNoIcon, std::move(text)); button->toggleOn(rpl::duplicate( optionValue @@ -384,9 +377,9 @@ void EditPrivacyBox::setupContent() { }); addButton(tr::lng_cancel(), [this] { closeBox(); }); - const auto linkHeight = st::settingsButton.padding.top() - + st::settingsButton.height - + st::settingsButton.padding.bottom(); + const auto linkHeight = st::settingsButtonNoIcon.padding.top() + + st::settingsButtonNoIcon.height + + st::settingsButtonNoIcon.padding.bottom(); widthValue( ) | rpl::start_with_next([=](int width) { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 724dd7c47..88bbdca03 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -666,7 +666,7 @@ void EditFilterBox( content, tr::lng_filters_add_chats(), st::settingsButtonActive, - { &st::settingsIconAdd, 0, IconType::Round, &st::windowBgActive }); + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); const auto include = SetupChatsPreview( content, @@ -693,7 +693,7 @@ void EditFilterBox( excludeInner, tr::lng_filters_remove_chats(), st::settingsButtonActive, - { &st::settingsIconRemove, 0, IconType::Round, &st::windowBgActive }); + { &st::settingsIconRemove, IconType::Round, &st::windowBgActive }); const auto exclude = SetupChatsPreview( excludeInner, @@ -746,13 +746,13 @@ void EditFilterBox( state->hasLinks.value() | rpl::map(!rpl::mappers::_1), tr::lng_filters_link_create(), st::settingsButtonActive, - { &st::settingsFolderShareIcon, 0, IconType::Simple }); + { &st::settingsFolderShareIcon, IconType::Simple }); const auto addLink = AddToggledButton( content, state->hasLinks.value(), tr::lng_group_invite_add(), st::settingsButtonActive, - { &st::settingsIconAdd, 0, IconType::Round, &st::windowBgActive }); + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); SetupFilterLinks( content, diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index 9727f6c61..d5dbc6789 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -354,6 +354,7 @@ struct IconSelector { &owner->reactions(), std::move(args), [=] { state->animation->repaint(); }, + [] { return st::windowFg->c; }, Data::CustomEmojiSizeTag::Large); } state->iconId = id; diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index 9e0bfa2d9..48bc383c9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/wrap/vertical_layout.h" -#include "ui/text/text_utilities.h" // Ui::Text::ToUpper +#include "ui/text/text_utilities.h" // Ui::Text::RichLangValue #include "boxes/peer_list_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/add_contact_box.h" @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "window/window_session_controller.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_boxes.h" #include "styles/style_info.h" #include "styles/style_settings.h" @@ -271,7 +272,7 @@ void Controller::choose(not_null chat) { above, tr::lng_manage_discussion_group_create(), st::infoCreateLinkedChatButton, - { &st::settingsIconChat, Settings::kIconLightBlue } + { &st::menuIconGroupCreate } )->addClickHandler([=, parent = above.data()] { const auto guarded = crl::guard(parent, callback); navigation->uiShow()->showBox(Box( @@ -291,7 +292,7 @@ void Controller::choose(not_null chat) { ? tr::lng_manage_discussion_group_unlink : tr::lng_manage_linked_channel_unlink)(), st::infoUnlinkChatButton, - { &st::settingsIconMinus, Settings::kIconRed } + { &st::menuIconRemove } )->addClickHandler([=] { callback(nullptr); }); Settings::AddSkip(below); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp index 5ef44326a..ed0a363a2 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_members_visible.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "lang/lang_keys.h" #include "styles/style_info.h" +#include "styles/style_menu_icons.h" namespace { @@ -52,8 +53,8 @@ namespace { tr::lng_profile_hide_participants(), rpl::single(QString()), [] {}, - st::manageGroupTopicsButton, - { &st::infoRoundedIconHideMembers, Settings::kIconDarkBlue } + st::manageGroupNoIconButton, + {} ))->toggleOn(rpl::single( (megagroup->flags() & ChannelDataFlag::ParticipantsHidden) != 0 ) | rpl::then(state->toggled.events())); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index e1d09efec..425e21c75 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -1505,6 +1505,11 @@ void ParticipantsBoxController::loadMoreRows() { LOG(("API Error: " "channels.channelParticipantsNotModified received!")); }); + if (_offset > 0 && _role == Role::Admins && channel->isMegagroup()) { + if (channel->mgInfo->admins.empty() && channel->mgInfo->adminsLoaded) { + channel->mgInfo->adminsLoaded = false; + } + } if (!firstLoad && !added) { _allLoaded = true; } @@ -1679,7 +1684,7 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( result->addAction( tr::lng_context_restrict_user(tr::now), crl::guard(this, [=] { showRestricted(user); }), - &st::menuIconRestrict); + &st::menuIconPermissions); } } if (user && _additional.canRemoveParticipant(participant)) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index febf08ed6..01251e1f8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_icon.h" #include "api/api_invite_links.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_boxes.h" #include "styles/style_info.h" #include "styles/style_settings.h" @@ -307,11 +308,9 @@ private: void fillPendingRequestsButton(); void fillBotUsernamesButton(); -#if 0 // Enable after design improvements. void fillBotEditIntroButton(); void fillBotEditCommandsButton(); void fillBotEditSettingsButton(); -#endif void submitTitle(); void submitDescription(); @@ -348,9 +347,7 @@ private: void continueSave(); void cancelSave(); -#if 0 // Enable after design improvements. void toggleBotManager(const QString &command); -#endif void togglePreHistoryHidden( not_null channel, @@ -605,7 +602,7 @@ object_ptr Controller::createStickersEdit() { controller->show( Box(controller->uiShow(), channel)); }, - { &st::settingsIconStickers, Settings::kIconLightOrange }); + { &st::menuIconStickers }); Settings::AddSkip(container, bottomSkip); @@ -770,9 +767,6 @@ void Controller::fillPrivacyTypeButton() { && _peer->asChannel()->requestToJoin()), }; const auto isGroup = (_peer->isChat() || _peer->isMegagroup()); - const auto icon = isGroup - ? &st::settingsIconGroup - : &st::settingsIconChannel; AddButtonWithText( _controls.buttonsLayout, (hasLocation @@ -798,7 +792,7 @@ void Controller::fillPrivacyTypeButton() { : tr::lng_manage_private_peer_title)(); }) | rpl::flatten_latest(), [=] { showEditPeerTypeBox(); }, - { icon, Settings::kIconLightBlue }); + { &st::menuIconCustomize }); _privacyTypeUpdates.fire_copy(_typeDataSavedValue->privacy); } @@ -839,7 +833,7 @@ void Controller::fillLinkedChatButton() { std::move(text), std::move(label), [=] { showEditLinkedChatBox(); }, - { &st::settingsIconChat, Settings::kIconGreen }); + { isGroup ? &st::menuIconChannel : &st::menuIconGroups }); _linkedChatUpdates.fire_copy(*_linkedChatSavedValue); } // @@ -867,7 +861,7 @@ void Controller::fillForumButton() { rpl::single(QString()), [] {}, st::manageGroupTopicsButton, - { &st::settingsIconTopics, Settings::kIconPurple })); + { &st::menuIconTopics })); const auto unlocks = std::make_shared>(); button->toggleOn( rpl::single(_peer->isForum()) | rpl::then(unlocks->events()) @@ -924,7 +918,7 @@ void Controller::fillSignaturesButton() { tr::lng_edit_sign_messages(), rpl::single(QString()), [] {}, - { &st::infoRoundedIconSignature, Settings::kIconLightBlue } + { &st::menuIconSigned } )->toggleOn(rpl::single(channel->addsSignature()) )->toggledValue( ) | rpl::start_with_next([=](bool toggled) { @@ -986,7 +980,7 @@ void Controller::fillHistoryVisibilityButton() { : tr::lng_manage_history_visibility_hidden)(); }) | rpl::flatten_latest(), buttonCallback, - { &st::settingsIconChat, Settings::kIconGreen }); + { &st::menuIconChatBubble }); updateHistoryVisibility->fire_copy(*_historyVisibilitySavedValue); @@ -1001,11 +995,9 @@ void Controller::fillManageSection() { AddSkip(container, 0); fillBotUsernamesButton(); -#if 0 // Enable after design improvements. fillBotEditIntroButton(); fillBotEditCommandsButton(); fillBotEditSettingsButton(); -#endif Settings::AddSkip( container, st::editPeerTopButtonsLayoutSkipCustomBottom); @@ -1126,10 +1118,7 @@ void Controller::fillManageSection() { //|| canEditInviteLinks || canViewOrEditLinkedChat || canEditType) { - AddSkip( - _controls.buttonsLayout, - st::editPeerTopButtonsLayoutSkip, - st::editPeerTopButtonsLayoutSkipCustomBottom); + AddSkip(_controls.buttonsLayout); } if (canEditReactions()) { @@ -1174,7 +1163,7 @@ void Controller::fillManageSection() { Data::PeerAllowedReactions(_peer), done)); }, - { &st::infoRoundedIconReactions, Settings::kIconRed }); + { &st::menuIconGroupReactions }); } if (canEditPermissions) { AddButtonWithCount( @@ -1193,7 +1182,7 @@ void Controller::fillManageSection() { }); }) | rpl::flatten_latest(), [=] { ShowEditPermissions(_navigation, _peer); }, - { &st::settingsIconKey, Settings::kIconGreen }); + { &st::menuIconPermissions }); } if (canEditInviteLinks) { auto count = Info::Profile::MigratedOrMeValue( @@ -1227,7 +1216,7 @@ void Controller::fillManageSection() { 0, 0)); }, - { &st::infoRoundedIconInviteLinks, Settings::kIconLightOrange }); + { &st::menuIconLinks }); wrap->toggle(true, anim::type::instant); } if (canViewAdmins) { @@ -1246,7 +1235,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Admins); }, - { &st::infoRoundedIconAdministrators, Settings::kIconLightBlue }); + { &st::menuIconAdmin }); } if (canViewMembers) { AddButtonWithCount( @@ -1266,7 +1255,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Members); }, - { &st::settingsIconGroup, Settings::kIconDarkBlue }); + { &st::menuIconGroups }); } fillPendingRequestsButton(); @@ -1283,7 +1272,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Kicked); }, - { &st::settingsIconMinus, Settings::kIconRed }); + { &st::menuIconRemove }); } if (hasRecentActions) { auto callback = [=] { @@ -1295,12 +1284,11 @@ void Controller::fillManageSection() { tr::lng_manage_peer_recent_actions(), rpl::single(QString()), //Empty count. std::move(callback), - { &st::infoRoundedIconRecentActions, Settings::kIconPurple }); + { &st::menuIconGroupLog }); } if (canEditStickers || canDeleteChannel) { - AddSkip(_controls.buttonsLayout, - st::editPeerTopButtonsLayoutSkipCustomTop); + AddSkip(_controls.buttonsLayout); } if (canEditStickers) { @@ -1316,6 +1304,10 @@ void Controller::fillManageSection() { [=]{ deleteWithConfirmation(); } ); } + + if (canEditStickers || canDeleteChannel) { + AddSkip(_controls.buttonsLayout); + } } void Controller::fillPendingRequestsButton() { @@ -1337,7 +1329,7 @@ void Controller::fillPendingRequestsButton() { : tr::lng_manage_peer_requests_channel()), rpl::duplicate(pendingRequestsCount) | ToPositiveNumberString(), [=] { RequestsBoxController::Start(_navigation, _peer); }, - { &st::infoRoundedIconRequests, Settings::kIconRed }); + { &st::menuIconInvite }); std::move( pendingRequestsCount ) | rpl::start_with_next([=](int count) { @@ -1396,10 +1388,9 @@ void Controller::fillBotUsernamesButton() { [=] { _navigation->uiShow()->showBox(Box(UsernamesBox, user)); }, - { &st::infoRoundedIconInviteLinks, Settings::kIconLightOrange }); + { &st::menuIconLinks }); } -#if 0 // Enable after design improvements. void Controller::fillBotEditIntroButton() { Expects(_isBot); @@ -1409,7 +1400,7 @@ void Controller::fillBotEditIntroButton() { tr::lng_manage_peer_bot_edit_intro(), rpl::never(), [=] { toggleBotManager(u"%1-intro"_q.arg(user->username())); }, - { &st::settingsIconChat, Settings::kIconLightBlue }); + { &st::menuIconEdit }); } void Controller::fillBotEditCommandsButton() { @@ -1421,7 +1412,7 @@ void Controller::fillBotEditCommandsButton() { tr::lng_manage_peer_bot_edit_commands(), rpl::never(), [=] { toggleBotManager(u"%1-commands"_q.arg(user->username())); }, - { &st::settingsIconChat, Settings::kIconLightBlue }); + { &st::menuIconBotCommands }); } void Controller::fillBotEditSettingsButton() { @@ -1433,9 +1424,8 @@ void Controller::fillBotEditSettingsButton() { tr::lng_manage_peer_bot_edit_settings(), rpl::never(), [=] { toggleBotManager(user->username()); }, - { &st::settingsIconChat, Settings::kIconLightBlue }); + { &st::menuIconSettings }); } -#endif void Controller::submitTitle() { Expects(_controls.title != nullptr); @@ -1910,7 +1900,6 @@ void Controller::saveHistoryVisibility() { [=] { cancelSave(); }); } -#if 0 // Enable after design improvements. void Controller::toggleBotManager(const QString &command) { const auto controller = _navigation->parentController(); _api.request(MTPcontacts_ResolveUsername( @@ -1926,7 +1915,6 @@ void Controller::toggleBotManager(const QString &command) { } }).send(); } -#endif void Controller::togglePreHistoryHidden( not_null channel, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 420d5dc52..4c33b38fc 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_info.h" +#include "styles/style_menu_icons.h" #include "styles/style_window.h" #include "styles/style_settings.h" @@ -402,7 +403,7 @@ not_null AddInnerToggle( (s.height() - label->height()) / 2); arrow->moveToLeft( std::min( - labelLeft + label->naturalWidth(), + labelLeft + label->textMaxWidth(), labelRight - arrow->width()), (s.height() - arrow->height()) / 2); }, button->lifetime()); @@ -818,7 +819,7 @@ void AddSuggestGigagroup( rpl::single(QString()), std::move(callback), st::manageGroupTopicsButton, - { &st::settingsIconAskQuestion, Settings::kIconGreen })); + { &st::menuIconChatDiscuss })); container->add( object_ptr( @@ -854,7 +855,7 @@ void AddBannedButtons( ParticipantsBoxController::Role::Restricted); }, st::manageGroupTopicsButton, - { &st::settingsIconKey, Settings::kIconLightOrange })); + { &st::menuIconPermissions })); if (channel) { container->add(EditPeerInfoBox::CreateButton( container, @@ -868,7 +869,7 @@ void AddBannedButtons( ParticipantsBoxController::Role::Kicked); }, st::manageGroupTopicsButton, - { &st::settingsIconMinus, Settings::kIconRed })); + { &st::menuIconRemove })); } } diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 20581a726..6505e31e6 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/padding_wrap.h" #include "ui/boxes/confirm_box.h" #include "ui/painter.h" +#include "settings/settings_common.h" #include "settings/settings_premium.h" #include "lottie/lottie_single_player.h" #include "history/view/media/history_view_sticker.h" @@ -92,6 +93,10 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] rpl::producer SectionTitle(PremiumPreview section) { switch (section) { + case PremiumPreview::Stories: + return tr::lng_premium_summary_subtitle_stories(); + case PremiumPreview::DoubleLimits: + return tr::lng_premium_summary_subtitle_double_limits(); case PremiumPreview::MoreUpload: return tr::lng_premium_summary_subtitle_more_upload(); case PremiumPreview::FasterDownload: @@ -122,6 +127,10 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] rpl::producer SectionAbout(PremiumPreview section) { switch (section) { + case PremiumPreview::Stories: + return tr::lng_premium_summary_about_stories(); + case PremiumPreview::DoubleLimits: + return tr::lng_premium_summary_about_double_limits(); case PremiumPreview::MoreUpload: return tr::lng_premium_summary_about_more_upload(); case PremiumPreview::FasterDownload: @@ -1117,6 +1126,56 @@ void Show(std::shared_ptr show, QImage back) { } } +void DecorateListPromoBox( + not_null box, + std::shared_ptr show, + const Descriptor &descriptor) { + const auto session = &show->session(); + + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + Data::AmPremiumValue( + session + ) | rpl::skip(1) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + + if (const auto &hidden = descriptor.hiddenCallback) { + box->boxClosing() | rpl::start_with_next(hidden, box->lifetime()); + } + + if (session->premium()) { + box->addButton(tr::lng_close(), [=] { + box->closeBox(); + }); + } else { + const auto button = Settings::CreateSubscribeButton({ + .parent = box, + .computeRef = [] { return u"double_limits"_q; }, + .show = show, + }); + + box->setShowFinishedCallback([=] { + button->startGlareAnimation(); + }); + + box->setStyle(st::premiumPreviewDoubledLimitsBox); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = + st::premiumPreviewDoubledLimitsBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + box->addButton( + object_ptr::fromRaw(button)); + } +} + void Show( std::shared_ptr show, Descriptor &&descriptor) { @@ -1128,6 +1187,18 @@ void Show( descriptor.shownCallback(raw); } return; + } else if (descriptor.section == PremiumPreview::DoubleLimits) { + show->showBox(Box([=](not_null box) { + DoubledLimitsPreviewBox(box, &show->session()); + DecorateListPromoBox(box, show, descriptor); + })); + return; + } else if (descriptor.section == PremiumPreview::Stories) { + show->showBox(Box([=](not_null box) { + UpgradedStoriesPreviewBox(box, &show->session()); + DecorateListPromoBox(box, show, descriptor); + })); + return; } auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { @@ -1240,11 +1311,13 @@ void PremiumUnavailableBox(not_null box) { void DoubledLimitsPreviewBox( not_null box, not_null session) { + box->setTitle(tr::lng_premium_summary_subtitle_double_limits()); + const auto limits = Data::PremiumLimits(session); auto entries = std::vector(); { const auto premium = limits.channelsPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_channels(), tr::lng_premium_double_limits_about_channels( lt_count, @@ -1256,7 +1329,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.dialogsPinnedPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_pins(), tr::lng_premium_double_limits_about_pins( lt_count, @@ -1268,7 +1341,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.channelsPublicPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_links(), tr::lng_premium_double_limits_about_links( lt_count, @@ -1280,7 +1353,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.gifsPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_gifs(), tr::lng_premium_double_limits_about_gifs( lt_count, @@ -1292,7 +1365,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.stickersFavedPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_stickers(), tr::lng_premium_double_limits_about_stickers( lt_count, @@ -1304,7 +1377,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.aboutLengthPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_bio(), tr::lng_premium_double_limits_about_bio( Ui::Text::RichLangValue), @@ -1314,7 +1387,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.captionLengthPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_captions(), tr::lng_premium_double_limits_about_captions( Ui::Text::RichLangValue), @@ -1324,7 +1397,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.dialogFiltersPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_folders(), tr::lng_premium_double_limits_about_folders( lt_count, @@ -1336,7 +1409,7 @@ void DoubledLimitsPreviewBox( } { const auto premium = limits.dialogFiltersChatsPremium(); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_folder_chats(), tr::lng_premium_double_limits_about_folder_chats( lt_count, @@ -1350,7 +1423,7 @@ void DoubledLimitsPreviewBox( const auto till = (nextMax >= Main::Domain::kPremiumMaxAccounts) ? QString::number(Main::Domain::kPremiumMaxAccounts) : (QString::number(nextMax) + QChar('+')); - entries.push_back(Ui::Premium::ListEntry{ + entries.push_back({ tr::lng_premium_double_limits_subtitle_accounts(), tr::lng_premium_double_limits_about_accounts( lt_count, @@ -1366,6 +1439,60 @@ void DoubledLimitsPreviewBox( std::move(entries)); } +void UpgradedStoriesPreviewBox( + not_null box, + not_null session) { + using namespace Ui::Text; + + box->setTitle(tr::lng_premium_summary_subtitle_stories()); + + auto entries = std::vector(); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_order(), + .about = tr::lng_premium_stories_about_order(WithEntities), + .icon = &st::settingsStoriesIconOrder, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_stealth(), + .about = tr::lng_premium_stories_about_stealth(WithEntities), + .icon = &st::settingsStoriesIconStealth, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_views(), + .about = tr::lng_premium_stories_about_views(WithEntities), + .icon = &st::settingsStoriesIconViews, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_expiration(), + .about = tr::lng_premium_stories_about_expiration(WithEntities), + .icon = &st::settingsStoriesIconExpiration, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_download(), + .about = tr::lng_premium_stories_about_download(WithEntities), + .icon = &st::settingsStoriesIconDownload, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_caption(), + .about = tr::lng_premium_stories_about_caption(WithEntities), + .icon = &st::settingsStoriesIconCaption, + }); + entries.push_back({ + .title = tr::lng_premium_stories_subtitle_links(), + .about = tr::lng_premium_stories_about_links(WithEntities), + .icon = &st::settingsStoriesIconLinks, + }); + + Ui::Premium::ShowListBox( + box, + st::defaultPremiumLimits, + std::move(entries)); + + Settings::AddDividerText( + box->verticalLayout(), + tr::lng_premium_stories_about_mobile()); +} + object_ptr CreateUnlockButton( QWidget *parent, rpl::producer text) { diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 51f9bcfbd..dbe2be446 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -41,7 +41,13 @@ void DoubledLimitsPreviewBox( not_null box, not_null session); +void UpgradedStoriesPreviewBox( + not_null box, + not_null session); + enum class PremiumPreview { + Stories, + DoubleLimits, MoreUpload, FasterDownload, VoiceToText, diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp index ef2d096ec..eb06f7066 100644 --- a/Telegram/SourceFiles/boxes/ringtones_box.cpp +++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp @@ -278,7 +278,6 @@ void RingtonesBox( st::ringtonesBoxButton, { &st::settingsIconAdd, - 0, Settings::IconType::Round, &st::windowBgActive }), diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 47cb78029..7313afd21 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -763,6 +763,8 @@ void SendFilesBox::pushBlock(int from, int till) { ) | rpl::filter([=] { return !_removingIndex; }) | rpl::start_with_next([=](int index) { + applyBlockChanges(); + _removingIndex = index; crl::on_main(this, [=] { const auto index = base::take(_removingIndex).value_or(-1); @@ -783,6 +785,8 @@ void SendFilesBox::pushBlock(int from, int till) { const auto show = uiShow(); block.itemReplaceRequest( ) | rpl::start_with_next([=](int index) { + applyBlockChanges(); + const auto replace = [=](Ui::PreparedList list) { if (list.files.empty()) { return; @@ -858,6 +862,8 @@ void SendFilesBox::pushBlock(int from, int till) { const auto openedOnce = widget->lifetime().make_state(false); block.itemModifyRequest( ) | rpl::start_with_next([=, show = _show](int index) { + applyBlockChanges(); + if (!(*openedOnce)) { show->session().settings().incrementPhotoEditorHintShown(); show->session().saveSettings(); diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index 55b5f499c..e30174be6 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -35,10 +35,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_settings.h" +#include "styles/style_menu_icons.h" namespace { -constexpr auto kSessionsShortPollTimeout = 60 * crl::time(1000); +constexpr auto kShortPollTimeout = 60 * crl::time(1000); constexpr auto kMaxDeviceModelLength = 32; using EntryData = Api::Authorizations::Entry; @@ -71,7 +72,6 @@ public: Row(not_null delegate, const EntryData &data); void update(const EntryData &data); - void updateName(const QString &name); [[nodiscard]] EntryData data() const; @@ -80,6 +80,14 @@ public: PaintRoundImageCallback generatePaintUserpicCallback( bool forceRound) override; + QSize rightActionSize() const override { + return elementGeometry(2, 0).size(); + } + QMargins rightActionMargins() const override { + const auto rect = elementGeometry(2, 0); + return QMargins(0, rect.y(), -(rect.x() + rect.width()), 0); + } + int elementsCount() const override; QRect elementGeometry(int element, int outerWidth) const override; bool elementDisabled(int element) const override; @@ -458,28 +466,27 @@ void SessionInfoBox( AddSkip(container, st::sessionSubtitleSkip); AddSubsectionTitle(container, tr::lng_sessions_info()); - const auto add = [&](rpl::producer label, QString value) { - if (value.isEmpty()) { - return; - } - container->add( - object_ptr( - container, - rpl::single(value), - st::boxLabel), - st::boxRowPadding + st::sessionValuePadding); - container->add( - object_ptr( - container, - std::move(label), - st::sessionValueLabel), - (st::boxRowPadding - + style::margins{ 0, 0, 0, st::sessionValueSkip })); - }; - add(tr::lng_sessions_application(), data.info); - add(tr::lng_sessions_system(), data.system); - add(tr::lng_sessions_ip(), data.ip); - add(tr::lng_sessions_location(), data.location); + AddSessionInfoRow( + container, + tr::lng_sessions_application(), + data.info, + st::menuIconDevices); + AddSessionInfoRow( + container, + tr::lng_sessions_system(), + data.system, + st::menuIconInfo); + AddSessionInfoRow( + container, + tr::lng_sessions_ip(), + data.ip, + st::menuIconIpAddress); + AddSessionInfoRow( + container, + tr::lng_sessions_location(), + data.location, + st::menuIconAddress); + AddSkip(container, st::sessionValueSkip); if (!data.location.isEmpty()) { AddDividerText(container, tr::lng_sessions_location_about()); @@ -517,12 +524,6 @@ void Row::update(const EntryData &data) { _delegate->rowUpdateRow(this); } -void Row::updateName(const QString &name) { - _data.name = name; - refreshName(st::sessionListItem); - _delegate->rowUpdateRow(this); -} - EntryData Row::data() const { return _data; } @@ -615,8 +616,6 @@ void Row::elementsPaint( outerWidth); } -} // namespace - class SessionsContent : public Ui::RpWidget { public: SessionsContent( @@ -683,8 +682,6 @@ public: style::margins margins = {}); private: - void subscribeToCustomDeviceModel(); - const not_null _session; rpl::event_stream _terminateRequests; @@ -760,7 +757,7 @@ void SessionsContent::setupContent() { _inner->setVisible(!value); }, lifetime()); - _authorizations->listChanges( + _authorizations->listValue( ) | rpl::start_with_next([=](const Api::Authorizations::List &list) { parse(list); }, lifetime()); @@ -791,7 +788,7 @@ void SessionsContent::parse(const Api::Authorizations::List &list) { _inner->showData(_data); - _shortPollTimer.callOnce(kSessionsShortPollTimeout); + _shortPollTimer.callOnce(kShortPollTimeout); } void SessionsContent::resizeEvent(QResizeEvent *e) { @@ -816,7 +813,7 @@ void SessionsContent::paintEvent(QPaintEvent *e) { } void SessionsContent::shortPollSessions() { - const auto left = kSessionsShortPollTimeout + const auto left = kShortPollTimeout - (crl::now() - _authorizations->lastReceivedTime()); if (left > 0) { parse(_authorizations->list()); @@ -1048,18 +1045,6 @@ Main::Session &SessionsContent::ListController::session() const { return *_session; } -void SessionsContent::ListController::subscribeToCustomDeviceModel() { - Core::App().settings().deviceModelChanges( - ) | rpl::start_with_next([=](const QString &model) { - for (auto i = 0; i != delegate()->peerListFullRowsCount(); ++i) { - const auto row = delegate()->peerListRowAt(i); - if (!row->id()) { - static_cast(row.get())->updateName(model); - } - } - }, lifetime()); -} - void SessionsContent::ListController::prepare() { } @@ -1148,27 +1133,7 @@ auto SessionsContent::ListController::Add( return controller; } -SessionsBox::SessionsBox( - QWidget*, - not_null controller) -: _controller(controller) { -} - -void SessionsBox::prepare() { - setTitle(tr::lng_sessions_other_header()); - - addButton(tr::lng_close(), [=] { closeBox(); }); - - const auto w = st::boxWideWidth; - - const auto content = setInnerWidget( - object_ptr(this, _controller), - st::sessionsScroll); - content->resize(w, st::noContactsHeight); - content->setupContent(); - - setDimensions(w, st::sessionsHeight); -} +} // namespace namespace Settings { @@ -1193,4 +1158,41 @@ void Sessions::setupContent(not_null controller) { Ui::ResizeFitChild(this, container); } +void AddSessionInfoRow( + not_null container, + rpl::producer label, + const QString &value, + const style::icon &icon) { + if (value.isEmpty()) { + return; + } + + const auto text = container->add( + object_ptr( + container, + rpl::single(value), + st::boxLabel), + st::boxRowPadding + st::sessionValuePadding); + const auto left = st::sessionValuePadding.left(); + container->add( + object_ptr( + container, + std::move(label), + st::sessionValueLabel), + (st::boxRowPadding + + style::margins{ left, 0, 0, st::sessionValueSkip })); + + const auto widget = Ui::CreateChild(container.get()); + widget->resize(icon.size()); + + text->topValue() | rpl::start_with_next([=](int top) { + widget->move(st::sessionValueIconPosition + QPoint(0, top)); + }, widget->lifetime()); + + widget->paintRequest() | rpl::start_with_next([=, &icon] { + auto p = QPainter(widget); + icon.paintInCenter(p, widget->rect()); + }, widget->lifetime()); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/boxes/sessions_box.h b/Telegram/SourceFiles/boxes/sessions_box.h index d735189a5..47d03de27 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.h +++ b/Telegram/SourceFiles/boxes/sessions_box.h @@ -7,12 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "boxes/abstract_box.h" #include "settings/settings_common.h" -namespace Main { -class Session; -} // namespace Main +namespace Ui { +class VerticalLayout; +} // namespace Ui namespace Settings { @@ -29,16 +28,10 @@ private: }; +void AddSessionInfoRow( + not_null container, + rpl::producer label, + const QString &value, + const style::icon &icon); + } // namespace Settings - -class SessionsBox : public Ui::BoxContent { -public: - SessionsBox(QWidget*, not_null controller); - -protected: - void prepare() override; - -private: - const not_null _controller; - -}; diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index f06f78cfd..17f0008e8 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -731,14 +731,14 @@ void TopBar::updateControlsGeometry() { width() - _mute->width() - _hangup->width(), height()); - auto fullWidth = _fullInfoLabel->naturalWidth(); + auto fullWidth = _fullInfoLabel->textMaxWidth(); auto showFull = (left + fullWidth + right <= width()); _fullInfoLabel->setVisible(showFull); _shortInfoLabel->setVisible(!showFull); auto setInfoLabelGeometry = [this, left, right](auto &&infoLabel) { auto minPadding = qMax(left, right); - auto infoWidth = infoLabel->naturalWidth(); + auto infoWidth = infoLabel->textMaxWidth(); auto infoLeft = (width() - infoWidth) / 2; if (infoLeft < minPadding) { infoLeft = left; diff --git a/Telegram/SourceFiles/calls/calls_video_incoming.cpp b/Telegram/SourceFiles/calls/calls_video_incoming.cpp index 916a058dd..9ea9fe41c 100644 --- a/Telegram/SourceFiles/calls/calls_video_incoming.cpp +++ b/Telegram/SourceFiles/calls/calls_video_incoming.cpp @@ -73,6 +73,7 @@ private: QSize _viewport; float _factor = 1.; + int _ifactor = 1; QVector2D _uniformViewport; std::optional _contentBuffer; @@ -189,9 +190,10 @@ void Panel::Incoming::RendererGL::paint( return; } - const auto factor = widget->devicePixelRatio(); + const auto factor = widget->devicePixelRatioF(); if (_factor != factor) { _factor = factor; + _ifactor = int(std::ceil(_factor)); _controlsShadowImage.invalidate(); } _viewport = widget->size(); @@ -375,9 +377,9 @@ void Panel::Incoming::RendererGL::validateShadowImage() { return; } const auto size = st::callTitleShadowLeft.size(); - const auto full = QSize(size.width(), 2 * size.height()) * int(_factor); + const auto full = QSize(size.width(), 2 * size.height()) * _ifactor; auto image = QImage(full, QImage::Format_ARGB32_Premultiplied); - image.setDevicePixelRatio(_factor); + image.setDevicePixelRatio(_ifactor); image.fill(Qt::transparent); { auto p = QPainter(&image); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index a240ce118..210eb0dce 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -146,6 +146,23 @@ Main::Session &Show::session() const { return panel->call()->peer()->session(); } +#ifdef Q_OS_WIN +void UnpinMaximized(not_null widget) { + SetWindowPos( + reinterpret_cast(widget->window()->windowHandle()->winId()), + HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + (SWP_NOMOVE + | SWP_NOSIZE + | SWP_NOOWNERZORDER + | SWP_FRAMECHANGED + | SWP_NOACTIVATE)); +} +#endif // Q_OS_WIN + } // namespace struct Panel::ControlsBackgroundNarrow { @@ -1257,7 +1274,12 @@ void Panel::createPinOnTop() { _pinOnTop->setVisible(!fullScreenOrMaximized); if (fullScreenOrMaximized) { +#ifdef Q_OS_WIN + UnpinMaximized(window()); + _unpinnedMaximized = true; +#else // Q_OS_WIN pin(false); +#endif // Q_OS_WIN _viewport->rp()->events( ) | rpl::filter([](not_null event) { @@ -1269,6 +1291,9 @@ void Panel::createPinOnTop() { _hideControlsTimer.callOnce(kHideControlsTimeout); } else { + if (_unpinnedMaximized) { + pin(false); + } _hideControlsTimerLifetime.destroy(); _hideControlsTimer.cancel(); refreshTitleGeometry(); @@ -2060,7 +2085,7 @@ void Panel::showNiceTooltip( (normal ? widget().get() : container), std::move(text), st::groupCallNiceTooltipLabel); - label->resizeToNaturalWidth(label->naturalWidth()); + label->resizeToWidth(label->textMaxWidth()); if (normal) { return label; } @@ -2522,8 +2547,8 @@ void Panel::refreshTitleGeometry() { fullRect.height()) : fullRect; const auto sep = st::groupCallTitleSeparator; - const auto best = _title->naturalWidth() + (_viewers - ? (_titleSeparator->width() + sep * 2 + _viewers->naturalWidth()) + const auto best = _title->textMaxWidth() + (_viewers + ? (_titleSeparator->width() + sep * 2 + _viewers->textMaxWidth()) : 0); const auto from = (widget()->width() - best) / 2; const auto shownTop = (mode() == PanelMode::Default) @@ -2541,8 +2566,8 @@ void Panel::refreshTitleGeometry() { const auto left = titleRect.x(); const auto notEnough = std::max(0, best - titleRect.width()); - const auto titleMaxWidth = _title->naturalWidth(); - const auto viewersMaxWidth = _viewers ? _viewers->naturalWidth() : 0; + const auto titleMaxWidth = _title->textMaxWidth(); + const auto viewersMaxWidth = _viewers ? _viewers->textMaxWidth() : 0; const auto viewersNotEnough = std::clamp( viewersMaxWidth - titleMaxWidth, 0, @@ -2551,9 +2576,9 @@ void Panel::refreshTitleGeometry() { (notEnough - std::abs(viewersMaxWidth - titleMaxWidth)) / 2, 0); _title->resizeToWidth( - _title->naturalWidth() - (notEnough - viewersNotEnough)); + _title->textMaxWidth() - (notEnough - viewersNotEnough)); if (_viewers) { - _viewers->resizeToWidth(_viewers->naturalWidth() - viewersNotEnough); + _viewers->resizeToWidth(_viewers->textMaxWidth() - viewersNotEnough); } const auto layout = [&](int position) { _title->moveToLeft(position, top); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 8fab14540..cf9352697 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -228,6 +228,7 @@ private: const std::unique_ptr _layerBg; rpl::variable _mode; rpl::variable _fullScreenOrMaximized = false; + bool _unpinnedMaximized = false; #ifndef Q_OS_MAC rpl::variable _controlsTop = 0; diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index e4a04f7c5..26a9cbed0 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -414,16 +414,20 @@ void Viewport::RendererGL::deinit( } void Viewport::RendererGL::setDefaultViewport(QOpenGLFunctions &f) { - const auto size = _viewport * _factor; - f.glViewport(0, 0, size.width(), size.height()); + f.glViewport( + 0, + 0, + _viewport.width() * _factor, + _viewport.height() * _factor); } void Viewport::RendererGL::paint( not_null widget, QOpenGLFunctions &f) { - const auto factor = widget->devicePixelRatio(); + const auto factor = widget->devicePixelRatioF(); if (_factor != factor) { _factor = factor; + _ifactor = int(std::ceil(_factor)); _buttons.invalidate(); } _viewport = widget->size(); @@ -773,7 +777,7 @@ void Viewport::RendererGL::paintTile( const auto program = _rgbaFrame ? &*_frameProgram.argb32 : &*_frameProgram.yuv420; - const auto uniformViewport = QSizeF(_viewport * _factor); + const auto uniformViewport = QSizeF(_viewport) * _factor; program->setUniformValue("viewport", uniformViewport); program->setUniformValue( @@ -1122,18 +1126,18 @@ void Viewport::RendererGL::ensureButtonsImage() { + backSize.height() + muteSize.height() + pausedSize.height())); - const auto imageSize = fullSize * _factor; + const auto imageSize = fullSize * _ifactor; auto image = _buttons.takeImage(); if (image.size() != imageSize) { image = QImage(imageSize, QImage::Format_ARGB32_Premultiplied); } image.fill(Qt::transparent); - image.setDevicePixelRatio(_factor); + image.setDevicePixelRatio(_ifactor); { auto p = Painter(&image); auto hq = PainterHighQualityEnabler(p); - _pinOn = QRect(QPoint(), pinOnSize * _factor); + _pinOn = QRect(QPoint(), pinOnSize * _ifactor); VideoTile::PaintPinButton( p, true, @@ -1145,8 +1149,8 @@ void Viewport::RendererGL::ensureButtonsImage() { const auto pinOffTop = pinOnSize.height(); _pinOff = QRect( - QPoint(0, pinOffTop) * _factor, - pinOffSize * _factor); + QPoint(0, pinOffTop) * _ifactor, + pinOffSize * _ifactor); VideoTile::PaintPinButton( p, false, @@ -1157,7 +1161,7 @@ void Viewport::RendererGL::ensureButtonsImage() { &_pinIcon); const auto backTop = pinOffTop + pinOffSize.height(); - _back = QRect(QPoint(0, backTop) * _factor, backSize * _factor); + _back = QRect(QPoint(0, backTop) * _ifactor, backSize * _ifactor); VideoTile::PaintBackButton( p, 0, @@ -1166,18 +1170,18 @@ void Viewport::RendererGL::ensureButtonsImage() { &_pinBackground); const auto muteTop = backTop + backSize.height(); - _muteOn = QRect(QPoint(0, muteTop) * _factor, muteSize * _factor); + _muteOn = QRect(QPoint(0, muteTop) * _ifactor, muteSize * _ifactor); _muteIcon.paint(p, { 0, muteTop }, 1.); _muteOff = QRect( - QPoint(muteSize.width(), muteTop) * _factor, - muteSize * _factor); + QPoint(muteSize.width(), muteTop) * _ifactor, + muteSize * _ifactor); _muteIcon.paint(p, { muteSize.width(), muteTop }, 0.); const auto pausedTop = muteTop + muteSize.height(); _paused = QRect( - QPoint(0, pausedTop) * _factor, - pausedSize * _factor); + QPoint(0, pausedTop) * _ifactor, + pausedSize * _ifactor); st::groupCallPaused.paint(p, 0, pausedTop, fullSize.width()); } _buttons.setImage(std::move(image)); diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h index aab98c6fd..848174f86 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h @@ -130,6 +130,7 @@ private: const not_null _owner; GLfloat _factor = 1.; + int _ifactor = 1; QSize _viewport; bool _rgbaFrame = false; bool _userpicFrame; diff --git a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp index 93ee5f624..5344d1184 100644 --- a/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp +++ b/Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp @@ -52,7 +52,9 @@ protected: void paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, - const QRect &rect) const override; + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; private: @@ -107,7 +109,9 @@ void Style::paintButtonIcon( void Style::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, - const QRect &rect) const { + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const { // Buttons with loading progress should not appear here. } diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 79c85a03d..fb85201eb 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -118,6 +118,8 @@ EmojiPan { tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + colorAll: IconButton; + colorAllLabel: FlatLabel; removeSet: IconButton; boxLabel: FlatLabel; icons: ComposeIcons; @@ -196,6 +198,8 @@ ComposeControls { send: SendButton; attach: IconButton; emoji: EmojiButton; + like: IconButton; + liked: icon; suggestions: EmojiSuggestions; tabbed: EmojiPan; tabbedHeightMin: pixels; @@ -446,6 +450,7 @@ inlineResultsMaxHeight: 640px; emojiPanHeaderFont: semiboldFont; emojiPanRemoveSkip: 10px; emojiPanRemoveTop: 10px; +emojiPanColorAllSkip: 9px; emojiColorsPadding: 5px; emojiColorsSep: 1px; @@ -488,6 +493,25 @@ stickerIconMove: 400; stickerPreviewDuration: 150; stickerPreviewMin: 0.1; +emojiPanColorAll: IconButton(stickerPanRemoveSet) { + width: 24px; + height: 24px; + rippleAreaSize: 24px; + icon: icon {{ "emoji/emoji_skin", smallCloseIconFg }}; + iconOver: icon {{ "emoji/emoji_skin", smallCloseIconFgOver }}; +} +emojiPanColorAllLabel: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; + align: align(top); + minWidth: 40px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px); + linkFontOver: font(12px); + } +} +emojiPanColorAllPadding: margins(10px, 6px, 10px, -1px); + stickerGroupCategorySize: 28px; stickerGroupCategoryAbout: defaultTextStyle; stickerGroupCategoryAddMargin: margins(0px, 10px, 0px, 5px); @@ -624,6 +648,8 @@ defaultEmojiPan: EmojiPan { tabs: emojiTabs; search: defaultTabbedSearch; searchMargin: margins(1px, 11px, 2px, 5px); + colorAll: emojiPanColorAll; + colorAllLabel: emojiPanColorAllLabel; removeSet: stickerPanRemoveSet; boxLabel: boxLabel; icons: defaultComposeIcons; diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h index 2f9915679..ba6f43b4e 100644 --- a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { struct ComposeFeatures { + bool likes = false; bool sendAs = true; bool ttlInfo = true; bool botCommandSend = true; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp index 684027624..500de20b6 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp @@ -672,17 +672,9 @@ std::vector EmojiKeywords::PrioritizeRecent( } std::vector EmojiKeywords::ApplyVariants(std::vector list) { + auto &settings = Core::App().settings(); for (auto &item : list) { - item.emoji = [&] { - const auto result = item.emoji; - const auto &variants = Core::App().settings().emojiVariants(); - const auto i = result->hasVariants() - ? variants.find(result->nonColoredId()) - : end(variants); - return (i != end(variants)) - ? result->variant(i->second) - : result; - }(); + item.emoji = settings.lookupEmojiVariant(item.emoji); } return list; } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index f41482c19..cb3b034ce 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/emoji_list_widget.h" #include "base/unixtime.h" +#include "ui/boxes/confirm_box.h" #include "ui/controls/tabbed_search.h" #include "ui/text/format_values.h" #include "ui/effects/animations.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" @@ -41,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_premium.h" #include "window/window_session_controller.h" #include "styles/style_chat_helpers.h" +#include "styles/style_menu_icons.h" namespace ChatHelpers { namespace { @@ -58,7 +62,7 @@ class EmojiColorPicker final : public Ui::RpWidget { public: EmojiColorPicker(QWidget *parent, const style::EmojiPan &st); - void showEmoji(EmojiPtr emoji); + void showEmoji(EmojiPtr emoji, bool allLabel = false); void clearSelection(); void handleMouseMove(QPoint globalPos); @@ -79,8 +83,10 @@ protected: void mouseMoveEvent(QMouseEvent *e) override; private: + void createAllLabel(); void animationCallback(); void updateSize(); + [[nodiscard]] int topColorAllSkip() const; void drawVariant(QPainter &p, int variant); @@ -106,6 +112,8 @@ private: QPixmap _cache; Ui::Animations::Simple _a_opacity; + std::unique_ptr _allLabel; + rpl::event_stream _chosen; rpl::event_stream<> _hidden; @@ -131,10 +139,15 @@ EmojiColorPicker::EmojiColorPicker( setMouseTracking(true); } -void EmojiColorPicker::showEmoji(EmojiPtr emoji) { +void EmojiColorPicker::showEmoji(EmojiPtr emoji, bool allLabel) { if (!emoji || !emoji->hasVariants()) { return; } + if (!allLabel) { + _allLabel = nullptr; + } else if (!_allLabel) { + createAllLabel(); + } _ignoreShow = false; _variants.resize(emoji->variantsCount() + 1); @@ -144,10 +157,21 @@ void EmojiColorPicker::showEmoji(EmojiPtr emoji) { updateSize(); - if (!_cache.isNull()) _cache = QPixmap(); + if (!_cache.isNull()) { + _cache = QPixmap(); + } showAnimated(); } +void EmojiColorPicker::createAllLabel() { + _allLabel = std::make_unique( + this, + tr::lng_emoji_color_all(), + _st.colorAllLabel); + _allLabel->show(); + _allLabel->setAttribute(Qt::WA_TransparentForMouseEvents); +} + void EmojiColorPicker::updateSize() { auto width = st::emojiPanMargins.left() + _singleSize.width() * _variants.size() @@ -158,6 +182,17 @@ void EmojiColorPicker::updateSize() { + 2 * st::emojiColorsPadding + _singleSize.height() + st::emojiPanMargins.bottom(); + if (_allLabel) { + _allLabel->resizeToWidth(width + - st::emojiPanMargins.left() + - st::emojiPanMargins.right() + - st::emojiPanColorAllPadding.left() + - st::emojiPanColorAllPadding.right()); + _allLabel->move( + st::emojiPanMargins.left() + st::emojiPanColorAllPadding.left(), + st::emojiPanMargins.top() + st::emojiPanColorAllPadding.top()); + height += topColorAllSkip(); + } resize(width, height); update(); updateSelected(); @@ -186,11 +221,15 @@ void EmojiColorPicker::paintEvent(QPaintEvent *e) { Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow); _backgroundRect.paint(p, inner); + const auto skip = topColorAllSkip(); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; - p.fillRect(x, st::emojiPanMargins.top() + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2, st::emojiColorsSepColor); + p.fillRect(x, st::emojiPanMargins.top() + skip + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2 - skip, st::emojiColorsSepColor); - if (_variants.isEmpty()) return; + if (_variants.isEmpty()) { + return; + } + p.translate(0, skip); for (auto i = 0, count = int(_variants.size()); i != count; ++i) { drawVariant(p, i); } @@ -248,6 +287,9 @@ void EmojiColorPicker::animationCallback() { update(); if (!_a_opacity.animating()) { _cache = QPixmap(); + if (_allLabel) { + _allLabel->show(); + } if (_hiding) { hide(); _hidden.fire({}); @@ -276,10 +318,16 @@ rpl::producer<> EmojiColorPicker::hidden() const { void EmojiColorPicker::hideAnimated() { if (_cache.isNull()) { + if (_allLabel) { + _allLabel->show(); + } _cache = Ui::GrabWidget(this); clearSelection(); } _hiding = true; + if (_allLabel) { + _allLabel->hide(); + } _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration); } @@ -291,10 +339,16 @@ void EmojiColorPicker::showAnimated() { } _hiding = false; if (_cache.isNull()) { + if (_allLabel) { + _allLabel->show(); + } _cache = Ui::GrabWidget(this); clearSelection(); } show(); + if (_allLabel) { + _allLabel->hide(); + } _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration); } @@ -304,10 +358,18 @@ void EmojiColorPicker::clearSelection() { _lastMousePos = mapToGlobal(QPoint(-10, -10)); } +int EmojiColorPicker::topColorAllSkip() const { + return _allLabel + ? (st::emojiPanColorAllPadding.top() + + _allLabel->height() + + st::emojiPanColorAllPadding.bottom()) + : 0; +} + void EmojiColorPicker::updateSelected() { auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); - auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - st::emojiColorsPadding; + auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - topColorAllSkip() - st::emojiColorsPadding; if (y >= 0 && y < _singleSize.height()) { auto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding; if (x >= 0 && x < _singleSize.width()) { @@ -327,7 +389,8 @@ void EmojiColorPicker::setSelected(int newSelected) { if (_selected == newSelected) { return; } - auto updateSelectedRect = [this] { + const auto skip = topColorAllSkip(); + const auto updateSelectedRect = [&] { if (_selected < 0) return; auto addedSkip = (_selected > 0) ? (2 * st::emojiColorsPadding + st::emojiColorsSep) @@ -338,7 +401,7 @@ void EmojiColorPicker::setSelected(int newSelected) { + addedSkip; rtlupdate( left, - st::emojiPanMargins.top() + st::emojiColorsPadding, + st::emojiPanMargins.top() + st::emojiColorsPadding + skip, _singleSize.width(), _singleSize.height()); }; @@ -851,6 +914,31 @@ void EmojiListWidget::setSingleSize(QSize size) { _picker->setSingleSize(_singleSize); } +void EmojiListWidget::setColorAllForceRippled(bool force) { + _colorAllRippleForced = force; + if (_colorAllRippleForced) { + _colorAllRippleForcedLifetime = style::PaletteChanged( + ) | rpl::filter([=] { + return _colorAllRipple != nullptr; + }) | rpl::start_with_next([=] { + _colorAllRipple->forceRepaint(); + }); + if (!_colorAllRipple) { + _colorAllRipple = createButtonRipple(int(Section::People)); + } + if (_colorAllRipple->empty()) { + _colorAllRipple->addFading(); + } else { + _colorAllRipple->lastUnstop(); + } + } else { + if (_colorAllRipple) { + _colorAllRipple->lastStop(); + } + _colorAllRippleForcedLifetime.destroy(); + } +} + int EmojiListWidget::countDesiredHeight(int newWidth) { const auto fullWidth = st().margin.left() + newWidth @@ -897,14 +985,9 @@ void EmojiListWidget::ensureLoaded(int section) { _emoji[section] = Ui::Emoji::GetSection(static_cast
(section)); _counts[section] = _emoji[section].size(); - const auto &variants = Core::App().settings().emojiVariants(); + const auto &settings = Core::App().settings(); for (auto &emoji : _emoji[section]) { - if (emoji->hasVariants()) { - const auto j = variants.find(emoji->nonColoredId()); - if (j != end(variants)) { - emoji = emoji->variant(j->second); - } - } + emoji = settings.lookupEmojiVariant(emoji); } } @@ -956,7 +1039,7 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) { base::unique_qptr EmojiListWidget::fillContextMenu( SendMenu::Type type) { - if (_mode != Mode::EmojiStatus || v::is_null(_selected)) { + if (v::is_null(_selected)) { return nullptr; } const auto over = std::get_if(&_selected); @@ -965,13 +1048,104 @@ base::unique_qptr EmojiListWidget::fillContextMenu( } const auto section = over->section; const auto index = over->index; - const auto chosen = lookupCustomEmoji(index, section); - if (!chosen) { - return nullptr; - } auto menu = base::make_unique_q( this, - st::defaultPopupMenu); + (_mode == Mode::Full + ? st::popupMenuWithIcons + : st::defaultPopupMenu)); + if (_mode == Mode::Full) { + fillRecentMenu(menu, section, index); + } else if (_mode == Mode::EmojiStatus) { + fillEmojiStatusMenu(menu, section, index); + } + if (menu->empty()) { + return nullptr; + } + return menu; +} + +void EmojiListWidget::fillRecentMenu( + not_null menu, + int section, + int index) { + if (section != int(Section::Recent)) { + return; + } + const auto addAction = Ui::Menu::CreateAddActionCallback(menu); + const auto over = OverEmoji{ section, index }; + const auto emoji = lookupOverEmoji(&over); + const auto custom = lookupCustomEmoji(index, section); + if (custom && custom->sticker()) { + const auto sticker = custom->sticker(); + const auto emoji = sticker->alt; + const auto setId = sticker->set.id; + if (!emoji.isEmpty()) { + auto data = TextForMimeData{ emoji, { emoji } }; + data.rich.entities.push_back({ + EntityType::CustomEmoji, + 0, + int(emoji.size()), + Data::SerializeCustomEmojiId(custom) + }); + addAction(tr::lng_emoji_copy(tr::now), [=] { + TextUtilities::SetClipboardText(data); + }, &st::menuIconCopy); + } + if (setId && _features.openStickerSets) { + addAction( + tr::lng_emoji_view_pack(tr::now), + crl::guard(this, [=] { displaySet(setId); }), + &st::menuIconShowAll); + } + } else if (emoji) { + addAction(tr::lng_emoji_copy(tr::now), [=] { + const auto text = emoji->text(); + TextUtilities::SetClipboardText({ text, { text } }); + }, &st::menuIconCopy); + } + auto id = RecentEmojiId{ emoji }; + if (custom) { + id.data = RecentEmojiDocument{ + .id = custom->id, + .test = custom->session().isTestMode(), + }; + } + addAction(tr::lng_emoji_remove_recent(tr::now), crl::guard(this, [=] { + Core::App().settings().hideRecentEmoji(id); + refreshRecent(); + }), &st::menuIconCancel); + + menu->addSeparator(&st().expandedSeparator); + + const auto resetRecent = [=] { + const auto sure = [=](Fn &&close) { + Core::App().settings().resetRecentEmoji(); + refreshRecent(); + close(); + }; + checkHideWithBox(Ui::MakeConfirmBox({ + .text = tr::lng_emoji_reset_recent_sure(), + .confirmed = crl::guard(this, sure), + .confirmText = tr::lng_emoji_reset_recent_button(tr::now), + .labelStyle = &st().boxLabel, + })); + }; + addAction({ + .text = tr::lng_emoji_reset_recent(tr::now), + .handler = crl::guard(this, resetRecent), + .icon = &st::menuIconRestoreAttention, + .isAttention = true, + }); +} + +void EmojiListWidget::fillEmojiStatusMenu( + not_null menu, + int section, + int index) { + const auto chosen = lookupCustomEmoji(index, section); + if (!chosen) { + return; + } const auto selectWith = [=](TimeId scheduled) { selectCustom( lookupChosen(chosen, nullptr, { .scheduled = scheduled })); @@ -989,7 +1163,6 @@ base::unique_qptr EmojiListWidget::fillContextMenu( tr::lng_manage_messages_ttl_after_custom(tr::now), crl::guard(this, [=] { selectWith( TabbedSelector::kPickCustomTimeId); })); - return menu; } void EmojiListWidget::paintEvent(QPaintEvent *e) { @@ -1366,8 +1539,7 @@ void EmojiListWidget::mousePressEvent(QMouseEvent *e) { if (emoji && emoji->hasVariants()) { _pickerSelected = _selected; setCursor(style::cur_default); - const auto &variants = Core::App().settings().emojiVariants(); - if (!variants.contains(emoji->nonColoredId())) { + if (!Core::App().settings().hasChosenEmojiVariant(emoji)) { showPicker(); } else { _showPickerTimer.callOnce(500); @@ -1385,12 +1557,11 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { return _picker->handleMouseRelease(QCursor::pos()); } else if (const auto over = std::get_if(&_pickerSelected)) { const auto emoji = lookupOverEmoji(over); - if (emoji && emoji->hasVariants()) { - const auto &variants = Core::App().settings().emojiVariants(); - if (variants.contains(emoji->nonColoredId())) { - _picker->hideAnimated(); - _pickerSelected = v::null; - } + if (emoji + && emoji->hasVariants() + && Core::App().settings().hasChosenEmojiVariant(emoji)) { + _picker->hideAnimated(); + _pickerSelected = v::null; } } } @@ -1429,11 +1600,15 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { && set->section < _staticCount + _custom.size()); displaySet(_custom[set->section - _staticCount].id); } else if (auto button = std::get_if(&pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - const auto id = _custom[button->section - _staticCount].id; + Assert(hasButton(button->section)); + const auto id = hasColorButton(button->section) + ? 0 + : _custom[button->section - _staticCount].id; const auto usage = ChatHelpers::WindowUsage::PremiumPromo; - if (hasRemoveButton(button->section)) { + if (hasColorButton(button->section)) { + _pickerSelected = pressed; + showPicker(); + } else if (hasRemoveButton(button->section)) { removeSet(id); } else if (hasAddButton(button->section)) { _localSetsManager->install(id); @@ -1496,23 +1671,35 @@ void EmojiListWidget::showPicker() { if (v::is_null(_pickerSelected)) { return; } - - const auto over = std::get_if(&_pickerSelected); - const auto emoji = lookupOverEmoji(over); - if (emoji && emoji->hasVariants()) { - _picker->showEmoji(emoji); - - auto y = emojiRect(over->section, over->index).y(); + const auto showAt = [&](float64 xCoef, int y, int height) { y -= _picker->height() - st::emojiPanRadius + getVisibleTop(); if (y < st().header) { - y += _picker->height() - st::emojiPanRadius + _singleSize.height() - st::emojiPanRadius; + y += _picker->height() + height; } auto xmax = width() - _picker->width(); - auto coef = float64(over->index % _columnCount) / float64(_columnCount - 1); - if (rtl()) coef = 1. - coef; - _picker->move(qRound(xmax * coef), y); + if (rtl()) xCoef = 1. - xCoef; + _picker->move(qRound(xmax * xCoef), y); disableScroll(true); + }; + if (const auto button = std::get_if(&_pickerSelected)) { + const auto hand = QString::fromUtf8("\xF0\x9F\x91\x8B"); + const auto emoji = Ui::Emoji::Find(hand); + Assert(emoji != nullptr && emoji->hasVariants()); + _picker->showEmoji(emoji, true); + setColorAllForceRippled(true); + const auto rect = buttonRect(button->section); + showAt(1., rect.y(), rect.height() - 2 * st::emojiPanRadius); + } else if (const auto over = std::get_if(&_pickerSelected)) { + const auto emoji = lookupOverEmoji(over); + if (emoji && emoji->hasVariants()) { + _picker->showEmoji(emoji); + + const auto coef = float64(over->index % _columnCount) + / float64(_columnCount - 1); + const auto h = _singleSize.height() - 2 * st::emojiPanRadius; + showAt(coef, emojiRect(over->section, over->index).y(), h); + } } } @@ -1520,11 +1707,34 @@ void EmojiListWidget::pickerHidden() { _pickerSelected = v::null; update(); disableScroll(false); + setColorAllForceRippled(false); _lastMousePos = QCursor::pos(); updateSelected(); } +bool EmojiListWidget::hasColorButton(int index) const { + return (_staticCount > int(Section::People)) + && (index == int(Section::People)); +} + +QRect EmojiListWidget::colorButtonRect(int index) const { + return colorButtonRect(sectionInfo(index)); +} + +QRect EmojiListWidget::colorButtonRect(const SectionInfo &info) const { + if (_mode != Mode::Full) { + return QRect(); + } + const auto &colorSt = st().colorAll; + const auto buttonw = colorSt.rippleAreaPosition.x() + + colorSt.rippleAreaSize; + const auto buttonh = colorSt.height; + const auto buttonx = emojiRight() - st::emojiPanColorAllSkip - buttonw; + const auto buttony = info.top + st::emojiPanRemoveTop; + return QRect(buttonx, buttony, buttonw, buttonh); +} + bool EmojiListWidget::hasRemoveButton(int index) const { if (index < _staticCount || index >= _staticCount + _custom.size()) { @@ -1581,15 +1791,18 @@ QRect EmojiListWidget::unlockButtonRect(int index) const { } bool EmojiListWidget::hasButton(int index) const { - if (index < _staticCount - || index >= _staticCount + _custom.size()) { - return false; + if (hasColorButton(index) + || (index >= _staticCount + && index < _staticCount + _custom.size())) { + return true; } - return true; + return false; } QRect EmojiListWidget::buttonRect(int index) const { - return hasRemoveButton(index) + return hasColorButton(index) + ? colorButtonRect(index) + : hasRemoveButton(index) ? removeButtonRect(index) : hasAddButton(index) ? addButtonRect(index) @@ -1637,19 +1850,33 @@ QRect EmojiListWidget::emojiRect(int section, int index) const { } void EmojiListWidget::colorChosen(EmojiChosen data) { + Expects(data.emoji != nullptr && data.emoji->hasVariants()); + const auto emoji = data.emoji; - if (emoji->hasVariants()) { - Core::App().settings().saveEmojiVariant(emoji); + auto &settings = Core::App().settings(); + if (const auto button = std::get_if(&_pickerSelected)) { + settings.saveAllEmojiVariants(emoji); + for (auto section = int(Section::People) + ; section < _staticCount + ; ++section) { + for (auto &emoji : _emoji[section]) { + emoji = settings.lookupEmojiVariant(emoji); + } + } + update(); + } else { + settings.saveEmojiVariant(emoji); + + const auto over = std::get_if(&_pickerSelected); + if (over + && over->section > int(Section::Recent) + && over->section < _staticCount + && over->index < _emoji[over->section].size()) { + _emoji[over->section][over->index] = emoji; + rtlupdate(emojiRect(over->section, over->index)); + } + selectEmoji(data); } - const auto over = std::get_if(&_pickerSelected); - if (over - && over->section > int(Section::Recent) - && over->section < _staticCount - && over->index < _emoji[over->section].size()) { - _emoji[over->section][over->index] = emoji; - rtlupdate(emojiRect(over->section, over->index)); - } - selectEmoji(data); _picker->hideAnimated(); } @@ -1751,6 +1978,7 @@ void EmojiListWidget::refreshRecent() { clearSelection(); fillRecent(); resizeToWidth(width()); + update(); } void EmojiListWidget::refreshCustom() { @@ -1967,47 +2195,54 @@ int EmojiListWidget::paintButtonGetWidth( const SectionInfo &info, bool selected, QRect clip) const { - if (info.section < _staticCount - || info.section >= _staticCount + _custom.size()) { + if (!hasButton(info.section)) { return 0; } - auto &custom = _custom[info.section - _staticCount]; - if (hasRemoveButton(info.section)) { - const auto remove = removeButtonRect(info); - if (remove.isEmpty()) { + auto &ripple = (info.section >= _staticCount) + ? _custom[info.section - _staticCount].ripple + : _colorAllRipple; + const auto colorAll = hasColorButton(info.section); + if (colorAll || hasRemoveButton(info.section)) { + const auto rect = colorAll + ? colorButtonRect(info) + : removeButtonRect(info); + if (rect.isEmpty()) { return 0; - } else if (remove.intersects(clip)) { - const auto &removeSt = st().removeSet; - if (custom.ripple) { - custom.ripple->paint( + } else if (rect.intersects(clip)) { + const auto &bst = colorAll ? st().colorAll : st().removeSet; + if (colorAll && _colorAllRippleForced) { + selected = true; + } + if (ripple) { + ripple->paint( p, - remove.x() + removeSt.rippleAreaPosition.x(), - remove.y() + removeSt.rippleAreaPosition.y(), + rect.x() + bst.rippleAreaPosition.x(), + rect.y() + bst.rippleAreaPosition.y(), width()); - if (custom.ripple->empty()) { - custom.ripple.reset(); + if (ripple->empty()) { + ripple.reset(); } } - const auto &icon = selected ? removeSt.iconOver : removeSt.icon; + const auto &icon = selected ? bst.iconOver : bst.icon; icon.paint( p, - (remove.topLeft() + (rect.topLeft() + QPoint( - remove.width() - icon.width(), - remove.height() - icon.height()) / 2), + rect.width() - icon.width(), + rect.height() - icon.height()) / 2), width()); } - return emojiRight() - remove.x(); + return emojiRight() - rect.x(); } const auto canAdd = hasAddButton(info.section); const auto &button = rightButton(info.section); const auto rect = buttonRect(info, button); p.drawImage(rect.topLeft(), selected ? button.backOver : button.back); - if (custom.ripple) { - const auto ripple = QColor(0, 0, 0, 36); - custom.ripple->paint(p, rect.x(), rect.y(), width(), &ripple); - if (custom.ripple->empty()) { - custom.ripple.reset(); + if (ripple) { + const auto color = QColor(0, 0, 0, 36); + ripple->paint(p, rect.x(), rect.y(), width(), &color); + if (ripple->empty()) { + ripple.reset(); } } p.setPen(!canAdd @@ -2108,22 +2343,28 @@ void EmojiListWidget::setSelected(OverState newSelected) { void EmojiListWidget::setPressed(OverState newPressed) { if (auto button = std::get_if(&_pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - auto &set = _custom[button->section - _staticCount]; - if (set.ripple) { - set.ripple->lastStop(); + Assert(hasColorButton(button->section) + || (button->section >= _staticCount + && button->section < _staticCount + _custom.size())); + auto &ripple = (button->section >= _staticCount) + ? _custom[button->section - _staticCount].ripple + : _colorAllRipple; + if (ripple) { + ripple->lastStop(); } } _pressed = newPressed; if (auto button = std::get_if(&_pressed)) { - Assert(button->section >= _staticCount - && button->section < _staticCount + _custom.size()); - auto &set = _custom[button->section - _staticCount]; - if (!set.ripple) { - set.ripple = createButtonRipple(button->section); + Assert(hasColorButton(button->section) + || (button->section >= _staticCount + && button->section < _staticCount + _custom.size())); + auto &ripple = (button->section >= _staticCount) + ? _custom[button->section - _staticCount].ripple + : _colorAllRipple; + if (!ripple) { + ripple = createButtonRipple(button->section); } - set.ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); + ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); } } @@ -2167,16 +2408,18 @@ void EmojiListWidget::initButton( std::unique_ptr EmojiListWidget::createButtonRipple( int section) { - Expects(section >= _staticCount - && section < _staticCount + _custom.size()); + Expects(hasButton(section)); + const auto colorAll = hasColorButton(section); const auto remove = hasRemoveButton(section); - const auto &removeSt = st().removeSet; - const auto &st = remove ? removeSt.ripple : st::emojiPanButton.ripple; - auto mask = remove + const auto &staticSt = colorAll ? st().colorAll : st().removeSet; + const auto &st = (colorAll || remove) + ? staticSt.ripple + : st::emojiPanButton.ripple; + auto mask = (colorAll || remove) ? Ui::RippleAnimation::EllipseMask(QSize( - removeSt.rippleAreaSize, - removeSt.rippleAreaSize)) + staticSt.rippleAreaSize, + staticSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, @@ -2185,11 +2428,12 @@ std::unique_ptr EmojiListWidget::createButtonRipple( } QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { - Expects(section >= _staticCount - && section < _staticCount + _custom.size()); + Expects(hasButton(section)); return myrtlrect(buttonRect(section)).topLeft() - + (hasRemoveButton(section) + + (hasColorButton(section) + ? st().colorAll.rippleAreaPosition + : hasRemoveButton(section) ? st().removeSet.rippleAreaPosition : QPoint()); } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 374b6e7b3..5cc197995 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -247,6 +247,7 @@ private: [[nodiscard]] SectionInfo sectionInfoByOffset(int yOffset) const; [[nodiscard]] int sectionsCount() const; void setSingleSize(QSize size); + void setColorAllForceRippled(bool force); void showPicker(); void pickerHidden(); @@ -265,6 +266,15 @@ private: void setSelected(OverState newSelected); void setPressed(OverState newPressed); + void fillRecentMenu( + not_null menu, + int section, + int index); + void fillEmojiStatusMenu( + not_null menu, + int section, + int index); + [[nodiscard]] EmojiPtr lookupOverEmoji(const OverEmoji *over) const; [[nodiscard]] DocumentData *lookupCustomEmoji( int index, @@ -297,6 +307,9 @@ private: int set, int index); void validateEmojiPaintContext(const ExpandingContext &context); + [[nodiscard]] bool hasColorButton(int index) const; + [[nodiscard]] QRect colorButtonRect(int index) const; + [[nodiscard]] QRect colorButtonRect(const SectionInfo &info) const; [[nodiscard]] bool hasRemoveButton(int index) const; [[nodiscard]] QRect removeButtonRect(int index) const; [[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const; @@ -378,6 +391,10 @@ private: Ui::RoundRect _overBg; QImage _searchExpandCache; + mutable std::unique_ptr _colorAllRipple; + bool _colorAllRippleForced = false; + rpl::lifetime _colorAllRippleForcedLifetime; + std::vector _nextSearchQuery; std::vector _searchQuery; base::flat_set _searchEmoji; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 7f5c7a52e..8f5f5fec5 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -422,7 +422,9 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { if (_chat) { maxListSize += (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size()); } else if (_channel && _channel->isMegagroup()) { - if (!_channel->lastParticipantsRequestNeeded()) { + if (!_channel->canViewMembers()) { + maxListSize += _channel->mgInfo->admins.size(); + } else if (!_channel->lastParticipantsRequestNeeded()) { maxListSize += _channel->mgInfo->lastParticipants.size(); } } @@ -488,10 +490,22 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { --i; mrows.push_back({ i->second }); } - } else if (_channel - && _channel->isMegagroup() - && _channel->canViewMembers()) { - if (_channel->lastParticipantsRequestNeeded()) { + } else if (_channel && _channel->isMegagroup()) { + if (!_channel->canViewMembers()) { + if (!_channel->mgInfo->adminsLoaded) { + _channel->session().api().chatParticipants().requestAdmins(_channel); + } else { + mrows.reserve(mrows.size() + _channel->mgInfo->admins.size()); + for (const auto &[userId, rank] : _channel->mgInfo->admins) { + if (const auto user = _channel->owner().userLoaded(userId)) { + if (user->isInaccessible()) continue; + if (!listAllSuggestions && filterNotPassedByName(user)) continue; + if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; + mrows.push_back({ user }); + } + } + } + } else if (_channel->lastParticipantsRequestNeeded()) { _channel->session().api().chatParticipants().requestLast( _channel); } else { diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index b0e263c4a..304c7d3a0 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -279,6 +279,19 @@ TextWithTags PrepareEditText(not_null item) { }; } +bool EditTextChanged( + not_null item, + const TextWithTags &updated) { + const auto original = PrepareEditText(item); + + // Tags can be different for the same entities, because for + // animated emoji each tag contains a different random number. + // So we compare entities instead of tags. + return (original.text != updated.text) + || (TextUtilities::ConvertTextTagsToEntities(original.tags) + != TextUtilities::ConvertTextTagsToEntities(updated.tags)); +} + Fntype() == QEvent::KeyPress) { @@ -625,7 +642,7 @@ void MessageLinksParser::parse() { const auto &text = textWithTags.text; const auto &tags = textWithTags.tags; const auto &markdownTags = _field->getMarkdownTags(); - if (text.isEmpty()) { + if (_disabled || text.isEmpty()) { _list = QStringList(); return; } diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 13012557b..acc2808a1 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -37,8 +37,11 @@ namespace Ui { class PopupMenu; } // namespace Ui -QString PrepareMentionTag(not_null user); -TextWithTags PrepareEditText(not_null item); +[[nodiscard]] QString PrepareMentionTag(not_null user); +[[nodiscard]] TextWithTags PrepareEditText(not_null item); +[[nodiscard]] bool EditTextChanged( + not_null item, + const TextWithTags &updated); Fn field); void parseNow(); + void setDisabled(bool disabled); [[nodiscard]] const rpl::variable &list() const; @@ -126,6 +130,7 @@ private: not_null _field; rpl::variable _list; int _lastLength = 0; + bool _disabled = false; base::Timer _timer; base::qt_connection _connection; diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index de5708322..15a7dc1f9 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -38,6 +38,7 @@ class SessionController; class PeerData; struct ClickHandlerContext { FullMsgId itemId; + QString attachBotWebviewUrl; // Is filled from sections. Fn elementDelegate; base::weak_ptr sessionWindow; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 8d620a0b6..4eaed6178 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -203,7 +203,10 @@ QByteArray Settings::serialize() const { + Serialize::bytearraySize(mediaViewPosition) + sizeof(qint32) + sizeof(quint64) - + sizeof(qint32); + + sizeof(qint32) * 2; + for (const auto &id : _recentEmojiSkip) { + size += Serialize::stringSize(id); + } auto result = QByteArray(); result.reserve(size); @@ -338,7 +341,11 @@ QByteArray Settings::serialize() const { << mediaViewPosition << qint32(_ignoreBatterySaving.current() ? 1 : 0) << quint64(_macRoundIconDigest.value_or(0)) - << qint32(_storiesClickTooltipHidden.current() ? 1 : 0); + << qint32(_storiesClickTooltipHidden.current() ? 1 : 0) + << qint32(_recentEmojiSkip.size()); + for (const auto &id : _recentEmojiSkip) { + stream << id; + } } return result; } @@ -447,6 +454,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 ignoreBatterySaving = _ignoreBatterySaving.current() ? 1 : 0; quint64 macRoundIconDigest = _macRoundIconDigest.value_or(0); qint32 storiesClickTooltipHidden = _storiesClickTooltipHidden.current() ? 1 : 0; + base::flat_set recentEmojiSkip; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -684,6 +692,19 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> storiesClickTooltipHidden; } + if (!stream.atEnd()) { + auto count = qint32(); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto id = QString(); + stream >> id; + if (stream.status() == QDataStream::Ok) { + recentEmojiSkip.emplace(id); + } + } + } + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -876,6 +897,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _ignoreBatterySaving = (ignoreBatterySaving == 1); _macRoundIconDigest = macRoundIconDigest ? macRoundIconDigest : std::optional(); _storiesClickTooltipHidden = (storiesClickTooltipHidden == 1); + _recentEmojiSkip = std::move(recentEmojiSkip); } QString Settings::getSoundPath(const QString &key) const { @@ -968,7 +990,8 @@ rpl::producer Settings::thirdColumnWidthChanges() const { } const std::vector &Settings::recentEmoji() const { - if (_recentEmoji.empty()) { + if (!_recentEmojiResolved) { + _recentEmojiResolved = true; resolveRecentEmoji(); } return _recentEmoji; @@ -1009,6 +1032,8 @@ void Settings::resolveRecentEmoji() const { for (const auto emoji : Ui::Emoji::GetDefaultRecent()) { if (_recentEmoji.size() >= specialCount + kRecentEmojiLimit) { break; + } else if (_recentEmojiSkip.contains(emoji->id())) { + continue; } else if (!haveAlready({ emoji })) { _recentEmoji.push_back({ { emoji }, 1 }); } @@ -1018,6 +1043,9 @@ void Settings::resolveRecentEmoji() const { void Settings::incrementRecentEmoji(RecentEmojiId id) { resolveRecentEmoji(); + if (const auto emoji = std::get_if(&id.data)) { + _recentEmojiSkip.remove((*emoji)->id()); + } auto i = _recentEmoji.begin(), e = _recentEmoji.end(); for (; i != e; ++i) { if (i->id == id) { @@ -1069,6 +1097,36 @@ void Settings::incrementRecentEmoji(RecentEmojiId id) { _saveDelayed.fire({}); } +void Settings::hideRecentEmoji(RecentEmojiId id) { + resolveRecentEmoji(); + + _recentEmoji.erase( + ranges::remove(_recentEmoji, id, &RecentEmoji::id), + end(_recentEmoji)); + if (const auto emoji = std::get_if(&id.data)) { + for (const auto always : Ui::Emoji::GetDefaultRecent()) { + if (always == *emoji) { + _recentEmojiSkip.emplace(always->id()); + break; + } + } + } + _recentEmojiUpdated.fire({}); + _saveDelayed.fire({}); +} + +void Settings::resetRecentEmoji() { + resolveRecentEmoji(); + + _recentEmoji.clear(); + _recentEmojiSkip.clear(); + _recentEmojiPreload.clear(); + _recentEmojiResolved = false; + + _recentEmojiUpdated.fire({}); + _saveDelayed.fire({}); +} + void Settings::setLegacyRecentEmojiPreload( QVector> data) { if (!_recentEmojiPreload.empty() || data.isEmpty()) { @@ -1080,11 +1138,40 @@ void Settings::setLegacyRecentEmojiPreload( } } +EmojiPtr Settings::lookupEmojiVariant(EmojiPtr emoji) const { + if (emoji->hasVariants()) { + const auto i = _emojiVariants.find(emoji->nonColoredId()); + if (i != end(_emojiVariants)) { + return emoji->variant(i->second); + } + const auto j = _emojiVariants.find(QString()); + if (j != end(_emojiVariants)) { + return emoji->variant(j->second); + } + } + return emoji; +} + +bool Settings::hasChosenEmojiVariant(EmojiPtr emoji) const { + return _emojiVariants.contains(QString()) + || _emojiVariants.contains(emoji->nonColoredId()); +} + void Settings::saveEmojiVariant(EmojiPtr emoji) { + Expects(emoji->hasVariants()); + _emojiVariants[emoji->nonColoredId()] = emoji->variantIndex(emoji); _saveDelayed.fire({}); } +void Settings::saveAllEmojiVariants(EmojiPtr emoji) { + Expects(emoji->hasVariants()); + + _emojiVariants.clear(); + _emojiVariants[QString()] = emoji->variantIndex(emoji); + _saveDelayed.fire({}); +} + void Settings::setLegacyEmojiVariants(QMap data) { if (!_emojiVariants.empty() || data.isEmpty()) { return; diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 72468e755..f25345da6 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -72,7 +72,7 @@ struct WindowTitleContent { WindowTitleContent) = default; }; -constexpr auto kRecentEmojiLimit = 42; +constexpr auto kRecentEmojiLimit = 54; struct RecentEmojiDocument { DocumentId id = 0; @@ -660,6 +660,8 @@ public: [[nodiscard]] const std::vector &recentEmoji() const; void incrementRecentEmoji(RecentEmojiId id); + void hideRecentEmoji(RecentEmojiId id); + void resetRecentEmoji(); void setLegacyRecentEmojiPreload(QVector> data); [[nodiscard]] rpl::producer<> recentEmojiUpdated() const { return _recentEmojiUpdated.events(); @@ -668,7 +670,10 @@ public: [[nodiscard]] const base::flat_map &emojiVariants() const { return _emojiVariants; } + [[nodiscard]] EmojiPtr lookupEmojiVariant(EmojiPtr emoji) const; + [[nodiscard]] bool hasChosenEmojiVariant(EmojiPtr emoji) const; void saveEmojiVariant(EmojiPtr emoji); + void saveAllEmojiVariants(EmojiPtr emoji); void setLegacyEmojiVariants(QMap data); [[nodiscard]] bool disableOpenGL() const { @@ -891,6 +896,8 @@ private: rpl::variable _mainMenuAccountsShown = true; mutable std::vector _recentEmojiPreload; mutable std::vector _recentEmoji; + base::flat_set _recentEmojiSkip; + mutable bool _recentEmojiResolved = false; base::flat_map _emojiVariants; rpl::event_stream<> _recentEmojiUpdated; bool _tabbedSelectorSectionEnabled = false; // per-window diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index 6c14ccd0e..fee3b0140 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -466,17 +466,9 @@ void Launcher::initQtMessageLogging() { QtMsgType type, const QMessageLogContext &context, const QString &msg) { - const auto InvokeOriginal = [&] { -#ifndef _DEBUG - if (Logs::DebugEnabled()) { - return; - } -#endif // _DEBUG - if (OriginalMessageHandler) { - OriginalMessageHandler(type, context, msg); - } - }; - InvokeOriginal(); + if (OriginalMessageHandler) { + OriginalMessageHandler(type, context, msg); + } if (Logs::DebugEnabled() || !Logs::started()) { if (!Logs::WritingEntry()) { // Sometimes Qt logs something inside our own logging. diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index d18b842c0..9daf74d62 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -412,6 +412,7 @@ bool ResolveUsernameOrPhone( } const auto myContext = context.value(); using Navigation = Window::SessionNavigation; + controller->window().activate(); controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = domain, .phone = phone, @@ -446,8 +447,8 @@ bool ResolveUsernameOrPhone( ? std::make_optional(params.value(u"voicechat"_q)) : std::nullopt), .clickFromMessageId = myContext.itemId, + .clickFromAttachBotWebviewUrl = myContext.attachBotWebviewUrl, }); - controller->window().activate(); return true; } @@ -473,7 +474,7 @@ bool ResolvePrivatePost( if (!channelId || (msgId && !IsServerMsgId(msgId))) { return false; } - const auto fromMessageId = context.value().itemId; + const auto my = context.value(); using Navigation = Window::SessionNavigation; controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = channelId, @@ -487,7 +488,8 @@ bool ResolvePrivatePost( Navigation::ThreadId{ threadId } } : Navigation::RepliesByLinkInfo{ v::null }, - .clickFromMessageId = fromMessageId, + .clickFromMessageId = my.itemId, + .clickFromAttachBotWebviewUrl = my.attachBotWebviewUrl, }); controller->window().activate(); return true; diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index c051a4375..1c1419bb2 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -586,7 +586,7 @@ void Sandbox::registerEnterFromEventLoop() { bool Sandbox::notifyOrInvoke(QObject *receiver, QEvent *e) { const auto type = e->type(); - if (type == base::InvokeQueuedEvent::kType) { + if (type == base::InvokeQueuedEvent::Type()) { static_cast(e)->invoke(); return true; } else if (receiver == this) { diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index e7351e8fd..ead942262 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -282,15 +282,10 @@ rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { const Ui::Emoji::One *UiIntegration::defaultEmojiVariant( const Ui::Emoji::One *emoji) { - if (!emoji || !emoji->hasVariants()) { + if (!emoji) { return emoji; } - const auto nonColored = emoji->nonColoredId(); - const auto &variants = Core::App().settings().emojiVariants(); - const auto i = variants.find(nonColored); - const auto result = (i != end(variants)) - ? emoji->variant(i->second) - : emoji; + const auto result = Core::App().settings().lookupEmojiVariant(emoji); Core::App().settings().incrementRecentEmoji({ result }); return result; } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index df60a4902..e6f40049b 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 4008011; -constexpr auto AppVersionStr = "4.8.11"; -constexpr auto AppBetaVersion = true; +constexpr auto AppVersion = 4009003; +constexpr auto AppVersionStr = "4.9.3"; +constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index d4fee22c8..304749900 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -226,8 +226,9 @@ struct StoryUpdate { NewAdded = (1U << 2), ViewsAdded = (1U << 3), MarkRead = (1U << 4), + Reaction = (1U << 5), - LastUsedBit = (1U << 4), + LastUsedBit = (1U << 5), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 9817da915..6c05d5c88 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -116,6 +116,7 @@ public: QString creatorRank; int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other bool joinedMessageFound = false; + bool adminsLoaded = false; StickerSetIdentifier stickerSet; enum LastParticipantsStatus { diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 088dc0b92..6b984e4cf 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -64,26 +64,43 @@ constexpr auto ByDocument = [](const auto &entry) { return 0; } -[[nodiscard]] PhotoData *ItemPhoto(not_null item) { - if (const auto media = item->media()) { - if (const auto page = media->webpage()) { - return page->document ? nullptr : page->photo; - } else if (const auto photo = media->photo()) { - return photo; +[[nodiscard]] bool ItemContainsMedia(const DownloadObject &object) { + if (const auto photo = object.photo) { + if (const auto media = object.item->media()) { + if (const auto page = media->webpage()) { + if (page->photo == photo) { + return true; + } + for (const auto &item : page->collage.items) { + if (const auto v = std::get_if(&item)) { + if ((*v) == photo) { + return true; + } + } + } + } else { + return (media->photo() == photo); + } + } + } else if (const auto document = object.document) { + if (const auto media = object.item->media()) { + if (const auto page = media->webpage()) { + if (page->document == document) { + return true; + } + for (const auto &item : page->collage.items) { + if (const auto v = std::get_if(&item)) { + if ((*v) == document) { + return true; + } + } + } + } else { + return (media->document() == document); + } } } - return nullptr; -} - -[[nodiscard]] DocumentData *ItemDocument(not_null item) { - if (const auto media = item->media()) { - if (const auto page = media->webpage()) { - return page->document; - } else if (const auto document = media->document()) { - return document; - } - } - return nullptr; + return false; } struct DocumentDescriptor { @@ -242,12 +259,12 @@ void DownloadManager::check( std::vector::iterator i) { auto &entry = *i; - const auto photo = ItemPhoto(entry.object.item); - const auto document = ItemDocument(entry.object.item); - if (entry.object.photo != photo || entry.object.document != document) { + if (!ItemContainsMedia(entry.object)) { cancel(data, i); return; } + const auto document = entry.object.document; + // Load with progress only documents for now. Assert(document != nullptr); @@ -794,11 +811,14 @@ void DownloadManager::cancel( SessionData &data, std::vector::iterator i) { const auto object = i->object; + const auto item = object.item; remove(data, i); - if (const auto document = object.document) { - document->cancel(); - } else if (const auto photo = object.photo) { - photo->cancel(); + if (!item->isAdminLogEntry()) { + if (const auto document = object.document) { + document->cancel(); + } else if (const auto photo = object.photo) { + photo->cancel(); + } } } diff --git a/Telegram/SourceFiles/data/data_file_click_handler.cpp b/Telegram/SourceFiles/data/data_file_click_handler.cpp index 9fee965f5..e42397d19 100644 --- a/Telegram/SourceFiles/data/data_file_click_handler.cpp +++ b/Telegram/SourceFiles/data/data_file_click_handler.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_download_manager.h" #include "data/data_photo.h" +#include "main/main_session.h" FileClickHandler::FileClickHandler(FullMsgId context) : _context(context) { @@ -73,14 +74,25 @@ void DocumentOpenClickHandler::onClickImpl() const { void DocumentSaveClickHandler::Save( Data::FileOrigin origin, not_null data, - Mode mode) { + Mode mode, + Fn started) { if (data->isNull()) { return; } auto savename = QString(); - if (mode != Mode::ToCacheOrFile || !data->saveToCache()) { + if (mode == Mode::ToCacheOrFile && data->saveToCache()) { + data->save(origin, savename); + return; + } + InvokeQueued(qApp, crl::guard(&data->session(), [=] { + // If we call file dialog synchronously, it will stop + // background thread timers from working which would + // stop audio playback in voice chats / live streams. if (mode != Mode::ToNewFile && data->saveFromData()) { + if (started) { + started(); + } return; } const auto filepath = data->filepath(true); @@ -92,31 +104,38 @@ void DocumentSaveClickHandler::Save( const auto filename = filepath.isEmpty() ? QString() : fileinfo.fileName(); - savename = DocumentFileNameForSave( + const auto savename = DocumentFileNameForSave( data, (mode == Mode::ToNewFile), filename, filedir); - if (savename.isEmpty()) { - return; + if (!savename.isEmpty()) { + data->save(origin, savename); + if (started) { + started(); + } } - } - data->save(origin, savename); + })); } void DocumentSaveClickHandler::SaveAndTrack( FullMsgId itemId, not_null document, - Mode mode) { - Save(itemId ? itemId : Data::FileOrigin(), document, mode); - if (document->loading() && !document->loadingFilePath().isEmpty()) { - if (const auto item = document->owner().message(itemId)) { - Core::App().downloadManager().addLoading({ - .item = item, - .document = document, - }); + Mode mode, + Fn started) { + Save(itemId ? itemId : Data::FileOrigin(), document, mode, [=] { + if (document->loading() && !document->loadingFilePath().isEmpty()) { + if (const auto item = document->owner().message(itemId)) { + Core::App().downloadManager().addLoading({ + .item = item, + .document = document, + }); + } } - } + if (started) { + started(); + } + }); } void DocumentSaveClickHandler::onClickImpl() const { diff --git a/Telegram/SourceFiles/data/data_file_click_handler.h b/Telegram/SourceFiles/data/data_file_click_handler.h index 472d04d15..46eaa5b82 100644 --- a/Telegram/SourceFiles/data/data_file_click_handler.h +++ b/Telegram/SourceFiles/data/data_file_click_handler.h @@ -53,11 +53,13 @@ public: static void Save( Data::FileOrigin origin, not_null document, - Mode mode = Mode::ToCacheOrFile); + Mode mode = Mode::ToCacheOrFile, + Fn started = nullptr); static void SaveAndTrack( FullMsgId itemId, not_null document, - Mode mode = Mode::ToCacheOrFile); + Mode mode = Mode::ToCacheOrFile, + Fn started = nullptr); protected: void onClickImpl() const override; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 19d89c500..7a36a5515 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1848,8 +1848,7 @@ ClickHandlerPtr MediaDice::MakeHandler( .multiline = true, }; if (CanSend(history->peer, ChatRestriction::SendOther)) { - auto link = Ui::Text::Link( - tr::lng_about_random_send(tr::now).toUpper()); + auto link = Ui::Text::Link(tr::lng_about_random_send(tr::now)); link.entities.push_back( EntityInText(EntityType::Semibold, 0, link.text.size())); config.text.append(' ').append(std::move(link)); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index e4279cbb3..7aa820101 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -381,7 +381,7 @@ void Reactions::preloadImageFor(const ReactionId &id) { loadImage(set, document, !i->centerIcon); } else if (!_waitingForList) { _waitingForList = true; - refreshRecent(); + refreshDefault(); } } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 273e60663..b879d4bc5 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -192,6 +192,14 @@ void Stories::apply(const MTPDupdateReadStories &data) { bumpReadTill(peerFromUser(data.vuser_id()), data.vmax_id().v); } +void Stories::apply(const MTPStoriesStealthMode &stealthMode) { + const auto &data = stealthMode.data(); + _stealthMode = StealthMode{ + .enabledTill = data.vactive_until_date().value_or_empty(), + .cooldownTill = data.vcooldown_until_date().value_or_empty(), + }; +} + void Stories::apply(not_null peer, const MTPUserStories *data) { // AyuGram disableStories const auto settings = &AyuSettings::getInstance(); @@ -369,7 +377,10 @@ void Stories::parseAndApply(const MTPUserStories &stories) { } sort(list); }; - if (result.user->isContact()) { + if (result.user->isSelf() + || result.user->isBot() + || result.user->isServiceUser() + || result.user->isContact()) { const auto hidden = result.user->hasStoriesHidden(); using List = StorySourcesList; add(hidden ? List::Hidden : List::NotHidden); @@ -568,6 +579,10 @@ void Stories::loadMore(StorySourcesList list) { }, [](const MTPDstories_allStoriesNotModified &) { }); + result.match([&](const auto &data) { + apply(data.vstealth_mode()); + }); + preloadListsMore(); }).fail([=] { _loadMoreRequestId[index] = 0; @@ -751,6 +766,7 @@ void Stories::applyDeleted(FullStoryId id) { } } if (_preloading && _preloading->id() == id) { + _preloading = nullptr; preloadFinished(id); } _owner->refreshStoryItemViews(id); @@ -868,6 +884,42 @@ std::shared_ptr Stories::lookupItem(not_null story) { return j->second.lock(); } +StealthMode Stories::stealthMode() const { + return _stealthMode.current(); +} + +rpl::producer Stories::stealthModeValue() const { + return _stealthMode.value(); +} + +void Stories::activateStealthMode(Fn done) { + const auto api = &session().api(); + using Flag = MTPstories_ActivateStealthMode::Flag; + api->request(MTPstories_ActivateStealthMode( + MTP_flags(Flag::f_past | Flag::f_future) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + if (done) done(); + }).fail([=] { + if (done) done(); + }).send(); +} + +void Stories::sendReaction(FullStoryId id, Data::ReactionId reaction) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + story->setReactionId(reaction); + + const auto api = &session().api(); + api->request(MTPstories_SendReaction( + MTP_flags(0), + story->peer()->asUser()->inputUser, + MTP_int(id.story), + ReactionToMTP(reaction) + )).send(); + } +} + std::shared_ptr Stories::resolveItem(not_null story) { auto &items = _items[story->peer()->id]; auto i = items.find(story->id()); @@ -1251,11 +1303,11 @@ void Stories::sendIncrementViewsRequests() { void Stories::loadViewsSlice( StoryId id, - std::optional offset, - Fn)> done) { + QString offset, + Fn done) { if (_viewsStoryId == id && _viewsOffset == offset - && (offset || _viewsRequestId)) { + && (!offset.isEmpty() || _viewsRequestId)) { if (_viewsRequestId) { _viewsDone = std::move(done); } @@ -1268,22 +1320,30 @@ void Stories::loadViewsSlice( const auto api = &_owner->session().api(); const auto perPage = _viewsDone ? kViewsPerPage : kPollingViewsPerPage; api->request(_viewsRequestId).cancel(); + using Flag = MTPstories_GetStoryViewsList::Flag; _viewsRequestId = api->request(MTPstories_GetStoryViewsList( + MTP_flags(Flag::f_reactions_first), + MTPstring(), // q MTP_int(id), - MTP_int(offset ? offset->date : 0), - MTP_long(offset ? peerToUser(offset->peer->id).bare : 0), + MTP_string(offset), MTP_int(perPage) )).done([=](const MTPstories_StoryViewsList &result) { _viewsRequestId = 0; - auto slice = std::vector(); - const auto &data = result.data(); + auto slice = StoryViews{ + .nextOffset = data.vnext_offset().value_or_empty(), + .reactions = data.vreactions_count().v, + .total = data.vcount().v, + }; _owner->processUsers(data.vusers()); - slice.reserve(data.vviews().v.size()); + slice.list.reserve(data.vviews().v.size()); for (const auto &view : data.vviews().v) { - slice.push_back({ + slice.list.push_back({ .peer = _owner->peer(peerFromUser(view.data().vuser_id())), + .reaction = (view.data().vreaction() + ? ReactionFromMTP(*view.data().vreaction()) + : Data::ReactionId()), .date = view.data().vdate().v, }); } @@ -1292,7 +1352,7 @@ void Stories::loadViewsSlice( .story = _viewsStoryId, }; if (const auto story = lookup(fullId)) { - (*story)->applyViewsSlice(_viewsOffset, slice, data.vcount().v); + (*story)->applyViewsSlice(_viewsOffset, slice); } if (const auto done = base::take(_viewsDone)) { done(std::move(slice)); @@ -1731,7 +1791,7 @@ void Stories::sendPollingViewsRequests() { return; } else if (!_viewsRequestId) { Assert(_viewsDone == nullptr); - loadViewsSlice(_pollingViews.front()->id(), std::nullopt, nullptr); + loadViewsSlice(_pollingViews.front()->id(), QString(), nullptr); } _pollingViewsTimer.callOnce(kPollViewsInterval); } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 48ea79688..8d8b23da4 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -116,6 +116,14 @@ struct StoriesContext { friend inline bool operator==(StoriesContext, StoriesContext) = default; }; +struct StealthMode { + TimeId enabledTill = 0; + TimeId cooldownTill = 0; + + friend inline auto operator<=>(StealthMode, StealthMode) = default; + friend inline bool operator==(StealthMode, StealthMode) = default; +}; + inline constexpr auto kStorySourcesListCount = 2; class Stories final : public base::has_weak_ptr { @@ -139,6 +147,7 @@ public: void loadMore(StorySourcesList list); void apply(const MTPDupdateStory &data); void apply(const MTPDupdateReadStories &data); + void apply(const MTPStoriesStealthMode &stealthMode); void apply(not_null peer, const MTPUserStories *data); Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story); void loadAround(FullStoryId id, StoriesContext context); @@ -170,8 +179,8 @@ public: static constexpr auto kViewsPerPage = 50; void loadViewsSlice( StoryId id, - std::optional offset, - Fn)> done); + QString offset, + Fn done); [[nodiscard]] const StoriesIds &archive() const; [[nodiscard]] rpl::producer<> archiveChanged() const; @@ -227,6 +236,12 @@ public: [[nodiscard]] std::shared_ptr lookupItem( not_null story); + [[nodiscard]] StealthMode stealthMode() const; + [[nodiscard]] rpl::producer stealthModeValue() const; + void activateStealthMode(Fn done = nullptr); + + void sendReaction(FullStoryId id, Data::ReactionId reaction); + private: struct Saved { StoriesIds ids; @@ -353,8 +368,8 @@ private: base::flat_set _incrementViewsRequests; StoryId _viewsStoryId = 0; - std::optional _viewsOffset; - Fn)> _viewsDone; + QString _viewsOffset; + Fn _viewsDone; mtpRequestId _viewsRequestId = 0; base::flat_set _preloaded; @@ -375,6 +390,8 @@ private: base::Timer _pollingTimer; base::Timer _pollingViewsTimer; + rpl::variable _stealthMode; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index fbc678581..d5faa4350 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -31,6 +31,47 @@ namespace { using UpdateFlag = StoryUpdate::Flag; +[[nodiscard]] StoryArea ParseArea(const MTPMediaAreaCoordinates &area) { + const auto &data = area.data(); + const auto center = QPointF(data.vx().v, data.vy().v); + const auto size = QSizeF(data.vw().v, data.vh().v); + const auto corner = center - QPointF(size.width(), size.height()) / 2.; + return { + .geometry = { corner / 100., size / 100. }, + .rotation = data.vrotation().v, + }; +} + +[[nodiscard]] auto ParseLocation(const MTPMediaArea &area) +-> std::optional { + auto result = std::optional(); + area.match([&](const MTPDmediaAreaVenue &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { + result.emplace(StoryLocation{ + .area = ParseArea(data.vcoordinates()), + .point = Data::LocationPoint(geo), + .title = qs(data.vtitle()), + .address = qs(data.vaddress()), + .provider = qs(data.vprovider()), + .venueId = qs(data.vvenue_id()), + .venueType = qs(data.vvenue_type()), + }); + }, [](const MTPDgeoPointEmpty &) { + }); + }, [&](const MTPDmediaAreaGeoPoint &data) { + data.vgeo().match([&](const MTPDgeoPoint &geo) { + result.emplace(StoryLocation{ + .area = ParseArea(data.vcoordinates()), + .point = Data::LocationPoint(geo), + }); + }, [](const MTPDgeoPointEmpty &) { + }); + }, [&](const MTPDinputMediaAreaVenue &data) { + LOG(("API Error: Unexpected inputMediaAreaVenue in API data.")); + }); + return result; +} + } // namespace class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask { @@ -276,8 +317,13 @@ bool Story::edited() const { return _edited; } -bool Story::canDownload() const { - return /*!forbidsForward() || */_peer->isSelf(); +bool Story::canDownloadIfPremium() const { + return !forbidsForward() || _peer->isSelf(); +} + +bool Story::canDownloadChecked() const { + return _peer->isSelf() + || (canDownloadIfPremium() && _peer->session().premium()); } bool Story::canShare() const { @@ -330,55 +376,61 @@ const TextWithEntities &Story::caption() const { return unsupported() ? empty : _caption; } +Data::ReactionId Story::sentReactionId() const { + return _sentReactionId; +} + +void Story::setReactionId(Data::ReactionId id) { + if (_sentReactionId != id) { + _sentReactionId = id; + session().changes().storyUpdated(this, UpdateFlag::Reaction); + } +} + const std::vector> &Story::recentViewers() const { return _recentViewers; } -const std::vector &Story::viewsList() const { - return _viewsList; -} - -int Story::views() const { +const StoryViews &Story::viewsList() const { return _views; } +int Story::views() const { + return _views.total; +} + +int Story::reactions() const { + return _views.reactions; +} + void Story::applyViewsSlice( - const std::optional &offset, - const std::vector &slice, - int total) { - const auto changed = (_views != total); - _views = total; - if (!offset) { - const auto i = _viewsList.empty() - ? end(slice) - : ranges::find(slice, _viewsList.front()); - const auto merge = (i != end(slice)) - && !ranges::contains(slice, _viewsList.back()); - if (merge) { - _viewsList.insert(begin(_viewsList), begin(slice), i); - } else { - _viewsList = slice; - } - } else if (!slice.empty()) { - const auto i = ranges::find(_viewsList, *offset); - const auto merge = (i != end(_viewsList)) - && !ranges::contains(_viewsList, slice.back()); - if (merge) { - const auto after = i + 1; - if (after == end(_viewsList)) { - _viewsList.insert(after, begin(slice), end(slice)); - } else { - const auto j = ranges::find(slice, _viewsList.back()); - if (j != end(slice)) { - _viewsList.insert(end(_viewsList), j + 1, end(slice)); - } - } + const QString &offset, + const StoryViews &slice) { + const auto changed = (_views.reactions != slice.reactions) + || (_views.total != slice.total); + _views.reactions = slice.reactions; + _views.total = slice.total; + if (offset.isEmpty()) { + _views = slice; + } else if (_views.nextOffset == offset) { + _views.list.insert( + end(_views.list), + begin(slice.list), + end(slice.list)); + _views.nextOffset = slice.nextOffset; + if (_views.nextOffset.isEmpty()) { + _views.total = int(_views.list.size()); + _views.reactions = _views.total + - ranges::count( + _views.list, + Data::ReactionId(), + &StoryView::reaction); } } - const auto known = int(_viewsList.size()); + const auto known = int(_views.list.size()); if (known >= _recentViewers.size()) { const auto take = std::min(known, kRecentViewersMax); - auto viewers = _viewsList + auto viewers = _views.list | ranges::views::take(take) | ranges::views::transform(&StoryView::peer) | ranges::to_vector; @@ -399,6 +451,10 @@ void Story::applyViewsSlice( } } +const std::vector &Story::locations() const { + return _locations; +} + void Story::applyChanges( StoryMedia media, const MTPDstoryItem &data, @@ -413,6 +469,9 @@ void Story::applyFields( bool initial) { _lastUpdateTime = now; + const auto reaction = data.vsent_reaction() + ? Data::ReactionFromMTP(*data.vsent_reaction()) + : Data::ReactionId(); const auto pinned = data.is_pinned(); const auto edited = data.is_edited(); const auto privacy = data.is_public() @@ -431,11 +490,13 @@ void Story::applyFields( &owner().session(), data.ventities().value_or_empty()), }; - auto views = _views; + auto views = _views.total; + auto reactions = _views.reactions; auto viewers = std::vector>(); if (!data.is_min()) { if (const auto info = data.vviews()) { views = info->data().vviews_count().v; + reactions = info->data().vreactions_count().v; if (const auto list = info->data().vrecent_viewers()) { viewers.reserve(list->v.size()); auto &owner = _peer->owner(); @@ -447,13 +508,25 @@ void Story::applyFields( } } } + auto locations = std::vector(); + if (const auto areas = data.vmedia_areas()) { + locations.reserve(areas->v.size()); + for (const auto &area : areas->v) { + if (const auto parsed = ParseLocation(area)) { + locations.push_back(*parsed); + } + } + } const auto pinnedChanged = (_pinned != pinned); const auto editedChanged = (_edited != edited); const auto mediaChanged = (_media != media); const auto captionChanged = (_caption != caption); - const auto viewsChanged = (_views != views) + const auto viewsChanged = (_views.total != views) + || (_views.reactions != reactions) || (_recentViewers != viewers); + const auto locationsChanged = (_locations != locations); + const auto reactionChanged = (_sentReactionId != reaction); _privacyPublic = (privacy == StoryPrivacy::Public); _privacyCloseFriends = (privacy == StoryPrivacy::CloseFriends); @@ -463,8 +536,10 @@ void Story::applyFields( _edited = edited; _pinned = pinned; _noForwards = noForwards; + if (_views.reactions != reactions || _views.total != views) { + _views = StoryViews{ .reactions = reactions, .total = views }; + } if (viewsChanged) { - _views = views; _recentViewers = std::move(viewers); } if (mediaChanged) { @@ -473,12 +548,22 @@ void Story::applyFields( if (captionChanged) { _caption = std::move(caption); } + if (locationsChanged) { + _locations = std::move(locations); + } + if (reactionChanged) { + _sentReactionId = reaction; + } - const auto changed = (editedChanged || captionChanged || mediaChanged); - if (!initial && (changed || viewsChanged)) { + const auto changed = editedChanged + || captionChanged + || mediaChanged + || locationsChanged; + if (!initial && (changed || viewsChanged || reactionChanged)) { _peer->session().changes().storyUpdated(this, UpdateFlag() | (changed ? UpdateFlag::Edited : UpdateFlag()) - | (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag())); + | (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag()) + | (reactionChanged ? UpdateFlag::Reaction : UpdateFlag())); } if (!initial && (captionChanged || mediaChanged)) { if (const auto item = _peer->owner().stories().lookupItem(this)) { diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h index 8ea17b7b7..dcfe4495c 100644 --- a/Telegram/SourceFiles/data/data_story.h +++ b/Telegram/SourceFiles/data/data_story.h @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/weak_ptr.h" +#include "data/data_location.h" +#include "data/data_message_reaction_id.h" class Image; class PhotoData; @@ -58,11 +60,42 @@ struct StoryMedia { struct StoryView { not_null peer; + Data::ReactionId reaction; TimeId date = 0; friend inline bool operator==(StoryView, StoryView) = default; }; +struct StoryViews { + std::vector list; + QString nextOffset; + int reactions = 0; + int total = 0; +}; + +struct StoryArea { + QRectF geometry; + float64 rotation = 0; + + friend inline bool operator==( + const StoryArea &, + const StoryArea &) = default; +}; + +struct StoryLocation { + StoryArea area; + Data::LocationPoint point; + QString title; + QString address; + QString provider; + QString venueId; + QString venueType; + + friend inline bool operator==( + const StoryLocation &, + const StoryLocation &) = default; +}; + class Story final { public: Story( @@ -100,7 +133,8 @@ public: [[nodiscard]] bool forbidsForward() const; [[nodiscard]] bool edited() const; - [[nodiscard]] bool canDownload() const; + [[nodiscard]] bool canDownloadIfPremium() const; + [[nodiscard]] bool canDownloadChecked() const; [[nodiscard]] bool canShare() const; [[nodiscard]] bool canDelete() const; [[nodiscard]] bool canReport() const; @@ -112,14 +146,17 @@ public: void setCaption(TextWithEntities &&caption); [[nodiscard]] const TextWithEntities &caption() const; + [[nodiscard]] Data::ReactionId sentReactionId() const; + void setReactionId(Data::ReactionId id); + [[nodiscard]] auto recentViewers() const -> const std::vector> &; - [[nodiscard]] const std::vector &viewsList() const; + [[nodiscard]] const StoryViews &viewsList() const; [[nodiscard]] int views() const; - void applyViewsSlice( - const std::optional &offset, - const std::vector &slice, - int total); + [[nodiscard]] int reactions() const; + void applyViewsSlice(const QString &offset, const StoryViews &slice); + + [[nodiscard]] const std::vector &locations() const; void applyChanges( StoryMedia media, @@ -136,11 +173,12 @@ private: const StoryId _id = 0; const not_null _peer; + Data::ReactionId _sentReactionId; StoryMedia _media; TextWithEntities _caption; std::vector> _recentViewers; - std::vector _viewsList; - int _views = 0; + std::vector _locations; + StoryViews _views; const TimeId _date = 0; const TimeId _expires = 0; TimeId _lastUpdateTime = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index d0d01778b..697862166 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -2818,12 +2818,9 @@ void InnerWidget::resizeEmptyLabel() { if (!_empty) { return; } - const auto useWidth = std::min( - _empty->naturalWidth(), - width() - 2 * st::dialogsEmptySkip); - const auto left = (width() - useWidth) / 2; - _empty->resizeToWidth(useWidth); - _empty->move(left, (st::dialogsEmptyHeight - _empty->height()) / 2); + const auto skip = st::dialogsEmptySkip; + _empty->resizeToWidth(width() - 2 * skip); + _empty->move(skip, (st::dialogsEmptyHeight - _empty->height()) / 2); } void InnerWidget::clearMouseSelection(bool clearSelection) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 9b02fa341..c19d5e4b1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -125,7 +125,7 @@ Widget::BottomButton::BottomButton( const style::icon &icon, const style::icon &iconOver) : RippleButton(parent, st.ripple) -, _text(text.toUpper()) +, _text(text) , _st(st) , _icon(icon) , _iconOver(iconOver) { @@ -133,7 +133,7 @@ Widget::BottomButton::BottomButton( } void Widget::BottomButton::setText(const QString &text) { - _text = text.toUpper(); + _text = text; update(); } @@ -1457,6 +1457,8 @@ void Widget::startWidthAnimation() { st::columnMinimalWidthLeft, scrollGeometry.height()); _scroll->setGeometry(grabGeometry); + _inner->resize(st::columnMinimalWidthLeft, _inner->height()); + _inner->setNarrowRatio(0.); Ui::SendPendingMoveResizeEvents(_scroll); auto image = QImage( grabGeometry.size() * cIntRetinaFactor(), @@ -1468,7 +1470,10 @@ void Widget::startWidthAnimation() { Ui::RenderWidget(p, _scroll); } _widthAnimationCache = Ui::PixmapFromImage(std::move(image)); - _scroll->setGeometry(scrollGeometry); + if (scrollGeometry != grabGeometry) { + _scroll->setGeometry(scrollGeometry); + updateControlsGeometry(); + } _scroll->hide(); updateStoriesVisibility(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index ed723b55e..f737d1615 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -347,9 +347,7 @@ Content State::next() { } result.elements.push_back({ .id = uint64(user->id.value), - .name = (user->isSelf() - ? tr::lng_stories_my_name(tr::now) - : user->shortName()), + .name = user->shortName(), .thumbnail = std::move(userpic), .count = info.count, .unreadCount = info.unreadCount, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 82854b90b..cda8da94d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -671,7 +671,14 @@ void List::validateName(not_null item) { const auto &full = _st.full; const auto &font = full.nameStyle.font; const auto available = AvailableNameWidth(_st); - const auto text = Ui::Text::String(full.nameStyle, element.name); + const auto my = element.skipSmall + ? tr::lng_stories_my_name(tr::now) + : QString(); + const auto use = (my.isEmpty() + || full.nameStyle.font->width(my) > available) + ? element.name + : my; + const auto text = Ui::Text::String(full.nameStyle, use); const auto ratio = style::DevicePixelRatio(); item->nameCacheColor = color->c; item->nameCache = QImage( diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 0d166a1e3..e105cd441 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #endif // LIB_FFMPEG_USE_QT_PRIVATE_API +#include + extern "C" { #include #if !defined DESKTOP_APP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index f6e5c92d5..2b49f8c1c 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1468,7 +1468,7 @@ void InnerWidget::suggestRestrictParticipant( editRestrictions(false, ChatRestrictionsInfo()); }).send(); } - }, &st::menuIconRestrict); + }, &st::menuIconPermissions); } void InnerWidget::restrictParticipant( @@ -1892,8 +1892,7 @@ void InnerWidget::performDrag() { // auto pressedMedia = static_cast(nullptr); // if (auto pressedItem = Element::Pressed()) { // pressedMedia = pressedItem->media(); - // if (_mouseCursorState == CursorState::Date - // || (pressedMedia && pressedMedia->dragItem())) { + // if (_mouseCursorState == CursorState::Date) { // forwardMimeType = u"application/x-td-forward"_q; // session().data().setMimeForwardIds( // session().data().itemOrItsGroup(pressedItem->data())); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index b1acb3727..ffbbc4d84 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -551,8 +551,10 @@ void History::destroyMessage(not_null item) { session().api().cancelLocalItem(item); } - const auto document = [&] { - const auto media = item->media(); + const auto documentToCancel = [&] { + const auto media = item->isAdminLogEntry() + ? nullptr + : item->media(); return media ? media->document() : nullptr; }(); @@ -566,8 +568,8 @@ void History::destroyMessage(not_null item) { Assert(i != end(_messages)); _messages.erase(i); - if (document) { - session().data().documentMessageRemoved(document); + if (documentToCancel) { + session().data().documentMessageRemoved(documentToCancel); } } @@ -1318,7 +1320,15 @@ void History::newItemAdded(not_null item) { Core::App().notifications().schedule(notification); } if (item->out()) { - destroyUnreadBar(); + if (item->isFromScheduled() && unreadCountRefreshNeeded(item->id)) { + if (unreadCountKnown()) { + setUnreadCount(unreadCount() + 1); + } else if (!isForum()) { + owner().histories().requestDialogEntry(this); + } + } else { + destroyUnreadBar(); + } if (!item->unread(this)) { outboxRead(item); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 60469fa16..595c253ce 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1692,9 +1692,7 @@ void HistoryInner::mouseActionStart(const QPoint &screenPos, Qt::MouseButton but if (uponSelected) { _mouseAction = MouseAction::PrepareDrag; // start text drag } else if (!_pressWasInactive) { - const auto media = Element::Pressed()->media(); - if ((media && media->dragItem()) - || _mouseCursorState == CursorState::Date) { + if (_mouseCursorState == CursorState::Date) { _mouseAction = MouseAction::PrepareDrag; // start sticker drag or by-date drag } else { if (dragState.afterSymbol) ++_mouseTextSymbol; @@ -1807,8 +1805,7 @@ std::unique_ptr HistoryInner::prepareDrag() { } else if (view->isHiddenByGroup() && pressedHandler) { forwardIds = MessageIdsList(1, _dragStateItem->fullId()); } else if (const auto media = view->media()) { - if (media->dragItemByHandler(pressedHandler) - || media->dragItem()) { + if (media->dragItemByHandler(pressedHandler)) { forwardIds = MessageIdsList(1, _dragStateItem->fullId()); } } @@ -2412,11 +2409,16 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }, &st::menuIconForward); } if (item->canDelete()) { - _menu->addAction(Ui::DeleteMessageContextAction( - _menu->menu(), - [=] { deleteItem(itemId); }, - item->ttlDestroyAt(), - [=] { _menu = nullptr; })); + const auto callback = [=] { deleteItem(itemId); }; + if (item->isUploading()) { + _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); + } else { + _menu->addAction(Ui::DeleteMessageContextAction( + _menu->menu(), + callback, + item->ttlDestroyAt(), + [=] { _menu = nullptr; })); + } } if (!blockSender && item->suggestReport()) { _menu->addAction(tr::lng_context_report_msg(tr::now), [=] { @@ -2757,9 +2759,12 @@ void HistoryInner::copyContextImage( FullMsgId itemId) { const auto item = session().data().message(itemId); const auto media = photo->activeMediaView(); + const auto restricted = item + ? showCopyMediaRestriction(item) + : IsServerMsgId(itemId.msg); if (photo->isNull() || !media || !media->loaded()) { return; - } else if (!showCopyMediaRestriction(item)) { + } else if (!restricted) { media->setToClipboard(); } } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index d8e7a9fc9..8ed9b9cac 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1474,6 +1474,7 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { if (!_savedLocalEditMediaData && edition.savePreviousMedia) { savePreviousMedia(); } + Assert(!updatingSavedLocalEdit || !isLocalUpdateMedia()); if (edition.isEditHide) { _flags |= MessageFlag::HideEdited; @@ -1493,15 +1494,13 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { if (!edition.useSameMarkup) { setReplyMarkup(base::take(edition.replyMarkup)); } - if (!isLocalUpdateMedia()) { - if (updatingSavedLocalEdit) { - _savedLocalEditMediaData->media = edition.mtpMedia - ? CreateMedia(this, *edition.mtpMedia) - : nullptr; - } else { - removeFromSharedMediaIndex(); - refreshMedia(edition.mtpMedia); - } + if (updatingSavedLocalEdit) { + _savedLocalEditMediaData->media = edition.mtpMedia + ? CreateMedia(this, *edition.mtpMedia) + : nullptr; + } else { + removeFromSharedMediaIndex(); + refreshMedia(edition.mtpMedia); } if (!edition.useSameReactions) { updateReactions(edition.mtpReactions); @@ -1522,9 +1521,7 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { _savedLocalEditMediaData->text = std::move(updatedText); } else { setText(std::move(updatedText)); - } - if (!isLocalUpdateMedia() && !updatingSavedLocalEdit) { - indexAsNewItem(); + addToSharedMediaIndex(); } if (!edition.useSameReplies) { if (!edition.replies.isNull) { @@ -1635,7 +1632,7 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { setPostAuthor(data.vpost_author().value_or_empty()); setIsPinned(data.is_pinned()); contributeToSlowmode(data.vdate().v); - indexAsNewItem(); + addToSharedMediaIndex(); invalidateChatListEntry(); if (const auto period = data.vttl_period(); period && period->v > 0) { applyTTL(data.vdate().v + period->v); @@ -1659,7 +1656,7 @@ void HistoryItem::applySentMessage( }, data.vmedia()); contributeToSlowmode(data.vdate().v); if (!wasAlready) { - indexAsNewItem(); + addToSharedMediaIndex(); } invalidateChatListEntry(); if (const auto period = data.vttl_period(); period && period->v > 0) { @@ -1821,6 +1818,12 @@ Storage::SharedMediaTypesMask HistoryItem::sharedMediaTypes() const { void HistoryItem::indexAsNewItem() { if (isRegular()) { addToUnreadThings(HistoryUnreadThings::AddType::New); + } + addToSharedMediaIndex(); +} + +void HistoryItem::addToSharedMediaIndex() { + if (isRegular()) { if (const auto types = sharedMediaTypes()) { _history->session().storage().add(Storage::SharedMediaAddNew( _history->peer->id, diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 973141ba5..1b15d91e8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -359,6 +359,7 @@ public: [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const; void indexAsNewItem(); + void addToSharedMediaIndex(); void removeFromSharedMediaIndex(); struct NotificationTextOptions { diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 3497ae688..9e82f9d06 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -508,6 +508,8 @@ void HistoryMessageReply::paint( const auto stm = context.messageStyle(); { + const auto opacity = p.opacity(); + const auto outerWidth = w + 2 * x; const auto &bar = !inBubble ? st->msgImgReplyBarColor() : replyToColorKey @@ -518,8 +520,22 @@ void HistoryMessageReply::paint( y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.width(), st::msgReplyBarSize.height(), - w + 2 * x); - const auto opacity = p.opacity(); + outerWidth); + + if (ripple.animation) { + const auto colorOverride = &stm->msgWaveformInactive->c; + p.setOpacity(st::historyPollRippleOpacity); + ripple.animation->paint( + p, + x - st::msgReplyPadding.left(), + y, + outerWidth, + colorOverride); + if (ripple.animation->empty()) { + ripple.animation.reset(); + } + } + p.setOpacity(opacity * kBarAlpha); p.fillRect(rbar, bar); p.setOpacity(opacity); @@ -1059,7 +1075,7 @@ void ReplyKeyboard::Style::paintButton( || button.type == HistoryMessageMarkupButton::Type::Game) { if (const auto data = button.link->getButton()) { if (data->requestId) { - paintButtonLoading(p, st, rect); + paintButtonLoading(p, st, rect, outerWidth, rounding); } } } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 5a8db07c9..75581f969 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/spellcheck_types.h" // LanguageId. #include "ui/empty_userpic.h" #include "ui/effects/animations.h" +#include "ui/effects/ripple_animation.h" #include "ui/chat/message_bubble.h" struct WebPageData; @@ -307,6 +308,11 @@ struct HistoryMessageReply bool topicPost = false; bool storyReply = false; + struct final { + mutable std::unique_ptr animation; + QPoint lastPoint; + } ripple; + }; struct HistoryMessageTranslation @@ -420,7 +426,9 @@ public: virtual void paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, - const QRect &rect) const = 0; + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const = 0; virtual int minButtonWidth( HistoryMessageMarkupButton::Type type) const = 0; diff --git a/Telegram/SourceFiles/history/history_view_highlight_manager.cpp b/Telegram/SourceFiles/history/history_view_highlight_manager.cpp index 137cd30e0..939e56980 100644 --- a/Telegram/SourceFiles/history/history_view_highlight_manager.cpp +++ b/Telegram/SourceFiles/history/history_view_highlight_manager.cpp @@ -115,6 +115,7 @@ void ElementHighlighter::updateMessage() { void ElementHighlighter::clear() { _animation.cancel(); _highlightedMessageId = FullMsgId(); + _lastHighlightedMessageId = FullMsgId(); _queue.clear(); } @@ -139,10 +140,17 @@ float64 ElementHighlighter::AnimationManager::progress() const { } } +MsgId ElementHighlighter::latestSingleHighlightedMsgId() const { + return _highlightedMessageId + ? _highlightedMessageId.msg + : _lastHighlightedMessageId.msg; +} + void ElementHighlighter::AnimationManager::start() { const auto finish = [=] { cancel(); - _parent._highlightedMessageId = FullMsgId(); + _parent._lastHighlightedMessageId = base::take( + _parent._highlightedMessageId); _parent.checkNextHighlight(); }; cancel(); diff --git a/Telegram/SourceFiles/history/history_view_highlight_manager.h b/Telegram/SourceFiles/history/history_view_highlight_manager.h index df43f446b..43372a8b9 100644 --- a/Telegram/SourceFiles/history/history_view_highlight_manager.h +++ b/Telegram/SourceFiles/history/history_view_highlight_manager.h @@ -34,6 +34,7 @@ public: void clear(); [[nodiscard]] float64 progress(not_null item) const; + [[nodiscard]] MsgId latestSingleHighlightedMsgId() const; private: void checkNextHighlight(); @@ -47,10 +48,12 @@ private: [[nodiscard]] float64 progress() const; void start(); void cancel(); + private: ElementHighlighter &_parent; Ui::Animations::Simple _simple; std::optional _timer; + }; const not_null _data; @@ -58,9 +61,11 @@ private: const RepaintView _repaintView; FullMsgId _highlightedMessageId; + FullMsgId _lastHighlightedMessageId; std::deque _queue; AnimationManager _animation; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 221438bb1..cf9466ba6 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1809,7 +1809,10 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived( void HistoryWidget::tryProcessKeyInput(not_null e) { e->accept(); keyPressEvent(e); - if (!e->isAccepted() && _canSendTexts && _field->isVisible()) { + if (!e->isAccepted() + && _canSendTexts + && _field->isVisible() + && !e->text().isEmpty()) { _field->setFocusFast(); QCoreApplication::sendEvent(_field->rawTextEdit(), e); } @@ -1918,12 +1921,6 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; - // Save links from _field to _parsedLinks without generating preview. - _previewState = Data::PreviewState::Cancelled; - _fieldLinksParser->parseNow(); - _parsedLinks = _fieldLinksParser->list().current(); - _previewState = draft->previewState; - _processingReplyItem = _replyEditMsg = nullptr; _processingReplyId = _replyToId = 0; setEditMsgId(editMsgId); @@ -1941,6 +1938,19 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { _processingReplyId = draft ? draft->msgId : MsgId(); processReply(); } + + // Save links from _field to _parsedLinks without generating preview. + _previewState = Data::PreviewState::Cancelled; + if (_editMsgId) { + _fieldLinksParser->setDisabled(!_replyEditMsg + || (_replyEditMsg->media() + && !_replyEditMsg->media()->webpage())); + } + _fieldLinksParser->parseNow(); + _parsedLinks = _fieldLinksParser->list().current(); + _previewState = draft->previewState; + checkPreview(); + return true; } @@ -2017,18 +2027,26 @@ void HistoryWidget::showHistory( return; } if (!IsServerMsgId(showAtMsgId) + && !IsClientMsgId(showAtMsgId) && !IsServerMsgId(-showAtMsgId)) { // To end or to unread. destroyUnreadBar(); } const auto canShowNow = _history->isReadyFor(showAtMsgId); if (!canShowNow) { - DEBUG_LOG(("JumpToEnd(%1, %2, %3): Showing delayed at %4." - ).arg(_history->peer->name() - ).arg(_history->inboxReadTillId().bare - ).arg(Logs::b(_history->loadedAtBottom()) - ).arg(showAtMsgId.bare)); - delayedShowAt(showAtMsgId); + if (!_firstLoadRequest) { + DEBUG_LOG(("JumpToEnd(%1, %2, %3): Showing delayed at %4." + ).arg(_history->peer->name() + ).arg(_history->inboxReadTillId().bare + ).arg(Logs::b(_history->loadedAtBottom()) + ).arg(showAtMsgId.bare)); + delayedShowAt(showAtMsgId); + } else if (_showAtMsgId != showAtMsgId) { + clearAllLoadRequests(); + setMsgId(showAtMsgId); + firstLoadMessages(); + doneShow(); + } } else { _history->forgetScrollState(); if (_migrated) { @@ -2472,6 +2490,9 @@ void HistoryWidget::registerDraftSource() { void HistoryWidget::setEditMsgId(MsgId msgId) { unregisterDraftSources(); _editMsgId = msgId; + if (_fieldLinksParser && !_editMsgId) { + _fieldLinksParser->setDisabled(false); + } if (!msgId) { _canReplaceMedia = false; } @@ -5632,6 +5653,7 @@ int HistoryWidget::countInitialScrollTop() { return _list->historyScrollTop(); } else if (_showAtMsgId && (IsServerMsgId(_showAtMsgId) + || IsClientMsgId(_showAtMsgId) || IsServerMsgId(-_showAtMsgId))) { const auto item = getItemFromHistoryOrMigrated(_showAtMsgId); const auto itemTop = _list->itemTop(item); @@ -6231,6 +6253,16 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { return; } _scroll->keyPressEvent(e); + } else if (e->key() == Qt::Key_Up + && commonModifiers == Qt::ControlModifier) { + if (!replyToPreviousMessage()) { + e->ignore(); + } + } else if (e->key() == Qt::Key_Down + && commonModifiers == Qt::ControlModifier) { + if (!replyToNextMessage()) { + e->ignore(); + } } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { if (!_botStart->isHidden()) { sendBotStartCommand(); @@ -6287,20 +6319,28 @@ bool HistoryWidget::replyToPreviousMessage() { if (!_history || _editMsgId || _history->isForum()) { return false; } - const auto fullId = FullMsgId(_history->peer->id, _replyToId); + const auto fullId = FullMsgId( + _history->peer->id, + _field->isVisible() + ? _replyToId + : _highlighter.latestSingleHighlightedMsgId()); if (const auto item = session().data().message(fullId)) { if (const auto view = item->mainView()) { if (const auto previousView = view->previousDisplayedInBlocks()) { const auto previous = previousView->data(); controller()->showMessage(previous); - replyToMessage(previous); + if (_field->isVisible()) { + replyToMessage(previous); + } return true; } } } else if (const auto previousView = _history->findLastDisplayed()) { const auto previous = previousView->data(); controller()->showMessage(previous); - replyToMessage(previous); + if (_field->isVisible()) { + replyToMessage(previous); + } return true; } return false; @@ -6310,13 +6350,19 @@ bool HistoryWidget::replyToNextMessage() { if (!_history || _editMsgId || _history->isForum()) { return false; } - const auto fullId = FullMsgId(_history->peer->id, _replyToId); + const auto fullId = FullMsgId( + _history->peer->id, + _field->isVisible() + ? _replyToId + : _highlighter.latestSingleHighlightedMsgId()); if (const auto item = session().data().message(fullId)) { if (const auto view = item->mainView()) { if (const auto nextView = view->nextDisplayedInBlocks()) { const auto next = nextView->data(); controller()->showMessage(next); - replyToMessage(next); + if (_field->isVisible()) { + replyToMessage(next); + } } else { _highlighter.clear(); cancelReply(false); @@ -7514,7 +7560,7 @@ void HistoryWidget::handlePeerUpdate() { if (!channel->mgInfo->botStatus) { session().api().chatParticipants().requestBots(channel); } - if (channel->mgInfo->admins.empty()) { + if (!channel->mgInfo->adminsLoaded) { session().api().chatParticipants().requestAdmins(channel); } } @@ -7598,7 +7644,7 @@ void HistoryWidget::escape() { cancelInlineBot(); } else if (_editMsgId) { if (_replyEditMsg - && PrepareEditText(_replyEditMsg) != _field->getTextWithTags()) { + && EditTextChanged(_replyEditMsg, _field->getTextWithTags())) { controller()->show(Ui::MakeConfirmBox({ .text = tr::lng_cancel_edit_post_sure(), .confirmed = crl::guard(this, [this](Fn &&close) { diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index 06f295ee8..8af04e830 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -41,6 +41,7 @@ struct SetHistoryArgs { Fn sendActionFactory; rpl::producer slowmodeSecondsLeft; rpl::producer sendDisabledBySlowmode; + rpl::producer liked; rpl::producer> writeRestriction; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 4901c3bf9..e6a4717d8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -129,7 +129,7 @@ public: [[nodiscard]] Data::PreviewState state() const; void setState(Data::PreviewState value); - void refreshState(Data::PreviewState value); + void refreshState(Data::PreviewState value, bool disable); [[nodiscard]] rpl::producer<> paintRequests() const; [[nodiscard]] rpl::producer titleChanges() const; @@ -219,12 +219,16 @@ void WebpageProcessor::setState(Data::PreviewState value) { _previewState = value; } -void WebpageProcessor::refreshState(Data::PreviewState value) { +void WebpageProcessor::refreshState( + Data::PreviewState value, + bool disable) { // Save links from _field to _parsedLinks without generating preview. _previewState = Data::PreviewState::Cancelled; + _fieldLinksParser.setDisabled(disable); _fieldLinksParser.parseNow(); _parsedLinks = _fieldLinksParser.list().current(); _previewState = value; + checkPreview(); } void WebpageProcessor::cancel() { @@ -1069,9 +1073,10 @@ ComposeControls::ComposeControls( , _wrap(std::make_unique(parent)) , _writeRestricted(std::make_unique(parent)) , _send(std::make_shared(_wrap.get(), _st.send)) -, _attachToggle(Ui::CreateChild( - _wrap.get(), - _st.attach)) +, _like(_features.likes + ? Ui::CreateChild(_wrap.get(), _st.like) + : nullptr) +, _attachToggle(Ui::CreateChild(_wrap.get(), _st.attach)) , _tabbedSelectorToggle(Ui::CreateChild( _wrap.get(), _st.emoji)) @@ -1138,6 +1143,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { | rpl::then(std::move(args.slowmodeSecondsLeft)); _sendDisabledBySlowmode = rpl::single(false) | rpl::then(std::move(args.sendDisabledBySlowmode)); + _liked = args.liked ? std::move(args.liked) : rpl::single(false); _writeRestriction = rpl::single(std::optional()) | rpl::then(std::move(args.writeRestriction)); const auto history = *args.history; @@ -1153,6 +1159,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { initWebpageProcess(); initForwardProcess(); updateBotCommandShown(); + updateLikeShown(); updateMessagesTTLShown(); updateControlsGeometry(_wrap->size()); updateControlsVisibility(); @@ -1238,9 +1245,12 @@ bool ComposeControls::focus() { return true; } +bool ComposeControls::focused() const { + return Ui::InFocusChain(_wrap.get()); +} + rpl::producer ComposeControls::focusedValue() const { - return rpl::single(Ui::InFocusChain(_wrap.get())) - | rpl::then(_focusChanges.events()); + return rpl::single(focused()) | rpl::then(_focusChanges.events()); } rpl::producer ComposeControls::tabbedPanelShownValue() const { @@ -1559,6 +1569,15 @@ void ComposeControls::init() { _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); } + if (_like) { + _like->setClickedCallback([=] { _likeToggled.fire({}); }); + _liked.value( + ) | rpl::start_with_next([=](bool liked) { + const auto icon = liked ? &_st.liked : nullptr; + _like->setIconOverride(icon, icon); + }, _like->lifetime()); + } + _wrap->sizeValue( ) | rpl::start_with_next([=](QSize size) { updateControlsGeometry(size); @@ -1980,7 +1999,7 @@ void ComposeControls::fieldChanged() { if (!_hasSendText.current() && _preview) { _preview->setState(Data::PreviewState::Allowed); } - if (updateBotCommandShown()) { + if (updateBotCommandShown() || updateLikeShown()) { updateControlsVisibility(); updateControlsGeometry(_wrap->size()); } @@ -2148,6 +2167,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _header->editMessage({}); _header->replyToMessage({}); + _preview->refreshState(Data::PreviewState::Allowed, false); _canReplaceMedia = false; _photoEditMedia = nullptr; return; @@ -2161,13 +2181,15 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { draft->cursor.applyTo(_field); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; if (_preview) { - _preview->refreshState(draft->previewState); + const auto disablePreview = (editDraft != nullptr); + _preview->refreshState(draft->previewState, disablePreview); } if (draft == editDraft) { const auto resolve = [=] { if (const auto item = _history->owner().message(editingId)) { const auto media = item->media(); + const auto disablePreview = media && !media->webpage(); _canReplaceMedia = media && media->allowsEditMedia(); _photoEditMedia = (_canReplaceMedia && _regularWindow @@ -2181,6 +2203,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { item->fullId()); } _header->editMessage(editingId, _photoEditMedia != nullptr); + _preview->refreshState(_preview->state(), disablePreview); return true; } _canReplaceMedia = false; @@ -2521,6 +2544,7 @@ void ComposeControls::updateControlsGeometry(QSize size) { - st::historySendRight - _send->width() - _tabbedSelectorToggle->width() + - (_likeShown ? _like->width() : 0) - (_botCommandShown ? _botCommandStart->width() : 0) - (_silent ? _silent->width() : 0) - (_ttlInfo ? _ttlInfo->width() : 0); @@ -2560,6 +2584,12 @@ void ComposeControls::updateControlsGeometry(QSize size) { right += _send->width(); _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); + if (_like) { + _like->moveToRight(right, buttonsTop); + if (_likeShown) { + right += _like->width(); + } + } if (_botCommandStart) { _botCommandStart->moveToRight(right, buttonsTop); if (_botCommandShown) { @@ -2584,6 +2614,9 @@ void ComposeControls::updateControlsVisibility() { if (_botCommandStart) { _botCommandStart->setVisible(_botCommandShown); } + if (_like) { + _like->setVisible(_likeShown); + } if (_ttlInfo) { _ttlInfo->show(); } @@ -2598,6 +2631,15 @@ void ComposeControls::updateControlsVisibility() { } } +bool ComposeControls::updateLikeShown() { + auto shown = _like && !HasSendText(_field); + if (_likeShown != shown) { + _likeShown = shown; + return true; + } + return false; +} + bool ComposeControls::updateBotCommandShown() { auto shown = false; const auto peer = _history ? _history->peer.get() : nullptr; @@ -2903,6 +2945,26 @@ void ComposeControls::cancelEditMessage() { saveDraft(); } +void ComposeControls::maybeCancelEditMessage() { + Expects(_history != nullptr); + + const auto item = _history->owner().message(_header->editMsgId()); + if (item && EditTextChanged(item, _field->getTextWithTags())) { + const auto guard = _field.get(); + _show->show(Ui::MakeConfirmBox({ + .text = tr::lng_cancel_edit_post_sure(), + .confirmed = crl::guard(guard, [this](Fn &&close) { + cancelEditMessage(); + close(); + }), + .confirmText = tr::lng_cancel_edit_post_yes(), + .cancelText = tr::lng_cancel_edit_post_no(), + })); + } else { + cancelEditMessage(); + } +} + void ComposeControls::replyToMessage(FullMsgId id) { Expects(_history != nullptr); Expects(draftKeyCurrent() != Data::DraftKey::None()); @@ -2978,7 +3040,7 @@ bool ComposeControls::handleCancelRequest() { _autocomplete->hideAnimated(); return true; } else if (isEditingMessage()) { - cancelEditMessage(); + maybeCancelEditMessage(); return true; } else if (readyToForward()) { cancelForward(); @@ -2991,7 +3053,7 @@ bool ComposeControls::handleCancelRequest() { } void ComposeControls::tryProcessKeyInput(not_null e) { - if (_field->isVisible()) { + if (_field->isVisible() && !e->text().isEmpty()) { _field->setFocusFast(); QCoreApplication::sendEvent(_field->rawTextEdit(), e); } @@ -3100,6 +3162,10 @@ rpl::producer> ComposeControls::viewportEvents() const { return _voiceRecordBar->lockViewportEvents(); } +rpl::producer<> ComposeControls::likeToggled() const { + return _likeToggled.events(); +} + bool ComposeControls::isRecording() const { return _voiceRecordBar->isRecording(); } @@ -3123,6 +3189,12 @@ rpl::producer ComposeControls::fieldMenuShownValue() const { return _field->menuShownValue(); } +not_null ComposeControls::likeAnimationTarget() const { + Expects(_like != nullptr); + + return _like; +} + bool ComposeControls::preventsClose(Fn &&continueCallback) const { if (_voiceRecordBar->isActive()) { _voiceRecordBar->showDiscardBox(std::move(continueCallback)); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index de7002012..06d8ae9a8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -147,6 +147,7 @@ public: [[nodiscard]] int heightCurrent() const; bool focus(); + [[nodiscard]] bool focused() const; [[nodiscard]] rpl::producer focusedValue() const; [[nodiscard]] rpl::producer tabbedPanelShownValue() const; [[nodiscard]] rpl::producer<> cancelRequests() const; @@ -161,6 +162,7 @@ public: [[nodiscard]] rpl::producer inlineResultChosen() const; [[nodiscard]] rpl::producer sendActionUpdates() const; [[nodiscard]] rpl::producer> viewportEvents() const; + [[nodiscard]] rpl::producer<> likeToggled() const; [[nodiscard]] auto scrollKeyEvents() const -> rpl::producer>; [[nodiscard]] auto editLastMessageRequests() const @@ -194,6 +196,7 @@ public: void editMessage(FullMsgId id); void cancelEditMessage(); + void maybeCancelEditMessage(); // Confirm if changed and cancel. void replyToMessage(FullMsgId id); void cancelReplyMessage(); @@ -221,6 +224,7 @@ public: [[nodiscard]] rpl::producer recordingActiveValue() const; [[nodiscard]] rpl::producer hasSendTextValue() const; [[nodiscard]] rpl::producer fieldMenuShownValue() const; + [[nodiscard]] not_null likeAnimationTarget() const; void applyCloudDraft(); void applyDraft( @@ -292,6 +296,7 @@ private: bool showRecordButton() const; void drawRestrictedWrite(QPainter &p, const QString &error); bool updateBotCommandShown(); + bool updateLikeShown(); void cancelInlineBot(); void clearInlineBot(); @@ -344,6 +349,7 @@ private: Fn _sendActionFactory; rpl::variable _slowmodeSecondsLeft; rpl::variable _sendDisabledBySlowmode; + rpl::variable _liked; rpl::variable> _writeRestriction; rpl::variable _hidden; Mode _mode = Mode::Normal; @@ -354,6 +360,7 @@ private: std::optional _backgroundRect; const std::shared_ptr _send; + Ui::IconButton * const _like = nullptr; const not_null _attachToggle; std::unique_ptr _replaceMedia; const not_null _tabbedSelectorToggle; @@ -386,6 +393,7 @@ private: rpl::event_stream> _scrollKeyEvents; rpl::event_stream> _editLastMessageRequests; rpl::event_stream> _attachRequests; + rpl::event_stream<> _likeToggled; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; rpl::variable _recording; @@ -407,6 +415,7 @@ private: mtpRequestId _inlineBotResolveRequestId = 0; bool _isInlineBot = false; bool _botCommandShown = false; + bool _likeShown = false; FullMsgId _editingId; std::shared_ptr _photoEditMedia; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index dac19c310..5bde649a4 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1228,7 +1228,7 @@ void AddPollActions( .confirmText = tr::lng_polls_stop_sure(), .cancelText = tr::lng_cancel(), })); - }, &st::menuIconStopPoll); + }, &st::menuIconRemove); } } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 1e2648125..3de0cb20e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1302,16 +1302,8 @@ auto ListWidget::itemUnderPressSelection() const : _selected.end(); } -bool ListWidget::requiredToStartDragging( - not_null view) const { - if (_mouseCursorState == CursorState::Date) { - return true; - } else if (const auto media = view->media()) { - if (media->dragItem()) { - return true; - } - } - return false; +bool ListWidget::requiredToStartDragging(not_null view) const { + return (_mouseCursorState == CursorState::Date); } bool ListWidget::isPressInSelectedText(TextState state) const { @@ -3625,8 +3617,7 @@ std::unique_ptr ListWidget::prepareDrag() { } } else if (const auto media = pressedView->media()) { if (pressedView->data()->allowsForward() - && (media->dragItemByHandler(pressedHandler) - || media->dragItem())) { + && media->dragItemByHandler(pressedHandler)) { forwardIds = MessageIdsList(1, exactItem->fullId()); } } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index ad06f53c4..e6f7fbcad 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -21,10 +21,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_view_button.h" // ViewButton. #include "history/history.h" #include "boxes/share_box.h" +#include "ui/effects/glare.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/chat/message_bubble.h" #include "ui/chat/chat_style.h" +#include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/text/text_entity.h" #include "ui/cached_round_corners.h" @@ -65,7 +67,7 @@ std::optional ExtractController( class KeyboardStyle : public ReplyKeyboard::Style { public: - using ReplyKeyboard::Style::Style; + KeyboardStyle(const style::BotKeyboardButton &st); Images::CornersMaskRef buttonRounding( Ui::BubbleRounding outer, @@ -93,11 +95,29 @@ protected: void paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, - const QRect &rect) const override; + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const override; int minButtonWidth(HistoryMessageMarkupButton::Type type) const override; +private: + using BubbleRoundingKey = uchar; + mutable base::flat_map _cachedBg; + mutable base::flat_map _cachedOutline; + mutable std::unique_ptr _glare; + rpl::lifetime _lifetime; + }; +KeyboardStyle::KeyboardStyle(const style::BotKeyboardButton &st) +: ReplyKeyboard::Style(st) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _cachedBg = {}; + _cachedOutline = {}; + }, _lifetime); +} + void KeyboardStyle::startPaint( QPainter &p, const Ui::ChatStyle *st) const { @@ -112,6 +132,15 @@ const style::TextStyle &KeyboardStyle::textStyle() const { void KeyboardStyle::repaint(not_null item) const { item->history()->owner().requestItemRepaint(item); + if (_glare && !_glare->glare.birthTime) { + constexpr auto kTimeout = crl::time(0); + constexpr auto kDuration = crl::time(1100); + _glare->validate( + st::premiumButtonFg->c, + [=] { repaint(item); }, + kTimeout, + kDuration); + } } Images::CornersMaskRef KeyboardStyle::buttonRounding( @@ -143,15 +172,42 @@ void KeyboardStyle::paintButtonBg( float64 howMuchOver) const { Expects(st != nullptr); - const auto sti = &st->imageStyle(false); - const auto &small = sti->msgServiceBgCornersSmall; - const auto &large = sti->msgServiceBgCornersLarge; - auto corners = Ui::CornersPixmaps(); using Corner = Ui::BubbleCornerRounding; - for (auto i = 0; i != 4; ++i) { - corners.p[i] = (rounding[i] == Corner::Large ? large : small).p[i]; + auto &cachedBg = _cachedBg[rounding.key()]; + + if (cachedBg.isNull() + || cachedBg.width() != (rect.width() * style::DevicePixelRatio())) { + cachedBg = QImage( + rect.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + cachedBg.setDevicePixelRatio(style::DevicePixelRatio()); + cachedBg.fill(Qt::transparent); + { + auto painter = QPainter(&cachedBg); + + const auto sti = &st->imageStyle(false); + const auto &small = sti->msgServiceBgCornersSmall; + const auto &large = sti->msgServiceBgCornersLarge; + auto corners = Ui::CornersPixmaps(); + int radiuses[4]; + for (auto i = 0; i != 4; ++i) { + const auto isLarge = (rounding[i] == Corner::Large); + corners.p[i] = (isLarge ? large : small).p[i]; + radiuses[i] = Ui::CachedCornerRadiusValue(isLarge + ? Ui::CachedCornerRadius::BubbleLarge + : Ui::CachedCornerRadius::BubbleSmall); + } + const auto r = Rect(rect.size()); + _cachedOutline[rounding.key()] = Ui::ComplexRoundedRectPath( + r - Margins(st::lineWidth), + radiuses[0], + radiuses[1], + radiuses[2], + radiuses[3]); + Ui::FillRoundRect(painter, r, sti->msgServiceBg, corners); + } } - Ui::FillRoundRect(p, rect, sti->msgServiceBg, corners); + p.drawImage(rect.topLeft(), cachedBg); if (howMuchOver > 0) { auto o = p.opacity(); p.setOpacity(o * howMuchOver); @@ -195,11 +251,74 @@ void KeyboardStyle::paintButtonIcon( void KeyboardStyle::paintButtonLoading( QPainter &p, const Ui::ChatStyle *st, - const QRect &rect) const { + const QRect &rect, + int outerWidth, + Ui::BubbleRounding rounding) const { Expects(st != nullptr); - const auto &icon = st->historySendingInvertedIcon(); - icon.paint(p, rect.x() + rect.width() - icon.width() - st::msgBotKbIconPadding, rect.y() + rect.height() - icon.height() - st::msgBotKbIconPadding, rect.x() * 2 + rect.width()); + if (anim::Disabled()) { + const auto &icon = st->historySendingInvertedIcon(); + icon.paint( + p, + rect::right(rect) - icon.width() - st::msgBotKbIconPadding, + rect::bottom(rect) - icon.height() - st::msgBotKbIconPadding, + rect.x() * 2 + rect.width()); + return; + } + + const auto cacheKey = rounding.key(); + auto &cachedBg = _cachedBg[cacheKey]; + if (!cachedBg.isNull()) { + if (_glare && _glare->glare.birthTime) { + const auto progress = _glare->progress(crl::now()); + const auto w = _glare->width; + const auto h = rect.height(); + const auto x = (-w) + (w * 2) * progress; + + auto frame = cachedBg; + frame.fill(Qt::transparent); + { + auto painter = QPainter(&frame); + auto hq = PainterHighQualityEnabler(painter); + painter.setPen(Qt::NoPen); + painter.drawTiledPixmap(x, 0, w, h, _glare->pixmap, 0, 0); + + auto path = QPainterPath(); + path.addRect(Rect(rect.size())); + path -= _cachedOutline[cacheKey]; + + constexpr auto kBgOutlineAlpha = 0.5; + constexpr auto kFgOutlineAlpha = 0.8; + const auto &c = st::premiumButtonFg->c; + painter.setPen(Qt::NoPen); + painter.setBrush(c); + painter.setOpacity(kBgOutlineAlpha); + painter.drawPath(path); + auto gradient = QLinearGradient(-w, 0, w * 2, 0); + { + constexpr auto kShiftLeft = 0.01; + constexpr auto kShiftRight = 0.99; + auto stops = _glare->computeGradient(c).stops(); + stops[1] = { + std::clamp(progress, kShiftLeft, kShiftRight), + QColor(c.red(), c.green(), c.blue(), kFgOutlineAlpha), + }; + gradient.setStops(std::move(stops)); + } + painter.setBrush(QBrush(gradient)); + painter.setOpacity(1); + painter.drawPath(path); + + painter.setCompositionMode( + QPainter::CompositionMode_DestinationIn); + painter.drawImage(0, 0, cachedBg); + } + p.drawImage(rect.x(), rect.y(), frame); + } else { + _glare = std::make_unique(); + _glare->width = outerWidth; + } + } } int KeyboardStyle::minButtonWidth( @@ -908,9 +1027,13 @@ void Message::draw(Painter &p, const PaintContext &context) const { auto trect = inner.marginsRemoved(st::msgPadding); - const auto reactionsTop = (reactionsInBubble && !_viewButton) - ? st::mediaInBubbleSkip + const auto additionalInfoSkip = (mediaDisplayed + && !media->additionalInfoString().isEmpty()) + ? st::msgDateFont->height : 0; + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? (additionalInfoSkip + st::mediaInBubbleSkip) + : additionalInfoSkip; const auto reactionsHeight = reactionsInBubble ? (reactionsTop + _reactions->height()) : 0; @@ -1600,6 +1723,9 @@ void Message::clickHandlerPressedChanged( toggleTopicButtonRipple(pressed); } else if (_viewButton) { _viewButton->checkLink(handler, pressed); + } else if (const auto reply = displayedReply(); + reply && (handler == reply->replyToLink())) { + toggleReplyRipple(pressed); } } @@ -1639,6 +1765,59 @@ void Message::toggleRightActionRipple(bool pressed) { } } +void Message::toggleReplyRipple(bool pressed) { + const auto reply = displayedReply(); + if (!reply) { + return; + } + + if (pressed) { + if (!reply->ripple.animation && !unwrapped()) { + const auto smallTop = displayFromName() + || displayedTopicButton() + || displayForwardedFrom(); + const auto rounding = countBubbleRounding(); + + using Corner = Ui::BubbleCornerRounding; + using Radius = Ui::CachedCornerRadius; + const auto &small = Ui::CachedCornersMasks(Radius::ThumbSmall); + const auto &large = Ui::CachedCornersMasks(Radius::ThumbLarge); + const auto corners = std::array{{ + ((smallTop || (rounding.topLeft == Corner::Small)) + ? small + : large)[0], + ((smallTop || (rounding.topRight == Corner::Small)) + ? small + : large)[1], + small[2], + small[3], + }}; + + const auto &padding = st::msgReplyPadding; + const auto geometry = countGeometry(); + const auto item = data(); + const auto size = QSize( + geometry.width() + - padding.left() / 2 + - padding.right(), + st::msgReplyBarSize.height() + + padding.top() + + padding.bottom()); + reply->ripple.animation = std::make_unique( + st::defaultRippleAnimation, + Images::Round( + Ui::RippleAnimation::MaskByDrawer(size, true, nullptr), + corners), + [=] { item->history()->owner().requestItemRepaint(item); }); + } + if (reply->ripple.animation) { + reply->ripple.animation->add(reply->ripple.lastPoint); + } + } else if (reply->ripple.animation) { + reply->ripple.animation->lastStop(); + } +} + BottomRippleMask Message::bottomRippleMask(int buttonHeight) const { using namespace Ui; using namespace Images; @@ -1870,9 +2049,13 @@ TextState Message::textState( return result; } auto trect = inner.marginsRemoved(st::msgPadding); - const auto reactionsTop = (reactionsInBubble && !_viewButton) - ? st::mediaInBubbleSkip + const auto additionalInfoSkip = (mediaDisplayed + && !media->additionalInfoString().isEmpty()) + ? st::msgDateFont->height : 0; + const auto reactionsTop = (reactionsInBubble && !_viewButton) + ? (additionalInfoSkip + st::mediaInBubbleSkip) + : additionalInfoSkip; const auto reactionsHeight = reactionsInBubble ? (reactionsTop + _reactions->height()) : 0; @@ -2268,9 +2451,15 @@ bool Message::getStateReplyInfo( if (auto reply = displayedReply()) { int32 h = st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); if (point.y() >= trect.top() && point.y() < trect.top() + h) { + const auto g = QRect( + trect.x(), + trect.y() + st::msgReplyPadding.top(), + trect.width(), + st::msgReplyBarSize.height()); if ((reply->replyToMsg || reply->replyToStory) - && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { + && g.contains(point)) { outResult->link = reply->replyToLink(); + reply->ripple.lastPoint = point - g.topLeft(); } return true; } @@ -3602,6 +3791,10 @@ int Message::resizeContentGetHeight(int newWidth) { if (reactionsInBubble) { if (!mediaDisplayed || _viewButton) { newHeight += st::mediaInBubbleSkip; + } else if (!media->additionalInfoString().isEmpty()) { + // In round videos in a web page status text is painted + // in the bottom left corner, reactions should be below. + newHeight += st::msgDateFont->height; } newHeight += _reactions->height(); } @@ -3634,6 +3827,7 @@ int Message::resizeContentGetHeight(int newWidth) { if (reply) { reply->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right()); + reply->ripple.animation = nullptr; newHeight += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } if (needInfoDisplay()) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index f9fd8afa7..2c0ebaf0e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -187,7 +187,8 @@ private: void createTopicButtonRipple(); void toggleRightActionRipple(bool pressed); - void createRightActionRipple(); + + void toggleReplyRipple(bool pressed); void paintCommentsButton( Painter &p, diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 4207c59ca..92b4a4744 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -310,7 +310,7 @@ void TranslateBar::setup(not_null history) { const auto updateLabelGeometry = [=] { const auto full = _wrap.width() - icon->width(); const auto skip = st::semiboldFont->spacew * 2; - const auto natural = label->naturalWidth(); + const auto natural = label->textMaxWidth(); const auto top = [&] { return (_wrap.height() - label->height()) / 2; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 0bee2fbf0..074f89dab 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1064,8 +1064,18 @@ TextState Gif::textState(QPoint point, StateRequest request) const { recth -= skip; } if (reply) { - if (QRect(rectx, recty, rectw, recth).contains(point)) { + const auto replyRect = QRect(rectx, recty, rectw, recth); + if (replyRect.contains(point)) { result.link = reply->replyToLink(); + reply->ripple.lastPoint = point - replyRect.topLeft(); + if (!reply->ripple.animation) { + reply->ripple.animation = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + replyRect.size(), + st::roundRadiusSmall), + [=] { item->history()->owner().requestItemRepaint(item); }); + } return result; } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 108dffc83..768107f65 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -126,11 +126,6 @@ public: [[nodiscard]] virtual bool toggleSelectionByHandlerClick( const ClickHandlerPtr &p) const = 0; - // if we press and drag on this media should we drag the item - [[nodiscard]] virtual bool dragItem() const { - return false; - } - [[nodiscard]] virtual TextSelection adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index cd3674de1..4ff49e604 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_media_unwrapped.h" +#include "data/data_session.h" +#include "history/history.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_sticker.h" #include "history/view/history_view_element.h" @@ -460,8 +462,18 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { recth -= skip; } if (reply) { - if (QRect(rectx, recty, rectw, recth).contains(point)) { + const auto replyRect = QRect(rectx, recty, rectw, recth); + if (replyRect.contains(point)) { result.link = reply->replyToLink(); + reply->ripple.lastPoint = point - replyRect.topLeft(); + if (!reply->ripple.animation) { + reply->ripple.animation = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + replyRect.size(), + st::roundRadiusSmall), + [=] { item->history()->owner().requestItemRepaint(item); }); + } return result; } } @@ -507,6 +519,12 @@ bool UnwrappedMedia::hasTextForCopy() const { return _content->hasTextForCopy(); } +bool UnwrappedMedia::dragItemByHandler( + const ClickHandlerPtr &p) const { + const auto reply = _parent->displayedReply(); + return !(reply && (reply->replyToLink() == p)); +} + QRect UnwrappedMedia::contentRectForReactions() const { const auto inWebPage = (_parent->media() != this); if (inWebPage) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h index 1ebe55e71..ca80a80c1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h @@ -73,12 +73,7 @@ public: bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { return true; } - bool dragItem() const override { - return true; - } - bool dragItemByHandler(const ClickHandlerPtr &p) const override { - return true; - } + bool dragItemByHandler(const ClickHandlerPtr &p) const override; DocumentData *getDocument() const override { return _content->document(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index cec353017..4fb1cef59 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -829,6 +829,10 @@ bool WebPage::isDisplayed() const { && !item->Has(); } +QString WebPage::additionalInfoString() const { + return _attach ? _attach->additionalInfoString() : QString(); +} + TextForMimeData WebPage::selectedText(TextSelection selection) const { auto siteNameResult = _siteName.toTextForMimeData(selection); auto titleResult = _title.toTextForMimeData( diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.h b/Telegram/SourceFiles/history/view/media/history_view_web_page.h index db7bea091..86005d13e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.h +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.h @@ -40,6 +40,7 @@ public: bool hasTextForCopy() const override { return false; // we do not add _title and _description in FullSelection text copy. } + QString additionalInfoString() const override; bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { return _attach && _attach->toggleSelectionByHandlerClick(p); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 4aa2370a8..e24577962 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -328,6 +328,10 @@ void Selector::updateShowState( update(); } +int Selector::countAppearedWidth(float64 progress) const { + return anim::interpolate(_skipx * 2 + _size, _inner.width(), progress); +} + void Selector::paintAppearing(QPainter &p) { Expects(_strip != nullptr); @@ -340,10 +344,7 @@ void Selector::paintAppearing(QPainter &p) { _paintBuffer.fill(_st.bg->c); auto q = QPainter(&_paintBuffer); const auto extents = extentsForShadow(); - const auto appearedWidth = anim::interpolate( - _skipx * 2 + _size, - _inner.width(), - _appearProgress); + const auto appearedWidth = countAppearedWidth(_appearProgress); const auto fullWidth = _inner.x() + appearedWidth + extents.right(); const auto size = QSize(fullWidth, _outer.height()); @@ -1035,21 +1036,65 @@ AttachSelectorResult AttachSelectorToMenu( Fn chosen, Fn showPremiumPromo, IconFactory iconFactory) { - auto reactions = Data::LookupPossibleReactions(item); + const auto result = AttachSelectorToMenu( + menu, + desiredPosition, + st::reactPanelEmojiPan, + controller->uiShow(), + Data::LookupPossibleReactions(item), + std::move(iconFactory)); + if (!result) { + return result.error(); + } + const auto selector = *result; + const auto itemId = item->fullId(); + + selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) { + menu->hideMenu(); + reaction.context = itemId; + chosen(std::move(reaction)); + }, selector->lifetime()); + + selector->premiumPromoChosen() | rpl::start_with_next([=] { + menu->hideMenu(); + showPremiumPromo(itemId); + }, selector->lifetime()); + + const auto weak = base::make_weak(controller); + controller->enableGifPauseReason( + Window::GifPauseReason::MediaPreview); + QObject::connect(menu.get(), &QObject::destroyed, [weak] { + if (const auto strong = weak.get()) { + strong->disableGifPauseReason( + Window::GifPauseReason::MediaPreview); + } + }); + + return AttachSelectorResult::Attached; +} + +auto AttachSelectorToMenu( + not_null menu, + QPoint desiredPosition, + const style::EmojiPan &st, + std::shared_ptr show, + const Data::PossibleItemReactionsRef &reactions, + IconFactory iconFactory) +-> base::expected, AttachSelectorResult> { if (reactions.recent.empty() && !reactions.morePremiumAvailable) { - return AttachSelectorResult::Skipped; + return base::make_unexpected(AttachSelectorResult::Skipped); } const auto withSearch = reactions.customAllowed; const auto selector = Ui::CreateChild( menu.get(), - st::reactPanelEmojiPan, - controller->uiShow(), + st, + std::move(show), std::move(reactions), std::move(iconFactory), [=](bool fast) { menu->hideMenu(fast); }, false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { - return AttachSelectorResult::Failed; + return base::make_unexpected(AttachSelectorResult::Failed); } if (withSearch) { Ui::Platform::FixPopupMenuNativeEmojiPopup(menu); @@ -1066,19 +1111,6 @@ AttachSelectorResult AttachSelectorToMenu( selector->initGeometry(selectorInnerTop); selector->show(); - const auto itemId = item->fullId(); - - selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) { - menu->hideMenu(); - reaction.context = itemId; - chosen(std::move(reaction)); - }, selector->lifetime()); - - selector->premiumPromoChosen() | rpl::start_with_next([=] { - menu->hideMenu(); - showPremiumPromo(itemId); - }, selector->lifetime()); - const auto correctTop = selector->y(); menu->showStateValue( ) | rpl::start_with_next([=](Ui::PopupMenu::ShowState state) { @@ -1099,17 +1131,7 @@ AttachSelectorResult AttachSelectorToMenu( state.toggling); }, selector->lifetime()); - const auto weak = base::make_weak(controller); - controller->enableGifPauseReason( - Window::GifPauseReason::MediaPreview); - QObject::connect(menu.get(), &QObject::destroyed, [weak] { - if (const auto strong = weak.get()) { - strong->disableGifPauseReason( - Window::GifPauseReason::MediaPreview); - } - }); - - return AttachSelectorResult::Attached; + return selector; } } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index b47dfa2d8..8d3b34a03 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "history/view/reactions/history_view_reactions_strip.h" -#include "data/data_message_reactions.h" +#include "base/expected.h" #include "base/unique_qptr.h" +#include "data/data_message_reactions.h" +#include "history/view/reactions/history_view_reactions_strip.h" #include "ui/effects/animation_value.h" #include "ui/effects/round_area_with_shadow.h" #include "ui/rp_widget.h" @@ -63,6 +64,7 @@ public: [[nodiscard]] QMargins extentsForShadow() const; [[nodiscard]] int extendTopForCategories() const; [[nodiscard]] int minimalHeight() const; + [[nodiscard]] int countAppearedWidth(float64 progress) const; void setSpecialExpandTopSkip(int skip); void initGeometry(int innerTop); void beforeDestroy(); @@ -209,4 +211,13 @@ AttachSelectorResult AttachSelectorToMenu( Fn showPremiumPromo, IconFactory iconFactory); +[[nodiscard]] auto AttachSelectorToMenu( + not_null menu, + QPoint desiredPosition, + const style::EmojiPan &st, + std::shared_ptr show, + const Data::PossibleItemReactionsRef &reactions, + IconFactory iconFactory +) -> base::expected, AttachSelectorResult>; + } // namespace HistoryView::Reactions diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index c5567dd79..ffd146b89 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -410,15 +410,6 @@ infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }}; infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; -infoRoundedIconRequests: icon {{ "info/edit/group_manage_join_requests", settingsIconFg }}; -infoRoundedIconRecentActions: icon {{ "info/edit/group_manage_actions", settingsIconFg }}; -infoRoundedIconAdministrators: icon {{ "info/edit/group_manage_admins", settingsIconFg }}; -infoRoundedIconInviteLinks: icon {{ "info/edit/group_manage_links", settingsIconFg }}; -infoRoundedIconReactions: icon {{ "info/edit/group_manage_reactions", settingsIconFg }}; -infoRoundedIconSignature: icon {{ "info/edit/channel_manage_signature", settingsIconFg }}; -infoRoundedIconAntiSpam: icon {{ "info/edit/antispam", settingsIconFg }}; -infoRoundedIconHideMembers: icon {{ "info/edit/hidden_members", settingsIconFg }}; - infoIconShare: icon {{ "info/info_share", infoIconFg }}; infoIconEdit: icon {{ "info/info_edit", infoIconFg }}; infoIconDelete: icon {{ "info/info_delete", infoIconFg }}; @@ -473,8 +464,10 @@ infoBlockButton: SettingsButton(infoProfileButton) { textFg: attentionButtonFg; textFgOver: attentionButtonFgOver; } -infoCreateLinkedChatButton: SettingsButton(infoMainButton) { +infoCreateLinkedChatButton: SettingsButton(infoProfileButton) { padding: margins(74px, 10px, 8px, 8px); + textFg: lightButtonFg; + textFgOver: lightButtonFgOver; } infoUnlinkChatButton: SettingsButton(infoCreateLinkedChatButton) { textFg: attentionButtonFg; @@ -607,15 +600,20 @@ manageGroupTopicsButton: SettingsCountButton(manageGroupTopButtonWithText) { } } } +manageGroupNoIconButtonInner: SettingsButton(infoProfileButton) { + padding: margins(25px, 11px, 24px, 8px); +} +manageGroupNoIconButton: SettingsCountButton(manageGroupTopButtonWithText) { + button: manageGroupNoIconButtonInner; + labelPosition: point(22px, 11px); + iconPosition: point(0px, 0px); +} -manageDeleteGroupButton: SettingsCountButton(manageGroupTopButtonWithText) { - button: SettingsButton(infoProfileButton) { - padding: margins(25px, 11px, 24px, 8px); +manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) { + button: SettingsButton(manageGroupNoIconButtonInner) { textFg: attentionButtonFg; textFgOver: attentionButtonFg; } - labelPosition: point(22px, 11px); - iconPosition: point(0px, 0px); } infoEmptyFg: windowSubTextFg; @@ -644,21 +642,35 @@ infoStoriesAboutArchivePadding: margins(22px, 12px, 22px, 12px); editPeerBottomButtonsLayoutMargins: margins(0px, 7px, 0px, 0px); -editPeerTopButtonsLayoutSkip: 13px; -editPeerTopButtonsLayoutSkipToBottom: 12px; +editPeerTopButtonsLayoutSkip: 5px; +editPeerTopButtonsLayoutSkipToBottom: 5px; -editPeerTopButtonsLayoutSkipCustomTop: 14px; -editPeerTopButtonsLayoutSkipCustomBottom: 11px; +editPeerTopButtonsLayoutSkipCustomBottom: 5px; editPeerHistoryVisibilityTopSkip: 8px; -editPeerPhotoMargins: margins(22px, 16px, 22px, 8px); +editPeerPhotoMargins: margins(22px, 8px, 22px, 8px); editPeerTitle: defaultInputField; -editPeerTitleMargins: margins(27px, 21px, 22px, 8px); -editPeerDescription: InputField(newGroupDescription) { - borderFg: transparent; +editPeerTitleMargins: margins(27px, 13px, 22px, 8px); +editPeerDescription: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 7px, 0px, 7px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + border: 0px; + borderActive: 0px; + + heightMin: 32px; + + font: boxTextFont; } -editPeerDescriptionMargins: margins(22px, 5px, 22px, 16px); +editPeerDescriptionMargins: margins(22px, 3px, 22px, 2px); editPeerPrivaciesMargins: margins(15px, 7px, 22px, 0px); editPeerPrivacyBottomSkip: 16px; editPeerPrivacyLabel: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index ae029cf9e..5bc2155cb 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -1815,7 +1815,7 @@ void ListWidget::performDrag() { // auto pressedMedia = static_cast(nullptr); // if (auto pressedItem = _pressState.layout) { // pressedMedia = pressedItem->getMedia(); - // if (_mouseCursorState == CursorState::Date || (pressedMedia && pressedMedia->dragItem())) { + // if (_mouseCursorState == CursorState::Date) { // session().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem)); // forwardMimeType = u"application/x-td-forward"_q; // } diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index 352aa8b45..7a9e18b92 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -229,6 +229,7 @@ void EmojiStatusPanel::startAnimation( &owner->reactions(), std::move(args), [=] { _animation->repaint(); }, + [] { return st::profileVerifiedCheckBg->c; }, _animationSizeTag); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp index 54bf15735..1939dde64 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp @@ -52,7 +52,7 @@ private: return label->height(); }; auto widthMin = basicWidth; - auto widthMax = label->naturalWidth(); + auto widthMax = label->textMaxWidth(); if (height(widthMin) <= heightLimit || height(widthMax) > heightLimit) { return basicWidth; } diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index e00e50d43..be7a81472 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/bot_attach_web_view.h" #include "api/api_common.h" +#include "core/click_handler_types.h" #include "data/data_bot_app.h" #include "data/data_user.h" #include "data/data_file_origin.h" @@ -568,7 +569,7 @@ void AttachWebView::cancel() { _session->api().request(base::take(_requestId)).cancel(); _session->api().request(base::take(_prolongId)).cancel(); _panel = nullptr; - _context = nullptr; + _lastShownContext = base::take(_context); _bot = nullptr; _app = nullptr; _botUsername = QString(); @@ -713,6 +714,14 @@ void AttachWebView::removeFromMenu(not_null bot) { }); } +std::optional AttachWebView::lookupLastAction( + const QString &url) const { + if (_lastShownUrl == url && _lastShownContext) { + return _lastShownContext->action; + } + return std::nullopt; +} + void AttachWebView::resolve() { resolveUsername(_botUsername, [=](not_null bot) { if (!_context) { @@ -1049,7 +1058,7 @@ void AttachWebView::show( } crl::on_main(this, [=] { cancel(); }); }); - const auto handleLocalUri = [close](QString uri) { + const auto handleLocalUri = [close, url](QString uri) { const auto local = Core::TryConvertUrlToLocal(uri); if (uri == local || Core::InternalPassportLink(local)) { return local.startsWith(u"tg://"_q); @@ -1058,7 +1067,10 @@ void AttachWebView::show( } close(); crl::on_main([=] { - UrlClickHandler::Open(local, {}); + const auto variant = QVariant::fromValue(ClickHandlerContext{ + .attachBotWebviewUrl = url, + }); + UrlClickHandler::Open(local, variant); }); return true; }; @@ -1142,6 +1154,7 @@ void AttachWebView::show( } }); + _lastShownUrl = url; _panel = Ui::BotWebView::Show({ .url = url, .userDataPath = _session->domain().local().webviewDataPath(), diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 658f59940..0cbd71ab4 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -122,6 +122,9 @@ public: PeerTypes chooseTypes); void removeFromMenu(not_null bot); + [[nodiscard]] std::optional lookupLastAction( + const QString &url) const; + static void ClearAll(); private: @@ -180,6 +183,8 @@ private: const not_null _session; std::unique_ptr _context; + std::unique_ptr _lastShownContext; + QString _lastShownUrl; UserData *_bot = nullptr; QString _botUsername; QString _botAppName; diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index 9cf84d338..e8b6181fb 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -1267,7 +1267,7 @@ Article::Article( , _url(getResultUrlHandler()) , _link(getResultPreviewHandler()) , _withThumb(withThumb) -, _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) +, _title(st::emojiPanWidth / 2) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { if (!_link) { if (const auto point = result->getLocationPoint()) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 237401606..0e776caaa 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -7,19 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_controller.h" -#include "base/timer.h" #include "base/power_save_blocker.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" #include "boxes/peers/prepare_short_info_box.h" #include "chat_helpers/compose/compose_show.h" #include "core/application.h" +#include "core/core_settings.h" #include "core/update_checker.h" -#include "data/stickers/data_custom_emoji.h" #include "data/data_changes.h" #include "data/data_document.h" #include "data/data_file_origin.h" -#include "data/data_message_reactions.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" @@ -35,26 +33,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_recent_views.h" #include "media/stories/media_stories_reply.h" #include "media/stories/media_stories_share.h" +#include "media/stories/media_stories_stealth.h" #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/report_box.h" -#include "ui/effects/emoji_fly_animation.h" -#include "ui/effects/message_sending_animation_common.h" -#include "ui/effects/reaction_fly_animation.h" -#include "ui/layers/box_content.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/round_rect.h" -#include "ui/rp_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "styles/style_chat.h" -#include "styles/style_chat_helpers.h" +#include "styles/style_chat_helpers.h" // defaultReportBox #include "styles/style_media_view.h" -#include "styles/style_widgets.h" #include "styles/style_boxes.h" // UserpicButton #include @@ -113,6 +105,19 @@ struct SameDayRange { return result; } +[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) { + if (std::abs(angle) < 1.) { + return point; + } + const auto alpha = angle / 180. * M_PI; + const auto acos = cos(alpha); + const auto asin = sin(alpha); + point -= origin; + return origin + QPoint( + int(base::SafeRound(acos * point.x() - asin * point.y())), + int(base::SafeRound(asin * point.x() + acos * point.y()))); +} + } // namespace class Controller::PhotoPlayback final { @@ -276,7 +281,7 @@ Controller::Controller(not_null delegate) rpl::combine( _replyArea->activeValue(), - _reactions->expandedValue(), + _reactions->activeValue(), _1 || _2 ) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool active) { @@ -284,37 +289,16 @@ Controller::Controller(not_null delegate) updateContentFaded(); }, _lifetime); - _replyArea->focusedValue( - ) | rpl::start_with_next([=](bool focused) { - _replyFocused = focused; - if (!_replyFocused) { - _reactions->hideIfCollapsed(); - } else if (!_hasSendText) { - _reactions->show(); - } - }, _lifetime); - - _replyArea->hasSendTextValue( - ) | rpl::start_with_next([=](bool has) { - _hasSendText = has; - if (_replyFocused) { - if (_hasSendText) { - _reactions->hide(); - } else { - _reactions->show(); - } - } - }, _lifetime); + _reactions->setReplyFieldState( + _replyArea->focusedValue(), + _replyArea->hasSendTextValue()); + if (const auto like = _replyArea->likeAnimationTarget()) { + _reactions->attachToReactionButton(like); + } _reactions->chosen( - ) | rpl::start_with_next([=](HistoryView::Reactions::ChosenReaction id) { - startReactionAnimation(id.id, { - .type = Ui::MessageSendingAnimationFrom::Type::Emoji, - .globalStartGeometry = id.globalGeometry, - .frame = id.icon, - }); - _replyArea->sendReaction(id.id); - unfocusReply(); + ) | rpl::start_with_next([=](Reactions::Chosen chosen) { + reactionChosen(chosen.mode, chosen.reaction); }, _lifetime); _delegate->storiesLayerShown( @@ -525,11 +509,30 @@ void Controller::initLayout() { .nameBoundingRect = nameBoundingRect(right, false), .nameFontSize = nameFontSize, }; - + if (!_locationAreas.empty()) { + rebuildLocationAreas(layout); + } return layout; }); } +void Controller::rebuildLocationAreas(const Layout &layout) const { + Expects(_locations.size() == _locationAreas.size()); + + const auto origin = layout.content.topLeft(); + const auto scale = layout.content.size(); + for (auto i = 0, count = int(_locations.size()); i != count; ++i) { + auto &area = _locationAreas[i]; + const auto &general = _locations[i].area.geometry; + area.geometry = QRect( + int(base::SafeRound(general.x() * scale.width())), + int(base::SafeRound(general.y() * scale.height())), + int(base::SafeRound(general.width() * scale.width())), + int(base::SafeRound(general.height() * scale.height())) + ).translated(origin); + } +} + Data::Story *Controller::story() const { if (!_session) { return nullptr; @@ -586,6 +589,19 @@ bool Controller::skipCaption() const { return _captionFullView != nullptr; } +void Controller::toggleLiked() { + _reactions->toggleLiked(); +} + +void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) { + if (mode == ReactionsMode::Message) { + _replyArea->sendReaction(chosen.id); + } else if (const auto user = shownUser()) { + user->owner().stories().sendReaction(_shown, chosen.id); + } + unfocusReply(); +} + void Controller::showFullCaption() { if (_captionText.empty()) { return; @@ -845,16 +861,18 @@ void Controller::show( _viewed = false; invalidate_weak_ptrs(&_viewsLoadGuard); _reactions->hide(); - if (_replyFocused) { + if (_replyArea->focused()) { unfocusReply(); } _replyArea->show({ .user = unsupported ? nullptr : user, .id = story->id(), - }); + }, _reactions->likedValue()); + _recentViews->show({ .list = story->recentViewers(), + .reactions = story->reactions(), .total = story->views(), .valid = user->isSelf(), }); @@ -891,6 +909,16 @@ bool Controller::changeShown(Data::Story *story) { story, Data::Stories::Polling::Viewer); } + _reactions->showLikeFrom(story); + + const auto &locations = story + ? story->locations() + : std::vector(); + if (_locations != locations) { + _locations = locations; + _locationAreas.clear(); + } + return true; } @@ -925,6 +953,7 @@ void Controller::subscribeToSession() { } else { _recentViews->show({ .list = update.story->recentViewers(), + .reactions = update.story->reactions(), .total = update.story->views(), .valid = update.story->peer()->isSelf(), }); @@ -1031,6 +1060,7 @@ void Controller::ready() { } _started = true; updatePlayingAllowed(); + _reactions->ready(); } void Controller::updateVideoPlayback(const Player::TrackState &state) { @@ -1052,6 +1082,32 @@ void Controller::updatePlayback(const Player::TrackState &state) { } } +ClickHandlerPtr Controller::lookupLocationHandler(QPoint point) const { + const auto &layout = _layout.current(); + if (_locations.empty() || !layout) { + return nullptr; + } else if (_locationAreas.empty()) { + _locationAreas = _locations | ranges::views::transform([]( + const Data::StoryLocation &location) { + return LocationArea{ + .rotation = location.area.rotation, + .handler = std::make_shared( + location.point), + }; + }) | ranges::to_vector; + rebuildLocationAreas(*layout); + } + + for (const auto &area : _locationAreas) { + const auto center = area.geometry.center(); + const auto angle = -area.rotation; + if (area.geometry.contains(Rotated(point, center, angle))) { + return area.handler; + } + } + return nullptr; +} + void Controller::maybeMarkAsRead(const Player::TrackState &state) { const auto length = state.length; const auto position = Player::IsStoppedAtEnd(state.state) @@ -1195,7 +1251,7 @@ void Controller::contentPressed(bool pressed) { _captionFullView->close(); } if (pressed) { - _reactions->collapse(); + _reactions->outsidePressed(); } } @@ -1226,12 +1282,17 @@ SiblingView Controller::sibling(SiblingType type) const { return {}; } -ViewsSlice Controller::views(PeerId offset) { +const Data::StoryViews &Controller::views(int limit, bool initial) { invalidate_weak_ptrs(&_viewsLoadGuard); - if (!offset) { + if (initial) { refreshViewsFromData(); - } else if (!sliceViewsTo(offset)) { - return { .left = _viewsSlice.left }; + } + if (_viewsSlice.total > _viewsSlice.list.size() + && _viewsSlice.list.size() < limit) { + const auto done = viewsGotMoreCallback(); + const auto user = shownUser(); + auto &stories = user->owner().stories(); + stories.loadViewsSlice(_shown.story, _viewsSlice.nextOffset, done); } return _viewsSlice; } @@ -1240,27 +1301,25 @@ rpl::producer<> Controller::moreViewsLoaded() const { return _moreViewsLoaded.events(); } -Fn)> Controller::viewsGotMoreCallback() { - return crl::guard(&_viewsLoadGuard, [=]( - const std::vector &result) { +Fn Controller::viewsGotMoreCallback() { + return crl::guard(&_viewsLoadGuard, [=](Data::StoryViews result) { if (_viewsSlice.list.empty()) { const auto user = shownUser(); auto &stories = user->owner().stories(); if (const auto maybeStory = stories.lookup(_shown)) { - _viewsSlice = { - .list = result, - .left = (*maybeStory)->views() - int(result.size()), - }; + _viewsSlice = (*maybeStory)->viewsList(); } else { _viewsSlice = {}; } } else { _viewsSlice.list.insert( end(_viewsSlice.list), - begin(result), - end(result)); - _viewsSlice.left - = std::max(_viewsSlice.left - int(result.size()), 0); + begin(result.list), + end(result.list)); + _viewsSlice.total = result.nextOffset.isEmpty() + ? int(_viewsSlice.list.size()) + : std::max(result.total, int(_viewsSlice.list.size())); + _viewsSlice.nextOffset = result.nextOffset; } _moreViewsLoaded.fire({}); }); @@ -1408,49 +1467,9 @@ void Controller::refreshViewsFromData() { const auto maybeStory = stories.lookup(_shown); if (!maybeStory || !user->isSelf()) { _viewsSlice = {}; - return; + } else { + _viewsSlice = (*maybeStory)->viewsList(); } - const auto story = *maybeStory; - const auto &list = story->viewsList(); - const auto total = story->views(); - _viewsSlice.list = list - | ranges::views::take(Data::Stories::kViewsPerPage) - | ranges::to_vector; - _viewsSlice.left = total - int(_viewsSlice.list.size()); - if (_viewsSlice.list.empty() && _viewsSlice.left > 0) { - const auto done = viewsGotMoreCallback(); - stories.loadViewsSlice(_shown.story, std::nullopt, done); - } -} - -bool Controller::sliceViewsTo(PeerId offset) { - Expects(shown()); - - const auto user = shownUser(); - auto &stories = user->owner().stories(); - const auto maybeStory = stories.lookup(_shown); - if (!maybeStory || !user->isSelf()) { - _viewsSlice = {}; - return true; - } - const auto story = *maybeStory; - const auto &list = story->viewsList(); - const auto proj = [&](const Data::StoryView &single) { - return single.peer->id; - }; - const auto i = ranges::find(list, _viewsSlice.list.back()); - const auto add = (i != end(list)) ? int(end(list) - i - 1) : 0; - const auto j = ranges::find(_viewsSlice.list, offset, proj); - Assert(j != end(_viewsSlice.list)); - if (!add && (j + 1) == end(_viewsSlice.list)) { - const auto done = viewsGotMoreCallback(); - stories.loadViewsSlice(_shown.story, _viewsSlice.list.back(), done); - return false; - } - _viewsSlice.list.erase(begin(_viewsSlice.list), j + 1); - _viewsSlice.list.insert(end(_viewsSlice.list), i + 1, end(list)); - _viewsSlice.left -= add; - return true; } void Controller::unfocusReply() { @@ -1522,6 +1541,24 @@ void Controller::tryProcessKeyInput(not_null e) { _replyArea->tryProcessKeyInput(e); } +bool Controller::allowStealthMode() const { + const auto story = this->story(); + return story + && !story->peer()->isSelf() + && story->peer()->session().premiumPossible(); +} + +void Controller::setupStealthMode() { + SetupStealthMode(uiShow()); +} + +auto Controller::attachReactionsToMenu( + not_null menu, + QPoint desiredPosition) +-> AttachStripResult { + return _reactions->attachToMenu(menu, desiredPosition); +} + rpl::lifetime &Controller::lifetime() { return _lifetime; } @@ -1537,34 +1574,6 @@ void Controller::updatePowerSaveBlocker(const Player::TrackState &state) { [=] { return _wrap->window()->windowHandle(); }); } -void Controller::startReactionAnimation( - Data::ReactionId id, - Ui::MessageSendingAnimationFrom from) { - Expects(shown()); - - auto args = Ui::ReactionFlyAnimationArgs{ - .id = id, - .flyIcon = from.frame, - .flyFrom = _wrap->mapFromGlobal(from.globalStartGeometry), - .scaleOutDuration = st::fadeWrapDuration * 2, - }; - _reactionAnimation = std::make_unique( - _wrap, - &shownUser()->owner().reactions(), - std::move(args), - [=] { _reactionAnimation->repaint(); }, - Data::CustomEmojiSizeTag::Isolated); - const auto layer = _reactionAnimation->layer(); - _wrap->paintRequest() | rpl::start_with_next([=] { - if (!_reactionAnimation->paintBadgeFrame(_wrap.get())) { - InvokeQueued(layer, [=] { - _reactionAnimation = nullptr; - _wrap->update(); - }); - } - }, layer->lifetime()); -} - Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) { return { .text = (pinned diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 35d82c9b0..f932c40a6 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -26,18 +26,19 @@ struct FileChosen; namespace Data { struct FileOrigin; -struct ReactionId; +class DocumentMedia; } // namespace Data namespace HistoryView::Reactions { class CachedIconFactory; +struct ChosenReaction; +enum class AttachSelectorResult; } // namespace HistoryView::Reactions namespace Ui { class RpWidget; -struct MessageSendingAnimationFrom; -class EmojiFlyAnimation; class BoxContent; +class PopupMenu; } // namespace Ui namespace Ui::Toast { @@ -66,6 +67,7 @@ struct SiblingView; enum class SiblingType; struct ContentLayout; class CaptionFullView; +enum class ReactionsMode; enum class HeaderLayout { Normal, @@ -104,11 +106,6 @@ struct Layout { friend inline bool operator==(Layout, Layout) = default; }; -struct ViewsSlice { - std::vector list; - int left = 0; -}; - class Controller final : public base::has_weak_ptr { public: explicit Controller(not_null delegate); @@ -123,6 +120,7 @@ public: [[nodiscard]] Data::FileOrigin fileOrigin() const; [[nodiscard]] TextWithEntities captionText() const; [[nodiscard]] bool skipCaption() const; + void toggleLiked(); void showFullCaption(); void captionClosing(); void captionClosed(); @@ -137,6 +135,7 @@ public: void ready(); void updateVideoPlayback(const Player::TrackState &state); + [[nodiscard]] ClickHandlerPtr lookupLocationHandler(QPoint point) const; [[nodiscard]] bool subjumpAvailable(int delta) const; [[nodiscard]] bool subjumpFor(int delta); @@ -155,7 +154,7 @@ public: void repaintSibling(not_null sibling); [[nodiscard]] SiblingView sibling(SiblingType type) const; - [[nodiscard]] ViewsSlice views(PeerId offset); + [[nodiscard]] const Data::StoryViews &views(int limit, bool initial); [[nodiscard]] rpl::producer<> moreViewsLoaded() const; void unfocusReply(); @@ -167,9 +166,20 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] bool allowStealthMode() const; + void setupStealthMode(); + + using AttachStripResult = HistoryView::Reactions::AttachSelectorResult; + [[nodiscard]] AttachStripResult attachReactionsToMenu( + not_null menu, + QPoint desiredPosition); + [[nodiscard]] rpl::lifetime &lifetime(); private: + class PhotoPlayback; + class Unsupported; + using ChosenReaction = HistoryView::Reactions::ChosenReaction; struct StoriesList { not_null user; Data::StoriesIds ids; @@ -187,8 +197,11 @@ private: return peerId != 0; } }; - class PhotoPlayback; - class Unsupported; + struct LocationArea { + QRect geometry; + float64 rotation = 0.; + ClickHandlerPtr handler; + }; void initLayout(); bool changeShown(Data::Story *story); @@ -202,6 +215,7 @@ private: void updateContentFaded(); void updatePlayingAllowed(); void setPlayingAllowed(bool allowed); + void rebuildLocationAreas(const Layout &layout) const; void hideSiblings(); void showSiblings(not_null session); @@ -215,9 +229,8 @@ private: void moveFromShown(); void refreshViewsFromData(); - bool sliceViewsTo(PeerId offset); [[nodiscard]] auto viewsGotMoreCallback() - -> Fn)>; + -> Fn; [[nodiscard]] bool shown() const; [[nodiscard]] UserData *shownUser() const; @@ -231,9 +244,7 @@ private: const std::vector &lists, int index); - void startReactionAnimation( - Data::ReactionId id, - Ui::MessageSendingAnimationFrom from); + void reactionChosen(ReactionsMode mode, ChosenReaction chosen); const not_null _delegate; @@ -253,9 +264,7 @@ private: bool _contentFaded = false; bool _windowActive = false; - bool _replyFocused = false; bool _replyActive = false; - bool _hasSendText = false; bool _layerShown = false; bool _menuShown = false; bool _tooltipShown = false; @@ -274,11 +283,14 @@ private: bool _started = false; bool _viewed = false; + std::vector _locations; + mutable std::vector _locationAreas; + std::vector _cachedSourcesList; int _cachedSourceIndex = -1; bool _showingUnreadSources = false; - ViewsSlice _viewsSlice; + Data::StoryViews _viewsSlice; rpl::event_stream<> _moreViewsLoaded; base::has_weak_ptr _viewsLoadGuard; @@ -286,7 +298,6 @@ private: std::unique_ptr _siblingRight; std::unique_ptr _powerSaveBlocker; - std::unique_ptr _reactionAnimation; Main::Session *_session = nullptr; rpl::lifetime _sessionLifetime; diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index ac85729ee..9156ac35d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -356,7 +356,7 @@ void Header::show(HeaderData data) { _widget.get(), std::move(counter), st::storiesHeaderDate); - _counter->resizeToNaturalWidth(_counter->naturalWidth()); + _counter->resizeToWidth(_counter->textMaxWidth()); _counter->setAttribute(Qt::WA_TransparentForMouseEvents); _counter->setOpacity(kNameOpacity); _counter->show(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 9d4265758..02b42d593 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -7,13 +7,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/stories/media_stories_reactions.h" +#include "base/event_filter.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.h" +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_message_reactions.h" +#include "data/data_peer.h" #include "data/data_session.h" #include "history/view/reactions/history_view_reactions_selector.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" +#include "ui/effects/emoji_fly_animation.h" +#include "ui/effects/reaction_fly_animation.h" +#include "ui/widgets/popup_menu.h" +#include "ui/animated_icon.h" +#include "ui/painter.h" #include "styles/style_chat_helpers.h" #include "styles/style_media_view.h" #include "styles/style_widgets.h" @@ -21,6 +31,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media::Stories { namespace { +constexpr auto kReactionScaleOutTarget = 0.7; +constexpr auto kReactionScaleOutDuration = crl::time(1000); +constexpr auto kMessageReactionScaleOutDuration = crl::time(400); + +[[nodiscard]] Data::ReactionId HeartReactionId() { + return { QString() + QChar(10084) }; +} + [[nodiscard]] Data::PossibleItemReactionsRef LookupPossibleReactions( not_null session) { auto result = Data::PossibleItemReactionsRef(); @@ -51,7 +69,50 @@ namespace { } // namespace -struct Reactions::Hiding { +class Reactions::Panel final { +public: + explicit Panel(not_null controller); + ~Panel(); + + [[nodiscard]] rpl::producer expandedValue() const { + return _expanded.value(); + } + [[nodiscard]] rpl::producer shownValue() const { + return _shown.value(); + } + + [[nodiscard]] rpl::producer chosen() const; + + void show(Mode mode); + void hide(Mode mode); + void hideIfCollapsed(Mode mode); + void collapse(Mode mode); + + void attachToReactionButton(not_null button); + +private: + struct Hiding; + + void create(); + void updateShowState(); + void fadeOutSelector(); + void startAnimation(); + + const not_null _controller; + + std::unique_ptr _parent; + std::unique_ptr _selector; + std::vector> _hiding; + rpl::event_stream _chosen; + Ui::Animations::Simple _showing; + rpl::variable _shownValue; + rpl::variable _expanded; + rpl::variable _mode; + rpl::variable _shown = false; + +}; + +struct Reactions::Panel::Hiding { explicit Hiding(not_null parent) : widget(parent) { } @@ -60,16 +121,24 @@ struct Reactions::Hiding { QImage frame; }; -Reactions::Reactions(not_null controller) +Reactions::Panel::Panel(not_null controller) : _controller(controller) { } -Reactions::~Reactions() = default; +Reactions::Panel::~Panel() = default; -void Reactions::show() { - if (_shown) { +auto Reactions::Panel::chosen() const -> rpl::producer { + return _chosen.events(); +} + +void Reactions::Panel::show(Mode mode) { + const auto was = _mode.current(); + if (_shown.current() && was == mode) { return; + } else if (_shown.current()) { + hide(was); } + _mode = mode; create(); if (!_selector) { return; @@ -82,8 +151,8 @@ void Reactions::show() { _parent->show(); } -void Reactions::hide() { - if (!_selector) { +void Reactions::Panel::hide(Mode mode) { + if (!_selector || _mode.current() != mode) { return; } _selector->beforeDestroy(); @@ -97,20 +166,32 @@ void Reactions::hide() { _parent = nullptr; } -void Reactions::hideIfCollapsed() { - if (!_expanded.current()) { - hide(); +void Reactions::Panel::hideIfCollapsed(Mode mode) { + if (!_expanded.current() && _mode.current() == mode) { + hide(mode); } } -void Reactions::collapse() { - if (_expanded.current()) { - hide(); - show(); +void Reactions::Panel::collapse(Mode mode) { + if (_expanded.current() && _mode.current() == mode) { + hide(mode); + show(mode); } } -void Reactions::create() { +void Reactions::Panel::attachToReactionButton(not_null button) { + base::install_event_filter(button, [=](not_null e) { + if (e->type() == QEvent::ContextMenu && !button->isHidden()) { + show(Reactions::Mode::Reaction); + return base::EventFilterResult::Cancel; + } else if (e->type() == QEvent::Hide) { + hide(Reactions::Mode::Reaction); + } + return base::EventFilterResult::Continue; + }); +} + +void Reactions::Panel::create() { auto reactions = LookupPossibleReactions( &_controller->uiShow()->session()); if (reactions.recent.empty() && !reactions.morePremiumAvailable) { @@ -119,13 +200,19 @@ void Reactions::create() { _parent = std::make_unique(_controller->wrap().get()); _parent->show(); + const auto mode = _mode.current(); + _parent->events() | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::MouseButtonPress) { const auto event = static_cast(e.get()); if (event->button() == Qt::LeftButton) { if (!_selector || !_selector->geometry().contains(event->pos())) { - collapse(); + if (mode == Mode::Message) { + collapse(mode); + } else { + hide(mode); + } } } } @@ -137,17 +224,17 @@ void Reactions::create() { _controller->uiShow(), std::move(reactions), _controller->cachedReactionIconFactory().createMethod(), - [=](bool fast) { hide(); }); + [=](bool fast) { hide(mode); }); _selector->chosen( ) | rpl::start_with_next([=]( HistoryView::Reactions::ChosenReaction reaction) { - _chosen.fire_copy(reaction); - hide(); + _chosen.fire({ .reaction = reaction, .mode = mode }); + hide(mode); }, _selector->lifetime()); _selector->premiumPromoChosen() | rpl::start_with_next([=] { - hide(); + hide(mode); ShowPremiumPreviewBox( _controller->uiShow(), PremiumPreview::InfiniteReactions); @@ -165,13 +252,23 @@ void Reactions::create() { _controller->layoutValue(), _shownValue.value() ) | rpl::start_with_next([=](const Layout &layout, float64 shown) { - const auto shift = int(base::SafeRound((full / 2.) * shown)); - _parent->setGeometry(QRect( - layout.reactions.x() + layout.reactions.width() / 2 - shift, - layout.reactions.y(), - full, - layout.reactions.height())); - const auto innerTop = layout.reactions.height() + const auto width = extents.left() + + _selector->countAppearedWidth(shown) + + extents.right(); + const auto height = layout.reactions.height(); + const auto shift = (width / 2); + const auto right = (mode == Mode::Message) + ? (layout.reactions.x() + layout.reactions.width() / 2 + shift) + : (layout.controlsBottomPosition.x() + + layout.controlsWidth + - st::storiesLikeReactionsPosition.x()); + const auto top = (mode == Mode::Message) + ? layout.reactions.y() + : (layout.controlsBottomPosition.y() + - height + - st::storiesLikeReactionsPosition.y()); + _parent->setGeometry(QRect((right - width), top, full, height)); + const auto innerTop = height - st::storiesReactionsBottomSkip - st::reactStripHeight; const auto maxAdded = innerTop - extents.top() - categoriesTop; @@ -186,11 +283,15 @@ void Reactions::create() { }, _selector->lifetime()); _selector->escapes() | rpl::start_with_next([=] { - collapse(); + if (mode == Mode::Message) { + collapse(mode); + } else { + hide(mode); + } }, _selector->lifetime()); } -void Reactions::fadeOutSelector() { +void Reactions::Panel::fadeOutSelector() { const auto wrap = _controller->wrap().get(); const auto geometry = Ui::MapFrom( wrap, @@ -226,8 +327,8 @@ void Reactions::fadeOutSelector() { }); } -void Reactions::updateShowState() { - const auto progress = _showing.value(_shown ? 1. : 0.); +void Reactions::Panel::updateShowState() { + const auto progress = _showing.value(_shown.current() ? 1. : 0.); const auto opacity = 1.; const auto appearing = _showing.animating(); const auto toggling = false; @@ -235,4 +336,390 @@ void Reactions::updateShowState() { _selector->updateShowState(progress, opacity, appearing, toggling); } +Reactions::Reactions(not_null controller) +: _controller(controller) +, _panel(std::make_unique(_controller)) { + _panel->chosen() | rpl::start_with_next([=](Chosen &&chosen) { + animateAndProcess(std::move(chosen)); + }, _lifetime); +} + +Reactions::~Reactions() = default; + +rpl::producer Reactions::activeValue() const { + using namespace rpl::mappers; + return rpl::combine( + _panel->expandedValue(), + _panel->shownValue(), + _1 || _2); +} + +auto Reactions::chosen() const -> rpl::producer { + return _chosen.events(); +} + +void Reactions::setReplyFieldState( + rpl::producer focused, + rpl::producer hasSendText) { + std::move( + focused + ) | rpl::start_with_next([=](bool focused) { + _replyFocused = focused; + if (!_replyFocused) { + _panel->hideIfCollapsed(Reactions::Mode::Message); + } else if (!_hasSendText) { + _panel->show(Reactions::Mode::Message); + } + }, _lifetime); + + std::move( + hasSendText + ) | rpl::start_with_next([=](bool has) { + _hasSendText = has; + if (_replyFocused) { + if (_hasSendText) { + _panel->hide(Reactions::Mode::Message); + } else { + _panel->show(Reactions::Mode::Message); + } + } + }, _lifetime); +} + +void Reactions::attachToReactionButton(not_null button) { + _likeButton = button; + _panel->attachToReactionButton(button); +} + +auto Reactions::attachToMenu( + not_null menu, + QPoint desiredPosition) +-> AttachStripResult { + using namespace HistoryView::Reactions; + + const auto story = _controller->story(); + if (!story || story->peer()->isSelf()) { + return AttachStripResult::Skipped; + } + + const auto show = _controller->uiShow(); + const auto result = AttachSelectorToMenu( + menu, + desiredPosition, + st::storiesReactionsPan, + show, + LookupPossibleReactions(&show->session()), + _controller->cachedReactionIconFactory().createMethod()); + if (!result) { + return result.error(); + } + const auto selector = *result; + + selector->chosen() | rpl::start_with_next([=](ChosenReaction reaction) { + menu->hideMenu(); + animateAndProcess({ reaction, ReactionsMode::Reaction }); + }, selector->lifetime()); + + return AttachSelectorResult::Attached; +} + +Data::ReactionId Reactions::liked() const { + return _liked.current(); +} + +rpl::producer Reactions::likedValue() const { + return _liked.value(); +} + +void Reactions::showLikeFrom(Data::Story *story) { + setLikedIdFrom(story); + + if (!story) { + _likeFromLifetime.destroy(); + return; + } + _likeFromLifetime = story->session().changes().storyUpdates( + story, + Data::StoryUpdate::Flag::Reaction + ) | rpl::start_with_next([=](const Data::StoryUpdate &update) { + setLikedIdFrom(update.story); + }); +} + +void Reactions::hide() { + _panel->hide(Reactions::Mode::Message); + _panel->hide(Reactions::Mode::Reaction); +} + +void Reactions::outsidePressed() { + _panel->hide(Reactions::Mode::Reaction); + _panel->collapse(Reactions::Mode::Message); +} + +void Reactions::toggleLiked() { + const auto liked = !_liked.current().empty(); + const auto now = liked ? Data::ReactionId() : HeartReactionId(); + if (_liked.current() != now) { + animateAndProcess({ { .id = now }, ReactionsMode::Reaction }); + } +} + +void Reactions::ready() { + if (const auto story = _controller->story()) { + story->owner().reactions().preloadAnimationsFor(HeartReactionId()); + } +} + +void Reactions::animateAndProcess(Chosen &&chosen) { + const auto like = (chosen.mode == Mode::Reaction); + const auto wrap = _controller->wrap(); + const auto target = like ? _likeButton : wrap.get(); + const auto story = _controller->story(); + if (!story || !target) { + return; + } + + auto done = like + ? setLikedIdIconInit(&story->owner(), chosen.reaction.id) + : Fn(); + const auto scaleOutDuration = like + ? kReactionScaleOutDuration + : kMessageReactionScaleOutDuration; + const auto scaleOutTarget = like ? kReactionScaleOutTarget : 0.; + + if (!chosen.reaction.id.empty()) { + startReactionAnimation({ + .id = chosen.reaction.id, + .flyIcon = chosen.reaction.icon, + .flyFrom = (chosen.reaction.globalGeometry.isEmpty() + ? QRect() + : wrap->mapFromGlobal(chosen.reaction.globalGeometry)), + .scaleOutDuration = scaleOutDuration, + .scaleOutTarget = scaleOutTarget, + }, target, std::move(done)); + } + + _chosen.fire(std::move(chosen)); +} + +void Reactions::assignLikedId(Data::ReactionId id) { + invalidate_weak_ptrs(&_likeIconGuard); + _likeIcon = nullptr; + _liked = id; +} + +Fn Reactions::setLikedIdIconInit( + not_null owner, + Data::ReactionId id, + bool force) { + if (_liked.current() != id) { + _likeIconMedia = nullptr; + } else if (!force) { + return nullptr; + } + assignLikedId(id); + if (id.empty() || !_likeButton) { + return nullptr; + } + return crl::guard(&_likeIconGuard, [=](Ui::ReactionFlyCenter center) { + if (!id.custom() && !center.icon && !_likeIconMedia) { + waitForLikeIcon(owner, id); + } else { + initLikeIcon(owner, id, std::move(center)); + } + }); +} + +void Reactions::initLikeIcon( + not_null owner, + Data::ReactionId id, + Ui::ReactionFlyCenter center) { + Expects(_likeButton != nullptr); + + _likeIcon = std::make_unique(_likeButton); + const auto icon = _likeIcon.get(); + icon->show(); + _likeButton->sizeValue() | rpl::start_with_next([=](QSize size) { + icon->setGeometry(QRect(QPoint(), size)); + }, icon->lifetime()); + + if (!id.custom() && !center.icon) { + return; + } + + struct State { + Ui::ReactionFlyCenter center; + QImage cache; + }; + const auto fly = icon->lifetime().make_state(State{ + .center = std::move(center), + }); + if (const auto customId = id.custom()) { + auto withCorrectCallback = owner->customEmojiManager().create( + customId, + [=] { icon->update(); }, + Data::CustomEmojiSizeTag::Isolated); + [[maybe_unused]] const auto load = withCorrectCallback->ready(); + fly->center.custom = std::move(withCorrectCallback); + fly->center.icon = nullptr; + } else { + fly->center.icon->jumpToStart(nullptr); + fly->center.custom = nullptr; + } + const auto paintNonCached = [=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + + const auto size = fly->center.size; + const auto target = QRect( + (icon->width() - size) / 2, + (icon->height() - size) / 2, + size, + size); + const auto scale = fly->center.scale; + if (scale < 1.) { + const auto shift = QRectF(target).center(); + p.translate(shift); + p.scale(scale, scale); + p.translate(-shift); + } + const auto multiplier = fly->center.centerSizeMultiplier; + const auto inner = int(base::SafeRound(size * multiplier)); + if (const auto icon = fly->center.icon.get()) { + const auto rect = QRect( + target.x() + (target.width() - inner) / 2, + target.y() + (target.height() - inner) / 2, + inner, + inner); + p.drawImage(rect, icon->frame(st::storiesComposeWhiteText->c)); + } else { + const auto customSize = fly->center.customSize; + const auto scaled = (inner != customSize); + fly->center.custom->paint(p, { + .textColor = st::storiesComposeWhiteText->c, + .size = { customSize, customSize }, + .now = crl::now(), + .scale = (scaled ? (inner / float64(customSize)) : 1.), + .position = QPoint( + target.x() + (target.width() - customSize) / 2, + target.y() + (target.height() - customSize) / 2), + .scaled = scaled, + }); + } + }; + icon->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(icon); + if (!fly->cache.isNull()) { + p.drawImage(0, 0, fly->cache); + } else if (fly->center.icon + || fly->center.custom->readyInDefaultState()) { + const auto ratio = style::DevicePixelRatio(); + fly->cache = QImage( + icon->size() * ratio, + QImage::Format_ARGB32_Premultiplied); + fly->cache.setDevicePixelRatio(ratio); + fly->cache.fill(Qt::transparent); + auto q = QPainter(&fly->cache); + paintNonCached(q); + q.end(); + + fly->center.icon = nullptr; + fly->center.custom = nullptr; + p.drawImage(0, 0, fly->cache); + } else { + paintNonCached(p); + } + }, icon->lifetime()); +} + +void Reactions::waitForLikeIcon( + not_null owner, + Data::ReactionId id) { + _likeIconWaitLifetime = rpl::single( + rpl::empty + ) | rpl::then( + owner->reactions().defaultUpdates() + ) | rpl::map([=]() -> rpl::producer { + const auto &list = owner->reactions().list( + Data::Reactions::Type::All); + const auto i = ranges::find(list, id, &Data::Reaction::id); + if (i == end(list)) { + return rpl::single(false); + } + const auto document = i->centerIcon + ? not_null(i->centerIcon) + : i->selectAnimation; + _likeIconMedia = document->createMediaView(); + _likeIconMedia->checkStickerLarge(); + return rpl::single( + rpl::empty + ) | rpl::then( + document->session().downloaderTaskFinished() + ) | rpl::map([=] { + return _likeIconMedia->loaded(); + }); + }) | rpl::flatten_latest( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + setLikedId(owner, id, true); + + crl::on_main(&_likeIconGuard, [=] { + _likeIconMedia = nullptr; + _likeIconWaitLifetime.destroy(); + }); + }); +} + +void Reactions::setLikedIdFrom(Data::Story *story) { + if (!story) { + assignLikedId({}); + } else { + setLikedId(&story->owner(), story->sentReactionId()); + } +} + +void Reactions::setLikedId( + not_null owner, + Data::ReactionId id, + bool force) { + if (const auto done = setLikedIdIconInit(owner, id, force)) { + const auto reactions = &owner->reactions(); + const auto colored = [] { return st::storiesComposeWhiteText->c; }; + const auto sizeTag = Data::CustomEmojiSizeTag::Isolated; + done(Ui::EmojiFlyAnimation(_controller->wrap(), reactions, { + .id = id, + .scaleOutDuration = kReactionScaleOutDuration, + .scaleOutTarget = kReactionScaleOutTarget, + }, [] {}, colored, sizeTag).grabBadgeCenter()); + } +} + +void Reactions::startReactionAnimation( + Ui::ReactionFlyAnimationArgs args, + not_null target, + Fn done) { + const auto wrap = _controller->wrap(); + const auto story = _controller->story(); + _reactionAnimation = std::make_unique( + wrap, + &story->owner().reactions(), + std::move(args), + [=] { _reactionAnimation->repaint(); }, + [] { return st::storiesComposeWhiteText->c; }, + Data::CustomEmojiSizeTag::Isolated); + const auto layer = _reactionAnimation->layer(); + wrap->paintRequest() | rpl::start_with_next([=] { + if (!_reactionAnimation->paintBadgeFrame(target)) { + InvokeQueued(layer, [=] { + _reactionAnimation = nullptr; + wrap->update(); + }); + if (done) { + done(_reactionAnimation->grabBadgeCenter()); + } + } + }, layer->lifetime()); + wrap->update(); +} + } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.h b/Telegram/SourceFiles/media/stories/media_stories_reactions.h index ca2200d7e..fbcf34b3c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.h @@ -7,60 +7,120 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_message_reaction_id.h" #include "ui/effects/animations.h" namespace Data { +class DocumentMedia; struct ReactionId; +class Session; +class Story; } // namespace Data namespace HistoryView::Reactions { class Selector; struct ChosenReaction; +enum class AttachSelectorResult; } // namespace HistoryView::Reactions namespace Ui { class RpWidget; +struct ReactionFlyAnimationArgs; +struct ReactionFlyCenter; +class EmojiFlyAnimation; +class PopupMenu; } // namespace Ui namespace Media::Stories { class Controller; +enum class ReactionsMode { + Message, + Reaction, +}; + class Reactions final { public: explicit Reactions(not_null controller); ~Reactions(); - using Chosen = HistoryView::Reactions::ChosenReaction; - [[nodiscard]] rpl::producer expandedValue() const { - return _expanded.value(); - } - [[nodiscard]] rpl::producer chosen() const { - return _chosen.events(); - } + using Mode = ReactionsMode; + + template + struct ChosenWrap { + Reaction reaction; + Mode mode; + }; + using Chosen = ChosenWrap; + + [[nodiscard]] rpl::producer activeValue() const; + [[nodiscard]] rpl::producer chosen() const; + + [[nodiscard]] Data::ReactionId liked() const; + [[nodiscard]] rpl::producer likedValue() const; + void showLikeFrom(Data::Story *story); - void show(); void hide(); - void hideIfCollapsed(); - void collapse(); + void outsidePressed(); + void toggleLiked(); + void ready(); + + void setReplyFieldState( + rpl::producer focused, + rpl::producer hasSendText); + void attachToReactionButton(not_null button); + + using AttachStripResult = HistoryView::Reactions::AttachSelectorResult; + [[nodiscard]] AttachStripResult attachToMenu( + not_null menu, + QPoint desiredPosition); private: - struct Hiding; + class Panel; - void create(); - void updateShowState(); - void fadeOutSelector(); + void animateAndProcess(Chosen &&chosen); + + void assignLikedId(Data::ReactionId id); + [[nodiscard]] Fn setLikedIdIconInit( + not_null owner, + Data::ReactionId id, + bool force = false); + void setLikedIdFrom(Data::Story *story); + void setLikedId( + not_null owner, + Data::ReactionId id, + bool force = false); + void startReactionAnimation( + Ui::ReactionFlyAnimationArgs from, + not_null target, + Fn done = nullptr); + void waitForLikeIcon( + not_null owner, + Data::ReactionId id); + void initLikeIcon( + not_null owner, + Data::ReactionId id, + Ui::ReactionFlyCenter center); const not_null _controller; + const std::unique_ptr _panel; - std::unique_ptr _parent; - std::unique_ptr _selector; - std::vector> _hiding; rpl::event_stream _chosen; - Ui::Animations::Simple _showing; - rpl::variable _shownValue; - rpl::variable _expanded; - bool _shown = false; + bool _replyFocused = false; + bool _hasSendText = false; + + Ui::RpWidget *_likeButton = nullptr; + rpl::variable _liked; + base::has_weak_ptr _likeIconGuard; + std::unique_ptr _likeIcon; + std::shared_ptr _likeIconMedia; + + std::unique_ptr _reactionAnimation; + + rpl::lifetime _likeIconWaitLifetime; + rpl::lifetime _likeFromLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index 806f21ebd..565cf5995 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_who_reacted.h" // FormatReadDate. #include "chat_helpers/compose/compose_show.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_peer.h" #include "data/data_stories.h" #include "main/main_session.h" @@ -131,7 +132,9 @@ void RecentViews::show(RecentViewsData data) { if (_data == data) { return; } - const auto totalChanged = _text.isEmpty() || (_data.total != data.total); + const auto countersChanged = _text.isEmpty() + || (_data.total != data.total) + || (_data.reactions != data.reactions); const auto usersChanged = !_userpics || (_data.list != data.list); _data = data; if (!_data.valid) { @@ -148,7 +151,7 @@ void RecentViews::show(RecentViewsData data) { if (!_userpics) { setupUserpics(); } - if (totalChanged) { + if (countersChanged) { updateText(); } if (usersChanged) { @@ -253,9 +256,13 @@ void RecentViews::updatePartsGeometry() { } void RecentViews::updateText() { - _text.setText(st::defaultTextStyle, _data.total - ? tr::lng_stories_views(tr::now, lt_count, _data.total) - : tr::lng_stories_no_views(tr::now)); + const auto text = _data.total + ? (tr::lng_stories_views(tr::now, lt_count, _data.total) + + (_data.reactions + ? (u" "_q + QChar(10084) + QString::number(_data.reactions)) + : QString())) + : tr::lng_stories_no_views(tr::now); + _text.setText(st::defaultTextStyle, text); updatePartsGeometry(); } @@ -264,8 +271,8 @@ void RecentViews::showMenu() { return; } - const auto views = _controller->views(PeerId()); - if (views.list.empty() && !views.left) { + const auto views = _controller->views(kAddPerPage * 2, true); + if (views.list.empty() && !views.total) { return; } @@ -275,8 +282,9 @@ void RecentViews::showMenu() { _widget.get(), st::storiesViewsMenu); auto count = 0; + const auto session = &_controller->story()->session(); const auto added = std::min(int(views.list.size()), kAddPerPage); - const auto add = std::min(added + views.left, kAddPerPage); + const auto add = std::min(views.total, kAddPerPage); const auto now = QDateTime::currentDateTime(); for (const auto &entry : views.list) { addMenuRow(entry, now); @@ -285,7 +293,7 @@ void RecentViews::showMenu() { } } while (count++ < add) { - addMenuRowPlaceholder(); + addMenuRowPlaceholder(session); } rpl::merge( _controller->moreViewsLoaded(), @@ -340,6 +348,7 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { return Ui::WhoReactedEntryData{ .text = peer->name(), .date = date, + .customEntityData = Data::ReactionEntityData(entry.reaction), .userpic = std::move(userpic), .callback = [=] { show->show(PrepareShortInfoBox(peer)); }, }; @@ -349,15 +358,17 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { auto data = prepare(i->view); i->peer = peer; i->date = date; + i->customEntityData = data.customEntityData; i->callback = data.callback; i->action->setData(std::move(data)); } else { auto view = Ui::PeerUserpicView(); auto data = prepare(view); auto callback = data.callback; + auto customEntityData = data.customEntityData; auto action = base::make_unique_q( _menu->menu(), - nullptr, + Data::ReactedMenuFactory(&entry.peer->session()), _menu->menu()->st(), prepare(view)); const auto raw = action.get(); @@ -366,6 +377,7 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { .action = raw, .peer = peer, .date = date, + .customEntityData = std::move(customEntityData), .callback = std::move(callback), .view = std::move(view), }); @@ -380,10 +392,10 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) { } } -void RecentViews::addMenuRowPlaceholder() { +void RecentViews::addMenuRowPlaceholder(not_null session) { auto action = base::make_unique_q( _menu->menu(), - nullptr, + Data::ReactedMenuFactory(session), _menu->menu()->st(), Ui::WhoReactedEntryData{ .preloader = true }); const auto raw = action.get(); @@ -393,23 +405,18 @@ void RecentViews::addMenuRowPlaceholder() { } void RecentViews::rebuildMenuTail() { - const auto offset = (_menuPlaceholderCount < _menuEntries.size()) - ? (end(_menuEntries) - _menuPlaceholderCount - 1)->peer->id - : PeerId(); - const auto views = _controller->views(offset); - if (views.list.empty()) { + const auto elements = _menuEntries.size() - _menuPlaceholderCount; + const auto views = _controller->views(elements + kAddPerPage, false); + if (views.list.size() <= elements) { return; } const auto now = QDateTime::currentDateTime(); const auto added = std::min( _menuPlaceholderCount + kAddPerPage, - int(views.list.size())); - auto add = added; - for (const auto &entry : views.list) { + int(views.list.size() - elements)); + for (auto i = elements, till = i + added; i != till; ++i) { + const auto &entry = views.list[i]; addMenuRow(entry, now); - if (!--add) { - break; - } } _menuEntriesCount = _menuEntriesCount.current() + added; } @@ -448,6 +455,7 @@ void RecentViews::subscribeToMenuUserpicsLoading( entry.action->setData({ .text = peer->name(), .date = entry.date, + .customEntityData = entry.customEntityData, .userpic = std::move(userpic), .callback = entry.callback, }); diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h index 14bbdba68..d469cbb5e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -32,6 +32,7 @@ class Controller; struct RecentViewsData { std::vector> list; + int reactions = 0; int total = 0; bool valid = false; @@ -55,6 +56,7 @@ private: not_null action; PeerData *peer = nullptr; QString date; + QString customEntityData; Fn callback; Ui::PeerUserpicView view; InMemoryKey key; @@ -68,7 +70,7 @@ private: void showMenu(); void addMenuRow(Data::StoryView entry, const QDateTime &now); - void addMenuRowPlaceholder(); + void addMenuRowPlaceholder(not_null session); void rebuildMenuTail(); void subscribeToMenuUserpicsLoading(not_null session); void refreshClickHandler(); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 7ea8ce8b7..48ee62d2e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_sending.h" #include "apiwrap.h" #include "base/call_delayed.h" +#include "base/timer_rpl.h" +#include "base/unixtime.h" #include "boxes/premium_limits_box.h" #include "boxes/send_files_box.h" #include "chat_helpers/compose/compose_show.h" @@ -30,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/stories/media_stories_controller.h" +#include "media/stories/media_stories_stealth.h" #include "menu/menu_send.h" #include "storage/localimageloader.h" #include "storage/storage_account.h" @@ -42,6 +45,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_media_view.h" namespace Media::Stories { +namespace { + +[[nodiscard]] rpl::producer PlaceholderText( + const std::shared_ptr &show) { + return show->session().data().stories().stealthModeValue( + ) | rpl::map([](Data::StealthMode value) { + return value.enabledTill; + }) | rpl::distinct_until_changed() | rpl::map([](TimeId till) { + return rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(250) + ) | rpl::map([=] { + return till - base::unixtime::now(); + }) | rpl::take_while([](TimeId left) { + return left > 0; + }) | rpl::then( + rpl::single(0) + ) | rpl::map([](TimeId left) { + return left + ? tr::lng_stealth_mode_countdown( + lt_left, + rpl::single(TimeLeftText(left))) + : tr::lng_story_reply_ph(); + }) | rpl::flatten_latest(); + }) | rpl::flatten_latest(); +} + +} // namespace class ReplyArea::Cant final : public Ui::RpWidget { public: @@ -85,10 +117,11 @@ ReplyArea::ReplyArea(not_null controller) .mode = HistoryView::ComposeControlsMode::Normal, .sendMenuType = SendMenu::Type::SilentOnly, .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), - .customPlaceholder = tr::lng_story_reply_ph(), + .customPlaceholder = PlaceholderText(_controller->uiShow()), .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), .voiceLockFromBottom = true, .features = { + .likes = true, .sendAs = false, .ttlInfo = false, .botCommandSend = false, @@ -588,6 +621,11 @@ void ReplyArea::initActions() { sendInlineResult(chosen.result, chosen.bot, chosen.options, localId); }, _lifetime); + _controls->likeToggled( + ) | rpl::start_with_next([=] { + _controller->toggleLiked(); + }, _lifetime); + _controls->setMimeDataHook([=]( not_null data, Ui::InputField::MimeAction action) { @@ -611,7 +649,9 @@ void ReplyArea::initActions() { _controls->showFinished(); } -void ReplyArea::show(ReplyAreaData data) { +void ReplyArea::show( + ReplyAreaData data, + rpl::producer likedValue) { if (_data == data) { return; } @@ -628,6 +668,11 @@ void ReplyArea::show(ReplyAreaData data) { const auto history = user ? user->owner().history(user).get() : nullptr; _controls->setHistory({ .history = history, + .liked = std::move( + likedValue + ) | rpl::map([](const Data::ReactionId &id) { + return !id.empty(); + }), }); _controls->clear(); const auto hidden = user && user->isSelf(); @@ -658,6 +703,10 @@ Main::Session &ReplyArea::session() const { return _data.user->session(); } +bool ReplyArea::focused() const { + return _controls->focused(); +} + rpl::producer ReplyArea::focusedValue() const { return _controls->focusedValue(); } @@ -686,6 +735,10 @@ void ReplyArea::tryProcessKeyInput(not_null e) { _controls->tryProcessKeyInput(e); } +not_null ReplyArea::likeAnimationTarget() const { + return _controls->likeAnimationTarget(); +} + void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index 9b9662b4a..34f40b856 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -41,6 +41,7 @@ class Session; namespace Ui { struct PreparedList; class SendFilesWay; +class RpWidget; } // namespace Ui namespace Media::Stories { @@ -60,9 +61,12 @@ public: explicit ReplyArea(not_null controller); ~ReplyArea(); - void show(ReplyAreaData data); + void show( + ReplyAreaData data, + rpl::producer likedValue); void sendReaction(const Data::ReactionId &id); + [[nodiscard]] bool focused() const; [[nodiscard]] rpl::producer focusedValue() const; [[nodiscard]] rpl::producer activeValue() const; [[nodiscard]] rpl::producer hasSendTextValue() const; @@ -70,6 +74,8 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] not_null likeAnimationTarget() const; + private: class Cant; diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp new file mode 100644 index 000000000..404ff1aea --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.cpp @@ -0,0 +1,406 @@ +/* +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 "media/stories/media_stories_stealth.h" + +#include "base/timer_rpl.h" +#include "base/unixtime.h" +#include "boxes/premium_preview_box.h" +#include "chat_helpers/compose/compose_show.h" +#include "data/data_peer_values.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "info/profile/info_profile_icon.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_premium.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_media_view.h" +#include "styles/style_layers.h" + +namespace Media::Stories { +namespace { + +constexpr auto kAlreadyToastDuration = 4 * crl::time(1000); +constexpr auto kCooldownButtonLabelOpacity = 0.5; + +struct State { + Data::StealthMode mode; + TimeId now = 0; + bool premium = false; +}; + +struct Feature { + const style::icon &icon; + QString title; + TextWithEntities about; +}; + +[[nodiscard]] Ui::Toast::Config ToastAlready(TimeId left) { + return { + .title = tr::lng_stealth_mode_already_title(tr::now), + .text = tr::lng_stealth_mode_already_about( + tr::now, + lt_left, + TextWithEntities{ TimeLeftText(left) }, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[nodiscard]] Ui::Toast::Config ToastActivated() { + return { + .title = tr::lng_stealth_mode_enabled_tip_title(tr::now), + .text = tr::lng_stealth_mode_enabled_tip( + tr::now, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[nodiscard]] Ui::Toast::Config ToastCooldown() { + return { + .text = tr::lng_stealth_mode_cooldown_tip( + tr::now, + Ui::Text::RichLangValue), + .st = &st::storiesStealthToast, + .duration = kAlreadyToastDuration, + .adaptive = true, + }; +} + +[[nodiscard]] rpl::producer StateValue( + not_null session) { + return rpl::combine( + session->data().stories().stealthModeValue(), + Data::AmPremiumValue(session) + ) | rpl::map([](Data::StealthMode mode, bool premium) { + return rpl::make_producer([=](auto consumer) { + struct Info { + base::Timer timer; + bool firstSent = false; + bool enabledSent = false; + bool cooldownSent = false; + }; + auto lifetime = rpl::lifetime(); + const auto info = lifetime.make_state(); + const auto check = [=] { + auto send = !info->firstSent; + const auto now = base::unixtime::now(); + const auto left1 = (mode.enabledTill - now); + const auto left2 = (mode.cooldownTill - now); + info->firstSent = true; + if (!info->enabledSent && left1 <= 0) { + send = true; + info->enabledSent = true; + } + if (!info->cooldownSent && left2 <= 0) { + send = true; + info->cooldownSent = true; + } + const auto left = (left1 <= 0) + ? left2 + : (left2 <= 0) + ? left1 + : std::min(left1, left2); + if (left > 0) { + info->timer.callOnce(left * crl::time(1000)); + } + if (send) { + consumer.put_next(State{ mode, now, premium }); + } + if (left <= 0) { + consumer.put_done(); + } + }; + info->timer.setCallback(check); + check(); + return lifetime; + }); + }) | rpl::flatten_latest(); +} + +[[nodiscard]] Feature FeaturePast() { + return { + .icon = st::storiesStealthFeaturePastIcon, + .title = tr::lng_stealth_mode_past_title(tr::now), + .about = { tr::lng_stealth_mode_past_about(tr::now) }, + }; +} + +[[nodiscard]] Feature FeatureNext() { + return { + .icon = st::storiesStealthFeatureNextIcon, + .title = tr::lng_stealth_mode_next_title(tr::now), + .about = { tr::lng_stealth_mode_next_about(tr::now) }, + }; +} + +[[nodiscard]] object_ptr MakeLogo(QWidget *parent) { + const auto add = st::storiesStealthLogoAdd; + const auto icon = &st::storiesStealthLogoIcon; + const auto size = QSize(2 * add, 2 * add) + icon->size(); + auto result = object_ptr>( + parent, + object_ptr(parent), + st::storiesStealthLogoMargin); + const auto inner = result->entity(); + inner->resize(size); + inner->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(inner); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::storiesComposeBlue); + p.setPen(Qt::NoPen); + const auto left = (inner->width() - size.width()) / 2; + const auto top = (inner->height() - size.height()) / 2; + const auto rect = QRect(QPoint(left, top), size); + p.drawEllipse(rect); + icon->paintInCenter(p, rect); + }, inner->lifetime()); + return result; +} + +[[nodiscard]] object_ptr MakeTitle(QWidget *parent) { + return object_ptr>( + parent, + object_ptr( + parent, + tr::lng_stealth_mode_title(tr::now), + st::storiesStealthBox.title), + st::storiesStealthTitleMargin); +} + +[[nodiscard]] object_ptr MakeAbout( + QWidget *parent, + rpl::producer state) { + auto text = std::move(state) | rpl::map([](const State &state) { + return state.premium + ? tr::lng_stealth_mode_about(tr::now) + : tr::lng_stealth_mode_unlock_about(tr::now); + }); + return object_ptr>( + parent, + object_ptr( + parent, + std::move(text), + st::storiesStealthAbout), + st::storiesStealthAboutMargin); +} + +[[nodiscard]] object_ptr MakeFeature( + QWidget *parent, + Feature feature) { + auto result = object_ptr>( + parent, + object_ptr(parent), + st::storiesStealthFeatureMargin); + const auto widget = result->entity(); + const auto icon = Ui::CreateChild( + widget, + feature.icon, + st::storiesStealthFeatureIconPosition); + const auto title = Ui::CreateChild( + widget, + feature.title, + st::storiesStealthFeatureTitle); + const auto about = Ui::CreateChild( + widget, + rpl::single(feature.about), + st::storiesStealthFeatureAbout); + icon->show(); + title->show(); + about->show(); + widget->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto left = st::storiesStealthFeatureLabelLeft; + const auto available = width - left; + title->resizeToWidth(available); + about->resizeToWidth(available); + auto top = 0; + title->move(left, top); + top += title->height() + st::storiesStealthFeatureSkip; + about->move(left, top); + top += about->height(); + widget->resize(width, top); + }, widget->lifetime()); + return result; +} + +[[nodiscard]] object_ptr MakeButton( + QWidget *parent, + rpl::producer state) { + auto text = rpl::duplicate(state) | rpl::map([](const State &state) { + if (!state.premium) { + return tr::lng_stealth_mode_unlock(); + } else if (state.mode.cooldownTill <= state.now) { + return tr::lng_stealth_mode_enable(); + } + return rpl::single( + rpl::empty + ) | rpl::then( + base::timer_each(250) + ) | rpl::map([=] { + const auto now = base::unixtime::now(); + const auto left = std::max(state.mode.cooldownTill - now, 1); + return tr::lng_stealth_mode_cooldown_in( + tr::now, + lt_left, + TimeLeftText(left)); + }) | rpl::type_erased(); + }) | rpl::flatten_latest(); + + auto result = object_ptr( + parent, + rpl::single(QString()), + st::storiesStealthBox.button); + const auto raw = result.data(); + + const auto label = Ui::CreateChild( + raw, + std::move(text), + st::storiesStealthButtonLabel); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); + + const auto lock = Ui::CreateChild(raw); + lock->setAttribute(Qt::WA_TransparentForMouseEvents); + lock->resize(st::storiesStealthLockIcon.size()); + lock->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(lock); + st::storiesStealthLockIcon.paintInCenter(p, lock->rect()); + }, lock->lifetime()); + + const auto lockLeft = -st::storiesStealthButtonLabel.style.font->height; + const auto updateLabelLockGeometry = [=] { + const auto outer = raw->width(); + const auto added = -st::storiesStealthBox.button.width; + const auto skip = lock->isHidden() ? 0 : (lockLeft + lock->width()); + const auto width = outer - added - skip; + const auto top = st::storiesStealthBox.button.textTop; + label->resizeToWidth(width); + label->move(added / 2, top); + const auto inner = std::min(label->textMaxWidth(), width); + const auto right = (added / 2) + (outer - inner) / 2 + inner; + const auto lockTop = (label->height() - lock->height()) / 2; + lock->move(right + lockLeft, top + lockTop); + }; + + std::move(state) | rpl::start_with_next([=](const State &state) { + const auto cooldown = state.premium + && (state.mode.cooldownTill > state.now); + label->setOpacity(cooldown ? kCooldownButtonLabelOpacity : 1.); + lock->setVisible(!state.premium); + updateLabelLockGeometry(); + }, label->lifetime()); + + raw->widthValue( + ) | rpl::start_with_next(updateLabelLockGeometry, label->lifetime()); + + return result; +} + +[[nodiscard]] object_ptr StealthModeBox( + std::shared_ptr show) { + return Box([=](not_null box) { + struct Data { + rpl::variable state; + bool requested = false; + }; + const auto data = box->lifetime().make_state(); + data->state = StateValue(&show->session()); + box->setWidth(st::boxWideWidth); + box->setStyle(st::storiesStealthBox); + box->addRow(MakeLogo(box)); + box->addRow(MakeTitle(box)); + box->addRow(MakeAbout(box, data->state.value())); + box->addRow(MakeFeature(box, FeaturePast())); + box->addRow( + MakeFeature(box, FeatureNext()), + (st::boxRowPadding + + QMargins(0, 0, 0, st::storiesStealthBoxBottom))); + box->setNoContentMargin(true); + box->addTopButton(st::storiesStealthBoxClose, [=] { + box->closeBox(); + }); + const auto button = box->addButton( + MakeButton(box, data->state.value())); + button->resizeToWidth(st::boxWideWidth + - st::storiesStealthBox.buttonPadding.left() + - st::storiesStealthBox.buttonPadding.right()); + button->setClickedCallback([=] { + const auto now = data->state.current(); + if (now.mode.enabledTill > now.now) { + show->showToast(ToastActivated()); + box->closeBox(); + } else if (!now.premium) { + data->requested = false; + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto window = show->resolveWindow(usage)) { + ShowPremiumPreviewBox(window, PremiumPreview::Stories); + window->window().activate(); + } + } else if (now.mode.cooldownTill > now.now) { + show->showToast(ToastCooldown()); + box->closeBox(); + } else if (!data->requested) { + data->requested = true; + show->session().data().stories().activateStealthMode( + crl::guard(box, [=] { data->requested = false; })); + } + }); + data->state.value() | rpl::filter([](const State &state) { + return state.mode.enabledTill > state.now; + }) | rpl::start_with_next([=] { + box->closeBox(); + show->showToast(ToastActivated()); + }, box->lifetime()); + }); +} + +} // namespace + +void SetupStealthMode(std::shared_ptr show) { + const auto now = base::unixtime::now(); + const auto mode = show->session().data().stories().stealthMode(); + if (const auto left = mode.enabledTill - now; left > 0) { + show->showToast(ToastAlready(left)); + } else { + show->show(StealthModeBox(show)); + } +} + +QString TimeLeftText(int left) { + Expects(left >= 0); + + const auto hours = left / 3600; + const auto minutes = (left % 3600) / 60; + const auto seconds = left % 60; + const auto zero = QChar('0'); + if (hours) { + return u"%1:%2:%3"_q + .arg(hours) + .arg(minutes, 2, 10, zero) + .arg(seconds, 2, 10, zero); + } else if (minutes) { + return u"%1:%2"_q.arg(minutes).arg(seconds, 2, 10, zero); + } + return u"0:%1"_q.arg(left, 2, 10, zero); +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_stealth.h b/Telegram/SourceFiles/media/stories/media_stories_stealth.h new file mode 100644 index 000000000..f40b77bc4 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_stealth.h @@ -0,0 +1,20 @@ +/* +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 ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Media::Stories { + +void SetupStealthMode(std::shared_ptr show); + +[[nodiscard]] QString TimeLeftText(int left); + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index dfbab352b..41390c3c4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -59,6 +59,10 @@ void View::updatePlayback(const Player::TrackState &state) { _controller->updateVideoPlayback(state); } +ClickHandlerPtr View::lookupLocationHandler(QPoint point) const { + return _controller->lookupLocationHandler(point); +} + bool View::subjumpAvailable(int delta) const { return _controller->subjumpAvailable(delta); } @@ -111,6 +115,21 @@ void View::tryProcessKeyInput(not_null e) { _controller->tryProcessKeyInput(e); } +bool View::allowStealthMode() const { + return _controller->allowStealthMode(); +} + +void View::setupStealthMode() { + _controller->setupStealthMode(); +} + +auto View::attachReactionsToMenu( + not_null menu, + QPoint desiredPosition) +-> AttachStripResult { + return _controller->attachReactionsToMenu(menu, desiredPosition); +} + SiblingView View::sibling(SiblingType type) const { return _controller->sibling(type); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index 4d6cdac1e..730488253 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -17,6 +17,14 @@ namespace Media::Player { struct TrackState; } // namespace Media::Player +namespace HistoryView::Reactions { +enum class AttachSelectorResult; +} // namespace HistoryView::Reactions + +namespace Ui { +class PopupMenu; +} // namespace Ui + namespace Media::Stories { class Delegate; @@ -73,6 +81,7 @@ public: void showFullCaption(); void updatePlayback(const Player::TrackState &state); + [[nodiscard]] ClickHandlerPtr lookupLocationHandler(QPoint point) const; [[nodiscard]] bool subjumpAvailable(int delta) const; [[nodiscard]] bool subjumpFor(int delta) const; @@ -91,6 +100,14 @@ public: [[nodiscard]] bool ignoreWindowMove(QPoint position) const; void tryProcessKeyInput(not_null e); + [[nodiscard]] bool allowStealthMode() const; + void setupStealthMode(); + + using AttachStripResult = HistoryView::Reactions::AttachSelectorResult; + [[nodiscard]] AttachStripResult attachReactionsToMenu( + not_null menu, + QPoint desiredPosition); + [[nodiscard]] rpl::lifetime &lifetime(); private: diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index adf770688..67e684764 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -109,6 +109,7 @@ mediaviewRight: icon { { "mediaview/next", mediaviewControlFg } }; mediaviewSave: icon {{ "mediaview/download", mediaviewControlFg }}; +mediaviewSaveLocked: icon {{ "mediaview/download_locked", mediaviewControlFg }}; mediaviewShare: icon {{ "mediaview/viewer_share", mediaviewControlFg }}; mediaviewRotate: icon {{ "mediaview/rotate", mediaviewControlFg }}; mediaviewMore: icon {{ "title_menu_dots", mediaviewControlFg }}; @@ -144,29 +145,30 @@ mediaviewFileIconSize: 80px; mediaviewFileLink: defaultLinkButton; mediaviewMenuSeparator: MenuSeparator(defaultMenuSeparator) { - fg: mediaviewMenuFg; + fg: groupCallMenuBgOver; } mediaviewMenu: Menu(menuWithIcons) { - itemBg: mediaviewMenuBg; - itemBgOver: mediaviewMenuBgOver; - itemFg: mediaviewMenuFg; - itemFgOver: mediaviewMenuFg; - itemFgDisabled: mediaviewMenuFg; - itemFgShortcut: mediaviewMenuFg; - itemFgShortcutOver: mediaviewMenuFg; - itemFgShortcutDisabled: mediaviewMenuFg; + itemBg: groupCallMenuBg; + itemBgOver: groupCallMenuBgOver; + itemFg: groupCallMembersFg; + itemFgOver: groupCallMembersFg; + itemFgDisabled: groupCallMemberNotJoinedStatus; + itemFgShortcut: groupCallMemberNotJoinedStatus; + itemFgShortcutOver: groupCallMemberNotJoinedStatus; + itemFgShortcutDisabled: groupCallMemberNotJoinedStatus; separator: mediaviewMenuSeparator; + arrow: icon {{ "menu/submenu_arrow", groupCallMemberNotJoinedStatus }}; ripple: RippleAnimation(defaultRippleAnimation) { - color: mediaviewMenuBgRipple; + color: groupCallMenuBgRipple; } } mediaviewMenuShadow: Shadow(defaultEmptyShadow) { - fallback: mediaviewMenuBg; + fallback: groupCallMenuBg; } mediaviewPanelAnimation: PanelAnimation(defaultPanelAnimation) { - fadeBg: mediaviewMenuBg; + fadeBg: groupCallMenuBg; shadow: mediaviewMenuShadow; } mediaviewPopupMenu: PopupMenu(defaultPopupMenu) { @@ -177,7 +179,7 @@ mediaviewPopupMenu: PopupMenu(defaultPopupMenu) { mediaviewDropdownMenu: DropdownMenu(defaultDropdownMenu) { menu: mediaviewMenu; wrap: InnerDropdown(defaultInnerDropdown) { - bg: mediaviewMenuBg; + bg: groupCallMenuBg; animation: mediaviewPanelAnimation; scrollPadding: margins(0px, 8px, 0px, 8px); shadow: mediaviewMenuShadow; @@ -311,7 +313,7 @@ mediaviewSpeedMenu: MediaSpeedMenu(mediaPlayerSpeedMenu) { dropdown: DropdownMenu(mediaviewDropdownMenu) { menu: Menu(mediaviewMenu) { separator: MenuSeparator(mediaviewMenuSeparator) { - fg: mediaviewMenuBgOver; + fg: groupCallMenuBgOver; padding: margins(0px, 4px, 0px, 4px); width: 6px; } @@ -413,8 +415,8 @@ storiesSiblingWidthMin: 200px; // Try making sibling not less than this. storiesMaxNameFontSize: 17px; storiesRadius: 8px; storiesControlSize: 64px; -storiesLeft: icon {{ "mediaview/stories_next-flip_horizontal", mediaviewControlFg }}; -storiesRight: icon {{ "mediaview/stories_next", mediaviewControlFg }}; +storiesLeft: icon {{ "stories/next-flip_horizontal", mediaviewControlFg }}; +storiesRight: icon {{ "stories/next", mediaviewControlFg }}; storiesSliderWidth: 2px; storiesSliderMargin: margins(8px, 7px, 8px, 6px); storiesSliderSkip: 4px; @@ -466,6 +468,10 @@ storiesAttach: IconButton(historyAttach) { iconOver: icon {{ "chat/input_attach", storiesComposeGrayIcon }}; ripple: storiesComposeRippleLight; } +storiesLike: IconButton(storiesAttach) { + icon: icon {{ "chat/input_like", storiesComposeGrayIcon }}; + iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }}; +} storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; storiesRemoveSet: IconButton(stickerPanRemoveSet) { @@ -473,9 +479,15 @@ storiesRemoveSet: IconButton(stickerPanRemoveSet) { iconOver: icon {{ "simple_close", storiesComposeGrayIcon }}; ripple: storiesComposeRippleLight; } -storiesMenuSeparator: MenuSeparator(defaultMenuSeparator) { - fg: groupCallMenuBgOver; +storiesColorAll: IconButton(emojiPanColorAll) { + icon: icon {{ "emoji/emoji_skin", storiesComposeGrayIcon }}; + iconOver: icon {{ "emoji/emoji_skin", storiesComposeGrayIcon }}; + ripple: storiesComposeRippleLight; } +storiesColorAllLabel: FlatLabel(emojiPanColorAllLabel) { + textFg: storiesComposeGrayText; +} +storiesMenuSeparator: mediaviewMenuSeparator; storiesMenu: Menu(defaultMenu) { itemBg: groupCallMenuBg; itemBgOver: groupCallMenuBgOver; @@ -493,22 +505,14 @@ storiesMenu: Menu(defaultMenu) { color: groupCallMenuBgRipple; } } -storiesMenuShadow: Shadow(defaultEmptyShadow) { - fallback: groupCallMenuBg; -} -storiesMenuAnimation: PanelAnimation(defaultPanelAnimation) { - fadeBg: groupCallMenuBg; - shadow: storiesMenuShadow; -} +storiesMenuShadow: mediaviewMenuShadow; +storiesMenuAnimation: mediaviewPanelAnimation; storiesPopupMenu: PopupMenu(defaultPopupMenu) { shadow: storiesMenuShadow; menu: storiesMenu; animation: storiesMenuAnimation; } -storiesMenuWithIcons: Menu(storiesMenu) { - itemIconPosition: point(15px, 5px); - itemPadding: margins(54px, 8px, 17px, 8px); -} +storiesMenuWithIcons: mediaviewMenu; storiesPopupMenuWithIcons: PopupMenu(storiesPopupMenu) { scrollPadding: margins(0px, 5px, 0px, 5px); menu: storiesMenuWithIcons; @@ -592,6 +596,8 @@ storiesEmojiPan: EmojiPan(defaultEmojiPan) { rippleBgActive: storiesComposeBgOver; } search: storiesEmojiTabbedSearch; + colorAll: storiesColorAll; + colorAllLabel: storiesColorAllLabel; removeSet: storiesRemoveSet; boxLabel: storiesBoxLabel; icons: ComposeIcons { @@ -676,6 +682,8 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } attach: storiesAttach; emoji: storiesAttachEmoji; + like: storiesLike; + liked: icon{}; suggestions: EmojiSuggestions(defaultEmojiSuggestions) { dropdown: InnerDropdown(emojiSuggestionsDropdown) { animation: PanelAnimation(defaultPanelAnimation) { @@ -771,8 +779,9 @@ storiesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) { scrollPadding: margins(0px, 6px, 0px, 4px); maxHeight: 320px; menu: Menu(storiesMenuWithIcons) { - widthMin: 215px; - widthMax: 215px; + itemPadding: margins(54px, 8px, 17px, 8px); + widthMin: 240px; + widthMax: 240px; } radius: 7px; } @@ -800,6 +809,7 @@ storiesReactionsPan: EmojiPan(storiesEmojiPan) { storiesReactionsWidth: 210px; storiesReactionsBottomSkip: 29px; storiesReactionsAddedTop: 200px; +storiesLikeReactionsPosition: point(85px, 30px); storiesUnsupportedLabel: FlatLabel(defaultFlatLabel) { textFg: mediaviewControlFg; @@ -911,3 +921,73 @@ storiesInfoTooltipMaxWidth: 360px; storiesCaptionPullThreshold: 50px; storiesShowMorePadding: margins(6px, 4px, 6px, 4px); storiesShowMoreFont: semiboldFont; + +storiesStealthLogoIcon: icon{{ "stories/stealth_logo", storiesComposeWhiteText }}; +storiesStealthLogoAdd: 12px; +storiesStealthLogoMargin: margins(0px, 28px, 0px, 7px); +storiesStealthBox: Box(defaultBox) { + buttonPadding: margins(10px, 10px, 10px, 10px); + buttonHeight: 42px; + button: RoundButton(defaultBoxButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + + textFg: storiesComposeWhiteText; + textFgOver: storiesComposeWhiteText; + numbersTextFg: storiesComposeWhiteText; + numbersTextFgOver: storiesComposeWhiteText; + textBg: storiesComposeBlue; + textBgOver: storiesComposeBlue; + + ripple: universalRippleAnimation; + } + margin: margins(0px, 56px, 0px, 10px); + bg: groupCallMembersBg; + title: FlatLabel(boxTitle) { + textFg: groupCallMembersFg; + align: align(top); + } + titleAdditionalFg: groupCallMemberNotJoinedStatus; +} +storiesStealthButtonLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: storiesComposeWhiteText; + align: align(top); + minWidth: 20px; + maxHeight: 20px; +} +storiesStealthLockIcon: icon {{ "dialogs/dialogs_lock_on", storiesComposeWhiteText }}; +storiesStealthTitleMargin: margins(0px, 10px, 0px, 0px); +storiesStealthBoxClose: IconButton(defaultIconButton) { + width: boxTitleHeight; + height: boxTitleHeight; + + icon: icon {{ "box_button_close", storiesComposeGrayIcon }}; + iconOver: icon {{ "box_button_close", storiesComposeGrayIcon }}; + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: storiesComposeRippleLight; +} +storiesStealthAbout: FlatLabel(defaultFlatLabel) { + textFg: storiesComposeGrayText; + align: align(top); + minWidth: 20px; +} +storiesStealthAboutMargin: margins(0px, 5px, 0px, 15px); +storiesStealthFeatureTitle: storiesHeaderName; +storiesStealthFeatureAbout: FlatLabel(defaultFlatLabel) { + textFg: storiesComposeGrayText; + minWidth: 20px; +} +storiesStealthFeaturePastIcon: icon{{ "stories/stealth_5m", storiesComposeBlue }}; +storiesStealthFeatureNextIcon: icon{{ "stories/stealth_25m", storiesComposeBlue }}; +storiesStealthFeatureIconPosition: point(3px, 7px); +storiesStealthFeatureMargin: margins(0px, 8px, 0px, 6px); +storiesStealthFeatureLabelLeft: 46px; +storiesStealthFeatureSkip: 2px; +storiesStealthBoxBottom: 11px; +storiesStealthToast: Toast(defaultMultilineToast) { + maxWidth: 340px; +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp index 7851ed7f9..97b59fc72 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/view/media_view_overlay_opengl.h" +#include "data/data_peer_values.h" // AmPremiumValue. #include "ui/gl/gl_shader.h" #include "ui/painter.h" #include "media/stories/media_stories_view.h" @@ -122,7 +123,16 @@ OverlayWidget::RendererGL::RendererGL(not_null owner) crl::on_main(this, [=] { _owner->_storiesChanged.events( ) | rpl::start_with_next([=] { - invalidateControls(); + if (_owner->_storiesSession) { + Data::AmPremiumValue( + _owner->_storiesSession + ) | rpl::start_with_next([=] { + invalidateControls(); + }, _storiesLifetime); + } else { + _storiesLifetime.destroy(); + invalidateControls(); + } }, _lifetime); }); } @@ -253,9 +263,10 @@ void OverlayWidget::RendererGL::paint( if (handleHideWorkaround(f)) { return; } - const auto factor = widget->devicePixelRatio(); + const auto factor = widget->devicePixelRatioF(); if (_factor != factor) { _factor = factor; + _ifactor = int(std::ceil(factor)); _controlsImage.invalidate(); // We use the fact that fade texture atlas @@ -648,8 +659,7 @@ void OverlayWidget::RendererGL::paintControl( QRect inner, float64 innerOpacity, const style::icon &icon) { - const auto stories = (_owner->_stories != nullptr); - const auto meta = ControlMeta(control, stories); + const auto meta = controlMeta(control); Assert(meta.icon == &icon); const auto overAlpha = overOpacity * kOverBackgroundOpacity; @@ -707,18 +717,25 @@ void OverlayWidget::RendererGL::paintControl( FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); } -auto OverlayWidget::RendererGL::ControlMeta(Over control, bool stories) --> Control { +auto OverlayWidget::RendererGL::controlMeta(Over control) const -> Control { + const auto stories = [&] { + return (_owner->_stories != nullptr); + }; switch (control) { case Over::Left: return { 0, - stories ? &st::storiesLeft : &st::mediaviewLeft + stories() ? &st::storiesLeft : &st::mediaviewLeft }; case Over::Right: return { 1, - stories ? &st::storiesRight : &st::mediaviewRight + stories() ? &st::storiesRight : &st::mediaviewRight + }; + case Over::Save: return { + 2, + (_owner->saveControlLocked() + ? &st::mediaviewSaveLocked + : &st::mediaviewSave) }; - case Over::Save: return { 2, &st::mediaviewSave }; case Over::Share: return { 3, &st::mediaviewShare }; case Over::Rotate: return { 4, &st::mediaviewRotate }; case Over::More: return { 5, &st::mediaviewMore }; @@ -730,14 +747,13 @@ void OverlayWidget::RendererGL::validateControls() { if (!_controlsImage.image().isNull()) { return; } - const auto stories = (_owner->_stories != nullptr); const auto metas = { - ControlMeta(Over::Left, stories), - ControlMeta(Over::Right, stories), - ControlMeta(Over::Save, stories), - ControlMeta(Over::Share, stories), - ControlMeta(Over::Rotate, stories), - ControlMeta(Over::More, stories), + controlMeta(Over::Left), + controlMeta(Over::Right), + controlMeta(Over::Save), + controlMeta(Over::Share), + controlMeta(Over::Rotate), + controlMeta(Over::More), }; auto maxWidth = 0; auto fullHeight = 0; @@ -748,10 +764,10 @@ void OverlayWidget::RendererGL::validateControls() { maxWidth = std::max(st::mediaviewIconOver, maxWidth); fullHeight += st::mediaviewIconOver; auto image = QImage( - QSize(maxWidth, fullHeight) * _factor, + QSize(maxWidth, fullHeight) * _ifactor, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); - image.setDevicePixelRatio(_factor); + image.setDevicePixelRatio(_ifactor); { auto p = QPainter(&image); auto index = 0; @@ -759,8 +775,8 @@ void OverlayWidget::RendererGL::validateControls() { for (const auto &meta : metas) { meta.icon->paint(p, 0, height, maxWidth); _controlsTextures[index++] = QRect( - QPoint(0, height) * _factor, - meta.icon->size() * _factor); + QPoint(0, height) * _ifactor, + meta.icon->size() * _ifactor); height += meta.icon->height(); } auto hq = PainterHighQualityEnabler(p); @@ -769,8 +785,8 @@ void OverlayWidget::RendererGL::validateControls() { p.drawEllipse( QRect(0, height, st::mediaviewIconOver, st::mediaviewIconOver)); _controlsTextures[index++] = QRect( - QPoint(0, height) * _factor, - QSize(st::mediaviewIconOver, st::mediaviewIconOver) * _factor); + QPoint(0, height) * _ifactor, + QSize(st::mediaviewIconOver, st::mediaviewIconOver) * _ifactor); height += st::mediaviewIconOver; } _controlsImage.setImage(std::move(image)); @@ -802,10 +818,10 @@ void OverlayWidget::RendererGL::validateControlsFade() { const auto height = bottomTop + bottom.height(); auto image = QImage( - QSize(width, height) * _factor, + QSize(width, height) * _ifactor, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); - image.setDevicePixelRatio(_factor); + image.setDevicePixelRatio(_ifactor); auto p = QPainter(&image); top.paint(p, 0, 0, width); @@ -992,18 +1008,18 @@ void OverlayWidget::RendererGL::paintUsingRaster( int bufferOffset, bool transparent) { auto raster = image.takeImage(); - const auto size = rect.size() * _factor; + const auto size = rect.size() * _ifactor; if (raster.width() < size.width() || raster.height() < size.height()) { raster = QImage(size, QImage::Format_ARGB32_Premultiplied); Assert(!raster.isNull()); - raster.setDevicePixelRatio(_factor); + raster.setDevicePixelRatio(_ifactor); if (!transparent && (raster.width() > size.width() || raster.height() > size.height())) { raster.fill(Qt::transparent); } - } else if (raster.devicePixelRatio() != _factor) { - raster.setDevicePixelRatio(_factor); + } else if (raster.devicePixelRatio() != _ifactor) { + raster.setDevicePixelRatio(_ifactor); } if (transparent) { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h index a0342518a..0138a492d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -114,6 +114,7 @@ private: QOpenGLFunctions *_f = nullptr; QSize _viewport; float _factor = 1.; + int _ifactor = 1; QVector2D _uniformViewport; std::optional _contentBuffer; @@ -149,7 +150,7 @@ private: Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount]; static constexpr auto kControlsCount = 6; - [[nodiscard]] static Control ControlMeta(Over control, bool stories); + [[nodiscard]] Control controlMeta(Over control) const; // Last one is for the over circle image. std::array _controlsTextures; @@ -158,6 +159,7 @@ private: bool _shadowsForStories = false; bool _blendingEnabled = false; + rpl::lifetime _storiesLifetime; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index fb30c3b4d..957679aba 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_peer_photo.h" #include "lang/lang_keys.h" #include "mainwindow.h" +#include "boxes/premium_preview_box.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/file_utilities.h" @@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/media/history_view_media.h" #include "history/view/reactions/history_view_reactions_strip.h" +#include "history/view/reactions/history_view_reactions_selector.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_stories.h" @@ -86,6 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session_settings.h" #include "layout/layout_document_generic_preview.h" #include "platform/platform_overlay_widget.h" +#include "settings/settings_premium.h" #include "storage/file_download.h" #include "storage/storage_account.h" #include "calls/calls_instance.h" @@ -130,6 +133,7 @@ constexpr auto kIdsPreloadAfter = 28; constexpr auto kLeftSiblingTextureIndex = 1; constexpr auto kRightSiblingTextureIndex = 2; constexpr auto kStoriesControlsOpacity = 1.; +constexpr auto kStorySavePromoDuration = 3 * crl::time(1000); class PipDelegate final : public Pip::Delegate { public: @@ -326,16 +330,18 @@ public: return _widget->_body; } bool valid() const override { - return _widget->_storiesSession != nullptr; + return _widget->_session || _widget->_storiesSession; } operator bool() const override { return valid(); } Main::Session &session() const override { - Expects(_widget->_storiesSession != nullptr); + Expects(_widget->_session || _widget->_storiesSession); - return *_widget->_storiesSession; + return _widget->_session + ? *_widget->_session + : *_widget->_storiesSession; } bool paused(ChatHelpers::PauseReason reason) const override { if (_widget->isHidden() @@ -486,6 +492,15 @@ OverlayWidget::OverlayWidget() return base::EventFilterResult::Cancel; } else if (type == QEvent::ThemeChange && Platform::IsLinux()) { _window->setWindowIcon(Window::CreateIcon(_session)); + } else if (type == QEvent::ContextMenu) { + const auto event = static_cast(e.get()); + const auto mouse = (event->reason() == QContextMenuEvent::Mouse); + const auto position = mouse + ? std::make_optional(event->pos()) + : std::nullopt; + if (handleContextMenu(position)) { + return base::EventFilterResult::Cancel; + } } return base::EventFilterResult::Continue; }); @@ -534,15 +549,6 @@ OverlayWidget::OverlayWidget() handleMouseRelease(mousePosition(e), mouseButton(e)); } else if (type == QEvent::MouseMove) { handleMouseMove(mousePosition(e)); - } else if (type == QEvent::ContextMenu) { - const auto event = static_cast(e.get()); - const auto mouse = (event->reason() == QContextMenuEvent::Mouse); - const auto position = mouse - ? std::make_optional(event->pos()) - : std::nullopt; - if (handleContextMenu(position)) { - return base::EventFilterResult::Cancel; - } } else if (type == QEvent::MouseButtonDblClick) { if (handleDoubleClick(mousePosition(e), mouseButton(e))) { return base::EventFilterResult::Cancel; @@ -1024,22 +1030,26 @@ QSize OverlayWidget::flipSizeByRotation(QSize size) const { return FlipSizeByRotation(size, _rotation); } -bool OverlayWidget::hasCopyMediaRestriction() const { - const auto story = _stories ? _stories->story() : nullptr; - return (story && !story->canDownload()) - || (_history && !_history->peer->allowsForwarding()) +bool OverlayWidget::hasCopyMediaRestriction(bool skipPremiumCheck) const { + if (const auto story = _stories ? _stories->story() : nullptr) { + return skipPremiumCheck + ? !story->canDownloadIfPremium() + : !story->canDownloadChecked(); + } + return (_history && !_history->peer->allowsForwarding()) || (_message && _message->forbidsSaving()); } -bool OverlayWidget::showCopyMediaRestriction() { - if (!hasCopyMediaRestriction()) { +bool OverlayWidget::showCopyMediaRestriction(bool skipPRemiumCheck) { + if (!hasCopyMediaRestriction(skipPRemiumCheck)) { return false; - } else if (!_history) { - return true; + } else if (_stories) { + uiShow()->showToast(tr::lng_error_nocopy_story(tr::now)); + } else if (_history) { + uiShow()->showToast(_history->peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); } - Ui::Toast::Show(_widget, _history->peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -1210,8 +1220,8 @@ void OverlayWidget::refreshNavVisibility() { } } -bool OverlayWidget::contentCanBeSaved() const { - if (hasCopyMediaRestriction()) { +bool OverlayWidget::computeSaveButtonVisible() const { + if (hasCopyMediaRestriction(true)) { return false; } else if (_photo) { return _photo->hasVideo() || _photoMedia->loaded(); @@ -1240,6 +1250,29 @@ void OverlayWidget::checkForSaveLoaded() { } } +void OverlayWidget::showPremiumDownloadPromo() { + const auto filter = [=](const auto &...) { + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto window = uiShow()->resolveWindow(usage)) { + ShowPremiumPreviewBox(window, PremiumPreview::Stories); + window->window().activate(); + } + return false; + }; + uiShow()->showToast({ + .text = tr::lng_stories_save_promo( + tr::now, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_as_premium_required_link(tr::now))), + Ui::Text::WithEntities), + .duration = kStorySavePromoDuration, + .adaptive = true, + .filter = filter, + }); +} + void OverlayWidget::updateControls() { if (_document && documentBubbleShown()) { _docRect = QRect( @@ -1290,7 +1323,7 @@ void OverlayWidget::updateControls() { const auto overRect = QRect( QPoint(), QSize(st::mediaviewIconOver, st::mediaviewIconOver)); - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); _shareVisible = story && story->canShare(); _rotateVisible = !_themePreviewShown && !story; const auto navRect = [&](int i) { @@ -1313,13 +1346,14 @@ void OverlayWidget::updateControls() { } _shareNav = navRect(index); _shareNavOver = style::centerrect(_shareNav, overRect); - _shareNavIcon = style::centerrect(_shareNav, st::mediaviewSave); + _shareNavIcon = style::centerrect(_shareNav, st::mediaviewShare); if (_shareVisible) { ++index; } _saveNav = navRect(index); _saveNavOver = style::centerrect(_saveNav, overRect); _saveNavIcon = style::centerrect(_saveNav, st::mediaviewSave); + Assert(st::mediaviewSave.size() == st::mediaviewSaveLocked.size()); const auto dNow = QDateTime::currentDateTime(); const auto d = [&] { @@ -1471,7 +1505,7 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { ? &st::mediaMenuIconArchiveStory : &st::mediaMenuIconSaveStory); } - if ((!story || story->canDownload()) + if ((!story || story->canDownloadChecked()) && _document && !_document->filepath(true).isEmpty()) { const auto text = Platform::IsMac() @@ -1533,11 +1567,13 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { [=] { deleteMedia(); }, &st::mediaMenuIconDelete); } - if (!hasCopyMediaRestriction()) { + if (!hasCopyMediaRestriction(true)) { addAction( tr::lng_mediaview_save_as(tr::now), [=] { saveAs(); }, - &st::mediaMenuIconDownload); + (saveControlLocked() + ? &st::mediaMenuIconDownloadLocked + : &st::mediaMenuIconDownload)); } if (const auto overviewType = computeOverviewType()) { @@ -1631,6 +1667,15 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { } }, &st::mediaMenuIconReport); }(); + if (_stories && _stories->allowStealthMode()) { + const auto now = base::unixtime::now(); + const auto stealth = _session->data().stories().stealthMode(); + addAction(tr::lng_stealth_mode_menu_item(tr::now), [=] { + _stories->setupStealthMode(); + }, ((_session->premium() || (stealth.enabledTill > now)) + ? &st::mediaMenuIconStealth + : &st::mediaMenuIconStealthLocked)); + } if (story && story->canReport()) { addAction(tr::lng_profile_report(tr::now), [=] { _stories->reportRequested(); @@ -1848,6 +1893,7 @@ void OverlayWidget::resizeContentByScreenSize() { _y = content.y(); _w = content.width(); _h = content.height(); + _zoom = 0; updateNavigationControlsGeometry(); return; } @@ -2009,6 +2055,9 @@ void OverlayWidget::zoomOut() { } void OverlayWidget::zoomReset() { + if (_stories || _fullScreenVideo) { + return; + } auto newZoom = _zoom; const auto full = _fullScreenVideo ? _zoomToScreen : _zoomToDefault; if (_zoom == 0) { @@ -2276,7 +2325,11 @@ void OverlayWidget::notifyFileDialogShown(bool shown) { } void OverlayWidget::saveAs() { - if (showCopyMediaRestriction()) { + if (showCopyMediaRestriction(true)) { + return; + } else if (hasCopyMediaRestriction()) { + Assert(_stories != nullptr); + showPremiumDownloadPromo(); return; } QString file; @@ -2412,9 +2465,13 @@ void OverlayWidget::handleDocumentClick() { void OverlayWidget::downloadMedia() { if (!_photo && !_document) { return; - } - if (Core::App().settings().askDownloadPath()) { + } else if (Core::App().settings().askDownloadPath()) { return saveAs(); + } else if (hasCopyMediaRestriction()) { + if (_stories && !hasCopyMediaRestriction(true)) { + showPremiumDownloadPromo(); + } + return; } QString path; @@ -2455,21 +2512,28 @@ void OverlayWidget::downloadMedia() { } else { if (_document->filepath(true).isEmpty() && !_document->loading()) { + const auto document = _document; + const auto checkSaveStarted = [=] { + if (isHidden() || _document != document) { + return; + } + _documentLoadingTo = _document->loadingFilePath(); + if (_stories && _documentLoadingTo.isEmpty()) { + const auto toName = _document->filepath(true); + if (!toName.isEmpty()) { + showSaveMsgToast( + toName, + tr::lng_mediaview_video_saved_to); + } + } + }; DocumentSaveClickHandler::SaveAndTrack( _message ? _message->fullId() : FullMsgId(), _document, - DocumentSaveClickHandler::Mode::ToFile); - _documentLoadingTo = _document->loadingFilePath(); - if (_stories && _documentLoadingTo.isEmpty()) { - toName = _document->filepath(true); - if (!toName.isEmpty()) { - showSaveMsgToast( - toName, - tr::lng_mediaview_video_saved_to); - } - } + DocumentSaveClickHandler::Mode::ToFile, + crl::guard(_widget, checkSaveStarted)); } else { - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); update(_saveNavOver); } updateOver(_lastMouseMovePos); @@ -2489,7 +2553,7 @@ void OverlayWidget::downloadMedia() { } } else { if (!_photo || !_photoMedia->loaded()) { - _saveVisible = contentCanBeSaved(); + _saveVisible = computeSaveButtonVisible(); update(_saveNavOver); } else { if (!QDir().exists(path)) { @@ -3890,8 +3954,7 @@ void OverlayWidget::initThemePreview() { _themeShare->setClickedCallback([=] { QGuiApplication::clipboard()->setText( session->createInternalLinkFull("addtheme/" + slug)); - Ui::Toast::Show( - _body, + uiShow()->showToast( tr::lng_background_link_copied(tr::now)); }); } else { @@ -4189,6 +4252,10 @@ not_null OverlayWidget::storiesWrap() { } std::shared_ptr OverlayWidget::storiesShow() { + return uiShow(); +} + +std::shared_ptr OverlayWidget::uiShow() { if (!_cachedShow) { _cachedShow = std::make_shared(this); } @@ -4315,7 +4382,9 @@ int OverlayWidget::storiesTopNotchSkip() { void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); - if (!videoShown() || (!_streamed->controls && !_fullScreenVideo)) { + if (_stories + || !videoShown() + || (!_streamed->controls && !_fullScreenVideo)) { return; } _fullScreenVideo = !_fullScreenVideo; @@ -4769,6 +4838,13 @@ void OverlayWidget::paintSaveMsgContent( p.setOpacity(1); } +bool OverlayWidget::saveControlLocked() const { + const auto story = _stories ? _stories->story() : nullptr; + return story + && story->canDownloadIfPremium() + && !story->canDownloadChecked(); +} + void OverlayWidget::paintControls( not_null renderer, float64 opacity) { @@ -4802,7 +4878,9 @@ void OverlayWidget::paintControls( _saveVisible, _saveNavOver, _saveNavIcon, - st::mediaviewSave }, + (saveControlLocked() + ? st::mediaviewSaveLocked + : st::mediaviewSave) }, { Over::Share, _shareVisible, @@ -5101,7 +5179,9 @@ void OverlayWidget::handleWheelEvent(not_null e) { } void OverlayWidget::setZoomLevel(int newZoom, bool force) { - if (!force && _zoom == newZoom) { + if (_stories + || (!force && _zoom == newZoom) + || (_fullScreenVideo && newZoom != kZoomToScreenLevel)) { return; } @@ -5249,6 +5329,7 @@ void OverlayWidget::setContext( { story->peer->id, story->id }); if (maybeStory) { _stories->show(*maybeStory, story->within); + _dropdown->raise(); } } else { _message = nullptr; @@ -5289,7 +5370,6 @@ void OverlayWidget::setStoriesPeer(PeerData *peer) { updateControlsGeometry(); }, _stories->lifetime()); _storiesChanged.fire({}); - _dropdown->raise(); } } @@ -5602,6 +5682,9 @@ void OverlayWidget::updateOver(QPoint pos) { const auto point = pos - QPoint(_groupThumbsLeft, _groupThumbsTop); lnk = _groupThumbs->getState(point); lnkhost = this; + } else if (_stories) { + lnk = _stories->lookupLocationHandler(pos); + lnkhost = this; } @@ -5782,20 +5865,33 @@ bool OverlayWidget::handleContextMenu(std::optional position) { const style::icon *icon) { _menu->addAction(text, std::move(handler), icon); }); + if (_menu->empty()) { _menu = nullptr; - } else { + return true; + } + if (_stories) { + _stories->menuShown(true); + } + _menu->setDestroyedCallback(crl::guard(_widget, [=] { if (_stories) { - _stories->menuShown(true); + _stories->menuShown(false); } - _menu->setDestroyedCallback(crl::guard(_widget, [=] { - if (_stories) { - _stories->menuShown(false); - } - activateControls(); - _receiveMouse = false; - InvokeQueued(_widget, [=] { receiveMouse(); }); - })); + activateControls(); + _receiveMouse = false; + InvokeQueued(_widget, [=] { receiveMouse(); }); + })); + + using HistoryView::Reactions::AttachSelectorResult; + const auto attached = _stories + ? _stories->attachReactionsToMenu(_menu.get(), QCursor::pos()) + : AttachSelectorResult::Skipped; + if (attached == AttachSelectorResult::Failed) { + _menu = nullptr; + return true; + } else if (attached == AttachSelectorResult::Attached) { + _menu->popupPrepared(); + } else { _menu->popup(QCursor::pos()); } activateControls(); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 06d0c0468..864e21084 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -245,6 +245,7 @@ private: void playbackPauseMusic(); void switchToPip(); [[nodiscard]] int topNotchSkip() const; + [[nodiscard]] std::shared_ptr uiShow(); not_null storiesWrap() override; std::shared_ptr storiesShow() override; @@ -310,8 +311,9 @@ private: void handleScreenChanged(QScreen *screen); - bool contentCanBeSaved() const; + [[nodiscard]] bool computeSaveButtonVisible() const; void checkForSaveLoaded(); + void showPremiumDownloadPromo(); Entity entityForUserPhotos(int index) const; Entity entityForSharedMedia(int index) const; @@ -495,8 +497,10 @@ private: void validatePhotoImage(Image *image, bool blurred); void validatePhotoCurrentImage(); - [[nodiscard]] bool hasCopyMediaRestriction() const; - [[nodiscard]] bool showCopyMediaRestriction(); + [[nodiscard]] bool hasCopyMediaRestriction( + bool skipPremiumCheck = false) const; + [[nodiscard]] bool showCopyMediaRestriction( + bool skipPRemiumCheck = false); [[nodiscard]] QSize flipSizeByRotation(QSize size) const; @@ -518,7 +522,8 @@ private: [[nodiscard]] bool contentShown() const; [[nodiscard]] bool opaqueContentShown() const; void clearStreaming(bool savePosition = true); - bool canInitStreaming() const; + [[nodiscard]] bool canInitStreaming() const; + [[nodiscard]] bool saveControlLocked() const; [[nodiscard]] bool topShadowOnTheRight() const; void applyHideWindowWorkaround(); diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp index a304578b2..8fa5f1d13 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp @@ -269,9 +269,10 @@ void Pip::RendererGL::createShadowTexture() { void Pip::RendererGL::paint( not_null widget, QOpenGLFunctions &f) { - const auto factor = widget->devicePixelRatio(); + const auto factor = widget->devicePixelRatioF(); if (_factor != factor) { _factor = factor; + _ifactor = int(std::ceil(_factor)); _controlsImage.invalidate(); } _blendingEnabled = false; @@ -667,10 +668,10 @@ void Pip::RendererGL::validateControls() { fullHeight += 2 * meta.icon->height(); } auto image = QImage( - QSize(maxWidth, fullHeight) * _factor, + QSize(maxWidth, fullHeight) * _ifactor, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); - image.setDevicePixelRatio(_factor); + image.setDevicePixelRatio(_ifactor); { auto p = QPainter(&image); auto index = 0; @@ -678,8 +679,8 @@ void Pip::RendererGL::validateControls() { const auto paint = [&](not_null icon) { icon->paint(p, 0, height, maxWidth); _controlsTextures[index++] = QRect( - QPoint(0, height) * _factor, - icon->size() * _factor); + QPoint(0, height) * _ifactor, + icon->size() * _ifactor); height += icon->height(); }; for (const auto &meta : metas) { @@ -702,7 +703,7 @@ void Pip::RendererGL::paintUsingRaster( int bufferOffset, bool transparent) { auto raster = image.takeImage(); - const auto size = rect.size() * _factor; + const auto size = rect.size() * _ifactor; if (raster.width() < size.width() || raster.height() < size.height()) { raster = QImage(size, QImage::Format_ARGB32_Premultiplied); raster.setDevicePixelRatio(_factor); @@ -711,8 +712,8 @@ void Pip::RendererGL::paintUsingRaster( || raster.height() > size.height())) { raster.fill(Qt::transparent); } - } else if (raster.devicePixelRatio() != _factor) { - raster.setDevicePixelRatio(_factor); + } else if (raster.devicePixelRatio() != _ifactor) { + raster.setDevicePixelRatio(_ifactor); } if (transparent) { diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.h b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h index 977240c50..21d9b521e 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip_opengl.h +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h @@ -93,6 +93,7 @@ private: QOpenGLFunctions *_f = nullptr; QSize _viewport; float _factor = 1.; + int _ifactor = 1; QVector2D _uniformViewport; std::optional _contentBuffer; diff --git a/Telegram/SourceFiles/menu/menu_antispam_validator.cpp b/Telegram/SourceFiles/menu/menu_antispam_validator.cpp index b15ad5809..50dfea593 100644 --- a/Telegram/SourceFiles/menu/menu_antispam_validator.cpp +++ b/Telegram/SourceFiles/menu/menu_antispam_validator.cpp @@ -73,7 +73,7 @@ object_ptr AntiSpamValidator::createButton() const { rpl::single(QString()), [] {}, st::manageGroupTopicsButton, - { &st::infoRoundedIconAntiSpam, Settings::kIconPurple } + { &st::menuIconAntispam } ))->toggleOn(rpl::single( _channel->antiSpamMode() ) | rpl::then(state->toggled.events())); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 9c54c97bc..bced7ee92 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -221,7 +221,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; +userFull#4fe1cc86 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?UserStories = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -350,7 +350,7 @@ updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; -updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; +updatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update; updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; @@ -385,6 +385,8 @@ updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; updateStory#205a4133 user_id:long story:StoryItem = Update; updateReadStories#feb5345a user_id:long max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; +updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; +updateSentStoryReaction#e3a73d20 user_id:long story_id:int reaction:Reaction = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1526,24 +1528,24 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess sponsoredWebPage#3db8ec63 flags:# url:string site_name:string photo:flags.0?Photo = SponsoredWebPage; -storyViews#d36760cf flags:# views_count:int recent_viewers:flags.0?Vector = StoryViews; +storyViews#c64c0b97 flags:# views_count:int reactions_count:int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#562aa637 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia privacy:flags.2?Vector views:flags.3?StoryViews = StoryItem; +storyItem#44c457ce flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true id:int date:int expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; userStories#8611a200 flags:# user_id:long max_read_id:flags.0?int stories:Vector = UserStories; -stories.allStoriesNotModified#47e0a07e state:string = stories.AllStories; -stories.allStories#839e0428 flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector = stories.AllStories; +stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; +stories.allStories#519d899e flags:# has_more:flags.0?true count:int state:string user_stories:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; stories.stories#4fe57df1 count:int stories:Vector users:Vector = stories.Stories; stories.userStories#37a6ff5f stories:UserStories users:Vector = stories.UserStories; -storyView#a71aacc2 user_id:long date:int = StoryView; +storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; -stories.storyViewsList#fb3f77ac count:int views:Vector users:Vector = stories.StoryViewsList; +stories.storyViewsList#46e9b9ec flags:# count:int reactions_count:int views:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; @@ -1552,6 +1554,14 @@ inputReplyToStory#15b0f283 user_id:InputUser story_id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; +storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; + +mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double = MediaAreaCoordinates; + +mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; +inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; +mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1685,9 +1695,9 @@ contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; contacts.deleteByPhones#1013fd9e phones:Vector = Bool; -contacts.block#68cc1411 id:InputPeer = Bool; -contacts.unblock#bea65d50 id:InputPeer = Bool; -contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked; +contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; +contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; @@ -1704,6 +1714,7 @@ contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.toggleStoriesHidden#753fb865 id:InputUser hidden:Bool = Bool; +contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2088,8 +2099,8 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.sendStory#424cd47a flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; -stories.editStory#2aae7a41 flags:# id:int media:flags.0?InputMedia caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.sendStory#d455fcec flags:# pinned:flags.2?true noforwards:flags.4?true media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int = Updates; +stories.editStory#a9b91ae4 flags:# id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#b5d501d7 id:Vector = Vector; stories.togglePinned#51602944 id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; @@ -2101,7 +2112,9 @@ stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool; stories.getAllReadUserStories#729c562c = Updates; stories.readStories#edc5105b user_id:InputUser max_id:int = Vector; stories.incrementStoryViews#22126127 user_id:InputUser id:Vector = Bool; -stories.getStoryViewsList#4b3b5e97 id:int offset_date:int offset_id:long limit:int = stories.StoryViewsList; +stories.getStoryViewsList#f95f61a4 flags:# just_contacts:flags.0?true reactions_first:flags.2?true q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.getStoriesViews#9a75d6a6 id:Vector = stories.StoryViews; stories.exportStoryLink#16e443ce user_id:InputUser id:int = ExportedStoryLink; stories.report#c95be06a user_id:InputUser id:Vector reason:ReportReason message:string = Bool; +stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; +stories.sendReaction#49aaa9b3 flags:# add_to_recent:flags.0?true user_id:InputUser story_id:int reaction:Reaction = Updates; diff --git a/Telegram/SourceFiles/mtproto/scheme/layer.tl b/Telegram/SourceFiles/mtproto/scheme/layer.tl index 5e910a52b..2f8c6ce82 100644 --- a/Telegram/SourceFiles/mtproto/scheme/layer.tl +++ b/Telegram/SourceFiles/mtproto/scheme/layer.tl @@ -1 +1 @@ -// LAYER 160 +// LAYER 161 diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index b8f77ff0d..66877542a 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -177,6 +177,7 @@ passportDetailsSeparator: FlatLabel(passportPasswordLabelBold) { font: font(semibold 14px); } textFg: windowSubTextFg; + align: align(topleft); } passportDetailsSeparatorPadding: margins(5px, 8px, 5px, 0px); passportContactField: InputField(defaultInputField) { diff --git a/Telegram/SourceFiles/payments/ui/payments_field.cpp b/Telegram/SourceFiles/payments/ui/payments_field.cpp index 601b3629d..914cc1ccf 100644 --- a/Telegram/SourceFiles/payments/ui/payments_field.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_field.cpp @@ -322,7 +322,7 @@ struct SimpleFieldState { QString(), st::paymentsFieldAdditional); const auto leftSkip = state->left - ? (state->left->naturalWidth() + state->currencySkip) + ? (state->left->textMaxWidth() + state->currencySkip) : 0; const auto rightSkip = st::paymentsFieldAdditional.style.font->width( QString(QChar(rule.decimal)) diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 4a9ab7e4c..4779e64be 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -305,7 +305,7 @@ void FormSummary::setupPrices(not_null layout) { padding.left(), padding.top(), (padding.right() - + right->naturalWidth() + + right->textMaxWidth() + 2 * st.style.font->spacew), padding.bottom())); rpl::combine( diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index f13a96bce..f842fd808 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include +#include #include #include @@ -212,6 +213,9 @@ LinuxIntegration::LinuxIntegration() const Glib::VariantBase &value) { if (group == "org.freedesktop.appearance" && key == "color-scheme") { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + QWindowSystemInterface::handleThemeChange(); +#else // Qt >= 6.5.0 try { const auto ivalue = value.get_dynamic(); @@ -220,6 +224,7 @@ LinuxIntegration::LinuxIntegration() }); } catch (...) { } +#endif // Qt < 6.5.0 } }) { LOG(("Icon theme: %1").arg(QIcon::themeName())); diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index 49737bb95..aa9dc99f6 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -683,7 +683,10 @@ bool psLaunchMaps(const Data::LocationPoint &point) { AT_URLPROTOCOL, AL_EFFECTIVE, handler.put()); - if (FAILED(result) || !handler) { + if (FAILED(result) + || !handler + || !handler.data() + || std::wstring(handler.data()) == L"bingmaps") { return false; } diff --git a/Telegram/SourceFiles/platform/win/tray_win.cpp b/Telegram/SourceFiles/platform/win/tray_win.cpp index 13a30858c..a0927702e 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.cpp +++ b/Telegram/SourceFiles/platform/win/tray_win.cpp @@ -18,7 +18,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_window.h" -#include +#include +#include +#include +#include +#include namespace Platform { @@ -96,28 +100,45 @@ Tray::Tray() { void Tray::createIcon() { if (!_icon) { - _icon = base::make_unique_q(nullptr); + if (const auto theme = QGuiApplicationPrivate::platformTheme()) { + _icon.reset(theme->createPlatformSystemTrayIcon()); + } + if (!_icon) { + return; + } + _icon->init(); updateIcon(); - _icon->setToolTip(AppName.utf16()); - using Reason = QSystemTrayIcon::ActivationReason; + _icon->updateToolTip(AppName.utf16()); + + using Reason = QPlatformSystemTrayIcon::ActivationReason; base::qt_signal_producer( _icon.get(), - &QSystemTrayIcon::activated - ) | rpl::start_with_next([=](Reason reason) { - if (reason == QSystemTrayIcon::Context && _menu) { - _aboutToShowRequests.fire({}); - InvokeQueued(_menu.get(), [=] { - _menu->popup(QCursor::pos()); - }); - } else { - _iconClicks.fire({}); - } + &QPlatformSystemTrayIcon::activated + ) | rpl::filter( + rpl::mappers::_1 != Reason::Context + ) | rpl::map_to( + rpl::empty + ) | rpl::start_to_stream(_iconClicks, _lifetime); + + base::qt_signal_producer( + _icon.get(), + &QPlatformSystemTrayIcon::contextMenuRequested + ) | rpl::filter([=] { + return _menu != nullptr; + }) | rpl::start_with_next([=]( + QPoint globalNativePosition, + const QPlatformScreen *screen) { + _aboutToShowRequests.fire({}); + const auto position = QHighDpi::fromNativePixels( + globalNativePosition, + screen ? screen->screen() : nullptr); + InvokeQueued(_menu.get(), [=] { + _menu->popup(position); + }); }, _lifetime); } else { updateIcon(); } - - _icon->show(); } void Tray::destroyIcon() { @@ -155,7 +176,7 @@ void Tray::updateIcon() { forTrayIcon.addPixmap(iconSizeWidth >= 20 ? iconSmallPixmap32 : iconSmallPixmap16); - _icon->setIcon(forTrayIcon); + _icon->updateIcon(forTrayIcon); } void Tray::createMenu() { @@ -199,7 +220,8 @@ void Tray::showTrayMessage() const { _icon->showMessage( AppName.utf16(), tr::lng_tray_icon_text(tr::now), - QSystemTrayIcon::Information, + QIcon(), + QPlatformSystemTrayIcon::Information, kTooltipDelay); cSetSeenTrayTooltip(true); Local::writeSettings(); diff --git a/Telegram/SourceFiles/platform/win/tray_win.h b/Telegram/SourceFiles/platform/win/tray_win.h index b71785b17..e399e5945 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.h +++ b/Telegram/SourceFiles/platform/win/tray_win.h @@ -19,7 +19,7 @@ namespace Ui { class PopupMenu; } // namespace Ui -class QSystemTrayIcon; +class QPlatformSystemTrayIcon; namespace Platform { @@ -60,7 +60,7 @@ public: bool supportMode); private: - base::unique_qptr _icon; + base::unique_qptr _icon; base::unique_qptr _menu; rpl::event_stream<> _iconClicks; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp index 7295604d2..2bc8e3964 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_manage.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" /* @@ -133,7 +134,7 @@ void Manage::setupContent() { content, tr::lng_settings_cloud_password_manage_password_change(), st::settingsButton, - { &st::settingsIconKey, kIconLightBlue } + { &st::menuIconPermissions } )->setClickedCallback([=] { showOtherAndRememberPassword(CloudPasswordInputId()); }); @@ -143,7 +144,7 @@ void Manage::setupContent() { ? tr::lng_settings_cloud_password_manage_email_change() : tr::lng_settings_cloud_password_manage_email_new(), st::settingsButton, - { &st::settingsIconEmail, kIconLightOrange } + { &st::menuIconRecoveryEmail } )->setClickedCallback([=] { auto data = stepData(); data.setOnlyRecoveryEmail = true; diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 2fd6327a4..4f53c9a33 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -74,41 +74,12 @@ settingsUpdate: SettingsButton(infoMainButton, settingsButtonNoIcon) { settingsUpdateStatePosition: point(22px, 29px); settingsDividerLabelPadding: margins(22px, 8px, 22px, 16px); -settingsIconAccount: icon {{ "settings/account", settingsIconFg }}; -settingsIconNotifications: icon {{ "settings/notifications", settingsIconFg }}; settingsIconChat: icon {{ "settings/chat", settingsIconFg }}; -settingsIconFolders: icon {{ "settings/folders", settingsIconFg }}; -settingsIconGeneral: icon {{ "settings/advanced", settingsIconFg }}; -settingsIconLock: icon {{ "settings/lock", settingsIconFg }}; -settingsIconLanguage: icon {{ "settings/language", settingsIconFg }}; -settingsIconBattery: icon {{ "settings/battery", settingsIconFg }}; settingsIconInterfaceScale: icon {{ "settings/interface_scale", settingsIconFg }}; -settingsIconFaq: icon {{ "settings/faq", settingsIconFg }}; -settingsIconCalls: icon {{ "settings/calls", settingsIconFg }}; -settingsIconAskQuestion: icon {{ "settings/ask_question", settingsIconFg }}; -settingsIconTips: icon {{ "settings/tips", settingsIconFg }}; settingsIconStickers: icon {{ "settings/stickers", settingsIconFg }}; settingsIconEmoji: icon {{ "settings/emoji", settingsIconFg }}; -settingsIconThemes: icon {{ "settings/palette", settingsIconFg }}; -settingsIconGroup: icon {{ "settings/group", settingsIconFg }}; -settingsIconChannel: icon {{ "settings/channel", settingsIconFg }}; -settingsIconUser: icon {{ "settings/user", settingsIconFg }}; -settingsIconKey: icon {{ "settings/key", settingsIconFg }}; -settingsIconPlus: icon {{ "settings/plus", settingsIconFg }}; -settingsIconMinus: icon {{ "settings/minus", settingsIconFg }}; -settingsIconTimer: icon {{ "settings/timer", settingsIconFg }}; -settingsIconLaptop: icon {{ "settings/laptop", settingsIconFg }}; -settingsIconArrows: icon {{ "settings/arrows", settingsIconFg }}; -settingsIconEmail: icon {{ "settings/email", settingsIconFg }}; -settingsIconSound: icon {{ "settings/sound", settingsIconFg }}; -settingsIconDock: icon {{ "settings/dock", settingsIconFg }}; -settingsIconPin: icon {{ "settings/pin", settingsIconFg }}; -settingsIconDownload: icon {{ "settings/download", settingsIconFg }}; -settingsIconMention: icon {{ "settings/mention", settingsIconFg }}; -settingsIconTopics: icon {{ "settings/topics", settingsIconFg }}; -settingsIconTTL: icon {{ "settings/ttl", settingsIconFg }}; -settingsIconPhoto: icon {{ "settings/photo", settingsIconFg }}; +settingsPremiumIconStories: icon {{ "settings/stories", settingsIconFg }}; settingsPremiumIconChannelsOff: icon {{ "settings/premium/channels_off", settingsIconFg }}; settingsPremiumIconDouble: icon {{ "settings/premium/double", settingsIconFg }}; settingsPremiumIconStatus: icon {{ "settings/premium/status", settingsIconFg }}; @@ -120,6 +91,25 @@ settingsPremiumIconVoice: icon {{ "settings/premium/voice", settingsIconFg }}; settingsPremiumIconFiles: icon {{ "settings/premium/files", settingsIconFg }}; settingsPremiumIconTranslations: icon {{ "settings/premium/translations", settingsIconFg }}; +settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; +settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; +settingsStoriesIconViews: icon {{ "menu/show_in_chat", premiumButtonBg1 }}; +settingsStoriesIconExpiration: icon {{ "settings/premium/timer", premiumButtonBg1 }}; +settingsStoriesIconDownload: icon {{ "menu/download", premiumButtonBg1 }}; +settingsStoriesIconCaption: icon {{ "settings/premium/stories_caption", premiumButtonBg1 }}; +settingsStoriesIconLinks: icon {{ "menu/links_profile", premiumButtonBg1 }}; + +settingsPremiumNewBadge: FlatLabel(defaultFlatLabel) { + style: TextStyle(semiboldTextStyle) { + font: font(10px semibold); + linkFont: font(10px semibold); + linkFontOver: font(10px semibold); + } + textFg: windowFgActive; +} +settingsPremiumNewBadgePosition: point(4px, 1px); +settingsPremiumNewBadgePadding: margins(4px, 1px, 4px, 1px); + settingsTTLChatsOff: icon {{ "settings/ttl/autodelete_off", windowSubTextFg }}; settingsTTLChatsOn: icon {{ "settings/ttl/autodelete_on", windowActiveTextFg }}; @@ -326,21 +316,15 @@ sessionLocationTop: 54px; sessionCurrentSkip: 8px; sessionSubtitleSkip: 14px; sessionInfoFg: windowSubTextFg; -sessionTerminateTop: 9px; -sessionTerminateSkip: 12px; +sessionTerminateTop: 8px; +sessionTerminateSkip: 11px; sessionTerminate: IconButton { - width: 20px; - height: 20px; + width: 34px; + height: 34px; icon: smallCloseIcon; iconOver: smallCloseIconOver; - iconPosition: point(5px, 5px); - - rippleAreaPosition: point(0px, 0px); - rippleAreaSize: 20px; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } + iconPosition: point(12px, 12px); } sessionIconWindows: icon{{ "settings/devices/device_desktop_win", historyPeerUserpicFg }}; sessionIconMac: icon{{ "settings/devices/device_desktop_mac", historyPeerUserpicFg }}; @@ -375,11 +359,12 @@ sessionDateLabel: FlatLabel(defaultFlatLabel) { align: align(top); } sessionDateSkip: 19px; -sessionValuePadding: margins(0px, 5px, 0px, 2px); +sessionValuePadding: margins(37px, 5px, 0px, 0px); sessionValueLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } sessionValueSkip: 8px; +sessionValueIconPosition: point(20px, 9px); sessionListItem: PeerListItem(defaultPeerListItem) { button: OutlineButton(defaultPeerListButton) { @@ -401,6 +386,21 @@ sessionList: PeerList(defaultPeerList) { item: sessionListItem; padding: margins(0px, 4px, 0px, 0px); } +websiteListItem: PeerListItem(sessionListItem) { + height: 72px; + photoPosition: point(18px, 10px); + namePosition: point(64px, 6px); + statusPosition: point(64px, 26px); + photoSize: 32px; +} +websiteList: PeerList(sessionList) { + item: websiteListItem; +} +websiteLocationTop: 46px; +websiteBigUserpic: UserpicButton(defaultUserpicButton) { + size: size(70px, 70px); + photoSize: 70px; +} settingsPhotoLeft: 22px; settingsPhotoTop: 8px; @@ -473,6 +473,9 @@ settingsPremiumRowAboutPadding: margins(59px, 0px, 46px, 6px); settingsPremiumPreviewTitlePadding: margins(24px, 13px, 24px, 3px); settingsPremiumPreviewAboutPadding: margins(24px, 0px, 24px, 11px); settingsPremiumPreviewLinePadding: margins(18px, 0px, 18px, 8px); +settingsPremiumPreviewIconTitlePadding: margins(62px, 13px, 24px, 1px); +settingsPremiumPreviewIconAboutPadding: margins(62px, 0px, 24px, 0px); +settingsPremiumPreviewIconPosition: point(20px, 7px); settingsPremiumTitlePadding: margins(0px, 18px, 0px, 11px); settingsPremiumAboutTextStyle: TextStyle(defaultTextStyle) { diff --git a/Telegram/SourceFiles/settings/settings_advanced.cpp b/Telegram/SourceFiles/settings/settings_advanced.cpp index 1829f92a2..8ac221a4f 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced.cpp @@ -7,17 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_advanced.h" +#include "api/api_global_privacy.h" +#include "apiwrap.h" #include "settings/settings_common.h" #include "settings/settings_chat.h" -#include "settings/settings_experimental.h" #include "settings/settings_power_saving.h" +#include "settings/settings_privacy_security.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" #include "ui/gl/gl_detection.h" -#include "ui/text/text_utilities.h" // Ui::Text::ToUpper +#include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/boxes/single_choice_box.h" #include "ui/painter.h" @@ -42,6 +44,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "mtproto/facade.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" #ifdef Q_OS_MAC @@ -91,7 +95,7 @@ void SetupConnectionType( tr::lng_connection_auto_connecting() | rpl::to_empty ) | rpl::map(connectionType), st::settingsButton, - { &st::settingsIconArrows, kIconGreen }); + { &st::menuIconNetwork }); button->addClickHandler([=] { controller->show(ProxiesBoxController::CreateOwningBox(account)); }); @@ -101,9 +105,7 @@ bool HasUpdate() { return !Core::UpdaterDisabled(); } -void SetupUpdate( - not_null container, - Fn showOther) { +void SetupUpdate(not_null container) { if (!HasUpdate()) { return; } @@ -135,31 +137,13 @@ void SetupUpdate( tr::lng_settings_install_beta(), st::settingsButtonNoIcon).get(); - if (showOther) { - const auto experimental = inner->add( - object_ptr>( - inner, - CreateButton( - inner, - tr::lng_settings_experimental(), - st::settingsButtonNoIcon))); - if (!install) { - experimental->toggle(true, anim::type::instant); - } else { - experimental->toggleOn(install->toggledValue()); - } - experimental->entity()->setClickedCallback([=] { - showOther(Experimental::Id()); - }); - } - const auto check = AddButton( inner, tr::lng_settings_check_now(), st::settingsButtonNoIcon); const auto update = Ui::CreateChild