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/docker.yml b/.github/workflows/docker.yml index 520caaf68..ad5ccedc5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,7 +25,12 @@ jobs: submodules: recursive - name: First set up. - run: curl -sSL https://install.python-poetry.org | python3 - + run: | + sudo apt update + curl -sSL https://install.python-poetry.org | python3 - + + - name: Free up some disk space. + uses: jlumbroso/free-disk-space@76866dbe54312617f00798d1762df7f43def6e5c - name: Docker image build. run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6f2ec36ec..162b6224d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -57,7 +57,6 @@ jobs: matrix: defines: - "" - - "DESKTOP_APP_DISABLE_DBUS_INTEGRATION" - "DESKTOP_APP_DISABLE_X11_INTEGRATION" - "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION" diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 18ad071b8..4191aced9 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -60,6 +60,9 @@ jobs: sudo snap run lxd init --auto sudo snap run lxd waitready + - name: Free up some disk space. + uses: jlumbroso/free-disk-space@76866dbe54312617f00798d1762df7f43def6e5c + - name: Telegram Desktop snap build. run: sg lxd -c 'snap run snapcraft -v' diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index a3f305ce4..3c86918f2 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -60,13 +60,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 @@ -100,7 +97,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- @@ -108,7 +105,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 @@ -132,6 +131,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/.github/workflows/winget.yml b/.github/workflows/winget.yml index a9f25a450..0ff2d50d8 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,9 +10,11 @@ jobs: uses: telegramdesktop/winget-releaser@main with: identifier: Telegram.TelegramDesktop + installers-regex: 't(setup|portable).*(exe|zip)$' token: ${{ secrets.WINGET_TOKEN }} - if: github.event.action == 'prereleased' uses: telegramdesktop/winget-releaser@main with: identifier: Telegram.TelegramDesktop.Beta + installers-regex: 't(setup|portable).*(exe|zip)$' token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitmodules b/.gitmodules index 052fbbe91..d3d445255 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/telegramdesktop/libtgvoip [submodule "Telegram/ThirdParty/GSL"] path = Telegram/ThirdParty/GSL - url = https://github.com/Microsoft/GSL.git + url = https://github.com/desktop-app/GSL.git [submodule "Telegram/ThirdParty/xxHash"] path = Telegram/ThirdParty/xxHash url = https://github.com/Cyan4973/xxHash.git @@ -100,3 +100,6 @@ [submodule "Telegram/ThirdParty/cld3"] path = Telegram/ThirdParty/cld3 url = https://github.com/google/cld3.git +[submodule "Telegram/ThirdParty/wayland"] + path = Telegram/ThirdParty/wayland + url = https://github.com/gitlab-freedesktop-mirrors/wayland.git diff --git a/CMakeLists.txt b/CMakeLists.txt index af94de3c0..7ec62b158 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,9 @@ desktop_app_parse_version(Telegram/build/version) set(project_langs C CXX) if (APPLE) - set(project_langs C CXX OBJC OBJCXX) + list(APPEND project_langs OBJC OBJCXX) +elseif (LINUX) + list(APPEND project_langs ASM) endif() project(Telegram @@ -37,15 +39,16 @@ get_filename_component(submodules_loc "Telegram" REALPATH) get_filename_component(cmake_helpers_loc "cmake" REALPATH) if (NOT DESKTOP_APP_USE_PACKAGED AND WIN32) - set(Python_EXECUTABLE ${CMAKE_CURRENT_SOURCE_DIR}/../ThirdParty/python/Scripts/python) + set(Python3_EXECUTABLE ${CMAKE_CURRENT_SOURCE_DIR}/../ThirdParty/python/Scripts/python) endif() include(cmake/variables.cmake) include(cmake/nice_target_sources.cmake) include(cmake/target_compile_options_if_exists.cmake) +include(cmake/target_link_frameworks.cmake) +include(cmake/target_link_optional_libraries.cmake) include(cmake/target_link_options_if_exists.cmake) include(cmake/target_link_static_libraries.cmake) -include(cmake/target_link_frameworks.cmake) include(cmake/init_target.cmake) include(cmake/generate_target.cmake) include(cmake/nuget.cmake) @@ -56,7 +59,7 @@ include(cmake/options.cmake) if (NOT DESKTOP_APP_USE_PACKAGED) if (WIN32) - set(qt_version 5.15.8) + 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 2419fa804..f15f85198 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -167,12 +167,16 @@ 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 boxes/filters/edit_filter_box.h boxes/filters/edit_filter_chats_list.cpp boxes/filters/edit_filter_chats_list.h + boxes/filters/edit_filter_links.cpp + boxes/filters/edit_filter_links.h boxes/peers/add_bot_to_chat_box.cpp boxes/peers/add_bot_to_chat_box.h boxes/peers/add_participants_box.cpp @@ -228,8 +232,6 @@ PRIVATE boxes/background_box.h boxes/background_preview_box.cpp boxes/background_preview_box.h - boxes/change_phone_box.cpp - boxes/change_phone_box.h boxes/choose_filter_box.cpp boxes/choose_filter_box.h boxes/connection_box.cpp @@ -348,6 +350,9 @@ PRIVATE calls/calls_video_bubble.h calls/calls_video_incoming.cpp calls/calls_video_incoming.h + chat_helpers/compose/compose_features.h + chat_helpers/compose/compose_show.cpp + chat_helpers/compose/compose_show.h chat_helpers/bot_command.cpp chat_helpers/bot_command.h chat_helpers/bot_keyboard.cpp @@ -546,6 +551,12 @@ PRIVATE data/data_sparse_ids.h data/data_sponsored_messages.cpp data/data_sponsored_messages.h + data/data_stories.cpp + data/data_stories.h + data/data_stories_ids.cpp + data/data_stories_ids.h + data/data_story.cpp + data/data_story.h data/data_streaming.cpp data/data_streaming.h data/data_thread.cpp @@ -586,6 +597,8 @@ PRIVATE dialogs/ui/dialogs_layout.h dialogs/ui/dialogs_message_view.cpp dialogs/ui/dialogs_message_view.h + dialogs/ui/dialogs_stories_content.cpp + dialogs/ui/dialogs_stories_content.h dialogs/ui/dialogs_topics_view.cpp dialogs/ui/dialogs_topics_view.h dialogs/ui/dialogs_video_userpic.cpp @@ -689,6 +702,8 @@ PRIVATE history/view/media/history_view_sticker_player.cpp history/view/media/history_view_sticker_player.h history/view/media/history_view_sticker_player_abstract.h + history/view/media/history_view_story_mention.cpp + history/view/media/history_view_story_mention.h history/view/media/history_view_theme_document.cpp history/view/media/history_view_theme_document.h history/view/media/history_view_userpic_suggestion.cpp @@ -864,6 +879,12 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/stories/info_stories_inner_widget.cpp + info/stories/info_stories_inner_widget.h + info/stories/info_stories_provider.cpp + info/stories/info_stories_provider.h + info/stories/info_stories_widget.cpp + info/stories/info_stories_widget.h info/userpic/info_userpic_colors_editor.cpp info/userpic/info_userpic_colors_editor.h info/userpic/info_userpic_emoji_builder.cpp @@ -922,6 +943,8 @@ PRIVATE main/main_account.h main/main_app_config.cpp main/main_app_config.h + main/main_app_config_values.cpp + main/main_app_config_values.h main/main_domain.cpp main/main_domain.h main/main_session.cpp @@ -930,8 +953,8 @@ PRIVATE main/main_session_settings.h main/session/send_as_peers.cpp main/session/send_as_peers.h - media/system_media_controls_manager.h - media/system_media_controls_manager.cpp + main/session/session_show.cpp + main/session/session_show.h media/audio/media_audio.cpp media/audio/media_audio.h media/audio/media_audio_capture.cpp @@ -956,6 +979,30 @@ PRIVATE media/player/media_player_volume_controller.h media/player/media_player_widget.cpp media/player/media_player_widget.h + media/stories/media_stories_caption_full_view.cpp + media/stories/media_stories_caption_full_view.h + media/stories/media_stories_controller.cpp + media/stories/media_stories_controller.h + media/stories/media_stories_delegate.cpp + media/stories/media_stories_delegate.h + media/stories/media_stories_header.cpp + media/stories/media_stories_header.h + media/stories/media_stories_reactions.cpp + media/stories/media_stories_reactions.h + media/stories/media_stories_recent_views.cpp + media/stories/media_stories_recent_views.h + media/stories/media_stories_reply.cpp + media/stories/media_stories_reply.h + media/stories/media_stories_share.cpp + media/stories/media_stories_share.h + media/stories/media_stories_sibling.cpp + 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 media/streaming/media_streaming_audio_track.h media/streaming/media_streaming_common.h @@ -1001,6 +1048,8 @@ PRIVATE media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h media/view/media_view_open_common.h + media/system_media_controls_manager.h + media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp menu/menu_antispam_validator.h menu/menu_item_download_files.cpp @@ -1078,7 +1127,6 @@ PRIVATE platform/linux/integration_linux.h platform/linux/main_window_linux.cpp platform/linux/main_window_linux.h - platform/linux/notifications_manager_linux_dummy.cpp platform/linux/notifications_manager_linux.cpp platform/linux/notifications_manager_linux.h platform/linux/overlay_widget_linux.h @@ -1224,6 +1272,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 @@ -1397,18 +1447,6 @@ if (NOT build_winstore) ) endif() -if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION) - remove_target_sources(Telegram ${src_loc} - platform/linux/linux_xdp_open_with_dialog.cpp - platform/linux/linux_xdp_open_with_dialog.h - platform/linux/notifications_manager_linux.cpp - ) -else() - remove_target_sources(Telegram ${src_loc} - platform/linux/notifications_manager_linux_dummy.cpp - ) -endif() - if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) remove_target_sources(Telegram ${src_loc} platform/linux/linux_wayland_integration.cpp @@ -1518,12 +1556,13 @@ elseif (APPLE) endif() endif() else() - if (NOT DESKTOP_APP_DISABLE_DBUS_INTEGRATION) - target_link_libraries(Telegram - PRIVATE - desktop-app::external_glibmm - ) - endif() + target_link_libraries(Telegram + PRIVATE + desktop-app::external_glibmm + ) + + include(${cmake_helpers_loc}/external/glib/generate_dbus.cmake) + generate_dbus(Telegram org.freedesktop.portal. XdpInhibit ${src_loc}/platform/linux/org.freedesktop.portal.Inhibit.xml) if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) target_link_libraries(Telegram @@ -1535,6 +1574,7 @@ else() if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) qt_generate_wayland_protocol_client_sources(Telegram FILES + ${third_party_loc}/wayland/protocol/wayland.xml ${third_party_loc}/plasma-wayland-protocols/src/protocols/plasma-shell.xml ) @@ -1724,6 +1764,7 @@ endif() if (LINUX AND DESKTOP_APP_USE_PACKAGED) include(GNUInstallDirs) + configure_file("../lib/xdg/org.telegram.desktop.service" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service" @ONLY) configure_file("../lib/xdg/org.telegram.desktop.metainfo.xml" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml" @ONLY) generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml") install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}") @@ -1735,5 +1776,6 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED) install(FILES "Resources/art/icon256.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/256x256/apps" RENAME "telegram.png") install(FILES "Resources/art/icon512.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/512x512/apps" RENAME "telegram.png") install(FILES "../lib/xdg/org.telegram.desktop.desktop" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") endif() diff --git a/Telegram/Resources/animations/cloud_filters.tgs b/Telegram/Resources/animations/cloud_filters.tgs new file mode 100644 index 000000000..3e71d25b4 Binary files /dev/null and b/Telegram/Resources/animations/cloud_filters.tgs differ diff --git a/Telegram/Resources/day-blue.tdesktop-theme b/Telegram/Resources/day-blue.tdesktop-theme index 04b07154d..54d4dfa29 100644 Binary files a/Telegram/Resources/day-blue.tdesktop-theme and b/Telegram/Resources/day-blue.tdesktop-theme differ diff --git a/Telegram/Resources/day-custom-base.tdesktop-theme b/Telegram/Resources/day-custom-base.tdesktop-theme index 36f91ef02..d10f68c75 100644 Binary files a/Telegram/Resources/day-custom-base.tdesktop-theme and b/Telegram/Resources/day-custom-base.tdesktop-theme differ diff --git a/Telegram/Resources/export_html/css/style.css b/Telegram/Resources/export_html/css/style.css index 456d59f46..79b680cc2 100644 --- a/Telegram/Resources/export_html/css/style.css +++ b/Telegram/Resources/export_html/css/style.css @@ -111,6 +111,11 @@ pre { border-radius: 50%; overflow: hidden; } +.story { + display: block; + border-radius: 4px; + overflow: hidden; +} .userpic .initials { display: block; color: #fff; @@ -194,6 +199,10 @@ a.block_link:hover { text-decoration: none !important; background-color: #f5f7f8; } +a.expanded { + padding: 2px 8px; + margin: -2px -8px; +} .sections { padding: 11px 0; } @@ -428,6 +437,9 @@ div.toast_shown { .section.sessions { background-image: url(../images/section_sessions.png); } +.section.stories { + background-image: url(../images/section_stories.png); +} .section.web { background-image: url(../images/section_web.png); } @@ -489,6 +501,9 @@ div.toast_shown { .section.sessions { background-image: url(../images/section_sessions@2x.png); } +.section.stories { + background-image: url(../images/section_stories@2x.png); +} .section.web { background-image: url(../images/section_web@2x.png); } diff --git a/Telegram/Resources/export_html/images/section_stories.png b/Telegram/Resources/export_html/images/section_stories.png new file mode 100644 index 000000000..650c69c91 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories.png differ diff --git a/Telegram/Resources/export_html/images/section_stories@2x.png b/Telegram/Resources/export_html/images/section_stories@2x.png new file mode 100644 index 000000000..429245138 Binary files /dev/null and b/Telegram/Resources/export_html/images/section_stories@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_draw.png b/Telegram/Resources/icons/chat/input_draw.png new file mode 100644 index 000000000..37f5c854c Binary files /dev/null and b/Telegram/Resources/icons/chat/input_draw.png differ diff --git a/Telegram/Resources/icons/chat/input_draw@2x.png b/Telegram/Resources/icons/chat/input_draw@2x.png new file mode 100644 index 000000000..0087392e5 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_draw@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_draw@3x.png b/Telegram/Resources/icons/chat/input_draw@3x.png new file mode 100644 index 000000000..e2fd682f6 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_draw@3x.png differ 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/chat/input_replace.png b/Telegram/Resources/icons/chat/input_replace.png new file mode 100644 index 000000000..e0bc68cb3 Binary files /dev/null and b/Telegram/Resources/icons/chat/input_replace.png differ diff --git a/Telegram/Resources/icons/chat/input_replace@2x.png b/Telegram/Resources/icons/chat/input_replace@2x.png new file mode 100644 index 000000000..3a634692a Binary files /dev/null and b/Telegram/Resources/icons/chat/input_replace@2x.png differ diff --git a/Telegram/Resources/icons/chat/input_replace@3x.png b/Telegram/Resources/icons/chat/input_replace@3x.png new file mode 100644 index 000000000..edcd082af Binary files /dev/null and b/Telegram/Resources/icons/chat/input_replace@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock.png b/Telegram/Resources/icons/dialogs/dialogs_lock.png deleted file mode 100644 index cd953f753..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png deleted file mode 100644 index d29e0579f..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png deleted file mode 100644 index 796427920..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_lock@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off.png new file mode 100644 index 000000000..2459e1137 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png new file mode 100644 index 000000000..33b77eb3f Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png new file mode 100644 index 000000000..7653aa431 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_off@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on.png new file mode 100644 index 000000000..8209991f3 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png new file mode 100644 index 000000000..c4e3d6b25 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png b/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png new file mode 100644 index 000000000..67c7d7c71 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_lock_on@3x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock.png b/Telegram/Resources/icons/dialogs/dialogs_unlock.png deleted file mode 100644 index 207f93bb9..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png deleted file mode 100644 index 64359ff67..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png b/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png deleted file mode 100644 index c62942302..000000000 Binary files a/Telegram/Resources/icons/dialogs/dialogs_unlock@3x.png and /dev/null 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/emoji/stickers_add.png b/Telegram/Resources/icons/emoji/stickers_add.png deleted file mode 100644 index d4c77a202..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add@2x.png b/Telegram/Resources/icons/emoji/stickers_add@2x.png deleted file mode 100644 index f188f440d..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add@3x.png b/Telegram/Resources/icons/emoji/stickers_add@3x.png deleted file mode 100644 index 48de8115e..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot.png b/Telegram/Resources/icons/emoji/stickers_add_dot.png deleted file mode 100644 index 1bb4078f4..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png deleted file mode 100644 index 1723a7f36..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png b/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png deleted file mode 100644 index ce7f1aa76..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_dot@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread.png b/Telegram/Resources/icons/emoji/stickers_add_unread.png deleted file mode 100644 index d8172e22b..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png deleted file mode 100644 index c48562529..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png b/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png deleted file mode 100644 index 46a03b994..000000000 Binary files a/Telegram/Resources/icons/emoji/stickers_add_unread@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/info/info_media_stories.png b/Telegram/Resources/icons/info/info_media_stories.png new file mode 100644 index 000000000..5b4cb99be Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories.png differ diff --git a/Telegram/Resources/icons/info/info_media_stories@2x.png b/Telegram/Resources/icons/info/info_media_stories@2x.png new file mode 100644 index 000000000..fc0a2f6e3 Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories@2x.png differ diff --git a/Telegram/Resources/icons/info/info_media_stories@3x.png b/Telegram/Resources/icons/info/info_media_stories@3x.png new file mode 100644 index 000000000..df8a23bbd Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_stories@3x.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty.png b/Telegram/Resources/icons/info/info_media_story_empty.png new file mode 100644 index 000000000..6196956ba Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty@2x.png b/Telegram/Resources/icons/info/info_media_story_empty@2x.png new file mode 100644 index 000000000..c3e6997cd Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty@2x.png differ diff --git a/Telegram/Resources/icons/info/info_media_story_empty@3x.png b/Telegram/Resources/icons/info/info_media_story_empty@3x.png new file mode 100644 index 000000000..b6422809d Binary files /dev/null and b/Telegram/Resources/icons/info/info_media_story_empty@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive.png b/Telegram/Resources/icons/info/info_stories_archive.png new file mode 100644 index 000000000..9b4b79ce4 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@2x.png b/Telegram/Resources/icons/info/info_stories_archive@2x.png new file mode 100644 index 000000000..831363fa5 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_archive@3x.png b/Telegram/Resources/icons/info/info_stories_archive@3x.png new file mode 100644 index 000000000..e02e85c6c Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_archive@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent.png b/Telegram/Resources/icons/info/info_stories_recent.png new file mode 100644 index 000000000..341ee2a06 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@2x.png b/Telegram/Resources/icons/info/info_stories_recent@2x.png new file mode 100644 index 000000000..ecb3fc72d Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_recent@3x.png b/Telegram/Resources/icons/info/info_stories_recent@3x.png new file mode 100644 index 000000000..bacf1a3c1 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_recent@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive.png b/Telegram/Resources/icons/info/info_stories_to_archive.png new file mode 100644 index 000000000..f9c81896e Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@2x.png b/Telegram/Resources/icons/info/info_stories_to_archive@2x.png new file mode 100644 index 000000000..ef905891c Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_archive@3x.png b/Telegram/Resources/icons/info/info_stories_to_archive@3x.png new file mode 100644 index 000000000..bc2fc30af Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_archive@3x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile.png b/Telegram/Resources/icons/info/info_stories_to_profile.png new file mode 100644 index 000000000..cfe465c5b Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@2x.png b/Telegram/Resources/icons/info/info_stories_to_profile@2x.png new file mode 100644 index 000000000..1e9990c2f Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile@2x.png differ diff --git a/Telegram/Resources/icons/info/info_stories_to_profile@3x.png b/Telegram/Resources/icons/info/info_stories_to_profile@3x.png new file mode 100644 index 000000000..414fa60a2 Binary files /dev/null and b/Telegram/Resources/icons/info/info_stories_to_profile@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/mediaview/mini_close_friends.png b/Telegram/Resources/icons/mediaview/mini_close_friends.png new file mode 100644 index 000000000..5c3072447 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png new file mode 100644 index 000000000..82dd44af9 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png b/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png new file mode 100644 index 000000000..1f024ce73 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_close_friends@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts.png b/Telegram/Resources/icons/mediaview/mini_contacts.png new file mode 100644 index 000000000..bc716cab4 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_contacts@2x.png new file mode 100644 index 000000000..a7df593e8 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_contacts@3x.png new file mode 100644 index 000000000..50c9ff55f Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_contacts@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts.png new file mode 100644 index 000000000..c3ca5b06d Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png new file mode 100644 index 000000000..d12f95496 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png b/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png new file mode 100644 index 000000000..90860348b Binary files /dev/null and b/Telegram/Resources/icons/mediaview/mini_selected_contacts@3x.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share.png b/Telegram/Resources/icons/mediaview/viewer_share.png new file mode 100644 index 000000000..14861b1ee Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share@2x.png b/Telegram/Resources/icons/mediaview/viewer_share@2x.png new file mode 100644 index 000000000..7c6e9341e Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share@2x.png differ diff --git a/Telegram/Resources/icons/mediaview/viewer_share@3x.png b/Telegram/Resources/icons/mediaview/viewer_share@3x.png new file mode 100644 index 000000000..43310c895 Binary files /dev/null and b/Telegram/Resources/icons/mediaview/viewer_share@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/archive_open.png b/Telegram/Resources/icons/menu/archive_open.png new file mode 100644 index 000000000..2e3090130 Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open.png differ diff --git a/Telegram/Resources/icons/menu/archive_open@2x.png b/Telegram/Resources/icons/menu/archive_open@2x.png new file mode 100644 index 000000000..c95bd9f66 Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open@2x.png differ diff --git a/Telegram/Resources/icons/menu/archive_open@3x.png b/Telegram/Resources/icons/menu/archive_open@3x.png new file mode 100644 index 000000000..74687f9ec Binary files /dev/null and b/Telegram/Resources/icons/menu/archive_open@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/channel.png b/Telegram/Resources/icons/menu/channel.png index df2823788..d6caf7131 100644 Binary files a/Telegram/Resources/icons/menu/channel.png and b/Telegram/Resources/icons/menu/channel.png differ diff --git a/Telegram/Resources/icons/menu/channel@2x.png b/Telegram/Resources/icons/menu/channel@2x.png index 68e65eade..c68e6a6b0 100644 Binary files a/Telegram/Resources/icons/menu/channel@2x.png and b/Telegram/Resources/icons/menu/channel@2x.png differ diff --git a/Telegram/Resources/icons/menu/channel@3x.png b/Telegram/Resources/icons/menu/channel@3x.png index e3f8e50d3..0ce3d46a9 100644 Binary files a/Telegram/Resources/icons/menu/channel@3x.png and b/Telegram/Resources/icons/menu/channel@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.png b/Telegram/Resources/icons/menu/groups.png new file mode 100644 index 000000000..2fc454a36 Binary files /dev/null and b/Telegram/Resources/icons/menu/groups.png differ diff --git a/Telegram/Resources/icons/menu/groups@2x.png b/Telegram/Resources/icons/menu/groups@2x.png new file mode 100644 index 000000000..986bc696d Binary files /dev/null and b/Telegram/Resources/icons/menu/groups@2x.png differ diff --git a/Telegram/Resources/icons/menu/groups@3x.png b/Telegram/Resources/icons/menu/groups@3x.png new file mode 100644 index 000000000..28ed4085f Binary files /dev/null and b/Telegram/Resources/icons/menu/groups@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/header_mode_day.png b/Telegram/Resources/icons/menu/header_mode_day.png new file mode 100644 index 000000000..9ad57435e Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_day.png differ diff --git a/Telegram/Resources/icons/menu/header_mode_day@2x.png b/Telegram/Resources/icons/menu/header_mode_day@2x.png new file mode 100644 index 000000000..13dd35f30 Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_day@2x.png differ diff --git a/Telegram/Resources/icons/menu/header_mode_day@3x.png b/Telegram/Resources/icons/menu/header_mode_day@3x.png new file mode 100644 index 000000000..5f6ad557f Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_day@3x.png differ diff --git a/Telegram/Resources/icons/menu/header_mode_night.png b/Telegram/Resources/icons/menu/header_mode_night.png new file mode 100644 index 000000000..43cf44547 Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_night.png differ diff --git a/Telegram/Resources/icons/menu/header_mode_night@2x.png b/Telegram/Resources/icons/menu/header_mode_night@2x.png new file mode 100644 index 000000000..0d397e35d Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_night@2x.png differ diff --git a/Telegram/Resources/icons/menu/header_mode_night@3x.png b/Telegram/Resources/icons/menu/header_mode_night@3x.png new file mode 100644 index 000000000..65ba723e2 Binary files /dev/null and b/Telegram/Resources/icons/menu/header_mode_night@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/night_mode.png b/Telegram/Resources/icons/menu/night_mode.png new file mode 100644 index 000000000..531195e73 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode.png differ diff --git a/Telegram/Resources/icons/menu/night_mode@2x.png b/Telegram/Resources/icons/menu/night_mode@2x.png new file mode 100644 index 000000000..1742270a3 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode@2x.png differ diff --git a/Telegram/Resources/icons/menu/night_mode@3x.png b/Telegram/Resources/icons/menu/night_mode@3x.png new file mode 100644 index 000000000..601217161 Binary files /dev/null and b/Telegram/Resources/icons/menu/night_mode@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/profile.png b/Telegram/Resources/icons/menu/profile.png index 962419484..99861b419 100644 Binary files a/Telegram/Resources/icons/menu/profile.png and b/Telegram/Resources/icons/menu/profile.png differ diff --git a/Telegram/Resources/icons/menu/profile@2x.png b/Telegram/Resources/icons/menu/profile@2x.png index fe742a412..14faac768 100644 Binary files a/Telegram/Resources/icons/menu/profile@2x.png and b/Telegram/Resources/icons/menu/profile@2x.png differ diff --git a/Telegram/Resources/icons/menu/profile@3x.png b/Telegram/Resources/icons/menu/profile@3x.png index e2cf03667..f760db92b 100644 Binary files a/Telegram/Resources/icons/menu/profile@3x.png and b/Telegram/Resources/icons/menu/profile@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/saved_messages.png b/Telegram/Resources/icons/menu/saved_messages.png new file mode 100644 index 000000000..3b8ba1eb7 Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages.png differ diff --git a/Telegram/Resources/icons/menu/saved_messages@2x.png b/Telegram/Resources/icons/menu/saved_messages@2x.png new file mode 100644 index 000000000..b89a09231 Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages@2x.png differ diff --git a/Telegram/Resources/icons/menu/saved_messages@3x.png b/Telegram/Resources/icons/menu/saved_messages@3x.png new file mode 100644 index 000000000..6fa7627de Binary files /dev/null and b/Telegram/Resources/icons/menu/saved_messages@3x.png differ diff --git a/Telegram/Resources/icons/menu/settings.png b/Telegram/Resources/icons/menu/settings.png index 4453866e9..27b0cbcd6 100644 Binary files a/Telegram/Resources/icons/menu/settings.png and b/Telegram/Resources/icons/menu/settings.png differ diff --git a/Telegram/Resources/icons/menu/settings@2x.png b/Telegram/Resources/icons/menu/settings@2x.png index e255fb46a..76e0fbbe4 100644 Binary files a/Telegram/Resources/icons/menu/settings@2x.png and b/Telegram/Resources/icons/menu/settings@2x.png differ diff --git a/Telegram/Resources/icons/menu/settings@3x.png b/Telegram/Resources/icons/menu/settings@3x.png index 79b2cf6d2..5135a603e 100644 Binary files a/Telegram/Resources/icons/menu/settings@3x.png and b/Telegram/Resources/icons/menu/settings@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/stories_archive.png b/Telegram/Resources/icons/menu/stories_archive.png new file mode 100644 index 000000000..a98e8583c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive@2x.png b/Telegram/Resources/icons/menu/stories_archive@2x.png new file mode 100644 index 000000000..5c5602569 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive@3x.png b/Telegram/Resources/icons/menu/stories_archive@3x.png new file mode 100644 index 000000000..715f241ba Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section.png b/Telegram/Resources/icons/menu/stories_archive_section.png new file mode 100644 index 000000000..d86b5dc87 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section@2x.png b/Telegram/Resources/icons/menu/stories_archive_section@2x.png new file mode 100644 index 000000000..f2c59be33 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_archive_section@3x.png b/Telegram/Resources/icons/menu/stories_archive_section@3x.png new file mode 100644 index 000000000..f6369c74c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_archive_section@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_save.png b/Telegram/Resources/icons/menu/stories_save.png new file mode 100644 index 000000000..68d2a12f4 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save.png differ diff --git a/Telegram/Resources/icons/menu/stories_save@2x.png b/Telegram/Resources/icons/menu/stories_save@2x.png new file mode 100644 index 000000000..327054159 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_save@3x.png b/Telegram/Resources/icons/menu/stories_save@3x.png new file mode 100644 index 000000000..e97dac0ab Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_save@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section.png b/Telegram/Resources/icons/menu/stories_saved_section.png new file mode 100644 index 000000000..31a29d96b Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section@2x.png b/Telegram/Resources/icons/menu/stories_saved_section@2x.png new file mode 100644 index 000000000..02a9f4687 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_saved_section@3x.png b/Telegram/Resources/icons/menu/stories_saved_section@3x.png new file mode 100644 index 000000000..8de09fb32 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_saved_section@3x.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats.png b/Telegram/Resources/icons/menu/stories_to_chats.png new file mode 100644 index 000000000..b5129521c Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats@2x.png b/Telegram/Resources/icons/menu/stories_to_chats@2x.png new file mode 100644 index 000000000..575fcc811 Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats@2x.png differ diff --git a/Telegram/Resources/icons/menu/stories_to_chats@3x.png b/Telegram/Resources/icons/menu/stories_to_chats@3x.png new file mode 100644 index 000000000..00f6b959b Binary files /dev/null and b/Telegram/Resources/icons/menu/stories_to_chats@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/folder_links.png b/Telegram/Resources/icons/settings/folder_links.png new file mode 100644 index 000000000..da8497860 Binary files /dev/null and b/Telegram/Resources/icons/settings/folder_links.png differ diff --git a/Telegram/Resources/icons/settings/folder_links@2x.png b/Telegram/Resources/icons/settings/folder_links@2x.png new file mode 100644 index 000000000..26dfa285e Binary files /dev/null and b/Telegram/Resources/icons/settings/folder_links@2x.png differ diff --git a/Telegram/Resources/icons/settings/folder_links@3x.png b/Telegram/Resources/icons/settings/folder_links@3x.png new file mode 100644 index 000000000..fef135b0d Binary files /dev/null and b/Telegram/Resources/icons/settings/folder_links@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/settings/stories.png b/Telegram/Resources/icons/settings/stories.png new file mode 100644 index 000000000..c41ac06dd Binary files /dev/null and b/Telegram/Resources/icons/settings/stories.png differ diff --git a/Telegram/Resources/icons/settings/stories@2x.png b/Telegram/Resources/icons/settings/stories@2x.png new file mode 100644 index 000000000..82e24fc67 Binary files /dev/null and b/Telegram/Resources/icons/settings/stories@2x.png differ diff --git a/Telegram/Resources/icons/settings/stories@3x.png b/Telegram/Resources/icons/settings/stories@3x.png new file mode 100644 index 000000000..23c70a8cc Binary files /dev/null and b/Telegram/Resources/icons/settings/stories@3x.png differ diff --git a/Telegram/Resources/icons/stories/next.png b/Telegram/Resources/icons/stories/next.png new file mode 100644 index 000000000..dd997b2d0 Binary files /dev/null and b/Telegram/Resources/icons/stories/next.png differ diff --git a/Telegram/Resources/icons/stories/next@2x.png b/Telegram/Resources/icons/stories/next@2x.png new file mode 100644 index 000000000..82b819e9e Binary files /dev/null and b/Telegram/Resources/icons/stories/next@2x.png differ diff --git a/Telegram/Resources/icons/stories/next@3x.png b/Telegram/Resources/icons/stories/next@3x.png new file mode 100644 index 000000000..3550c8cce Binary files /dev/null and b/Telegram/Resources/icons/stories/next@3x.png differ 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 2f044dcc5..76399cc23 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_activate" = "Activate"; "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; +"lng_menu_my_stories" = "My Stories"; "lng_disable_notifications_from_tray" = "Disable notifications"; "lng_enable_notifications_from_tray" = "Enable notifications"; @@ -188,6 +189,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filter_chats_limit_title" = "Limit Reached"; "lng_filter_chats_limit1#one" = "Sorry, you can't add more than **{count}** chat to a folder."; "lng_filter_chats_limit1#other" = "Sorry, you can't add more than **{count}** chats to a folder."; +"lng_filter_chats_exlude_limit1#one" = "Sorry, you can't exlude more than **{count}** chat from a folder."; +"lng_filter_chats_exlude_limit1#other" = "Sorry, you can't exlude more than **{count}** chats from a folder."; "lng_filter_chats_limit2#one" = "You can increase this limit to **{count}** by upgrading to **Telegram Premium**."; "lng_filter_chats_limit2#other" = "You can increase this limit to **{count}** by upgrading to **Telegram Premium**."; @@ -232,6 +235,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_file_size_limit1" = "The document can't be sent, because it is larger than {size}."; "lng_file_size_limit2" = "You can double this limit to {size} per document by subscribing to **Telegram Premium**."; +"lng_filter_links_limit_title" = "Limit Reached"; +"lng_filter_links_limit1#one" = "Sorry, you can't create more than **{count}** invite link."; +"lng_filter_links_limit1#other" = "Sorry, you can't create more than **{count}** invite links."; +"lng_filter_links_limit2#one" = "You can increase the limit to **{count}** link by subscribing to **Telegram Premium**."; +"lng_filter_links_limit2#other" = "You can increase the limit to **{count}** links by subscribing to **Telegram Premium**."; + +"lng_filter_shared_limit_title" = "Limit Reached"; +"lng_filter_shared_limit1#one" = "Sorry, you can't add more than **{count}** shareable folders."; +"lng_filter_shared_limit1#other" = "Sorry, you can't add more than **{count}** shareable folders."; +"lng_filter_shared_limit2#one" = "You can increase the limit up to **{count}** folder by subscribing to **Telegram Premium**."; +"lng_filter_shared_limit2#other" = "You can increase the limit up to **{count}** folders by subscribing to **Telegram Premium**."; + "lng_limits_increase" = "Increase Limit"; "lng_sticker_premium_text" = "This pack contains premium stickers like this one."; @@ -257,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?"; @@ -273,6 +289,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_message_text" = "New message text..."; "lng_deleted" = "Deleted Account"; "lng_deleted_message" = "Deleted message"; +"lng_deleted_story" = "Deleted story"; "lng_pinned_message" = "Pinned message"; "lng_pinned_previous" = "Previous message"; "lng_pinned_unpin_sure" = "Would you like to unpin this message?"; @@ -373,6 +390,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_dlg_filter" = "Search"; "lng_dlg_new_group_name" = "Group name"; "lng_dlg_new_channel_name" = "Channel name"; +"lng_dlg_new_bot_name" = "Bot name"; "lng_no_chats" = "Your chats will be here"; "lng_no_chats_filter" = "No chats currently belong to this folder."; "lng_contacts_loading" = "Loading..."; @@ -386,7 +404,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_save" = "Save"; "lng_username_title" = "Username"; -"lng_username_description" = "You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can use **a-z**, **0-9** and **underscores**.\nMinimum length is **5 characters**."; +"lng_username_description1" = "You can choose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number."; +"lng_username_description2" = "You can use **a-z**, **0-9** and **underscores**.\nMinimum length is **5 characters**."; "lng_username_choose" = "Choose your username."; "lng_username_invalid" = "This username is invalid."; "lng_username_occupied" = "This username is already occupied."; @@ -417,6 +436,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_usernames_deactivate_description" = "Do you want to hide this link from the channel info page?"; "lng_channel_usernames_description" = "Drag and drop links to change the order in which they will be displayed on the channel info page."; +"lng_bot_username_title" = "Public Link"; +"lng_bot_username_description1" = "This link cannot be edited. You can acquire additional usernames on {link}."; +"lng_bot_username_description1_link" = "Fragment"; +"lng_bot_usernames_activate_description" = "Do you want to show this link on the bot info page?"; +"lng_bot_usernames_deactivate_description" = "Do you want to hide this link from the bot info page?"; +"lng_bot_usernames_description" = "Drag and drop links to change the order in which they will be displayed on the bot info page."; + "lng_bio_placeholder" = "Bio"; "lng_settings_section_info" = "My info"; @@ -562,6 +588,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_forwards_privacy" = "Forwarded messages"; "lng_settings_profile_photo_privacy" = "Profile photo"; "lng_settings_voices_privacy" = "Voice messages"; +"lng_settings_bio_privacy" = "Bio"; "lng_settings_privacy_premium" = "Only subscribers of {link} can restrict receiving voice messages."; "lng_settings_privacy_premium_link" = "Telegram Premium"; "lng_settings_passcode_disable" = "Disable Passcode"; @@ -573,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"; @@ -585,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"; @@ -609,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"; @@ -708,7 +756,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_manage_enabled_dictionary" = "Dictionary is enabled"; "lng_settings_manage_remove_dictionary" = "Remove Dictionary"; -"lng_backgrounds_header" = "Choose your new chat background"; +"lng_backgrounds_header" = "Choose Wallpaper"; "lng_theme_sure_keep" = "Keep this theme?"; "lng_theme_reverting#one" = "Reverting to the old theme in {count} second."; "lng_theme_reverting#other" = "Reverting to the old theme in {count} seconds."; @@ -728,6 +776,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_background_link_copied" = "Link copied to clipboard"; "lng_background_blur" = "Blurred"; "lng_background_sure_delete" = "Are you sure you want to delete this background?"; +"lng_background_other_info" = "{user} will be able to apply this wallpaper"; +"lng_background_apply1" = "Apply the wallpaper in this chat."; +"lng_background_apply2" = "Enjoy the view."; +"lng_background_apply_button" = "Apply For This Chat"; +"lng_background_dimming" = "Background dimming"; +"lng_background_sure_reset_default" = "Are you sure you want to reset the wallpaper?"; +"lng_background_reset_default" = "Reset"; "lng_download_path_ask" = "Ask download path for each file"; "lng_download_path" = "Download path"; @@ -911,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."; @@ -935,6 +991,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_everyone" = "Everybody"; "lng_edit_privacy_contacts" = "My contacts"; +"lng_edit_privacy_close_friends" = "Close friends"; "lng_edit_privacy_nobody" = "Nobody"; "lng_edit_privacy_exceptions" = "Add exceptions"; @@ -971,6 +1028,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_edit_privacy_groups_always_title" = "Always allow"; "lng_edit_privacy_groups_never_title" = "Never allow"; +"lng_edit_privacy_about_title" = "Bio privacy settings"; +"lng_edit_privacy_about_header" = "Who can see my bio"; +"lng_edit_privacy_about_always_empty" = "Always allow"; +"lng_edit_privacy_about_never_empty" = "Never allow"; +"lng_edit_privacy_about_exceptions" = "These users will or will not be able to see your profile bio regardless of the settings above."; +"lng_edit_privacy_about_always_title" = "Always allow"; +"lng_edit_privacy_about_never_title" = "Never allow"; + "lng_edit_privacy_calls_title" = "Voice calls privacy"; "lng_edit_privacy_calls_header" = "Who can call you"; "lng_edit_privacy_calls_always_empty" = "Always allow"; @@ -1030,17 +1095,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_self_destruct_sessions_title" = "Session termination"; "lng_self_destruct_sessions_description" = "If you don't come online from a specific session at least once within this period, it will be terminated."; -"lng_change_phone_title" = "Change phone number"; -"lng_change_phone_about" = "You can change your Telegram number\nhere. Your account and all your cloud data\n— messages, media, contacts, etc. will be\nmoved to the new number.\n\n**Important**: all your Telegram contacts will\nget your **new number** added to their address\nbook, provided they had your old number and\nyou haven't blocked them in Telegram."; -"lng_change_phone_warning" = "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram."; -"lng_change_phone_occupied" = "The number {phone} is already connected to a Telegram account. Please delete that account before migrating to the new number."; -"lng_change_phone_button" = "Change number"; -"lng_change_phone_new_title" = "Enter new number"; -"lng_change_phone_new_description" = "We will send an SMS with a confirmation code to your new number."; "lng_change_phone_new_submit" = "Submit"; "lng_change_phone_code_title" = "Enter code"; -"lng_change_phone_code_description" = "We've sent an SMS with a confirmation code to your phone {phone}."; -"lng_change_phone_success" = "Your phone number has been changed."; +"lng_change_phone_error" = "You can only change your phone number using mobile Telegram apps. Please use an official Telegram app on your phone to update your number."; "lng_mute_menu_mute" = "Mute"; "lng_mute_menu_unmute" = "Unmute"; @@ -1093,6 +1150,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_sure_kick_channel" = "Remove {user} from the channel?"; "lng_profile_sure_remove_admin" = "Remove {user} from admins?"; "lng_profile_loading" = "Loading..."; +"lng_profile_saved_stories#one" = "{count} saved story"; +"lng_profile_saved_stories#other" = "{count} saved stories"; "lng_profile_photos#one" = "{count} photo"; "lng_profile_photos#other" = "{count} photos"; "lng_profile_gifs#one" = "{count} GIF"; @@ -1109,6 +1168,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_shared_links#other" = "{count} shared links"; "lng_profile_copy_phone" = "Copy Phone Number"; "lng_profile_copy_fullname" = "Copy Name"; +"lng_profile_photo_by_you" = "photo set by you"; +"lng_profile_public_photo" = "public photo"; "lng_via_link_group_one" = "**{user}** restricts adding them to groups.\nYou can send them an invite link as message instead."; "lng_via_link_group_many#one" = "**{count} user** restricts adding them to groups.\nYou can send them an invite link as message instead."; @@ -1202,6 +1263,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_group_title" = "Manage Group"; "lng_manage_channel_title" = "Manage Channel"; +"lng_manage_bot_title" = "Manage Bot"; "lng_manage_peer_recent_actions" = "Recent Actions"; "lng_manage_peer_members" = "Members"; "lng_manage_peer_subscribers" = "Subscribers"; @@ -1252,6 +1314,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_no_forwards_about" = "Members won't be able to forward messages from this group or save media files."; "lng_manage_peer_no_forwards_about_channel" = "Subscribers won't be able to forward messages from this channel or save media files."; +"lng_manage_peer_bot_public_link" = "Public Link"; +"lng_manage_peer_bot_public_links" = "Public Links"; +"lng_manage_peer_bot_edit_intro" = "Edit Intro"; +"lng_manage_peer_bot_edit_commands" = "Edit Commands"; +"lng_manage_peer_bot_edit_settings" = "Change Bot Settings"; +"lng_manage_peer_bot_about" = "Use {bot} to manage this bot."; +"lng_manage_peer_bot_delete" = "Delete Bot"; + "lng_manage_discussion_group" = "Discussion"; "lng_manage_discussion_group_add" = "Add a group"; "lng_manage_linked_channel" = "Linked channel"; @@ -1307,6 +1377,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_report_group_video_title" = "Report group video"; "lng_report_channel_photo_title" = "Report channel photo"; "lng_report_channel_video_title" = "Report channel video"; +"lng_report_story" = "Report story"; "lng_report_please_select_messages" = "Please select messages to report."; "lng_report_select_messages" = "Select messages"; "lng_report_messages_none" = "Select Messages"; @@ -1464,6 +1535,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_pinned_media_sticker" = "a sticker"; "lng_action_pinned_media_emoji_sticker" = "a {emoji} sticker"; "lng_action_pinned_media_game" = "the game «{game}»"; +"lng_action_pinned_media_story" = "a story"; "lng_action_game_score#one" = "{from} scored {count} in {game}"; "lng_action_game_score#other" = "{from} scored {count} in {game}"; "lng_action_game_you_scored#one" = "You scored {count} in {game}"; @@ -1509,6 +1581,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_suggested_video" = "{user} suggests you to use this profile video."; "lng_action_suggested_video_button" = "View Video"; "lng_action_attach_menu_bot_allowed" = "You allowed this bot to message you when you added it in the attachment menu."; +"lng_action_set_wallpaper_me" = "You set a new wallpaper for this chat"; +"lng_action_set_wallpaper" = "{user} set a new wallpaper for this chat"; +"lng_action_set_wallpaper_button" = "View Wallpaper"; +"lng_action_set_same_wallpaper_me" = "You set the same wallpaper for this chat"; +"lng_action_set_same_wallpaper" = "{user} set the same wallpaper for this chat"; "lng_action_topic_created_inside" = "Topic created"; "lng_action_topic_closed_inside" = "Topic closed"; "lng_action_topic_reopened_inside" = "Topic reopened"; @@ -1524,6 +1601,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_topic_icon_changed" = "{from} changed the {link} icon to {emoji}"; "lng_action_topic_icon_removed" = "{from} removed the {link} icon"; "lng_action_shared_chat_with_bot" = "You shared {chat} with {bot}"; +"lng_action_story_mention_me" = "You mentioned {user} in a story"; +"lng_action_story_mention" = "{user} mentioned you in a story"; +"lng_action_story_mention_button" = "View Story"; +"lng_action_story_mention_me_unavailable" = "The story where you mentioned {user} is no longer available."; +"lng_action_story_mention_unavailable" = "The story where {user} mentioned you is no longer available."; "lng_premium_gift_duration_months#one" = "for {count} month"; "lng_premium_gift_duration_months#other" = "for {count} months"; @@ -1568,6 +1650,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_no_joined" = "No one joined yet"; "lng_group_invite_joined#one" = "{count} joined"; "lng_group_invite_joined#other" = "{count} joined"; +"lng_group_invite_joined_via_filter" = "joined via a folder invite link"; "lng_group_invite_remaining#one" = "{count} remaining"; "lng_group_invite_remaining#other" = "{count} remaining"; "lng_group_invite_requested#one" = "{count} requested"; @@ -1653,6 +1736,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_about_private_link" = "This link will only work for members of this chat."; "lng_forwarded" = "Forwarded from {user}"; +"lng_forwarded_story" = "Story from {user}"; +"lng_forwarded_story_expired" = "This story has expired."; "lng_forwarded_date" = "Original: {date}"; "lng_forwarded_channel" = "Forwarded from {channel}"; "lng_forwarded_psa_default" = "Forwarded from {channel}"; @@ -1727,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"; @@ -1765,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"; @@ -1795,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"; @@ -1866,6 +1986,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_switch_gifs" = "GIFs"; "lng_switch_masks" = "Masks"; "lng_stickers_featured_add" = "Add"; +"lng_stickers_featured_installed" = "Added"; "lng_emoji_featured_unlock" = "Unlock"; "lng_emoji_premium_restore" = "Restore"; "lng_gifs_search" = "Search GIFs"; @@ -1927,6 +2048,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_sticker" = "Sticker"; "lng_in_dlg_sticker_emoji" = "{emoji} Sticker"; "lng_in_dlg_poll" = "Poll"; +"lng_in_dlg_story" = "Story"; +"lng_in_dlg_story_expired" = "Expired story"; "lng_in_dlg_media_count#one" = "{count} media"; "lng_in_dlg_media_count#other" = "{count} media"; "lng_in_dlg_photo_count#one" = "{count} photo"; @@ -1985,6 +2108,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_broadcast_ph" = "Broadcast a message..."; "lng_broadcast_silent_ph" = "Silent broadcast..."; "lng_send_anonymous_ph" = "Send anonymously..."; +"lng_story_reply_ph" = "Reply privately..."; "lng_send_text_no" = "Text not allowed."; "lng_send_text_no_about" = "The admins of this group only allow sending {types}."; "lng_send_text_type_and_last" = "{types} and {last}"; @@ -2002,6 +2126,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_as_premium_required" = "Subscribe to {link} to be able to comment on behalf of your channels in group chats."; "lng_send_as_premium_required_link" = "Telegram Premium"; "lng_record_cancel" = "Release outside this field to cancel"; +"lng_record_cancel_stories" = "Release outside to cancel"; "lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?"; "lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?"; "lng_record_lock_discard" = "Discard"; @@ -2182,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"; @@ -2340,6 +2466,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_contact_phone_will_be_shared" = "You can make your phone visible to {user}."; "lng_edit_contact_title" = "Edit contact name"; "lng_edit_channel_title" = "Edit channel"; +"lng_edit_bot_title" = "Edit bot"; "lng_edit_sign_messages" = "Sign messages"; "lng_edit_group" = "Edit group"; "lng_edit_self_title" = "Edit your name"; @@ -2347,6 +2474,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_add_contact" = "Create"; "lng_add_contact_button" = "New contact"; "lng_contacts_header" = "Contacts"; +"lng_contacts_hidden_stories" = "Hidden Stories"; +"lng_contacts_stories_status#one" = "{count} story"; +"lng_contacts_stories_status#other" = "{count} stories"; +"lng_contacts_stories_status_new#one" = "{count} new story"; +"lng_contacts_stories_status_new#other" = "{count} new stories"; "lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram."; "lng_try_other_contact" = "Try someone else"; "lng_create_group_link" = "Link"; @@ -2417,6 +2549,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_copy" = "Copy"; "lng_mediaview_forward" = "Forward"; "lng_mediaview_delete" = "Delete"; +"lng_mediaview_save_to_profile" = "Save to Profile"; +"lng_mediaview_archive_story" = "Archive Story"; "lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_files_all" = "View all files"; "lng_mediaview_single_photo" = "Single Photo"; @@ -2430,6 +2564,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_doc_image" = "File"; "lng_mediaview_today" = "today at {time}"; "lng_mediaview_yesterday" = "yesterday at {time}"; +"lng_mediaview_just_now" = "just now"; +"lng_mediaview_minutes_ago#one" = "{count} minute ago"; +"lng_mediaview_minutes_ago#other" = "{count} minutes ago"; +"lng_mediaview_hours_ago#one" = "{count} hour ago"; +"lng_mediaview_hours_ago#other" = "{count} hours ago"; "lng_mediaview_date_time" = "{date} at {time}"; "lng_mediaview_set_userpic" = "Set as Main"; "lng_mediaview_report_profile_photo" = "Report"; @@ -2437,6 +2576,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_title" = "Media viewer"; "lng_mediaview_saved_to" = "Image was saved to your {downloads} folder"; "lng_mediaview_saved_images_to" = "Images were saved to your {downloads} folder"; +"lng_mediaview_video_saved_to" = "Video file was saved to your {downloads} folder"; "lng_mediaview_downloads" = "Downloads"; "lng_mediaview_playback_speed" = "Playback speed: {speed}"; "lng_mediaview_rotate_video" = "Rotate video"; @@ -3083,6 +3223,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_participant_joined_channel" = "{from} joined the channel"; "lng_admin_log_participant_joined_by_link" = "{from} joined the group via {link}"; "lng_admin_log_participant_joined_by_link_channel" = "{from} joined the channel via {link}"; +"lng_admin_log_participant_joined_by_filter_link" = "{from} joined the group via {link} from a folder"; +"lng_admin_log_participant_joined_by_filter_link_channel" = "{from} joined the channel via {link} from a folder"; "lng_admin_log_participant_approved_by_link" = "{from} was approved to join the group via {link} by {user}"; "lng_admin_log_participant_approved_by_link_channel" = "{from} was approved to join the channel via {link} by {user}"; "lng_admin_log_participant_approved_by_request" = "{from} joined to the group via public request, approved by {user}"; @@ -3168,6 +3310,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_admin_post_messages" = "Post messages"; "lng_admin_log_admin_edit_messages" = "Edit messages"; "lng_admin_log_admin_delete_messages" = "Delete messages"; +"lng_admin_log_admin_remain_anonymous" = "Remain anonymous"; "lng_admin_log_admin_ban_users" = "Ban users"; "lng_admin_log_admin_invite_users" = "Add members"; "lng_admin_log_admin_invite_link" = "Invite users via link"; @@ -3352,6 +3495,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_export_option_info_about" = "Your chosen screen name, username, phone number and profile pictures."; "lng_export_option_contacts" = "Contacts list"; "lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices."; +"lng_export_option_stories" = "Stories archive"; +"lng_export_option_stories_about" = "All stories you posted from Telegram mobile apps."; "lng_export_option_sessions" = "Active sessions"; "lng_export_option_sessions_about" = "We store this to display your connected devices in Settings > Privacy & Security > Active Sessions."; "lng_export_header_other" = "Other"; @@ -3504,6 +3649,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_restore" = "Undo"; "lng_filters_new" = "New Folder"; "lng_filters_edit" = "Edit Folder"; +"lng_filters_setup_menu" = "Edit Folders"; "lng_filters_new_name" = "Folder name"; "lng_filters_add_chats" = "Add chats"; "lng_filters_remove_chats" = "Remove chats"; @@ -3541,11 +3687,83 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_menu_add" = "Add to folder"; "lng_filters_toast_add" = "{chat} added to {folder} folder"; "lng_filters_toast_remove" = "{chat} removed from {folder} folder"; +"lng_filters_shareable_status" = "shareable folder"; + +"lng_filters_delete_sure" = "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder."; +"lng_filters_link" = "Share Folder"; +"lng_filters_link_has" = "Invite links"; +"lng_filters_link_badge" = "New"; +"lng_filters_link_create" = "Create an Invite Link"; +"lng_filters_link_cant" = "No way to share folders with chat types or excluded chats."; +"lng_filters_link_about" = "Share access to some of this folder's groups and channels with others."; +"lng_filters_link_about_many" = "Create more links to set up different access levels for different people."; +"lng_filters_link_title" = "Share Folder"; +"lng_filters_link_share_about" = "Anyone with this link can add the {folder} folder and the chats selected below."; +"lng_filters_link_subtitle" = "Invite link"; +"lng_filters_link_chats_none" = "No chats selected"; +"lng_filters_link_chats#one" = "{count} chat selected"; +"lng_filters_link_chats#other" = "{count} chats selected"; +"lng_filters_link_bot_status" = "you can't share chats with bots"; +"lng_filters_link_bot_error" = "Chats with bots can't be shared."; +"lng_filters_link_private_status" = "you can't share private chats"; +"lng_filters_link_private_error" = "Private chats can't be shared."; +"lng_filters_link_noadmin_status" = "you can't invite others here"; +"lng_filters_link_noadmin_group_error" = "You don't have the admin rights to share invite links to this private group."; +"lng_filters_link_noadmin_channel_error" = "You don't have the admin rights to share invite links to this private channel."; +"lng_filters_link_already_group" = "you are already a member"; +"lng_filters_link_already_channel" = "you are already subscribed"; +"lng_filters_link_inaccessible" = "chat is inaccessible"; +"lng_filters_link_chats_about" = "Select groups and channels that you want everyone who adds the folder via invite link to join."; +"lng_filters_link_no_about" = "There are no chats in this folder that you can share with others."; +"lng_filters_link_chats_no" = "These chats cannot be shared"; +"lng_filters_link_chats_no_about" = "You can only share groups and channels in which you are allowed to create invite links."; +"lng_filters_link_name_it" = "Name Link"; +"lng_filters_link_delete_sure" = "Are you sure you want to delete this link?"; +"lng_filters_link_qr_about" = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link."; +"lng_filters_link_group_admin_error" = "One of the groups in this folder can’t be added because one of its admins has too many groups and channels."; +"lng_filters_by_link_title" = "Add Folder"; +"lng_filters_by_link_sure" = "Do you want to add a new chat folder {folder} and join its groups and channels?"; +"lng_filters_by_link_join#one" = "{count} chat to join"; +"lng_filters_by_link_join#other" = "{count} chats to join"; +"lng_filters_by_link_add_button" = "Add {folder}"; +"lng_filters_by_link_add_no" = "Do not add this folder"; +"lng_filters_by_link_more" = "Add Chats to Folder"; +"lng_filters_by_link_more_sure" = "Do you want to join chats and add them to the folder {folder}?"; +"lng_filters_by_link_about" = "You can deselect the chats you don't want to join."; +"lng_filters_by_link_and_join_button#one" = "Join Chat"; +"lng_filters_by_link_and_join_button#other" = "Join Chats"; +"lng_filters_by_link_join_no" = "Do not join any chats"; +"lng_filters_by_link_already" = "Folder already added"; +"lng_filters_by_link_already_about" = "You have already added the folder {folder} and all its chats."; +"lng_filters_by_link_in#one" = "{count} chat in this folder"; +"lng_filters_by_link_in#other" = "{count} chats in this folder"; +"lng_filters_by_link_remove" = "Remove Folder"; +"lng_filters_by_link_remove_sure" = "Do you also want to quit the chats included in the folder {folder}?"; +"lng_filters_by_link_quit#one" = "{count} chat to quit"; +"lng_filters_by_link_quit#other" = "{count} chats to quit"; +"lng_filters_by_link_select" = "Select All"; +"lng_filters_by_link_deselect" = "Deselect All"; +"lng_filters_by_link_about_quit" = "You can deselect the chats you don't want to quit."; +"lng_filters_by_link_remove_button" = "Remove Folder and Keep Chats"; +"lng_filters_by_link_and_quit_button#one" = "Remove Folder and Chat"; +"lng_filters_by_link_and_quit_button#other" = "Remove Folder and Chats"; +"lng_filters_added_title" = "Folder {folder} Added"; +"lng_filters_added_also#one" = "You also joined {count} chat."; +"lng_filters_added_also#other" = "You also joined {count} chats."; +"lng_filters_updated_title" = "Folder {folder} Updated"; +"lng_filters_updated_also#one" = "You have joined {count} new chat."; +"lng_filters_updated_also#other" = "You have joined {count} new chats."; +"lng_filters_bar_you_can#one" = "You can join {count} new chat"; +"lng_filters_bar_you_can#other" = "You can join {count} new chats"; +"lng_filters_bar_view#one" = "Click here to view it"; +"lng_filters_bar_view#other" = "Click here to view them"; "lng_chat_theme_change" = "Change colors"; +"lng_chat_theme_wallpaper" = "Set Wallpaper"; "lng_chat_theme_none" = "No\nTheme"; "lng_chat_theme_apply" = "Apply Theme"; -"lng_chat_theme_title" = "Select theme"; +"lng_chat_theme_change_wallpaper" = "Change Wallpaper"; +"lng_chat_theme_title" = "Choose theme"; "lng_chat_theme_cant_voice" = "Sorry, you can't change the chat theme while you're having an unsent voice message."; "lng_photo_editor_menu_delete" = "Delete"; @@ -3567,9 +3785,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_view_button_background" = "View background"; "lng_view_button_theme" = "View theme"; "lng_view_button_message" = "View message"; +"lng_view_button_story" = "View story"; "lng_view_button_voice_chat" = "Voice chat"; "lng_view_button_voice_chat_channel" = "Live stream"; "lng_view_button_request_join" = "Request to Join"; +"lng_view_button_external_link" = "Open link"; "lng_sponsored_hide_ads" = "Hide"; "lng_sponsored_title" = "What are sponsored messages?"; @@ -3669,6 +3889,75 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_userpic_builder_color_subtitle" = "Choose background"; "lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; +"lng_stories_my_name" = "My Story"; +"lng_stories_archive" = "Hide Stories"; +"lng_stories_unarchive" = "Unhide Stories"; +"lng_stories_row_count#one" = "{count} Story"; +"lng_stories_row_count#other" = "{count} Stories"; +"lng_stories_views#one" = "{count} view"; +"lng_stories_views#other" = "{count} views"; +"lng_stories_no_views" = "No views"; +"lng_stories_unsupported" = "This story is not supported\nby your version of Telegram."; +"lng_stories_cant_reply" = "You can't reply to this story."; +"lng_stories_about_silent" = "This video has no sound."; +"lng_stories_about_close_friends" = "You're seeing this story because {user} added you to their list of **Close Friends**."; +"lng_stories_about_contacts" = "Only {user}'s contacts can view this story."; +"lng_stories_about_selected_contacts" = "Only some contacts {user} selected can view this story."; +"lng_stories_about_close_friends_my" = "Only your list of **Close Friends** can view this story."; +"lng_stories_about_contacts_my" = "Only your contacts can view this story."; +"lng_stories_about_selected_contacts_my" = "Only some contacts you selected can view this story."; +"lng_stories_click_to_view_mine" = "Click here to view your story."; +"lng_stories_click_to_view" = "Click here to view updates from {users}."; +"lng_stories_click_to_view_and_one" = "{accumulated}, {user}"; +"lng_stories_click_to_view_and_last" = "{accumulated} and {user}"; +"lng_stories_show_more" = "Show more"; + +"lng_stories_my_title" = "Saved Stories"; +"lng_stories_archive_button" = "Stories Archive"; +"lng_stories_recent_button" = "Recent Stories"; +"lng_stories_archive_title" = "Stories Archive"; +"lng_stories_archive_about" = "Only you can see archived stories unless you choose to save them to your profile."; +"lng_stories_reply_sent" = "Message Sent"; +"lng_stories_hidden_to_contacts" = "Stories from {user} will now be shown in **Archived Chats**."; +"lng_stories_shown_in_chats" = "Stories from {user} will now be shown in the **Chats List**."; +"lng_stories_delete_one_sure" = "Are you sure you want to delete this story?"; +"lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?"; +"lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?"; +"lng_stories_save_sure" = "Do you want to save this story to your profile?"; +"lng_stories_save_sure_many#one" = "Do you want to save {count} story to your profile?"; +"lng_stories_save_sure_many#other" = "Do you want to save {count} stories to your profile?"; +"lng_stories_save_done" = "This story is saved to your profile."; +"lng_stories_save_done_many#one" = "{count} story is saved to your profile."; +"lng_stories_save_done_many#other" = "{count} stories are saved to your profile."; +"lng_stories_save_done_about" = "Saved stories can be viewed by others on your profile until you remove them."; +"lng_stories_archive_sure" = "Do you want to hide this story from your profile?"; +"lng_stories_archive_sure_many#one" = "Do you want to hide {count} story from your profile?"; +"lng_stories_archive_sure_many#other" = "Do you want to hide {count} stories from your profile?"; +"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."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 06a600c00..28b0c58cc 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -1,8 +1,8 @@ - ../../animations/change_number.tgs ../../animations/blocked_peers_empty.tgs ../../animations/filters.tgs + ../../animations/cloud_filters.tgs ../../animations/local_passcode_enter.tgs ../../animations/cloud_password/intro.tgs ../../animations/cloud_password/password_input.tgs diff --git a/Telegram/Resources/qrc/telegram/export.qrc b/Telegram/Resources/qrc/telegram/export.qrc index 06ecc7eb7..290d24a30 100644 --- a/Telegram/Resources/qrc/telegram/export.qrc +++ b/Telegram/Resources/qrc/telegram/export.qrc @@ -37,6 +37,8 @@ ../../export_html/images/section_photos@2x.png ../../export_html/images/section_sessions.png ../../export_html/images/section_sessions@2x.png + ../../export_html/images/section_stories.png + ../../export_html/images/section_stories@2x.png ../../export_html/images/section_web.png ../../export_html/images/section_web@2x.png ../../export_html/js/script.js diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 5ed2e5918..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 7d67f211f..1f7799c44 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,7,1,0 - PRODUCTVERSION 4,7,1,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", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "4.7.1.0" + VALUE "FileVersion", "4.9.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.7.1.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 38757ce7c..a5c085ccb 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,7,1,0 - PRODUCTVERSION 4,7,1,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", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop Updater" - VALUE "FileVersion", "4.7.1.0" + VALUE "FileVersion", "4.9.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "4.7.1.0" + VALUE "ProductVersion", "4.9.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/_other/packer.cpp b/Telegram/SourceFiles/_other/packer.cpp index 2bf651bc6..0ba79c1be 100644 --- a/Telegram/SourceFiles/_other/packer.cpp +++ b/Telegram/SourceFiles/_other/packer.cpp @@ -7,12 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "packer.h" -#include - -#ifdef Q_OS_MAC -//Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin) -#endif - bool BetaChannel = false; quint64 AlphaVersion = 0; bool OnlyAlphaKey = false; diff --git a/Telegram/SourceFiles/_other/updater_linux.cpp b/Telegram/SourceFiles/_other/updater_linux.cpp index 40e887c52..f286b05be 100644 --- a/Telegram/SourceFiles/_other/updater_linux.cpp +++ b/Telegram/SourceFiles/_other/updater_linux.cpp @@ -46,6 +46,7 @@ string updaterName; string workDir; string exeName; string exePath; +string argv0; FILE *_logFile = 0; void openLog() { @@ -388,6 +389,8 @@ int main(int argc, char *argv[]) { exeName = argv[i]; } else if (equal(argv[i], "-exepath") && ++i < argc) { exePath = argv[i]; + } else if (equal(argv[i], "-argv0") && ++i < argc) { + argv0 = argv[i]; } } if (exeName.empty() || exeName.find('/') != string::npos) { @@ -461,15 +464,14 @@ int main(int argc, char *argv[]) { writeLog("Error: short exe name!"); } - auto fullBinaryPath = exePath + exeName; - const auto path = fullBinaryPath.c_str(); + const auto fullBinaryPath = exePath + exeName; auto values = vector(); const auto push = [&](string arg) { // Force null-terminated .data() call result. values.push_back(arg + char(0)); }; - push(path); + push(!argv0.empty() ? argv0 : fullBinaryPath); push("-noupdate"); if (autostart) push("-autostart"); if (debug) push("-debug"); @@ -498,7 +500,7 @@ int main(int argc, char *argv[]) { writeLog("fork() failed!"); return 1; case 0: - execv(args[0], args.data()); + execv(fullBinaryPath.c_str(), args.data()); return 1; } } diff --git a/Telegram/SourceFiles/api/api_attached_stickers.cpp b/Telegram/SourceFiles/api/api_attached_stickers.cpp index be87544be..0f18e7648 100644 --- a/Telegram/SourceFiles/api/api_attached_stickers.cpp +++ b/Telegram/SourceFiles/api/api_attached_stickers.cpp @@ -41,7 +41,7 @@ void AttachedStickers::request( return; } else if (result.v.size() > 1) { strongController->show( - Box(strongController, result.v)); + Box(strongController->uiShow(), result.v)); return; } // Single attached sticker pack. @@ -54,16 +54,14 @@ void AttachedStickers::request( .id = data->vid().v, .accessHash = data->vaccess_hash().v } : StickerSetIdentifier{ .shortName = qs(data->vshort_name()) }; - strongController->show( - Box( - strongController, - setId, - (data->is_emojis() - ? Data::StickersType::Emoji - : data->is_masks() - ? Data::StickersType::Masks - : Data::StickersType::Stickers)), - Ui::LayerOption::KeepOther); + strongController->show(Box( + strongController->uiShow(), + setId, + (data->is_emojis() + ? Data::StickersType::Emoji + : data->is_masks() + ? Data::StickersType::Masks + : Data::StickersType::Stickers))); }).fail([=] { _requestId = 0; if (const auto strongController = weak.get()) { 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_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 4c19b4c85..65826aa2f 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -82,7 +82,7 @@ void SendBotCallbackData( flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_password; } const auto weak = base::make_weak(controller); - const auto show = std::make_shared(controller); + const auto show = controller->uiShow(); button->requestId = api->request(MTPmessages_GetBotCallbackAnswer( MTP_flags(flags), history->peer->input, @@ -119,7 +119,7 @@ void SendBotCallbackData( if (withPassword) { show->hideLayer(); } - Ui::Toast::Show(show->toastParent(), message); + show->showToast(message); } } else if (!link.isEmpty()) { if (!isGame) { @@ -210,7 +210,7 @@ void SendBotCallbackDataWithPassword( } api->cloudPassword().reload(); const auto weak = base::make_weak(controller); - const auto show = std::make_shared(controller); + const auto show = controller->uiShow(); SendBotCallbackData(controller, item, row, column, {}, {}, [=]( const QString &error) { auto box = PrePasswordErrorBox( @@ -279,11 +279,11 @@ void SendBotCallbackDataWithPassword( bool SwitchInlineBotButtonReceived( not_null controller, - const QString &query, + const QByteArray &queryWithPeerTypes, UserData *samePeerBot, MsgId samePeerReplyTo) { return controller->content()->notify_switchInlineBotButtonReceived( - query, + QString::fromUtf8(queryWithPeerTypes), samePeerBot, samePeerReplyTo); } @@ -375,8 +375,10 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { ShowAtTheEndMsgId); auto action = Api::SendAction(history); action.clearDraft = false; - action.replyTo = itemId; - action.topicRootId = topicRootId; + action.replyTo = { + .msgId = itemId, + .topicRootId = topicRootId, + }; history->session().api().shareContact( history->session().user(), action); @@ -441,14 +443,14 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { if (samePeer) { SwitchInlineBotButtonReceived( controller, - QString::fromUtf8(button->data), + button->data, bot, item->id); return true; } else if (bot->isBot() && bot->botInfo->inlineReturnTo.key) { const auto switched = SwitchInlineBotButtonReceived( controller, - QString::fromUtf8(button->data)); + button->data); if (switched) { return true; } @@ -466,7 +468,9 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { Window::ShowChooseRecipientBox( controller, chosen, - tr::lng_inline_switch_choose()); + tr::lng_inline_switch_choose(), + nullptr, + button->peerTypes); } } } break; diff --git a/Telegram/SourceFiles/api/api_bot.h b/Telegram/SourceFiles/api/api_bot.h index 198ad926b..7e26dd103 100644 --- a/Telegram/SourceFiles/api/api_bot.h +++ b/Telegram/SourceFiles/api/api_bot.h @@ -30,7 +30,7 @@ void SendBotCallbackDataWithPassword( bool SwitchInlineBotButtonReceived( not_null controller, - const QString &query, + const QByteArray &queryWithPeerTypes, UserData *samePeerBot = nullptr, MsgId samePeerReplyTo = 0); diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 7efcaf01a..c045263ae 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -7,12 +7,663 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_chat_filters.h" -#include "data/data_session.h" -#include "data/data_chat_filters.h" -#include "main/main_session.h" #include "apiwrap.h" +#include "boxes/peer_list_box.h" +#include "boxes/premium_limits_box.h" +#include "boxes/filters/edit_filter_links.h" // FilterChatStatusText +#include "core/application.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_chat_filters.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/filter_link_header.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/filter_icons.h" +#include "window/window_session_controller.h" +#include "styles/style_filter_icons.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" namespace Api { +namespace { + +enum class ToggleAction { + Adding, + Removing, +}; + +class ToggleChatsController final + : public PeerListController + , public base::has_weak_ptr { +public: + ToggleChatsController( + not_null window, + ToggleAction action, + const QString &title, + std::vector> chats, + std::vector> additional); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + + [[nodiscard]] auto selectedValue() const + -> rpl::producer>>; + + void adjust(int minHeight, int maxHeight, int addedTopHeight); + void setRealContentHeight(rpl::producer value); + rpl::producer boxHeightValue() const override; + +private: + void setupAboveWidget(); + void setupBelowWidget(); + void initDesiredHeightValue(); + void toggleAllSelected(bool select); + + const not_null _window; + Ui::RpWidget *_addedTopWidget = nullptr; + Ui::RpWidget *_addedBottomWidget = nullptr; + + ToggleAction _action = ToggleAction::Adding; + QString _filterTitle; + base::flat_set> _checkable; + std::vector> _chats; + std::vector> _additional; + rpl::variable>> _selected; + + int _minTopHeight = 0; + rpl::variable _maxTopHeight; + rpl::variable _aboveHeight; + rpl::variable _belowHeight; + rpl::variable _desiredHeight; + + base::unique_qptr _menu; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) { + using Type = Ui::FilterLinkHeaderType; + switch (type) { + case Type::AddingFilter: return tr::lng_filters_by_link_title; + case Type::AddingChats: return tr::lng_filters_by_link_more; + case Type::AllAdded: return tr::lng_filters_by_link_already; + case Type::Removing: return tr::lng_filters_by_link_remove; + } + Unexpected("Ui::FilterLinkHeaderType in TitleText."); +} + +[[nodiscard]] TextWithEntities AboutText( + Ui::FilterLinkHeaderType type, + const QString &title) { + using Type = Ui::FilterLinkHeaderType; + auto boldTitle = Ui::Text::Bold(title); + return (type == Type::AddingFilter) + ? tr::lng_filters_by_link_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : (type == Type::AddingChats) + ? tr::lng_filters_by_link_more_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : (type == Type::AllAdded) + ? tr::lng_filters_by_link_already_about( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities) + : tr::lng_filters_by_link_remove_sure( + tr::now, + lt_folder, + std::move(boldTitle), + Ui::Text::WithEntities); +} + +void InitFilterLinkHeader( + not_null box, + Fn adjust, + Ui::FilterLinkHeaderType type, + const QString &title, + const QString &iconEmoji, + rpl::producer count) { + const auto icon = Ui::LookupFilterIcon( + Ui::LookupFilterIconByEmoji( + iconEmoji + ).value_or(Ui::FilterIcon::Custom)).active; + auto header = Ui::MakeFilterLinkHeader(box, { + .type = type, + .title = TitleText(type)(tr::now), + .about = AboutText(type, title), + .folderTitle = title, + .folderIcon = icon, + .badge = (type == Ui::FilterLinkHeaderType::AddingChats + ? std::move(count) + : rpl::single(0)), + }); + const auto widget = header.widget; + widget->resizeToWidth(st::boxWideWidth); + Ui::SendPendingMoveResizeEvents(widget); + + const auto min = widget->minimumHeight(), max = widget->maximumHeight(); + widget->resize(st::boxWideWidth, max); + + box->setAddedTopScrollSkip(max); + std::move( + header.wheelEvents + ) | rpl::start_with_next([=](not_null e) { + box->sendScrollViewportEvent(e); + }, widget->lifetime()); + + std::move( + header.closeRequests + ) | rpl::start_with_next([=] { + box->closeBox(); + }, widget->lifetime()); + + struct State { + bool processing = false; + int addedTopHeight = 0; + }; + const auto state = widget->lifetime().make_state(); + + box->scrolls( + ) | rpl::filter([=] { + return !state->processing; + }) | rpl::start_with_next([=] { + state->processing = true; + const auto guard = gsl::finally([&] { state->processing = false; }); + + const auto top = box->scrollTop(); + const auto headerHeight = std::max(max - top, min); + const auto addedTopHeight = max - headerHeight; + widget->resize(widget->width(), headerHeight); + if (state->addedTopHeight < addedTopHeight) { + adjust(min, max, addedTopHeight); + box->setAddedTopScrollSkip(headerHeight); + } else { + box->setAddedTopScrollSkip(headerHeight); + adjust(min, max, addedTopHeight); + } + state->addedTopHeight = addedTopHeight; + box->peerListRefreshRows(); + }, widget->lifetime()); + + box->setNoContentMargin(true); + adjust(min, max, 0); +} + +void ImportInvite( + const QString &slug, + FilterId filterId, + const base::flat_set> &peers, + Fn done, + Fn fail) { + Expects(!peers.empty()); + + const auto peer = peers.front(); + const auto api = &peer->session().api(); + const auto callback = [=](const MTPUpdates &result) { + api->applyUpdates(result); + if (slug.isEmpty()) { + peer->owner().chatsFilters().moreChatsHide(filterId, true); + } + done(); + }; + const auto error = [=](const MTP::Error &error) { + fail(error.type()); + }; + auto inputs = peers | ranges::views::transform([](auto peer) { + return MTPInputPeer(peer->input); + }) | ranges::to>(); + if (!slug.isEmpty()) { + api->request(MTPchatlists_JoinChatlistInvite( + MTP_string(slug), + MTP_vector(std::move(inputs)) + )).done(callback).fail(error).send(); + } else { + api->request(MTPchatlists_JoinChatlistUpdates( + MTP_inputChatlistDialogFilter(MTP_int(filterId)), + MTP_vector(std::move(inputs)) + )).done(callback).fail(error).send(); + } +} + +ToggleChatsController::ToggleChatsController( + not_null window, + ToggleAction action, + const QString &title, + std::vector> chats, + std::vector> additional) +: _window(window) +, _action(action) +, _filterTitle(title) +, _chats(std::move(chats)) +, _additional(std::move(additional)) { + setStyleOverrides(&st::filterLinkChatsList); +} + +void ToggleChatsController::prepare() { + auto selected = base::flat_set>(); + const auto disabled = [](not_null peer) { + return peer->isChat() + ? peer->asChat()->isForbidden() + : peer->isChannel() + ? peer->asChannel()->isForbidden() + : false; + }; + const auto add = [&](not_null peer, bool additional = false) { + const auto disable = disabled(peer); + auto row = (additional || !disable) + ? std::make_unique(peer) + : MakeFilterChatRow( + peer, + tr::lng_filters_link_inaccessible(tr::now), + true); + if (delegate()->peerListFindRow(peer->id.value)) { + return; + } + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (!disable + && (!additional || _action == ToggleAction::Removing)) { + _checkable.emplace(peer); + if (const auto status = FilterChatStatusText(peer) + ; !status.isEmpty()) { + raw->setCustomStatus(status); + } + } + if (disable) { + } else if (!additional) { + delegate()->peerListSetRowChecked(raw, true); + raw->finishCheckedAnimation(); + selected.emplace(peer); + } else if (_action == ToggleAction::Adding) { + raw->setDisabledState(PeerListRow::State::DisabledChecked); + raw->setCustomStatus(peer->isBroadcast() + ? tr::lng_filters_link_already_channel(tr::now) + : tr::lng_filters_link_already_group(tr::now)); + } + }; + for (const auto &peer : _chats) { + if (!disabled(peer)) { + add(peer); + } + } + for (const auto &peer : _additional) { + add(peer, true); + } + for (const auto &peer : _chats) { + if (disabled(peer)) { + add(peer); + } + } + setupAboveWidget(); + setupBelowWidget(); + initDesiredHeightValue(); + delegate()->peerListRefreshRows(); + _selected = std::move(selected); +} + +void ToggleChatsController::rowClicked(not_null row) { + const auto peer = row->peer(); + if (!_checkable.contains(peer)) { + return; + } + const auto checked = row->checked(); + auto selected = _selected.current(); + delegate()->peerListSetRowChecked(row, !checked); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + _selected = std::move(selected); +} + +void ToggleChatsController::setupAboveWidget() { + using namespace Settings; + + auto wrap = object_ptr((QWidget*)nullptr); + const auto container = wrap.data(); + + _addedTopWidget = container->add(object_ptr(container)); + const auto realAbove = container->add( + object_ptr(container)); + AddDivider(realAbove); + const auto totalCount = [&] { + if (_chats.empty()) { + return _additional.size(); + } else if (_additional.empty()) { + return _chats.size(); + } + auto result = _chats.size(); + for (const auto &peer : _additional) { + if (!ranges::contains(_chats, peer)) { + ++result; + } + } + return result; + }; + const auto count = (_action == ToggleAction::Removing) + ? totalCount() + : _chats.empty() + ? _additional.size() + : _chats.size(); + const auto selectableCount = int(_checkable.size()); + auto selectedCount = _selected.value( + ) | rpl::map([](const base::flat_set> &selected) { + return int(selected.size()); + }); + AddFilterSubtitleWithToggles( + realAbove, + (_action == ToggleAction::Removing + ? tr::lng_filters_by_link_quit + : _chats.empty() + ? tr::lng_filters_by_link_in + : tr::lng_filters_by_link_join)( + lt_count, + rpl::single(float64(count))), + selectableCount, + std::move(selectedCount), + [=](bool select) { toggleAllSelected(select); }); + + _aboveHeight = realAbove->heightValue(); + delegate()->peerListSetAboveWidget(std::move(wrap)); +} + +void ToggleChatsController::toggleAllSelected(bool select) { + auto selected = _selected.current(); + if (!select) { + if (selected.empty()) { + return; + } + for (const auto &peer : selected) { + const auto row = delegate()->peerListFindRow(peer->id.value); + Assert(row != nullptr); + delegate()->peerListSetRowChecked(row, false); + } + selected = {}; + } else { + const auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + const auto row = delegate()->peerListRowAt(i); + const auto peer = row->peer(); + if (_action != ToggleAction::Adding || + !ranges::contains(_additional, peer)) { + delegate()->peerListSetRowChecked(row, true); + selected.emplace(peer); + } + } + } + _selected = std::move(selected); +} + +void ToggleChatsController::setupBelowWidget() { + if (_chats.empty()) { + auto widget = object_ptr((QWidget*)nullptr); + _addedBottomWidget = widget.data(); + delegate()->peerListSetBelowWidget(std::move(widget)); + return; + } + auto layout = object_ptr((QWidget*)nullptr); + const auto raw = layout.data(); + auto widget = object_ptr( + (QWidget*)nullptr, + std::move(layout), + st::settingsDividerLabelPadding); + raw->add(object_ptr( + raw, + (_action == ToggleAction::Removing + ? tr::lng_filters_by_link_about_quit + : tr::lng_filters_by_link_about)(tr::now), + st::boxDividerLabel)); + _addedBottomWidget = raw->add(object_ptr(raw)); + _belowHeight = widget->heightValue() | rpl::map([=](int value) { + return value - _addedBottomWidget->height(); + }); + delegate()->peerListSetBelowWidget(std::move(widget)); +} + +Main::Session &ToggleChatsController::session() const { + return _window->session(); +} + +auto ToggleChatsController::selectedValue() const +-> rpl::producer>> { + return _selected.value(); +} + +void ToggleChatsController::adjust( + int minHeight, + int maxHeight, + int addedTopHeight) { + Expects(addedTopHeight >= 0); + + _addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight); + _minTopHeight = minHeight; + _maxTopHeight = maxHeight; +} + +void ToggleChatsController::setRealContentHeight(rpl::producer value) { + std::move( + value + ) | rpl::start_with_next([=](int height) { + const auto desired = _desiredHeight.current(); + if (height <= computeListSt().item.height) { + return; + } else if (height >= desired) { + _addedBottomWidget->resize(_addedBottomWidget->width(), 0); + } else { + const auto available = desired - height; + const auto required = _maxTopHeight.current() - _minTopHeight; + const auto added = required - available; + _addedBottomWidget->resize( + _addedBottomWidget->width(), + std::max(added, 0)); + } + }, _lifetime); +} + +void ToggleChatsController::initDesiredHeightValue() { + using namespace rpl::mappers; + + const auto &st = computeListSt(); + const auto count = int(delegate()->peerListFullRowsCount()); + const auto middle = st.padding.top() + + (count * st.item.height) + + st.padding.bottom(); + _desiredHeight = rpl::combine( + _maxTopHeight.value(), + _aboveHeight.value(), + _belowHeight.value(), + _1 + _2 + middle + _3); +} + +rpl::producer ToggleChatsController::boxHeightValue() const { + return _desiredHeight.value() | rpl::map([=](int value) { + return std::min(value, st::boxMaxListHeight); + }); +} + +void ShowImportError( + not_null window, + FilterId id, + int added, + const QString &error) { + const auto session = &window->session(); + const auto &list = session->data().chatsFilters().list(); + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + const auto count = added + + ((i != end(list)) ? int(i->always().size()) : 0); + if (error == u"CHANNELS_TOO_MUCH"_q) { + window->show(Box(ChannelsLimitBox, session)); + } else if (error == u"FILTER_INCLUDE_TOO_MUCH"_q) { + window->show(Box(FilterChatsLimitBox, session, count, true)); + } else if (error == u"CHATLISTS_TOO_MUCH"_q) { + window->show(Box(ShareableFiltersLimitBox, session)); + } else { + window->showToast((error == u"INVITE_SLUG_EXPIRED"_q) + ? tr::lng_group_invite_bad_link(tr::now) + : error); + } +} + +void ShowImportToast( + base::weak_ptr weak, + const QString &title, + Ui::FilterLinkHeaderType type, + int added) { + const auto strong = weak.get(); + if (!strong) { + return; + } + const auto created = (type == Ui::FilterLinkHeaderType::AddingFilter); + const auto phrase = created + ? tr::lng_filters_added_title + : tr::lng_filters_updated_title; + auto text = Ui::Text::Bold(phrase(tr::now, lt_folder, title)); + if (added > 0) { + const auto phrase = created + ? tr::lng_filters_added_also + : tr::lng_filters_updated_also; + text.append('\n').append(phrase(tr::now, lt_count, added)); + } + strong->showToast(std::move(text)); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + const QString &title, + const QString &iconEmoji, + std::vector> peers, + std::vector> already) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + if (peers.empty() && !filterId) { + strong->showToast(tr::lng_group_invite_bad_link(tr::now)); + return; + } + const auto fullyAdded = (peers.empty() && filterId); + auto controller = std::make_unique( + strong, + ToggleAction::Adding, + title, + std::move(peers), + std::move(already)); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setStyle(st::filterInviteBox); + + using Type = Ui::FilterLinkHeaderType; + const auto type = fullyAdded + ? Type::AllAdded + : !filterId + ? Type::AddingFilter + : Type::AddingChats; + auto badge = raw->selectedValue( + ) | rpl::map([=](const base::flat_set> &peers) { + return int(peers.size()); + }); + InitFilterLinkHeader(box, [=](int min, int max, int addedTop) { + raw->adjust(min, max, addedTop); + }, type, title, iconEmoji, rpl::duplicate(badge)); + + raw->setRealContentHeight(box->heightValue()); + + auto owned = Ui::FilterLinkProcessButton( + box, + type, + title, + std::move(badge)); + + const auto button = owned.data(); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + + box->addButton(std::move(owned)); + + struct State { + bool importing = false; + }; + const auto state = box->lifetime().make_state(); + + raw->selectedValue( + ) | rpl::start_with_next([=]( + base::flat_set> &&peers) { + button->setClickedCallback([=] { + if (peers.empty()) { + box->closeBox(); + } else if (!state->importing) { + state->importing = true; + const auto added = int(peers.size()); + ImportInvite(slug, filterId, peers, crl::guard(box, [=] { + ShowImportToast(weak, title, type, peers.size()); + box->closeBox(); + }), crl::guard(box, [=](QString text) { + if (const auto strong = weak.get()) { + ShowImportError(strong, filterId, added, text); + } + state->importing = false; + })); + } + }); + }, box->lifetime()); + }; + strong->show( + Box(std::move(controller), std::move(initBox))); +} + +void ProcessFilterInvite( + base::weak_ptr weak, + const QString &slug, + FilterId filterId, + std::vector> peers, + std::vector> already) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + const auto &list = strong->session().data().chatsFilters().list(); + const auto it = ranges::find(list, filterId, &Data::ChatFilter::id); + if (it == end(list)) { + strong->showToast(u"Filter not found :shrug:"_q); + return; + } + ProcessFilterInvite( + weak, + slug, + filterId, + it->title(), + it->iconEmoji(), + std::move(peers), + std::move(already)); +} + +} // namespace void SaveNewFilterPinned( not_null session, @@ -25,7 +676,176 @@ void SaveNewFilterPinned( MTP_int(filterId), filter.tl() )).send(); +} +void CheckFilterInvite( + not_null controller, + const QString &slug) { + const auto session = &controller->session(); + const auto weak = base::make_weak(controller); + session->api().checkFilterInvite(slug, [=]( + const MTPchatlists_ChatlistInvite &result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + auto title = QString(); + auto iconEmoji = QString(); + auto filterId = FilterId(); + auto peers = std::vector>(); + auto already = std::vector>(); + auto &owner = strong->session().data(); + result.match([&](const auto &data) { + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); + }); + const auto parseList = [&](const MTPVector &list) { + auto result = std::vector>(); + result.reserve(list.v.size()); + for (const auto &peer : list.v) { + result.push_back(owner.peer(peerFromMTP(peer))); + } + return result; + }; + result.match([&](const MTPDchatlists_chatlistInvite &data) { + title = qs(data.vtitle()); + iconEmoji = data.vemoticon().value_or_empty(); + peers = parseList(data.vpeers()); + }, [&](const MTPDchatlists_chatlistInviteAlready &data) { + filterId = data.vfilter_id().v; + peers = parseList(data.vmissing_peers()); + already = parseList(data.valready_peers()); + }); + + const auto notLoaded = filterId + && !ranges::contains( + owner.chatsFilters().list(), + filterId, + &Data::ChatFilter::id); + if (notLoaded) { + const auto lifetime = std::make_shared(); + owner.chatsFilters().changed( + ) | rpl::start_with_next([=] { + lifetime->destroy(); + ProcessFilterInvite( + weak, + slug, + filterId, + std::move(peers), + std::move(already)); + }, *lifetime); + owner.chatsFilters().reload(); + } else if (filterId) { + ProcessFilterInvite( + weak, + slug, + filterId, + std::move(peers), + std::move(already)); + } else { + ProcessFilterInvite( + weak, + slug, + filterId, + title, + iconEmoji, + std::move(peers), + std::move(already)); + } + }, [=](const MTP::Error &error) { + if (error.code() != 400) { + return; + } + ProcessFilterInvite(weak, slug, {}, {}, {}, {}, {}); + }); +} + +void ProcessFilterUpdate( + base::weak_ptr weak, + FilterId filterId, + std::vector> missing) { + if (const auto strong = missing.empty() ? weak.get() : nullptr) { + strong->session().data().chatsFilters().moreChatsHide(filterId); + return; + } + ProcessFilterInvite(weak, QString(), filterId, std::move(missing), {}); +} + +void ProcessFilterRemove( + base::weak_ptr weak, + const QString &title, + const QString &iconEmoji, + std::vector> all, + std::vector> suggest, + Fn>)> done) { + const auto strong = weak.get(); + if (!strong) { + return; + } + Core::App().hideMediaView(); + if (all.empty() && suggest.empty()) { + done({}); + return; + } + auto controller = std::make_unique( + strong, + ToggleAction::Removing, + title, + std::move(suggest), + std::move(all)); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setStyle(st::filterInviteBox); + + const auto type = Ui::FilterLinkHeaderType::Removing; + auto badge = raw->selectedValue( + ) | rpl::map([=](const base::flat_set> &peers) { + return int(peers.size()); + }); + InitFilterLinkHeader(box, [=](int min, int max, int addedTop) { + raw->adjust(min, max, addedTop); + }, type, title, iconEmoji, rpl::single(0)); + + auto owned = Ui::FilterLinkProcessButton( + box, + type, + title, + std::move(badge)); + + const auto button = owned.data(); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + + box->addButton(std::move(owned)); + + raw->selectedValue( + ) | rpl::start_with_next([=]( + base::flat_set> &&peers) { + button->setClickedCallback([=] { + done(peers | ranges::to_vector); + box->closeBox(); + }); + }, box->lifetime()); + }; + strong->show( + Box(std::move(controller), std::move(initBox))); +} + +[[nodiscard]] std::vector> ExtractSuggestRemoving( + const Data::ChatFilter &filter) { + if (!filter.chatlist()) { + return {}; + } + return filter.always() | ranges::views::filter([]( + not_null history) { + return history->peer->isChannel(); + }) | ranges::views::transform(&History::peer) | ranges::to_vector; } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_filters.h b/Telegram/SourceFiles/api/api_chat_filters.h index 59c29f72b..9167a41db 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.h +++ b/Telegram/SourceFiles/api/api_chat_filters.h @@ -11,10 +11,38 @@ namespace Main { class Session; } // namespace Main +namespace Window { +class SessionController; +} // namespace Window + +namespace Data { +class ChatFilter; +} // namespace Data + namespace Api { void SaveNewFilterPinned( not_null session, FilterId filterId); +void CheckFilterInvite( + not_null controller, + const QString &slug); + +void ProcessFilterUpdate( + base::weak_ptr weak, + FilterId filterId, + std::vector> missing); + +void ProcessFilterRemove( + base::weak_ptr weak, + const QString &title, + const QString &iconEmoji, + std::vector> all, + std::vector> suggest, + Fn>)> done); + +[[nodiscard]] std::vector> ExtractSuggestRemoving( + const Data::ChatFilter &filter); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index 674061653..8b395d9a9 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -22,7 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_file_origin.h" #include "ui/boxes/confirm_box.h" -#include "ui/toasts/common_toasts.h" +#include "ui/toast/toast.h" #include "boxes/premium_limits_box.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -85,20 +85,17 @@ void SubmitChatInvite( } strongController->hideLayer(); - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(strongController).toastParent(), - .text = { [&] { - if (type == u"INVITE_REQUEST_SENT"_q) { - return isGroup - ? tr::lng_group_request_sent(tr::now) - : tr::lng_group_request_sent_channel(tr::now); - } else if (type == u"USERS_TOO_MUCH"_q) { - return tr::lng_group_invite_no_room(tr::now); - } else { - return tr::lng_group_invite_bad_link(tr::now); - } - }() }, - .duration = ApiWrap::kJoinErrorDuration }); + strongController->showToast([&] { + if (type == u"INVITE_REQUEST_SENT"_q) { + return isGroup + ? tr::lng_group_request_sent(tr::now) + : tr::lng_group_request_sent_channel(tr::now); + } else if (type == u"USERS_TOO_MUCH"_q) { + return tr::lng_group_invite_no_room(tr::now); + } else { + return tr::lng_group_invite_bad_link(tr::now); + } + }(), ApiWrap::kJoinErrorDuration); }).send(); } 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_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index 2fe4fa802..0a44a916f 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_common.h" +#include "base/qt/qt_key_modifiers.h" +#include "data/data_histories.h" #include "data/data_thread.h" +#include "history/history.h" namespace Api { @@ -16,8 +19,19 @@ SendAction::SendAction( SendOptions options) : history(thread->owningHistory()) , options(options) -, replyTo(thread->topicRootId()) -, topicRootId(replyTo) { +, replyTo({ .msgId = thread->topicRootId() }) { + replyTo.topicRootId = replyTo.msgId; +} + +SendOptions DefaultSendWhenOnlineOptions() { + return { + .scheduled = kScheduledUntilOnlineTimestamp, + .silent = base::IsCtrlPressed(), + }; +} + +MTPInputReplyTo SendAction::mtpReplyTo() const { + return Data::ReplyToForMTP(&history->owner(), replyTo); } } // namespace Api diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index 34631c8a8..08ee59ca1 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -15,6 +15,8 @@ class Thread; namespace Api { +inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); + struct SendOptions { PeerData *sendAs = nullptr; TimeId scheduled = 0; @@ -23,6 +25,7 @@ struct SendOptions { bool removeWebPageId = false; bool hideViaBot = false; }; +[[nodiscard]] SendOptions DefaultSendWhenOnlineOptions(); enum class SendType { Normal, @@ -37,11 +40,12 @@ struct SendAction { not_null history; SendOptions options; - MsgId replyTo = 0; - MsgId topicRootId = 0; + FullReplyTo replyTo; bool clearDraft = true; bool generateLocal = true; MsgId replaceMediaOf = 0; + + [[nodiscard]] MTPInputReplyTo mtpReplyTo() const; }; struct MessageToSend { diff --git a/Telegram/SourceFiles/api/api_global_privacy.cpp b/Telegram/SourceFiles/api/api_global_privacy.cpp index c940dbe86..219d335f1 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.cpp +++ b/Telegram/SourceFiles/api/api_global_privacy.cpp @@ -56,6 +56,15 @@ rpl::producer GlobalPrivacy::archiveAndMute() const { return _archiveAndMute.value(); } +UnarchiveOnNewMessage GlobalPrivacy::unarchiveOnNewMessageCurrent() const { + return _unarchiveOnNewMessage.current(); +} + +auto GlobalPrivacy::unarchiveOnNewMessage() const +-> rpl::producer { + return _unarchiveOnNewMessage.value(); +} + rpl::producer GlobalPrivacy::showArchiveAndMute() const { using namespace rpl::mappers; @@ -75,14 +84,33 @@ 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; _api.request(_requestId).cancel(); + const auto flags = Flag() + | (archiveAndMute + ? Flag::f_archive_and_mute_new_noncontact_peers + : Flag()) + | (unarchiveOnNewMessage == UnarchiveOnNewMessage::None + ? Flag::f_keep_archived_unmuted + : Flag()) + | (unarchiveOnNewMessage != UnarchiveOnNewMessage::AnyUnmuted + ? Flag::f_keep_archived_folders + : Flag()); _requestId = _api.request(MTPaccount_SetGlobalPrivacySettings( - MTP_globalPrivacySettings( - MTP_flags(Flag::f_archive_and_mute_new_noncontact_peers), - MTP_bool(archiveAndMute)) + MTP_globalPrivacySettings(MTP_flags(flags)) )).done([=](const MTPGlobalPrivacySettings &result) { _requestId = 0; apply(result); @@ -90,13 +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.varchive_and_mute_new_noncontact_peers() - ? mtpIsTrue(*data.varchive_and_mute_new_noncontact_peers()) - : false; + _archiveAndMute = data.is_archive_and_mute_new_noncontact_peers(); + _unarchiveOnNewMessage = data.is_keep_archived_unmuted() + ? UnarchiveOnNewMessage::None + : data.is_keep_archived_folders() + ? UnarchiveOnNewMessage::NotInFoldersUnmuted + : UnarchiveOnNewMessage::AnyUnmuted; }); } diff --git a/Telegram/SourceFiles/api/api_global_privacy.h b/Telegram/SourceFiles/api/api_global_privacy.h index a1a693499..9e4b8e121 100644 --- a/Telegram/SourceFiles/api/api_global_privacy.h +++ b/Telegram/SourceFiles/api/api_global_privacy.h @@ -17,15 +17,26 @@ class Session; namespace Api { +enum class UnarchiveOnNewMessage { + None, + NotInFoldersUnmuted, + AnyUnmuted, +}; + class GlobalPrivacy final { 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; + [[nodiscard]] auto unarchiveOnNewMessageCurrent() const + -> UnarchiveOnNewMessage; + [[nodiscard]] auto unarchiveOnNewMessage() const + -> rpl::producer; [[nodiscard]] rpl::producer showArchiveAndMute() const; [[nodiscard]] rpl::producer<> suggestArchiveAndMute() const; void dismissArchiveAndMuteSuggestion(); @@ -33,10 +44,16 @@ public: private: void apply(const MTPGlobalPrivacySettings &data); + void update( + bool archiveAndMute, + UnarchiveOnNewMessage unarchiveOnNewMessage); + const not_null _session; MTP::Sender _api; mtpRequestId _requestId = 0; rpl::variable _archiveAndMute = false; + rpl::variable _unarchiveOnNewMessage + = UnarchiveOnNewMessage::None; rpl::variable _showArchiveAndMute = false; std::vector> _callbacks; diff --git a/Telegram/SourceFiles/api/api_invite_links.cpp b/Telegram/SourceFiles/api/api_invite_links.cpp index e6d8182f3..1f02b78c1 100644 --- a/Telegram/SourceFiles/api/api_invite_links.cpp +++ b/Telegram/SourceFiles/api/api_invite_links.cpp @@ -58,6 +58,7 @@ JoinedByLinkSlice ParseJoinedByLinkSlice( result.users.push_back({ .user = owner.user(data.vuser_id()), .date = data.vdate().v, + .viaFilterLink = data.is_via_chatlist(), }); }); } diff --git a/Telegram/SourceFiles/api/api_invite_links.h b/Telegram/SourceFiles/api/api_invite_links.h index 14f510796..09b43c086 100644 --- a/Telegram/SourceFiles/api/api_invite_links.h +++ b/Telegram/SourceFiles/api/api_invite_links.h @@ -34,6 +34,7 @@ struct PeerInviteLinks { struct JoinedByLinkUser { not_null user; TimeId date = 0; + bool viaFilterLink = false; }; struct JoinedByLinkSlice { diff --git a/Telegram/SourceFiles/api/api_media.cpp b/Telegram/SourceFiles/api/api_media.cpp index 7dd916140..ec8c1e4b2 100644 --- a/Telegram/SourceFiles/api/api_media.cpp +++ b/Telegram/SourceFiles/api/api_media.cpp @@ -22,8 +22,7 @@ MTPVector ComposeSendingDocumentAttributes( const auto dimensions = document->dimensions; auto attributes = QVector(1, filenameAttribute); if (dimensions.width() > 0 && dimensions.height() > 0) { - const auto duration = document->getDuration(); - if (duration >= 0 && !document->hasMimeType(u"image/gif"_q)) { + if (document->hasDuration() && !document->hasMimeType(u"image/gif"_q)) { auto flags = MTPDdocumentAttributeVideo::Flags(0); using VideoFlag = MTPDdocumentAttributeVideo::Flag; if (document->isVideoMessage()) { @@ -34,9 +33,10 @@ MTPVector ComposeSendingDocumentAttributes( } attributes.push_back(MTP_documentAttributeVideo( MTP_flags(flags), - MTP_int(duration), + MTP_double(document->duration() / 1000.), MTP_int(dimensions.width()), - MTP_int(dimensions.height()))); + MTP_int(dimensions.height()), + MTPint())); // preload_prefix_size } else { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(dimensions.width()), @@ -56,7 +56,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_performer; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(song->duration), + MTP_int(document->duration() / 1000), MTP_string(song->title), MTP_string(song->performer), MTPstring())); @@ -65,7 +65,7 @@ MTPVector ComposeSendingDocumentAttributes( | MTPDdocumentAttributeAudio::Flag::f_waveform; attributes.push_back(MTP_documentAttributeAudio( MTP_flags(flags), - MTP_int(voice->duration), + MTP_int(document->duration() / 1000), MTPstring(), MTPstring(), MTP_bytes(documentWaveformEncode5bit(voice->waveform)))); diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 892ca1090..71512172b 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -97,8 +97,7 @@ constexpr auto kSharedMediaLimit = 100; photo, photoThumbs, MTP_documentEmpty(MTP_long(0)), - jpeg, - 0); + jpeg); } [[nodiscard]] std::optional PrepareMtpMarkup( @@ -186,6 +185,7 @@ void PeerPhoto::updateSelf( const auto usedFileReference = photo->fileReference(); _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot photo->mtpInput() )).done([=](const MTPphotos_Photo &result) { result.match([&](const MTPDphotos_photo &data) { @@ -252,6 +252,7 @@ void PeerPhoto::clear(not_null photo) { if (self->userpicPhotoId() == photo->id) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot MTP_inputPhotoEmpty() )).done([=](const MTPphotos_Photo &result) { self->setPhoto(MTP_userProfilePhotoEmpty()); @@ -276,6 +277,7 @@ void PeerPhoto::clear(not_null photo) { if (fallbackPhotoId && (*fallbackPhotoId) == photo->id) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(MTPphotos_UpdateProfilePhoto::Flag::f_fallback), + MTPInputUser(), // bot MTP_inputPhotoEmpty() )).send(); _session->storage().add(Storage::UserPhotosSetBack( @@ -321,6 +323,7 @@ void PeerPhoto::set(not_null peer, not_null photo) { if (peer == _session->user()) { _api.request(MTPphotos_UpdateProfilePhoto( MTP_flags(0), + MTPInputUser(), // bot photo->mtpInput() )).done([=](const MTPphotos_Photo &result) { result.match([&](const MTPDphotos_photo &data) { @@ -363,13 +366,21 @@ void PeerPhoto::ready( done(); } }; - if (peer->isSelf()) { + const auto botUserInput = [&] { + const auto user = peer->asUser(); + return (user && user->botInfo && user->botInfo->canEditInformation) + ? std::make_optional(user->inputUser) + : std::nullopt; + }(); + if (peer->isSelf() || botUserInput) { using Flag = MTPphotos_UploadProfilePhoto::Flag; const auto none = MTPphotos_UploadProfilePhoto::Flags(0); _api.request(MTPphotos_UploadProfilePhoto( MTP_flags((file ? Flag::f_file : none) + | (botUserInput ? Flag::f_bot : none) | (videoSize ? Flag::f_video_emoji_markup : none) | ((type == UploadType::Fallback) ? Flag::f_fallback : none)), + botUserInput ? (*botUserInput) : MTPInputUser(), // bot file ? (*file) : MTPInputFile(), MTPInputFile(), // video MTPdouble(), // video_start_ts diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index d18c947c1..227b6e9db 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -43,13 +43,12 @@ void Polls::create( const auto history = action.history; const auto peer = history->peer; - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; auto sendFlags = MTPmessages_SendMedia::Flags(0); if (action.replyTo) { - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto clearCloudDraft = action.clearDraft; if (clearCloudDraft) { @@ -74,13 +73,11 @@ void Polls::create( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), PollDataToInputMedia(&data), MTP_string(), MTP_long(randomId), diff --git a/Telegram/SourceFiles/api/api_report.cpp b/Telegram/SourceFiles/api/api_report.cpp index 663daf767..501870180 100644 --- a/Telegram/SourceFiles/api/api_report.cpp +++ b/Telegram/SourceFiles/api/api_report.cpp @@ -10,10 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_peer.h" #include "data/data_photo.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/boxes/report_box.h" -#include "ui/toast/toast.h" +#include "ui/layers/show.h" namespace Api { @@ -39,15 +40,18 @@ MTPreportReason ReasonToTL(const Ui::ReportReason &reason) { } // namespace void SendReport( - not_null toastParent, - not_null peer, - Ui::ReportReason reason, - const QString &comment, - std::variant> data) { - auto weak = Ui::MakeWeak(toastParent.get()); - auto done = crl::guard(toastParent, [=] { - Ui::Toast::Show(toastParent, tr::lng_report_thanks(tr::now)); - }); + std::shared_ptr show, + not_null peer, + Ui::ReportReason reason, + const QString &comment, + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data) { + auto done = [=] { + show->showToast(tr::lng_report_thanks(tr::now)); + }; v::match(data, [&](v::null_t) { peer->session().api().request(MTPaccount_ReportPeer( peer->input, @@ -73,6 +77,17 @@ void SendReport( ReasonToTL(reason), MTP_string(comment) )).done(std::move(done)).send(); + }, [&](StoryId id) { + const auto user = peer->asUser(); + if (!user) { + return; + } + peer->session().api().request(MTPstories_Report( + user->inputUser, + MTP_vector(1, MTP_int(id)), + ReasonToTL(reason), + MTP_string(comment) + )).done(std::move(done)).send(); }); } diff --git a/Telegram/SourceFiles/api/api_report.h b/Telegram/SourceFiles/api/api_report.h index 9472221e3..14e9d4ef1 100644 --- a/Telegram/SourceFiles/api/api_report.h +++ b/Telegram/SourceFiles/api/api_report.h @@ -11,16 +11,21 @@ class PeerData; class PhotoData; namespace Ui { +class Show; enum class ReportReason; } // namespace Ui namespace Api { void SendReport( - not_null toastParent, + std::shared_ptr show, not_null peer, Ui::ReportReason reason, const QString &comment, - std::variant> data); + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data); } // namespace Api diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index 40b07e3f3..5b471851e 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -60,8 +60,7 @@ SendMediaReady PrepareRingtoneDocument( MTP_photoEmpty(MTP_long(0)), PreparedPhotoThumbs(), document, - QByteArray(), - 0); + QByteArray()); } } // namespace @@ -203,8 +202,8 @@ int Ringtones::maxSavedCount() const { 100); } -int Ringtones::maxDuration() const { - return _session->account().appConfig().get( +crl::time Ringtones::maxDuration() const { + return crl::time(1000) * _session->account().appConfig().get( "ringtone_duration_max", 5); } diff --git a/Telegram/SourceFiles/api/api_ringtones.h b/Telegram/SourceFiles/api/api_ringtones.h index 08918a89e..ea97db349 100644 --- a/Telegram/SourceFiles/api/api_ringtones.h +++ b/Telegram/SourceFiles/api/api_ringtones.h @@ -40,7 +40,7 @@ public: [[nodiscard]] int64 maxSize() const; [[nodiscard]] int maxSavedCount() const; - [[nodiscard]] int maxDuration() const; + [[nodiscard]] crl::time maxDuration() const; private: struct UploadedData { diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index dfba1d930..291a2675a 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -86,10 +86,7 @@ void SendExistingMedia( auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, message.action.options); @@ -150,13 +147,11 @@ void SendExistingMedia( histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), inputMedia(), MTP_string(captionText), MTP_long(randomId), @@ -273,10 +268,7 @@ bool SendDice(MessageToSend &message) { auto sendFlags = MTPmessages_SendMedia::Flags(0); if (message.action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id; - if (message.action.topicRootId) { - sendFlags |= MTPmessages_SendMedia::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(message.action); const auto anonymousPost = peer->amAnonymous(); @@ -320,13 +312,11 @@ bool SendDice(MessageToSend &message) { histories.sendPreparedMessage( history, message.action.replyTo, - message.action.topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_inputMediaDice(MTP_string(emoji)), MTP_string(), MTP_long(randomId), @@ -378,12 +368,12 @@ void SendConfirmedFile( if (!isEditing) { const auto histories = &session->data().histories(); - file->to.replyTo = histories->convertTopicReplyTo( + file->to.replyTo.msgId = histories->convertTopicReplyToId( history, - file->to.replyTo); - file->to.topicRootId = histories->convertTopicReplyTo( + file->to.replyTo.msgId); + file->to.replyTo.topicRootId = histories->convertTopicReplyToId( history, - file->to.topicRootId); + file->to.replyTo.topicRootId); } session->uploader().upload(newId, file); @@ -391,7 +381,6 @@ void SendConfirmedFile( auto action = SendAction(history, file->to.options); action.clearDraft = false; action.replyTo = file->to.replyTo; - action.topicRootId = file->to.topicRootId; action.generateLocal = true; action.replaceMediaOf = file->to.replaceMediaOf; session->api().sendAction(action); @@ -453,11 +442,13 @@ void SendConfirmedFile( MTP_flags(Flag::f_document | (file->spoiler ? Flag::f_spoiler : Flag())), file->document, + MTPDocument(), // alt_document MTPint()); } else if (file->type == SendMediaType::Audio) { return MTP_messageMediaDocument( MTP_flags(MTPDmessageMediaDocument::Flag::f_document), file->document, + MTPDocument(), // alt_document MTPint()); } else { Unexpected("Type in sendFilesConfirmed."); @@ -465,7 +456,6 @@ void SendConfirmedFile( }()); if (itemToEdit) { - itemToEdit->savePreviousMedia(); auto edition = HistoryMessageEdition(); edition.isEditHide = (flags & MessageFlag::HideEdited); edition.editDate = 0; @@ -477,6 +467,7 @@ void SendConfirmedFile( edition.useSameMarkup = true; edition.useSameReplies = true; edition.useSameReactions = true; + edition.savePreviousMedia = true; itemToEdit->applyEdition(std::move(edition)); } else { const auto viaBotId = UserId(); diff --git a/Telegram/SourceFiles/api/api_toggling_media.cpp b/Telegram/SourceFiles/api/api_toggling_media.cpp index 9293043a8..65d2ce317 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.cpp +++ b/Telegram/SourceFiles/api/api_toggling_media.cpp @@ -48,27 +48,26 @@ void ToggleExistingMedia( } // namespace void ToggleFavedSticker( - not_null controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin) { ToggleFavedSticker( - controller, + std::move(show), document, std::move(origin), !document->owner().stickers().isFaved(document)); } void ToggleFavedSticker( - not_null controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin, bool faved) { if (faved && !document->sticker()) { return; } - const auto weak = base::make_weak(controller); auto done = [=] { - document->owner().stickers().setFaved(weak.get(), document, faved); + document->owner().stickers().setFaved(show, document, faved); }; ToggleExistingMedia( document, @@ -104,17 +103,16 @@ void ToggleRecentSticker( } void ToggleSavedGif( - Window::SessionController *controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin, bool saved) { if (saved && !document->isGifv()) { return; } - const auto weak = base::make_weak(controller); auto done = [=] { if (saved) { - document->owner().stickers().addSavedGif(weak.get(), document); + document->owner().stickers().addSavedGif(show, document); } }; ToggleExistingMedia( diff --git a/Telegram/SourceFiles/api/api_toggling_media.h b/Telegram/SourceFiles/api/api_toggling_media.h index a028824d5..10f0090af 100644 --- a/Telegram/SourceFiles/api/api_toggling_media.h +++ b/Telegram/SourceFiles/api/api_toggling_media.h @@ -7,19 +7,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Window { -class SessionController; -} // namespace Window +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers namespace Api { void ToggleFavedSticker( - not_null controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin); void ToggleFavedSticker( - not_null controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin, bool faved); @@ -30,7 +30,7 @@ void ToggleRecentSticker( bool saved); void ToggleSavedGif( - Window::SessionController *controller, + std::shared_ptr show, not_null document, Data::FileOrigin origin, bool saved); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 59f8b098b..78b286db0 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_scheduled_messages.h" #include "data/data_send_action.h" +#include "data/data_stories.h" #include "data/data_message_reactions.h" #include "inline_bots/bot_attach_web_view.h" #include "chat_helpers/emoji_interactions.h" @@ -677,9 +678,11 @@ void Updates::getDifference() { api().request(MTPupdates_GetDifference( MTP_flags(0), MTP_int(_ptsWaiter.current()), - MTPint(), + MTPint(), // pts_limit + MTPint(), // pts_total_limit MTP_int(_updatesDate), - MTP_int(_updatesQts) + MTP_int(_updatesQts), + MTPint() // qts_limit )).done([=](const MTPupdates_Difference &result) { differenceDone(result); }).fail([=](const MTP::Error &error) { @@ -1984,7 +1987,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; @@ -2515,7 +2518,20 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateTranscribedAudio: { const auto &data = update.c_updateTranscribedAudio(); _session->api().transcribes().apply(data); - } + } break; + + case mtpc_updateStory: { + _session->data().stories().apply(update.c_updateStory()); + } break; + + case mtpc_updateReadStories: { + _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_user_names.cpp b/Telegram/SourceFiles/api/api_user_names.cpp index 3e74dd65c..fa040f17e 100644 --- a/Telegram/SourceFiles/api/api_user_names.cpp +++ b/Telegram/SourceFiles/api/api_user_names.cpp @@ -24,6 +24,14 @@ namespace { }; } +[[nodiscard]] std::optional BotUserInput( + not_null peer) { + const auto user = peer->asUser(); + return (user && user->botInfo && user->botInfo->canEditInformation) + ? std::make_optional(user->inputUser) + : std::nullopt; +} + } // namespace Usernames::Usernames(not_null api) @@ -157,6 +165,12 @@ rpl::producer Usernames::toggle( MTP_string(username), MTP_bool(active) )).done(done).fail(fail).send(); + } else if (const auto botUserInput = BotUserInput(peer)) { + _api.request(MTPbots_ToggleUsername( + *botUserInput, + MTP_string(username), + MTP_bool(active) + )).done(done).fail(fail).send(); } else { return rpl::never(); } @@ -204,6 +218,12 @@ rpl::producer<> Usernames::reorder( MTP_vector(std::move(tlUsernames)) )).done(finish).fail(finish).send(); _reorderRequests.emplace(peerId, requestId); + } else if (const auto botUserInput = BotUserInput(peer)) { + const auto requestId = _api.request(MTPbots_ReorderUsernames( + *botUserInput, + MTP_vector(std::move(tlUsernames)) + )).done(finish).fail(finish).send(); + _reorderRequests.emplace(peerId, requestId); } return lifetime; }; diff --git a/Telegram/SourceFiles/api/api_user_privacy.cpp b/Telegram/SourceFiles/api/api_user_privacy.cpp index 8c97479c6..edaae7c97 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.cpp +++ b/Telegram/SourceFiles/api/api_user_privacy.cpp @@ -82,6 +82,8 @@ TLInputRules RulesToTL(const UserPrivacy::Rule &rule) { switch (rule.option) { case Option::Everyone: return MTP_inputPrivacyValueAllowAll(); case Option::Contacts: return MTP_inputPrivacyValueAllowContacts(); + case Option::CloseFriends: + return MTP_inputPrivacyValueAllowCloseFriends(); case Option::Nobody: return MTP_inputPrivacyValueDisallowAll(); } Unexpected("Option value in Api::UserPrivacy::RulesToTL."); @@ -110,6 +112,8 @@ UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) { setOption(Option::Everyone); }, [&](const MTPDprivacyValueAllowContacts &) { setOption(Option::Contacts); + }, [&](const MTPDprivacyValueAllowCloseFriends &) { + setOption(Option::CloseFriends); }, [&](const MTPDprivacyValueAllowUsers &data) { const auto &users = data.vusers().v; always.reserve(always.size() + users.size()); @@ -177,18 +181,13 @@ MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) { case Key::Calls: return MTP_inputPrivacyKeyPhoneCall(); case Key::Invites: return MTP_inputPrivacyKeyChatInvite(); case Key::PhoneNumber: return MTP_inputPrivacyKeyPhoneNumber(); - case Key::AddedByPhone: - return MTP_inputPrivacyKeyAddedByPhone(); - case Key::LastSeen: - return MTP_inputPrivacyKeyStatusTimestamp(); - case Key::CallsPeer2Peer: - return MTP_inputPrivacyKeyPhoneP2P(); - case Key::Forwards: - return MTP_inputPrivacyKeyForwards(); - case Key::ProfilePhoto: - return MTP_inputPrivacyKeyProfilePhoto(); - case Key::Voices: - return MTP_inputPrivacyKeyVoiceMessages(); + case Key::AddedByPhone: return MTP_inputPrivacyKeyAddedByPhone(); + case Key::LastSeen: return MTP_inputPrivacyKeyStatusTimestamp(); + case Key::CallsPeer2Peer: return MTP_inputPrivacyKeyPhoneP2P(); + case Key::Forwards: return MTP_inputPrivacyKeyForwards(); + case Key::ProfilePhoto: return MTP_inputPrivacyKeyProfilePhoto(); + case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages(); + case Key::About: return MTP_inputPrivacyKeyAbout(); } Unexpected("Key in Api::UserPrivacy::KetToTL."); } @@ -214,6 +213,8 @@ std::optional TLToKey(mtpTypeId type) { case mtpc_inputPrivacyKeyProfilePhoto: return Key::ProfilePhoto; case mtpc_privacyKeyVoiceMessages: case mtpc_inputPrivacyKeyVoiceMessages: return Key::Voices; + case mtpc_privacyKeyAbout: + case mtpc_inputPrivacyKeyAbout: return Key::About; } return std::nullopt; } diff --git a/Telegram/SourceFiles/api/api_user_privacy.h b/Telegram/SourceFiles/api/api_user_privacy.h index 39d8025b5..be8e452d9 100644 --- a/Telegram/SourceFiles/api/api_user_privacy.h +++ b/Telegram/SourceFiles/api/api_user_privacy.h @@ -29,10 +29,12 @@ public: Forwards, ProfilePhoto, Voices, + About, }; enum class Option { Everyone, Contacts, + CloseFriends, Nobody, }; struct Rule { 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/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 86e0a5881..f32358c06 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/who_reacted_context_action.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace Api { namespace { @@ -357,37 +358,6 @@ struct State { }); } -[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now) { - if (!date) { - return {}; - } - const auto parsed = base::unixtime::parse(date); - const auto readDate = parsed.date(); - const auto nowDate = now.date(); - if (readDate == nowDate) { - return tr::lng_mediaview_today( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } else if (readDate.addDays(1) == nowDate) { - return tr::lng_mediaview_yesterday( - tr::now, - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); - } - return tr::lng_mediaview_date_time( - tr::now, - lt_date, - tr::lng_month_day( - tr::now, - lt_month, - Lang::MonthDay(readDate.month())(tr::now), - lt_day, - QString::number(readDate.day())), - lt_time, - QLocale().toString(parsed.time(), QLocale::ShortFormat)); -} - bool UpdateUserpics( not_null state, not_null item, @@ -614,13 +584,47 @@ rpl::producer WhoReacted( } // namespace +QString FormatReadDate(TimeId date, const QDateTime &now) { + if (!date) { + return {}; + } + const auto parsed = base::unixtime::parse(date); + const auto readDate = parsed.date(); + const auto nowDate = now.date(); + if (readDate == nowDate) { + return tr::lng_mediaview_today( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } else if (readDate.addDays(1) == nowDate) { + return tr::lng_mediaview_yesterday( + tr::now, + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); + } + return tr::lng_mediaview_date_time( + tr::now, + lt_date, + tr::lng_month_day( + tr::now, + lt_month, + Lang::MonthDay(readDate.month())(tr::now), + lt_day, + QString::number(readDate.day())), + lt_time, + QLocale().toString(parsed.time(), QLocale::ShortFormat)); +} + bool WhoReadExists(not_null item) { if (!item->out()) { return false; } const auto type = DetectSeenType(item); + const auto thread = item->topic() + ? (Data::Thread*)item->topic() + : item->history(); const auto unseen = (type == Ui::WhoReadType::Seen) - ? item->unread(item->history()) + ? item->unread(thread) : item->isUnreadMedia(); if (unseen) { return false; @@ -630,7 +634,6 @@ bool WhoReadExists(not_null item) { const auto chat = peer->asChat(); const auto megagroup = peer->asMegagroup(); if ((!chat && !megagroup) - || peer->isForum() || (megagroup && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))) { return false; diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index 3fdcd8a56..9a9100535 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -29,6 +29,7 @@ enum class WhoReactedList { One, }; +[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now); [[nodiscard]] bool WhoReadExists(not_null item); [[nodiscard]] bool WhoReactedExists( not_null item, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 282683741..e71e03953 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" @@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_scheduled_messages.h" #include "data/data_channel_admins.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" @@ -87,7 +89,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/emoji_config.h" #include "ui/chat/attach/attach_prepare.h" -#include "ui/toasts/common_toasts.h" +#include "ui/toast/toast.h" #include "support/support_helper.h" #include "settings/settings_premium.h" #include "storage/localimageloader.h" @@ -121,9 +123,16 @@ using UpdatedFileReferences = Data::UpdatedFileReferences; return TimeId(msgId >> 32); } -[[nodiscard]] std::shared_ptr ShowForPeer( +[[nodiscard]] std::shared_ptr ShowForPeer( not_null peer) { - return std::make_shared(Core::App().windowFor(peer)); + if (const auto window = Core::App().windowFor(peer)) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == &peer->session()) { + return controller->uiShow(); + } + } + } + return nullptr; } void ShowChannelsLimitBox(not_null peer) { @@ -168,7 +177,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. @@ -380,6 +390,16 @@ void ApiWrap::checkChatInvite( )).done(std::move(done)).fail(std::move(fail)).send(); } +void ApiWrap::checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail) { + request(base::take(_checkFilterInviteRequestId)).cancel(); + _checkFilterInviteRequestId = request( + MTPchatlists_CheckChatlistInvite(MTP_string(slug)) + ).done(std::move(done)).fail(std::move(fail)).send(); +} + void ApiWrap::savePinnedOrder(Data::Folder *folder) { const auto &order = _session->data().pinnedChatsOrder(folder); const auto input = [](Dialogs::Key key) { @@ -474,13 +494,12 @@ void ApiWrap::sendMessageFail( uint64 randomId, FullMsgId itemId) { const auto show = ShowForPeer(peer); - - if (error == u"PEER_FLOOD"_q) { + if (show && error == u"PEER_FLOOD"_q) { show->showBox( Ui::MakeInformBox( PeerFloodErrorText(&session(), PeerFloodType::Send)), Ui::LayerOption::CloseOther); - } else if (error == u"USER_BANNED_IN_CHANNEL"_q) { + } else if (show && error == u"USER_BANNED_IN_CHANNEL"_q) { const auto link = Ui::Text::Link( tr::lng_cant_more_info(tr::now), session().createInternalLinkFull(u"spambot"_q)); @@ -509,21 +528,16 @@ void ApiWrap::sendMessageFail( Assert(peer->isUser()); if (const auto item = scheduled.lookupItem(peer->id, itemId.msg)) { scheduled.removeSending(item); - show->showBox( - Ui::MakeInformBox(tr::lng_cant_do_this()), - Ui::LayerOption::CloseOther); - } - } else if (error == u"CHAT_FORWARDS_RESTRICTED"_q) { - if (show->valid()) { - Ui::ShowMultilineToast({ - .parentOverride = show->toastParent(), - .text = { peer->isBroadcast() - ? tr::lng_error_noforwards_channel(tr::now) - : tr::lng_error_noforwards_group(tr::now) - }, - .duration = kJoinErrorDuration - }); + if (show) { + show->showBox( + Ui::MakeInformBox(tr::lng_cant_do_this()), + Ui::LayerOption::CloseOther); + } } + } else if (show && error == u"CHAT_FORWARDS_RESTRICTED"_q) { + show->showToast(peer->isBroadcast() + ? tr::lng_error_noforwards_channel(tr::now) + : tr::lng_error_noforwards_group(tr::now), kJoinErrorDuration); } else if (error == u"PREMIUM_ACCOUNT_REQUIRED"_q) { Settings::ShowPremium(&session(), "premium_stickers"); } @@ -752,9 +766,7 @@ QString ApiWrap::exportDirectMessageLink( channel->inputChannel, MTP_int(item->id) )).done([=](const MTPExportedMessageLink &result) { - const auto link = result.match([&](const auto &data) { - return qs(data.vlink()); - }); + const auto link = qs(result.data().vlink()); if (current != link) { _unlikelyMessageLinks.emplace_or_assign(itemId, link); } @@ -762,6 +774,32 @@ QString ApiWrap::exportDirectMessageLink( return current; } +QString ApiWrap::exportDirectStoryLink(not_null story) { + const auto storyId = story->fullId(); + const auto user = story->peer()->asUser(); + Assert(user != nullptr); + const auto fallback = [&] { + const auto base = user->username(); + const auto story = QString::number(storyId.story); + const auto query = base + "/s/" + story; + return session().createInternalLinkFull(query); + }; + const auto i = _unlikelyStoryLinks.find(storyId); + const auto current = (i != end(_unlikelyStoryLinks)) + ? i->second + : fallback(); + request(MTPstories_ExportStoryLink( + story->peer()->asUser()->inputUser, + MTP_int(story->id()) + )).done([=](const MTPExportedStoryLink &result) { + const auto link = qs(result.data().vlink()); + if (current != link) { + _unlikelyStoryLinks.emplace_or_assign(storyId, link); + } + }).send(); + return current; +} + void ApiWrap::requestContacts() { if (_session->data().contactsLoaded().current() || _contactsRequestId) { return; @@ -1692,12 +1730,8 @@ void ApiWrap::joinChannel(not_null channel) { } return QString(); }(); - if (!text.isEmpty() && show->valid()) { - Ui::ShowMultilineToast({ - .parentOverride = show->toastParent(), - .text = { text }, - .duration = kJoinErrorDuration, - }); + if (!text.isEmpty()) { + show->showToast(text, kJoinErrorDuration); } } _channelAmInRequests.remove(channel); @@ -1797,6 +1831,11 @@ void ApiWrap::requestNotifySettings(const MTPInputNotifyPeer &peer) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound())); _notifySettingRequests.erase(key); }).send(); @@ -2058,8 +2097,8 @@ void ApiWrap::applyAffectedMessages( } void ApiWrap::saveCurrentDraftToCloud() { + Core::App().materializeLocalDrafts(); for (const auto &controller : _session->windows()) { - controller->materializeLocalDrafts(); if (const auto thread = controller->activeChatCurrent().thread()) { const auto topic = thread->asTopic(); if (topic && topic->creating()) { @@ -2502,6 +2541,15 @@ void ApiWrap::refreshFileReference( request(MTPaccount_GetSavedRingtones(MTP_long(0))); }, [&](Data::FileOriginPremiumPreviews data) { request(MTPhelp_GetPremiumPromo()); + }, [&](Data::FileOriginStory data) { + const auto user = _session->data().peer(data.peerId)->asUser(); + if (user) { + request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(1, MTP_int(data.storyId)))); + } else { + fail(); + } }, [&](v::null_t) { fail(); }); @@ -3084,8 +3132,9 @@ void ApiWrap::sharedMediaDone( void ApiWrap::sendAction(const SendAction &action) { if (!action.options.scheduled && !action.replaceMediaOf) { - const auto topic = action.topicRootId - ? action.history->peer->forumTopicFor(action.topicRootId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topic = topicRootId + ? action.history->peer->forumTopicFor(topicRootId) : nullptr; if (topic) { topic->readTillEnd(); @@ -3099,12 +3148,13 @@ void ApiWrap::sendAction(const SendAction &action) { void ApiWrap::finishForwarding(const SendAction &action) { const auto history = action.history; - auto toForward = history->resolveForwardDraft(action.topicRootId); + const auto topicRootId = action.replyTo.topicRootId; + auto toForward = history->resolveForwardDraft(topicRootId); if (!toForward.items.empty()) { const auto error = GetErrorTextForSending( history->peer, { - .topicRootId = action.topicRootId, + .topicRootId = topicRootId, .forward = &toForward.items, }); if (!error.isEmpty()) { @@ -3112,7 +3162,7 @@ void ApiWrap::finishForwarding(const SendAction &action) { } forwardMessages(std::move(toForward), action); - history->setForwardDraft(action.topicRootId, {}); + history->setForwardDraft(topicRootId, {}); } _session->data().sendHistoryChangeNotifications(); @@ -3156,31 +3206,33 @@ void ApiWrap::forwardMessages( const auto silentPost = ShouldSendSilent(peer, action.options); const auto sendAs = action.options.sendAs; + using SendFlag = MTPmessages_ForwardMessages::Flag; auto flags = MessageFlags(); - auto sendFlags = MTPmessages_ForwardMessages::Flags(0); + auto sendFlags = SendFlag() | SendFlag(); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_ForwardMessages::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (draft.options != Data::ForwardOptions::PreserveInfo) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_author; + sendFlags |= SendFlag::f_drop_author; } if (draft.options == Data::ForwardOptions::NoNamesAndCaptions) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_drop_media_captions; + sendFlags |= SendFlag::f_drop_media_captions; } if (sendAs) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_send_as; + sendFlags |= SendFlag::f_send_as; } const auto kGeneralId = Data::ForumTopic::kGeneralId; - const auto topMsgId = (action.topicRootId == kGeneralId) + const auto topicRootId = action.replyTo.topicRootId; + const auto topMsgId = (topicRootId == kGeneralId) ? MsgId(0) - : action.topicRootId; + : topicRootId; if (topMsgId) { - sendFlags |= MTPmessages_ForwardMessages::Flag::f_top_msg_id; + sendFlags |= SendFlag::f_top_msg_id; } auto forwardFrom = draft.items.front()->history()->peer; @@ -3367,7 +3419,7 @@ void ApiWrap::sendSharedContact( void ApiWrap::sendVoiceMessage( QByteArray result, VoiceWaveform waveform, - int duration, + crl::time duration, const SendAction &action) { const auto caption = TextWithTags(); const auto to = fileLoadTaskOptions(action); @@ -3522,14 +3574,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { action.generateLocal = true; sendAction(action); - const auto replyToId = action.replyTo; + const auto replyToId = action.replyTo.msgId; const auto replyTo = replyToId ? peer->owner().message(peer, replyToId) : nullptr; const auto topicRootId = replyTo ? replyTo->topicRootId() - : action.topicRootId - ? action.topicRootId + : action.replyTo.topicRootId + ? action.replyTo.topicRootId : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) @@ -3568,10 +3620,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { auto sendFlags = MTPmessages_SendMessage::Flags(0); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id; - if (action.topicRootId) { - sendFlags |= MTPmessages_SendMessage::Flag::f_top_msg_id; - } + sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to; } const auto replyHeader = NewMessageReplyHeader(action); MTPMessageMedia media = MTP_messageMediaEmpty(); @@ -3598,7 +3647,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_entities; } const auto clearCloudDraft = action.clearDraft; - const auto topicRootId = action.topicRootId; + const auto topicRootId = action.replyTo.topicRootId; if (clearCloudDraft) { sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft; history->clearCloudDraft(topicRootId); @@ -3635,13 +3684,11 @@ void ApiWrap::sendMessage(MessageToSend &&message) { histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), msgText, MTP_long(randomId), MTPReplyMarkup(), @@ -3730,29 +3777,29 @@ void ApiWrap::sendInlineResult( ? (*localMessageId) : _session->data().nextLocalMessageId()); const auto randomId = base::RandomValue(); - const auto topicRootId = action.replyTo ? action.topicRootId : 0; + const auto topicRootId = action.replyTo.msgId + ? action.replyTo.topicRootId + : 0; + using SendFlag = MTPmessages_SendInlineBotResult::Flag; auto flags = NewMessageFlags(peer); - auto sendFlags = MTPmessages_SendInlineBotResult::Flag::f_clear_draft | 0; + auto sendFlags = SendFlag::f_clear_draft | SendFlag(); if (action.replyTo) { flags |= MessageFlag::HasReplyInfo; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_reply_to_msg_id; - if (topicRootId) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_top_msg_id; - } + sendFlags |= SendFlag::f_reply_to; } const auto anonymousPost = peer->amAnonymous(); const auto silentPost = ShouldSendSilent(peer, action.options); FillMessagePostFlags(action, peer, flags); if (silentPost) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_silent; + sendFlags |= SendFlag::f_silent; } if (action.options.scheduled) { flags |= MessageFlag::IsOrWasScheduled; - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_schedule_date; + sendFlags |= SendFlag::f_schedule_date; } if (action.options.hideViaBot) { - sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_hide_via; + sendFlags |= SendFlag::f_hide_via; } const auto sendAs = action.options.sendAs; @@ -3786,13 +3833,11 @@ void ApiWrap::sendInlineResult( histories.sendPreparedMessage( history, action.replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(sendFlags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_long(randomId), MTP_long(data->getQueryId()), MTP_string(data->getId()), @@ -3905,8 +3950,7 @@ void ApiWrap::sendMediaWithRandomId( Api::SendOptions options, uint64 randomId) { const auto history = item->history(); - const auto replyTo = item->replyToId(); - const auto topicRootId = item->topicRootId(); + const auto replyTo = item->replyTo(); auto caption = item->originalText(); TextUtilities::Trim(caption); @@ -3919,8 +3963,7 @@ void ApiWrap::sendMediaWithRandomId( using Flag = MTPmessages_SendMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, options) ? Flag::f_silent : Flag(0)) @@ -3934,13 +3977,11 @@ void ApiWrap::sendMediaWithRandomId( histories.sendPreparedMessage( history, replyTo, - topicRootId, randomId, Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), media, MTP_string(caption.text), MTP_long(randomId), @@ -4021,13 +4062,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { return; } const auto history = sample->history(); - const auto replyTo = sample->replyToId(); - const auto topicRootId = sample->topicRootId(); + const auto replyTo = sample->replyTo(); const auto sendAs = album->options.sendAs; using Flag = MTPmessages_SendMultiMedia::Flag; const auto flags = Flag(0) - | (replyTo ? Flag::f_reply_to_msg_id : Flag(0)) - | (topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (replyTo ? Flag::f_reply_to : Flag(0)) | (ShouldSendSilent(history->peer, album->options) ? Flag::f_silent : Flag(0)) @@ -4038,13 +4077,11 @@ void ApiWrap::sendAlbumIfReady(not_null album) { histories.sendPreparedMessage( history, replyTo, - topicRootId, uint64(0), // randomId Data::Histories::PrepareMessage( MTP_flags(flags), peer->input, Data::Histories::ReplyToPlaceholder(), - Data::Histories::TopicRootPlaceholder(), MTP_vector(medias), MTP_int(album->options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()) @@ -4067,7 +4104,6 @@ FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const { peer->id, action.options, action.replyTo, - action.topicRootId, action.replaceMediaOf); } @@ -4256,3 +4292,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 ec71c781b..954500f76 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -35,6 +35,7 @@ enum class StickersType : uchar; class Forum; class ForumTopic; class Thread; +class Story; } // namespace Data namespace InlineBots { @@ -79,6 +80,7 @@ class Ringtones; class Transcribes; class Premium; class Usernames; +class Websites; namespace details { @@ -160,6 +162,7 @@ public: QString exportDirectMessageLink( not_null item, bool inRepliesContext); + QString exportDirectStoryLink(not_null item); void requestContacts(); void requestDialogs(Data::Folder *folder = nullptr); @@ -202,6 +205,10 @@ public: const QString &hash, FnMut done, Fn fail); + void checkFilterInvite( + const QString &slug, + FnMut done, + Fn fail); void processFullPeer( not_null peer, @@ -296,7 +303,7 @@ public: void sendVoiceMessage( QByteArray result, VoiceWaveform waveform, - int duration, + crl::time duration, const SendAction &action); void sendFiles( Ui::PreparedList &&list, @@ -377,6 +384,7 @@ public: [[nodiscard]] Api::Transcribes &transcribes(); [[nodiscard]] Api::Premium &premium(); [[nodiscard]] Api::Usernames &usernames(); + [[nodiscard]] Api::Websites &websites(); void updatePrivacyLastSeens(); @@ -653,6 +661,7 @@ private: mtpRequestId _termsUpdateRequestId = 0; mtpRequestId _checkInviteRequestId = 0; + mtpRequestId _checkFilterInviteRequestId = 0; struct MigrateCallbacks { FnMut)> done; @@ -686,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; @@ -702,5 +712,6 @@ private: base::flat_map, Fn> _botCommonGroupsRequests; base::flat_map _unlikelyMessageLinks; + base::flat_map _unlikelyStoryLinks; }; diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 52908506c..ccea61d70 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -112,7 +112,7 @@ void ChatCreateDone( if (done) { done(chat); } else { - const auto show = std::make_shared(navigation); + const auto show = navigation->uiShow(); navigation->showPeerHistory(chat); ChatInviteForbidden( show, @@ -141,7 +141,7 @@ void MustBePublicFailed( const auto text = channel->isMegagroup() ? "Can't create a public group :(" : "Can't create a public channel :("; - Ui::Toast::Show(Window::Show(navigation).toastParent(), text); + navigation->showToast(text); MustBePublicDestroy(channel); } @@ -607,7 +607,7 @@ void GroupInfoBox::prepare() { : QString()); (*menu)->addAction( text, - [=, show = std::make_shared(this)] { + [=, show = uiShow()] { show->showBox(Box(TTLMenu::TTLBox, TTLMenu::Args{ .show = show, .startTtl = _ttlPeriod, @@ -727,19 +727,14 @@ void GroupInfoBox::createGroup( } } else if (type == u"USERS_TOO_FEW"_q) { controller->show( - Ui::MakeInformBox(tr::lng_cant_invite_privacy()), - Ui::LayerOption::KeepOther); + Ui::MakeInformBox(tr::lng_cant_invite_privacy())); } else if (type == u"PEER_FLOOD"_q) { - controller->show( - Ui::MakeInformBox( - PeerFloodErrorText( - &_navigation->session(), - PeerFloodType::InviteGroup)), - Ui::LayerOption::KeepOther); + controller->show(Ui::MakeInformBox( + PeerFloodErrorText( + &_navigation->session(), + PeerFloodType::InviteGroup))); } else if (type == u"USER_RESTRICTED"_q) { - controller->show( - Ui::MakeInformBox(tr::lng_cant_do_this()), - Ui::LayerOption::KeepOther); + controller->show(Ui::MakeInformBox(tr::lng_cant_do_this())); } }).send(); } @@ -1220,9 +1215,7 @@ void SetupChannelBox::mousePressEvent(QMouseEvent *e) { return; } else if (!_channel->inviteLink().isEmpty()) { QGuiApplication::clipboard()->setText(_channel->inviteLink()); - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - tr::lng_create_channel_link_copied(tr::now)); + showToast(tr::lng_create_channel_link_copied(tr::now)); } else if (_channel->isFullLoaded() && !_creatingInviteLink) { _creatingInviteLink = true; _channel->session().api().inviteLinks().create(_channel); @@ -1456,12 +1449,10 @@ void SetupChannelBox::showRevokePublicLinkBoxForEdit() { const auto callback = [=] { *revoked = true; navigation->parentController()->show( - Box(navigation, channel, mustBePublic, done), - Ui::LayerOption::KeepOther); + Box(navigation, channel, mustBePublic, done)); }; const auto revoker = navigation->parentController()->show( - Box(PublicLinksLimitBox, navigation, callback), - Ui::LayerOption::KeepOther); + Box(PublicLinksLimitBox, navigation, callback)); const auto session = &navigation->session(); revoker->boxClosing( ) | rpl::start_with_next(crl::guard(session, [=] { diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp index a90f5a6ac..a7b92aa32 100644 --- a/Telegram/SourceFiles/boxes/background_box.cpp +++ b/Telegram/SourceFiles/boxes/background_box.cpp @@ -10,23 +10,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/effects/round_checkbox.h" #include "ui/image/image.h" +#include "ui/chat/attach/attach_extensions.h" #include "ui/chat/chat_theme.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "apiwrap.h" #include "mtproto/sender.h" +#include "core/file_utilities.h" +#include "data/data_peer.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "boxes/background_preview_box.h" +#include "info/profile/info_profile_icon.h" +#include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" +#include "ui/widgets/buttons.h" #include "window/window_session_controller.h" #include "window/themes/window_theme.h" #include "styles/style_overview.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" +#include "styles/style_info.h" namespace { @@ -61,11 +68,15 @@ class BackgroundBox::Inner final : public Ui::RpWidget { public: Inner( QWidget *parent, - not_null session); + not_null session, + PeerData *forPeer); ~Inner(); - rpl::producer chooseEvents() const; - rpl::producer removeRequests() const; + [[nodiscard]] rpl::producer chooseEvents() const; + [[nodiscard]] rpl::producer removeRequests() const; + + [[nodiscard]] auto resolveResetCustomPaper() const + ->std::optional; void removePaper(const Data::WallPaper &data); @@ -109,6 +120,7 @@ private: void resizeToContentAndPreload(); void updatePapers(); void requestPapers(); + void pushCustomPapers(); void sortPapers(); void paintPaper( QPainter &p, @@ -118,9 +130,13 @@ private: void validatePaperThumbnail(const Paper &paper) const; const not_null _session; + PeerData * const _forPeer = nullptr; + MTP::Sender _api; std::vector _papers; + uint64 _currentId = 0; + uint64 _insertedResetId = 0; Selection _over; Selection _overDown; @@ -133,8 +149,10 @@ private: BackgroundBox::BackgroundBox( QWidget*, - not_null controller) -: _controller(controller) { + not_null controller, + PeerData *forPeer) +: _controller(controller) +, _forPeer(forPeer) { } void BackgroundBox::prepare() { @@ -144,15 +162,38 @@ void BackgroundBox::prepare() { setDimensions(st::boxWideWidth, st::boxMaxListHeight); - _inner = setInnerWidget( - object_ptr(this, &_controller->session()), - st::backgroundScroll); + auto wrap = object_ptr(this); + const auto container = wrap.data(); + + Settings::AddSkip(container); + + const auto button = container->add(Settings::CreateButton( + container, + tr::lng_settings_bg_from_file(), + st::infoProfileButton)); + object_ptr( + button, + st::infoIconMediaPhoto, + st::infoSharedMediaButtonIconPosition); + + button->setClickedCallback([=] { + chooseFromFile(); + }); + + Settings::AddSkip(container); + Settings::AddDivider(container); + + _inner = container->add( + object_ptr(this, &_controller->session(), _forPeer)); + + container->resizeToWidth(st::boxWideWidth); + + setInnerWidget(std::move(wrap), st::backgroundScroll); + setInnerTopSkip(st::lineWidth); _inner->chooseEvents( ) | rpl::start_with_next([=](const Data::WallPaper &paper) { - _controller->show( - Box(_controller, paper), - Ui::LayerOption::KeepOther); + chosen(paper); }, _inner->lifetime()); _inner->removeRequests( @@ -161,6 +202,120 @@ void BackgroundBox::prepare() { }, _inner->lifetime()); } +void BackgroundBox::chooseFromFile() { + const auto filterStart = _forPeer + ? u"Image files (*"_q + : u"Theme files (*.tdesktop-theme *.tdesktop-palette *"_q; + auto filters = QStringList( + filterStart + + Ui::ImageExtensions().join(u" *"_q) + + u")"_q); + filters.push_back(FileDialog::AllFilesFilter()); + const auto callback = [=](const FileDialog::OpenResult &result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!_forPeer && !result.paths.isEmpty()) { + const auto filePath = result.paths.front(); + const auto hasExtension = [&](QLatin1String extension) { + return filePath.endsWith(extension, Qt::CaseInsensitive); + }; + if (hasExtension(qstr(".tdesktop-theme")) + || hasExtension(qstr(".tdesktop-palette"))) { + Window::Theme::Apply(filePath); + return; + } + } + + auto image = Images::Read({ + .path = result.paths.isEmpty() ? QString() : result.paths.front(), + .content = result.remoteContent, + .forceOpaque = true, + }).image; + if (image.isNull() || image.width() <= 0 || image.height() <= 0) { + return; + } + auto local = Data::CustomWallPaper(); + local.setLocalImageAsThumbnail(std::make_shared( + std::move(image))); + _controller->show(Box( + _controller, + local, + BackgroundPreviewArgs{ _forPeer })); + }; + FileDialog::GetOpenPath( + this, + tr::lng_choose_image(tr::now), + filters.join(u";;"_q), + crl::guard(this, callback)); +} + +bool BackgroundBox::hasDefaultForPeer() const { + Expects(_forPeer != nullptr); + + const auto paper = _forPeer->wallPaper(); + if (!paper) { + return true; + } + const auto reset = _inner->resolveResetCustomPaper(); + Assert(reset.has_value()); + return (paper->id() == reset->id()); +} + +bool BackgroundBox::chosenDefaultForPeer( + const Data::WallPaper &paper) const { + if (!_forPeer) { + return false; + } + + const auto reset = _inner->resolveResetCustomPaper(); + Assert(reset.has_value()); + return (paper.id() == reset->id()); +} + +void BackgroundBox::chosen(const Data::WallPaper &paper) { + if (chosenDefaultForPeer(paper)) { + if (!hasDefaultForPeer()) { + const auto reset = crl::guard(this, [=](Fn close) { + resetForPeer(); + close(); + }); + _controller->show(Ui::MakeConfirmBox({ + .text = tr::lng_background_sure_reset_default(), + .confirmed = reset, + .confirmText = tr::lng_background_reset_default(), + })); + } else { + closeBox(); + } + return; + } + _controller->show(Box( + _controller, + paper, + BackgroundPreviewArgs{ _forPeer })); +} + +void BackgroundBox::resetForPeer() { + const auto api = &_controller->session().api(); + api->request(MTPmessages_SetChatWallPaper( + MTP_flags(0), + _forPeer->input, + MTPInputWallPaper(), + MTPWallPaperSettings(), + MTPint() + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + + const auto weak = Ui::MakeWeak(this); + _forPeer->setWallPaper(std::nullopt); + if (weak) { + _controller->finishChatThemeEdit(_forPeer); + } +} + void BackgroundBox::removePaper(const Data::WallPaper &paper) { const auto session = &_controller->session(); const auto remove = [=, weak = Ui::MakeWeak(this)](Fn &&close) { @@ -175,28 +330,28 @@ void BackgroundBox::removePaper(const Data::WallPaper &paper) { paper.mtpSettings() )).send(); }; - _controller->show( - Ui::MakeConfirmBox({ - .text = tr::lng_background_sure_delete(), - .confirmed = remove, - .confirmText = tr::lng_selected_delete(), - }), - Ui::LayerOption::KeepOther); + _controller->show(Ui::MakeConfirmBox({ + .text = tr::lng_background_sure_delete(), + .confirmed = remove, + .confirmText = tr::lng_selected_delete(), + })); } BackgroundBox::Inner::Inner( QWidget *parent, - not_null session) + not_null session, + PeerData *forPeer) : RpWidget(parent) , _session(session) +, _forPeer(forPeer) , _api(&_session->mtp()) , _check(std::make_unique(st::overviewCheck, [=] { update(); })) { _check->setChecked(true, anim::type::instant); - if (_session->data().wallpapers().empty()) { - resize(st::boxWideWidth, 2 * (st::backgroundSize.height() + st::backgroundPadding) + st::backgroundPadding); - } else { + resize(st::boxWideWidth, 2 * (st::backgroundSize.height() + st::backgroundPadding) + st::backgroundPadding); + Window::Theme::IsNightModeValue( + ) | rpl::start_with_next([=] { updatePapers(); - } + }, lifetime()); requestPapers(); _session->downloaderTaskFinished( @@ -219,6 +374,7 @@ BackgroundBox::Inner::Inner( } }, lifetime()); + setMouseTracking(true); } @@ -232,35 +388,106 @@ void BackgroundBox::Inner::requestPapers() { }).send(); } +auto BackgroundBox::Inner::resolveResetCustomPaper() const +-> std::optional { + if (!_forPeer) { + return {}; + } + const auto nonCustom = Window::Theme::Background()->paper(); + const auto themeEmoji = _forPeer->themeEmoji(); + if (themeEmoji.isEmpty()) { + return nonCustom; + } + const auto &themes = _forPeer->owner().cloudThemes(); + const auto theme = themes.themeForEmoji(themeEmoji); + if (!theme) { + return nonCustom; + } + using Type = Data::CloudTheme::Type; + const auto dark = Window::Theme::IsNightMode(); + const auto i = theme->settings.find(dark ? Type::Dark : Type::Light); + if (i != end(theme->settings) && i->second.paper) { + return *i->second.paper; + } + return nonCustom; +} + +void BackgroundBox::Inner::pushCustomPapers() { + auto customId = uint64(); + if (const auto custom = _forPeer ? _forPeer->wallPaper() : nullptr) { + customId = custom->id(); + const auto j = ranges::find( + _papers, + custom->id(), + [](const Paper &paper) { return paper.data.id(); }); + if (j != end(_papers)) { + j->data = j->data.withParamsFrom(*custom); + } else { + _papers.insert(begin(_papers), Paper{ *custom }); + } + } + if (const auto reset = resolveResetCustomPaper()) { + _insertedResetId = reset->id(); + const auto j = ranges::find( + _papers, + _insertedResetId, + [](const Paper &paper) { return paper.data.id(); }); + if (j != end(_papers)) { + if (_insertedResetId != customId) { + j->data = j->data.withParamsFrom(*reset); + } + } else { + _papers.insert(begin(_papers), Paper{ *reset }); + } + } +} + void BackgroundBox::Inner::sortPapers() { - const auto current = Window::Theme::Background()->id(); - const auto night = Window::Theme::IsNightMode(); + const auto currentCustom = _forPeer ? _forPeer->wallPaper() : nullptr; + _currentId = currentCustom + ? currentCustom->id() + : _insertedResetId + ? _insertedResetId + : Window::Theme::Background()->id(); + const auto dark = Window::Theme::IsNightMode(); ranges::stable_sort(_papers, std::greater<>(), [&](const Paper &paper) { const auto &data = paper.data; return std::make_tuple( - data.id() == current, - night ? data.isDark() : !data.isDark(), + _insertedResetId && (_insertedResetId == data.id()), + data.id() == _currentId, + dark ? data.isDark() : !data.isDark(), Data::IsDefaultWallPaper(data), !data.isDefault() && !Data::IsLegacy1DefaultWallPaper(data), Data::IsLegacy3DefaultWallPaper(data), Data::IsLegacy2DefaultWallPaper(data), Data::IsLegacy1DefaultWallPaper(data)); }); - if (!_papers.empty() && _papers.front().data.id() == current) { + if (!_papers.empty() + && _papers.front().data.id() == _currentId + && !currentCustom + && !_insertedResetId) { _papers.front().data = _papers.front().data.withParamsFrom( Window::Theme::Background()->paper()); } } void BackgroundBox::Inner::updatePapers() { + if (_session->data().wallpapers().empty()) { + return; + } _over = _overDown = Selection(); _papers = _session->data().wallpapers( - ) | ranges::views::filter([](const Data::WallPaper &paper) { - return !paper.isPattern() || !paper.backgroundColors().empty(); + ) | ranges::views::filter([&](const Data::WallPaper &paper) { + return (!paper.isPattern() || !paper.backgroundColors().empty()) + && (!_forPeer + || (!Data::IsDefaultWallPaper(paper) + && (Data::IsCloudWallPaper(paper) + || Data::IsCustomWallPaper(paper)))); }) | ranges::views::transform([](const Data::WallPaper &paper) { return Paper{ paper }; }) | ranges::to_vector; + pushCustomPapers(); sortPapers(); resizeToContentAndPreload(); } @@ -373,7 +600,7 @@ void BackgroundBox::Inner::paintPaper( } const auto over = !v::is_null(_overDown) ? _overDown : _over; - if (paper.data.id() == Window::Theme::Background()->id()) { + if (paper.data.id() == _currentId) { const auto checkLeft = x + st::backgroundSize.width() - st::overviewCheckSkip - st::overviewCheck.size; const auto checkTop = y + st::backgroundSize.height() - st::overviewCheckSkip - st::overviewCheck.size; _check->paint(p, checkLeft, checkTop, width()); @@ -415,14 +642,13 @@ void BackgroundBox::Inner::mouseMoveEvent(QMouseEvent *e) { - st::stickerPanDeleteIconBg.width(); const auto deleteBottom = row * (height + skip) + skip + st::stickerPanDeleteIconBg.height(); - const auto currentId = Window::Theme::Background()->id(); const auto inDelete = (x >= deleteLeft) && (y < deleteBottom) && Data::IsCloudWallPaper(data) && !Data::IsDefaultWallPaper(data) && !Data::IsLegacy2DefaultWallPaper(data) && !Data::IsLegacy3DefaultWallPaper(data) - && (currentId != data.id()); + && (_currentId != data.id()); return (result >= _papers.size()) ? Selection() : inDelete diff --git a/Telegram/SourceFiles/boxes/background_box.h b/Telegram/SourceFiles/boxes/background_box.h index e3d5994ae..4e0bc3727 100644 --- a/Telegram/SourceFiles/boxes/background_box.h +++ b/Telegram/SourceFiles/boxes/background_box.h @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" +class PeerData; + namespace Window { class SessionController; } // namespace Window @@ -19,7 +21,10 @@ class WallPaper; class BackgroundBox : public Ui::BoxContent { public: - BackgroundBox(QWidget*, not_null controller); + BackgroundBox( + QWidget*, + not_null controller, + PeerData *forPeer = nullptr); protected: void prepare() override; @@ -27,10 +32,18 @@ protected: private: class Inner; + void chosen(const Data::WallPaper &paper); + [[nodiscard]] bool hasDefaultForPeer() const; + [[nodiscard]] bool chosenDefaultForPeer( + const Data::WallPaper &paper) const; void removePaper(const Data::WallPaper &paper); + void resetForPeer(); + + void chooseFromFile(); const not_null _controller; QPointer _inner; + PeerData *_forPeer = nullptr; }; diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 1a2d3fe24..c83af953d 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -17,10 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/image/image.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_item_helpers.h" #include "history/view/history_view_message.h" #include "main/main_session.h" #include "apiwrap.h" @@ -33,10 +36,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/background_preview_box.h" #include "window/window_session_controller.h" +#include "window/themes/window_themes_embedded.h" #include "settings/settings_common.h" +#include "storage/file_upload.h" +#include "storage/localimageloader.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" +#include "styles/style_settings.h" #include #include @@ -44,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kMaxWallPaperSlugLength = 255; +constexpr auto kDefaultDimming = 50; [[nodiscard]] bool IsValidWallPaperSlug(const QString &slug) { if (slug.isEmpty() || slug.size() > kMaxWallPaperSlugLength) { @@ -59,6 +67,24 @@ constexpr auto kMaxWallPaperSlugLength = 255; }); } +[[nodiscard]] AdminLog::OwnedItem GenerateServiceItem( + not_null delegate, + not_null history, + const QString &text, + bool out) { + Expects(history->peer->isUser()); + + const auto flags = MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | (out ? MessageFlag::Outgoing : MessageFlag(0)); + const auto item = history->makeMessage( + history->owner().nextLocalMessageId(), + flags, + base::unixtime::now(), + PreparedServiceText{ { text } }); + return AdminLog::OwnedItem(delegate, item); +} + [[nodiscard]] AdminLog::OwnedItem GenerateTextItem( not_null delegate, not_null history, @@ -69,7 +95,7 @@ constexpr auto kMaxWallPaperSlugLength = 255; const auto flags = MessageFlag::FakeHistoryItem | MessageFlag::HasFromId | (out ? MessageFlag::Outgoing : MessageFlag(0)); - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto groupedId = uint64(); const auto item = history->makeMessage( @@ -133,28 +159,49 @@ constexpr auto kMaxWallPaperSlugLength = 255; } // namespace +struct BackgroundPreviewBox::OverridenStyle { + style::Box box; + style::IconButton toggle; + style::MediaSlider slider; + style::FlatLabel subtitle; +}; + BackgroundPreviewBox::BackgroundPreviewBox( QWidget*, not_null controller, - const Data::WallPaper &paper) + const Data::WallPaper &paper, + BackgroundPreviewArgs args) : SimpleElementDelegate(controller, [=] { update(); }) , _controller(controller) +, _forPeer(args.forPeer) +, _fromMessageId(args.fromMessageId) , _chatStyle(std::make_unique()) +, _serviceHistory(_controller->session().data().history( + PeerData::kServiceNotificationsId)) +, _service(nullptr) , _text1(GenerateTextItem( delegate(), - _controller->session().data().history(PeerData::kServiceNotificationsId), - tr::lng_background_text1(tr::now), + _serviceHistory, + (_forPeer + ? tr::lng_background_apply1(tr::now) + : tr::lng_background_text1(tr::now)), false)) , _text2(GenerateTextItem( delegate(), - _controller->session().data().history(PeerData::kServiceNotificationsId), - tr::lng_background_text2(tr::now), + _serviceHistory, + (_forPeer + ? tr::lng_background_apply2(tr::now) + : tr::lng_background_text2(tr::now)), true)) , _paper(paper) , _media(_paper.document() ? _paper.document()->createMediaView() : nullptr) -, _radial([=](crl::time now) { radialAnimationCallback(now); }) { - _chatStyle->apply(controller->defaultChatTheme().get()); - +, _radial([=](crl::time now) { radialAnimationCallback(now); }) +, _appNightMode(Window::Theme::IsNightModeValue()) +, _boxDarkMode(_appNightMode.current()) +, _dimmingIntensity(std::clamp(paper.patternIntensity(), 0, 100)) +, _dimmed(_forPeer + && (paper.document() || paper.localThumbnail()) + && !paper.isPattern()) { if (_media) { _media->thumbnailWanted(_paper.fileOrigin()); } @@ -163,6 +210,201 @@ BackgroundPreviewBox::BackgroundPreviewBox( ) | rpl::start_with_next([=] { update(); }, lifetime()); + + _appNightMode.changes( + ) | rpl::start_with_next([=](bool night) { + _boxDarkMode = night; + update(); + }, lifetime()); + + _boxDarkMode.changes( + ) | rpl::start_with_next([=](bool dark) { + applyDarkMode(dark); + }, lifetime()); + + const auto prepare = [=](bool dark, auto pointer) { + const auto weak = Ui::MakeWeak(this); + crl::async([=] { + auto result = std::make_unique(); + Window::Theme::PreparePaletteCallback(dark, {})(*result); + crl::on_main([=, result = std::move(result)]() mutable { + if (const auto strong = weak.data()) { + strong->*pointer = std::move(result); + strong->paletteReady(); + } + }); + }); + }; + prepare(false, &BackgroundPreviewBox::_lightPalette); + prepare(true, &BackgroundPreviewBox::_darkPalette); +} + +BackgroundPreviewBox::~BackgroundPreviewBox() = default; + +void BackgroundPreviewBox::applyDarkMode(bool dark) { + const auto equals = (dark == Window::Theme::IsNightMode()); + const auto &palette = (dark ? _darkPalette : _lightPalette); + if (!equals && !palette) { + _waitingForPalette = true; + return; + } + _waitingForPalette = false; + if (equals) { + setStyle(st::defaultBox); + _chatStyle->applyCustomPalette(nullptr); + _paletteServiceBg = rpl::single( + rpl::empty + ) | rpl::then( + style::PaletteChanged() + ) | rpl::map([=] { + return st::msgServiceBg->c; + }); + } else { + setStyle(overridenStyle(dark)); + _chatStyle->applyCustomPalette(palette.get()); + _paletteServiceBg = palette->msgServiceBg()->c; + } + resetTitle(); + rebuildButtons(dark); + update(); + if (const auto parent = parentWidget()) { + parent->update(); + } + + if (_dimmed) { + createDimmingSlider(dark); + } +} + +void BackgroundPreviewBox::createDimmingSlider(bool dark) { + const auto created = !_dimmingWrap; + if (created) { + _dimmingWrap.create(this, object_ptr(this)); + _dimmingContent = _dimmingWrap->entity(); + } + _dimmingSlider = nullptr; + for (const auto &child : _dimmingContent->children()) { + if (child->isWidgetType()) { + static_cast(child)->hide(); + child->deleteLater(); + } + } + const auto equals = (dark == Window::Theme::IsNightMode()); + const auto inner = Ui::CreateChild(_dimmingContent); + inner->show(); + Settings::AddSubsectionTitle( + inner, + tr::lng_background_dimming(), + style::margins(0, st::settingsSectionSkip, 0, 0), + equals ? nullptr : dark ? &_dark->subtitle : &_light->subtitle); + _dimmingSlider = inner->add( + object_ptr( + inner, + (equals + ? st::defaultContinuousSlider + : dark + ? _dark->slider + : _light->slider)), + st::localStorageLimitMargin); + _dimmingSlider->setValue(_dimmingIntensity / 100.); + _dimmingSlider->setAlwaysDisplayMarker(true); + _dimmingSlider->resize(st::defaultContinuousSlider.seekSize); + const auto handle = [=](float64 value) { + const auto intensity = std::clamp( + int(base::SafeRound(value * 100)), + 0, + 100); + _paper = _paper.withPatternIntensity(intensity); + _dimmingIntensity = intensity; + update(); + }; + _dimmingSlider->setChangeProgressCallback(handle); + _dimmingSlider->setChangeFinishedCallback(handle); + inner->resizeToWidth(st::boxWideWidth); + Ui::SendPendingMoveResizeEvents(inner); + inner->move(0, 0); + _dimmingContent->resize(inner->size()); + + _dimmingContent->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(_dimmingContent); + const auto palette = (dark ? _darkPalette : _lightPalette).get(); + p.fillRect(clip, equals ? st::boxBg : palette->boxBg()); + }, _dimmingContent->lifetime()); + + _dimmingToggleScheduled = true; + + if (created) { + rpl::combine( + heightValue(), + _dimmingWrap->heightValue(), + rpl::mappers::_1 - rpl::mappers::_2 + ) | rpl::start_with_next([=](int top) { + _dimmingWrap->move(0, top); + }, _dimmingWrap->lifetime()); + + _dimmingWrap->toggle(dark, anim::type::instant); + _dimmingHeight = _dimmingWrap->heightValue(); + _dimmingHeight.changes() | rpl::start_with_next([=] { + update(); + }, _dimmingWrap->lifetime()); + } +} + +void BackgroundPreviewBox::paletteReady() { + if (_waitingForPalette) { + applyDarkMode(_boxDarkMode.current()); + } +} + +const style::Box &BackgroundPreviewBox::overridenStyle(bool dark) { + auto &st = dark ? _dark : _light; + if (!st) { + st = std::make_unique(prepareOverridenStyle(dark)); + } + return st->box; +} + +auto BackgroundPreviewBox::prepareOverridenStyle(bool dark) +-> OverridenStyle { + const auto p = (dark ? _darkPalette : _lightPalette).get(); + Assert(p != nullptr); + + const auto &toggle = dark + ? st::backgroundSwitchToLight + : st::backgroundSwitchToDark; + auto result = OverridenStyle{ + .box = st::defaultBox, + .toggle = toggle, + .slider = st::defaultContinuousSlider, + .subtitle = st::settingsSubsectionTitle, + }; + result.box.button.textFg = p->lightButtonFg(); + result.box.button.textFgOver = p->lightButtonFgOver(); + result.box.button.numbersTextFg = p->lightButtonFg(); + result.box.button.numbersTextFgOver = p->lightButtonFgOver(); + result.box.button.textBg = p->lightButtonBg(); + result.box.button.textBgOver = p->lightButtonBgOver(); + result.box.button.ripple.color = p->lightButtonBgRipple(); + result.box.title.textFg = p->boxTitleFg(); + result.box.bg = p->boxBg(); + result.box.titleAdditionalFg = p->boxTitleAdditionalFg(); + + result.toggle.ripple.color = p->windowBgOver(); + result.toggle.icon = toggle.icon.withPalette(*p); + result.toggle.iconOver = toggle.iconOver.withPalette(*p); + + result.slider.activeFg = p->mediaPlayerActiveFg(); + result.slider.inactiveFg = p->mediaPlayerInactiveFg(); + result.slider.activeFgOver = p->mediaPlayerActiveFg(); + result.slider.inactiveFgOver = p->mediaPlayerInactiveFg(); + result.slider.activeFgDisabled = p->mediaPlayerInactiveFg(); + result.slider.inactiveFgDisabled = p->windowBg(); + result.slider.receivedTillFg = p->mediaPlayerInactiveFg(); + + result.subtitle.textFg = p->windowActiveTextFg(); + + return result; } void BackgroundPreviewBox::generateBackground() { @@ -184,30 +426,45 @@ not_null BackgroundPreviewBox::delegate() { return static_cast(this); } -void BackgroundPreviewBox::prepare() { +void BackgroundPreviewBox::resetTitle() { setTitle(tr::lng_background_header()); +} - addButton(tr::lng_background_apply(), [=] { apply(); }); +void BackgroundPreviewBox::rebuildButtons(bool dark) { + clearButtons(); + addButton(_forPeer + ? tr::lng_background_apply_button() + : tr::lng_background_apply(), [=] { apply(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); - if (_paper.hasShareUrl()) { + if (!_forPeer && _paper.hasShareUrl()) { addLeftButton(tr::lng_background_share(), [=] { share(); }); } - updateServiceBg(_paper.backgroundColors()); + const auto equals = (dark == Window::Theme::IsNightMode()); + auto toggle = object_ptr(this, equals + ? (dark ? st::backgroundSwitchToLight : st::backgroundSwitchToDark) + : dark ? _dark->toggle : _light->toggle); + toggle->setClickedCallback([=] { + _boxDarkMode = !_boxDarkMode.current(); + }); + addTopButton(std::move(toggle)); +} + +void BackgroundPreviewBox::prepare() { + applyDarkMode(Window::Theme::IsNightMode()); _paper.loadDocument(); - const auto document = _paper.document(); - if (document && document->loading()) { - _radial.start(_media->progress()); - } - if (!_paper.isPattern() - && (_paper.localThumbnail() - || (document && document->hasThumbnail()))) { - createBlurCheckbox(); + if (const auto document = _paper.document()) { + if (document->loading()) { + _radial.start(_media->progress()); + } } + + updateServiceBg(_paper.backgroundColors()); + setScaledFromThumb(); checkLoadedDocument(); - _text1->setDisplayDate(true); + _text1->setDisplayDate(false); _text1->initDimensions(); _text1->resizeGetHeight(st::boxWideWidth); _text2->initDimensions(); @@ -216,34 +473,158 @@ void BackgroundPreviewBox::prepare() { setDimensions(st::boxWideWidth, st::boxWideWidth); } -void BackgroundPreviewBox::createBlurCheckbox() { +void BackgroundPreviewBox::recreateBlurCheckbox() { + const auto document = _paper.document(); + if (_paper.isPattern() + || (!_paper.localThumbnail() + && (!document || !document->hasThumbnail()))) { + return; + } + + const auto blurred = _blur ? _blur->checked() : _paper.isBlurred(); _blur = Ui::MakeChatServiceCheckbox( this, tr::lng_background_blur(tr::now), st::backgroundCheckbox, st::backgroundCheck, - _paper.isBlurred(), + blurred, [=] { return _serviceBg.value_or(QColor(255, 255, 255, 0)); }); + _blur->show(); rpl::combine( sizeValue(), - _blur->sizeValue() - ) | rpl::start_with_next([=](QSize outer, QSize inner) { + _blur->sizeValue(), + _dimmingHeight.value() + ) | rpl::start_with_next([=](QSize outer, QSize inner, int dimming) { + const auto bottom = st::historyPaddingBottom; _blur->move( (outer.width() - inner.width()) / 2, - outer.height() - st::historyPaddingBottom - inner.height()); + outer.height() - dimming - bottom - inner.height()); }, _blur->lifetime()); _blur->checkedChanges( ) | rpl::start_with_next([=](bool checked) { checkBlurAnimationStart(); update(); - }, lifetime()); + }, _blur->lifetime()); - _blur->setDisabled(true); + _blur->setDisabled(_paper.document() && _full.isNull()); } void BackgroundPreviewBox::apply() { + if (_forPeer) { + applyForPeer(); + } else { + applyForEveryone(); + } +} + +void BackgroundPreviewBox::uploadForPeer() { + Expects(_forPeer != nullptr); + + if (_uploadId) { + return; + } + + const auto session = &_controller->session(); + const auto ready = Window::Theme::PrepareWallPaper( + session->mainDcId(), + _paper.localThumbnail()->original()); + const auto documentId = ready.id; + _uploadId = FullMsgId( + session->userPeerId(), + session->data().nextLocalMessageId()); + session->uploader().uploadMedia(_uploadId, ready); + if (_uploadLifetime) { + return; + } + + const auto document = session->data().document(documentId); + document->uploadingData = std::make_unique( + document->size); + + session->uploader().documentProgress( + ) | rpl::start_with_next([=](const FullMsgId &fullId) { + if (fullId != _uploadId) { + return; + } + _uploadProgress = document->uploading() + ? ((document->uploadingData->offset * 100) + / document->uploadingData->size) + : 0.; + update(radialRect()); + }, _uploadLifetime); + + session->uploader().documentReady( + ) | rpl::start_with_next([=](const Storage::UploadedMedia &data) { + if (data.fullId != _uploadId) { + return; + } + _uploadProgress = 1.; + _uploadLifetime.destroy(); + update(radialRect()); + session->api().request(MTPaccount_UploadWallPaper( + MTP_flags(MTPaccount_UploadWallPaper::Flag::f_for_chat), + data.info.file, + MTP_string("image/jpeg"), + _paper.mtpSettings() + )).done([=](const MTPWallPaper &result) { + result.match([&](const MTPDwallPaper &data) { + session->data().documentConvert( + session->data().document(documentId), + data.vdocument()); + }, [&](const MTPDwallPaperNoFile &data) { + LOG(("API Error: " + "Got wallPaperNoFile after account.UploadWallPaper.")); + }); + if (const auto paper = Data::WallPaper::Create(session, result)) { + setExistingForPeer(*paper); + } + }).send(); + }, _uploadLifetime); + + _uploadProgress = 0.; + _radial.start(_uploadProgress); +} + +void BackgroundPreviewBox::setExistingForPeer(const Data::WallPaper &paper) { + Expects(_forPeer != nullptr); + + if (const auto already = _forPeer->wallPaper()) { + if (already->equals(paper)) { + _controller->finishChatThemeEdit(_forPeer); + return; + } + } + const auto api = &_controller->session().api(); + using Flag = MTPmessages_SetChatWallPaper::Flag; + api->request(MTPmessages_SetChatWallPaper( + MTP_flags((_fromMessageId ? Flag::f_id : Flag()) + | (_fromMessageId ? Flag() : Flag::f_wallpaper) + | Flag::f_settings), + _forPeer->input, + paper.mtpInput(&_controller->session()), + paper.mtpSettings(), + MTP_int(_fromMessageId.msg) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + + _forPeer->setWallPaper(paper); + _controller->finishChatThemeEdit(_forPeer); +} + +void BackgroundPreviewBox::applyForPeer() { + Expects(_forPeer != nullptr); + + if (Data::IsCustomWallPaper(_paper)) { + uploadForPeer(); + } else { + setExistingForPeer(_paper); + } +} + +void BackgroundPreviewBox::applyForEveryone() { const auto install = (_paper.id() != Window::Theme::Background()->id()) && Data::IsCloudWallPaper(_paper); _controller->content()->setChatBackground(_paper, std::move(_full)); @@ -259,9 +640,7 @@ void BackgroundPreviewBox::apply() { void BackgroundPreviewBox::share() { QGuiApplication::clipboard()->setText( _paper.shareUrl(&_controller->session())); - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - tr::lng_background_link_copied(tr::now)); + showToast(tr::lng_background_link_copied(tr::now)); } void BackgroundPreviewBox::paintEvent(QPaintEvent *e) { @@ -278,6 +657,13 @@ void BackgroundPreviewBox::paintEvent(QPaintEvent *e) { } if (!_scaled.isNull()) { paintImage(p); + const auto dimming = (_dimmed && _boxDarkMode.current()) + ? _dimmingIntensity + : 0; + if (dimming > 0) { + const auto alpha = 255 * dimming / 100; + p.fillRect(e->rect(), QColor(0, 0, 0, alpha)); + } paintRadial(p); } else if (_generated.isNull()) { p.fillRect(e->rect(), st::boxBg); @@ -287,6 +673,15 @@ void BackgroundPreviewBox::paintEvent(QPaintEvent *e) { paintRadial(p); } paintTexts(p, ms); + if (_dimmingToggleScheduled) { + crl::on_main(this, [=] { + if (!_dimmingToggleScheduled) { + return; + } + _dimmingToggleScheduled = false; + _dimmingWrap->toggle(_boxDarkMode.current(), anim::type::normal); + }); + } } void BackgroundPreviewBox::paintImage(Painter &p) { @@ -336,9 +731,12 @@ void BackgroundPreviewBox::paintRadial(Painter &p) { } int BackgroundPreviewBox::textsTop() const { - const auto bottom = _blur ? _blur->y() : height(); + const auto bottom = _blur + ? _blur->y() + : (height() - _dimmingHeight.current()); return bottom - st::historyPaddingBottom + - (_service ? _service->height() : 0) - _text1->height() - _text2->height(); } @@ -353,6 +751,7 @@ QRect BackgroundPreviewBox::radialRect() const { } void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) { + const auto heights = _service ? _service->height() : 0; const auto height1 = _text1->height(); const auto height2 = _text2->height(); auto context = _controller->defaultChatTheme()->preparePaintContext( @@ -361,7 +760,10 @@ void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) { rect(), _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer)); p.translate(0, textsTop()); - paintDate(p); + if (_service) { + _service->draw(p, context); + p.translate(0, heights); + } context.outbg = _text1->hasOutLayout(); _text1->draw(p, context); @@ -372,36 +774,12 @@ void BackgroundPreviewBox::paintTexts(Painter &p, crl::time ms) { p.translate(0, height2); } -void BackgroundPreviewBox::paintDate(Painter &p) { - const auto date = _text1->Get(); - if (!date || !_serviceBg) { - return; - } - auto hq = PainterHighQualityEnabler(p); - const auto text = date->text; - const auto bubbleHeight = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); - const auto bubbleTop = st::msgServiceMargin.top(); - const auto textWidth = st::msgServiceFont->width(text); - const auto bubbleWidth = st::msgServicePadding.left() + textWidth + st::msgServicePadding.right(); - const auto bubbleLeft = (width() - bubbleWidth) / 2; - const auto radius = bubbleHeight / 2; - p.setPen(Qt::NoPen); - p.setBrush(*_serviceBg); - p.drawRoundedRect(bubbleLeft, bubbleTop, bubbleWidth, bubbleHeight, radius, radius); - p.setPen(st::msgServiceFg); - p.setFont(st::msgServiceFont); - p.drawText(bubbleLeft + st::msgServicePadding.left(), bubbleTop + st::msgServicePadding.top() + st::msgServiceFont->ascent, text); -} - void BackgroundPreviewBox::radialAnimationCallback(crl::time now) { - Expects(_paper.document() != nullptr); - const auto document = _paper.document(); const auto wasAnimating = _radial.animating(); - const auto updated = _radial.update( - _media->progress(), - !document->loading(), - now); + const auto updated = _uploadId + ? _radial.update(_uploadProgress, !_uploadLifetime, now) + : _radial.update(_media->progress(), !document->loading(), now); if ((wasAnimating || _radial.animating()) && (!anim::Disabled() || updated)) { update(radialRect()); @@ -448,8 +826,8 @@ void BackgroundPreviewBox::setScaledFromImage( } _scaled = Ui::PixmapFromImage(std::move(image)); _blurred = Ui::PixmapFromImage(std::move(blurred)); - if (_blur && (!_paper.document() || !_full.isNull())) { - _blur->setDisabled(false); + if (_blur) { + _blur->setDisabled(_paper.document() && _full.isNull()); } } @@ -474,15 +852,34 @@ void BackgroundPreviewBox::updateServiceBg(const std::vector &bg) { if (!count) { return; } - auto red = 0, green = 0, blue = 0; + auto red = 0LL, green = 0LL, blue = 0LL; for (const auto &color : bg) { red += color.red(); green += color.green(); blue += color.blue(); } - _serviceBg = Ui::ThemeAdjustedColor( - st::msgServiceBg->c, - QColor(red / count, green / count, blue / count)); + + _serviceBgLifetime = _paletteServiceBg.value( + ) | rpl::start_with_next([=](QColor color) { + _serviceBg = Ui::ThemeAdjustedColor( + color, + QColor(red / count, green / count, blue / count)); + _chatStyle->applyAdjustedServiceBg(*_serviceBg); + recreateBlurCheckbox(); + }); + + _service = GenerateServiceItem( + delegate(), + _serviceHistory, + ((_forPeer && !_fromMessageId) + ? tr::lng_background_other_info( + tr::now, + lt_user, + _forPeer->shortName()) + : ItemDateText(_text1->data(), false)), + false); + _service->initDimensions(); + _service->resizeGetHeight(st::boxWideWidth); } void BackgroundPreviewBox::checkLoadedDocument() { diff --git a/Telegram/SourceFiles/boxes/background_preview_box.h b/Telegram/SourceFiles/boxes/background_preview_box.h index 24554cd84..dc44f9509 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.h +++ b/Telegram/SourceFiles/boxes/background_preview_box.h @@ -26,8 +26,16 @@ class SessionController; namespace Ui { class Checkbox; class ChatStyle; +class MediaSlider; +template +class SlideWrap; } // namespace Ui +struct BackgroundPreviewArgs { + PeerData *forPeer = nullptr; + FullMsgId fromMessageId; +}; + class BackgroundPreviewBox : public Ui::BoxContent , private HistoryView::SimpleElementDelegate { @@ -35,7 +43,9 @@ public: BackgroundPreviewBox( QWidget*, not_null controller, - const Data::WallPaper &paper); + const Data::WallPaper &paper, + BackgroundPreviewArgs args = {}); + ~BackgroundPreviewBox(); static bool Start( not_null controller, @@ -48,11 +58,17 @@ protected: void paintEvent(QPaintEvent *e) override; private: + struct OverridenStyle; + using Element = HistoryView::Element; not_null delegate(); HistoryView::Context elementContext() override; void apply(); + void applyForPeer(); + void applyForEveryone(); + void uploadForPeer(); + void setExistingForPeer(const Data::WallPaper &paper); void share(); void radialAnimationCallback(crl::time now); QRect radialRect() const; @@ -65,14 +81,26 @@ private: void paintImage(Painter &p); void paintRadial(Painter &p); void paintTexts(Painter &p, crl::time ms); - void paintDate(Painter &p); - void createBlurCheckbox(); + void recreateBlurCheckbox(); int textsTop() const; void startFadeInFrom(QPixmap previous); void checkBlurAnimationStart(); + [[nodiscard]] const style::Box &overridenStyle(bool dark); + void paletteReady(); + void applyDarkMode(bool dark); + [[nodiscard]] OverridenStyle prepareOverridenStyle(bool dark); + + void resetTitle(); + void rebuildButtons(bool dark); + void createDimmingSlider(bool dark); + const not_null _controller; + PeerData * const _forPeer = nullptr; + FullMsgId _fromMessageId; std::unique_ptr _chatStyle; + const not_null _serviceHistory; + AdminLog::OwnedItem _service; AdminLog::OwnedItem _text1; AdminLog::OwnedItem _text2; Data::WallPaper _paper; @@ -85,4 +113,25 @@ private: std::optional _serviceBg; object_ptr _blur = { nullptr }; + rpl::variable _appNightMode; + rpl::variable _boxDarkMode; + std::unique_ptr _light, _dark; + std::unique_ptr _lightPalette, _darkPalette; + bool _waitingForPalette = false; + + object_ptr> _dimmingWrap = { nullptr }; + Ui::RpWidget *_dimmingContent = nullptr; + Ui::MediaSlider *_dimmingSlider = nullptr; + int _dimmingIntensity = 0; + rpl::variable _dimmingHeight = 0; + bool _dimmed = false; + bool _dimmingToggleScheduled = false; + + FullMsgId _uploadId; + float64 _uploadProgress = 0.; + rpl::lifetime _uploadLifetime; + + rpl::variable _paletteServiceBg; + rpl::lifetime _serviceBgLifetime; + }; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 54f915a45..b952e5a6e 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -24,6 +24,11 @@ UserpicButton { uploadIcon: icon; uploadIconPosition: point; } +ShortInfoBox { + label: FlatLabel; + labeled: FlatLabel; + labeledOneLine: FlatLabel; +} countryRowHeight: 36px; countryRowNameFont: semiboldFont; @@ -48,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; @@ -344,11 +349,6 @@ autoDownloadLimitSlider: MediaSlider(defaultContinuousSlider) { } autoDownloadLimitPadding: margins(22px, 8px, 22px, 8px); -confirmCaptionArea: InputField(defaultInputField) { - textMargins: margins(1px, 26px, 31px, 4px); - heightMax: 158px; -} -confirmBg: windowBgOver; confirmMaxHeight: 245px; supportInfoField: InputField(defaultInputField) { @@ -391,51 +391,11 @@ sendMediaPreviewSize: 308px; sendMediaPreviewHeightMax: 1280; sendMediaRowSkip: 10px; -editMediaButtonSize: 32px; - -editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; -editMediaButton: IconButton(defaultIconButton) { - width: editMediaButtonSize; - height: editMediaButtonSize; - - icon: editMediaButtonIconFile; - - rippleAreaSize: editMediaButtonSize; - ripple: defaultRippleAnimation; -} - editMediaHintLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; minWidth: sendMediaPreviewSize; } -// SendFilesBox - -sendBoxAlbumGroupEditInternalSkip: 8px; -sendBoxAlbumGroupSkipRight: 5px; -sendBoxAlbumGroupSkipTop: 5px; -sendBoxAlbumGroupRadius: 4px; -sendBoxAlbumGroupSize: size(62px, 25px); -sendBoxAlbumSmallGroupSize: size(30px, 25px); - -sendBoxFileGroupSkipTop: 2px; -sendBoxFileGroupSkipRight: 5px; -sendBoxFileGroupEditInternalSkip: -1px; - -sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgRipple; - } -} -sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; -sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; - -sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; -sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; -sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; - -// End of SendFilesBox - calendarTitleHeight: boxTitleHeight; calendarPrevious: IconButton { width: calendarTitleHeight; @@ -647,7 +607,7 @@ groupStickersField: InputField(defaultMultiSelectSearchField) { textBg: boxBg; heightMin: 32px; } -groupStickersSubTitleHeight: 36px; +groupStickersSubTitleHeight: 48px; proxyUsePadding: margins(22px, 6px, 22px, 5px); proxyTryIPv6Padding: margins(22px, 8px, 22px, 5px); @@ -982,6 +942,27 @@ requestsBoxList: PeerList(peerListBox) { padding: margins(0px, 12px, 0px, 12px); item: requestsBoxItem; } +contactsWithStories: PeerList(peerListBox) { + padding: margins(0px, 0px, 0px, 0px); + item: PeerListItem(peerListBoxItem) { + height: 52px; + photoPosition: point(18px, 5px); + namePosition: point(70px, 7px); + statusPosition: point(70px, 27px); + + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; + } +} +storiesReadLineTwice: 2px; +storiesUnreadLineTwice: 4px; requestsAcceptButton: RoundButton(defaultActiveButton) { width: -28px; height: 30px; @@ -1006,3 +987,26 @@ ringtonesBoxSkip: 7px; gradientButtonGlareDuration: 700; gradientButtonGlareTimeout: 2000; gradientButtonGlareWidth: 100px; + +infoLabeledOneLine: FlatLabel(defaultFlatLabel) { + maxHeight: 20px; + style: TextStyle(defaultTextStyle) { + lineHeight: 19px; + } + margin: margins(5px, 5px, 5px, 5px); +} +infoLabelSkip: 2px; +infoLabeled: FlatLabel(infoLabeledOneLine) { + minWidth: 180px; + maxHeight: 0px; + margin: margins(5px, 5px, 5px, 5px); +} +infoLabel: FlatLabel(infoLabeled) { + textFg: windowSubTextFg; +} + +shortInfoBox: ShortInfoBox { + label: infoLabel; + labeled: infoLabeled; + labeledOneLine: infoLabeledOneLine; +} diff --git a/Telegram/SourceFiles/boxes/change_phone_box.cpp b/Telegram/SourceFiles/boxes/change_phone_box.cpp deleted file mode 100644 index bddf57c71..000000000 --- a/Telegram/SourceFiles/boxes/change_phone_box.cpp +++ /dev/null @@ -1,575 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#include "boxes/change_phone_box.h" - -#include "core/file_utilities.h" -#include "lang/lang_keys.h" -#include "ui/widgets/labels.h" -#include "ui/widgets/sent_code_field.h" -#include "ui/widgets/buttons.h" -#include "ui/wrap/fade_wrap.h" -#include "ui/toast/toast.h" -#include "ui/text/format_values.h" // Ui::FormatPhone -#include "ui/text/text_utilities.h" -#include "ui/widgets/fields/special_fields.h" -#include "ui/boxes/confirm_box.h" -#include "boxes/phone_banned_box.h" -#include "countries/countries_instance.h" // Countries::ExtractPhoneCode. -#include "main/main_account.h" -#include "main/main_session.h" -#include "data/data_session.h" -#include "data/data_user.h" -#include "info/profile/info_profile_values.h" -#include "lottie/lottie_icon.h" -#include "mtproto/sender.h" -#include "apiwrap.h" -#include "window/window_session_controller.h" -#include "styles/style_layers.h" -#include "styles/style_boxes.h" - -namespace { - -void CreateErrorLabel( - QWidget *parent, - object_ptr> &label, - const QString &text, - int x, - int y) { - if (label) { - label->hide(anim::type::normal); - - auto saved = label.data(); - auto destroy = [old = std::move(label)]() mutable { - old.destroyDelayed(); - }; - - using namespace rpl::mappers; - saved->shownValue() - | rpl::filter(_1 == false) - | rpl::take(1) - | rpl::start_with_done( - std::move(destroy), - saved->lifetime()); - } - if (!text.isEmpty()) { - label.create( - parent, - object_ptr( - parent, - text, - st::changePhoneError)); - label->hide(anim::type::instant); - label->moveToLeft(x, y); - label->show(anim::type::normal); - } -} - -[[nodiscard]] int ErrorSkip() { - return st::boxLittleSkip + st::changePhoneError.style.font->height; -} - -} // namespace - -namespace Settings { - -class ChangePhone::EnterPhone : public Ui::BoxContent { -public: - EnterPhone(QWidget*, not_null controller); - - void setInnerFocus() override { - _phone->setFocusFast(); - } - -protected: - void prepare() override; - -private: - void submit(); - void sendPhoneDone( - const MTPauth_SentCode &result, - const QString &phoneNumber); - void sendPhoneFail(const MTP::Error &error, const QString &phoneNumber); - void showError(const QString &text); - void hideError() { - showError(QString()); - } - - const not_null _controller; - MTP::Sender _api; - - object_ptr _phone = { nullptr }; - object_ptr> _error = { nullptr }; - mtpRequestId _requestId = 0; - -}; - -class ChangePhone::EnterCode : public Ui::BoxContent { -public: - EnterCode( - QWidget*, - not_null controller, - const QString &phone, - const QString &hash, - const QString &openUrl, - int codeLength, - int callTimeout); - - void setInnerFocus() override { - _code->setFocusFast(); - } - -protected: - void prepare() override; - -private: - void submit(const QString &code); - void sendCall(); - void updateCall(); - void sendCodeFail(const MTP::Error &error); - void showError(const QString &text); - void hideError() { - showError(QString()); - } - [[nodiscard]] int countHeight() const; - - const not_null _controller; - MTP::Sender _api; - - QString _phone; - QString _hash; - QString _openUrl; - int _codeLength = 0; - int _callTimeout = 0; - object_ptr _code = { nullptr }; - object_ptr> _error = { nullptr }; - object_ptr _callLabel = { nullptr }; - object_ptr _fragment = { nullptr }; - mtpRequestId _requestId = 0; - Ui::SentCodeCall _call; - -}; - -ChangePhone::EnterPhone::EnterPhone( - QWidget*, - not_null controller) -: _controller(controller) -, _api(&controller->session().mtp()) { -} - -void ChangePhone::EnterPhone::prepare() { - setTitle(tr::lng_change_phone_title()); - - const auto phoneValue = QString(); - _phone.create( - this, - st::defaultInputField, - tr::lng_change_phone_new_title(), - Countries::ExtractPhoneCode(_controller->session().user()->phone()), - phoneValue, - [](const QString &s) { return Countries::Groups(s); }); - - _phone->resize( - st::boxWidth - 2 * st::boxPadding.left(), - _phone->height()); - _phone->moveToLeft(st::boxPadding.left(), st::boxLittleSkip); - connect(_phone, &Ui::PhoneInput::submitted, [=] { submit(); }); - - const auto description = object_ptr( - this, - tr::lng_change_phone_new_description(tr::now), - st::changePhoneLabel); - description->moveToLeft( - st::boxPadding.left(), - _phone->y() + _phone->height() + ErrorSkip() + st::boxLittleSkip); - - setDimensions( - st::boxWidth, - description->bottomNoMargins() + st::boxLittleSkip); - - addButton(tr::lng_change_phone_new_submit(), [this] { submit(); }); - addButton(tr::lng_cancel(), [this] { closeBox(); }); -} - -void ChangePhone::EnterPhone::submit() { - if (_requestId) { - return; - } - hideError(); - - const auto phoneNumber = _phone->getLastText().trimmed(); - _requestId = _api.request(MTPaccount_SendChangePhoneCode( - MTP_string(phoneNumber), - MTP_codeSettings( - MTP_flags(0), - MTPVector(), - MTPstring(), - MTPBool()) - )).done([=](const MTPauth_SentCode &result) { - _requestId = 0; - sendPhoneDone(result, phoneNumber); - }).fail([=](const MTP::Error &error) { - _requestId = 0; - sendPhoneFail(error, phoneNumber); - }).handleFloodErrors().send(); -} - -void ChangePhone::EnterPhone::sendPhoneDone( - const MTPauth_SentCode &result, - const QString &phoneNumber) { - const auto data = result.match([](const MTPDauth_sentCode &data) { - return &data; - }, [](const MTPDauth_sentCodeSuccess &) -> const MTPDauth_sentCode* { - LOG(("API Error: Unexpected auth.sentCodeSuccess " - "(ChangePhone::EnterPhone).")); - return nullptr; - }); - if (!data) { - showError(Lang::Hard::ServerError()); - return; - } - - const auto bad = [&](const char *type) { - LOG(("API Error: Should not be '%1'.").arg(type)); - showError(Lang::Hard::ServerError()); - return false; - }; - auto codeLength = 0; - auto codeByFragmentUrl = QString(); - const auto hasLength = data->vtype().match([&]( - const MTPDauth_sentCodeTypeApp &typeData) { - LOG(("Error: should not be in-app code!")); - showError(Lang::Hard::ServerError()); - return false; - }, [&](const MTPDauth_sentCodeTypeSms &typeData) { - codeLength = typeData.vlength().v; - return true; - }, [&](const MTPDauth_sentCodeTypeFragmentSms &typeData) { - codeLength = typeData.vlength().v; - codeByFragmentUrl = qs(typeData.vurl()); - return true; - }, [&](const MTPDauth_sentCodeTypeCall &typeData) { - codeLength = typeData.vlength().v; - return true; - }, [&](const MTPDauth_sentCodeTypeFlashCall &) { - return bad("FlashCall"); - }, [&](const MTPDauth_sentCodeTypeMissedCall &) { - return bad("MissedCall"); - }, [&](const MTPDauth_sentCodeTypeFirebaseSms &) { - return bad("FirebaseSms"); - }, [&](const MTPDauth_sentCodeTypeEmailCode &) { - return bad("EmailCode"); - }, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) { - return bad("SetUpEmailRequired"); - }); - if (!hasLength) { - return; - } - const auto phoneCodeHash = qs(data->vphone_code_hash()); - const auto callTimeout = [&] { - if (const auto nextType = data->vnext_type()) { - return nextType->match([&](const MTPDauth_sentCodeTypeCall &) { - return data->vtimeout().value_or(60); - }, [](const auto &) { - return 0; - }); - } - return 0; - }(); - _controller->show( - Box( - _controller, - phoneNumber, - phoneCodeHash, - codeByFragmentUrl, - codeLength, - callTimeout), - Ui::LayerOption::KeepOther); -} - -void ChangePhone::EnterPhone::sendPhoneFail( - const MTP::Error &error, - const QString &phoneNumber) { - if (MTP::IsFloodError(error)) { - showError(tr::lng_flood_error(tr::now)); - } else if (error.type() == u"PHONE_NUMBER_INVALID"_q) { - showError(tr::lng_bad_phone(tr::now)); - } else if (error.type() == u"PHONE_NUMBER_BANNED"_q) { - Ui::ShowPhoneBannedError(&_controller->window(), phoneNumber); - } else if (error.type() == u"PHONE_NUMBER_OCCUPIED"_q) { - _controller->show( - Ui::MakeInformBox( - tr::lng_change_phone_occupied( - tr::now, - lt_phone, - Ui::FormatPhone(phoneNumber))), - Ui::LayerOption::CloseOther); - } else { - showError(Lang::Hard::ServerError()); - } -} - -void ChangePhone::EnterPhone::showError(const QString &text) { - CreateErrorLabel( - this, - _error, - text, - st::boxPadding.left(), - _phone->y() + _phone->height() + st::boxLittleSkip); - if (!text.isEmpty()) { - _phone->showError(); - } -} - -ChangePhone::EnterCode::EnterCode( - QWidget*, - not_null controller, - const QString &phone, - const QString &hash, - const QString &openUrl, - int codeLength, - int callTimeout) -: _controller(controller) -, _api(&controller->session().mtp()) -, _phone(phone) -, _hash(hash) -, _openUrl(openUrl) -, _codeLength(codeLength) -, _callTimeout(callTimeout) -, _call([this] { sendCall(); }, [this] { updateCall(); }) { -} - -void ChangePhone::EnterCode::prepare() { - const auto width = st::boxWidth; - setTitle(tr::lng_change_phone_title()); - - const auto descriptionText = tr::lng_change_phone_code_description( - tr::now, - lt_phone, - Ui::Text::Bold(Ui::FormatPhone(_phone)), - Ui::Text::WithEntities); - const auto description = object_ptr( - this, - rpl::single(descriptionText), - st::changePhoneLabel); - description->moveToLeft(st::boxPadding.left(), 0); - - const auto submitInput = [=] { submit(_code->getDigitsOnly()); }; - - const auto phoneValue = QString(); - _code.create( - this, - st::defaultInputField, - tr::lng_change_phone_code_title(), - phoneValue); - _code->setAutoSubmit(_codeLength, submitInput); - _code->setChangedCallback([=] { hideError(); }); - - _code->resize(width - 2 * st::boxPadding.left(), _code->height()); - _code->moveToLeft(st::boxPadding.left(), description->bottomNoMargins()); - connect(_code, &Ui::InputField::submitted, submitInput); - - if (!_openUrl.isEmpty()) { - _fragment.create( - this, - tr::lng_intro_fragment_button(), - st::fragmentBoxButton); - _fragment->setClickedCallback([=] { File::OpenUrl(_openUrl); }); - _fragment->setTextTransform( - Ui::RoundButton::TextTransform::NoTransform); - const auto codeBottom = _code->y() + _code->height(); - _fragment->setFullWidth(_code->width()); - _fragment->moveToLeft( - (width - _fragment->width()) / 2, - codeBottom + ErrorSkip() + st::boxLittleSkip); - } - - _controller->session().account().setHandleLoginCode([=](QString code) { - submit(code); - }); - boxClosing( - ) | rpl::start_with_next([controller = _controller] { - controller->session().account().setHandleLoginCode(nullptr); - }, lifetime()); - - setDimensions(width, countHeight()); - - if (_callTimeout > 0) { - _call.setStatus({ Ui::SentCodeCall::State::Waiting, _callTimeout }); - updateCall(); - } - - addButton(tr::lng_change_phone_new_submit(), submitInput); - addButton(tr::lng_cancel(), [=] { closeBox(); }); -} - -int ChangePhone::EnterCode::countHeight() const { - return _code->bottomNoMargins() - + ErrorSkip() - + 3 * st::boxLittleSkip - + (_fragment ? _fragment->height() : 0); -} - -void ChangePhone::EnterCode::submit(const QString &code) { - if (_requestId) { - return; - } - hideError(); - - const auto session = &_controller->session(); - const auto weak = Ui::MakeWeak(this); - _requestId = session->api().request(MTPaccount_ChangePhone( - MTP_string(_phone), - MTP_string(_hash), - MTP_string(code) - )).done([=, show = Window::Show(_controller)](const MTPUser &result) { - _requestId = 0; - session->data().processUser(result); - if (show.valid()) { - if (weak) { - show.hideLayer(); - } - Ui::Toast::Show( - show.toastParent(), - tr::lng_change_phone_success(tr::now)); - } - }).fail(crl::guard(this, [=](const MTP::Error &error) { - _requestId = 0; - sendCodeFail(error); - })).handleFloodErrors().send(); -} - -void ChangePhone::EnterCode::sendCall() { - _api.request(MTPauth_ResendCode( - MTP_string(_phone), - MTP_string(_hash) - )).done([=] { - _call.callDone(); - }).send(); -} - -void ChangePhone::EnterCode::updateCall() { - const auto text = _call.getText(); - if (text.isEmpty()) { - _callLabel.destroy(); - } else if (!_callLabel) { - _callLabel.create(this, text, st::changePhoneLabel); - _callLabel->moveToLeft( - st::boxPadding.left(), - countHeight() - _callLabel->height()); - _callLabel->show(); - } else { - _callLabel->setText(text); - } -} - -void ChangePhone::EnterCode::showError(const QString &text) { - CreateErrorLabel( - this, - _error, - text, - st::boxPadding.left(), - _code->y() + _code->height() + st::boxLittleSkip); - if (!text.isEmpty()) { - _code->showError(); - } -} - -void ChangePhone::EnterCode::sendCodeFail(const MTP::Error &error) { - if (MTP::IsFloodError(error)) { - showError(tr::lng_flood_error(tr::now)); - } else if (error.type() == u"PHONE_CODE_EMPTY"_q - || error.type() == u"PHONE_CODE_INVALID"_q) { - showError(tr::lng_bad_code(tr::now)); - } else if (error.type() == u"PHONE_CODE_EXPIRED"_q - || error.type() == u"PHONE_NUMBER_BANNED"_q) { - closeBox(); // Go back to phone input. - } else if (error.type() == u"PHONE_NUMBER_INVALID"_q) { - showError(tr::lng_bad_phone(tr::now)); - } else { - showError(Lang::Hard::ServerError()); - } -} - -ChangePhone::ChangePhone( - QWidget *parent, - not_null controller) -: Section(parent) -, _controller(controller) { - setupContent(); -} - -rpl::producer ChangePhone::title() { - return Info::Profile::PhoneValue( - _controller->session().user() - ) | rpl::map([](const TextWithEntities &text) { - return text.text; - }); -} - -void ChangePhone::setupContent() { - const auto content = Ui::CreateChild(this); - - auto icon = CreateLottieIcon(content, { - .name = u"change_number"_q, - .sizeOverride = { - st::changePhoneIconSize, - st::changePhoneIconSize, - }, - }, st::changePhoneIconPadding); - content->add(std::move(icon.widget)); - _animate = std::move(icon.animate); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_change_phone_button(), - st::changePhoneTitle)), - st::changePhoneTitlePadding); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_change_phone_about(Ui::Text::RichLangValue), - st::changePhoneDescription)), - st::changePhoneDescriptionPadding); - - const auto button = content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_change_phone_button(), - st::changePhoneButton)), - st::changePhoneButtonPadding)->entity(); - button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - button->setClickedCallback([=] { - auto callback = [=] { - _controller->show( - Box(_controller), - Ui::LayerOption::CloseOther); - }; - _controller->show( - Ui::MakeConfirmBox({ - .text = tr::lng_change_phone_warning(), - .confirmed = std::move(callback), - }), - Ui::LayerOption::CloseOther); - }); - - Ui::ResizeFitChild(this, content); -} - -void ChangePhone::showFinished() { - _animate(anim::repeat::loop); -} - -} // namespace Settings diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 6ea39d2e2..181dccb1e 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -17,9 +17,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "ui/filter_icons.h" #include "ui/text/text_utilities.h" // Ui::Text::Bold -#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_settings.h" #include "styles/style_payments.h" // paymentsSectionButton @@ -70,11 +70,9 @@ 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)) { - auto text = (add + controller->showToast((add ? tr::lng_filters_toast_add : tr::lng_filters_toast_remove)( tr::now, @@ -82,10 +80,7 @@ void ChangeFilterById( Ui::Text::Bold(chat), lt_folder, Ui::Text::Bold(name), - Ui::Text::WithEntities); - Ui::Toast::Show( - Window::Show(controller).toastParent(), - { .text = std::move(text), .st = &st::defaultToast }); + Ui::Text::WithEntities)); } }).fail([=](const MTP::Error &error) { // Revert filter on fail. @@ -123,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()), }; } @@ -159,17 +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)); - } else if (validator.canAdd()) { - validator.add(id); + r.count, + 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/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 7c27f59ed..626671cb4 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -1265,7 +1265,7 @@ object_ptr ProxiesBoxController::CreateOwningBox( object_ptr ProxiesBoxController::create() { auto result = Box(this, _settings); - _show = std::make_shared(result.data()); + _show = result->uiShow(); for (const auto &item : _list) { updateView(item); } @@ -1548,9 +1548,7 @@ void ProxiesBoxController::share(const ProxyData &proxy) { + ((proxy.type == Type::Mtproto && !proxy.password.isEmpty()) ? "&secret=" + proxy.password : ""); QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show( - _show->toastParent(), - tr::lng_username_copied(tr::now)); + _show->showToast(tr::lng_username_copied(tr::now)); } ProxiesBoxController::~ProxiesBoxController() { diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 8c4cd47a5..ca1eb70f3 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -775,7 +775,7 @@ void CreatePollBox::setInnerFocus() { } void CreatePollBox::submitFailed(const QString &error) { - Ui::Toast::Show(Ui::BoxShow(this).toastParent(), error); + showToast(error); } not_null CreatePollBox::setupQuestion( @@ -850,10 +850,7 @@ not_null CreatePollBox::setupSolution( Core::App().settings().replaceEmojiValue()); solution->setMarkdownReplacesEnabled(rpl::single(true)); solution->setEditLinkCallback( - DefaultEditLinkCallback( - std::make_shared(_controller), - session, - solution)); + DefaultEditLinkCallback(_controller->uiShow(), solution)); solution->customTab(true); const auto warning = CreateWarningLabel( @@ -988,12 +985,10 @@ object_ptr CreatePollBox::setupContent() { || (_chosen & PollData::Flag::Quiz)); multiple->events( ) | rpl::filter([=](not_null e) { - return (e->type() == QEvent::MouseButtonPress) && quiz->checked(); - }) | rpl::start_with_next([ - toastParent = Ui::BoxShow(this).toastParent()] { - Ui::Toast::Show( - toastParent, - tr::lng_polls_create_one_answer(tr::now)); + return (e->type() == QEvent::MouseButtonPress) + && quiz->checked(); + }) | rpl::start_with_next([show = uiShow()] { + show->showToast(tr::lng_polls_create_one_answer(tr::now)); }, multiple->lifetime()); } @@ -1070,10 +1065,9 @@ object_ptr CreatePollBox::setupContent() { *error &= ~Error::Solution; } }; - const auto showError = [ - toastParent = Ui::BoxShow(this).toastParent()]( + const auto showError = [show = uiShow()]( tr::phrase<> text) { - Ui::Toast::Show(toastParent, text(tr::now)); + show->showToast(text(tr::now)); }; const auto send = [=](Api::SendOptions sendOptions) { collectError(); @@ -1099,8 +1093,10 @@ object_ptr CreatePollBox::setupContent() { HistoryView::PrepareScheduleBox( this, SendMenu::Type::Scheduled, - send), - Ui::LayerOption::KeepOther); + send)); + }; + const auto sendWhenOnline = [=] { + send(Api::DefaultSendWhenOnlineOptions()); }; options->scrollToWidget( @@ -1130,7 +1126,8 @@ object_ptr CreatePollBox::setupContent() { submit.data(), sendMenuType, sendSilent, - sendScheduled); + sendScheduled, + sendWhenOnline); addButton(tr::lng_cancel(), [=] { closeBox(); }); return result; diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index 28a4ad97b..99fe6b883 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -285,7 +285,7 @@ void DeleteMessagesBox::prepare() { if (_wipeHistoryJustClear && _wipeHistoryPeer) { const auto validator = TTLMenu::TTLValidator( - std::make_shared(this), + uiShow(), _wipeHistoryPeer); if (validator.can()) { _wipeHistoryPeer->updateFull(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 253d42213..90a989ce7 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "mainwidget.h" // controller->content() -> QWidget* #include "mtproto/mtproto_config.h" #include "platform/platform_specific.h" #include "storage/localimageloader.h" // SendMediaType @@ -69,7 +70,9 @@ namespace { constexpr auto kChangesDebounceTimeout = crl::time(1000); -auto ListFromMimeData(not_null data, bool premium) { +[[nodiscard]] Ui::PreparedList ListFromMimeData( + not_null data, + bool premium) { using Error = Ui::PreparedList::Error; const auto list = Core::ReadMimeUrls(data); auto result = !list.isEmpty() @@ -89,7 +92,7 @@ auto ListFromMimeData(not_null data, bool premium) { return result; } -Ui::AlbumType ComputeAlbumType(not_null item) { +[[nodiscard]] Ui::AlbumType ComputeAlbumType(not_null item) { if (item->groupId().empty()) { return Ui::AlbumType(); } @@ -109,17 +112,130 @@ Ui::AlbumType ComputeAlbumType(not_null item) { return Ui::AlbumType(); } -bool CanBeCompressed(Ui::AlbumType type) { +[[nodiscard]] bool CanBeCompressed(Ui::AlbumType type) { return (type == Ui::AlbumType::None) || (type == Ui::AlbumType::PhotoVideo); } +void ChooseReplacement( + not_null controller, + Ui::AlbumType type, + Fn chosen) { + const auto weak = base::make_weak(controller); + const auto callback = [=](FileDialog::OpenResult &&result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + const auto showError = [=](tr::phrase<> t) { + if (const auto strong = weak.get()) { + strong->showToast(t(tr::now)); + } + }; + + const auto checkResult = [=](const Ui::PreparedList &list) { + if (list.files.size() != 1) { + return false; + } + const auto &file = list.files.front(); + const auto mime = file.information->filemime; + if (Core::IsMimeSticker(mime)) { + showError(tr::lng_edit_media_invalid_file); + return false; + } else if (type != Ui::AlbumType::None + && !file.canBeInAlbumType(type)) { + showError(tr::lng_edit_media_album_error); + return false; + } + return true; + }; + const auto premium = strong->session().premium(); + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + checkResult, + showError, + st::sendMediaPreviewSize, + premium); + + if (list) { + chosen(std::move(*list)); + } + }; + + const auto filters = (type == Ui::AlbumType::PhotoVideo) + ? FileDialog::PhotoVideoFilesFilter() + : FileDialog::AllFilesFilter(); + FileDialog::GetOpenPath( + controller->content().get(), + tr::lng_choose_file(tr::now), + filters, + crl::guard(controller, callback)); +} + +void EditPhotoImage( + not_null controller, + std::shared_ptr media, + bool wasSpoiler, + Fn done) { + const auto large = media + ? media->image(Data::PhotoSize::Large) + : nullptr; + const auto parent = controller->content(); + const auto previewWidth = st::sendMediaPreviewSize; + auto callback = [=](const Editor::PhotoModifications &mods) { + if (!mods) { + return; + } + const auto large = media->image(Data::PhotoSize::Large); + if (!large) { + return; + } + auto copy = large->original(); + auto list = Storage::PrepareMediaFromImage( + std::move(copy), + QByteArray(), + previewWidth); + + using ImageInfo = Ui::PreparedFileInformation::Image; + auto &file = list.files.front(); + file.spoiler = wasSpoiler; + const auto image = std::get_if(&file.information->media); + + image->modifications = mods; + const auto sideLimit = PhotoSideLimit(); + Storage::UpdateImageDetails(file, previewWidth, sideLimit); + done(std::move(list)); + }; + const auto fileImage = std::make_shared(*large); + auto editor = base::make_unique_q( + parent, + &controller->window(), + fileImage, + Editor::PhotoModifications()); + const auto raw = editor.get(); + auto layer = std::make_unique( + parent, + std::move(editor)); + Editor::InitEditorLayer(layer.get(), raw, std::move(callback)); + controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); +} + } // namespace EditCaptionBox::EditCaptionBox( QWidget*, not_null controller, not_null item) +: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) { +} + +EditCaptionBox::EditCaptionBox( + QWidget*, + not_null controller, + not_null item, + TextWithTags &&text, + Ui::PreparedList &&list, + Fn saved) : _controller(controller) , _historyItem(item) , _isAllowedEditMedia(item->media() @@ -130,12 +246,15 @@ EditCaptionBox::EditCaptionBox( , _scroll(base::make_unique_q(this, st::boxScroll)) , _field(base::make_unique_q( this, - st::confirmCaptionArea, + st::defaultComposeFiles.caption, Ui::InputField::Mode::MultiLine, tr::lng_photo_caption())) , _emojiToggle(base::make_unique_q( this, - st::boxAttachEmoji)) { + st::defaultComposeFiles.emoji)) +, _initialText(std::move(text)) +, _initialList(std::move(list)) +, _saved(std::move(saved)) { Expects(item->media() != nullptr); Expects(item->media()->allowsEditCaption()); @@ -148,6 +267,100 @@ EditCaptionBox::EditCaptionBox( EditCaptionBox::~EditCaptionBox() = default; +void EditCaptionBox::StartMediaReplace( + not_null controller, + FullMsgId itemId, + TextWithTags text, + Fn saved) { + const auto session = &controller->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto show = [=](Ui::PreparedList &&list) mutable { + controller->show(Box( + controller, + item, + std::move(text), + std::move(list), + std::move(saved))); + }; + ChooseReplacement( + controller, + ComputeAlbumType(item), + crl::guard(controller, show)); +} + +void EditCaptionBox::StartMediaReplace( + not_null controller, + FullMsgId itemId, + Ui::PreparedList &&list, + TextWithTags text, + Fn saved) { + const auto session = &controller->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto type = ComputeAlbumType(item); + const auto showError = [=](tr::phrase<> t) { + controller->showToast(t(tr::now)); + }; + const auto checkResult = [=](const Ui::PreparedList &list) { + if (list.files.size() != 1) { + return false; + } + const auto &file = list.files.front(); + const auto mime = file.information->filemime; + if (Core::IsMimeSticker(mime)) { + showError(tr::lng_edit_media_invalid_file); + return false; + } else if (type != Ui::AlbumType::None + && !file.canBeInAlbumType(type)) { + showError(tr::lng_edit_media_album_error); + return false; + } + return true; + }; + if (list.error != Ui::PreparedList::Error::None) { + showError(tr::lng_send_media_invalid_files); + } else if (checkResult(list)) { + controller->show(Box( + controller, + item, + std::move(text), + std::move(list), + std::move(saved))); + } +} + +void EditCaptionBox::StartPhotoEdit( + not_null controller, + std::shared_ptr media, + FullMsgId itemId, + TextWithTags text, + Fn saved) { + const auto session = &controller->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto hasSpoiler = item->media() && item->media()->hasSpoiler(); + EditPhotoImage(controller, media, hasSpoiler, [=]( + Ui::PreparedList &&list) mutable { + const auto item = session->data().message(itemId); + if (!item) { + return; + } + controller->show(Box( + controller, + item, + std::move(text), + std::move(list), + std::move(saved))); + }); +} + void EditCaptionBox::prepare() { addButton(tr::lng_settings_save(), [=] { save(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); @@ -158,7 +371,9 @@ void EditCaptionBox::prepare() { setupEmojiPanel(); setInitialText(); - rebuildPreview(); + if (!setPreparedList(std::move(_initialList))) { + rebuildPreview(); + } setupEditEventHandler(); SetupShadowsToScrollContent(this, _scroll, _contentHeight.events()); @@ -187,6 +402,7 @@ void EditCaptionBox::rebuildPreview() { if (photo || document->isVideoFile() || document->isAnimation()) { const auto media = Ui::CreateChild( this, + st::defaultComposeControls, gifPaused, _historyItem, Ui::AttachControls::Type::EditOnly); @@ -195,6 +411,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, _historyItem, Ui::AttachControls::Type::EditOnly)); } @@ -203,6 +420,7 @@ void EditCaptionBox::rebuildPreview() { const auto media = Ui::SingleMediaPreview::Create( this, + st::defaultComposeControls, gifPaused, file, Ui::AttachControls::Type::EditOnly); @@ -214,6 +432,7 @@ void EditCaptionBox::rebuildPreview() { } else { _content.reset(Ui::CreateChild( this, + st::defaultComposeControls, file, Ui::AttachControls::Type::EditOnly)); } @@ -267,7 +486,7 @@ void EditCaptionBox::setupField() { _field->setSubmitSettings( Core::App().settings().sendSubmitWay()); - _field->setMaxHeight(st::confirmCaptionArea.heightMax); + _field->setMaxHeight(st::defaultComposeFiles.caption.heightMax); connect(_field, &Ui::InputField::submitted, [=] { save(); }); connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); }); @@ -290,16 +509,16 @@ void EditCaptionBox::setupField() { } void EditCaptionBox::setInitialText() { - const auto initial = PrepareEditText(_historyItem); _field->setTextWithTags( - initial, + _initialText, Ui::InputField::HistoryAction::Clear); auto cursor = _field->textCursor(); cursor.movePosition(QTextCursor::End); _field->setTextCursor(cursor); _checkChangedTimer.setCallback([=] { - if (_field->getTextWithAppliedMarkdown() == initial) { + if (_field->getTextWithAppliedMarkdown() == _initialText + && _preparedList.files.empty()) { setCloseByOutsideClick(true); } }); @@ -353,132 +572,44 @@ void EditCaptionBox::setupControls() { } void EditCaptionBox::setupEditEventHandler() { - const auto toastParent = Ui::BoxShow(this).toastParent(); - const auto callback = [=](FileDialog::OpenResult &&result) { - auto showError = [toastParent](tr::phrase<> t) { - Ui::Toast::Show(toastParent, t(tr::now)); - }; - - const auto checkResult = [=](const Ui::PreparedList &list) { - if (list.files.size() != 1) { - return false; - } - const auto &file = list.files.front(); - const auto mime = file.information->filemime; - if (Core::IsMimeSticker(mime)) { - showError(tr::lng_edit_media_invalid_file); - return false; - } else if (_albumType != Ui::AlbumType::None - && !file.canBeInAlbumType(_albumType)) { - showError(tr::lng_edit_media_album_error); - return false; - } - return true; - }; - const auto premium = _controller->session().premium(); - auto list = Storage::PreparedFileFromFilesDialog( - std::move(result), - checkResult, - showError, - st::sendMediaPreviewSize, - premium); - - if (list) { - setPreparedList(std::move(*list)); - } - }; - - const auto buttonCallback = [=] { - const auto filters = (_albumType == Ui::AlbumType::PhotoVideo) - ? FileDialog::PhotoVideoFilesFilter() - : FileDialog::AllFilesFilter(); - FileDialog::GetOpenPath( - this, - tr::lng_choose_file(tr::now), - filters, - crl::guard(this, callback)); - }; - _editMediaClicks.events( - ) | rpl::start_with_next( - buttonCallback, - lifetime()); + ) | rpl::start_with_next([=] { + ChooseReplacement(_controller, _albumType, crl::guard(this, [=]( + Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + })); + }, lifetime()); } void EditCaptionBox::setupPhotoEditorEventHandler() { const auto openedOnce = lifetime().make_state(false); _photoEditorOpens.events( ) | rpl::start_with_next([=, controller = _controller] { - const auto increment = [=] { - if (*openedOnce) { - return; - } + if (_preparedList.files.empty() + && (!_photoMedia + || !_photoMedia->image(Data::PhotoSize::Large))) { + return; + } else if (!*openedOnce) { *openedOnce = true; controller->session().settings().incrementPhotoEditorHintShown(); controller->session().saveSettings(); - }; - const auto clearError = [=] { + } + if (!_error.isEmpty()) { _error = QString(); update(); - }; - const auto previewWidth = st::sendMediaPreviewSize; + } if (!_preparedList.files.empty()) { - increment(); - clearError(); Editor::OpenWithPreparedFile( this, - controller, + controller->uiShow(), &_preparedList.files.front(), - previewWidth, + st::sendMediaPreviewSize, [=] { rebuildPreview(); }); - } else if (_photoMedia) { - const auto large = _photoMedia->image(Data::PhotoSize::Large); - if (!large) { - return; - } - increment(); - clearError(); - auto callback = [=](const Editor::PhotoModifications &mods) { - if (!mods || !_photoMedia) { - return; - } - const auto large = _photoMedia->image(Data::PhotoSize::Large); - if (!large) { - return; - } - auto copy = large->original(); - const auto wasSpoiler = hasSpoiler(); - - _preparedList = Storage::PrepareMediaFromImage( - std::move(copy), - QByteArray(), - previewWidth); - - using ImageInfo = Ui::PreparedFileInformation::Image; - auto &file = _preparedList.files.front(); - file.spoiler = wasSpoiler; - const auto image = std::get_if( - &file.information->media); - - image->modifications = mods; - const auto sideLimit = PhotoSideLimit(); - Storage::UpdateImageDetails(file, previewWidth, sideLimit); - rebuildPreview(); - }; - const auto fileImage = std::make_shared(*large); - auto editor = base::make_unique_q( - this, - &controller->window(), - fileImage, - Editor::PhotoModifications()); - const auto raw = editor.get(); - auto layer = std::make_unique( - this, - std::move(editor)); - Editor::InitEditorLayer(layer.get(), raw, std::move(callback)); - controller->showLayer( - std::move(layer), - Ui::LayerOption::KeepOther); + } else { + EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=]( + Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + }); } }, lifetime()); } @@ -521,7 +652,7 @@ void EditCaptionBox::setupEmojiPanel() { _controller, object_ptr( nullptr, - _controller, + _controller->uiShow(), Window::GifPauseReason::Layer, Selector::Mode::EmojiOnly)); _emojiPanel->setDesiredHeightValues( @@ -602,14 +733,13 @@ bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) { } } if (invalidForAlbum) { - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - tr::lng_edit_media_album_error(tr::now)); + showToast(tr::lng_edit_media_album_error(tr::now)); return false; } const auto wasSpoiler = hasSpoiler(); _preparedList = std::move(list); _preparedList.files.front().spoiler = wasSpoiler; + setCloseByOutsideClick(false); rebuildPreview(); return true; } @@ -721,7 +851,8 @@ bool EditCaptionBox::validateLength(const QString &text) const { if (remove <= 0) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _controller->show( + Box(CaptionLimitReachedBox, session, remove, nullptr)); return false; } @@ -780,13 +911,13 @@ void EditCaptionBox::save() { : SendMediaType::File, _field->getTextWithAppliedMarkdown(), action); - closeBox(); + closeAfterSave(); return; } const auto done = crl::guard(this, [=] { _saveRequestId = 0; - closeBox(); + closeAfterSave(); }); const auto fail = crl::guard(this, [=](const QString &error) { @@ -795,7 +926,7 @@ void EditCaptionBox::save() { _error = tr::lng_edit_error(tr::now); update(); } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { - closeBox(); + closeAfterSave(); } else if (error == u"MESSAGE_EMPTY"_q) { _field->setFocus(); _field->showError(); @@ -816,6 +947,16 @@ void EditCaptionBox::save() { _saveRequestId = Api::EditCaption(item, sending, options, done, fail); } +void EditCaptionBox::closeAfterSave() { + const auto weak = MakeWeak(this); + if (_saved) { + _saved(); + } + if (weak) { + closeBox(); + } +} + void EditCaptionBox::keyPressEvent(QKeyEvent *e) { const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier); if ((e->key() == Qt::Key_E) && ctrl) { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index deb756b50..80a7b0a82 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -36,8 +36,33 @@ public: QWidget*, not_null controller, not_null item); + EditCaptionBox( + QWidget*, + not_null controller, + not_null item, + TextWithTags &&text, + Ui::PreparedList &&list, + Fn saved); ~EditCaptionBox(); + static void StartMediaReplace( + not_null controller, + FullMsgId itemId, + TextWithTags text, + Fn saved); + static void StartMediaReplace( + not_null controller, + FullMsgId itemId, + Ui::PreparedList &&list, + TextWithTags text, + Fn saved); + static void StartPhotoEdit( + not_null controller, + std::shared_ptr media, + FullMsgId itemId, + TextWithTags text, + Fn saved); + protected: void prepare() override; void setInnerFocus() override; @@ -66,6 +91,7 @@ private: bool validateLength(const QString &text) const; void applyChanges(); void save(); + void closeAfterSave(); bool fileFromClipboard(not_null data); @@ -89,6 +115,10 @@ private: base::unique_qptr _emojiPanel; base::unique_qptr _emojiFilter; + const TextWithTags _initialText; + Ui::PreparedList _initialList; + Fn _saved; + std::shared_ptr _photoMedia; Ui::PreparedList _preparedList; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 69aa79d16..e5c601881 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -112,10 +112,16 @@ std::unique_ptr PrivacyExceptionsBoxControl } // namespace +bool EditPrivacyController::hasOption(Option option) const { + return (option != Option::CloseFriends); +} + QString EditPrivacyController::optionLabel(Option option) const { switch (option) { case Option::Everyone: return tr::lng_edit_privacy_everyone(tr::now); case Option::Contacts: return tr::lng_edit_privacy_contacts(tr::now); + case Option::CloseFriends: + return tr::lng_edit_privacy_close_friends(tr::now); case Option::Nobody: return tr::lng_edit_privacy_nobody(tr::now); } Unexpected("Option value in optionsLabelKey."); @@ -167,8 +173,7 @@ void EditPrivacyBox::editExceptions( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }; _window->show( - Box(std::move(controller), std::move(initBox)), - Ui::LayerOption::KeepOther); + Box(std::move(controller), std::move(initBox))); } std::vector> &EditPrivacyBox::exceptions(Exception exception) { @@ -183,10 +188,12 @@ bool EditPrivacyBox::showExceptionLink(Exception exception) const { switch (exception) { case Exception::Always: return (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends) || (_value.option == Option::Nobody); case Exception::Never: return (_value.option == Option::Everyone) - || (_value.option == Option::Contacts); + || (_value.option == Option::Contacts) + || (_value.option == Option::CloseFriends); } Unexpected("Invalid exception value."); } @@ -217,16 +224,18 @@ Ui::FlatLabel *EditPrivacyBox::addLabel( if (!text) { return nullptr; } - return container->add( + auto label = object_ptr( + container, + rpl::duplicate(text), + st::boxDividerLabel); + const auto result = label.data(); + container->add( object_ptr( container, - object_ptr( - container, - rpl::duplicate(text), - st::boxDividerLabel), + std::move(label), st::settingsDividerLabelPadding), - { 0, topSkip, 0, 0 } - )->entity(); + { 0, topSkip, 0, 0 }); + return result; } Ui::FlatLabel *EditPrivacyBox::addLabelOrDivider( @@ -282,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 @@ -325,6 +327,7 @@ void EditPrivacyBox::setupContent() { { 0, st::settingsPrivacySkipTop, 0, 0 }); addOptionRow(Option::Everyone); addOptionRow(Option::Contacts); + addOptionRow(Option::CloseFriends); addOptionRow(Option::Nobody); const auto warning = addLabelOrDivider( content, @@ -374,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/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 6a78c41ae..d9ba7dab9 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -41,9 +41,7 @@ public: [[nodiscard]] virtual Key key() const = 0; [[nodiscard]] virtual rpl::producer title() const = 0; - [[nodiscard]] virtual bool hasOption(Option option) const { - return true; - } + [[nodiscard]] virtual bool hasOption(Option option) const; [[nodiscard]] virtual rpl::producer optionsTitleKey() const = 0; [[nodiscard]] virtual QString optionLabel(Option option) const; [[nodiscard]] virtual rpl::producer warning() const { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index e8fd6a843..88bbdca03 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -8,20 +8,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/filters/edit_filter_box.h" #include "boxes/filters/edit_filter_chats_list.h" +#include "boxes/filters/edit_filter_links.h" +#include "boxes/premium_limits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/widgets/buttons.h" #include "ui/widgets/input_fields.h" +#include "ui/wrap/slide_wrap.h" #include "ui/effects/panel_animation.h" #include "ui/filter_icons.h" #include "ui/filter_icon_panel.h" #include "ui/painter.h" +#include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_peer.h" #include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_session.h" +#include "data/data_user.h" #include "core/application.h" #include "core/core_settings.h" #include "settings/settings_common.h" @@ -37,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_window.h" #include "styles/style_chat.h" +#include "styles/style_menu_icons.h" namespace { @@ -330,14 +336,18 @@ void EditExceptions( Fn refresh) { const auto include = (options & Flag::Contacts) != Flags(0); const auto rules = data->current(); + const auto session = &window->session(); auto controller = std::make_unique( - &window->session(), + session, (include ? tr::lng_filters_include_title() : tr::lng_filters_exclude_title()), options, rules.flags() & options, - include ? rules.always() : rules.never()); + include ? rules.always() : rules.never(), + [=](int count) { + return Box(FilterChatsLimitBox, session, count, include); + }); const auto rawController = controller.get(); auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); @@ -380,10 +390,7 @@ void EditExceptions( box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }; window->window().show( - Box( - std::move(controller), - std::move(initBox)), - Ui::LayerOption::KeepOther); + Box(std::move(controller), std::move(initBox))); } void CreateIconSelector( @@ -499,6 +506,24 @@ void CreateIconSelector( return QString(); } +not_null AddToggledButton( + not_null container, + rpl::producer shown, + rpl::producer text, + const style::SettingsButton &st, + IconDescriptor &&descriptor) { + const auto toggled = container->add( + object_ptr>( + container, + CreateButton( + container, + std::move(text), + st, + std::move(descriptor))) + )->toggleOn(std::move(shown), anim::type::instant)->setDuration(0); + return toggled->entity(); +} + [[nodiscard]] QString TrimDefaultTitle(const QString &title) { return (title.size() <= kMaxFilterTitleLength) ? title : QString(); } @@ -509,10 +534,58 @@ void EditFilterBox( not_null box, not_null window, const Data::ChatFilter &filter, - Fn doneCallback) { - const auto creating = filter.title().isEmpty(); + Fn doneCallback, + Fn next)> saveAnd) { + using namespace rpl::mappers; + + struct State { + rpl::variable rules; + rpl::variable> links; + rpl::variable hasLinks; + rpl::variable chatlist; + rpl::variable creating; + }; + const auto owner = &window->session().data(); + const auto state = box->lifetime().make_state(State{ + .rules = filter, + .chatlist = filter.chatlist(), + .creating = filter.title().isEmpty(), + }); + state->links = owner->chatsFilters().chatlistLinks(filter.id()), + state->hasLinks = state->links.value() | rpl::map([=](const auto &v) { + return !v.empty(); + }); + state->hasLinks.value() | rpl::filter( + _1 + ) | rpl::start_with_next([=] { + state->chatlist = true; + }, box->lifetime()); + + const auto data = &state->rules; + + owner->chatsFilters().isChatlistChanged( + ) | rpl::filter([=](FilterId id) { + return (id == data->current().id()); + }) | rpl::start_with_next([=](FilterId id) { + const auto filters = &owner->chatsFilters(); + const auto &list = filters->list(); + const auto i = ranges::find(list, id, &Data::ChatFilter::id); + if (i == end(list)) { + return; + } + *data = data->current().withChatlist(i->chatlist(), i->hasMyLinks()); + if (!i->chatlist() && !state->hasLinks.current()) { + state->chatlist = false; + } + }, box->lifetime()); + box->setWidth(st::boxWideWidth); - box->setTitle(creating ? tr::lng_filters_new() : tr::lng_filters_edit()); + box->setTitle(rpl::conditional( + state->creating.value(), + tr::lng_filters_new(), + tr::lng_filters_edit())); box->setCloseByOutsideClick(false); Data::AmPremiumValue( @@ -521,9 +594,6 @@ void EditFilterBox( box->closeBox(); }, box->lifetime()); - using State = rpl::variable; - const auto data = box->lifetime().make_state(filter); - const auto content = box->verticalLayout(); const auto name = content->add( object_ptr( @@ -543,7 +613,12 @@ void EditFilterBox( const auto nameEditing = box->lifetime().make_state( NameEditing{ name }); - nameEditing->custom = !creating; + + state->creating.value( + ) | rpl::filter(!_1) | rpl::start_with_next([=] { + nameEditing->custom = true; + }, box->lifetime()); + QObject::connect(name, &Ui::InputField::changed, [=] { if (!nameEditing->settingDefault) { nameEditing->custom = true; @@ -591,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, @@ -604,24 +679,141 @@ void EditFilterBox( AddDividerText(content, tr::lng_filters_include_about()); AddSkip(content); - AddSubsectionTitle(content, tr::lng_filters_exclude()); + auto excludeWrap = content->add( + object_ptr>( + content, + object_ptr(content)) + )->setDuration(0); + excludeWrap->toggleOn(state->chatlist.value() | rpl::map(!_1)); + const auto excludeInner = excludeWrap->entity(); + + AddSubsectionTitle(excludeInner, tr::lng_filters_exclude()); const auto excludeAdd = AddButton( - content, + 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( - content, + excludeInner, data, updateDefaultTitle, kExcludeTypes, &Data::ChatFilter::never); - AddSkip(content); - AddDividerText(content, tr::lng_filters_exclude_about()); + AddSkip(excludeInner); + AddDividerText(excludeInner, tr::lng_filters_exclude_about()); + AddSkip(excludeInner); + const auto collect = [=]() -> std::optional { + const auto title = name->getLastText().trimmed(); + const auto rules = data->current(); + if (title.isEmpty()) { + name->showError(); + box->scrollToY(0); + return {}; + } else if (!(rules.flags() & kTypes) && rules.always().empty()) { + window->window().showToast(tr::lng_filters_empty(tr::now)); + return {}; + } else if ((rules.flags() == (kTypes | Flag::NoArchived)) + && rules.always().empty() + && rules.never().empty()) { + window->window().showToast(tr::lng_filters_default(tr::now)); + return {}; + } + return rules.withTitle(title); + }; + + AddSubsectionTitle( + content, + rpl::conditional( + state->hasLinks.value(), + tr::lng_filters_link_has(), + tr::lng_filters_link())); + + state->hasLinks.changes() | rpl::start_with_next([=] { + content->resizeToWidth(content->widthNoMargins()); + }, content->lifetime()); + + if (filter.chatlist()) { + window->session().data().chatsFilters().reloadChatlistLinks( + filter.id()); + } + + const auto createLink = AddToggledButton( + content, + state->hasLinks.value() | rpl::map(!rpl::mappers::_1), + tr::lng_filters_link_create(), + st::settingsButtonActive, + { &st::settingsFolderShareIcon, IconType::Simple }); + const auto addLink = AddToggledButton( + content, + state->hasLinks.value(), + tr::lng_group_invite_add(), + st::settingsButtonActive, + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive }); + + SetupFilterLinks( + content, + window, + state->links.value(), + [=] { return collect().value_or(Data::ChatFilter()); }); + + rpl::merge( + createLink->clicks(), + addLink->clicks() + ) | rpl::filter( + (rpl::mappers::_1 == Qt::LeftButton) + ) | rpl::start_with_next([=](Qt::MouseButton button) { + const auto result = collect(); + if (!result || !GoodForExportFilterLink(window, *result)) { + return; + } + const auto shared = CollectFilterLinkChats(*result); + if (shared.empty()) { + window->show(ShowLinkBox(window, *result, {})); + return; + } + saveAnd(*result, crl::guard(box, [=](Data::ChatFilter updated) { + state->creating = false; + + // Comparison of ChatFilter-s don't take id into account! + data->force_assign(updated); + const auto id = updated.id(); + state->links = owner->chatsFilters().chatlistLinks(id); + ExportFilterLink(id, shared, crl::guard(box, [=]( + Data::ChatFilterLink link) { + Expects(link.id == id); + + *data = data->current().withChatlist(true, true); + window->show(ShowLinkBox(window, updated, link)); + }), crl::guard(box, [=](QString error) { + const auto session = &window->session(); + if (error == u"CHATLISTS_TOO_MUCH"_q) { + window->show(Box(ShareableFiltersLimitBox, session)); + } else if (error == u"INVITES_TOO_MUCH"_q) { + window->show(Box(FilterLinksLimitBox, session)); + } else if (error == u"CHANNELS_TOO_MUCH"_q) { + window->show(Box(ChannelsLimitBox, session)); + } else if (error == u"USER_CHANNELS_TOO_MUCH"_q) { + window->showToast( + { tr::lng_filters_link_group_admin_error(tr::now) }); + } else { + window->show(ShowLinkBox(window, updated, { .id = id })); + } + })); + })); + }, createLink->lifetime()); + AddSkip(content); + AddDividerText( + content, + rpl::conditional( + state->hasLinks.value(), + tr::lng_filters_link_about_many(), + tr::lng_filters_link_about())); + + const auto show = box->uiShow(); const auto refreshPreviews = [=] { include->updateData( data->current().flags() & kTypes, @@ -634,7 +826,7 @@ void EditFilterBox( EditExceptions( window, box, - kTypes, + kTypes | (state->chatlist.current() ? Flag::Chatlist : Flag()), data, updateDefaultTitle, refreshPreviews); @@ -650,35 +842,17 @@ void EditFilterBox( }); const auto save = [=] { - const auto title = name->getLastText().trimmed(); - const auto rules = data->current(); - const auto result = Data::ChatFilter( - rules.id(), - title, - rules.iconEmoji(), - rules.flags(), - rules.always(), - rules.pinned(), - rules.never()); - if (title.isEmpty()) { - name->showError(); - return; - } else if (!(rules.flags() & kTypes) && rules.always().empty()) { - window->window().showToast(tr::lng_filters_empty(tr::now)); - return; - } else if ((rules.flags() == (kTypes | Flag::NoArchived)) - && rules.always().empty() - && rules.never().empty()) { - window->window().showToast(tr::lng_filters_default(tr::now)); - return; + if (const auto result = collect()) { + box->closeBox(); + doneCallback(*result); } - box->closeBox(); - - doneCallback(result); }; - box->addButton( - creating ? tr::lng_filters_create_button() : tr::lng_settings_save(), - save); + + box->addButton(rpl::conditional( + state->creating.value(), + tr::lng_filters_create_button(), + tr::lng_settings_save() + ), save); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); } @@ -707,9 +881,16 @@ void EditExistingFilter( tl )).send(); }; + const auto saveAnd = [=]( + const Data::ChatFilter &data, + Fn next) { + doneCallback(data); + next(data); + }; window->window().show(Box( EditFilterBox, window, *i, - crl::guard(session, doneCallback))); + crl::guard(session, doneCallback), + crl::guard(session, saveAnd))); } diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h index 43b97adb1..695dbbe92 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.h @@ -21,7 +21,8 @@ void EditFilterBox( not_null box, not_null window, const Data::ChatFilter &filter, - Fn doneCallback); + Fn doneCallback, + Fn)> saveAnd); void EditExistingFilter( not_null window, diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index e1aacddf9..61e937dfe 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -7,9 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/filters/edit_filter_chats_list.h" +#include "data/data_premium_limits.h" #include "history/history.h" #include "window/window_session_controller.h" -#include "boxes/premium_limits_box.h" #include "lang/lang_keys.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" @@ -99,22 +99,6 @@ private: return PeerId(FakeChatId(static_cast(flag))).value; } -[[nodiscard]] int Limit( - not_null session, - const QString &key, - int fallback) { - return session->account().appConfig().get(key, fallback); -} - -[[nodiscard]] int Limit(not_null session) { - const auto premium = session->premium(); - return Limit(session, - (premium - ? "dialog_filters_chats_limit_premium" - : "dialog_filters_chats_limit_default"), - premium ? 200 : 100); -} - TypeRow::TypeRow(Flag flag) : PeerListRow(TypeId(flag)) { } @@ -309,7 +293,7 @@ object_ptr CreatePeerListSectionSubtitle( rpl::producer text) { auto result = object_ptr( parent, - st::searchedBarHeight); + st::windowFilterChatsSectionSubtitleHeight); const auto raw = result.data(); raw->paintRequest( @@ -338,14 +322,18 @@ EditFilterChatsListController::EditFilterChatsListController( rpl::producer title, Flags options, Flags selected, - const base::flat_set> &peers) + const base::flat_set> &peers, + LimitBoxFactory limitBox) : ChatsListBoxController(session) , _session(session) +, _limitBox(std::move(limitBox)) , _title(std::move(title)) , _peers(peers) -, _options(options) +, _options(options & ~Flag::Chatlist) , _selected(selected) -, _limit(Limit(session)) { +, _limit(Data::PremiumLimits(session).dialogFiltersChatsCurrent()) +, _chatlist(options & Flag::Chatlist) { + Expects(_limitBox != nullptr); } Main::Session &EditFilterChatsListController::session() const { @@ -353,8 +341,11 @@ Main::Session &EditFilterChatsListController::session() const { } int EditFilterChatsListController::selectedTypesCount() const { - Expects(_typesDelegate != nullptr); + Expects(_chatlist || _typesDelegate != nullptr); + if (_chatlist) { + return 0; + } auto result = 0; for (auto i = 0; i != _typesDelegate->peerListFullRowsCount(); ++i) { if (_typesDelegate->peerListRowAt(i)->checked()) { @@ -371,8 +362,7 @@ void EditFilterChatsListController::rowClicked(not_null row) { delegate()->peerListSetRowChecked(row, !row->checked()); updateTitle(); } else { - delegate()->peerListShowBox( - Box(FilterChatsLimitBox, _session, count)); + delegate()->peerListShowBox(_limitBox(count)); } } @@ -396,7 +386,9 @@ bool EditFilterChatsListController::handleDeselectForeignRow( void EditFilterChatsListController::prepareViewHook() { delegate()->peerListSetTitle(std::move(_title)); - delegate()->peerListSetAboveWidget(prepareTypesList()); + if (!_chatlist) { + delegate()->peerListSetAboveWidget(prepareTypesList()); + } const auto count = int(_peers.size()); const auto rows = std::make_unique[]>(count); diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index f6a658672..a9dfd3fa2 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -43,13 +43,15 @@ class EditFilterChatsListController final : public ChatsListBoxController { public: using Flag = Data::ChatFilter::Flag; using Flags = Data::ChatFilter::Flags; + using LimitBoxFactory = Fn(int)>; EditFilterChatsListController( not_null session, rpl::producer title, Flags options, Flags selected, - const base::flat_set> &peers); + const base::flat_set> &peers, + LimitBoxFactory limitBox); [[nodiscard]] Main::Session &session() const override; [[nodiscard]] Flags chosenOptions() const { @@ -70,11 +72,13 @@ private: void updateTitle(); const not_null _session; + const LimitBoxFactory _limitBox; rpl::producer _title; base::flat_set> _peers; Flags _options; Flags _selected; int _limit = 0; + bool _chatlist = false; Fn _deselectOption; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp new file mode 100644 index 000000000..d7e2c0920 --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -0,0 +1,1202 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/filters/edit_filter_links.h" + +#include "apiwrap.h" +#include "boxes/peers/edit_peer_invite_link.h" // InviteLinkQrBox. +#include "boxes/peer_list_box.h" +#include "boxes/premium_limits_box.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_chat_filters.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/invite_link_buttons.h" +#include "ui/controls/invite_link_label.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/input_fields.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +#include + +namespace { + +constexpr auto kMaxLinkTitleLength = 32; + +using InviteLinkData = Data::ChatFilterLink; +class LinkRow; + +enum class Color { + Permanent, + + Count, +}; + +struct InviteLinkAction { + enum class Type { + Copy, + Share, + Edit, + Delete, + }; + QString link; + Type type = Type::Copy; +}; + +struct Errors { + QString status; + QString toast; +}; + +[[nodiscard]] std::optional ErrorForSharing( + not_null history) { + const auto result = [](const QString &status, const QString &toast) { + return Errors{ status, toast }; + }; + const auto peer = history->peer; + if (const auto user = peer->asUser()) { + return user->isBot() + ? result( + tr::lng_filters_link_bot_status(tr::now), + tr::lng_filters_link_bot_error(tr::now)) + : result( + tr::lng_filters_link_private_status(tr::now), + tr::lng_filters_link_private_error(tr::now)); + } else if (const auto chat = history->peer->asChat()) { + if (!chat->canHaveInviteLink()) { + return result( + tr::lng_filters_link_noadmin_status(tr::now), + tr::lng_filters_link_noadmin_group_error(tr::now)); + } + return std::nullopt; + } else if (const auto channel = history->peer->asChannel()) { + if (!channel->canHaveInviteLink() + && (!channel->hasUsername() || channel->requestToJoin())) { + return result( + tr::lng_filters_link_noadmin_status(tr::now), + (channel->isMegagroup() + ? tr::lng_filters_link_noadmin_group_error(tr::now) + : tr::lng_filters_link_noadmin_channel_error(tr::now))); + } + return std::nullopt; + } + Unexpected("Peer type in ErrorForSharing."); +} + +void ShowSaveError( + not_null window, + QString error) { + const auto session = &window->session(); + if (error == u"CHATLISTS_TOO_MUCH"_q) { + window->show(Box(ShareableFiltersLimitBox, session)); + } else if (error == u"INVITES_TOO_MUCH"_q) { + window->show(Box(FilterLinksLimitBox, session)); + } else if (error == u"CHANNELS_TOO_MUCH"_q) { + window->show(Box(ChannelsLimitBox, session)); + } else if (error == u"USER_CHANNELS_TOO_MUCH"_q) { + window->showToast( + { tr::lng_filters_link_group_admin_error(tr::now) }); + } else { + window->showToast(error); + } +} + +void ShowEmptyLinkError(not_null window) { + ShowSaveError(window, tr::lng_filters_empty(tr::now)); +} + +void ChatFilterLinkBox( + not_null box, + not_null session, + Data::ChatFilterLink data) { + using namespace rpl::mappers; + + const auto link = data.url; + box->setTitle(tr::lng_group_invite_edit_title()); + + const auto container = box->verticalLayout(); + const auto labelField = container->add( + object_ptr( + container, + st::defaultInputField, + tr::lng_group_invite_label_header(), + data.title), + style::margins( + st::settingsSubsectionTitlePadding.left(), + st::settingsSectionSkip, + st::settingsSubsectionTitlePadding.right(), + st::settingsSectionSkip * 2)); + labelField->setMaxLength(kMaxLinkTitleLength); + Settings::AddDivider(container); + + box->setFocusCallback([=] { + labelField->setFocusFast(); + }); + + const auto &saveLabel = link.isEmpty() + ? tr::lng_formatting_link_create + : tr::lng_settings_save; + box->addButton(saveLabel(), [=] { + session->data().chatsFilters().edit( + data.id, + data.url, + labelField->getLastText().trimmed()); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +class LinkRowDelegate { +public: + virtual void rowUpdateRow(not_null row) = 0; + virtual void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) = 0; +}; + +class LinkRow final : public PeerListRow { +public: + LinkRow(not_null delegate, const InviteLinkData &data); + + void update(const InviteLinkData &data); + + [[nodiscard]] InviteLinkData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + +private: + const not_null _delegate; + InviteLinkData _data; + QString _status; + Color _color = Color::Permanent; + +}; + +class ChatRow final : public PeerListRow { +public: + ChatRow(not_null peer, const QString &status, bool disabled); + + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + +private: + const bool _disabled = false; + QImage _disabledFrame; + InMemoryKey _userpicKey; + int _paletteVersion = 0; + +}; + +[[nodiscard]] uint64 ComputeRowId(const QString &link) { + return XXH64(link.data(), link.size() * sizeof(ushort), 0); +} + +[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) { + return ComputeRowId(data.url); +} + +[[nodiscard]] Color ComputeColor(const InviteLinkData &link) { + return Color::Permanent; +} + +[[nodiscard]] QString ComputeStatus(const InviteLinkData &link) { + return tr::lng_filters_chats_count(tr::now, lt_count, link.chats.size()); +} + +LinkRow::LinkRow( + not_null delegate, + const InviteLinkData &data) +: PeerListRow(ComputeRowId(data)) +, _delegate(delegate) +, _data(data) +, _color(ComputeColor(data)) { + setCustomStatus(ComputeStatus(data)); +} + +void LinkRow::update(const InviteLinkData &data) { + _data = data; + _color = ComputeColor(data); + setCustomStatus(ComputeStatus(data)); + refreshName(st::inviteLinkList.item); + _delegate->rowUpdateRow(this); +} + +InviteLinkData LinkRow::data() const { + return _data; +} + +QString LinkRow::generateName() { + if (!_data.title.isEmpty()) { + return _data.title; + } + auto result = _data.url; + return result.replace( + u"https://"_q, + QString() + ).replace( + u"t.me/+"_q, + QString() + ).replace( + u"t.me/joinchat/"_q, + QString() + ); +} + +QString LinkRow::generateShortName() { + return generateName(); +} + +PaintRoundImageCallback LinkRow::generatePaintUserpicCallback( + bool forceRound) { + return [=]( + QPainter &p, + int x, + int y, + int outerWidth, + int size) { + _delegate->rowPaintIcon(p, x, y, size, _color); + }; +} + +QSize LinkRow::rightActionSize() const { + return QSize( + st::inviteLinkThreeDotsIcon.width(), + st::inviteLinkThreeDotsIcon.height()); +} + +QMargins LinkRow::rightActionMargins() const { + return QMargins( + 0, + (st::inviteLinkList.item.height - rightActionSize().height()) / 2, + st::inviteLinkThreeDotsSkip, + 0); +} + +void LinkRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + (actionSelected + ? st::inviteLinkThreeDotsIconOver + : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); +} + +ChatRow::ChatRow( + not_null peer, + const QString &status, + bool disabled) +: PeerListRow(peer) +, _disabled(disabled) { + if (!status.isEmpty()) { + setCustomStatus(status); + } +} + +PaintRoundImageCallback ChatRow::generatePaintUserpicCallback( + bool forceRound) { + const auto peer = this->peer(); + const auto saved = peer->isSelf(); + const auto replies = peer->isRepliesChat(); + auto userpic = (saved || replies) + ? Ui::PeerUserpicView() + : ensureUserpicView(); + auto paint = [=]( + Painter &p, + int x, + int y, + int outerWidth, + int size) mutable { + if (forceRound && peer->isForum()) { + ForceRoundUserpicCallback(peer)(p, x, y, outerWidth, size); + } else if (saved) { + Ui::EmptyUserpic::PaintSavedMessages(p, x, y, outerWidth, size); + } else if (replies) { + Ui::EmptyUserpic::PaintRepliesMessages(p, x, y, outerWidth, size); + } else { + peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + } + }; + if (!_disabled) { + return paint; + } + return [=]( + Painter &p, + int x, + int y, + int outerWidth, + int size) mutable { + const auto wide = size + style::ConvertScale(3); + const auto full = QSize(wide, wide) * style::DevicePixelRatio(); + auto repaint = false; + if (_disabledFrame.size() != full) { + repaint = true; + _disabledFrame = QImage( + full, + QImage::Format_ARGB32_Premultiplied); + _disabledFrame.setDevicePixelRatio(style::DevicePixelRatio()); + } else { + repaint = (_paletteVersion != style::PaletteVersion()) + || (!saved + && !replies + && (_userpicKey != peer->userpicUniqueKey(userpic))); + } + if (repaint) { + _paletteVersion = style::PaletteVersion(); + _userpicKey = peer->userpicUniqueKey(userpic); + + _disabledFrame.fill(Qt::transparent); + auto p = Painter(&_disabledFrame); + paint(p, 0, 0, wide, size); + + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::boxBg); + p.setPen(Qt::NoPen); + const auto two = style::ConvertScaleExact(2.5); + const auto half = size / 2.; + const auto rect = QRectF(half, half, half, half).translated( + { two, two }); + p.drawEllipse(rect); + + auto pen = st::windowSubTextFg->p; + const auto width = style::ConvertScaleExact(1.5); + const auto dash = 0.55; + const auto dashWithCaps = dash + 1.; + pen.setWidthF(width); + // 11 parts = M_PI * half / ((dashWithCaps + space) * width) + // 11 = M_PI * half / ((dashWithCaps + space) * width) + // space = (M_PI * half / (11 * width)) - dashWithCaps + const auto space = M_PI * half / (11 * width) - dashWithCaps; + pen.setDashPattern(QVector{ dash, space }); + pen.setDashOffset(1.); + pen.setCapStyle(Qt::RoundCap); + p.setBrush(Qt::NoBrush); + p.setPen(pen); + p.drawEllipse(rect.marginsRemoved({ two, two, two, two })); + } + p.drawImage(x, y, _disabledFrame); + }; +} + +class LinksController final + : public PeerListController + , public LinkRowDelegate + , public base::has_weak_ptr { +public: + LinksController( + not_null window, + rpl::producer> content, + Fn currentFilter); + + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + Main::Session &session() const override; + + void rowUpdateRow(not_null row) override; + void rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) override; + +private: + void appendRow(const InviteLinkData &data); + + void rebuild(const std::vector &rows); + + [[nodiscard]] base::unique_qptr createRowContextMenu( + QWidget *parent, + not_null row); + + const not_null _window; + Fn _currentFilter; + rpl::variable> _rows; + base::unique_qptr _menu; + + std::array _icons; + rpl::lifetime _lifetime; + +}; + +class LinkController final + : public PeerListController + , public base::has_weak_ptr { +public: + LinkController( + not_null window, + const Data::ChatFilter &filter, + InviteLinkData data); + + void prepare() override; + void rowClicked(not_null row) override; + Main::Session &session() const override; + + void showFinished() override; + + [[nodiscard]] rpl::producer hasChangesValue() const; + [[nodiscard]] base::flat_set> selected() const; + +private: + void setupAboveWidget(); + void setupBelowWidget(); + void addHeader(not_null container); + void addLinkBlock(not_null container); + void toggleAllSelected(bool select); + + const not_null _window; + InviteLinkData _data; + + QString _filterTitle; + base::flat_set> _filterChats; + base::flat_map, QString> _denied; + rpl::variable>> _selected; + base::flat_set> _initial; + + base::unique_qptr _menu; + + QString _link; + + rpl::variable _hasChanges = false; + + rpl::event_stream<> _showFinished; + + rpl::lifetime _lifetime; + +}; + +LinkController::LinkController( + not_null window, + const Data::ChatFilter &filter, + InviteLinkData data) +: _window(window) +, _filterTitle(filter.title()) +, _filterChats(filter.always()) { + _data = std::move(data); + _link = _data.url; +} + +void LinkController::addHeader(not_null container) { + using namespace Settings; + + const auto divider = Ui::CreateChild( + container.get()); + const auto verticalLayout = container->add( + object_ptr(container.get())); + + auto icon = CreateLottieIcon( + verticalLayout, + { + .name = u"cloud_filters"_q, + .sizeOverride = { + st::settingsFilterIconSize, + st::settingsFilterIconSize, + }, + }, + st::settingsFilterIconPadding); + _showFinished.events( + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::once); + }, verticalLayout->lifetime()); + verticalLayout->add(std::move(icon.widget)); + + verticalLayout->add( + object_ptr>( + verticalLayout, + object_ptr( + verticalLayout, + (_data.url.isEmpty() + ? tr::lng_filters_link_no_about(Ui::Text::WithEntities) + : tr::lng_filters_link_share_about( + lt_folder, + rpl::single(Ui::Text::Bold(_filterTitle)), + Ui::Text::WithEntities)), + st::settingsFilterDividerLabel)), + st::filterLinkDividerLabelPadding); + + verticalLayout->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + divider->setGeometry(r); + }, divider->lifetime()); +} + +object_ptr DeleteLinkBox( + not_null window, + const InviteLinkData &link) { + const auto sure = [=](Fn &&close) { + window->session().data().chatsFilters().destroy(link.id, link.url); + close(); + }; + return Ui::MakeConfirmBox({ + .text = tr::lng_filters_link_delete_sure(tr::now), + .confirmed = sure, + .confirmText = tr::lng_box_delete(tr::now), + }); +} + +void LinkController::addLinkBlock(not_null container) { + using namespace Settings; + + const auto link = _data.url; + const auto weak = Ui::MakeWeak(container); + const auto copyLink = crl::guard(weak, [=] { + CopyInviteLink(delegate()->peerListUiShow(), link); + }); + const auto shareLink = crl::guard(weak, [=] { + delegate()->peerListShowBox( + ShareInviteLinkBox(&_window->session(), link)); + }); + const auto getLinkQr = crl::guard(weak, [=] { + delegate()->peerListShowBox( + InviteLinkQrBox(link, tr::lng_filters_link_qr_about())); + }); + const auto editLink = crl::guard(weak, [=] { + delegate()->peerListShowBox( + Box(ChatFilterLinkBox, &_window->session(), _data)); + }); + const auto deleteLink = crl::guard(weak, [=] { + delegate()->peerListShowBox(DeleteLinkBox(_window, _data)); + }); + + const auto createMenu = [=] { + auto result = base::make_unique_q( + container, + st::popupMenuWithIcons); + result->addAction( + tr::lng_group_invite_context_copy(tr::now), + copyLink, + &st::menuIconCopy); + result->addAction( + tr::lng_group_invite_context_share(tr::now), + shareLink, + &st::menuIconShare); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + getLinkQr, + &st::menuIconQrCode); + result->addAction( + tr::lng_filters_link_name_it(tr::now), + editLink, + &st::menuIconEdit); + result->addAction( + tr::lng_group_invite_context_delete(tr::now), + deleteLink, + &st::menuIconDelete); + return result; + }; + AddSubsectionTitle( + container, + tr::lng_filters_link_subtitle(), + st::filterLinkSubsectionTitlePadding); + + const auto prefix = u"https://"_q; + const auto label = container->lifetime().make_state( + container, + rpl::single(link.startsWith(prefix) + ? link.mid(prefix.size()) + : link), + createMenu); + container->add( + label->take(), + st::inviteLinkFieldPadding); + + label->clicks( + ) | rpl::start_with_next(copyLink, label->lifetime()); + + AddCopyShareLinkButtons(container, copyLink, shareLink); + + AddSkip(container, st::inviteLinkJoinedRowPadding.bottom() * 2); + + AddSkip(container); + + AddDivider(container); +} + +void LinkController::prepare() { + Expects(!_data.url.isEmpty() || _data.chats.empty()); + + for (const auto &history : _data.chats) { + const auto peer = history->peer; + auto row = std::make_unique( + peer, + FilterChatStatusText(peer), + false); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListSetRowChecked(raw, true); + raw->finishCheckedAnimation(); + _initial.emplace(peer); + } + for (const auto &history : _filterChats) { + if (delegate()->peerListFindRow(history->peer->id.value)) { + continue; + } + const auto peer = history->peer; + const auto error = ErrorForSharing(history); + auto row = std::make_unique( + peer, + error ? error->status : FilterChatStatusText(peer), + error.has_value()); + delegate()->peerListAppendRow(std::move(row)); + if (error) { + _denied.emplace(peer, error->toast); + } else if (_data.url.isEmpty()) { + _denied.emplace(peer); + } + } + setupAboveWidget(); + setupBelowWidget(); + delegate()->peerListRefreshRows(); + _selected = _initial; +} + +void LinkController::rowClicked(not_null row) { + const auto peer = row->peer(); + if (const auto i = _denied.find(peer); i != end(_denied)) { + if (!i->second.isEmpty()) { + delegate()->peerListUiShow()->showToast(i->second); + } + } else { + const auto checked = row->checked(); + auto selected = _selected.current(); + delegate()->peerListSetRowChecked(row, !checked); + if (checked) { + selected.remove(peer); + } else { + selected.emplace(peer); + } + const auto has = (_initial != selected); + _selected = std::move(selected); + _hasChanges = has; + } +} + +void LinkController::toggleAllSelected(bool select) { + auto selected = _selected.current(); + if (!select) { + if (selected.empty()) { + return; + } + for (const auto &peer : selected) { + const auto row = delegate()->peerListFindRow(peer->id.value); + Assert(row != nullptr); + delegate()->peerListSetRowChecked(row, false); + } + selected = {}; + } else { + const auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + const auto row = delegate()->peerListRowAt(i); + const auto peer = row->peer(); + if (!_denied.contains(peer)) { + delegate()->peerListSetRowChecked(row, true); + selected.emplace(peer); + } + } + } + const auto has = (_initial != selected); + _selected = std::move(selected); + _hasChanges = has; +} + +void LinkController::showFinished() { + _showFinished.fire({}); +} + +void LinkController::setupAboveWidget() { + using namespace Settings; + + auto wrap = object_ptr((QWidget*)nullptr); + const auto container = wrap.data(); + + addHeader(container); + if (!_data.url.isEmpty()) { + addLinkBlock(container); + } + + auto subtitle = _selected.value( + ) | rpl::map([=](const base::flat_set> &selected) { + return _data.url.isEmpty() + ? tr::lng_filters_link_chats_no(tr::now) + : selected.empty() + ? tr::lng_filters_link_chats_none(tr::now) + : tr::lng_filters_link_chats( + tr::now, + lt_count, + float64(selected.size())); + }); + const auto mayBeSelected = delegate()->peerListFullRowsCount() + - int(_denied.size()); + auto selectedCount = _selected.value( + ) | rpl::map([](const base::flat_set> &selected) { + return int(selected.size()); + }); + AddFilterSubtitleWithToggles( + container, + std::move(subtitle), + mayBeSelected, + std::move(selectedCount), + [=](bool select) { toggleAllSelected(select); }); + + // Fix label cutting on text change from smaller to longer. + _selected.changes() | rpl::start_with_next([=] { + container->resizeToWidth(container->widthNoMargins()); + }, container->lifetime()); + + delegate()->peerListSetAboveWidget(std::move(wrap)); +} + +void LinkController::setupBelowWidget() { + delegate()->peerListSetBelowWidget( + object_ptr( + (QWidget*)nullptr, + object_ptr( + (QWidget*)nullptr, + (_data.url.isEmpty() + ? tr::lng_filters_link_chats_no_about() + : tr::lng_filters_link_chats_about()), + st::boxDividerLabel), + st::settingsDividerLabelPadding)); +} + +Main::Session &LinkController::session() const { + return _window->session(); +} + +rpl::producer LinkController::hasChangesValue() const { + return _hasChanges.value(); +} + +base::flat_set> LinkController::selected() const { + return _selected.current(); +} + +LinksController::LinksController( + not_null window, + rpl::producer> content, + Fn currentFilter) +: _window(window) +, _currentFilter(std::move(currentFilter)) +, _rows(std::move(content)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + for (auto &image : _icons) { + image = QImage(); + } + }, _lifetime); +} + +void LinksController::prepare() { + _rows.value( + ) | rpl::start_with_next([=](const std::vector &rows) { + rebuild(rows); + }, _lifetime); +} + +void LinksController::rebuild(const std::vector &rows) { + auto i = 0; + auto count = delegate()->peerListFullRowsCount(); + while (i < rows.size()) { + if (i < count) { + const auto row = delegate()->peerListRowAt(i); + static_cast(row.get())->update(rows[i]); + } else { + appendRow(rows[i]); + } + ++i; + } + while (i < count) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(i)); + --count; + } + delegate()->peerListRefreshRows(); +} + +void LinksController::rowClicked(not_null row) { + const auto link = static_cast(row.get())->data(); + delegate()->peerListShowBox( + ShowLinkBox(_window, _currentFilter(), link)); +} + +void LinksController::rowRightActionClicked(not_null row) { + delegate()->peerListShowRowMenu(row, true); +} + +base::unique_qptr LinksController::rowContextMenu( + QWidget *parent, + not_null row) { + auto result = createRowContextMenu(parent, row); + + if (result) { + // First clear _menu value, so that we don't check row positions yet. + base::take(_menu); + + // Here unique_qptr is used like a shared pointer, where + // not the last destroyed pointer destroys the object, but the first. + _menu = base::unique_qptr(result.get()); + } + + return result; +} + +base::unique_qptr LinksController::createRowContextMenu( + QWidget *parent, + not_null row) { + const auto real = static_cast(row.get()); + const auto data = real->data(); + const auto link = data.url; + const auto copyLink = [=] { + CopyInviteLink(delegate()->peerListUiShow(), link); + }; + const auto shareLink = [=] { + delegate()->peerListShowBox( + ShareInviteLinkBox(&_window->session(), link)); + }; + const auto getLinkQr = [=] { + delegate()->peerListShowBox( + InviteLinkQrBox(link, tr::lng_filters_link_qr_about())); + }; + const auto editLink = [=] { + delegate()->peerListShowBox( + Box(ChatFilterLinkBox, &_window->session(), data)); + }; + const auto deleteLink = [=] { + delegate()->peerListShowBox(DeleteLinkBox(_window, data)); + }; + auto result = base::make_unique_q( + parent, + st::popupMenuWithIcons); + result->addAction( + tr::lng_group_invite_context_copy(tr::now), + copyLink, + &st::menuIconCopy); + result->addAction( + tr::lng_group_invite_context_share(tr::now), + shareLink, + &st::menuIconShare); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + getLinkQr, + &st::menuIconQrCode); + result->addAction( + tr::lng_filters_link_name_it(tr::now), + editLink, + &st::menuIconEdit); + result->addAction( + tr::lng_group_invite_context_delete(tr::now), + deleteLink, + &st::menuIconDelete); + return result; +} + +Main::Session &LinksController::session() const { + return _window->session(); +} + +void LinksController::appendRow(const InviteLinkData &data) { + delegate()->peerListAppendRow(std::make_unique(this, data)); +} + +void LinksController::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + +void LinksController::rowPaintIcon( + QPainter &p, + int x, + int y, + int size, + Color color) { + const auto skip = st::inviteLinkIconSkip; + const auto inner = size - 2 * skip; + const auto bg = [&] { + switch (color) { + case Color::Permanent: return &st::msgFile1Bg; + } + Unexpected("Color in LinksController::rowPaintIcon."); + }(); + auto &icon = _icons[int(color)]; + if (icon.isNull()) { + icon = QImage( + QSize(inner, inner) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + icon.fill(Qt::transparent); + icon.setDevicePixelRatio(style::DevicePixelRatio()); + + auto p = QPainter(&icon); + p.setPen(Qt::NoPen); + p.setBrush(*bg); + { + auto hq = PainterHighQualityEnabler(p); + p.drawEllipse(QRect(0, 0, inner, inner)); + } + st::inviteLinkIcon.paintInCenter(p, { 0, 0, inner, inner }); + } + p.drawImage(x + skip, y + skip, icon); +} + +} // namespace + +std::vector> CollectFilterLinkChats( + const Data::ChatFilter &filter) { + return filter.always() | ranges::views::filter([]( + not_null history) { + return !ErrorForSharing(history); + }) | ranges::views::transform(&History::peer) | ranges::to_vector; +} + +bool GoodForExportFilterLink( + not_null window, + const Data::ChatFilter &filter) { + using Flag = Data::ChatFilter::Flag; + const auto listflags = Flag::Chatlist | Flag::HasMyLinks; + if (!filter.never().empty() || (filter.flags() & ~listflags)) { + window->showToast(tr::lng_filters_link_cant(tr::now)); + return false; + } + return true; +} + +void ExportFilterLink( + FilterId id, + const std::vector> &peers, + Fn done, + Fn fail) { + Expects(!peers.empty()); + + const auto front = peers.front(); + const auto session = &front->session(); + auto mtpPeers = peers | ranges::views::transform( + [](not_null peer) { return MTPInputPeer(peer->input); } + ) | ranges::to>(); + session->api().request(MTPchatlists_ExportChatlistInvite( + MTP_inputChatlistDialogFilter(MTP_int(id)), + MTP_string(), // title + MTP_vector(std::move(mtpPeers)) + )).done([=](const MTPchatlists_ExportedChatlistInvite &result) { + const auto &data = result.data(); + session->data().chatsFilters().apply(MTP_updateDialogFilter( + MTP_flags(MTPDupdateDialogFilter::Flag::f_filter), + MTP_int(id), + data.vfilter())); + const auto link = session->data().chatsFilters().add( + id, + data.vinvite()); + done(link); + }).fail([=](const MTP::Error &error) { + fail(error.type()); + }).send(); +} + +void EditLinkChats( + const Data::ChatFilterLink &link, + base::flat_set> peers, + Fn done) { + Expects(!peers.empty()); + Expects(link.id != 0); + Expects(!link.url.isEmpty()); + + const auto id = link.id; + const auto front = peers.front(); + const auto session = &front->session(); + auto mtpPeers = peers | ranges::views::transform( + [](not_null peer) { return MTPInputPeer(peer->input); } + ) | ranges::to>(); + session->api().request(MTPchatlists_EditExportedInvite( + MTP_flags(MTPchatlists_EditExportedInvite::Flag::f_peers), + MTP_inputChatlistDialogFilter(MTP_int(link.id)), + MTP_string(link.url), + MTPstring(), // title + MTP_vector(std::move(mtpPeers)) + )).done([=](const MTPExportedChatlistInvite &result) { + const auto link = session->data().chatsFilters().add(id, result); + done(QString()); + }).fail([=](const MTP::Error &error) { + done(error.type()); + }).send(); +} + +object_ptr ShowLinkBox( + not_null window, + const Data::ChatFilter &filter, + const Data::ChatFilterLink &link) { + auto controller = std::make_unique(window, filter, link); + controller->setStyleOverrides(&st::inviteLinkChatList); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + box->setTitle(!link.title.isEmpty() + ? rpl::single(link.title) + : tr::lng_filters_link_title()); + + const auto saving = std::make_shared(false); + raw->hasChangesValue( + ) | rpl::start_with_next([=](bool has) { + box->setCloseByOutsideClick(!has); + box->setCloseByEscape(!has); + box->clearButtons(); + if (has) { + box->addButton(tr::lng_settings_save(), [=] { + if (*saving) { + return; + } + const auto chosen = raw->selected(); + if (chosen.empty()) { + ShowEmptyLinkError(window); + } else { + *saving = true; + EditLinkChats(link, chosen, crl::guard(box, [=]( + QString error) { + *saving = false; + if (error.isEmpty()) { + box->closeBox(); + } else { + ShowSaveError(window, error); + } + })); + } + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + } else { + box->addButton(tr::lng_about_done(), [=] { + box->closeBox(); + }); + } + }, box->lifetime()); + }; + return Box(std::move(controller), std::move(initBox)); +} + +QString FilterChatStatusText(not_null peer) { + if (const auto chat = peer->asChat()) { + if (const auto count = chat->count; count > 0) { + return tr::lng_chat_status_members(tr::now, lt_count, count); + } + } else if (const auto channel = peer->asChannel()) { + if (channel->membersCountKnown()) { + return (channel->isBroadcast() + ? tr::lng_chat_status_subscribers + : tr::lng_chat_status_members)( + tr::now, + lt_count, + channel->membersCount()); + } + } + return QString(); +} + +void SetupFilterLinks( + not_null container, + not_null window, + rpl::producer> value, + Fn currentFilter) { + auto &lifetime = container->lifetime(); + const auto delegate = lifetime.make_state( + window->uiShow()); + const auto controller = lifetime.make_state( + window, + std::move(value), + std::move(currentFilter)); + controller->setStyleOverrides(&st::inviteLinkList); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); +} + +void AddFilterSubtitleWithToggles( + not_null container, + rpl::producer text, + int selectableCount, + rpl::producer selectedCount, + Fn toggle) { + using namespace rpl::mappers; + + const auto selectable = (selectableCount > 0); + auto padding = st::filterLinkSubsectionTitlePadding; + if (selectable) { + const auto font = st::boxLinkButton.font; + padding.setRight(padding.right() + font->spacew + std::max( + font->width(tr::lng_filters_by_link_select(tr::now)), + font->width(tr::lng_filters_by_link_deselect(tr::now)))); + } + const auto title = Settings::AddSubsectionTitle( + container, + std::move(text), + padding); + if (!selectable) { + return; + } + const auto link = Ui::CreateChild( + container.get(), + tr::lng_filters_by_link_select(tr::now), + st::boxLinkButton); + const auto canSelect = link->lifetime().make_state>( + std::move(selectedCount) | rpl::map(_1 < selectableCount)); + canSelect->value( + ) | rpl::start_with_next([=](bool can) { + link->setText(can + ? tr::lng_filters_by_link_select(tr::now) + : tr::lng_filters_by_link_deselect(tr::now)); + }, link->lifetime()); + link->setClickedCallback([=] { + toggle(canSelect->current()); + }); + + rpl::combine( + container->widthValue(), + title->topValue(), + link->widthValue() + ) | rpl::start_with_next([=](int outer, int y, int width) { + link->move(outer - st::boxRowPadding.right() - width, y); + }, link->lifetime()); +} + +std::unique_ptr MakeFilterChatRow( + not_null peer, + const QString &status, + bool disabled) { + return std::make_unique(peer, status, disabled); +} diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.h b/Telegram/SourceFiles/boxes/filters/edit_filter_links.h new file mode 100644 index 000000000..4cc88f95c --- /dev/null +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.h @@ -0,0 +1,63 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +class PeerListRow; + +namespace Ui { +class Show; +class BoxContent; +class VerticalLayout; +} // namespace Ui + +namespace Data { +class ChatFilter; +struct ChatFilterLink; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +[[nodiscard]] std::vector> CollectFilterLinkChats( + const Data::ChatFilter &filter); +[[nodiscard]] bool GoodForExportFilterLink( + not_null window, + const Data::ChatFilter &filter); + +void ExportFilterLink( + FilterId id, + const std::vector> &peers, + Fn done, + Fn fail); + +[[nodiscard]] object_ptr ShowLinkBox( + not_null window, + const Data::ChatFilter &filter, + const Data::ChatFilterLink &link); +[[nodiscard]] QString FilterChatStatusText(not_null peer); + +void SetupFilterLinks( + not_null container, + not_null window, + rpl::producer> value, + Fn currentFilter); + +void AddFilterSubtitleWithToggles( + not_null container, + rpl::producer text, + int selectableCount, + rpl::producer selectedCount, + Fn toggle); + +[[nodiscard]] std::unique_ptr MakeFilterChatRow( + not_null peer, + const QString &status, + bool disabled); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 199ee4cad..6720339c2 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -156,14 +156,15 @@ void GiftBox( // List. const auto group = std::make_shared(); - group->setChangedCallback([=](int value) { + const auto groupValueChangedCallback = [=](int value) { Expects(value < options.size() && value >= 0); auto text = tr::lng_premium_gift_button( tr::now, lt_cost, options[value].costTotal); state->buttonText.fire(std::move(text)); - }); + }; + group->setChangedCallback(groupValueChangedCallback); Ui::Premium::AddGiftOptions( buttonsParent, group, @@ -215,7 +216,7 @@ void GiftBox( }); box->addButton(std::move(button)); - group->setValue(0); + groupValueChangedCallback(0); Data::PeerPremiumValue( user diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 7ce2731d1..dca50a08c 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1248,7 +1248,7 @@ void LanguageBox::setupTop(not_null container) { st::settingsButtonNoIcon); translateSkip->setClickedCallback([=] { - Ui::BoxShow(this).showBox(Ui::EditSkipTranslationLanguages()); + uiShow()->showBox(Ui::EditSkipTranslationLanguages()); }); Settings::AddSkip(container); Settings::AddDividerText( diff --git a/Telegram/SourceFiles/boxes/max_invite_box.cpp b/Telegram/SourceFiles/boxes/max_invite_box.cpp index 7c8f29092..b6230b673 100644 --- a/Telegram/SourceFiles/boxes/max_invite_box.cpp +++ b/Telegram/SourceFiles/boxes/max_invite_box.cpp @@ -91,9 +91,7 @@ void MaxInviteBox::mousePressEvent(QMouseEvent *e) { if (_linkOver) { if (!_channel->inviteLink().isEmpty()) { QGuiApplication::clipboard()->setText(_channel->inviteLink()); - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - tr::lng_create_channel_link_copied(tr::now)); + showToast(tr::lng_create_channel_link_copied(tr::now)); } else if (_channel->isFullLoaded() && !_creatingInviteLink) { _creatingInviteLink = true; _channel->session().api().inviteLinks().create(_channel); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index eda808ae6..6f27553d0 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -7,15 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peer_list_box.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "mainwidget.h" +#include "ui/effects/loading_element.h" +#include "ui/effects/outline_segments.h" +#include "ui/effects/round_checkbox.h" +#include "ui/effects/ripple_animation.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" -#include "ui/effects/loading_element.h" -#include "ui/effects/round_checkbox.h" -#include "ui/effects/ripple_animation.h" #include "ui/empty_userpic.h" #include "ui/wrap/slide_wrap.h" #include "ui/text/text_options.h" @@ -32,8 +34,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" #include "styles/style_widgets.h" -#include - PaintRoundImageCallback PaintUserpicCallback( not_null peer, bool respectSavedMessagesChat) { @@ -74,7 +74,7 @@ PaintRoundImageCallback ForceRoundUserpicCallback(not_null peer) { } PeerListContentDelegateShow::PeerListContentDelegateShow( - std::shared_ptr show) + std::shared_ptr show) : _show(show) { } @@ -88,15 +88,16 @@ void PeerListContentDelegateShow::peerListHideLayer() { _show->hideLayer(); } -not_null PeerListContentDelegateShow::peerListToastParent() { - return _show->toastParent(); +auto PeerListContentDelegateShow::peerListUiShow() +-> std::shared_ptr{ + return _show; } PeerListBox::PeerListBox( QWidget*, std::unique_ptr controller, Fn)> init) -: _show(this) +: _show(Main::MakeSessionShow(uiShow(), &controller->session())) , _controller(std::move(controller)) , _init(std::move(init)) { Expects(_controller != nullptr); @@ -140,9 +141,14 @@ void PeerListBox::createMultiSelect() { void PeerListBox::setAddedTopScrollSkip(int skip) { _addedTopScrollSkip = skip; + _scrollBottomFixed = false; updateScrollSkips(); } +void PeerListBox::showFinished() { + _controller->showFinished(); +} + int PeerListBox::getTopScrollSkip() const { auto result = _addedTopScrollSkip; if (_select && !_select->isHidden()) { @@ -256,15 +262,23 @@ void PeerListBox::peerListSetRowChecked( not_null row, bool checked) { if (checked) { - addSelectItem(row, anim::type::normal); + if (_controller->trackSelectedList()) { + addSelectItem(row, anim::type::normal); + } PeerListContentDelegate::peerListSetRowChecked(row, checked); peerListUpdateRow(row); // This call deletes row from _searchRows. - _select->entity()->clearQuery(); + if (_select) { + _select->entity()->clearQuery(); + } } else { // The itemRemovedCallback will call changeCheckState() here. - _select->entity()->removeItem(row->id()); + if (_select) { + _select->entity()->removeItem(row->id()); + } else { + PeerListContentDelegate::peerListSetRowChecked(row, checked); + } peerListUpdateRow(row); } } @@ -306,18 +320,20 @@ void PeerListBox::peerListSetSearchMode(PeerListSearchMode mode) { void PeerListBox::peerListShowBox( object_ptr content, Ui::LayerOptions options) { - _show.showBox(std::move(content), options); + _show->showBox(std::move(content), options); } void PeerListBox::peerListHideLayer() { - _show.hideLayer(); + _show->hideLayer(); } -not_null PeerListBox::peerListToastParent() { - return _show.toastParent(); +std::shared_ptr PeerListBox::peerListUiShow() { + return _show; } -PeerListController::PeerListController(std::unique_ptr searchController) : _searchController(std::move(searchController)) { +PeerListController::PeerListController( + std::unique_ptr searchController) +: _searchController(std::move(searchController)) { if (_searchController) { _searchController->setDelegate(this); } @@ -873,9 +889,18 @@ void PeerListRow::createCheckbox( } void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { + Expects(!checked || _checkbox != nullptr); + + if (_checkbox) { + _checkbox->setChecked(checked, animated); + } +} + +void PeerListRow::setCustomizedCheckSegments( + std::vector segments) { Expects(_checkbox != nullptr); - _checkbox->setChecked(checked, animated); + _checkbox->setCustomizedSegments(std::move(segments)); } void PeerListRow::finishCheckedAnimation() { @@ -1117,6 +1142,24 @@ PeerListRow *PeerListContent::findRow(PeerListRowId id) { return (it == _rowsById.cend()) ? nullptr : it->second.get(); } +std::optional PeerListContent::lastRowMousePosition() const { + if (!_lastMousePosition) { + return std::nullopt; + } + const auto point = mapFromGlobal(*_lastMousePosition); + auto in = parentWidget()->rect().contains( + parentWidget()->mapFromGlobal(*_lastMousePosition)); + auto rowsPointY = point.y() - rowsTop(); + const auto index = (in + && rowsPointY >= 0 + && rowsPointY < shownRowsCount() * _rowHeight) + ? (rowsPointY / _rowHeight) + : -1; + return (index >= 0 && index == _selected.index.value) + ? QPoint(point.x(), rowsPointY) + : std::optional(); +} + void PeerListContent::removeRow(not_null row) { auto index = row->absoluteIndex(); auto isSearchResult = row->isSearchResult(); @@ -1216,18 +1259,14 @@ void PeerListContent::setSearchNoResults(object_ptr noResults) { } } -void PeerListContent::setAboveWidget(object_ptr widget) { +void PeerListContent::setAboveWidget(object_ptr widget) { _aboveWidget = std::move(widget); - if (_aboveWidget) { - _aboveWidget->setParent(this); - } + initDecorateWidget(_aboveWidget.data()); } -void PeerListContent::setAboveSearchWidget(object_ptr widget) { +void PeerListContent::setAboveSearchWidget(object_ptr widget) { _aboveSearchWidget = std::move(widget); - if (_aboveSearchWidget) { - _aboveSearchWidget->setParent(this); - } + initDecorateWidget(_aboveSearchWidget.data()); } void PeerListContent::setHideEmpty(bool hide) { @@ -1235,10 +1274,23 @@ void PeerListContent::setHideEmpty(bool hide) { resizeToWidth(width()); } -void PeerListContent::setBelowWidget(object_ptr widget) { +void PeerListContent::setBelowWidget(object_ptr widget) { _belowWidget = std::move(widget); - if (_belowWidget) { - _belowWidget->setParent(this); + initDecorateWidget(_belowWidget.data()); +} + +void PeerListContent::initDecorateWidget(Ui::RpWidget *widget) { + if (widget) { + widget->setParent(this); + widget->events( + ) | rpl::filter([=](not_null e) { + return (e->type() == QEvent::Enter) && widget->isVisible(); + }) | rpl::start_with_next([=] { + mouseLeftGeometry(); + }, widget->lifetime()); + widget->heightValue() | rpl::skip(1) | rpl::start_with_next([=] { + resizeToWidth(width()); + }, widget->lifetime()); } } @@ -1975,10 +2027,12 @@ void PeerListContent::setSearchQuery( bool PeerListContent::submitted() { if (const auto row = getRow(_selected.index)) { + _lastMousePosition = std::nullopt; _controller->rowClicked(row); return true; } else if (showingSearch()) { if (const auto row = getRow(RowIndex(0))) { + _lastMousePosition = std::nullopt; _controller->rowClicked(row); return true; } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 3a09aebf9..ac8ab554b 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -24,6 +24,7 @@ struct MultiSelect; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Ui { @@ -35,6 +36,7 @@ class SlideWrap; class FlatLabel; struct ScrollToRequest; class PopupMenu; +struct OutlineSegment; } // namespace Ui using PaintRoundImageCallback = Fn segments); void setHidden(bool hidden) { _hidden = hidden; } @@ -298,9 +302,9 @@ public: virtual void peerListSetHideEmpty(bool hide) = 0; virtual void peerListSetDescription(object_ptr description) = 0; virtual void peerListSetSearchNoResults(object_ptr noResults) = 0; - virtual void peerListSetAboveWidget(object_ptr aboveWidget) = 0; - virtual void peerListSetAboveSearchWidget(object_ptr aboveWidget) = 0; - virtual void peerListSetBelowWidget(object_ptr belowWidget) = 0; + virtual void peerListSetAboveWidget(object_ptr aboveWidget) = 0; + virtual void peerListSetAboveSearchWidget(object_ptr aboveWidget) = 0; + virtual void peerListSetBelowWidget(object_ptr belowWidget) = 0; virtual void peerListMouseLeftGeometry() = 0; virtual void peerListSetSearchMode(PeerListSearchMode mode) = 0; virtual void peerListAppendRow(std::unique_ptr row) = 0; @@ -323,13 +327,14 @@ public: virtual void peerListScrollToTop() = 0; virtual int peerListFullRowsCount() = 0; virtual PeerListRow *peerListFindRow(PeerListRowId id) = 0; + virtual std::optional peerListLastRowMousePosition() = 0; virtual void peerListSortRows(Fn compare) = 0; virtual int peerListPartitionRows(Fn border) = 0; virtual void peerListShowBox( object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) = 0; virtual void peerListHideLayer() = 0; - virtual not_null peerListToastParent() = 0; + virtual std::shared_ptr peerListUiShow() = 0; template void peerListAddSelectedPeers(PeerDataRange &&range) { @@ -447,6 +452,9 @@ public: virtual void prepare() = 0; + virtual void showFinished() { + } + virtual void rowClicked(not_null row) = 0; virtual void rowRightActionClicked(not_null row) { } @@ -496,6 +504,9 @@ public: return delegate()->peerListIsRowChecked(row); } + virtual bool trackSelectedList() { + return true; + } virtual bool searchInLocal() { return true; } @@ -605,6 +616,7 @@ public: void prependRow(std::unique_ptr row); void prependRowFromSearchResult(not_null row); PeerListRow *findRow(PeerListRowId id); + std::optional lastRowMousePosition() const; void updateRow(not_null row) { updateRow(row, RowIndex()); } @@ -615,9 +627,9 @@ public: void setDescription(object_ptr description); void setSearchLoading(object_ptr loading); void setSearchNoResults(object_ptr noResults); - void setAboveWidget(object_ptr widget); - void setAboveSearchWidget(object_ptr widget); - void setBelowWidget(object_ptr width); + void setAboveWidget(object_ptr widget); + void setAboveSearchWidget(object_ptr widget); + void setBelowWidget(object_ptr width); void setHideEmpty(bool hide); void refreshRows(); @@ -778,6 +790,7 @@ private: void clearAllContent(); void handleMouseMove(QPoint globalPosition); void mousePressReleased(Qt::MouseButton button); + void initDecorateWidget(Ui::RpWidget *widget); const style::PeerList &_st; not_null _controller; @@ -812,9 +825,9 @@ private: int _aboveHeight = 0; int _belowHeight = 0; bool _hideEmpty = false; - object_ptr _aboveWidget = { nullptr }; - object_ptr _aboveSearchWidget = { nullptr }; - object_ptr _belowWidget = { nullptr }; + object_ptr _aboveWidget = { nullptr }; + object_ptr _aboveSearchWidget = { nullptr }; + object_ptr _belowWidget = { nullptr }; object_ptr _description = { nullptr }; object_ptr _searchNoResults = { nullptr }; object_ptr _searchLoading = { nullptr }; @@ -858,6 +871,9 @@ public: PeerListRow *peerListFindRow(PeerListRowId id) override { return _content->findRow(id); } + std::optional peerListLastRowMousePosition() override { + return _content->lastRowMousePosition(); + } void peerListUpdateRow(not_null row) override { _content->updateRow(row); } @@ -898,13 +914,13 @@ public: void peerListSetSearchNoResults(object_ptr noResults) override { _content->setSearchNoResults(std::move(noResults)); } - void peerListSetAboveWidget(object_ptr aboveWidget) override { + void peerListSetAboveWidget(object_ptr aboveWidget) override { _content->setAboveWidget(std::move(aboveWidget)); } - void peerListSetAboveSearchWidget(object_ptr aboveWidget) override { + void peerListSetAboveSearchWidget(object_ptr aboveWidget) override { _content->setAboveSearchWidget(std::move(aboveWidget)); } - void peerListSetBelowWidget(object_ptr belowWidget) override { + void peerListSetBelowWidget(object_ptr belowWidget) override { _content->setBelowWidget(std::move(belowWidget)); } void peerListSetSearchMode(PeerListSearchMode mode) override { @@ -995,22 +1011,24 @@ public: void peerListHideLayer() override { Unexpected("...DelegateSimple::peerListHideLayer"); } - not_null peerListToastParent() override { - Unexpected("...DelegateSimple::peerListToastParent"); + std::shared_ptr peerListUiShow() override { + Unexpected("...DelegateSimple::peerListUiShow"); } }; class PeerListContentDelegateShow : public PeerListContentDelegateSimple { public: - PeerListContentDelegateShow(std::shared_ptr show); + explicit PeerListContentDelegateShow( + std::shared_ptr show); void peerListShowBox( object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; + private: - std::shared_ptr _show; + std::shared_ptr _show; }; @@ -1046,10 +1064,12 @@ public: object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; void setAddedTopScrollSkip(int skip); + void showFinished() override; + protected: void prepare() override; void setInnerFocus() override; @@ -1086,7 +1106,7 @@ private: object_ptr> _select = { nullptr }; - const Ui::BoxShow _show; + const std::shared_ptr _show; std::unique_ptr _controller; Fn _init; bool _scrollBottomFixed = false; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 0679d4dc9..335531fc9 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -9,12 +9,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "base/random.h" +#include "boxes/filters/edit_filter_chats_list.h" #include "ui/boxes/confirm_box.h" +#include "ui/effects/round_checkbox.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/padding_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" @@ -31,12 +37,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" +#include "ui/effects/outline_segments.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" // showAddContact() #include "base/unixtime.h" #include "styles/style_boxes.h" #include "styles/style_profile.h" #include "styles/style_dialogs.h" +#include "data/data_stories.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" + namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); @@ -47,23 +59,41 @@ constexpr auto kSearchPerPage = 50; object_ptr PrepareContactsBox( not_null sessionController) { using Mode = ContactsBoxController::SortMode; - auto controller = std::make_unique( + class Controller final : public ContactsBoxController { + public: + using ContactsBoxController::ContactsBoxController; + + protected: + std::unique_ptr createRow( + not_null user) override { + return !user->isSelf() + ? ContactsBoxController::createRow(user) + : nullptr; + } + + }; + auto controller = std::make_unique( &sessionController->session()); + controller->setStyleOverrides(&st::contactsWithStories); + controller->setStoriesShown(true); const auto raw = controller.get(); auto init = [=](not_null box) { struct State { - QPointer toggleSort; - Mode mode = ContactsBoxController::SortMode::Online; + QPointer<::Ui::IconButton> toggleSort; + rpl::variable mode = Mode::Online; + ::Ui::Animations::Simple scrollAnimation; }; + const auto state = box->lifetime().make_state(); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); box->addLeftButton( tr::lng_profile_add_contact(), [=] { sessionController->showAddContact(); }); state->toggleSort = box->addTopButton(st::contactsSortButton, [=] { - const auto online = (state->mode == Mode::Online); - state->mode = online ? Mode::Alphabet : Mode::Online; - raw->setSortMode(state->mode); + const auto online = (state->mode.current() == Mode::Online); + const auto mode = online ? Mode::Alphabet : Mode::Online; + state->mode = mode; + raw->setSortMode(mode); state->toggleSort->setIconOverride( online ? &st::contactsSortOnlineIcon : nullptr, online ? &st::contactsSortOnlineIconOver : nullptr); @@ -73,6 +103,39 @@ object_ptr PrepareContactsBox( return Box(std::move(controller), std::move(init)); } +QBrush PeerListStoriesGradient(const style::PeerList &st) { + const auto left = st.item.photoPosition.x(); + const auto top = st.item.photoPosition.y(); + const auto size = st.item.photoSize; + return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size)); +} + +std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush) { + Expects(unread <= count); + Expects(count > 0); + + auto result = std::vector(); + const auto add = [&](bool unread) { + result.push_back({ + .brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b, + .width = (unread + ? st::dialogsStoriesFull.lineTwice / 2. + : st::dialogsStoriesFull.lineReadTwice / 2.), + }); + }; + result.reserve(count); + for (auto i = 0, till = count - unread; i != till; ++i) { + add(false); + } + for (auto i = 0; i != unread; ++i) { + add(true); + } + return result; +} + void PeerListRowWithLink::setActionLink(const QString &action) { _action = action; refreshActionLink(); @@ -308,6 +371,115 @@ bool ChatsListBoxController::appendRow(not_null history) { return false; } +PeerListStories::PeerListStories( + not_null controller, + not_null session) +: _controller(controller) +, _session(session) { +} + +void PeerListStories::updateColors() { + for (auto i = begin(_counts); i != end(_counts); ++i) { + if (const auto row = _delegate->peerListFindRow(i->first)) { + if (i->second.count >= 0 && i->second.unread >= 0) { + applyForRow(row, i->second.count, i->second.unread, true); + } + } + } +} + +void PeerListStories::updateFor( + uint64 id, + int count, + int unread) { + if (const auto row = _delegate->peerListFindRow(id)) { + applyForRow(row, count, unread); + _delegate->peerListUpdateRow(row); + } +} + +void PeerListStories::process(not_null row) { + const auto user = row->peer()->asUser(); + if (!user) { + return; + } + const auto stories = &_session->data().stories(); + const auto source = stories->source(user->id); + const auto count = source + ? int(source->ids.size()) + : user->hasActiveStories() + ? 1 + : 0; + const auto unread = source + ? source->info().unreadCount + : user->hasUnreadStories() + ? 1 + : 0; + applyForRow(row, count, unread, true); +} + +bool PeerListStories::handleClick(not_null peer) { + const auto point = _delegate->peerListLastRowMousePosition(); + const auto &st = _controller->listSt()->item; + if (point && point->x() < st.photoPosition.x() + st.photoSize) { + if (const auto window = peer->session().tryResolveWindow()) { + if (const auto user = peer->asUser()) { + if (user->hasActiveStories()) { + window->openPeerStories(peer->id); + return true; + } + } + } + } + return false; +} + +void PeerListStories::prepare(not_null delegate) { + _delegate = delegate; + + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + style::PaletteChanged() | rpl::start_with_next([=] { + _unreadBrush = PeerListStoriesGradient(*_controller->listSt()); + updateColors(); + }, _lifetime); + + _session->changes().peerUpdates( + Data::PeerUpdate::Flag::StoriesState + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + const auto id = update.peer->id.value; + if (const auto row = _delegate->peerListFindRow(id)) { + process(row); + } + }, _lifetime); + + const auto stories = &_session->data().stories(); + stories->sourceChanged() | rpl::start_with_next([=](PeerId id) { + const auto source = stories->source(id); + const auto info = source + ? source->info() + : Data::StoriesSourceInfo(); + updateFor(id.value, info.count, info.unreadCount); + }, _lifetime); +} + +void PeerListStories::applyForRow( + not_null row, + int count, + int unread, + bool force) { + auto &counts = _counts[row->id()]; + if (!force && counts.count == count && counts.unread == unread) { + return; + } + counts.count = count; + counts.unread = unread; + _delegate->peerListSetRowChecked(row, count > 0); + if (count > 0) { + row->setCustomizedCheckSegments( + PeerListStoriesSegments(count, unread, _unreadBrush)); + } +} + ContactsBoxController::ContactsBoxController( not_null session) : ContactsBoxController( @@ -334,6 +506,10 @@ void ContactsBoxController::prepare() { prepareViewHook(); + if (_stories) { + _stories->prepare(delegate()); + } + session().data().contactsLoaded().value( ) | rpl::start_with_next([=] { rebuildRows(); @@ -378,8 +554,10 @@ std::unique_ptr ContactsBoxController::createSearchRow( void ContactsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); - if (const auto window = peer->session().tryResolveWindow()) { - window->showPeerHistory(row->peer()); + if (_stories && _stories->handleClick(peer)) { + return; + } else if (const auto window = peer->session().tryResolveWindow()) { + window->showPeerHistory(peer); } } @@ -404,6 +582,10 @@ void ContactsBoxController::setSortMode(SortMode mode) { } } +void ContactsBoxController::setStoriesShown(bool shown) { + _stories = std::make_unique(this, _session); +} + void ContactsBoxController::sort() { switch (_sortMode) { case SortMode::Alphabet: sortByName(); break; @@ -449,7 +631,11 @@ bool ContactsBoxController::appendRow(not_null user) { return false; } if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } return true; } return false; diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 00106c392..0ba108f01 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -20,12 +20,21 @@ class Forum; class ForumTopic; } // namespace Data +namespace Ui { +struct OutlineSegment; +} // namespace Ui + namespace Window { class SessionController; } // namespace Window [[nodiscard]] object_ptr PrepareContactsBox( not_null sessionController); +[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st); +[[nodiscard]] std::vector PeerListStoriesSegments( + int count, + int unread, + const QBrush &unreadBrush); class PeerListRowWithLink : public PeerListRow { public: @@ -116,6 +125,41 @@ private: }; +class PeerListStories final { +public: + PeerListStories( + not_null controller, + not_null session); + + void prepare(not_null delegate); + + void process(not_null row); + bool handleClick(not_null peer); + +private: + struct Counts { + int count = 0; + int unread = 0; + }; + + void updateColors(); + void updateFor(uint64 id, int count, int unread); + void applyForRow( + not_null row, + int count, + int unread, + bool force = false); + + const not_null _controller; + const not_null _session; + PeerListDelegate *_delegate = nullptr; + + QBrush _unreadBrush; + base::flat_map _counts; + rpl::lifetime _lifetime; + +}; + class ContactsBoxController : public PeerListController { public: explicit ContactsBoxController(not_null session); @@ -128,12 +172,16 @@ public: [[nodiscard]] std::unique_ptr createSearchRow( not_null peer) override final; void rowClicked(not_null row) override; + bool trackSelectedList() override { + return !_stories; + } enum class SortMode { Alphabet, Online, }; void setSortMode(SortMode mode); + void setStoriesShown(bool shown); protected: virtual std::unique_ptr createRow(not_null user); @@ -155,6 +203,8 @@ private: base::Timer _sortByOnlineTimer; rpl::lifetime _sortByOnlineLifetime; + std::unique_ptr _stories; + }; class ChooseRecipientBoxController diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.cpp b/Telegram/SourceFiles/boxes/peer_lists_box.cpp index 9c65f294b..e445a21e2 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_lists_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/scroll_area.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_peer.h" @@ -301,7 +302,7 @@ PeerListsBox::Delegate::Delegate( not_null controller) : _box(box) , _controller(controller) -, _show(_box) { +, _show(Main::MakeSessionShow(_box->uiShow(), &_controller->session())) { } void PeerListsBox::Delegate::peerListSetTitle(rpl::producer title) { @@ -374,15 +375,16 @@ void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() { void PeerListsBox::Delegate::peerListShowBox( object_ptr content, Ui::LayerOptions options) { - _show.showBox(std::move(content), options); + _show->showBox(std::move(content), options); } void PeerListsBox::Delegate::peerListHideLayer() { - _show.hideLayer(); + _show->hideLayer(); } -not_null PeerListsBox::Delegate::peerListToastParent() { - return _show.toastParent(); +auto PeerListsBox::Delegate::peerListUiShow() +-> std::shared_ptr { + return _show; } bool PeerListsBox::Delegate::peerListIsRowChecked( diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.h b/Telegram/SourceFiles/boxes/peer_lists_box.h index fcbbd984e..94732b3e2 100644 --- a/Telegram/SourceFiles/boxes/peer_lists_box.h +++ b/Telegram/SourceFiles/boxes/peer_lists_box.h @@ -58,12 +58,12 @@ private: object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; private: const not_null _box; const not_null _controller; - const Ui::BoxShow _show; + const std::shared_ptr _show; }; struct List { diff --git a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp index 6f11e0593..1bffcb2ee 100644 --- a/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_bot_to_chat_box.cpp @@ -182,8 +182,7 @@ void AddBotToGroupBoxController::addBotToGroup(not_null chat) { if (const auto megagroup = chat->asMegagroup()) { if (!megagroup->canAddMembers()) { _controller->show( - Ui::MakeInformBox(tr::lng_error_cant_add_member()), - Ui::LayerOption::KeepOther); + Ui::MakeInformBox(tr::lng_error_cant_add_member())); return; } } @@ -243,18 +242,16 @@ void AddBotToGroupBoxController::addBotToGroup(not_null chat) { _token, _existingRights.value_or(ChatAdminRights()) }); box->setSaveCallback(saveCallback); - controller->show(std::move(box), Ui::LayerOption::KeepOther); + controller->show(std::move(box)); } else { auto callback = crl::guard(this, [=] { AddBotToGroup(bot, chat, _token); controller->hideLayer(); }); - controller->show( - Ui::MakeConfirmBox({ - tr::lng_bot_sure_invite(tr::now, lt_group, chat->name()), - std::move(callback), - }), - Ui::LayerOption::KeepOther); + controller->show(Ui::MakeConfirmBox({ + tr::lng_bot_sure_invite(tr::now, lt_group, chat->name()), + std::move(callback), + })); } } diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index a7b3a6a2f..b40e1a0d2 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -202,9 +202,7 @@ void InviteForbiddenController::send( int(list.size()), Ui::Text::RichLangValue); close(); - Ui::Toast::Show( - show->toastParent(), - { .text = std::move(text), .st = &st::defaultToast }); + show->showToast(std::move(text)); return true; }; const auto sendForFull = [=] { @@ -368,7 +366,7 @@ bool AddParticipantsBoxController::needsInviteLinkButton() { QPointer AddParticipantsBoxController::showBox( object_ptr box) const { const auto weak = Ui::MakeWeak(box.data()); - delegate()->peerListShowBox(std::move(box), Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(std::move(box)); return weak; } @@ -425,7 +423,7 @@ void AddParticipantsBoxController::inviteSelectedUsers( if (users.empty()) { return; } - const auto show = std::make_shared(box); + const auto show = box->uiShow(); const auto request = [=](bool checked) { _peer->session().api().chatParticipants().add( _peer, @@ -493,9 +491,8 @@ void AddParticipantsBoxController::Start( }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }; - Window::Show(navigation).showBox( - Box(std::move(controller), std::move(initBox)), - Ui::LayerOption::KeepOther); + parent->show( + Box(std::move(controller), std::move(initBox))); } void AddParticipantsBoxController::Start( @@ -538,9 +535,8 @@ void AddParticipantsBoxController::Start( }, box->lifetime()); } }; - Window::Show(navigation).showBox( - Box(std::move(controller), std::move(initBox)), - Ui::LayerOption::KeepOther); + parent->show( + Box(std::move(controller), std::move(initBox))); } void AddParticipantsBoxController::Start( @@ -616,7 +612,7 @@ bool ChatInviteForbidden( box->addButton(tr::lng_via_link_send(), [=] { weak->send( box->collectSelectedRows(), - std::make_shared(box), + box->uiShow(), crl::guard(box, [=] { box->closeBox(); })); }); } @@ -626,8 +622,7 @@ bool ChatInviteForbidden( }, box->lifetime()); }; show->showBox( - Box(std::move(controller), std::move(initBox)), - Ui::LayerOption::KeepOther); + Box(std::move(controller), std::move(initBox))); return true; } @@ -673,7 +668,7 @@ void AddSpecialBoxController::migrate( QPointer AddSpecialBoxController::showBox( object_ptr box) const { const auto weak = Ui::MakeWeak(box.data()); - delegate()->peerListShowBox(std::move(box), Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(std::move(box)); return weak; } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 91ce22ef8..60031e0ce 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -505,5 +505,5 @@ void ShowChoosePeerBox( bot, query, std::move(callback)), - std::move(initBox)), Ui::LayerOption::KeepOther); + std::move(initBox))); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp index 89115f0bf..0aa24c79e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_contact_box.cpp @@ -69,8 +69,7 @@ void SendRequest( } if (box) { if (!wasContact) { - Ui::Toast::Show( - Ui::BoxShow(box.data()).toastParent(), + box->showToast( tr::lng_new_contact_add_done(tr::now, lt_user, first)); } box->closeBox(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index 400fcb95a..d5dbc6789 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -274,12 +274,9 @@ struct IconSelector { }; const auto selector = body->add( object_ptr(body, EmojiListDescriptor{ - .session = &controller->session(), + .show = controller->uiShow(), .mode = EmojiListWidget::Mode::TopicIcon, - .controller = controller, - .paused = Window::PausedIn( - controller, - Window::GifPauseReason::Layer), + .paused = Window::PausedIn(controller, PauseReason::Layer), .customRecentList = recent(), .customRecentFactory = std::move(factory), .st = &st::reactPanelEmojiPan, @@ -357,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 cdba89701..48bc383c9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -15,15 +15,15 @@ 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 "ui/toasts/common_toasts.h" #include "boxes/add_contact_box.h" #include "apiwrap.h" #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" @@ -168,13 +168,11 @@ void Controller::choose(not_null chat) { const auto onstack = _callback; onstack(chat); }; - delegate()->peerListShowBox( - Ui::MakeConfirmBox({ - .text = text, - .confirmed = sure, - .confirmText = tr::lng_manage_discussion_group_link(tr::now), - }), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(Ui::MakeConfirmBox({ + .text = text, + .confirmed = sure, + .confirmText = tr::lng_manage_discussion_group_link(tr::now), + })); } void Controller::choose(not_null chat) { @@ -201,13 +199,11 @@ void Controller::choose(not_null chat) { }; chat->session().api().migrateChat(chat, crl::guard(this, done)); }; - delegate()->peerListShowBox( - Ui::MakeConfirmBox({ - .text = text, - .confirmed = sure, - .confirmText = tr::lng_manage_discussion_group_link(tr::now), - }), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(Ui::MakeConfirmBox({ + .text = text, + .confirmed = sure, + .confirmText = tr::lng_manage_discussion_group_link(tr::now), + })); } [[nodiscard]] rpl::producer About( @@ -276,16 +272,14 @@ 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); - Window::Show(navigation).showBox( - Box( - navigation, - GroupInfoBox::Type::Megagroup, - channel->name() + " Chat", - guarded), - Ui::LayerOption::KeepOther); + navigation->uiShow()->showBox(Box( + navigation, + GroupInfoBox::Type::Megagroup, + channel->name() + " Chat", + guarded)); }); } box->peerListSetAboveWidget(std::move(above)); @@ -298,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); } @@ -363,10 +357,8 @@ object_ptr EditLinkedChatBox( void ShowForumForDiscussionError( not_null navigation) { - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(navigation).toastParent(), - .text = tr::lng_forum_topics_no_discussion( + navigation->showToast( + tr::lng_forum_topics_no_discussion( tr::now, - Ui::Text::RichLangValue), - }); + Ui::Text::RichLangValue)); } 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_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 7006606ce..f47beae75 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -203,7 +203,6 @@ EditAdminBox::EditAdminBox( peer, user, (rights.flags != 0)) -, _show(this) , _oldRights(rights) , _oldRank(rank) , _addingBot(std::move(addingBot)) { @@ -399,7 +398,7 @@ void EditAdminBox::prepare() { Ui::Text::Bold(peer()->name()), Ui::Text::WithEntities), crl::guard(this, [=] { finishAddAdmin(); }) - }), Ui::LayerOption::KeepOther); + })); } else { _finishSave(); } @@ -623,16 +622,15 @@ void EditAdminBox::sendTransferRequestFrom( if (!box && !weak) { return; } - - Ui::Toast::Show( - (box ? Ui::BoxShow(box) : weak->_show).toastParent(), + const auto show = box ? box->uiShow() : weak->uiShow(); + show->showToast( (channel->isBroadcast() ? tr::lng_rights_transfer_done_channel : tr::lng_rights_transfer_done_group)( tr::now, lt_user, user->shortName())); - (box ? Ui::BoxShow(box) : weak->_show).hideLayer(); + show->hideLayer(); }).fail(crl::guard(this, [=](const MTP::Error &error) { if (weak) { _transferRequestId = 0; @@ -694,7 +692,6 @@ EditRestrictedBox::EditRestrictedBox( bool hasAdminRights, ChatRestrictionsInfo rights) : EditParticipantBox(nullptr, peer, user, hasAdminRights) -, _show(this) , _oldRights(rights) { } @@ -788,7 +785,7 @@ ChatRestrictionsInfo EditRestrictedBox::defaultRights() const { } void EditRestrictedBox::showRestrictUntil() { - _show.showBox(Box([=](not_null box) { + uiShow()->showBox(Box([=](not_null box) { const auto save = [=](TimeId result) { if (!result) { return; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h index 4deda7ac9..b770730d5 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h @@ -114,7 +114,6 @@ private: not_null container, bool isGroup); - const Ui::BoxShow _show; const ChatAdminRightsInfo _oldRights; const QString _oldRank; Fn _saveCallback; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index c9d759cdd..425e21c75 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -25,11 +25,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_indexed_list.h" #include "data/data_peer_values.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_changes.h" #include "base/unixtime.h" +#include "ui/effects/outline_segments.h" #include "ui/widgets/popup_menu.h" #include "ui/ui_utility.h" #include "info/profile/info_profile_values.h" @@ -900,7 +902,11 @@ void ParticipantsBoxController::setupListChangeViewers() { return (row.peer() == user); }); } else if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } refreshRows(); if (_onlineSorter) { _onlineSorter->sort(); @@ -1159,9 +1165,19 @@ void ParticipantsBoxController::restoreState( if (my->wasLoading) { loadMoreRows(); } + const auto was = _fullCountValue.current(); PeerListController::restoreState(std::move(state)); - if (delegate()->peerListFullRowsCount() > 0 || _allLoaded) { + const auto now = delegate()->peerListFullRowsCount(); + if (now > 0 || _allLoaded) { refreshDescription(); + if (_stories) { + for (auto i = 0; i != now; ++i) { + _stories->process(delegate()->peerListRowAt(i)); + } + } + if (now != was) { + refreshRows(); + } } if (_onlineSorter) { _onlineSorter->sort(); @@ -1177,14 +1193,21 @@ rpl::producer ParticipantsBoxController::fullCountValue() const { return _fullCountValue.value(); } +void ParticipantsBoxController::setStoriesShown(bool shown) { + _stories = std::make_unique( + this, + &_navigation->session()); +} + void ParticipantsBoxController::prepare() { auto title = [&] { switch (_role) { case Role::Admins: return tr::lng_channel_admins(); case Role::Profile: - case Role::Members: return (_peer->isChannel() && !_peer->isMegagroup() - ? tr::lng_profile_subscribers_section() - : tr::lng_profile_participants_section()); + case Role::Members: + return ((_peer->isChannel() && !_peer->isMegagroup()) + ? tr::lng_profile_subscribers_section() + : tr::lng_profile_participants_section()); case Role::Restricted: return tr::lng_exceptions_list_title(); case Role::Kicked: return tr::lng_removed_list_title(); } @@ -1207,6 +1230,10 @@ void ParticipantsBoxController::prepare() { setDescriptionText(tr::lng_contacts_loading(tr::now)); setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); + if (_stories) { + _stories->prepare(delegate()); + } + if (_role == Role::Profile) { auto visible = _peer->isMegagroup() ? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup()) @@ -1254,7 +1281,7 @@ void ParticipantsBoxController::rebuild() { QPointer ParticipantsBoxController::showBox( object_ptr box) const { const auto weak = Ui::MakeWeak(box.data()); - delegate()->peerListShowBox(std::move(box), Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(std::move(box)); return weak; } @@ -1318,8 +1345,14 @@ void ParticipantsBoxController::rebuildChatParticipants( } } for (const auto &user : participants) { - if (auto row = createRow(user)) { - delegate()->peerListAppendRow(std::move(row)); + if (!delegate()->peerListFindRow(user->id.value)) { + if (auto row = createRow(user)) { + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } + } } } _onlineSorter->sort(); @@ -1373,7 +1406,11 @@ void ParticipantsBoxController::rebuildChatAdmins( } for (const auto user : list) { if (auto row = createRow(user)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } } } @@ -1468,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; } @@ -1543,6 +1585,11 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() { void ParticipantsBoxController::rowClicked(not_null row) { const auto participant = row->peer(); const auto user = participant->asUser(); + + if (_stories && _stories->handleClick(participant)) { + return; + } + if (_role == Role::Admins) { Assert(user != nullptr); showAdmin(user); @@ -1637,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)) { @@ -1890,7 +1937,11 @@ bool ParticipantsBoxController::appendRow(not_null participant) { recomputeTypeFor(participant); return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListAppendRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } @@ -1906,10 +1957,17 @@ bool ParticipantsBoxController::prependRow(not_null participant) { if (_role == Role::Admins) { // Perhaps we've added a new admin from search. delegate()->peerListPrependRowFromSearchResult(row); + if (_stories) { + _stories->process(row); + } } return false; } else if (auto row = createRow(participant)) { + const auto raw = row.get(); delegate()->peerListPrependRow(std::move(row)); + if (_stories) { + _stories->process(raw); + } if (_role != Role::Kicked) { setDescriptionText(QString()); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 30b445813..0882f214a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "info/profile/info_profile_members_controllers.h" +class PeerListStories; struct ChatAdminRightsInfo; struct ChatRestrictionsInfo; @@ -174,6 +175,9 @@ public: QWidget *parent, not_null row) override; void loadMoreRows() override; + bool trackSelectedList() override { + return !_stories; + } void peerListSearchAddRow(not_null peer) override; std::unique_ptr createSearchRow( @@ -187,6 +191,8 @@ public: [[nodiscard]] rpl::producer onlineCountValue() const; [[nodiscard]] rpl::producer fullCountValue() const; + void setStoriesShown(bool shown); + protected: // Allow child controllers not providing navigation. // This is their responsibility to override all methods that use it. @@ -288,6 +294,8 @@ private: Ui::BoxPointer _addBox; QPointer _editParticipantBox; + std::unique_ptr _stories; + }; // Members, banned and restricted users server side search. diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 0754484bd..01251e1f8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_reactions.h" #include "boxes/stickers_box.h" +#include "boxes/username_box.h" #include "ui/boxes/single_choice_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "core/application.h" @@ -35,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_message_reactions.h" #include "data/data_peer_values.h" +#include "data/data_user.h" #include "history/admin_log/history_admin_log_section.h" #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" @@ -46,7 +48,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/userpic_button.h" #include "ui/rp_widget.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" @@ -60,12 +61,15 @@ 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" namespace { +constexpr auto kBotManagerUsername = "BotFather"_cs; + [[nodiscard]] auto ToPositiveNumberString() { return rpl::map([](int count) { return count ? QString::number(count) : QString(); @@ -237,9 +241,7 @@ void ShowEditPermissions( }; ShowEditPeerPermissionsBox(box, navigation, peer, std::move(done)); }; - navigation->parentController()->show( - Box(std::move(createBox)), - Ui::LayerOption::KeepOther); + navigation->parentController()->show(Box(std::move(createBox))); } } // namespace @@ -305,6 +307,11 @@ private: void fillManageSection(); void fillPendingRequestsButton(); + void fillBotUsernamesButton(); + void fillBotEditIntroButton(); + void fillBotEditCommandsButton(); + void fillBotEditSettingsButton(); + void submitTitle(); void submitDescription(); void deleteWithConfirmation(); @@ -340,6 +347,8 @@ private: void continueSave(); void cancelSave(); + void toggleBotManager(const QString &command); + void togglePreHistoryHidden( not_null channel, bool hidden, @@ -362,6 +371,7 @@ private: not_null _peer; MTP::Sender _api; const bool _isGroup = false; + const bool _isBot = false; base::unique_qptr _wrap; Controls _controls; @@ -385,8 +395,11 @@ Controller::Controller( , _box(box) , _peer(peer) , _api(&_peer->session().mtp()) -, _isGroup(_peer->isChat() || _peer->isMegagroup()) { - _box->setTitle(_isGroup +, _isGroup(_peer->isChat() || _peer->isMegagroup()) +, _isBot(_peer->isUser() && _peer->asUser()->botInfo) { + _box->setTitle(_isBot + ? tr::lng_edit_bot_title() + : _isGroup ? tr::lng_edit_group() : tr::lng_edit_channel_title()); _box->addButton(tr::lng_settings_save(), [=] { @@ -490,7 +503,9 @@ object_ptr Controller::createTitleEdit() { object_ptr( _wrap, st::defaultInputField, - (_isGroup + (_isBot + ? tr::lng_dlg_new_bot_name + : _isGroup ? tr::lng_dlg_new_group_name : tr::lng_dlg_new_channel_name)(), _peer->name()), @@ -585,10 +600,9 @@ object_ptr Controller::createStickersEdit() { rpl::single(QString()), //Empty count. [=, controller = _navigation->parentController()] { controller->show( - Box(controller, channel), - Ui::LayerOption::KeepOther); + Box(controller->uiShow(), channel)); }, - { &st::settingsIconStickers, Settings::kIconLightOrange }); + { &st::menuIconStickers }); Settings::AddSkip(container, bottomSkip); @@ -602,7 +616,9 @@ object_ptr Controller::createStickersEdit() { } bool Controller::canEditInformation() const { - if (const auto channel = _peer->asChannel()) { + if (_isBot) { + return _peer->asUser()->botInfo->canEditInformation; + } else if (const auto channel = _peer->asChannel()) { return channel->canEditInformation(); } else if (const auto chat = _peer->asChat()) { return chat->canEditInformation(); @@ -651,8 +667,7 @@ void Controller::showEditPeerTypeBox( _channelHasLocationOriginalValue, boxCallback, _typeDataSavedValue, - error), - Ui::LayerOption::KeepOther); + error)); box->boxClosing( ) | rpl::start_with_next([peer = _peer] { peer->session().api().usernames().requestToCache(peer); @@ -686,14 +701,12 @@ void Controller::showEditLinkedChatBox() { || channel->canEditPreHistoryHidden())); if (const auto chat = *_linkedChatSavedValue) { - *box = _navigation->parentController()->show( - EditLinkedChatBox( - _navigation, - channel, - chat, - canEdit, - callback), - Ui::LayerOption::KeepOther); + *box = _navigation->parentController()->show(EditLinkedChatBox( + _navigation, + channel, + chat, + canEdit, + callback)); return; } else if (!canEdit || _linkedChatsRequestId) { return; @@ -720,13 +733,11 @@ void Controller::showEditLinkedChatBox() { for (const auto &item : list) { chats.emplace_back(_peer->owner().processChat(item)); } - *box = _navigation->parentController()->show( - EditLinkedChatBox( - _navigation, - channel, - std::move(chats), - callback), - Ui::LayerOption::KeepOther); + *box = _navigation->parentController()->show(EditLinkedChatBox( + _navigation, + channel, + std::move(chats), + callback)); }).fail([=] { _linkedChatsRequestId = 0; }).send(); @@ -756,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 @@ -784,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); } @@ -825,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); } // @@ -853,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()) @@ -864,14 +872,12 @@ void Controller::fillForumButton() { if (_linkedChatSavedValue && *_linkedChatSavedValue) { ShowForumForDiscussionError(_navigation); } else { - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(_navigation).toastParent(), - .text = tr::lng_forum_topics_not_enough( + _navigation->showToast( + tr::lng_forum_topics_not_enough( tr::now, lt_count, EnableForumMinMembers(_peer), - Ui::Text::RichLangValue), - }); + Ui::Text::RichLangValue)); } } else { _forumSavedValue = toggled; @@ -912,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) { @@ -974,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); @@ -984,12 +990,39 @@ void Controller::fillHistoryVisibilityButton() { void Controller::fillManageSection() { Expects(_controls.buttonsLayout != nullptr); - using namespace rpl::mappers; + if (_isBot) { + const auto &container = _controls.buttonsLayout; + + AddSkip(container, 0); + fillBotUsernamesButton(); + fillBotEditIntroButton(); + fillBotEditCommandsButton(); + fillBotEditSettingsButton(); + Settings::AddSkip( + container, + st::editPeerTopButtonsLayoutSkipCustomBottom); + container->add(object_ptr( + container, + object_ptr( + container, + tr::lng_manage_peer_bot_about( + lt_bot, + rpl::single(Ui::Text::Link( + '@' + kBotManagerUsername.utf16(), + _peer->session().createInternalLinkFull( + kBotManagerUsername.utf16()))), + Ui::Text::RichLangValue), + st::boxDividerLabel), + st::settingsDividerLabelPadding)); + return; + } const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); const auto isChannel = (!chat); - if (!chat && !channel) return; + if (!chat && !channel) { + return; + } const auto canEditType = [&] { return isChannel @@ -1042,7 +1075,6 @@ void Controller::fillManageSection() { }(); const auto canEditStickers = [&] { - // return true; return isChannel ? channel->canEditStickers() : false; @@ -1086,10 +1118,7 @@ void Controller::fillManageSection() { //|| canEditInviteLinks || canViewOrEditLinkedChat || canEditType) { - AddSkip( - _controls.buttonsLayout, - st::editPeerTopButtonsLayoutSkip, - st::editPeerTopButtonsLayoutSkipCustomBottom); + AddSkip(_controls.buttonsLayout); } if (canEditReactions()) { @@ -1134,7 +1163,7 @@ void Controller::fillManageSection() { Data::PeerAllowedReactions(_peer), done)); }, - { &st::infoRoundedIconReactions, Settings::kIconRed }); + { &st::menuIconGroupReactions }); } if (canEditPermissions) { AddButtonWithCount( @@ -1153,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( @@ -1180,16 +1209,14 @@ void Controller::fillManageSection() { tr::lng_manage_peer_invite_links(), rpl::duplicate(count) | ToPositiveNumberString(), [=] { - _navigation->parentController()->show( - Box( - ManageInviteLinksBox, - _peer, - _peer->session().user(), - 0, - 0), - Ui::LayerOption::KeepOther); + _navigation->parentController()->show(Box( + ManageInviteLinksBox, + _peer, + _peer->session().user(), + 0, + 0)); }, - { &st::infoRoundedIconInviteLinks, Settings::kIconLightOrange }); + { &st::menuIconLinks }); wrap->toggle(true, anim::type::instant); } if (canViewAdmins) { @@ -1208,7 +1235,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Admins); }, - { &st::infoRoundedIconAdministrators, Settings::kIconLightBlue }); + { &st::menuIconAdmin }); } if (canViewMembers) { AddButtonWithCount( @@ -1228,7 +1255,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Members); }, - { &st::settingsIconGroup, Settings::kIconDarkBlue }); + { &st::menuIconGroups }); } fillPendingRequestsButton(); @@ -1245,7 +1272,7 @@ void Controller::fillManageSection() { _peer, ParticipantsBoxController::Role::Kicked); }, - { &st::settingsIconMinus, Settings::kIconRed }); + { &st::menuIconRemove }); } if (hasRecentActions) { auto callback = [=] { @@ -1257,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) { @@ -1278,6 +1304,10 @@ void Controller::fillManageSection() { [=]{ deleteWithConfirmation(); } ); } + + if (canEditStickers || canDeleteChannel) { + AddSkip(_controls.buttonsLayout); + } } void Controller::fillPendingRequestsButton() { @@ -1299,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) { @@ -1307,6 +1337,96 @@ void Controller::fillPendingRequestsButton() { }, wrap->lifetime()); } +void Controller::fillBotUsernamesButton() { + Expects(_isBot); + + const auto user = _peer->asUser(); + + auto localUsernames = rpl::single( + user->usernames() + ) | rpl::map([](const std::vector &usernames) { + return ranges::views::all( + usernames + ) | ranges::views::transform([](const QString &u) { + return Data::Username{ u }; + }) | ranges::to_vector; + }); + auto usernamesValue = std::move( + localUsernames + ) | rpl::then( + _peer->session().api().usernames().loadUsernames(_peer) + ); + auto rightLabel = rpl::duplicate( + usernamesValue + ) | rpl::map([=](const Data::Usernames &usernames) { + if (usernames.size() <= 1) { + return user->session().createInternalLink(user->username()); + } else { + const auto active = ranges::count_if( + usernames, + [](const Data::Username &u) { return u.active; }); + return u"%1/%2"_q.arg(active).arg(usernames.size()); + } + }); + auto leftLabel = std::move( + usernamesValue + ) | rpl::map([=](const Data::Usernames &usernames) { + return (usernames.size() <= 1) + ? tr::lng_manage_peer_bot_public_link() + : tr::lng_manage_peer_bot_public_links(); + }) | rpl::flatten_latest(); + + _controls.buttonsLayout->add( + object_ptr>( + _controls.buttonsLayout, + object_ptr( + _controls.buttonsLayout))); + AddButtonWithCount( + _controls.buttonsLayout, + std::move(leftLabel), + std::move(rightLabel), + [=] { + _navigation->uiShow()->showBox(Box(UsernamesBox, user)); + }, + { &st::menuIconLinks }); +} + +void Controller::fillBotEditIntroButton() { + Expects(_isBot); + + const auto user = _peer->asUser(); + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_edit_intro(), + rpl::never(), + [=] { toggleBotManager(u"%1-intro"_q.arg(user->username())); }, + { &st::menuIconEdit }); +} + +void Controller::fillBotEditCommandsButton() { + Expects(_isBot); + + const auto user = _peer->asUser(); + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_edit_commands(), + rpl::never(), + [=] { toggleBotManager(u"%1-commands"_q.arg(user->username())); }, + { &st::menuIconBotCommands }); +} + +void Controller::fillBotEditSettingsButton() { + Expects(_isBot); + + const auto user = _peer->asUser(); + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_bot_edit_settings(), + rpl::never(), + [=] { toggleBotManager(user->username()); }, + { &st::menuIconSettings }); +} + void Controller::submitTitle() { Expects(_controls.title != nullptr); @@ -1669,6 +1789,31 @@ void Controller::saveTitle() { )).done(std::move(onDone) ).fail(std::move(onFail) ).send(); + } else if (_isBot) { + _api.request(MTPbots_GetBotInfo( + MTP_flags(MTPbots_GetBotInfo::Flag::f_bot), + _peer->asUser()->inputUser, + MTPstring() // Lang code. + )).done([=](const MTPbots_BotInfo &result) { + const auto was = qs(result.data().vname()); + const auto now = *_savingData.title; + if (was == now) { + return continueSave(); + } + using Flag = MTPbots_SetBotInfo::Flag; + _api.request(MTPbots_SetBotInfo( + MTP_flags(Flag::f_bot | Flag::f_name), + _peer->asUser()->inputUser, + MTPstring(), // Lang code. + MTP_string(now), // Name. + MTPstring(), // About. + MTPstring() // Description. + )).done([=] { + continueSave(); + }).fail(std::move(onFail) + ).send(); + }).fail(std::move(onFail) + ).send(); } else { continueSave(); } @@ -1683,6 +1828,36 @@ void Controller::saveDescription() { _peer->setAbout(*_savingData.description); continueSave(); }; + if (_isBot) { + _api.request(MTPbots_GetBotInfo( + MTP_flags(MTPbots_GetBotInfo::Flag::f_bot), + _peer->asUser()->inputUser, + MTPstring() // Lang code. + )).done([=](const MTPbots_BotInfo &result) { + const auto was = qs(result.data().vabout()); + const auto now = *_savingData.description; + if (was == now) { + return continueSave(); + } + using Flag = MTPbots_SetBotInfo::Flag; + _api.request(MTPbots_SetBotInfo( + MTP_flags(Flag::f_bot | Flag::f_about), + _peer->asUser()->inputUser, + MTPstring(), // Lang code. + MTPstring(), // Name. + MTP_string(now), // About. + MTPstring() // Description. + )).done([=] { + successCallback(); + }).fail([=] { + _controls.description->showError(); + cancelSave(); + }).send(); + }).fail([=] { + continueSave(); + }).send(); + return; + } _api.request(MTPmessages_EditChatAbout( _peer->input, MTP_string(*_savingData.description) @@ -1725,6 +1900,22 @@ void Controller::saveHistoryVisibility() { [=] { cancelSave(); }); } +void Controller::toggleBotManager(const QString &command) { + const auto controller = _navigation->parentController(); + _api.request(MTPcontacts_ResolveUsername( + MTP_string(kBotManagerUsername.utf16()) + )).done([=](const MTPcontacts_ResolvedPeer &result) { + _peer->owner().processUsers(result.data().vusers()); + _peer->owner().processChats(result.data().vchats()); + const auto botPeer = _peer->owner().peerLoaded( + peerFromMTP(result.data().vpeer())); + if (const auto bot = botPeer ? botPeer->asUser() : nullptr) { + _peer->session().api().sendBotStart(bot, bot, command); + controller->showPeerHistory(bot); + } + }).send(); +} + void Controller::togglePreHistoryHidden( not_null channel, bool hidden, @@ -1902,8 +2093,7 @@ void Controller::deleteWithConfirmation() { .confirmed = deleteCallback, .confirmText = tr::lng_box_delete(), .confirmStyle = &st::attentionBoxButton, - }), - Ui::LayerOption::KeepOther); + })); } void Controller::deleteChannel() { @@ -2014,7 +2204,9 @@ object_ptr EditPeerInfoBox::CreateButton( } bool EditPeerInfoBox::Available(not_null peer) { - if (const auto chat = peer->asChat()) { + if (const auto bot = peer->asUser()) { + return bot->botInfo && bot->botInfo->canEditInformation; + } else if (const auto chat = peer->asChat()) { return false || chat->canEditInformation() || chat->canEditPermissions(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 416c4ecb7..00f9eac3f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -26,7 +26,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/abstract_button.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/text/text_utilities.h" #include "ui/boxes/edit_invite_link.h" #include "ui/painter.h" @@ -274,12 +273,13 @@ QImage QrForShare(const QString &text) { void QrBox( not_null box, const QString &link, - Fn)> share) { + rpl::producer about, + Fn)> share) { box->setTitle(tr::lng_group_invite_qr_title()); box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); - const auto copyCallback = [=, show = std::make_shared(box)] { + const auto copyCallback = [=, show = box->uiShow()] { share(QrForShare(link), show); }; @@ -307,7 +307,7 @@ void QrBox( box->addRow( object_ptr( box, - tr::lng_group_invite_qr_about(), + std::move(about), st::boxLabel), st::inviteLinkQrValuePadding); @@ -345,32 +345,23 @@ void Controller::addHeaderBlock(not_null container) { const auto admin = current.admin; const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { - CopyInviteLink(delegate()->peerListToastParent(), link); + CopyInviteLink(delegate()->peerListUiShow(), link); }); const auto shareLink = crl::guard(weak, [=] { - delegate()->peerListShowBox( - ShareInviteLinkBox(_peer, link), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(ShareInviteLinkBox(_peer, link)); }); const auto getLinkQr = crl::guard(weak, [=] { delegate()->peerListShowBox( - InviteLinkQrBox(link), - Ui::LayerOption::KeepOther); + InviteLinkQrBox(link, tr::lng_group_invite_qr_about())); }); const auto revokeLink = crl::guard(weak, [=] { - delegate()->peerListShowBox( - RevokeLinkBox(_peer, admin, link), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(RevokeLinkBox(_peer, admin, link)); }); const auto editLink = crl::guard(weak, [=] { - delegate()->peerListShowBox( - EditLinkBox(_peer, _data.current()), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(EditLinkBox(_peer, _data.current())); }); const auto deleteLink = crl::guard(weak, [=] { - delegate()->peerListShowBox( - DeleteLinkBox(_peer, admin, link), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(DeleteLinkBox(_peer, admin, link)); }); const auto createMenu = [=] { @@ -731,9 +722,14 @@ void Controller::loadMoreRows() { void Controller::appendSlice(const Api::JoinedByLinkSlice &slice) { for (const auto &user : slice.users) { _lastUser = user; - delegate()->peerListAppendRow((_role == Role::Requested) + auto row = (_role == Role::Requested) ? std::make_unique(user.user, user.date) - : std::make_unique(user.user)); + : std::make_unique(user.user); + if (_role != Role::Requested && user.viaFilterLink) { + row->setCustomStatus( + tr::lng_group_invite_joined_via_filter(tr::now)); + } + delegate()->peerListAppendRow(std::move(row)); } delegate()->peerListRefreshRows(); if (delegate()->peerListFullRowsCount() > 0) { @@ -803,16 +799,13 @@ void Controller::processRequest( delegate()->peerListRefreshRows(); } if (approved) { - Ui::ShowMultilineToast({ - .parentOverride = delegate()->peerListToastParent(), - .text = (_peer->isBroadcast() - ? tr::lng_group_requests_was_added_channel - : tr::lng_group_requests_was_added)( - tr::now, - lt_user, - Ui::Text::Bold(user->name()), - Ui::Text::WithEntities) - }); + delegate()->peerListUiShow()->showToast((_peer->isBroadcast() + ? tr::lng_group_requests_was_added_channel + : tr::lng_group_requests_was_added)( + tr::now, + lt_user, + Ui::Text::Bold(user->name()), + Ui::Text::WithEntities)); } }); const auto fail = crl::guard(this, [=] { @@ -955,28 +948,24 @@ void AddPermanentLinkBlock( const auto weak = Ui::MakeWeak(container); const auto copyLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { - CopyInviteLink(show->toastParent(), current.link); + CopyInviteLink(show, current.link); } }); const auto shareLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { - show->showBox( - ShareInviteLinkBox(peer, current.link), - Ui::LayerOption::KeepOther); + show->showBox(ShareInviteLinkBox(peer, current.link)); } }); const auto getLinkQr = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { - show->showBox( - InviteLinkQrBox(current.link), - Ui::LayerOption::KeepOther); + show->showBox(InviteLinkQrBox( + current.link, + tr::lng_group_invite_qr_about())); } }); const auto revokeLink = crl::guard(weak, [=] { if (const auto current = value->current(); !current.link.isEmpty()) { - show->showBox( - RevokeLinkBox(peer, admin, current.link, true), - Ui::LayerOption::KeepOther); + show->showBox(RevokeLinkBox(peer, admin, current.link, true)); } }); @@ -1103,9 +1092,7 @@ void AddPermanentLinkBlock( st::inviteLinkJoinedRowPadding )->setClickedCallback([=] { if (!currentLinkFields->link.isEmpty()) { - show->showBox( - ShowInviteLinkBox(peer, *currentLinkFields), - Ui::LayerOption::KeepOther); + show->showBox(ShowInviteLinkBox(peer, *currentLinkFields)); } }); @@ -1120,20 +1107,26 @@ void AddPermanentLinkBlock( })); } -void CopyInviteLink(not_null toastParent, const QString &link) { +void CopyInviteLink(std::shared_ptr show, const QString &link) { QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show(toastParent, tr::lng_group_invite_copied(tr::now)); + show->showToast(tr::lng_group_invite_copied(tr::now)); } object_ptr ShareInviteLinkBox( not_null peer, const QString &link) { + return ShareInviteLinkBox(&peer->session(), link); +} + +object_ptr ShareInviteLinkBox( + not_null session, + const QString &link) { const auto sending = std::make_shared(); const auto box = std::make_shared>(); const auto showToast = [=](const QString &text) { if (*box) { - Ui::Toast::Show(Ui::BoxShow(*box).toastParent(), text); + (*box)->showToast(text); } }; @@ -1170,9 +1163,7 @@ object_ptr ShareInviteLinkBox( } text.append(error.first); if (*box) { - Ui::BoxShow(*box).showBox( - Ui::MakeInformBox(text), - Ui::LayerOption::KeepOther); + (*box)->uiShow()->showBox(Ui::MakeInformBox(text)); } return; } @@ -1187,7 +1178,7 @@ object_ptr ShareInviteLinkBox( } else { comment.text = link; } - auto &api = peer->session().api(); + auto &api = session->api(); for (const auto thread : result) { auto message = Api::MessageToSend( Api::SendAction(thread, options)); @@ -1204,7 +1195,7 @@ object_ptr ShareInviteLinkBox( return Data::CanSendTexts(thread); }; auto object = Box(ShareBox::Descriptor{ - .session = &peer->session(), + .session = session, .copyCallback = std::move(copyCallback), .submitCallback = std::move(submitCallback), .filterCallback = std::move(filterCallback), @@ -1213,17 +1204,16 @@ object_ptr ShareInviteLinkBox( return object; } -object_ptr InviteLinkQrBox(const QString &link) { - return Box(QrBox, link, [=]( +object_ptr InviteLinkQrBox( + const QString &link, + rpl::producer about) { + return Box(QrBox, link, std::move(about), [=]( const QImage &image, - std::shared_ptr show) { + std::shared_ptr show) { auto mime = std::make_unique(); mime->setImageData(image); QGuiApplication::clipboard()->setMimeData(mime.release()); - - Ui::Toast::Show( - show->toastParent(), - tr::lng_group_invite_qr_copied(tr::now)); + show->showToast(tr::lng_group_invite_qr_copied(tr::now)); }); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h index 0c190da4c..b4f54ef70 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h @@ -15,6 +15,10 @@ namespace Api { struct InviteLink; } // namespace Api +namespace Main { +class Session; +} // namespace Main + namespace Ui { class VerticalLayout; class Show; @@ -34,11 +38,16 @@ void AddPermanentLinkBlock( not_null admin, rpl::producer fromList); -void CopyInviteLink(not_null toastParent, const QString &link); +void CopyInviteLink(std::shared_ptr show, const QString &link); [[nodiscard]] object_ptr ShareInviteLinkBox( not_null peer, const QString &link); -[[nodiscard]] object_ptr InviteLinkQrBox(const QString &link); +[[nodiscard]] object_ptr ShareInviteLinkBox( + not_null session, + const QString &link); +[[nodiscard]] object_ptr InviteLinkQrBox( + const QString &link, + rpl::producer about); [[nodiscard]] object_ptr RevokeLinkBox( not_null peer, not_null admin, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 1be67226d..45a4c0ea0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_session.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "api/api_invite_links.h" #include "ui/wrap/slide_wrap.h" @@ -543,8 +544,7 @@ void LinksController::appendSlice(const InviteLinksSlice &slice) { void LinksController::rowClicked(not_null row) { delegate()->peerListShowBox( - ShowInviteLinkBox(_peer, static_cast(row.get())->data()), - Ui::LayerOption::KeepOther); + ShowInviteLinkBox(_peer, static_cast(row.get())->data())); } void LinksController::rowRightActionClicked(not_null row) { @@ -579,33 +579,25 @@ base::unique_qptr LinksController::createRowContextMenu( st::popupMenuWithIcons); if (data.revoked) { result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] { - delegate()->peerListShowBox( - DeleteLinkBox(_peer, _admin, link), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(DeleteLinkBox(_peer, _admin, link)); }, &st::menuIconDelete); } else { result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] { - CopyInviteLink(delegate()->peerListToastParent(), link); + CopyInviteLink(delegate()->peerListUiShow(), link); }, &st::menuIconCopy); result->addAction(tr::lng_group_invite_context_share(tr::now), [=] { delegate()->peerListShowBox( - ShareInviteLinkBox(_peer, link), - Ui::LayerOption::KeepOther); + ShareInviteLinkBox(_peer, link)); }, &st::menuIconShare); result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] { delegate()->peerListShowBox( - InviteLinkQrBox(link), - Ui::LayerOption::KeepOther); + InviteLinkQrBox(link, tr::lng_group_invite_qr_about())); }, &st::menuIconQrCode); result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] { - delegate()->peerListShowBox( - EditLinkBox(_peer, data), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(EditLinkBox(_peer, data)); }, &st::menuIconEdit); result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] { - delegate()->peerListShowBox( - RevokeLinkBox(_peer, _admin, link), - Ui::LayerOption::KeepOther); + delegate()->peerListShowBox(RevokeLinkBox(_peer, _admin, link)); }, &st::menuIconRemove); } return result; @@ -813,8 +805,7 @@ void AdminsController::loadMoreRows() { void AdminsController::rowClicked(not_null row) { delegate()->peerListShowBox( - Box(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0), - Ui::LayerOption::KeepOther); + Box(ManageInviteLinksBox, _peer, row->peer()->asUser(), 0, 0)); } Main::Session &AdminsController::session() const { @@ -836,7 +827,7 @@ struct LinksList { }; LinksList AddLinksList( - std::shared_ptr show, + std::shared_ptr show, not_null container, not_null peer, not_null admin, @@ -861,7 +852,7 @@ LinksList AddLinksList( } not_null AddAdminsList( - std::shared_ptr show, + std::shared_ptr show, not_null container, not_null peer, not_null admin) { @@ -889,7 +880,9 @@ void ManageInviteLinksBox( int revokedCount) { using namespace Settings; - const auto show = std::make_shared(box); + const auto show = Main::MakeSessionShow( + box->uiShow(), + &peer->session()); box->setTitle(tr::lng_group_invite_title()); box->setWidth(st::boxWideWidth); @@ -925,8 +918,7 @@ void ManageInviteLinksBox( const auto add = AddCreateLinkButton(container); add->setClickedCallback([=] { show->showBox( - EditLinkBox(peer, InviteLinkData{ .admin = admin }), - Ui::LayerOption::KeepOther); + EditLinkBox(peer, InviteLinkData{ .admin = admin })); }); } else { otherHeader = container->add(object_ptr>( @@ -1006,8 +998,8 @@ void ManageInviteLinksBox( top + st::inviteLinkRevokedTitlePadding.top(), outerWidth); }, deleteAll->lifetime()); - deleteAll->setClickedCallback([=, show = Ui::BoxShow(box)] { - show.showBox(DeleteAllRevokedBox(peer, admin)); + deleteAll->setClickedCallback([=, show = box->uiShow()] { + show->showBox(DeleteAllRevokedBox(peer, admin)); }); rpl::combine( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index ed7ba6df5..4c33b38fc 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -21,7 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/box_content_divider.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" +#include "ui/toast/toast.h" #include "info/profile/info_profile_icon.h" #include "info/profile/info_profile_values.h" #include "boxes/peers/edit_participants_box.h" @@ -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()); @@ -417,10 +418,7 @@ not_null AddInnerToggle( const auto handleLocked = [=] { if (locked.has_value()) { - Ui::ShowMultilineToast({ - .parentOverride = container, - .text = { *locked }, - }); + Ui::Toast::Show(container, *locked); return true; } return false; @@ -597,8 +595,7 @@ template ) | rpl::start_with_next([=](bool checked) { if (checked && state->forceDisabled.current()) { if (!state->toast) { - state->toast = Ui::ShowMultilineToast({ - .parentOverride = container, + state->toast = Ui::Toast::Show(container, { .text = { state->forceDisabledMessage.current() }, .duration = kForceDisableTooltipDuration, }); @@ -607,8 +604,7 @@ template } else if (locked.has_value()) { if (checked != toggled) { if (!state->toast) { - state->toast = Ui::ShowMultilineToast({ - .parentOverride = container, + state->toast = Ui::Toast::Show(container, { .text = { *locked }, .duration = kForceDisableTooltipDuration, }); @@ -823,7 +819,7 @@ void AddSuggestGigagroup( rpl::single(QString()), std::move(callback), st::manageGroupTopicsButton, - { &st::settingsIconAskQuestion, Settings::kIconGreen })); + { &st::menuIconChatDiscuss })); container->add( object_ptr( @@ -859,7 +855,7 @@ void AddBannedButtons( ParticipantsBoxController::Role::Restricted); }, st::manageGroupTopicsButton, - { &st::settingsIconKey, Settings::kIconLightOrange })); + { &st::menuIconPermissions })); if (channel) { container->add(EditPeerInfoBox::CreateButton( container, @@ -873,7 +869,7 @@ void AddBannedButtons( ParticipantsBoxController::Role::Kicked); }, st::manageGroupTopicsButton, - { &st::settingsIconMinus, Settings::kIconRed })); + { &st::menuIconRemove })); } } @@ -970,23 +966,20 @@ Fn AboutGigagroupCallback( channel->inputChannel )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); - if (const auto strongController = weak.get()) { - strongController->window().hideSettingsAndLayer(); - Ui::ShowMultilineToast({ - .parentOverride = strongController->widget(), - .text = { tr::lng_gigagroup_done(tr::now) }, - }); + if (const auto strong = weak.get()) { + strong->window().hideSettingsAndLayer(); + strong->showToast(tr::lng_gigagroup_done(tr::now)); } }).fail([=] { *converting = false; }).send(); }; const auto convertWarn = [=] { - const auto strongController = weak.get(); - if (*converting || !strongController) { + const auto strong = weak.get(); + if (*converting || !strong) { return; } - strongController->show(Box([=](not_null box) { + strong->show(Box([=](not_null box) { box->setTitle(tr::lng_gigagroup_warning_title()); box->addRow( object_ptr( @@ -996,14 +989,14 @@ Fn AboutGigagroupCallback( st::infoAboutGigagroup)); box->addButton(tr::lng_gigagroup_convert_sure(), convertSure); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - }), Ui::LayerOption::KeepOther); + })); }; return [=] { - const auto strongController = weak.get(); - if (*converting || !strongController) { + const auto strong = weak.get(); + if (*converting || !strong) { return; } - strongController->show(Box([=](not_null box) { + strong->show(Box([=](not_null box) { box->setTitle(tr::lng_gigagroup_convert_title()); const auto addFeature = [&](rpl::producer text) { using namespace rpl::mappers; @@ -1024,7 +1017,7 @@ Fn AboutGigagroupCallback( addFeature(tr::lng_gigagroup_convert_feature3()); box->addButton(tr::lng_gigagroup_convert_sure(), convertWarn); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - }), Ui::LayerOption::KeepOther); + })); }; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp index 928032923..0f21d164b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp @@ -22,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "mtproto/sender.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" #include "ui/painter.h" #include "lang/lang_keys.h" #include "window/window_session_controller.h" @@ -382,16 +381,13 @@ void RequestsBoxController::processRequest( const auto done = crl::guard(this, [=] { remove(); if (approved) { - Ui::ShowMultilineToast({ - .parentOverride = delegate()->peerListToastParent(), - .text = (_peer->isBroadcast() - ? tr::lng_group_requests_was_added_channel - : tr::lng_group_requests_was_added)( - tr::now, - lt_user, - Ui::Text::Bold(user->name()), - Ui::Text::WithEntities) - }); + delegate()->peerListUiShow()->showToast((_peer->isBroadcast() + ? tr::lng_group_requests_was_added_channel + : tr::lng_group_requests_was_added)( + tr::now, + lt_user, + Ui::Text::Bold(user->name()), + Ui::Text::WithEntities)); } }); const auto fail = crl::guard(this, remove); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp index de5a5c0e7..2739eb125 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_type_box.cpp @@ -52,7 +52,7 @@ class Controller : public base::has_weak_ptr { public: Controller( Window::SessionNavigation *navigation, - std::shared_ptr show, + std::shared_ptr show, not_null container, not_null peer, bool useLocationPhrases, @@ -140,7 +140,7 @@ private: rpl::producer about); Window::SessionNavigation *_navigation = nullptr; - std::shared_ptr _show; + std::shared_ptr _show; not_null _peer; bool _linkOnly = false; @@ -168,7 +168,7 @@ private: Controller::Controller( Window::SessionNavigation *navigation, - std::shared_ptr show, + std::shared_ptr show, not_null container, not_null peer, bool useLocationPhrases, @@ -601,9 +601,7 @@ void Controller::askUsernameRevoke() { _controls.privacy->setValue(Privacy::HasUsername); checkUsernameAvailability(); }); - _show->showBox( - Box(PublicLinksLimitBox, _navigation, revokeCallback), - Ui::LayerOption::KeepOther); + _show->showBox(Box(PublicLinksLimitBox, _navigation, revokeCallback)); } void Controller::usernameChanged() { @@ -735,7 +733,7 @@ void EditPeerTypeBox::prepare() { const auto controller = Ui::CreateChild( this, _navigation, - std::make_shared(this), + uiShow(), content.data(), _peer, _useLocationPhrases, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp index 2c785024d..c5da08715 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/event_filter.h" #include "data/data_peer.h" +#include "data/data_user.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/settings_common.h" @@ -64,6 +65,7 @@ public: not_null parent, const Data::Username &data, std::shared_ptr show, + QString status, QString link); [[nodiscard]] const Data::Username &username() const; @@ -90,15 +92,12 @@ UsernamesList::Row::Row( not_null parent, const Data::Username &data, std::shared_ptr show, + QString status, QString link) : Ui::SettingsButton(parent, rpl::never()) , _st(st::inviteLinkListItem) , _data(data) -, _status(data.editable - ? tr::lng_usernames_edit(tr::now) - : data.active - ? tr::lng_usernames_active(tr::now) - : tr::lng_usernames_non_active(tr::now)) +, _status(std::move(status)) , _rightAction(Ui::CreateChild(this)) , _iconRect( _st.photoPosition.x() + st::inviteLinkIconSkip, @@ -118,8 +117,7 @@ UsernamesList::Row::Row( tr::lng_group_invite_context_copy(tr::now), [=] { QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show( - show->toastParent(), + show->showToast( tr::lng_create_channel_link_copied(tr::now)); }, &st::menuIconCopy); @@ -197,6 +195,9 @@ UsernamesList::UsernamesList( : RpWidget(parent) , _show(show) , _peer(peer) +, _isBot(peer->isUser() + && peer->asUser()->botInfo + && peer->asUser()->botInfo->canEditInformation) , _focusCallback(std::move(focusCallback)) { { auto &api = _peer->session().api(); @@ -246,16 +247,24 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) { for (const auto &username : usernames) { const auto link = _peer->session().createInternalLinkFull( username.username); + const auto status = (username.editable && _focusCallback) + ? tr::lng_usernames_edit(tr::now) + : username.active + ? tr::lng_usernames_active(tr::now) + : tr::lng_usernames_non_active(tr::now); const auto row = content->add( - object_ptr(content, username, _show, link)); + object_ptr(content, username, _show, status, link)); _rows.push_back(row); row->addClickHandler([=] { - if (_reordering || (!_peer->isSelf() && !_peer->isChannel())) { + if (_reordering + || (!_peer->isSelf() && !_peer->isChannel() && !_isBot)) { return; } if (username.editable) { - _focusCallback(); + if (_focusCallback) { + _focusCallback(); + } return; } @@ -263,6 +272,10 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) { ? (username.active ? tr::lng_usernames_deactivate_description() : tr::lng_usernames_activate_description()) + : _isBot + ? (username.active + ? tr::lng_bot_usernames_deactivate_description() + : tr::lng_bot_usernames_activate_description()) : (username.active ? tr::lng_channel_usernames_deactivate_description() : tr::lng_channel_usernames_activate_description()); @@ -293,8 +306,7 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) { tr::lng_usernames_activate_error( lt_count, rpl::single(kMaxUsernames), - Ui::Text::RichLangValue)), - Ui::LayerOption::KeepOther); + Ui::Text::RichLangValue))); } load(); _toggleLifetime.destroy(); @@ -307,9 +319,7 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) { }), .confirmText = std::move(confirmText), }; - _show->showBox( - Ui::MakeConfirmBox(std::move(args)), - Ui::LayerOption::KeepOther); + _show->showBox(Ui::MakeConfirmBox(std::move(args))); }); } @@ -359,6 +369,8 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) { _container, _peer->isSelf() ? tr::lng_usernames_description() + : _isBot + ? tr::lng_bot_usernames_description() : tr::lng_channel_usernames_description()); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h b/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h index c93bd232a..483ca2453 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_usernames_list.h @@ -41,6 +41,7 @@ private: const std::shared_ptr _show; const not_null _peer; + const bool _isBot = false; Fn _focusCallback; base::unique_qptr _container; diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index 619542edb..bc07210f6 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_player.h" #include "base/event_filter.h" #include "lang/lang_keys.h" +#include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_info.h" @@ -104,6 +105,7 @@ PeerShortInfoCover::PeerShortInfoCover( userpic ) | rpl::start_with_next([=](PeerShortInfoUserpic &&value) { applyUserpic(std::move(value)); + applyAdditionalStatus(value.additionalStatus); }, lifetime()); style::PaletteChanged( @@ -135,16 +137,7 @@ PeerShortInfoCover::PeerShortInfoCover( return base::EventFilterResult::Cancel; }); - _name->moveToLeft( - _st.namePosition.x(), - _st.size - _st.namePosition.y() - _name->height(), - _st.size); - _status->moveToLeft( - _st.statusPosition.x(), - (_st.size - - _st.statusPosition.y() - - _status->height()), - _st.size); + refreshLabelsGeometry(); _roundedTopImage = QImage( QSize(_st.size, _st.radius) * style::DevicePixelRatio(), @@ -414,6 +407,23 @@ QImage PeerShortInfoCover::currentVideoFrame() const { : QImage(); } +void PeerShortInfoCover::applyAdditionalStatus(const QString &status) { + if (status.isEmpty()) { + if (_additionalStatus) { + _additionalStatus.destroy(); + refreshLabelsGeometry(); + } + return; + } + if (_additionalStatus) { + _additionalStatus->setText(status); + } else { + _additionalStatus.create(_widget.get(), status, _statusStyle->st); + _additionalStatus->show(); + refreshLabelsGeometry(); + } +} + void PeerShortInfoCover::applyUserpic(PeerShortInfoUserpic &&value) { if (_index != value.index) { _index = value.index; @@ -592,6 +602,28 @@ void PeerShortInfoCover::refreshBarImages() { _barLarge = makeBar(_largeWidth); } +void PeerShortInfoCover::refreshLabelsGeometry() { + const auto statusTop = _st.size + - _st.statusPosition.y() + - _status->height(); + const auto diff = _st.namePosition.y() + - _name->height() + - _st.statusPosition.y(); + if (_additionalStatus) { + _additionalStatus->moveToLeft( + _status->x(), + statusTop - diff - _additionalStatus->height()); + } + _name->moveToLeft( + _st.namePosition.x(), + _st.size + - _st.namePosition.y() + - _name->height() + - (_additionalStatus ? (diff + _additionalStatus->height()) : 0), + _st.size); + _status->moveToLeft(_st.statusPosition.x(), statusTop, _st.size); +} + QRect PeerShortInfoCover::radialRect() const { const auto cover = _widget->rect(); const auto size = st::boxLoadingSize; @@ -614,8 +646,10 @@ PeerShortInfoBox::PeerShortInfoBox( rpl::producer fields, rpl::producer status, rpl::producer userpic, - Fn videoPaused) -: _type(type) + Fn videoPaused, + const style::ShortInfoBox *stOverride) +: _st(stOverride ? *stOverride : st::shortInfoBox) +, _type(type) , _fields(std::move(fields)) , _topRoundBackground(this) , _scroll(this, st::shortInfoScroll) @@ -651,12 +685,16 @@ rpl::producer PeerShortInfoBox::moveRequests() const { void PeerShortInfoBox::prepare() { addButton(tr::lng_close(), [=] { closeBox(); }); - // Perhaps a new lang key should be added for opening a group. - addLeftButton((_type == PeerShortInfoType::User) - ? tr::lng_profile_send_message() - : (_type == PeerShortInfoType::Group) - ? tr::lng_view_button_group() - : tr::lng_profile_view_channel(), [=] { _openRequests.fire({}); }); + if (_type != PeerShortInfoType::Self) { + // Perhaps a new lang key should be added for opening a group. + addLeftButton( + (_type == PeerShortInfoType::User) + ? tr::lng_profile_send_message() + : (_type == PeerShortInfoType::Group) + ? tr::lng_view_button_group() + : tr::lng_profile_view_channel(), + [=] { _openRequests.fire({}); }); + } prepareRows(); @@ -691,11 +729,12 @@ void PeerShortInfoBox::prepareRows() { auto addInfoLineGeneric = [&]( rpl::producer &&label, rpl::producer &&text, - const style::FlatLabel &textSt = st::infoLabeled) { + const style::FlatLabel &textSt) { auto line = CreateTextWithLabel( _rows, rpl::duplicate(label) | Ui::Text::ToWithEntities(), rpl::duplicate(text), + _st.label, textSt, st::shortInfoLabeledPadding); _rows->add(object_ptr( @@ -715,7 +754,7 @@ void PeerShortInfoBox::prepareRows() { auto addInfoLine = [&]( rpl::producer &&label, rpl::producer &&text, - const style::FlatLabel &textSt = st::infoLabeled) { + const style::FlatLabel &textSt) { return addInfoLineGeneric( std::move(label), std::move(text), @@ -728,7 +767,7 @@ void PeerShortInfoBox::prepareRows() { auto result = addInfoLine( std::move(label), std::move(text), - st::infoLabeledOneLine); + _st.labeledOneLine); result->setDoubleClickSelectsParagraph(true); result->setContextCopyText(contextCopyText); return result; @@ -744,7 +783,7 @@ void PeerShortInfoBox::prepareRows() { auto label = _fields.current().isBio ? tr::lng_info_bio_label() : tr::lng_info_about_label(); - addInfoLine(std::move(label), aboutValue()); + addInfoLine(std::move(label), aboutValue(), _st.labeled); addInfoOneLine( tr::lng_info_username_label(), usernameValue() | Ui::Text::ToWithEntities(), diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index fc29b7062..bf59ff7aa 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct ShortInfoCover; +struct ShortInfoBox; } // namespace style namespace Media::Streaming { @@ -27,6 +28,7 @@ class RpWidget; } // namespace Ui enum class PeerShortInfoType { + Self, User, Group, Channel, @@ -49,6 +51,7 @@ struct PeerShortInfoUserpic { float64 photoLoadingProgress = 0.; std::shared_ptr videoDocument; crl::time videoStartPosition = 0; + QString additionalStatus; }; class PeerShortInfoCover final { @@ -86,6 +89,7 @@ private: [[nodiscard]] QImage currentVideoFrame() const; void applyUserpic(PeerShortInfoUserpic &&value); + void applyAdditionalStatus(const QString &status); [[nodiscard]] QRect radialRect() const; void videoWaiting(); @@ -98,6 +102,7 @@ private: void updateRadialState(); void refreshCoverCursor(); void refreshBarImages(); + void refreshLabelsGeometry(); const style::ShortInfoCover &_st; @@ -107,6 +112,7 @@ private: object_ptr _name; std::unique_ptr _statusStyle; object_ptr _status; + object_ptr _additionalStatus = { nullptr }; std::array _roundMask; QImage _userpicImage; @@ -144,7 +150,8 @@ public: rpl::producer fields, rpl::producer status, rpl::producer userpic, - Fn videoPaused); + Fn videoPaused, + const style::ShortInfoBox *stOverride); ~PeerShortInfoBox(); [[nodiscard]] rpl::producer<> openRequests() const; @@ -166,6 +173,7 @@ private: [[nodiscard]] rpl::producer usernameValue() const; [[nodiscard]] rpl::producer aboutValue() const; + const style::ShortInfoBox &_st; const PeerShortInfoType _type = PeerShortInfoType::User; rpl::variable _fields; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index a39b7eb47..d71645316 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -335,6 +335,15 @@ bool ProcessCurrent( : state->photoView ? state->photoView->owner().get() : nullptr; + state->current.additionalStatus = (!peer->isUser()) + ? QString() + : ((state->photoId == userpicPhotoId) + && peer->asUser()->hasPersonalPhoto()) + ? tr::lng_profile_photo_by_you(tr::now) + : ((state->current.index == (state->current.count - 1)) + && SyncUserFallbackPhotoViewer(peer->asUser())) + ? tr::lng_profile_public_photo(tr::now) + : QString(); state->waitingLoad = false; if (!changedPhotoId && (state->current.index > 0 || !changedUserpic) @@ -420,8 +429,11 @@ bool ProcessCurrent( object_ptr PrepareShortInfoBox( not_null peer, Fn open, - Fn videoPaused) { - const auto type = peer->isUser() + Fn videoPaused, + const style::ShortInfoBox *stOverride) { + const auto type = peer->isSelf() + ? PeerShortInfoType::Self + : peer->isUser() ? PeerShortInfoType::User : peer->isBroadcast() ? PeerShortInfoType::Channel @@ -432,7 +444,8 @@ object_ptr PrepareShortInfoBox( FieldsValue(peer), StatusValue(peer), std::move(userpic.value), - std::move(videoPaused)); + std::move(videoPaused), + stOverride); result->openRequests( ) | rpl::start_with_next(open, result->lifetime()); @@ -445,7 +458,8 @@ object_ptr PrepareShortInfoBox( object_ptr PrepareShortInfoBox( not_null peer, - not_null navigation) { + not_null navigation, + const style::ShortInfoBox *stOverride) { const auto open = [=] { navigation->showPeerHistory(peer); }; const auto videoIsPaused = [=] { return navigation->parentController()->isGifPausedAtLeastFor( @@ -454,7 +468,8 @@ object_ptr PrepareShortInfoBox( return PrepareShortInfoBox( peer, open, - videoIsPaused); + videoIsPaused, + stOverride); } rpl::producer PrepareShortInfoStatus(not_null peer) { diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h index f50a23bf7..327ce373b 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h @@ -13,6 +13,7 @@ class PeerData; namespace style { struct ShortInfoCover; +struct ShortInfoBox; } // namespace style namespace Ui { @@ -33,11 +34,13 @@ struct PreparedShortInfoUserpic { [[nodiscard]] object_ptr PrepareShortInfoBox( not_null peer, Fn open, - Fn videoPaused); + Fn videoPaused, + const style::ShortInfoBox *stOverride = nullptr); [[nodiscard]] object_ptr PrepareShortInfoBox( not_null peer, - not_null navigation); + not_null navigation, + const style::ShortInfoBox *stOverride = nullptr); [[nodiscard]] rpl::producer PrepareShortInfoStatus( not_null peer); diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.cpp b/Telegram/SourceFiles/boxes/pin_messages_box.cpp index 3471ef226..163e9dedc 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/pin_messages_box.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_user.h" #include "lang/lang_keys.h" +#include "history/history.h" +#include "history/history_item.h" #include "main/main_session.h" #include "ui/boxes/confirm_box.h" #include "ui/widgets/checkbox.h" @@ -41,15 +43,17 @@ namespace { void PinMessageBox( not_null box, - not_null peer, - MsgId msgId) { + not_null item) { struct State { QPointer pinForPeer; QPointer notify; mtpRequestId requestId = 0; }; - const auto pinningOld = IsOldForPin(msgId, peer, MsgId(0)); + const auto peer = item->history()->peer; + const auto msgId = item->id; + const auto topicRootId = item->topic() ? item->topicRootId() : MsgId(); + const auto pinningOld = IsOldForPin(msgId, peer, topicRootId); const auto state = box->lifetime().make_state(); const auto api = box->lifetime().make_state( &peer->session().mtp()); diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.h b/Telegram/SourceFiles/boxes/pin_messages_box.h index c3ff2e92e..e19d9a1f1 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.h +++ b/Telegram/SourceFiles/boxes/pin_messages_box.h @@ -13,5 +13,4 @@ class GenericBox; void PinMessageBox( not_null box, - not_null peer, - MsgId msgId); + not_null item); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 9afb6b039..8980a9028 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -13,7 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/wrap/padding_wrap.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" #include "main/main_session.h" #include "main/main_account.h" #include "main/main_domain.h" @@ -46,6 +45,7 @@ struct InfographicDescriptor { float64 premiumLimit = 0; const style::icon *icon; std::optional> phrase; + bool complexRatio = false; }; [[nodiscard]] rpl::producer<> BoxShowFinishes(not_null box) { @@ -132,7 +132,7 @@ public: object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; void peerListSetRowChecked( not_null row, bool checked) override; @@ -204,8 +204,8 @@ void InactiveDelegate::peerListShowBox( void InactiveDelegate::peerListHideLayer() { } -not_null InactiveDelegate::peerListToastParent() { - Unexpected("...InactiveDelegate::peerListToastParent"); +std::shared_ptr InactiveDelegate::peerListUiShow() { + Unexpected("...InactiveDelegate::peerListUiShow"); } rpl::producer InactiveDelegate::selectedCountChanges() const { @@ -384,8 +384,7 @@ void PublicsController::rowRightActionClicked(not_null row) { .text = text, .confirmed = std::move(callback), .confirmText = confirmText, - }), - Ui::LayerOption::KeepOther); + })); } void PublicsController::appendRow(not_null participant) { @@ -405,6 +404,7 @@ std::unique_ptr PublicsController::createRow( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, bool premiumPossible, rpl::producer title, @@ -412,6 +412,8 @@ void SimpleLimitBox( const QString &refAddition, const InfographicDescriptor &descriptor, bool fixed = false) { + const auto &st = stOverride ? *stOverride : st::defaultPremiumLimits; + box->setWidth(st::boxWideWidth); const auto top = fixed @@ -432,8 +434,13 @@ void SimpleLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st, descriptor.premiumLimit, - descriptor.phrase); + descriptor.phrase, + 0, + (descriptor.complexRatio + ? (float64(descriptor.current) / descriptor.premiumLimit) + : Ui::Premium::kLimitRowRatio)); Settings::AddSkip(top, st::premiumInfographicPadding.bottom()); } @@ -470,6 +477,7 @@ void SimpleLimitBox( void SimpleLimitBox( not_null box, + const style::PremiumLimits *stOverride, not_null session, rpl::producer title, rpl::producer text, @@ -478,6 +486,7 @@ void SimpleLimitBox( bool fixed = false) { SimpleLimitBox( box, + stOverride, session, session->premiumPossible(), std::move(title), @@ -521,6 +530,7 @@ void SimplePinsLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filter_pin_limit_title(), std::move(text), @@ -558,6 +568,7 @@ void ChannelsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_channels_limit_title(), std::move(text), @@ -597,10 +608,7 @@ void ChannelsLimitBox( session->api().leaveChannel(channel); } } - Ui::ShowMultilineToast({ - .parentOverride = Ui::BoxShow(box).toastParent(), - .text = { tr::lng_channels_leave_done(tr::now) }, - }); + box->showToast(tr::lng_channels_leave_done(tr::now)); box->closeBox(); }; box->clearButtons(); @@ -650,6 +658,7 @@ void PublicLinksLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_links_limit_title(), std::move(text), @@ -685,7 +694,8 @@ void PublicLinksLimitBox( void FilterChatsLimitBox( not_null box, not_null session, - int currentCount) { + int currentCount, + bool include) { const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); @@ -698,10 +708,12 @@ void FilterChatsLimitBox( premiumLimit); auto text = rpl::combine( - tr::lng_filter_chats_limit1( - lt_count, - rpl::single(premium ? premiumLimit : defaultLimit), - Ui::Text::RichLangValue), + (include + ? tr::lng_filter_chats_limit1 + : tr::lng_filter_chats_exlude_limit1)( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), ((premium || !premiumPossible) ? rpl::single(TextWithEntities()) : tr::lng_filter_chats_limit2( @@ -716,6 +728,7 @@ void FilterChatsLimitBox( SimpleLimitBox( box, + nullptr, session, tr::lng_filter_chats_limit_title(), std::move(text), @@ -723,6 +736,51 @@ void FilterChatsLimitBox( { defaultLimit, current, premiumLimit, &st::premiumIconChats }); } +void FilterLinksLimitBox( + not_null box, + not_null session) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.dialogFiltersLinksDefault()); + const auto premiumLimit = float64(limits.dialogFiltersLinksPremium()); + const auto current = (premium ? premiumLimit : defaultLimit); + + auto text = rpl::combine( + tr::lng_filter_links_limit1( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? rpl::single(TextWithEntities()) + : tr::lng_filter_links_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + + SimpleLimitBox( + box, + nullptr, + session, + tr::lng_filter_links_limit_title(), + std::move(text), + "chatlist_invites", + { + defaultLimit, + current, + premiumLimit, + &st::premiumIconChats, + std::nullopt, + true }); +} + + void FiltersLimitBox( not_null box, not_null session) { @@ -754,6 +812,7 @@ void FiltersLimitBox( }); SimpleLimitBox( box, + nullptr, session, tr::lng_filters_limit_title(), std::move(text), @@ -761,6 +820,51 @@ void FiltersLimitBox( { defaultLimit, current, premiumLimit, &st::premiumIconFolders }); } +void ShareableFiltersLimitBox( + not_null box, + not_null session) { + const auto premium = session->premium(); + const auto premiumPossible = session->premiumPossible(); + + const auto limits = Data::PremiumLimits(session); + const auto defaultLimit = float64(limits.dialogShareableFiltersDefault()); + const auto premiumLimit = float64(limits.dialogShareableFiltersPremium()); + const auto current = float64(ranges::count_if( + session->data().chatsFilters().list(), + [](const Data::ChatFilter &f) { return f.chatlist(); })); + + auto text = rpl::combine( + tr::lng_filter_shared_limit1( + lt_count, + rpl::single(premium ? premiumLimit : defaultLimit), + Ui::Text::RichLangValue), + ((premium || !premiumPossible) + ? rpl::single(TextWithEntities()) + : tr::lng_filter_shared_limit2( + lt_count, + rpl::single(premiumLimit), + Ui::Text::RichLangValue)) + ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) { + return b.text.isEmpty() + ? a + : a.append(QChar(' ')).append(std::move(b)); + }); + SimpleLimitBox( + box, + nullptr, + session, + tr::lng_filter_shared_limit_title(), + std::move(text), + "chatlists_joined", + { + defaultLimit, + current, + premiumLimit, + &st::premiumIconFolders, + std::nullopt, + true }); +} + void FilterPinsLimitBox( not_null box, not_null session, @@ -812,6 +916,7 @@ void ForumPinsLimitBox( Ui::Text::RichLangValue); SimpleLimitBox( box, + nullptr, &forum->session(), false, tr::lng_filter_pin_limit_title(), @@ -823,7 +928,8 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { const auto premium = session->premium(); const auto premiumPossible = session->premiumPossible(); @@ -855,6 +961,7 @@ void CaptionLimitBox( SimpleLimitBox( box, + stOverride, session, tr::lng_caption_limit_title(), std::move(text), @@ -865,15 +972,17 @@ void CaptionLimitBox( void CaptionLimitReachedBox( not_null box, not_null session, - int remove) { + int remove, + const style::PremiumLimits *stOverride) { Ui::ConfirmBox(box, Ui::ConfirmBoxArgs{ .text = tr::lng_caption_limit_reached(tr::now, lt_count, remove), + .labelStyle = stOverride ? &stOverride->boxLabel : nullptr, .inform = true, }); if (!session->premium()) { box->addLeftButton(tr::lng_limits_increase(), [=] { box->getDelegate()->showBox( - Box(CaptionLimitBox, session, remove), + Box(CaptionLimitBox, session, remove, stOverride), Ui::LayerOption::KeepOther, anim::type::normal); box->closeBox(); @@ -884,7 +993,8 @@ void CaptionLimitReachedBox( void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes) { + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride) { const auto limits = Data::PremiumLimits(session); const auto defaultLimit = float64(limits.uploadMaxDefault()); const auto premiumLimit = float64(limits.uploadMaxPremium()); @@ -923,6 +1033,7 @@ void FileSizeLimitBox( SimpleLimitBox( box, + stOverride, session, premiumPossible, tr::lng_file_size_limit_title(), @@ -996,6 +1107,7 @@ void AccountsLimitBox( if (premiumPossible) { Ui::Premium::AddLimitRow( top, + st::defaultPremiumLimits, (QString::number(std::max(current, defaultLimit) + 1) + ((current + 1 == premiumLimit) ? "" : "+")), QString::number(defaultLimit)); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.h b/Telegram/SourceFiles/boxes/premium_limits_box.h index 04a607de2..7bcda6e53 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.h +++ b/Telegram/SourceFiles/boxes/premium_limits_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" +namespace style { +struct PremiumLimits; +} // namespace style + namespace Data { class Forum; } // namespace Data @@ -31,10 +35,17 @@ void PublicLinksLimitBox( void FilterChatsLimitBox( not_null box, not_null session, - int currentCount); + int currentCount, + bool include); +void FilterLinksLimitBox( + not_null box, + not_null session); void FiltersLimitBox( not_null box, not_null session); +void ShareableFiltersLimitBox( + not_null box, + not_null session); void FilterPinsLimitBox( not_null box, not_null session, @@ -51,15 +62,18 @@ void ForumPinsLimitBox( void CaptionLimitBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void CaptionLimitReachedBox( not_null box, not_null session, - int remove); + int remove, + const style::PremiumLimits *stOverride = nullptr); void FileSizeLimitBox( not_null box, not_null session, - uint64 fileSizeBytes); + uint64 fileSizeBytes, + const style::PremiumLimits *stOverride = nullptr); void AccountsLimitBox( not_null box, not_null session); diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index ffc5bc709..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" @@ -76,7 +77,7 @@ bool operator==(const Descriptor &a, const Descriptor &b) { struct Preload { Descriptor descriptor; std::shared_ptr media; - base::weak_ptr controller; + std::weak_ptr show; }; [[nodiscard]] std::vector &Preloads() { @@ -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: @@ -168,7 +177,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickerPreview( not_null parent, - not_null controller, + std::shared_ptr show, const std::shared_ptr &media, Fn readyCallback = nullptr) { using namespace HistoryView; @@ -194,6 +203,8 @@ void PreloadSticker(const std::shared_ptr &media) { struct State { std::unique_ptr lottie; std::unique_ptr effect; + style::owned_color pathFg = style::owned_color( + QColor(255, 255, 255, 64)); std::unique_ptr pathGradient; bool readyInvoked = false; }; @@ -239,15 +250,17 @@ void PreloadSticker(const std::shared_ptr &media) { }; createLottieIfReady(); if (!state->lottie || !state->effect) { - controller->session().downloaderTaskFinished( + show->session().downloaderTaskFinished( ) | rpl::take_while([=] { createLottieIfReady(); return !state->lottie || !state->effect; }) | rpl::start(result->lifetime()); } - state->pathGradient = MakePathShiftGradient( - controller->chatStyle(), - [=] { result->update(); }); + state->pathGradient = std::make_unique( + st::shadowFg, + state->pathFg.color(), + [=] { result->update(); }, + rpl::never<>()); result->paintRequest( ) | rpl::start_with_next([=] { @@ -262,7 +275,7 @@ void PreloadSticker(const std::shared_ptr &media) { if (!state->lottie || !state->lottie->ready() || !state->effect->ready()) { - p.setBrush(controller->chatStyle()->msgServiceBg()); + p.setBrush(st::shadowFg); ChatHelpers::PaintStickerThumbnailPath( p, media.get(), @@ -302,7 +315,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[nodiscard]] not_null StickersPreview( not_null parent, - not_null controller, + std::shared_ptr show, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); result->show(); @@ -327,7 +340,7 @@ void PreloadSticker(const std::shared_ptr &media) { bool nextReady = false; int index = 0; }; - const auto premium = &controller->session().api().premium(); + const auto premium = &show->session().api().premium(); const auto state = lifetime.make_state(); const auto create = [=](std::shared_ptr media) { const auto outer = Ui::CreateChild(result); @@ -340,7 +353,7 @@ void PreloadSticker(const std::shared_ptr &media) { [[maybe_unused]] const auto sticker = StickerPreview( outer, - controller, + show, media, state->singleReadyCallback); @@ -520,7 +533,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null VideoPreview( not_null parent, - not_null controller, + std::shared_ptr show, not_null document, bool alignToBottom, Fn readyCallback) { @@ -683,7 +696,7 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenericPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { const auto result = Ui::CreateChild(parent.get()); @@ -699,7 +712,7 @@ struct VideoPreviewDocument { std::vector> medias; Ui::RpWidget *single = nullptr; }; - const auto session = &controller->session(); + const auto session = &show->session(); const auto state = lifetime.make_state(); const auto create = [=] { const auto document = LookupVideo(session, section); @@ -708,7 +721,7 @@ struct VideoPreviewDocument { } state->single = VideoPreview( result, - controller, + show, document, !VideoAlignToTop(section), readyCallback); @@ -724,14 +737,18 @@ struct VideoPreviewDocument { [[nodiscard]] not_null GenerateDefaultPreview( not_null parent, - not_null controller, + std::shared_ptr show, PremiumPreview section, Fn readyCallback) { switch (section) { case PremiumPreview::Stickers: - return StickersPreview(parent, controller, readyCallback); + return StickersPreview(parent, std::move(show), readyCallback); default: - return GenericPreview(parent, controller, section, readyCallback); + return GenericPreview( + parent, + std::move(show), + section, + readyCallback); } } @@ -792,7 +809,7 @@ struct VideoPreviewDocument { void PreviewBox( not_null box, - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, const QImage &back) { @@ -825,7 +842,7 @@ void PreviewBox( }; const auto state = outer->lifetime().make_state(); state->selected = descriptor.section; - state->order = Settings::PremiumPreviewOrder(&controller->session()); + state->order = Settings::PremiumPreviewOrder(&show->session()); const auto index = [=](PremiumPreview section) { const auto it = ranges::find(state->order, section); @@ -880,7 +897,7 @@ void PreviewBox( }; state->stickersPreload = GenerateDefaultPreview( outer, - controller, + show, PremiumPreview::Stickers, ready); state->stickersPreload->hide(); @@ -890,13 +907,13 @@ void PreviewBox( switch (descriptor.section) { case PremiumPreview::Stickers: state->content = media - ? StickerPreview(outer, controller, media, state->preload) - : StickersPreview(outer, controller, state->preload); + ? StickerPreview(outer, show, media, state->preload) + : StickersPreview(outer, show, state->preload); break; default: state->content = GenericPreview( outer, - controller, + show, descriptor.section, state->preload); break; @@ -955,7 +972,7 @@ void PreviewBox( } else { state->content = GenerateDefaultPreview( outer, - controller, + show, now, state->preload); } @@ -1003,7 +1020,7 @@ void PreviewBox( state->preload(); } }; - if (descriptor.fromSettings && controller->session().premium()) { + if (descriptor.fromSettings && show->session().premium()) { box->setShowFinishedCallback(showFinished); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); } else { @@ -1030,16 +1047,21 @@ void PreviewBox( auto button = descriptor.fromSettings ? object_ptr::fromRaw( Settings::CreateSubscribeButton({ - controller, - box, - computeRef, + .parent = box, + .computeRef = computeRef, + .show = show, })) : CreateUnlockButton(box, std::move(unlock)); button->resizeToWidth(width); if (!descriptor.fromSettings) { button->setClickedCallback([=] { + const auto window = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (!window) { + return; + } Settings::ShowPremium( - controller, + window, Settings::LookupPremiumRef(state->selected.current())); }); } @@ -1052,7 +1074,7 @@ void PreviewBox( if (descriptor.fromSettings) { Data::AmPremiumValue( - &controller->session() + &show->session() ) | rpl::skip(1) | rpl::start_with_next([=] { box->closeBox(); }, box->lifetime()); @@ -1076,25 +1098,26 @@ void PreviewBox( } void Show( - not_null controller, + std::shared_ptr show, const Descriptor &descriptor, const std::shared_ptr &media, QImage back) { - const auto box = controller->show( - Box(PreviewBox, controller, descriptor, media, back)); + auto box = Box(PreviewBox, show, descriptor, media, back); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + descriptor.shownCallback(raw); } } -void Show(not_null controller, QImage back) { +void Show(std::shared_ptr show, QImage back) { auto &list = Preloads(); for (auto i = begin(list); i != end(list);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { - Show(controller, i->descriptor, i->media, back); + } else if (already == show) { + Show(std::move(show), i->descriptor, i->media, back); i = list.erase(i); return; } else { @@ -1103,22 +1126,86 @@ void Show(not_null controller, 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( - not_null controller, + std::shared_ptr show, Descriptor &&descriptor) { - if (!controller->session().premiumPossible()) { - const auto box = controller->show(Box(PremiumUnavailableBox)); + if (!show->session().premiumPossible()) { + auto box = Box(PremiumUnavailableBox); + const auto raw = box.data(); + show->showBox(std::move(box)); if (descriptor.shownCallback) { - descriptor.shownCallback(box); + 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);) { - const auto already = i->controller.get(); + const auto already = i->show.lock(); if (!already) { i = list.erase(i); - } else if (already == controller) { + } else if (already == show) { if (i->descriptor == descriptor) { return; } @@ -1135,13 +1222,13 @@ void Show( } } - const auto weak = base::make_weak(controller); + const auto weak = std::weak_ptr(show); list.push_back({ .descriptor = descriptor, .media = (descriptor.requestedSticker ? descriptor.requestedSticker->createMediaView() : nullptr), - .controller = weak, + .show = weak, }); if (const auto &media = list.back().media) { PreloadSticker(media); @@ -1166,8 +1253,8 @@ void Show( Images::CornersMask(st::boxRadius), RectPart::TopLeft | RectPart::TopRight); crl::on_main([=] { - if (const auto strong = weak.get()) { - Show(strong, result); + if (auto strong = weak.lock()) { + Show(std::move(strong), result); } }); }); @@ -1176,9 +1263,9 @@ void Show( } // namespace void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document) { - Show(controller, Descriptor{ + Show(std::move(show), Descriptor{ .section = PremiumPreview::Stickers, .requestedSticker = document, }); @@ -1188,7 +1275,14 @@ void ShowPremiumPreviewBox( not_null controller, PremiumPreview section, Fn)> shown) { - Show(controller, Descriptor{ + ShowPremiumPreviewBox(controller->uiShow(), section, std::move(shown)); +} + +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown) { + Show(std::move(show), Descriptor{ .section = section, .shownCallback = std::move(shown), }); @@ -1198,7 +1292,7 @@ void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, Fn hiddenCallback) { - Show(controller, Descriptor{ + Show(controller->uiShow(), Descriptor{ .section = section, .fromSettings = true, .hiddenCallback = std::move(hiddenCallback), @@ -1217,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, @@ -1233,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, @@ -1245,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, @@ -1257,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, @@ -1269,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, @@ -1281,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), @@ -1291,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), @@ -1301,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, @@ -1313,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, @@ -1327,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, @@ -1337,7 +1433,64 @@ void DoubledLimitsPreviewBox( Main::Domain::kPremiumMaxAccounts, till, }); - Ui::Premium::ShowListBox(box, std::move(entries)); + Ui::Premium::ShowListBox( + box, + st::defaultPremiumLimits, + 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( diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index 0bf439d29..dbe2be446 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class DocumentData; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct ReactionId; } // namespace Data @@ -30,14 +34,20 @@ class Session; } // namespace Main void ShowStickerPreviewBox( - not_null controller, + std::shared_ptr show, not_null document); void DoubledLimitsPreviewBox( not_null box, not_null session); +void UpgradedStoriesPreviewBox( + not_null box, + not_null session); + enum class PremiumPreview { + Stories, + DoubleLimits, MoreUpload, FasterDownload, VoiceToText, @@ -59,6 +69,11 @@ void ShowPremiumPreviewBox( PremiumPreview section, Fn)> shown = nullptr); +void ShowPremiumPreviewBox( + std::shared_ptr show, + PremiumPreview section, + Fn)> shown = nullptr); + void ShowPremiumPreviewToBuy( not_null controller, PremiumPreview section, diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp index e58a1de8e..f0282306b 100644 --- a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -27,7 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/effects/scroll_content_shadow.h" #include "ui/layers/generic_box.h" -#include "ui/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/scroll_area.h" @@ -63,7 +62,8 @@ PeerId GenerateUser(not_null history, const QString &name) { MTPstring(), // bot placeholder MTPstring(), // lang code MTPEmojiStatus(), - MTPVector())); + MTPVector(), + MTPint())); // stories_max_id return peerId; } @@ -77,11 +77,11 @@ AdminLog::OwnedItem GenerateItem( const auto item = history->addNewLocalMessage( history->nextNonHistoryEntryId(), - MessageFlag::FakeHistoryItem + (MessageFlag::FakeHistoryItem | MessageFlag::HasFromId - | MessageFlag::HasReplyInfo, + | MessageFlag::HasReplyInfo), UserId(), // via - replyTo, + FullReplyTo{ .msgId = replyTo }, base::unixtime::now(), // date from, QString(), // postAuthor @@ -384,7 +384,7 @@ void AddReactionAnimatedIcon( const auto paintCallback = [=](not_null widget, QPainter &p) { const auto paintFrame = [&](not_null animation) { - const auto frame = animation->frame(); + const auto frame = animation->frame(st::windowFg->c); p.drawImage( QRect( (widget->width() - iconSize) / 2, @@ -404,7 +404,6 @@ void AddReactionAnimatedIcon( } else if (const auto select = state->select.icon.get()) { paintFrame(select); } - }; const auto widget = AddReactionIconWrap( parent, diff --git a/Telegram/SourceFiles/boxes/report_messages_box.cpp b/Telegram/SourceFiles/boxes/report_messages_box.cpp index 73223c1fd..5c8d59133 100644 --- a/Telegram/SourceFiles/boxes/report_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/report_messages_box.cpp @@ -14,12 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/report_box.h" #include "ui/layers/generic_box.h" #include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" namespace { [[nodiscard]] object_ptr Report( not_null peer, - std::variant> data) { + std::variant< + v::null_t, + MessageIdsList, + not_null, + StoryId> data, + const style::ReportBox *stOverride) { const auto source = v::match(data, [](const MessageIdsList &ids) { return Ui::ReportSource::Message; }, [&](not_null photo) { @@ -34,18 +40,20 @@ namespace { : (photo->hasVideo() ? Ui::ReportSource::ChannelVideo : Ui::ReportSource::ChannelPhoto); + }, [&](StoryId id) { + return Ui::ReportSource::Story; }, [](v::null_t) { Unexpected("Bad source report."); return Ui::ReportSource::Bot; }); + const auto st = stOverride ? stOverride : &st::defaultReportBox; return Box([=](not_null box) { - Ui::ReportReasonBox(box, source, [=](Ui::ReportReason reason) { - Ui::BoxShow(box).showBox(Box([=](not_null box) { - const auto show = Ui::BoxShow(box); - Ui::ReportDetailsBox(box, [=](const QString &text) { - const auto toastParent = show.toastParent(); - Api::SendReport(toastParent, peer, reason, text, data); - show.hideLayer(); + const auto show = box->uiShow(); + Ui::ReportReasonBox(box, *st, source, [=](Ui::ReportReason reason) { + show->showBox(Box([=](not_null box) { + Ui::ReportDetailsBox(box, *st, [=](const QString &text) { + Api::SendReport(show, peer, reason, text, data); + show->hideLayer(); }); })); }); @@ -57,13 +65,13 @@ namespace { object_ptr ReportItemsBox( not_null peer, MessageIdsList ids) { - return Report(peer, ids); + return Report(peer, ids, nullptr); } object_ptr ReportProfilePhotoBox( not_null peer, not_null photo) { - return Report(peer, photo); + return Report(peer, photo, nullptr); } void ShowReportPeerBox( @@ -79,7 +87,7 @@ void ShowReportPeerBox( const auto send = [=](const QString &text) { window->clearChooseReportMessages(); Api::SendReport( - Window::Show(window).toastParent(), + window->uiShow(), peer, reason, text, @@ -94,17 +102,20 @@ void ShowReportPeerBox( if (reason == Ui::ReportReason::Fake || reason == Ui::ReportReason::Other) { state->ids = {}; - state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send)); + state->detailsBox = window->show( + Box(Ui::ReportDetailsBox, st::defaultReportBox, send)); return; } window->showChooseReportMessages(peer, reason, [=]( MessageIdsList ids) { state->ids = std::move(ids); - state->detailsBox = window->show(Box(Ui::ReportDetailsBox, send)); + state->detailsBox = window->show( + Box(Ui::ReportDetailsBox, st::defaultReportBox, send)); }); }; state->reasonBox = window->show(Box( Ui::ReportReasonBox, + st::defaultReportBox, (peer->isBroadcast() ? Ui::ReportSource::Channel : peer->isUser() 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 205d0ba9a..c639460ba 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -83,9 +83,9 @@ void FileDialogCallback( Fn checkResult, Fn callback, bool premium, - not_null toastParent) { + std::shared_ptr show) { auto showError = [=](tr::phrase<> text) { - Ui::Toast::Show(toastParent, text(tr::now)); + show->showToast(text(tr::now)); }; auto list = Storage::PreparedFileFromFilesDialog( @@ -137,13 +137,19 @@ SendFilesLimits DefaultLimitsForPeer(not_null peer) { SendFilesCheck DefaultCheckForPeer( not_null controller, not_null peer) { + return DefaultCheckForPeer(controller->uiShow(), peer); +} + +SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer) { return [=]( const Ui::PreparedFile &file, bool compress, bool silent) { const auto error = Data::FileRestrictionError(peer, file, compress); if (error && !silent) { - controller->showToast({ *error }); + show->showToast(*error); } return !error.has_value(); }; @@ -151,6 +157,7 @@ SendFilesCheck DefaultCheckForPeer( SendFilesBox::Block::Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -170,20 +177,24 @@ SendFilesBox::Block::Block( if (_isAlbum) { const auto preview = Ui::CreateChild( parent.get(), + st, my, way); _preview.reset(preview); } else { const auto media = Ui::SingleMediaPreview::Create( parent, + st, gifPaused, first); if (media) { _isSingleMedia = true; _preview.reset(media); } else { - _preview.reset( - Ui::CreateChild(parent.get(), first)); + _preview.reset(Ui::CreateChild( + parent.get(), + st, + first)); } } _preview->show(); @@ -328,15 +339,32 @@ SendFilesBox::SendFilesBox( SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType) -: _controller(controller) -, _sendType(sendType) +: SendFilesBox(nullptr, { + .show = controller->uiShow(), + .list = std::move(list), + .caption = caption, + .limits = limits, + .check = check, + .sendType = sendType, + .sendMenuType = sendMenuType, +}) { +} + +SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor) +: _show(std::move(descriptor.show)) +, _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _sendType(descriptor.sendType) , _titleHeight(st::boxTitleHeight) -, _list(std::move(list)) -, _limits(limits) -, _sendMenuType(sendMenuType) -, _check(std::move(check)) -, _caption(this, st::confirmCaptionArea, Ui::InputField::Mode::MultiLine) -, _prefilledCaptionText(std::move(caption)) +, _list(std::move(descriptor.list)) +, _limits(descriptor.limits) +, _sendMenuType(descriptor.sendMenuType) +, _check(std::move(descriptor.check)) +, _confirmedCallback(std::move(descriptor.confirmed)) +, _cancelledCallback(std::move(descriptor.cancelled)) +, _caption(this, _st.files.caption, Ui::InputField::Mode::MultiLine) +, _prefilledCaptionText(std::move(descriptor.caption)) , _scroll(this, st::boxScroll) , _inner( _scroll->setOwnedWidget( @@ -431,7 +459,7 @@ void SendFilesBox::setupDragArea() { const auto droppedCallback = [=](bool compress) { return [=](const QMimeData *data) { addFiles(data); - Window::ActivateWindow(_controller); + _show->activate(); }; }; areas.document->setDroppedCallback(droppedCallback(false)); @@ -468,24 +496,24 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn perform) { } void SendFilesBox::openDialogToAddFileToAlbum() { - const auto toastParent = Ui::BoxShow(this).toastParent(); + const auto show = uiShow(); const auto checkResult = [=](const Ui::PreparedList &list) { if (!(_limits & SendFilesAllow::OnlyOne)) { return true; } else if (!_list.canBeSentInSlowmodeWith(list)) { - Ui::Toast::Show(toastParent, tr::lng_slowmode_no_many(tr::now)); + showToast(tr::lng_slowmode_no_many(tr::now)); return false; } return true; }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, [=](Ui::PreparedList list) { addFiles(std::move(list)); }, premium, - toastParent); + show); }; FileDialog::GetOpenPaths( @@ -508,7 +536,8 @@ void SendFilesBox::refreshButtons() { _send, [=] { return _sendMenuType; }, [=] { sendSilent(); }, - [=] { sendScheduled(); }); + [=] { sendScheduled(); }, + [=] { sendWhenOnline(); }); } addButton(tr::lng_cancel(), [=] { closeBox(); }); _addFile = addLeftButton( @@ -562,11 +591,11 @@ void SendFilesBox::addMenuButton() { return; } - const auto top = addTopButton(st::infoTopBarMenu); + const auto top = addTopButton(_st.files.menu); top->setClickedCallback([=] { - _menu = base::make_unique_q( - top, - st::popupMenuExpandedSeparator); + const auto &tabbed = _st.tabbed; + const auto &icons = tabbed.icons; + _menu = base::make_unique_q(top, tabbed.menu); if (hasSpoilerMenu()) { const auto spoilered = allWithSpoilers(); _menu->addAction( @@ -574,9 +603,9 @@ void SendFilesBox::addMenuButton() { ? tr::lng_context_disable_spoiler(tr::now) : tr::lng_context_spoiler_effect(tr::now)), [=] { toggleSpoilers(!spoilered); }, - spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler); + spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler); if (hasSendMenu()) { - _menu->addSeparator(); + _menu->addSeparator(&tabbed.expandedSeparator); } } if (hasSendMenu()) { @@ -584,7 +613,9 @@ void SendFilesBox::addMenuButton() { _menu.get(), _sendMenuType, [=] { sendSilent(); }, - [=] { sendScheduled(); }); + [=] { sendScheduled(); }, + [=] { sendWhenOnline(); }, + &_st.tabbed.icons); } _menu->popup(QCursor::pos()); return true; @@ -709,12 +740,12 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) { } void SendFilesBox::pushBlock(int from, int till) { - const auto gifPaused = [controller = _controller] { - return controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); + const auto gifPaused = [show = _show] { + return show->paused(Window::GifPauseReason::Layer); }; _blocks.emplace_back( _inner.data(), + _st, &_list.files, from, till, @@ -729,6 +760,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); @@ -746,9 +779,11 @@ void SendFilesBox::pushBlock(int from, int till) { }); }, widget->lifetime()); - const auto toastParent = Ui::BoxShow(this).toastParent(); + const auto show = uiShow(); block.itemReplaceRequest( ) | rpl::start_with_next([=](int index) { + applyBlockChanges(); + const auto replace = [=](Ui::PreparedList list) { if (list.files.empty()) { return; @@ -768,9 +803,7 @@ void SendFilesBox::pushBlock(int from, int till) { _list.files.push_back(std::move(removing)); std::swap(_list.files[index], _list.files.back()); if (!result) { - Ui::Toast::Show( - toastParent, - tr::lng_slowmode_no_many(tr::now)); + show->showToast(tr::lng_slowmode_no_many(tr::now)); return false; } return true; @@ -807,13 +840,13 @@ void SendFilesBox::pushBlock(int from, int till) { return checkSlowmode(list) && checkRights(list); }; const auto callback = [=](FileDialog::OpenResult &&result) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); FileDialogCallback( std::move(result), checkResult, replace, premium, - toastParent); + show); }; FileDialog::GetOpenPath( @@ -825,15 +858,17 @@ void SendFilesBox::pushBlock(int from, int till) { const auto openedOnce = widget->lifetime().make_state(false); block.itemModifyRequest( - ) | rpl::start_with_next([=, controller = _controller](int index) { + ) | rpl::start_with_next([=, show = _show](int index) { + applyBlockChanges(); + if (!(*openedOnce)) { - controller->session().settings().incrementPhotoEditorHintShown(); - controller->session().saveSettings(); + show->session().settings().incrementPhotoEditorHintShown(); + show->session().saveSettings(); } *openedOnce = true; Editor::OpenWithPreparedFile( this, - controller, + show, &_list.files[index], st::sendMediaPreviewSize, [=] { refreshAllAfterChanges(from); }); @@ -856,12 +891,14 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_send_grouped(tr::now), groupFilesFirst, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendImagesAsPhotos.create( this, tr::lng_send_compressed(tr::now), _sendWay.current().sendImagesAsPhotos(), - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _sendWay.changes( ) | rpl::start_with_next([=](SendFilesWay value) { @@ -905,7 +942,8 @@ void SendFilesBox::setupSendWayControls() { this, tr::lng_remember(tr::now), false, - st::defaultBoxCheckbox); + _st.files.checkbox, + _st.files.check); _wayRemember->hide(); rpl::combine( _groupFiles->checkedValue(), @@ -953,25 +991,32 @@ void SendFilesBox::updateSendWayControls() { : tr::lng_send_compressed_one(tr::now)); _hintLabel->setVisible( - _controller->session().settings().photoEditorHintShown() + _show->session().settings().photoEditorHintShown() ? _list.canHaveEditorHintLabel() : false); } void SendFilesBox::setupCaption() { - const auto allow = [=](const auto&) { + const auto allow = [=](const auto &) { return (_limits & SendFilesAllow::EmojiWithoutPremium); }; + const auto show = _show; InitMessageFieldHandlers( - _controller, + &show->session(), + show, _caption.data(), - Window::GifPauseReason::Layer, - allow); + [=] { return show->paused(Window::GifPauseReason::Layer); }, + allow, + &_st.files.caption); Ui::Emoji::SuggestionsController::Init( getDelegate()->outerContainer(), _caption, - &_controller->session(), - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + &_show->session(), + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); if (!_prefilledCaptionText.text.isEmpty()) { _caption->setTextWithTags( @@ -1019,12 +1064,21 @@ void SendFilesBox::setupEmojiPanel() { using Selector = ChatHelpers::TabbedSelector; _emojiPanel = base::make_unique_q( container, - _controller, - object_ptr( - nullptr, - _controller, - Window::GifPauseReason::Layer, - Selector::Mode::EmojiOnly)); + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + }); _emojiPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -1041,11 +1095,9 @@ void SendFilesBox::setupEmojiPanel() { const auto info = data.document->sticker(); if (info && info->setType == Data::StickersType::Emoji - && !_controller->session().premium() + && !_show->session().premium() && !(_limits & SendFilesAllow::EmojiWithoutPremium)) { - ShowPremiumPreviewBox( - _controller, - PremiumPreview::AnimatedEmoji); + ShowPremiumPreviewBox(_show, PremiumPreview::AnimatedEmoji); } else { Data::InsertCustomEmoji(_caption.data(), data.document); } @@ -1057,7 +1109,7 @@ void SendFilesBox::setupEmojiPanel() { }; _emojiFilter.reset(base::install_event_filter(container, filterCallback)); - _emojiToggle.create(this, st::boxAttachEmoji); + _emojiToggle.create(this, _st.files.emoji); _emojiToggle->setVisible(!_caption->isHidden()); _emojiToggle->installEventFilter(_emojiPanel); _emojiToggle->addClickHandler([=] { @@ -1095,7 +1147,7 @@ bool SendFilesBox::canAddFiles(not_null data) const { } bool SendFilesBox::addFiles(not_null data) { - const auto premium = _controller->session().premium(); + const auto premium = _show->session().premium(); auto list = [&] { const auto urls = Core::ReadMimeUrls(data); auto result = CanAddUrls(urls) @@ -1242,7 +1294,7 @@ void SendFilesBox::paintEvent(QPaintEvent *e) { Painter p(this); p.setFont(st::boxTitleFont); - p.setPen(st::boxTitleFg); + p.setPen(getDelegate()->style().title.textFg); p.drawTextLeft( st::boxPhotoTitlePosition.x(), st::boxTitlePosition.y() - st::boxTopMargin, @@ -1318,7 +1370,7 @@ void SendFilesBox::saveSendWaySettings() { } bool SendFilesBox::validateLength(const QString &text) const { - const auto session = &_controller->session(); + const auto session = &_show->session(); const auto limit = Data::PremiumLimits(session).captionLengthCurrent(); const auto remove = int(text.size()) - limit; const auto way = _sendWay.current(); @@ -1328,7 +1380,8 @@ bool SendFilesBox::validateLength(const QString &text) const { way.sendImagesAsPhotos())) { return true; } - _controller->show(Box(CaptionLimitReachedBox, session, remove)); + _show->showBox( + Box(CaptionLimitReachedBox, session, remove, &_st.premium)); return false; } @@ -1385,9 +1438,11 @@ void SendFilesBox::sendScheduled() { ? SendMenu::Type::ScheduledToUser : _sendMenuType; const auto callback = [=](Api::SendOptions options) { send(options); }; - _controller->show( - HistoryView::PrepareScheduleBox(this, type, callback), - Ui::LayerOption::KeepOther); + _show->showBox(HistoryView::PrepareScheduleBox(this, type, callback)); +} + +void SendFilesBox::sendWhenOnline() { + send(Api::DefaultSendWhenOnlineOptions()); } SendFilesBox::~SendFilesBox() = default; diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 22510b9f3..6e6c6fa82 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localimageloader.h" #include "storage/storage_media_prepare.h" +namespace style { +struct ComposeControls; +} // namespace style + namespace Window { class SessionController; } // namespace Window @@ -26,6 +30,7 @@ enum class SendType; namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { @@ -71,6 +76,29 @@ using SendFilesCheck = Fn controller, not_null peer); +[[nodiscard]] SendFilesCheck DefaultCheckForPeer( + std::shared_ptr show, + not_null peer); + +using SendFilesConfirmed = Fn; + +struct SendFilesBoxDescriptor { + std::shared_ptr show; + Ui::PreparedList list; + TextWithTags caption; + SendFilesLimits limits = {}; + SendFilesCheck check; + Api::SendType sendType = {}; + SendMenu::Type sendMenuType = {}; + const style::ComposeControls *stOverride = nullptr; + SendFilesConfirmed confirmed; + Fn cancelled; +}; class SendFilesBox : public Ui::BoxContent { public: @@ -87,14 +115,9 @@ public: SendFilesCheck check, Api::SendType sendType, SendMenu::Type sendMenuType); + SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor); - void setConfirmedCallback( - Fn callback) { + void setConfirmedCallback(SendFilesConfirmed callback) { _confirmedCallback = std::move(callback); } void setCancelledCallback(Fn callback) { @@ -116,6 +139,7 @@ private: public: Block( not_null parent, + const style::ComposeControls &st, not_null*> items, int from, int till, @@ -179,6 +203,7 @@ private: void send(Api::SendOptions options, bool ctrlShiftEnter = false); void sendSilent(); void sendScheduled(); + void sendWhenOnline(); void captionResized(); void saveSendWaySettings(); @@ -200,7 +225,8 @@ private: void enqueueNextPrepare(); void addPreparedAsyncFile(Ui::PreparedFile &&file); - const not_null _controller; + const std::shared_ptr _show; + const style::ComposeControls &_st; const Api::SendType _sendType = Api::SendType(); QString _titleText; @@ -210,15 +236,10 @@ private: std::optional _removingIndex; SendFilesLimits _limits = {}; - SendMenu::Type _sendMenuType = SendMenu::Type(); + SendMenu::Type _sendMenuType = {}; SendFilesCheck _check; - Fn _confirmedCallback; + SendFilesConfirmed _confirmedCallback; Fn _cancelledCallback; bool _confirmed = false; diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index a60927da4..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()); @@ -846,7 +843,7 @@ void SessionsContent::terminate(Fn terminateRequest, QString message) { .confirmStyle = &st::attentionBoxButton, }); _terminateBox = Ui::MakeWeak(box.data()); - _controller->show(std::move(box), Ui::LayerOption::KeepOther); + _controller->show(std::move(box)); } void SessionsContent::terminateOne(uint64 hash) { @@ -928,7 +925,7 @@ void SessionsContent::Inner::setupContent() { rename->moveToRight(x, y, outer.width()); }, rename->lifetime()); rename->setClickedCallback([=] { - _controller->show(Box(RenameBox), Ui::LayerOption::KeepOther); + _controller->show(Box(RenameBox)); }); const auto session = &_controller->session(); @@ -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/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 124f43e36..7cba7daba 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -63,7 +63,7 @@ public: Inner( QWidget *parent, const Descriptor &descriptor, - std::shared_ptr show); + std::shared_ptr show); void setPeerSelectedChangedCallback( Fn thread, bool selected)> callback); @@ -146,7 +146,7 @@ private: void refresh(); const Descriptor &_descriptor; - const std::shared_ptr _show; + const std::shared_ptr _show; const style::PeerList &_st; float64 _columnSkip = 0.; @@ -181,7 +181,6 @@ private: ShareBox::ShareBox(QWidget*, Descriptor &&descriptor) : _descriptor(std::move(descriptor)) , _api(&_descriptor.session->mtp()) -, _show(std::make_shared(this)) , _select( this, (_descriptor.stMultiSelect @@ -231,10 +230,10 @@ void ShareBox::prepareCommentField() { connect(field, &Ui::InputField::submitted, [=] { submit({}); }); - if (_show->valid()) { + if (const auto show = uiShow(); show->valid()) { InitMessageFieldHandlers( _descriptor.session, - _show, + Main::MakeSessionShow(show, _descriptor.session), field, nullptr, nullptr, @@ -257,7 +256,7 @@ void ShareBox::prepare() { setTitle(tr::lng_share_title()); _inner = setInnerWidget( - object_ptr(this, _descriptor, _show), + object_ptr(this, _descriptor, uiShow()), getTopScrollSkip(), getBottomScrollSkip()); @@ -502,7 +501,8 @@ void ShareBox::showMenu(not_null parent) { _menu.get(), sendMenuType(), [=] { submitSilent(); }, - [=] { submitScheduled(); }); + [=] { submitScheduled(); }, + [=] { submitWhenOnline(); }); const auto success = (result == SendMenu::FillMenuResult::Success); if (_descriptor.forwardOptions.show || success) { _menu->setForcedVerticalOrigin(Ui::PopupMenu::VerticalOrigin::Bottom); @@ -589,14 +589,17 @@ void ShareBox::submitSilent() { void ShareBox::submitScheduled() { const auto callback = [=](Api::SendOptions options) { submit(options); }; - _show->showBox( + uiShow()->showBox( HistoryView::PrepareScheduleBox( this, sendMenuType(), callback, HistoryView::DefaultScheduleTime(), - _descriptor.scheduleBoxStyle), - Ui::LayerOption::KeepOther); + _descriptor.scheduleBoxStyle)); +} + +void ShareBox::submitWhenOnline() { + submit(Api::DefaultSendWhenOnlineOptions()); } void ShareBox::copyLink() { @@ -638,7 +641,7 @@ void ShareBox::scrollAnimationCallback() { ShareBox::Inner::Inner( QWidget *parent, const Descriptor &descriptor, - std::shared_ptr show) + std::shared_ptr show) : RpWidget(parent) , _descriptor(descriptor) , _show(std::move(show)) @@ -1368,9 +1371,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( ).append("\n\n"); } text.append(error.first); - show->showBox( - Ui::MakeInformBox(text), - Ui::LayerOption::KeepOther); + show->showBox(Ui::MakeInformBox(text)); return; } @@ -1413,15 +1414,16 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( ? MsgId(0) : topicRootId; const auto peer = thread->peer(); - histories.sendRequest(history, requestType, [=]( + const auto threadHistory = thread->owningHistory(); + histories.sendRequest(threadHistory, requestType, [=]( Fn finish) { - auto &api = history->session().api(); + auto &api = threadHistory->session().api(); const auto sendFlags = commonSendFlags | (topMsgId ? Flag::f_top_msg_id : Flag(0)) | (ShouldSendSilent(peer, options) ? Flag::f_silent : Flag(0)); - history->sendRequestId = api.request( + threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), history->peer->input, @@ -1432,33 +1434,28 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_int(options.scheduled), MTP_inputPeerEmpty() // send_as )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { - history->session().api().applyUpdates(updates); + threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); if (state->requests.empty()) { if (show->valid()) { - Ui::Toast::Show( - show->toastParent(), - tr::lng_share_done(tr::now)); + show->showToast(tr::lng_share_done(tr::now)); show->hideLayer(); } } finish(); }).fail([=](const MTP::Error &error) { if (error.type() == u"VOICE_MESSAGES_FORBIDDEN"_q) { - if (show->valid()) { - Ui::Toast::Show( - show->toastParent(), - tr::lng_restricted_send_voice_messages( - tr::now, - lt_user, - peer->name())); - } + show->showToast( + tr::lng_restricted_send_voice_messages( + tr::now, + lt_user, + peer->name())); } finish(); - }).afterRequest(history->sendRequestId).send(); - return history->sendRequestId; + }).afterRequest(threadHistory->sendRequestId).send(); + return threadHistory->sendRequestId; }); - state->requests.insert(history->sendRequestId); + state->requests.insert(threadHistory->sendRequestId); } }; } @@ -1466,7 +1463,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( void FastShareMessage( not_null controller, not_null item) { - const auto show = std::make_shared(controller); + const auto show = controller->uiShow(); const auto history = item->history(); const auto owner = &history->owner(); const auto session = &history->session(); @@ -1488,7 +1485,7 @@ void FastShareMessage( return item->media() && item->media()->forceForwardedInfo(); }); - auto copyCallback = [=, toastParent = show->toastParent()] { + auto copyCallback = [=] { const auto item = owner->message(msgIds[0]); if (!item) { return; @@ -1504,8 +1501,7 @@ void FastShareMessage( QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show( - toastParent, + show->showToast( tr::lng_share_game_link_copied(tr::now)); } } diff --git a/Telegram/SourceFiles/boxes/share_box.h b/Telegram/SourceFiles/boxes/share_box.h index f61669efc..b13638577 100644 --- a/Telegram/SourceFiles/boxes/share_box.h +++ b/Telegram/SourceFiles/boxes/share_box.h @@ -118,6 +118,7 @@ private: void submit(Api::SendOptions options); void submitSilent(); void submitScheduled(); + void submitWhenOnline(); void copyLink(); bool searchByUsername(bool useCache = false); @@ -146,8 +147,6 @@ private: Descriptor _descriptor; MTP::Sender _api; - std::shared_ptr _show; - object_ptr _select; object_ptr> _comment; object_ptr _bottomWidget; diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index d5b7e717d..82e4e3deb 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -38,10 +38,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/cached_round_corners.h" #include "lottie/lottie_multi_player.h" #include "lottie/lottie_animation.h" +#include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" #include "chat_helpers/stickers_list_widget.h" #include "media/clip/media_clip_reader.h" -#include "window/window_session_controller.h" #include "window/window_controller.h" #include "settings/settings_premium.h" #include "base/unixtime.h" @@ -230,7 +230,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { public: Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, const StickerSetIdentifier &set, Data::StickersType type); @@ -322,11 +322,14 @@ private: not_null getLottiePlayer(); void showPreview(); + void showPreviewAt(QPoint globalPos); void updateItems(); void repaintItems(crl::time now = 0); - const not_null _controller; + const std::shared_ptr _show; + const not_null _session; + MTP::Sender _api; std::vector _elements; std::unique_ptr _lottiePlayer; @@ -381,33 +384,35 @@ private: }; StickerSetBox::StickerSetBox( - QWidget*, - not_null controller, + QWidget *parent, + std::shared_ptr show, const StickerSetIdentifier &set, Data::StickersType type) -: _controller(controller) +: _show(std::move(show)) +, _session(&_show->session()) , _set(set) , _type(type) { } StickerSetBox::StickerSetBox( QWidget *parent, - not_null controller, + std::shared_ptr show, not_null set) -: StickerSetBox(parent, controller, set->identifier(), set->type()) { +: StickerSetBox(parent, std::move(show), set->identifier(), set->type()) { } QPointer StickerSetBox::Show( - not_null controller, + std::shared_ptr show, not_null document) { if (const auto sticker = document->sticker()) { if (sticker->set) { - return controller->show( - Box( - controller, - sticker->set, - sticker->setType), - Ui::LayerOption::KeepOther).data(); + auto box = Box( + show, + sticker->set, + sticker->setType); + const auto result = QPointer(box.data()); + show->showBox(std::move(box)); + return result; } } return nullptr; @@ -417,9 +422,9 @@ void StickerSetBox::prepare() { setTitle(tr::lng_contacts_loading()); _inner = setInnerWidget( - object_ptr(this, _controller, _set, _type), + object_ptr(this, _show, _set, _type), st::stickersScroll); - _controller->session().data().stickers().updated( + _session->data().stickers().updated( _type ) | rpl::start_with_next([=] { updateButtons(); @@ -441,14 +446,12 @@ void StickerSetBox::prepare() { _inner->setInstalled( ) | rpl::start_with_next([=](uint64 setId) { if (_inner->setType() == Data::StickersType::Masks) { - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - tr::lng_masks_installed(tr::now)); + showToast(tr::lng_masks_installed(tr::now)); } else if (_inner->setType() == Data::StickersType::Emoji) { - auto &stickers = _controller->session().data().stickers(); + auto &stickers = _session->data().stickers(); stickers.notifyEmojiSetInstalled(setId); } else if (_inner->setType() == Data::StickersType::Stickers) { - auto &stickers = _controller->session().data().stickers(); + auto &stickers = _session->data().stickers(); stickers.notifyStickerSetInstalled(setId); } closeBox(); @@ -466,20 +469,18 @@ void StickerSetBox::prepare() { return; } - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - (type == Data::StickersType::Masks) + showToast((type == Data::StickersType::Masks) ? tr::lng_masks_has_been_archived(tr::now) : tr::lng_stickers_has_been_archived(tr::now)); auto &order = (type == Data::StickersType::Masks) - ? _controller->session().data().stickers().maskSetsOrderRef() - : _controller->session().data().stickers().setsOrderRef(); + ? _session->data().stickers().maskSetsOrderRef() + : _session->data().stickers().setsOrderRef(); const auto index = order.indexOf(setId); if (index != -1) { order.removeAt(index); - auto &local = _controller->session().local(); + auto &local = _session->local(); if (type == Data::StickersType::Masks) { local.writeInstalledMasks(); local.writeArchivedMasks(); @@ -489,7 +490,7 @@ void StickerSetBox::prepare() { } } - _controller->session().data().stickers().notifyUpdated(type); + _session->data().stickers().notifyUpdated(type); closeBox(); }, lifetime()); @@ -501,7 +502,7 @@ void StickerSetBox::addStickers() { void StickerSetBox::copyStickersLink() { const auto part = _inner->isEmojiSet() ? u"addemoji"_q : "addstickers"; - const auto url = _controller->session().createInternalLinkFull( + const auto url = _session->createInternalLinkFull( part + '/' + _inner->shortName()); QGuiApplication::clipboard()->setText(url); } @@ -513,7 +514,7 @@ void StickerSetBox::handleError(Error error) { switch (error) { case Error::NotFound: - _controller->show( + _show->showBox( Ui::MakeInformBox(tr::lng_stickers_not_found(tr::now))); break; default: Unexpected("Error in StickerSetBox::handleError."); @@ -531,15 +532,13 @@ void StickerSetBox::updateButtons() { const auto type = _inner->setType(); const auto share = [=] { copyStickersLink(); - Ui::Toast::Show( - Ui::BoxShow(this).toastParent(), - (type == Data::StickersType::Emoji + showToast(type == Data::StickersType::Emoji ? tr::lng_stickers_copied_emoji(tr::now) - : tr::lng_stickers_copied(tr::now))); + : tr::lng_stickers_copied(tr::now)); }; if (_inner->notInstalled()) { - if (!_controller->session().premium() - && _controller->session().premiumPossible() + if (!_session->premium() + && _session->premiumPossible() && _inner->premiumEmojiSet()) { const auto &st = st::premiumPreviewDoubledLimitsBox; setStyle(st); @@ -550,7 +549,11 @@ void StickerSetBox::updateButtons() { - st.buttonPadding.left() - st.buttonPadding.left()); button->setClickedCallback([=] { - Settings::ShowPremium(_controller, u"animated_emoji"_q); + using namespace ChatHelpers; + const auto usage = WindowUsage::PremiumPromo; + if (const auto window = _show->resolveWindow(usage)) { + Settings::ShowPremium(window, u"animated_emoji"_q); + } }); addButton(std::move(button)); } else { @@ -600,14 +603,13 @@ void StickerSetBox::updateButtons() { _inner->archiveStickers(); }; const auto remove = [=] { - const auto session = &_controller->session(); + const auto session = &_show->session(); auto box = ChatHelpers::MakeConfirmRemoveSetBox( session, + st::boxLabel, _inner->setId()); if (box) { - _controller->show( - std::move(box), - Ui::LayerOption::KeepOther); + _show->showBox(std::move(box)); } }; const auto menu = @@ -647,12 +649,13 @@ void StickerSetBox::resizeEvent(QResizeEvent *e) { StickerSetBox::Inner::Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, const StickerSetIdentifier &set, Data::StickersType type) : RpWidget(parent) -, _controller(controller) -, _api(&_controller->session().mtp()) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _setId(set.id) , _setAccessHash(set.accessHash) , _setShortName(set.shortName) @@ -660,7 +663,7 @@ StickerSetBox::Inner::Inner( st::windowBgRipple, st::windowBgOver, [=] { repaintItems(); })) -, _premiumMark(&controller->session()) +, _premiumMark(_session) , _updateItemsTimer([=] { updateItems(); }) , _input(set) , _padding((type == Data::StickersType::Emoji) @@ -679,9 +682,9 @@ StickerSetBox::Inner::Inner( _errors.fire(Error::NotFound); }).send(); - _controller->session().api().updateStickers(); + _session->api().updateStickers(); - _controller->session().downloaderTaskFinished( + _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { updateItems(); }, lifetime()); @@ -695,8 +698,8 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { _elements.clear(); _selected = -1; setCursor(style::cur_default); - const auto owner = &_controller->session().data(); - const auto premiumPossible = _controller->session().premiumPossible(); + const auto owner = &_session->data(); + const auto premiumPossible = _session->premiumPossible(); set.match([&](const MTPDmessages_stickerSet &data) { const auto &v = data.vdocuments().v; _pack.reserve(v.size()); @@ -724,7 +727,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { auto p = StickersPack(); p.reserve(stickers.size()); for (auto j = 0, c = int(stickers.size()); j != c; ++j) { - auto doc = _controller->session().data().document(stickers[j].v); + auto doc = _session->data().document(stickers[j].v); if (!doc || !doc->sticker()) continue; p.push_back(doc); @@ -734,7 +737,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { }); } data.vset().match([&](const MTPDstickerSet &set) { - _setTitle = _controller->session().data().stickers().getSetTitle( + _setTitle = _session->data().stickers().getSetTitle( set); _setShortName = qs(set.vshort_name()); _setId = set.vid().v; @@ -748,7 +751,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { if (const auto thumbs = set.vthumbs()) { for (const auto &thumb : thumbs->v) { const auto result = Images::FromPhotoSize( - &_controller->session(), + _session, set, thumb); if (result.location.valid()) { @@ -758,7 +761,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { } return ImageWithLocation(); }(); - const auto &sets = _controller->session().data().stickers().sets(); + const auto &sets = _session->data().stickers().sets(); const auto it = sets.find(_setId); if (it != sets.cend()) { const auto set = it->second.get(); @@ -814,7 +817,7 @@ rpl::producer StickerSetBox::Inner::errors() const { void StickerSetBox::Inner::installDone( const MTPmessages_StickerSetInstallResult &result) { - auto &stickers = _controller->session().data().stickers(); + auto &stickers = _session->data().stickers(); auto &sets = stickers.setsRef(); const auto type = setType(); @@ -837,7 +840,7 @@ void StickerSetBox::Inner::installDone( it = sets.emplace( _setId, std::make_unique( - &_controller->session().data(), + &_session->data(), _setId, _setAccessHash, _setHash, @@ -887,7 +890,7 @@ void StickerSetBox::Inner::installDone( stickers.applyArchivedResult( result.c_messages_stickerSetInstallResultArchive()); } else { - auto &storage = _controller->session().local(); + auto &storage = _session->local(); if (wasArchived && type != Data::StickersType::Emoji) { if (type == Data::StickersType::Masks) { storage.writeArchivedMasks(); @@ -921,13 +924,19 @@ void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) { void StickerSetBox::Inner::mouseMoveEvent(QMouseEvent *e) { updateSelected(); if (_previewShown >= 0) { - int index = stickerFromGlobalPos(e->globalPos()); - if (index >= 0 && index < _pack.size() && index != _previewShown) { - _previewShown = index; - _controller->widget()->showMediaPreview( - Data::FileOriginStickerSet(_setId, _setAccessHash), - _pack[_previewShown]); - } + showPreviewAt(e->globalPos()); + } +} + +void StickerSetBox::Inner::showPreviewAt(QPoint globalPos) { + const auto index = stickerFromGlobalPos(globalPos); + if (index >= 0 + && index < _pack.size() + && index != _previewShown) { + _previewShown = index; + _show->showMediaPreview( + Data::FileOriginStickerSet(_setId, _setAccessHash), + _pack[_previewShown]); } } @@ -955,16 +964,13 @@ void StickerSetBox::Inner::chosen( int index, not_null sticker, Api::SendOptions options) { - const auto controller = _controller; const auto animation = options.scheduled ? Ui::MessageSendingAnimationFrom() : messageSentAnimationInfo(index, sticker); - Ui::PostponeCall(controller, [=] { - controller->stickerOrEmojiChosen({ - .document = sticker, - .options = options, - .messageSendingFrom = animation, - }); + _show->processChosenSticker({ + .document = sticker, + .options = options, + .messageSendingFrom = animation, }); } @@ -988,7 +994,7 @@ auto StickerSetBox::Inner::messageSentAnimationInfo( (rect.height() - size.height()) / 2); return { .type = Ui::MessageSendingAnimationFrom::Type::Sticker, - .localId = _controller->session().data().nextLocalMessageId(), + .localId = _session->data().nextLocalMessageId(), .globalStartGeometry = mapToGlobal( QRect(rect.topLeft() + innerPos, size)), }; @@ -1005,7 +1011,7 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { _menu = base::make_unique_q( this, st::popupMenuWithIcons); - const auto type = _controller->content()->sendMenuType(); + const auto type = _show->sendMenuType(); if (setType() == Data::StickersType::Emoji) { if (const auto t = PrepareTextFromEmoji(_pack[index]); !t.empty()) { _menu->addAction(tr::lng_mediaview_copy(tr::now), [=] { @@ -1023,12 +1029,13 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) { _menu.get(), type, SendMenu::DefaultSilentCallback(sendSelected), - SendMenu::DefaultScheduleCallback(this, type, sendSelected)); + SendMenu::DefaultScheduleCallback(this, type, sendSelected), + SendMenu::DefaultWhenOnlineCallback(sendSelected)); - const auto controller = _controller; + const auto show = _show; const auto toggleFavedSticker = [=] { Api::ToggleFavedSticker( - controller, + show, document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0)); }; @@ -1077,13 +1084,8 @@ void StickerSetBox::Inner::startOverAnimation(int index, float64 from, float64 t } void StickerSetBox::Inner::showPreview() { - int index = stickerFromGlobalPos(QCursor::pos()); - if (index >= 0 && index < _pack.size()) { - _previewShown = index; - _controller->widget()->showMediaPreview( - Data::FileOriginStickerSet(_setId, _setAccessHash), - _pack[_previewShown]); - } + _previewShown = -1; + showPreviewAt(QCursor::pos()); } not_null StickerSetBox::Inner::getLottiePlayer() { @@ -1127,7 +1129,7 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) { const auto now = crl::now(); const auto paused = On(PowerSaving::kStickersPanel) - || _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer); + || _show->paused(ChatHelpers::PauseReason::Layer); for (int32 i = from; i < to; ++i) { for (int32 j = 0; j < _perRow; ++j) { int32 index = i * _perRow + j; @@ -1391,7 +1393,7 @@ bool StickerSetBox::Inner::notInstalled() const { if (!_loaded) { return false; } - const auto &sets = _controller->session().data().stickers().sets(); + const auto &sets = _session->data().stickers().sets(); const auto it = sets.find(_setId); if ((it == sets.cend()) || !(it->second->flags & SetFlag::Installed) @@ -1435,7 +1437,6 @@ void StickerSetBox::Inner::install() { } void StickerSetBox::Inner::archiveStickers() { - const auto toastParent = Window::Show(_controller).toastParent(); _api.request(MTPmessages_InstallStickerSet( Data::InputStickerSet(_input), MTP_boolTrue() @@ -1443,9 +1444,9 @@ void StickerSetBox::Inner::archiveStickers() { if (result.type() == mtpc_messages_stickerSetInstallResultSuccess) { _setArchived.fire_copy(_setId); } - }).fail(crl::guard(toastParent, [=] { - Ui::Toast::Show(toastParent, Lang::Hard::ServerError()); - })).send(); + }).fail([=] { + _show->showToast(Lang::Hard::ServerError()); + }).send(); } void StickerSetBox::Inner::updateItems() { diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index 73d99dc7d..f1992fa66 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -23,6 +23,15 @@ namespace Data { class StickersSet; } // namespace Data +namespace SendMenu { +enum class Type; +} // namespace SendMenu + +namespace ChatHelpers { +struct FileChosen; +class Show; +} // namespace ChatHelpers + class StickerPremiumMark final { public: explicit StickerPremiumMark(not_null session); @@ -51,16 +60,16 @@ class StickerSetBox final : public Ui::BoxContent { public: StickerSetBox( QWidget*, - not_null controller, + std::shared_ptr show, const StickerSetIdentifier &set, Data::StickersType type); StickerSetBox( QWidget*, - not_null controller, + std::shared_ptr show, not_null set); static QPointer Show( - not_null controller, + std::shared_ptr show, not_null document); protected: @@ -79,7 +88,8 @@ private: void copyStickersLink(); void handleError(Error error); - const not_null _controller; + const std::shared_ptr _show; + const not_null _session; const StickerSetIdentifier _set; const Data::StickersType _type; diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index cb38b11df..237306d84 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "storage/storage_account.h" #include "lottie/lottie_single_player.h" +#include "chat_helpers/compose/compose_show.h" #include "chat_helpers/stickers_lottie.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" @@ -35,7 +36,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/unread_badge_paint.h" -#include "window/window_session_controller.h" #include "media/clip/media_clip_reader.h" #include "main/main_session.h" #include "styles/style_layers.h" @@ -77,11 +77,11 @@ public: Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, Section section); Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, not_null megagroup); [[nodiscard]] Main::Session &session() const; @@ -211,7 +211,7 @@ private: void setActionDown(int newActionDown); void setPressed(SelectedRow pressed); void setup(); - QRect relativeButtonRect(bool removeButton) const; + QRect relativeButtonRect(bool removeButton, bool installedSet) const; void ensureRipple(const style::RippleAnimation &st, QImage mask, bool removeButton); bool shiftingAnimationCallback(crl::time now); @@ -234,7 +234,7 @@ private: void readVisibleSets(); void updateControlsGeometry(); - void rebuildAppendSet(not_null set, int maxNameWidth); + void rebuildAppendSet(not_null set); void fillSetCover(not_null set, DocumentData **outSticker, int *outWidth, int *outHeight) const; int fillSetCount(not_null set) const; [[nodiscard]] QString fillSetTitle( @@ -247,16 +247,18 @@ private: void handleMegagroupSetAddressChange(); void setMegagroupSelectedSet(const StickerSetIdentifier &set); - int countMaxNameWidth() const; + int countMaxNameWidth(bool installedSet) const; [[nodiscard]] bool skipPremium() const; - const not_null _controller; + const style::PeerListItem &_st; + const std::shared_ptr _show; + const not_null _session; MTP::Sender _api; const Section _section; - const bool _isInstalled; + const bool _isInstalledTab; - Ui::RoundRect _buttonBgOver, _buttonBg; + Ui::RoundRect _buttonBgOver, _buttonBg, _inactiveButtonBg; int32 _rowHeight = 0; @@ -281,6 +283,8 @@ private: int _addWidth = 0; QString _undoText; int _undoWidth = 0; + QString _installedText; + int _installedWidth = 0; QPoint _mouse; bool _inDragArea = false; @@ -380,35 +384,39 @@ void StickersBox::Tab::saveScrollTop() { StickersBox::StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, Section section, bool masks) -: _controller(controller) -, _api(&controller->session().mtp()) +: _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _tabs(this, st::stickersTabs) , _unreadBadge( this, - controller->session().data().stickers().featuredSetsUnreadCountValue()) + _session->data().stickers().featuredSetsUnreadCountValue()) , _section(section) , _isMasks(masks) , _isEmoji(false) -, _installed(_isMasks ? Tab() : Tab(0, this, controller, Section::Installed)) -, _masks(_isMasks ? Tab(0, this, controller, Section::Masks) : Tab()) -, _featured(_isMasks ? Tab() : Tab(1, this, controller, Section::Featured)) -, _archived((_isMasks ? 1 : 2), this, controller, Section::Archived) { +, _installed(_isMasks ? Tab() : Tab(0, this, _show, Section::Installed)) +, _masks(_isMasks ? Tab(0, this, _show, Section::Masks) : Tab()) +, _featured(_isMasks ? Tab() : Tab(1, this, _show, Section::Featured)) +, _archived((_isMasks ? 1 : 2), this, _show, Section::Archived) { _tabs->setRippleTopRoundRadius(st::boxRadius); } StickersBox::StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, not_null megagroup) -: _controller(controller) -, _api(&controller->session().mtp()) +: _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _section(Section::Installed) , _isMasks(false) , _isEmoji(false) -, _installed(0, this, controller, megagroup) +, _installed(0, this, _show, megagroup) , _megagroupSet(megagroup) { _installed.widget()->scrollsToY( ) | rpl::start_with_next([=](int y) { @@ -418,34 +426,38 @@ StickersBox::StickersBox( StickersBox::StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, const QVector &attachedSets) -: _controller(controller) -, _api(&controller->session().mtp()) +: _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _section(Section::Attached) , _isMasks(false) , _isEmoji(false) -, _attached(0, this, controller, Section::Attached) +, _attached(0, this, _show, Section::Attached) , _attachedType(Data::StickersType::Stickers) , _attachedSets(attachedSets) { } StickersBox::StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, const std::vector &emojiSets) -: _controller(controller) -, _api(&controller->session().mtp()) +: _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _section(Section::Attached) , _isMasks(false) , _isEmoji(true) -, _attached(0, this, controller, Section::Attached) +, _attached(0, this, _show, Section::Attached) , _attachedType(Data::StickersType::Emoji) , _emojiSets(emojiSets) { } Main::Session &StickersBox::session() const { - return _controller->session(); + return *_session; } void StickersBox::showAttachedStickers() { @@ -673,19 +685,19 @@ void StickersBox::refreshTabs() { _tabIndices.clear(); auto sections = std::vector(); if (_installed.widget()) { - sections.push_back(tr::lng_stickers_installed_tab(tr::now).toUpper()); + sections.push_back(tr::lng_stickers_installed_tab(tr::now)); _tabIndices.push_back(Section::Installed); } if (_masks.widget()) { - sections.push_back(tr::lng_stickers_masks_tab(tr::now).toUpper()); + sections.push_back(tr::lng_stickers_masks_tab(tr::now)); _tabIndices.push_back(Section::Masks); } if (!stickers.featuredSetsOrder().isEmpty() && _featured.widget()) { - sections.push_back(tr::lng_stickers_featured_tab(tr::now).toUpper()); + sections.push_back(tr::lng_stickers_featured_tab(tr::now)); _tabIndices.push_back(Section::Featured); } if (!archivedSetsOrder().isEmpty() && _archived.widget()) { - sections.push_back(tr::lng_stickers_archived_tab(tr::now).toUpper()); + sections.push_back(tr::lng_stickers_archived_tab(tr::now)); _tabIndices.push_back(Section::Archived); } _tabs->setSections(sections); @@ -766,7 +778,7 @@ void StickersBox::updateTabsGeometry() { auto featuredLeft = width() / maxTabs; auto featuredRight = 2 * width() / maxTabs; - auto featuredTextWidth = st::stickersTabs.labelStyle.font->width(tr::lng_stickers_featured_tab(tr::now).toUpper()); + auto featuredTextWidth = st::stickersTabs.labelStyle.font->width(tr::lng_stickers_featured_tab(tr::now)); auto featuredTextRight = featuredLeft + (featuredRight - featuredLeft - featuredTextWidth) / 2 + featuredTextWidth; auto unreadBadgeLeft = featuredTextRight - st::stickersFeaturedBadgeSkip; auto unreadBadgeTop = st::stickersFeaturedBadgeTop; @@ -1121,59 +1133,73 @@ bool StickersBox::Inner::Row::isArchived() const { StickersBox::Inner::Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, StickersBox::Section section) : RpWidget(parent) -, _controller(controller) -, _api(&_controller->session().mtp()) +, _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _section(section) -, _isInstalled(_section == Section::Installed || _section == Section::Masks) +, _isInstalledTab(_section == Section::Installed + || _section == Section::Masks) , _buttonBgOver( - ImageRoundRadius::Small, - (_isInstalled + ImageRoundRadius::Large, + (_isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd).textBgOver) , _buttonBg( - ImageRoundRadius::Small, - (_isInstalled + ImageRoundRadius::Large, + (_isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd).textBg) -, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom()) +, _inactiveButtonBg( + ImageRoundRadius::Large, + st::stickersTrendingInstalled.textBg) +, _rowHeight(_st.height) , _shiftingAnimation([=](crl::time now) { return shiftingAnimationCallback(now); }) -, _itemsTop(st::membersMarginTop) -, _addText(tr::lng_stickers_featured_add(tr::now).toUpper()) +, _itemsTop(st::lineWidth) +, _addText(tr::lng_stickers_featured_add(tr::now)) , _addWidth(st::stickersTrendingAdd.font->width(_addText)) -, _undoText(tr::lng_stickers_return(tr::now).toUpper()) -, _undoWidth(st::stickersUndoRemove.font->width(_undoText)) { +, _undoText(tr::lng_stickers_return(tr::now)) +, _undoWidth(st::stickersUndoRemove.font->width(_undoText)) +, _installedText(tr::lng_stickers_featured_installed(tr::now)) +, _installedWidth(st::stickersTrendingInstalled.font->width(_installedText)) { setup(); } StickersBox::Inner::Inner( QWidget *parent, - not_null controller, + std::shared_ptr show, not_null megagroup) : RpWidget(parent) -, _controller(controller) -, _api(&_controller->session().mtp()) +, _st(st::stickersRowItem) +, _show(std::move(show)) +, _session(&_show->session()) +, _api(&_session->mtp()) , _section(StickersBox::Section::Installed) -, _isInstalled(_section == Section::Installed || _section == Section::Masks) +, _isInstalledTab(_section == Section::Installed + || _section == Section::Masks) , _buttonBgOver( - ImageRoundRadius::Small, - (_isInstalled + ImageRoundRadius::Large, + (_isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd).textBgOver) , _buttonBg( - ImageRoundRadius::Small, - (_isInstalled + ImageRoundRadius::Large, + (_isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd).textBg) -, _rowHeight(st::contactsPadding.top() + st::contactsPhotoSize + st::contactsPadding.bottom()) +, _inactiveButtonBg( + ImageRoundRadius::Large, + st::stickersTrendingInstalled.textBg) +, _rowHeight(_st.height) , _shiftingAnimation([=](crl::time now) { return shiftingAnimationCallback(now); }) -, _itemsTop(st::membersMarginTop) +, _itemsTop(st::lineWidth) , _megagroupSet(megagroup) , _megagroupSetInput(_megagroupSet->mgInfo->stickerSet) , _megagroupSetField( @@ -1181,11 +1207,11 @@ StickersBox::Inner::Inner( st::groupStickersField, rpl::single(u"stickerset"_q), QString(), - _controller->session().createInternalLink(QString())) + _session->createInternalLink(QString())) , _megagroupDivider(this) , _megagroupSubTitle(this, tr::lng_stickers_group_from_your(tr::now), st::boxTitle) { _megagroupSetField->setLinkPlaceholder( - _controller->session().createInternalLink(u"addstickers/"_q)); + _session->createInternalLink(u"addstickers/"_q)); _megagroupSetField->setPlaceholderHidden(false); _megagroupSetAddressChangedTimer.setCallback([this] { handleMegagroupSetAddressChange(); }); connect( @@ -1207,7 +1233,7 @@ StickersBox::Inner::Inner( } Main::Session &StickersBox::Inner::session() const { - return _controller->session(); + return *_session; } void StickersBox::Inner::setup() { @@ -1288,21 +1314,29 @@ void StickersBox::Inner::updateControlsGeometry() { } } -QRect StickersBox::Inner::relativeButtonRect(bool removeButton) const { +QRect StickersBox::Inner::relativeButtonRect( + bool removeButton, + bool installedSet) const { auto buttonw = st::stickersRemove.width; auto buttonh = st::stickersRemove.height; auto buttonshift = st::stickersRemoveSkip; if (!removeButton) { - const auto &st = _isInstalled + const auto &st = installedSet + ? st::stickersTrendingInstalled + : _isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd; - const auto textWidth = _isInstalled ? _undoWidth : _addWidth; + const auto textWidth = installedSet + ? _installedWidth + : _isInstalledTab + ? _undoWidth + : _addWidth; buttonw = textWidth - st.width; buttonh = st.height; buttonshift = 0; } - auto buttonx = width() - st::contactsPadding.right() - st::contactsCheckPosition.x() - buttonw + buttonshift; - auto buttony = st::contactsPadding.top() + (st::contactsPhotoSize - buttonh) / 2; + auto buttonx = width() - st::contactsPadding.right() - buttonw + buttonshift; + auto buttony = (_st.height - buttonh) / 2; return QRect(buttonx, buttony, buttonw, buttonh); } @@ -1318,14 +1352,14 @@ void StickersBox::Inner::paintRow(Painter &p, not_null row, int index) { return -1; }(); if (index >= 0 && index == selectedIndex) { - p.fillRect(0, 0, width(), _rowHeight, st::contactsBgOver); + p.fillRect(0, 0, width(), _rowHeight, _st.button.textBgOver); if (row->ripple) { row->ripple->paint(p, 0, 0, width()); } } } - if (_isInstalled) { + if (_isInstalledTab) { if (index >= 0 && index == _above) { auto current = _aboveShadowFadeOpacity.current(); if (_started >= 0) { @@ -1335,7 +1369,7 @@ void StickersBox::Inner::paintRow(Painter &p, not_null row, int index) { current = reachedOpacity; } } - auto rect = myrtlrect(st::contactsPadding.left() / 2, st::contactsPadding.top() / 2, width() - (st::contactsPadding.left() / 2) - _scrollbar - st::contactsPadding.left() / 2, _rowHeight - ((st::contactsPadding.top() + st::contactsPadding.bottom()) / 2)); + auto rect = myrtlrect(_st.photoPosition.x() / 2, _st.photoPosition.y() / 2, width() - _st.photoPosition.x() - _scrollbar, _rowHeight - _st.photoPosition.y()); p.setOpacity(current); Ui::Shadow::paint(p, rect, width(), st::boxRoundShadow); p.setOpacity(1); @@ -1352,31 +1386,31 @@ void StickersBox::Inner::paintRow(Painter &p, not_null row, int index) { paintFakeButton(p, row, index); } - if (row->removed && _isInstalled) { + if (row->removed && _isInstalledTab) { p.setOpacity(st::stickersRowDisabledOpacity); } - auto stickerx = st::contactsPadding.left(); + auto stickerskip = 0; - if (!_megagroupSet && _isInstalled) { - stickerx += st::stickersReorderIcon.width() + st::stickersReorderSkip; + if (!_megagroupSet && _isInstalledTab) { + stickerskip += st::stickersReorderIcon.width() + st::stickersReorderSkip; if (!row->isRecentSet()) { - st::stickersReorderIcon.paint(p, st::contactsPadding.left(), (_rowHeight - st::stickersReorderIcon.height()) / 2, width()); + st::stickersReorderIcon.paint(p, _st.photoPosition.x(), (_rowHeight - st::stickersReorderIcon.height()) / 2, width()); } } if (row->sticker) { - paintRowThumbnail(p, row, stickerx); + paintRowThumbnail(p, row, stickerskip + _st.photoPosition.x()); } - int namex = stickerx + st::contactsPhotoSize + st::contactsPadding.left(); - int namey = st::contactsPadding.top() + st::contactsNameTop; + int namex = stickerskip + _st.namePosition.x(); + int namey = _st.namePosition.y(); - int statusx = namex; - int statusy = st::contactsPadding.top() + st::contactsStatusTop; + int statusx = stickerskip + _st.statusPosition.x(); + int statusy = _st.statusPosition.y(); p.setFont(st::contactsNameStyle.font); - p.setPen(st::contactsNameFg); + p.setPen(_st.nameFg); p.drawTextLeft(namex, namey, width(), row->title, row->titleWidth); if (row->isUnread()) { @@ -1398,7 +1432,7 @@ void StickersBox::Inner::paintRow(Painter &p, not_null row, int index) { : tr::lng_stickers_count(tr::now, lt_count, row->count); p.setFont(st::contactsStatusFont); - p.setPen(st::contactsStatusFg); + p.setPen(_st.statusFg); p.drawTextLeft(statusx, statusy, width(), statusText); p.setOpacity(1); @@ -1429,17 +1463,16 @@ void StickersBox::Inner::paintRowThumbnail( : row->stickerMedia ? row->stickerMedia->thumbnail() : nullptr; - const auto paused = _controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Layer); - const auto x = left + (st::contactsPhotoSize - row->pixw) / 2; - const auto y = st::contactsPadding.top() + (st::contactsPhotoSize - row->pixh) / 2; + const auto paused = _show->paused(ChatHelpers::PauseReason::Layer); + const auto x = left + (_st.photoSize - row->pixw) / 2; + const auto y = _st.photoPosition.y() + (_st.photoSize - row->pixh) / 2; if (row->lottie && row->lottie->ready()) { const auto frame = row->lottie->frame(); const auto size = frame.size() / cIntRetinaFactor(); p.drawImage( QRect( - left + (st::contactsPhotoSize - size.width()) / 2, - st::contactsPadding.top() + (st::contactsPhotoSize - size.height()) / 2, + left + (_st.photoSize - size.width()) / 2, + _st.photoPosition.y() + (_st.photoSize - size.height()) / 2, size.width(), size.height()), frame); @@ -1474,9 +1507,7 @@ void StickersBox::Inner::validateLottieAnimation(not_null row) { row->thumbnailMedia.get(), row->stickerMedia.get(), ChatHelpers::StickerLottieSize::SetsListThumbnail, - QSize( - st::contactsPhotoSize, - st::contactsPhotoSize) * cIntRetinaFactor()); + QSize(_st.photoSize, _st.photoSize) * cIntRetinaFactor()); if (!player) { return; } @@ -1546,26 +1577,28 @@ void StickersBox::Inner::updateRowThumbnail(not_null row) { } Unexpected("StickersBox::Inner::updateRowThumbnail: row not found"); }(); - const auto left = st::contactsPadding.left() - + ((!_megagroupSet && _isInstalled) + const auto left = _st.photoPosition.x() + + ((!_megagroupSet && _isInstalledTab) ? st::stickersReorderIcon.width() + st::stickersReorderSkip : 0); - update( - left, - rowTop + st::contactsPadding.top(), - st::contactsPhotoSize, - st::contactsPhotoSize); + const auto top = rowTop + _st.photoPosition.y(); + update(left, top, _st.photoSize, _st.photoSize); } void StickersBox::Inner::paintFakeButton(Painter &p, not_null row, int index) { - auto removeButton = (_isInstalled && !row->removed); - auto rect = relativeButtonRect(removeButton); - if (!_isInstalled && row->isInstalled() && !row->isArchived() && !row->removed) { - // Checkbox after installed from Trending or Archived. - int checkx = width() - (st::contactsPadding.right() + st::contactsCheckPosition.x() + (rect.width() + st::stickersFeaturedInstalled.width()) / 2); - int checky = st::contactsPadding.top() + (st::contactsPhotoSize - st::stickersFeaturedInstalled.height()) / 2; - st::stickersFeaturedInstalled.paint(p, QPoint(checkx, checky), width()); + const auto removeButton = (_isInstalledTab && !row->removed); + if (!_isInstalledTab && row->isInstalled() && !row->isArchived() && !row->removed) { + // Round button "Added" after installed from Trending or Archived. + const auto rect = relativeButtonRect(removeButton, true); + const auto &st = st::stickersTrendingInstalled; + const auto textWidth = _installedWidth; + const auto &text = _installedText; + _inactiveButtonBg.paint(p, myrtlrect(rect)); + p.setFont(st.font); + p.setPen(st.textFg); + p.drawTextLeft(rect.x() - (st.width / 2), rect.y() + st.textTop, width(), text, textWidth); } else { + const auto rect = relativeButtonRect(removeButton, false); auto selected = (index == _actionSel && _actionDown < 0) || (index == _actionDown); if (removeButton) { // Trash icon button when not disabled in Installed. @@ -1583,11 +1616,11 @@ void StickersBox::Inner::paintFakeButton(Painter &p, not_null row, int ind } else { // Round button ADD when not installed from Trending or Archived. // Or round button UNDO after disabled from Installed. - const auto &st = _isInstalled + const auto &st = _isInstalledTab ? st::stickersUndoRemove : st::stickersTrendingAdd; - const auto textWidth = _isInstalled ? _undoWidth : _addWidth; - const auto &text = _isInstalled ? _undoText : _addText; + const auto textWidth = _isInstalledTab ? _undoWidth : _addWidth; + const auto &text = _isInstalledTab ? _undoText : _addText; (selected ? _buttonBgOver : _buttonBg).paint(p, myrtlrect(rect)); if (row->ripple) { row->ripple->paint(p, rect.x(), rect.y(), width()); @@ -1612,7 +1645,7 @@ void StickersBox::Inner::mousePressEvent(QMouseEvent *e) { setActionDown(_actionSel); update(0, _itemsTop + _actionSel * _rowHeight, width(), _rowHeight); } else if (auto selectedIndex = std::get_if(&_selected)) { - if (_isInstalled && !_rows[*selectedIndex]->isRecentSet() && _inDragArea) { + if (_isInstalledTab && !_rows[*selectedIndex]->isRecentSet() && _inDragArea) { _above = _dragging = _started = *selectedIndex; _dragStart = mapFromGlobal(_mouse); } @@ -1634,12 +1667,12 @@ void StickersBox::Inner::setActionDown(int newActionDown) { if (_actionDown >= 0 && _actionDown < _rows.size()) { update(0, _itemsTop + _actionDown * _rowHeight, width(), _rowHeight); const auto row = _rows[_actionDown].get(); - auto removeButton = (_isInstalled && !row->removed); + auto removeButton = (_isInstalledTab && !row->removed); if (!row->ripple) { - if (_isInstalled) { + if (_isInstalledTab) { if (row->removed) { auto rippleSize = QSize(_undoWidth - st::stickersUndoRemove.width, st::stickersUndoRemove.height); - auto rippleMask = Ui::RippleAnimation::RoundRectMask(rippleSize, st::roundRadiusSmall); + auto rippleMask = Ui::RippleAnimation::RoundRectMask(rippleSize, st::roundRadiusLarge); ensureRipple(st::stickersUndoRemove.ripple, std::move(rippleMask), removeButton); } else { auto rippleSize = st::stickersRemove.rippleAreaSize; @@ -1648,12 +1681,12 @@ void StickersBox::Inner::setActionDown(int newActionDown) { } } else if (!row->isInstalled() || row->isArchived() || row->removed) { auto rippleSize = QSize(_addWidth - st::stickersTrendingAdd.width, st::stickersTrendingAdd.height); - auto rippleMask = Ui::RippleAnimation::RoundRectMask(rippleSize, st::roundRadiusSmall); + auto rippleMask = Ui::RippleAnimation::RoundRectMask(rippleSize, st::roundRadiusLarge); ensureRipple(st::stickersTrendingAdd.ripple, std::move(rippleMask), removeButton); } } if (row->ripple) { - auto rect = relativeButtonRect(removeButton); + auto rect = relativeButtonRect(removeButton, false); row->ripple->add(mapFromGlobal(QCursor::pos()) - QPoint(myrtlrect(rect).x(), _itemsTop + _actionDown * _rowHeight + rect.y())); } } @@ -1716,7 +1749,7 @@ void StickersBox::Inner::setPressed(SelectedRow pressed) { void StickersBox::Inner::ensureRipple(const style::RippleAnimation &st, QImage mask, bool removeButton) { _rows[_actionDown]->ripple = std::make_unique(st, std::move(mask), [this, index = _actionDown, removeButton] { - update(myrtlrect(relativeButtonRect(removeButton).translated(0, _itemsTop + index * _rowHeight))); + update(myrtlrect(relativeButtonRect(removeButton, false).translated(0, _itemsTop + index * _rowHeight))); }); } @@ -1779,15 +1812,15 @@ void StickersBox::Inner::updateSelected() { selected = selectedIndex; local.setY(local.y() - _itemsTop - selectedIndex * _rowHeight); const auto row = _rows[selectedIndex].get(); - if (!_megagroupSet && (_isInstalled || !row->isInstalled() || row->isArchived() || row->removed)) { - auto removeButton = (_isInstalled && !row->removed); - auto rect = myrtlrect(relativeButtonRect(removeButton)); + if (!_megagroupSet && (_isInstalledTab || !row->isInstalled() || row->isArchived() || row->removed)) { + auto removeButton = (_isInstalledTab && !row->removed); + auto rect = myrtlrect(relativeButtonRect(removeButton, false)); actionSel = rect.contains(local) ? selectedIndex : -1; } else { actionSel = -1; } - if (!_megagroupSet && _isInstalled && !row->isRecentSet()) { - auto dragAreaWidth = st::contactsPadding.left() + st::stickersReorderIcon.width() + st::stickersReorderSkip; + if (!_megagroupSet && _isInstalledTab && !row->isRecentSet()) { + auto dragAreaWidth = _st.photoPosition.x() + st::stickersReorderIcon.width() + st::stickersReorderSkip; auto dragArea = myrtlrect(0, 0, dragAreaWidth, _rowHeight); inDragArea = dragArea.contains(local); } @@ -1810,7 +1843,7 @@ void StickersBox::Inner::updateSelected() { void StickersBox::Inner::updateCursor() { setCursor(_inDragArea ? style::cur_sizeall - : (!_megagroupSet && _isInstalled) + : (!_megagroupSet && _isInstalledTab) ? ((_actionSel >= 0 && (_actionDown < 0 || _actionDown == _actionSel)) ? style::cur_pointer : style::cur_default) @@ -1835,7 +1868,7 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { _mouse = e->globalPos(); updateSelected(); if (_actionDown == _actionSel && _actionSel >= 0) { - if (_isInstalled) { + if (_isInstalledTab) { setRowRemoved(_actionDown, !_rows[_actionDown]->removed); } else if (_installSetCallback) { _installSetCallback(_rows[_actionDown]->set->id); @@ -1858,9 +1891,7 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { }(); const auto showSetByRow = [&](const Row &row) { setSelected(SelectedRow()); - _controller->show( - Box(_controller, row.set), - Ui::LayerOption::KeepOther); + _show->showBox(Box(_show, row.set)); }; if (selectedIndex >= 0 && !_inDragArea) { const auto row = _rows[selectedIndex].get(); @@ -2075,15 +2106,15 @@ void StickersBox::Inner::rebuildMegagroupSet() { } const auto set = it->second.get(); - auto maxNameWidth = countMaxNameWidth(); - auto titleWidth = 0; - auto title = fillSetTitle(set, maxNameWidth, &titleWidth); auto count = fillSetCount(set); auto sticker = (DocumentData*)nullptr; auto pixw = 0, pixh = 0; fillSetCover(set, &sticker, &pixw, &pixh); auto flagsOverride = SetFlag::Installed; auto removed = false; + auto maxNameWidth = countMaxNameWidth(!_isInstalledTab); + auto titleWidth = 0; + auto title = fillSetTitle(set, maxNameWidth, &titleWidth); if (!_megagroupSelectedSet || _megagroupSelectedSet->set->id != set->id) { _megagroupSetField->setText(set->shortName); @@ -2113,7 +2144,7 @@ void StickersBox::Inner::rebuildMegagroupSet() { } void StickersBox::Inner::rebuild(bool masks) { - _itemsTop = st::membersMarginTop; + _itemsTop = st::lineWidth; if (_megagroupSet) { _itemsTop += st::groupStickersFieldPadding.top() + _megagroupSetField->height() + st::groupStickersFieldPadding.bottom(); @@ -2121,8 +2152,6 @@ void StickersBox::Inner::rebuild(bool masks) { rebuildMegagroupSet(); } - auto maxNameWidth = countMaxNameWidth(); - _oldRows = std::move(_rows); clear(); const auto &order = ([&]() -> const StickersSetsOrder & { @@ -2151,12 +2180,12 @@ void StickersBox::Inner::rebuild(bool masks) { ? tr::lng_stickers_group_from_featured(tr::now) : tr::lng_stickers_group_from_your(tr::now)); updateControlsGeometry(); - } else if (_isInstalled) { + } else if (_isInstalledTab) { const auto cloudIt = sets.find((_section == Section::Masks) ? Data::Stickers::CloudRecentAttachedSetId : Data::Stickers::CloudRecentSetId); // Section::Installed. if (cloudIt != sets.cend() && !cloudIt->second->stickers.isEmpty()) { - rebuildAppendSet(cloudIt->second.get(), maxNameWidth); + rebuildAppendSet(cloudIt->second.get()); } } for (const auto setId : order) { @@ -2166,7 +2195,7 @@ void StickersBox::Inner::rebuild(bool masks) { } const auto set = it->second.get(); - rebuildAppendSet(set, maxNameWidth); + rebuildAppendSet(set); if (set->stickers.isEmpty() || (set->flags & SetFlag::NotLoaded)) { @@ -2201,7 +2230,8 @@ void StickersBox::Inner::updateSize(int newWidth) { } void StickersBox::Inner::updateRows() { - int maxNameWidth = countMaxNameWidth(); + const auto maxNameWidth = countMaxNameWidth(false); + const auto maxNameWidthInstalled = countMaxNameWidth(true); const auto &sets = session().data().stickers().sets(); for (const auto &row : _rows) { const auto it = sets.find(row->set->id); @@ -2227,7 +2257,7 @@ void StickersBox::Inner::updateRows() { auto wasInstalled = row->isInstalled(); auto wasArchived = row->isArchived(); row->flagsOverride = fillSetFlags(set); - if (_isInstalled) { + if (_isInstalledTab) { row->flagsOverride &= ~SetFlag::Archived; } if (row->isInstalled() != wasInstalled @@ -2235,7 +2265,14 @@ void StickersBox::Inner::updateRows() { row->ripple.reset(); } } - row->title = fillSetTitle(set, maxNameWidth, &row->titleWidth); + const auto installedSet = (!_isInstalledTab + && row->isInstalled() + && !row->isArchived() + && !row->removed); + row->title = fillSetTitle( + set, + installedSet ? maxNameWidthInstalled : maxNameWidth, + &row->titleWidth); row->count = fillSetCount(set); } update(); @@ -2247,26 +2284,28 @@ bool StickersBox::Inner::appendSet(not_null set) { return false; } } - rebuildAppendSet(set, countMaxNameWidth()); + rebuildAppendSet(set); return true; } bool StickersBox::Inner::skipPremium() const { - return !_controller->session().premiumPossible(); + return !_session->premiumPossible(); } -int StickersBox::Inner::countMaxNameWidth() const { - int namex = st::contactsPadding.left() + st::contactsPhotoSize + st::contactsPadding.left(); - if (!_megagroupSet && _isInstalled) { +int StickersBox::Inner::countMaxNameWidth(bool installedSet) const { + int namex = _st.namePosition.x(); + if (!_megagroupSet && _isInstalledTab) { namex += st::stickersReorderIcon.width() + st::stickersReorderSkip; } - int namew = st::boxWideWidth - namex - st::contactsPadding.right() - st::contactsCheckPosition.x(); - if (_isInstalled) { + int namew = st::boxWideWidth - namex - st::contactsPadding.right(); + if (_isInstalledTab) { if (!_megagroupSet) { namew -= _undoWidth - st::stickersUndoRemove.width; } } else { - namew -= _addWidth - st::stickersTrendingAdd.width; + namew -= installedSet + ? (_installedWidth - st::stickersTrendingInstalled.width) + : (_addWidth - st::stickersTrendingAdd.width); if (_section == Section::Featured) { namew -= st::stickersFeaturedUnreadSize + st::stickersFeaturedUnreadSkip; } @@ -2274,14 +2313,12 @@ int StickersBox::Inner::countMaxNameWidth() const { return namew; } -void StickersBox::Inner::rebuildAppendSet( - not_null set, - int maxNameWidth) { +void StickersBox::Inner::rebuildAppendSet(not_null set) { auto flagsOverride = (set->id != Data::Stickers::CloudRecentSetId) ? fillSetFlags(set) : SetFlag::Installed; auto removed = false; - if (_isInstalled && (flagsOverride & SetFlag::Archived)) { + if (_isInstalledTab && (flagsOverride & SetFlag::Archived)) { return; } @@ -2289,6 +2326,10 @@ void StickersBox::Inner::rebuildAppendSet( int pixw = 0, pixh = 0; fillSetCover(set, &sticker, &pixw, &pixh); + const auto maxNameWidth = countMaxNameWidth(!_isInstalledTab + && (flagsOverride & SetFlag::Installed) + && !(flagsOverride & SetFlag::Archived) + && !removed); int titleWidth = 0; QString title = fillSetTitle(set, maxNameWidth, &titleWidth); int count = fillSetCount(set); @@ -2362,17 +2403,17 @@ void StickersBox::Inner::fillSetCover( : QSize(1, 1); auto pixw = size.width(); auto pixh = size.height(); - if (pixw > st::contactsPhotoSize) { + if (pixw > _st.photoSize) { if (pixw > pixh) { - pixh = (pixh * st::contactsPhotoSize) / pixw; - pixw = st::contactsPhotoSize; + pixh = (pixh * _st.photoSize) / pixw; + pixw = _st.photoSize; } else { - pixw = (pixw * st::contactsPhotoSize) / pixh; - pixh = st::contactsPhotoSize; + pixw = (pixw * _st.photoSize) / pixh; + pixh = _st.photoSize; } - } else if (pixh > st::contactsPhotoSize) { - pixw = (pixw * st::contactsPhotoSize) / pixh; - pixh = st::contactsPhotoSize; + } else if (pixh > _st.photoSize) { + pixw = (pixw * _st.photoSize) / pixh; + pixh = _st.photoSize; } *outWidth = pixw; *outHeight = pixh; diff --git a/Telegram/SourceFiles/boxes/stickers_box.h b/Telegram/SourceFiles/boxes/stickers_box.h index c9709b07b..f0e54374a 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.h +++ b/Telegram/SourceFiles/boxes/stickers_box.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct RippleAnimation; +struct PeerListItem; } // namespace style namespace Ui { @@ -27,9 +28,9 @@ class CrossButton; class BoxContentDivider; } // namespace Ui -namespace Window { -class SessionController; -} // namespace Window +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers namespace Main { class Session; @@ -60,20 +61,20 @@ public: StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, Section section, bool masks = false); StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, not_null megagroup); StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, const QVector &attachedSets); StickersBox( QWidget*, - not_null controller, + std::shared_ptr show, const std::vector &emojiSets); ~StickersBox(); @@ -142,7 +143,9 @@ private: std::array widgets() const; - const not_null _controller; + const style::PeerListItem &_st; + const std::shared_ptr _show; + const not_null _session; MTP::Sender _api; object_ptr _tabs = { nullptr }; diff --git a/Telegram/SourceFiles/boxes/translate_box.cpp b/Telegram/SourceFiles/boxes/translate_box.cpp index de64a704e..b21cf229c 100644 --- a/Telegram/SourceFiles/boxes/translate_box.cpp +++ b/Telegram/SourceFiles/boxes/translate_box.cpp @@ -24,8 +24,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/loading_element.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" #include "ui/painter.h" +#include "ui/power_saving.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/multi_select.h" @@ -64,7 +64,7 @@ ShowButton::ShowButton(not_null parent) _button.sizeValue( ) | rpl::start_with_next([=](const QSize &s) { resize( - s.width() + st::emojiSuggestionsFadeRight.width(), + s.width() + st::defaultEmojiSuggestions.fadeRight.width(), s.height()); _button.moveToRight(0, 0); }, lifetime()); @@ -75,7 +75,7 @@ void ShowButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto clip = e->rect(); - const auto &icon = st::emojiSuggestionsFadeRight; + const auto &icon = st::defaultEmojiSuggestions.fadeRight; const auto fade = QRect(0, 0, icon.width(), height()); if (fade.intersects(clip)) { icon.fill(p, fade); @@ -132,6 +132,14 @@ void TranslateBox( // container, // tr::lng_translate_box_original()); + const auto animationsPaused = [] { + using Which = FlatLabel::WhichAnimationsPaused; + const auto emoji = On(PowerSaving::kEmojiChat); + const auto spoiler = On(PowerSaving::kChatSpoiler); + return emoji + ? (spoiler ? Which::All : Which::CustomEmoji) + : (spoiler ? Which::Spoiler : Which::None); + }; const auto original = box->addRow(object_ptr>( box, object_ptr(box, stLabel))); @@ -140,6 +148,7 @@ void TranslateBox( original->entity()->setContextMenuHook([](auto&&) { }); } + original->entity()->setAnimationsPausedCallback(animationsPaused); original->entity()->setMarkedText( text, Core::MarkedTextContext{ @@ -195,6 +204,7 @@ void TranslateBox( box, object_ptr(box, stLabel))); translated->entity()->setSelectable(!hasCopyRestriction); + translated->entity()->setAnimationsPausedCallback(animationsPaused); constexpr auto kMaxLines = 3; container->resizeToWidth(box->width()); @@ -263,7 +273,7 @@ void TranslateBox( if (loading->toggled()) { return; } - Ui::BoxShow(box).showBox(ChooseTranslateToBox( + box->uiShow()->showBox(ChooseTranslateToBox( state->to.current(), crl::guard(box, [=](LanguageId id) { state->to = id; }))); }); @@ -314,11 +324,9 @@ object_ptr EditSkipTranslationLanguages() { } if (already && selected->empty()) { if (const auto strong = weak->data()) { - Ui::ShowMultilineToast({ - .parentOverride = BoxShow(strong).toastParent(), - .text = { tr::lng_translate_settings_one(tr::now) }, - .duration = kSkipAtLeastOneDuration, - }); + strong->showToast( + tr::lng_translate_settings_one(tr::now), + kSkipAtLeastOneDuration); } return false; } diff --git a/Telegram/SourceFiles/boxes/username_box.cpp b/Telegram/SourceFiles/boxes/username_box.cpp index 6422de285..ded98183a 100644 --- a/Telegram/SourceFiles/boxes/username_box.cpp +++ b/Telegram/SourceFiles/boxes/username_box.cpp @@ -10,9 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_usernames_list.h" #include "base/timer.h" #include "boxes/peers/edit_peer_common.h" +#include "data/data_channel.h" #include "data/data_session.h" #include "data/data_user.h" #include "lang/lang_keys.h" +#include "main/main_app_config_values.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "settings/settings_common.h" @@ -45,9 +47,10 @@ namespace { class UsernameEditor final : public Ui::RpWidget { public: - UsernameEditor(not_null, not_null session); + UsernameEditor(not_null, not_null peer); void setInnerFocus(); + void setEnabled(bool value); [[nodiscard]] rpl::producer<> submitted() const; [[nodiscard]] rpl::producer<> save(); [[nodiscard]] rpl::producer checkInfoChanged() const; @@ -66,8 +69,11 @@ private: void checkInfoChange(); + [[nodiscard]] QString editableUsername() const; + QString getName() const; + const not_null _peer; const not_null _session; const style::margins &_padding; MTP::Sender _api; @@ -87,18 +93,19 @@ private: UsernameEditor::UsernameEditor( not_null, - not_null session) -: _session(session) + not_null peer) +: _peer(peer) +, _session(&peer->session()) , _padding(st::usernamePadding) , _api(&_session->mtp()) , _username( this, st::defaultInputField, rpl::single(u"@username"_q), - session->user()->editableUsername(), + editableUsername(), QString()) , _checkTimer([=] { check(); }) { - _goodText = _session->user()->editableUsername().isEmpty() + _goodText = editableUsername().isEmpty() ? QString() : tr::lng_username_available(tr::now); @@ -119,7 +126,14 @@ rpl::producer<> UsernameEditor::submitted() const { } void UsernameEditor::setInnerFocus() { - _username->setFocusFast(); + if (_username->isEnabled()) { + _username->setFocusFast(); + } +} + +void UsernameEditor::setEnabled(bool value) { + _username->setEnabled(value); + _username->setDisplayFocused(value); } void UsernameEditor::resizeEvent(QResizeEvent *e) { @@ -148,6 +162,16 @@ rpl::producer<> UsernameEditor::save() { return _saved.events(); } +QString UsernameEditor::editableUsername() const { + if (const auto user = _peer->asUser()) { + return user->editableUsername(); + } else if (const auto channel = _peer->asChannel()) { + return channel->editableUsername(); + } else { + return QString(); + } +} + rpl::producer UsernameEditor::checkInfoChanged() const { return _checkInfoChanged.events(); } @@ -166,7 +190,7 @@ void UsernameEditor::check() { _checkRequestId = 0; _errorText = (mtpIsTrue(result) - || _checkUsername == _session->user()->editableUsername()) + || (_checkUsername == editableUsername())) ? QString() : tr::lng_username_occupied(tr::now); _goodText = _errorText.isEmpty() @@ -251,14 +275,15 @@ void UsernameEditor::checkInfoPurchaseAvailable() { } void UsernameEditor::updateFail(const QString &error) { - const auto self = _session->user(); if ((error == u"USERNAME_NOT_MODIFIED"_q) - || (_sentUsername == self->editableUsername())) { - self->setName( - TextUtilities::SingleLine(self->firstName), - TextUtilities::SingleLine(self->lastName), - TextUtilities::SingleLine(self->nameOrPhone), - TextUtilities::SingleLine(_sentUsername)); + || (_sentUsername == editableUsername())) { + if (const auto user = _peer->asUser()) { + user->setName( + TextUtilities::SingleLine(user->firstName), + TextUtilities::SingleLine(user->lastName), + TextUtilities::SingleLine(user->nameOrPhone), + TextUtilities::SingleLine(_sentUsername)); + } _saved.fire_done(); } else if (error == u"USERNAME_INVALID"_q) { _username->setFocus(); @@ -283,7 +308,7 @@ void UsernameEditor::checkFail(const QString &error) { _errorText = tr::lng_username_invalid(tr::now); checkInfoChange(); } else if ((error == u"USERNAME_OCCUPIED"_q) - && (_checkUsername != _session->user()->editableUsername())) { + && (_checkUsername != editableUsername())) { _errorText = tr::lng_username_occupied(tr::now); checkInfoChange(); } else if (error == u"USERNAME_PURCHASE_AVAILABLE"_q) { @@ -302,35 +327,58 @@ QString UsernameEditor::getName() const { void UsernamesBox( not_null box, - not_null session) { - box->setTitle(tr::lng_username_title()); + not_null peer) { + const auto isBot = peer && peer->isUser() && peer->asUser()->isBot(); + box->setTitle(isBot + ? tr::lng_bot_username_title() + : tr::lng_username_title()); const auto container = box->verticalLayout(); const auto editor = box->addRow( - object_ptr(box, session), + object_ptr(box, peer), {}); + editor->setEnabled(!isBot); box->setFocusCallback([=] { editor->setInnerFocus(); }); AddUsernameCheckLabel(container, editor->checkInfoChanged()); + auto description = [&]() -> rpl::producer { + if (!isBot) { + return rpl::combine( + tr::lng_username_description1(Ui::Text::RichLangValue), + tr::lng_username_description2(Ui::Text::RichLangValue) + ) | rpl::map([](TextWithEntities d1, TextWithEntities d2) { + return d1.append("\n\n").append(std::move(d2)); + }); + } + if (const auto url = AppConfig::FragmentLink(&peer->session())) { + const auto link = Ui::Text::Link( + tr::lng_bot_username_description1_link(tr::now), + *url); + return tr::lng_bot_username_description1( + lt_link, + rpl::single(link), + Ui::Text::RichLangValue); + } + return rpl::single({}); + }(); container->add(object_ptr( container, object_ptr( container, - tr::lng_username_description(Ui::Text::RichLangValue), + std::move(description), st::boxDividerLabel), st::settingsDividerLabelPadding)); const auto list = box->addRow( object_ptr( box, - session->user(), - std::make_shared(box), - [=] { - box->scrollToY(0); - editor->setInnerFocus(); - }), + peer, + box->uiShow(), + !isBot + ? [=] { box->scrollToY(0); editor->setInnerFocus(); } + : Fn(nullptr)), {}); const auto finish = [=] { @@ -345,8 +393,12 @@ void UsernamesBox( editor->submitted( ) | rpl::start_with_next(finish, editor->lifetime()); - box->addButton(tr::lng_settings_save(), finish); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + if (isBot) { + box->addButton(tr::lng_close(), [=] { box->closeBox(); }); + } else { + box->addButton(tr::lng_settings_save(), finish); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + } } void AddUsernameCheckLabel( diff --git a/Telegram/SourceFiles/boxes/username_box.h b/Telegram/SourceFiles/boxes/username_box.h index 46183e3ef..423631dd1 100644 --- a/Telegram/SourceFiles/boxes/username_box.h +++ b/Telegram/SourceFiles/boxes/username_box.h @@ -12,13 +12,11 @@ class GenericBox; class VerticalLayout; } // namespace Ui -namespace Main { -class Session; -} // namespace Main +class PeerData; void UsernamesBox( not_null box, - not_null session); + not_null peer); struct UsernameCheckInfo final { enum class Type { diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 28b5f90a6..2791724dd 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -744,6 +744,11 @@ groupCallShareBoxComment: InputField(groupCallField) { } groupCallShareBoxList: PeerList(groupCallMembersList) { item: PeerListItem(groupCallMembersListItem) { + nameStyle: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px); + } checkbox: RoundImageCheckbox(groupCallMembersListCheckbox) { imageRadius: 28px; imageSmallRadius: 24px; @@ -1333,13 +1338,7 @@ groupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) { radius: 4px; arrow: 4px; } -groupCallNiceTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { - style: TextStyle(defaultTextStyle) { - font: font(11px); - linkFont: font(11px); - linkFontOver: font(11px underline); - } -} +groupCallNiceTooltipLabel: defaultImportantTooltipLabel; groupCallStickedTooltip: ImportantTooltip(groupCallNiceTooltip) { padding: margins(10px, 1px, 6px, 3px); } diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index ad9eff91a..66a4e01f5 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -568,8 +568,7 @@ base::unique_qptr BoxController::rowContextMenu( st::popupMenuWithIcons); result->addAction(tr::lng_context_delete_selected(tr::now), [=] { _window->show( - Box(session, base::duplicate(ids)), - Ui::LayerOption::KeepOther); + Box(session, base::duplicate(ids))); }, &st::menuIconDelete); return result; } diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index da0d591db..5fe9ac677 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -128,6 +128,7 @@ void AppendServer( .login = username, .password = password, .isTurn = true, + .isTcp = data.is_tcp(), }); }; pushTurn(host); diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 4ea1264c4..d205ab4b0 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -556,6 +556,11 @@ FnMut Instance::addAsyncWaiter() { }; } +bool Instance::isSharingScreen() const { + return (_currentCall && _currentCall->isSharingScreen()) + || (_currentGroupCall && _currentGroupCall->isSharingScreen()); +} + bool Instance::isQuitPrevent() { if (!_currentCall || _currentCall->isIncomingWaiting()) { return false; @@ -584,6 +589,15 @@ void Instance::handleCallUpdate( // May be a repeated phoneCallRequested update from getDifference. return; } + if (inCall() + && _currentCall->type() == Call::Type::Outgoing + && _currentCall->user()->id == session->userPeerId() + && (peerFromUser(phoneCall.vparticipant_id()) + == _currentCall->user()->session().userPeerId())) { + // Ignore call from the same running app, other account. + return; + } + const auto &config = session->serverConfig(); if (inCall() || inGroupCall() || !user || user->isSelf()) { const auto flags = phoneCall.is_video() diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 679419711..4113a600e 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -105,6 +105,7 @@ public: [[nodiscard]] FnMut addAsyncWaiter(); + [[nodiscard]] bool isSharingScreen() const; [[nodiscard]] bool isQuitPrevent(); private: diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index fcb0a1c7b..46481cad6 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -95,6 +95,7 @@ Panel::Panel(not_null call) _decline->entity()->setText(tr::lng_call_decline()); _cancel->setDuration(st::callPanelDuration); _cancel->entity()->setText(tr::lng_call_cancel()); + _screencast->setDuration(st::callPanelDuration); initWindow(); initWidget(); @@ -299,6 +300,7 @@ void Panel::initControls() { _decline->finishAnimating(); _cancel->finishAnimating(); + _screencast->finishAnimating(); } void Panel::setIncomingSize(QSize size) { @@ -595,6 +597,7 @@ void Panel::showControls() { widget()->showChildren(); _decline->setVisible(_decline->toggled()); _cancel->setVisible(_cancel->toggled()); + _screencast->setVisible(_screencast->toggled()); const auto shown = !_incomingFrameSize.isEmpty(); _incoming->widget()->setVisible(shown); @@ -753,12 +756,6 @@ void Panel::updateControlsGeometry() { updateOutgoingVideoBubbleGeometry(); } - auto threeWidth = _answerHangupRedial->width() - + st::callCancel.button.width - - _screencast->width(); - _decline->moveToLeft((widget()->width() - threeWidth) / 2, _buttonsTop); - _cancel->moveToLeft((widget()->width() - threeWidth) / 2, _buttonsTop); - updateHangupGeometry(); } @@ -779,22 +776,28 @@ void Panel::updateOutgoingVideoBubbleGeometry() { } void Panel::updateHangupGeometry() { - auto twoWidth = _answerHangupRedial->width() + _screencast->width(); - auto threeWidth = twoWidth + st::callCancel.button.width; - auto rightFrom = (widget()->width() - threeWidth) / 2; - auto rightTo = (widget()->width() - twoWidth) / 2; - auto hangupProgress = (_call - && _call->state() == State::WaitingUserConfirmation) + const auto isWaitingUser = (_call + && _call->state() == State::WaitingUserConfirmation); + const auto hangupProgress = isWaitingUser ? 0. : _hangupShownProgress.value(_hangupShown ? 1. : 0.); - auto hangupRight = anim::interpolate(rightFrom, rightTo, hangupProgress); - _answerHangupRedial->moveToRight(hangupRight, _buttonsTop); _answerHangupRedial->setProgress(hangupProgress); - _mute->moveToRight(hangupRight - _mute->width(), _buttonsTop); - _screencast->moveToLeft(hangupRight - _mute->width(), _buttonsTop); - _camera->moveToLeft( - hangupRight - _mute->width() + _screencast->width(), - _buttonsTop); + + // Screencast - Camera - Cancel/Decline - Answer/Hangup/Redial - Mute. + const auto buttonWidth = st::callCancel.button.width; + const auto cancelWidth = buttonWidth * (1. - hangupProgress); + const auto cancelLeft = (isWaitingUser) + ? ((widget()->width() - buttonWidth) / 2) + : (_mute->animating()) + ? ((widget()->width() - cancelWidth) / 2) + : ((widget()->width() / 2) - cancelWidth); + + _cancel->moveToLeft(cancelLeft, _buttonsTop); + _decline->moveToLeft(cancelLeft, _buttonsTop); + _camera->moveToLeft(cancelLeft - buttonWidth, _buttonsTop); + _screencast->moveToLeft(_camera->x() - buttonWidth, _buttonsTop); + _answerHangupRedial->moveToLeft(cancelLeft + cancelWidth, _buttonsTop); + _mute->moveToLeft(_answerHangupRedial->x() + buttonWidth, _buttonsTop); if (_startVideo) { _startVideo->moveToLeft(_camera->x(), _camera->y()); } @@ -877,7 +880,9 @@ void Panel::stateChanged(State state) { toggleButton(_decline, incomingWaiting); toggleButton(_cancel, (isBusy || isWaitingUser)); toggleButton(_mute, !isWaitingUser); - toggleButton(_screencast, !isWaitingUser); + toggleButton( + _screencast, + !(isBusy || isWaitingUser || incomingWaiting)); const auto hangupShown = !_decline->toggled() && !_cancel->toggled(); if (_hangupShown != hangupShown) { diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index c66805f43..17f0008e8 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" #include "base/timer.h" #include "styles/style_calls.h" -#include "styles/style_chat.h" // style::GroupCallUserpics +#include "styles/style_chat_helpers.h" // style::GroupCallUserpics #include "styles/style_layers.h" namespace Calls { @@ -292,8 +292,7 @@ void TopBar::initControls() { call->setMuted(!call->muted()); } else if (const auto group = _groupCall.get()) { if (group->mutedByAdmin()) { - Ui::Toast::Show( - _show->toastParent(), + _show->showToast( tr::lng_group_call_force_muted_sub(tr::now)); } else { group->setMuted((group->muted() == MuteState::Muted) @@ -732,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_choose_join_as.cpp b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp index 2ac790288..a3f57de10 100644 --- a/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp +++ b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp @@ -406,9 +406,7 @@ void ChooseJoinAsProcess::processList( auto info = JoinInfo{ .peer = peer, .joinAs = self }; const auto selectedId = peer->groupCallDefaultJoinAs(); if (list.empty()) { - Ui::Toast::Show( - _request->show->toastParent(), - Lang::Hard::ServerError()); + _request->show->showToast(Lang::Hard::ServerError()); return; } info.joinAs = [&]() -> not_null { diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index f62573c90..49dd3db91 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lang/lang_hardcoded.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. -#include "ui/toasts/common_toasts.h" +#include "ui/toast/toast.h" #include "base/unixtime.h" #include "core/application.h" #include "core/core_settings.h" @@ -1413,11 +1413,9 @@ void GroupCall::rejoin(not_null as) { } hangup(); - Ui::ShowMultilineToast({ - .text = { type == u"GROUPCALL_FORBIDDEN"_q - ? tr::lng_group_not_accessible(tr::now) - : Lang::Hard::ServerError() }, - }); + Ui::Toast::Show((type == u"GROUPCALL_FORBIDDEN"_q) + ? tr::lng_group_not_accessible(tr::now) + : Lang::Hard::ServerError()); }).send(); }); }); diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index fd0225634..ffb685968 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -259,9 +259,7 @@ object_ptr PrepareInviteBox( finish(); }; const auto done = [=] { - const auto show = (*shared) - ? std::make_shared(*shared) - : nullptr; + const auto show = (*shared) ? (*shared)->uiShow() : nullptr; inviteWithAdd(show, users, nonMembers, finishWithConfirm); }; auto box = ConfirmBox({ diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index 10e1123cd..cc3c33ddb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -1983,8 +1983,8 @@ void Members::peerListShowBox( void Members::peerListHideLayer() { } -not_null Members::peerListToastParent() { - Unexpected("...Members::peerListToastParent"); +std::shared_ptr Members::peerListUiShow() { + Unexpected("...Members::peerListUiShow"); } } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index 312161b3a..f8b4c8c52 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -92,7 +92,7 @@ private: object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; void setupAddMember(not_null call); void resizeToList(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 7214a344c..210eb0dce 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -32,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/image/image_prepare.h" #include "ui/painter.h" #include "ui/round_rect.h" @@ -46,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" #include "data/data_session.h" #include "data/data_changes.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "base/event_filter.h" #include "base/unixtime.h" @@ -75,6 +75,94 @@ constexpr auto kControlsBackgroundOpacity = 0.8; constexpr auto kOverrideActiveColorBgAlpha = 172; constexpr auto kHideControlsTimeout = 5 * crl::time(1000); +class Show final : public Main::SessionShow { +public: + explicit Show(not_null panel); + ~Show(); + + void showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const override; + [[nodiscard]] not_null toastParent() const override; + [[nodiscard]] bool valid() const override; + operator bool() const override; + + [[nodiscard]] Main::Session &session() const override; + +private: + const base::weak_ptr _panel; + +}; + +Show::Show(not_null panel) +: _panel(base::make_weak(panel)) { +} + +Show::~Show() = default; + +void Show::showOrHideBoxOrLayer( + std::variant< + v::null_t, + object_ptr, + std::unique_ptr> &&layer, + Ui::LayerOptions options, + anim::type animated) const { + using UniqueLayer = std::unique_ptr; + using ObjectBox = object_ptr; + if (auto layerWidget = std::get_if(&layer)) { + if (const auto panel = _panel.get()) { + panel->showLayer(std::move(*layerWidget), options, animated); + } + } else if (auto box = std::get_if(&layer)) { + if (const auto panel = _panel.get()) { + panel->showBox(std::move(*box), options, animated); + } + } else if (const auto panel = _panel.get()) { + panel->hideLayer(animated); + } +} + +not_null Show::toastParent() const { + const auto panel = _panel.get(); + Assert(panel != nullptr); + return panel->widget(); +} + +bool Show::valid() const { + return !_panel.empty(); +} + +Show::operator bool() const { + return valid(); +} + +Main::Session &Show::session() const { + const auto panel = _panel.get(); + Assert(panel != nullptr); + 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 { @@ -176,15 +264,25 @@ bool Panel::isActive() const { && !(window()->windowState() & Qt::WindowMinimized); } -void Panel::showToast(TextWithEntities &&text, crl::time duration) { - if (const auto strong = _lastToast.get()) { - strong->hideAnimated(); - } - _lastToast = Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = std::move(text), - .duration = duration, - }); +base::weak_ptr Panel::showToast( + const QString &text, + crl::time duration) { + return Show(this).showToast(text, duration); +} + +base::weak_ptr Panel::showToast( + TextWithEntities &&text, + crl::time duration) { + return Show(this).showToast(std::move(text), duration); +} + +base::weak_ptr Panel::showToast( + Ui::Toast::Config &&config) { + return Show(this).showToast(std::move(config)); +} + +std::shared_ptr Panel::uiShow() { + return std::make_shared(this); } void Panel::minimize() { @@ -634,16 +732,9 @@ void Panel::hideNiceTooltip() { } void Panel::initShareAction() { - const auto showBoxCallback = [=](object_ptr next) { - showBox(std::move(next)); - }; - const auto showToastCallback = [=](QString text) { - showToast({ text }); - }; auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction( _peer, - showBoxCallback, - showToastCallback); + uiShow()); _callShareLinkCallback = [=, callback = std::move(shareLinkCallback)] { if (_call->lookupReal()) { callback(); @@ -1183,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) { @@ -1195,6 +1291,9 @@ void Panel::createPinOnTop() { _hideControlsTimer.callOnce(kHideControlsTimeout); } else { + if (_unpinnedMaximized) { + pin(false); + } _hideControlsTimerLifetime.destroy(); _hideControlsTimer.cancel(); refreshTitleGeometry(); @@ -1463,10 +1562,28 @@ void Panel::showBox( _layerBg->showBox(std::move(box), options, animated); } +void Panel::showLayer( + std::unique_ptr layer, + Ui::LayerOptions options, + anim::type animated) { + hideStickedTooltip(StickedTooltipHide::Unavailable); + if (window()->width() < st::groupCallWidth + || window()->height() < st::groupCallWidth) { + window()->resize( + std::max(window()->width(), st::groupCallWidth), + std::max(window()->height(), st::groupCallWidth)); + } + _layerBg->showLayer(std::move(layer), options, animated); +} + void Panel::hideLayer(anim::type animated) { _layerBg->hideAll(animated); } +bool Panel::isLayerShown() const { + return _layerBg->topShownLayer() != nullptr; +} + void Panel::kickParticipantSure(not_null participantPeer) { if (const auto chat = _peer->asChat()) { chat->session().api().chatParticipants().kick(chat, participantPeer); @@ -1968,6 +2085,7 @@ void Panel::showNiceTooltip( (normal ? widget().get() : container), std::move(text), st::groupCallNiceTooltipLabel); + label->resizeToWidth(label->textMaxWidth()); if (normal) { return label; } @@ -2429,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) @@ -2448,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, @@ -2458,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); @@ -2550,38 +2668,4 @@ not_null Panel::widget() const { return _window.widget(); } -Show::Show(not_null panel) -: _panel(base::make_weak(panel)) { -} - -Show::~Show() = default; - -void Show::showBox( - object_ptr content, - Ui::LayerOptions options) const { - if (const auto panel = _panel.get()) { - panel->showBox(std::move(content), options); - } -} - -void Show::hideLayer() const { - if (const auto panel = _panel.get()) { - panel->hideLayer(); - } -} - -not_null Show::toastParent() const { - const auto panel = _panel.get(); - Assert(panel != nullptr); - return panel->widget(); -} - -bool Show::valid() const { - return !_panel.empty(); -} - -Show::operator bool() const { - return valid(); -} - } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index e607d0c95..cf9352697 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -34,6 +34,7 @@ class GroupCall; namespace Ui { class BoxContent; +class LayerWidget; enum class LayerOption; using LayerOptions = base::flags; class AbstractButton; @@ -52,14 +53,21 @@ class ScrollArea; class GenericBox; class LayerManager; class GroupCallScheduledLeft; -namespace Toast { -class Instance; -} // namespace Toast -namespace Platform { -struct SeparateTitleControls; -} // namespace Platform } // namespace Ui +namespace Ui::Toast { +class Instance; +struct Config; +} // namespace Ui::Toast + +namespace Ui::Platform { +struct SeparateTitleControls; +} // namespace Ui::Platform + +namespace Main { +class SessionShow; +} // namespace Main + namespace style { struct CallSignalBars; struct CallBodyLayout; @@ -85,13 +93,26 @@ public: [[nodiscard]] not_null call() const; [[nodiscard]] bool isActive() const; - void showToast(TextWithEntities &&text, crl::time duration = 0); + base::weak_ptr showToast( + const QString &text, + crl::time duration = 0); + base::weak_ptr showToast( + TextWithEntities &&text, + crl::time duration = 0); + base::weak_ptr showToast( + Ui::Toast::Config &&config); + void showBox(object_ptr box); void showBox( object_ptr box, Ui::LayerOptions options, anim::type animated = anim::type::normal); + void showLayer( + std::unique_ptr layer, + Ui::LayerOptions options, + anim::type animated = anim::type::normal); void hideLayer(anim::type animated = anim::type::normal); + [[nodiscard]] bool isLayerShown() const; void minimize(); void toggleFullScreen(); @@ -99,6 +120,8 @@ public: void showAndActivate(); void closeBeforeDestroy(); + [[nodiscard]] std::shared_ptr uiShow(); + rpl::lifetime &lifetime(); private: @@ -205,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; @@ -260,7 +284,6 @@ private: Fn _callShareLinkCallback; const std::unique_ptr _toasts; - base::weak_ptr _lastToast; std::unique_ptr _micLevelTester; @@ -272,21 +295,4 @@ private: }; -class Show : public Ui::Show { -public: - explicit Show(not_null panel); - ~Show(); - void showBox( - object_ptr content, - Ui::LayerOptions options = Ui::LayerOption::KeepOther) const override; - void hideLayer() const override; - [[nodiscard]] not_null toastParent() const override; - [[nodiscard]] bool valid() const override; - operator bool() const override; - -private: - const base::weak_ptr _panel; - -}; - } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp index 1891a2ca0..69cd78107 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp @@ -151,9 +151,7 @@ void StartRtmpProcess::requestUrl(bool revoke) { }); processUrl(std::move(data)); }).fail([=] { - Ui::Toast::Show( - _request->show->toastParent(), - Lang::Hard::ServerError()); + _request->show->showToast(Lang::Hard::ServerError()); }).send(); } @@ -229,7 +227,7 @@ void StartRtmpProcess::FillRtmpRows( ) | rpl::map([=](const auto &d) { return d.url; }); const auto showToast = [=](const QString &text) { - Ui::Toast::Show(show->toastParent(), text); + show->showToast(text); }; const auto addButton = [&]( bool key, diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 831fcd3c2..59ed26a62 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -20,7 +20,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" #include "lang/lang_keys.h" #include "boxes/share_box.h" #include "history/view/history_view_schedule_box.h" @@ -108,7 +107,7 @@ object_ptr ShareInviteLinkBox( not_null peer, const QString &linkSpeaker, const QString &linkListener, - Fn showToast) { + std::shared_ptr show) { const auto sending = std::make_shared(); const auto box = std::make_shared>(); @@ -130,7 +129,7 @@ object_ptr ShareInviteLinkBox( }; auto copyCallback = [=] { QGuiApplication::clipboard()->setText(currentLink()); - showToast(tr::lng_group_invite_copied(tr::now)); + show->showToast(tr::lng_group_invite_copied(tr::now)); }; auto submitCallback = [=]( std::vector> &&result, @@ -191,7 +190,7 @@ object_ptr ShareInviteLinkBox( if (*box) { (*box)->closeBox(); } - showToast(tr::lng_share_done(tr::now)); + show->showToast(tr::lng_share_done(tr::now)); }; auto filterCallback = [](not_null thread) { return Data::CanSend(thread, ChatRestriction::SendOther); @@ -591,15 +590,11 @@ void SettingsBox( box->getDelegate()->show(std::move(next)); }); const auto showToast = crl::guard(box, [=](QString text) { - Ui::ShowMultilineToast({ - .parentOverride = Ui::BoxShow(box).toastParent(), - .text = { text }, - }); + box->showToast(text); }); auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction( peer, - showBox, - showToast); + box->uiShow()); shareLink = std::move(shareLinkCallback); box->lifetime().add(std::move(shareLinkLifetime)); } else { @@ -635,10 +630,8 @@ void SettingsBox( } QGuiApplication::clipboard()->setText(link); if (weakBox) { - Ui::ShowMultilineToast({ - .parentOverride = Ui::BoxShow(box).toastParent(), - .text = { tr::lng_create_channel_link_copied(tr::now) }, - }); + box->showToast( + tr::lng_create_channel_link_copied(tr::now)); } return true; }; @@ -735,7 +728,7 @@ void SettingsBox( StartRtmpProcess::FillRtmpRows( layout, false, - std::make_shared(box), + box->uiShow(), state->data.events(), &st::groupCallBoxLabel, &st::groupCallSettingsRtmpShowButton, @@ -795,8 +788,7 @@ void SettingsBox( std::pair, rpl::lifetime> ShareInviteLinkAction( not_null peer, - Fn)> showBox, - Fn showToast) { + std::shared_ptr show) { auto lifetime = rpl::lifetime(); struct State { State(not_null session) : session(session) { @@ -823,11 +815,11 @@ std::pair, rpl::lifetime> ShareInviteLinkAction( || state->linkListener.isEmpty()) { return false; } - showBox(ShareInviteLinkBox( + show->showBox(ShareInviteLinkBox( peer, *state->linkSpeaker, state->linkListener, - showToast)); + show)); return true; }; auto callback = [=] { diff --git a/Telegram/SourceFiles/calls/group/calls_group_settings.h b/Telegram/SourceFiles/calls/group/calls_group_settings.h index a4a829e90..598814ed2 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_settings.h +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.h @@ -25,8 +25,7 @@ void SettingsBox( [[nodiscard]] std::pair, rpl::lifetime> ShareInviteLinkAction( not_null peer, - Fn)> showBox, - Fn showToast); + std::shared_ptr show); class MicLevelTester final { public: diff --git a/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp index 4abaab3ec..55e67be95 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp @@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_group_call.h" #include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" +#include "ui/toast/toast.h" #include "lang/lang_keys.h" namespace Calls::Group { @@ -83,15 +83,13 @@ void Toasts::setupAllowedToSpeak() { _call->allowedToSpeakNotifications( ) | rpl::start_with_next([=] { if (_panel->isActive()) { - _panel->showToast({ - tr::lng_group_call_can_speak_here(tr::now), - }); + _panel->showToast(tr::lng_group_call_can_speak_here(tr::now)); } else { const auto real = _call->lookupReal(); const auto name = (real && !real->title().isEmpty()) ? real->title() : _call->peer()->name(); - Ui::ShowMultilineToast({ + Ui::Toast::Show({ .text = tr::lng_group_call_can_speak( tr::now, lt_chat, @@ -139,7 +137,7 @@ void Toasts::setupPinnedVideo() { : tr::lng_group_call_unpinned_screen); return key(tr::now, lt_user, peer->shortName()); }(); - _panel->showToast({ text }); + _panel->showToast(text); }, _lifetime); } @@ -148,9 +146,8 @@ void Toasts::setupRequestedToSpeak() { ) | rpl::combine_previous( ) | rpl::start_with_next([=](MuteState was, MuteState now) { if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) { - _panel->showToast({ - tr::lng_group_call_tooltip_raised_hand(tr::now), - }); + _panel->showToast( + tr::lng_group_call_tooltip_raised_hand(tr::now)); } }, _lifetime); } 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 97f94cb19..fb85201eb 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -10,10 +10,20 @@ using "ui/basic.style"; using "boxes/boxes.style"; using "ui/layers/layers.style"; using "ui/widgets/widgets.style"; +using "ui/menu_icons.style"; +using "ui/effects/premium.style"; + +GroupCallUserpics { + size: pixels; + shift: pixels; + stroke: pixels; + align: align; +} TabbedSearch { outer: color; bg: color; + bgActive: color; fg: color; fgActive: color; fadeLeft: icon; @@ -28,9 +38,57 @@ TabbedSearch { height: pixels; } +ComposeIcons { + settings: icon; + + recent: icon; + recentActive: icon; + people: icon; + peopleActive: icon; + nature: icon; + natureActive: icon; + food: icon; + foodActive: icon; + activity: icon; + activityActive: icon; + travel: icon; + travelActive: icon; + objects: icon; + objectsActive: icon; + symbols: icon; + symbolsActive: icon; + + menuFave: icon; + menuUnfave: icon; + menuStickerSet: icon; + menuRecentRemove: icon; + menuGifAdd: icon; + menuGifRemove: icon; + menuMute: icon; + menuSchedule: icon; + menuWhenOnline: icon; + menuSpoiler: icon; + menuSpoilerOff: icon; + + stripBubble: icon; + stripPremiumLocked: icon; + stripExpandPanel: icon; + stripExpandDropdown: icon; +} + +EmojiSuggestions { + dropdown: InnerDropdown; + bg: color; + overBg: color; + textFg: color; + fadeLeft: icon; + fadeRight: icon; +} + EmojiPan { margin: margins; padding: margins; + showAnimation: PanelAnimation; desiredSize: pixels; verticalSizeSub: pixels; header: pixels; @@ -43,13 +101,153 @@ EmojiPan { iconWidth: pixels; iconArea: pixels; bg: color; + headerFg: color; + trendingHeaderFg: color; + trendingSubheaderFg: color; + trendingUnreadFg: color; overBg: color; + pathBg: color; + pathFg: color; + textFg: color; categoriesBg: color; categoriesBgOver: color; fadeLeft: icon; fadeRight: icon; + menu: PopupMenu; + expandedSeparator: MenuSeparator; + tabs: SettingsSlider; search: TabbedSearch; searchMargin: margins; + colorAll: IconButton; + colorAllLabel: FlatLabel; + removeSet: IconButton; + boxLabel: FlatLabel; + icons: ComposeIcons; + autocompleteBottomSkip: pixels; +} + +MessageBar { + title: TextStyle; + titleFg: color; + text: TextStyle; + textFg: color; + textPalette: TextPalette; + duration: int; +} + +EmojiButton { + inner: IconButton; + bg: color; + lineFg: color; + lineFgOver: color; +} + +SendButton { + inner: IconButton; + record: icon; + recordOver: icon; + sendDisabledFg: color; +} + +RecordBarLock { + ripple: RippleAnimation; + originTop: icon; + originBottom: icon; + originBody: icon; + shadowTop: icon; + shadowBottom: icon; + shadowBody: icon; + arrow: icon; + fg: color; +} + +RecordBar { + radius: pixels; + bg: color; + durationFg: color; + cancel: color; + cancelActive: color; + cancelRipple: RippleAnimation; + lock: RecordBarLock; + remove: IconButton; +} + +ComposeFiles { + check: Check; + checkbox: Checkbox; + menu: IconButton; + caption: InputField; + emoji: EmojiButton; + confirmBg: color; + buttonFile: IconButton; + buttonFileEdit: icon; + buttonFileDelete: icon; + iconBg: color; + iconPlay: icon; + iconImage: icon; + iconDocument: icon; + nameFg: color; + statusFg: color; +} + +ComposeControls { + bg: color; + radius: pixels; + + field: InputField; + send: SendButton; + attach: IconButton; + emoji: EmojiButton; + like: IconButton; + liked: icon; + suggestions: EmojiSuggestions; + tabbed: EmojiPan; + tabbedHeightMin: pixels; + tabbedHeightMax: pixels; + record: RecordBar; + files: ComposeFiles; + premium: PremiumLimits; + boxField: InputField; +} + +ReportBox { + button: SettingsButton; + label: FlatLabel; + field: InputField; + spam: icon; + fake: icon; + violence: icon; + children: icon; + pornography: icon; + copyright: icon; + drugs: icon; + personal: icon; + other: icon; +} + +WhoRead { + userpics: GroupCallUserpics; + photoLeft: pixels; + photoSize: pixels; + photoSkip: pixels; + nameLeft: pixels; + iconPosition: point; + itemPadding: margins; +} + +defaultWhoRead: WhoRead { + userpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(right); + } + photoLeft: 13px; + photoSize: 30px; + photoSkip: 5px; + nameLeft: 57px; + iconPosition: point(15px, 7px); + itemPadding: margins(44px, 9px, 17px, 7px); } switchPmButton: RoundButton(defaultBoxButton) { @@ -93,6 +291,12 @@ stickersTrendingAdd: RoundButton(defaultActiveButton) { height: 26px; textTop: 4px; } +stickersTrendingInstalled: RoundButton(stickersTrendingAdd) { + textFg: activeButtonBg; + textFgOver: activeButtonBgOver; + textBg: activeButtonSecondaryFg; + textBgOver: activeButtonSecondaryFgOver; +} stickersRemove: IconButton(defaultIconButton) { width: 40px; height: 40px; @@ -113,10 +317,26 @@ stickersUndoRemove: RoundButton(defaultLightButton) { } stickersRemoveSkip: 4px; stickersReorderIcon: icon {{ "stickers_reorder", menuIconFg }}; -stickersReorderSkip: 13px; +stickersReorderSkip: 18px; stickersTabs: defaultTabsSlider; +stickersRowItem: PeerListItem(defaultPeerListItem) { + height: 52px; + photoSize: 32px; + photoPosition: point(18px, 10px); + namePosition: point(66px, 7px); + statusPosition: point(66px, 26px); + button: OutlineButton(defaultPeerListButton) { + textBg: contactsBg; + textBgOver: contactsBgOver; + ripple: defaultRippleAnimation; + } + statusFg: contactsStatusFg; + statusFgOver: contactsStatusFgOver; + statusFgActive: contactsStatusFgOnline; +} + stickerEmojiSkip: 5px; stickersFeaturedBadgeFont: font(12px bold); @@ -129,7 +349,6 @@ stickersFeaturedUnreadBg: msgFileInBg; stickersFeaturedUnreadSize: 5px; stickersFeaturedUnreadSkip: 5px; stickersFeaturedUnreadTop: 7px; -stickersFeaturedInstalled: icon {{ "chat/input_save", lightButtonFg }}; stickersMaxHeight: 320px; stickersPadding: margins(19px, 13px, 19px, 13px); @@ -144,12 +363,6 @@ stickersScroll: ScrollArea(boxScroll) { stickersRowDisabledOpacity: 0.4; stickersRowDuration: 200; -stickersSettings: icon {{ "emoji/emoji_settings", emojiIconFg }}; -stickersTrending: icon {{ "emoji/stickers_add", emojiIconFg }}; -stickersTrendingUnread: icon { - { "emoji/stickers_add_unread", emojiIconFg }, - { "emoji/stickers_add_dot", dialogsUnreadBg } -}; emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }}; filtersRemove: IconButton(stickersRemove) { @@ -158,27 +371,7 @@ filtersRemove: IconButton(stickersRemove) { emojiPanMargins: margins(10px, 10px, 10px, 10px); -emojiTabs: SettingsSlider(defaultTabsSlider) { - height: 43px; - barTop: 40px; - labelTop: 12px; -} -emojiRecent: icon {{ "emoji/emoji_recent", emojiIconFg }}; -emojiRecentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; -emojiPeople: icon {{ "emoji/emoji_smile", emojiIconFg }}; -emojiPeopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; -emojiNature: icon {{ "emoji/emoji_nature", emojiIconFg }}; -emojiNatureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; -emojiFood: icon {{ "emoji/emoji_food", emojiIconFg }}; -emojiFoodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; -emojiActivity: icon {{ "emoji/emoji_activities", emojiIconFg }}; -emojiActivityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; -emojiTravel: icon {{ "emoji/emoji_travel", emojiIconFg }}; -emojiTravelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; -emojiObjects: icon {{ "emoji/emoji_objects", emojiIconFg }}; -emojiObjectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; -emojiSymbols: icon {{ "emoji/emoji_love", emojiIconFg }}; -emojiSymbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; +emojiTabs: defaultTabsSlider; emojiCategoryIconTop: 6px; emojiPanAnimation: PanelAnimation(defaultPanelAnimation) { @@ -196,84 +389,60 @@ emojiPanSlideDuration: 200; emojiPanArea: size(34px, 32px); emojiPanRadius: 8px; +defaultTabbedSearchCancel: CrossButton { + width: 33px; + height: 33px; + + cross: CrossAnimation { + size: 27px; + skip: 8px; + stroke: 1.; + minScale: 0.3; + } + crossFg: menuIconFg; + crossFgOver: menuIconFg; + crossPosition: point(1px, 3px); + + duration: 150; + loadingPeriod: 1000; + ripple: emptyRippleAnimation; +} +defaultTabbedSearchField: InputField(defaultMultiSelectSearchField) { + textMargins: margins(2px, 7px, 2px, 0px); +} +defaultTabbedSearchButton: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} +defaultTabbedSearchBack: IconButton(defaultIconButton) { + width: 33px; + height: 33px; + icon: icon{{ "emoji/emoji_back", menuIconFg }}; + iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; + iconPosition: point(7px, -1px); + ripple: emptyRippleAnimation; +} defaultTabbedSearch: TabbedSearch { outer: emojiPanBg; bg: emojiPanHover; + bgActive: windowBgRipple; fg: emojiIconFg; fgActive: emojiSubIconFgActive; fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanHover }}; fadeRight: icon {{ "fade_horizontal", emojiPanHover }}; - field: InputField(defaultMultiSelectSearchField) { - textMargins: margins(2px, 7px, 2px, 0px); - } - search: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - back: IconButton(defaultIconButton) { - width: 33px; - height: 33px; - icon: icon{{ "emoji/emoji_back", menuIconFg }}; - iconOver: icon{{ "emoji/emoji_back", menuIconFg }}; - iconPosition: point(7px, -1px); - ripple: emptyRippleAnimation; - } - cancel: CrossButton { - width: 33px; - height: 33px; - - cross: CrossAnimation { - size: 27px; - skip: 8px; - stroke: 1.; - minScale: 0.3; - } - crossFg: menuIconFg; - crossFgOver: menuIconFg; - crossPosition: point(1px, 3px); - - duration: 150; - loadingPeriod: 1000; - ripple: emptyRippleAnimation; - } + field: defaultTabbedSearchField; + search: defaultTabbedSearchButton; + back: defaultTabbedSearchBack; + cancel: defaultTabbedSearchCancel; defaultFieldWidth: 103px; groupWidth: 30px; groupSkip: 2px; height: 33px; } -defaultEmojiPan: EmojiPan { - margin: margins(7px, 0px, 7px, 0px); - padding: margins(7px, 0px, 4px, 7px); - desiredSize: 37px; - verticalSizeSub: 1px; - header: 33px; - headerLeft: 14px; - headerLockLeft: 7px; - headerLockedLeft: 26px; - headerTop: 10px; - footer: 36px; - iconSkip: 3px; - iconWidth: 30px; - iconArea: 28px; - bg: emojiPanBg; - overBg: emojiPanHover; - categoriesBg: emojiPanCategories; - categoriesBgOver: windowBgRipple; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; - fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; - search: defaultTabbedSearch; - searchMargin: margins(1px, 11px, 2px, 5px); -} -statusEmojiPan: EmojiPan(defaultEmojiPan) { - categoriesBg: windowBg; - categoriesBgOver: windowBgOver; - fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; - fadeRight: icon {{ "fade_horizontal", windowBg }}; -} inlineResultsMinHeight: 278px; inlineResultsMaxHeight: 640px; @@ -281,6 +450,7 @@ inlineResultsMaxHeight: 640px; emojiPanHeaderFont: semiboldFont; emojiPanRemoveSkip: 10px; emojiPanRemoveTop: 10px; +emojiPanColorAllSkip: 9px; emojiColorsPadding: 5px; emojiColorsSep: 1px; @@ -323,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); @@ -337,6 +526,142 @@ stickersToast: Toast(defaultToast) { stickersEmpty: icon {{ "stickers_empty", windowSubTextFg }}; emojiEmpty: icon {{ "emoji_empty", windowSubTextFg }}; +editMediaButtonSize: 32px; + +editMediaButtonIconFile: icon {{ "send_media/send_media_replace", menuIconFg }}; +editMediaButton: IconButton(defaultIconButton) { + width: editMediaButtonSize; + height: editMediaButtonSize; + + icon: editMediaButtonIconFile; + + rippleAreaSize: editMediaButtonSize; + ripple: defaultRippleAnimation; +} + +sendBoxAlbumGroupEditInternalSkip: 8px; +sendBoxAlbumGroupSkipRight: 5px; +sendBoxAlbumGroupSkipTop: 5px; +sendBoxAlbumGroupRadius: 4px; +sendBoxAlbumGroupSize: size(62px, 25px); +sendBoxAlbumGroupSizeVertical: size(30px, 50px); +sendBoxAlbumSmallGroupSize: size(30px, 25px); + +sendBoxFileGroupSkipTop: 2px; +sendBoxFileGroupSkipRight: 5px; +sendBoxFileGroupEditInternalSkip: -1px; + +sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgRipple; + } +} +sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; +sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_delete", menuIconFg }}; + +sendBoxAlbumButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg }}; +sendBoxAlbumGroupButtonMediaEdit: icon {{ "send_media/send_media_replace", roundedFg, point(4px, 1px) }}; +sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_delete", roundedFg }}; + +defaultComposeIcons: ComposeIcons { + settings: icon {{ "emoji/emoji_settings", emojiIconFg }}; + + recent: icon {{ "emoji/emoji_recent", emojiIconFg }}; + recentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }}; + people: icon {{ "emoji/emoji_smile", emojiIconFg }}; + peopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }}; + nature: icon {{ "emoji/emoji_nature", emojiIconFg }}; + natureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }}; + food: icon {{ "emoji/emoji_food", emojiIconFg }}; + foodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }}; + activity: icon {{ "emoji/emoji_activities", emojiIconFg }}; + activityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }}; + travel: icon {{ "emoji/emoji_travel", emojiIconFg }}; + travelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }}; + objects: icon {{ "emoji/emoji_objects", emojiIconFg }}; + objectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }}; + symbols: icon {{ "emoji/emoji_love", emojiIconFg }}; + symbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }}; + + menuFave: menuIconFave; + menuUnfave: menuIconUnfave; + menuStickerSet: menuIconStickers; + menuRecentRemove: menuIconDelete; + menuGifAdd: menuIconGif; + menuGifRemove: menuIconDelete; + menuMute: menuIconMute; + menuSchedule: menuIconSchedule; + menuWhenOnline: menuIconWhenOnline; + menuSpoiler: menuIconSpoiler; + menuSpoilerOff: menuIconSpoilerOff; + + stripBubble: icon{ + { "chat/reactions_bubble_shadow", windowShadowFg }, + { "chat/reactions_bubble", windowBg }, + }; + stripPremiumLocked: icon{ + { "chat/reactions_premium_bg", historyPeerArchiveUserpicBg }, + { "chat/reactions_premium_star", historyPeerUserpicFg }, + }; + stripExpandPanel: icon{ + { "chat/reactions_round_big", windowBgRipple }, + { "chat/reactions_expand_panel", windowSubTextFg }, + }; + stripExpandDropdown: icon{ + { "chat/reactions_round_small", windowBgRipple }, + { "chat/reactions_expand_panel", windowSubTextFg }, + }; +} +defaultEmojiPan: EmojiPan { + margin: margins(7px, 0px, 7px, 0px); + padding: margins(7px, 0px, 4px, 7px); + showAnimation: emojiPanAnimation; + desiredSize: 37px; + verticalSizeSub: 1px; + header: 33px; + headerLeft: 14px; + headerLockLeft: 7px; + headerLockedLeft: 26px; + headerTop: 10px; + footer: 36px; + iconSkip: 3px; + iconWidth: 30px; + iconArea: 28px; + bg: emojiPanBg; + headerFg: emojiPanHeaderFg; + trendingHeaderFg: stickersTrendingHeaderFg; + trendingSubheaderFg: stickersTrendingSubheaderFg; + trendingUnreadFg: stickersFeaturedUnreadBg; + overBg: emojiPanHover; + pathBg: windowBgRipple; + pathFg: windowBgOver; + textFg: windowFg; + categoriesBg: emojiPanCategories; + categoriesBgOver: windowBgRipple; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }}; + fadeRight: icon {{ "fade_horizontal", emojiPanCategories }}; + menu: popupMenuWithIcons; + expandedSeparator: MenuSeparator(defaultMenuSeparator) { + padding: margins(0px, 4px, 0px, 4px); + width: 6px; + } + tabs: emojiTabs; + search: defaultTabbedSearch; + searchMargin: margins(1px, 11px, 2px, 5px); + colorAll: emojiPanColorAll; + colorAllLabel: emojiPanColorAllLabel; + removeSet: stickerPanRemoveSet; + boxLabel: boxLabel; + icons: defaultComposeIcons; + autocompleteBottomSkip: 0px; +} +statusEmojiPan: EmojiPan(defaultEmojiPan) { + categoriesBg: windowBg; + categoriesBgOver: windowBgOver; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; + fadeRight: icon {{ "fade_horizontal", windowBg }}; +} + inlineBotsScroll: ScrollArea(defaultSolidScroll) { deltat: stickerPanPadding; deltab: stickerPanPadding; @@ -353,6 +678,15 @@ emojiSuggestionsScrolledWidth: 240px; emojiSuggestionsPadding: margins(emojiColorsPadding, 0px, emojiColorsPadding, 0px); emojiSuggestionsFadeAfter: 20px; +defaultEmojiSuggestions: EmojiSuggestions { + dropdown: emojiSuggestionsDropdown; + bg: menuBg; + overBg: emojiPanHover; + textFg: windowFg; + fadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; + fadeRight: icon {{ "fade_horizontal", boxBg }}; +} + mentionHeight: 40px; mentionPadding: margins(8px, 5px, 8px, 5px); mentionTop: 11px; @@ -392,10 +726,6 @@ reactStripSize: 32px; reactStripMinWidth: 60px; reactStripImage: 26px; reactStripSkip: 7px; -reactStripBubble: icon{ - { "chat/reactions_bubble_shadow", windowShadowFg }, - { "chat/reactions_bubble", windowBg }, -}; reactStripBubbleRight: 20px; userpicBuilderEmojiPan: EmojiPan(statusEmojiPan) { margin: margins(reactStripSkip, 0px, reactStripSkip, 0px); @@ -422,9 +752,484 @@ reactPanelScroll: ScrollArea(emojiScroll) { deltab: 7px; } -emojiSuggestionsFadeLeft: icon {{ "fade_horizontal-flip_horizontal", boxBg }}; -emojiSuggestionsFadeRight: icon {{ "fade_horizontal", boxBg }}; - choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }}; choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }}; choosePeerCreateIconLeft: 25px; + +historyRequestsUserpics: GroupCallUserpics { + size: 22px; + shift: 8px; + stroke: 4px; + align: align(left); +} +historyRequestsHeight: 33px; + +historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); + +historyComposeAreaPalette: TextPalette(defaultTextPalette) { + linkFg: historyComposeAreaFgService; +} + +defaultMessageBar: MessageBar { + title: semiboldTextStyle; + titleFg: windowActiveTextFg; + text: defaultTextStyle; + textFg: historyComposeAreaFg; + textPalette: historyComposeAreaPalette; + duration: 160; +} + +historyComposeButton: FlatButton { + color: windowActiveTextFg; + overColor: windowActiveTextFg; + + bgColor: historyComposeButtonBg; + overBgColor: historyComposeButtonBgOver; + + width: -32px; + height: 46px; + + textTop: 14px; + + font: semiboldFont; + overFont: semiboldFont; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgRipple; + } +} +historyUnblock: FlatButton(historyComposeButton) { + color: attentionButtonFg; + overColor: attentionButtonFgOver; +} +historyContactStatusButton: FlatButton(historyComposeButton) { + height: 49px; + textTop: 16px; + overBgColor: historyComposeButtonBg; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyComposeButtonBgOver; + } +} +historyContactStatusBlock: FlatButton(historyContactStatusButton) { + color: attentionButtonFg; + overColor: attentionButtonFg; +} +historyContactStatusLabel: FlatLabel(defaultFlatLabel) { + minWidth: 240px; +} +historyEmojiStatusInfoLabel: FlatLabel(historyContactStatusLabel) { + align: align(top); + textFg: windowSubTextFg; +} +historyContactStatusMinSkip: 16px; + +historyReplySkip: 51px; +historyReplyNameFg: windowActiveTextFg; +historyReplyHeight: 49px; +historyReplyIconPosition: point(5px, 5px); +historyReplyIcon: icon {{ "chat/input_reply", historyReplyIconFg }}; +historyForwardIcon: icon {{ "chat/input_forward", historyReplyIconFg }}; +historyEditIcon: icon {{ "chat/input_edit", historyReplyIconFg }}; +historyReplyCancel: IconButton { + width: 49px; + height: 49px; + + icon: historyReplyCancelIcon; + iconOver: historyReplyCancelIconOver; + iconPosition: point(-1px, -1px); + + rippleAreaPosition: point(4px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +historyPinnedShowAll: IconButton(historyReplyCancel) { + icon: icon {{ "pinned_show_all", historyReplyCancelFg }}; + iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }}; +} +historyPinnedBotButton: RoundButton(defaultActiveButton) { + width: -34px; + height: 30px; + textTop: 6px; + padding: margins(2px, 10px, 10px, 9px); +} +historyPinnedBotButtonMaxWidth: 150px; + +historyToDownPosition: point(12px, 10px); +historyToDownAbove: icon {{ "history_down_arrow", historyToDownFg }}; +historyToDownAboveOver: icon {{ "history_down_arrow", historyToDownFgOver }}; +historyToDownPaddingTop: 10px; +historyToDownBelow: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBg }, +}; +historyToDownBelowOver: icon { + { "history_down_shadow", historyToDownShadow }, + { "history_down_circle", historyToDownBgOver }, +}; +historyToDown: TwoIconButton { + width: 52px; + height: 62px; + + iconBelow: historyToDownBelow; + iconBelowOver: historyToDownBelowOver; + iconAbove: historyToDownAbove; + iconAboveOver: historyToDownAboveOver; + iconPosition: point(0px, historyToDownPaddingTop); + + rippleAreaPosition: point(5px, 15px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: historyToDownBgRipple; + } +} +historyToDownBadgeFont: semiboldFont; +historyToDownBadgeSize: 22px; + +historyToDownShownAfter: 480px; +historyToDownDuration: 150; + +dialogsToUpAbove: icon {{ "history_down_arrow-flip_vertical", historyToDownFg, point(0px, 1px) }}; +dialogsToUpAboveOver: icon {{ "history_down_arrow-flip_vertical", historyToDownFgOver, point(0px, 1px) }}; + +dialogsToUp: TwoIconButton(historyToDown) { + iconAbove: dialogsToUpAbove; + iconAboveOver: dialogsToUpAboveOver; +} + +historyUnreadMentions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_mention", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_mention", historyToDownFgOver }}; +} +historyUnreadReactions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_reaction", historyToDownFg }}; + iconAboveOver: icon {{ "history_unread_reaction", historyToDownFgOver }}; +} +historyUnreadThingsSkip: 4px; + +historyComposeField: InputField(defaultInputField) { + font: normalFont; + textMargins: margins(0px, 0px, 0px, 0px); + textAlign: align(left); + textFg: historyComposeAreaFg; + textBg: historyComposeAreaBg; + heightMin: 36px; + heightMax: 72px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(7px, 5px, 7px, 5px); + placeholderAlign: align(topleft); + placeholderScale: 0.; + placeholderFont: normalFont; + placeholderShift: -50px; + border: 0px; + borderActive: 0px; + duration: 100; +} +historyComposeFieldMaxHeight: 224px; +// historyMinHeight: 56px; + +historyAttach: IconButton(defaultIconButton) { + width: 44px; + height: 46px; + + icon: icon {{ "chat/input_attach", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_attach", historyComposeIconFgOver }}; + + rippleAreaPosition: point(2px, 3px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +historyMessagesTTL: IconButtonWithText { + iconButton: IconButton(historyAttach) { + icon: icon {{ "chat/input_autodelete", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_autodelete", historyComposeIconFgOver }}; + } + textFg: historyComposeIconFg; + textFgOver: historyComposeIconFgOver; + textPadding: margins(21px, 20px, 3px, 7px); + textAlign: align(left); + + font: font(10px semibold); +} +historyReplaceMedia: IconButton(historyAttach) { + icon: icon {{ "chat/input_replace", windowBgActive }}; + iconOver: icon {{ "chat/input_replace", windowBgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgOver; + } +} + +historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }}; +historyEmojiCircle: size(20px, 20px); +historyEmojiCircleLine: 1.5; +historyEmojiCircleFg: historyComposeIconFg; +historyEmojiCircleFgOver: historyComposeIconFgOver; +historyBotKeyboardShow: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard", historyComposeIconFgOver }}; +} +historyBotKeyboardHide: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_keyboard_hide", historyComposeIconFgOver }}; +} +historyBotCommandStart: IconButton(historyAttach) { + icon: icon {{ "chat/input_bot_command", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_bot_command", historyComposeIconFgOver }}; +} +historyScheduledToggle: IconButton(historyAttach) { + icon: icon { + { "chat/input_scheduled", historyComposeIconFg }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; + iconOver: icon { + { "chat/input_scheduled", historyComposeIconFgOver }, + { "chat/input_scheduled_dot", attentionButtonFg } + }; +} + +historyAttachEmojiInner: IconButton(historyAttach) { + icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }}; +} +historyAttachEmoji: EmojiButton { + inner: historyAttachEmojiInner; + bg: historyComposeAreaBg; + lineFg: historyEmojiCircleFg; + lineFgOver: historyEmojiCircleFgOver; +} +boxAttachEmoji: EmojiButton(historyAttachEmoji) { + inner: IconButton(historyAttachEmojiInner) { + width: 30px; + height: 30px; + rippleAreaSize: 0px; + } +} +boxAttachEmojiTop: 20px; + +historySendIcon: icon {{ "chat/input_send", historySendIconFg }}; +historySendIconOver: icon {{ "chat/input_send", historySendIconFgOver }}; +historySendIconPosition: point(10px, 11px); +historySendSize: size(44px, 46px); +historyScheduleIcon: icon {{ "chat/input_schedule", historyComposeAreaBg }}; +historyScheduleIconPosition: point(7px, 8px); +historyEditSaveIcon: icon {{ "chat/input_save", historySendIconFg }}; +historyEditSaveIconOver: icon {{ "chat/input_save", historySendIconFgOver }}; + +historyEditMediaBg: videoPlayIconBg; +historyEditMedia: icon{{ "chat/input_draw", videoPlayIconFg }}; +historyMessagesTTLPickerHeight: 200px; +historyMessagesTTLPickerItemHeight: 40px; +historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + align: align(topleft); + textFg: windowSubTextFg; +} + +historyRecordVoiceFg: historyComposeIconFg; +historyRecordVoiceFgOver: historyComposeIconFgOver; +historyRecordVoiceFgInactive: attentionButtonFg; +historyRecordVoiceFgActive: windowBgActive; +historyRecordVoiceFgActiveIcon: windowFgActive; +historyRecordVoiceShowDuration: 120; +historyRecordVoiceDuration: 120; +historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; +historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; +historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; +historyRecordSendIconPosition: point(2px, 0px); +historyRecordVoiceRippleBgActive: lightButtonBgOver; +historyRecordSignalRadius: 5px; +historyRecordCancel: windowSubTextFg; +historyRecordCancelActive: windowActiveTextFg; +historyRecordFont: font(13px); +historyRecordDurationSkip: 12px; +historyRecordDurationFg: historyComposeAreaFg; + +historyRecordMainBlobMinRadius: 23px; +historyRecordMainBlobMaxRadius: 37px; +historyRecordMinorBlobMinRadius: 40px; +historyRecordMinorBlobMaxRadius: 47px; +historyRecordMajorBlobMinRadius: 43px; +historyRecordMajorBlobMaxRadius: 50px; + +historyRecordTextStyle: TextStyle(defaultTextStyle) { + font: historyRecordFont; +} + +historyRecordTextWidthForWrap: 210px; +historyRecordTextLeft: 15px; +historyRecordTextRight: 25px; + +historyRecordLockShowDuration: historyToDownDuration; +historyRecordLockSize: size(75px, 133px); + +historyRecordLockIconSize: size(14px, 17px); +historyRecordLockIconBottomHeight: 9px; +historyRecordLockIconLineHeight: 2px; +historyRecordLockIconLineSkip: 3px; +historyRecordLockIconLineWidth: 2px; +historyRecordLockIconArcHeight: 4px; +historyRecordStopIconWidth: 12px; + +historyRecordLockTopShadow: icon {{ "voice_lock/record_lock_top_shadow", historyToDownShadow }}; +historyRecordLockTop: icon {{ "voice_lock/record_lock_top", historyToDownBg }}; +historyRecordLockBottomShadow: icon {{ "voice_lock/record_lock_bottom_shadow", historyToDownShadow }}; +historyRecordLockBottom: icon {{ "voice_lock/record_lock_bottom", historyToDownBg }}; +historyRecordLockBodyShadow: icon {{ "voice_lock/record_lock_body_shadow", historyToDownShadow }}; +historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}; +historyRecordLockMargin: margins(4px, 4px, 4px, 4px); +historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }}; +historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px); + +historyRecordDelete: IconButton(historyAttach) { + icon: icon {{ "info/info_media_delete", historyComposeIconFg }}; + iconOver: icon {{ "info/info_media_delete", historyComposeIconFgOver }}; + iconPosition: point(10px, 11px); +} +historyRecordWaveformRightSkip: 10px; +historyRecordWaveformBgMargins: margins(5px, 7px, 5px, 7px); + +historyRecordWaveformBar: 3px; + +historyRecordLockPosition: point(1px, 35px); + +historyRecordCancelButtonWidth: 100px; +historyRecordCancelButtonFg: lightButtonFg; + +historySilentToggle: IconButton(historyBotKeyboardShow) { + icon: icon {{ "chat/input_silent", historyComposeIconFg }}; + iconOver: icon {{ "chat/input_silent", historyComposeIconFgOver }}; +} +historySilentToggleOn: icon {{ "chat/input_silent_on", historyComposeIconFg }}; +historySilentToggleOnOver: icon {{ "chat/input_silent_on", historyComposeIconFgOver }}; + +defaultRecordBarLock: RecordBarLock { + ripple: defaultRippleAnimation; + originTop: historyRecordLockTop; + originBottom: historyRecordLockBottom; + originBody: historyRecordLockBody; + shadowTop: historyRecordLockTopShadow; + shadowBottom: historyRecordLockBottomShadow; + shadowBody: historyRecordLockBodyShadow; + arrow: historyRecordLockArrow; + fg: historyToDownFg; +} +defaultRecordBar: RecordBar { + bg: historyComposeAreaBg; + durationFg: historyRecordDurationFg; + cancel: historyRecordCancel; + cancelActive: historyRecordCancelActive; + cancelRipple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgRipple; + } + lock: defaultRecordBarLock; + remove: historyRecordDelete; +} + +historySend: SendButton { + inner: IconButton(historyAttach) { + icon: historySendIcon; + iconOver: historySendIconOver; + } + record: historyRecordVoice; + recordOver: historyRecordVoiceOver; + sendDisabledFg: historyComposeIconFg; +} + +defaultComposeFilesMenu: IconButton(defaultIconButton) { + width: 48px; + height: 54px; + + icon: icon {{ "title_menu_dots", boxTitleCloseFg }}; + iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }}; + iconPosition: point(18px, -1px); + + rippleAreaPosition: point(1px, 6px); + rippleAreaSize: 42px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +defaultComposeFilesField: InputField(defaultInputField) { + textMargins: margins(1px, 26px, 31px, 4px); + heightMax: 158px; +} +defaultComposeFiles: ComposeFiles { + check: defaultCheck; + checkbox: defaultBoxCheckbox; + menu: defaultComposeFilesMenu; + caption: defaultComposeFilesField; + emoji: boxAttachEmoji; + confirmBg: windowBgOver; + buttonFile: sendBoxAlbumGroupButtonFile; + buttonFileEdit: sendBoxAlbumGroupEditButtonIconFile; + buttonFileDelete: sendBoxAlbumGroupDeleteButtonIconFile; + iconBg: msgFileInBg; + iconPlay: icon {{ "history_file_play", historyFileInIconFg }}; + iconImage: icon {{ "history_file_image", historyFileInIconFg }}; + iconDocument: icon {{ "history_file_document", historyFileInIconFg }}; + nameFg: historyFileNameInFg; + statusFg: mediaInFg; +} +defaultComposeControls: ComposeControls { + bg: historyComposeAreaBg; + radius: 0px; + + field: historyComposeField; + send: historySend; + attach: historyAttach; + emoji: historyAttachEmoji; + suggestions: defaultEmojiSuggestions; + tabbed: defaultEmojiPan; + tabbedHeightMin: emojiPanMinHeight; + tabbedHeightMax: emojiPanMaxHeight; + record: defaultRecordBar; + files: defaultComposeFiles; + premium: defaultPremiumLimits; + boxField: defaultInputField; +} + +moreChatsBarHeight: 48px; +moreChatsBarTextPosition: point(12px, 4px); +moreChatsBarStatusPosition: point(12px, 24px); +moreChatsBarClose: IconButton(defaultIconButton) { + width: 48px; + height: 48px; + + icon: boxTitleCloseIcon; + iconOver: boxTitleCloseIconOver; + iconPosition: point(12px, -1px); + + rippleAreaPosition: point(0px, 4px); + rippleAreaSize: 40px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} + +reportReasonTopSkip: 8px; +reportReasonButton: SettingsButton(defaultSettingsButton) { + style: boxTextStyle; + padding: margins(62px, 7px, 8px, 7px); + iconLeft: 22px; +} + +defaultReportBox: ReportBox { + button: reportReasonButton; + label: boxLabel; + field: newGroupDescription; + spam: menuIconDelete; + fake: menuIconFake; + violence: menuIconViolence; + children: menuIconBlock; + pornography: menuIconPorn; + copyright: menuIconCopyright; + drugs: menuIconDrugs; + personal: menuIconPersonal; + other: menuIconReport; +} diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_features.h b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h new file mode 100644 index 000000000..ba6f43b4e --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_features.h @@ -0,0 +1,28 @@ +/* +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 { + +struct ComposeFeatures { + bool likes = false; + bool sendAs = true; + bool ttlInfo = true; + bool botCommandSend = true; + bool silentBroadcastToggle = true; + bool attachBotsMenu = true; + bool inlineBots = true; + bool megagroupSet = true; + bool stickersSettings = true; + bool openStickerSets = true; + bool autocompleteHashtags = true; + bool autocompleteMentions = true; + bool autocompleteCommands = true; +}; + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp b/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp new file mode 100644 index 000000000..9a27c6f82 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp @@ -0,0 +1,46 @@ +/* +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 "chat_helpers/compose/compose_show.h" + +#include "core/application.h" +#include "main/main_session.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" + +namespace ChatHelpers { + +rpl::producer Show::adjustShadowLeft() const { + return rpl::single(false); +} + +Window::SessionController *Show::resolveWindow(WindowUsage usage) const { + const auto session = &this->session(); + const auto check = [&](Window::Controller *window) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == session) { + return controller; + } + } + return (Window::SessionController*)nullptr; + }; + auto &app = Core::App(); + if (const auto a = check(app.activeWindow())) { + return a; + } else if (const auto b = check(app.activePrimaryWindow())) { + return b; + } else if (const auto c = check(app.windowFor(&session->account()))) { + return c; + } else if (const auto d = check( + app.ensureSeparateWindowForAccount( + &session->account()))) { + return d; + } + return nullptr; +} + +} // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/compose/compose_show.h b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h new file mode 100644 index 000000000..4a418c042 --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/compose/compose_show.h @@ -0,0 +1,70 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/flags.h" +#include "main/session/session_show.h" + +class PhotoData; +class DocumentData; + +namespace Data { +struct FileOrigin; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace SendMenu { +enum class Type; +} // namespace SendMenu + +namespace ChatHelpers { + +struct FileChosen; + +enum class PauseReason { + Any = 0, + InlineResults = (1 << 0), + TabbedPanel = (1 << 1), + Layer = (1 << 2), + RoundPlaying = (1 << 3), + MediaPreview = (1 << 4), +}; +using PauseReasons = base::flags; +inline constexpr bool is_flag_type(PauseReason) { return true; }; + +enum class WindowUsage { + PremiumPromo, +}; + +class Show : public Main::SessionShow { +public: + virtual void activate() = 0; + + [[nodiscard]] virtual bool paused(PauseReason reason) const = 0; + [[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0; + + [[nodiscard]] virtual rpl::producer adjustShadowLeft() const; + [[nodiscard]] virtual SendMenu::Type sendMenuType() const = 0; + + virtual bool showMediaPreview( + Data::FileOrigin origin, + not_null document) const = 0; + virtual bool showMediaPreview( + Data::FileOrigin origin, + not_null photo) const = 0; + + virtual void processChosenSticker(FileChosen &&chosen) const = 0; + + [[nodiscard]] virtual Window::SessionController *resolveWindow( + WindowUsage) const; +}; + +} // namespace ChatHelpers 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 2183be406..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 { @@ -56,9 +60,9 @@ using Core::RecentEmojiDocument; class EmojiColorPicker final : public Ui::RpWidget { public: - EmojiColorPicker(QWidget *parent); + 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,14 +83,18 @@ protected: void mouseMoveEvent(QMouseEvent *e) override; private: + void createAllLabel(); void animationCallback(); void updateSize(); + [[nodiscard]] int topColorAllSkip() const; void drawVariant(QPainter &p, int variant); void updateSelected(); void setSelected(int newSelected); + const style::EmojiPan &_st; + bool _ignoreShow = false; QVector _variants; @@ -97,12 +105,15 @@ private: QSize _singleSize; QPoint _areaPosition; QPoint _innerPosition; + Ui::RoundRect _backgroundRect; Ui::RoundRect _overBg; bool _hiding = false; QPixmap _cache; Ui::Animations::Simple _a_opacity; + std::unique_ptr _allLabel; + rpl::event_stream _chosen; rpl::event_stream<> _hidden; @@ -118,16 +129,25 @@ struct EmojiListWidget::RecentOne { RecentEmojiId id; }; -EmojiColorPicker::EmojiColorPicker(QWidget *parent) +EmojiColorPicker::EmojiColorPicker( + QWidget *parent, + const style::EmojiPan &st) : RpWidget(parent) -, _overBg(st::emojiPanRadius, st::emojiPanHover) { +, _st(st) +, _backgroundRect(st::emojiPanRadius, _st.bg) +, _overBg(st::emojiPanRadius, _st.overBg) { 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); @@ -137,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() @@ -151,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(); @@ -176,14 +218,18 @@ void EmojiColorPicker::paintEvent(QPaintEvent *e) { p.drawPixmap(0, 0, _cache); return; } - Ui::Shadow::paint(p, inner, width(), st::emojiPanAnimation.shadow); - Ui::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners); + 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); } @@ -241,6 +287,9 @@ void EmojiColorPicker::animationCallback() { update(); if (!_a_opacity.animating()) { _cache = QPixmap(); + if (_allLabel) { + _allLabel->show(); + } if (_hiding) { hide(); _hidden.fire({}); @@ -269,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); } @@ -284,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); } @@ -297,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()) { @@ -320,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) @@ -331,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()); }; @@ -370,12 +440,11 @@ void EmojiColorPicker::drawVariant(QPainter &p, int variant) { EmojiListWidget::EmojiListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level, + PauseReason level, Mode mode) : EmojiListWidget(parent, { - .session = &controller->session(), + .show = controller->uiShow(), .mode = mode, - .controller = controller, .paused = Window::PausedIn(controller, level), }) { } @@ -386,9 +455,10 @@ EmojiListWidget::EmojiListWidget( : Inner( parent, descriptor.st ? *descriptor.st : st::defaultEmojiPan, - descriptor.session, + descriptor.show, std::move(descriptor.paused)) -, _controller(descriptor.controller) +, _show(std::move(descriptor.show)) +, _features(descriptor.features) , _mode(descriptor.mode) , _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1) , _premiumIcon(_mode == Mode::EmojiStatus @@ -398,8 +468,8 @@ EmojiListWidget::EmojiListWidget( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) , _overBg(st::emojiPanRadius, st().overBg) -, _collapsedBg(st::emojiPanExpand.height / 2, st::emojiPanHeaderFg) -, _picker(this) +, _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) +, _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) { setMouseTracking(true); if (st().bg->c.alpha() > 0) { @@ -466,7 +536,7 @@ EmojiListWidget::~EmojiListWidget() { } void EmojiListWidget::setupSearch() { - const auto session = &_controller->session(); + const auto session = &_show->session(); _search = MakeSearch(this, st(), [=](std::vector &&query) { _nextSearchQuery = std::move(query); InvokeQueued(this, [=] { @@ -616,6 +686,10 @@ rpl::producer<> EmojiListWidget::jumpedToPremium() const { return _jumpedToPremium.events(); } +rpl::producer<> EmojiListWidget::escapes() const { + return _search ? _search->escapes() : rpl::never<>(); +} + void EmojiListWidget::prepareExpanding() { if (_search) { _searchExpandCache = _search->grab(); @@ -626,13 +700,14 @@ void EmojiListWidget::paintExpanding( Painter &p, QRect clip, int finalBottom, - float64 progress, + float64 geometryProgress, + float64 fullProgress, RectPart origin) { const auto searchShift = _search ? anim::interpolate( st().padding.top() - _search->height(), 0, - progress) + geometryProgress) : 0; const auto shift = clip.topLeft() + QPoint(0, searchShift); const auto adjusted = clip.translated(-shift); @@ -647,7 +722,7 @@ void EmojiListWidget::paintExpanding( p.translate(shift); p.setClipRect(adjusted); paint(p, ExpandingContext{ - .progress = progress, + .progress = fullProgress, .finalHeight = finalHeight, .expanding = true, }, adjusted); @@ -719,6 +794,7 @@ object_ptr EmojiListWidget::createFooter() { .paused = footerPaused, .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; @@ -838,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 @@ -884,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); } } @@ -943,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); @@ -952,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 })); @@ -976,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) { @@ -1007,7 +1193,7 @@ void EmojiListWidget::validateEmojiPaintContext( st::stickerPanPremium1, st::stickerPanPremium2, 0.5) - : st::windowFg->c), + : st().textFg->c), .size = QSize(_customSingleSize, _customSingleSize), .now = crl::now(), .scale = context.progress, @@ -1068,7 +1254,7 @@ void EmojiListWidget::paint( - paintButtonGetWidth(p, info, buttonSelected, clip); if (info.section > 0 && clip.top() < info.rowsTop) { p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto titleText = (info.section < _staticCount) ? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now) : _custom[info.section - _staticCount].title; @@ -1087,7 +1273,7 @@ void EmojiListWidget::paint( } const auto textBaseline = top + st::emojiPanHeaderFont->ascent; p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawText(titleLeft, textBaseline, titleText); } if (clip.top() + clip.height() > info.rowsTop) { @@ -1179,7 +1365,7 @@ void EmojiListWidget::drawCollapsedBadge( const auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2; const auto buttony = position.y() + (_singleSize.height() - buttonh) / 2; _collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh)); - p.setPen(st::emojiPanBg); + p.setPen(this->st().bg); p.setFont(st.font); p.drawText( buttonx + (buttonw - textWidth) / 2, @@ -1353,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); @@ -1372,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; } } } @@ -1416,29 +1600,34 @@ 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; - if (hasRemoveButton(button->section)) { + Assert(hasButton(button->section)); + const auto id = hasColorButton(button->section) + ? 0 + : _custom[button->section - _staticCount].id; + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (hasColorButton(button->section)) { + _pickerSelected = pressed; + showPicker(); + } else if (hasRemoveButton(button->section)) { removeSet(id); } else if (hasAddButton(button->section)) { _localSetsManager->install(id); - } else if (_controller) { + } else if (const auto resolved = _show->resolveWindow(usage)) { _jumpedToPremium.fire({}); switch (_mode) { case Mode::Full: case Mode::UserpicBuilder: - Settings::ShowPremium(_controller, u"animated_emoji"_q); + Settings::ShowPremium(resolved, u"animated_emoji"_q); break; case Mode::FullReactions: case Mode::RecentReactions: - Settings::ShowPremium(_controller, u"infinite_reactions"_q); + Settings::ShowPremium(resolved, u"infinite_reactions"_q); break; case Mode::EmojiStatus: - Settings::ShowPremium(_controller, u"emoji_status"_q); + Settings::ShowPremium(resolved, u"emoji_status"_q); break; case Mode::TopicIcon: - Settings::ShowPremium(_controller, u"forum_topic_icon"_q); + Settings::ShowPremium(resolved, u"forum_topic_icon"_q); break; } } @@ -1448,20 +1637,15 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { void EmojiListWidget::displaySet(uint64 setId) { const auto &sets = session().data().stickers().sets(); auto it = sets.find(setId); - if (it != sets.cend() && _controller) { - checkHideWithBox(_controller->show( - Box(_controller, it->second.get()), - Ui::LayerOption::KeepOther).data()); + if (it != sets.cend()) { + checkHideWithBox(Box(_show, it->second.get())); } } void EmojiListWidget::removeSet(uint64 setId) { - if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { - if (_controller) { - checkHideWithBox(_controller->show( - std::move(box), - Ui::LayerOption::KeepOther)); - } + const auto &labelSt = st().boxLabel; + if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) { + checkHideWithBox(std::move(box)); } } @@ -1487,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); + } } } @@ -1511,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()) { @@ -1533,9 +1752,10 @@ QRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const { if (_mode != Mode::Full) { return QRect(); } - const auto buttonw = st::stickerPanRemoveSet.rippleAreaPosition.x() - + st::stickerPanRemoveSet.rippleAreaSize; - const auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + const auto buttonw = removeSt.rippleAreaPosition.x() + + removeSt.rippleAreaSize; + const auto buttonh = removeSt.height; const auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw; const auto buttony = info.top + st::emojiPanRemoveTop; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1571,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) @@ -1627,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(); } @@ -1741,6 +1978,7 @@ void EmojiListWidget::refreshRecent() { clearSelection(); fillRecent(); resizeToWidth(width()); + update(); } void EmojiListWidget::refreshCustom() { @@ -1957,48 +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)) { - 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() + st::stickerPanRemoveSet.rippleAreaPosition.x(), - remove.y() + st::stickerPanRemoveSet.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 - ? st::stickerPanRemoveSet.iconOver - : st::stickerPanRemoveSet.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 @@ -2046,7 +2290,9 @@ void EmojiListWidget::updateSelected() { if (hasButton(section) && myrtlrect(buttonRect(section)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (section >= _staticCount && _mode == Mode::Full) { + } else if (_features.openStickerSets + && section >= _staticCount + && _mode == Mode::Full) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) { @@ -2097,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)); } } @@ -2156,17 +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 &st = remove - ? st::stickerPanRemoveSet.ripple + const auto &staticSt = colorAll ? st().colorAll : st().removeSet; + const auto &st = (colorAll || remove) + ? staticSt.ripple : st::emojiPanButton.ripple; - auto mask = remove + auto mask = (colorAll || remove) ? Ui::RippleAnimation::EllipseMask(QSize( - st::stickerPanRemoveSet.rippleAreaSize, - st::stickerPanRemoveSet.rippleAreaSize)) + staticSt.rippleAreaSize, + staticSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, @@ -2175,12 +2428,13 @@ 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) - ? st::stickerPanRemoveSet.rippleAreaPosition + + (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 e928bda69..5cc197995 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "ui/widgets/tooltip.h" #include "ui/round_rect.h" @@ -76,18 +77,18 @@ enum class EmojiListMode { }; struct EmojiListDescriptor { - not_null session; + std::shared_ptr show; EmojiListMode mode = EmojiListMode::Full; - Window::SessionController *controller = nullptr; Fn paused; std::vector customRecentList; Fn( DocumentId, Fn)> customRecentFactory; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; -class EmojiListWidget +class EmojiListWidget final : public TabbedSelector::Inner , public Ui::AbstractTooltipShower { public: @@ -96,7 +97,7 @@ public: EmojiListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level, + PauseReason level, Mode mode); EmojiListWidget(QWidget *parent, EmojiListDescriptor &&descriptor); ~EmojiListWidget(); @@ -124,6 +125,7 @@ public: [[nodiscard]] rpl::producer chosen() const; [[nodiscard]] rpl::producer customChosen() const; [[nodiscard]] rpl::producer<> jumpedToPremium() const; + [[nodiscard]] rpl::producer<> escapes() const; void provideRecent(const std::vector &customRecentList); @@ -132,7 +134,8 @@ public: Painter &p, QRect clip, int finalBottom, - float64 progress, + float64 geometryProgress, + float64 fullProgress, RectPart origin); base::unique_qptr fillContextMenu( @@ -244,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(); @@ -262,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, @@ -294,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; @@ -345,7 +361,8 @@ private: void applyNextSearchQuery(); - Window::SessionController *_controller = nullptr; + const std::shared_ptr _show; + const ComposeFeatures _features; Mode _mode = Mode::Full; std::unique_ptr _search; const int _staticCount = 0; @@ -374,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/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index b74e37708..a97d1fe42 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" +#include "ui/round_rect.h" #include "platform/platform_specific.h" #include "core/application.h" #include "base/event_filter.h" @@ -41,15 +42,133 @@ constexpr auto kAnimationDuration = crl::time(120); } // namespace +class SuggestionsWidget final : public Ui::RpWidget { +public: + SuggestionsWidget( + QWidget *parent, + const style::EmojiSuggestions &st, + not_null session, + bool suggestCustomEmoji, + Fn)> allowCustomWithoutPremium); + ~SuggestionsWidget(); + + void showWithQuery(SuggestionsQuery query, bool force = false); + void selectFirstResult(); + bool handleKeyEvent(int key); + + [[nodiscard]] rpl::producer toggleAnimated() const; + + struct Chosen { + QString emoji; + QString customData; + }; + [[nodiscard]] rpl::producer triggered() const; + +private: + struct Row { + Row(not_null emoji, const QString &replacement); + + Ui::Text::CustomEmoji *custom = nullptr; + DocumentData *document = nullptr; + not_null emoji; + QString replacement; + }; + struct Custom { + not_null document; + not_null emoji; + QString replacement; + }; + + bool eventHook(QEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void enterEventHook(QEnterEvent *e) override; + void leaveEventHook(QEvent *e) override; + + void scrollByWheelEvent(not_null e); + void paintFadings(QPainter &p) const; + + [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; + [[nodiscard]] base::flat_multi_map lookupCustom( + const std::vector &rows) const; + [[nodiscard]] std::vector appendCustom( + std::vector rows); + [[nodiscard]] std::vector appendCustom( + std::vector rows, + const base::flat_multi_map &custom); + void resizeToRows(); + void setSelected( + int selected, + anim::type animated = anim::type::instant); + void setPressed(int pressed); + void clearMouseSelection(); + void clearSelection(); + void updateSelectedItem(); + void updateItem(int index); + [[nodiscard]] QRect inner() const; + [[nodiscard]] QPoint innerShift() const; + [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; + void selectByMouse(QPoint globalPosition); + bool triggerSelectedRow() const; + void triggerRow(const Row &row) const; + + [[nodiscard]] int scrollCurrent() const; + void scrollTo(int value, anim::type animated = anim::type::instant); + void stopAnimations(); + + [[nodiscard]] not_null resolveCustomEmoji( + not_null document); + void customEmojiRepaint(); + + const style::EmojiSuggestions &_st; + const not_null _session; + SuggestionsQuery _query; + std::vector _rows; + bool _suggestCustomEmoji = false; + Fn)> _allowCustomWithoutPremium; + + Ui::RoundRect _overRect; + + base::flat_map< + not_null, + std::unique_ptr> _customEmoji; + bool _repaintScheduled = false; + + std::optional _lastMousePosition; + bool _mouseSelection = false; + int _selected = -1; + int _pressed = -1; + + int _scrollValue = 0; + Ui::Animations::Simple _scrollAnimation; + Ui::Animations::Simple _selectedAnimation; + int _scrollMax = 0; + int _oneWidth = 0; + QMargins _padding; + + QPoint _mousePressPosition; + int _dragScrollStart = -1; + + rpl::event_stream _toggleAnimated; + rpl::event_stream _triggered; + +}; + SuggestionsWidget::SuggestionsWidget( QWidget *parent, + const style::EmojiSuggestions &st, not_null session, bool suggestCustomEmoji, Fn)> allowCustomWithoutPremium) : RpWidget(parent) +, _st(st) , _session(session) , _suggestCustomEmoji(suggestCustomEmoji) , _allowCustomWithoutPremium(std::move(allowCustomWithoutPremium)) +, _overRect(st::roundRadiusSmall, _st.overBg) , _oneWidth(st::emojiSuggestionSize) , _padding(st::emojiSuggestionsPadding) { resize( @@ -284,7 +403,7 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { _repaintScheduled = false; const auto clip = e->rect(); - p.fillRect(clip, st::boxBg); + p.fillRect(clip, _st.bg); const auto shift = innerShift(); p.translate(-shift); @@ -298,15 +417,13 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { ? _pressed : _selectedAnimation.value(_selected); if (selected > -1.) { - Ui::FillRoundRect( + _overRect.paint( p, - QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth), - st::emojiPanHover, - Ui::StickerHoverCorners); + QRect(selected * _oneWidth, 0, _oneWidth, _oneWidth)); } auto context = Ui::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = _st.textFg->c, .now = crl::now(), }; for (auto i = from; i != till; ++i) { @@ -338,9 +455,9 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { const auto rect = myrtlrect( shift.x(), 0, - st::emojiSuggestionsFadeLeft.width(), + _st.fadeLeft.width(), height()); - st::emojiSuggestionsFadeLeft.fill(p, rect); + _st.fadeLeft.fill(p, rect); p.setOpacity(1.); } const auto o_right = std::clamp( @@ -350,11 +467,11 @@ void SuggestionsWidget::paintFadings(QPainter &p) const { if (o_right > 0.) { p.setOpacity(o_right); const auto rect = myrtlrect( - shift.x() + width() - st::emojiSuggestionsFadeRight.width(), + shift.x() + width() - _st.fadeRight.width(), 0, - st::emojiSuggestionsFadeRight.width(), + _st.fadeRight.width(), height()); - st::emojiSuggestionsFadeRight.fill(p, rect); + _st.fadeRight.fill(p, rect); p.setOpacity(1.); } } @@ -601,17 +718,17 @@ SuggestionsController::SuggestionsController( not_null field, not_null session, const Options &options) -: _field(field) +: _st(options.st ? *options.st : st::defaultEmojiSuggestions) +, _field(field) , _session(session) , _showExactTimer([=] { showWithQuery(getEmojiQuery()); }) , _options(options) { - _container = base::make_unique_q( - outer, - st::emojiSuggestionsDropdown); + _container = base::make_unique_q(outer, _st.dropdown); _container->setAutoHiding(false); _suggestions = _container->setOwnedWidget( object_ptr( _container, + _st, session, _options.suggestCustomEmoji, _options.allowCustomWithoutPremium)); @@ -661,6 +778,13 @@ SuggestionsController::SuggestionsController( updateForceHidden(); + _container->shownValue( + ) | rpl::filter([=](bool shown) { + return shown && !_shown; + }) | rpl::start_with_next([=] { + _container->hide(); + }, _container->lifetime()); + handleTextChange(); } @@ -749,6 +873,7 @@ void SuggestionsController::showWithQuery(SuggestionsQuery query) { const auto force = base::take(_keywordsRefreshed); _lastShownQuery = query; _suggestions->showWithQuery(_lastShownQuery, force); + _container->resizeToContent(); } SuggestionsQuery SuggestionsController::getEmojiQuery() { @@ -910,7 +1035,7 @@ void SuggestionsController::updateGeometry() { auto boundingRect = _container->parentWidget()->rect(); auto origin = rtl() ? PanelAnimation::Origin::BottomRight : PanelAnimation::Origin::BottomLeft; auto point = rtl() ? (aroundRect.topLeft() + QPoint(aroundRect.width(), 0)) : aroundRect.topLeft(); - const auto padding = st::emojiSuggestionsDropdown.padding; + const auto padding = _st.dropdown.padding; const auto shift = std::min(_container->width() - padding.left() - padding.right(), st::emojiSuggestionSize) / 2; point -= rtl() ? QPoint(_container->width() - padding.right() - shift, _container->height()) : QPoint(padding.left() + shift, _container->height()); if (rtl()) { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index 513fb518d..b89de0ad9 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct EmojiSuggestions; +} // namespace style + namespace Main { class Session; } // namespace Main @@ -29,125 +33,17 @@ class CustomEmoji; namespace Ui::Emoji { +class SuggestionsWidget; + using SuggestionsQuery = std::variant; -class SuggestionsWidget final : public Ui::RpWidget { -public: - SuggestionsWidget( - QWidget *parent, - not_null session, - bool suggestCustomEmoji, - Fn)> allowCustomWithoutPremium); - ~SuggestionsWidget(); - - void showWithQuery(SuggestionsQuery query, bool force = false); - void selectFirstResult(); - bool handleKeyEvent(int key); - - [[nodiscard]] rpl::producer toggleAnimated() const; - - struct Chosen { - QString emoji; - QString customData; - }; - [[nodiscard]] rpl::producer triggered() const; - -private: - struct Row { - Row(not_null emoji, const QString &replacement); - - Ui::Text::CustomEmoji *custom = nullptr; - DocumentData *document = nullptr; - not_null emoji; - QString replacement; - }; - struct Custom { - not_null document; - not_null emoji; - QString replacement; - }; - - bool eventHook(QEvent *e) override; - void paintEvent(QPaintEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void enterEventHook(QEnterEvent *e) override; - void leaveEventHook(QEvent *e) override; - - void scrollByWheelEvent(not_null e); - void paintFadings(QPainter &p) const; - - [[nodiscard]] std::vector getRowsByQuery(const QString &text) const; - [[nodiscard]] base::flat_multi_map lookupCustom( - const std::vector &rows) const; - [[nodiscard]] std::vector appendCustom( - std::vector rows); - [[nodiscard]] std::vector appendCustom( - std::vector rows, - const base::flat_multi_map &custom); - void resizeToRows(); - void setSelected( - int selected, - anim::type animated = anim::type::instant); - void setPressed(int pressed); - void clearMouseSelection(); - void clearSelection(); - void updateSelectedItem(); - void updateItem(int index); - [[nodiscard]] QRect inner() const; - [[nodiscard]] QPoint innerShift() const; - [[nodiscard]] QPoint mapToInner(QPoint globalPosition) const; - void selectByMouse(QPoint globalPosition); - bool triggerSelectedRow() const; - void triggerRow(const Row &row) const; - - [[nodiscard]] int scrollCurrent() const; - void scrollTo(int value, anim::type animated = anim::type::instant); - void stopAnimations(); - - [[nodiscard]] not_null resolveCustomEmoji( - not_null document); - void customEmojiRepaint(); - - const not_null _session; - SuggestionsQuery _query; - std::vector _rows; - bool _suggestCustomEmoji = false; - Fn)> _allowCustomWithoutPremium; - - base::flat_map< - not_null, - std::unique_ptr> _customEmoji; - bool _repaintScheduled = false; - - std::optional _lastMousePosition; - bool _mouseSelection = false; - int _selected = -1; - int _pressed = -1; - - int _scrollValue = 0; - Ui::Animations::Simple _scrollAnimation; - Ui::Animations::Simple _selectedAnimation; - int _scrollMax = 0; - int _oneWidth = 0; - QMargins _padding; - - QPoint _mousePressPosition; - int _dragScrollStart = -1; - - rpl::event_stream _toggleAnimated; - rpl::event_stream _triggered; - -}; - class SuggestionsController { public: struct Options { bool suggestExactFirstWord = true; bool suggestCustomEmoji = false; Fn)> allowCustomWithoutPremium; + const style::EmojiSuggestions *st = nullptr; }; SuggestionsController( @@ -189,6 +85,7 @@ private: bool fieldFilter(not_null event); bool outerFilter(not_null event); + const style::EmojiSuggestions &_st; bool _shown = false; bool _forceHidden = false; int _queryStartPosition = 0; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 2156fa805..8f5f5fec5 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -51,11 +51,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace { + [[nodiscard]] QString PrimaryUsername(not_null user) { const auto &usernames = user->usernames(); return usernames.empty() ? user->username() : usernames.front(); } +} // namespace + class FieldAutocomplete::Inner final : public Ui::RpWidget { public: struct ScrollTo { @@ -64,7 +68,8 @@ public: }; Inner( - not_null controller, + std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, @@ -120,12 +125,15 @@ private: Media::Clip::Notification notification, not_null document); - const not_null _controller; + const std::shared_ptr _show; + const not_null _session; + const style::EmojiPan &_st; const not_null _parent; const not_null _mrows; const not_null _hrows; const not_null _brows; const not_null _srows; + Ui::RoundRect _overBg; rpl::lifetime _stickersLifetime; std::weak_ptr _lottieRenderer; base::unique_qptr _menu; @@ -140,7 +148,7 @@ private: bool _previewShown = false; - bool _isOneColumn = false; + bool _adjustShadowLeft = false; const std::unique_ptr _pathGradient; StickerPremiumMark _premiumMark; @@ -182,8 +190,17 @@ struct FieldAutocomplete::BotCommandRow { FieldAutocomplete::FieldAutocomplete( QWidget *parent, not_null controller) +: FieldAutocomplete(parent, controller->uiShow()) { +} + +FieldAutocomplete::FieldAutocomplete( + QWidget *parent, + std::shared_ptr show, + const style::EmojiPan *stOverride) : RpWidget(parent) -, _controller(controller) +, _show(std::move(show)) +, _session(&_show->session()) +, _st(stOverride ? *stOverride : st::defaultEmojiPan) , _scroll(this) { hide(); @@ -191,7 +208,8 @@ FieldAutocomplete::FieldAutocomplete( _inner = _scroll->setOwnedWidget( object_ptr( - _controller, + _show, + _st, this, &_mrows, &_hrows, @@ -215,8 +233,8 @@ FieldAutocomplete::FieldAutocomplete( }), lifetime()); } -not_null FieldAutocomplete::controller() const { - return _controller; +std::shared_ptr FieldAutocomplete::uiShow() const { + return _show; } auto FieldAutocomplete::mentionChosen() const @@ -273,7 +291,7 @@ void FieldAutocomplete::paintEvent(QPaintEvent *e) { return; } - p.fillRect(rect(), st::mentionBg); + p.fillRect(rect(), _st.bg); } void FieldAutocomplete::showFiltered( @@ -365,7 +383,7 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) { } FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() { - const auto data = &_controller->session().data().stickers(); + const auto data = &_session->data().stickers(); const auto list = data->getListByEmoji({ _emoji }, _stickersSeed); auto result = ranges::views::all( list @@ -404,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(); } } @@ -470,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 { @@ -667,6 +699,7 @@ void FieldAutocomplete::recount(bool resetScroll) { } else if (!_brows.empty()) { h = _brows.size() * st::mentionHeight; } + h += _st.autocompleteBottomSkip; if (_inner->width() != _boundings.width() || _inner->height() != h) { _inner->resize(_boundings.width(), h); @@ -674,9 +707,14 @@ void FieldAutocomplete::recount(bool resetScroll) { if (h > _boundings.height()) h = _boundings.height(); if (h > maxh) h = maxh; if (width() != _boundings.width() || height() != h) { - setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h); + setGeometry( + _boundings.x(), + _boundings.y() + _boundings.height() - h, + _boundings.width(), + h); _scroll->resize(_boundings.width(), h); - } else if (y() != _boundings.y() + _boundings.height() - h) { + } else if (x() != _boundings.x() + || y() != _boundings.y() + _boundings.height() - h) { move(_boundings.x(), _boundings.y() + _boundings.height() - h); } if (resetScroll) st = 0; @@ -804,32 +842,36 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { } FieldAutocomplete::Inner::Inner( - not_null controller, + std::shared_ptr show, + const style::EmojiPan &st, not_null parent, not_null mrows, not_null hrows, not_null brows, not_null srows) -: _controller(controller) +: _show(std::move(show)) +, _session(&_show->session()) +, _st(st) , _parent(parent) , _mrows(mrows) , _hrows(hrows) , _brows(brows) , _srows(srows) +, _overBg(st::roundRadiusSmall, _st.overBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + _st.pathBg, + _st.pathFg, [=] { update(); })) -, _premiumMark(&controller->session()) +, _premiumMark(_session) , _previewTimer([=] { showPreview(); }) { - controller->session().downloaderTaskFinished( + _session->downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); - controller->adaptive().value( - ) | rpl::start_with_next([=] { - _isOneColumn = controller->adaptive().isOneColumn(); + _show->adjustShadowLeft( + ) | rpl::start_with_next([=](bool adjust) { + _adjustShadowLeft = adjust; update(); }, lifetime()); } @@ -887,12 +929,12 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { if (_sel == index) { QPoint tl(pos); if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width()); - Ui::FillRoundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, st::stickerPanSize)); } media->checkStickerSmall(); - const auto paused = _controller->isGifPausedAtLeastFor( - Window::GifPauseReason::TabbedPanel); + const auto paused = _show->paused( + ChatHelpers::PauseReason::TabbedPanel); const auto size = ChatHelpers::ComputeStickerSize( document, stickerBoundingBox()); @@ -1062,9 +1104,19 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { } } } - p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); + p.fillRect( + _adjustShadowLeft ? st::lineWidth : 0, + _parent->innerBottom() - st::lineWidth, + width() - (_adjustShadowLeft ? st::lineWidth : 0), + st::lineWidth, + st::shadowFg); } - p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerTop(), width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); + p.fillRect( + _adjustShadowLeft ? st::lineWidth : 0, + _parent->innerTop(), + width() - (_adjustShadowLeft ? st::lineWidth : 0), + st::lineWidth, + st::shadowFg); } void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) { @@ -1158,7 +1210,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex( contentRect.moveCenter(bounding.center()); return { Ui::MessageSendingAnimationFrom::Type::Sticker, - _controller->session().data().nextLocalMessageId(), + _show->session().data().nextLocalMessageId(), mapToGlobal(std::move(contentRect)), }; }; @@ -1231,7 +1283,7 @@ void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) { } } if (removed) { - _controller->session().local().writeRecentHashtagsAndBots(); + _show->session().local().writeRecentHashtagsAndBots(); } _parent->updateFiltered(); @@ -1283,7 +1335,8 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) { _menu, type, SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(this, type, send)); + SendMenu::DefaultScheduleCallback(this, type, send), + SendMenu::DefaultWhenOnlineCallback(send)); if (!_menu->empty()) { _menu->popup(QCursor::pos()); @@ -1342,8 +1395,10 @@ void FieldAutocomplete::Inner::setSel(int sel, bool scroll) { int32 row = _sel / _stickersPerRow; const auto padding = st::stickerPanPadding; _scrollToRequested.fire({ - padding + row * st::stickerPanSize.height(), - padding + (row + 1) * st::stickerPanSize.height() }); + (row ? padding : 0) + row * st::stickerPanSize.height(), + (padding + + (row + 1) * st::stickerPanSize.height() + + _st.autocompleteBottomSkip) }); } } } @@ -1482,11 +1537,7 @@ void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) { setSel(sel); if (_down >= 0 && _sel >= 0 && _down != _sel) { _down = _sel; - if (_down >= 0 && _down < _srows->size()) { - _controller->widget()->showMediaPreview( - (*_srows)[_down].document->stickerSetOrigin(), - (*_srows)[_down].document); - } + showPreview(); } } } @@ -1503,9 +1554,8 @@ void FieldAutocomplete::Inner::onParentGeometryChanged() { void FieldAutocomplete::Inner::showPreview() { if (_down >= 0 && _down < _srows->size()) { - _controller->widget()->showMediaPreview( - (*_srows)[_down].document->stickerSetOrigin(), - (*_srows)[_down].document); + const auto document = (*_srows)[_down].document; + _show->showMediaPreview(document->stickerSetOrigin(), document); _previewShown = true; } } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index a03d4c840..2606dc1f2 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/object_ptr.h" +namespace style { +struct EmojiPan; +} // namespace style + namespace Ui { class PopupMenu; class ScrollArea; @@ -25,6 +29,10 @@ class SinglePlayer; class FrameRenderer; } // namespace Lottie; +namespace Main { +class Session; +} // namespace Main + namespace Window { class SessionController; } // namespace Window @@ -39,6 +47,7 @@ enum class Type; namespace ChatHelpers { struct FileChosen; +class Show; } // namespace ChatHelpers class FieldAutocomplete final : public Ui::RpWidget { @@ -46,9 +55,13 @@ public: FieldAutocomplete( QWidget *parent, not_null controller); + FieldAutocomplete( + QWidget *parent, + std::shared_ptr show, + const style::EmojiPan *stOverride = nullptr); ~FieldAutocomplete(); - [[nodiscard]] not_null controller() const; + [[nodiscard]] std::shared_ptr uiShow() const; bool clearFilteredBotCommands(); void showFiltered( @@ -112,6 +125,8 @@ public: void setSendMenuType(Fn &&callback); void hideFast(); + void showAnimated(); + void hideAnimated(); rpl::producer mentionChosen() const; rpl::producer hashtagChosen() const; @@ -119,10 +134,6 @@ public: rpl::producer stickerChosen() const; rpl::producer choosingProcesses() const; -public Q_SLOTS: - void showAnimated(); - void hideAnimated(); - protected: void paintEvent(QPaintEvent *e) override; @@ -145,7 +156,9 @@ private: void recount(bool resetScroll = false); StickerRows getStickerSuggestions(); - const not_null _controller; + const std::shared_ptr _show; + const not_null _session; + const style::EmojiPan &_st; QPixmap _cache; MentionRows _mrows; HashtagRows _hrows; diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index fae4dd8f6..0ae43c3dd 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -56,8 +56,9 @@ constexpr auto kMinAfterScrollDelay = crl::time(33); void AddGifAction( Fn &&, const style::icon*)> callback, - Window::SessionController *controller, - not_null document) { + std::shared_ptr show, + not_null document, + const style::ComposeIcons *iconsOverride) { if (!document->isGifv()) { return; } @@ -67,9 +68,12 @@ void AddGifAction( const auto text = (saved ? tr::lng_context_delete_gif : tr::lng_context_save_gif)(tr::now); + const auto &icons = iconsOverride + ? *iconsOverride + : st::defaultComposeIcons; callback(text, [=] { Api::ToggleSavedGif( - controller, + show, document, Data::FileOriginSavedGifs(), !saved); @@ -80,19 +84,28 @@ void AddGifAction( document->session().local().writeSavedGifs(); } data.stickers().notifySavedGifsUpdated(); - }, saved ? &st::menuIconDelete : &st::menuIconGif); + }, saved ? &icons.menuGifRemove : &icons.menuGifAdd); } GifsListWidget::GifsListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level) + PauseReason level) +: GifsListWidget(parent, { + .show = controller->uiShow(), + .paused = Window::PausedIn(controller, level), +}) { +} + +GifsListWidget::GifsListWidget( + QWidget *parent, + GifsListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, - &controller->session(), - Window::PausedIn(controller, level)) -, _controller(controller) + descriptor.st ? *descriptor.st : st::defaultEmojiPan, + descriptor.show, + descriptor.paused) +, _show(std::move(descriptor.show)) , _api(&session().mtp()) , _section(Section::Gifs) , _updateInlineItems([=] { updateInlineItems(); }) @@ -120,7 +133,7 @@ GifsListWidget::GifsListWidget( updateInlineItems(); }, lifetime()); - controller->gifPauseLevelChanged( + _show->pauseChanged( ) | rpl::start_with_next([=] { if (!paused()) { updateInlineItems(); @@ -159,6 +172,7 @@ object_ptr GifsListWidget::createFooter() { .paused = pausedMethod(), .parent = this, .st = &st(), + .features = { .stickersSettings = false }, }); _footer = result; _chosenSetId = Data::Stickers::RecentSetId; @@ -323,7 +337,7 @@ void GifsListWidget::inlineResultsDone(const MTPmessages_BotResults &result) { void GifsListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintInlineItems(p, clip); } @@ -371,17 +385,18 @@ base::unique_qptr GifsListWidget::fillContextMenu( return nullptr; } - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto send = [=, selected = _selected](Api::SendOptions options) { selectInlineResult(selected, options, true); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(this, type, send)); + SendMenu::DefaultScheduleCallback(this, type, send), + SendMenu::DefaultWhenOnlineCallback(send), + icons); if (const auto item = _mosaic.maybeItemAt(_selected)) { const auto document = item->getDocument() @@ -394,7 +409,7 @@ base::unique_qptr GifsListWidget::fillContextMenu( const style::icon *icon) { menu->addAction(text, std::move(done), icon); }; - AddGifAction(std::move(callback), _controller, document); + AddGifAction(std::move(callback), _show, document, icons); } } return menu; @@ -424,7 +439,7 @@ void GifsListWidget::mouseReleaseEvent(QMouseEvent *e) { ActivateClickHandler(window(), activated, { e->button(), QVariant::fromValue(ClickHandlerContext{ - .sessionWindow = base::make_weak(_controller), + .show = _show, }) }); } @@ -793,7 +808,7 @@ bool GifsListWidget::refreshInlineRows(int32 *added) { } void GifsListWidget::setupSearch() { - const auto session = &_controller->session(); + const auto session = &_show->session(); _search = MakeSearch(this, st(), [=](std::vector &&query) { const auto accumulated = ranges::accumulate(query, QString(), []( QString a, @@ -938,13 +953,11 @@ void GifsListWidget::updateSelected() { _pressed = _selected; if (item) { if (const auto preview = item->getPreviewDocument()) { - _controller->widget()->showMediaPreview( + _show->showMediaPreview( Data::FileOriginSavedGifs(), preview); } else if (const auto preview = item->getPreviewPhoto()) { - _controller->widget()->showMediaPreview( - Data::FileOrigin(), - preview); + _show->showMediaPreview(Data::FileOrigin(), preview); } } } @@ -960,11 +973,11 @@ void GifsListWidget::showPreview() { } if (const auto layout = _mosaic.maybeItemAt(_pressed)) { if (const auto previewDocument = layout->getPreviewDocument()) { - _previewShown = _controller->widget()->showMediaPreview( + _previewShown = _show->showMediaPreview( Data::FileOriginSavedGifs(), previewDocument); } else if (const auto previewPhoto = layout->getPreviewPhoto()) { - _previewShown = _controller->widget()->showMediaPreview( + _previewShown = _show->showMediaPreview( Data::FileOrigin(), previewPhoto); } diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index c4e007f73..84e20c864 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include +namespace style { +struct ComposeIcons; +} // namespace style + namespace Api { struct SendOptions; } // namespace Api @@ -47,21 +51,29 @@ namespace ChatHelpers { void AddGifAction( Fn &&, const style::icon*)> callback, - Window::SessionController *controller, - not_null document); + std::shared_ptr show, + not_null document, + const style::ComposeIcons *iconsOverride = nullptr); class StickersListFooter; struct StickerIcon; struct GifSection; -class GifsListWidget +struct GifsListDescriptor { + std::shared_ptr show; + Fn paused; + const style::EmojiPan *st = nullptr; +}; + +class GifsListWidget final : public TabbedSelector::Inner , public InlineBots::Layout::Context { public: GifsListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level); + PauseReason level); + GifsListWidget(QWidget *parent, GifsListDescriptor &&descriptor); rpl::producer fileChosen() const; rpl::producer photoChosen() const; @@ -162,7 +174,7 @@ private: Api::SendOptions options, bool forceSend = false); - not_null _controller; + const std::shared_ptr _show; std::unique_ptr _search; MTP::Sender _api; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index ddceac33f..304c7d3a0 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "base/qt/qt_common_adapters.h" #include @@ -125,8 +126,7 @@ QString FieldTagMimeProcessor::operator()(QStringView mimeTag) { void EditLinkBox( not_null box, - std::shared_ptr show, - not_null session, + std::shared_ptr show, const QString &startText, const QString &startLink, Fn callback, @@ -149,8 +149,8 @@ void EditLinkBox( Ui::Emoji::SuggestionsController::Init( box->getDelegate()->outerContainer(), text, - session); - InitSpellchecker(std::move(show), session, text, fieldStyle != nullptr); + &show->session()); + InitSpellchecker(show, text, fieldStyle != nullptr); const auto placeholder = content->add( object_ptr(content), @@ -279,13 +279,25 @@ 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)); +} + Fn DefaultEditLinkCallback( - std::shared_ptr show, - not_null session, + std::shared_ptr show, not_null field, const style::InputField *fieldStyle) { const auto weak = Ui::MakeWeak(field); @@ -303,23 +315,20 @@ FncommitMarkdownLinkEdit(selection, text, link); } }; - show->showBox( - Box( - EditLinkBox, - show, - session, - text, - link, - std::move(callback), - fieldStyle), - Ui::LayerOption::KeepOther); + show->showBox(Box( + EditLinkBox, + show, + text, + link, + std::move(callback), + fieldStyle)); return true; }; } void InitMessageFieldHandlers( not_null session, - std::shared_ptr show, + std::shared_ptr show, not_null field, Fn customEmojiPaused, Fn)> allowPremiumEmoji, @@ -338,19 +347,19 @@ void InitMessageFieldHandlers( field->setMarkdownReplacesEnabled(rpl::single(true)); if (show) { field->setEditLinkCallback( - DefaultEditLinkCallback(show, session, field, fieldStyle)); - InitSpellchecker(show, session, field, fieldStyle != nullptr); + DefaultEditLinkCallback(show, field, fieldStyle)); + InitSpellchecker(show, field, fieldStyle != nullptr); } } void InitMessageFieldHandlers( not_null controller, not_null field, - Window::GifPauseReason pauseReasonLevel, + ChatHelpers::PauseReason pauseReasonLevel, Fn)> allowPremiumEmoji) { InitMessageFieldHandlers( &controller->session(), - std::make_shared(controller), + controller->uiShow(), field, [=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); }, allowPremiumEmoji); @@ -366,25 +375,36 @@ void InitMessageFieldGeometry(not_null field) { } void InitMessageField( - not_null controller, + std::shared_ptr show, not_null field, Fn)> allowPremiumEmoji) { InitMessageFieldHandlers( - controller, + &show->session(), + show, field, - Window::GifPauseReason::Any, - allowPremiumEmoji); + [=] { return show->paused(ChatHelpers::PauseReason::Any); }, + std::move(allowPremiumEmoji)); InitMessageFieldGeometry(field); field->customTab(true); } +void InitMessageField( + not_null controller, + not_null field, + Fn)> allowPremiumEmoji) { + return InitMessageField( + controller->uiShow(), + field, + std::move(allowPremiumEmoji)); +} + void InitSpellchecker( - std::shared_ptr show, - not_null session, + std::shared_ptr show, not_null field, bool skipDictionariesManager) { #ifndef TDESKTOP_DISABLE_SPELLCHECK using namespace Spellchecker; + const auto session = &show->session(); const auto menuItem = skipDictionariesManager ? std::nullopt : std::make_optional(SpellingHighlighter::CustomContextMenuItem{ @@ -491,7 +511,8 @@ InlineBotQuery ParseInlineBotQuery( } AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field) { + not_null field, + ChatHelpers::ComposeFeatures features) { auto result = AutocompleteQuery(); const auto cursor = field->textCursor(); @@ -523,6 +544,9 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( const auto text = fragment.text(); for (auto i = position - fragmentPosition; i != 0; --i) { if (text[i - 1] == '@') { + if (!features.autocompleteMentions) { + return {}; + } if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); @@ -533,12 +557,18 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery( } return result; } else if (text[i - 1] == '#') { + if (!features.autocompleteHashtags) { + return {}; + } if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); } return result; } else if (text[i - 1] == '/') { + if (!features.autocompleteCommands) { + return {}; + } if (i < 2 && !fragmentPosition) { result.fromStart = (i == 1) && (fragmentPosition == 0); result.query = text.mid(i - 1, position - fragmentPosition - i + 1); @@ -578,6 +608,10 @@ void MessageLinksParser::parseNow() { parse(); } +void MessageLinksParser::setDisabled(bool disabled) { + _disabled = disabled; +} + bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) { if (object == _field) { if (event->type() == QEvent::KeyPress) { @@ -608,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; } @@ -856,7 +890,7 @@ base::unique_qptr CreateDisabledFieldView( *toast = Ui::Toast::Show(parent, { .text = { tr::lng_send_text_no_about(tr::now, lt_types, types) }, .st = &st::defaultMultilineToast, - .durationMs = kTypesDuration, + .duration = kTypesDuration, .multiline = true, .slideSide = RectPart::Bottom, }); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 8efa27c2c..acc2808a1 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "base/timer.h" #include "base/qt_connection.h" +#include "chat_helpers/compose/compose_features.h" #ifndef TDESKTOP_DISABLE_SPELLCHECK #include "boxes/dictionaries_manager.h" @@ -20,33 +21,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Main { class Session; +class SessionShow; } // namespace Main namespace Window { class SessionController; -enum class GifPauseReason; } // namespace Window +namespace ChatHelpers { +enum class PauseReason; +class Show; +} // namespace ChatHelpers + namespace Ui { class PopupMenu; -class Show; } // 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 DefaultEditLinkCallback( - std::shared_ptr show, - not_null session, + std::shared_ptr show, not_null field, const style::InputField *fieldStyle = nullptr); void InitMessageFieldHandlers( not_null session, - std::shared_ptr show, + std::shared_ptr show, // may be null not_null field, Fn customEmojiPaused, Fn)> allowPremiumEmoji = nullptr, @@ -54,16 +61,19 @@ void InitMessageFieldHandlers( void InitMessageFieldHandlers( not_null controller, not_null field, - Window::GifPauseReason pauseReasonLevel, + ChatHelpers::PauseReason pauseReasonLevel, Fn)> allowPremiumEmoji = nullptr); +void InitMessageField( + std::shared_ptr show, + not_null field, + Fn)> allowPremiumEmoji); void InitMessageField( not_null controller, not_null field, Fn)> allowPremiumEmoji); void InitSpellchecker( - std::shared_ptr show, - not_null session, + std::shared_ptr show, not_null field, bool skipDictionariesManager = false); @@ -84,13 +94,15 @@ struct AutocompleteQuery { bool fromStart = false; }; AutocompleteQuery ParseMentionHashtagBotCommandQuery( - not_null field); + not_null field, + ChatHelpers::ComposeFeatures features); class MessageLinksParser : private QObject { public: MessageLinksParser(not_null field); void parseNow(); + void setDisabled(bool disabled); [[nodiscard]] const rpl::variable &list() const; @@ -118,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/chat_helpers/stickers_dice_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp index fed572b75..dcebbfdc4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp @@ -128,7 +128,7 @@ void DicePack::generateLocal(int index, const QString &name) { QByteArray(), nullptr, SendMediaType::File, - FileLoadTo(0, {}, 0, 0, 0), + FileLoadTo(0, {}, {}, 0), {}, false); task.process({ .generateGoodThumbnail = false }); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index a846b5362..15e2e8c34 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -292,7 +292,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) descriptor.st ? *descriptor.st : st::defaultEmojiPan) , _session(descriptor.session) , _paused(descriptor.paused) -, _settingsButtonVisible(descriptor.settingsButtonVisible) +, _features(descriptor.features) , _iconState([=] { update(); }) , _subiconState([=] { update(); }) , _selectionBg(st::emojiPanRadius, st().categoriesBgOver) @@ -300,7 +300,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) setMouseTracking(true); _iconsLeft = st().iconSkip - + (_settingsButtonVisible ? st().iconWidth : 0); + + (_features.stickersSettings ? st().iconWidth : 0); _iconsRight = st().iconSkip; _session->downloaderTaskFinished( @@ -618,7 +618,7 @@ void StickersListFooter::paint( return; } - if (_settingsButtonVisible && !hasOnlyFeaturedSets()) { + if (_features.stickersSettings) { paintStickerSettingsIcon(p); } @@ -1012,12 +1012,12 @@ void StickersListFooter::updateSelected() { if (rtl()) x = width() - x; const auto settingsLeft = _iconsLeft - _singleWidth; auto newOver = OverState(SpecialOver::None); - if (_settingsButtonVisible + if (_features.stickersSettings && x >= settingsLeft && x < settingsLeft + _singleWidth && y >= _iconsTop && y < _iconsTop + st().footer) { - if (!_icons.empty() && !hasOnlyFeaturedSets()) { + if (!_icons.empty()) { newOver = SpecialOver::Settings; } } else if (!_icons.empty()) { @@ -1161,17 +1161,11 @@ void StickersListFooter::refreshSubiconsGeometry() { updateEmojiWidthCallback(); } -bool StickersListFooter::hasOnlyFeaturedSets() const { - return (_icons.size() == 1) - && (_icons[0].setId == Data::Stickers::FeaturedSetId); -} - void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const { const auto settingsLeft = _iconsLeft - _singleWidth; - st::stickersSettings.paint( + st().icons.settings.paint( p, - settingsLeft - + (_singleWidth - st::stickersSettings.width()) / 2, + (settingsLeft + (_singleWidth - st().icons.settings.width()) / 2), _iconsTop + st::emojiCategoryIconTop, width()); } @@ -1351,7 +1345,7 @@ void StickersListFooter::paintSetIconToCache( const auto y = (st().footer - icon.pixh) / 2; if (icon.custom) { icon.custom->paint(p, Ui::Text::CustomEmoji::Context{ - .textColor = st::windowFg->c, + .textColor = st().textFg->c, .size = QSize(icon.pixw, icon.pixh), .now = now, .scale = context.progress, @@ -1411,22 +1405,22 @@ void StickersListFooter::paintSetIconToCache( using Section = Ui::Emoji::Section; const auto sectionIcon = [&](Section section, bool active) { const auto icons = std::array{ - &st::emojiRecent, - &st::emojiRecentActive, - &st::emojiPeople, - &st::emojiPeopleActive, - &st::emojiNature, - &st::emojiNatureActive, - &st::emojiFood, - &st::emojiFoodActive, - &st::emojiActivity, - &st::emojiActivityActive, - &st::emojiTravel, - &st::emojiTravelActive, - &st::emojiObjects, - &st::emojiObjectsActive, - &st::emojiSymbols, - &st::emojiSymbolsActive, + &st().icons.recent, + &st().icons.recentActive, + &st().icons.people, + &st().icons.peopleActive, + &st().icons.nature, + &st().icons.natureActive, + &st().icons.food, + &st().icons.foodActive, + &st().icons.activity, + &st().icons.activityActive, + &st().icons.travel, + &st().icons.travelActive, + &st().icons.objects, + &st().icons.objectsActive, + &st().icons.symbols, + &st().icons.symbolsActive, }; const auto index = int(section) * 2 + (active ? 1 : 0); @@ -1464,15 +1458,8 @@ void StickersListFooter::paintSetIconToCache( } else { paintOne(0, [&] { const auto selected = (info.index == _iconState.selected); - if (icon.setId == Data::Stickers::FeaturedSetId) { - const auto &stickers = _session->data().stickers(); - return stickers.featuredSetsUnreadCount() - ? &st::stickersTrendingUnread - : &st::stickersTrending; - //} else if (setId == Stickers::FavedSetId) { - // return &st::stickersFaved; - } else if (icon.setId == AllEmojiSectionSetId()) { - return &st::emojiPeople; + if (icon.setId == AllEmojiSectionSetId()) { + return &st().icons.people; } else if (const auto section = SetIdEmojiSection(icon.setId)) { return sectionIcon(*section, selected); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index 1a08f6853..f7a0afc1d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "media/clip/media_clip_reader.h" +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" +#include "media/clip/media_clip_reader.h" #include "mtproto/sender.h" #include "ui/dpr/dpr_image.h" #include "ui/round_rect.h" @@ -116,8 +117,8 @@ public: not_null session; Fn paused; not_null parent; - bool settingsButtonVisible = false; const style::EmojiPan *st = nullptr; + ComposeFeatures features; }; explicit StickersListFooter(Descriptor &&descriptor); @@ -130,7 +131,6 @@ public: uint64 activeSetId, Fn()> renderer, ValidateIconAnimations animations); - [[nodiscard]] bool hasOnlyFeaturedSets() const; void leaveToChildEvent(QEvent *e, QWidget *child) override; @@ -270,7 +270,7 @@ private: const not_null _session; const Fn _paused; - const bool _settingsButtonVisible = false; + const ComposeFeatures _features; static constexpr auto kVisibleIconsCount = 8; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index d4d299709..3675c0483 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "chat_helpers/stickers_list_widget.h" +#include "core/application.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" @@ -165,38 +166,55 @@ void StickersListWidget::Sticker::ensureMediaCreated() { StickersListWidget::StickersListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level, + PauseReason level, Mode mode) +: StickersListWidget(parent, { + .show = controller->uiShow(), + .mode = mode, + .paused = Window::PausedIn(controller, level), +}) { +} + +StickersListWidget::StickersListWidget( + QWidget *parent, + StickersListDescriptor &&descriptor) : Inner( parent, - st::defaultEmojiPan, - &controller->session(), - Window::PausedIn(controller, level)) -, _mode(mode) -, _controller(controller) + descriptor.st ? *descriptor.st : st::defaultEmojiPan, + descriptor.show, + descriptor.paused) +, _mode(descriptor.mode) +, _show(std::move(descriptor.show)) +, _features(descriptor.features) +, _overBg(st::roundRadiusLarge, st().overBg) , _api(&session().mtp()) , _localSetsManager(std::make_unique(&session())) , _section(Section::Stickers) -, _isMasks(mode == Mode::Masks) +, _isMasks(_mode == Mode::Masks) , _updateItemsTimer([=] { updateItems(); }) , _updateSetsTimer([=] { updateSets(); }) , _trendingAddBgOver( - ImageRoundRadius::Small, + ImageRoundRadius::Large, st::stickersTrendingAdd.textBgOver) -, _trendingAddBg(ImageRoundRadius::Small, st::stickersTrendingAdd.textBg) +, _trendingAddBg(ImageRoundRadius::Large, st::stickersTrendingAdd.textBg) +, _inactiveButtonBg( + ImageRoundRadius::Large, + st::stickersTrendingInstalled.textBg) , _groupCategoryAddBgOver( - ImageRoundRadius::Small, + ImageRoundRadius::Large, st::stickerGroupCategoryAdd.textBgOver) , _groupCategoryAddBg( - ImageRoundRadius::Small, + ImageRoundRadius::Large, st::stickerGroupCategoryAdd.textBg) , _pathGradient(std::make_unique( - st::windowBgRipple, - st::windowBgOver, + st().pathBg, + st().pathFg, [=] { update(); })) , _megagroupSetAbout(st::columnMinimalWidthThird - st::emojiScroll.width - st().headerLeft) -, _addText(tr::lng_stickers_featured_add(tr::now).toUpper()) +, _addText(tr::lng_stickers_featured_add(tr::now)) , _addWidth(st::stickersTrendingAdd.font->width(_addText)) +, _installedText(tr::lng_stickers_featured_installed(tr::now)) +, _installedWidth(st::stickersTrendingInstalled.font->width(_installedText)) , _settings(this, tr::lng_stickers_you_have(tr::now)) , _previewTimer([=] { showPreview(); }) , _premiumMark(std::make_unique(&session())) @@ -209,10 +227,15 @@ StickersListWidget::StickersListWidget( } _settings->addClickHandler([=] { - using Section = StickersBox::Section; - controller->show( - Box(controller, Section::Installed, _isMasks), - Ui::LayerOption::KeepOther); + if (const auto window = _show->resolveWindow( + WindowUsage::PremiumPromo)) { + // While media viewer can't show StickersBox. + using Section = StickersBox::Section; + window->show( + Box(_show, Section::Installed, _isMasks)); + Core::App().hideMediaView(); + Window::ActivateWindow(window); + } }); session().downloaderTaskFinished( @@ -275,7 +298,8 @@ object_ptr StickersListWidget::createFooter() { .session = &session(), .paused = footerPaused, .parent = this, - .settingsButtonVisible = true, + .st = &st(), + .features = _features, }); _footer = result; @@ -286,16 +310,15 @@ object_ptr StickersListWidget::createFooter() { _footer->openSettingsRequests( ) | rpl::start_with_next([=] { - const auto onlyFeatured = _footer->hasOnlyFeaturedSets(); - _controller->show(Box( - _controller, + const auto onlyFeatured = !_isMasks && _mySets.empty(); + _show->showBox(Box( + _show, (onlyFeatured ? StickersBox::Section::Featured : _isMasks ? StickersBox::Section::Masks : StickersBox::Section::Installed), - onlyFeatured ? false : _isMasks), - Ui::LayerOption::KeepOther); + onlyFeatured ? false : _isMasks)); }, _footer->lifetime()); return result; @@ -836,7 +859,7 @@ QRect StickersListWidget::stickerRect(int section, int sel) { void StickersListWidget::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); - p.fillRect(clip, st::emojiPanBg); + p.fillRect(clip, st().bg); paintStickers(p, clip); } @@ -878,26 +901,40 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { : loadedCount; auto widthForTitle = stickersRight() - (st().headerLeft - st().margin.left()); - if (featuredHasAddButton(info.section)) { - auto add = featuredAddRect(info); - auto selected = selectedButton ? (selectedButton->section == info.section) : false; - (selected ? _trendingAddBgOver : _trendingAddBg).paint(p, myrtlrect(add)); + { + const auto installedSet = !featuredHasAddButton(info.section); + const auto add = featuredAddRect(info, installedSet); + const auto selected = selectedButton + ? (selectedButton->section == info.section) + : false; + (installedSet + ? _inactiveButtonBg + : selected + ? _trendingAddBgOver + : _trendingAddBg).paint(p, myrtlrect(add)); if (set.ripple) { set.ripple->paint(p, add.x(), add.y(), width()); if (set.ripple->empty()) { set.ripple.reset(); } } - p.setFont(st::stickersTrendingAdd.font); - p.setPen(selected ? st::stickersTrendingAdd.textFgOver : st::stickersTrendingAdd.textFg); - p.drawTextLeft(add.x() - (st::stickersTrendingAdd.width / 2), add.y() + st::stickersTrendingAdd.textTop, width(), _addText, _addWidth); + const auto &text = installedSet ? _installedText : _addText; + const auto textWidth = installedSet + ? _installedWidth + : _addWidth; + const auto &st = installedSet + ? st::stickersTrendingInstalled + : st::stickersTrendingAdd; + p.setFont(st.font); + p.setPen(selected ? st.textFgOver : st.textFg); + p.drawTextLeft( + add.x() - (st.width / 2), + add.y() + st.textTop, + width(), + text, + textWidth); - widthForTitle -= add.width() - (st::stickersTrendingAdd.width / 2); - } else { - auto add = featuredAddRect(info); - int checkx = add.left() + (add.width() - st::stickersFeaturedInstalled.width()) / 2; - int checky = add.top() + (add.height() - st::stickersFeaturedInstalled.height()) / 2; - st::stickersFeaturedInstalled.paint(p, QPoint(checkx, checky), width()); + widthForTitle -= add.width() - (st.width / 2); } if (set.flags & SetFlag::Unread) { widthForTitle -= st::stickersFeaturedUnreadSize + st::stickersFeaturedUnreadSkip; @@ -910,12 +947,12 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::stickersTrendingHeaderFont); - p.setPen(st::stickersTrendingHeaderFg); + p.setPen(st().trendingHeaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingHeaderTop, width(), titleText, titleWidth); if (set.flags & SetFlag::Unread) { p.setPen(Qt::NoPen); - p.setBrush(st::stickersFeaturedUnreadBg); + p.setBrush(st().trendingUnreadFg); { PainterHighQualityEnabler hq(p); @@ -925,7 +962,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { auto statusText = (count > 0) ? tr::lng_stickers_count(tr::now, lt_count, count) : tr::lng_contacts_loading(tr::now); p.setFont(st::stickersTrendingSubheaderFont); - p.setPen(st::stickersTrendingSubheaderFg); + p.setPen(st().trendingSubheaderFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st::stickersTrendingSubheaderTop, width(), statusText); if (info.rowsTop >= clip.y() + clip.height()) { @@ -952,13 +989,14 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { if (hasRemoveButton(info.section)) { auto remove = removeButtonRect(info); auto selected = selectedButton ? (selectedButton->section == info.section) : false; + const auto &removeSt = st().removeSet; if (set.ripple) { - set.ripple->paint(p, remove.x() + st::stickerPanRemoveSet.rippleAreaPosition.x(), remove.y() + st::stickerPanRemoveSet.rippleAreaPosition.y(), width()); + set.ripple->paint(p, remove.x() + removeSt.rippleAreaPosition.x(), remove.y() + removeSt.rippleAreaPosition.y(), width()); if (set.ripple->empty()) { set.ripple.reset(); } } - const auto &icon = selected ? st::stickerPanRemoveSet.iconOver : st::stickerPanRemoveSet.icon; + const auto &icon = selected ? removeSt.iconOver : removeSt.icon; icon.paint( p, remove.x() + (remove.width() - icon.width()) / 2, @@ -972,7 +1010,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) { titleWidth = st::stickersTrendingHeaderFont->width(titleText); } p.setFont(st::emojiPanHeaderFont); - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); p.drawTextLeft(st().headerLeft - st().margin.left(), info.top + st().headerTop, width(), titleText, titleWidth); } if (clip.top() + clip.height() <= info.rowsTop) { @@ -1097,7 +1135,7 @@ int StickersListWidget::megagroupSetInfoLeft() const { } void StickersListWidget::paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected) { - p.setPen(st::emojiPanHeaderFg); + p.setPen(st().headerFg); auto infoLeft = megagroupSetInfoLeft(); _megagroupSetAbout.drawLeft(p, infoLeft, y, width() - infoLeft, width()); @@ -1334,7 +1372,7 @@ void StickersListWidget::paintSticker( if (selected) { auto tl = pos; if (rtl()) tl.setX(width() - tl.x() - _singleSize.width()); - Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners); + _overBg.paint(p, QRect(tl, _singleSize)); } media->checkStickerSmall(); @@ -1434,14 +1472,17 @@ bool StickersListWidget::featuredHasAddButton(int index) const { } QRect StickersListWidget::featuredAddRect(int index) const { - return featuredAddRect(sectionInfo(index)); + return featuredAddRect(sectionInfo(index), false); } -QRect StickersListWidget::featuredAddRect(const SectionInfo &info) const { - auto addw = _addWidth - st::stickersTrendingAdd.width; - auto addh = st::stickersTrendingAdd.height; - auto addx = stickersRight() - addw; - auto addy = info.top + st::stickersTrendingAddTop; +QRect StickersListWidget::featuredAddRect( + const SectionInfo &info, + bool installedSet) const { + const auto addw = (installedSet ? _installedWidth : _addWidth) + - st::stickersTrendingAdd.width; + const auto addh = st::stickersTrendingAdd.height; + const auto addx = stickersRight() - addw; + const auto addy = info.top + st::stickersTrendingAddTop; return QRect(addx, addy, addw, addh); } @@ -1472,8 +1513,9 @@ QRect StickersListWidget::removeButtonRect(int index) const { } QRect StickersListWidget::removeButtonRect(const SectionInfo &info) const { - auto buttonw = st::stickerPanRemoveSet.width; - auto buttonh = st::stickerPanRemoveSet.height; + const auto &removeSt = st().removeSet; + auto buttonw = removeSt.width; + auto buttonh = removeSt.height; auto buttonx = stickersRight() - buttonw; auto buttony = info.top + (st().header - buttonh) / 2; return QRect(buttonx, buttony, buttonw, buttonh); @@ -1516,7 +1558,7 @@ void StickersListWidget::setPressed(OverState newPressed) { } else if (std::get_if(&_pressed)) { if (!_megagroupSetButtonRipple) { auto maskSize = _megagroupSetButtonRect.size(); - auto mask = Ui::RippleAnimation::RoundRectMask(maskSize, st::roundRadiusSmall); + auto mask = Ui::RippleAnimation::RoundRectMask(maskSize, st::roundRadiusLarge); _megagroupSetButtonRipple = std::make_unique(st::stickerGroupCategoryAdd.ripple, std::move(mask), [this] { rtlupdate(megagroupSetButtonRectFinal()); }); @@ -1544,16 +1586,17 @@ std::unique_ptr StickersListWidget::createButtonRipple(int if (shownSets()[section].externalLayout) { auto maskSize = QSize(_addWidth - st::stickersTrendingAdd.width, st::stickersTrendingAdd.height); - auto mask = Ui::RippleAnimation::RoundRectMask(maskSize, st::roundRadiusSmall); + auto mask = Ui::RippleAnimation::RoundRectMask(maskSize, st::roundRadiusLarge); return std::make_unique( st::stickersTrendingAdd.ripple, std::move(mask), [this, section] { rtlupdate(featuredAddRect(section)); }); } - auto maskSize = QSize(st::stickerPanRemoveSet.rippleAreaSize, st::stickerPanRemoveSet.rippleAreaSize); + const auto &removeSt = st().removeSet; + auto maskSize = QSize(removeSt.rippleAreaSize, removeSt.rippleAreaSize); auto mask = Ui::RippleAnimation::EllipseMask(maskSize); return std::make_unique( - st::stickerPanRemoveSet.ripple, + removeSt.ripple, std::move(mask), [this, section] { rtlupdate(removeButtonRect(section)); }); } @@ -1564,12 +1607,16 @@ QPoint StickersListWidget::buttonRippleTopLeft(int section) const { if (shownSets()[section].externalLayout) { return myrtlrect(featuredAddRect(section)).topLeft(); } - return myrtlrect(removeButtonRect(section)).topLeft() + st::stickerPanRemoveSet.rippleAreaPosition; + return myrtlrect(removeButtonRect(section)).topLeft() + + st().removeSet.rippleAreaPosition; } void StickersListWidget::showStickerSetBox(not_null document) { if (document->sticker() && document->sticker()->set) { - checkHideWithBox(StickerSetBox::Show(_controller, document)); + checkHideWithBox(Box( + _show, + document->sticker()->set, + document->sticker()->setType)); } } @@ -1590,9 +1637,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( auto &set = sets[section]; Assert(index >= 0 && index < set.stickers.size()); - auto menu = base::make_unique_q( - this, - st::popupMenuWithIcons); + auto menu = base::make_unique_q(this, st().menu); const auto document = set.stickers[sticker->index].document; const auto send = [=](Api::SendOptions options) { @@ -1604,16 +1649,19 @@ base::unique_qptr StickersListWidget::fillContextMenu( : messageSentAnimationInfo(section, index, document), }); }; + const auto icons = &st().icons; SendMenu::FillSendMenu( menu, type, SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(this, type, send)); + SendMenu::DefaultScheduleCallback(this, type, send), + SendMenu::DefaultWhenOnlineCallback(send), + icons); - const auto window = _controller; + const auto show = _show; const auto toggleFavedSticker = [=] { Api::ToggleFavedSticker( - window, + show, document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0)); }; @@ -1623,11 +1671,13 @@ base::unique_qptr StickersListWidget::fillContextMenu( ? tr::lng_faved_stickers_remove : tr::lng_faved_stickers_add)(tr::now), toggleFavedSticker, - isFaved ? &st::menuIconUnfave : &st::menuIconFave); + isFaved ? &icons->menuUnfave : &icons->menuFave); - menu->addAction(tr::lng_context_pack_info(tr::now), [=] { - showStickerSetBox(document); - }, &st::menuIconStickers); + if (_features.openStickerSets) { + menu->addAction(tr::lng_context_pack_info(tr::now), [=] { + showStickerSetBox(document); + }, &icons->menuStickerSet); + } if (const auto id = set.id; id == Data::Stickers::RecentSetId) { menu->addAction(tr::lng_recent_stickers_remove(tr::now), [=] { @@ -1635,7 +1685,7 @@ base::unique_qptr StickersListWidget::fillContextMenu( document, Data::FileOriginStickerSet(id, 0), false); - }, &st::menuIconDelete); + }, &icons->menuRecentRemove); } return menu; } @@ -1693,7 +1743,8 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { return; } const auto document = set.stickers[sticker->index].document; - if (e->modifiers() & Qt::ControlModifier) { + if (_features.openStickerSets + && (e->modifiers() & Qt::ControlModifier)) { showStickerSetBox(document); } else { _chosen.fire({ @@ -1711,11 +1762,12 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { Assert(button->section >= 0 && button->section < sets.size()); if (sets[button->section].externalLayout) { _localSetsManager->install(sets[button->section].id); + update(); } else { removeSet(sets[button->section].id); } } else if (std::get_if(&pressed)) { - _controller->show(Box(_controller, _megagroupSet)); + _show->showBox(Box(_show, _megagroupSet)); } } } @@ -1773,9 +1825,9 @@ void StickersListWidget::removeFavedSticker(int section, int index) { clearSelection(); const auto &sticker = _mySets[section].stickers[index]; const auto document = sticker.document; - session().data().stickers().setFaved(_controller, document, false); + session().data().stickers().setFaved(_show, document, false); Api::ToggleFavedSticker( - _controller, + _show, document, Data::FileOriginStickerSet(Data::Stickers::FavedSetId, 0), false); @@ -1844,12 +1896,6 @@ void StickersListWidget::processPanelHideFinished() { if (_footer) { _footer->clearHeavyData(); } - // Preserve panel state through visibility toggles. - //// Reset to the recent stickers section. - //if (_section == Section::Featured && (!_footer || !_footer->hasOnlyFeaturedSets())) { - // setSection(Section::Stickers); - // validateSelectedIcon(ValidateIconAnimations::None); - //} } void StickersListWidget::setSection(Section section) { @@ -1979,9 +2025,6 @@ void StickersListWidget::refreshSettingsVisibility() { void StickersListWidget::refreshFooterIcons() { refreshIcons(ValidateIconAnimations::None); - if (_footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } void StickersListWidget::preloadImages() { @@ -2049,9 +2092,6 @@ void StickersListWidget::refreshRecent() { if (_section == Section::Stickers) { refreshRecentStickers(); } - if (_footer && _footer->hasOnlyFeaturedSets() && _section != Section::Featured) { - showStickerSet(Data::Stickers::FeaturedSetId); - } } auto StickersListWidget::collectRecentStickers() -> std::vector { @@ -2186,7 +2226,7 @@ void StickersListWidget::refreshFavedStickers() { } void StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) { - if (!_megagroupSet || _isMasks) { + if (!_features.megagroupSet || !_megagroupSet || _isMasks) { return; } auto canEdit = _megagroupSet->canEditStickers(); @@ -2337,12 +2377,14 @@ void StickersListWidget::updateSelected() { if (p.y() >= info.top && p.y() < info.rowsTop) { if (hasRemoveButton(section) && myrtlrect(removeButtonRect(info)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (featuredHasAddButton(section) && myrtlrect(featuredAddRect(info)).contains(p.x(), p.y())) { + } else if (featuredHasAddButton(section) && myrtlrect(featuredAddRect(info, false)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; - } else if (!(sets[section].flags & SetFlag::Special)) { + } else if (_features.openStickerSets + && !(sets[section].flags & SetFlag::Special)) { newSelected = OverSet{ section }; - } else if (sets[section].id == Data::Stickers::MegagroupSetId - && (_megagroupSet->canEditStickers() || !sets[section].stickers.empty())) { + } else if ((sets[section].id == Data::Stickers::MegagroupSetId) + && (_megagroupSet->canEditStickers() + || !sets[section].stickers.empty())) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom && sx >= 0) { @@ -2426,9 +2468,7 @@ void StickersListWidget::setSelected(OverState newSelected) { const auto &set = sets[sticker->section]; Assert(sticker->index >= 0 && sticker->index < set.stickers.size()); const auto document = set.stickers[sticker->index].document; - _controller->widget()->showMediaPreview( - document->stickerSetOrigin(), - document); + _show->showMediaPreview(document->stickerSetOrigin(), document); } } } @@ -2441,9 +2481,7 @@ void StickersListWidget::showPreview() { const auto &set = sets[sticker->section]; Assert(sticker->index >= 0 && sticker->index < set.stickers.size()); const auto document = set.stickers[sticker->index].document; - _controller->widget()->showMediaPreview( - document->stickerSetOrigin(), - document); + _show->showMediaPreview(document->stickerSetOrigin(), document); _previewShown = true; } } @@ -2542,7 +2580,7 @@ void StickersListWidget::showMegagroupSet(ChannelData *megagroup) { _megagroupSetAbout.setText( st::stickerGroupCategoryAbout, tr::lng_group_stickers_description(tr::now)); - _megagroupSetButtonText = tr::lng_group_stickers_add(tr::now).toUpper(); + _megagroupSetButtonText = tr::lng_group_stickers_add(tr::now); refreshMegagroupSetGeometry(); } _megagroupSetButtonRipple.reset(); @@ -2564,7 +2602,7 @@ void StickersListWidget::beforeHiding() { } void StickersListWidget::setupSearch() { - const auto session = &_controller->session(); + const auto session = &_show->session(); _search = MakeSearch(this, st(), [=](std::vector &&query) { auto set = base::flat_set(); auto text = ranges::accumulate(query, QString(), []( @@ -2579,9 +2617,7 @@ void StickersListWidget::setupSearch() { void StickersListWidget::displaySet(uint64 setId) { if (setId == Data::Stickers::MegagroupSetId) { if (_megagroupSet->canEditStickers()) { - checkHideWithBox(_controller->show( - Box(_controller, _megagroupSet), - Ui::LayerOption::KeepOther).data()); + checkHideWithBox(Box(_show, _megagroupSet)); return; } else if (_megagroupSet->mgInfo->stickerSet.id) { setId = _megagroupSet->mgInfo->stickerSet.id; @@ -2592,9 +2628,7 @@ void StickersListWidget::displaySet(uint64 setId) { const auto &sets = session().data().stickers().sets(); auto it = sets.find(setId); if (it != sets.cend()) { - checkHideWithBox(_controller->show( - Box(_controller, it->second.get()), - Ui::LayerOption::KeepOther).data()); + checkHideWithBox(Box(_show, it->second.get())); } } @@ -2605,10 +2639,7 @@ void StickersListWidget::removeMegagroupSet(bool locally) { refreshStickers(); return; } - const auto cancelled = [](Fn &&close) { - close(); - }; - checkHideWithBox(_controller->show(Ui::MakeConfirmBox({ + checkHideWithBox(Ui::MakeConfirmBox({ .text = tr::lng_stickers_remove_group_set(), .confirmed = crl::guard(this, [this, group = _megagroupSet]( Fn &&close) { @@ -2619,11 +2650,13 @@ void StickersListWidget::removeMegagroupSet(bool locally) { } close(); }), - .cancelled = cancelled, - }))); + .cancelled = [](Fn &&close) { close(); }, + .labelStyle = &st().boxLabel, + })); } void StickersListWidget::removeSet(uint64 setId) { + const auto &st = this->st().boxLabel; if (setId == Data::Stickers::MegagroupSetId) { const auto &sets = shownSets(); const auto i = ranges::find(sets, setId, &Set::id); @@ -2631,10 +2664,8 @@ void StickersListWidget::removeSet(uint64 setId) { const auto removeLocally = i->stickers.empty() || !_megagroupSet->canEditStickers(); removeMegagroupSet(removeLocally); - } else if (auto box = MakeConfirmRemoveSetBox(&session(), setId)) { - checkHideWithBox(_controller->show( - std::move(box), - Ui::LayerOption::KeepOther)); + } else if (auto box = MakeConfirmRemoveSetBox(&session(), st, setId)) { + checkHideWithBox(std::move(box)); } } @@ -2658,6 +2689,7 @@ StickersListWidget::~StickersListWidget() = default; object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId) { const auto &sets = session->data().stickers().sets(); const auto it = sets.find(setId); @@ -2724,6 +2756,7 @@ object_ptr MakeConfirmRemoveSetBox( } }, .confirmText = tr::lng_stickers_remove_pack_confirm(), + .labelStyle = &st, }); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h index 1c0d8445c..2e1ef9c1c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "chat_helpers/compose/compose_features.h" #include "chat_helpers/tabbed_selector.h" #include "data/stickers/data_stickers.h" #include "ui/round_rect.h" @@ -50,6 +51,7 @@ enum class Notification; namespace style { struct EmojiPan; +struct FlatLabel; } // namespace style namespace ChatHelpers { @@ -65,6 +67,14 @@ enum class StickersListMode { UserpicBuilder, }; +struct StickersListDescriptor { + std::shared_ptr show; + StickersListMode mode = StickersListMode::Full; + Fn paused; + const style::EmojiPan *st = nullptr; + ComposeFeatures features; +}; + class StickersListWidget final : public TabbedSelector::Inner { public: using Mode = StickersListMode; @@ -72,8 +82,11 @@ public: StickersListWidget( QWidget *parent, not_null controller, - Window::GifPauseReason level, + PauseReason level, Mode mode = Mode::Full); + StickersListWidget( + QWidget *parent, + StickersListDescriptor &&descriptor); rpl::producer chosen() const; rpl::producer<> scrollUpdated() const; @@ -289,7 +302,9 @@ private: [[nodiscard]] int stickersRight() const; [[nodiscard]] bool featuredHasAddButton(int index) const; [[nodiscard]] QRect featuredAddRect(int index) const; - [[nodiscard]] QRect featuredAddRect(const SectionInfo &info) const; + [[nodiscard]] QRect featuredAddRect( + const SectionInfo &info, + bool installedSet) const; [[nodiscard]] bool hasRemoveButton(int index) const; [[nodiscard]] QRect removeButtonRect(int index) const; [[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const; @@ -340,8 +355,9 @@ private: not_null document); const Mode _mode; - - not_null _controller; + const std::shared_ptr _show; + const ComposeFeatures _features; + Ui::RoundRect _overBg; std::unique_ptr _search; MTP::Sender _api; std::unique_ptr _localSetsManager; @@ -378,7 +394,7 @@ private: OverState _pressed; QPoint _lastMousePosition; - Ui::RoundRect _trendingAddBgOver, _trendingAddBg; + Ui::RoundRect _trendingAddBgOver, _trendingAddBg, _inactiveButtonBg; Ui::RoundRect _groupCategoryAddBgOver, _groupCategoryAddBg; const std::unique_ptr _pathGradient; @@ -391,6 +407,8 @@ private: QString _addText; int _addWidth; + QString _installedText; + int _installedWidth; object_ptr _settings; @@ -414,6 +432,7 @@ private: [[nodiscard]] object_ptr MakeConfirmRemoveSetBox( not_null session, + const style::FlatLabel &st, uint64 setId); } // namespace ChatHelpers diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index 606145eec..e8ec060fb 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -37,25 +37,31 @@ TabbedPanel::TabbedPanel( QWidget *parent, not_null controller, not_null selector) -: TabbedPanel(parent, controller, { nullptr }, selector) { +: TabbedPanel(parent, { + .regularWindow = controller, + .nonOwnedSelector = selector, +}) { } TabbedPanel::TabbedPanel( QWidget *parent, not_null controller, object_ptr selector) -: TabbedPanel(parent, controller, std::move(selector), nullptr) { +: TabbedPanel(parent, { + .regularWindow = controller, + .ownedSelector = std::move(selector), +}) { } TabbedPanel::TabbedPanel( QWidget *parent, - not_null controller, - object_ptr ownedSelector, - TabbedSelector *nonOwnedSelector) + TabbedPanelDescriptor &&descriptor) : RpWidget(parent) -, _controller(controller) -, _ownedSelector(std::move(ownedSelector)) -, _selector(nonOwnedSelector ? nonOwnedSelector : _ownedSelector.data()) +, _regularWindow(descriptor.regularWindow) +, _ownedSelector(std::move(descriptor.ownedSelector)) +, _selector(descriptor.nonOwnedSelector + ? descriptor.nonOwnedSelector + : _ownedSelector.data()) , _heightRatio(st::emojiPanHeightRatio) , _minContentHeight(st::emojiPanMinHeight) , _maxContentHeight(st::emojiPanMaxHeight) { @@ -64,17 +70,25 @@ TabbedPanel::TabbedPanel( _selector->setParent(this); _selector->setRoundRadius(st::emojiPanRadius); _selector->setAfterShownCallback([=](SelectorTab tab) { - _controller->enableGifPauseReason(_selector->level()); + if (_regularWindow) { + _regularWindow->enableGifPauseReason(_selector->level()); + } + _pauseAnimations.fire(true); }); _selector->setBeforeHidingCallback([=](SelectorTab tab) { - _controller->disableGifPauseReason(_selector->level()); + if (_regularWindow) { + _regularWindow->disableGifPauseReason(_selector->level()); + } + _pauseAnimations.fire(false); }); _selector->showRequests( ) | rpl::start_with_next([=] { showFromSelector(); }, lifetime()); - resize(QRect(0, 0, st::emojiPanWidth, st::emojiPanMaxHeight).marginsAdded(innerPadding()).size()); + resize( + QRect(0, 0, st::emojiPanWidth, st::emojiPanMaxHeight).marginsAdded( + innerPadding()).size()); _contentMaxHeight = st::emojiPanMaxHeight; _contentHeight = _contentMaxHeight; @@ -122,6 +136,10 @@ not_null TabbedPanel::selector() const { return _selector; } +rpl::producer TabbedPanel::pauseAnimations() const { + return _pauseAnimations.events(); +} + bool TabbedPanel::isSelectorStolen() const { return (_selector->parent() != this); } @@ -222,7 +240,7 @@ void TabbedPanel::paintEvent(QPaintEvent *e) { hideFinished(); } else { if (!_cache.isNull()) _cache = QPixmap(); - Ui::Shadow::paint(p, innerRect(), width(), st::emojiPanAnimation.shadow); + Ui::Shadow::paint(p, innerRect(), width(), _selector->st().showAnimation.shadow); } } @@ -344,7 +362,11 @@ void TabbedPanel::startShowAnimation() { if (!_a_show.animating()) { auto image = grabForAnimation(); - _showAnimation = std::make_unique(st::emojiPanAnimation, _dropDown ? Ui::PanelAnimation::Origin::TopRight : Ui::PanelAnimation::Origin::BottomRight); + _showAnimation = std::make_unique( + _selector->st().showAnimation, + (_dropDown + ? Ui::PanelAnimation::Origin::TopRight + : Ui::PanelAnimation::Origin::BottomRight)); auto inner = rect().marginsRemoved(st::emojiPanMargins); _showAnimation->setFinalImage(std::move(image), QRect(inner.topLeft() * cIntRetinaFactor(), inner.size() * cIntRetinaFactor())); _showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius)); @@ -478,8 +500,8 @@ bool TabbedPanel::overlaps(const QRect &globalRect) const { TabbedPanel::~TabbedPanel() { hideFast(); - if (!_ownedSelector) { - _controller->takeTabbedSelectorOwnershipFrom(this); + if (!_ownedSelector && _regularWindow) { + _regularWindow->takeTabbedSelectorOwnershipFrom(this); } } diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h index c592c4a48..fcedb5efc 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.h @@ -26,6 +26,12 @@ class TabbedSelector; extern const char kOptionTabbedPanelShowOnClick[]; +struct TabbedPanelDescriptor { + Window::SessionController *regularWindow = nullptr; + object_ptr ownedSelector = { nullptr }; + TabbedSelector *nonOwnedSelector = nullptr; +}; + class TabbedPanel : public Ui::RpWidget { public: TabbedPanel( @@ -36,9 +42,11 @@ public: QWidget *parent, not_null controller, object_ptr selector); + TabbedPanel(QWidget *parent, TabbedPanelDescriptor &&descriptor); [[nodiscard]] bool isSelectorStolen() const; [[nodiscard]] not_null selector() const; + [[nodiscard]] rpl::producer pauseAnimations() const; void moveBottomRight(int bottom, int right); void moveTopRight(int top, int right); @@ -71,12 +79,6 @@ protected: bool eventFilter(QObject *obj, QEvent *e) override; private: - TabbedPanel( - QWidget *parent, - not_null controller, - object_ptr ownedSelector, - TabbedSelector *nonOwnedSelector); - void hideByTimerOrLeave(); void moveHorizontally(); void showFromSelector(); @@ -99,9 +101,10 @@ private: bool preventAutoHide() const; void updateContentHeight(); - const not_null _controller; + Window::SessionController * const _regularWindow = nullptr; const object_ptr _ownedSelector = { nullptr }; const not_null _selector; + rpl::event_stream _pauseAnimations; int _contentMaxHeight = 0; int _contentHeight = 0; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index d34884edf..43b08c4a4 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -50,7 +50,7 @@ public: void setFinalImages(Direction direction, QImage &&left, QImage &&right, QRect inner, bool wasSectionIcons); void start(); - void paintFrame(QPainter &p, float64 dt, float64 opacity); + void paintFrame(QPainter &p, const style::EmojiPan &st, float64 dt, float64 opacity); private: Direction _direction = Direction::LeftToRight; @@ -131,7 +131,11 @@ void TabbedSelector::SlideAnimation::start() { _frameIntsPerLineAdd = (_width - _innerWidth) + _frameIntsPerLineAdded; } -void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 opacity) { +void TabbedSelector::SlideAnimation::paintFrame( + QPainter &p, + const style::EmojiPan &st, + float64 dt, + float64 opacity) { Expects(started()); Expects(dt >= 0.); @@ -168,8 +172,8 @@ void TabbedSelector::SlideAnimation::paintFrame(QPainter &p, float64 dt, float64 { auto p = QPainter(&_frame); p.setOpacity(opacity); - p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st::emojiPanBg); - p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st::emojiPanCategories : st::emojiPanBg); + p.fillRect(_painterInnerLeft, _painterInnerTop, _painterInnerWidth, _painterCategoriesTop - _painterInnerTop, st.bg); + p.fillRect(_painterInnerLeft, _painterCategoriesTop, _painterInnerWidth, _painterInnerBottom - _painterCategoriesTop, _wasSectionIcons ? st.categoriesBg : st.bg); p.setCompositionMode(QPainter::CompositionMode_SourceOver); if (leftTo > _innerLeft) { p.setOpacity(opacity * leftAlpha); @@ -322,14 +326,28 @@ std::unique_ptr MakeSearch( TabbedSelector::TabbedSelector( QWidget *parent, - not_null controller, - Window::GifPauseReason level, + std::shared_ptr show, + PauseReason level, Mode mode) +: TabbedSelector(parent, { + .show = std::move(show), + .st = (mode == Mode::EmojiStatus + ? st::statusEmojiPan + : st::defaultEmojiPan), + .level = level, + .mode = mode, +}) { +} + +TabbedSelector::TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor) : RpWidget(parent) -, _st((mode == Mode::EmojiStatus) ? st::statusEmojiPan : st::defaultEmojiPan) -, _controller(controller) -, _level(level) -, _mode(mode) +, _st(descriptor.st) +, _features(descriptor.features) +, _show(std::move(descriptor.show)) +, _level(descriptor.level) +, _mode(descriptor.mode) , _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg)) , _categoriesRounding( Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.categoriesBg)) @@ -445,10 +463,10 @@ TabbedSelector::TabbedSelector( ) | rpl::start_with_next([=] { _panelRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanBg); + _st.bg); _categoriesRounding = Ui::PrepareCornerPixmaps( st::emojiPanRadius, - st::emojiPanCategories); + _st.categoriesBg); }, lifetime()); if (hasEmojiTab()) { @@ -469,39 +487,67 @@ TabbedSelector::TabbedSelector( TabbedSelector::~TabbedSelector() = default; -Main::Session &TabbedSelector::session() const { - return _controller->session(); +const style::EmojiPan &TabbedSelector::st() const { + return _st; } -Window::GifPauseReason TabbedSelector::level() const { +Main::Session &TabbedSelector::session() const { + return _show->session(); +} + +PauseReason TabbedSelector::level() const { return _level; } TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { auto createWidget = [&]() -> object_ptr { + const auto paused = [show = _show, level = _level] { + return show->paused(level); + }; switch (type) { - case SelectorTab::Emoji: + case SelectorTab::Emoji: { using EmojiMode = EmojiListWidget::Mode; using Descriptor = EmojiListDescriptor; return object_ptr(this, Descriptor{ - .session = &_controller->session(), + .show = _show, .mode = (_mode == Mode::EmojiStatus ? EmojiMode::EmojiStatus : EmojiMode::Full), - .controller = _controller, - .paused = Window::PausedIn(_controller, _level), + .paused = paused, + .st = &_st, + .features = _features, + }); + } + case SelectorTab::Stickers: { + using StickersMode = StickersListWidget::Mode; + using Descriptor = StickersListDescriptor; + return object_ptr(this, Descriptor{ + .show = _show, + .mode = StickersMode::Full, + .paused = paused, + .st = &_st, + .features = _features, + }); + } + case SelectorTab::Gifs: { + using Descriptor = GifsListDescriptor; + return object_ptr(this, Descriptor{ + .show = _show, + .paused = paused, .st = &_st, }); - case SelectorTab::Stickers: - return object_ptr(this, _controller, _level); - case SelectorTab::Gifs: - return object_ptr(this, _controller, _level); - case SelectorTab::Masks: - return object_ptr( - this, - _controller, - _level, - StickersListWidget::Mode::Masks); + } + case SelectorTab::Masks: { + using StickersMode = StickersListWidget::Mode; + using Descriptor = StickersListDescriptor; + return object_ptr(this, Descriptor{ + .show = _show, + .mode = StickersMode::Masks, + .paused = paused, + .st = &_st, + .features = _features, + }); + } } Unexpected("Type in TabbedSelector::createTab."); }; @@ -683,10 +729,10 @@ void TabbedSelector::paintSlideFrame(QPainter &p) { if (_roundRadius > 0) { paintBgRoundedPart(p); } else if (_tabsSlider) { - p.fillRect(0, 0, width(), _tabsSlider->height(), st::emojiPanBg); + p.fillRect(0, 0, width(), _tabsSlider->height(), _st.bg); } auto slideDt = _a_slide.value(1.); - _slideAnimation->paintFrame(p, slideDt, 1.); + _slideAnimation->paintFrame(p, _st, slideDt, 1.); } void TabbedSelector::paintBgRoundedPart(QPainter &p) { @@ -695,7 +741,7 @@ void TabbedSelector::paintBgRoundedPart(QPainter &p) { : _tabsSlider ? QRect(0, 0, width(), _tabsSlider->height()) : QRect(0, 0, width(), _roundRadius); - Ui::FillRoundRect(p, fill, st::emojiPanBg, { + Ui::FillRoundRect(p, fill, _st.bg, { .p = { _dropDown ? QPixmap() : _panelRounding.p[0], _dropDown ? QPixmap() : _panelRounding.p[1], @@ -744,10 +790,10 @@ void TabbedSelector::paintContent(QPainter &p) { sidesTop, st::emojiScroll.width, sidesHeight), - st::emojiPanBg); + _st.bg); p.fillRect( myrtlrect(0, sidesTop, st::emojiPanRadius, sidesHeight), - st::emojiPanBg); + _st.bg); } } @@ -1009,7 +1055,7 @@ void TabbedSelector::setAllowEmojiWithoutPremium(bool allow) { } void TabbedSelector::createTabsSlider() { - _tabsSlider.create(this, st::emojiTabs); + _tabsSlider.create(this, _st.tabs); fillTabsSliderSections(); @@ -1237,23 +1283,24 @@ not_null TabbedSelector::currentTab() const { TabbedSelector::Inner::Inner( QWidget *parent, - not_null controller, - Window::GifPauseReason level) + std::shared_ptr show, + PauseReason level) : Inner( parent, st::defaultEmojiPan, - &controller->session(), - Window::PausedIn(controller, level)) { + show, + [show, level] { return show->paused(level); }) { } TabbedSelector::Inner::Inner( QWidget *parent, const style::EmojiPan &st, - not_null session, + std::shared_ptr show, Fn paused) : RpWidget(parent) , _st(st) -, _session(session) +, _show(std::move(show)) +, _session(&_show->session()) , _paused(paused) { } @@ -1273,12 +1320,15 @@ void TabbedSelector::Inner::disableScroll(bool disabled) { _disableScrollRequests.fire_copy(disabled); } -void TabbedSelector::Inner::checkHideWithBox(QPointer box) { - if (!box) { +void TabbedSelector::Inner::checkHideWithBox( + object_ptr box) { + const auto raw = QPointer(box.data()); + _show->showBox(std::move(box)); + if (!raw) { return; } _preventHideWithBox = true; - connect(box, &QObject::destroyed, this, [=] { + connect(raw, &QObject::destroyed, this, [=] { _preventHideWithBox = false; _checkForHide.fire({}); }); @@ -1299,7 +1349,7 @@ void TabbedSelector::Inner::paintEmptySearchResults( iconTop + icon.height() - st::normalFont->height, height() - 2 * st::normalFont->height); p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); + p.setPen(_st.tabs.labelFg); p.drawTextLeft( (width() - textWidth) / 2, textTop, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index 74036ab30..d41aebb80 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "api/api_common.h" +#include "chat_helpers/compose/compose_features.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/effects/message_sending_animation_common.h" @@ -34,11 +35,6 @@ class BoxContent; class TabbedSearch; } // namespace Ui -namespace Window { -class SessionController; -enum class GifPauseReason; -} // namespace Window - namespace SendMenu { enum class Type; } // namespace SendMenu @@ -49,9 +45,11 @@ struct EmojiPan; namespace ChatHelpers { +class Show; class EmojiListWidget; class StickersListWidget; class GifsListWidget; +enum class PauseReason; enum class SelectorTab { Emoji, @@ -78,6 +76,21 @@ struct EmojiChosen { using InlineChosen = InlineBots::ResultSelected; +enum class TabbedSelectorMode { + Full, + EmojiOnly, + MediaEditor, + EmojiStatus, +}; + +struct TabbedSelectorDescriptor { + std::shared_ptr show; + const style::EmojiPan &st; + PauseReason level = {}; + TabbedSelectorMode mode = TabbedSelectorMode::Full; + ComposeFeatures features; +}; + [[nodiscard]] std::unique_ptr MakeSearch( not_null parent, const style::EmojiPan &st, @@ -89,12 +102,7 @@ using InlineChosen = InlineBots::ResultSelected; class TabbedSelector : public Ui::RpWidget { public: static constexpr auto kPickCustomTimeId = -1; - enum class Mode { - Full, - EmojiOnly, - MediaEditor, - EmojiStatus, - }; + using Mode = TabbedSelectorMode; enum class Action { Update, Cancel, @@ -102,31 +110,36 @@ public: TabbedSelector( QWidget *parent, - not_null controller, - Window::GifPauseReason level, + std::shared_ptr show, + PauseReason level, Mode mode = Mode::Full); + TabbedSelector( + QWidget *parent, + TabbedSelectorDescriptor &&descriptor); ~TabbedSelector(); - Main::Session &session() const; - Window::GifPauseReason level() const; + [[nodiscard]] const style::EmojiPan &st() const; + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] PauseReason level() const; - rpl::producer emojiChosen() const; - rpl::producer customEmojiChosen() const; - rpl::producer fileChosen() const; - rpl::producer photoChosen() const; - rpl::producer inlineResultChosen() const; + [[nodiscard]] rpl::producer emojiChosen() const; + [[nodiscard]] rpl::producer customEmojiChosen() const; + [[nodiscard]] rpl::producer fileChosen() const; + [[nodiscard]] rpl::producer photoChosen() const; + [[nodiscard]] rpl::producer inlineResultChosen() const; - rpl::producer<> cancelled() const; - rpl::producer<> checkForHide() const; - rpl::producer<> slideFinished() const; - rpl::producer<> contextMenuRequested() const; - rpl::producer choosingStickerUpdated() const; + [[nodiscard]] rpl::producer<> cancelled() const; + [[nodiscard]] rpl::producer<> checkForHide() const; + [[nodiscard]] rpl::producer<> slideFinished() const; + [[nodiscard]] rpl::producer<> contextMenuRequested() const; + [[nodiscard]] rpl::producer choosingStickerUpdated() const; void setAllowEmojiWithoutPremium(bool allow); void setRoundRadius(int radius); void refreshStickers(); void setCurrentPeer(PeerData *peer); - void provideRecentEmoji(const std::vector &customRecentList); + void provideRecentEmoji( + const std::vector &customRecentList); void hideFinished(); void showStarted(); @@ -256,8 +269,9 @@ private: not_null masks() const; const style::EmojiPan &_st; - const not_null _controller; - const Window::GifPauseReason _level = {}; + const ComposeFeatures _features; + const std::shared_ptr _show; + const PauseReason _level = {}; Mode _mode = Mode::Full; int _roundRadius = 0; @@ -299,12 +313,12 @@ class TabbedSelector::Inner : public Ui::RpWidget { public: Inner( QWidget *parent, - not_null controller, - Window::GifPauseReason level); + std::shared_ptr show, + PauseReason level); Inner( QWidget *parent, const style::EmojiPan &st, - not_null session, + std::shared_ptr show, Fn paused); [[nodiscard]] Main::Session &session() const { @@ -374,7 +388,7 @@ protected: void scrollTo(int y); void disableScroll(bool disabled); - void checkHideWithBox(QPointer box); + void checkHideWithBox(object_ptr box); void paintEmptySearchResults( Painter &p, @@ -383,6 +397,7 @@ protected: private: const style::EmojiPan &_st; + const std::shared_ptr _show; const not_null _session; const Fn _paused; diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 4c1bc7233..104d887c2 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_download_manager.h" @@ -100,6 +101,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include namespace Core { namespace { @@ -107,6 +109,7 @@ namespace { constexpr auto kQuitPreventTimeoutMs = crl::time(1500); constexpr auto kAutoLockTimeoutLateMs = crl::time(3000); constexpr auto kClearEmojiImageSourceTimeout = 10 * crl::time(1000); +constexpr auto kFileOpenTimeoutMs = crl::time(1000); LaunchState GlobalLaunchState/* = LaunchState::Running*/; @@ -120,7 +123,7 @@ void SetCrashAnnotationsGL() { case Ui::GL::ANGLE::D3D11: return "Direct3D 11"; case Ui::GL::ANGLE::D3D9: return "Direct3D 9"; case Ui::GL::ANGLE::D3D11on12: return "D3D11on12"; - case Ui::GL::ANGLE::OpenGL: return "OpenGL"; + //case Ui::GL::ANGLE::OpenGL: return "OpenGL"; } Unexpected("Ui::GL::CurrentANGLE value in SetupANGLE."); }()); @@ -141,9 +144,8 @@ struct Application::Private { Settings settings; }; -Application::Application(not_null launcher) +Application::Application() : QObject() -, _launcher(launcher) , _private(std::make_unique()) , _platformIntegration(Platform::Integration::Create()) , _batterySaving(std::make_unique()) @@ -161,7 +163,8 @@ Application::Application(not_null launcher) , _langCloudManager(std::make_unique(langpack())) , _emojiKeywords(std::make_unique()) , _tray(std::make_unique()) -, _autoLockTimer([=] { checkAutoLock(); }) { +, _autoLockTimer([=] { checkAutoLock(); }) +, _fileOpenTimer([=] { checkFileOpen(); }) { Ui::Integration::Set(&_private->uiIntegration); _platformIntegration->init(); @@ -656,15 +659,29 @@ bool Application::eventFilter(QObject *object, QEvent *e) { case QEvent::FileOpen: { if (object == QCoreApplication::instance()) { const auto event = static_cast(e); - const auto url = QString::fromUtf8( - event->url().toEncoded().trimmed()); - if (url.startsWith(u"tg://"_q, Qt::CaseInsensitive)) { + if (const auto file = event->file(); !file.isEmpty()) { + _filesToOpen.append(file); + _fileOpenTimer.callOnce(kFileOpenTimeoutMs); + } else if (event->url().scheme() == u"tg"_q) { + const auto url = QString::fromUtf8( + event->url().toEncoded().trimmed()); cSetStartUrl(url.mid(0, 8192)); checkStartUrl(); + if (_lastActivePrimaryWindow + && StartUrlRequiresActivate(url)) { + _lastActivePrimaryWindow->activate(); + } + } else if (event->url().scheme() == u"interpret"_q) { + _filesToOpen.append(event->url().toString()); + _fileOpenTimer.callOnce(kFileOpenTimeoutMs); } - if (_lastActivePrimaryWindow && StartUrlRequiresActivate(url)) { - _lastActivePrimaryWindow->activate(); - } + } + } break; + + case QEvent::ThemeChange: { + if (Platform::IsLinux() && object == QGuiApplication::allWindows().first()) { + Core::App().refreshApplicationIcon(); + Core::App().tray().updateIconCounters(); } } break; } @@ -917,14 +934,22 @@ rpl::producer Application::appDeactivatedValue() const { }); } +void Application::materializeLocalDrafts() { + _materializeLocalDraftsRequests.fire({}); +} + +rpl::producer<> Application::materializeLocalDraftsRequests() const { + return _materializeLocalDraftsRequests.events(); +} + void Application::switchDebugMode() { if (Logs::DebugEnabled()) { Logs::SetDebugEnabled(false); - _launcher->writeDebugModeSetting(); + Launcher::Instance().writeDebugModeSetting(); Restart(); } else { Logs::SetDebugEnabled(true); - _launcher->writeDebugModeSetting(); + Launcher::Instance().writeDebugModeSetting(); DEBUG_LOG(("Debug logs started.")); if (_lastActivePrimaryWindow) { _lastActivePrimaryWindow->hideLayer(); @@ -932,10 +957,6 @@ void Application::switchDebugMode() { } } -void Application::writeInstallBetaVersionsSetting() { - _launcher->writeInstallBetaVersionsSetting(); -} - Main::Account &Application::activeAccount() const { return _domain->active(); } @@ -1025,6 +1046,12 @@ bool Application::canApplyLangPackWithoutRestart() const { return true; } +void Application::checkFileOpen() { + cSetSendPaths(_filesToOpen); + _filesToOpen.clear(); + checkSendPaths(); +} + void Application::checkSendPaths() { if (!cSendPaths().isEmpty() && _lastActivePrimaryWindow @@ -1130,7 +1157,9 @@ void Application::lockByPasscode() { enumerateWindows([&](not_null w) { w->setupPasscodeLock(); }); - hideMediaView(); + if (_mediaView) { + _mediaView->close(); + } } void Application::maybeLockByPasscode() { @@ -1573,6 +1602,10 @@ QPoint Application::getPointForCallPanelCenter() const { return QGuiApplication::primaryScreen()->geometry().center(); } +bool Application::isSharingScreen() const { + return _calls->isSharingScreen(); +} + // macOS Qt bug workaround, sometimes no leaveEvent() gets to the nested widgets. void Application::registerLeaveSubscription(not_null widget) { #ifdef Q_OS_MAC @@ -1653,6 +1686,9 @@ bool Application::readyToQuit() { if (session->api().isQuitPrevent()) { prevented = true; } + if (session->data().stories().isQuitPrevent()) { + prevented = true; + } } } } @@ -1730,10 +1766,8 @@ void Application::startShortcuts() { void Application::RegisterUrlScheme() { base::Platform::RegisterUrlScheme(base::Platform::UrlSchemeDescriptor{ - .executable = (!Platform::IsLinux() || !Core::UpdaterDisabled()) - ? (cExeDir() + cExeName()) - : cExeName(), - .arguments = Sandbox::Instance().customWorkingDir() + .executable = Platform::ExecutablePathForShortcuts(), + .arguments = Launcher::Instance().customWorkingDir() ? u"-workdir \"%1\""_q.arg(cWorkingDir()) : QString(), .protocol = u"tg"_q, diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index 0488dec3c..f9059da8d 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -103,7 +103,6 @@ class Instance; namespace Core { -class Launcher; struct LocalUrlHandler; class Settings; class Tray; @@ -126,16 +125,13 @@ public: MTP::ProxyData now; }; - Application(not_null launcher); + Application(); Application(const Application &other) = delete; Application &operator=(const Application &other) = delete; ~Application(); void run(); - [[nodiscard]] Launcher &launcher() const { - return *_launcher; - } [[nodiscard]] Platform::Integration &platformIntegration() const { return *_platformIntegration; } @@ -195,6 +191,7 @@ public: bool hideMediaView(); [[nodiscard]] QPoint getPointForCallPanelCenter() const; + [[nodiscard]] bool isSharingScreen() const; void startSettingsAndBackground(); [[nodiscard]] Settings &settings(); @@ -262,6 +259,7 @@ public: // Internal links. void checkStartUrl(); void checkSendPaths(); + void checkFileOpen(); bool openLocalUrl(const QString &url, QVariant context); bool openInternalUrl(const QString &url, QVariant context); [[nodiscard]] QString changelogLink() const; @@ -313,8 +311,10 @@ public: void handleAppDeactivated(); [[nodiscard]] rpl::producer appDeactivatedValue() const; + void materializeLocalDrafts(); + [[nodiscard]] rpl::producer<> materializeLocalDraftsRequests() const; + void switchDebugMode(); - void writeInstallBetaVersionsSetting(); void preventOrInvoke(Fn &&callback); @@ -376,7 +376,6 @@ private: }; InstanceSetter _setter = { this }; - const not_null _launcher; rpl::event_stream _proxyChanges; // Some fields are just moved from the declaration. @@ -433,6 +432,9 @@ private: crl::time _shouldLockAt = 0; base::Timer _autoLockTimer; + QStringList _filesToOpen; + base::Timer _fileOpenTimer; + std::optional _saveSettingsTimer; struct LeaveFilter { @@ -443,6 +445,8 @@ private: rpl::event_stream _openInMediaViewRequests; + rpl::event_stream<> _materializeLocalDraftsRequests; + rpl::lifetime _lifetime; crl::time _lastNonIdleTime = 0; diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index a3eeafb31..957b83f18 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -125,7 +125,15 @@ std::map BetaLogs() { "- Fix several possible crashes.\n" "- Deprecate macOS 10.12, Ubuntu 18.04 and CentOS 7 in July.\n" - } + }, + { + 4008011, + "- Fix initial video playback speed.\n" + + "- Use native window resize on Windows 11.\n" + + "- Fix memory leak in Direct3D 11 media viewer on Windows.\n" + }, }; }; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 39ba3369e..e3bbba7d1 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -146,7 +146,8 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) { if (my.show) { my.show->showBox(std::move(box)); } else if (use) { - use->show(std::move(box), Ui::LayerOption::KeepOther); + use->show(std::move(box)); + use->activate(); } } else { open(); @@ -335,16 +336,13 @@ void MonospaceClickHandler::onClick(ClickContext context) const { const auto hasCopyRestriction = item && (!item->history()->peer->allowsForwarding() || item->forbidsForward()); - const auto toastParent = Window::Show(controller).toastParent(); if (hasCopyRestriction) { - Ui::Toast::Show( - toastParent, - item->history()->peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now)); + controller->showToast(item->history()->peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return; } - Ui::Toast::Show(toastParent, tr::lng_text_copied(tr::now)); + controller->showToast(tr::lng_text_copied(tr::now)); } TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed())); } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 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 d1e9bfb5a..4a2cb3023 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -200,7 +200,11 @@ QByteArray Settings::serialize() const { + sizeof(qint32) * 3 + Serialize::bytearraySize(mediaViewPosition) + sizeof(qint32) - + sizeof(quint64); + + sizeof(quint64) + + sizeof(qint32) * 2; + for (const auto &id : _recentEmojiSkip) { + size += Serialize::stringSize(id); + } auto result = QByteArray(); result.reserve(size); @@ -334,7 +338,12 @@ QByteArray Settings::serialize() const { << qint32(_windowTitleContent.current().hideTotalUnread ? 1 : 0) << mediaViewPosition << qint32(_ignoreBatterySaving.current() ? 1 : 0) - << quint64(_macRoundIconDigest.value_or(0)); + << quint64(_macRoundIconDigest.value_or(0)) + << qint32(_storiesClickTooltipHidden.current() ? 1 : 0) + << qint32(_recentEmojiSkip.size()); + for (const auto &id : _recentEmojiSkip) { + stream << id; + } } return result; } @@ -440,6 +449,8 @@ void Settings::addFromSerialized(const QByteArray &serialized) { QByteArray mediaViewPosition; 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()) { @@ -674,6 +685,22 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> macRoundIconDigest; } + 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()")); @@ -865,6 +892,8 @@ 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 { @@ -957,7 +986,8 @@ rpl::producer Settings::thirdColumnWidthChanges() const { } const std::vector &Settings::recentEmoji() const { - if (_recentEmoji.empty()) { + if (!_recentEmojiResolved) { + _recentEmojiResolved = true; resolveRecentEmoji(); } return _recentEmoji; @@ -998,6 +1028,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 }); } @@ -1007,6 +1039,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) { @@ -1058,6 +1093,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()) { @@ -1069,11 +1134,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; @@ -1155,6 +1249,7 @@ void Settings::resetOnLastLogout() { _tabbedReplacedWithInfo = false; // per-window _systemDarkModeEnabled = false; _hiddenGroupCallTooltips = 0; + _storiesClickTooltipHidden = 0; _recentEmojiPreload.clear(); _recentEmoji.clear(); diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 111bf6c80..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 { @@ -802,6 +807,15 @@ public: [[nodiscard]] std::optional macRoundIconDigest() const { return _macRoundIconDigest; } + [[nodiscard]] bool storiesClickTooltipHidden() const { + return _storiesClickTooltipHidden.current(); + } + [[nodiscard]] rpl::producer storiesClickTooltipHiddenValue() const { + return _storiesClickTooltipHidden.value(); + } + void setStoriesClickTooltipHidden(bool value) { + _storiesClickTooltipHidden = value; + } [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); @@ -882,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 @@ -923,6 +939,7 @@ private: WindowPosition _mediaViewPosition = { .maximized = 2 }; rpl::variable _ignoreBatterySaving = false; std::optional _macRoundIconDigest; + rpl::variable _storiesClickTooltipHidden = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/crash_report_window.cpp b/Telegram/SourceFiles/core/crash_report_window.cpp index e8e866ab2..41bacf4ba 100644 --- a/Telegram/SourceFiles/core/crash_report_window.cpp +++ b/Telegram/SourceFiles/core/crash_report_window.cpp @@ -9,7 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/crash_reports.h" #include "core/application.h" -#include "core/launcher.h" #include "core/sandbox.h" #include "core/update_checker.h" #include "core/ui_integration.h" @@ -250,7 +249,6 @@ LastCrashedWindow::UpdaterData::UpdaterData(QWidget *buttonParent) } LastCrashedWindow::LastCrashedWindow( - not_null launcher, const QByteArray &crashdump, Fn launch) : _dumpraw(crashdump) diff --git a/Telegram/SourceFiles/core/crash_report_window.h b/Telegram/SourceFiles/core/crash_report_window.h index 75e17ba23..f0c438119 100644 --- a/Telegram/SourceFiles/core/crash_report_window.h +++ b/Telegram/SourceFiles/core/crash_report_window.h @@ -20,10 +20,6 @@ namespace MTP { struct ProxyData; } // namespace MTP -namespace Core { -class Launcher; -} // namespace Core - class PreLaunchWindow : public QWidget { public: PreLaunchWindow(QString title = QString()); @@ -94,10 +90,7 @@ private: class LastCrashedWindow : public PreLaunchWindow { public: - LastCrashedWindow( - not_null launcher, - const QByteArray &crashdump, - Fn launch); + LastCrashedWindow(const QByteArray &crashdump, Fn launch); rpl::producer proxyChanges() const; diff --git a/Telegram/SourceFiles/core/crash_reports.cpp b/Telegram/SourceFiles/core/crash_reports.cpp index 9419e9c89..be7e77cdd 100644 --- a/Telegram/SourceFiles/core/crash_reports.cpp +++ b/Telegram/SourceFiles/core/crash_reports.cpp @@ -313,7 +313,7 @@ QString PlatformString() { Unexpected("Platform in CrashReports::PlatformString."); } -void StartCatching(not_null launcher) { +void StartCatching() { #ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS ProcessAnnotations["Binary"] = cExeName().toUtf8().constData(); ProcessAnnotations["ApiId"] = QString::number(ApiId).toUtf8().constData(); @@ -324,7 +324,7 @@ void StartCatching(not_null launcher) { : u"%1"_q).arg(AppVersion)).toUtf8().constData(); ProcessAnnotations["Launched"] = QDateTime::currentDateTime().toString("dd.MM.yyyy hh:mm:ss").toUtf8().constData(); ProcessAnnotations["Platform"] = PlatformString().toUtf8().constData(); - ProcessAnnotations["UserTag"] = QString::number(launcher->installationTag(), 16).toUtf8().constData(); + ProcessAnnotations["UserTag"] = QString::number(Core::Launcher::Instance().installationTag(), 16).toUtf8().constData(); QString dumpspath = cWorkingDir() + u"tdata/dumps"_q; QDir().mkpath(dumpspath); diff --git a/Telegram/SourceFiles/core/crash_reports.h b/Telegram/SourceFiles/core/crash_reports.h index b60941642..6a1f0e1d4 100644 --- a/Telegram/SourceFiles/core/crash_reports.h +++ b/Telegram/SourceFiles/core/crash_reports.h @@ -7,10 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Core { -class Launcher; -} // namespace Core - namespace CrashReports { QString PlatformString(); @@ -53,7 +49,7 @@ inline void ClearAnnotationRef(const std::string &key) { SetAnnotationRef(key, nullptr); } -void StartCatching(not_null launcher); +void StartCatching(); void FinishCatching(); } // namespace CrashReports diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index 68ccc8768..7103e1bd6 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -334,7 +334,7 @@ QString ImagesOrAllFilter() { } QString PhotoVideoFilesFilter() { - return u"Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov);;"_q + return u"Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov *.m4v);;"_q + AllFilesFilter(); } @@ -371,6 +371,9 @@ bool GetDefault( ? parent->window() : Core::App().getFileDialogParent(); Core::App().notifyFileDialogShown(true); + const auto guard = gsl::finally([] { + Core::App().notifyFileDialogShown(false); + }); if (type == Type::ReadFiles) { files = QFileDialog::getOpenFileNames(resolvedParent, caption, startFile, filter); QString path = files.isEmpty() ? QString() : QFileInfo(files.back()).absoluteDir().absolutePath(); @@ -386,7 +389,6 @@ bool GetDefault( } else { file = QFileDialog::getOpenFileName(resolvedParent, caption, startFile, filter); } - Core::App().notifyFileDialogShown(false); if (file.isEmpty()) { files = QStringList(); diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index e5cc426d0..7eb611296 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -281,6 +281,8 @@ base::options::toggle OptionFractionalScalingEnabled({ const char kOptionFractionalScalingEnabled[] = "fractional-scaling-enabled"; const char kOptionFreeType[] = "freetype"; +Launcher *Launcher::InstanceSetter::Instance = nullptr; + std::unique_ptr Launcher::Create(int argc, char *argv[]) { return std::make_unique(argc, argv); } @@ -288,15 +290,18 @@ std::unique_ptr Launcher::Create(int argc, char *argv[]) { Launcher::Launcher(int argc, char *argv[]) : _argc(argc) , _argv(argv) +, _arguments(readArguments(_argc, _argv)) , _baseIntegration(_argc, _argv) { crl::toggle_fp_exceptions(true); base::Integration::Set(&_baseIntegration); } -void Launcher::init() { - _arguments = readArguments(_argc, _argv); +Launcher::~Launcher() { + InstanceSetter::Instance = nullptr; +} +void Launcher::init() { prepareSettings(); initQtMessageLogging(); @@ -343,7 +348,7 @@ int Launcher::exec() { } // Must be started before Platform is started. - Logs::start(this); + Logs::start(); base::options::init(cWorkingDir() + "tdata/experimental_options.json"); // Must be called after options are inited. @@ -389,6 +394,18 @@ int Launcher::exec() { return result; } +bool Launcher::validateCustomWorkingDir() { + if (customWorkingDir()) { + if (_customWorkingDir == cWorkingDir()) { + _customWorkingDir = QString(); + return false; + } + cForceWorkingDir(_customWorkingDir); + return true; + } + return false; +} + void Launcher::workingFolderReady() { srand((unsigned int)time(nullptr)); @@ -425,28 +442,17 @@ QStringList Launcher::readArguments(int argc, char *argv[]) const { return result; } -QString Launcher::argumentsString() const { - return _arguments.join(' '); +const QStringList &Launcher::arguments() const { + return _arguments; } bool Launcher::customWorkingDir() const { - return _customWorkingDir; + return !_customWorkingDir.isEmpty(); } void Launcher::prepareSettings() { auto path = base::Platform::CurrentExecutablePath(_argc, _argv); LOG(("Executable path before check: %1").arg(path)); - if (!path.isEmpty()) { - auto info = QFileInfo(path); - if (info.isSymLink()) { - info = QFileInfo(info.symLinkTarget()); - } - if (info.exists()) { - const auto dir = info.absoluteDir().absolutePath(); - gExeDir = (dir.endsWith('/') ? dir : (dir + '/')); - gExeName = info.fileName(); - } - } if (cExeName().isEmpty()) { LOG(("WARNING: Could not compute executable path, some features will be disabled.")); } @@ -460,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. @@ -540,9 +538,9 @@ void Launcher::processArguments() { gStartInTray = parseResult.contains("-startintray"); gQuit = parseResult.contains("-quit"); gSendPaths = parseResult.value("-sendpath", {}); - cForceWorkingDir(parseResult.value("-workdir", {}).join(QString())); - if (!gWorkingDir.isEmpty()) { - _customWorkingDir = true; + _customWorkingDir = parseResult.value("-workdir", {}).join(QString()); + if (!_customWorkingDir.isEmpty()) { + _customWorkingDir = QDir(_customWorkingDir).absolutePath() + '/'; } gStartUrl = parseResult.value("--", {}).join(QString()); @@ -558,7 +556,7 @@ void Launcher::processArguments() { int Launcher::executeApplication() { FilteredCommandLineArguments arguments(_argc, _argv); - Sandbox sandbox(this, arguments.count(), arguments.values()); + Sandbox sandbox(arguments.count(), arguments.values()); Ui::MainQueueProcessor processor; base::ConcurrentTimerEnvironment environment; return sandbox.start(); diff --git a/Telegram/SourceFiles/core/launcher.h b/Telegram/SourceFiles/core/launcher.h index 047218951..a5e5117a5 100644 --- a/Telegram/SourceFiles/core/launcher.h +++ b/Telegram/SourceFiles/core/launcher.h @@ -20,19 +20,26 @@ public: static std::unique_ptr Create(int argc, char *argv[]); + static Launcher &Instance() { + Expects(InstanceSetter::Instance != nullptr); + + return *InstanceSetter::Instance; + } + virtual int exec(); - QString argumentsString() const; + const QStringList &arguments() const; bool customWorkingDir() const; uint64 installationTag() const; bool checkPortableVersionFolder(); + bool validateCustomWorkingDir(); void workingFolderReady(); void writeDebugModeSetting(); void writeInstallBetaVersionsSetting(); - virtual ~Launcher() = default; + virtual ~Launcher(); protected: enum class UpdaterLaunch { @@ -61,12 +68,23 @@ private: int executeApplication(); + struct InstanceSetter { + InstanceSetter(not_null instance) { + Expects(Instance == nullptr); + + Instance = instance; + } + + static Launcher *Instance; + }; + InstanceSetter _setter = { this }; + int _argc; char **_argv; QStringList _arguments; BaseIntegration _baseIntegration; - bool _customWorkingDir = false; + QString _customWorkingDir; }; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 19fce2ad3..9daf74d62 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_authorizations.h" #include "api/api_confirm_phone.h" #include "api/api_text_entities.h" +#include "api/api_chat_filters.h" #include "api/api_chat_invite.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" @@ -25,7 +26,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/sticker_set_box.h" #include "boxes/sessions_box.h" #include "boxes/language_box.h" -#include "boxes/change_phone_box.h" #include "passport/passport_form_controller.h" #include "window/window_session_controller.h" #include "ui/toast/toast.h" @@ -78,6 +78,18 @@ bool JoinGroupByHash( return true; } +bool JoinFilterBySlug( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + Api::CheckFilterInvite(controller, match->captured(1)); + controller->window().activate(); + return true; +} + bool ShowStickerSet( Window::SessionController *controller, const Match &match, @@ -87,7 +99,7 @@ bool ShowStickerSet( } Core::App().hideMediaView(); controller->show(Box( - controller, + controller->uiShow(), StickerSetIdentifier{ .shortName = match->captured(2) }, (match->captured(1) == "addemoji" ? Data::StickersType::Emoji @@ -333,7 +345,13 @@ bool ResolveUsernameOrPhone( const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); - const auto domain = params.value(u"domain"_q); + const auto domainParam = params.value(u"domain"_q); + const auto appnameParam = params.value(u"appname"_q); + + // Fix t.me/s/username links. + const auto webChannelPreviewLink = (domainParam == u"s"_q) + && !appnameParam.isEmpty(); + const auto domain = webChannelPreviewLink ? appnameParam : domainParam; const auto phone = params.value(u"phone"_q); const auto validDomain = [](const QString &domain) { return qthelp::regex_match( @@ -371,7 +389,9 @@ bool ResolveUsernameOrPhone( if (const auto postId = postParam.toInt()) { post = postId; } - const auto appname = params.value(u"appname"_q); + const auto storyParam = params.value(u"story"_q); + const auto storyId = storyParam.toInt(); + const auto appname = webChannelPreviewLink ? QString() : appnameParam; const auto appstart = params.value(u"startapp"_q); const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); @@ -392,10 +412,12 @@ bool ResolveUsernameOrPhone( } const auto myContext = context.value(); using Navigation = Window::SessionNavigation; + controller->window().activate(); controller->showPeerByLink(Navigation::PeerByLinkInfo{ .usernameOrId = domain, .phone = phone, .messageId = post, + .storyId = storyId, .repliesInfo = commentId ? Navigation::RepliesByLinkInfo{ Navigation::CommentId{ commentId } @@ -409,7 +431,7 @@ bool ResolveUsernameOrPhone( .startToken = startToken, .startAdminRights = adminRights, .startAutoSubmit = myContext.botStartAutoSubmit, - .botAppName = appname.isEmpty() ? postParam : appname, + .botAppName = (appname.isEmpty() ? postParam : appname), .botAppForceConfirmation = myContext.mayShowConfirmation, .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) @@ -425,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; } @@ -452,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, @@ -466,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; @@ -490,7 +513,9 @@ bool ResolveSettings( } else if (section == u"themes"_q) { return ::Settings::Chat::Id(); } else if (section == u"change_number"_q) { - return ::Settings::ChangePhone::Id(); + controller->show( + Ui::MakeInformBox(tr::lng_change_phone_error())); + return {}; } else if (section == u"auto_delete"_q) { return ::Settings::GlobalTTLId(); } else if (section == u"information"_q) { @@ -594,9 +619,7 @@ bool ShowInviteLink( return false; } QGuiApplication::clipboard()->setText(link); - Ui::Toast::Show( - Window::Show(controller).toastParent(), - tr::lng_group_invite_copied(tr::now)); + controller->showToast(tr::lng_group_invite_copied(tr::now)); return true; } @@ -613,12 +636,12 @@ void ExportTestChatTheme( not_null controller, not_null theme) { const auto session = &controller->session(); - const auto show = std::make_shared(controller); + const auto show = controller->uiShow(); const auto inputSettings = [&](Data::CloudThemeType type) -> std::optional { const auto i = theme->settings.find(type); if (i == end(theme->settings)) { - Ui::Toast::Show(show->toastParent(), "Something went wrong :("); + show->showToast(u"Something went wrong :("_q); return std::nullopt; } const auto &fields = i->second; @@ -626,17 +649,15 @@ void ExportTestChatTheme( || !fields.paper->isPattern() || fields.paper->backgroundColors().empty() || !fields.paper->hasShareUrl()) { - Ui::Toast::Show(show->toastParent(), "Something went wrong :("); + show->showToast(u"Something went wrong :("_q); return std::nullopt; } const auto &bg = fields.paper->backgroundColors(); - const auto url = fields.paper->shareUrl(session); + const auto url = fields.paper->shareUrl(&show->session()); const auto from = url.indexOf("bg/"); const auto till = url.indexOf("?"); if (from < 0 || till <= from) { - Ui::Toast::Show( - show->toastParent(), - "Bad WallPaper link: " + url); + show->showToast(u"Bad WallPaper link: "_q + url); return std::nullopt; } @@ -718,15 +739,9 @@ void ExportTestChatTheme( const auto slug = Data::CloudTheme::Parse(session, result, true).slug; QGuiApplication::clipboard()->setText( session->createInternalLinkFull("addtheme/" + slug)); - if (show->valid()) { - Ui::Toast::Show( - show->toastParent(), - tr::lng_background_link_copied(tr::now)); - } + show->showToast(tr::lng_background_link_copied(tr::now)); }).fail([=](const MTP::Error &error) { - if (show->valid()) { - Ui::Toast::Show(show->toastParent(), "Error: " + error.type()); - } + show->showToast(u"Error: "_q + error.type()); }).send(); } @@ -751,7 +766,10 @@ bool ResolveTestChatTheme( } const auto recache = [&](Data::CloudThemeType type) { [[maybe_unused]] auto value = theme->settings.contains(type) - ? controller->cachedChatThemeValue(*theme, type) + ? controller->cachedChatThemeValue( + *theme, + Data::WallPaper(0), + type) : nullptr; }; recache(Data::CloudThemeType::Dark); @@ -829,6 +847,10 @@ const std::vector &LocalUrlHandlers() { u"^join/?\\?invite=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, JoinGroupByHash }, + { + u"^addlist/?\\?slug=([a-zA-Z0-9\\.\\_\\-]+)(&|$)"_q, + JoinFilterBySlug + }, { u"^(addstickers|addemoji)/?\\?set=([a-zA-Z0-9\\.\\_]+)(&|$)"_q, ShowStickerSet @@ -953,6 +975,8 @@ QString TryConvertUrlToLocal(QString url) { return u"tg://resolve?phone="_q + phoneMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params); } else if (const auto joinChatMatch = regex_match(u"^(joinchat/|\\+|\\%20)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://join?invite="_q + url_encode(joinChatMatch->captured(2)); + } else if (const auto joinFilterMatch = regex_match(u"^(addlist/)([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) { + return u"tg://addlist?slug="_q + url_encode(joinFilterMatch->captured(2)); } else if (const auto stickerSetMatch = regex_match(u"^(addstickers|addemoji)/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2)); } else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) { @@ -1012,8 +1036,9 @@ QString TryConvertUrlToLocal(QString url) { "(" "/?\\?|" "/?$|" - "/[a-zA-Z0-9\\.\\_]+|" + "/[a-zA-Z0-9\\.\\_]+/?(\\?|$)|" "/\\d+/?(\\?|$)|" + "/s/\\d+/?(\\?|$)|" "/\\d+/\\d+/?(\\?|$)" ")"_q, query, matchOptions)) { const auto params = query.mid(usernameMatch->captured(0).size()).toString(); @@ -1023,6 +1048,8 @@ QString TryConvertUrlToLocal(QString url) { added = u"&topic=%1&post=%2"_q.arg(threadPostMatch->captured(1)).arg(threadPostMatch->captured(2)); } else if (const auto postMatch = regex_match(u"^/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&post="_q + postMatch->captured(1); + } else if (const auto storyMatch = regex_match(u"^/s/(\\d+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { + added = u"&story="_q + storyMatch->captured(1); } else if (const auto appNameMatch = regex_match(u"^/([a-zA-Z0-9\\.\\_]+)(/?\\?|/?$)"_q, usernameMatch->captured(2))) { added = u"&appname="_q + appNameMatch->captured(1); } diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp index e75a5fc94..8ba1b6494 100644 --- a/Telegram/SourceFiles/core/mime_type.cpp +++ b/Telegram/SourceFiles/core/mime_type.cpp @@ -229,4 +229,15 @@ QList ReadMimeUrls(not_null data) { : QList(); } +bool CanSendFiles(not_null data) { + if (data->hasImage()) { + return true; + } else if (const auto urls = ReadMimeUrls(data); !urls.empty()) { + if (ranges::all_of(urls, &QUrl::isLocalFile)) { + return true; + } + } + return false; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/mime_type.h b/Telegram/SourceFiles/core/mime_type.h index d5aaa9eb0..3271adafe 100644 --- a/Telegram/SourceFiles/core/mime_type.h +++ b/Telegram/SourceFiles/core/mime_type.h @@ -67,5 +67,6 @@ struct MimeImageData { [[nodiscard]] MimeImageData ReadMimeImage(not_null data); [[nodiscard]] QString ReadMimeText(not_null data); [[nodiscard]] QList ReadMimeUrls(not_null data); +[[nodiscard]] bool CanSendFiles(not_null data); } // namespace Core diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index 741f7d072..1a6c949cf 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_regex.h" #include "ui/ui_utility.h" #include "ui/effects/animations.h" +#include "ui/platform/ui_platform_utility.h" #include #include @@ -78,13 +79,9 @@ QString _escapeFrom7bit(const QString &str) { bool Sandbox::QuitOnStartRequested = false; -Sandbox::Sandbox( - not_null launcher, - int &argc, - char **argv) +Sandbox::Sandbox(int &argc, char **argv) : QApplication(argc, argv) -, _mainThreadId(QThread::currentThreadId()) -, _launcher(launcher) { +, _mainThreadId(QThread::currentThreadId()) { setQuitOnLastWindowClosed(false); } @@ -107,7 +104,8 @@ int Sandbox::start() { hashMd5Hex(d.constData(), d.size(), h.data()); _lockFile = std::make_unique(QDir::tempPath() + '/' + h + '-' + cGUIDStr()); _lockFile->setStaleLockTime(0); - if (!_lockFile->tryLock() && _launcher->customWorkingDir()) { + if (!_lockFile->tryLock() + && Launcher::Instance().customWorkingDir()) { // On Windows, QLockFile has problems detecting a stale lock // if the machine's hostname contains characters outside the US-ASCII character set. if constexpr (Platform::IsWindows()) { @@ -200,7 +198,7 @@ void Sandbox::launchApplication() { } setupScreenScale(); - _application = std::make_unique(_launcher); + _application = std::make_unique(); // Ideally this should go to constructor. // But we want to catch all native events and Application installs @@ -401,7 +399,6 @@ void Sandbox::singleInstanceChecked() { } _lastCrashDump = crashdump; auto window = new LastCrashedWindow( - _launcher, _lastCrashDump, [=] { launchApplication(); }); window->proxyChanges( @@ -529,14 +526,6 @@ void Sandbox::refreshGlobalProxy() { } } -bool Sandbox::customWorkingDir() const { - return _launcher->customWorkingDir(); -} - -uint64 Sandbox::installationTag() const { - return _launcher->installationTag(); -} - void Sandbox::checkForEmptyLoopNestingLevel() { // _loopNestingLevel == _eventNestingLevel means that we had a // native event in a nesting loop that didn't get a notify() call @@ -592,9 +581,18 @@ void Sandbox::registerEnterFromEventLoop() { } bool Sandbox::notifyOrInvoke(QObject *receiver, QEvent *e) { - if (e->type() == base::InvokeQueuedEvent::kType) { + const auto type = e->type(); + if (type == base::InvokeQueuedEvent::Type()) { static_cast(e)->invoke(); return true; + } else if (receiver == this) { + if (type == QEvent::ApplicationDeactivate) { + if (Ui::Platform::SkipApplicationDeactivateEvent()) { + return true; + } + } else if (type == QEvent::ApplicationActivate) { + Ui::Platform::GotApplicationActivateEvent(); + } } return QApplication::notify(receiver, e); } diff --git a/Telegram/SourceFiles/core/sandbox.h b/Telegram/SourceFiles/core/sandbox.h index bcf83982a..d8f47cd5a 100644 --- a/Telegram/SourceFiles/core/sandbox.h +++ b/Telegram/SourceFiles/core/sandbox.h @@ -19,7 +19,6 @@ class QLockFile; namespace Core { -class Launcher; class UpdateChecker; class Application; @@ -33,7 +32,7 @@ private: } public: - Sandbox(not_null launcher, int &argc, char **argv); + Sandbox(int &argc, char **argv); Sandbox(const Sandbox &other) = delete; Sandbox &operator=(const Sandbox &other) = delete; @@ -41,8 +40,6 @@ public: int start(); void refreshGlobalProxy(); - bool customWorkingDir() const; - uint64 installationTag() const; void postponeCall(FnMut &&callable); bool notify(QObject *receiver, QEvent *e) override; @@ -116,7 +113,6 @@ private: std::vector _previousLoopNestingLevels; std::vector _postponedCalls; - not_null _launcher; std::unique_ptr _application; QString _localServerName, _localSocketReadData; diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index d0d7580cc..ead942262 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_session.h" +#include "data/data_sponsored_messages.h" #include "ui/text/text_custom_emoji.h" #include "ui/basic_click_handlers.h" #include "ui/emoji_config.h" @@ -26,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "mtproto/mtproto_config.h" #include "window/window_controller.h" +#include "window/window_session_controller.h" #include "mainwindow.h" namespace Core { @@ -264,21 +266,26 @@ Fn UiIntegration::createSpoilerRepaint(const std::any &context) { return my ? my->customEmojiRepaint : nullptr; } +bool UiIntegration::allowClickHandlerActivation( + const std::shared_ptr &handler, + const ClickContext &context) { + const auto my = context.other.value(); + if (const auto window = my.sessionWindow.get()) { + window->session().data().sponsoredMessages().clicked(my.itemId); + } + return true; +} + rpl::producer<> UiIntegration::forcePopupMenuHideRequests() { return Core::App().passcodeLockChanges() | rpl::to_empty; } 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/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index a71c9cfbe..65661ecfd 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -60,6 +60,9 @@ public: const QString &data, const std::any &context) override; Fn createSpoilerRepaint(const std::any &context) override; + bool allowClickHandlerActivation( + const std::shared_ptr &handler, + const ClickContext &context) override; QString phraseContextCopyText() override; QString phraseContextCopyEmail() override; diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index ea4c9d849..1ec86fab3 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 4007001; -constexpr auto AppVersionStr = "4.7.1"; +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_abstract_sparse_ids.h b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h index d48542d19..4a4d62d17 100644 --- a/Telegram/SourceFiles/data/data_abstract_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_abstract_sparse_ids.h @@ -57,7 +57,7 @@ public: return std::nullopt; } [[nodiscard]] std::optional nearest(Id id) const { - static_assert(std::is_same_v>); + static_assert(std::is_same_v>); if (const auto it = ranges::lower_bound(_ids, id); it != _ids.end()) { return *it; } else if (_ids.empty()) { @@ -70,6 +70,10 @@ public: std::swap(_skippedBefore, _skippedAfter); } + friend inline bool operator==( + const AbstractSparseIds&, + const AbstractSparseIds&) = default; + private: IdsContainer _ids; std::optional _fullCount; diff --git a/Telegram/SourceFiles/data/data_audio_msg_id.cpp b/Telegram/SourceFiles/data/data_audio_msg_id.cpp index 4c95989ac..d8365bee2 100644 --- a/Telegram/SourceFiles/data/data_audio_msg_id.cpp +++ b/Telegram/SourceFiles/data/data_audio_msg_id.cpp @@ -27,7 +27,7 @@ AudioMsgId::AudioMsgId( , _externalPlayId(externalPlayId) , _changeablePlaybackSpeed(_audio->isVoiceMessage() || _audio->isVideoMessage() - || (_audio->getDuration() >= kMinLengthForChangeablePlaybackSpeed)) { + || (_audio->duration() >= kMinLengthForChangeablePlaybackSpeed)) { setTypeFromAudio(); } diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 9f28a7af0..f20041565 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -272,6 +272,38 @@ void Changes::entryRemoved(not_null entry) { _entryChanges.drop(entry); } +void Changes::storyUpdated( + not_null story, + StoryUpdate::Flags flags) { + const auto drop = (flags & StoryUpdate::Flag::Destroyed); + _storyChanges.updated(story, flags, drop); + if (!drop) { + scheduleNotifications(); + } +} + +rpl::producer Changes::storyUpdates( + StoryUpdate::Flags flags) const { + return _storyChanges.updates(flags); +} + +rpl::producer Changes::storyUpdates( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.updates(story, flags); +} + +rpl::producer Changes::storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const { + return _storyChanges.flagsValue(story, flags); +} + +rpl::producer Changes::realtimeStoryUpdates( + StoryUpdate::Flag flag) const { + return _storyChanges.realtimeUpdates(flag); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; @@ -291,6 +323,7 @@ void Changes::sendNotifications() { _messageChanges.sendNotifications(); _entryChanges.sendNotifications(); _topicChanges.sendNotifications(); + _storyChanges.sendNotifications(); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 7b75478e9..304749900 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -38,6 +38,7 @@ inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) { namespace Data { class ForumTopic; +class Story; struct NameUpdate { NameUpdate( @@ -64,45 +65,47 @@ struct PeerUpdate { Migration = (1ULL << 5), UnavailableReason = (1ULL << 6), ChatThemeEmoji = (1ULL << 7), - IsBlocked = (1ULL << 8), - MessagesTTL = (1ULL << 9), - FullInfo = (1ULL << 10), - Usernames = (1ULL << 11), - TranslationDisabled = (1ULL << 12), + ChatWallPaper = (1ULL << 8), + IsBlocked = (1ULL << 9), + MessagesTTL = (1ULL << 10), + FullInfo = (1ULL << 11), + Usernames = (1ULL << 12), + TranslationDisabled = (1ULL << 13), // For users - CanShareContact = (1ULL << 13), - IsContact = (1ULL << 14), - PhoneNumber = (1ULL << 15), - OnlineStatus = (1ULL << 16), - BotCommands = (1ULL << 17), - BotCanBeInvited = (1ULL << 18), - BotStartToken = (1ULL << 19), - CommonChats = (1ULL << 20), - HasCalls = (1ULL << 21), - SupportInfo = (1ULL << 22), - IsBot = (1ULL << 23), - EmojiStatus = (1ULL << 24), + CanShareContact = (1ULL << 14), + IsContact = (1ULL << 15), + PhoneNumber = (1ULL << 16), + OnlineStatus = (1ULL << 17), + BotCommands = (1ULL << 18), + BotCanBeInvited = (1ULL << 19), + BotStartToken = (1ULL << 20), + CommonChats = (1ULL << 21), + HasCalls = (1ULL << 22), + SupportInfo = (1ULL << 23), + IsBot = (1ULL << 24), + EmojiStatus = (1ULL << 25), + StoriesState = (1ULL << 26), // For chats and channels - InviteLinks = (1ULL << 25), - Members = (1ULL << 26), - Admins = (1ULL << 27), - BannedUsers = (1ULL << 28), - Rights = (1ULL << 29), - PendingRequests = (1ULL << 30), - Reactions = (1ULL << 31), + InviteLinks = (1ULL << 27), + Members = (1ULL << 28), + Admins = (1ULL << 29), + BannedUsers = (1ULL << 30), + Rights = (1ULL << 31), + PendingRequests = (1ULL << 32), + Reactions = (1ULL << 33), // For channels - ChannelAmIn = (1ULL << 32), - StickersSet = (1ULL << 33), - ChannelLinkedChat = (1ULL << 34), - ChannelLocation = (1ULL << 35), - Slowmode = (1ULL << 36), - GroupCall = (1ULL << 37), + ChannelAmIn = (1ULL << 34), + StickersSet = (1ULL << 35), + ChannelLinkedChat = (1ULL << 36), + ChannelLocation = (1ULL << 37), + Slowmode = (1ULL << 38), + GroupCall = (1ULL << 39), // For iteration - LastUsedBit = (1ULL << 37), + LastUsedBit = (1ULL << 39), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -146,19 +149,19 @@ struct TopicUpdate { enum class Flag : uint32 { None = 0, - UnreadView = (1U << 1), - UnreadMentions = (1U << 2), + UnreadView = (1U << 1), + UnreadMentions = (1U << 2), UnreadReactions = (1U << 3), - Notifications = (1U << 4), - Title = (1U << 5), - IconId = (1U << 6), - ColorId = (1U << 7), - CloudDraft = (1U << 8), - Closed = (1U << 9), - Creator = (1U << 10), - Destroyed = (1U << 11), + Notifications = (1U << 4), + Title = (1U << 5), + IconId = (1U << 6), + ColorId = (1U << 7), + CloudDraft = (1U << 8), + Closed = (1U << 9), + Creator = (1U << 10), + Destroyed = (1U << 11), - LastUsedBit = (1U << 11), + LastUsedBit = (1U << 11), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -197,19 +200,40 @@ struct EntryUpdate { enum class Flag : uint32 { None = 0, - Repaint = (1U << 0), + Repaint = (1U << 0), HasPinnedMessages = (1U << 1), - ForwardDraft = (1U << 2), - LocalDraftSet = (1U << 3), - Height = (1U << 4), - Destroyed = (1U << 5), + ForwardDraft = (1U << 2), + LocalDraftSet = (1U << 3), + Height = (1U << 4), + Destroyed = (1U << 5), + + LastUsedBit = (1U << 5), + }; + using Flags = base::flags; + friend inline constexpr auto is_flag_type(Flag) { return true; } + + not_null entry; + Flags flags = 0; + +}; + +struct StoryUpdate { + enum class Flag : uint32 { + None = 0, + + Edited = (1U << 0), + Destroyed = (1U << 1), + NewAdded = (1U << 2), + ViewsAdded = (1U << 3), + MarkRead = (1U << 4), + Reaction = (1U << 5), LastUsedBit = (1U << 5), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } - not_null entry; + not_null story; Flags flags = 0; }; @@ -297,6 +321,20 @@ public: EntryUpdate::Flag flag) const; void entryRemoved(not_null entry); + void storyUpdated( + not_null story, + StoryUpdate::Flags flags); + [[nodiscard]] rpl::producer storyUpdates( + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyUpdates( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer storyFlagsValue( + not_null story, + StoryUpdate::Flags flags) const; + [[nodiscard]] rpl::producer realtimeStoryUpdates( + StoryUpdate::Flag flag) const; + void sendNotifications(); private: @@ -347,6 +385,7 @@ private: Manager _topicChanges; Manager _messageChanges; Manager _entryChanges; + Manager _storyChanges; bool _notify = false; 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_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 02c308b38..26c6b6287 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_unread_things.h" #include "ui/ui_utility.h" +#include "ui/chat/more_chats_bar.h" #include "main/main_session.h" #include "main/main_account.h" #include "main/main_app_config.h" @@ -31,6 +32,12 @@ constexpr auto kRefreshSuggestedTimeout = 7200 * crl::time(1000); constexpr auto kLoadExceptionsAfter = 100; constexpr auto kLoadExceptionsPerRequest = 100; +[[nodiscard]] crl::time RequestUpdatesEach(not_null owner) { + const auto appConfig = &owner->session().account().appConfig(); + return appConfig->get(u"chatlist_update_period"_q, 3600) + * crl::time(1000); +} + } // namespace ChatFilter::ChatFilter( @@ -109,22 +116,81 @@ ChatFilter ChatFilter::FromTL( { never.begin(), never.end() }); }, [](const MTPDdialogFilterDefault &d) { return ChatFilter(); + }, [&](const MTPDdialogFilterChatlist &data) { + auto &&to_histories = ranges::views::transform([&]( + const MTPInputPeer &data) { + const auto peer = data.match([&](const MTPDinputPeerUser &data) { + const auto user = owner->user(data.vuser_id().v); + user->setAccessHash(data.vaccess_hash().v); + return (PeerData*)user; + }, [&](const MTPDinputPeerChat &data) { + return (PeerData*)owner->chat(data.vchat_id().v); + }, [&](const MTPDinputPeerChannel &data) { + const auto channel = owner->channel(data.vchannel_id().v); + channel->setAccessHash(data.vaccess_hash().v); + return (PeerData*)channel; + }, [&](const MTPDinputPeerSelf &data) { + return (PeerData*)owner->session().user(); + }, [&](const auto &data) { + return (PeerData*)nullptr; + }); + return peer ? owner->history(peer).get() : nullptr; + }) | ranges::views::filter([](History *history) { + return history != nullptr; + }) | ranges::views::transform([](History *history) { + return not_null(history); + }); + auto &&always = ranges::views::concat( + data.vinclude_peers().v + ) | to_histories; + auto pinned = ranges::views::all( + data.vpinned_peers().v + ) | to_histories | ranges::to_vector; + auto &&all = ranges::views::concat(always, pinned); + auto list = base::flat_set>{ + all.begin(), + all.end() + }; + return ChatFilter( + data.vid().v, + qs(data.vtitle()), + qs(data.vemoticon().value_or_empty()), + (Flag::Chatlist + | (data.is_has_my_invites() ? Flag::HasMyLinks : Flag())), + std::move(list), + std::move(pinned), + {}); }); } +ChatFilter ChatFilter::withId(FilterId id) const { + auto result = *this; + result._id = id; + return result; +} + +ChatFilter ChatFilter::withTitle(const QString &title) const { + auto result = *this; + result._title = title; + return result; +} + +ChatFilter ChatFilter::withChatlist(bool chatlist, bool hasMyLinks) const { + auto result = *this; + if (chatlist) { + result._flags |= Flag::Chatlist; + if (hasMyLinks) { + result._flags |= Flag::HasMyLinks; + } else { + result._flags &= ~Flag::HasMyLinks; + } + } else { + result._flags &= ~(Flag::Chatlist | Flag::HasMyLinks); + } + return result; +} + MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { - using TLFlag = MTPDdialogFilter::Flag; - const auto flags = TLFlag(0) - | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) - | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) - | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) - | ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0)) - | ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0)) - | ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0)) - | ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0)) - | ((_flags & Flag::NoArchived) - ? TLFlag::f_exclude_archived - : TLFlag(0)); auto always = _always; auto pinned = QVector(); pinned.reserve(_pinned.size()); @@ -137,13 +203,36 @@ MTPDialogFilter ChatFilter::tl(FilterId replaceId) const { for (const auto &history : always) { include.push_back(history->peer->input); } + if (_flags & Flag::Chatlist) { + using TLFlag = MTPDdialogFilterChatlist::Flag; + const auto flags = TLFlag::f_emoticon; + return MTP_dialogFilterChatlist( + MTP_flags(flags), + MTP_int(replaceId ? replaceId : _id), + MTP_string(_title), + MTP_string(_iconEmoji), + MTP_vector(pinned), + MTP_vector(include)); + } + using TLFlag = MTPDdialogFilter::Flag; + const auto flags = TLFlag::f_emoticon + | ((_flags & Flag::Contacts) ? TLFlag::f_contacts : TLFlag(0)) + | ((_flags & Flag::NonContacts) ? TLFlag::f_non_contacts : TLFlag(0)) + | ((_flags & Flag::Groups) ? TLFlag::f_groups : TLFlag(0)) + | ((_flags & Flag::Channels) ? TLFlag::f_broadcasts : TLFlag(0)) + | ((_flags & Flag::Bots) ? TLFlag::f_bots : TLFlag(0)) + | ((_flags & Flag::NoMuted) ? TLFlag::f_exclude_muted : TLFlag(0)) + | ((_flags & Flag::NoRead) ? TLFlag::f_exclude_read : TLFlag(0)) + | ((_flags & Flag::NoArchived) + ? TLFlag::f_exclude_archived + : TLFlag(0)); auto never = QVector(); never.reserve(_never.size()); for (const auto &history : _never) { never.push_back(history->peer->input); } return MTP_dialogFilter( - MTP_flags(flags | TLFlag::f_emoticon), + MTP_flags(flags), MTP_int(replaceId ? replaceId : _id), MTP_string(_title), MTP_string(_iconEmoji), @@ -168,6 +257,14 @@ ChatFilter::Flags ChatFilter::flags() const { return _flags; } +bool ChatFilter::chatlist() const { + return _flags & Flag::Chatlist; +} + +bool ChatFilter::hasMyLinks() const { + return _flags & Flag::HasMyLinks; +} + const base::flat_set> &ChatFilter::always() const { return _always; } @@ -223,7 +320,9 @@ bool ChatFilter::contains(not_null history) const { || _always.contains(history); } -ChatFilters::ChatFilters(not_null owner) : _owner(owner) { +ChatFilters::ChatFilters(not_null owner) +: _owner(owner) +, _moreChatsTimer([=] { checkLoadMoreChatsLists(); }) { _list.emplace_back(); crl::on_main(&owner->session(), [=] { load(); }); } @@ -265,6 +364,11 @@ void ChatFilters::load() { load(false); } +void ChatFilters::reload() { + _reloading = true; + load(); +} + void ChatFilters::load(bool force) { if (_loadRequestId && !force) { return; @@ -277,6 +381,10 @@ void ChatFilters::load(bool force) { _loadRequestId = 0; }).fail([=] { _loadRequestId = 0; + if (_reloading) { + _reloading = false; + _listChanged.fire({}); + } }).send(); } @@ -308,8 +416,9 @@ void ChatFilters::received(const QVector &list) { if (!ranges::contains(begin(_list), end(_list), 0, &ChatFilter::id)) { _list.insert(begin(_list), ChatFilter()); } - if (changed || !_loaded) { + if (changed || !_loaded || _reloading) { _loaded = true; + _reloading = false; _listChanged.fire({}); } } @@ -334,6 +443,114 @@ void ChatFilters::apply(const MTPUpdate &update) { }); } +ChatFilterLink ChatFilters::add( + FilterId id, + const MTPExportedChatlistInvite &update) { + const auto i = ranges::find(_list, id, &ChatFilter::id); + if (i == end(_list) || !i->chatlist()) { + LOG(("Api Error: " + "Attempt to add chatlist link to a non-chatlist filter: %1" + ).arg(id)); + return {}; + } + auto &links = _chatlistLinks[id]; + const auto &data = update.data(); + const auto url = qs(data.vurl()); + const auto title = qs(data.vtitle()); + auto chats = data.vpeers().v | ranges::views::transform([&]( + const MTPPeer &peer) { + return _owner->history(peerFromMTP(peer)); + }) | ranges::to_vector; + const auto j = ranges::find(links, url, &ChatFilterLink::url); + if (j != end(links)) { + if (j->title != title || j->chats != chats) { + j->title = title; + j->chats = std::move(chats); + _chatlistLinksUpdated.fire_copy(id); + } + return *j; + } + links.push_back({ + .id = id, + .url = url, + .title = title, + .chats = std::move(chats), + }); + _chatlistLinksUpdated.fire_copy(id); + return links.back(); +} + +void ChatFilters::edit( + FilterId id, + const QString &url, + const QString &title) { + auto &links = _chatlistLinks[id]; + const auto i = ranges::find(links, url, &ChatFilterLink::url); + if (i != end(links)) { + i->title = title; + _chatlistLinksUpdated.fire_copy(id); + + _owner->session().api().request(MTPchatlists_EditExportedInvite( + MTP_flags(MTPchatlists_EditExportedInvite::Flag::f_title), + MTP_inputChatlistDialogFilter(MTP_int(id)), + MTP_string(url), + MTP_string(title), + MTPVector() // peers + )).done([=](const MTPExportedChatlistInvite &result) { + //const auto &data = result.data(); + //const auto link = _owner->chatsFilters().add(id, result); + //done(link); + }).fail([=](const MTP::Error &error) { + //done({ .id = id }); + }).send(); + } +} + +void ChatFilters::destroy(FilterId id, const QString &url) { + auto &links = _chatlistLinks[id]; + const auto i = ranges::find(links, url, &ChatFilterLink::url); + if (i != end(links)) { + links.erase(i); + _chatlistLinksUpdated.fire_copy(id); + + const auto api = &_owner->session().api(); + api->request(_linksRequestId).cancel(); + _linksRequestId = api->request(MTPchatlists_DeleteExportedInvite( + MTP_inputChatlistDialogFilter(MTP_int(id)), + MTP_string(url) + )).send(); + } +} + +rpl::producer> ChatFilters::chatlistLinks( + FilterId id) const { + return _chatlistLinksUpdated.events_starting_with_copy( + id + ) | rpl::filter(rpl::mappers::_1 == id) | rpl::map([=] { + const auto i = _chatlistLinks.find(id); + return (i != end(_chatlistLinks)) + ? i->second + : std::vector(); + }); +} + +void ChatFilters::reloadChatlistLinks(FilterId id) { + const auto api = &_owner->session().api(); + api->request(_linksRequestId).cancel(); + _linksRequestId = api->request(MTPchatlists_GetExportedInvites( + MTP_inputChatlistDialogFilter(MTP_int(id)) + )).done([=](const MTPchatlists_ExportedInvites &result) { + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); + _chatlistLinks[id].clear(); + for (const auto &link : data.vinvites().v) { + add(id, link); + } + _chatlistLinksUpdated.fire_copy(id); + }).send(); +} + void ChatFilters::set(ChatFilter filter) { if (!filter.id()) { return; @@ -386,16 +603,22 @@ void ChatFilters::applyRemove(int position) { bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) { Expects(filter.id() == updated.id()); + using Flag = ChatFilter::Flag; + const auto id = filter.id(); const auto exceptionsChanged = filter.always() != updated.always(); + const auto rulesMask = ~(Flag::Chatlist | Flag::HasMyLinks); const auto rulesChanged = exceptionsChanged - || (filter.flags() != updated.flags()) + || ((filter.flags() & rulesMask) != (updated.flags() & rulesMask)) || (filter.never() != updated.never()); const auto pinnedChanged = (filter.pinned() != updated.pinned()); - if (!rulesChanged - && !pinnedChanged - && filter.title() == updated.title() - && filter.iconEmoji() == updated.iconEmoji()) { + const auto chatlistChanged = (filter.chatlist() != updated.chatlist()) + || (filter.hasMyLinks() != updated.hasMyLinks()); + const auto listUpdated = rulesChanged + || pinnedChanged + || (filter.title() != updated.title()) + || (filter.iconEmoji() != updated.iconEmoji()); + if (!listUpdated && !chatlistChanged) { return false; } if (rulesChanged) { @@ -434,7 +657,10 @@ bool ChatFilters::applyChange(ChatFilter &filter, ChatFilter &&updated) { const auto filterList = _owner->chatsFilters().chatsList(id); filterList->pinned()->applyList(filter.pinned()); } - return true; + if (chatlistChanged) { + _isChatlistChanged.fire_copy(id); + } + return listUpdated; } bool ChatFilters::applyOrder(const QVector &order) { @@ -571,6 +797,10 @@ rpl::producer<> ChatFilters::changed() const { return _listChanged.events(); } +rpl::producer ChatFilters::isChatlistChanged() const { + return _isChatlistChanged.events(); +} + bool ChatFilters::loadNextExceptions(bool chatsListLoaded) { if (_exceptionsLoadRequestId) { return true; @@ -673,4 +903,119 @@ rpl::producer<> ChatFilters::suggestedUpdated() const { return _suggestedUpdated.events(); } +rpl::producer ChatFilters::moreChatsContent( + FilterId id) { + if (!id) { + return rpl::single(Ui::MoreChatsBarContent{ .count = 0 }); + } + return [=](auto consumer) { + auto result = rpl::lifetime(); + + auto &entry = _moreChatsData[id]; + auto watching = entry.watching.lock(); + if (!watching) { + watching = std::make_shared(true); + entry.watching = watching; + } + result.add([watching] {}); + + _moreChatsUpdated.events_starting_with_copy( + id + ) | rpl::start_with_next([=] { + consumer.put_next(Ui::MoreChatsBarContent{ + .count = int(moreChats(id).size()), + }); + }, result); + loadMoreChatsList(id); + + return result; + }; +} + +const std::vector> &ChatFilters::moreChats( + FilterId id) const { + static const auto kEmpty = std::vector>(); + if (!id) { + return kEmpty; + } + const auto i = _moreChatsData.find(id); + return (i != end(_moreChatsData)) ? i->second.missing : kEmpty; +} + +void ChatFilters::moreChatsHide(FilterId id, bool localOnly) { + if (!localOnly) { + const auto api = &_owner->session().api(); + api->request(MTPchatlists_HideChatlistUpdates( + MTP_inputChatlistDialogFilter(MTP_int(id)) + )).send(); + } + + const auto i = _moreChatsData.find(id); + if (i != end(_moreChatsData)) { + if (const auto requestId = base::take(i->second.requestId)) { + _owner->session().api().request(requestId).cancel(); + } + i->second.missing = {}; + i->second.lastUpdate = crl::now(); + _moreChatsUpdated.fire_copy(id); + } +} + +void ChatFilters::loadMoreChatsList(FilterId id) { + Expects(id != 0); + + const auto i = ranges::find(_list, id, &ChatFilter::id); + if (i == end(_list) || !i->chatlist()) { + return; + } + + auto &entry = _moreChatsData[id]; + const auto now = crl::now(); + if (!entry.watching.lock() || entry.requestId) { + return; + } + const auto last = entry.lastUpdate; + const auto next = last ? (last + RequestUpdatesEach(_owner)) : 0; + if (next > now) { + if (!_moreChatsTimer.isActive()) { + _moreChatsTimer.callOnce(next - now); + } + return; + } + auto &api = _owner->session().api(); + entry.requestId = api.request(MTPchatlists_GetChatlistUpdates( + MTP_inputChatlistDialogFilter(MTP_int(id)) + )).done([=](const MTPchatlists_ChatlistUpdates &result) { + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + _owner->processChats(data.vchats()); + auto list = ranges::views::all( + data.vmissing_peers().v + ) | ranges::views::transform([&](const MTPPeer &peer) { + return _owner->peer(peerFromMTP(peer)); + }) | ranges::to_vector; + + auto &entry = _moreChatsData[id]; + entry.requestId = 0; + entry.lastUpdate = crl::now(); + if (!_moreChatsTimer.isActive()) { + _moreChatsTimer.callOnce(RequestUpdatesEach(_owner)); + } + if (entry.missing != list) { + entry.missing = std::move(list); + _moreChatsUpdated.fire_copy(id); + } + }).fail([=] { + auto &entry = _moreChatsData[id]; + entry.requestId = 0; + entry.lastUpdate = crl::now(); + }).send(); +} + +void ChatFilters::checkLoadMoreChatsLists() { + for (const auto &[id, entry] : _moreChatsData) { + loadMoreChatsList(id); + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index 4e2cf9cc0..987d55ebe 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/flags.h" +#include "base/timer.h" class History; @@ -16,21 +17,28 @@ class MainList; class Key; } // namespace Dialogs +namespace Ui { +struct MoreChatsBarContent; +} // namespace Ui + namespace Data { class Session; class ChatFilter final { public: - enum class Flag : uchar { - Contacts = 0x01, - NonContacts = 0x02, - Groups = 0x04, - Channels = 0x08, - Bots = 0x10, - NoMuted = 0x20, - NoRead = 0x40, - NoArchived = 0x80, + enum class Flag : ushort { + Contacts = (1 << 0), + NonContacts = (1 << 1), + Groups = (1 << 2), + Channels = (1 << 3), + Bots = (1 << 4), + NoMuted = (1 << 5), + NoRead = (1 << 6), + NoArchived = (1 << 7), + + Chatlist = (1 << 8), + HasMyLinks = (1 << 9), }; friend constexpr inline bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -45,6 +53,12 @@ public: std::vector> pinned, base::flat_set> never); + [[nodiscard]] ChatFilter withId(FilterId id) const; + [[nodiscard]] ChatFilter withTitle(const QString &title) const; + [[nodiscard]] ChatFilter withChatlist( + bool chatlist, + bool hasMyLinks) const; + [[nodiscard]] static ChatFilter FromTL( const MTPDialogFilter &data, not_null owner); @@ -54,6 +68,8 @@ public: [[nodiscard]] QString title() const; [[nodiscard]] QString iconEmoji() const; [[nodiscard]] Flags flags() const; + [[nodiscard]] bool chatlist() const; + [[nodiscard]] bool hasMyLinks() const; [[nodiscard]] const base::flat_set> &always() const; [[nodiscard]] const std::vector> &pinned() const; [[nodiscard]] const base::flat_set> &never() const; @@ -83,6 +99,17 @@ inline bool operator!=(const ChatFilter &a, const ChatFilter &b) { return !(a == b); } +struct ChatFilterLink { + FilterId id = 0; + QString url; + QString title; + std::vector> chats; + + friend inline bool operator==( + const ChatFilterLink &a, + const ChatFilterLink &b) = default; +}; + struct SuggestedFilter { ChatFilter filter; QString description; @@ -96,12 +123,14 @@ public: void setPreloaded(const QVector &result); void load(); + void reload(); void apply(const MTPUpdate &update); void set(ChatFilter filter); void remove(FilterId id); void moveAllToFront(); [[nodiscard]] const std::vector &list() const; [[nodiscard]] rpl::producer<> changed() const; + [[nodiscard]] rpl::producer isChatlistChanged() const; [[nodiscard]] bool loaded() const; [[nodiscard]] bool has() const; @@ -130,7 +159,32 @@ public: -> const std::vector &; [[nodiscard]] rpl::producer<> suggestedUpdated() const; + ChatFilterLink add( + FilterId id, + const MTPExportedChatlistInvite &update); + void edit( + FilterId id, + const QString &url, + const QString &title); + void destroy(FilterId id, const QString &url); + rpl::producer> chatlistLinks( + FilterId id) const; + void reloadChatlistLinks(FilterId id); + + [[nodiscard]] rpl::producer moreChatsContent( + FilterId id); + [[nodiscard]] const std::vector> &moreChats( + FilterId id) const; + void moreChatsHide(FilterId id, bool localOnly = false); + private: + struct MoreChatsData { + std::vector> missing; + crl::time lastUpdate = 0; + mtpRequestId requestId = 0; + std::weak_ptr watching; + }; + void load(bool force); void received(const QVector &list); bool applyOrder(const QVector &order); @@ -138,15 +192,20 @@ private: void applyInsert(ChatFilter filter, int position); void applyRemove(int position); + void checkLoadMoreChatsLists(); + void loadMoreChatsList(FilterId id); + const not_null _owner; std::vector _list; base::flat_map> _chatsLists; rpl::event_stream<> _listChanged; + rpl::event_stream _isChatlistChanged; mtpRequestId _loadRequestId = 0; mtpRequestId _saveOrderRequestId = 0; mtpRequestId _saveOrderAfterId = 0; bool _loaded = false; + bool _reloading = false; mtpRequestId _suggestedRequestId = 0; std::vector _suggested; @@ -156,6 +215,14 @@ private: std::deque _exceptionsToLoad; mtpRequestId _exceptionsLoadRequestId = 0; + base::flat_map> _chatlistLinks; + rpl::event_stream _chatlistLinksUpdated; + mtpRequestId _linksRequestId = 0; + + base::flat_map _moreChatsData; + rpl::event_stream _moreChatsUpdated; + base::Timer _moreChatsTimer; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index a1d8539e6..5852a9606 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kDefaultCoverThumbnailSize = 100; +constexpr auto kMaxAllowedPreloadPrefix = 6 * 1024 * 1024; const auto kLottieStickerDimensions = QSize( kStickerSideSize, @@ -326,14 +327,17 @@ Main::Session &DocumentData::session() const { void DocumentData::setattributes( const QVector &attributes) { + _duration = -1; _flags &= ~(Flag::ImageType | Flag::HasAttachedStickers | Flag::UseTextColor + | Flag::SilentVideo | kStreamingSupportedMask); _flags |= kStreamingSupportedUnknown; validateLottieSticker(); + _videoPreloadPrefix = 0; for (const auto &attribute : attributes) { attribute.match([&](const MTPDdocumentAttributeImageSize &data) { dimensions = QSize(data.vw().v, data.vh().v); @@ -389,13 +393,20 @@ void DocumentData::setattributes( : VideoDocument; if (data.is_round_message()) { _additional = std::make_unique(); - round()->duration = data.vduration().v; + } else if (const auto size = data.vpreload_prefix_size()) { + if (size->v > 0 && size->v < kMaxAllowedPreloadPrefix) { + _videoPreloadPrefix = size->v; + } } } else if (const auto info = sticker()) { info->type = StickerType::Webm; } - _duration = data.vduration().v; + _duration = crl::time( + base::SafeRound(data.vduration().v * 1000)); setMaybeSupportsStreaming(data.is_supports_streaming()); + if (data.is_nosound()) { + _flags |= Flag::SilentVideo; + } dimensions = QSize(data.vw().v, data.vh().v); }, [&](const MTPDdocumentAttributeAudio &data) { if (type == FileDocument) { @@ -408,14 +419,14 @@ void DocumentData::setattributes( } } if (const auto voiceData = voice() ? voice() : round()) { - voiceData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); voiceData->waveform = documentWaveformDecode( data.vwaveform().value_or_empty()); voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) : *ranges::max_element(voiceData->waveform); } else if (const auto songData = song()) { - songData->duration = data.vduration().v; + _duration = data.vduration().v * crl::time(1000); songData->title = qs(data.vtitle().value_or_empty()); songData->performer = qs(data.vperformer().value_or_empty()); refreshPossibleCoverThumbnail(); @@ -442,7 +453,10 @@ void DocumentData::setattributes( _additional = std::make_unique(); sticker()->type = StickerType::Webm; } - if (isAudioFile() || isAnimation() || isVoiceMessage()) { + if (isAudioFile() + || isAnimation() + || isVoiceMessage() + || storyMedia()) { setMaybeSupportsStreaming(true); } } @@ -794,7 +808,7 @@ QString DocumentData::loadingFilePath() const { bool DocumentData::displayLoading() const { return loading() - ? (!_loader->loadingLocal() || !_loader->autoLoading()) + ? !_loader->loadingLocal() : (uploading() && !waitingForAlbum()); } @@ -1318,6 +1332,7 @@ bool DocumentData::canBeStreamed(HistoryItem *item) const { return hasRemoteLocation() && supportsStreaming() && (!isVideoFile() + || storyMedia() || !ExternalVideoPlayer.value() || (item && !item->allowsForward())); } @@ -1330,6 +1345,23 @@ bool DocumentData::inappPlaybackFailed() const { return (_flags & Flag::StreamingPlaybackFailed); } +int DocumentData::videoPreloadPrefix() const { + return _videoPreloadPrefix; +} + +StorageFileLocation DocumentData::videoPreloadLocation() const { + return hasRemoteLocation() + ? StorageFileLocation( + _dc, + session().userId(), + MTP_inputDocumentFileLocation( + MTP_long(id), + MTP_long(_access), + MTP_bytes(_fileReference), + MTP_string())) + : StorageFileLocation(); +} + auto DocumentData::createStreamingLoader( Data::FileOrigin origin, bool forceRemoteLoader) const @@ -1516,19 +1548,16 @@ bool DocumentData::isVideoFile() const { return (type == VideoDocument); } -TimeId DocumentData::getDuration() const { - if (const auto song = this->song()) { - return std::max(song->duration, 0); - } else if (const auto voice = this->voice()) { - return std::max(voice->duration, 0); - } else if (isAnimation() || isVideoFile()) { - return std::max(_duration, 0); - } else if (const auto sticker = this->sticker()) { - if (sticker->isWebm()) { - return std::max(_duration, 0); - } - } - return -1; +bool DocumentData::isSilentVideo() const { + return _flags & Flag::SilentVideo; +} + +crl::time DocumentData::duration() const { + return std::max(_duration, crl::time()); +} + +bool DocumentData::hasDuration() const { + return _duration >= 0; } bool DocumentData::isImage() const { @@ -1594,6 +1623,19 @@ void DocumentData::setRemoteLocation( } } +void DocumentData::setStoryMedia(bool value) { + if (value) { + _flags |= Flag::StoryDocument; + setMaybeSupportsStreaming(true); + } else { + _flags &= ~Flag::StoryDocument; + } +} + +bool DocumentData::storyMedia() const { + return (_flags & Flag::StoryDocument); +} + void DocumentData::setContentUrl(const QString &url) { _url = url; } diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 42d49058d..59891c005 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -79,14 +79,12 @@ struct StickerData : public DocumentAdditionalData { }; struct SongData : public DocumentAdditionalData { - int32 duration = 0; QString title, performer; }; struct VoiceData : public DocumentAdditionalData { ~VoiceData(); - int duration = 0; VoiceWaveform waveform; char wavemax = 0; }; @@ -168,11 +166,13 @@ public: [[nodiscard]] bool isSongWithCover() const; [[nodiscard]] bool isAudioFile() const; [[nodiscard]] bool isVideoFile() const; + [[nodiscard]] bool isSilentVideo() const; [[nodiscard]] bool isAnimation() const; [[nodiscard]] bool isGifv() const; [[nodiscard]] bool isTheme() const; [[nodiscard]] bool isSharedMediaMusic() const; - [[nodiscard]] TimeId getDuration() const; + [[nodiscard]] crl::time duration() const; + [[nodiscard]] bool hasDuration() const; [[nodiscard]] bool isImage() const; void recountIsImage(); [[nodiscard]] bool supportsStreaming() const; @@ -233,6 +233,9 @@ public: [[nodiscard]] Storage::Cache::Key bigFileBaseCacheKey() const; + void setStoryMedia(bool value); + [[nodiscard]] bool storyMedia() const; + void setRemoteLocation( int32 dc, uint64 access, @@ -272,6 +275,8 @@ public: void setInappPlaybackFailed(); [[nodiscard]] bool inappPlaybackFailed() const; + [[nodiscard]] int videoPreloadPrefix() const; + [[nodiscard]] StorageFileLocation videoPreloadLocation() const; DocumentId id = 0; int64 size = 0; @@ -284,18 +289,20 @@ public: private: enum class Flag : ushort { - StreamingMaybeYes = 0x001, - StreamingMaybeNo = 0x002, - StreamingPlaybackFailed = 0x004, - ImageType = 0x008, - DownloadCancelled = 0x010, - LoadedInMediaCache = 0x020, - HasAttachedStickers = 0x040, - InlineThumbnailIsPath = 0x080, - ForceToCache = 0x100, - PremiumSticker = 0x200, - PossibleCoverThumbnail = 0x400, - UseTextColor = 0x800, + StreamingMaybeYes = 0x0001, + StreamingMaybeNo = 0x0002, + StreamingPlaybackFailed = 0x0004, + ImageType = 0x0008, + DownloadCancelled = 0x0010, + LoadedInMediaCache = 0x0020, + HasAttachedStickers = 0x0040, + InlineThumbnailIsPath = 0x0080, + ForceToCache = 0x0100, + PremiumSticker = 0x0200, + PossibleCoverThumbnail = 0x0400, + UseTextColor = 0x0800, + StoryDocument = 0x1000, + SilentVideo = 0x2000, }; using Flags = base::flags; friend constexpr bool is_flag_type(Flag) { return true; }; @@ -341,6 +348,7 @@ private: const not_null _owner; + int _videoPreloadPrefix = 0; // Two types of location: from MTProto by dc+access or from web by url int32 _dc = 0; uint64 _access = 0; @@ -356,10 +364,10 @@ private: std::unique_ptr _replyPreview; std::weak_ptr _media; PhotoData *_goodThumbnailPhoto = nullptr; + crl::time _duration = -1; Core::FileLocation _location; std::unique_ptr _additional; - int32 _duration = -1; mutable Flags _flags = kStreamingSupportedUnknown; GoodThumbnailState _goodThumbnailState = GoodThumbnailState(); std::unique_ptr _loader; diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 2a03aaa7c..2839eeb76 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -254,7 +254,10 @@ void ResolveDocument( && !document->filepath().isEmpty()) { File::Launch(document->location(false).fname); } else if (controller) { - controller->openDocument(document, msgId, topicRootId, true); + controller->openDocument( + document, + true, + { msgId, topicRootId }); } }; diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 05080dfcb..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(); + } } } @@ -865,7 +885,7 @@ not_null DownloadManager::generateItem( ? previousItem->history() : session->data().history(session->user()); const auto flags = MessageFlag::FakeHistoryItem; - const auto replyTo = MsgId(); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(); const auto date = base::unixtime::now(); const auto postAuthor = QString(); 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_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index ee9a24ca4..d37663208 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -40,10 +40,8 @@ struct FileReferenceAccumulator { }); } void push(const MTPPage &data) { - data.match([&](const auto &data) { - push(data.vphotos()); - push(data.vdocuments()); - }); + push(data.data().vphotos()); + push(data.data().vdocuments()); } void push(const MTPWallPaper &data) { data.match([&](const MTPDwallPaper &data) { @@ -52,12 +50,12 @@ struct FileReferenceAccumulator { }); } void push(const MTPTheme &data) { - data.match([&](const MTPDtheme &data) { - push(data.vdocument()); - }); + push(data.data().vdocument()); } void push(const MTPWebPageAttribute &data) { - data.match([&](const MTPDwebPageAttributeTheme &data) { + data.match([&](const MTPDwebPageAttributeStory &data) { + push(data.vstory()); + }, [&](const MTPDwebPageAttributeTheme &data) { push(data.vdocuments()); }); } @@ -97,11 +95,20 @@ struct FileReferenceAccumulator { push(data.vphoto()); }, [&](const MTPDmessageActionSuggestProfilePhoto &data) { push(data.vphoto()); + }, [&](const MTPDmessageActionSetChatWallPaper &data) { + push(data.vwallpaper()); }, [](const auto &data) { }); }, [](const MTPDmessageEmpty &data) { }); } + void push(const MTPStoryItem &data) { + data.match([&](const MTPDstoryItem &data) { + push(data.vmedia()); + }, [](const MTPDstoryItemDeleted &) { + }, [](const MTPDstoryItemSkipped &) { + }); + } void push(const MTPmessages_Messages &data) { data.match([](const MTPDmessages_messagesNotModified &) { }, [&](const auto &data) { @@ -114,9 +121,7 @@ struct FileReferenceAccumulator { }); } void push(const MTPusers_UserFull &data) { - data.match([&](const auto &data) { - push(data.vfull_user().data().vpersonal_photo()); - }); + push(data.data().vfull_user().data().vpersonal_photo()); } void push(const MTPmessages_RecentStickers &data) { data.match([&](const MTPDmessages_recentStickers &data) { @@ -149,9 +154,10 @@ struct FileReferenceAccumulator { }); } void push(const MTPhelp_PremiumPromo &data) { - data.match([&](const MTPDhelp_premiumPromo &data) { - push(data.vvideos()); - }); + push(data.data().vvideos()); + } + void push(const MTPstories_Stories &data) { + push(data.data().vstories()); } UpdatedFileReferences result; @@ -214,6 +220,10 @@ UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data) { return GetFileReferencesHelper(data); } +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data) { + return GetFileReferencesHelper(data); +} + UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data) { return GetFileReferencesHelper(data); } diff --git a/Telegram/SourceFiles/data/data_file_origin.h b/Telegram/SourceFiles/data/data_file_origin.h index 195ae9188..b3185d2df 100644 --- a/Telegram/SourceFiles/data/data_file_origin.h +++ b/Telegram/SourceFiles/data/data_file_origin.h @@ -120,6 +120,20 @@ struct FileOriginPremiumPreviews { } }; +struct FileOriginStory { + FileOriginStory(PeerId peerId, StoryId storyId) + : peerId(peerId) + , storyId(storyId) { + } + + PeerId peerId = 0; + StoryId storyId = 0; + + friend inline auto operator<=>( + FileOriginStory, + FileOriginStory) = default; +}; + struct FileOrigin { using Variant = std::variant< v::null_t, @@ -132,7 +146,8 @@ struct FileOrigin { FileOriginWallpaper, FileOriginTheme, FileOriginRingtones, - FileOriginPremiumPreviews>; + FileOriginPremiumPreviews, + FileOriginStory>; FileOrigin() = default; FileOrigin(FileOriginMessage data) : data(data) { @@ -155,6 +170,8 @@ struct FileOrigin { } FileOrigin(FileOriginPremiumPreviews data) : data(data) { } + FileOrigin(FileOriginStory data) : data(data) { + } explicit operator bool() const { return !v::is_null(data); @@ -204,6 +221,7 @@ UpdatedFileReferences GetFileReferences(const MTPTheme &data); UpdatedFileReferences GetFileReferences( const MTPaccount_SavedRingtones &data); UpdatedFileReferences GetFileReferences(const MTPhelp_PremiumPromo &data); +UpdatedFileReferences GetFileReferences(const MTPstories_Stories &data); // Admin Log Event. UpdatedFileReferences GetFileReferences(const MTPMessageMedia &data); diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 4faa19f71..743487c85 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -39,6 +39,21 @@ constexpr auto kShowChatNamesCount = 8; not_null folder) { const auto &list = folder->lastHistories(); if (list.empty()) { + if (const auto storiesUnread = folder->storiesUnreadCount()) { + return { + tr::lng_contacts_stories_status_new( + tr::now, + lt_count, + storiesUnread), + }; + } else if (const auto storiesCount = folder->storiesCount()) { + return { + tr::lng_contacts_stories_status( + tr::now, + lt_count, + storiesCount), + }; + } return {}; } @@ -301,6 +316,33 @@ void Folder::validateListEntryCache() { Ui::ItemTextDefaultOptions()); } +void Folder::updateStoriesCount(int count, int unread) { + if (_storiesCount == count && _storiesUnreadCount == unread) { + return; + } + const auto limit = (1 << 16) - 1; + const auto was = (_storiesCount > 0); + _storiesCount = std::min(count, limit); + _storiesUnreadCount = std::min(unread, limit); + const auto now = (_storiesCount > 0); + if (was == now) { + updateChatListEntryPostponed(); + } else if (now) { + updateChatListSortPosition(); + } else { + updateChatListExistence(); + } + ++_chatListViewVersion; +} + +int Folder::storiesCount() const { + return _storiesCount; +} + +int Folder::storiesUnreadCount() const { + return _storiesUnreadCount; +} + void Folder::requestChatListMessage() { if (!chatListMessageKnown()) { owner().histories().requestDialogEntry(this); @@ -339,7 +381,7 @@ int Folder::fixedOnTopIndex() const { } bool Folder::shouldBeInChatList() const { - return !_chatsList.empty(); + return !_chatsList.empty() || (_storiesCount > 0); } Dialogs::UnreadState Folder::chatListUnreadState() const { diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index 9e62fcabe..7008adac0 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -75,6 +75,10 @@ public: return _listEntryCache; } + void updateStoriesCount(int count, int unread); + [[nodiscard]] int storiesCount() const; + [[nodiscard]] int storiesUnreadCount() const; + private: void indexNameParts(); @@ -104,6 +108,9 @@ private: int _chatListViewVersion = 0; //rpl::variable _unreadPosition; + uint16_t _storiesCount = 0; + uint16_t _storiesUnreadCount = 0; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 5925f303d..7b5391a19 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -767,6 +767,7 @@ void ForumTopic::maybeSetLastMessage(not_null item) { Expects(item->topicRootId() == _rootId); if (!_lastMessage + || !(*_lastMessage) || ((*_lastMessage)->date() < item->date()) || ((*_lastMessage)->date() == item->date() && (*_lastMessage)->id < item->id)) { diff --git a/Telegram/SourceFiles/data/data_groups.h b/Telegram/SourceFiles/data/data_groups.h index b58c1a551..f8cf259ae 100644 --- a/Telegram/SourceFiles/data/data_groups.h +++ b/Telegram/SourceFiles/data/data_groups.h @@ -41,7 +41,6 @@ private: not_null _data; std::map _groups; - std::map _alias; }; diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 7c80d75ac..4ca3cb635 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_scheduled_messages.h" +#include "data/data_user.h" #include "base/unixtime.h" #include "base/random.h" #include "main/main_session.h" @@ -31,6 +32,29 @@ constexpr auto kReadRequestTimeout = 3 * crl::time(1000); } // namespace +MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo) { + if (replyTo.storyId) { + if (const auto peer = owner->peerLoaded(replyTo.storyId.peer)) { + if (const auto user = peer->asUser()) { + return MTP_inputReplyToStory( + user->inputUser, + MTP_int(replyTo.storyId.story)); + } + } + } else if (replyTo.msgId || replyTo.topicRootId) { + using Flag = MTPDinputReplyToMessage::Flag; + return MTP_inputReplyToMessage( + (replyTo.topicRootId + ? MTP_flags(Flag::f_top_msg_id) + : MTP_flags(0)), + MTP_int(replyTo.msgId ? replyTo.msgId : replyTo.topicRootId), + MTP_int(replyTo.topicRootId)); + } + return MTPInputReplyTo(); +} + Histories::Histories(not_null owner) : _owner(owner) , _readRequestsTimer([=] { sendReadRequests(); }) { @@ -885,23 +909,24 @@ bool Histories::isCreatingTopic( int Histories::sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail) { - if (isCreatingTopic(history, topicRootId)) { + if (isCreatingTopic(history, replyTo.topicRootId)) { const auto id = ++_requestAutoincrement; - const auto creatingId = FullMsgId(history->peer->id, topicRootId); + const auto creatingId = FullMsgId( + history->peer->id, + replyTo.topicRootId); auto i = _creatingTopics.find(creatingId); if (i == end(_creatingTopics)) { - sendCreateTopicRequest(history, topicRootId); + sendCreateTopicRequest(history, replyTo.topicRootId); i = _creatingTopics.emplace(creatingId).first; } i->second.push_back({ .randomId = randomId, - .replyTo = replyTo, + .replyTo = replyTo.msgId, .message = std::move(message), .done = std::move(done), .fail = std::move(fail), @@ -910,9 +935,12 @@ int Histories::sendPreparedMessage( _creatingTopicRequests.emplace(id); return id; } - const auto realReply = convertTopicReplyTo(history, replyTo); - const auto realRoot = convertTopicReplyTo(history, topicRootId); - return v::match(message(realReply, realRoot), [&](const auto &request) { + const auto realReplyTo = FullReplyTo{ + .msgId = convertTopicReplyToId(history, replyTo.msgId), + .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), + .storyId = replyTo.storyId, + }; + return v::match(message(_owner, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { const auto session = &_owner->session(); @@ -955,8 +983,10 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { _creatingTopicRequests.erase(entry.requestId); sendPreparedMessage( history, - entry.replyTo, - realRoot, + FullReplyTo{ + .msgId = entry.replyTo, + .topicRootId = realRoot, + }, entry.randomId, std::move(entry.message), std::move(entry.done), @@ -976,14 +1006,14 @@ void Histories::checkTopicCreated(FullMsgId rootId, MsgId realRoot) { } } -MsgId Histories::convertTopicReplyTo( +MsgId Histories::convertTopicReplyToId( not_null history, - MsgId replyTo) const { - if (!replyTo) { + MsgId replyToId) const { + if (!replyToId) { return {}; } - const auto i = _createdTopicIds.find({ history->peer->id, replyTo }); - return (i != end(_createdTopicIds)) ? i->second : replyTo; + const auto i = _createdTopicIds.find({ history->peer->id, replyToId }); + return (i != end(_createdTopicIds)) ? i->second : replyToId; } void Histories::checkPostponed(not_null history, int id) { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index 05f7534f4..c98dc352c 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -26,6 +26,10 @@ namespace Data { class Session; class Folder; +[[nodiscard]] MTPInputReplyTo ReplyToForMTP( + not_null owner, + FullReplyTo replyTo); + class Histories final { public: enum class RequestType : uchar { @@ -102,29 +106,27 @@ public: MTPmessages_SendMultiMedia>; int sendPreparedMessage( not_null history, - MsgId replyTo, - MsgId topicRootId, + FullReplyTo replyTo, uint64 randomId, - Fn message, + Fn, FullReplyTo)> message, Fn done, Fn fail); struct ReplyToPlaceholder { }; - struct TopicRootPlaceholder { - }; template - static Fn PrepareMessage( - const Args &...args) { - return [=](MsgId replyTo, MsgId topicRootId) -> RequestType { - return { ReplaceReplyIds(args, replyTo, topicRootId)... }; + static auto PrepareMessage(const Args &...args) + -> Fn, FullReplyTo)> { + return [=](not_null owner, FullReplyTo replyTo) + -> RequestType { + return { ReplaceReplyIds(owner, args, replyTo)... }; }; } void checkTopicCreated(FullMsgId rootId, MsgId realRoot); - [[nodiscard]] MsgId convertTopicReplyTo( + [[nodiscard]] MsgId convertTopicReplyToId( not_null history, - MsgId replyTo) const; + MsgId replyToId) const; private: struct PostponedHistoryRequest { @@ -151,7 +153,7 @@ private: struct DelayedByTopicMessage { uint64 randomId = 0; MsgId replyTo = 0; - Fn message; + Fn, FullReplyTo)> message; Fn done; Fn fail; int requestId = 0; @@ -166,11 +168,12 @@ private: }; template - static auto ReplaceReplyIds(Arg arg, MsgId replyTo, MsgId topicRootId) { + static auto ReplaceReplyIds( + not_null owner, + Arg arg, + FullReplyTo replyTo) { if constexpr (std::is_same_v) { - return MTP_int(replyTo); - } else if constexpr (std::is_same_v) { - return MTP_int(topicRootId); + return ReplyToForMTP(owner, replyTo); } else { return arg; } diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 151bb3f89..7a36a5515 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_slot_machine.h" #include "history/view/media/history_view_dice.h" #include "history/view/media/history_view_service_box.h" +#include "history/view/media/history_view_story_mention.h" #include "history/view/media/history_view_premium_gift.h" #include "history/view/media/history_view_userpic_suggestion.h" #include "dialogs/ui/dialogs_message_view.h" @@ -56,13 +57,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_poll.h" #include "data/data_channel.h" #include "data/data_file_origin.h" +#include "data/data_stories.h" +#include "data/data_story.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "core/application.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "lang/lang_keys.h" #include "storage/file_upload.h" -#include "window/window_session_controller.h" // Window::Show +#include "window/window_session_controller.h" // SessionController::uiShow. #include "apiwrap.h" #include "styles/style_chat.h" #include "styles/style_dialogs.h" @@ -72,6 +75,7 @@ namespace { constexpr auto kFastRevokeRestriction = 24 * 60 * TimeId(60); constexpr auto kMaxPreviewImages = 3; +constexpr auto kLoadingStoryPhotoId = PhotoId(0x7FFF'DEAD'FFFF'FFFFULL); using ItemPreview = HistoryView::ItemPreview; using ItemPreviewImage = HistoryView::ItemPreviewImage; @@ -392,7 +396,7 @@ const Invoice *Media::invoice() const { return nullptr; } -Data::CloudImage *Media::location() const { +CloudImage *Media::location() const { return nullptr; } @@ -400,6 +404,22 @@ PollData *Media::poll() const { return nullptr; } +const WallPaper *Media::paper() const { + return nullptr; +} + +FullStoryId Media::storyId() const { + return {}; +} + +bool Media::storyExpired(bool revalidate) { + return false; +} + +bool Media::storyMention() const { + return false; +} + bool Media::uploading() const { return false; } @@ -1238,7 +1258,7 @@ std::unique_ptr MediaLocation::clone(not_null parent) { _description); } -Data::CloudImage *MediaLocation::location() const { +CloudImage *MediaLocation::location() const { return _location; } @@ -1824,12 +1844,11 @@ ClickHandlerPtr MediaDice::MakeHandler( auto config = Ui::Toast::Config{ .text = { tr::lng_about_random(tr::now, lt_emoji, emoji) }, .st = &st::historyDiceToast, - .durationMs = Ui::Toast::kDefaultDuration * 2, + .duration = Ui::Toast::kDefaultDuration * 2, .multiline = true, }; - if (Data::CanSend(history->peer, ChatRestriction::SendOther)) { - auto link = Ui::Text::Link( - tr::lng_about_random_send(tr::now).toUpper()); + if (CanSend(history->peer, ChatRestriction::SendOther)) { + 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)); @@ -1853,9 +1872,7 @@ ClickHandlerPtr MediaDice::MakeHandler( const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto strong = weak.get()) { - ShownToast = Ui::Toast::Show( - Window::Show(strong).toastParent(), - config); + ShownToast = strong->showToast(std::move(config)); } else { ShownToast = Ui::Toast::Show(config); } @@ -1883,10 +1900,6 @@ int MediaGiftBox::months() const { return _months; } -bool MediaGiftBox::allowsRevoke(TimeId now) const { - return false; -} - TextWithEntities MediaGiftBox::notificationText() const { return {}; } @@ -1899,10 +1912,6 @@ TextForMimeData MediaGiftBox::clipboardText() const { return {}; } -bool MediaGiftBox::forceForwardedInfo() const { - return false; -} - bool MediaGiftBox::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } @@ -1928,4 +1937,207 @@ void MediaGiftBox::setActivated(bool activated) { _activated = activated; } +MediaWallPaper::MediaWallPaper( + not_null parent, + const WallPaper &paper) +: Media(parent) +, _paper(paper) { +} + +MediaWallPaper::~MediaWallPaper() = default; + +std::unique_ptr MediaWallPaper::clone(not_null parent) { + return std::make_unique(parent, _paper); +} + +const WallPaper *MediaWallPaper::paper() const { + return &_paper; +} + +TextWithEntities MediaWallPaper::notificationText() const { + return {}; +} + +QString MediaWallPaper::pinnedTextSubstring() const { + return {}; +} + +TextForMimeData MediaWallPaper::clipboardText() const { + return {}; +} + +bool MediaWallPaper::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaWallPaper::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +std::unique_ptr MediaWallPaper::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + return std::make_unique( + message, + std::make_unique(message, _paper)); +} + +MediaStory::MediaStory( + not_null parent, + FullStoryId storyId, + bool mention) +: Media(parent) +, _storyId(storyId) +, _mention(mention) { + const auto owner = &parent->history()->owner(); + owner->registerStoryItem(storyId, parent); + + const auto stories = &owner->stories(); + if (const auto maybeStory = stories->lookup(storyId)) { + if (!_mention) { + parent->setText((*maybeStory)->caption()); + } + } else { + if (maybeStory.error() == NoStory::Unknown) { + stories->resolve(storyId, crl::guard(this, [=] { + if (const auto maybeStory = stories->lookup(storyId)) { + if (!_mention) { + parent->setText((*maybeStory)->caption()); + } + } else { + _expired = true; + } + if (_mention) { + parent->updateStoryMentionText(); + } + parent->history()->owner().requestItemViewRefresh(parent); + })); + } else { + _expired = true; + } + } +} + +MediaStory::~MediaStory() { + const auto owner = &parent()->history()->owner(); + owner->unregisterStoryItem(_storyId, parent()); +} + +std::unique_ptr MediaStory::clone(not_null parent) { + return std::make_unique(parent, _storyId, false); +} + +FullStoryId MediaStory::storyId() const { + return _storyId; +} + +bool MediaStory::storyExpired(bool revalidate) { + if (revalidate) { + const auto stories = &parent()->history()->owner().stories(); + if (const auto maybeStory = stories->lookup(_storyId)) { + _expired = false; + } else if (maybeStory.error() == Data::NoStory::Deleted) { + _expired = true; + } + } + return _expired; +} + +bool MediaStory::storyMention() const { + return _mention; +} + +TextWithEntities MediaStory::notificationText() const { + const auto stories = &parent()->history()->owner().stories(); + const auto maybeStory = stories->lookup(_storyId); + return WithCaptionNotificationText( + ((_expired + || (!maybeStory + && maybeStory.error() == Data::NoStory::Deleted)) + ? tr::lng_in_dlg_story_expired + : tr::lng_in_dlg_story)(tr::now), + (maybeStory + ? (*maybeStory)->caption() + : TextWithEntities())); +} + +QString MediaStory::pinnedTextSubstring() const { + return tr::lng_action_pinned_media_story(tr::now); +} + +TextForMimeData MediaStory::clipboardText() const { + return WithCaptionClipboardText( + (_expired + ? tr::lng_in_dlg_story_expired + : tr::lng_in_dlg_story)(tr::now), + parent()->clipboardText()); +} + +bool MediaStory::dropForwardedInfo() const { + return true; +} + +bool MediaStory::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaStory::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +not_null MediaStory::LoadingStoryPhoto( + not_null owner) { + return owner->photo(kLoadingStoryPhotoId); +} + +std::unique_ptr MediaStory::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + const auto spoiler = false; + const auto stories = &parent()->history()->owner().stories(); + const auto maybeStory = stories->lookup(_storyId); + if (!maybeStory) { + if (!_mention) { + realParent->setText(TextWithEntities()); + } + if (maybeStory.error() == Data::NoStory::Deleted) { + _expired = true; + return nullptr; + } + _expired = false; + if (_mention) { + return nullptr; + } + return std::make_unique( + message, + realParent, + LoadingStoryPhoto(&realParent->history()->owner()), + spoiler); + } + _expired = false; + const auto story = *maybeStory; + if (_mention) { + return std::make_unique( + message, + std::make_unique(message, story)); + } else { + realParent->setText(story->caption()); + if (const auto photo = story->photo()) { + return std::make_unique( + message, + realParent, + photo, + spoiler); + } else { + return std::make_unique( + message, + realParent, + story->document(), + spoiler); + } + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index a21df7354..7649a37ed 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/weak_ptr.h" #include "data/data_location.h" +#include "data/data_wall_paper.h" class Image; class History; @@ -35,6 +37,8 @@ struct ToPreviewOptions; namespace Data { class CloudImage; +class WallPaper; +class Session; enum class CallFinishReason : char { Missed, @@ -56,6 +60,7 @@ struct Call { int duration = 0; FinishReason finishReason = FinishReason::Missed; bool video = false; + }; struct ExtendedPreview { @@ -105,8 +110,12 @@ public: virtual const Call *call() const; virtual GameData *game() const; virtual const Invoice *invoice() const; - virtual Data::CloudImage *location() const; + virtual CloudImage *location() const; virtual PollData *poll() const; + virtual const WallPaper *paper() const; + virtual FullStoryId storyId() const; + virtual bool storyExpired(bool revalidate = false); + virtual bool storyMention() const; virtual bool uploading() const; virtual Storage::SharedMediaTypesMask sharedMediaTypes() const; @@ -291,7 +300,7 @@ public: std::unique_ptr clone(not_null parent) override; - Data::CloudImage *location() const override; + CloudImage *location() const override; ItemPreview toPreview(ToPreviewOptions options) const override; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; @@ -306,7 +315,7 @@ public: private: LocationPoint _point; - not_null _location; + not_null _location; QString _title; QString _description; @@ -517,11 +526,9 @@ public: [[nodiscard]] bool activated() const; void setActivated(bool activated); - bool allowsRevoke(TimeId now) const override; TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; TextForMimeData clipboardText() const override; - bool forceForwardedInfo() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; @@ -537,6 +544,67 @@ private: }; +class MediaWallPaper final : public Media { +public: + MediaWallPaper(not_null parent, const WallPaper &paper); + ~MediaWallPaper(); + + std::unique_ptr clone(not_null parent) override; + + const WallPaper *paper() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + +private: + const WallPaper _paper; + +}; + +class MediaStory final : public Media, public base::has_weak_ptr { +public: + MediaStory( + not_null parent, + FullStoryId storyId, + bool mention); + ~MediaStory(); + + std::unique_ptr clone(not_null parent) override; + + FullStoryId storyId() const override; + bool storyExpired(bool revalidate = false) override; + bool storyMention() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + bool dropForwardedInfo() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + + [[nodiscard]] static not_null LoadingStoryPhoto( + not_null owner); + +private: + const FullStoryId _storyId; + const bool _mention = false; + bool _expired = false; + +}; + [[nodiscard]] TextForMimeData WithCaptionClipboardText( const QString &attachType, TextForMimeData &&caption); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index a918aa08c..7aa820101 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_account.h" #include "main/main_app_config.h" +#include "main/session/send_as_peers.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_histories.h" @@ -82,6 +83,30 @@ constexpr auto kTopReactionsLimit = 14; : config->get("reactions_user_max_default", 1); } +bool IsMyRecent( + const MTPDmessagePeerReaction &data, + const ReactionId &id, + not_null peer, + const base::flat_map< + ReactionId, + std::vector> &recent, + bool ignoreChosen) { + if (peer->id == peer->session().userPeerId()) { + return true; + } else if (!ignoreChosen) { + return data.is_my(); + } + const auto j = recent.find(id); + if (j == end(recent)) { + return false; + } + const auto k = ranges::find( + j->second, + peer, + &RecentReaction::peer); + return (k != end(j->second)) && k->my; +} + } // namespace PossibleItemReactionsRef LookupPossibleReactions( @@ -356,7 +381,7 @@ void Reactions::preloadImageFor(const ReactionId &id) { loadImage(set, document, !i->centerIcon); } else if (!_waitingForList) { _waitingForList = true; - refreshRecent(); + refreshDefault(); } } @@ -399,7 +424,8 @@ QImage Reactions::resolveImageFor( const auto frameSize = set.fromSelectAnimation ? (size / 2) : size; - image = set.icon->frame().scaled( + // Must not be colored to text. + image = set.icon->frame(QColor()).scaled( frameSize * factor, frameSize * factor, Qt::IgnoreAspectRatio, @@ -480,6 +506,7 @@ void Reactions::setAnimatedIcon(ImageSet &set) { set.icon = Ui::MakeAnimatedIcon({ .generator = DocumentIconFrameGenerator(set.media), .sizeOverride = QSize(size, size), + .colorized = set.media->owner()->emojiUsesTextColor(), }); set.media = nullptr; } @@ -951,7 +978,6 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { Expects(!id.empty()); const auto history = _item->history(); - const auto self = history->session().user(); const auto myLimit = SentReactionsLimit(_item); if (ranges::contains(chosen(), id)) { return; @@ -966,20 +992,28 @@ void MessageReactions::add(const ReactionId &id, bool addToRecent) { const auto removed = !--one.count; const auto j = _recent.find(one.id); if (j != end(_recent)) { - j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), - end(j->second)); - if (j->second.empty()) { + if (removed) { + j->second.clear(); _recent.erase(j); } else { - Assert(!removed); + j->second.erase( + ranges::remove(j->second, true, &RecentReaction::my), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } } } return removed; }), end(_list)); - if (_item->canViewReactions() || history->peer->isUser()) { + const auto peer = history->peer; + if (_item->canViewReactions() || peer->isUser()) { auto &list = _recent[id]; - list.insert(begin(list), RecentReaction{ self }); + const auto from = peer->session().sendAsPeers().resolveChosen(peer); + list.insert(begin(list), RecentReaction{ + .peer = from, + .my = true, + }); } const auto i = ranges::find(_list, id, &MessageReaction::id); if (i != end(_list)) { @@ -1013,13 +1047,16 @@ void MessageReactions::remove(const ReactionId &id) { _list.erase(i); } if (j != end(_recent)) { - j->second.erase( - ranges::remove(j->second, self, &RecentReaction::peer), - end(j->second)); - if (j->second.empty()) { + if (removed) { + j->second.clear(); _recent.erase(j); } else { - Assert(!removed); + j->second.erase( + ranges::remove(j->second, true, &RecentReaction::my), + end(j->second)); + if (j->second.empty()) { + _recent.erase(j); + } } } auto &owner = history->owner(); @@ -1029,7 +1066,8 @@ void MessageReactions::remove(const ReactionId &id) { bool MessageReactions::checkIfChanged( const QVector &list, - const QVector &recent) const { + const QVector &recent, + bool min) const { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1061,13 +1099,18 @@ bool MessageReactions::checkIfChanged( for (const auto &reaction : recent) { reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); - if (ranges::contains(_list, id, &MessageReaction::id)) { - parsed[id].push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); + if (!ranges::contains(_list, id, &MessageReaction::id)) { + return; } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + parsed[id].push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } return !ranges::equal(_recent, parsed, []( @@ -1076,7 +1119,7 @@ bool MessageReactions::checkIfChanged( return ranges::equal(a.second, b.second, []( const RecentReaction &a, const RecentReaction &b) { - return (a.peer == b.peer) && (a.big == b.big); + return (a.peer == b.peer) && (a.big == b.big) && (a.my == b.my); }); }); } @@ -1084,7 +1127,7 @@ bool MessageReactions::checkIfChanged( bool MessageReactions::change( const QVector &list, const QVector &recent, - bool ignoreChosen) { + bool min) { auto &owner = _item->history()->owner(); if (owner.reactions().sending(_item)) { // We'll apply non-stale data from the request response. @@ -1097,7 +1140,7 @@ bool MessageReactions::change( count.match([&](const MTPDreactionCount &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto &chosen = data.vchosen_order(); - if (!ignoreChosen && chosen) { + if (!min && chosen) { order[id] = chosen->v; } const auto i = ranges::find(_list, id, &MessageReaction::id); @@ -1107,10 +1150,10 @@ bool MessageReactions::change( _list.push_back({ .id = id, .count = nowCount, - .my = (!ignoreChosen && chosen) + .my = (!min && chosen) }); } else { - const auto nowMy = ignoreChosen ? i->my : chosen.has_value(); + const auto nowMy = min ? i->my : chosen.has_value(); if (i->count != nowCount || i->my != nowMy) { i->count = nowCount; i->my = nowMy; @@ -1120,13 +1163,13 @@ bool MessageReactions::change( existing.emplace(id); }); } - if (!ignoreChosen && !order.empty()) { - const auto min = std::numeric_limits::min(); + if (!min && !order.empty()) { + const auto minimal = std::numeric_limits::min(); const auto proj = [&](const MessageReaction &reaction) { - return reaction.my ? order[reaction.id] : min; + return reaction.my ? order[reaction.id] : minimal; }; const auto correctOrder = [&] { - auto previousOrder = min; + auto previousOrder = minimal; for (const auto &reaction : _list) { const auto nowOrder = proj(reaction); if (nowOrder < previousOrder) { @@ -1156,16 +1199,22 @@ bool MessageReactions::change( reaction.match([&](const MTPDmessagePeerReaction &data) { const auto id = ReactionFromMTP(data.vreaction()); const auto i = ranges::find(_list, id, &MessageReaction::id); - if (i != end(_list)) { - auto &list = parsed[id]; - if (list.size() < i->count) { - list.push_back(RecentReaction{ - .peer = owner.peer(peerFromMTP(data.vpeer_id())), - .unread = data.is_unread(), - .big = data.is_big(), - }); - } + if (i == end(_list)) { + return; } + auto &list = parsed[id]; + if (list.size() >= i->count) { + return; + } + const auto peerId = peerFromMTP(data.vpeer_id()); + const auto peer = owner.peer(peerId); + const auto my = IsMyRecent(data, id, peer, _recent, min); + list.push_back({ + .peer = peer, + .unread = data.is_unread(), + .big = data.is_big(), + .my = my, + }); }); } if (_recent != parsed) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index c1aeb6f73..61b29107d 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -225,14 +225,14 @@ struct RecentReaction { not_null peer; bool unread = false; bool big = false; + bool my = false; - inline friend constexpr bool operator==( - const RecentReaction &a, - const RecentReaction &b) noexcept { - return (a.peer.get() == b.peer.get()) - && (a.unread == b.unread) - && (a.big == b.big); - } + friend inline auto operator<=>( + const RecentReaction &a, + const RecentReaction &b) = default; + friend inline bool operator==( + const RecentReaction &a, + const RecentReaction &b) = default; }; class MessageReactions final { @@ -247,7 +247,8 @@ public: bool ignoreChosen); [[nodiscard]] bool checkIfChanged( const QVector &list, - const QVector &recent) const; + const QVector &recent, + bool ignoreChosen) const; [[nodiscard]] const std::vector &list() const; [[nodiscard]] auto recent() const -> const base::flat_map> &; diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index ac39eddfb..49357d88f 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -51,14 +51,49 @@ Q_DECLARE_METATYPE(MsgId); return MsgId(a.bare - b.bare); } +using StoryId = int32; + +struct FullStoryId { + PeerId peer = 0; + StoryId story = 0; + + [[nodiscard]] bool valid() const { + return peer != 0 && story != 0; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullStoryId, FullStoryId) = default; + friend inline bool operator==(FullStoryId, FullStoryId) = default; +}; + +struct FullReplyTo { + MsgId msgId = 0; + MsgId topicRootId = 0; + FullStoryId storyId; + + [[nodiscard]] bool valid() const { + return msgId || storyId; + } + explicit operator bool() const { + return valid(); + } + friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; + friend inline bool operator==(FullReplyTo, FullReplyTo) = default; +}; + constexpr auto StartClientMsgId = MsgId(0x01 - (1LL << 58)); constexpr auto ClientMsgIds = (1LL << 31); constexpr auto EndClientMsgId = MsgId(StartClientMsgId.bare + ClientMsgIds); +constexpr auto StartStoryMsgId = MsgId(EndClientMsgId.bare + 1); +constexpr auto ServerMaxStoryId = StoryId(1 << 30); +constexpr auto StoryMsgIds = int64(ServerMaxStoryId); +constexpr auto EndStoryMsgId = MsgId(StartStoryMsgId.bare + StoryMsgIds); constexpr auto ServerMaxMsgId = MsgId(1LL << 56); constexpr auto ScheduledMsgIdsRange = (1LL << 32); constexpr auto ShowAtUnreadMsgId = MsgId(0); -constexpr auto SpecialMsgIdShift = EndClientMsgId.bare; +constexpr auto SpecialMsgIdShift = EndStoryMsgId.bare; constexpr auto ShowAtTheEndMsgId = MsgId(SpecialMsgIdShift + 1); constexpr auto SwitchAtTopMsgId = MsgId(SpecialMsgIdShift + 2); constexpr auto ShowAndStartBotMsgId = MsgId(SpecialMsgIdShift + 4); @@ -81,6 +116,20 @@ static_assert(-(SpecialMsgIdShift + 0xFF) > ServerMaxMsgId); return MsgId(StartClientMsgId.bare + index); } +[[nodiscrd]] constexpr inline bool IsStoryMsgId(MsgId id) noexcept { + return (id >= StartStoryMsgId && id < EndStoryMsgId); +} +[[nodiscard]] constexpr inline StoryId StoryIdFromMsgId(MsgId id) noexcept { + Expects(IsStoryMsgId(id)); + + return StoryId(id.bare - StartStoryMsgId.bare); +} +[[nodiscard]] constexpr inline MsgId StoryIdToMsgId(StoryId id) noexcept { + Expects(id >= 0); + + return MsgId(StartStoryMsgId.bare + id); +} + [[nodiscard]] constexpr inline bool IsServerMsgId(MsgId id) noexcept { return (id > 0 && id < ServerMaxMsgId); } @@ -145,4 +194,13 @@ struct hash : private hash { } }; +template <> +struct hash { + size_t operator()(FullStoryId value) const { + return QtPrivate::QHashCombine().operator()( + std::hash()(value.peer.value), + value.story); + } +}; + } // namespace std diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 560736b46..c096853c6 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -39,7 +39,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/image/image.h" #include "ui/empty_userpic.h" #include "ui/text/text_options.h" -#include "ui/toasts/common_toasts.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "history/history.h" @@ -1093,6 +1092,22 @@ const QString &PeerData::themeEmoji() const { return _themeEmoticon; } +void PeerData::setWallPaper(std::optional paper) { + if (!paper && !_wallPaper) { + return; + } else if (paper && _wallPaper && _wallPaper->equals(*paper)) { + return; + } + _wallPaper = paper + ? std::make_unique(std::move(*paper)) + : nullptr; + session().changes().peerUpdated(this, UpdateFlag::ChatWallPaper); +} + +const Data::WallPaper *PeerData::wallPaper() const { + return _wallPaper.get(); +} + void PeerData::setIsBlocked(bool is) { const auto status = is ? BlockStatus::Blocked diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 1940e573b..17b19bf63 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -37,6 +37,7 @@ class ForumTopic; class Session; class GroupCall; struct ReactionId; +class WallPaper; [[nodiscard]] int PeerColorIndex(PeerId peerId); @@ -403,6 +404,9 @@ public: void setThemeEmoji(const QString &emoticon); [[nodiscard]] const QString &themeEmoji() const; + void setWallPaper(std::optional paper); + [[nodiscard]] const Data::WallPaper *wallPaper() const; + const PeerId id; MTPinputPeer input = MTP_inputPeerEmpty(); @@ -457,6 +461,7 @@ private: QString _about; QString _themeEmoticon; + std::unique_ptr _wallPaper; }; diff --git a/Telegram/SourceFiles/data/data_photo_media.cpp b/Telegram/SourceFiles/data/data_photo_media.cpp index 7bbdcdd1e..be713b4a9 100644 --- a/Telegram/SourceFiles/data/data_photo_media.cpp +++ b/Telegram/SourceFiles/data/data_photo_media.cpp @@ -166,14 +166,22 @@ bool PhotoMedia::autoLoadThumbnailAllowed(not_null peer) const { } void PhotoMedia::automaticLoad( - Data::FileOrigin origin, + FileOrigin origin, const HistoryItem *item) { - if (!item || loaded() || _owner->cancelled()) { + if (item) { + automaticLoad(origin, item->history()->peer); + } +} + +void PhotoMedia::automaticLoad( + FileOrigin origin, + not_null peer) { + if (loaded() || _owner->cancelled()) { return; } const auto loadFromCloud = Data::AutoDownload::Should( _owner->session().settings().autoDownload(), - item->history()->peer, + peer, _owner); _owner->load( origin, diff --git a/Telegram/SourceFiles/data/data_photo_media.h b/Telegram/SourceFiles/data/data_photo_media.h index 7c5c34bd4..f9e3c7708 100644 --- a/Telegram/SourceFiles/data/data_photo_media.h +++ b/Telegram/SourceFiles/data/data_photo_media.h @@ -43,7 +43,8 @@ public: [[nodiscard]] bool autoLoadThumbnailAllowed( not_null peer) const; - void automaticLoad(Data::FileOrigin origin, const HistoryItem *item); + void automaticLoad(FileOrigin origin, const HistoryItem *item); + void automaticLoad(FileOrigin origin, not_null peer); void collectLocalData(not_null local); diff --git a/Telegram/SourceFiles/data/data_poll.cpp b/Telegram/SourceFiles/data/data_poll.cpp index ce2cbee35..f781f95bc 100644 --- a/Telegram/SourceFiles/data/data_poll.cpp +++ b/Telegram/SourceFiles/data/data_poll.cpp @@ -132,26 +132,23 @@ bool PollData::applyResults(const MTPPollResults &results) { } } if (const auto recent = results.vrecent_voters()) { - const auto bareProj = [](not_null user) { - return peerToUser(user->id).bare; - }; const auto recentChanged = !ranges::equal( recentVoters, recent->v, ranges::equal_to(), - bareProj, - &MTPlong::v); + &PeerData::id, + peerFromMTP); if (recentChanged) { changed = true; recentVoters = ranges::views::all( recent->v - ) | ranges::views::transform([&](MTPlong userId) { - const auto user = _owner->user(userId.v); - return user->isMinimalLoaded() ? user.get() : nullptr; - }) | ranges::views::filter([](UserData *user) { - return user != nullptr; - }) | ranges::views::transform([](UserData *user) { - return not_null(user); + ) | ranges::views::transform([&](MTPPeer peerId) { + const auto peer = _owner->peer(peerFromMTP(peerId)); + return peer->isMinimalLoaded() ? peer.get() : nullptr; + }) | ranges::views::filter([](PeerData *peer) { + return peer != nullptr; + }) | ranges::views::transform([](PeerData *peer) { + return not_null(peer); }) | ranges::to_vector; } } diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 72d2731d6..4f428479b 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -67,7 +67,7 @@ struct PollData { PollId id = 0; QString question; std::vector answers; - std::vector> recentVoters; + std::vector> recentVoters; std::vector sendingVotes; TextWithEntities solution; TimeId closePeriod = 0; diff --git a/Telegram/SourceFiles/data/data_premium_limits.cpp b/Telegram/SourceFiles/data/data_premium_limits.cpp index 0348feed8..41397c383 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.cpp +++ b/Telegram/SourceFiles/data/data_premium_limits.cpp @@ -65,6 +65,18 @@ int PremiumLimits::dialogFiltersCurrent() const { : dialogFiltersDefault(); } +int PremiumLimits::dialogShareableFiltersDefault() const { + return appConfigLimit("chatlists_joined_limit_default", 2); +} +int PremiumLimits::dialogShareableFiltersPremium() const { + return appConfigLimit("chatlists_joined_limit_premium", 20); +} +int PremiumLimits::dialogShareableFiltersCurrent() const { + return isPremium() + ? dialogShareableFiltersPremium() + : dialogShareableFiltersDefault(); +} + int PremiumLimits::dialogFiltersChatsDefault() const { return appConfigLimit("dialog_filters_chats_limit_default", 100); } @@ -77,6 +89,18 @@ int PremiumLimits::dialogFiltersChatsCurrent() const { : dialogFiltersChatsDefault(); } +int PremiumLimits::dialogFiltersLinksDefault() const { + return appConfigLimit("chatlist_invites_limit_default", 3); +} +int PremiumLimits::dialogFiltersLinksPremium() const { + return appConfigLimit("chatlist_invites_limit_premium", 20); +} +int PremiumLimits::dialogFiltersLinksCurrent() const { + return isPremium() + ? dialogFiltersLinksPremium() + : dialogFiltersLinksDefault(); +} + int PremiumLimits::dialogsPinnedDefault() const { return appConfigLimit("dialogs_pinned_limit_default", 5); } diff --git a/Telegram/SourceFiles/data/data_premium_limits.h b/Telegram/SourceFiles/data/data_premium_limits.h index e25ab880f..17ffa1247 100644 --- a/Telegram/SourceFiles/data/data_premium_limits.h +++ b/Telegram/SourceFiles/data/data_premium_limits.h @@ -33,10 +33,18 @@ public: [[nodiscard]] int dialogFiltersPremium() const; [[nodiscard]] int dialogFiltersCurrent() const; + [[nodiscard]] int dialogShareableFiltersDefault() const; + [[nodiscard]] int dialogShareableFiltersPremium() const; + [[nodiscard]] int dialogShareableFiltersCurrent() const; + [[nodiscard]] int dialogFiltersChatsDefault() const; [[nodiscard]] int dialogFiltersChatsPremium() const; [[nodiscard]] int dialogFiltersChatsCurrent() const; + [[nodiscard]] int dialogFiltersLinksDefault() const; + [[nodiscard]] int dialogFiltersLinksPremium() const; + [[nodiscard]] int dialogFiltersLinksCurrent() const; + [[nodiscard]] int dialogsPinnedDefault() const; [[nodiscard]] int dialogsPinnedPremium() const; [[nodiscard]] int dialogsPinnedCurrent() const; diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.cpp b/Telegram/SourceFiles/data/data_scheduled_messages.cpp index b8438d123..7f5821b7c 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/data_scheduled_messages.cpp @@ -40,7 +40,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); } [[nodiscard]] bool HasScheduledDate(not_null item) { - return (item->date() != ScheduledMessages::kScheduledUntilOnlineTimestamp) + return (item->date() != Api::kScheduledUntilOnlineTimestamp) && (item->date() > base::unixtime::now()); } @@ -192,12 +192,13 @@ void ScheduledMessages::sendNowSimpleMessage( const auto history = local->history(); auto action = Api::SendAction(history); - action.replyTo = local->replyToId(); + action.replyTo = local->replyTo(); const auto replyHeader = NewMessageReplyHeader(action); - const auto localFlags = NewMessageFlags(history->peer); + const auto localFlags = NewMessageFlags(history->peer) + & ~MessageFlag::BeingSent; const auto flags = MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id - | (local->replyToId() + | (action.replyTo ? MTPDmessage::Flag::f_reply_to : MTPDmessage::Flag(0)) | (update.vttl_period() @@ -466,16 +467,17 @@ HistoryItem *ScheduledMessages::append( // probably this message was edited. if (data.is_edit_hide()) { existing->applyEdition(HistoryMessageEdition(_session, data)); + } else { + existing->updateSentContent({ + qs(data.vmessage()), + Api::EntitiesFromMTP( + _session, + data.ventities().value_or_empty()) + }, data.vmedia()); + existing->updateReplyMarkup( + HistoryMessageMarkupData(data.vreply_markup())); + existing->updateForwardedInfo(data.vfwd_from()); } - existing->updateSentContent({ - qs(data.vmessage()), - Api::EntitiesFromMTP( - _session, - data.ventities().value_or_empty()) - }, data.vmedia()); - existing->updateReplyMarkup( - HistoryMessageMarkupData(data.vreply_markup())); - existing->updateForwardedInfo(data.vfwd_from()); existing->updateDate(data.vdate().v); history->owner().requestItemTextRefresh(existing); }, [&](const auto &data) {}); diff --git a/Telegram/SourceFiles/data/data_scheduled_messages.h b/Telegram/SourceFiles/data/data_scheduled_messages.h index 7d546ad1b..e4abea70a 100644 --- a/Telegram/SourceFiles/data/data_scheduled_messages.h +++ b/Telegram/SourceFiles/data/data_scheduled_messages.h @@ -53,8 +53,6 @@ public: [[nodiscard]] rpl::producer<> updates(not_null history); [[nodiscard]] Data::MessagesSlice list(not_null history); - static constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); - private: using OwnedItem = std::unique_ptr; struct List { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 18b74ec05..73ebae0e1 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -66,6 +66,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_emoji_statuses.h" #include "data/data_forum_icons.h" #include "data/data_cloud_themes.h" +#include "data/data_stories.h" +#include "data/data_story.h" #include "data/data_streaming.h" #include "data/data_media_rotation.h" #include "data/data_histories.h" @@ -117,7 +119,7 @@ void CheckForSwitchInlineButton(not_null item) { if (!windows.empty()) { Api::SwitchInlineBotButtonReceived( windows.front(), - QString::fromUtf8(button.data)); + button.data); } return; } @@ -266,7 +268,8 @@ Session::Session(not_null session) , _emojiStatuses(std::make_unique(this)) , _forumIcons(std::make_unique(this)) , _notifySettings(std::make_unique(this)) -, _customEmojiManager(std::make_unique(this)) { +, _customEmojiManager(std::make_unique(this)) +, _stories(std::make_unique(this)) { _cache->open(_session->local().cacheKey()); _bigFileCache->open(_session->local().cacheBigFileKey()); @@ -311,6 +314,8 @@ Session::Session(not_null session) } } }, _lifetime); + + _stories->loadMore(Data::StorySourcesList::NotHidden); }); } @@ -518,7 +523,15 @@ not_null Session::processUser(const MTPUser &data) { ? Flag::Contact | Flag::MutualContact | Flag::DiscardMinPhoto + | Flag::StoriesHidden : Flag()); + const auto storiesState = minimal + ? std::optional() + : data.is_stories_unavailable() + ? Data::Stories::PeerSourceState() + : !data.vstories_max_id() + ? std::optional() + : stories().peerSourceState(result, data.vstories_max_id()->v); const auto flagsSet = (data.is_deleted() ? Flag::Deleted : Flag()) | (data.is_verified() ? Flag::Verified : Flag()) | (data.is_scam() ? Flag::Scam : Flag()) @@ -530,6 +543,7 @@ not_null Session::processUser(const MTPUser &data) { ? (data.is_contact() ? Flag::Contact : Flag()) | (data.is_mutual_contact() ? Flag::MutualContact : Flag()) | (data.is_apply_min_photo() ? Flag() : Flag::DiscardMinPhoto) + | (data.is_stories_hidden() ? Flag::StoriesHidden : Flag()) : Flag()); result->setFlags((result->flags() & ~flagsMask) | flagsSet); if (minimal) { @@ -544,6 +558,13 @@ not_null Session::processUser(const MTPUser &data) { MTP_long(data.vaccess_hash().value_or_empty())); } } else { + if (storiesState) { + result->setStoriesState(!storiesState->maxId + ? UserData::StoriesState::None + : (storiesState->maxId > storiesState->readTill) + ? UserData::StoriesState::HasUnread + : UserData::StoriesState::HasRead); + } if (data.is_self()) { result->input = MTP_inputPeerSelf(); result->inputUser = MTP_inputUserSelf(); @@ -667,6 +688,7 @@ not_null Session::processUser(const MTPUser &data) { result->botInfo->inlinePlaceholder = QString(); } result->botInfo->supportsAttachMenu = data.is_bot_attach_menu(); + result->botInfo->canEditInformation = data.is_bot_can_edit(); } else { result->setBotInfoVersion(-1); } @@ -1136,7 +1158,8 @@ UserData *Session::userByPhone(const QString &phone) const { PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); for (const auto &[peerId, peer] : _peers) { - if (!peer->userName().compare(uname, Qt::CaseInsensitive)) { + if (peer->isLoaded() + && !peer->userName().compare(uname, Qt::CaseInsensitive)) { return peer.get(); } } @@ -1354,6 +1377,9 @@ void Session::setupChannelLeavingViewer() { history->removeJoinedMessage(); history->updateChatListExistence(); history->updateChatListSortPosition(); + if (!history->inChatList()) { + history->clearFolder(); + } } } }, _lifetime); @@ -3205,6 +3231,7 @@ not_null Session::processWebpage(const MTPDwebPagePending &data) { QString(), QString(), TextWithEntities(), + FullStoryId(), nullptr, nullptr, WebPageCollage(), @@ -3259,6 +3286,7 @@ not_null Session::webpage( siteName, title, description, + FullStoryId(), photo, document, std::move(collage), @@ -3301,6 +3329,8 @@ void Session::webpageApplyFields( const auto result = attribute.match([&]( const MTPDwebPageAttributeTheme &data) { return lookupInAttribute(data); + }, [&](const MTPDwebPageAttributeStory &data) { + return (DocumentData*)nullptr; }); if (result) { return result; @@ -3309,16 +3339,55 @@ void Session::webpageApplyFields( } return nullptr; }; + auto story = (Data::Story*)nullptr; + auto storyId = FullStoryId(); + if (const auto attributes = data.vattributes()) { + for (const auto &attribute : attributes->v) { + attribute.match([&](const MTPDwebPageAttributeStory &data) { + storyId = FullStoryId{ + peerFromUser(data.vuser_id()), + data.vid().v, + }; + if (const auto embed = data.vstory()) { + story = stories().applyFromWebpage( + peerFromUser(data.vuser_id()), + *embed); + } else if (const auto maybe = stories().lookup(storyId)) { + story = *maybe; + } else if (maybe.error() == Data::NoStory::Unknown) { + stories().resolve(storyId, [=] { + if (const auto maybe = stories().lookup(storyId)) { + const auto story = *maybe; + page->document = story->document(); + page->photo = story->photo(); + page->description = story->caption(); + page->type = WebPageType::Story; + notifyWebPageUpdateDelayed(page); + } + }); + } + }, [](const auto &) {}); + } + } webpageApplyFields( page, - ParseWebPageType(data), + (story ? WebPageType::Story : ParseWebPageType(data)), qs(data.vurl()), qs(data.vdisplay_url()), siteName, qs(data.vtitle().value_or_empty()), - description, - photo ? processPhoto(*photo).get() : nullptr, - document ? processDocument(*document).get() : lookupThemeDocument(), + (story ? story->caption() : description), + storyId, + (story + ? story->photo() + : photo + ? processPhoto(*photo).get() + : nullptr), + (story + ? story->document() + : document + ? processDocument(*document).get() + : lookupThemeDocument()), WebPageCollage(this, data), data.vduration().value_or_empty(), qs(data.vauthor().value_or_empty()), @@ -3333,6 +3402,7 @@ void Session::webpageApplyFields( const QString &siteName, const QString &title, const TextWithEntities &description, + FullStoryId storyId, PhotoData *photo, DocumentData *document, WebPageCollage &&collage, @@ -3347,6 +3417,7 @@ void Session::webpageApplyFields( siteName, title, description, + storyId, photo, document, std::move(collage), @@ -3825,6 +3896,38 @@ void Session::destroyAllCallItems() { } } +void Session::registerStoryItem( + FullStoryId id, + not_null item) { + _storyItems[id].emplace(item); +} + +void Session::unregisterStoryItem( + FullStoryId id, + not_null item) { + const auto i = _storyItems.find(id); + if (i != _storyItems.end()) { + auto &items = i->second; + if (items.remove(item) && items.empty()) { + _storyItems.erase(i); + } + } +} + +void Session::refreshStoryItemViews(FullStoryId id) { + const auto i = _storyItems.find(id); + if (i != _storyItems.end()) { + for (const auto item : i->second) { + if (const auto media = item->media()) { + if (media->storyMention()) { + item->updateStoryMentionText(); + } + } + requestItemViewRefresh(item); + } + } +} + void Session::documentMessageRemoved(not_null document) { if (_documentItems.find(document) != _documentItems.end()) { return; @@ -4175,7 +4278,8 @@ void Session::serviceNotification( MTPstring(), // bot_inline_placeholder MTPstring(), // lang_code MTPEmojiStatus(), - MTPVector())); + MTPVector(), + MTPint())); // stories_max_id } const auto history = this->history(PeerData::kServiceNotificationsId); if (!history->folderKnown()) { diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 16c56c23e..9ba13d1a9 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -63,6 +63,7 @@ class Stickers; class GroupCall; class NotifySettings; class CustomEmojiManager; +class Stories; struct RepliesReadTillUpdate { FullMsgId id; @@ -136,6 +137,9 @@ public: [[nodiscard]] CustomEmojiManager &customEmojiManager() const { return *_customEmojiManager; } + [[nodiscard]] Stories &stories() const { + return *_stories; + } [[nodiscard]] MsgId nextNonHistoryEntryId() { return ++_nonHistoryEntryId; @@ -633,6 +637,9 @@ public: not_null item); void registerCallItem(not_null item); void unregisterCallItem(not_null item); + void registerStoryItem(FullStoryId id, not_null item); + void unregisterStoryItem(FullStoryId id, not_null item); + void refreshStoryItemViews(FullStoryId id); void documentMessageRemoved(not_null document); @@ -807,6 +814,7 @@ private: const QString &siteName, const QString &title, const TextWithEntities &description, + FullStoryId storyId, PhotoData *photo, DocumentData *document, WebPageCollage &&collage, @@ -944,6 +952,9 @@ private: UserId, base::flat_set>> _contactViews; std::unordered_set> _callItems; + std::unordered_map< + FullStoryId, + base::flat_set>> _storyItems; base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; @@ -1007,6 +1018,7 @@ private: const std::unique_ptr _forumIcons; const std::unique_ptr _notifySettings; const std::unique_ptr _customEmojiManager; + const std::unique_ptr _stories; MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange; diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index e2a69482e..8033fba9c 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -185,7 +185,8 @@ rpl::producer SharedScheduledMediaViewer( SharedMediaMergedKey key, int limitBefore, int limitAfter) { - Expects(!IsServerMsgId(key.mergedKey.universalId)); + Expects(!key.mergedKey.universalId + || Data::IsScheduledMsgId(key.mergedKey.universalId)); Expects((key.mergedKey.universalId != 0) || (limitBefore == 0 && limitAfter == 0)); diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index 9f4f992ed..fa21af469 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -270,28 +270,45 @@ void SponsoredMessages::append( .isForceUserpicDisplay = data.is_show_peer_photo(), }; }; + const auto externalLink = data.vwebpage() + ? qs(data.vwebpage()->data().vurl()) + : QString(); + const auto userpicFromPhoto = [&](const MTPphoto &photo) { + return photo.match([&](const MTPDphoto &data) { + for (const auto &size : data.vsizes().v) { + const auto result = Images::FromPhotoSize( + _session, + data, + size); + if (result.location.valid()) { + return result; + } + } + return ImageWithLocation{}; + }, [](const MTPDphotoEmpty &) { + return ImageWithLocation{}; + }); + }; const auto from = [&]() -> SponsoredFrom { - if (data.vfrom_id()) { + if (const auto webpage = data.vwebpage()) { + const auto &data = webpage->data(); + auto userpic = data.vphoto() + ? userpicFromPhoto(*data.vphoto()) + : ImageWithLocation{}; + return SponsoredFrom{ + .title = qs(data.vsite_name()), + .isExternalLink = true, + .userpic = std::move(userpic), + .isForceUserpicDisplay = message.data().is_show_peer_photo(), + }; + } else if (const auto fromId = data.vfrom_id()) { return makeFrom( - _session->data().peer(peerFromMTP(*data.vfrom_id())), + _session->data().peer(peerFromMTP(*fromId)), (data.vchannel_post() != nullptr)); } Assert(data.vchat_invite()); return data.vchat_invite()->match([&](const MTPDchatInvite &data) { - auto userpic = data.vphoto().match([&](const MTPDphoto &data) { - for (const auto &size : data.vsizes().v) { - const auto result = Images::FromPhotoSize( - _session, - data, - size); - if (result.location.valid()) { - return result; - } - } - return ImageWithLocation{}; - }, [](const MTPDphotoEmpty &) { - return ImageWithLocation{}; - }); + auto userpic = userpicFromPhoto(data.vphoto()); return SponsoredFrom{ .title = qs(data.vtitle()), .isBroadcast = data.is_broadcast(), @@ -336,6 +353,7 @@ void SponsoredMessages::append( .history = history, .msgId = data.vchannel_post().value_or_empty(), .chatInviteHash = hash, + .externalLink = externalLink, .sponsorInfo = std::move(sponsorInfo), .additionalInfo = std::move(additionalInfo), }; @@ -367,7 +385,7 @@ const SponsoredMessages::Entry *SponsoredMessages::find( } auto &list = it->second; const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) { - return e.item->fullId() == fullId; + return e.item && e.item->fullId() == fullId; }); if (entryIt == end(list.entries)) { return nullptr; @@ -423,9 +441,24 @@ SponsoredMessages::Details SponsoredMessages::lookupDetails( .peer = data.from.peer, .msgId = data.msgId, .info = std::move(info), + .externalLink = data.externalLink, }; } +void SponsoredMessages::clicked(const FullMsgId &fullId) { + const auto entryPtr = find(fullId); + if (!entryPtr) { + return; + } + const auto randomId = entryPtr->sponsored.randomId; + const auto channel = entryPtr->item->history()->peer->asChannel(); + Assert(channel != nullptr); + _session->api().request(MTPchannels_ClickSponsoredMessage( + channel->inputChannel, + MTP_bytes(randomId) + )).send(); +} + SponsoredMessages::State SponsoredMessages::state( not_null history) const { const auto it = _data.find(history); diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.h b/Telegram/SourceFiles/data/data_sponsored_messages.h index 7abbe2ea3..e9cd46b45 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.h +++ b/Telegram/SourceFiles/data/data_sponsored_messages.h @@ -31,6 +31,7 @@ struct SponsoredFrom { bool isBot = false; bool isExactPost = false; bool isRecommended = false; + bool isExternalLink = false; ImageWithLocation userpic; bool isForceUserpicDisplay = false; }; @@ -42,6 +43,7 @@ struct SponsoredMessage { History *history = nullptr; MsgId msgId; QString chatInviteHash; + QString externalLink; TextWithEntities sponsorInfo; TextWithEntities additionalInfo; }; @@ -58,6 +60,7 @@ public: PeerData *peer = nullptr; MsgId msgId; std::vector info; + QString externalLink; }; using RandomId = QByteArray; explicit SponsoredMessages(not_null owner); @@ -69,6 +72,7 @@ public: void request(not_null history, Fn done); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; + void clicked(const FullMsgId &fullId); [[nodiscard]] bool append(not_null history); void inject( diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp new file mode 100644 index 000000000..661c28777 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -0,0 +1,1873 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_stories.h" + +#include "api/api_report.h" +#include "base/unixtime.h" +#include "apiwrap.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_folder.h" +#include "data/data_photo.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/layers/show.h" +#include "ui/text/text_utilities.h" + +namespace Data { +namespace { + +constexpr auto kMaxResolveTogether = 100; +constexpr auto kIgnorePreloadAroundIfLoaded = 15; +constexpr auto kPreloadAroundCount = 30; +constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); +constexpr auto kIncrementViewsDelay = 5 * crl::time(1000); +constexpr auto kArchiveFirstPerPage = 30; +constexpr auto kArchivePerPage = 100; +constexpr auto kSavedFirstPerPage = 30; +constexpr auto kSavedPerPage = 100; +constexpr auto kMaxPreloadSources = 10; +constexpr auto kStillPreloadFromFirst = 3; +constexpr auto kMaxSegmentsCount = 180; +constexpr auto kPollingIntervalChat = 5 * TimeId(60); +constexpr auto kPollingIntervalViewer = 1 * TimeId(60); +constexpr auto kPollViewsInterval = 10 * crl::time(1000); +constexpr auto kPollingViewsPerPage = Story::kRecentViewersMax; + +using UpdateFlag = StoryUpdate::Flag; + +[[nodiscard]] std::optional ParseMedia( + not_null owner, + const MTPMessageMedia &media) { + return media.match([&](const MTPDmessageMediaPhoto &data) + -> std::optional { + if (const auto photo = data.vphoto()) { + const auto result = owner->processPhoto(*photo); + if (!result->isNull()) { + return StoryMedia{ result }; + } + } + return {}; + }, [&](const MTPDmessageMediaDocument &data) + -> std::optional { + if (const auto document = data.vdocument()) { + const auto result = owner->processDocument(*document); + if (!result->isNull() + && (result->isGifv() || result->isVideoFile())) { + result->setStoryMedia(true); + return StoryMedia{ result }; + } + } + return {}; + }, [&](const MTPDmessageMediaUnsupported &data) { + return std::make_optional(StoryMedia{ v::null }); + }, [](const auto &) { return std::optional(); }); +} + +} // namespace + +StoriesSourceInfo StoriesSource::info() const { + return { + .id = user->id, + .last = ids.empty() ? 0 : ids.back().date, + .count = uint32(std::min(int(ids.size()), kMaxSegmentsCount)), + .unreadCount = uint32(std::min(unreadCount(), kMaxSegmentsCount)), + .premium = user->isPremium() ? 1U : 0U, + }; +} + +int StoriesSource::unreadCount() const { + const auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 }); + return int(end(ids) - i); +} + +StoryIdDates StoriesSource::toOpen() const { + if (ids.empty()) { + return {}; + } + const auto i = ids.lower_bound(StoryIdDates{ readTill + 1 }); + return (i != end(ids)) ? *i : ids.front(); +} + +Stories::Stories(not_null owner) +: _owner(owner) +, _expireTimer([=] { processExpired(); }) +, _markReadTimer([=] { sendMarkAsReadRequests(); }) +, _incrementViewsTimer([=] { sendIncrementViewsRequests(); }) +, _pollingTimer([=] { sendPollingRequests(); }) +, _pollingViewsTimer([=] { sendPollingViewsRequests(); }) { +} + +Stories::~Stories() { + Expects(_pollingSettings.empty()); + Expects(_pollingViews.empty()); +} + +Session &Stories::owner() const { + return *_owner; +} + +Main::Session &Stories::session() const { + return _owner->session(); +} + +void Stories::apply(const MTPDupdateStory &data) { + const auto peerId = peerFromUser(data.vuser_id()); + const auto user = not_null(_owner->peer(peerId)->asUser()); + const auto now = base::unixtime::now(); + const auto idDates = parseAndApply(user, data.vstory(), now); + if (!idDates) { + return; + } + const auto expired = (idDates.expires <= now); + if (expired) { + applyExpired({ peerId, idDates.id }); + return; + } + const auto i = _all.find(peerId); + if (i == end(_all)) { + requestUserStories(user); + return; + } else if (i->second.ids.contains(idDates)) { + return; + } + const auto wasInfo = i->second.info(); + i->second.ids.emplace(idDates); + const auto nowInfo = i->second.info(); + if (user->isSelf() && i->second.readTill < idDates.id) { + _readTill[user->id] = i->second.readTill = idDates.id; + } + if (wasInfo == nowInfo) { + return; + } + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i != end(sources)) { + *i = nowInfo; + sort(list); + } + }; + if (user->hasStoriesHidden()) { + refreshInList(StorySourcesList::Hidden); + } else { + refreshInList(StorySourcesList::NotHidden); + } + _sourceChanged.fire_copy(peerId); + updateUserStoriesState(user); +} + +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) { + if (!data) { + applyDeletedFromSources(peer->id, StorySourcesList::NotHidden); + applyDeletedFromSources(peer->id, StorySourcesList::Hidden); + _all.erase(peer->id); + _sourceChanged.fire_copy(peer->id); + updateUserStoriesState(peer); + } else { + parseAndApply(*data); + } +} + +Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) { + const auto idDates = parseAndApply( + _owner->peer(peerId), + story, + base::unixtime::now()); + const auto value = idDates + ? lookup({ peerId, idDates.id }) + : base::make_unexpected(NoStory::Deleted); + return value ? value->get() : nullptr; +} + +void Stories::requestUserStories( + not_null user, + Fn done) { + const auto [i, ok] = _requestingUserStories.emplace(user); + if (done) { + i->second.push_back(std::move(done)); + } + if (!ok) { + return; + } + const auto finish = [=] { + if (const auto callbacks = _requestingUserStories.take(user)) { + for (const auto &callback : *callbacks) { + callback(); + } + } + }; + _owner->session().api().request(MTPstories_GetUserStories( + user->inputUser + )).done([=](const MTPstories_UserStories &result) { + const auto &data = result.data(); + _owner->processUsers(data.vusers()); + parseAndApply(data.vstories()); + finish(); + }).fail([=] { + applyDeletedFromSources(user->id, StorySourcesList::NotHidden); + applyDeletedFromSources(user->id, StorySourcesList::Hidden); + finish(); + }).send(); +} + +void Stories::registerExpiring(TimeId expires, FullStoryId id) { + for (auto i = _expiring.findFirst(expires) + ; (i != end(_expiring)) && (i->first == expires) + ; ++i) { + if (i->second == id) { + return; + } + } + const auto reschedule = _expiring.empty() + || (_expiring.front().first > expires); + _expiring.emplace(expires, id); + if (reschedule) { + scheduleExpireTimer(); + } +} + +void Stories::scheduleExpireTimer() { + if (_expireSchedulePosted) { + return; + } + _expireSchedulePosted = true; + crl::on_main(this, [=] { + if (!_expireSchedulePosted) { + return; + } + _expireSchedulePosted = false; + if (_expiring.empty()) { + _expireTimer.cancel(); + } else { + const auto nearest = _expiring.front().first; + const auto now = base::unixtime::now(); + const auto delay = (nearest > now) + ? (nearest - now) + : 0; + _expireTimer.callOnce(delay * crl::time(1000)); + } + }); +} + +void Stories::processExpired() { + const auto now = base::unixtime::now(); + auto expired = base::flat_set(); + auto i = begin(_expiring); + for (; i != end(_expiring) && i->first <= now; ++i) { + expired.emplace(i->second); + } + _expiring.erase(begin(_expiring), i); + for (const auto &id : expired) { + applyExpired(id); + } + if (!_expiring.empty()) { + scheduleExpireTimer(); + } +} + +void Stories::parseAndApply(const MTPUserStories &stories) { + const auto &data = stories.data(); + const auto peerId = peerFromUser(data.vuser_id()); + const auto already = _readTill.find(peerId); + const auto readTill = std::max( + data.vmax_read_id().value_or_empty(), + (already != end(_readTill) ? already->second : 0)); + const auto user = _owner->peer(peerId)->asUser(); + auto result = StoriesSource{ + .user = user, + .readTill = readTill, + .hidden = user->hasStoriesHidden(), + }; + const auto &list = data.vstories().v; + const auto now = base::unixtime::now(); + result.ids.reserve(list.size()); + for (const auto &story : list) { + if (const auto id = parseAndApply(result.user, story, now)) { + result.ids.emplace(id); + } + } + if (result.ids.empty()) { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::Hidden); + user->setStoriesState(UserData::StoriesState::None); + return; + } else if (user->isSelf()) { + result.readTill = result.ids.back().id; + } + _readTill[peerId] = result.readTill; + const auto info = result.info(); + const auto i = _all.find(peerId); + if (i != end(_all)) { + if (i->second != result) { + i->second = std::move(result); + } + } else { + _all.emplace(peerId, std::move(result)); + } + const auto add = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i == end(sources)) { + sources.push_back(info); + } else if (*i == info) { + return; + } else { + *i = info; + } + sort(list); + }; + 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); + applyDeletedFromSources( + peerId, + hidden ? List::NotHidden : List::Hidden); + } else { + applyDeletedFromSources(peerId, StorySourcesList::NotHidden); + applyDeletedFromSources(peerId, StorySourcesList::Hidden); + } + _sourceChanged.fire_copy(peerId); + updateUserStoriesState(result.user); +} + +Story *Stories::parseAndApply( + not_null peer, + const MTPDstoryItem &data, + TimeId now) { + const auto media = ParseMedia(_owner, data.vmedia()); + if (!media) { + return nullptr; + } + const auto expires = data.vexpire_date().v; + const auto expired = (expires <= now); + if (expired && !data.is_pinned() && !peer->isSelf()) { + return nullptr; + } + const auto id = data.vid().v; + const auto fullId = FullStoryId{ peer->id, id }; + auto &stories = _stories[peer->id]; + const auto i = stories.find(id); + if (i != end(stories)) { + const auto result = i->second.get(); + const auto mediaChanged = (result->media() != *media); + result->applyChanges(*media, data, now); + const auto j = _pollingSettings.find(result); + if (j != end(_pollingSettings)) { + maybeSchedulePolling(result, j->second, now); + } + if (mediaChanged) { + _preloaded.remove(fullId); + if (_preloading && _preloading->id() == fullId) { + _preloading = nullptr; + rebuildPreloadSources(StorySourcesList::NotHidden); + rebuildPreloadSources(StorySourcesList::Hidden); + continuePreloading(); + } + _owner->refreshStoryItemViews(fullId); + } + return result; + } + const auto wasDeleted = _deleted.remove(fullId); + const auto result = stories.emplace(id, std::make_unique( + id, + peer, + StoryMedia{ *media }, + data, + now + )).first->second.get(); + + if (peer->isSelf()) { + const auto added = _archive.list.emplace(id).second; + if (added) { + if (_archiveTotal >= 0 && id > _archiveLastId) { + ++_archiveTotal; + } + _archiveChanged.fire({}); + } + } + + if (expired) { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } else { + registerExpiring(expires, fullId); + } + + if (wasDeleted) { + _owner->refreshStoryItemViews(fullId); + } + + return result; +} + +StoryIdDates Stories::parseAndApply( + not_null peer, + const MTPstoryItem &story, + TimeId now) { + return story.match([&](const MTPDstoryItem &data) { + if (const auto story = parseAndApply(peer, data, now)) { + return story->idDates(); + } + applyDeleted({ peer->id, data.vid().v }); + return StoryIdDates(); + }, [&](const MTPDstoryItemSkipped &data) { + const auto expires = data.vexpire_date().v; + const auto expired = (expires <= now); + const auto fullId = FullStoryId{ peer->id, data.vid().v }; + if (!expired) { + registerExpiring(expires, fullId); + } else if (!peer->isSelf()) { + applyDeleted(fullId); + return StoryIdDates(); + } else { + _expiring.remove(expires, fullId); + applyExpired(fullId); + } + return StoryIdDates{ + data.vid().v, + data.vdate().v, + data.vexpire_date().v, + }; + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + return StoryIdDates(); + }); +} + +void Stories::updateDependentMessages(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + for (const auto &dependent : i->second) { + dependent->updateDependencyItem(); + } + } + session().changes().storyUpdated( + story, + Data::StoryUpdate::Flag::Edited); +} + +void Stories::registerDependentMessage( + not_null dependent, + not_null dependency) { + _dependentMessages[dependency].emplace(dependent); +} + +void Stories::unregisterDependentMessage( + not_null dependent, + not_null dependency) { + const auto i = _dependentMessages.find(dependency); + if (i != end(_dependentMessages)) { + if (i->second.remove(dependent) && i->second.empty()) { + _dependentMessages.erase(i); + } + } +} + +void Stories::savedStateChanged(not_null story) { + const auto id = story->id(); + const auto peer = story->peer()->id; + const auto pinned = story->pinned(); + if (pinned) { + auto &saved = _saved[peer]; + const auto added = saved.ids.list.emplace(id).second; + if (added) { + if (saved.total >= 0 && id > saved.lastId) { + ++saved.total; + } + _savedChanged.fire_copy(peer); + } + } else if (const auto i = _saved.find(peer); i != end(_saved)) { + auto &saved = i->second; + if (saved.ids.list.remove(id)) { + if (saved.total > 0) { + --saved.total; + } + _savedChanged.fire_copy(peer); + } + } +} + +void Stories::loadMore(StorySourcesList list) { + const auto index = static_cast(list); + if (_loadMoreRequestId[index] || _sourcesLoaded[index]) { + return; + } + const auto hidden = (list == StorySourcesList::Hidden); + const auto api = &_owner->session().api(); + using Flag = MTPstories_GetAllStories::Flag; + _loadMoreRequestId[index] = api->request(MTPstories_GetAllStories( + MTP_flags((hidden ? Flag::f_hidden : Flag()) + | (_sourcesStates[index].isEmpty() + ? Flag(0) + : (Flag::f_next | Flag::f_state))), + MTP_string(_sourcesStates[index]) + )).done([=](const MTPstories_AllStories &result) { + _loadMoreRequestId[index] = 0; + + result.match([&](const MTPDstories_allStories &data) { + _owner->processUsers(data.vusers()); + _sourcesStates[index] = qs(data.vstate()); + _sourcesLoaded[index] = !data.is_has_more(); + for (const auto &single : data.vuser_stories().v) { + parseAndApply(single); + } + }, [](const MTPDstories_allStoriesNotModified &) { + }); + + result.match([&](const auto &data) { + apply(data.vstealth_mode()); + }); + + preloadListsMore(); + }).fail([=] { + _loadMoreRequestId[index] = 0; + }).send(); +} + +void Stories::preloadListsMore() { + if (_loadMoreRequestId[static_cast(StorySourcesList::NotHidden)] + || _loadMoreRequestId[static_cast(StorySourcesList::Hidden)]) { + return; + } + const auto loading = [&](StorySourcesList list) { + return _loadMoreRequestId[static_cast(list)] != 0; + }; + const auto countLoaded = [&](StorySourcesList list) { + const auto index = static_cast(list); + return _sourcesLoaded[index] || !_sourcesStates[index].isEmpty(); + }; + if (loading(StorySourcesList::NotHidden) + || loading(StorySourcesList::Hidden)) { + return; + } else if (!countLoaded(StorySourcesList::NotHidden)) { + loadMore(StorySourcesList::NotHidden); + } else if (!countLoaded(StorySourcesList::Hidden)) { + loadMore(StorySourcesList::Hidden); + } else if (!archiveCountKnown()) { + archiveLoadMore(); + } +} + +void Stories::notifySourcesChanged(StorySourcesList list) { + _sourcesChanged[static_cast(list)].fire({}); + if (list == StorySourcesList::Hidden) { + pushHiddenCountsToFolder(); + } +} + +void Stories::pushHiddenCountsToFolder() { + const auto &list = sources(StorySourcesList::Hidden); + if (list.empty()) { + if (_folderForHidden) { + _folderForHidden->updateStoriesCount(0, 0); + } + return; + } + if (!_folderForHidden) { + _folderForHidden = _owner->folder(Folder::kId); + } + const auto count = int(list.size()); + const auto unread = ranges::count_if( + list, + [](const StoriesSourceInfo &info) { return info.unreadCount > 0; }); + _folderForHidden->updateStoriesCount(count, unread); +} + +void Stories::sendResolveRequests() { + if (!_resolveSent.empty()) { + return; + } + auto leftToSend = kMaxResolveTogether; + auto byPeer = base::flat_map>(); + for (auto i = begin(_resolvePending); i != end(_resolvePending);) { + const auto peerId = i->first; + auto &ids = i->second; + auto &sent = _resolveSent[peerId]; + if (ids.size() <= leftToSend) { + sent = base::take(ids); + i = _resolvePending.erase(i); // Invalidates `ids`. + leftToSend -= int(sent.size()); + } else { + sent = { + std::make_move_iterator(begin(ids)), + std::make_move_iterator(begin(ids) + leftToSend) + }; + ids.erase(begin(ids), begin(ids) + leftToSend); + leftToSend = 0; + } + auto &prepared = byPeer[peerId]; + for (auto &[storyId, callbacks] : sent) { + prepared.push_back(MTP_int(storyId)); + } + if (!leftToSend) { + break; + } + } + const auto api = &_owner->session().api(); + for (auto &entry : byPeer) { + const auto peerId = entry.first; + auto &prepared = entry.second; + const auto finish = [=](PeerId peerId) { + const auto sent = _resolveSent.take(peerId); + Assert(sent.has_value()); + for (const auto &[storyId, list] : *sent) { + finalizeResolve({ peerId, storyId }); + for (const auto &callback : list) { + callback(); + } + } + _itemsChanged.fire_copy(peerId); + if (_resolveSent.empty() && !_resolvePending.empty()) { + crl::on_main(&session(), [=] { sendResolveRequests(); }); + } + }; + const auto user = _owner->session().data().peer(peerId)->asUser(); + if (!user) { + finish(peerId); + continue; + } + api->request(MTPstories_GetStoriesByID( + user->inputUser, + MTP_vector(prepared) + )).done([=](const MTPstories_Stories &result) { + owner().processUsers(result.data().vusers()); + processResolvedStories(user, result.data().vstories().v); + finish(user->id); + }).fail([=] { + finish(peerId); + }).send(); + } +} + +void Stories::processResolvedStories( + not_null peer, + const QVector &list) { + const auto now = base::unixtime::now(); + for (const auto &item : list) { + item.match([&](const MTPDstoryItem &data) { + if (!parseAndApply(peer, data, now)) { + applyDeleted({ peer->id, data.vid().v }); + } + }, [&](const MTPDstoryItemSkipped &data) { + LOG(("API Error: Unexpected storyItemSkipped in resolve.")); + }, [&](const MTPDstoryItemDeleted &data) { + applyDeleted({ peer->id, data.vid().v }); + }); + } +} + +void Stories::finalizeResolve(FullStoryId id) { + const auto already = lookup(id); + if (!already.has_value() && already.error() == NoStory::Unknown) { + LOG(("API Error: Could not resolve story %1_%2" + ).arg(id.peer.value + ).arg(id.story)); + applyDeleted(id); + } +} + +void Stories::applyDeleted(FullStoryId id) { + applyRemovedFromActive(id); + + _deleted.emplace(id); + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + const auto &story = _deletingStories[id] = std::move(j->second); + _expiring.remove(story->expires(), story->fullId()); + i->second.erase(j); + + session().changes().storyUpdated( + story.get(), + UpdateFlag::Destroyed); + removeDependencyStory(story.get()); + if (id.peer == session().userPeerId() + && _archive.list.remove(id.story)) { + if (_archiveTotal > 0) { + --_archiveTotal; + } + _archiveChanged.fire({}); + } + if (story->pinned()) { + if (const auto k = _saved.find(id.peer); k != end(_saved)) { + const auto saved = &k->second; + if (saved->ids.list.remove(id.story)) { + if (saved->total > 0) { + --saved->total; + } + _savedChanged.fire_copy(id.peer); + } + } + } + if (_preloading && _preloading->id() == id) { + _preloading = nullptr; + preloadFinished(id); + } + _owner->refreshStoryItemViews(id); + Assert(!_pollingSettings.contains(story.get())); + if (const auto j = _items.find(id.peer); j != end(_items)) { + const auto k = j->second.find(id.story); + if (k != end(j->second)) { + Assert(!k->second.lock()); + j->second.erase(k); + if (j->second.empty()) { + _items.erase(j); + } + } + } + if (i->second.empty()) { + _stories.erase(i); + } + _deletingStories.remove(id); + } + } +} + +void Stories::applyExpired(FullStoryId id) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + if (!story->peer()->isSelf() && !story->pinned()) { + applyDeleted(id); + return; + } + } + applyRemovedFromActive(id); +} + +void Stories::applyRemovedFromActive(FullStoryId id) { + const auto removeFromList = [&](StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto i = ranges::find( + sources, + id.peer, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); + notifySourcesChanged(list); + } + }; + const auto i = _all.find(id.peer); + if (i != end(_all)) { + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); + if (j != end(i->second.ids) && j->id == id.story) { + i->second.ids.erase(j); + const auto user = i->second.user; + if (i->second.ids.empty()) { + _all.erase(i); + removeFromList(StorySourcesList::NotHidden); + removeFromList(StorySourcesList::Hidden); + } + _sourceChanged.fire_copy(id.peer); + updateUserStoriesState(user); + } + } +} + +void Stories::applyDeletedFromSources(PeerId id, StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + id, + &StoriesSourceInfo::id); + if (i != end(sources)) { + sources.erase(i); + } + notifySourcesChanged(list); +} + +void Stories::removeDependencyStory(not_null story) { + const auto i = _dependentMessages.find(story); + if (i != end(_dependentMessages)) { + const auto items = std::move(i->second); + _dependentMessages.erase(i); + + for (const auto &dependent : items) { + dependent->dependencyStoryRemoved(story); + } + } +} + +void Stories::sort(StorySourcesList list) { + const auto index = static_cast(list); + auto &sources = _sources[index]; + const auto self = _owner->session().userPeerId(); + const auto changelogSenderId = UserData::kServiceNotificationsId; + const auto proj = [&](const StoriesSourceInfo &info) { + const auto key = int64(info.last) + + (info.premium ? (int64(1) << 47) : 0) + + ((info.id == changelogSenderId) ? (int64(1) << 47) : 0) + + ((info.unreadCount > 0) ? (int64(1) << 49) : 0) + + ((info.id == self) ? (int64(1) << 50) : 0); + return std::make_pair(key, info.id); + }; + ranges::sort(sources, ranges::greater(), proj); + notifySourcesChanged(list); + preloadSourcesChanged(list); +} + +std::shared_ptr Stories::lookupItem(not_null story) { + const auto i = _items.find(story->peer()->id); + if (i == end(_items)) { + return nullptr; + } + const auto j = i->second.find(story->id()); + if (j == end(i->second)) { + return nullptr; + } + 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()); + if (i == end(items)) { + i = items.emplace(story->id()).first; + } else if (const auto result = i->second.lock()) { + return result; + } + const auto history = _owner->history(story->peer()); + auto result = std::shared_ptr( + history->makeMessage(story).get(), + HistoryItem::Destroyer()); + i->second = result; + return result; +} + +std::shared_ptr Stories::resolveItem(FullStoryId id) { + const auto story = lookup(id); + return story ? resolveItem(*story) : std::shared_ptr(); +} + +const StoriesSource *Stories::source(PeerId id) const { + const auto i = _all.find(id); + return (i != end(_all)) ? &i->second : nullptr; +} + +const std::vector &Stories::sources( + StorySourcesList list) const { + return _sources[static_cast(list)]; +} + +bool Stories::sourcesLoaded(StorySourcesList list) const { + return _sourcesLoaded[static_cast(list)]; +} + +rpl::producer<> Stories::sourcesChanged(StorySourcesList list) const { + return _sourcesChanged[static_cast(list)].events(); +} + +rpl::producer Stories::sourceChanged() const { + return _sourceChanged.events(); +} + +rpl::producer Stories::itemsChanged() const { + return _itemsChanged.events(); +} + +base::expected, NoStory> Stories::lookup( + FullStoryId id) const { + const auto i = _stories.find(id.peer); + if (i != end(_stories)) { + const auto j = i->second.find(id.story); + if (j != end(i->second)) { + return j->second.get(); + } + } + return base::make_unexpected( + _deleted.contains(id) ? NoStory::Deleted : NoStory::Unknown); +} + +void Stories::resolve(FullStoryId id, Fn done, bool force) { + if (!force) { + const auto already = lookup(id); + if (already.has_value() || already.error() != NoStory::Unknown) { + if (done) { + done(); + } + return; + } + } + if (const auto i = _resolveSent.find(id.peer); i != end(_resolveSent)) { + if (const auto j = i->second.find(id.story); j != end(i->second)) { + if (done) { + j->second.push_back(std::move(done)); + } + return; + } + } + auto &ids = _resolvePending[id.peer]; + if (ids.empty()) { + crl::on_main(&session(), [=] { + sendResolveRequests(); + }); + } + auto &callbacks = ids[id.story]; + if (done) { + callbacks.push_back(std::move(done)); + } +} + +void Stories::loadAround(FullStoryId id, StoriesContext context) { + if (v::is(context.data)) { + return; + } else if (v::is(context.data) + || v::is(context.data)) { + return; + } + const auto i = _all.find(id.peer); + if (i == end(_all)) { + return; + } + const auto j = i->second.ids.lower_bound(StoryIdDates{ id.story }); + if (j == end(i->second.ids) || j->id != id.story) { + return; + } + const auto ignore = [&] { + const auto side = kIgnorePreloadAroundIfLoaded; + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); + for (auto k = j - left; k != j + right; ++k) { + const auto maybeStory = lookup({ id.peer, k->id }); + if (!maybeStory && maybeStory.error() == NoStory::Unknown) { + return false; + } + } + return true; + }(); + if (ignore) { + return; + } + const auto side = kPreloadAroundCount; + const auto left = ranges::min(int(j - begin(i->second.ids)), side); + const auto right = ranges::min(int(end(i->second.ids) - j), side); + const auto from = j - left; + const auto till = j + right; + for (auto k = from; k != till; ++k) { + resolve({ id.peer, k->id }, nullptr); + } +} + +void Stories::markAsRead(FullStoryId id, bool viewed) { + if (id.peer == _owner->session().userPeerId()) { + return; + } + const auto maybeStory = lookup(id); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; + if (story->expired() && story->pinned()) { + _incrementViewsPending[id.peer].emplace(id.story); + if (!_incrementViewsTimer.isActive()) { + _incrementViewsTimer.callOnce(kIncrementViewsDelay); + } + } + if (!bumpReadTill(id.peer, id.story)) { + return; + } + if (!_markReadPending.contains(id.peer)) { + sendMarkAsReadRequests(); + } + _markReadPending.emplace(id.peer); + _markReadTimer.callOnce(kMarkAsReadDelay); +} + +bool Stories::bumpReadTill(PeerId peerId, StoryId maxReadTill) { + auto &till = _readTill[peerId]; + auto refreshItems = std::vector(); + const auto guard = gsl::finally([&] { + for (const auto id : refreshItems) { + _owner->refreshStoryItemViews({ peerId, id }); + } + }); + if (till < maxReadTill) { + const auto from = till; + till = maxReadTill; + updateUserStoriesState(_owner->peer(peerId)); + const auto i = _stories.find(peerId); + if (i != end(_stories)) { + refreshItems = ranges::make_subrange( + i->second.lower_bound(from + 1), + i->second.lower_bound(till + 1) + ) | ranges::views::transform([=](const auto &pair) { + _owner->session().changes().storyUpdated( + pair.second.get(), + StoryUpdate::Flag::MarkRead); + return pair.first; + }) | ranges::to_vector; + } + } + const auto i = _all.find(peerId); + if (i == end(_all) || i->second.readTill >= maxReadTill) { + return false; + } + const auto wasUnreadCount = i->second.unreadCount(); + i->second.readTill = maxReadTill; + const auto nowUnreadCount = i->second.unreadCount(); + if (wasUnreadCount != nowUnreadCount) { + const auto refreshInList = [&](StorySourcesList list) { + auto &sources = _sources[static_cast(list)]; + const auto i = ranges::find( + sources, + peerId, + &StoriesSourceInfo::id); + if (i != end(sources)) { + i->unreadCount = nowUnreadCount; + sort(list); + } + }; + refreshInList(StorySourcesList::NotHidden); + refreshInList(StorySourcesList::Hidden); + } + return true; +} + +void Stories::toggleHidden( + PeerId peerId, + bool hidden, + std::shared_ptr show) { + const auto user = _owner->peer(peerId)->asUser(); + Assert(user != nullptr); + if (user->hasStoriesHidden() != hidden) { + user->setFlags(hidden + ? (user->flags() | UserDataFlag::StoriesHidden) + : (user->flags() & ~UserDataFlag::StoriesHidden)); + session().api().request(MTPcontacts_ToggleStoriesHidden( + user->inputUser, + MTP_bool(hidden) + )).send(); + } + + const auto name = user->shortName(); + const auto guard = gsl::finally([&] { + if (show) { + const auto phrase = hidden + ? tr::lng_stories_hidden_to_contacts + : tr::lng_stories_shown_in_chats; + show->showToast(phrase( + tr::now, + lt_user, + Ui::Text::Bold(name), + Ui::Text::RichLangValue)); + } + }); + + const auto i = _all.find(peerId); + if (i == end(_all)) { + return; + } + i->second.hidden = hidden; + const auto info = i->second.info(); + const auto main = static_cast(StorySourcesList::NotHidden); + const auto other = static_cast(StorySourcesList::Hidden); + const auto proj = &StoriesSourceInfo::id; + if (hidden) { + const auto i = ranges::find(_sources[main], peerId, proj); + if (i != end(_sources[main])) { + _sources[main].erase(i); + notifySourcesChanged(StorySourcesList::NotHidden); + preloadSourcesChanged(StorySourcesList::NotHidden); + } + const auto j = ranges::find(_sources[other], peerId, proj); + if (j == end(_sources[other])) { + _sources[other].push_back(info); + } else { + *j = info; + } + sort(StorySourcesList::Hidden); + } else { + const auto i = ranges::find(_sources[other], peerId, proj); + if (i != end(_sources[other])) { + _sources[other].erase(i); + notifySourcesChanged(StorySourcesList::Hidden); + preloadSourcesChanged(StorySourcesList::Hidden); + } + const auto j = ranges::find(_sources[main], peerId, proj); + if (j == end(_sources[main])) { + _sources[main].push_back(info); + } else { + *j = info; + } + sort(StorySourcesList::NotHidden); + } +} + +void Stories::sendMarkAsReadRequest( + not_null peer, + StoryId tillId) { + Expects(peer->isUser()); + + const auto peerId = peer->id; + _markReadRequests.emplace(peerId); + const auto finish = [=] { + _markReadRequests.remove(peerId); + if (!_markReadTimer.isActive() + && _markReadPending.contains(peerId)) { + sendMarkAsReadRequests(); + } + checkQuitPreventFinished(); + }; + + const auto api = &_owner->session().api(); + api->request(MTPstories_ReadStories( + peer->asUser()->inputUser, + MTP_int(tillId) + )).done(finish).fail(finish).send(); +} + +void Stories::checkQuitPreventFinished() { + if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { + if (Core::Quitting()) { + LOG(("Stories doesn't prevent quit any more.")); + } + Core::App().quitPreventFinished(); + } +} + +void Stories::sendMarkAsReadRequests() { + _markReadTimer.cancel(); + for (auto i = begin(_markReadPending); i != end(_markReadPending);) { + const auto peerId = *i; + if (_markReadRequests.contains(peerId)) { + ++i; + continue; + } + const auto j = _all.find(peerId); + if (j != end(_all)) { + sendMarkAsReadRequest(j->second.user, j->second.readTill); + } + i = _markReadPending.erase(i); + } +} + +void Stories::sendIncrementViewsRequests() { + if (_incrementViewsPending.empty()) { + return; + } + auto ids = QVector(); + struct Prepared { + PeerId peer = 0; + QVector ids; + }; + auto prepared = std::vector(); + for (const auto &[peer, ids] : _incrementViewsPending) { + if (_incrementViewsRequests.contains(peer)) { + continue; + } + prepared.push_back({ .peer = peer }); + for (const auto &id : ids) { + prepared.back().ids.push_back(MTP_int(id)); + } + } + + const auto api = &_owner->session().api(); + for (auto &[peer, ids] : prepared) { + _incrementViewsRequests.emplace(peer); + const auto finish = [=, peer = peer] { + _incrementViewsRequests.remove(peer); + if (!_incrementViewsTimer.isActive() + && _incrementViewsPending.contains(peer)) { + sendIncrementViewsRequests(); + } + checkQuitPreventFinished(); + }; + api->request(MTPstories_IncrementStoryViews( + _owner->peer(peer)->asUser()->inputUser, + MTP_vector(std::move(ids)) + )).done(finish).fail(finish).send(); + _incrementViewsPending.remove(peer); + } +} + +void Stories::loadViewsSlice( + StoryId id, + QString offset, + Fn done) { + if (_viewsStoryId == id + && _viewsOffset == offset + && (!offset.isEmpty() || _viewsRequestId)) { + if (_viewsRequestId) { + _viewsDone = std::move(done); + } + return; + } + _viewsStoryId = id; + _viewsOffset = offset; + _viewsDone = std::move(done); + + 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_string(offset), + MTP_int(perPage) + )).done([=](const MTPstories_StoryViewsList &result) { + _viewsRequestId = 0; + + 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.list.reserve(data.vviews().v.size()); + for (const auto &view : data.vviews().v) { + 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, + }); + } + const auto fullId = FullStoryId{ + .peer = _owner->session().userPeerId(), + .story = _viewsStoryId, + }; + if (const auto story = lookup(fullId)) { + (*story)->applyViewsSlice(_viewsOffset, slice); + } + if (const auto done = base::take(_viewsDone)) { + done(std::move(slice)); + } + }).fail([=] { + _viewsRequestId = 0; + if (const auto done = base::take(_viewsDone)) { + done({}); + } + }).send(); +} + +const StoriesIds &Stories::archive() const { + return _archive; +} + +rpl::producer<> Stories::archiveChanged() const { + return _archiveChanged.events(); +} + +int Stories::archiveCount() const { + return std::max(_archiveTotal, 0); +} + +bool Stories::archiveCountKnown() const { + return _archiveTotal >= 0; +} + +bool Stories::archiveLoaded() const { + return _archiveLoaded; +} + +const StoriesIds *Stories::saved(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? &i->second.ids : nullptr; +} + +rpl::producer Stories::savedChanged() const { + return _savedChanged.events(); +} + +int Stories::savedCount(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) ? i->second.total : 0; +} + +bool Stories::savedCountKnown(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && (i->second.total >= 0); +} + +bool Stories::savedLoaded(PeerId peerId) const { + const auto i = _saved.find(peerId); + return (i != end(_saved)) && i->second.loaded; +} + +void Stories::archiveLoadMore() { + if (_archiveRequestId || _archiveLoaded) { + return; + } + const auto api = &_owner->session().api(); + _archiveRequestId = api->request(MTPstories_GetStoriesArchive( + MTP_int(_archiveLastId), + MTP_int(_archiveLastId ? kArchivePerPage : kArchiveFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + _archiveRequestId = 0; + + const auto &data = result.data(); + const auto self = _owner->session().user(); + const auto now = base::unixtime::now(); + _archiveTotal = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + _archive.list.emplace(id); + _archiveLastId = id; + if (!parseAndApply(self, story, now)) { + _archive.list.remove(id); + if (_archiveTotal > 0) { + --_archiveTotal; + } + } + } + const auto ids = int(_archive.list.size()); + _archiveLoaded = data.vstories().v.empty(); + _archiveTotal = _archiveLoaded ? ids : std::max(_archiveTotal, ids); + _archiveChanged.fire({}); + }).fail([=] { + _archiveRequestId = 0; + _archiveLoaded = true; + _archiveTotal = int(_archive.list.size()); + _archiveChanged.fire({}); + }).send(); +} + +void Stories::savedLoadMore(PeerId peerId) { + Expects(peerIsUser(peerId)); + + auto &saved = _saved[peerId]; + if (saved.requestId || saved.loaded) { + return; + } + const auto api = &_owner->session().api(); + const auto peer = _owner->peer(peerId); + saved.requestId = api->request(MTPstories_GetPinnedStories( + peer->asUser()->inputUser, + MTP_int(saved.lastId), + MTP_int(saved.lastId ? kSavedPerPage : kSavedFirstPerPage) + )).done([=](const MTPstories_Stories &result) { + auto &saved = _saved[peerId]; + saved.requestId = 0; + + const auto &data = result.data(); + const auto now = base::unixtime::now(); + saved.total = data.vcount().v; + for (const auto &story : data.vstories().v) { + const auto id = story.match([&](const auto &id) { + return id.vid().v; + }); + saved.ids.list.emplace(id); + saved.lastId = id; + if (!parseAndApply(peer, story, now)) { + saved.ids.list.remove(id); + if (saved.total > 0) { + --saved.total; + } + } + } + const auto ids = int(saved.ids.list.size()); + saved.loaded = data.vstories().v.empty(); + saved.total = saved.loaded ? ids : std::max(saved.total, ids); + _savedChanged.fire_copy(peerId); + }).fail([=] { + auto &saved = _saved[peerId]; + saved.requestId = 0; + saved.loaded = true; + saved.total = int(saved.ids.list.size()); + _savedChanged.fire_copy(peerId); + }).send(); +} + +void Stories::deleteList(const std::vector &ids) { + auto list = QVector(); + list.reserve(ids.size()); + const auto selfId = session().userPeerId(); + for (const auto &id : ids) { + if (id.peer == selfId) { + list.push_back(MTP_int(id.story)); + } + } + if (!list.empty()) { + const auto api = &_owner->session().api(); + api->request(MTPstories_DeleteStories( + MTP_vector(list) + )).done([=](const MTPVector &result) { + for (const auto &id : result.v) { + applyDeleted({ selfId, id.v }); + } + }).send(); + } +} + +void Stories::togglePinnedList( + const std::vector &ids, + bool pinned) { + auto list = QVector(); + list.reserve(ids.size()); + const auto selfId = session().userPeerId(); + for (const auto &id : ids) { + if (id.peer == selfId) { + list.push_back(MTP_int(id.story)); + } + } + if (list.empty()) { + return; + } + const auto api = &_owner->session().api(); + api->request(MTPstories_TogglePinned( + MTP_vector(list), + MTP_bool(pinned) + )).done([=](const MTPVector &result) { + auto &saved = _saved[selfId]; + const auto loaded = saved.loaded; + const auto lastId = !saved.ids.list.empty() + ? saved.ids.list.back() + : saved.lastId + ? saved.lastId + : std::numeric_limits::max(); + auto dirty = false; + for (const auto &id : result.v) { + if (const auto maybeStory = lookup({ selfId, id.v })) { + const auto story = *maybeStory; + story->setPinned(pinned); + if (pinned) { + const auto add = loaded || (id.v >= lastId); + if (!add) { + dirty = true; + } else if (saved.ids.list.emplace(id.v).second) { + if (saved.total >= 0) { + ++saved.total; + } + } + } else if (saved.ids.list.remove(id.v)) { + if (saved.total > 0) { + --saved.total; + } + } else if (!loaded) { + dirty = true; + } + } else if (!loaded) { + dirty = true; + } + } + if (dirty) { + savedLoadMore(selfId); + } else { + _savedChanged.fire_copy(selfId); + } + }).send(); +} + +void Stories::report( + std::shared_ptr show, + FullStoryId id, + Ui::ReportReason reason, + QString text) { + if (const auto maybeStory = lookup(id)) { + const auto story = *maybeStory; + Api::SendReport(show, story->peer(), reason, text, story->id()); + } +} + +bool Stories::isQuitPrevent() { + if (!_markReadPending.empty()) { + sendMarkAsReadRequests(); + } + if (!_incrementViewsPending.empty()) { + sendIncrementViewsRequests(); + } + if (_markReadRequests.empty() && _incrementViewsRequests.empty()) { + return false; + } + LOG(("Stories prevents quit, marking as read...")); + return true; +} + +void Stories::incrementPreloadingMainSources() { + Expects(_preloadingMainSourcesCounter >= 0); + + if (++_preloadingMainSourcesCounter == 1 + && rebuildPreloadSources(StorySourcesList::NotHidden)) { + continuePreloading(); + } +} + +void Stories::decrementPreloadingMainSources() { + Expects(_preloadingMainSourcesCounter > 0); + + if (!--_preloadingMainSourcesCounter + && rebuildPreloadSources(StorySourcesList::NotHidden)) { + continuePreloading(); + } +} + +void Stories::incrementPreloadingHiddenSources() { + Expects(_preloadingHiddenSourcesCounter >= 0); + + if (++_preloadingHiddenSourcesCounter == 1 + && rebuildPreloadSources(StorySourcesList::Hidden)) { + continuePreloading(); + } +} + +void Stories::decrementPreloadingHiddenSources() { + Expects(_preloadingHiddenSourcesCounter > 0); + + if (!--_preloadingHiddenSourcesCounter + && rebuildPreloadSources(StorySourcesList::Hidden)) { + continuePreloading(); + } +} + +void Stories::setPreloadingInViewer(std::vector ids) { + ids.erase(ranges::remove_if(ids, [&](FullStoryId id) { + return _preloaded.contains(id); + }), end(ids)); + if (_toPreloadViewer != ids) { + _toPreloadViewer = std::move(ids); + continuePreloading(); + } +} + +std::optional Stories::peerSourceState( + not_null peer, + StoryId storyMaxId) { + const auto i = _readTill.find(peer->id); + if (_readTillReceived || (i != end(_readTill))) { + return PeerSourceState{ + .maxId = storyMaxId, + .readTill = std::min( + storyMaxId, + (i != end(_readTill)) ? i->second : 0), + }; + } + requestReadTills(); + _pendingUserStateMaxId[peer] = storyMaxId; + return std::nullopt; +} + +void Stories::requestReadTills() { + if (_readTillReceived || _readTillsRequestId) { + return; + } + const auto api = &_owner->session().api(); + _readTillsRequestId = api->request(MTPstories_GetAllReadUserStories( + )).done([=](const MTPUpdates &result) { + _readTillReceived = true; + api->applyUpdates(result); + for (auto &[peer, maxId] : base::take(_pendingUserStateMaxId)) { + updateUserStoriesState(peer); + } + for (const auto &storyId : base::take(_pendingReadTillItems)) { + _owner->refreshStoryItemViews(storyId); + } + }).send(); +} + +bool Stories::isUnread(not_null story) { + const auto till = _readTill.find(story->peer()->id); + if (till == end(_readTill) && !_readTillReceived) { + requestReadTills(); + _pendingReadTillItems.emplace(story->fullId()); + return false; + } + const auto readTill = (till != end(_readTill)) ? till->second : 0; + return (story->id() > readTill); +} + +void Stories::registerPolling(not_null story, Polling polling) { + auto &settings = _pollingSettings[story]; + switch (polling) { + case Polling::Chat: ++settings.chat; break; + case Polling::Viewer: + ++settings.viewer; + if (story->peer()->isSelf() + && _pollingViews.emplace(story).second) { + sendPollingViewsRequests(); + } + break; + } + maybeSchedulePolling(story, settings, base::unixtime::now()); +} + +void Stories::unregisterPolling(not_null story, Polling polling) { + const auto i = _pollingSettings.find(story); + Assert(i != end(_pollingSettings)); + + switch (polling) { + case Polling::Chat: + Assert(i->second.chat > 0); + --i->second.chat; + break; + case Polling::Viewer: + Assert(i->second.viewer > 0); + if (!--i->second.viewer) { + _pollingViews.remove(story); + if (_pollingViews.empty()) { + _pollingViewsTimer.cancel(); + } + } + break; + } + if (!i->second.chat && !i->second.viewer) { + _pollingSettings.erase(i); + } +} + +bool Stories::registerPolling(FullStoryId id, Polling polling) { + if (const auto maybeStory = lookup(id)) { + registerPolling(*maybeStory, polling); + return true; + } + return false; +} + +void Stories::unregisterPolling(FullStoryId id, Polling polling) { + if (const auto maybeStory = lookup(id)) { + unregisterPolling(*maybeStory, polling); + } else if (const auto i = _deletingStories.find(id) + ; i != end(_deletingStories)) { + unregisterPolling(i->second.get(), polling); + } else { + Unexpected("Couldn't find story for unregistering polling."); + } +} + +int Stories::pollingInterval(const PollingSettings &settings) const { + return settings.viewer ? kPollingIntervalViewer : kPollingIntervalChat; +} + +void Stories::maybeSchedulePolling( + not_null story, + const PollingSettings &settings, + TimeId now) { + const auto last = story->lastUpdateTime(); + const auto next = last + pollingInterval(settings); + const auto left = std::max(next - now, 0) * crl::time(1000) + 1; + if (!_pollingTimer.isActive() || _pollingTimer.remainingTime() > left) { + _pollingTimer.callOnce(left); + } +} + +void Stories::sendPollingRequests() { + auto min = 0; + const auto now = base::unixtime::now(); + for (const auto &[story, settings] : _pollingSettings) { + const auto last = story->lastUpdateTime(); + const auto next = last + pollingInterval(settings); + if (now >= next) { + resolve(story->fullId(), nullptr, true); + } else { + const auto left = (next - now) * crl::time(1000) + 1; + if (!min || left < min) { + min = left; + } + } + } + if (min > 0) { + _pollingTimer.callOnce(min); + } +} + +void Stories::sendPollingViewsRequests() { + if (_pollingViews.empty()) { + return; + } else if (!_viewsRequestId) { + Assert(_viewsDone == nullptr); + loadViewsSlice(_pollingViews.front()->id(), QString(), nullptr); + } + _pollingViewsTimer.callOnce(kPollViewsInterval); +} + +void Stories::updateUserStoriesState(not_null peer) { + const auto till = _readTill.find(peer->id); + const auto readTill = (till != end(_readTill)) ? till->second : 0; + const auto pendingMaxId = [&] { + const auto j = _pendingUserStateMaxId.find(peer); + return (j != end(_pendingUserStateMaxId)) ? j->second : 0; + }; + const auto i = _all.find(peer->id); + const auto max = (i != end(_all)) + ? (i->second.ids.empty() ? 0 : i->second.ids.back().id) + : pendingMaxId(); + if (const auto user = peer->asUser()) { + user->setStoriesState(!max + ? UserData::StoriesState::None + : (max <= readTill) + ? UserData::StoriesState::HasRead + : UserData::StoriesState::HasUnread); + } +} + +void Stories::preloadSourcesChanged(StorySourcesList list) { + if (rebuildPreloadSources(list)) { + continuePreloading(); + } +} + +bool Stories::rebuildPreloadSources(StorySourcesList list) { + const auto index = static_cast(list); + const auto &counter = (list == StorySourcesList::Hidden) + ? _preloadingHiddenSourcesCounter + : _preloadingMainSourcesCounter; + if (!counter) { + return !base::take(_toPreloadSources[index]).empty(); + } + auto now = std::vector(); + auto processed = 0; + for (const auto &source : _sources[index]) { + const auto i = _all.find(source.id); + if (i != end(_all)) { + if (const auto id = i->second.toOpen().id) { + const auto fullId = FullStoryId{ source.id, id }; + if (!_preloaded.contains(fullId)) { + now.push_back(fullId); + } + } + } + if (++processed >= kMaxPreloadSources) { + break; + } + } + if (now != _toPreloadSources[index]) { + _toPreloadSources[index] = std::move(now); + return true; + } + return false; +} + +void Stories::continuePreloading() { + const auto now = _preloading ? _preloading->id() : FullStoryId(); + if (now) { + if (shouldContinuePreload(now)) { + return; + } + _preloading = nullptr; + } + const auto id = nextPreloadId(); + if (!id) { + return; + } else if (const auto maybeStory = lookup(id)) { + startPreloading(*maybeStory); + } +} + +bool Stories::shouldContinuePreload(FullStoryId id) const { + const auto first = ranges::views::concat( + _toPreloadViewer, + _toPreloadSources[static_cast(StorySourcesList::Hidden)], + _toPreloadSources[static_cast(StorySourcesList::NotHidden)] + ) | ranges::views::take(kStillPreloadFromFirst); + return ranges::contains(first, id); +} + +FullStoryId Stories::nextPreloadId() const { + const auto hidden = static_cast(StorySourcesList::Hidden); + const auto main = static_cast(StorySourcesList::NotHidden); + const auto result = !_toPreloadViewer.empty() + ? _toPreloadViewer.front() + : !_toPreloadSources[hidden].empty() + ? _toPreloadSources[hidden].front() + : !_toPreloadSources[main].empty() + ? _toPreloadSources[main].front() + : FullStoryId(); + + Ensures(!_preloaded.contains(result)); + return result; +} + +void Stories::startPreloading(not_null story) { + Expects(!_preloaded.contains(story->fullId())); + + const auto id = story->fullId(); + auto preloading = std::make_unique(story, [=] { + _preloading = nullptr; + preloadFinished(id, true); + }); + if (!_preloaded.contains(id)) { + _preloading = std::move(preloading); + } +} + +void Stories::preloadFinished(FullStoryId id, bool markAsPreloaded) { + for (auto &sources : _toPreloadSources) { + sources.erase(ranges::remove(sources, id), end(sources)); + } + _toPreloadViewer.erase( + ranges::remove(_toPreloadViewer, id), + end(_toPreloadViewer)); + if (markAsPreloaded) { + _preloaded.emplace(id); + } + crl::on_main(this, [=] { + continuePreloading(); + }); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h new file mode 100644 index 000000000..8d8b23da4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories.h @@ -0,0 +1,397 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/qt/qt_compare.h" +#include "base/expected.h" +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "data/data_story.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class Show; +enum class ReportReason; +} // namespace Ui + +namespace Data { + +class Folder; +class Session; +struct StoryView; +struct StoryIdDates; +class Story; +class StoryPreload; + +struct StoriesIds { + base::flat_set> list; + + friend inline bool operator==( + const StoriesIds&, + const StoriesIds&) = default; +}; + +struct StoriesSourceInfo { + PeerId id = 0; + TimeId last = 0; + uint32 count : 15 = 0; + uint32 unreadCount : 15 = 0; + uint32 premium : 1 = 0; + + friend inline bool operator==( + StoriesSourceInfo, + StoriesSourceInfo) = default; +}; + +struct StoriesSource { + not_null user; + base::flat_set ids; + StoryId readTill = 0; + bool hidden = false; + + [[nodiscard]] StoriesSourceInfo info() const; + [[nodiscard]] int unreadCount() const; + [[nodiscard]] StoryIdDates toOpen() const; + + friend inline bool operator==(StoriesSource, StoriesSource) = default; +}; + +enum class NoStory : uchar { + Unknown, + Deleted, +}; + +enum class StorySourcesList : uchar { + NotHidden, + Hidden, +}; + +struct StoriesContextSingle { + friend inline auto operator<=>( + StoriesContextSingle, + StoriesContextSingle) = default; + friend inline bool operator==(StoriesContextSingle, StoriesContextSingle) = default; +}; + +struct StoriesContextPeer { + friend inline auto operator<=>( + StoriesContextPeer, + StoriesContextPeer) = default; + friend inline bool operator==(StoriesContextPeer, StoriesContextPeer) = default; +}; + +struct StoriesContextSaved { + friend inline auto operator<=>( + StoriesContextSaved, + StoriesContextSaved) = default; + friend inline bool operator==(StoriesContextSaved, StoriesContextSaved) = default; +}; + +struct StoriesContextArchive { + friend inline auto operator<=>( + StoriesContextArchive, + StoriesContextArchive) = default; + friend inline bool operator==(StoriesContextArchive, StoriesContextArchive) = default; +}; + +struct StoriesContext { + std::variant< + StoriesContextSingle, + StoriesContextPeer, + StoriesContextSaved, + StoriesContextArchive, + StorySourcesList> data; + + friend inline auto operator<=>( + StoriesContext, + StoriesContext) = default; + 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 { +public: + explicit Stories(not_null owner); + ~Stories(); + + static constexpr auto kPinnedToastDuration = 4 * crl::time(1000); + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + void updateDependentMessages(not_null story); + void registerDependentMessage( + not_null dependent, + not_null dependency); + void unregisterDependentMessage( + not_null dependent, + not_null dependency); + + 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); + + const StoriesSource *source(PeerId id) const; + [[nodiscard]] const std::vector &sources( + StorySourcesList list) const; + [[nodiscard]] bool sourcesLoaded(StorySourcesList list) const; + [[nodiscard]] rpl::producer<> sourcesChanged( + StorySourcesList list) const; + [[nodiscard]] rpl::producer sourceChanged() const; + [[nodiscard]] rpl::producer itemsChanged() const; + + [[nodiscard]] base::expected, NoStory> lookup( + FullStoryId id) const; + void resolve(FullStoryId id, Fn done, bool force = false); + [[nodiscard]] std::shared_ptr resolveItem(FullStoryId id); + [[nodiscard]] std::shared_ptr resolveItem( + not_null story); + + [[nodiscard]] bool isQuitPrevent(); + void markAsRead(FullStoryId id, bool viewed); + + void toggleHidden( + PeerId peerId, + bool hidden, + std::shared_ptr show); + + static constexpr auto kViewsPerPage = 50; + void loadViewsSlice( + StoryId id, + QString offset, + Fn done); + + [[nodiscard]] const StoriesIds &archive() const; + [[nodiscard]] rpl::producer<> archiveChanged() const; + [[nodiscard]] int archiveCount() const; + [[nodiscard]] bool archiveCountKnown() const; + [[nodiscard]] bool archiveLoaded() const; + void archiveLoadMore(); + + [[nodiscard]] const StoriesIds *saved(PeerId peerId) const; + [[nodiscard]] rpl::producer savedChanged() const; + [[nodiscard]] int savedCount(PeerId peerId) const; + [[nodiscard]] bool savedCountKnown(PeerId peerId) const; + [[nodiscard]] bool savedLoaded(PeerId peerId) const; + void savedLoadMore(PeerId peerId); + + void deleteList(const std::vector &ids); + void togglePinnedList(const std::vector &ids, bool pinned); + void report( + std::shared_ptr show, + FullStoryId id, + Ui::ReportReason reason, + QString text); + + void incrementPreloadingMainSources(); + void decrementPreloadingMainSources(); + void incrementPreloadingHiddenSources(); + void decrementPreloadingHiddenSources(); + void setPreloadingInViewer(std::vector ids); + + struct PeerSourceState { + StoryId maxId = 0; + StoryId readTill = 0; + }; + [[nodiscard]] std::optional peerSourceState( + not_null peer, + StoryId storyMaxId); + [[nodiscard]] bool isUnread(not_null story); + + enum class Polling { + Chat, + Viewer, + }; + void registerPolling(not_null story, Polling polling); + void unregisterPolling(not_null story, Polling polling); + + [[nodiscard]] bool registerPolling(FullStoryId id, Polling polling); + void unregisterPolling(FullStoryId id, Polling polling); + void requestUserStories( + not_null user, + Fn done = nullptr); + + void savedStateChanged(not_null story); + [[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; + int total = -1; + StoryId lastId = 0; + bool loaded = false; + mtpRequestId requestId = 0; + }; + struct PollingSettings { + int chat = 0; + int viewer = 0; + }; + + void parseAndApply(const MTPUserStories &stories); + [[nodiscard]] Story *parseAndApply( + not_null peer, + const MTPDstoryItem &data, + TimeId now); + StoryIdDates parseAndApply( + not_null peer, + const MTPstoryItem &story, + TimeId now); + void processResolvedStories( + not_null peer, + const QVector &list); + void sendResolveRequests(); + void finalizeResolve(FullStoryId id); + void updateUserStoriesState(not_null peer); + + void applyDeleted(FullStoryId id); + void applyExpired(FullStoryId id); + void applyRemovedFromActive(FullStoryId id); + void applyDeletedFromSources(PeerId id, StorySourcesList list); + void removeDependencyStory(not_null story); + void sort(StorySourcesList list); + bool bumpReadTill(PeerId peerId, StoryId maxReadTill); + void requestReadTills(); + + void sendMarkAsReadRequests(); + void sendMarkAsReadRequest(not_null peer, StoryId tillId); + void sendIncrementViewsRequests(); + void checkQuitPreventFinished(); + + void registerExpiring(TimeId expires, FullStoryId id); + void scheduleExpireTimer(); + void processExpired(); + + void preloadSourcesChanged(StorySourcesList list); + bool rebuildPreloadSources(StorySourcesList list); + void continuePreloading(); + [[nodiscard]] bool shouldContinuePreload(FullStoryId id) const; + [[nodiscard]] FullStoryId nextPreloadId() const; + void startPreloading(not_null story); + void preloadFinished(FullStoryId id, bool markAsPreloaded = false); + void preloadListsMore(); + + void notifySourcesChanged(StorySourcesList list); + void pushHiddenCountsToFolder(); + + [[nodiscard]] int pollingInterval( + const PollingSettings &settings) const; + void maybeSchedulePolling( + not_null story, + const PollingSettings &settings, + TimeId now); + void sendPollingRequests(); + void sendPollingViewsRequests(); + + const not_null _owner; + std::unordered_map< + PeerId, + base::flat_map>> _stories; + base::flat_map> _deletingStories; + std::unordered_map< + PeerId, + base::flat_map>> _items; + base::flat_multi_map _expiring; + base::flat_set _deleted; + base::Timer _expireTimer; + bool _expireSchedulePosted = false; + + base::flat_map< + PeerId, + base::flat_map>>> _resolvePending; + base::flat_map< + PeerId, + base::flat_map>>> _resolveSent; + + std::unordered_map< + not_null, + base::flat_set>> _dependentMessages; + + std::unordered_map _all; + std::vector _sources[kStorySourcesListCount]; + rpl::event_stream<> _sourcesChanged[kStorySourcesListCount]; + bool _sourcesLoaded[kStorySourcesListCount] = { false }; + QString _sourcesStates[kStorySourcesListCount]; + Folder *_folderForHidden = nullptr; + + mtpRequestId _loadMoreRequestId[kStorySourcesListCount] = { 0 }; + + rpl::event_stream _sourceChanged; + rpl::event_stream _itemsChanged; + + StoriesIds _archive; + int _archiveTotal = -1; + StoryId _archiveLastId = 0; + bool _archiveLoaded = false; + rpl::event_stream<> _archiveChanged; + mtpRequestId _archiveRequestId = 0; + + std::unordered_map _saved; + rpl::event_stream _savedChanged; + + base::flat_set _markReadPending; + base::Timer _markReadTimer; + base::flat_set _markReadRequests; + base::flat_map< + not_null, + std::vector>> _requestingUserStories; + + base::flat_map> _incrementViewsPending; + base::Timer _incrementViewsTimer; + base::flat_set _incrementViewsRequests; + + StoryId _viewsStoryId = 0; + QString _viewsOffset; + Fn _viewsDone; + mtpRequestId _viewsRequestId = 0; + + base::flat_set _preloaded; + std::vector _toPreloadSources[kStorySourcesListCount]; + std::vector _toPreloadViewer; + std::unique_ptr _preloading; + int _preloadingHiddenSourcesCounter = 0; + int _preloadingMainSourcesCounter = 0; + + base::flat_map _readTill; + base::flat_set _pendingReadTillItems; + base::flat_map, StoryId> _pendingUserStateMaxId; + mtpRequestId _readTillsRequestId = 0; + bool _readTillReceived = false; + + base::flat_map, PollingSettings> _pollingSettings; + base::flat_set> _pollingViews; + base::Timer _pollingTimer; + base::Timer _pollingViewsTimer; + + rpl::variable _stealthMode; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.cpp b/Telegram/SourceFiles/data/data_stories_ids.cpp new file mode 100644 index 000000000..7506abea5 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.cpp @@ -0,0 +1,161 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_stories_ids.h" + +#include "data/data_changes.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "main/main_session.h" + +namespace Data { + +rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto stories = &peer->owner().stories(); + if (!stories->savedCountKnown(peer->id)) { + return; + } + + const auto saved = stories->saved(peer->id); + Assert(saved != nullptr); + const auto count = stories->savedCount(peer->id); + const auto around = saved->list.lower_bound(aroundId); + const auto hasBefore = int(around - begin(saved->list)); + const auto hasAfter = int(end(saved->list) - around); + if (hasAfter < limit) { + stories->savedLoadMore(peer->id); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = base::flat_set{ + std::make_reverse_iterator(around + takeAfter), + std::make_reverse_iterator(around - takeBefore) + }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto stories = &peer->owner().stories(); + stories->savedChanged( + ) | rpl::filter( + rpl::mappers::_1 == peer->id + ) | rpl::start_with_next(schedule, lifetime); + + if (!stories->savedCountKnown(peer->id)) { + stories->savedLoadMore(peer->id); + } + + push(); + + return lifetime; + }; +} + +rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + struct State { + StoriesIdsSlice slice; + base::has_weak_ptr guard; + bool scheduled = false; + }; + const auto state = lifetime.make_state(); + + const auto push = [=] { + state->scheduled = false; + + const auto stories = &session->data().stories(); + if (!stories->archiveCountKnown()) { + return; + } + + const auto &archive = stories->archive(); + const auto count = stories->archiveCount(); + const auto i = archive.list.lower_bound(aroundId); + const auto hasBefore = int(i - begin(archive.list)); + const auto hasAfter = int(end(archive.list) - i); + if (hasAfter < limit) { + stories->archiveLoadMore(); + } + const auto takeBefore = std::min(hasBefore, limit); + const auto takeAfter = std::min(hasAfter, limit); + auto ids = base::flat_set{ + std::make_reverse_iterator(i + takeAfter), + std::make_reverse_iterator(i - takeBefore) + }; + const auto added = int(ids.size()); + state->slice = StoriesIdsSlice( + std::move(ids), + count, + (hasBefore - takeBefore), + count - hasBefore - added); + consumer.put_next_copy(state->slice); + }; + const auto schedule = [=] { + if (state->scheduled) { + return; + } + state->scheduled = true; + Ui::PostponeCall(&state->guard, [=] { + if (state->scheduled) { + push(); + } + }); + }; + + const auto stories = &session->data().stories(); + stories->archiveChanged( + ) | rpl::start_with_next(schedule, lifetime); + + if (!stories->archiveCountKnown()) { + stories->archiveLoadMore(); + } + + push(); + + return lifetime; + }; +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_stories_ids.h b/Telegram/SourceFiles/data/data_stories_ids.h new file mode 100644 index 000000000..26f827f96 --- /dev/null +++ b/Telegram/SourceFiles/data/data_stories_ids.h @@ -0,0 +1,32 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "data/data_abstract_sparse_ids.h" + +class PeerData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +using StoriesIdsSlice = AbstractSparseIds>; + +[[nodiscard]] rpl::producer SavedStoriesIds( + not_null peer, + StoryId aroundId, + int limit); + +[[nodiscard]] rpl::producer ArchiveStoriesIds( + not_null session, + StoryId aroundId, + int limit); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp new file mode 100644 index 000000000..d5faa4350 --- /dev/null +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -0,0 +1,678 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_story.h" + +#include "base/unixtime.h" +#include "api/api_text_entities.h" +#include "data/data_document.h" +#include "data/data_changes.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_thread.h" +#include "history/history_item.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "media/streaming/media_streaming_reader.h" +#include "storage/download_manager_mtproto.h" +#include "storage/file_download.h" // kMaxFileInMemory +#include "ui/text/text_utilities.h" + +namespace Data { +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 { +public: + LoadTask( + FullStoryId id, + not_null document, + Fn done); + ~LoadTask(); + +private: + bool readyToRequest() const override; + int64 takeNextRequestOffset() override; + bool feedPart(int64 offset, const QByteArray &bytes) override; + void cancelOnFail() override; + bool setWebFileSizeHook(int64 size) override; + + base::flat_map _parts; + Fn _done; + base::flat_set _requestedOffsets; + int64 _full = 0; + int _nextRequestOffset = 0; + bool _finished = false; + bool _failed = false; + +}; + +StoryPreload::LoadTask::LoadTask( + FullStoryId id, + not_null document, + Fn done) +: DownloadMtprotoTask( + &document->session().downloader(), + document->videoPreloadLocation(), + FileOriginStory(id.peer, id.story)) +, _done(std::move(done)) +, _full(document->size) { + const auto prefix = document->videoPreloadPrefix(); + Assert(prefix > 0 && prefix <= document->size); + const auto part = Storage::kDownloadPartSize; + const auto parts = (prefix + part - 1) / part; + for (auto i = 0; i != parts; ++i) { + _parts.emplace(i * part, QByteArray()); + } + addToQueue(); +} + +StoryPreload::LoadTask::~LoadTask() { + if (!_finished && !_failed) { + cancelAllRequests(); + } +} + +bool StoryPreload::LoadTask::readyToRequest() const { + const auto part = Storage::kDownloadPartSize; + return !_failed && (_nextRequestOffset < _parts.size() * part); +} + +int64 StoryPreload::LoadTask::takeNextRequestOffset() { + Expects(readyToRequest()); + + _requestedOffsets.emplace(_nextRequestOffset); + _nextRequestOffset += Storage::kDownloadPartSize; + return _requestedOffsets.back(); +} + +bool StoryPreload::LoadTask::feedPart( + int64 offset, + const QByteArray &bytes) { + Expects(offset < _parts.size() * Storage::kDownloadPartSize); + Expects(_requestedOffsets.contains(int(offset))); + Expects(bytes.size() <= Storage::kDownloadPartSize); + + const auto part = Storage::kDownloadPartSize; + _requestedOffsets.remove(int(offset)); + _parts[offset] = bytes; + if ((_nextRequestOffset + part >= _parts.size() * part) + && _requestedOffsets.empty()) { + _finished = true; + removeFromQueue(); + auto result = ::Media::Streaming::SerializeComplexPartsMap(_parts); + if (result.size() == _full) { + // Make sure it is parsed as a complex map. + result.push_back(char(0)); + } + _done(result); + } + return true; +} + +void StoryPreload::LoadTask::cancelOnFail() { + _failed = true; + cancelAllRequests(); + _done({}); +} + +bool StoryPreload::LoadTask::setWebFileSizeHook(int64 size) { + _failed = true; + cancelAllRequests(); + _done({}); + return false; +} + +Story::Story( + StoryId id, + not_null peer, + StoryMedia media, + const MTPDstoryItem &data, + TimeId now) +: _id(id) +, _peer(peer) +, _date(data.vdate().v) +, _expires(data.vexpire_date().v) { + applyFields(std::move(media), data, now, true); +} + +Session &Story::owner() const { + return _peer->owner(); +} + +Main::Session &Story::session() const { + return _peer->session(); +} + +not_null Story::peer() const { + return _peer; +} + +StoryId Story::id() const { + return _id; +} + +bool Story::mine() const { + return _peer->isSelf(); +} + +StoryIdDates Story::idDates() const { + return { _id, _date, _expires }; +} + +FullStoryId Story::fullId() const { + return { _peer->id, _id }; +} + +TimeId Story::date() const { + return _date; +} + +TimeId Story::expires() const { + return _expires; +} + +bool Story::expired(TimeId now) const { + return _expires <= (now ? now : base::unixtime::now()); +} + +bool Story::unsupported() const { + return v::is_null(_media.data); +} + +const StoryMedia &Story::media() const { + return _media; +} + +PhotoData *Story::photo() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +DocumentData *Story::document() const { + const auto result = std::get_if>(&_media.data); + return result ? result->get() : nullptr; +} + +bool Story::hasReplyPreview() const { + return v::match(_media.data, [](not_null photo) { + return !photo->isNull(); + }, [](not_null document) { + return document->hasThumbnail(); + }, [](v::null_t) { + return false; + }); +} + +Image *Story::replyPreview() const { + return v::match(_media.data, [&](not_null photo) { + return photo->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }, [&](not_null document) { + return document->getReplyPreview( + Data::FileOriginStory(_peer->id, _id), + _peer, + false); + }, [](v::null_t) { + return (Image*)nullptr; + }); +} + +TextWithEntities Story::inReplyText() const { + const auto type = tr::lng_in_dlg_story(tr::now); + return _caption.text.isEmpty() + ? Ui::Text::PlainLink(type) + : tr::lng_dialogs_text_media( + tr::now, + lt_media_part, + tr::lng_dialogs_text_media_wrapped( + tr::now, + lt_media, + Ui::Text::PlainLink(type), + Ui::Text::WithEntities), + lt_caption, + _caption, + Ui::Text::WithEntities); +} + +void Story::setPinned(bool pinned) { + _pinned = pinned; +} + +bool Story::pinned() const { + return _pinned; +} + +StoryPrivacy Story::privacy() const { + return _privacyPublic + ? StoryPrivacy::Public + : _privacyCloseFriends + ? StoryPrivacy::CloseFriends + : _privacyContacts + ? StoryPrivacy::Contacts + : _privacySelectedContacts + ? StoryPrivacy::SelectedContacts + : StoryPrivacy::Other; +} + +bool Story::forbidsForward() const { + return _noForwards; +} + +bool Story::edited() const { + return _edited; +} + +bool Story::canDownloadIfPremium() const { + return !forbidsForward() || _peer->isSelf(); +} + +bool Story::canDownloadChecked() const { + return _peer->isSelf() + || (canDownloadIfPremium() && _peer->session().premium()); +} + +bool Story::canShare() const { + return _privacyPublic && !forbidsForward() && (pinned() || !expired()); +} + +bool Story::canDelete() const { + return _peer->isSelf(); +} + +bool Story::canReport() const { + return !_peer->isSelf(); +} + +bool Story::hasDirectLink() const { + if (!_privacyPublic || (!_pinned && expired())) { + return false; + } + const auto user = _peer->asUser(); + return user && !user->username().isEmpty(); +} + +std::optional Story::errorTextForForward( + not_null to) const { + const auto peer = to->peer(); + const auto holdsPhoto = v::is>(_media.data); + const auto first = holdsPhoto + ? ChatRestriction::SendPhotos + : ChatRestriction::SendVideos; + const auto second = holdsPhoto + ? ChatRestriction::SendVideos + : ChatRestriction::SendPhotos; + if (const auto error = Data::RestrictionError(peer, first)) { + return *error; + } else if (const auto error = Data::RestrictionError(peer, second)) { + return *error; + } else if (!Data::CanSend(to, first, false) + || !Data::CanSend(to, second, false)) { + return tr::lng_forward_cant(tr::now); + } + return {}; +} + +void Story::setCaption(TextWithEntities &&caption) { + _caption = std::move(caption); +} + +const TextWithEntities &Story::caption() const { + static const auto empty = TextWithEntities(); + 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 StoryViews &Story::viewsList() const { + return _views; +} + +int Story::views() const { + return _views.total; +} + +int Story::reactions() const { + return _views.reactions; +} + +void Story::applyViewsSlice( + 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(_views.list.size()); + if (known >= _recentViewers.size()) { + const auto take = std::min(known, kRecentViewersMax); + auto viewers = _views.list + | ranges::views::take(take) + | ranges::views::transform(&StoryView::peer) + | ranges::to_vector; + if (_recentViewers != viewers) { + _recentViewers = std::move(viewers); + if (!changed) { + // Count not changed, but list of recent viewers changed. + _peer->session().changes().storyUpdated( + this, + UpdateFlag::ViewsAdded); + } + } + } + if (changed) { + _peer->session().changes().storyUpdated( + this, + UpdateFlag::ViewsAdded); + } +} + +const std::vector &Story::locations() const { + return _locations; +} + +void Story::applyChanges( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now) { + applyFields(std::move(media), data, now, false); +} + +void Story::applyFields( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now, + 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() + ? StoryPrivacy::Public + : data.is_close_friends() + ? StoryPrivacy::CloseFriends + : data.is_contacts() + ? StoryPrivacy::Contacts + : data.is_selected_contacts() + ? StoryPrivacy::SelectedContacts + : StoryPrivacy::Other; + const auto noForwards = data.is_noforwards(); + auto caption = TextWithEntities{ + data.vcaption().value_or_empty(), + Api::EntitiesFromMTP( + &owner().session(), + data.ventities().value_or_empty()), + }; + 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(); + auto &&cut = list->v + | ranges::views::take(kRecentViewersMax); + for (const auto &id : cut) { + viewers.push_back(owner.peer(peerFromUser(id))); + } + } + } + } + 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.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); + _privacyContacts = (privacy == StoryPrivacy::Contacts); + _privacySelectedContacts = (privacy == StoryPrivacy::SelectedContacts); + _noForwards = noForwards; + _edited = edited; + _pinned = pinned; + _noForwards = noForwards; + if (_views.reactions != reactions || _views.total != views) { + _views = StoryViews{ .reactions = reactions, .total = views }; + } + if (viewsChanged) { + _recentViewers = std::move(viewers); + } + if (mediaChanged) { + _media = std::move(media); + } + if (captionChanged) { + _caption = std::move(caption); + } + if (locationsChanged) { + _locations = std::move(locations); + } + if (reactionChanged) { + _sentReactionId = reaction; + } + + 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()) + | (reactionChanged ? UpdateFlag::Reaction : UpdateFlag())); + } + if (!initial && (captionChanged || mediaChanged)) { + if (const auto item = _peer->owner().stories().lookupItem(this)) { + item->applyChanges(this); + } + _peer->owner().refreshStoryItemViews(fullId()); + } + if (pinnedChanged) { + _peer->owner().stories().savedStateChanged(this); + } +} + +TimeId Story::lastUpdateTime() const { + return _lastUpdateTime; +} + +StoryPreload::StoryPreload(not_null story, Fn done) +: _story(story) +, _done(std::move(done)) { + start(); +} + +StoryPreload::~StoryPreload() { + if (_photo) { + base::take(_photo)->owner()->cancel(); + } +} + +FullStoryId StoryPreload::id() const { + return _story->fullId(); +} + +not_null StoryPreload::story() const { + return _story; +} + +void StoryPreload::start() { + const auto origin = FileOriginStory( + _story->peer()->id, + _story->id()); + if (const auto photo = _story->photo()) { + _photo = photo->createMediaView(); + if (_photo->loaded()) { + callDone(); + } else { + _photo->automaticLoad(origin, _story->peer()); + photo->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return _photo->loaded(); + }) | rpl::start_with_next([=] { callDone(); }, _lifetime); + } + } else if (const auto video = _story->document()) { + if (video->canBeStreamed(nullptr) && video->videoPreloadPrefix()) { + const auto key = video->bigFileBaseCacheKey(); + if (key) { + const auto weak = base::make_weak(this); + video->owner().cacheBigFile().get(key, [weak]( + const QByteArray &result) { + if (!result.isEmpty()) { + crl::on_main([weak] { + if (const auto strong = weak.get()) { + strong->callDone(); + } + }); + } else { + crl::on_main([weak] { + if (const auto strong = weak.get()) { + strong->load(); + } + }); + } + }); + } else { + callDone(); + } + } else { + callDone(); + } + } else { + callDone(); + } +} + +void StoryPreload::load() { + Expects(_story->document() != nullptr); + + const auto video = _story->document(); + const auto valid = video->videoPreloadLocation().valid(); + const auto prefix = video->videoPreloadPrefix(); + const auto key = video->bigFileBaseCacheKey(); + if (!valid || prefix <= 0 || prefix > video->size || !key) { + callDone(); + return; + } + _task = std::make_unique(id(), video, [=](QByteArray data) { + if (!data.isEmpty()) { + Assert(data.size() < Storage::kMaxFileInMemory); + _story->owner().cacheBigFile().putIfEmpty( + key, + Storage::Cache::Database::TaggedValue(std::move(data), 0)); + } + callDone(); + }); +} + +void StoryPreload::callDone() { + if (const auto onstack = _done) { + onstack(); + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_story.h b/Telegram/SourceFiles/data/data_story.h new file mode 100644 index 000000000..dcfe4495c --- /dev/null +++ b/Telegram/SourceFiles/data/data_story.h @@ -0,0 +1,219 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/weak_ptr.h" +#include "data/data_location.h" +#include "data/data_message_reaction_id.h" + +class Image; +class PhotoData; +class DocumentData; + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class Session; +class Thread; +class PhotoMedia; + +enum class StoryPrivacy : uchar { + Public, + CloseFriends, + Contacts, + SelectedContacts, + Other, +}; + +struct StoryIdDates { + StoryId id = 0; + TimeId date = 0; + TimeId expires = 0; + + [[nodiscard]] bool valid() const { + return id != 0; + } + explicit operator bool() const { + return valid(); + } + + friend inline auto operator<=>(StoryIdDates, StoryIdDates) = default; + friend inline bool operator==(StoryIdDates, StoryIdDates) = default; +}; + +struct StoryMedia { + std::variant< + v::null_t, + not_null, + not_null> data; + + friend inline bool operator==(StoryMedia, StoryMedia) = default; +}; + +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( + StoryId id, + not_null peer, + StoryMedia media, + const MTPDstoryItem &data, + TimeId now); + + static constexpr int kRecentViewersMax = 3; + + [[nodiscard]] Session &owner() const; + [[nodiscard]] Main::Session &session() const; + [[nodiscard]] not_null peer() const; + + [[nodiscard]] StoryId id() const; + [[nodiscard]] bool mine() const; + [[nodiscard]] StoryIdDates idDates() const; + [[nodiscard]] FullStoryId fullId() const; + [[nodiscard]] TimeId date() const; + [[nodiscard]] TimeId expires() const; + [[nodiscard]] bool unsupported() const; + [[nodiscard]] bool expired(TimeId now = 0) const; + [[nodiscard]] const StoryMedia &media() const; + [[nodiscard]] PhotoData *photo() const; + [[nodiscard]] DocumentData *document() const; + + [[nodiscard]] bool hasReplyPreview() const; + [[nodiscard]] Image *replyPreview() const; + [[nodiscard]] TextWithEntities inReplyText() const; + + void setPinned(bool pinned); + [[nodiscard]] bool pinned() const; + [[nodiscard]] StoryPrivacy privacy() const; + [[nodiscard]] bool forbidsForward() const; + [[nodiscard]] bool edited() const; + + [[nodiscard]] bool canDownloadIfPremium() const; + [[nodiscard]] bool canDownloadChecked() const; + [[nodiscard]] bool canShare() const; + [[nodiscard]] bool canDelete() const; + [[nodiscard]] bool canReport() const; + + [[nodiscard]] bool hasDirectLink() const; + [[nodiscard]] std::optional errorTextForForward( + not_null to) const; + + 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 StoryViews &viewsList() const; + [[nodiscard]] int views() const; + [[nodiscard]] int reactions() const; + void applyViewsSlice(const QString &offset, const StoryViews &slice); + + [[nodiscard]] const std::vector &locations() const; + + void applyChanges( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now); + [[nodiscard]] TimeId lastUpdateTime() const; + +private: + void applyFields( + StoryMedia media, + const MTPDstoryItem &data, + TimeId now, + bool initial); + + const StoryId _id = 0; + const not_null _peer; + Data::ReactionId _sentReactionId; + StoryMedia _media; + TextWithEntities _caption; + std::vector> _recentViewers; + std::vector _locations; + StoryViews _views; + const TimeId _date = 0; + const TimeId _expires = 0; + TimeId _lastUpdateTime = 0; + bool _pinned : 1 = false; + bool _privacyPublic : 1 = false; + bool _privacyCloseFriends : 1 = false; + bool _privacyContacts : 1 = false; + bool _privacySelectedContacts : 1 = false; + bool _noForwards : 1 = false; + bool _edited : 1 = false; + +}; + +class StoryPreload final : public base::has_weak_ptr { +public: + StoryPreload(not_null story, Fn done); + ~StoryPreload(); + + [[nodiscard]] FullStoryId id() const; + [[nodiscard]] not_null story() const; + +private: + class LoadTask; + + void start(); + void load(); + void callDone(); + + const not_null _story; + Fn _done; + + std::shared_ptr _photo; + std::unique_ptr _task; + rpl::lifetime _lifetime; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index aa3ade241..a296e6cf4 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -63,13 +63,17 @@ struct FileOrigin; } // namespace Data struct MessageGroupId { - PeerId peer = 0; + uint64 peerAndScheduledFlag = 0; uint64 value = 0; MessageGroupId() = default; - static MessageGroupId FromRaw(PeerId peer, uint64 value) { + static MessageGroupId FromRaw( + PeerId peer, + uint64 value, + bool scheduled) { auto result = MessageGroupId(); - result.peer = peer; + result.peerAndScheduledFlag = peer.value + | (scheduled ? (1ULL << 55) : 0); result.value = value; return result; } @@ -131,6 +135,7 @@ using PollId = uint64; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; + constexpr auto CancelledWebPageId = WebPageId(0xFFFFFFFFFFFFFFFFULL); struct PreparedPhotoThumb { @@ -296,6 +301,8 @@ enum class MessageFlag : uint64 { // Fake message with bot cover and information. FakeBotAbout = (1ULL << 36), + + StoryItem = (1ULL << 37), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index f474f701e..09c66a922 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -14,9 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_peer_bot_command.h" #include "data/data_photo.h" +#include "data/data_stories.h" #include "data/data_emoji_statuses.h" #include "data/data_user_names.h" +#include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" +#include "history/history.h" #include "api/api_peer_photo.h" #include "apiwrap.h" #include "ui/text/text_options.h" @@ -111,6 +114,51 @@ void UserData::setCommonChatsCount(int count) { } } +bool UserData::hasPrivateForwardName() const { + return !_privateForwardName.isEmpty(); +} + +QString UserData::privateForwardName() const { + return _privateForwardName; +} + +void UserData::setPrivateForwardName(const QString &name) { + _privateForwardName = name; +} + +bool UserData::hasActiveStories() const { + return flags() & UserDataFlag::HasActiveStories; +} + +bool UserData::hasUnreadStories() const { + return flags() & UserDataFlag::HasUnreadStories; +} + +void UserData::setStoriesState(StoriesState state) { + Expects(state != StoriesState::Unknown); + + const auto was = flags(); + using Flag = UserDataFlag; + switch (state) { + case StoriesState::None: + _flags.remove(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + case StoriesState::HasRead: + _flags.set( + (flags() & ~Flag::HasUnreadStories) | Flag::HasActiveStories); + break; + case StoriesState::HasUnread: + _flags.add(Flag::HasActiveStories | Flag::HasUnreadStories); + break; + } + if (flags() != was) { + if (const auto history = owner().historyLoaded(this)) { + history->updateChatListEntryPostponed(); + } + session().changes().peerUpdated(this, UpdateFlag::StoriesState); + } +} + void UserData::setName(const QString &newFirstName, const QString &newLastName, const QString &newPhoneName, const QString &newUsername) { bool changeName = !newFirstName.isEmpty() || !newLastName.isEmpty(); @@ -320,6 +368,10 @@ bool UserData::hasPersonalPhoto() const { return (flags() & UserDataFlag::PersonalPhoto); } +bool UserData::hasStoriesHidden() const { + return (flags() & UserDataFlag::StoriesHidden); +} + bool UserData::canAddContact() const { return canShareThisContact() && !isContact(); } @@ -443,6 +495,8 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->checkFolder(update.vfolder_id().value_or_empty()); user->setThemeEmoji(qs(update.vtheme_emoticon().value_or_empty())); user->setTranslationDisabled(update.is_translations_disabled()); + user->setPrivateForwardName( + update.vprivate_forward_name().value_or_empty()); if (const auto info = user->botInfo.get()) { const auto group = update.vbot_group_admin_rights() @@ -462,6 +516,15 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { } } + if (const auto paper = update.vwallpaper()) { + user->setWallPaper( + Data::WallPaper::Create(&user->session(), *paper)); + } else { + user->setWallPaper({}); + } + + user->owner().stories().apply(user, update.vstories()); + user->fullUpdated(); } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index e0355d8e9..ad6d0b7ae 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -23,6 +23,7 @@ struct BotInfo { bool readsAllHistory = false; bool cantJoinGroups = false; bool supportsAttachMenu = false; + bool canEditInformation = false; int version = 0; int descriptionVersion = 0; QString description; @@ -61,6 +62,9 @@ enum class UserDataFlag { CanReceiveGifts = (1 << 15), VoiceMessagesForbidden = (1 << 16), PersonalPhoto = (1 << 17), + StoriesHidden = (1 << 18), + HasActiveStories = (1 << 19), + HasUnreadStories = (1 << 20), }; inline constexpr bool is_flag_type(UserDataFlag) { return true; }; using UserDataFlags = base::flags; @@ -118,6 +122,7 @@ public: [[nodiscard]] bool isInaccessible() const; [[nodiscard]] bool applyMinPhoto() const; [[nodiscard]] bool hasPersonalPhoto() const; + [[nodiscard]] bool hasStoriesHidden() const; [[nodiscard]] bool canShareThisContact() const; [[nodiscard]] bool canAddContact() const; @@ -167,6 +172,20 @@ public: int commonChatsCount() const; void setCommonChatsCount(int count); + [[nodiscard]] bool hasPrivateForwardName() const; + [[nodiscard]] QString privateForwardName() const; + void setPrivateForwardName(const QString &name); + + enum class StoriesState { + Unknown, + None, + HasRead, + HasUnread, + }; + [[nodiscard]] bool hasActiveStories() const; + [[nodiscard]] bool hasUnreadStories() const; + void setStoriesState(StoriesState state); + private: auto unavailableReasons() const -> const std::vector & override; @@ -177,6 +196,7 @@ private: std::vector _unavailableReasons; QString _phone; + QString _privateForwardName; ContactStatus _contactStatus = ContactStatus::Unknown; CallsStatus _callsStatus = CallsStatus::Unknown; int _commonChatsCount = 0; diff --git a/Telegram/SourceFiles/data/data_wall_paper.cpp b/Telegram/SourceFiles/data/data_wall_paper.cpp index b2313f169..f8532a158 100644 --- a/Telegram/SourceFiles/data/data_wall_paper.cpp +++ b/Telegram/SourceFiles/data/data_wall_paper.cpp @@ -198,6 +198,16 @@ WallPaperId WallPaper::id() const { return _id; } +bool WallPaper::equals(const WallPaper &paper) const { + return (_flags == paper._flags) + && (_slug == paper._slug) + && (_backgroundColors == paper._backgroundColors) + && (_rotation == paper._rotation) + && (_intensity == paper._intensity) + && (_blurred == paper._blurred) + && (_document == paper._document); +} + const std::vector WallPaper::backgroundColors() const { return _backgroundColors; } @@ -251,34 +261,57 @@ bool WallPaper::hasShareUrl() const { return !_slug.isEmpty(); } -QString WallPaper::shareUrl(not_null session) const { - if (!hasShareUrl()) { - return QString(); - } - const auto base = session->createInternalLinkFull("bg/" + _slug); - auto params = QStringList(); +QStringList WallPaper::collectShareParams() const { + auto result = QStringList(); if (isPattern()) { if (!backgroundColors().empty()) { - params.push_back( + result.push_back( "bg_color=" + StringFromColors(backgroundColors())); } if (_intensity) { - params.push_back("intensity=" + QString::number(_intensity)); + result.push_back("intensity=" + QString::number(_intensity)); } } if (_rotation && backgroundColors().size() == 2) { - params.push_back("rotation=" + QString::number(_rotation)); + result.push_back("rotation=" + QString::number(_rotation)); } auto mode = QStringList(); if (_blurred) { mode.push_back("blur"); } if (!mode.isEmpty()) { - params.push_back("mode=" + mode.join('+')); + result.push_back("mode=" + mode.join('+')); } - return params.isEmpty() - ? base - : base + '?' + params.join('&'); + return result; +} + +bool WallPaper::isNull() const { + return !_id && _slug.isEmpty() && _backgroundColors.empty(); +} + +QString WallPaper::key() const { + if (isNull()) { + return QString(); + } + const auto base = _slug.isEmpty() + ? (_id + ? QString::number(_id) + : StringFromColors(backgroundColors())) + : ("bg/" + _slug); + auto params = collectShareParams(); + if (_document && !isPattern()) { + params += u"&intensity="_q + QString::number(_intensity); + } + return params.isEmpty() ? base : (base + '?' + params.join('&')); +} + +QString WallPaper::shareUrl(not_null session) const { + if (!hasShareUrl()) { + return QString(); + } + const auto base = session->createInternalLinkFull("bg/" + _slug); + const auto params = collectShareParams(); + return params.isEmpty() ? base : (base + '?' + params.join('&')); } void WallPaper::loadDocumentThumbnail() const { @@ -327,6 +360,8 @@ MTPWallPaperSettings WallPaper::mtpSettings() const { }; return MTP_wallPaperSettings( MTP_flags((_blurred ? Flag::f_blur : Flag(0)) + | Flag::f_intensity + | Flag::f_rotation | flagForIndex(0) | flagForIndex(1) | flagForIndex(2) @@ -457,11 +492,11 @@ std::optional WallPaper::Create( if (const auto settings = data.vsettings()) { settings->match([&](const MTPDwallPaperSettings &data) { result._blurred = data.is_blur(); + if (const auto intensity = data.vintensity()) { + result._intensity = intensity->v; + } if (result.isPattern()) { result._backgroundColors = ColorsFromMTP(data); - if (const auto intensity = data.vintensity()) { - result._intensity = intensity->v; - } if (const auto rotation = data.vrotation()) { result._rotation = rotation->v; } diff --git a/Telegram/SourceFiles/data/data_wall_paper.h b/Telegram/SourceFiles/data/data_wall_paper.h index df38478ee..fb98eb9de 100644 --- a/Telegram/SourceFiles/data/data_wall_paper.h +++ b/Telegram/SourceFiles/data/data_wall_paper.h @@ -42,7 +42,11 @@ public: void setLocalImageAsThumbnail(std::shared_ptr image); + [[nodiscard]] bool equals(const WallPaper &paper) const; + [[nodiscard]] WallPaperId id() const; + [[nodiscard]] bool isNull() const; + [[nodiscard]] QString key() const; [[nodiscard]] const std::vector backgroundColors() const; [[nodiscard]] DocumentData *document() const; [[nodiscard]] Image *localThumbnail() const; @@ -103,6 +107,8 @@ public: private: static constexpr auto kDefaultIntensity = 50; + [[nodiscard]] QStringList collectShareParams() const; + WallPaperId _id = WallPaperId(); uint64 _accessHash = 0; UserId _ownerId = 0; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 57b31d0f6..4a7b036e3 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -152,6 +152,8 @@ WebPageType ParseWebPageType( return WebPageType::WallPaper; } else if (type == u"telegram_theme"_q) { return WebPageType::Theme; + } else if (type == u"telegram_story"_q) { + return WebPageType::Story; } else if (type == u"telegram_channel"_q) { return WebPageType::Channel; } else if (type == u"telegram_channel_request"_q) { @@ -214,6 +216,7 @@ bool WebPageData::applyChanges( const QString &newSiteName, const QString &newTitle, const TextWithEntities &newDescription, + FullStoryId newStoryId, PhotoData *newPhoto, DocumentData *newDocument, WebPageCollage &&newCollage, @@ -254,6 +257,7 @@ bool WebPageData::applyChanges( && siteName == resultSiteName && title == resultTitle && description.text == newDescription.text + && storyId == newStoryId && photo == newPhoto && document == newDocument && collage.items == newCollage.items @@ -271,6 +275,7 @@ bool WebPageData::applyChanges( siteName = resultSiteName; title = resultTitle; description = newDescription; + storyId = newStoryId; photo = newPhoto; document = newDocument; collage = std::move(newCollage); diff --git a/Telegram/SourceFiles/data/data_web_page.h b/Telegram/SourceFiles/data/data_web_page.h index 8481a7b3b..83e7bde80 100644 --- a/Telegram/SourceFiles/data/data_web_page.h +++ b/Telegram/SourceFiles/data/data_web_page.h @@ -35,6 +35,7 @@ enum class WebPageType { WallPaper, Theme, + Story, Article, ArticleWithIV, @@ -70,6 +71,7 @@ struct WebPageData { const QString &newSiteName, const QString &newTitle, const TextWithEntities &newDescription, + FullStoryId newStoryId, PhotoData *newPhoto, DocumentData *newDocument, WebPageCollage &&newCollage, @@ -89,6 +91,7 @@ struct WebPageData { QString siteName; QString title; TextWithEntities description; + FullStoryId storyId; int duration = 0; QString author; PhotoData *photo = nullptr; diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp index cee0afe8a..1675d1e92 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.cpp @@ -174,8 +174,13 @@ void NotifySettings::update( not_null thread, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (thread->notify().change(muteForSeconds, silentPosts, sound)) { + std::optional sound, + std::optional storiesMuted) { + if (thread->notify().change( + muteForSeconds, + silentPosts, + sound, + storiesMuted)) { updateLocal(thread); thread->session().api().updateNotifySettingsDelayed(thread); } @@ -189,6 +194,11 @@ void NotifySettings::resetToDefault(not_null thread) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound()); if (thread->notify().change(empty)) { updateLocal(thread); @@ -200,8 +210,13 @@ void NotifySettings::update( not_null peer, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (peer->notify().change(muteForSeconds, silentPosts, sound)) { + std::optional sound, + std::optional storiesMuted) { + if (peer->notify().change( + muteForSeconds, + silentPosts, + sound, + storiesMuted)) { updateLocal(peer); peer->session().api().updateNotifySettingsDelayed(peer); } @@ -215,6 +230,11 @@ void NotifySettings::resetToDefault(not_null peer) { MTPint(), MTPNotificationSound(), MTPNotificationSound(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), + MTPNotificationSound(), + MTPNotificationSound(), MTPNotificationSound()); if (peer->notify().change(empty)) { updateLocal(peer); @@ -262,9 +282,10 @@ void NotifySettings::defaultUpdate( DefaultNotify type, Data::MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { + std::optional sound, + std::optional storiesMuted) { auto &settings = defaultValue(type).settings; - if (settings.change(muteForSeconds, silentPosts, sound)) { + if (settings.change(muteForSeconds, silentPosts, sound, storiesMuted)) { updateLocal(type); _owner->session().api().updateNotifySettingsDelayed(type); } diff --git a/Telegram/SourceFiles/data/notify/data_notify_settings.h b/Telegram/SourceFiles/data/notify/data_notify_settings.h index be41ce8dc..6e51b87ac 100644 --- a/Telegram/SourceFiles/data/notify/data_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_notify_settings.h @@ -57,13 +57,15 @@ public: not_null thread, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); void resetToDefault(not_null thread); void update( not_null peer, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); void resetToDefault(not_null peer); void forumParentMuteUpdated(not_null forum); @@ -84,7 +86,8 @@ public: DefaultNotify type, Data::MuteValue muteForSeconds, std::optional silentPosts = std::nullopt, - std::optional sound = std::nullopt); + std::optional sound = std::nullopt, + std::optional storiesMuted = std::nullopt); [[nodiscard]] bool isMuted(not_null thread) const; [[nodiscard]] NotifySound sound( diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp index 6d5735624..4b756a611 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.cpp @@ -18,6 +18,9 @@ namespace { MTPBool(), MTPBool(), MTPint(), + MTPNotificationSound(), + MTPBool(), + MTPBool(), MTPNotificationSound()); } @@ -73,7 +76,8 @@ public: bool change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound); + std::optional sound, + std::optional storiesMuted); std::optional muteUntil() const; std::optional silentPosts() const; @@ -85,12 +89,14 @@ private: std::optional mute, std::optional sound, std::optional showPreviews, - std::optional silentPosts); + std::optional silentPosts, + std::optional storiesMuted); std::optional _mute; std::optional _sound; std::optional _silent; std::optional _showPreviews; + std::optional _storiesMuted; }; @@ -104,19 +110,24 @@ bool NotifyPeerSettingsValue::change(const MTPDpeerNotifySettings &data) { const auto sound = data.vother_sound(); const auto showPreviews = data.vshow_previews(); const auto silent = data.vsilent(); + const auto storiesMuted = data.vstories_muted(); return change( mute ? std::make_optional(mute->v) : std::nullopt, sound ? std::make_optional(ParseSound(*sound)) : std::nullopt, (showPreviews ? std::make_optional(mtpIsTrue(*showPreviews)) : std::nullopt), - silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt); + silent ? std::make_optional(mtpIsTrue(*silent)) : std::nullopt, + (storiesMuted + ? std::make_optional(mtpIsTrue(*storiesMuted)) + : std::nullopt)); } bool NotifyPeerSettingsValue::change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { + std::optional sound, + std::optional storiesMuted) { const auto newMute = muteForSeconds ? base::make_optional(muteForSeconds.until()) : _mute; @@ -126,28 +137,35 @@ bool NotifyPeerSettingsValue::change( const auto newSound = sound ? base::make_optional(*sound) : _sound; + const auto newStoriesMuted = storiesMuted + ? base::make_optional(*storiesMuted) + : _storiesMuted; return change( newMute, newSound, _showPreviews, - newSilentPosts); + newSilentPosts, + newStoriesMuted); } bool NotifyPeerSettingsValue::change( std::optional mute, std::optional sound, std::optional showPreviews, - std::optional silentPosts) { + std::optional silentPosts, + std::optional storiesMuted) { if (_mute == mute && _sound == sound && _showPreviews == showPreviews - && _silent == silentPosts) { + && _silent == silentPosts + && _storiesMuted == storiesMuted) { return false; } _mute = mute; _sound = sound; _showPreviews = showPreviews; _silent = silentPosts; + _storiesMuted = storiesMuted; return true; } @@ -172,11 +190,15 @@ MTPinputPeerNotifySettings NotifyPeerSettingsValue::serialize() const { MTP_flags(flag(_mute, Flag::f_mute_until) | flag(_sound, Flag::f_sound) | flag(_silent, Flag::f_silent) - | flag(_showPreviews, Flag::f_show_previews)), - MTP_bool(_showPreviews ? *_showPreviews : true), - MTP_bool(_silent ? *_silent : false), - MTP_int(_mute ? *_mute : false), - SerializeSound(_sound)); + | flag(_showPreviews, Flag::f_show_previews) + | flag(_storiesMuted, Flag::f_stories_muted)), + MTP_bool(_showPreviews.value_or(true)), + MTP_bool(_silent.value_or(false)), + MTP_int(_mute.value_or(false)), + SerializeSound(_sound), + MTP_bool(_storiesMuted.value_or(false)), + MTP_bool(false), // stories_hide_sender + SerializeSound(std::nullopt)); // stories_sound } PeerNotifySettings::PeerNotifySettings() = default; @@ -203,16 +225,22 @@ bool PeerNotifySettings::change(const MTPPeerNotifySettings &settings) { bool PeerNotifySettings::change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound) { - if (!muteForSeconds && !silentPosts && !sound) { + std::optional sound, + std::optional storiesMuted) { + if (!muteForSeconds && !silentPosts && !sound && !storiesMuted) { return false; } else if (_value) { - return _value->change(muteForSeconds, silentPosts, sound); + return _value->change( + muteForSeconds, + silentPosts, + sound, + storiesMuted); } using Flag = MTPDpeerNotifySettings::Flag; const auto flags = (muteForSeconds ? Flag::f_mute_until : Flag(0)) | (silentPosts ? Flag::f_silent : Flag(0)) - | (sound ? Flag::f_other_sound : Flag(0)); + | (sound ? Flag::f_other_sound : Flag(0)) + | (storiesMuted ? Flag::f_stories_muted : Flag(0)); return change(MTP_peerNotifySettings( MTP_flags(flags), MTPBool(), @@ -220,7 +248,12 @@ bool PeerNotifySettings::change( MTP_int(muteForSeconds.until()), MTPNotificationSound(), MTPNotificationSound(), - SerializeSound(sound))); + SerializeSound(sound), + storiesMuted ? MTP_bool(*storiesMuted) : MTPBool(), + MTPBool(), // stories_hide_sender + MTPNotificationSound(), + MTPNotificationSound(), + SerializeSound(std::nullopt))); // stories_sound } std::optional PeerNotifySettings::muteUntil() const { diff --git a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h index 3973d7820..76a8ecfd9 100644 --- a/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h +++ b/Telegram/SourceFiles/data/notify/data_peer_notify_settings.h @@ -44,7 +44,8 @@ public: bool change( MuteValue muteForSeconds, std::optional silentPosts, - std::optional sound); + std::optional sound, + std::optional storiesMuted); bool settingsUnknown() const; std::optional muteUntil() const; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 04565a741..b9f34f030 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lottie/lottie_frame_generator.h" #include "ffmpeg/ffmpeg_frame_generator.h" #include "chat_helpers/stickers_lottie.h" +#include "storage/file_download.h" // kMaxFileInMemory #include "ui/widgets/input_fields.h" #include "ui/text/text_custom_emoji.h" #include "ui/text/text_utilities.h" @@ -344,7 +345,12 @@ void CustomEmojiLoader::check() { sizeOverride); }; auto put = [=, key = cacheKey(document)](QByteArray value) { - document->owner().cacheBigFile().put(key, std::move(value)); + const auto size = value.size(); + if (size <= Storage::kMaxFileInMemory) { + document->owner().cacheBigFile().put(key, std::move(value)); + } else { + LOG(("Data Error: Cached emoji size too big: %1.").arg(size)); + } }; const auto type = document->sticker()->type; auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]() diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 4b778d0b1..c18a1c3bf 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_stickers.h" #include "api/api_hash.h" +#include "chat_helpers/compose/compose_show.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" @@ -29,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_app_config.h" #include "mtproto/mtproto_config.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/image/image_location_factory.h" #include "window/window_controller.h" #include "window/window_session_controller.h" @@ -78,24 +78,24 @@ using SetFlag = StickersSetFlag; } void MaybeShowPremiumToast( - Window::SessionController *controller, + std::shared_ptr show, TextWithEntities text, const QString &ref) { - if (!controller) { + if (!show) { return; } - const auto session = &controller->session(); + const auto session = &show->session(); if (session->user()->isPremium()) { return; } - const auto widget = QPointer( - controller->window().widget()->bodyWidget()); const auto filter = [=](const auto ...) { - Settings::ShowPremium(controller, ref); + const auto usage = ChatHelpers::WindowUsage::PremiumPromo; + if (const auto controller = show->resolveWindow(usage)) { + Settings::ShowPremium(controller, ref); + } return false; }; - Ui::ShowMultilineToast({ - .parentOverride = widget, + show->showToast({ .text = std::move(text), .duration = kPremiumToastDuration, .filter = filter, @@ -313,7 +313,7 @@ void Stickers::incrementSticker(not_null document) { } void Stickers::addSavedGif( - Window::SessionController *controller, + std::shared_ptr show, not_null document) { const auto index = _savedGifs.indexOf(document); if (!index) { @@ -328,7 +328,7 @@ void Stickers::addSavedGif( if (_savedGifs.size() > limits.gifsCurrent()) { _savedGifs.pop_back(); MaybeShowPremiumToast( - controller, + show, SavedGifsToast(limits), LimitsPremiumRef("saved_gifs")); } @@ -518,7 +518,7 @@ bool Stickers::isFaved(not_null document) { void Stickers::checkFavedLimit( StickersSet &set, - Window::SessionController *controller) { + std::shared_ptr show) { const auto session = &_owner->session(); const auto limits = Data::PremiumLimits(session); if (set.stickers.size() <= limits.stickersFavedCurrent()) { @@ -538,21 +538,21 @@ void Stickers::checkFavedLimit( ++i; } MaybeShowPremiumToast( - controller, + std::move(show), FaveStickersToast(limits), LimitsPremiumRef("stickers_faved")); } void Stickers::pushFavedToFront( StickersSet &set, - Window::SessionController *controller, + std::shared_ptr show, not_null document, const std::vector> &emojiList) { set.stickers.push_front(document); for (auto emoji : emojiList) { set.emoji[emoji].push_front(document); } - checkFavedLimit(set, controller); + checkFavedLimit(set, std::move(show)); } void Stickers::moveFavedToFront(StickersSet &set, int index) { @@ -575,7 +575,7 @@ void Stickers::moveFavedToFront(StickersSet &set, int index) { } void Stickers::setIsFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document, std::optional>> emojiList) { auto &sets = setsRef(); @@ -600,11 +600,11 @@ void Stickers::setIsFaved( if (index > 0) { moveFavedToFront(*set, index); } else if (emojiList) { - pushFavedToFront(*set, controller, document, *emojiList); + pushFavedToFront(*set, show, document, *emojiList); } else if (auto list = getEmojiListFromSet(document)) { - pushFavedToFront(*set, controller, document, *list); + pushFavedToFront(*set, show, document, *list); } else { - requestSetToPushFaved(controller, document); + requestSetToPushFaved(show, document); return; } session().local().writeFavedStickers(); @@ -613,10 +613,8 @@ void Stickers::setIsFaved( } void Stickers::requestSetToPushFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document) { - controller = nullptr; - const auto weak = base::make_weak(controller); auto addAnyway = [=](std::vector> list) { if (list.empty()) { if (auto sticker = document->sticker()) { @@ -625,7 +623,7 @@ void Stickers::requestSetToPushFaved( } } } - setIsFaved(weak.get(), document, std::move(list)); + setIsFaved(nullptr, document, std::move(list)); }; session().api().request(MTPmessages_GetStickerSet( Data::InputStickerSet(document->sticker()->set), @@ -668,11 +666,11 @@ void Stickers::setIsNotFaved(not_null document) { } void Stickers::setFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document, bool faved) { if (faved) { - setIsFaved(controller, document); + setIsFaved(std::move(show), document); } else { setIsNotFaved(document); } diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.h b/Telegram/SourceFiles/data/stickers/data_stickers.h index da202f337..f469f173c 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers.h @@ -22,6 +22,10 @@ namespace Window { class SessionController; } // namespace Window +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { class Session; @@ -200,7 +204,7 @@ public: void removeFromRecentSet(not_null document); void addSavedGif( - Window::SessionController *controller, + std::shared_ptr show, not_null document); void checkSavedGif(not_null item); @@ -210,7 +214,7 @@ public: void undoInstallLocally(uint64 setId); bool isFaved(not_null document); void setFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document, bool faved); @@ -260,21 +264,21 @@ private: } void checkFavedLimit( StickersSet &set, - Window::SessionController *controller = nullptr); + std::shared_ptr show); void setIsFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document, std::optional>> emojiList = std::nullopt); void setIsNotFaved(not_null document); void pushFavedToFront( StickersSet &set, - Window::SessionController *controller, + std::shared_ptr show, not_null document, const std::vector> &emojiList); void moveFavedToFront(StickersSet &set, int index); void requestSetToPushFaved( - Window::SessionController *controller, + std::shared_ptr show, not_null document); void setPackAndEmoji( StickersSet &set, diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index f1ae4dbaa..622016bb8 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -218,22 +218,28 @@ dialogsMenuToggleUnreadMuted: icon { { "dialogs/dialogs_menu_unread_dot", dialogsMenuIconFg }, }; -dialogsLock: IconButton(dialogsMenuToggle) { - icon: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFg }}; - iconOver: icon {{ "dialogs/dialogs_lock", dialogsMenuIconFgOver }}; +dialogsLock: IconButton { + width: 36px; + height: 38px; + + icon: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFg }}; + iconOver: icon {{ "dialogs/dialogs_lock_off", dialogsMenuIconFgOver }}; + iconPosition: point(-1px, -1px); + + ripple: emptyRippleAnimation; } -dialogsUnlockIcon: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFg }}; -dialogsUnlockIconOver: icon {{ "dialogs/dialogs_unlock", dialogsMenuIconFgOver }}; +dialogsUnlockIcon: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFg }}; +dialogsUnlockIconOver: icon {{ "dialogs/dialogs_lock_on", dialogsMenuIconFgOver }}; dialogsCalendar: IconButton { - width: 29px; - height: 32px; + width: 32px; + height: 35px; icon: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFg }}; iconOver: icon {{ "dialogs/dialogs_calendar", dialogsMenuIconFgOver }}; - iconPosition: point(0px, 5px); + iconPosition: point(1px, 6px); } dialogsSearchFrom: IconButton(dialogsCalendar) { - width: 26px; + width: 29px; icon: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFg }}; iconOver: icon {{ "dialogs/dialogs_search_from", dialogsMenuIconFgOver }}; } @@ -246,27 +252,28 @@ dialogsSearchForNarrowFilters: IconButton(dialogsMenuToggle) { dialogsFilter: InputField(defaultInputField) { textBg: filterInputInactiveBg; textBgActive: filterInputActiveBg; - textMargins: margins(12px, 7px, 30px, 3px); + textMargins: margins(12px, 8px, 30px, 5px); placeholderFg: placeholderFg; placeholderFgActive: placeholderFgActive; placeholderFgError: placeholderFgActive; - placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderMargins: margins(5px, 0px, 2px, 0px); placeholderScale: 0.; placeholderShift: -50px; placeholderFont: normalFont; borderFg: filterInputInactiveBg; - borderFgActive: filterInputBorderFg; + borderFgActive: windowBgRipple; borderFgError: activeLineFgError; - border: 2px; + border: 3px; borderActive: 2px; - borderRadius: roundRadiusSmall; + borderRadius: 18px; + borderDenominator: 2; font: normalFont; - heightMin: 32px; + heightMin: 35px; } dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) { icon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; @@ -276,12 +283,12 @@ dialogsCancelSearchInPeer: IconButton(dialogsMenuToggle) { rippleAreaSize: 34px; } dialogsCancelSearch: CrossButton { - width: 32px; - height: 32px; + width: 35px; + height: 35px; cross: CrossAnimation { - size: 32px; - skip: 10px; + size: 35px; + skip: 12px; stroke: 1.5; minScale: 0.3; } @@ -485,3 +492,86 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) { chooseTopicList: PeerList(defaultPeerList) { item: chooseTopicListItem; } + +DialogsStories { + left: pixels; + height: pixels; + photo: pixels; + photoLeft: pixels; + photoTop: pixels; + shift: pixels; + lineTwice: pixels; + lineReadTwice: pixels; + nameLeft: pixels; + nameRight: pixels; + nameTop: pixels; + nameStyle: TextStyle; +} +DialogsStoriesList { + small: DialogsStories; + full: DialogsStories; + bg: color; + readOpacity: double; + fullClickable: int; +} + +dialogsStories: DialogsStories { + left: 4px; + height: 35px; + photo: 21px; + photoTop: 4px; + photoLeft: 4px; + shift: 16px; + lineTwice: 3px; + lineReadTwice: 0px; + nameLeft: 11px; + nameRight: 10px; + nameTop: 3px; + nameStyle: semiboldTextStyle; +} + +dialogsStoriesFull: DialogsStories { + left: 4px; + height: 77px; + photo: 42px; + photoLeft: 10px; + photoTop: 9px; + lineTwice: 4px; + lineReadTwice: 2px; + nameLeft: 0px; + nameRight: 0px; + nameTop: 56px; + nameStyle: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px); + } +} + +dialogsStoriesList: DialogsStoriesList { + small: dialogsStories; + full: dialogsStoriesFull; + bg: dialogsBg; + readOpacity: 0.6; + fullClickable: 0; +} +dialogsStoriesListInfo: DialogsStoriesList(dialogsStoriesList) { + bg: transparent; + fullClickable: 1; +} +dialogsStoriesListMine: DialogsStoriesList(dialogsStoriesListInfo) { + readOpacity: 1.; +} +dialogsStoriesTooltip: ImportantTooltip(defaultImportantTooltip) { + padding: margins(0px, 0px, 0px, 0px); +} +dialogsStoriesTooltipLabel: defaultImportantTooltipLabel; +dialogsStoriesTooltipMaxWidth: 200px; +dialogsStoriesTooltipHide: IconButton(defaultIconButton) { + width: 34px; + height: 20px; + iconPosition: point(-1px, -1px); + icon: icon {{ "calls/video_tooltip", importantTooltipFg }}; + iconOver: icon {{ "calls/video_tooltip", importantTooltipFg }}; + ripple: emptyRippleAnimation; +} diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index b763ca574..697862166 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -7,9 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_inner_widget.h" -#include "dialogs/dialogs_indexed_list.h" #include "dialogs/ui/dialogs_layout.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "dialogs/ui/dialogs_video_userpic.h" +#include "dialogs/dialogs_indexed_list.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "history/history.h" @@ -18,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" +#include "ui/widgets/scroll_area.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" #include "ui/painter.h" @@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" +#include "data/data_stories.h" #include "data/stickers/data_stickers.h" #include "data/data_send_action.h" #include "base/unixtime.h" @@ -317,6 +321,8 @@ InnerWidget::InnerWidget( switchToFilter(filterId); }, lifetime()); + session().data().stories().incrementPreloadingMainSources(); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -406,8 +412,13 @@ int InnerWidget::skipTopHeight() const { : 0; } +int InnerWidget::collapsedRowsOffset() const { + return 0; +} + int InnerWidget::dialogsOffset() const { - return _collapsedRows.size() * st::dialogsImportantBarHeight + return collapsedRowsOffset() + + (_collapsedRows.size() * st::dialogsImportantBarHeight) - skipTopHeight(); } @@ -569,6 +580,10 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .paused = videoPaused, .narrow = (fullWidth < st::columnMinimalWidthLeft / 2), }; + const auto fillGuard = gsl::finally([&] { + // We translate painter down, but it'll be cropped below rect. + p.fillRect(rect(), context.currentBg); + }); const auto paintRow = [&]( not_null row, bool selected, @@ -596,7 +611,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), context); }; if (_state == WidgetState::Default) { - paintCollapsedRows(p, r); + const auto collapsedSkip = collapsedRowsOffset(); + p.translate(0, collapsedSkip); + paintCollapsedRows(p, r.translated(0, -collapsedSkip)); const auto &list = _shownList->all(); const auto shownBottom = _shownList->height() - skipTopHeight(); @@ -1215,6 +1232,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { } _mouseSelection = true; _lastMousePosition = globalPosition; + _lastRowLocalMouseX = local.x(); const auto w = width(); const auto mouseY = local.y(); @@ -2122,6 +2140,7 @@ FilterId InnerWidget::filterId() const { void InnerWidget::clearSelection() { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (isSelected()) { updateSelectedRow(); _collapsedSelected = -1; @@ -2255,7 +2274,7 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) { if (_filter.isEmpty() && !_searchFromPeer) { clearFilter(); } else { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; _filterResults.clear(); _filterResultsGlobal.clear(); @@ -2339,6 +2358,7 @@ void InnerWidget::appendToFiltered(Key key) { } InnerWidget::~InnerWidget() { + session().data().stories().decrementPreloadingMainSources(); clearSearchResults(); } @@ -2725,7 +2745,7 @@ void InnerWidget::refresh(bool toTop) { resize(width(), h); if (toTop) { stopReorderPinned(); - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); preloadRowsData(); } _controller->setDialogsListDisplayForced( @@ -2798,17 +2818,15 @@ 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) { _mouseSelection = false; _lastMousePosition = std::nullopt; + _lastRowLocalMouseX = -1; if (clearSelection) { if (_state == WidgetState::Default) { _collapsedSelected = -1; @@ -2915,10 +2933,10 @@ void InnerWidget::repaintSearchResult(int index) { void InnerWidget::clearFilter() { if (_state == WidgetState::Filtered || _searchInChat) { if (_searchInChat) { - _state = WidgetState::Filtered; + setState(WidgetState::Filtered); _waitingForSearch = true; } else { - _state = WidgetState::Default; + setState(WidgetState::Default); } _hashtagResults.clear(); _filterResults.clear(); @@ -2930,6 +2948,10 @@ void InnerWidget::clearFilter() { } } +void InnerWidget::setState(WidgetState state) { + _state = state; +} + void InnerWidget::selectSkip(int32 direction) { clearMouseSelection(); if (_state == WidgetState::Default) { @@ -3196,7 +3218,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { filterId = 0; } if (_filterId == filterId) { - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); return; } saveChatsFilterScrollState(_filterId); @@ -3221,6 +3243,10 @@ void InnerWidget::switchToFilter(FilterId filterId) { } } +void InnerWidget::jumpToTop() { + _mustScrollTo.fire({ 0, -1 }); +} + void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { _chatsFilterScrollStates[filterId] = -y(); } @@ -3228,7 +3254,7 @@ void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { void InnerWidget::restoreChatsFilterScrollState(FilterId filterId) { const auto it = _chatsFilterScrollStates.find(filterId); if (it != end(_chatsFilterScrollStates)) { - _mustScrollTo.fire({ it->second, -1 }); + _mustScrollTo.fire({ std::max(it->second, 0), -1 }); } } @@ -3264,29 +3290,30 @@ ChosenRow InnerWidget::computeChosenRow() const { if (_state == WidgetState::Default) { if (_selected) { return { - _selected->key(), - Data::UnreadMessagePosition + .key = _selected->key(), + .message = Data::UnreadMessagePosition, }; } } else if (_state == WidgetState::Filtered) { if (base::in_range(_filteredSelected, 0, _filterResults.size())) { return { - _filterResults[_filteredSelected].key(), - Data::UnreadMessagePosition, - true + .key = _filterResults[_filteredSelected].key(), + .message = Data::UnreadMessagePosition, + .filteredRow = true, }; } else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) { + const auto peer = _peerSearchResults[_peerSearchSelected]->peer; return { - session().data().history(_peerSearchResults[_peerSearchSelected]->peer), - Data::UnreadMessagePosition + .key = session().data().history(peer), + .message = Data::UnreadMessagePosition }; } else if (base::in_range(_searchedSelected, 0, _searchResults.size())) { const auto result = _searchResults[_searchedSelected].get(); const auto topic = result->topic(); const auto item = result->item(); return { - (topic ? (Entry*)topic : (Entry*)item->history()), - item->position() + .key = (topic ? (Entry*)topic : (Entry*)item->history()), + .message = item->position() }; } } @@ -3301,10 +3328,13 @@ bool InnerWidget::chooseRow( } else if (chooseHashtag()) { return true; } - const auto modifyChosenRow = []( + const auto modifyChosenRow = [&]( ChosenRow row, Qt::KeyboardModifiers modifiers) { row.newWindow = (modifiers & Qt::ControlModifier); + row.userpicClick = (_lastRowLocalMouseX >= 0) + && (_lastRowLocalMouseX < _st->nameLeft) + && (width() > _narrowWidth); return row; }; auto chosen = modifyChosenRow(computeChosenRow(), modifiers); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 6b01e940f..608513c85 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -61,8 +61,9 @@ class IndexedList; struct ChosenRow { Key key; Data::MessagePosition message; - bool filteredRow = false; - bool newWindow = false; + bool userpicClick : 1 = false; + bool filteredRow : 1 = false; + bool newWindow : 1 = false; }; enum class SearchRequestType { @@ -219,6 +220,7 @@ private: void dialogRowReplaced(Row *oldRow, Row *newRow); + void setState(WidgetState state); void editOpenedFilter(); void repaintCollapsedFolderRow(not_null folder); void refreshWithCollapsedRows(bool toTop = false); @@ -274,6 +276,7 @@ private: int defaultRowTop(not_null row) const; void setupOnlineStatusCheck(); + void jumpToTop(); void updateRowCornerStatusShown(not_null history); void repaintDialogRowCornerStatus(not_null history); @@ -310,6 +313,7 @@ private: void refreshShownList(); [[nodiscard]] int skipTopHeight() const; + [[nodiscard]] int collapsedRowsOffset() const; [[nodiscard]] int dialogsOffset() const; [[nodiscard]] int shownHeight(int till = -1) const; [[nodiscard]] int fixedOnTopCount() const; @@ -398,6 +402,7 @@ private: FilterId _filterId = 0; bool _mouseSelection = false; std::optional _lastMousePosition; + int _lastRowLocalMouseX = -1; Qt::MouseButton _pressButton = Qt::LeftButton; Data::Folder *_openedFolder = nullptr; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index c5145c368..c937f10f2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" // CountAverageColor. #include "ui/color_contrast.h" +#include "ui/effects/outline_segments.h" #include "ui/effects/ripple_animation.h" #include "ui/image/image_prepare.h" #include "ui/text/format_values.h" @@ -21,7 +22,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_peer_values.h" +#include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" @@ -230,20 +233,11 @@ void BasicRow::paintRipple( void BasicRow::paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const { - PaintUserpic( - p, - peer, - videoUserpic, - _userpic, - context.st->padding.left(), - context.st->padding.top(), - context.width, - context.st->photoSize, - context.paused); + PaintUserpic(p, entry, peer, videoUserpic, _userpic, context); } Row::Row(Key key, int index, int top) : _id(key), _top(top), _index(index) { @@ -330,28 +324,72 @@ void Row::ensureCornerBadgeUserpic() const { void Row::PaintCornerBadgeFrame( not_null data, - not_null peer, + int framePadding, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, const Ui::PaintContext &context) { data->frame.fill(Qt::transparent); Painter q(&data->frame); + q.translate(framePadding, framePadding); + auto hq = std::optional(); + const auto photoSize = context.st->photoSize; + const auto storiesCount = data->storiesCount; + if (storiesCount) { + hq.emplace(q); + const auto line = st::dialogsStoriesFull.lineTwice / 2.; + const auto skip = line * 3 / 2.; + const auto scale = 1. - (2 * skip / photoSize); + const auto center = photoSize / 2.; + q.save(); + q.translate(center, center); + q.scale(scale, scale); + q.translate(-center, -center); + } + q.translate(-context.st->padding.left(), -context.st->padding.top()); PaintUserpic( q, + entry, peer, videoUserpic, view, - 0, - 0, - data->frame.width() / data->frame.devicePixelRatio(), - context.st->photoSize, - context.paused); + context); + q.translate(context.st->padding.left(), context.st->padding.top()); + if (storiesCount) { + q.restore(); + + const auto outline = QRectF(0, 0, photoSize, photoSize); + const auto storiesUnreadCount = data->storiesUnreadCount; + const auto storiesUnreadBrush = [&] { + if (context.active || !storiesUnreadCount) { + return st::dialogsUnreadBgMutedActive->b; + } + auto gradient = Ui::UnreadStoryOutlineGradient(outline); + return QBrush(gradient); + }(); + const auto storiesBrush = context.active + ? st::dialogsUnreadBgMutedActive->b + : st::dialogsUnreadBgMuted->b; + const auto storiesUnread = st::dialogsStoriesFull.lineTwice / 2.; + const auto storiesLine = st::dialogsStoriesFull.lineReadTwice / 2.; + auto segments = std::vector(); + segments.reserve(storiesCount); + const auto storiesReadCount = storiesCount - storiesUnreadCount; + for (auto i = 0; i != storiesReadCount; ++i) { + segments.push_back({ storiesBrush, storiesLine }); + } + for (auto i = 0; i != storiesUnreadCount; ++i) { + segments.push_back({ storiesUnreadBrush, storiesUnread }); + } + Ui::PaintOutlineSegments(q, outline, segments); + } const auto &manager = data->layersManager; - if (const auto p = manager.progressForLayer(kBottomLayer); p) { - const auto size = context.st->photoSize; - if (data->cacheTTL.isNull() && peer->messagesTTL()) { + if (const auto p = manager.progressForLayer(kBottomLayer); p > 0.) { + const auto size = photoSize; + if (data->cacheTTL.isNull() && peer && peer->messagesTTL()) { data->cacheTTL = CornerBadgeTTL(peer, view, size); } q.setOpacity(p); @@ -364,14 +402,17 @@ void Row::PaintCornerBadgeFrame( return; } - PainterHighQualityEnabler hq(q); + if (!hq) { + hq.emplace(q); + } q.setCompositionMode(QPainter::CompositionMode_Source); - const auto size = peer->isUser() + const auto online = peer && peer->isUser(); + const auto size = online ? st::dialogsOnlineBadgeSize : st::dialogsCallBadgeSize; const auto stroke = st::dialogsOnlineBadgeStroke; - const auto skip = peer->isUser() + const auto skip = online ? st::dialogsOnlineBadgeSkip : st::dialogsCallBadgeSkip; const auto shrink = (size / 2) * (1. - topLayerProgress); @@ -383,8 +424,8 @@ void Row::PaintCornerBadgeFrame( ? st::dialogsOnlineBadgeFgActive : st::dialogsOnlineBadgeFg); q.drawEllipse(QRectF( - context.st->photoSize - skip.x() - size, - context.st->photoSize - skip.y() - size, + photoSize - skip.x() - size, + photoSize - skip.y() - size, size, size ).marginsRemoved({ shrink, shrink, shrink, shrink })); @@ -392,46 +433,72 @@ void Row::PaintCornerBadgeFrame( void Row::paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const { - updateCornerBadgeShown(peer); + if (peer) { + updateCornerBadgeShown(peer); + } const auto cornerBadgeShown = !_cornerBadgeUserpic ? _cornerBadgeShown : !_cornerBadgeUserpic->layersManager.isDisplayedNone(); - if (!historyForCornerBadge || !cornerBadgeShown) { - BasicRow::paintUserpic( - p, - peer, - videoUserpic, - historyForCornerBadge, - context); - if (!historyForCornerBadge || !_cornerBadgeShown) { + const auto storiesUser = peer ? peer->asUser() : nullptr; + const auto storiesFolder = peer ? nullptr : _id.folder(); + const auto storiesHas = storiesUser + ? storiesUser->hasActiveStories() + : storiesFolder + ? storiesFolder->storiesCount() + : false; + if (!cornerBadgeShown && !storiesHas) { + BasicRow::paintUserpic(p, entry, peer, videoUserpic, context); + if (!peer || !_cornerBadgeShown) { _cornerBadgeUserpic = nullptr; } return; } ensureCornerBadgeUserpic(); const auto ratio = style::DevicePixelRatio(); - const auto added = std::max({ + const auto framePadding = std::max({ -st::dialogsCallBadgeSkip.x(), -st::dialogsCallBadgeSkip.y(), - 0 }); - const auto frameSide = (context.st->photoSize + added) - * style::DevicePixelRatio(); + st::lineWidth * 2 }); + const auto frameSide = (2 * framePadding + context.st->photoSize) + * ratio; const auto frameSize = QSize(frameSide, frameSide); + const auto storiesSource = (storiesHas && storiesUser) + ? storiesUser->owner().stories().source(storiesUser->id) + : nullptr; + const auto storiesCountReal = storiesSource + ? int(storiesSource->ids.size()) + : storiesFolder + ? storiesFolder->storiesCount() + : storiesHas + ? 1 + : 0; + const auto storiesUnreadCountReal = storiesSource + ? storiesSource->unreadCount() + : storiesFolder + ? storiesFolder->storiesUnreadCount() + : (storiesUser && storiesUser->hasUnreadStories()) + ? 1 + : 0; + const auto limit = Ui::kOutlineSegmentsMax; + const auto storiesCount = std::min(storiesCountReal, limit); + const auto storiesUnreadCount = std::min(storiesUnreadCountReal, limit); if (_cornerBadgeUserpic->frame.size() != frameSize) { _cornerBadgeUserpic->frame = QImage( frameSize, QImage::Format_ARGB32_Premultiplied); _cornerBadgeUserpic->frame.setDevicePixelRatio(ratio); } - auto key = peer->userpicUniqueKey(userpicView()); - key.first += peer->messagesTTL(); + auto key = peer ? peer->userpicUniqueKey(userpicView()) : InMemoryKey(); + key.first += peer ? peer->messagesTTL() : 0; const auto frameIndex = videoUserpic ? videoUserpic->frameIndex() : -1; - const auto paletteVersion = style::PaletteVersion(); + const auto paletteVersionReal = style::PaletteVersion(); + const auto paletteVersion = (paletteVersionReal & ((1 << 17) - 1)); + const auto active = context.active ? 1 : 0; const auto keyChanged = (_cornerBadgeUserpic->key != key) || (_cornerBadgeUserpic->paletteVersion != paletteVersion); if (keyChanged) { @@ -439,29 +506,36 @@ void Row::paintUserpic( } if (keyChanged || !_cornerBadgeUserpic->layersManager.isFinished() - || _cornerBadgeUserpic->active != context.active + || _cornerBadgeUserpic->active != active || _cornerBadgeUserpic->frameIndex != frameIndex + || _cornerBadgeUserpic->storiesCount != storiesCount + || _cornerBadgeUserpic->storiesUnreadCount != storiesUnreadCount || videoUserpic) { _cornerBadgeUserpic->key = key; _cornerBadgeUserpic->paletteVersion = paletteVersion; - _cornerBadgeUserpic->active = context.active; + _cornerBadgeUserpic->active = active; + _cornerBadgeUserpic->storiesCount = storiesCount; + _cornerBadgeUserpic->storiesUnreadCount = storiesUnreadCount; _cornerBadgeUserpic->frameIndex = frameIndex; _cornerBadgeUserpic->layersManager.markFrameShown(); PaintCornerBadgeFrame( _cornerBadgeUserpic.get(), + framePadding, + _id.entry(), peer, videoUserpic, userpicView(), context); } p.drawImage( - context.st->padding.left(), - context.st->padding.top(), + context.st->padding.left() - framePadding, + context.st->padding.top() - framePadding, _cornerBadgeUserpic->frame); - if (historyForCornerBadge->peer->isUser()) { + const auto history = _id.history(); + if (!history || history->peer->isUser()) { return; } - const auto actionPainter = historyForCornerBadge->sendActionPainter(); + const auto actionPainter = history->sendActionPainter(); const auto bg = context.active ? st::dialogsBgActive : st::dialogsBg; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index d79c2efdf..eecbd9c4f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -35,6 +35,7 @@ struct TopicJumpCache; namespace Dialogs { +class Entry; enum class SortMode; [[nodiscard]] QRect CornerBadgeTTLRect(int photoSize); @@ -46,9 +47,9 @@ public: virtual void paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const; void addRipple(QPoint origin, QSize size, Fn updateCallback); @@ -99,9 +100,9 @@ public: Fn updateCallback = nullptr) const; void paintUserpic( Painter &p, - not_null peer, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, - History *historyForCornerBadge, const Ui::PaintContext &context) const final override; [[nodiscard]] bool lookupIsInTopicJump(int x, int y) const; @@ -167,11 +168,13 @@ private: struct CornerBadgeUserpic { InMemoryKey key; CornerLayersManager layersManager; - int paletteVersion = 0; - int frameIndex = -1; - bool active = false; QImage frame; QImage cacheTTL; + int frameIndex = -1; + uint32 paletteVersion : 17 = 0; + uint32 storiesCount : 7 = 0; + uint32 storiesUnreadCount : 7 = 0; + uint32 active : 1 = 0; }; void setCornerBadgeShown( @@ -180,7 +183,9 @@ private: void ensureCornerBadgeUserpic() const; static void PaintCornerBadgeFrame( not_null data, - not_null peer, + int framePadding, + not_null entry, + PeerData *peer, Ui::VideoUserpic *videoUserpic, Ui::PeerUserpicView &view, const Ui::PaintContext &context); @@ -189,9 +194,9 @@ private: mutable std::unique_ptr _cornerBadgeUserpic; int _top = 0; int _height = 0; - int _index : 30 = 0; - int _cornerBadgeShown : 1 = 0; - int _topicJumpRipple : 1 = 0; + uint32 _index : 30 = 0; + uint32 _cornerBadgeShown : 1 = 0; + uint32 _topicJumpRipple : 1 = 0; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 91e5ac701..7997b183f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/dialogs_widget.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_key.h" @@ -19,11 +21,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_group_call_bar.h" #include "boxes/peers/edit_peer_requests_box.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/elastic_scroll.h" #include "ui/widgets/input_fields.h" #include "ui/wrap/fade_wrap.h" #include "ui/effects/radial_animation.h" #include "ui/chat/requests_bar.h" #include "ui/chat/group_call_bar.h" +#include "ui/chat/more_chats_bar.h" #include "ui/controls/download_bar.h" #include "ui/controls/jump_down_button.h" #include "ui/painter.h" @@ -34,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "api/api_chat_filters.h" #include "apiwrap.h" #include "base/event_filter.h" #include "core/application.h" @@ -61,20 +66,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" +#include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" #include "info/info_memento.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_window.h" #include "base/qt/qt_common_adapters.h" #include +#include namespace Dialogs { namespace { constexpr auto kSearchPerPage = 50; +constexpr auto kStoriesExpandDuration = crl::time(200); } // namespace @@ -112,7 +121,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) { @@ -120,7 +129,7 @@ Widget::BottomButton::BottomButton( } void Widget::BottomButton::setText(const QString &text) { - _text = text.toUpper(); + _text = text; update(); } @@ -203,14 +212,26 @@ Widget::Widget( _searchControls, object_ptr(this, st::dialogsCalendar)) , _cancelSearch(_searchControls, st::dialogsCancelSearch) -, _lockUnlock(_searchControls, st::dialogsLock) +, _lockUnlock( + _searchControls, + object_ptr(this, st::dialogsLock)) , _scroll(this) , _scrollToTop(_scroll, st::dialogsToUp) +, _stories((_layout != Layout::Child) + ? std::make_unique( + this, + st::dialogsStoriesList, + _storiesContents.events() | rpl::flatten_latest()) + : nullptr) , _searchTimer([=] { searchMessages(); }) , _singleMessageSearch(&controller->session()) { const auto makeChildListShown = [](PeerId peerId, float64 shown) { return InnerWidget::ChildListShown{ peerId, shown }; }; + using OverscrollType = Ui::ElasticScroll::OverscrollType; + _scroll->setOverscrollTypes( + _stories ? OverscrollType::Virtual : OverscrollType::Real, + OverscrollType::Real); _inner = _scroll->setOwnedWidget(object_ptr( this, controller, @@ -218,6 +239,8 @@ Widget::Widget( _childListPeerId.value(), _childListShown.value(), makeChildListShown))); + _scrollToTop->raise(); + _lockUnlock->toggle(false, anim::type::instant); _inner->updated( ) | rpl::start_with_next([=] { @@ -350,14 +373,20 @@ Widget::Widget( ) | rpl::start_with_next([=] { updateLockUnlockVisibility(); }, lifetime()); - _lockUnlock->setClickedCallback([this] { - _lockUnlock->setIconOverride(&st::dialogsUnlockIcon, &st::dialogsUnlockIconOver); + const auto lockUnlock = _lockUnlock->entity(); + lockUnlock->setClickedCallback([=] { + lockUnlock->setIconOverride( + &st::dialogsUnlockIcon, + &st::dialogsUnlockIconOver); Core::App().maybeLockByPasscode(); - _lockUnlock->setIconOverride(nullptr); + lockUnlock->setIconOverride(nullptr); }); setupMainMenuToggle(); setupShortcuts(); + if (_stories) { + setupStories(); + } _searchForNarrowFilters->setClickedCallback([=] { _filter->setFocusFast(); @@ -406,6 +435,18 @@ Widget::Widget( setupSupportMode(); setupScrollUpButton(); + const auto overscrollBg = [=] { + return anim::color( + st::dialogsBg, + st::dialogsBgOver, + _childListShown.current()); + }; + _scroll->setOverscrollBg(overscrollBg()); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _scroll->setOverscrollBg(overscrollBg()); + }, lifetime()); + if (_layout != Layout::Child) { setupConnectingWidget(); @@ -429,6 +470,7 @@ Widget::Widget( _childListShown.changes( ) | rpl::start_with_next([=] { + _scroll->setOverscrollBg(overscrollBg()); updateControlsGeometry(); }, lifetime()); @@ -440,11 +482,14 @@ Widget::Widget( _searchForNarrowFilters->setRippleColorOverride(color); }, lifetime()); + setupMoreChatsBar(); setupDownloadBar(); } } void Widget::chosenRow(const ChosenRow &row) { + storiesToggleExplicitExpand(false); + const auto history = row.key.history(); const auto topicJump = history ? history->peer->forumTopicFor(row.message.fullId.msg) @@ -481,6 +526,16 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history) { const auto peer = history->peer; + if (const auto user = peer->asUser()) { + if (row.message.fullId.msg == ShowAtUnreadMsgId) { + if (row.userpicClick + && user->hasActiveStories() + && !user->isSelf()) { + controller()->openPeerStories(user->id); + return; + } + } + } const auto showAtMsgId = controller()->uniqueChatsInSearchResults() ? ShowAtUnreadMsgId : row.message.fullId.msg; @@ -494,6 +549,14 @@ void Widget::chosenRow(const ChosenRow &row) { hideChildList(); } } else if (const auto folder = row.key.folder()) { + if (row.userpicClick) { + const auto list = Data::StorySourcesList::Hidden; + const auto &sources = session().data().stories().sources(list); + if (!sources.empty()) { + controller()->openPeerStories(sources.front().id, list); + return; + } + } controller()->openFolder(folder); hideChildList(); } @@ -524,22 +587,65 @@ void Widget::setGeometryWithTopMoved( _topDelta = 0; } +void Widget::scrollToDefaultChecked(bool verytop) { + if (_scrollToAnimation.animating()) { + return; + } + scrollToDefault(verytop); +} + void Widget::setupScrollUpButton() { - _scrollToTop->setClickedCallback([=] { - if (_scrollToAnimation.animating()) { + _scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); }); + trackScroll(_scrollToTop); + trackScroll(this); + updateScrollUpVisibility(); +} + +void Widget::setupMoreChatsBar() { + if (_layout == Layout::Child) { + return; + } + controller()->activeChatsFilter( + ) | rpl::start_with_next([=](FilterId id) { + storiesToggleExplicitExpand(false); + + if (!id) { + _moreChatsBar = nullptr; + updateControlsGeometry(); return; } - scrollToTop(); - }); - base::install_event_filter(_scrollToTop, [=](not_null event) { - if (event->type() != QEvent::Wheel) { - return base::EventFilterResult::Continue; + const auto filters = &session().data().chatsFilters(); + _moreChatsBar = std::make_unique( + this, + filters->moreChatsContent(id)); + + trackScroll(_moreChatsBar->wrap()); + + _moreChatsBar->barClicks( + ) | rpl::start_with_next([=] { + if (const auto missing = filters->moreChats(id) + ; !missing.empty()) { + Api::ProcessFilterUpdate(controller(), id, missing); + } + }, _moreChatsBar->lifetime()); + + _moreChatsBar->closeClicks( + ) | rpl::start_with_next([=] { + Api::ProcessFilterUpdate(controller(), id, {}); + }, _moreChatsBar->lifetime()); + + if (_showAnimation) { + _moreChatsBar->hide(); + } else { + _moreChatsBar->show(); + _moreChatsBar->finishAnimating(); } - return _scroll->viewportEvent(event) - ? base::EventFilterResult::Cancel - : base::EventFilterResult::Continue; - }); - updateScrollUpVisibility(); + + _moreChatsBar->heightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _moreChatsBar->lifetime()); + }, lifetime()); } void Widget::setupDownloadBar() { @@ -686,6 +792,156 @@ void Widget::setupMainMenuToggle() { }, _mainMenu.toggle->lifetime()); } +void Widget::setupStories() { + _stories->verticalScrollEvents( + ) | rpl::start_with_next([=](not_null e) { + _scroll->viewportEvent(e); + }, _stories->lifetime()); + + if (!Core::App().settings().storiesClickTooltipHidden()) { + // Don't create tooltip + // until storiesClickTooltipHidden can be returned to false. + const auto hideTooltip = [=] { + Core::App().settings().setStoriesClickTooltipHidden(true); + Core::App().saveSettingsDelayed(); + }; + _stories->setShowTooltip( + parentWidget(), + rpl::combine( + Core::App().settings().storiesClickTooltipHiddenValue(), + shownValue(), + !rpl::mappers::_1 && rpl::mappers::_2), + hideTooltip); + } + + _storiesContents.fire(Stories::ContentForSession( + &controller()->session(), + Data::StorySourcesList::NotHidden)); + + const auto currentSource = [=] { + using List = Data::StorySourcesList; + return _openedFolder ? List::Hidden : List::NotHidden; + }; + + rpl::combine( + _scroll->positionValue(), + _scroll->movementValue(), + _storiesExplicitExpandValue.value() + ) | rpl::start_with_next([=]( + Ui::ElasticScrollPosition position, + Ui::ElasticScrollMovement movement, + int explicitlyExpanded) { + if (_stories->isHidden()) { + return; + } + const auto overscrollTop = std::max(-position.overscroll, 0); + if (overscrollTop > 0 && _storiesExplicitExpand) { + _scroll->setOverscrollDefaults( + -st::dialogsStoriesFull.height, + 0, + true); + } + if (explicitlyExpanded > 0 && explicitlyExpanded < overscrollTop) { + _storiesExplicitExpandAnimation.stop(); + _storiesExplicitExpand = false; + _storiesExplicitExpandValue = 0; + return; + } + const auto above = std::max(explicitlyExpanded, overscrollTop); + if (_aboveScrollAdded != above) { + _aboveScrollAdded = above; + if (_updateScrollGeometryCached) { + _updateScrollGeometryCached(); + } + } + using Phase = Ui::ElasticScrollMovement; + _stories->setExpandedHeight( + _aboveScrollAdded, + ((movement == Phase::Momentum || movement == Phase::Returning) + && (explicitlyExpanded < above))); + if (position.overscroll > 0 + || (position.value + > (_storiesExplicitExpandScrollTop + + st::dialogsRowHeight))) { + storiesToggleExplicitExpand(false); + } + updateLockUnlockPosition(); + }, lifetime()); + + _stories->collapsedGeometryChanged( + ) | rpl::start_with_next([=] { + updateLockUnlockPosition(); + }, lifetime()); + + _stories->clicks( + ) | rpl::start_with_next([=](uint64 id) { + controller()->openPeerStories(PeerId(int64(id)), currentSource()); + }, lifetime()); + + _stories->showMenuRequests( + ) | rpl::start_with_next([=](const Stories::ShowMenuRequest &request) { + FillSourceMenu(controller(), request); + }, lifetime()); + + _stories->loadMoreRequests( + ) | rpl::start_with_next([=] { + session().data().stories().loadMore(currentSource()); + }, lifetime()); + + _stories->toggleExpandedRequests( + ) | rpl::start_with_next([=](bool expanded) { + const auto position = _scroll->position(); + if (!expanded) { + _scroll->setOverscrollDefaults(0, 0); + } else if (position.value > 0 || position.overscroll >= 0) { + storiesToggleExplicitExpand(true); + _scroll->setOverscrollDefaults(0, 0); + } else { + _scroll->setOverscrollDefaults( + -st::dialogsStoriesFull.height, + 0); + } + }, lifetime()); + + _stories->emptyValue() | rpl::skip(1) | rpl::start_with_next([=] { + updateStoriesVisibility(); + }, lifetime()); + + _stories->widthValue() | rpl::start_with_next([=] { + updateLockUnlockPosition(); + }, lifetime()); +} + +void Widget::storiesToggleExplicitExpand(bool expand) { + if (_storiesExplicitExpand == expand) { + return; + } + _storiesExplicitExpand = expand; + if (!expand) { + _scroll->setOverscrollDefaults(0, 0, true); + } + const auto height = st::dialogsStoriesFull.height; + const auto duration = kStoriesExpandDuration; + _storiesExplicitExpandScrollTop = _scroll->position().value; + _storiesExplicitExpandAnimation.start([=](float64 value) { + _storiesExplicitExpandValue = int(base::SafeRound(value)); + }, expand ? 0 : height, expand ? height : 0, duration, anim::sineInOut); +} + +void Widget::trackScroll(not_null widget) { + widget->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd + || type == QEvent::TouchCancel + || type == QEvent::Wheel) { + _scroll->viewportEvent(e); + } + }, widget->lifetime()); +} + void Widget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { @@ -728,6 +984,7 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) { void Widget::updateControlsVisibility(bool fast) { updateLoadMoreChatsVisibility(); _scroll->show(); + updateStoriesVisibility(); if ((_openedFolder || _openedForum) && _filter->hasFocus()) { setInnerFocus(); } @@ -735,6 +992,9 @@ void Widget::updateControlsVisibility(bool fast) { _updateTelegram->show(); } _searchControls->setVisible(!_openedFolder && !_openedForum); + if (_moreChatsBar) { + _moreChatsBar->show(); + } if (_openedFolder || _openedForum) { _subsectionTopBar->show(); if (_forumTopShadow) { @@ -770,6 +1030,23 @@ void Widget::updateControlsVisibility(bool fast) { if (_childList && _filter->hasFocus()) { setInnerFocus(); } + updateLockUnlockPosition(); +} + +void Widget::updateLockUnlockPosition() { + if (_lockUnlock->isHidden()) { + return; + } + const auto stories = (_stories && !_stories->isHidden()) + ? _stories->collapsedGeometryCurrent() + : Stories::List::CollapsedGeometry(); + const auto simple = _filter->x() + _filter->width(); + const auto right = stories.geometry.isEmpty() + ? simple + : anim::interpolate(stories.geometry.x(), simple, stories.expanded); + _lockUnlock->move( + right - _lockUnlock->width(), + st::dialogsFilterPadding.y()); } void Widget::changeOpenedSubsection( @@ -789,6 +1066,7 @@ void Widget::changeOpenedSubsection( } oldContentCache = grabForFolderSlideAnimation(); } + //_scroll->verticalScrollBar()->setMinimum(0); _showAnimation = nullptr; destroyChildListCanvas(); change(); @@ -821,13 +1099,55 @@ void Widget::changeOpenedFolder(Data::Folder *folder, anim::type animated) { return; } changeOpenedSubsection([&] { + cancelSearch(); closeChildList(anim::type::instant); controller()->closeForum(); _openedFolder = folder; _inner->changeOpenedFolder(folder); + if (_stories) { + storiesExplicitCollapse(); + } }, (folder != nullptr), animated); } +void Widget::storiesExplicitCollapse() { + if (_storiesExplicitExpand) { + storiesToggleExplicitExpand(false); + } else if (_stories) { + using Type = Ui::ElasticScroll::OverscrollType; + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::None, Type::Real); + _scroll->setOverscrollTypes( + _stories->isHidden() ? Type::Real : Type::Virtual, + Type::Real); + } + _storiesExplicitExpandAnimation.stop(); + _storiesExplicitExpandValue = 0; + + using List = Data::StorySourcesList; + collectStoriesUserpicsViews(_openedFolder + ? List::NotHidden + : List::Hidden); + _storiesContents.fire(Stories::ContentForSession( + &session(), + _openedFolder ? List::Hidden : List::NotHidden)); +} + +void Widget::collectStoriesUserpicsViews(Data::StorySourcesList list) { + auto &map = (list == Data::StorySourcesList::Hidden) + ? _storiesUserpicsViewsHidden + : _storiesUserpicsViewsShown; + map.clear(); + auto &owner = session().data(); + for (const auto &source : owner.stories().sources(list)) { + if (const auto peer = owner.peerLoaded(source.id)) { + if (auto view = peer->activeUserpicView(); view.cloud) { + map.emplace(source.id, std::move(view)); + } + } + } +} + void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { if (_openedForum == forum) { return; @@ -838,6 +1158,8 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { _openedForum = forum; _api.request(base::take(_topicSearchRequest)).cancel(); _inner->changeOpenedForum(forum); + storiesToggleExplicitExpand(false); + updateStoriesVisibility(); }, (forum != nullptr), animated); } @@ -851,6 +1173,9 @@ void Widget::refreshTopBars() { if (_openedFolder || _openedForum) { if (!_subsectionTopBar) { _subsectionTopBar.create(this, controller()); + if (_stories) { + _stories->raise(); + } _subsectionTopBar->searchCancelled( ) | rpl::start_with_next([=] { escape(); @@ -1061,10 +1386,25 @@ void Widget::jumpToTop(bool belowPinned) { } } -void Widget::scrollToTop() { +void Widget::raiseWithTooltip() { + raise(); + if (_stories) { + Ui::PostponeCall(this, [=] { + _stories->raiseTooltip(); + }); + } +} + +void Widget::scrollToDefault(bool verytop) { + if (verytop) { + //_scroll->verticalScrollBar()->setMinimum(0); + } _scrollToAnimation.stop(); auto scrollTop = _scroll->scrollTop(); const auto scrollTo = 0; + if (scrollTop == scrollTo) { + return; + } const auto maxAnimatedDelta = _scroll->height(); if (scrollTo + maxAnimatedDelta < scrollTop) { scrollTop = scrollTo + maxAnimatedDelta; @@ -1074,9 +1414,19 @@ void Widget::scrollToTop() { startScrollUpButtonAnimation(false); const auto scroll = [=] { - _scroll->scrollToY(qRound(_scrollToAnimation.value(scrollTo))); + const auto animated = qRound(_scrollToAnimation.value(scrollTo)); + const auto animatedDelta = animated - scrollTo; + const auto realDelta = _scroll->scrollTop() - scrollTo; + if (base::OppositeSigns(realDelta, animatedDelta)) { + // We scrolled manually to the other side of target 'scrollTo'. + _scrollToAnimation.stop(); + } else if (std::abs(realDelta) > std::abs(animatedDelta)) { + // We scroll by animation only if it gets us closer to target. + _scroll->scrollToY(animated); + } }; + _scrollAnimationTo = scrollTo; _scrollToAnimation.start( scroll, scrollTop, @@ -1096,6 +1446,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(), @@ -1107,8 +1459,12 @@ 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(); } void Widget::stopWidthAnimation() { @@ -1116,9 +1472,45 @@ void Widget::stopWidthAnimation() { if (!_showAnimation) { _scroll->show(); } + updateStoriesVisibility(); update(); } +void Widget::updateStoriesVisibility() { + updateLockUnlockVisibility(); + if (!_stories) { + return; + } + const auto hidden = (_showAnimation != nullptr) + || _openedForum + || !_widthAnimationCache.isNull() + || _childList + || !_filter->getLastText().isEmpty() + || _searchInChat + || _stories->empty(); + if (_stories->isHidden() != hidden) { + _stories->setVisible(!hidden); + using Type = Ui::ElasticScroll::OverscrollType; + if (hidden) { + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::Real, Type::Real); + if (_scroll->position().overscroll < 0) { + _scroll->scrollToY(0); + } + _scroll->update(); + } else { + _scroll->setOverscrollDefaults(0, 0); + _scroll->setOverscrollTypes(Type::Virtual, Type::Real); + _storiesExplicitExpandValue.force_assign( + _storiesExplicitExpandValue.current()); + } + if (_aboveScrollAdded > 0 && _updateScrollGeometryCached) { + _updateScrollGeometryCached(); + } + updateLockUnlockPosition(); + } +} + void Widget::showFast() { if (isHidden()) { _inner->clearSelection(); @@ -1161,10 +1553,16 @@ void Widget::startSlideAnimation( QPixmap newContentCache, Window::SlideDirection direction) { _scroll->hide(); + if (_stories) { + _stories->hide(); + } _searchControls->hide(); if (_subsectionTopBar) { _subsectionTopBar->hide(); } + if (_moreChatsBar) { + _moreChatsBar->hide(); + } if (_forumTopShadow) { _forumTopShadow->hide(); } @@ -1938,7 +2336,8 @@ void Widget::dropEvent(QDropEvent *e) { if (_scroll->geometry().contains(e->pos())) { const auto point = mapToGlobal(e->pos()); if (const auto thread = _inner->updateFromParentDrag(point)) { - e->acceptProposedAction(); + e->setDropAction(Qt::CopyAction); + e->accept(); controller()->content()->filesOrForwardDrop( thread, e->mimeData()); @@ -1965,6 +2364,8 @@ void Widget::applyFilterUpdate(bool force) { return; } + updateLockUnlockVisibility(anim::type::normal); + updateStoriesVisibility(); const auto filterText = currentSearchQuery(); _inner->applyFilterUpdate(filterText, force); if (filterText.isEmpty() && !_searchFromAuthor) { @@ -1973,6 +2374,7 @@ void Widget::applyFilterUpdate(bool force) { _cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal); updateLoadMoreChatsVisibility(); updateJumpToDateVisibility(); + updateLockUnlockPosition(); if (filterText.isEmpty()) { _peerSearchCache.clear(); @@ -2118,6 +2520,7 @@ void Widget::closeChildList(anim::type animated) { } else { _childListShadow = nullptr; } + updateStoriesVisibility(); } void Widget::searchInChat(Key chat) { @@ -2171,11 +2574,13 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) { _searchInChat = chat; controller()->searchInChat = _searchInChat; updateJumpToDateVisibility(); + updateStoriesVisibility(); } if (searchFromUpdated) { updateSearchFromVisibility(); clearSearchCache(); } + updateLockUnlockPosition(); if (_searchInChat && _layout == Layout::Main) { controller()->closeFolder(); } @@ -2233,7 +2638,7 @@ void Widget::showSearchFrom() { }), crl::guard(this, [=] { _filter->setFocus(); })); if (box) { - Window::Show(controller()).showBox(std::move(box)); + controller()->show(std::move(box)); } } } @@ -2294,13 +2699,25 @@ void Widget::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } -void Widget::updateLockUnlockVisibility() { +void Widget::updateLockUnlockVisibility(anim::type animated) { if (_showAnimation) { return; } - const auto hidden = !session().domain().local().hasLocalPasscode(); - if (_lockUnlock->isHidden() != hidden) { - _lockUnlock->setVisible(!hidden); + const auto hidden = !session().domain().local().hasLocalPasscode() + || (_showAnimation != nullptr) + || _openedForum + || !_widthAnimationCache.isNull() + || _childList + || !_filter->getLastText().isEmpty() + || _searchInChat; + if (_lockUnlock->toggled() == hidden) { + const auto stories = _stories && !_stories->empty(); + _lockUnlock->toggle( + !hidden, + stories ? anim::type::instant : animated); + if (!hidden) { + updateLockUnlockPosition(); + } updateControlsGeometry(); } } @@ -2369,11 +2786,9 @@ void Widget::updateControlsGeometry() { ? st::dialogsFilterSkip : (st::dialogsFilterPadding.x() + _mainMenu.toggle->width())) + st::dialogsFilterPadding.x(); - auto filterRight = (session().domain().local().hasLocalPasscode() - ? (st::dialogsFilterPadding.x() + _lockUnlock->width()) - : st::dialogsFilterSkip) + st::dialogsFilterPadding.x(); - auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight; - auto filterAreaHeight = st::topBarHeight; + const auto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x(); + const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight; + const auto filterAreaHeight = st::topBarHeight; _searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight); if (_subsectionTopBar) { _subsectionTopBar->setGeometryWithNarrowRatio( @@ -2385,6 +2800,7 @@ void Widget::updateControlsGeometry() { auto filterTop = (filterAreaHeight - _filter->height()) / 2; filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio); _filter->setGeometryToLeft(filterLeft, filterTop, filterWidth, _filter->height()); + auto mainMenuLeft = anim::interpolate( st::dialogsFilterPadding.x(), (_narrowWidth - _mainMenu.toggle->width()) / 2, @@ -2404,46 +2820,39 @@ void Widget::updateControlsGeometry() { _searchForNarrowFilters->moveToLeft(searchLeft, st::dialogsFilterPadding.y()); auto right = filterLeft + filterWidth; - _lockUnlock->moveToLeft(right + st::dialogsFilterPadding.x(), st::dialogsFilterPadding.y()); _cancelSearch->moveToLeft(right - _cancelSearch->width(), _filter->y()); right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _filter->y()); right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _filter->y()); const auto barw = width(); + const auto expandedStoriesTop = filterAreaTop + filterAreaHeight; + const auto storiesHeight = 2 * st::dialogsStories.photoTop + + st::dialogsStories.photo; + const auto added = (st::dialogsFilter.heightMin - storiesHeight) / 2; + if (_stories) { + _stories->setLayoutConstraints( + { filterLeft + filterWidth, filterTop + added }, + style::al_right, + { 0, expandedStoriesTop, barw, st::dialogsStoriesFull.height }); + } if (_forumTopShadow) { _forumTopShadow->setGeometry( 0, - filterAreaTop + filterAreaHeight, + expandedStoriesTop, barw, st::lineWidth); } - const auto forumGroupCallTop = filterAreaTop + filterAreaHeight; - if (_forumGroupCallBar) { - _forumGroupCallBar->move(0, forumGroupCallTop); - _forumGroupCallBar->resizeToWidth(barw); - } - const auto forumRequestsTop = forumGroupCallTop - + (_forumGroupCallBar ? _forumGroupCallBar->height() : 0); - if (_forumRequestsBar) { - _forumRequestsBar->move(0, forumRequestsTop); - _forumRequestsBar->resizeToWidth(barw); - } - const auto forumReportTop = forumRequestsTop - + (_forumRequestsBar ? _forumRequestsBar->height() : 0); - if (_forumReportBar) { - _forumReportBar->bar().move(0, forumReportTop); - } - auto scrollTop = forumReportTop - + (_forumReportBar ? _forumReportBar->bar().height() : 0); - auto newScrollTop = _scroll->scrollTop() + _topDelta; - auto scrollHeight = height() - scrollTop; + + updateLockUnlockPosition(); + + auto bottomSkip = 0; const auto putBottomButton = [&](auto &button) { if (button && !button->isHidden()) { const auto buttonHeight = button->height(); - scrollHeight -= buttonHeight; + bottomSkip += buttonHeight; button->setGeometry( 0, - scrollTop + scrollHeight, + height() - bottomSkip, barw, buttonHeight); } @@ -2451,21 +2860,61 @@ void Widget::updateControlsGeometry() { putBottomButton(_updateTelegram); putBottomButton(_downloadBar); putBottomButton(_loadMoreChats); - const auto bottomSkip = (height() - scrollTop) - scrollHeight; if (_connecting) { _connecting->setBottomSkip(bottomSkip); } controller()->setConnectingBottomSkip(bottomSkip); - const auto scrollw = _childList ? _narrowWidth : barw; - const auto wasScrollHeight = _scroll->height(); - _scroll->setGeometry(0, scrollTop, scrollw, scrollHeight); - _inner->resize(scrollw, _inner->height()); - _inner->setNarrowRatio(narrowRatio); - if (scrollHeight != wasScrollHeight) { - controller()->floatPlayerAreaUpdated(); + const auto wasScrollTop = _scroll->scrollTop(); + const auto newScrollTop = (_topDelta < 0 && wasScrollTop <= 0) + ? wasScrollTop + : (wasScrollTop + _topDelta); + + const auto scrollWidth = _childList ? _narrowWidth : barw; + if (_moreChatsBar) { + _moreChatsBar->resizeToWidth(barw); } - if (_topDelta) { + if (_forumGroupCallBar) { + _forumGroupCallBar->resizeToWidth(barw); + } + if (_forumRequestsBar) { + _forumRequestsBar->resizeToWidth(barw); + } + _updateScrollGeometryCached = [=] { + const auto moreChatsBarTop = expandedStoriesTop + + ((!_stories || _stories->isHidden()) ? 0 : _aboveScrollAdded); + if (_moreChatsBar) { + _moreChatsBar->move(0, moreChatsBarTop); + } + const auto forumGroupCallTop = moreChatsBarTop + + (_moreChatsBar ? _moreChatsBar->height() : 0); + if (_forumGroupCallBar) { + _forumGroupCallBar->move(0, forumGroupCallTop); + } + const auto forumRequestsTop = forumGroupCallTop + + (_forumGroupCallBar ? _forumGroupCallBar->height() : 0); + if (_forumRequestsBar) { + _forumRequestsBar->move(0, forumRequestsTop); + } + const auto forumReportTop = forumRequestsTop + + (_forumRequestsBar ? _forumRequestsBar->height() : 0); + if (_forumReportBar) { + _forumReportBar->bar().move(0, forumReportTop); + } + const auto scrollTop = forumReportTop + + (_forumReportBar ? _forumReportBar->bar().height() : 0); + const auto scrollHeight = height() - scrollTop; + const auto wasScrollHeight = _scroll->height(); + _scroll->setGeometry(0, scrollTop, scrollWidth, scrollHeight); + if (scrollHeight != wasScrollHeight) { + controller()->floatPlayerAreaUpdated(); + } + }; + _updateScrollGeometryCached(); + + _inner->resize(scrollWidth, _inner->height()); + _inner->setNarrowRatio(narrowRatio); + if (newScrollTop != wasScrollTop) { _scroll->scrollToY(newScrollTop); } else { listScrollUpdated(); @@ -2475,8 +2924,8 @@ void Widget::updateControlsGeometry() { } if (_childList) { - const auto childw = std::max(_narrowWidth, width() - scrollw); - const auto childh = scrollTop + scrollHeight; + const auto childw = std::max(_narrowWidth, width() - scrollWidth); + const auto childh = _scroll->y() + _scroll->height(); const auto childx = width() - childw; _childList->setGeometryWithTopMoved( { childx, 0, childw, childh }, @@ -2542,7 +2991,7 @@ void Widget::paintEvent(QPaintEvent *e) { p.fillRect(above.intersected(r), bg); } - auto belowTop = _scroll->y() + qMin(_scroll->height(), _inner->height()); + auto belowTop = _scroll->y() + _scroll->height(); if (!_widthAnimationCache.isNull()) { p.drawPixmapLeft(0, _scroll->y(), width(), _widthAnimationCache); belowTop = _scroll->y() + (_widthAnimationCache.height() / cIntRetinaFactor()); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index b33dce8fd..e1d1a64ef 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_key.h" #include "window/section_widget.h" #include "ui/effects/animations.h" -#include "ui/widgets/scroll_area.h" +#include "ui/userpic_view.h" #include "mtproto/sender.h" #include "api/api_single_message_search.h" @@ -21,6 +21,7 @@ class Error; namespace Data { class Forum; +enum class StorySourcesList : uchar; } // namespace Data namespace Main { @@ -44,7 +45,9 @@ class PlainShadow; class DownloadBar; class GroupCallBar; class RequestsBar; +class MoreChatsBar; class JumpDownButton; +class ElasticScroll; template class FadeWrapScaled; } // namespace Ui @@ -55,6 +58,11 @@ class ConnectionState; struct SectionShow; } // namespace Window +namespace Dialogs::Stories { +class List; +struct Content; +} // namespace Dialogs::Stories + namespace Dialogs { struct RowDescriptor; @@ -90,6 +98,7 @@ public: void setInnerFocus(); void jumpToTop(bool belowPinned = false); + void raiseWithTooltip(); void startWidthAnimation(); void stopWidthAnimation(); @@ -158,8 +167,14 @@ private: void setupSupportMode(); void setupConnectingWidget(); void setupMainMenuToggle(); + void setupMoreChatsBar(); void setupDownloadBar(); void setupShortcuts(); + void setupStories(); + void storiesExplicitCollapse(); + void collectStoriesUserpicsViews(Data::StorySourcesList list); + void storiesToggleExplicitExpand(bool expand); + void trackScroll(not_null widget); [[nodiscard]] bool searchForPeersRequired(const QString &query) const; [[nodiscard]] bool searchForTopicsRequired(const QString &query) const; bool setSearchInChat(Key chat, PeerData *from = nullptr); @@ -169,8 +184,10 @@ private: void clearSearchCache(); void setSearchQuery(const QString &query); void updateControlsVisibility(bool fast = false); - void updateLockUnlockVisibility(); + void updateLockUnlockVisibility( + anim::type animated = anim::type::instant); void updateLoadMoreChatsVisibility(); + void updateStoriesVisibility(); void updateJumpToDateVisibility(bool fast = false); void updateSearchFromVisibility(bool fast = false); void updateControlsGeometry(); @@ -207,11 +224,13 @@ private: mtpRequestId requestId); void peopleFailed(const MTP::Error &error, mtpRequestId requestId); - void scrollToTop(); + void scrollToDefault(bool verytop = false); + void scrollToDefaultChecked(bool verytop = false); void setupScrollUpButton(); void updateScrollUpVisibility(); void startScrollUpButtonAnimation(bool shown); void updateScrollUpPosition(); + void updateLockUnlockPosition(); MTP::Sender _api; @@ -232,14 +251,16 @@ private: object_ptr> _chooseFromUser; object_ptr> _jumpToDate; object_ptr _cancelSearch; - object_ptr _lockUnlock; + object_ptr< Ui::FadeWrapScaled> _lockUnlock; + + std::unique_ptr _moreChatsBar; std::unique_ptr _forumTopShadow; std::unique_ptr _forumGroupCallBar; std::unique_ptr _forumRequestsBar; std::unique_ptr _forumReportBar; - object_ptr _scroll; + object_ptr _scroll; QPointer _inner; class BottomButton; object_ptr _updateTelegram = { nullptr }; @@ -248,6 +269,7 @@ private: std::unique_ptr _connecting; Ui::Animations::Simple _scrollToAnimation; + int _scrollAnimationTo = 0; std::unique_ptr _showAnimation; rpl::variable _shownProgressValue; @@ -263,6 +285,17 @@ private: PeerData *_searchFromAuthor = nullptr; QString _lastFilterText; + rpl::event_stream> _storiesContents; + base::flat_map _storiesUserpicsViewsHidden; + base::flat_map _storiesUserpicsViewsShown; + Fn _updateScrollGeometryCached; + std::unique_ptr _stories; + Ui::Animations::Simple _storiesExplicitExpandAnimation; + rpl::variable _storiesExplicitExpandValue = 0; + int _storiesExplicitExpandScrollTop = 0; + int _aboveScrollAdded = 0; + bool _storiesExplicitExpand = false; + base::Timer _searchTimer; QString _peerSearchQuery; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 7e195ba94..91674bfed 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -331,22 +331,23 @@ void PaintRow( context.st->padding.top(), context.width, context.st->photoSize); - } else if (from) { - row->paintUserpic( - p, - from, - videoUserpic, - (flags & Flag::AllowUserOnline) ? history : nullptr, - context); - } else if (hiddenSenderInfo) { + } else if (!from && hiddenSenderInfo) { hiddenSenderInfo->emptyUserpic.paintCircle( p, context.st->padding.left(), context.st->padding.top(), context.width, context.st->photoSize); + } else if (!(flags & Flag::AllowUserOnline)) { + PaintUserpic( + p, + entry, + from, + videoUserpic, + row->userpicView(), + context); } else { - entry->paintUserpic(p, row->userpicView(), context); + row->paintUserpic(p, entry, from, videoUserpic, context); } auto nameleft = context.st->nameLeft; @@ -785,7 +786,7 @@ void RowPainter::Paint( ? history->peer->migrateTo() : history->peer.get()) : nullptr; - const auto allowUserOnline = !context.narrow || badgesState.empty(); + const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) | (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0)) | (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0)) diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index cb9fb4ee9..b1f59fb43 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -157,6 +157,7 @@ void MessageView::prepare( } options.existing = &_imagesCache; options.ignoreTopic = true; + options.spoilerLoginCode = true; auto preview = item->toPreview(options); const auto hasImages = !preview.images.empty(); const auto history = item->history(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp new file mode 100644 index 000000000..f737d1615 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -0,0 +1,546 @@ +/* +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 "dialogs/ui/dialogs_stories_content.h" + +#include "data/data_changes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_user.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "info/stories/info_stories_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "main/main_session.h" +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_menu_icons.h" + +namespace Dialogs::Stories { +namespace { + +constexpr auto kShownLastCount = 3; + +class PeerUserpic final : public Thumbnail { +public: + explicit PeerUserpic(not_null peer); + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + Ui::PeerUserpicView view; + Fn callback; + InMemoryKey key; + rpl::lifetime photoLifetime; + rpl::lifetime downloadLifetime; + }; + + [[nodiscard]] bool waitingUserpicLoad() const; + void processNewPhoto(); + + const not_null _peer; + QImage _frame; + std::unique_ptr _subscribed; + +}; + +class StoryThumbnail : public Thumbnail { +public: + explicit StoryThumbnail(FullStoryId id); + virtual ~StoryThumbnail() = default; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +protected: + struct Thumb { + Image *image = nullptr; + bool blurred = false; + }; + [[nodiscard]] virtual Main::Session &session() = 0; + [[nodiscard]] virtual Thumb loaded(FullStoryId id) = 0; + virtual void clear() = 0; + +private: + const FullStoryId _id; + QImage _full; + rpl::lifetime _subscription; + QImage _prepared; + bool _blurred = false; + +}; + +class PhotoThumbnail final : public StoryThumbnail { +public: + PhotoThumbnail(not_null photo, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _photo; + std::shared_ptr _media; + +}; + +class VideoThumbnail final : public StoryThumbnail { +public: + VideoThumbnail(not_null video, FullStoryId id); + +private: + Main::Session &session() override; + Thumb loaded(FullStoryId id) override; + void clear() override; + + const not_null _video; + std::shared_ptr _media; + +}; + +class EmptyThumbnail final : public Thumbnail { +public: + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + QImage _cached; + +}; + +class State final { +public: + State(not_null data, Data::StorySourcesList list); + + [[nodiscard]] Content next(); + +private: + const not_null _data; + const Data::StorySourcesList _list; + base::flat_map< + not_null, + std::shared_ptr> _userpics; + +}; + +PeerUserpic::PeerUserpic(not_null peer) +: _peer(peer) { +} + +QImage PeerUserpic::image(int size) { + Expects(_subscribed != nullptr); + + const auto good = (_frame.width() == size * _frame.devicePixelRatio()); + const auto key = _peer->userpicUniqueKey(_subscribed->view); + if (!good || (_subscribed->key != key && !waitingUserpicLoad())) { + const auto ratio = style::DevicePixelRatio(); + _subscribed->key = key; + _frame = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + _frame.fill(Qt::transparent); + + auto p = Painter(&_frame); + _peer->paintUserpic(p, _subscribed->view, 0, 0, size); + } + return _frame; +} + +bool PeerUserpic::waitingUserpicLoad() const { + return _peer->hasUserpic() && _peer->useEmptyUserpic(_subscribed->view); +} + +void PeerUserpic::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(std::move(callback)); + + _peer->session().changes().peerUpdates( + _peer, + Data::PeerUpdate::Flag::Photo + ) | rpl::start_with_next([=] { + _subscribed->callback(); + processNewPhoto(); + }, _subscribed->photoLifetime); + + processNewPhoto(); +} + +void PeerUserpic::processNewPhoto() { + Expects(_subscribed != nullptr); + + if (!waitingUserpicLoad()) { + _subscribed->downloadLifetime.destroy(); + return; + } + _peer->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return !waitingUserpicLoad(); + }) | rpl::start_with_next([=] { + _subscribed->callback(); + _subscribed->downloadLifetime.destroy(); + }, _subscribed->downloadLifetime); +} + +StoryThumbnail::StoryThumbnail(FullStoryId id) +: _id(id) { +} + +QImage StoryThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_prepared.width() != size * ratio) { + if (_full.isNull()) { + _prepared = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _prepared.fill(Qt::black); + } else { + const auto width = _full.width(); + const auto skip = std::max((_full.height() - width) / 2, 0); + _prepared = _full.copy(0, skip, width, width).scaled( + QSize(size, size) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + _prepared = Images::Circle(std::move(_prepared)); + _prepared.setDevicePixelRatio(ratio); + } + return _prepared; +} + +void StoryThumbnail::subscribeToUpdates(Fn callback) { + _subscription.destroy(); + if (!callback) { + clear(); + return; + } else if (!_full.isNull() && !_blurred) { + return; + } + const auto thumbnail = loaded(_id); + if (const auto image = thumbnail.image) { + _full = image->original(); + } + _blurred = thumbnail.blurred; + if (!_blurred) { + _prepared = QImage(); + } else { + _subscription = session().downloaderTaskFinished( + ) | rpl::filter([=] { + const auto thumbnail = loaded(_id); + if (!thumbnail.blurred) { + _full = thumbnail.image->original(); + _prepared = QImage(); + _blurred = false; + return true; + } + return false; + }) | rpl::take(1) | rpl::start_with_next(callback); + } +} + +PhotoThumbnail::PhotoThumbnail(not_null photo, FullStoryId id) +: StoryThumbnail(id) +, _photo(photo) { +} + +Main::Session &PhotoThumbnail::session() { + return _photo->session(); +} + +StoryThumbnail::Thumb PhotoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _photo->createMediaView(); + _media->wanted( + Data::PhotoSize::Small, + Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->image(Data::PhotoSize::Small)) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void PhotoThumbnail::clear() { + _media = nullptr; +} + +VideoThumbnail::VideoThumbnail( + not_null video, + FullStoryId id) +: StoryThumbnail(id) +, _video(video) { +} + +Main::Session &VideoThumbnail::session() { + return _video->session(); +} + +StoryThumbnail::Thumb VideoThumbnail::loaded(FullStoryId id) { + if (!_media) { + _media = _video->createMediaView(); + _media->thumbnailWanted(Data::FileOriginStory(id.peer, id.story)); + } + if (const auto small = _media->thumbnail()) { + return { .image = small }; + } + return { .image = _media->thumbnailInline(), .blurred = true }; +} + +void VideoThumbnail::clear() { + _media = nullptr; +} + +QImage EmptyThumbnail::image(int size) { + const auto ratio = style::DevicePixelRatio(); + if (_cached.width() != size * ratio) { + _cached = QImage( + QSize(size, size) * ratio, + QImage::Format_ARGB32_Premultiplied); + _cached.fill(Qt::black); + _cached.setDevicePixelRatio(ratio); + } + return _cached; +} + +void EmptyThumbnail::subscribeToUpdates(Fn callback) { +} + +State::State(not_null data, Data::StorySourcesList list) +: _data(data) +, _list(list) { +} + +Content State::next() { + auto result = Content(); + const auto &sources = _data->sources(_list); + result.elements.reserve(sources.size()); + for (const auto &info : sources) { + const auto source = _data->source(info.id); + Assert(source != nullptr); + + auto userpic = std::shared_ptr(); + const auto user = source->user; + if (const auto i = _userpics.find(user); i != end(_userpics)) { + userpic = i->second; + } else { + userpic = MakeUserpicThumbnail(user); + _userpics.emplace(user, userpic); + } + result.elements.push_back({ + .id = uint64(user->id.value), + .name = user->shortName(), + .thumbnail = std::move(userpic), + .count = info.count, + .unreadCount = info.unreadCount, + .skipSmall = user->isSelf() ? 1U : 0U, + }); + } + return result; +} + +} // namespace + +rpl::producer ContentForSession( + not_null session, + Data::StorySourcesList list) { + return [=](auto consumer) { + auto result = rpl::lifetime(); + const auto stories = &session->data().stories(); + const auto state = result.make_state(stories, list); + rpl::single( + rpl::empty + ) | rpl::then( + stories->sourcesChanged(list) + ) | rpl::start_with_next([=] { + consumer.put_next(state->next()); + }, result); + return result; + }; +} + +rpl::producer LastForPeer(not_null peer) { + using namespace rpl::mappers; + + const auto stories = &peer->owner().stories(); + const auto peerId = peer->id; + + return rpl::single( + peerId + ) | rpl::then( + stories->sourceChanged() | rpl::filter(_1 == peerId) + ) | rpl::map([=] { + auto ids = std::vector(); + auto readTill = StoryId(); + if (const auto source = stories->source(peerId)) { + readTill = source->readTill; + ids = ranges::views::all(source->ids) + | ranges::views::reverse + | ranges::views::take(kShownLastCount) + | ranges::views::transform(&Data::StoryIdDates::id) + | ranges::to_vector; + } + return rpl::make_producer([=](auto consumer) { + auto lifetime = rpl::lifetime(); + if (ids.empty()) { + consumer.put_next(Content()); + consumer.put_done(); + return lifetime; + } + + struct State { + Fn check; + base::has_weak_ptr guard; + int readTill = StoryId(); + bool pushed = false; + }; + const auto state = lifetime.make_state(); + state->readTill = readTill; + state->check = [=] { + if (state->pushed) { + return; + } + auto done = true; + auto resolving = false; + auto result = Content{}; + for (const auto id : ids) { + const auto storyId = FullStoryId{ peerId, id }; + const auto maybe = stories->lookup(storyId); + if (maybe) { + if (!resolving) { + const auto unread = (id > state->readTill); + result.elements.reserve(ids.size()); + result.elements.push_back({ + .id = uint64(id), + .thumbnail = MakeStoryThumbnail(*maybe), + .count = 1U, + .unreadCount = unread ? 1U : 0U, + }); + if (unread) { + done = false; + } + } + } else if (maybe.error() == Data::NoStory::Unknown) { + resolving = true; + stories->resolve( + storyId, + crl::guard(&state->guard, state->check)); + } + } + if (resolving) { + return; + } + state->pushed = true; + consumer.put_next(std::move(result)); + if (done) { + consumer.put_done(); + } + }; + + rpl::single(peerId) | rpl::then( + stories->itemsChanged() | rpl::filter(_1 == peerId) + ) | rpl::start_with_next(state->check, lifetime); + + stories->session().changes().storyUpdates( + Data::StoryUpdate::Flag::MarkRead + ) | rpl::start_with_next([=](const Data::StoryUpdate &update) { + if (update.story->peer()->id == peerId) { + if (update.story->id() > state->readTill) { + state->readTill = update.story->id(); + if (ranges::contains(ids, state->readTill) + || state->readTill > ids.front()) { + state->pushed = false; + state->check(); + } + } + } + }, lifetime); + + return lifetime; + }); + }) | rpl::flatten_latest(); +} + +std::shared_ptr MakeUserpicThumbnail(not_null peer) { + return std::make_shared(peer); +} + +std::shared_ptr MakeStoryThumbnail( + not_null story) { + using Result = std::shared_ptr; + const auto id = story->fullId(); + return v::match(story->media().data, [](v::null_t) -> Result { + return std::make_shared(); + }, [&](not_null photo) -> Result { + return std::make_shared(photo, id); + }, [&](not_null video) -> Result { + return std::make_shared(video, id); + }); +} + +void FillSourceMenu( + not_null controller, + const ShowMenuRequest &request) { + const auto owner = &controller->session().data(); + const auto peer = owner->peer(PeerId(request.id)); + const auto &add = request.callback; + if (peer->isSelf()) { + add(tr::lng_stories_archive_button(tr::now), [=] { + controller->showSection(Info::Stories::Make( + peer, + Info::Stories::Tab::Archive)); + }, &st::menuIconStoriesArchiveSection); + add(tr::lng_stories_my_title(tr::now), [=] { + controller->showSection(Info::Stories::Make(peer)); + }, &st::menuIconStoriesSavedSection); + } else { + add(tr::lng_profile_send_message(tr::now), [=] { + controller->showPeerHistory(peer); + }, &st::menuIconChatBubble); + add(tr::lng_context_view_profile(tr::now), [=] { + controller->showPeerInfo(peer); + }, &st::menuIconProfile); + const auto in = [&](Data::StorySourcesList list) { + return ranges::contains( + owner->stories().sources(list), + peer->id, + &Data::StoriesSourceInfo::id); + }; + const auto toggle = [=](bool shown) { + owner->stories().toggleHidden( + peer->id, + !shown, + controller->uiShow()); + }; + if (in(Data::StorySourcesList::NotHidden)) { + add(tr::lng_stories_archive(tr::now), [=] { + toggle(false); + }, &st::menuIconArchive); + } + if (in(Data::StorySourcesList::Hidden)) { + add(tr::lng_stories_unarchive(tr::now), [=] { + toggle(true); + }, &st::menuIconUnarchive); + } + } +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h new file mode 100644 index 000000000..b42715d54 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h @@ -0,0 +1,44 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +enum class StorySourcesList : uchar; +class Story; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Dialogs::Stories { + +struct Content; +class Thumbnail; +struct ShowMenuRequest; + +[[nodiscard]] rpl::producer ContentForSession( + not_null session, + Data::StorySourcesList list); + +[[nodiscard]] rpl::producer LastForPeer(not_null peer); + +[[nodiscard]] std::shared_ptr MakeUserpicThumbnail( + not_null peer); +[[nodiscard]] std::shared_ptr MakeStoryThumbnail( + not_null story); + +void FillSourceMenu( + not_null controller, + const ShowMenuRequest &request); + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp new file mode 100644 index 000000000..cda8da94d --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -0,0 +1,1209 @@ +/* +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 "dialogs/ui/dialogs_stories_list.h" + +#include "base/event_filter.h" +#include "base/qt_signal_producer.h" +#include "lang/lang_keys.h" +#include "ui/effects/outline_segments.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/tooltip.h" +#include "ui/painter.h" +#include "styles/style_dialogs.h" + +#include +#include +#include + +#include "base/debug_log.h" + +namespace Dialogs::Stories { +namespace { + +constexpr auto kSmallThumbsShown = 3; +constexpr auto kPreloadPages = 2; +constexpr auto kExpandAfterRatio = 0.72; +constexpr auto kCollapseAfterRatio = 0.68; +constexpr auto kFrictionRatio = 0.15; +constexpr auto kExpandCatchUpDuration = crl::time(200); +constexpr auto kMaxTooltipNames = 3; +constexpr auto kStoriesTooltipHideBgOpacity = 0.2; + +[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) { + const auto &full = st.full; + const auto &font = full.nameStyle.font; + const auto skip = font->spacew; + return full.photoLeft * 2 + full.photo - 2 * skip; +} + +[[nodiscard]] object_ptr MakeTooltipContent( + not_null parent, + rpl::producer text, + Fn hide) { + const auto size = st::dialogsStoriesTooltipHide.width; + const auto skip = st::defaultImportantTooltip.padding.right(); + auto result = object_ptr>( + parent, + Ui::MakeNiceTooltipLabel( + parent, + std::move(text), + st::dialogsStoriesTooltipMaxWidth, + st::dialogsStoriesTooltipLabel), + (st::defaultImportantTooltip.padding + + QMargins(0, 0, skip + size, 0))); + const auto button = Ui::CreateChild( + result.data(), + st::dialogsStoriesTooltipHide); + result->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + button->resize(button->width(), size.height()); + button->moveToRight(0, 0, size.width()); + }, button->lifetime()); + button->setClickedCallback(std::move(hide)); + return result; +} + +} // namespace + +struct List::Layout { + int itemsCount = 0; + QPointF geometryShift; + float64 expandedRatio = 0.; + float64 expandRatio = 0.; + float64 ratio = 0.; + float64 segmentsSpinProgress = 0.; + float64 thumbnailLeft = 0.; + float64 photoLeft = 0.; + float64 left = 0.; + float64 single = 0.; + int smallSkip = 0; + int leftFull = 0; + int leftSmall = 0; + int singleFull = 0; + int singleSmall = 0; + int startIndexSmall = 0; + int endIndexSmall = 0; + int startIndexFull = 0; + int endIndexFull = 0; +}; + +List::List( + not_null parent, + const style::DialogsStoriesList &st, + rpl::producer content) +: RpWidget(parent) +, _st(st) { + setCursor(style::cur_default); + + std::move(content) | rpl::start_with_next([=](Content &&content) { + showContent(std::move(content)); + }, lifetime()); + + setMouseTracking(true); + resize(0, _data.empty() ? 0 : st.full.height); +} + +List::~List() = default; + +void List::showContent(Content &&content) { + if (_content == content) { + return; + } + if (content.elements.empty()) { + _data = {}; + _empty = true; + return; + } + const auto wasCount = int(_data.items.size()); + _content = std::move(content); + auto items = base::take(_data.items); + _data.items.reserve(_content.elements.size()); + for (const auto &element : _content.elements) { + const auto id = element.id; + const auto i = ranges::find(items, id, [](const Item &item) { + return item.element.id; + }); + if (i != end(items)) { + _data.items.push_back(std::move(*i)); + auto &item = _data.items.back(); + if (item.element.thumbnail != element.thumbnail) { + item.element.thumbnail = element.thumbnail; + item.subscribed = false; + } + if (item.element.name != element.name) { + item.element.name = element.name; + item.nameCache = QImage(); + } + item.element.count = element.count; + item.element.unreadCount = element.unreadCount; + } else { + _data.items.emplace_back(Item{ .element = element }); + } + } + _lastCollapsedGeometry = {}; + if (int(_data.items.size()) != wasCount) { + updateGeometry(); + } + updateScrollMax(); + update(); + if (!wasCount) { + _empty = false; + } + _tooltipText = computeTooltipText(); + updateTooltipGeometry(); +} + +void List::updateScrollMax() { + const auto &full = _st.full; + const auto singleFull = full.photoLeft * 2 + full.photo; + const auto widthFull = full.left + int(_data.items.size()) * singleFull; + _scrollLeftMax = std::max(widthFull - width(), 0); + _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); + checkLoadMore(); + update(); +} + +rpl::producer List::clicks() const { + return _clicks.events(); +} + +rpl::producer List::showMenuRequests() const { + return _showMenuRequests.events(); +} + +rpl::producer List::toggleExpandedRequests() const { + return _toggleExpandedRequests.events(); +} + +rpl::producer<> List::entered() const { + return _entered.events(); +} + +rpl::producer<> List::loadMoreRequests() const { + return _loadMoreRequests.events(); +} + +rpl::producer> List::verticalScrollEvents() const { + return _verticalScrollEvents.events(); +} + +void List::requestExpanded(bool expanded) { + if (_expanded != expanded) { + _expanded = expanded; + const auto from = _expanded ? 0. : 1.; + const auto till = _expanded ? 2. : 0.; + const auto duration = (_expanded ? 2 : 1) * st::slideWrapDuration; + if (!isHidden() && _expanded) { + toggleTooltip(false); + } + _expandedAnimation.start([=] { + checkForFullState(); + update(); + _collapsedGeometryChanged.fire({}); + if (!isHidden() && !_expandedAnimation.animating()) { + toggleTooltip(false); + } + }, from, till, duration, anim::sineInOut); + } + _toggleExpandedRequests.fire_copy(_expanded); +} + +void List::enterEventHook(QEnterEvent *e) { + _entered.fire({}); +} + +void List::resizeEvent(QResizeEvent *e) { + updateScrollMax(); +} + +void List::updateExpanding() { + updateExpanding( + _lastExpandedHeight * _expandCatchUpAnimation.value(1.), + _st.full.height); +} + +void List::updateExpanding(int expandingHeight, int expandedHeight) { + Expects(!expandingHeight || expandedHeight > 0); + + const auto ratio = !expandingHeight + ? 0. + : (float64(expandingHeight) / expandedHeight); + if (_lastRatio == ratio) { + return; + } + const auto expanding = (ratio > _lastRatio); + _lastRatio = ratio; + const auto change = _expanded + ? (!expanding && ratio < kCollapseAfterRatio) + : (expanding && ratio > kExpandAfterRatio); + if (change) { + requestExpanded(!_expanded); + } + updateTooltipGeometry(); +} + +List::Layout List::computeLayout() { + return computeLayout(_expandedAnimation.value(_expanded ? 2. : 0.)); +} + +List::Layout List::computeLayout(float64 expanded) const { + const auto segmentsSpinProgress = expanded / 2.; + expanded = std::min(expanded, 1.); + + const auto &st = _st.small; + const auto &full = _st.full; + const auto expandedRatio = _lastRatio; + const auto collapsedRatio = expandedRatio * kFrictionRatio; + const auto ratio = expandedRatio * expanded + + collapsedRatio * (1. - expanded); + const auto expandRatio = (ratio >= kCollapseAfterRatio) + ? 1. + : (ratio <= kExpandAfterRatio * kFrictionRatio) + ? 0. + : ((ratio - (kExpandAfterRatio * kFrictionRatio)) + / (kCollapseAfterRatio - (kExpandAfterRatio * kFrictionRatio))); + + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto widthFull = width(); + const auto itemsCount = int(_data.items.size()); + const auto leftFullMin = full.left; + const auto singleFullMin = full.photoLeft * 2 + full.photo; + const auto totalFull = leftFullMin + singleFullMin * itemsCount; + const auto skipSide = (totalFull < widthFull) + ? (widthFull - totalFull) / (itemsCount + 1) + : 0; + const auto skipBetween = (totalFull < widthFull && itemsCount > 1) + ? (widthFull - totalFull - 2 * skipSide) / (itemsCount - 1) + : skipSide; + const auto singleFull = singleFullMin + skipBetween; + const auto smallSkip = (itemsCount > 1 + && _data.items[0].element.skipSmall) + ? 1 + : 0; + const auto smallCount = std::min( + kSmallThumbsShown, + itemsCount - smallSkip); + const auto leftSmall = st.left - (smallSkip ? st.shift : 0); + const auto leftFull = full.left - _scrollLeft + skipSide; + const auto startIndexFull = std::max(-leftFull, 0) / singleFull; + const auto cellLeftFull = leftFull + (startIndexFull * singleFull); + const auto endIndexFull = std::min( + (width() - leftFull + singleFull - 1) / singleFull, + itemsCount); + const auto startIndexSmall = std::min(startIndexFull, smallSkip); + const auto endIndexSmall = smallSkip + smallCount; + const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift); + const auto thumbnailLeftFull = cellLeftFull + full.photoLeft; + const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft; + const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull); + const auto photoLeft = lerp(st.photoLeft, full.photoLeft); + return Layout{ + .itemsCount = itemsCount, + .geometryShift = QPointF( + (_state == State::Changing + ? (lerp(_changingGeometryFrom.x(), _geometryFull.x()) - x()) + : 0.), + (_state == State::Changing + ? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y()) + : 0.)), + .expandedRatio = expandedRatio, + .expandRatio = expandRatio, + .ratio = ratio, + .segmentsSpinProgress = segmentsSpinProgress, + .thumbnailLeft = thumbnailLeft, + .photoLeft = photoLeft, + .left = thumbnailLeft - photoLeft, + .single = lerp(st.shift, singleFull), + .smallSkip = smallSkip, + .leftFull = leftFull, + .leftSmall = leftSmall, + .singleFull = singleFull, + .singleSmall = st.shift, + .startIndexSmall = startIndexSmall, + .endIndexSmall = endIndexSmall, + .startIndexFull = startIndexFull, + .endIndexFull = endIndexFull, + }; +} + +void List::paintEvent(QPaintEvent *e) { + const auto &st = _st.small; + const auto &full = _st.full; + const auto layout = computeLayout(); + const auto ratio = layout.ratio; + const auto expandRatio = layout.expandRatio; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * ratio; + }; + const auto elerp = [&](float64 a, float64 b) { + return a + (b - a) * expandRatio; + }; + const auto line = elerp(st.lineTwice, full.lineTwice) / 2.; + const auto photo = lerp(st.photo, full.photo); + const auto layered = layout.single < (photo + 4 * line); + auto p = QPainter(this); + if (layered) { + ensureLayer(); + auto q = QPainter(&_layer); + paint(q, layout, photo, line, true); + q.end(); + p.drawImage(0, 0, _layer); + } else { + paint(p, layout, photo, line, false); + } +} + +void List::ensureLayer() { + const auto ratio = style::DevicePixelRatio(); + const auto layer = size() * ratio; + if (_layer.size() != layer) { + _layer = QImage(layer, QImage::Format_ARGB32_Premultiplied); + _layer.setDevicePixelRatio(ratio); + } + _layer.fill(Qt::transparent); +} + +void List::paint( + QPainter &p, + const Layout &layout, + float64 photo, + float64 line, + bool layered) { + const auto &st = _st.small; + const auto &full = _st.full; + const auto expandRatio = layout.expandRatio; + const auto elerp = [&](float64 a, float64 b) { + return a + (b - a) * expandRatio; + }; + const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto photoTopSmall = st.photoTop; + const auto photoTop = photoTopSmall + + (full.photoTop - photoTopSmall) * layout.expandedRatio; + const auto nameScale = _lastRatio; + const auto nameTop = full.nameTop + + (photoTop + photo - full.photoTop - full.photo); + const auto nameWidth = nameScale * AvailableNameWidth(_st); + const auto nameHeight = nameScale * full.nameStyle.font->height; + const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; + const auto readUserpicOpacity = elerp(_st.readOpacity, 1.); + const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.); + if (_state == State::Changing) { + p.translate(layout.geometryShift); + } + + const auto drawSmall = (expandRatio < 1.); + const auto drawFull = (expandRatio > 0.); + auto hq = PainterHighQualityEnabler(p); + + const auto count = std::max( + layout.endIndexFull - layout.startIndexFull, + layout.endIndexSmall - layout.startIndexSmall); + + struct Single { + float64 x = 0.; + int indexSmall = 0; + Item *itemSmall = nullptr; + int indexFull = 0; + Item *itemFull = nullptr; + float64 photoTop = 0.; + + explicit operator bool() const { + return itemSmall || itemFull; + } + }; + const auto lookup = [&](int index) { + const auto indexSmall = layout.startIndexSmall + index; + const auto indexFull = layout.startIndexFull + index; + const auto ySmall = photoTopSmall + + ((photoTop - photoTopSmall) + * (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5); + const auto y = elerp(ySmall, photoTop); + + const auto small = (drawSmall + && indexSmall < layout.endIndexSmall + && indexSmall >= layout.smallSkip) + ? &_data.items[indexSmall] + : nullptr; + const auto full = (drawFull && indexFull < layout.endIndexFull) + ? &_data.items[indexFull] + : nullptr; + const auto x = layout.left + layout.single * index; + return Single{ x, indexSmall, small, indexFull, full, y }; + }; + const auto hasUnread = [&](const Single &single) { + return (single.itemSmall && single.itemSmall->element.unreadCount) + || (single.itemFull && single.itemFull->element.unreadCount); + }; + const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { + auto nextGradientPainted = false; + auto skippedPainted = false; + const auto first = layout.smallSkip - layout.startIndexSmall; + for (auto i = count; i != first;) { + --i; + const auto next = (i > 0) ? lookup(i - 1) : Single(); + const auto gradientPainted = nextGradientPainted; + nextGradientPainted = false; + if (const auto current = lookup(i)) { + if (i == first && next && !skippedPainted) { + skippedPainted = true; + paintGradient(next); + paintOther(next); + } + if (!gradientPainted) { + paintGradient(current); + } + if (i > first && hasUnread(current) && next) { + if (current.itemSmall || !next.itemSmall) { + if (i - 1 == first + && first > 0 + && !skippedPainted) { + if (const auto skipped = lookup(i - 2)) { + skippedPainted = true; + paintGradient(skipped); + paintOther(skipped); + } + } + nextGradientPainted = true; + paintGradient(next); + } + } + paintOther(current); + } + } + }; + auto gradient = Ui::UnreadStoryOutlineGradient(); + enumerate([&](Single single) { + // Name. + if (const auto full = single.itemFull) { + validateName(full); + if (expandRatio > 0.) { + p.setOpacity(expandRatio); + p.drawImage(QRectF( + single.x + nameLeft, + nameTop, + nameWidth, + nameHeight + ), full->nameCache); + } + } + + // Unread gradient. + const auto x = single.x; + const auto userpic = QRectF( + x + layout.photoLeft, + single.photoTop, + photo, + photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = (small && small->element.unreadCount); + const auto fullUnreadCount = itemFull + ? itemFull->element.unreadCount + : 0; + const auto unreadOpacity = (smallUnread && fullUnreadCount) + ? 1. + : smallUnread + ? (1. - expandRatio) + : fullUnreadCount + ? expandRatio + : 0.; + if (unreadOpacity > 0.) { + p.setOpacity(unreadOpacity); + const auto outerAdd = 1.5 * line; + const auto outer = userpic.marginsAdded( + { outerAdd, outerAdd, outerAdd, outerAdd }); + gradient.setStart(userpic.topRight()); + gradient.setFinalStop(userpic.bottomLeft()); + if (!fullUnreadCount) { + p.setPen(QPen(gradient, line)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(outer); + } else { + validateSegments(itemFull, gradient, line, true); + Ui::PaintOutlineSegments( + p, + outer, + itemFull->segments, + layout.segmentsSpinProgress); + } + } + p.setOpacity(1.); + }, [&](Single single) { + Expects(single.itemSmall || single.itemFull); + + const auto x = single.x; + const auto userpic = QRectF( + x + layout.photoLeft, + single.photoTop, + photo, + photo); + const auto small = single.itemSmall; + const auto itemFull = single.itemFull; + const auto smallUnread = small && small->element.unreadCount; + const auto fullUnreadCount = itemFull + ? itemFull->element.unreadCount + : 0; + const auto fullCount = itemFull ? itemFull->element.count : 0; + + // White circle with possible read gray line. + const auto hasReadLine = (itemFull && fullUnreadCount < fullCount); + p.setOpacity((small && itemFull) + ? 1. + : small + ? (1. - expandRatio) + : expandRatio); + const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.); + const auto rect = userpic.marginsAdded({ add, add, add, add }); + if (layered) { + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setPen(Qt::NoPen); + p.setBrush(st::transparent); + p.drawEllipse(rect); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + if (hasReadLine) { + if (small && !small->element.unreadCount) { + p.setOpacity(expandRatio); + } + validateSegments( + itemFull, + st::dialogsUnreadBgMuted->b, + lineRead, + false); + Ui::PaintOutlineSegments( + p, + rect, + itemFull->segments, + layout.segmentsSpinProgress); + } + + // Userpic. + if (itemFull == small) { + p.setOpacity(smallUnread ? 1. : readUserpicOpacity); + validateThumbnail(itemFull); + const auto size = full.photo; + p.drawImage(userpic, itemFull->element.thumbnail->image(size)); + } else { + if (small) { + p.setOpacity(smallUnread + ? (itemFull ? 1. : (1. - expandRatio)) + : (itemFull + ? _st.readOpacity + : readUserpicAppearingOpacity)); + validateThumbnail(small); + const auto size = (expandRatio > 0.) + ? full.photo + : st.photo; + p.drawImage(userpic, small->element.thumbnail->image(size)); + } + if (itemFull) { + p.setOpacity(expandRatio); + validateThumbnail(itemFull); + const auto size = full.photo; + p.drawImage( + userpic, + itemFull->element.thumbnail->image(size)); + } + } + p.setOpacity(1.); + }); +} + +void List::validateThumbnail(not_null item) { + if (!item->subscribed) { + item->subscribed = true; + //const auto id = item.element.id; + item->element.thumbnail->subscribeToUpdates([=] { + update(); + }); + } +} + +void List::validateSegments( + not_null item, + const QBrush &brush, + float64 line, + bool forUnread) { + const auto count = item->element.count; + const auto unread = item->element.unreadCount; + if (int(item->segments.size()) != count) { + item->segments.resize(count); + } + auto i = 0; + if (forUnread) { + for (; i != count - unread; ++i) { + item->segments[i].width = 0.; + } + for (; i != count; ++i) { + item->segments[i].brush = brush; + item->segments[i].width = line; + } + } else { + for (; i != count - unread; ++i) { + item->segments[i].brush = brush; + item->segments[i].width = line; + } + for (; i != count; ++i) { + item->segments[i].width = 0.; + } + } +} + +void List::validateName(not_null item) { + const auto &element = item->element; + const auto &color = (element.unreadCount || element.skipSmall) + ? st::dialogsNameFg + : st::windowSubTextFg; + if (!item->nameCache.isNull() && item->nameCacheColor == color->c) { + return; + } + const auto &full = _st.full; + const auto &font = full.nameStyle.font; + const auto available = AvailableNameWidth(_st); + 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( + QSize(available, font->height) * ratio, + QImage::Format_ARGB32_Premultiplied); + item->nameCache.setDevicePixelRatio(ratio); + item->nameCache.fill(Qt::transparent); + auto p = Painter(&item->nameCache); + p.setPen(color); + text.drawElided(p, 0, 0, available, 1, style::al_top); +} + +void List::wheelEvent(QWheelEvent *e) { + const auto phase = e->phase(); + const auto fullDelta = e->pixelDelta().isNull() + ? e->angleDelta() + : e->pixelDelta(); + if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) { + _scrollingLock = Qt::Orientation(); + if (fullDelta.isNull()) { + return; + } + } + const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y()); + if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) { + _scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal; + } + if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) { + _verticalScrollEvents.fire(e); + return; + } else if (_state == State::Small) { + e->ignore(); + return; + } + const auto delta = vertical + ? fullDelta.y() + : ((style::RightToLeft() ? -1 : 1) * fullDelta.x()); + + const auto now = _scrollLeft; + const auto used = now - delta; + const auto next = std::clamp(used, 0, _scrollLeftMax); + if (next != now) { + requestExpanded(true); + _scrollLeft = next; + updateSelected(); + checkLoadMore(); + update(); + } + e->accept(); +} + +void List::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } else if (_state == State::Small) { + requestExpanded(true); + if (const auto onstack = _tooltipHide) { + onstack(); + } + return; + } else if (_state != State::Full) { + return; + } + _lastMousePosition = e->globalPos(); + updateSelected(); + + _mouseDownPosition = _lastMousePosition; + _pressed = _selected; +} + +void List::mouseMoveEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + updateSelected(); + + if (!_dragging && _mouseDownPosition && _state == State::Full) { + if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() + >= QApplication::startDragDistance()) { + _dragging = true; + _startDraggingLeft = _scrollLeft; + } + } + checkDragging(); +} + +void List::checkDragging() { + if (_dragging) { + const auto sign = (style::RightToLeft() ? -1 : 1); + const auto newLeft = std::clamp( + (sign * (_mouseDownPosition->x() - _lastMousePosition.x()) + + _startDraggingLeft), + 0, + _scrollLeftMax); + if (newLeft != _scrollLeft) { + _scrollLeft = newLeft; + checkLoadMore(); + update(); + } + } +} + +void List::checkLoadMore() { + if (_scrollLeftMax - _scrollLeft < width() * kPreloadPages) { + _loadMoreRequests.fire({}); + } +} + +void List::mouseReleaseEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + const auto guard = gsl::finally([&] { + _mouseDownPosition = std::nullopt; + }); + + const auto pressed = std::exchange(_pressed, -1); + if (finishDragging()) { + return; + } + updateSelected(); + if (_selected == pressed) { + if (!_expanded) { + requestExpanded(true); + } else if (_selected < _data.items.size()) { + _clicks.fire_copy(_data.items[_selected].element.id); + } + } +} + +void List::setExpandedHeight(int height, bool momentum) { + height = std::clamp(height, 0, _st.full.height); + if (_lastExpandedHeight == height) { + return; + } else if (momentum && _expandIgnored) { + return; + } else if (momentum && height > 0 && !_lastExpandedHeight) { + _expandIgnored = true; + return; + } else if (!momentum && _expandIgnored && height > 0) { + _expandIgnored = false; + _expandCatchUpAnimation.start([=] { + updateExpanding(); + update(); + checkForFullState(); + }, 0., 1., kExpandCatchUpDuration); + } else if (!height && _expandCatchUpAnimation.animating()) { + _expandCatchUpAnimation.stop(); + } + _lastExpandedHeight = height; + updateExpanding(); + if (!checkForFullState()) { + setState(!height ? State::Small : State::Changing); + } + update(); +} + +bool List::checkForFullState() { + if (_expandCatchUpAnimation.animating() + || _expandedAnimation.animating() + || _lastExpandedHeight < _st.full.height) { + return false; + } + setState(State::Full); + return true; +} + +void List::setLayoutConstraints( + QPoint positionSmall, + style::align alignSmall, + QRect geometryFull) { + if (_positionSmall == positionSmall + && _alignSmall == alignSmall + && _geometryFull == geometryFull) { + return; + } + _positionSmall = positionSmall; + _alignSmall = alignSmall; + _geometryFull = geometryFull; + _lastCollapsedGeometry = {}; + updateGeometry(); + update(); +} + +TextWithEntities List::computeTooltipText() const { + const auto &list = _data.items; + if (list.empty()) { + return {}; + } else if (list.size() == 1 && list.front().element.skipSmall) { + return { tr::lng_stories_click_to_view_mine(tr::now) }; + } + auto names = QStringList(); + for (const auto &item : list) { + if (item.element.skipSmall) { + continue; + } + names.append(item.element.name); + if (names.size() >= kMaxTooltipNames) { + break; + } + } + auto sequence = Ui::Text::Bold(names.front()); + if (names.size() > 1) { + for (auto i = 1; i + 1 != names.size(); ++i) { + sequence = tr::lng_stories_click_to_view_and_one( + tr::now, + lt_accumulated, + sequence, + lt_user, + Ui::Text::Bold(names[i]), + Ui::Text::WithEntities); + } + sequence = tr::lng_stories_click_to_view_and_last( + tr::now, + lt_accumulated, + sequence, + lt_user, + Ui::Text::Bold(names.back()), + Ui::Text::WithEntities); + } + return tr::lng_stories_click_to_view( + tr::now, + lt_users, + sequence, + Ui::Text::WithEntities); +} + +void List::setShowTooltip( + not_null tooltipParent, + rpl::producer shown, + Fn hide) { + _tooltip = nullptr; + _tooltipHide = std::move(hide); + _tooltipNotHidden = std::move(shown); + _tooltipText = computeTooltipText(); + const auto notEmpty = [](const TextWithEntities &text) { + return !text.empty(); + }; + _tooltip = std::make_unique( + tooltipParent, + MakeTooltipContent( + tooltipParent, + _tooltipText.value() | rpl::filter(notEmpty), + _tooltipHide), + st::dialogsStoriesTooltip); + const auto tooltip = _tooltip.get(); + const auto weak = QPointer(tooltip); + tooltip->toggleFast(false); + updateTooltipGeometry(); + + const auto handle = tooltipParent->window()->windowHandle(); + auto windowActive = rpl::single( + handle->isActive() + ) | rpl::then(base::qt_signal_producer( + handle, + &QWindow::activeChanged + ) | rpl::map([=] { + return handle->isActive(); + })) | rpl::distinct_until_changed(); + + { + const auto recompute = [=] { + updateTooltipGeometry(); + tooltip->raise(); + }; + using namespace base; + using Event = not_null; + install_event_filter(tooltip, tooltipParent, [=](Event e) { + if (e->type() == QEvent::ChildAdded) { + recompute(); + } + return EventFilterResult::Continue; + }); + } + + rpl::combine( + _tooltipNotHidden.value(), + _tooltipText.value() | rpl::map( + notEmpty + ) | rpl::distinct_until_changed(), + std::move(windowActive) + ) | rpl::start_with_next([=](bool, bool, bool active) { + _tooltipWindowActive = active; + if (!isHidden()) { + toggleTooltip(false); + } + }, tooltip->lifetime()); + + shownValue( + ) | rpl::skip(1) | rpl::start_with_next([=](bool shown) { + toggleTooltip(true); + }, tooltip->lifetime()); +} + +void List::raiseTooltip() { + if (_tooltip) { + _tooltip->raise(); + } +} + +void List::toggleTooltip(bool fast) { + const auto shown = !_expanded + && !_expandedAnimation.animating() + && !isHidden() + && _tooltipNotHidden.current() + && !_tooltipText.current().empty() + && window()->windowHandle()->isActive(); + if (_tooltip) { + if (fast) { + _tooltip->toggleFast(shown); + } else { + _tooltip->toggleAnimated(shown); + } + } + if (shown) { + updateTooltipGeometry(); + } +} + +void List::updateTooltipGeometry() { + if (!_tooltip || _expanded || _expandedAnimation.animating()) { + return; + } + const auto collapsed = collapsedGeometryCurrent(); + const auto geometry = Ui::MapFrom( + _tooltip->parentWidget(), + parentWidget(), + QRect( + collapsed.geometry.x(), + collapsed.geometry.y(), + int(std::ceil(collapsed.singleWidth)), + collapsed.geometry.height())); + const auto weak = QPointer(_tooltip.get()); + const auto countPosition = [=](QSize size) { + const auto left = geometry.x() + + (geometry.width() - size.width()) / 2; + const auto right = _tooltip->parentWidget()->width() + - st::dialogsStoriesTooltip.padding.right(); + return QPoint( + std::max(std::min(left, right - size.width()), 0), + geometry.y() + geometry.height()); + }; + _tooltip->pointAt(geometry, RectPart::Bottom, countPosition); +} + +List::CollapsedGeometry List::collapsedGeometryCurrent() const { + const auto expanded = _expandedAnimation.value(_expanded ? 2. : 0.); + if (expanded >= 1.) { + const auto single = 2 * _st.full.photoLeft + _st.full.photo; + return { QRect(), 1., float64(single) }; + } else if (_lastCollapsedRatio == _lastRatio + && _lastCollapsedGeometry.expanded == expanded + && !_lastCollapsedGeometry.geometry.isEmpty()) { + return _lastCollapsedGeometry; + } + const auto layout = computeLayout(0.); + const auto small = countSmallGeometry(); + const auto index = layout.smallSkip - layout.startIndexSmall; + const auto shift = x() + layout.geometryShift.x(); + const auto left = int(base::SafeRound( + shift + layout.left + layout.single * index)); + const auto width = small.x() + small.width() - left; + const auto photoTopSmall = _st.small.photoTop; + const auto photoTop = photoTopSmall + + (_st.full.photoTop - photoTopSmall) * layout.expandedRatio; + const auto ySmall = photoTopSmall + + ((photoTop - photoTopSmall) * kSmallThumbsShown / 0.5); + const auto photo = _st.small.photo + + (_st.full.photo - _st.small.photo) * layout.ratio; + const auto top = y() + layout.geometryShift.y(); + _lastCollapsedRatio = _lastRatio; + _lastCollapsedGeometry = { + QRect(left, top, width, ySmall + photo + _st.full.photoTop), + expanded, + layout.photoLeft * 2 + photo, + }; + return _lastCollapsedGeometry; +} + +rpl::producer<> List::collapsedGeometryChanged() const { + return _collapsedGeometryChanged.events(); +} + +void List::updateGeometry() { + switch (_state) { + case State::Small: setGeometry(countSmallGeometry()); break; + case State::Changing: { + _changingGeometryFrom = countSmallGeometry(); + setGeometry(_geometryFull.united(_changingGeometryFrom)); + } break; + case State::Full: setGeometry(_geometryFull); + } + updateTooltipGeometry(); + update(); +} + +QRect List::countSmallGeometry() const { + const auto &st = _st.small; + const auto layout = computeLayout(0.); + const auto count = layout.endIndexSmall + - std::max(layout.startIndexSmall, layout.smallSkip); + const auto width = st.left + + st.photoLeft + + st.photo + (count - 1) * st.shift + + st.photoLeft + + st.left; + const auto left = ((_alignSmall & Qt::AlignRight) == Qt::AlignRight) + ? (_positionSmall.x() - width) + : ((_alignSmall & Qt::AlignCenter) == Qt::AlignCenter) + ? (_positionSmall.x() - (width / 2)) + : _positionSmall.x(); + return QRect( + left, + _positionSmall.y(), + width, + st.photoTop + st.photo + st.photoTop); +} + +void List::setState(State state) { + if (_state == state) { + return; + } + _state = state; + updateGeometry(); +} + +void List::contextMenuEvent(QContextMenuEvent *e) { + _menu = nullptr; + + if (e->reason() == QContextMenuEvent::Mouse) { + _lastMousePosition = e->globalPos(); + updateSelected(); + } + if (_selected < 0 || _data.empty() || !_expanded) { + return; + } + _menu = base::make_unique_q( + this, + st::popupMenuWithIcons); + _showMenuRequests.fire({ + _data.items[_selected].element.id, + Ui::Menu::CreateAddActionCallback(_menu), + }); + if (_menu->empty()) { + _menu = nullptr; + return; + } + const auto updateAfterMenuDestroyed = [=] { + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + _lastMousePosition = globalPosition; + updateSelected(); + } + }; + QObject::connect( + _menu.get(), + &QObject::destroyed, + crl::guard(&_menuGuard, updateAfterMenuDestroyed)); + _menu->popup(e->globalPos()); + e->accept(); +} + +bool List::finishDragging() { + if (!_dragging) { + return false; + } + checkDragging(); + _dragging = false; + updateSelected(); + return true; +} + +void List::updateSelected() { + if (_pressed >= 0) { + return; + } + const auto &st = _st.small; + const auto p = mapFromGlobal(_lastMousePosition); + const auto layout = computeLayout(); + const auto firstRightFull = layout.leftFull + + (layout.startIndexFull + 1) * layout.singleFull; + const auto secondLeftFull = firstRightFull; + const auto firstRightSmall = layout.leftSmall + + st.photoLeft + + st.photo; + const auto secondLeftSmall = layout.smallSkip + ? (layout.leftSmall + st.photoLeft + st.shift) + : firstRightSmall; + const auto lastRightAddFull = 0; + const auto lastRightAddSmall = st.photoLeft; + const auto lerp = [&](float64 a, float64 b) { + return a + (b - a) * layout.ratio; + }; + const auto firstRight = lerp(firstRightSmall, firstRightFull); + const auto secondLeft = lerp(secondLeftSmall, secondLeftFull); + const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull); + const auto activateFull = (layout.ratio >= 0.5); + const auto startIndex = activateFull + ? layout.startIndexFull + : layout.startIndexSmall; + const auto endIndex = activateFull + ? layout.endIndexFull + : layout.endIndexSmall; + const auto x = p.x(); + const auto infiniteIndex = (x < secondLeft) + ? 0 + : int( + std::floor((std::max(x - firstRight, 0.)) / layout.single) + 1); + const auto index = (endIndex == startIndex) + ? -1 + : (infiniteIndex == endIndex - startIndex + && x < firstRight + + (endIndex - startIndex - 1) * layout.single + + lastRightAdd) + ? (infiniteIndex - 1) // Last small part should still be clickable. + : (startIndex + infiniteIndex >= endIndex) + ? (_st.fullClickable ? (endIndex - 1) : -1) + : infiniteIndex; + const auto selected = (index < 0 + || startIndex + index >= layout.itemsCount) + ? -1 + : (startIndex + index); + if (_selected != selected) { + const auto over = (selected >= 0); + if (over != (_selected >= 0)) { + setCursor(over ? style::cur_pointer : style::cur_default); + } + _selected = selected; + } +} + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h new file mode 100644 index 000000000..24468850a --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -0,0 +1,224 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/qt/qt_compare.h" +#include "base/timer.h" +#include "base/weak_ptr.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/rp_widget.h" + +class QPainter; + +namespace style { +struct DialogsStories; +struct DialogsStoriesList; +} // namespace style + +namespace Ui { +class PopupMenu; +struct OutlineSegment; +class ImportantTooltip; +} // namespace Ui + +namespace Dialogs::Stories { + +class Thumbnail { +public: + [[nodiscard]] virtual QImage image(int size) = 0; + virtual void subscribeToUpdates(Fn callback) = 0; +}; + +struct Element { + uint64 id = 0; + QString name; + std::shared_ptr thumbnail; + uint32 count : 15 = 0; + uint32 unreadCount : 15 = 0; + uint32 skipSmall : 1 = 0; + + friend inline bool operator==( + const Element &a, + const Element &b) = default; +}; + +struct Content { + std::vector elements; + + friend inline bool operator==( + const Content &a, + const Content &b) = default; +}; + +struct ShowMenuRequest { + uint64 id = 0; + Ui::Menu::MenuCallback callback; +}; + +class List final : public Ui::RpWidget { +public: + List( + not_null parent, + const style::DialogsStoriesList &st, + rpl::producer content); + ~List(); + + void setExpandedHeight(int height, bool momentum = false); + void setLayoutConstraints( + QPoint positionSmall, + style::align alignSmall, + QRect geometryFull = QRect()); + void setShowTooltip( + not_null tooltipParent, + rpl::producer shown, + Fn hide); + void raiseTooltip(); + + struct CollapsedGeometry { + QRect geometry; + float64 expanded = 0.; + float64 singleWidth = 0.; + }; + [[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const; + [[nodiscard]] rpl::producer<> collapsedGeometryChanged() const; + + [[nodiscard]] bool empty() const { + return _empty.current(); + } + [[nodiscard]] rpl::producer emptyValue() const { + return _empty.value(); + } + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer showMenuRequests() const; + [[nodiscard]] rpl::producer toggleExpandedRequests() const; + [[nodiscard]] rpl::producer<> entered() const; + [[nodiscard]] rpl::producer<> loadMoreRequests() const; + + [[nodiscard]] auto verticalScrollEvents() const + -> rpl::producer>; + +private: + struct Layout; + enum class State { + Small, + Changing, + Full, + }; + struct Item { + Element element; + QImage nameCache; + QColor nameCacheColor; + std::vector segments; + bool subscribed = false; + }; + struct Data { + std::vector items; + + [[nodiscard]] bool empty() const { + return items.empty(); + } + }; + + void showContent(Content &&content); + void enterEventHook(QEnterEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + void paint( + QPainter &p, + const Layout &layout, + float64 photo, + float64 line, + bool layered); + void ensureLayer(); + void validateThumbnail(not_null item); + void validateName(not_null item); + void updateScrollMax(); + void updateSelected(); + void checkDragging(); + bool finishDragging(); + void checkLoadMore(); + void requestExpanded(bool expanded); + + void updateTooltipGeometry(); + [[nodiscard]] TextWithEntities computeTooltipText() const; + void toggleTooltip(bool fast); + + bool checkForFullState(); + void setState(State state); + void updateGeometry(); + [[nodiscard]] QRect countSmallGeometry() const; + void updateExpanding(); + void updateExpanding(int expandingHeight, int expandedHeight); + void validateSegments( + not_null item, + const QBrush &brush, + float64 line, + bool forUnread); + + [[nodiscard]] Layout computeLayout(); + [[nodiscard]] Layout computeLayout(float64 expanded) const; + + const style::DialogsStoriesList &_st; + Content _content; + Data _data; + rpl::event_stream _clicks; + rpl::event_stream _showMenuRequests; + rpl::event_stream _toggleExpandedRequests; + rpl::event_stream<> _entered; + rpl::event_stream<> _loadMoreRequests; + rpl::event_stream<> _collapsedGeometryChanged; + + QImage _layer; + QPoint _positionSmall; + style::align _alignSmall = {}; + QRect _geometryFull; + QRect _changingGeometryFrom; + State _state = State::Small; + rpl::variable _empty = true; + + QPoint _lastMousePosition; + std::optional _mouseDownPosition; + int _startDraggingLeft = 0; + int _scrollLeft = 0; + int _scrollLeftMax = 0; + bool _dragging = false; + Qt::Orientation _scrollingLock = {}; + + Ui::Animations::Simple _expandedAnimation; + Ui::Animations::Simple _expandCatchUpAnimation; + float64 _lastRatio = 0.; + int _lastExpandedHeight = 0; + bool _expandIgnored : 1 = false; + bool _expanded : 1 = false; + + mutable CollapsedGeometry _lastCollapsedGeometry; + mutable float64 _lastCollapsedRatio = 0.; + + int _selected = -1; + int _pressed = -1; + + rpl::event_stream> _verticalScrollEvents; + + rpl::variable _tooltipText; + rpl::variable _tooltipNotHidden; + Fn _tooltipHide; + std::unique_ptr _tooltip; + bool _tooltipWindowActive = false; + + base::unique_qptr _menu; + base::has_weak_ptr _menuGuard; + +}; + +} // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp index cdebd9da4..bfc056243 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp @@ -7,13 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_video_userpic.h" -#include "ui/painter.h" #include "core/file_location.h" #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_file_origin.h" #include "data/data_session.h" +#include "dialogs/ui/dialogs_layout.h" +#include "ui/painter.h" +#include "styles/style_dialogs.h" namespace Dialogs::Ui { @@ -129,6 +131,29 @@ void VideoUserpic::clipCallback(Media::Clip::Notification notification) { } } +void PaintUserpic( + Painter &p, + not_null entry, + PeerData *peer, + VideoUserpic *videoUserpic, + PeerUserpicView &view, + const Ui::PaintContext &context) { + if (peer) { + PaintUserpic( + p, + peer, + videoUserpic, + view, + context.st->padding.left(), + context.st->padding.top(), + context.width, + context.st->photoSize, + context.paused); + } else { + entry->paintUserpic(p, view, context); + } +} + void PaintUserpic( Painter &p, not_null peer, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h index 6c285ce97..cc06a008c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h @@ -19,10 +19,16 @@ namespace Ui { struct PeerUserpicView; } // namespace Ui +namespace Dialogs { +class Entry; +} // namespace Dialogs + namespace Dialogs::Ui { using namespace ::Ui; +struct PaintContext; + class VideoUserpic final { public: VideoUserpic(not_null peer, Fn repaint); @@ -54,6 +60,14 @@ private: }; +void PaintUserpic( + Painter &p, + not_null entry, + PeerData *peer, + VideoUserpic *videoUserpic, + PeerUserpicView &view, + const Ui::PaintContext &context); + void PaintUserpic( Painter &p, not_null peer, diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp index 91748f82d..2c0482c33 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.cpp @@ -12,21 +12,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" // Window::GifPauseReason #include "styles/style_chat_helpers.h" +#include "styles/style_media_view.h" namespace Editor { StickersPanelController::StickersPanelController( not_null panelContainer, - not_null controller) + std::shared_ptr show) : _stickersPanel( base::make_unique_q( panelContainer, - controller, - object_ptr( - nullptr, - controller, - Window::GifPauseReason::Layer, - ChatHelpers::TabbedSelector::Mode::MediaEditor))) { + ChatHelpers::TabbedPanelDescriptor{ + .ownedSelector = object_ptr( + nullptr, + ChatHelpers::TabbedSelectorDescriptor{ + .show = show, + .st = st::storiesComposeControls.tabbed, + .level = Window::GifPauseReason::Layer, + .mode = ChatHelpers::TabbedSelector::Mode::MediaEditor, + .features = { + .megagroupSet = false, + .stickersSettings = false, + .openStickerSets = false, + }, + }), + })) { _stickersPanel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, diff --git a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h index 5404a8adb..9514ea23b 100644 --- a/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h +++ b/Telegram/SourceFiles/editor/controllers/stickers_panel_controller.h @@ -11,16 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { class TabbedPanel; +class Show; } // namespace ChatHelpers namespace Ui { class RpWidget; } // namespace Ui -namespace Window { -class SessionController; -} // namespace Window - namespace Editor { class StickersPanelController final { @@ -34,7 +31,7 @@ public: StickersPanelController( not_null panelContainer, - not_null controller); + std::shared_ptr show); [[nodiscard]] auto stickerChosen() const -> rpl::producer>; diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index ef91a1aa8..801e12258 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -52,16 +52,34 @@ PhotoEditor::PhotoEditor( std::shared_ptr photo, PhotoModifications modifications, EditorData data) +: PhotoEditor( + parent, + controller->uiShow(), + (controller->sessionController() + ? controller->sessionController()->uiShow() + : nullptr), + std::move(photo), + std::move(modifications), + std::move(data)) { +} + +PhotoEditor::PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data) : RpWidget(parent) , _modifications(std::move(modifications)) , _controllers(std::make_shared( - controller->sessionController() + sessionShow ? std::make_unique( this, - controller->sessionController()) + std::move(sessionShow)) : nullptr, std::make_unique(), - std::make_shared(controller))) + std::move(show))) , _content(base::make_unique_q( this, photo, diff --git a/Telegram/SourceFiles/editor/photo_editor.h b/Telegram/SourceFiles/editor/photo_editor.h index e226b1ddb..4169339a6 100644 --- a/Telegram/SourceFiles/editor/photo_editor.h +++ b/Telegram/SourceFiles/editor/photo_editor.h @@ -15,8 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class LayerWidget; +class Show; } // namespace Ui +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Window { class Controller; } // namespace Window @@ -36,6 +41,13 @@ public: std::shared_ptr photo, PhotoModifications modifications, EditorData data = EditorData()); + PhotoEditor( + not_null parent, + std::shared_ptr show, + std::shared_ptr sessionShow, + std::shared_ptr photo, + PhotoModifications modifications, + EditorData data = EditorData()); void save(); [[nodiscard]] rpl::producer doneRequests() const; diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp index c58fb1cc0..daf93c08d 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.cpp @@ -22,7 +22,7 @@ namespace Editor { void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback) { @@ -56,13 +56,14 @@ void OpenWithPreparedFile( const auto fileImage = std::make_shared(std::move(copy)); auto editor = base::make_unique_q( parent, - &controller->window(), + show, + show, fileImage, image->modifications); const auto raw = editor.get(); auto layer = std::make_unique(parent, std::move(editor)); InitEditorLayer(layer.get(), raw, std::move(callback)); - controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); + show->showLayer(std::move(layer), Ui::LayerOption::KeepOther); } void PrepareProfilePhoto( diff --git a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h index d7fd19a75..f1c2fd445 100644 --- a/Telegram/SourceFiles/editor/photo_editor_layer_widget.h +++ b/Telegram/SourceFiles/editor/photo_editor_layer_widget.h @@ -6,13 +6,13 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -// -//#include "ui/image/image.h" -//#include "editor/photo_editor_common.h" -//#include "base/unique_qptr.h" enum class ImageRoundRadius; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class RpWidget; struct PreparedFile; @@ -31,7 +31,7 @@ struct EditorData; void OpenWithPreparedFile( not_null parent, - not_null controller, + std::shared_ptr show, not_null file, int previewWidth, Fn &&doneCallback); diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index e8d9b86bf..6b9fc2997 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -42,6 +42,16 @@ QString PreparePhotoFileName(int index, TimeId date) { + ".jpg"; } +QString PrepareStoryFileName( + int index, + TimeId date, + const Utf8String &extension) { + return "story_" + + QString::number(index) + + PrepareFileNameDatePart(date) + + extension; +} + } // namespace int PeerColorIndex(BareId bareId) { @@ -295,7 +305,7 @@ void ParseAttributes( } result.width = data.vw().v; result.height = data.vh().v; - result.duration = data.vduration().v; + result.duration = int(data.vduration().v); }, [&](const MTPDdocumentAttributeAudio &data) { if (data.is_voice()) { result.isVoiceMessage = true; @@ -584,6 +594,98 @@ UserpicsSlice ParseUserpicsSlice( return result; } +File &Story::file() { + return media.file(); +} + +const File &Story::file() const { + return media.file(); +} + +Image &Story::thumb() { + return media.thumb(); +} + +const Image &Story::thumb() const { + return media.thumb(); +} + +StoriesSlice ParseStoriesSlice( + const MTPVector &data, + int baseIndex) { + const auto &list = data.v; + auto result = StoriesSlice(); + result.list.reserve(list.size()); + for (const auto &story : list) { + result.lastId = story.match([](const auto &data) { + return data.vid().v; + }); + ++result.skipped; + story.match([&](const MTPDstoryItem &data) { + const auto date = data.vdate().v; + auto media = Media(); + data.vmedia().match([&](const MTPDmessageMediaPhoto &data) { + const auto suggestedPath = "stories/" + + PrepareStoryFileName( + ++baseIndex, + date, + ".jpg"_q); + const auto photo = data.vphoto(); + auto content = photo + ? ParsePhoto(*photo, suggestedPath) + : Photo(); + media.content = content; + }, [&](const MTPDmessageMediaDocument &data) { + const auto document = data.vdocument(); + auto fake = ParseMediaContext(); + auto content = document + ? ParseDocument(fake, *document, "stories", date) + : Document(); + const auto extension = (content.mime == "image/jpeg") + ? ".jpg"_q + : (content.mime == "image/png") + ? ".png"_q + : [&] { + const auto mimeType = Core::MimeTypeForName( + content.mime); + QStringList patterns = mimeType.globPatterns(); + if (!patterns.isEmpty()) { + return patterns.front().replace( + '*', + QString()).toUtf8(); + } + return QByteArray(); + }(); + const auto path = content.file.suggestedPath = "stories/" + + PrepareStoryFileName( + ++baseIndex, + date, + extension); + content.thumb.file.suggestedPath = path + "_thumb.jpg"; + media.content = content; + }, [&](const auto &data) { + media.content = UnsupportedMedia(); + }); + if (!v::is(media.content)) { + result.list.push_back(Story{ + .id = data.vid().v, + .date = date, + .expires = data.vexpire_date().v, + .media = std::move(media), + .pinned = data.is_pinned(), + .caption = (data.vcaption() + ? ParseText( + *data.vcaption(), + data.ventities().value_or_empty()) + : std::vector()), + }); + --result.skipped; + } + }, [](const auto &) {}); + } + return result; +} + std::pair WriteImageThumb( const QString &basePath, const QString &largePath, @@ -954,6 +1056,8 @@ Media ParseMedia( result.content = ParsePoll(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice + }, [](const MTPDmessageMediaStory &data) { + // #TODO stories export }, [](const MTPDmessageMediaEmpty &data) {}); return result; } @@ -1182,6 +1286,12 @@ ServiceAction ParseServiceAction( + "photos/" + PreparePhotoFileName(++context.photos, date)); result.content = content; + }, [&](const MTPDmessageActionSetChatWallPaper &data) { + auto content = ActionSetChatWallPaper(); + // #TODO wallpapers + result.content = content; + }, [&](const MTPDmessageActionSetSameChatWallPaper &data) { + result.content = ActionSetSameChatWallPaper(); }, [&](const MTPDmessageActionRequestedPeer &data) { auto content = ActionRequestedPeer(); content.peerId = ParsePeerId(data.vpeer()); @@ -1198,6 +1308,9 @@ File &Message::file() { } else if (const auto photo = std::get_if( content)) { return photo->photo.image.file; + } else if (const auto wallpaper = std::get_if( + content)) { + // #TODO wallpapers } return media.file(); } @@ -1209,6 +1322,9 @@ const File &Message::file() const { } else if (const auto photo = std::get_if( content)) { return photo->photo.image.file; + } else if (const auto wallpaper = std::get_if( + content)) { + // #TODO wallpapers } return media.file(); } @@ -1248,6 +1364,8 @@ Message ParseMessage( if (result.replyToPeerId == result.peerId) { result.replyToPeerId = 0; } + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } } @@ -1295,6 +1413,8 @@ Message ParseMessage( result.replyToPeerId = data.vreply_to_peer_id() ? ParsePeerId(*data.vreply_to_peer_id()) : PeerId(0); + }, [&](const MTPDmessageReplyStoryHeader &data) { + // #TODO stories export }); } if (const auto viaBotId = data.vvia_bot_id()) { diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 71bae17f8..27fb14ea1 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -48,6 +48,10 @@ struct UserpicsInfo { int count = 0; }; +struct StoriesInfo { + int count = 0; +}; + struct FileLocation { int dcId = 0; MTPInputFileLocation data; @@ -515,6 +519,13 @@ struct ActionSuggestProfilePhoto { Photo photo; }; +struct ActionSetChatWallPaper { + // #TODO wallpapers +}; + +struct ActionSetSameChatWallPaper { +}; + struct ActionRequestedPeer { PeerId peerId = 0; int buttonId = 0; @@ -556,7 +567,9 @@ struct ServiceAction { ActionTopicCreate, ActionTopicEdit, ActionSuggestProfilePhoto, - ActionRequestedPeer> content; + ActionRequestedPeer, + ActionSetChatWallPaper, + ActionSetSameChatWallPaper> content; }; ServiceAction ParseServiceAction( @@ -654,9 +667,34 @@ struct FileOrigin { int split = 0; MTPInputPeer peer; int32 messageId = 0; + int32 storyId = 0; uint64 customEmojiId = 0; }; +struct Story { + int32 id = 0; + TimeId date = 0; + TimeId expires = 0; + Media media; + bool pinned = false; + std::vector caption; + + File &file(); + const File &file() const; + Image &thumb(); + const Image &thumb() const; +}; + +struct StoriesSlice { + std::vector list; + int32 lastId = 0; + int skipped = 0; +}; + +StoriesSlice ParseStoriesSlice( + const MTPVector &data, + int baseIndex); + Message ParseMessage( ParseMediaContext &context, const MTPMessage &data, diff --git a/Telegram/SourceFiles/export/export_api_wrap.cpp b/Telegram/SourceFiles/export/export_api_wrap.cpp index 3e31914d4..1ff8d6774 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.cpp +++ b/Telegram/SourceFiles/export/export_api_wrap.cpp @@ -30,6 +30,7 @@ constexpr auto kTopPeerSliceLimit = 100; constexpr auto kFileMaxSize = 4000 * int64(1024 * 1024); constexpr auto kLocationCacheSize = 100'000; constexpr auto kMaxEmojiPerRequest = 100; +constexpr auto kStoriesSliceLimit = 100; struct LocationKey { uint64 type; @@ -109,6 +110,7 @@ struct ApiWrap::StartProcess { enum class Step { UserpicsCount, + StoriesCount, SplitRanges, DialogsCount, LeftChannelsCount, @@ -139,6 +141,19 @@ struct ApiWrap::UserpicsProcess { int fileIndex = 0; }; +struct ApiWrap::StoriesProcess { + FnMut start; + Fn fileProgress; + Fn handleSlice; + FnMut finish; + + int processed = 0; + std::optional slice; + int offsetId = 0; + bool lastSlice = false; + int fileIndex = 0; +}; + struct ApiWrap::OtherDataProcess { Data::File file; FnMut done; @@ -417,6 +432,9 @@ void ApiWrap::startExport( if (_settings->types & Settings::Type::Userpics) { _startProcess->steps.push_back(Step::UserpicsCount); } + if (_settings->types & Settings::Type::Stories) { + _startProcess->steps.push_back(Step::StoriesCount); + } if (_settings->types & Settings::Type::AnyChatsMask) { _startProcess->steps.push_back(Step::SplitRanges); } @@ -447,6 +465,8 @@ void ApiWrap::sendNextStartRequest() { switch (step) { case Step::UserpicsCount: return requestUserpicsCount(); + case Step::StoriesCount: + return requestStoriesCount(); case Step::SplitRanges: return requestSplitRanges(); case Step::DialogsCount: @@ -480,6 +500,22 @@ void ApiWrap::requestUserpicsCount() { }).send(); } +void ApiWrap::requestStoriesCount() { + Expects(_startProcess != nullptr); + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(0), // offset_id + MTP_int(0) // limit + )).done([=](const MTPstories_Stories &result) { + Expects(_settings != nullptr); + Expects(_startProcess != nullptr); + + _startProcess->info.storiesCount = result.data().vcount().v; + + sendNextStartRequest(); + }).send(); +} + void ApiWrap::requestSplitRanges() { Expects(_startProcess != nullptr); @@ -616,7 +652,8 @@ void ApiWrap::startMainSession(FnMut done) { using Type = Settings::Type; const auto sizeLimit = _settings->media.sizeLimit; const auto hasFiles = ((_settings->media.types != 0) && (sizeLimit > 0)) - || (_settings->types & Type::Userpics); + || (_settings->types & Type::Userpics) + || (_settings->types & Type::Stories); using Flag = MTPaccount_InitTakeoutSession::Flag; const auto flags = Flag(0) @@ -856,6 +893,171 @@ void ApiWrap::finishUserpics() { base::take(_userpicsProcess)->finish(); } +void ApiWrap::requestStories( + FnMut start, + Fn progress, + Fn slice, + FnMut finish) { + Expects(_storiesProcess == nullptr); + + _storiesProcess = std::make_unique(); + _storiesProcess->start = std::move(start); + _storiesProcess->fileProgress = std::move(progress); + _storiesProcess->handleSlice = std::move(slice); + _storiesProcess->finish = std::move(finish); + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(_storiesProcess->offsetId), + MTP_int(kStoriesSliceLimit) + )).done([=](const MTPstories_Stories &result) mutable { + Expects(_storiesProcess != nullptr); + + auto startInfo = Data::StoriesInfo{ result.data().vcount().v }; + if (!_storiesProcess->start(std::move(startInfo))) { + return; + } + + handleStoriesSlice(result); + }).send(); +} + +void ApiWrap::handleStoriesSlice(const MTPstories_Stories &result) { + Expects(_storiesProcess != nullptr); + + loadStoriesFiles(Data::ParseStoriesSlice( + result.data().vstories(), + _storiesProcess->processed)); +} + +void ApiWrap::loadStoriesFiles(Data::StoriesSlice &&slice) { + Expects(_storiesProcess != nullptr); + Expects(!_storiesProcess->slice.has_value()); + + if (!slice.lastId) { + _storiesProcess->lastSlice = true; + } + _storiesProcess->slice = std::move(slice); + _storiesProcess->fileIndex = 0; + loadNextStory(); +} + +void ApiWrap::loadNextStory() { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + + for (auto &list = _storiesProcess->slice->list + ; _storiesProcess->fileIndex < list.size() + ; ++_storiesProcess->fileIndex) { + auto &story = list[_storiesProcess->fileIndex]; + const auto origin = Data::FileOrigin{ .storyId = story.id }; + const auto ready = processFileLoad( + story.file(), + origin, + [=](FileProgress value) { return loadStoryProgress(value); }, + [=](const QString &path) { loadStoryDone(path); }); + if (!ready) { + return; + } + const auto thumbProgress = [=](FileProgress value) { + return loadStoryThumbProgress(value); + }; + const auto thumbReady = processFileLoad( + story.thumb().file, + origin, + thumbProgress, + [=](const QString &path) { loadStoryThumbDone(path); }, + nullptr, + &story); + if (!thumbReady) { + return; + } + } + finishStoriesSlice(); +} + +void ApiWrap::finishStoriesSlice() { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + + auto slice = *base::take(_storiesProcess->slice); + if (slice.lastId) { + _storiesProcess->processed += slice.list.size(); + _storiesProcess->offsetId = slice.lastId; + if (!_storiesProcess->handleSlice(std::move(slice))) { + return; + } + } + if (_storiesProcess->lastSlice) { + finishStories(); + return; + } + + mainRequest(MTPstories_GetStoriesArchive( + MTP_int(_storiesProcess->offsetId), + MTP_int(kStoriesSliceLimit) + )).done([=](const MTPstories_Stories &result) { + handleStoriesSlice(result); + }).send(); +} + +bool ApiWrap::loadStoryProgress(FileProgress progress) { + Expects(_fileProcess != nullptr); + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + return _storiesProcess->fileProgress(DownloadProgress{ + _fileProcess->randomId, + _fileProcess->relativePath, + _storiesProcess->fileIndex, + progress.ready, + progress.total }); +} + +void ApiWrap::loadStoryDone(const QString &relativePath) { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + const auto index = _storiesProcess->fileIndex; + auto &file = _storiesProcess->slice->list[index].file(); + file.relativePath = relativePath; + if (relativePath.isEmpty()) { + file.skipReason = Data::File::SkipReason::Unavailable; + } + loadNextStory(); +} + +bool ApiWrap::loadStoryThumbProgress(FileProgress progress) { + return loadStoryProgress(progress); +} + +void ApiWrap::loadStoryThumbDone(const QString &relativePath) { + Expects(_storiesProcess != nullptr); + Expects(_storiesProcess->slice.has_value()); + Expects((_storiesProcess->fileIndex >= 0) + && (_storiesProcess->fileIndex + < _storiesProcess->slice->list.size())); + + const auto index = _storiesProcess->fileIndex; + auto &file = _storiesProcess->slice->list[index].thumb().file; + file.relativePath = relativePath; + if (relativePath.isEmpty()) { + file.skipReason = Data::File::SkipReason::Unavailable; + } + loadNextStory(); +} + +void ApiWrap::finishStories() { + Expects(_storiesProcess != nullptr); + + base::take(_storiesProcess)->finish(); +} + void ApiWrap::requestContacts(FnMut done) { Expects(_contactsProcess == nullptr); @@ -1753,7 +1955,8 @@ bool ApiWrap::processFileLoad( const Data::FileOrigin &origin, Fn progress, FnMut done, - Data::Message *message) { + Data::Message *message, + Data::Story *story) { using SkipReason = Data::File::SkipReason; if (!file.relativePath.isEmpty() @@ -1767,7 +1970,12 @@ bool ApiWrap::processFileLoad( } using Type = MediaSettings::Type; - const auto type = message ? v::match(message->media.content, [&]( + const auto media = message + ? &message->media + : story + ? &story->media + : nullptr; + const auto type = media ? v::match(media->content, [&]( const Data::Document &data) { if (data.isSticker) { return Type::Sticker; @@ -1786,14 +1994,18 @@ bool ApiWrap::processFileLoad( return Type::Photo; }) : Type(0); - const auto limit = _settings->media.sizeLimit; + const auto fullSize = message + ? message->file().size + : story + ? story->file().size + : file.size; if (message && Data::SkipMessageByDate(*message, *_settings)) { file.skipReason = SkipReason::DateLimits; return true; - } else if ((_settings->media.types & type) != type) { + } else if (!story && (_settings->media.types & type) != type) { file.skipReason = SkipReason::FileType; return true; - } else if ((message ? message->file().size : file.size) >= limit) { + } else if (!story && fullSize >= _settings->media.sizeLimit) { // Don't load thumbs for large files that we skip. file.skipReason = SkipReason::FileSize; return true; @@ -1972,7 +2184,20 @@ void ApiWrap::filePartRefreshReference(int64 offset) { Expects(_fileProcess->requestId == 0); const auto &origin = _fileProcess->origin; - if (!origin.messageId) { + if (origin.storyId) { + _fileProcess->requestId = mainRequest(MTPstories_GetStoriesByID( + MTP_inputUserSelf(), + MTP_vector(1, MTP_int(origin.storyId)) + )).fail([=](const MTP::Error &error) { + _fileProcess->requestId = 0; + filePartUnavailable(); + return true; + }).done([=](const MTPstories_Stories &result) { + _fileProcess->requestId = 0; + filePartExtractReference(offset, result); + }).send(); + return; + } else if (!origin.messageId) { error("FILE_REFERENCE error for non-message file."); return; } @@ -2061,6 +2286,38 @@ void ApiWrap::filePartExtractReference( }); } +void ApiWrap::filePartExtractReference( + int64 offset, + const MTPstories_Stories &result) { + Expects(_fileProcess != nullptr); + Expects(_fileProcess->requestId == 0); + + const auto stories = Data::ParseStoriesSlice( + result.data().vstories(), + 0); + for (const auto &story : stories.list) { + if (story.id == _fileProcess->origin.storyId) { + const auto refresh1 = Data::RefreshFileReference( + _fileProcess->location, + story.file().location); + const auto refresh2 = Data::RefreshFileReference( + _fileProcess->location, + story.thumb().file.location); + if (refresh1 || refresh2) { + _fileProcess->requestId = fileRequest( + _fileProcess->location, + offset + ).done([=](const MTPupload_File &result) { + _fileProcess->requestId = 0; + filePartDone(offset, result); + }).send(); + return; + } + } + } + filePartUnavailable(); +} + void ApiWrap::filePartUnavailable() { Expects(_fileProcess != nullptr); Expects(!_fileProcess->requests.empty()); diff --git a/Telegram/SourceFiles/export/export_api_wrap.h b/Telegram/SourceFiles/export/export_api_wrap.h index 6384457d7..4723cd882 100644 --- a/Telegram/SourceFiles/export/export_api_wrap.h +++ b/Telegram/SourceFiles/export/export_api_wrap.h @@ -19,12 +19,15 @@ struct FileLocation; struct PersonalInfo; struct UserpicsInfo; struct UserpicsSlice; +struct StoriesInfo; +struct StoriesSlice; struct ContactsList; struct SessionsList; struct DialogsInfo; struct DialogInfo; struct MessagesSlice; struct Message; +struct Story; struct FileOrigin; } // namespace Data @@ -44,6 +47,7 @@ public: struct StartInfo { int userpicsCount = 0; + int storiesCount = 0; int dialogsCount = 0; }; void startExport( @@ -74,6 +78,12 @@ public: Fn slice, FnMut finish); + void requestStories( + FnMut start, + Fn progress, + Fn slice, + FnMut finish); + void requestContacts(FnMut done); void requestSessions(FnMut done); @@ -96,6 +106,7 @@ private: struct StartProcess; struct ContactsProcess; struct UserpicsProcess; + struct StoriesProcess; struct OtherDataProcess; struct FileProcess; struct FileProgress; @@ -107,6 +118,7 @@ private: void startMainSession(FnMut done); void sendNextStartRequest(); void requestUserpicsCount(); + void requestStoriesCount(); void requestSplitRanges(); void requestDialogsCount(); void requestLeftChannelsCount(); @@ -122,6 +134,16 @@ private: void finishUserpicsSlice(); void finishUserpics(); + void handleStoriesSlice(const MTPstories_Stories &result); + void loadStoriesFiles(Data::StoriesSlice &&slice); + void loadNextStory(); + bool loadStoryProgress(FileProgress value); + void loadStoryDone(const QString &relativePath); + bool loadStoryThumbProgress(FileProgress value); + void loadStoryThumbDone(const QString &relativePath); + void finishStoriesSlice(); + void finishStories(); + void otherDataDone(const QString &relativePath); bool useOnlyLastSplit() const; @@ -179,7 +201,8 @@ private: const Data::FileOrigin &origin, Fn progress, FnMut done, - Data::Message *message = nullptr); + Data::Message *message = nullptr, + Data::Story *story = nullptr); std::unique_ptr prepareFileProcess( const Data::File &file, const Data::FileOrigin &origin) const; @@ -198,6 +221,9 @@ private: void filePartExtractReference( int64 offset, const MTPmessages_Messages &result); + void filePartExtractReference( + int64 offset, + const MTPstories_Stories &result); template class RequestBuilder; @@ -228,6 +254,7 @@ private: std::unique_ptr _fileCache; std::unique_ptr _contactsProcess; std::unique_ptr _userpicsProcess; + std::unique_ptr _storiesProcess; std::unique_ptr _otherDataProcess; std::unique_ptr _fileProcess; std::unique_ptr _leftChannelsProcess; diff --git a/Telegram/SourceFiles/export/export_controller.cpp b/Telegram/SourceFiles/export/export_controller.cpp index 516c75cf7..b3d543f4c 100644 --- a/Telegram/SourceFiles/export/export_controller.cpp +++ b/Telegram/SourceFiles/export/export_controller.cpp @@ -75,6 +75,7 @@ private: void collectDialogsList(); void exportPersonalInfo(); void exportUserpics(); + void exportStories(); void exportContacts(); void exportSessions(); void exportOtherData(); @@ -89,6 +90,7 @@ private: ProcessingState stateDialogsList(int processed) const; ProcessingState statePersonalInfo() const; ProcessingState stateUserpics(const DownloadProgress &progress) const; + ProcessingState stateStories(const DownloadProgress &progress) const; ProcessingState stateContacts() const; ProcessingState stateSessions() const; ProcessingState stateOtherData() const; @@ -114,6 +116,9 @@ private: int _userpicsWritten = 0; int _userpicsCount = 0; + int _storiesWritten = 0; + int _storiesCount = 0; + // rpl::variable fails to compile in MSVC :( State _state; rpl::event_stream _stateChanges; @@ -273,6 +278,9 @@ void ControllerObject::fillExportSteps() { if (_settings.types & Type::Userpics) { _steps.push_back(Step::Userpics); } + if (_settings.types & Type::Stories) { + _steps.push_back(Step::Stories); + } if (_settings.types & Type::Contacts) { _steps.push_back(Step::Contacts); } @@ -306,6 +314,9 @@ void ControllerObject::fillSubstepsInSteps(const ApiWrap::StartInfo &info) { if (_settings.types & Settings::Type::Userpics) { push(Step::Userpics, 1); } + if (_settings.types & Settings::Type::Stories) { + push(Step::Stories, 1); + } if (_settings.types & Settings::Type::Contacts) { push(Step::Contacts, 1); } @@ -344,6 +355,7 @@ void ControllerObject::exportNext() { case Step::DialogsList: return collectDialogsList(); case Step::PersonalInfo: return exportPersonalInfo(); case Step::Userpics: return exportUserpics(); + case Step::Stories: return exportStories(); case Step::Contacts: return exportContacts(); case Step::Sessions: return exportSessions(); case Step::OtherData: return exportOtherData(); @@ -416,6 +428,32 @@ void ControllerObject::exportUserpics() { }); } +void ControllerObject::exportStories() { + _api.requestStories([=](Data::StoriesInfo &&start) { + if (ioCatchError(_writer->writeStoriesStart(start))) { + return false; + } + _storiesWritten = 0; + _storiesCount = start.count; + return true; + }, [=](DownloadProgress progress) { + setState(stateStories(progress)); + return true; + }, [=](Data::StoriesSlice &&slice) { + if (ioCatchError(_writer->writeStoriesSlice(slice))) { + return false; + } + _storiesWritten += slice.list.size(); + setState(stateStories(DownloadProgress())); + return true; + }, [=] { + if (ioCatchError(_writer->writeStoriesEnd())) { + return; + } + exportNext(); + }); +} + void ControllerObject::exportContacts() { setState(stateContacts()); _api.requestContacts([=](Data::ContactsList &&result) { @@ -533,7 +571,21 @@ ProcessingState ControllerObject::stateUserpics( return prepareState(Step::Userpics, [&](ProcessingState &result) { result.entityIndex = _userpicsWritten + progress.itemIndex; result.entityCount = std::max(_userpicsCount, result.entityIndex); - result.bytesType = ProcessingState::FileType::Photo; + result.bytesRandomId = progress.randomId; + if (!progress.path.isEmpty()) { + const auto last = progress.path.lastIndexOf('/'); + result.bytesName = progress.path.mid(last + 1); + } + result.bytesLoaded = progress.ready; + result.bytesCount = progress.total; + }); +} + +ProcessingState ControllerObject::stateStories( + const DownloadProgress &progress) const { + return prepareState(Step::Stories, [&](ProcessingState &result) { + result.entityIndex = _storiesWritten + progress.itemIndex; + result.entityCount = std::max(_storiesCount, result.entityIndex); result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); @@ -586,7 +638,6 @@ void ControllerObject::fillMessagesState( : ProcessingState::EntityType::Chat; result.itemIndex = _messagesWritten + progress.itemIndex; result.itemCount = std::max(_messagesCount, result.itemIndex); - result.bytesType = ProcessingState::FileType::File; // TODO result.bytesRandomId = progress.randomId; if (!progress.path.isEmpty()) { const auto last = progress.path.lastIndexOf('/'); diff --git a/Telegram/SourceFiles/export/export_controller.h b/Telegram/SourceFiles/export/export_controller.h index e9b08acdc..fae4b54a1 100644 --- a/Telegram/SourceFiles/export/export_controller.h +++ b/Telegram/SourceFiles/export/export_controller.h @@ -38,21 +38,12 @@ struct ProcessingState { DialogsList, PersonalInfo, Userpics, + Stories, Contacts, Sessions, OtherData, Dialogs, }; - enum class FileType { - None, - Photo, - Video, - VoiceMessage, - VideoMessage, - Sticker, - GIF, - File, - }; enum class EntityType { Chat, SavedMessages, @@ -75,7 +66,6 @@ struct ProcessingState { int itemCount = 0; uint64 bytesRandomId = 0; - FileType bytesType = FileType::None; QString bytesName; int64 bytesLoaded = 0; int64 bytesCount = 0; diff --git a/Telegram/SourceFiles/export/export_settings.h b/Telegram/SourceFiles/export/export_settings.h index beac47760..be2e1b782 100644 --- a/Telegram/SourceFiles/export/export_settings.h +++ b/Telegram/SourceFiles/export/export_settings.h @@ -57,13 +57,18 @@ struct Settings { PublicGroups = 0x100, PrivateChannels = 0x200, PublicChannels = 0x400, + Stories = 0x800, GroupsMask = PrivateGroups | PublicGroups, ChannelsMask = PrivateChannels | PublicChannels, GroupsChannelsMask = GroupsMask | ChannelsMask, NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups, AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask, - NonChatsMask = PersonalInfo | Userpics | Contacts | Sessions, + NonChatsMask = (PersonalInfo + | Userpics + | Contacts + | Stories + | Sessions), AllMask = NonChatsMask | OtherData | AnyChatsMask, }; using Types = base::flags; @@ -91,6 +96,7 @@ struct Settings { return Type::PersonalInfo | Type::Userpics | Type::Contacts + | Type::Stories | Type::PersonalChats | Type::PrivateGroups; } diff --git a/Telegram/SourceFiles/export/output/export_output_abstract.h b/Telegram/SourceFiles/export/output/export_output_abstract.h index ae24354f7..13afc29d6 100644 --- a/Telegram/SourceFiles/export/output/export_output_abstract.h +++ b/Telegram/SourceFiles/export/output/export_output_abstract.h @@ -14,6 +14,8 @@ namespace Data { struct PersonalInfo; struct UserpicsInfo; struct UserpicsSlice; +struct StoriesInfo; +struct StoriesSlice; struct ContactsList; struct SessionsList; struct DialogsInfo; @@ -56,6 +58,12 @@ public: const Data::UserpicsSlice &data) = 0; [[nodiscard]] virtual Result writeUserpicsEnd() = 0; + [[nodiscard]] virtual Result writeStoriesStart( + const Data::StoriesInfo &data) = 0; + [[nodiscard]] virtual Result writeStoriesSlice( + const Data::StoriesSlice &data) = 0; + [[nodiscard]] virtual Result writeStoriesEnd() = 0; + [[nodiscard]] virtual Result writeContactsList( const Data::ContactsList &data) = 0; diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 59c176bae..efa4a7599 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -35,11 +35,23 @@ constexpr auto kStickerMaxWidth = 384; constexpr auto kStickerMaxHeight = 384; constexpr auto kStickerMinWidth = 80; constexpr auto kStickerMinHeight = 80; +constexpr auto kStoryThumbWidth = 45; +constexpr auto kStoryThumbHeight = 80; + +constexpr auto kChatsPriority = 0; +constexpr auto kContactsPriority = 2; +constexpr auto kFrequentContactsPriority = 3; +constexpr auto kUserpicsPriority = 4; +constexpr auto kStoriesPriority = 5; +constexpr auto kSessionsPriority = 6; +constexpr auto kWebSessionsPriority = 7; +constexpr auto kOtherPriority = 8; const auto kLineBreak = QByteArrayLiteral("
"); using Context = details::HtmlContext; using UserpicData = details::UserpicData; +using StoryData = details::StoryData; using PeersMap = details::PeersMap; using MediaData = details::MediaData; @@ -347,6 +359,11 @@ struct UserpicData { QByteArray lastName; }; +struct StoryData { + QString imageLink; + QString largeLink; +}; + class PeersMap { public: using Peer = Data::Peer; @@ -503,6 +520,14 @@ public: const QByteArray &details, const QByteArray &info, const QString &link = QString()); + [[nodiscard]] QByteArray pushStoriesListEntry( + const StoryData &story, + const QByteArray &name, + const QByteArrayList &details, + const QByteArray &info, + const std::vector &caption, + const QString &internalLinksDomain, + const QString &link = QString()); [[nodiscard]] QByteArray pushSessionListEntry( int apiId, const QByteArray &name, @@ -750,6 +775,75 @@ QByteArray HtmlWriter::Wrap::pushListEntry( info); } +QByteArray HtmlWriter::Wrap::pushStoriesListEntry( + const StoryData &story, + const QByteArray &name, + const QByteArrayList &details, + const QByteArray &info, + const std::vector &caption, + const QString &internalLinksDomain, + const QString &link) { + auto result = pushDiv("entry clearfix"); + if (!link.isEmpty()) { + result.append(pushTag("a", { + { "class", "pull_left userpic_wrap" }, + { "href", relativePath(link).toUtf8() + "#allow_back" }, + })); + } else { + result.append(pushDiv("pull_left userpic_wrap")); + } + if (!story.imageLink.isEmpty()) { + const auto sizeStyle = "width: " + + Data::NumberToString(kStoryThumbWidth) + + "px; height: " + + Data::NumberToString(kStoryThumbHeight) + + "px"; + result.append(pushTag("img", { + { "class", "story" }, + { "style", sizeStyle }, + { "src", relativePath(story.imageLink).toUtf8() }, + { "empty", "" } + })); + } + result.append(popTag()); + result.append(pushDiv("body")); + if (!info.isEmpty()) { + result.append(pushDiv("pull_right info details")); + result.append(SerializeString(info)); + result.append(popTag()); + } + if (!name.isEmpty()) { + if (!link.isEmpty()) { + result.append(pushTag("a", { + { "class", "block_link expanded" }, + { "href", relativePath(link).toUtf8() + "#allow_back" }, + })); + } + result.append(pushDiv("name bold")); + result.append(SerializeString(name)); + result.append(popTag()); + if (!link.isEmpty()) { + result.append(popTag()); + } + } + const auto text = caption.empty() + ? QByteArray() + : FormatText(caption, internalLinksDomain, _base); + if (!text.isEmpty()) { + result.append(pushDiv("text")); + result.append(text); + result.append(popTag()); + } + for (const auto &detail : details) { + result.append(pushDiv("details_entry details")); + result.append(SerializeString(detail)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + return result; +} + QByteArray HtmlWriter::Wrap::pushSessionListEntry( int apiId, const QByteArray &name, @@ -1173,6 +1267,13 @@ auto HtmlWriter::Wrap::pushMessage( return serviceFrom + " suggests to use this photo"; }, [&](const ActionRequestedPeer &data) { return "requested: "_q/* + data.peerId*/; + }, [&](const ActionSetChatWallPaper &data) { + return serviceFrom + " set a new background for this chat"; + }, [&](const ActionSetSameChatWallPaper &data) { + return serviceFrom + + " set " + + wrapReplyToLink("the same background") + + " for this chat"; }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -1973,6 +2074,7 @@ Result HtmlWriter::start( "images/section_other.png", "images/section_photos.png", "images/section_sessions.png", + "images/section_stories.png", "images/section_web.png", "js/script.js", }; @@ -2169,13 +2271,114 @@ QString HtmlWriter::userpicsFilePath() const { void HtmlWriter::pushUserpicsSection() { pushSection( - 4, + kUserpicsPriority, "Profile pictures", "photos", _userpicsCount, userpicsFilePath()); } +Result HtmlWriter::writeStoriesStart(const Data::StoriesInfo &data) { + Expects(_summary != nullptr); + Expects(_stories == nullptr); + + _storiesCount = data.count; + if (!_storiesCount) { + return Result::Success(); + } + _stories = fileWithRelativePath(storiesFilePath()); + + auto block = _stories->pushHeader( + "Stories archive", + mainFileRelativePath()); + block.append(_stories->pushDiv("page_body list_page")); + block.append(_stories->pushDiv("entry_list")); + if (const auto result = _stories->writeBlock(block); !result) { + return result; + } + return Result::Success(); +} + +Result HtmlWriter::writeStoriesSlice(const Data::StoriesSlice &data) { + Expects(_stories != nullptr); + + _storiesCount -= data.skipped; + if (data.list.empty()) { + return Result::Success(); + } + auto block = QByteArray(); + for (const auto &story : data.list) { + auto data = StoryData{}; + using SkipReason = Data::File::SkipReason; + const auto &file = story.file(); + Assert(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + auto status = QByteArrayList(); + if (story.pinned) { + status.append("Saved to Profile"); + } + if (story.expires > 0) { + status.append("Expiring: " + Data::FormatDateTime(story.expires)); + } + status.append([&]() -> Data::Utf8String { + switch (file.skipReason) { + case SkipReason::Unavailable: + return "(Story unavailable, please try again later)"; + case SkipReason::FileSize: + return "(Story exceeds maximum size. " + "Change data exporting settings to download.)"; + case SkipReason::FileType: + return "(Story not included. " + "Change data exporting settings to download.)"; + case SkipReason::None: return Data::FormatFileSize(file.size); + } + Unexpected("Skip reason while writing story path."); + }()); + const auto &path = story.file().relativePath; + const auto &image = story.thumb().file.relativePath.isEmpty() + ? story.file().relativePath + : story.thumb().file.relativePath; + data.imageLink = Data::WriteImageThumb( + _settings.path, + image, + kStoryThumbWidth * 2, + kStoryThumbHeight * 2); + const auto info = (story.date > 0) + ? Data::FormatDateTime(story.date) + : QByteArray(); + block.append(_stories->pushStoriesListEntry( + data, + (path.isEmpty() ? QString("Story unavailable") : path).toUtf8(), + status, + info, + story.caption, + _environment.internalLinksDomain, + path)); + } + return _stories->writeBlock(block); +} + +Result HtmlWriter::writeStoriesEnd() { + pushStoriesSection(); + if (_stories) { + return base::take(_stories)->close(); + } + return Result::Success(); +} + +QString HtmlWriter::storiesFilePath() const { + return "lists/stories.html"; +} + +void HtmlWriter::pushStoriesSection() { + pushSection( + kStoriesPriority, + "Stories archive", + "stories", + _storiesCount, + storiesFilePath()); +} + Result HtmlWriter::writeContactsList(const Data::ContactsList &data) { Expects(_summary != nullptr); @@ -2221,7 +2424,7 @@ Result HtmlWriter::writeSavedContacts(const Data::ContactsList &data) { } pushSection( - 2, + kContactsPriority, "Contacts", "contacts", data.list.size(), @@ -2287,7 +2490,7 @@ Result HtmlWriter::writeFrequentContacts(const Data::ContactsList &data) { } pushSection( - 3, + kFrequentContactsPriority, "Frequent contacts", "frequent", size, @@ -2353,7 +2556,7 @@ Result HtmlWriter::writeSessions(const Data::SessionsList &data) { } pushSection( - 5, + kSessionsPriority, "Sessions", "sessions", data.list.size(), @@ -2399,7 +2602,7 @@ Result HtmlWriter::writeWebSessions(const Data::SessionsList &data) { } pushSection( - 6, + kWebSessionsPriority, "Web sessions", "web", data.webList.size(), @@ -2411,7 +2614,7 @@ Result HtmlWriter::writeOtherData(const Data::File &data) { Expects(_summary != nullptr); pushSection( - 7, + kOtherPriority, "Other data", "other", 1, @@ -2440,7 +2643,7 @@ Result HtmlWriter::writeDialogsStart(const Data::DialogsInfo &data) { } pushSection( - 0, + kChatsPriority, "Chats", "chats", data.chats.size() + data.left.size(), diff --git a/Telegram/SourceFiles/export/output/export_output_html.h b/Telegram/SourceFiles/export/output/export_output_html.h index a2235d130..c1dee2ffd 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.h +++ b/Telegram/SourceFiles/export/output/export_output_html.h @@ -35,6 +35,7 @@ private: }; struct UserpicData; +struct StoryData; class PeersMap; struct MediaData; @@ -59,6 +60,10 @@ public: Result writeUserpicsSlice(const Data::UserpicsSlice &data) override; Result writeUserpicsEnd() override; + Result writeStoriesStart(const Data::StoriesInfo &data) override; + Result writeStoriesSlice(const Data::StoriesSlice &data) override; + Result writeStoriesEnd() override; + Result writeContactsList(const Data::ContactsList &data) override; Result writeSessionsList(const Data::SessionsList &data) override; @@ -125,8 +130,10 @@ private: const Data::PersonalInfo &data, const QString &userpicPath); void pushUserpicsSection(); + void pushStoriesSection(); [[nodiscard]] QString userpicsFilePath() const; + [[nodiscard]] QString storiesFilePath() const; [[nodiscard]] QByteArray wrapMessageLink( int messageId, @@ -149,6 +156,9 @@ private: int _userpicsCount = 0; std::unique_ptr _userpics; + int _storiesCount = 0; + std::unique_ptr _stories; + QString _dialogsRelativePath; Data::DialogInfo _dialog; DialogsMode _dialogsMode = DialogsMode::None; diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index e24a364ba..9dfd21620 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -590,6 +590,13 @@ QByteArray SerializeMessage( pushAction("requested_peer"); push("button_id", data.buttonId); push("peer_id", data.peerId.value); + }, [&](const ActionSetChatWallPaper &data) { + pushActor(); + pushAction("set_chat_wallpaper"); + }, [&](const ActionSetSameChatWallPaper &data) { + pushActor(); + pushAction("set_same_chat_wallpaper"); + pushReplyToMsgId("message_id"); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { @@ -880,6 +887,77 @@ Result JsonWriter::writeUserpicsEnd() { return _output->writeBlock(popNesting()); } +Result JsonWriter::writeStoriesStart(const Data::StoriesInfo &data) { + Expects(_output != nullptr); + + auto block = prepareObjectItemStart("stories"); + return _output->writeBlock(block + pushNesting(Context::kArray)); +} + +Result JsonWriter::writeStoriesSlice(const Data::StoriesSlice &data) { + Expects(_output != nullptr); + + if (data.list.empty()) { + return Result::Success(); + } + + auto block = QByteArray(); + for (const auto &story : data.list) { + using SkipReason = Data::File::SkipReason; + const auto &file = story.file(); + Assert(!file.relativePath.isEmpty() + || file.skipReason != SkipReason::None); + const auto path = [&]() -> Data::Utf8String { + switch (file.skipReason) { + case SkipReason::Unavailable: + return "(Photo unavailable, please try again later)"; + case SkipReason::FileSize: + return "(Photo exceeds maximum size. " + "Change data exporting settings to download.)"; + case SkipReason::FileType: + return "(Photo not included. " + "Change data exporting settings to download.)"; + case SkipReason::None: return FormatFilePath(file); + } + Unexpected("Skip reason while writing story path."); + }(); + block.append(prepareArrayItemStart()); + block.append(SerializeObject(_context, { + { + "date", + story.date ? SerializeDate(story.date) : QByteArray() + }, + { + "date_unixtime", + story.date ? SerializeDateRaw(story.date) : QByteArray() + }, + { + "expires", + story.expires ? SerializeDate(story.expires) : QByteArray() + }, + { + "expires_unixtime", + story.expires ? SerializeDateRaw(story.expires) : QByteArray() + }, + { + "pinned", + story.pinned ? "true" : "false" + }, + { + "media", + SerializeString(path) + }, + })); + } + return _output->writeBlock(block); +} + +Result JsonWriter::writeStoriesEnd() { + Expects(_output != nullptr); + + return _output->writeBlock(popNesting()); +} + Result JsonWriter::writeContactsList(const Data::ContactsList &data) { Expects(_output != nullptr); diff --git a/Telegram/SourceFiles/export/output/export_output_json.h b/Telegram/SourceFiles/export/output/export_output_json.h index 49f7035b0..879918fce 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.h +++ b/Telegram/SourceFiles/export/output/export_output_json.h @@ -44,6 +44,10 @@ public: Result writeUserpicsSlice(const Data::UserpicsSlice &data) override; Result writeUserpicsEnd() override; + Result writeStoriesStart(const Data::StoriesInfo &data) override; + Result writeStoriesSlice(const Data::StoriesSlice &data) override; + Result writeStoriesEnd() override; + Result writeContactsList(const Data::ContactsList &data) override; Result writeSessionsList(const Data::SessionsList &data) override; diff --git a/Telegram/SourceFiles/export/view/export_view_content.cpp b/Telegram/SourceFiles/export/view/export_view_content.cpp index fac8b3d76..9813321ef 100644 --- a/Telegram/SourceFiles/export/view/export_view_content.cpp +++ b/Telegram/SourceFiles/export/view/export_view_content.cpp @@ -89,6 +89,13 @@ Content ContentFromState( case Step::Contacts: pushMain(tr::lng_export_option_contacts(tr::now)); break; + case Step::Stories: + pushMain(tr::lng_export_option_stories(tr::now)); + pushBytes( + "story" + QString::number(state.entityIndex), + state.bytesName, + state.bytesRandomId); + break; case Step::Sessions: pushMain(tr::lng_export_option_sessions(tr::now)); break; diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index 24293ca84..4aef9c36e 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -156,7 +156,7 @@ PanelController::~PanelController() { saveSettings(); } if (_panel) { - _panel->destroyLayer(); + _panel->hideLayer(anim::type::instant); } } diff --git a/Telegram/SourceFiles/export/view/export_view_settings.cpp b/Telegram/SourceFiles/export/view/export_view_settings.cpp index 3e4c0ce2c..d7f4e5e90 100644 --- a/Telegram/SourceFiles/export/view/export_view_settings.cpp +++ b/Telegram/SourceFiles/export/view/export_view_settings.cpp @@ -174,6 +174,11 @@ void SettingsWidget::setupFullExportOptions( tr::lng_export_option_contacts(tr::now), Type::Contacts, tr::lng_export_option_contacts_about(tr::now)); + addOptionWithAbout( + container, + tr::lng_export_option_stories(tr::now), + Type::Stories, + tr::lng_export_option_stories_about(tr::now)); addHeader(container, tr::lng_export_header_chats(tr::now)); addOption( container, diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index a674a4b82..e105cd441 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -16,8 +16,13 @@ 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 +#include +#endif // !DESKTOP_APP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC } // extern "C" namespace FFmpeg { @@ -85,6 +90,47 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { #endif // LIB_FFMPEG_USE_QT_PRIVATE_API } +#if !defined DESKTOP_APP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +[[nodiscard]] auto CheckHwLibs() { + auto list = std::deque{ + AV_PIX_FMT_CUDA, + }; + const auto vdpau = [&] { + if (const auto handle = dlopen("libvdpau.so.1", RTLD_LAZY)) { + dlclose(handle); + } + if (dlerror()) { + return false; + } + return true; + }(); + if (vdpau) { + list.push_front(AV_PIX_FMT_VDPAU); + } + const auto va = [&] { + const auto list = std::array{ + "libva-drm.so.1", + "libva-x11.so.1", + "libva.so.1", + "libdrm.so.2", + }; + for (const auto lib : list) { + if (const auto handle = dlopen(lib, RTLD_LAZY)) { + dlclose(handle); + } + if (dlerror()) { + return false; + } + } + return true; + }(); + if (va) { + list.push_front(AV_PIX_FMT_VAAPI); + } + return list; +} +#endif // !DESKTOP_APP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC + [[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) { AVCodecContext *parent = static_cast(context->opaque); @@ -125,6 +171,9 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return false; }; +#if !defined DESKTOP_APP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC + static const auto list = CheckHwLibs(); +#else // !DESKTOP_APP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC const auto list = std::array{ #ifdef Q_OS_WIN AV_PIX_FMT_D3D11, @@ -138,6 +187,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { AV_PIX_FMT_CUDA, #endif // Q_OS_WIN || Q_OS_MAC }; +#endif // DESKTOP_APP_USE_PACKAGED || Q_OS_WIN || Q_OS_MAC for (const auto format : list) { if (!has(format)) { continue; @@ -230,6 +280,7 @@ FormatPointer MakeFormatPointer( if (!io) { return {}; } + io->seekable = (seek != nullptr); auto result = avformat_alloc_context(); if (!result) { LogError(u"avformat_alloc_context"_q); @@ -250,7 +301,9 @@ FormatPointer MakeFormatPointer( LogError(u"avformat_open_input"_q, error); return {}; } - result->flags |= AVFMT_FLAG_FAST_SEEK; + if (seek) { + result->flags |= AVFMT_FLAG_FAST_SEEK; + } // Now FormatPointer will own and free the IO context. io.release(); 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 c2dd3e4ce..2b49f8c1c 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -610,14 +610,14 @@ void InnerWidget::elementShowPollResults( void InnerWidget::elementOpenPhoto( not_null photo, FullMsgId context) { - _controller->openPhoto(photo, context, MsgId(0)); + _controller->openPhoto(photo, { context }); } void InnerWidget::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - _controller->openDocument(document, context, MsgId(0), showInMediaView); + _controller->openDocument(document, showInMediaView, { context }); } void InnerWidget::elementCancelUpload(const FullMsgId &context) { @@ -1362,7 +1362,7 @@ void InnerWidget::copySelectedText() { } void InnerWidget::showStickerPackInfo(not_null document) { - StickerSetBox::Show(_controller, document); + StickerSetBox::Show(_controller->uiShow(), document); } void InnerWidget::cancelContextDownload(not_null document) { @@ -1380,7 +1380,7 @@ void InnerWidget::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { - _controller->openDocument(document, itemId, MsgId(), true); + _controller->openDocument(document, true, { itemId }); } } } @@ -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/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index b92ed2736..4e2e3cd41 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -226,6 +226,7 @@ TextWithEntities GenerateAdminChangeText( { Flag::PinMessages, tr::lng_admin_log_admin_pin_messages }, { Flag::ManageCall, tr::lng_admin_log_admin_manage_calls }, { Flag::AddAdmins, tr::lng_admin_log_admin_add_admins }, + { Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous }, }; phraseMap[Flag::InviteByLinkOrAdd] = invitePhrase; phraseMap[Flag::ManageCall] = callPhrase; @@ -356,9 +357,6 @@ QString GenerateInviteLinkText(const MTPExportedChatInvite &data) { return label.isEmpty() ? ExtractInviteLink(data).replace( u"https://"_q, QString() - ).replace( - u"t.me/+"_q, - QString() ).replace( u"t.me/joinchat/"_q, QString() @@ -562,10 +560,20 @@ auto GenerateParticipantChangeText( switch (participant.type()) { case Api::ChatParticipant::Type::Creator: { // No valid string here :( + const auto user = GenerateParticipantString( + &channel->session(), + peerId); + if (peerId == channel->session().userPeerId()) { + return GenerateAdminChangeText( + channel, + user, + participant.rights(), + oldRights); + } return tr::lng_admin_log_transferred( tr::now, lt_user, - GenerateParticipantString(&channel->session(), peerId), + user, Ui::Text::WithEntities); } case Api::ChatParticipant::Type::Admin: { @@ -812,7 +820,7 @@ void GenerateItems( const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { const auto bodyFlags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry; - const auto bodyReplyTo = MsgId(); + const auto bodyReplyTo = FullReplyTo(); const auto bodyViaBotId = UserId(); const auto bodyGroupedId = uint64(); return history->makeMessage( @@ -1120,7 +1128,7 @@ void GenerateItems( if (const auto controller = my.sessionWindow.get()) { controller->show( Box( - controller, + controller->uiShow(), Data::FromInputSet(set), Data::StickersType::Stickers), Ui::LayerOption::CloseOther); @@ -1392,9 +1400,13 @@ void GenerateItems( const auto createParticipantJoinByInvite = [&]( const LogJoinByInvite &data) { - const auto text = (channel->isMegagroup() - ? tr::lng_admin_log_participant_joined_by_link - : tr::lng_admin_log_participant_joined_by_link_channel); + const auto text = data.is_via_chatlist() + ? (channel->isMegagroup() + ? tr::lng_admin_log_participant_joined_by_filter_link + : tr::lng_admin_log_participant_joined_by_filter_link_channel) + : (channel->isMegagroup() + ? tr::lng_admin_log_participant_joined_by_link + : tr::lng_admin_log_participant_joined_by_link_channel); addInviteLinkServiceMessage( text( tr::now, diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp index fb504a8b3..a64f98f47 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 5b965667b..5b75925d8 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -54,8 +54,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "support/support_helper.h" #include "ui/image/image.h" #include "ui/text/text_options.h" -#include "ui/toasts/common_toasts.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "payments/payments_checkout_process.h" #include "core/crash_reports.h" #include "core/application.h" @@ -495,8 +495,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; }(); @@ -510,8 +512,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); } } @@ -631,7 +633,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -679,7 +681,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -705,7 +707,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -731,7 +733,7 @@ not_null History::addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -1144,6 +1146,8 @@ void History::applyServiceChanges( topic->setHasPinnedMessages(true); } } + }, [&](const MTPDmessageReplyStoryHeader &data) { + LOG(("API Error: story reply in messageActionPinMessage.")); }); } }, [&](const MTPDmessageActionGroupCall &data) { @@ -1176,7 +1180,7 @@ void History::applyServiceChanges( } if (paid) { // Toast on a current active window. - Ui::ShowMultilineToast({ + Ui::Toast::Show({ .text = tr::lng_payments_success( tr::now, lt_amount, @@ -1260,10 +1264,21 @@ 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); } + if (item->changesWallPaper()) { + peer->updateFullForced(); + } } else { if (item->unread(this)) { if (unreadCountKnown()) { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index d2b80be10..933832d79 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -148,7 +148,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -168,7 +168,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -179,7 +179,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, @@ -190,7 +190,7 @@ public: MsgId id, MessageFlags flags, UserId viaBotId, - MsgId replyTo, + FullReplyTo replyTo, TimeId date, PeerId from, const QString &postAuthor, diff --git a/Telegram/SourceFiles/history/history_drag_area.cpp b/Telegram/SourceFiles/history/history_drag_area.cpp index 8b64a46bf..5c2cebf38 100644 --- a/Telegram/SourceFiles/history/history_drag_area.cpp +++ b/Telegram/SourceFiles/history/history_drag_area.cpp @@ -206,7 +206,8 @@ DragArea::Areas DragArea::SetupDragAreaToContainer( *attachDragState = DragState::None; updateDragAreas(); - e->acceptProposedAction(); + e->setDropAction(Qt::CopyAction); + e->accept(); }; const auto processDragEvents = [=](not_null event) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index ca9d06436..35026b095 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -33,7 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "ui/image/image.h" -#include "ui/toasts/common_toasts.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/reaction_fly_animation.h" @@ -139,7 +138,7 @@ void FillSponsoredMessagesMenu( not_null menu) { const auto &data = controller->session().data().sponsoredMessages(); const auto info = data.lookupDetails(itemId).info; - const auto toastParent = Window::Show(controller).toastParent(); + const auto show = controller->uiShow(); if (!info.empty()) { auto fillSubmenu = [&](not_null menu) { const auto allText = ranges::accumulate( @@ -150,10 +149,7 @@ void FillSponsoredMessagesMenu( }).text; const auto callback = [=] { QGuiApplication::clipboard()->setText(allText); - Ui::ShowMultilineToast({ - .parentOverride = toastParent, - .text = { tr::lng_text_copied(tr::now) }, - }); + show->showToast(tr::lng_text_copied(tr::now)); }; for (const auto &i : info) { auto item = base::make_unique_q( @@ -394,7 +390,7 @@ bool HistoryInner::BotAbout::refresh() { | MessageFlag::Local; const auto postAuthor = QString(); const auto date = TimeId(0); - const auto replyTo = MsgId(0); + const auto replyTo = FullReplyTo(); const auto viaBotId = UserId(0); const auto groupedId = uint64(0); const auto textWithEntities = TextUtilities::ParseEntities( @@ -1056,6 +1052,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { _translateTracker->startBunch(); auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); + const auto markingAsViewed = _widget->markingContentsRead(); const auto guard = gsl::finally([&] { if (_pinnedItem) { _translateTracker->add(_pinnedItem); @@ -1064,7 +1061,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { if (readTill && _widget->markingMessagesRead()) { session().data().histories().readInboxTill(readTill); } - if (!readContents.empty() && _widget->markingContentsRead()) { + if (markingAsViewed && !readContents.empty()) { session().api().markContentsRead(readContents); } _userpicsCache.clear(); @@ -1097,7 +1094,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { } else if (isUnread) { readTill = item; } - if (item->hasViews()) { + if (markingAsViewed && item->hasViews()) { session().api().views().scheduleIncrement(item); } if (withReaction) { @@ -1692,9 +1689,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 +1802,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()); } } @@ -1844,13 +1838,12 @@ void HistoryInner::performDrag() { } void HistoryInner::itemRemoved(not_null item) { - if (_history != item->history() && _migrated != item->history()) { - return; - } - if (_pinnedItem == item) { _pinnedItem = nullptr; } + if (_history != item->history() && _migrated != item->history()) { + return; + } if (_reactionsItem.current() == item) { _reactionsItem = nullptr; } @@ -2042,7 +2035,8 @@ void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) { && (_mouseCursorState == CursorState::None || _mouseCursorState == CursorState::Date) && !inSelectionMode() - && !_emptyPainter) { + && !_emptyPainter + && e->button() == Qt::LeftButton) { if (const auto view = Element::Moused()) { mouseActionCancel(); switch (HistoryView::CurrentQuickAction()) { @@ -2128,21 +2122,30 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { isUponSelected = hasSelected; } - const auto hasWhoReactedItem = _dragStateItem - && Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::All); + const auto groupLeaderOrSelf = [](HistoryItem *item) -> HistoryItem* { + if (!item) { + return nullptr; + } else if (const auto group = item->history()->owner().groups().find(item)) { + return group->items.front(); + } + return item; + }; + const auto whoReactedItem = groupLeaderOrSelf(_dragStateItem); + const auto hasWhoReactedItem = whoReactedItem + && Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::All); const auto clickedReaction = link ? link->property( kReactionsCountEmojiProperty).value() : Data::ReactionId(); _whoReactedMenuLifetime.destroy(); if (!clickedReaction.empty() - && _dragStateItem - && Api::WhoReactedExists(_dragStateItem, Api::WhoReactedList::One)) { + && whoReactedItem + && Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::One)) { HistoryView::ShowWhoReactedMenu( &_menu, e->globalPos(), this, - _dragStateItem, + whoReactedItem, clickedReaction, _controller, _whoReactedMenuLifetime); @@ -2152,14 +2155,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { _menu = base::make_unique_q(this, st::popupMenuWithIcons); const auto session = &this->session(); const auto controller = _controller; - const auto groupLeaderOrSelf = [](HistoryItem *item) -> HistoryItem* { - if (!item) { - return nullptr; - } else if (const auto group = item->history()->owner().groups().find(item)) { - return group->items.front(); - } - return item; - }; const auto addItemActions = [&]( HistoryItem *item, HistoryItem *albumPartItem) { @@ -2343,7 +2338,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { const auto item = _dragStateItem; const auto itemId = item ? item->fullId() : FullMsgId(); if (isUponSelected > 0) { - if (!hasCopyRestrictionForSelected()) { + if (!hasCopyRestrictionForSelected() + && !getSelectedText().empty()) { _menu->addAction( (isUponSelected > 1 ? tr::lng_context_copy_selected_items(tr::now) @@ -2402,11 +2398,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), [=] { @@ -2444,7 +2445,8 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : QString(); if (isUponSelected > 0) { - if (!hasCopyRestrictionForSelected()) { + if (!hasCopyRestrictionForSelected() + && !getSelectedText().empty()) { _menu->addAction( ((isUponSelected > 1) ? tr::lng_context_copy_selected_items(tr::now) @@ -2476,7 +2478,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }, &st::menuIconStickers); const auto isFaved = session->data().stickers().isFaved(document); _menu->addAction(isFaved ? tr::lng_faved_stickers_remove(tr::now) : tr::lng_faved_stickers_add(tr::now), [=] { - Api::ToggleFavedSticker(controller, document, itemId); + Api::ToggleFavedSticker(controller->uiShow(), document, itemId); }, isFaved ? &st::menuIconUnfave : &st::menuIconFave); } if (!hasCopyMediaRestriction(item)) { @@ -2630,7 +2632,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { HistoryView::AddWhoReactedAction( _menu, this, - _dragStateItem, + whoReactedItem, _controller); } @@ -2679,12 +2681,9 @@ bool HistoryInner::showCopyRestriction(HistoryItem *item) { if (!hasCopyRestriction(item)) { return false; } - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(_controller).toastParent(), - .text = { _peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now) }, - }); + _controller->showToast(_peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -2692,12 +2691,9 @@ bool HistoryInner::showCopyMediaRestriction(not_null item) { if (!hasCopyMediaRestriction(item)) { return false; } - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(_controller).toastParent(), - .text = { _peer->isBroadcast() - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now) }, - }); + _controller->showToast(_peer->isBroadcast() + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -2752,15 +2748,18 @@ 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(); } } void HistoryInner::showStickerPackInfo(not_null document) { - StickerSetBox::Show(_controller, document); + StickerSetBox::Show(_controller->uiShow(), document); } void HistoryInner::cancelContextDownload(not_null document) { @@ -2787,7 +2786,7 @@ void HistoryInner::openContextGif(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { - _controller->openDocument(document, itemId, MsgId(), true); + _controller->openDocument(document, true, { itemId }); } } } @@ -2799,7 +2798,7 @@ void HistoryInner::saveContextGif(FullMsgId itemId) { if (const auto media = item->media()) { if (const auto document = media->document()) { Api::ToggleSavedGif( - _controller, + _controller->uiShow(), document, item->fullId(), true); @@ -2923,6 +2922,9 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) { && selectedState.canDeleteCount == selectedState.count) { _widget->confirmDeleteSelected(); } + } else if (!(e->modifiers() & ~Qt::ShiftModifier) + && e->key() != Qt::Key_Shift) { + _widget->tryProcessKeyInput(e); } else { e->ignore(); } @@ -3384,14 +3386,14 @@ void HistoryInner::elementShowPollResults( void HistoryInner::elementOpenPhoto( not_null photo, FullMsgId context) { - _controller->openPhoto(photo, context, MsgId(0)); + _controller->openPhoto(photo, { context }); } void HistoryInner::elementOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - _controller->openDocument(document, context, MsgId(0), showInMediaView); + _controller->openDocument(document, showInMediaView, { context }); } void HistoryInner::elementCancelUpload(const FullMsgId &context) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 2a312bb4a..27727f94d 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -50,7 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_message_view.h" #include "data/notify/data_notify_settings.h" #include "data/data_bot_app.h" -#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp +#include "data/data_scheduled_messages.h" #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_message_reactions.h" @@ -66,6 +66,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. #include "data/data_sponsored_messages.h" +#include "data/data_stories.h" +#include "data/data_wall_paper.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "payments/payments_checkout_process.h" // CheckoutProcess::Start. @@ -79,6 +81,26 @@ constexpr auto kPinnedMessageTextLimit = 16; using ItemPreview = HistoryView::ItemPreview; +[[nodiscard]] TextWithEntities SpoilerLoginCode(TextWithEntities text) { + const auto r = QRegularExpression(u"([\\d\\-]{5,7})"_q); + const auto m = r.match(text.text); + if (!m.hasMatch()) { + return text; + } + const auto codeStart = int(m.capturedStart(1)); + const auto codeLength = int(m.capturedLength(1)); + auto i = text.entities.begin(); + const auto e = text.entities.end(); + while (i != e && i->offset() < codeStart) { + if (i->offset() + i->length() > codeStart) { + return text; // Entities should not intersect code. + } + ++i; + } + text.entities.insert(i, { EntityType::Spoiler, codeStart, codeLength }); + return text; +} + [[nodiscard]] bool HasNotEmojiAndSpaces(const QString &text) { if (text.isEmpty()) { return false; @@ -110,6 +132,7 @@ struct HistoryItem::CreateConfig { PeerId replyToPeer = 0; MsgId replyTo = 0; MsgId replyToTop = 0; + StoryId replyToStory = 0; bool replyIsTopicPost = false; UserId viaBotId = 0; int viewsCount = -1; @@ -267,6 +290,11 @@ std::unique_ptr HistoryItem::CreateMedia( item, qs(media.vemoticon()), media.vvalue().v); + }, [&](const MTPDmessageMediaStory &media) -> Result { + return std::make_unique(item, FullStoryId{ + peerFromUser(media.vuser_id()), + media.vid().v, + }, media.is_via_mention()); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { @@ -302,6 +330,10 @@ HistoryItem::HistoryItem( } else if (checked == MediaCheckResult::HasTimeToLive) { createServiceFromMtp(data); applyTTL(data); + } else if (checked == MediaCheckResult::HasStoryMention) { + setMedia(*data.vmedia()); + createServiceFromMtp(data); + applyTTL(data); } else { createComponents(data); if (const auto media = data.vmedia()) { @@ -316,7 +348,10 @@ HistoryItem::HistoryItem( setText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities)); if (const auto groupedId = data.vgrouped_id()) { setGroupId( - MessageGroupId::FromRaw(history->peer->id, groupedId->v)); + MessageGroupId::FromRaw( + history->peer->id, + groupedId->v, + _flags & MessageFlag::IsOrWasScheduled)); } setReactions(data.vreactions()); applyTTL(data); @@ -482,7 +517,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -506,7 +541,10 @@ HistoryItem::HistoryItem( setMedia(media); setText(textWithEntities); if (groupedId) { - setGroupId(MessageGroupId::FromRaw(history->peer->id, groupedId)); + setGroupId(MessageGroupId::FromRaw( + history->peer->id, + groupedId, + flags & MessageFlag::IsOrWasScheduled)); } } @@ -514,7 +552,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -549,7 +587,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -579,7 +617,7 @@ HistoryItem::HistoryItem( not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -621,7 +659,7 @@ HistoryItem::HistoryItem( /*from.peer ? from.peer->id : */PeerId(0)) { createComponentsHelper( _flags, - MsgId(0), // replyTo + FullReplyTo(), UserId(0), // viaBotId QString(), // postAuthor HistoryMessageMarkupData()); @@ -645,6 +683,20 @@ HistoryItem::HistoryItem( } } +HistoryItem::HistoryItem( + not_null history, + not_null story) +: id(StoryIdToMsgId(story->id())) +, _history(history) +, _from(history->peer) +, _flags(MessageFlag::Local + | MessageFlag::Outgoing + | MessageFlag::FakeHistoryItem + | MessageFlag::StoryItem) +, _date(story->date()) { + setStoryFields(story); +} + HistoryItem::~HistoryItem() { _media = nullptr; clearSavedMedia(); @@ -672,6 +724,8 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return payment; } else if (const auto info = Get()) { return info; + } else if (const auto same = Get()) { + return same; } return nullptr; } @@ -695,6 +749,18 @@ void HistoryItem::dependencyItemRemoved(not_null dependency) { } } +void HistoryItem::dependencyStoryRemoved( + not_null dependency) { + if (const auto reply = Get()) { + const auto documentId = reply->replyToDocumentId; + reply->storyRemoved(this, dependency); + if (documentId != reply->replyToDocumentId + && generateLocalEntitiesByReply()) { + _history->owner().requestItemTextRefresh(this); + } + } +} + void HistoryItem::updateDependencyItem() { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; @@ -993,6 +1059,10 @@ void HistoryItem::updateServiceText(PreparedServiceText &&text) { _history->owner().updateDependentMessages(this); } +void HistoryItem::updateStoryMentionText() { + setServiceText(prepareStoryMentionText()); +} + HistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() { if (const auto markup = Get()) { if (markup->data.flags & ReplyMarkupFlag::Inline) { @@ -1397,6 +1467,13 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { // } //} + const auto updatingSavedLocalEdit = !edition.savePreviousMedia + && (_savedLocalEditMediaData != nullptr); + if (!_savedLocalEditMediaData && edition.savePreviousMedia) { + savePreviousMedia(); + } + Assert(!updatingSavedLocalEdit || !isLocalUpdateMedia()); + if (edition.isEditHide) { _flags |= MessageFlag::HideEdited; } else { @@ -1415,7 +1492,11 @@ 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); } @@ -1428,11 +1509,17 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { if (!edition.useSameForwards) { setForwardsCount(edition.forwards); } - setText(_media + const auto &checkedMedia = updatingSavedLocalEdit + ? _savedLocalEditMediaData->media + : _media; + auto updatedText = checkedMedia ? edition.textWithEntities - : EnsureNonEmpty(edition.textWithEntities)); - if (!isLocalUpdateMedia()) { - indexAsNewItem(); + : EnsureNonEmpty(edition.textWithEntities); + if (updatingSavedLocalEdit) { + _savedLocalEditMediaData->text = std::move(updatedText); + } else { + setText(std::move(updatedText)); + addToSharedMediaIndex(); } if (!edition.useSameReplies) { if (!edition.replies.isNull) { @@ -1449,6 +1536,30 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { finishEdition(keyboardTop); } +void HistoryItem::applyChanges(not_null story) { + Expects(_flags & MessageFlag::StoryItem); + Expects(StoryIdFromMsgId(id) == story->id()); + + _media = nullptr; + setStoryFields(story); + + finishEdition(-1); +} + +void HistoryItem::setStoryFields(not_null story) { + const auto spoiler = false; + if (const auto photo = story->photo()) { + _media = std::make_unique(this, photo, spoiler); + } else if (const auto document = story->document()) { + _media = std::make_unique( + this, + document, + /*skipPremiumEffect=*/false, + spoiler); + } + setText(story->caption()); +} + void HistoryItem::applyEdition(const MTPDmessageService &message) { if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); @@ -1513,12 +1624,13 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { data.vreply_to_top_id().value_or( data.vreply_to_msg_id().v), data.is_forum_topic()); + }, [](const MTPDmessageReplyStoryHeader &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); @@ -1542,7 +1654,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) { @@ -1555,6 +1667,9 @@ void HistoryItem::applySentMessage( void HistoryItem::updateSentContent( const TextWithEntities &textWithEntities, const MTPMessageMedia *media) { + if (_savedLocalEditMediaData) { + return; + } setText(textWithEntities); if (_flags & MessageFlag::FromInlineBot) { if (!media || !_media || !_media->updateInlineResultMedia(*media)) { @@ -1701,6 +1816,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, @@ -1892,6 +2013,8 @@ bool HistoryItem::forbidsSaving() const { bool HistoryItem::canDelete() const { if (isSponsored()) { return false; + } else if (IsStoryMsgId(id)) { + return false; } else if (isService() && !isRegular()) { return false; } else if (topicRootId() == id) { @@ -2210,6 +2333,13 @@ bool HistoryItem::hasDirectLink() const { return isRegular() && _history->peer->isChannel(); } +bool HistoryItem::changesWallPaper() const { + if (const auto media = _media.get()) { + return media->paper() != nullptr; + } + return Has(); +} + FullMsgId HistoryItem::fullId() const { return FullMsgId(_history->peer->id, id); } @@ -2506,7 +2636,7 @@ void HistoryItem::setReplyFields( && !IsServerMsgId(reply->replyToMsgId)) { reply->replyToMsgId = replyTo; if (!reply->updateData(this)) { - RequestDependentMessageData( + RequestDependentMessageItem( this, reply->replyToPeerId, reply->replyToMsgId); @@ -2706,6 +2836,26 @@ MsgId HistoryItem::topicRootId() const { return Data::ForumTopic::kGeneralId; } +FullStoryId HistoryItem::replyToStory() const { + if (const auto reply = Get()) { + if (reply->replyToStoryId) { + const auto peerId = reply->replyToPeerId + ? reply->replyToPeerId + : _history->peer->id; + return { .peer = peerId, .story = reply->replyToStoryId }; + } + } + return {}; +} + +FullReplyTo HistoryItem::replyTo() const { + return { + .msgId = replyToId(), + .topicRootId = topicRootId(), + .storyId = replyToStory(), + }; +} + void HistoryItem::setText(const TextWithEntities &textWithEntities) { for (const auto &entity : textWithEntities.entities) { auto type = entity.type(); @@ -2754,8 +2904,9 @@ bool HistoryItem::isEmpty() const { && !Has(); } -TextWithEntities HistoryItem::notificationText() const { - const auto result = [&] { +TextWithEntities HistoryItem::notificationText( + NotificationTextOptions options) const { + auto result = [&] { if (_media && !isService()) { return _media->notificationText(); } else if (!emptyText()) { @@ -2763,6 +2914,11 @@ TextWithEntities HistoryItem::notificationText() const { } return TextWithEntities(); }(); + if (options.spoilerLoginCode + && !out() + && history()->peer->isNotificationsUser()) { + result = SpoilerLoginCode(std::move(result)); + } if (result.text.size() <= kNotificationTextLimit) { return result; } @@ -2792,6 +2948,11 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const { } return {}; }(); + if (options.spoilerLoginCode + && !out() + && history()->peer->isNotificationsUser()) { + result.text = SpoilerLoginCode(std::move(result.text)); + } const auto fromSender = [](not_null sender) { return sender->isSelf() ? tr::lng_from_you(tr::now) @@ -2856,7 +3017,7 @@ const std::vector &HistoryItem::customTextLinks() const { void HistoryItem::createComponents(CreateConfig &&config) { uint64 mask = 0; - if (config.replyTo) { + if (config.replyTo || config.replyToStory) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { @@ -2897,12 +3058,21 @@ void HistoryItem::createComponents(CreateConfig &&config) { reply->replyToPeerId = config.replyToPeer; reply->replyToMsgId = config.replyTo; reply->replyToMsgTop = isScheduled() ? 0 : config.replyToTop; + reply->replyToStoryId = config.replyToStory; + reply->storyReply = (config.replyToStory != 0); reply->topicPost = config.replyIsTopicPost; if (!reply->updateData(this)) { - RequestDependentMessageData( - this, - reply->replyToPeerId, - reply->replyToMsgId); + if (reply->replyToMsgId) { + RequestDependentMessageItem( + this, + reply->replyToPeerId, + reply->replyToMsgId); + } else if (reply->replyToStoryId) { + RequestDependentMessageStory( + this, + reply->replyToPeerId, + reply->replyToStoryId); + } } } if (const auto via = Get()) { @@ -3065,7 +3235,9 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { } using Type = HistoryMessageSponsored::Type; - sponsored->type = from.isExactPost + sponsored->type = from.isExternalLink + ? Type::ExternalLink + : from.isExactPost ? Type::Post : from.isBot ? Type::Bot @@ -3078,17 +3250,19 @@ void HistoryItem::setSponsoredFrom(const Data::SponsoredFrom &from) { void HistoryItem::createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup) { auto config = CreateConfig(); config.viaBotId = viaBotId; if (flags & MessageFlag::HasReplyInfo) { - config.replyTo = replyTo; - const auto to = LookupReplyTo(_history, replyTo); + config.replyTo = replyTo.msgId; + config.replyToStory = replyTo.storyId.story; + config.replyToPeer = replyTo.storyId ? replyTo.storyId.peer : 0; + const auto to = LookupReplyTo(_history, replyTo.msgId); const auto replyToTop = LookupReplyToTop(to); - config.replyToTop = replyToTop ? replyToTop : replyTo; + config.replyToTop = replyToTop ? replyToTop : replyTo.msgId; const auto forum = _history->asForum(); config.replyIsTopicPost = LookupReplyIsTopicPost(to) || (to && to->Has()) @@ -3152,7 +3326,7 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { const auto &recent = data.vrecent_reactions().value_or_empty(); if (min && hasUnreadReaction()) { // We can't update reactions from min if we have unread. - if (_reactions->checkIfChanged(list, recent)) { + if (_reactions->checkIfChanged(list, recent, min)) { updateReactionsUnknown(); } return false; @@ -3198,6 +3372,9 @@ void HistoryItem::createComponents(const MTPDmessage &data) { : id; config.replyToTop = data.vreply_to_top_id().value_or(id); config.replyIsTopicPost = data.is_forum_topic(); + }, [&](const MTPDmessageReplyStoryHeader &data) { + config.replyToPeer = peerFromUser(data.vuser_id()); + config.replyToStory = data.vstory_id().v; }); } config.viaBotId = data.vvia_bot_id().value_or_empty(); @@ -3243,15 +3420,13 @@ void HistoryItem::refreshSentMedia(const MTPMessageMedia *media) { void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { AddComponents(HistoryServiceData::Bit()); + const auto unread = message.is_media_unread(); const auto media = message.vmedia(); Assert(media != nullptr); - const auto mediaType = media->type(); - switch (mediaType) { - case mtpc_messageMediaPhoto: { - if (message.is_media_unread()) { - const auto &photo = media->c_messageMediaPhoto(); - const auto ttl = photo.vttl_seconds(); + media->match([&](const MTPDmessageMediaPhoto &data) { + if (unread) { + const auto ttl = data.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, ttl->v); @@ -3274,11 +3449,9 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities) }); } - } break; - case mtpc_messageMediaDocument: { - if (message.is_media_unread()) { - const auto &document = media->c_messageMediaDocument(); - const auto ttl = document.vttl_seconds(); + }, [&](const MTPDmessageMediaDocument &data) { + if (unread) { + const auto ttl = data.vttl_seconds(); Assert(ttl != nullptr); setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, ttl->v); @@ -3301,10 +3474,11 @@ void HistoryItem::createServiceFromMtp(const MTPDmessage &message) { tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities) }); } - } break; - - default: Unexpected("Media type in HistoryItem::createServiceFromMtp()"); - } + }, [&](const MTPDmessageMediaStory &data) { + setServiceText(prepareStoryMentionText()); + }, [](const auto &) { + Unexpected("Media type in HistoryItem::createServiceFromMtp()"); + }); if (const auto reactions = message.vreactions()) { updateReactions(reactions); @@ -3426,6 +3600,8 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { } }, call->lifetime); } + } else if (type == mtpc_messageActionSetSameChatWallPaper) { + UpdateComponents(HistoryServiceSameBackground::Bit()); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -3442,7 +3618,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->topicPost = data.is_forum_topic() || Has(); if (!updateServiceDependent()) { - RequestDependentMessageData( + RequestDependentMessageItem( this, (dependent->peerId ? dependent->peerId @@ -3450,6 +3626,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { dependent->msgId); } } + }, [](const MTPDmessageReplyStoryHeader &data) { }); } setServiceMessageByAction(action); @@ -3457,9 +3634,29 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { void HistoryItem::setMedia(const MTPMessageMedia &media) { _media = CreateMedia(this, media); + checkStoryForwardInfo(); checkBuyButton(); } +void HistoryItem::checkStoryForwardInfo() { + if (const auto storyId = _media ? _media->storyId() : FullStoryId()) { + const auto adding = !Has(); + if (adding) { + AddComponents(HistoryMessageForwarded::Bit()); + } + const auto forwarded = Get(); + if (forwarded->story || adding) { + const auto peer = history()->owner().peer(storyId.peer); + forwarded->story = true; + forwarded->originalSender = peer; + } + } else if (const auto forwarded = Get()) { + if (forwarded->story) { + RemoveComponents(HistoryMessageForwarded::Bit()); + } + } +} + void HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) { const auto date = data.vdate().v; if (_date == date) { @@ -4157,6 +4354,52 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareSetChatWallPaper = [&]( + const MTPDmessageActionSetChatWallPaper &action) { + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? history()->peer : _from; + const auto user = peer->asUser(); + const auto name = (user && !user->firstName.isEmpty()) + ? user->firstName + : peer->name(); + auto result = PreparedServiceText{}; + result.links.push_back(peer->createOpenLink()); + result.text = isSelf + ? tr::lng_action_set_wallpaper_me( + tr::now, + Ui::Text::WithEntities) + : tr::lng_action_set_wallpaper( + tr::now, + lt_user, + Ui::Text::Link(name, 1), // Link 1. + Ui::Text::WithEntities); + return result; + }; + + auto prepareSetSameChatWallPaper = [&]( + const MTPDmessageActionSetSameChatWallPaper &action) { + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? history()->peer : _from; + const auto user = peer->asUser(); + const auto name = (user && !user->firstName.isEmpty()) + ? user->firstName + : peer->name(); + auto result = PreparedServiceText{}; + if (!isSelf) { + result.links.push_back(peer->createOpenLink()); + } + result.text = isSelf + ? tr::lng_action_set_same_wallpaper_me( + tr::now, + Ui::Text::WithEntities) + : tr::lng_action_set_same_wallpaper( + tr::now, + lt_user, + Ui::Text::Link(name, 1), // Link 1. + Ui::Text::WithEntities); + return result; + }; + setServiceText(action.match([&]( const MTPDmessageActionChatAddUser &data) { return prepareChatAddUserText(data); @@ -4233,6 +4476,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return prepareSuggestProfilePhoto(data); }, [&](const MTPDmessageActionRequestedPeer &data) { return prepareRequestedPeer(data); + }, [&](const MTPDmessageActionSetChatWallPaper &data) { + return prepareSetChatWallPaper(data); + }, [&](const MTPDmessageActionSetSameChatWallPaper &data) { + return prepareSetSameChatWallPaper(data); }, [](const MTPDmessageActionEmpty &) { return PreparedServiceText{ { tr::lng_message_empty(tr::now) } }; })); @@ -4296,6 +4543,12 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { history()->owner().processPhoto(photo)); }, [](const MTPDphotoEmpty &) { }); + }, [&](const MTPDmessageActionSetChatWallPaper &data) { + const auto session = &history()->session(); + const auto &attached = data.vwallpaper(); + if (const auto paper = Data::WallPaper::Create(session, attached)) { + _media = std::make_unique(this, *paper); + } }, [](const auto &) { }); } @@ -4594,6 +4847,28 @@ PreparedServiceText HistoryItem::preparePaymentSentText() { return result; } +PreparedServiceText HistoryItem::prepareStoryMentionText() { + auto result = PreparedServiceText(); + const auto peer = history()->peer; + result.links.push_back(peer->createOpenLink()); + const auto phrase = (this->media() && this->media()->storyExpired(true)) + ? (out() + ? tr::lng_action_story_mention_me_unavailable + : tr::lng_action_story_mention_unavailable) + : (out() + ? tr::lng_action_story_mention_me + : tr::lng_action_story_mention); + result.text = phrase( + tr::now, + lt_user, + Ui::Text::Wrapped( + Ui::Text::Bold(peer->shortName()), + EntityType::CustomUrl, + u"internal:index"_q + QChar(1)), + Ui::Text::WithEntities); + return result; +} + PreparedServiceText HistoryItem::prepareCallScheduledText( TimeId scheduleDate) { const auto call = Get(); @@ -4731,7 +5006,7 @@ void HistoryItem::setupTTLChange() { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { const auto validator = TTLMenu::TTLValidator( - std::make_shared(controller), + controller->uiShow(), peer); if (validator.can()) { validator.showBox(); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 629cc7267..1b15d91e8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -57,6 +57,7 @@ class MessageReactions; class ForumTopic; class Thread; struct SponsoredFrom; +class Story; } // namespace Data namespace Main { @@ -117,7 +118,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -147,7 +148,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -159,7 +160,7 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, @@ -171,13 +172,14 @@ public: not_null history, MsgId id, MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup); + HistoryItem(not_null history, not_null story); ~HistoryItem(); struct Destroyer { @@ -185,13 +187,16 @@ public: }; void dependencyItemRemoved(not_null dependency); + void dependencyStoryRemoved(not_null dependency); void updateDependencyItem(); [[nodiscard]] MsgId dependencyMsgId() const; [[nodiscard]] bool notificationReady() const; [[nodiscard]] PeerData *specialNotificationPeer() const; + void checkStoryForwardInfo(); void checkBuyButton(); void updateServiceText(PreparedServiceText &&text); + void updateStoryMentionText(); [[nodiscard]] UserData *viaBot() const; [[nodiscard]] UserData *getMessageBot() const; @@ -330,6 +335,7 @@ public: [[nodiscard]] bool isService() const; void applyEdition(HistoryMessageEdition &&edition); + void applyChanges(not_null story); void applyEdition(const MTPDmessageService &message); void applyEdition(const MTPMessageExtendedMedia &media); @@ -353,10 +359,18 @@ public: [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const; void indexAsNewItem(); + void addToSharedMediaIndex(); void removeFromSharedMediaIndex(); + struct NotificationTextOptions { + bool spoilerLoginCode = false; + }; [[nodiscard]] QString notificationHeader() const; - [[nodiscard]] TextWithEntities notificationText() const; + [[nodiscard]] TextWithEntities notificationText( + NotificationTextOptions options) const; + [[nodiscard]] TextWithEntities notificationText() const { + return notificationText({}); + } using ToPreviewOptions = HistoryView::ToPreviewOptions; using ItemPreview = HistoryView::ItemPreview; @@ -433,6 +447,7 @@ public: [[nodiscard]] crl::time lastReactionsRefreshTime() const; [[nodiscard]] bool hasDirectLink() const; + [[nodiscard]] bool changesWallPaper() const; [[nodiscard]] FullMsgId fullId() const; [[nodiscard]] GlobalMsgId globalId() const; @@ -450,6 +465,8 @@ public: [[nodiscard]] MsgId replyToId() const; [[nodiscard]] MsgId replyToTop() const; [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] FullStoryId replyToStory() const; + [[nodiscard]] FullReplyTo replyTo() const; [[nodiscard]] bool inThread(MsgId rootId) const; [[nodiscard]] not_null author() const; @@ -510,7 +527,7 @@ private: void createComponentsHelper( MessageFlags flags, - MsgId replyTo, + FullReplyTo replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup); @@ -547,6 +564,7 @@ private: bool updateServiceDependent(bool force = false); void setServiceText(PreparedServiceText &&prepared); + void setStoryFields(not_null story); void finishEdition(int oldKeyboardTop); void finishEditionToEmpty(); @@ -592,6 +610,7 @@ private: [[nodiscard]] PreparedServiceText preparePinnedText(); [[nodiscard]] PreparedServiceText prepareGameScoreText(); [[nodiscard]] PreparedServiceText preparePaymentSentText(); + [[nodiscard]] PreparedServiceText prepareStoryMentionText(); [[nodiscard]] PreparedServiceText prepareInvitedToCallText( const std::vector> &users, CallId linkCallId); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 8b6573935..f9e1c403f 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_web_page.h" #include "data/data_file_click_handler.h" +#include "data/data_stories.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" @@ -135,9 +136,7 @@ ClickHandlerPtr HiddenSenderInfo::ForwardClickHandler() { const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto strong = weak.get()) { - Ui::Toast::Show( - Window::Show(strong).toastParent(), - tr::lng_forwarded_hidden(tr::now)); + strong->showToast(tr::lng_forwarded_hidden(tr::now)); } }); return hidden; @@ -193,7 +192,13 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } else { phrase = name; } - if (via && psaType.isEmpty()) { + if (story) { + phrase = tr::lng_forwarded_story( + tr::now, + lt_user, + Ui::Text::Link(phrase.text, QString()), // Link 1. + Ui::Text::WithEntities); + } else if (via && psaType.isEmpty()) { if (fromChannel) { phrase = tr::lng_forwarded_channel_via( tr::now, @@ -259,15 +264,17 @@ bool HistoryMessageReply::updateData( bool force) { const auto guard = gsl::finally([&] { refreshReplyToMedia(); }); if (!force) { - if (replyToMsg || !replyToMsgId) { + if ((replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId)) { return true; } } - if (!replyToMsg) { + const auto peerId = replyToPeerId + ? replyToPeerId + : holder->history()->peer->id; + if (!replyToMsg && replyToMsgId) { replyToMsg = holder->history()->owner().message( - (replyToPeerId - ? replyToPeerId - : holder->history()->peer->id), + peerId, replyToMsgId); if (replyToMsg) { if (replyToMsg->isEmpty()) { @@ -281,8 +288,22 @@ bool HistoryMessageReply::updateData( } } } + if (!replyToStory && replyToStoryId) { + const auto maybe = holder->history()->owner().stories().lookup({ + peerId, + replyToStoryId, + }); + if (maybe) { + replyToStory = *maybe; + holder->history()->owner().stories().registerDependentMessage( + holder, + replyToStory.get()); + } else if (maybe.error() == Data::NoStory::Deleted) { + force = true; + } + } - if (replyToMsg) { + if (replyToMsg || replyToStory) { const auto repaint = [=] { holder->customEmojiRepaint(); }; const auto context = Core::MarkedTextContext{ .session = &holder->history()->session(), @@ -290,14 +311,16 @@ bool HistoryMessageReply::updateData( }; replyToText.setMarkedText( st::messageTextStyle, - replyToMsg->inReplyText(), + (replyToMsg + ? replyToMsg->inReplyText() + : replyToStory->inReplyText()), Ui::DialogTextOptions(), context); updateName(holder); setReplyToLinkFrom(holder); - if (!replyToMsg->Has()) { + if (replyToMsg && !replyToMsg->Has()) { if (auto bot = replyToMsg->viaBot()) { replyToVia = std::make_unique(); replyToVia->create( @@ -306,15 +329,17 @@ bool HistoryMessageReply::updateData( } } - { + if (replyToMsg) { const auto peer = replyToMsg->history()->peer; replyToColorKey = (!holder->out() && (peer->isMegagroup() || peer->isChat())) ? replyToMsg->from()->id : PeerId(0); + } else { + replyToColorKey = PeerId(0); } - const auto media = replyToMsg->media(); + const auto media = replyToMsg ? replyToMsg->media() : nullptr; if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { spoiler = nullptr; } else if (!spoiler) { @@ -322,19 +347,23 @@ bool HistoryMessageReply::updateData( } } else if (force) { replyToMsgId = 0; + replyToStoryId = 0; replyToColorKey = PeerId(0); spoiler = nullptr; } if (force) { holder->history()->owner().requestItemResize(holder); } - return (replyToMsg || !replyToMsgId); + return (replyToMsg || !replyToMsgId) + && (replyToStory || !replyToStoryId); } void HistoryMessageReply::setReplyToLinkFrom( not_null holder) { replyToLnk = replyToMsg ? JumpToMessageClickHandler(replyToMsg.get(), holder->fullId()) + : replyToStory + ? JumpToStoryClickHandler(replyToStory.get()) : nullptr; } @@ -346,7 +375,14 @@ void HistoryMessageReply::clearData(not_null holder) { replyToMsg.get()); replyToMsg = nullptr; } + if (replyToStory) { + holder->history()->owner().stories().unregisterDependentMessage( + holder, + replyToStory.get()); + replyToStory = nullptr; + } replyToMsgId = 0; + replyToStoryId = 0; refreshReplyToMedia(); } @@ -367,7 +403,9 @@ PeerData *HistoryMessageReply::replyToFrom( QString HistoryMessageReply::replyToFromName( not_null holder) const { - if (!replyToMsg) { + if (replyToStory) { + return replyToFromName(replyToStory->peer()); + } else if (!replyToMsg) { return QString(); } else if (holder->Has()) { if (const auto fwd = replyToMsg->Get()) { @@ -407,10 +445,15 @@ void HistoryMessageReply::updateName( replyToName.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); if (const auto from = replyToFrom(holder)) { replyToVersion = from->nameVersion(); - } else { + } else if (replyToMsg) { replyToVersion = replyToMsg->author()->nameVersion(); + } else { + replyToVersion = replyToStory->peer()->nameVersion(); } - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int32 previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; int32 w = replyToName.maxWidth(); if (replyToVia) { @@ -419,28 +462,40 @@ void HistoryMessageReply::updateName( maxReplyWidth = previewSkip + qMax(w, qMin(replyToText.maxWidth(), int32(st::maxSignatureSize))); } else { - maxReplyWidth = st::msgDateFont->width(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now)); + maxReplyWidth = st::msgDateFont->width(statePhrase()); } maxReplyWidth = st::msgReplyPadding.left() + st::msgReplyBarSkip + maxReplyWidth + st::msgReplyPadding.right(); } void HistoryMessageReply::resize(int width) const { if (replyToVia) { - bool hasPreview = replyToMsg->media() ? replyToMsg->media()->hasReplyPreview() : false; + bool hasPreview = (replyToStory && replyToStory->hasReplyPreview()) + || (replyToMsg + && replyToMsg->media() + && replyToMsg->media()->hasReplyPreview()); int previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; replyToVia->resize(width - st::msgReplyBarSkip - previewSkip - replyToName.maxWidth() - st::msgServiceFont->spacew); } } void HistoryMessageReply::itemRemoved( - HistoryItem *holder, - HistoryItem *removed) { + not_null holder, + not_null removed) { if (replyToMsg.get() == removed) { clearData(holder); holder->history()->owner().requestItemResize(holder); } } +void HistoryMessageReply::storyRemoved( + not_null holder, + not_null removed) { + if (replyToStory.get() == removed) { + clearData(holder); + holder->history()->owner().requestItemResize(holder); + } +} + void HistoryMessageReply::paint( Painter &p, not_null holder, @@ -453,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 @@ -463,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); @@ -473,16 +544,19 @@ void HistoryMessageReply::paint( const auto pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler); if (w > st::msgReplyBarSkip) { - if (replyToMsg) { - const auto media = replyToMsg->media(); - auto hasPreview = media && media->hasReplyPreview(); + if (replyToMsg || replyToStory) { + const auto media = replyToMsg ? replyToMsg->media() : nullptr; + auto hasPreview = (replyToStory && replyToStory->hasReplyPreview()) || (media && media->hasReplyPreview()); if (hasPreview && w < st::msgReplyBarSkip + st::msgReplyBarSize.height()) { hasPreview = false; } auto previewSkip = hasPreview ? (st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x()) : 0; if (hasPreview) { - if (const auto image = media->replyPreview()) { + const auto image = media + ? media->replyPreview() + : replyToStory->replyPreview(); + if (image) { auto to = style::rtlrect(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + st::msgReplyBarPos.y(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height(), w + 2 * x); const auto preview = image->pixSingle( image->size() / style::DevicePixelRatio(), @@ -544,11 +618,19 @@ void HistoryMessageReply::paint( p.setPen(inBubble ? stm->msgDateFg : st->msgDateImgFg()); - p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(replyToMsgId ? tr::lng_profile_loading(tr::now) : tr::lng_deleted_message(tr::now), w - st::msgReplyBarSkip)); + p.drawTextLeft(x + st::msgReplyBarSkip, y + st::msgReplyPadding.top() + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2, w + 2 * x, st::msgDateFont->elided(statePhrase(), w - st::msgReplyBarSkip)); } } } +QString HistoryMessageReply::statePhrase() const { + return (replyToMsgId || replyToStoryId) + ? tr::lng_profile_loading(tr::now) + : storyReply + ? tr::lng_deleted_story(tr::now) + : tr::lng_deleted_message(tr::now); +} + void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; @@ -986,7 +1068,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 5fc0e6e1c..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; @@ -25,6 +26,7 @@ struct PeerUserpicView; namespace Data { class Session; +class Story; } // namespace Data namespace Media::Player { @@ -128,6 +130,7 @@ struct HistoryMessageForwarded : public RuntimeComponent { @@ -137,6 +140,7 @@ struct HistoryMessageSponsored : public RuntimeComponent sender; Type type = Type::User; @@ -182,6 +186,44 @@ private: }; +class ReplyToStoryPointer final { +public: + ReplyToStoryPointer(Data::Story *story = nullptr) : _data(story) { + } + ReplyToStoryPointer(ReplyToStoryPointer &&other) + : _data(base::take(other._data)) { + } + ReplyToStoryPointer &operator=(ReplyToStoryPointer &&other) { + _data = base::take(other._data); + return *this; + } + ReplyToStoryPointer &operator=(Data::Story *item) { + _data = item; + return *this; + } + + [[nodiscard]] bool empty() const { + return !_data; + } + [[nodiscard]] Data::Story *get() const { + return _data; + } + explicit operator bool() const { + return !empty(); + } + + [[nodiscard]] Data::Story *operator->() const { + return _data; + } + [[nodiscard]] Data::Story &operator*() const { + return *_data; + } + +private: + Data::Story *_data = nullptr; + +}; + struct HistoryMessageReply : public RuntimeComponent { HistoryMessageReply() = default; @@ -210,7 +252,12 @@ struct HistoryMessageReply [[nodiscard]] bool isNameUpdated(not_null holder) const; void updateName(not_null holder) const; void resize(int width) const; - void itemRemoved(HistoryItem *holder, HistoryItem *removed); + void itemRemoved( + not_null holder, + not_null removed); + void storyRemoved( + not_null holder, + not_null removed); void paint( Painter &p, @@ -236,6 +283,7 @@ struct HistoryMessageReply [[nodiscard]] ClickHandlerPtr replyToLink() const { return replyToLnk; } + [[nodiscard]] QString statePhrase() const; void setReplyToLinkFrom(not_null holder); void refreshReplyToMedia(); @@ -243,11 +291,13 @@ struct HistoryMessageReply PeerId replyToPeerId = 0; MsgId replyToMsgId = 0; MsgId replyToMsgTop = 0; + StoryId replyToStoryId = 0; using ColorKey = PeerId; ColorKey replyToColorKey = 0; DocumentId replyToDocumentId = 0; WebPageId replyToWebPageId = 0; ReplyToMessagePointer replyToMsg; + ReplyToStoryPointer replyToStory; std::unique_ptr replyToVia; std::unique_ptr spoiler; ClickHandlerPtr replyToLnk; @@ -256,6 +306,12 @@ struct HistoryMessageReply mutable int maxReplyWidth = 0; int toWidth = 0; bool topicPost = false; + bool storyReply = false; + + struct final { + mutable std::unique_ptr animation; + QPoint lastPoint; + } ripple; }; @@ -370,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; @@ -530,6 +588,11 @@ struct HistoryServicePayment bool recurringUsed = false; }; +struct HistoryServiceSameBackground +: public RuntimeComponent +, public HistoryServiceDependentData { +}; + enum class HistorySelfDestructType { Photo, Video, diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index 2ca37c71a..b24bf8836 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -29,6 +29,7 @@ struct HistoryMessageEdition { bool useSameReplies = false; bool useSameMarkup = false; bool useSameReactions = false; + bool savePreviousMedia = false; TextWithEntities textWithEntities; HistoryMessageMarkupData replyMarkup; HistoryMessageRepliesData replies; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index cac95deeb..c89788248 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -18,8 +18,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_media_types.h" #include "data/data_message_reactions.h" -#include "data/data_scheduled_messages.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" @@ -64,6 +64,11 @@ QString GetErrorTextForSending( const auto thread = topic ? not_null(topic) : peer->owner().history(peer); + if (request.story) { + if (const auto error = request.story->errorTextForForward(thread)) { + return *error; + } + } if (request.forward) { for (const auto &item : *request.forward) { if (const auto error = item->errorTextForForward(thread)) { @@ -84,6 +89,7 @@ QString GetErrorTextForSending( } if (peer->slowmodeApplied()) { const auto count = (hasText ? 1 : 0) + + (request.story ? 1 : 0) + (request.forward ? int(request.forward->size()) : 0); if (const auto history = peer->owner().historyLoaded(peer)) { if (!request.ignoreSlowmodeCountdown @@ -94,7 +100,7 @@ QString GetErrorTextForSending( } if (request.text && request.text->text.size() > MaxMessageSize) { return tr::lng_slowmode_too_long(tr::now); - } else if (hasText && count > 1) { + } else if ((hasText || request.story) && count > 1) { return tr::lng_slowmode_no_many(tr::now); } else if (count > 1) { const auto albumForward = [&] { @@ -133,7 +139,7 @@ QString GetErrorTextForSending( return GetErrorTextForSending(thread->peer(), std::move(request)); } -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId) { @@ -154,6 +160,23 @@ void RequestDependentMessageData( done); } +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId) { + const auto fullId = item->fullId(); + const auto history = item->history(); + const auto session = &history->session(); + const auto done = [=] { + if (const auto item = session->data().message(fullId)) { + item->updateDependencyItem(); + } + }; + history->owner().stories().resolve( + { peerId ? peerId : history->peer->id, storyId }, + done); +} + MessageFlags NewMessageFlags(not_null peer) { return MessageFlag::BeingSent | (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing); @@ -230,8 +253,7 @@ QString ItemDateText(not_null item, bool isUntilOnline) { bool IsItemScheduledUntilOnline(not_null item) { return item->isScheduled() - && (item->date() == - Data::ScheduledMessages::kScheduledUntilOnlineTimestamp); + && (item->date() == Api::kScheduledUntilOnlineTimestamp); } ClickHandlerPtr JumpToMessageClickHandler( @@ -268,6 +290,27 @@ ClickHandlerPtr JumpToMessageClickHandler( }); } +ClickHandlerPtr JumpToStoryClickHandler(not_null story) { + return JumpToStoryClickHandler(story->peer(), story->id()); +} + +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId) { + return std::make_shared([=] { + const auto separate = Core::App().separateWindowForPeer(peer); + const auto controller = separate + ? separate->sessionController() + : peer->session().tryResolveWindow(); + if (controller) { + controller->openPeerStory( + peer, + storyId, + { Data::StoriesContextSingle() }); + } + }); +} + MessageFlags FlagsFromMTP( MsgId id, MTPDmessage::Flags flags, @@ -311,8 +354,13 @@ MessageFlags FlagsFromMTP( } MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { - if (const auto id = action.replyTo) { - const auto to = LookupReplyTo(action.history, id); + if (const auto replyTo = action.replyTo) { + if (replyTo.storyId) { + return MTP_messageReplyStoryHeader( + MTP_long(peerToUser(replyTo.storyId.peer).bare), + MTP_int(replyTo.storyId.story)); + } + const auto to = LookupReplyTo(action.history, replyTo.msgId); if (const auto replyToTop = LookupReplyToTop(to)) { using Flag = MTPDmessageReplyHeader::Flag; return MTP_messageReplyHeader( @@ -320,13 +368,13 @@ MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { | (LookupReplyIsTopicPost(to) ? Flag::f_forum_topic : Flag(0))), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTP_int(replyToTop)); } return MTP_messageReplyHeader( MTP_flags(0), - MTP_int(id), + MTP_int(replyTo.msgId), MTPPeer(), MTPint()); } @@ -401,6 +449,10 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; + }, [](const MTPDmessageMediaStory &data) { + return data.is_via_mention() + ? Result::HasStoryMention + : Result::Good; }, [](const MTPDmessageMediaUnsupported &) { return Result::Unsupported; }); diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 46b772517..e7ed74290 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -15,6 +15,7 @@ struct SendAction; } // namespace Api namespace Data { +class Story; class Thread; } // namespace Data @@ -43,6 +44,7 @@ enum class MediaCheckResult { Unsupported, Empty, HasTimeToLive, + HasStoryMention, }; [[nodiscard]] MediaCheckResult CheckMessageMedia( const MTPMessageMedia &media); @@ -69,10 +71,14 @@ void CheckReactionNotificationSchedule( const TextWithEntities &text = TextWithEntities()); [[nodiscard]] TextWithEntities UnsupportedMessageText(); -void RequestDependentMessageData( +void RequestDependentMessageItem( not_null item, PeerId peerId, MsgId msgId); +void RequestDependentMessageStory( + not_null item, + PeerId peerId, + StoryId storyId); [[nodiscard]] MessageFlags NewMessageFlags(not_null peer); [[nodiscard]] bool ShouldSendSilent( not_null peer, @@ -86,6 +92,7 @@ void RequestDependentMessageData( struct SendingErrorRequest { MsgId topicRootId = 0; const HistoryItemsList *forward = nullptr; + const Data::Story *story = nullptr; const TextWithTags *text = nullptr; bool ignoreSlowmodeCountdown = false; }; @@ -115,6 +122,11 @@ struct SendingErrorRequest { [[nodiscard]] ClickHandlerPtr JumpToMessageClickHandler( not_null item, FullMsgId returnToId = FullMsgId()); +[[nodiscard]] ClickHandlerPtr JumpToStoryClickHandler( + not_null story); +ClickHandlerPtr JumpToStoryClickHandler( + not_null peer, + StoryId storyId); [[nodiscard]] not_null GenerateJoinedMessage( not_null history, diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index fecb3598e..c15381e92 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -10,9 +10,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/history_item.h" #include "history/history_item_components.h" +#include "inline_bots/bot_attach_web_view.h" namespace { +[[nodiscard]] InlineBots::PeerTypes PeerTypesFromMTP( + const MTPvector &types) { + using namespace InlineBots; + auto result = PeerTypes(0); + for (const auto &type : types.v) { + result |= type.match([&](const MTPDinlineQueryPeerTypePM &data) { + return PeerType::User; + }, [&](const MTPDinlineQueryPeerTypeChat &data) { + return PeerType::Group; + }, [&](const MTPDinlineQueryPeerTypeMegagroup &data) { + return PeerType::Group; + }, [&](const MTPDinlineQueryPeerTypeBroadcast &data) { + return PeerType::Broadcast; + }, [&](const MTPDinlineQueryPeerTypeBotPM &data) { + return PeerType::Bot; + }, [&](const MTPDinlineQueryPeerTypeSameBotPM &data) { + return PeerType(); + }); + } + return result; +} + [[nodiscard]] RequestPeerQuery RequestPeerQueryFromTL( const MTPRequestPeerType &query) { using Type = RequestPeerQuery::Type; @@ -134,6 +157,9 @@ void HistoryMessageMarkupData::fillRows( // Optimization flag. // Fast check on all new messages if there is a switch button to auto-click it. flags |= ReplyMarkupFlag::HasSwitchInlineButton; + if (const auto types = data.vpeer_types()) { + row.back().peerTypes = PeerTypesFromMTP(*types); + } } }, [&](const MTPDkeyboardButtonGame &data) { row.emplace_back(Type::Game, qs(data.vtext())); diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 719711d53..828432113 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -14,6 +14,11 @@ namespace Data { class Session; } // namespace Data +namespace InlineBots { +enum class PeerType : uint8; +using PeerTypes = base::flags; +} // namespace InlineBots + enum class ReplyMarkupFlag : uint32 { None = (1U << 0), ForceReply = (1U << 1), @@ -89,6 +94,7 @@ struct HistoryMessageMarkupButton { QString text, forwardText; QByteArray data; int64 buttonId = 0; + InlineBots::PeerTypes peerTypes = 0; mutable mtpRequestId requestId = 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_view_top_toast.cpp b/Telegram/SourceFiles/history/history_view_top_toast.cpp index 2315e20c6..155f13996 100644 --- a/Telegram/SourceFiles/history/history_view_top_toast.cpp +++ b/Telegram/SourceFiles/history/history_view_top_toast.cpp @@ -41,7 +41,7 @@ void InfoTooltip::show( _topToast = Ui::Toast::Show(parent, Ui::Toast::Config{ .text = text, .st = &st::historyInfoToast, - .durationMs = CountToastDuration(text), + .duration = CountToastDuration(text), .multiline = true, .dark = true, .slideSide = RectPart::Top, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 71e023d04..e4dad4d47 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_document.h" #include "data/data_photo.h" +#include "data/data_photo_media.h" #include "data/data_media_types.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -224,11 +225,11 @@ HistoryWidget::HistoryWidget( _scroll.data(), controller->chatStyle(), static_cast(this)) -, _fieldAutocomplete(this, controller) +, _fieldAutocomplete(this, controller->uiShow()) , _supportAutocomplete(session().supportMode() ? object_ptr(this, &session()) : nullptr) -, _send(std::make_shared(this)) +, _send(std::make_shared(this, st::historySend)) , _unblock(this, tr::lng_unblock_button(tr::now).toUpper(), st::historyUnblock) , _botStart(this, tr::lng_bot_start(tr::now).toUpper(), st::historyComposeButton) , _joinChannel( @@ -247,7 +248,7 @@ HistoryWidget::HistoryWidget( , _botCommandStart(this, st::historyBotCommandStart) , _voiceRecordBar(std::make_unique( this, - controller, + controller->uiShow(), _send, st::historySendSize.height())) , _forwardPanel(std::make_unique([=] { updateField(); })) @@ -304,7 +305,8 @@ HistoryWidget::HistoryWidget( _send.get(), [=] { return sendButtonMenuType(); }, [=] { sendSilent(); }, - [=] { sendScheduled(); }); + [=] { sendScheduled(); }, + [=] { sendWhenOnline(); }); _unblock->addClickHandler([=] { unblockUser(); }); _botStart->addClickHandler([=] { sendBotStartCommand(); }); @@ -744,7 +746,7 @@ HistoryWidget::HistoryWidget( const auto account = &_peer->account(); closeCurrent(); if (const auto primary = Core::App().windowFor(account)) { - controller->showToast({ unavailable }); + controller->showToast(unavailable); } return; } @@ -851,7 +853,7 @@ HistoryWidget::HistoryWidget( }) | rpl::start_with_next([=](const Api::SendAction &action) { const auto lastKeyboardUsed = lastForceReplyReplied(FullMsgId( action.history->peer->id, - action.replyTo)); + action.replyTo.msgId)); if (action.replaceMediaOf) { } else if (action.options.scheduled) { cancelReply(lastKeyboardUsed); @@ -877,7 +879,7 @@ HistoryWidget::HistoryWidget( }, lifetime()); } - controller->materializeLocalDraftsRequests( + Core::App().materializeLocalDraftsRequests( ) | rpl::start_with_next([=] { saveFieldToHistoryLocalDraft(); }, lifetime()); @@ -941,18 +943,6 @@ void HistoryWidget::refreshTabbedPanel() { } void HistoryWidget::initVoiceRecordBar() { - { - auto scrollHeight = rpl::combine( - _scroll->topValue(), - _scroll->heightValue() - ) | rpl::map([](int top, int height) { - return top + height - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(scrollHeight)); - } - - _voiceRecordBar->setSendButtonGeometryValue(_send->geometryValue()); - _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { if (_peer) { @@ -965,7 +955,7 @@ void HistoryWidget::initVoiceRecordBar() { return std::nullopt; }(); if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return true; } else if (showSlowmodeError()) { return true; @@ -1026,7 +1016,12 @@ void HistoryWidget::initVoiceRecordBar() { _voiceRecordBar->recordingTipRequests( ) | rpl::start_with_next([=] { - controller()->showToast({ tr::lng_record_hold_tip(tr::now) }); + controller()->showToast(tr::lng_record_hold_tip(tr::now)); + }, lifetime()); + + _voiceRecordBar->recordingStateChanges( + ) | rpl::start_with_next([=](bool active) { + controller()->widget()->setInnerFocus(); }, lifetime()); _voiceRecordBar->hideFast(); @@ -1381,7 +1376,7 @@ AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { const auto result = (isChoosingTheme() || (_inlineBot && !_inlineLookingUpBot)) ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, {}); if (result.query.isEmpty()) { return result; } else if (result.query[0] == '#' @@ -1472,9 +1467,9 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { if (result.open) { const auto request = result.result->openRequest(); if (const auto photo = request.photo()) { - controller()->openPhoto(photo, {}, {}); + controller()->openPhoto(photo, {}); } else if (const auto document = request.document()) { - controller()->openDocument(document, {}, {}); + controller()->openDocument(document, false, {}); } } else { sendInlineResult(result); @@ -1557,7 +1552,9 @@ bool HistoryWidget::updateStickersByEmoji() { return (emoji != nullptr); } -void HistoryWidget::toggleChooseChatTheme(not_null peer) { +void HistoryWidget::toggleChooseChatTheme( + not_null peer, + std::optional show) { const auto update = [=] { updateInlineBotQuery(); updateControlsGeometry(); @@ -1566,7 +1563,7 @@ void HistoryWidget::toggleChooseChatTheme(not_null peer) { if (peer.get() != _peer) { return; } else if (_chooseTheme) { - if (isChoosingTheme()) { + if (isChoosingTheme() && !show.value_or(false)) { const auto was = base::take(_chooseTheme); if (Ui::InFocusChain(this)) { setInnerFocus(); @@ -1574,8 +1571,10 @@ void HistoryWidget::toggleChooseChatTheme(not_null peer) { update(); } return; + } else if (!show.value_or(true)) { + return; } else if (_voiceRecordBar->isActive()) { - controller()->showToast({ tr::lng_chat_theme_cant_voice(tr::now) }); + controller()->showToast(tr::lng_chat_theme_cant_voice(tr::now)); return; } _chooseTheme = std::make_unique( @@ -1742,9 +1741,7 @@ void HistoryWidget::activate() { } void HistoryWidget::setInnerFocus() { - if (_scroll->isHidden()) { - setFocus(); - } else if (_list) { + if (_list) { if (isSearching()) { _composeSearch->setInnerFocus(); } else if (_chooseTheme && _chooseTheme->shouldBeShown()) { @@ -1754,11 +1751,17 @@ void HistoryWidget::setInnerFocus() { || isRecording() || isBotStart() || isBlocked() - || !_canSendTexts) { - _list->setFocus(); + || (!_canSendTexts && !_editMsgId)) { + if (_scroll->isHidden()) { + setFocus(); + } else { + _list->setFocus(); + } } else { _field->setFocus(); } + } else if (_scroll->isHidden()) { + setFocus(); } } @@ -1800,6 +1803,18 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived( return false; } +void HistoryWidget::tryProcessKeyInput(not_null e) { + e->accept(); + keyPressEvent(e); + if (!e->isAccepted() + && _canSendTexts + && _field->isVisible() + && !e->text().isEmpty()) { + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + } +} + void HistoryWidget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { @@ -1866,10 +1881,6 @@ void HistoryWidget::fastShowAtEnd(not_null history) { bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { InvokeQueued(this, [=] { updateStickersByEmoji(); }); - if (_voiceRecordBar->isActive() || !_canSendTexts) { - return false; - } - const auto editDraft = _history ? _history->localEditDraft({}) : nullptr; const auto draft = editDraft ? editDraft @@ -1877,7 +1888,15 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { ? _history->localDraft({}) : nullptr; auto fieldAvailable = canWriteMessage(); - if (!draft || (!_history->localEditDraft({}) && !fieldAvailable)) { + const auto editMsgId = editDraft ? editDraft->msgId : 0; + if (_voiceRecordBar->isActive() || (!_canSendTexts && !editMsgId)) { + if (!_canSendTexts) { + clearFieldText(0, fieldHistoryAction); + } + return false; + } + + if (!draft || (!editDraft && !fieldAvailable)) { auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0); clearFieldText(0, fieldHistoryAction); setInnerFocus(); @@ -1899,19 +1918,9 @@ 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; - if (const auto editDraft = _history->localEditDraft({})) { - setEditMsgId(editDraft->msgId); - } else { - setEditMsgId(0); - } + setEditMsgId(editMsgId); updateCmdStartShown(); updateControlsVisibility(); updateControlsGeometry(); @@ -1926,6 +1935,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; } @@ -2002,18 +2024,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) { @@ -2121,6 +2151,9 @@ void HistoryWidget::showHistory( _saveEditMsgRequestId = 0; _processingReplyItem = _replyEditMsg = nullptr; _processingReplyId = _editMsgId = _replyToId = 0; + _canReplaceMedia = false; + _photoEditMedia = nullptr; + updateReplaceMediaButton(); _previewData = nullptr; _previewCache.clear(); _fieldBarCancel->hide(); @@ -2454,6 +2487,12 @@ void HistoryWidget::registerDraftSource() { void HistoryWidget::setEditMsgId(MsgId msgId) { unregisterDraftSources(); _editMsgId = msgId; + if (_fieldLinksParser && !_editMsgId) { + _fieldLinksParser->setDisabled(false); + } + if (!msgId) { + _canReplaceMedia = false; + } if (_history) { refreshSendAsToggle(); orderWidgets(); @@ -2504,6 +2543,25 @@ void HistoryWidget::clearAllLoadRequests() { } } +bool HistoryWidget::updateReplaceMediaButton() { + if (!_canReplaceMedia) { + const auto result = (_replaceMedia != nullptr); + _replaceMedia.destroy(); + return result; + } else if (_replaceMedia) { + return false; + } + _replaceMedia.create(this, st::historyReplaceMedia); + _replaceMedia->setClickedCallback([=] { + EditCaptionBox::StartMediaReplace( + controller(), + { _history->peer->id, _editMsgId }, + _field->getTextWithTags(), + crl::guard(_list, [=] { cancelEdit(); })); + }); + return true; +} + void HistoryWidget::updateFieldSubmitSettings() { const auto settings = _isInlineBot ? Ui::InputField::SubmitSettings::None @@ -2653,6 +2711,10 @@ void HistoryWidget::updateControlsVisibility() { } if (_firstLoadRequest && !_scroll->isHidden()) { + if (Ui::InFocusChain(_scroll.data())) { + // Don't loose focus back to chats list. + setFocus(); + } _scroll->hide(); } else if (!_firstLoadRequest && _scroll->isHidden()) { _scroll->show(); @@ -2731,6 +2793,9 @@ void HistoryWidget::updateControlsVisibility() { _kbScroll->hide(); _fieldBarCancel->hide(); _attachToggle->hide(); + if (_replaceMedia) { + _replaceMedia->hide(); + } _tabbedSelectorToggle->hide(); _botKeyboardShow->hide(); _botKeyboardHide->hide(); @@ -2758,7 +2823,7 @@ void HistoryWidget::updateControlsVisibility() { _send->show(); updateSendButtonType(); - if (_canSendTexts) { + if (_canSendTexts || _editMsgId) { _field->show(); } else { fieldDisabledRemoved = false; @@ -2795,18 +2860,44 @@ void HistoryWidget::updateControlsVisibility() { _botCommandStart->setVisible(_cmdStartShown); } } - _attachToggle->show(); + if (_replaceMedia) { + _replaceMedia->show(); + _attachToggle->hide(); + } else { + _attachToggle->show(); + } if (_botMenuButton) { _botMenuButton->show(); } - if (_silent) { - _silent->setVisible(!_editMsgId); - } - if (_scheduled) { - _scheduled->show(); - } - if (_ttlInfo) { - _ttlInfo->show(); + { + auto rightButtonsChanged = false; + if (_silent) { + const auto was = _silent->isVisible(); + const auto now = (!_editMsgId); + if (was != now) { + _silent->setVisible(now); + rightButtonsChanged = true; + } + } + if (_scheduled) { + const auto was = _scheduled->isVisible(); + const auto now = (!_editMsgId); + if (was != now) { + _scheduled->setVisible(now); + rightButtonsChanged = true; + } + } + if (_ttlInfo) { + const auto was = _ttlInfo->isVisible(); + const auto now = (!_editMsgId); + if (was != now) { + _ttlInfo->setVisible(now); + rightButtonsChanged = true; + } + } + if (rightButtonsChanged) { + updateFieldSize(); + } } if (_sendAs) { _sendAs->show(); @@ -3049,9 +3140,9 @@ void HistoryWidget::messagesFailed(const MTP::Error &error, int requestId) { auto was = _peer; closeCurrent(); if (const auto primary = Core::App().windowFor(&was->account())) { - controller()->showToast({ (was && was->isMegagroup()) + controller()->showToast((was && was->isMegagroup()) ? tr::lng_group_not_accessible(tr::now) - : tr::lng_channel_not_accessible(tr::now) }); + : tr::lng_channel_not_accessible(tr::now)); } return; } @@ -3685,16 +3776,16 @@ void HistoryWidget::saveEditMsg() { TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); - if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { + if (!TextUtilities::CutPart(sending, left, MaxMessageSize) + && (!item->media() || !item->media()->allowsEditCaption())) { const auto suggestModerateActions = false; controller()->show( Box(item, suggestModerateActions)); return; } else if (!left.text.isEmpty()) { const auto remove = left.text.size(); - controller()->showToast({ - tr::lng_edit_limit_reached(tr::now, lt_count, remove) - }); + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); return; } @@ -3727,14 +3818,14 @@ void HistoryWidget::saveEditMsg() { _saveEditMsgRequestId = 0; } if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { cancelEdit(); } else if (error == u"MESSAGE_EMPTY"_q) { _field->selectAll(); setInnerFocus(); } else { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } update(); })(); @@ -3801,8 +3892,7 @@ void HistoryWidget::hideSelectorControlsAnimated() { Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = 0; + result.replyTo = { .msgId = replyToId() }; result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -3835,7 +3925,7 @@ void HistoryWidget::send(Api::SendOptions options) { ? _previewData->id : WebPageId(0)); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _field->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -3887,8 +3977,11 @@ void HistoryWidget::sendScheduled() { } const auto callback = [=](Api::SendOptions options) { send(options); }; controller()->show( - HistoryView::PrepareScheduleBox(_list, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + HistoryView::PrepareScheduleBox(_list, sendMenuType(), callback)); +} + +void HistoryWidget::sendWhenOnline() { + send(Api::DefaultSendWhenOnlineOptions()); } SendMenu::Type HistoryWidget::sendMenuType() const { @@ -3967,13 +4060,14 @@ void HistoryWidget::reportSelectedMessages() { const auto reason = _chooseForReport->reason; const auto weak = Ui::MakeWeak(_list.data()); controller()->window().show(Box([=](not_null box) { - Ui::ReportDetailsBox(box, [=](const QString &text) { + const auto &st = st::defaultReportBox; + Ui::ReportDetailsBox(box, st, [=](const QString &text) { if (weak) { clearSelected(); controller()->clearChooseReportMessages(); } - const auto toastParent = Window::Show(controller()).toastParent(); - Api::SendReport(toastParent, peer, reason, text, ids); + const auto show = controller()->uiShow(); + Api::SendReport(show, peer, reason, text, ids); box->closeBox(); }); })); @@ -4171,14 +4265,14 @@ void HistoryWidget::finishAnimating() { void HistoryWidget::chooseAttach( std::optional overrideSendImagesAsPhotos) { if (_editMsgId) { - controller()->showToast({ tr::lng_edit_caption_attach(tr::now) }); + controller()->showToast(tr::lng_edit_caption_attach(tr::now)); return; } if (!_peer || !_canSendMessages) { return; } else if (const auto error = Data::AnyFileRestrictionError(_peer)) { - controller()->showToast({ *error }); + controller()->showToast(*error); return; } else if (showSlowmodeError()) { return; @@ -4239,9 +4333,34 @@ void HistoryWidget::mouseMoveEvent(QMouseEvent *e) { } void HistoryWidget::updateOverStates(QPoint pos) { - auto inReplyEditForward = QRect(st::historyReplySkip, _field->y() - st::historySendPadding - st::historyReplyHeight, width() - st::historyReplySkip - _fieldBarCancel->width(), st::historyReplyHeight).contains(pos) && (_editMsgId || replyToId() || readyToForward()); + const auto replyEditForwardInfoRect = QRect( + st::historyReplySkip, + _field->y() - st::historySendPadding - st::historyReplyHeight, + width() - st::historyReplySkip - _fieldBarCancel->width(), + st::historyReplyHeight); + auto inReplyEditForward = (_editMsgId || replyToId() || readyToForward()) + && replyEditForwardInfoRect.contains(pos); + auto inPhotoEdit = inReplyEditForward + && _photoEditMedia + && QRect( + replyEditForwardInfoRect.x(), + replyEditForwardInfoRect.y() + st::msgReplyPadding.top(), + st::msgReplyBarSize.height(), + st::msgReplyBarSize.height()).contains(pos); auto inClickable = inReplyEditForward; - _inReplyEditForward = inReplyEditForward; + if (_inPhotoEdit != inPhotoEdit) { + _inPhotoEdit = inPhotoEdit; + if (_photoEditMedia) { + _inPhotoEditOver.start( + [=] { updateField(); }, + _inPhotoEdit ? 0. : 1., + _inPhotoEdit ? 1. : 0., + st::defaultMessageBar.duration); + } else { + _inPhotoEditOver.stop(); + } + } + _inReplyEditForward = inReplyEditForward && !inPhotoEdit; if (inClickable != _inClickable) { _inClickable = inClickable; setCursor(_inClickable ? style::cur_pointer : style::cur_default); @@ -4280,7 +4399,7 @@ void HistoryWidget::sendBotCommand(const Bot::SendCommandRequest &request) { auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { toSend, TextWithTags::Tags() }; - message.action.replyTo = request.replyTo + message.action.replyTo.msgId = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? request.replyTo : replyToId()) @@ -4842,7 +4961,7 @@ void HistoryWidget::recountChatWidth() { } int HistoryWidget::fieldHeight() const { - return _canSendTexts + return (_canSendTexts || _editMsgId) ? _field->height() : (st::historySendSize.height() - 2 * st::historySendPadding); } @@ -4862,7 +4981,7 @@ void HistoryWidget::moveFieldControls() { _kbScroll->setGeometryToLeft(0, bottom, width(), keyboardHeight); } -// (_botMenuButton) _attachToggle (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel +// (_botMenuButton) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) @@ -4872,6 +4991,9 @@ void HistoryWidget::moveFieldControls() { const auto skip = st::historyBotMenuSkip; _botMenuButton->moveToLeft(left + skip, buttonsBottom + skip); left += skip + _botMenuButton->width(); } + if (_replaceMedia) { + _replaceMedia->moveToLeft(left, buttonsBottom); + } _attachToggle->moveToLeft(left, buttonsBottom); left += _attachToggle->width(); if (_sendAs) { _sendAs->moveToLeft(left, buttonsBottom); left += _sendAs->width(); @@ -4930,19 +5052,33 @@ void HistoryWidget::moveFieldControls() { } void HistoryWidget::updateFieldSize() { - auto kbShowShown = _history && !_kbShown && _keyboard->hasMarkup(); + const auto kbShowShown = _history && !_kbShown && _keyboard->hasMarkup(); auto fieldWidth = width() - _attachToggle->width() - st::historySendRight - _send->width() - _tabbedSelectorToggle->width(); - if (_botMenuButton) fieldWidth -= st::historyBotMenuSkip + _botMenuButton->width(); - if (_sendAs) fieldWidth -= _sendAs->width(); - if (kbShowShown) fieldWidth -= _botKeyboardShow->width(); - if (_cmdStartShown) fieldWidth -= _botCommandStart->width(); - if (_silent && !_silent->isHidden()) fieldWidth -= _silent->width(); - if (_scheduled) fieldWidth -= _scheduled->width(); - if (_ttlInfo) fieldWidth -= _ttlInfo->width(); + if (_botMenuButton) { + fieldWidth -= st::historyBotMenuSkip + _botMenuButton->width(); + } + if (_sendAs) { + fieldWidth -= _sendAs->width(); + } + if (kbShowShown) { + fieldWidth -= _botKeyboardShow->width(); + } + if (_cmdStartShown) { + fieldWidth -= _botCommandStart->width(); + } + if (_silent && _silent->isVisible()) { + fieldWidth -= _silent->width(); + } + if (_scheduled && _scheduled->isVisible()) { + fieldWidth -= _scheduled->width(); + } + if (_ttlInfo && _ttlInfo->isVisible()) { + fieldWidth -= _ttlInfo->width(); + } if (_fieldDisabled) { _fieldDisabled->resize(fieldWidth, fieldHeight()); @@ -5071,10 +5207,11 @@ bool HistoryWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } - controller()->showToast({ text }); + controller()->showToast(text); return true; } @@ -5098,7 +5235,7 @@ bool HistoryWidget::showSendMessageError( if (error.isEmpty()) { return false; } - controller()->showToast({ error }); + controller()->showToast(error); return true; } @@ -5123,7 +5260,16 @@ bool HistoryWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { if (_editMsgId) { - controller()->showToast({ tr::lng_edit_caption_attach(tr::now) }); + if (_canReplaceMedia) { + EditCaptionBox::StartMediaReplace( + controller(), + { _history->peer->id, _editMsgId }, + std::move(list), + _field->getTextWithTags(), + crl::guard(_list, [=] { cancelEdit(); })); + return true; + } + controller()->showToast(tr::lng_edit_caption_attach(tr::now)); return false; } else if (showSendingFilesError(list)) { return false; @@ -5488,12 +5634,13 @@ 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); if (itemTop < 0) { setMsgId(0); - controller()->showToast({ tr::lng_message_not_found(tr::now) }); + controller()->showToast(tr::lng_message_not_found(tr::now)); return countInitialScrollTop(); } else { const auto view = item->mainView(); @@ -6038,9 +6185,16 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { st::historyReplyHeight).contains(e->pos()); if (_replyForwardPressed && !_fieldBarCancel->isHidden()) { updateField(); + } else if (_inPhotoEdit && _photoEditMedia) { + EditCaptionBox::StartPhotoEdit( + controller(), + _photoEditMedia, + { _history->peer->id, _editMsgId }, + _field->getTextWithTags(), + crl::guard(_list, [=] { cancelEdit(); })); } else if (_inReplyEditForward) { if (readyToForward()) { - _forwardPanel->editOptions(controller()); + _forwardPanel->editOptions(controller()->uiShow()); } else { controller()->showPeerHistory( _peer, @@ -6055,7 +6209,11 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { const auto commonModifiers = e->modifiers() & kCommonModifiers; if (e->key() == Qt::Key_Escape) { - e->ignore(); + if (hasFocus()) { + escape(); + } else { + e->ignore(); + } } else if (e->key() == Qt::Key_Back) { _cancelRequests.fire({}); } else if (e->key() == Qt::Key_PageDown) { @@ -6076,6 +6234,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(); @@ -6132,20 +6300,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; @@ -6155,13 +6331,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); @@ -6193,7 +6375,7 @@ bool HistoryWidget::showSlowmodeError() { if (text.isEmpty()) { return false; } - controller()->showToast({ text }); + controller()->showToast(text); return true; } @@ -6214,7 +6396,7 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { auto errorText = result.result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - controller()->showToast({ errorText }); + controller()->showToast(errorText); return; } @@ -6428,6 +6610,8 @@ void HistoryWidget::checkPinnedBarState() { _list->setShownPinned( session().data().message( _pinnedTracker->currentMessageId().message)); + } else { + _list->setShownPinned(nullptr); } return std::move(content); })); @@ -6500,7 +6684,7 @@ void HistoryWidget::checkMessagesTTL() { } else if (!_ttlInfo || _ttlInfo->peer() != _peer) { _ttlInfo = std::make_unique( this, - std::make_shared(controller()), + controller()->uiShow(), _peer); orderWidgets(); updateControlsGeometry(); @@ -6721,7 +6905,7 @@ bool HistoryWidget::sendExistingDocument( ? Data::RestrictionError(_peer, ChatRestriction::SendStickers) : std::nullopt; if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } else if (!_peer || !_canSendMessages @@ -6756,7 +6940,7 @@ bool HistoryWidget::sendExistingPhoto( ? Data::RestrictionError(_peer, ChatRestriction::SendPhotos) : std::nullopt; if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } else if (!_peer || !_canSendMessages) { return false; @@ -6872,7 +7056,7 @@ void HistoryWidget::processReply() { return; } else if (_processingReplyItem->history() == _migrated) { if (_processingReplyItem->isService()) { - controller()->showToast({ tr::lng_reply_cant(tr::now) }); + controller()->showToast(tr::lng_reply_cant(tr::now)); } else { const auto itemId = _processingReplyItem->fullId(); controller()->show( @@ -6957,15 +7141,10 @@ void HistoryWidget::editMessage(FullMsgId itemId) { } void HistoryWidget::editMessage(not_null item) { - if (const auto media = item->media()) { - if (media->allowsEditCaption()) { - controller()->show(Box(controller(), item)); - return; - } - } else if (_chooseTheme) { + if (_chooseTheme) { toggleChooseChatTheme(_peer); } else if (_voiceRecordBar->isActive()) { - controller()->showToast({ tr::lng_edit_caption_voice(tr::now) }); + controller()->showToast(tr::lng_edit_caption_voice(tr::now)); return; } else if (_composeSearch) { _composeSearch->hideAnimated(); @@ -7132,6 +7311,9 @@ void HistoryWidget::cancelEdit() { return; } + _canReplaceMedia = false; + _photoEditMedia = nullptr; + updateReplaceMediaButton(); _replyEditMsg = nullptr; setEditMsgId(0); _history->clearLocalEditDraft({}); @@ -7359,7 +7541,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); } } @@ -7443,7 +7625,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) { @@ -7540,7 +7722,7 @@ void HistoryWidget::updateTopBarSelection() { || isRecording() || isBotStart() || isBlocked() - || !_canSendTexts) { + || (!_canSendTexts && !_editMsgId)) { _list->setFocus(); } else { _field->setFocus(); @@ -7588,6 +7770,22 @@ void HistoryWidget::updateReplyEditTexts(bool force) { _editMsgId ? _editMsgId : _replyToId); } if (_replyEditMsg) { + const auto media = _replyEditMsg->media(); + _canReplaceMedia = media && media->allowsEditMedia(); + _photoEditMedia = (_canReplaceMedia + && media->photo() + && !media->photo()->isNull()) + ? media->photo()->createMediaView() + : nullptr; + if (_photoEditMedia) { + _photoEditMedia->wanted( + Data::PhotoSize::Large, + _replyEditMsg->fullId()); + } + if (updateReplaceMediaButton()) { + updateControlsVisibility(); + updateControlsGeometry(); + } updateReplyEditText(_replyEditMsg); updateBotKeyboard(); updateReplyToName(); @@ -7695,6 +7893,9 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { if (drawMsgText) { if (hasPreview) { if (preview) { + const auto overEdit = _photoEditMedia + ? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.) + : 0.; auto to = QRect(replyLeft, backy + st::msgReplyPadding.top(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height()); p.drawPixmap(to.x(), to.y(), preview->pixSingle( preview->size() / style::DevicePixelRatio(), @@ -7703,12 +7904,21 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { .outer = to.size(), })); if (_replySpoiler) { + if (overEdit > 0.) { + p.setOpacity(1. - overEdit); + } Ui::FillSpoilerRect( p, to, Ui::DefaultImageSpoiler().frame( _replySpoiler->index(now, pausedSpoiler))); } + if (overEdit > 0.) { + p.setOpacity(overEdit); + p.fillRect(to, st::historyEditMediaBg); + st::historyEditMedia.paintInCenter(p, to); + p.setOpacity(1.); + } } replyLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index dd19eb3bd..ae1f9bf54 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -31,6 +31,7 @@ class Error; namespace Data { enum class PreviewState : char; +class PhotoMedia; } // namespace Data namespace SendMenu { @@ -145,6 +146,7 @@ public: void firstLoadMessages(); void delayedShowAt(MsgId showAtMsgId); + bool updateReplaceMediaButton(); void updateFieldPlaceholder(); bool updateStickersByEmoji(); @@ -224,7 +226,9 @@ public: void clearDelayedShowAtRequest(); void clearDelayedShowAt(); - void toggleChooseChatTheme(not_null peer); + void toggleChooseChatTheme( + not_null peer, + std::optional show = std::nullopt); [[nodiscard]] Ui::ChatTheme *customChatTheme() const; void applyCloudDraft(History *history); @@ -276,6 +280,8 @@ public: bool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo); + void tryProcessKeyInput(not_null e); + ~HistoryWidget(); protected: @@ -375,6 +381,7 @@ private: void sendWithModifiers(Qt::KeyboardModifiers modifiers); void sendSilent(); void sendScheduled(); + void sendWhenOnline(); [[nodiscard]] SendMenu::Type sendButtonMenuType() const; void handlePendingHistoryUpdate(); void fullInfoUpdated(); @@ -634,6 +641,8 @@ private: HistoryItem *_processingReplyItem = nullptr; MsgId _editMsgId = 0; + std::shared_ptr _photoEditMedia; + bool _canReplaceMedia = false; HistoryItem *_replyEditMsg = nullptr; Ui::Text::String _replyEditMsgText; @@ -732,6 +741,7 @@ private: object_ptr _botMenuButton = { nullptr }; QString _botMenuButtonText; object_ptr _attachToggle; + object_ptr _replaceMedia = { nullptr }; object_ptr _sendAs = { nullptr }; object_ptr _tabbedSelectorToggle; object_ptr _botKeyboardShow; @@ -746,7 +756,9 @@ private: bool _cmdStartShown = false; object_ptr _field; base::unique_qptr _fieldDisabled; + Ui::Animations::Simple _inPhotoEditOver; bool _inReplyEditForward = false; + bool _inPhotoEdit = false; bool _inClickable = false; bool _kbShown = false; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index f32132ccb..8af04e830 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -25,7 +25,7 @@ struct MessageToEdit { struct VoiceToSend { QByteArray bytes; VoiceWaveform waveform; - int duration = 0; + crl::time duration = 0; Api::SendOptions options; }; struct SendActionUpdate { @@ -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 faf3626c8..e6a4717d8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" +#include "boxes/edit_caption_box.h" +#include "chat_helpers/compose/compose_show.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "menu/menu_send.h" @@ -29,8 +31,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_file_origin.h" #include "data/data_forum_topic.h" #include "data/data_peer_values.h" +#include "data/data_photo_media.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_web_page.h" @@ -54,7 +58,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/session/send_as_peers.h" #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" -#include "styles/style_chat.h" #include "ui/text/text_options.h" #include "ui/ui_utility.h" #include "ui/widgets/input_fields.h" @@ -68,6 +71,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -124,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; @@ -214,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() { @@ -337,12 +346,12 @@ class FieldHeader final : public Ui::RpWidget { public: FieldHeader( QWidget *parent, - not_null controller); + std::shared_ptr show); void setHistory(const SetHistoryArgs &args); void init(); - void editMessage(FullMsgId id); + void editMessage(FullMsgId id, bool photoEditAllowed = false); void replyToMessage(FullMsgId id); void updateForwarding( Data::Thread *thread, @@ -351,14 +360,17 @@ public: rpl::producer title, rpl::producer description, rpl::producer page); + void previewUnregister(); [[nodiscard]] bool isDisplayed() const; [[nodiscard]] bool isEditingMessage() const; [[nodiscard]] bool readyToForward() const; [[nodiscard]] const HistoryItemsList &forwardItems() const; [[nodiscard]] FullMsgId replyingToMessage() const; - [[nodiscard]] rpl::producer editMsgId() const; + [[nodiscard]] FullMsgId editMsgId() const; + [[nodiscard]] rpl::producer editMsgIdValue() const; [[nodiscard]] rpl::producer scrollToItemRequests() const; + [[nodiscard]] rpl::producer<> editPhotoRequests() const; [[nodiscard]] MessageToEdit queryToEdit(); [[nodiscard]] WebPageId webPageId() const; @@ -399,7 +411,7 @@ private: bool cancelled = false; }; - const not_null _controller; + const std::shared_ptr _show; History *_history = nullptr; rpl::variable _title; rpl::variable _description; @@ -409,6 +421,7 @@ private: rpl::event_stream<> _replyCancelled; rpl::event_stream<> _forwardCancelled; rpl::event_stream<> _previewCancelled; + rpl::lifetime _previewLifetime; rpl::variable _editMsgId; rpl::variable _replyToId; @@ -418,27 +431,35 @@ private: HistoryItem *_shownMessage = nullptr; Ui::Text::String _shownMessageName; Ui::Text::String _shownMessageText; + std::unique_ptr _shownPreviewSpoiler; + Ui::Animations::Simple _inPhotoEditOver; int _shownMessageNameVersion = -1; - bool _repaintScheduled = false; + bool _shownMessageHasPreview : 1 = false; + bool _inPhotoEdit : 1 = false; + bool _photoEditAllowed : 1 = false; + bool _repaintScheduled : 1 = false; + bool _inClickable : 1 = false; const not_null _data; const not_null _cancel; QRect _clickableRect; + QRect _shownMessagePreviewRect; rpl::event_stream _visibleChanged; rpl::event_stream _scrollToItemRequests; + rpl::event_stream<> _editPhotoRequests; }; FieldHeader::FieldHeader( QWidget *parent, - not_null controller) + std::shared_ptr show) : RpWidget(parent) -, _controller(controller) +, _show(std::move(show)) , _forwardPanel( std::make_unique([=] { customEmojiRepaint(); })) -, _data(&controller->session().data()) +, _data(&_show->session().data()) , _cancel(Ui::CreateChild(this, st::historyReplyCancel)) { resize(QSize(parent->width(), st::historyReplyHeight)); init(); @@ -463,8 +484,7 @@ void FieldHeader::init() { paintRequest( ) | rpl::start_with_next([=] { Painter p(this); - p.setInactive( - _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)); + p.setInactive(_show->paused(Window::GifPauseReason::Any)); p.fillRect(rect(), st::historyComposeAreaBg); const auto position = st::historyReplyIconPosition; @@ -548,26 +568,45 @@ void FieldHeader::init() { }, lifetime()); setMouseTracking(true); - const auto inClickable = lifetime().make_state(false); events( ) | rpl::filter([=](not_null event) { - return ranges::contains(kMouseEvents, event->type()) + const auto type = event->type(); + const auto leaving = (type == QEvent::Leave); + return (ranges::contains(kMouseEvents, type) || leaving) && (isEditingMessage() || readyToForward() || replyingToMessage()); }) | rpl::start_with_next([=](not_null event) { - const auto type = event->type(); - const auto e = static_cast(event.get()); - const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos()); - const auto inPreviewRect = _clickableRect.contains(pos); - - if (type == QEvent::MouseMove) { - if (inPreviewRect != *inClickable) { - *inClickable = inPreviewRect; - setCursor(*inClickable + const auto updateOver = [&](bool inClickable, bool inPhotoEdit) { + if (_inClickable != inClickable) { + _inClickable = inClickable; + setCursor(_inClickable ? style::cur_pointer : style::cur_default); } + if (_inPhotoEdit != inPhotoEdit) { + _inPhotoEdit = inPhotoEdit; + _inPhotoEditOver.start( + [=] { update(); }, + _inPhotoEdit ? 0. : 1., + _inPhotoEdit ? 1. : 0., + st::defaultMessageBar.duration); + } + }; + const auto type = event->type(); + if (type == QEvent::Leave) { + updateOver(false, false); + return; + } + const auto e = static_cast(event.get()); + const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos()); + const auto inPreviewRect = _clickableRect.contains(pos); + const auto inPhotoEdit = _shownMessageHasPreview + && _photoEditAllowed + && _shownMessagePreviewRect.contains(pos); + + if (type == QEvent::MouseMove) { + updateOver(inPreviewRect, inPhotoEdit); return; } const auto isLeftIcon = (pos.x() < st::historyReplySkip); @@ -576,9 +615,11 @@ void FieldHeader::init() { if (isLeftButton && isLeftIcon) { *leftIconPressed = true; update(); + } else if (isLeftButton && inPhotoEdit) { + _editPhotoRequests.fire({}); } else if (isLeftButton && inPreviewRect) { if (!isEditingMessage() && readyToForward()) { - _forwardPanel->editOptions(_controller); + _forwardPanel->editOptions(_show); } else { auto id = isEditingMessage() ? _editMsgId.current() @@ -674,17 +715,18 @@ void FieldHeader::resolveMessageData() { } void FieldHeader::previewRequested( - rpl::producer title, - rpl::producer description, - rpl::producer page) { + rpl::producer title, + rpl::producer description, + rpl::producer page) { + _previewLifetime.destroy(); std::move( title ) | rpl::filter([=] { return !_preview.cancelled; - }) | start_with_next([=](const QString &t) { + }) | rpl::start_with_next([=](const QString &t) { _title = t; - }, lifetime()); + }, _previewLifetime); std::move( description @@ -692,7 +734,7 @@ void FieldHeader::previewRequested( return !_preview.cancelled; }) | rpl::start_with_next([=](const QString &d) { _description = d; - }, lifetime()); + }, _previewLifetime); std::move( page @@ -701,8 +743,11 @@ void FieldHeader::previewRequested( }) | rpl::start_with_next([=](WebPageData *p) { _preview.data = p; updateVisible(); - }, lifetime()); + }, _previewLifetime); +} +void FieldHeader::previewUnregister() { + _previewLifetime.destroy(); } void FieldHeader::paintWebPage(Painter &p, not_null context) { @@ -784,20 +829,74 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { } } + const auto media = _shownMessage->media(); + _shownMessageHasPreview = media && media->hasReplyPreview(); + const auto preview = _shownMessageHasPreview + ? media->replyPreview() + : nullptr; + const auto spoilered = preview && media->hasSpoiler(); + if (!spoilered) { + _shownPreviewSpoiler = nullptr; + } else if (!_shownPreviewSpoiler) { + _shownPreviewSpoiler = std::make_unique([=] { + update(); + }); + } + const auto previewSkipValue = st::msgReplyBarSize.height() + + st::msgReplyBarSkip + - st::msgReplyBarSize.width() + - st::msgReplyBarPos.x(); + const auto previewSkip = _shownMessageHasPreview ? previewSkipValue : 0; + const auto textLeft = replySkip + previewSkip; + const auto textAvailableWidth = availableWidth - previewSkip; + if (preview) { + const auto overEdit = _photoEditAllowed + ? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.) + : 0.; + const auto to = QRect( + replySkip, + st::msgReplyPadding.top(), + st::msgReplyBarSize.height(), + st::msgReplyBarSize.height()); + p.drawPixmap(to.x(), to.y(), preview->pixSingle( + preview->size() / style::DevicePixelRatio(), + { + .options = Images::Option::RoundSmall, + .outer = to.size(), + })); + if (_shownPreviewSpoiler) { + if (overEdit > 0.) { + p.setOpacity(1. - overEdit); + } + Ui::FillSpoilerRect( + p, + to, + Ui::DefaultImageSpoiler().frame( + _shownPreviewSpoiler->index(crl::now(), p.inactive()))); + } + if (overEdit > 0.) { + p.setOpacity(overEdit); + p.fillRect(to, st::historyEditMediaBg); + st::historyEditMedia.paintInCenter(p, to); + p.setOpacity(1.); + } + + } + p.setPen(st::historyReplyNameFg); p.setFont(st::msgServiceNameFont); _shownMessageName.drawElided( p, - replySkip, + textLeft, st::msgReplyPadding.top(), - availableWidth); + textAvailableWidth); p.setPen(st::historyComposeAreaFg); _shownMessageText.draw(p, { .position = QPoint( - replySkip, + textLeft, st::msgReplyPadding.top() + st::msgServiceNameFont->height), - .availableWidth = availableWidth, + .availableWidth = textAvailableWidth, .palette = &st::historyComposeAreaPalette, .spoiler = Ui::Text::DefaultSpoilerCache(), .now = crl::now(), @@ -838,6 +937,10 @@ bool FieldHeader::isEditingMessage() const { return !!_editMsgId.current(); } +FullMsgId FieldHeader::editMsgId() const { + return _editMsgId.current(); +} + bool FieldHeader::readyToForward() const { return !_forwardPanel->empty(); } @@ -869,10 +972,21 @@ void FieldHeader::updateControlsGeometry(QSize size) { 0, width() - st::historyReplySkip - _cancel->width(), height()); + _shownMessagePreviewRect = QRect( + st::historyReplySkip, + st::msgReplyPadding.top(), + st::msgReplyBarSize.height(), + st::msgReplyBarSize.height()); } -void FieldHeader::editMessage(FullMsgId id) { +void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { + _photoEditAllowed = photoEditAllowed; _editMsgId = id; + if (!photoEditAllowed) { + _inPhotoEdit = false; + _inPhotoEditOver.stop(); + } + update(); } void FieldHeader::replyToMessage(FullMsgId id) { @@ -888,7 +1002,7 @@ void FieldHeader::updateForwarding( } } -rpl::producer FieldHeader::editMsgId() const { +rpl::producer FieldHeader::editMsgIdValue() const { return _editMsgId.value(); } @@ -896,6 +1010,10 @@ rpl::producer FieldHeader::scrollToItemRequests() const { return _scrollToItemRequests.events(); } +rpl::producer<> FieldHeader::editPhotoRequests() const { + return _editPhotoRequests.events(); +} + MessageToEdit FieldHeader::queryToEdit() { const auto item = _data->message(_editMsgId.current()); if (!isEditingMessage() || !item) { @@ -912,45 +1030,98 @@ MessageToEdit FieldHeader::queryToEdit() { ComposeControls::ComposeControls( not_null parent, - not_null window, + not_null controller, Fn)> unavailableEmojiPasted, Mode mode, SendMenu::Type sendMenuType) -: _parent(parent) -, _window(window) -, _mode(mode) +: ComposeControls(parent, ComposeControlsDescriptor{ + .show = controller->uiShow(), + .unavailableEmojiPasted = std::move(unavailableEmojiPasted), + .mode = mode, + .sendMenuType = sendMenuType, + .regularWindow = controller, + .stickerOrEmojiChosen = controller->stickerOrEmojiChosen(), +}) { +} + +ComposeControls::ComposeControls( + not_null parent, + ComposeControlsDescriptor descriptor) +: _st(descriptor.stOverride + ? *descriptor.stOverride + : st::defaultComposeControls) +, _features(descriptor.features) +, _parent(parent) +, _show(std::move(descriptor.show)) +, _session(&_show->session()) +, _regularWindow(descriptor.regularWindow) +, _ownedSelector(_regularWindow + ? nullptr + : std::make_unique( + _parent, + ChatHelpers::TabbedSelectorDescriptor{ + .show = _show, + .st = _st.tabbed, + .level = Window::GifPauseReason::TabbedPanel, + .mode = ChatHelpers::TabbedSelector::Mode::Full, + .features = _features, + })) +, _selector(_regularWindow + ? _regularWindow->tabbedSelector() + : not_null(_ownedSelector.get())) +, _mode(descriptor.mode) , _wrap(std::make_unique(parent)) , _writeRestricted(std::make_unique(parent)) -, _send(std::make_shared(_wrap.get())) -, _attachToggle(Ui::CreateChild( - _wrap.get(), - st::historyAttach)) +, _send(std::make_shared(_wrap.get(), _st.send)) +, _like(_features.likes + ? Ui::CreateChild(_wrap.get(), _st.like) + : nullptr) +, _attachToggle(Ui::CreateChild(_wrap.get(), _st.attach)) , _tabbedSelectorToggle(Ui::CreateChild( _wrap.get(), - st::historyAttachEmoji)) + _st.emoji)) +, _fieldCustomPlaceholder(std::move(descriptor.customPlaceholder)) , _field( Ui::CreateChild( _wrap.get(), - st::historyComposeField, + _st.field, Ui::InputField::Mode::MultiLine, - tr::lng_message_ph())) -, _botCommandStart(Ui::CreateChild( - _wrap.get(), - st::historyBotCommandStart)) + (_fieldCustomPlaceholder + ? rpl::duplicate(_fieldCustomPlaceholder) + : tr::lng_message_ph()))) +, _botCommandStart(_features.botCommandSend + ? Ui::CreateChild( + _wrap.get(), + st::historyBotCommandStart) + : nullptr) , _autocomplete(std::make_unique( parent, - window)) -, _header(std::make_unique(_wrap.get(), _window)) + _show, + &_st.tabbed)) +, _header(std::make_unique(_wrap.get(), _show)) , _voiceRecordBar(std::make_unique( _wrap.get(), - parent, - window, - _send, - st::historySendSize.height())) -, _sendMenuType(sendMenuType) -, _unavailableEmojiPasted(unavailableEmojiPasted) + Controls::VoiceRecordBarDescriptor{ + .outerContainer = parent, + .show = _show, + .send = _send, + .customCancelText = descriptor.voiceCustomCancelText, + .stOverride = &_st.record, + .recorderHeight = st::historySendSize.height(), + .lockFromBottom = descriptor.voiceLockFromBottom, + })) +, _sendMenuType(descriptor.sendMenuType) +, _unavailableEmojiPasted(std::move(descriptor.unavailableEmojiPasted)) , _saveDraftTimer([=] { saveDraft(); }) , _saveCloudDraftTimer([=] { saveCloudDraft(); }) { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } + if (descriptor.stickerOrEmojiChosen) { + std::move( + descriptor.stickerOrEmojiChosen + ) | rpl::start_to_stream(_stickerOrEmojiChosen, _wrap->lifetime()); + } init(); } @@ -962,20 +1133,17 @@ ComposeControls::~ComposeControls() { } Main::Session &ComposeControls::session() const { - return _window->session(); + return _show->session(); } void ComposeControls::setHistory(SetHistoryArgs &&args) { - // Right now only single non-null set of history is supported. - // Otherwise initWebpageProcess should be updated / rewritten. - Expects(!_history && (*args.history)); - _showSlowmodeError = std::move(args.showSlowmodeError); _sendActionFactory = std::move(args.sendActionFactory); _slowmodeSecondsLeft = rpl::single(0) | 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; @@ -984,21 +1152,25 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { } unregisterDraftSources(); _history = history; + _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); - _window->tabbedSelector()->setCurrentPeer( - history ? history->peer.get() : nullptr); + _selector->setCurrentPeer(history ? history->peer.get() : nullptr); initWebpageProcess(); initForwardProcess(); updateBotCommandShown(); + updateLikeShown(); updateMessagesTTLShown(); updateControlsGeometry(_wrap->size()); updateControlsVisibility(); updateFieldPlaceholder(); updateAttachBotsMenu(); - //if (!_history) { - // return; - //} + + _sendAs = nullptr; + _silent = nullptr; + if (!_history) { + return; + } const auto peer = _history->peer; initSendAsButton(peer); if (peer->isChat() && peer->asChat()->noParticipantInfo()) { @@ -1066,13 +1238,25 @@ const HistoryItemsList &ComposeControls::forwardItems() const { } bool ComposeControls::focus() { - if (isRecording()) { + if (_wrap->isHidden() || _field->isHidden() || isRecording()) { return false; } _field->setFocus(); return true; } +bool ComposeControls::focused() const { + return Ui::InFocusChain(_wrap.get()); +} + +rpl::producer ComposeControls::focusedValue() const { + return rpl::single(focused()) | rpl::then(_focusChanges.events()); +} + +rpl::producer ComposeControls::tabbedPanelShownValue() const { + return _tabbedPanel ? _tabbedPanel->shownValue() : rpl::single(false); +} + rpl::producer<> ComposeControls::cancelRequests() const { return _cancelRequests.events(); } @@ -1092,6 +1276,10 @@ auto ComposeControls::replyNextRequests() const return _replyNextRequests.events(); } +rpl::producer<> ComposeControls::focusRequests() const { + return _focusRequests.events(); +} + auto ComposeControls::sendContentRequests(SendRequestType requestType) const { auto filter = rpl::filter([=] { const auto type = (_mode == Mode::Normal) @@ -1143,7 +1331,8 @@ rpl::producer> ComposeControls::attachRequests() const { _attachRequests.events() ) | rpl::filter([=] { if (isEditingMessage()) { - _window->show(Ui::MakeInformBox(tr::lng_edit_caption_attach())); + _show->showBox( + Ui::MakeInformBox(tr::lng_edit_caption_attach())); return false; } return true; @@ -1154,6 +1343,22 @@ void ComposeControls::setMimeDataHook(MimeDataHook hook) { _field->setMimeDataHook(std::move(hook)); } +bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { + if (!isEditingMessage() || !_regularWindow) { + return false; + } else if (_canReplaceMedia) { + EditCaptionBox::StartMediaReplace( + _regularWindow, + _editingId, + std::move(list), + _field->getTextWithTags(), + crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); + } else { + _show->showToast(tr::lng_edit_caption_attach(tr::now)); + } + return true; +} + rpl::producer ComposeControls::fileChosen() const { return _fileChosen.events(); } @@ -1313,7 +1518,7 @@ void ComposeControls::checkAutocomplete() { const auto peer = _history->peer; const auto autocomplete = _isInlineBot ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); + : ParseMentionHashtagBotCommandQuery(_field, _features); if (!autocomplete.query.isEmpty()) { if (autocomplete.query[0] == '#' && cRecentWriteHashtags().isEmpty() @@ -1360,7 +1565,18 @@ void ComposeControls::init() { updateWrappingVisibility(); }, _wrap->lifetime()); - _botCommandStart->setClickedCallback([=] { setText({ "/" }); }); + if (_botCommandStart) { + _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) { @@ -1377,7 +1593,7 @@ void ComposeControls::init() { paintBackground(clip); }, _wrap->lifetime()); - _header->editMsgId( + _header->editMsgIdValue( ) | rpl::start_with_next([=](const auto &id) { unregisterDraftSources(); updateSendButtonType(); @@ -1389,6 +1605,16 @@ void ComposeControls::init() { registerDraftSource(); }, _wrap->lifetime()); + _header->editPhotoRequests( + ) | rpl::start_with_next([=] { + EditCaptionBox::StartPhotoEdit( + _regularWindow, + _photoEditMedia, + _editingId, + _field->getTextWithTags(), + crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); + }, _wrap->lifetime()); + _header->previewCancelled( ) | rpl::start_with_next([=] { if (_preview) { @@ -1428,25 +1654,19 @@ void ComposeControls::init() { _voiceRecordBar->requestToSendWithOptions(options); }, _wrap->lifetime()); - { - const auto lastMsgId = _wrap->lifetime().make_state(); + _header->editMsgIdValue( + ) | rpl::start_with_next([=](const auto &id) { + _editingId = id; + }, _wrap->lifetime()); - _header->editMsgId( - ) | rpl::filter([=](const auto &id) { - return !!id; - }) | rpl::start_with_next([=](const auto &id) { - *lastMsgId = id; - }, _wrap->lifetime()); + session().data().itemRemoved( + ) | rpl::filter([=](not_null item) { + return (_editingId == item->fullId()); + }) | rpl::start_with_next([=] { + cancelEditMessage(); + }, _wrap->lifetime()); - session().data().itemRemoved( - ) | rpl::filter([=](not_null item) { - return item->id && ((*lastMsgId) == item->fullId()); - }) | rpl::start_with_next([=] { - cancelEditMessage(); - }, _wrap->lifetime()); - } - - _window->materializeLocalDraftsRequests( + Core::App().materializeLocalDraftsRequests( ) | rpl::start_with_next([=] { saveFieldToHistoryLocalDraft(); }, _wrap->lifetime()); @@ -1546,6 +1766,8 @@ void ComposeControls::initKeyHandler() { }); return Result::Cancel; } + } else if (k->key() == Qt::Key_Escape) { + return Result::Cancel; } return Result::Continue; }); @@ -1558,9 +1780,14 @@ void ComposeControls::initField() { Ui::Connect(_field, &Ui::InputField::cancelled, [=] { escape(); }); Ui::Connect(_field, &Ui::InputField::tabbed, [=] { fieldTabbed(); }); Ui::Connect(_field, &Ui::InputField::resized, [=] { updateHeight(); }); - //Ui::Connect(_field, &Ui::InputField::focused, [=] { fieldFocused(); }); + Ui::Connect(_field, &Ui::InputField::focused, [=] { + _focusChanges.fire(true); + }); + Ui::Connect(_field, &Ui::InputField::blurred, [=] { + _focusChanges.fire(false); + }); Ui::Connect(_field, &Ui::InputField::changed, [=] { fieldChanged(); }); - InitMessageField(_window, _field, [=](not_null emoji) { + InitMessageField(_show, _field, [=](not_null emoji) { if (_history && Data::AllowEmojiWithoutPremium(_history->peer)) { return true; } @@ -1569,6 +1796,8 @@ void ComposeControls::initField() { } return false; }); + _field->setEditLinkCallback( + DefaultEditLinkCallback(_show, _field, &_st.boxField)); initAutocomplete(); const auto allow = [=](const auto &) { return _history && Data::AllowEmojiWithoutPremium(_history->peer); @@ -1576,8 +1805,12 @@ void ComposeControls::initField() { const auto suggestions = Ui::Emoji::SuggestionsController::Init( _parent, _field, - &_window->session(), - { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow }); + _session, + { + .suggestCustomEmoji = true, + .allowCustomWithoutPremium = allow, + .st = &_st.suggestions, + }); _raiseEmojiSuggestions = [=] { suggestions->raise(); }; const auto rawTextEdit = _field->rawTextEdit().get(); @@ -1665,7 +1898,7 @@ void ComposeControls::initAutocomplete() { _field->rawTextEdit()->installEventFilter(_autocomplete.get()); - _window->session().data().botCommandsChanges( + _session->data().botCommandsChanges( ) | rpl::filter([=](not_null peer) { return _history && (_history->peer == peer); }) | rpl::start_with_next([=] { @@ -1674,7 +1907,7 @@ void ComposeControls::initAutocomplete() { } }, _autocomplete->lifetime()); - _window->session().data().stickers().updated( + _session->data().stickers().updated( Data::StickersType::Stickers ) | rpl::start_with_next([=] { updateStickersByEmoji(); @@ -1722,7 +1955,9 @@ void ComposeControls::updateFieldPlaceholder() { } _field->setPlaceholder([&] { - if (isEditingMessage()) { + if (_fieldCustomPlaceholder) { + return rpl::duplicate(_fieldCustomPlaceholder); + } else if (isEditingMessage()) { return tr::lng_edit_message_text(); } else if (!_history) { return tr::lng_message_ph(); @@ -1760,10 +1995,11 @@ void ComposeControls::fieldChanged() { && !_header->isEditingMessage() && (_textUpdateEvents & TextUpdateEvent::SendTyping)); updateSendButtonType(); - if (!HasSendText(_field) && _preview) { + _hasSendText = HasSendText(_field); + if (!_hasSendText.current() && _preview) { _preview->setState(Data::PreviewState::Allowed); } - if (updateBotCommandShown()) { + if (updateBotCommandShown() || updateLikeShown()) { updateControlsVisibility(); updateControlsGeometry(_wrap->size()); } @@ -1907,38 +2143,92 @@ void ComposeControls::applyCloudDraft() { void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { Expects(_history != nullptr); - InvokeQueued(_autocomplete.get(), [=] { updateStickersByEmoji(); }); - const auto guard = gsl::finally([&] { - updateSendButtonType(); - updateControlsVisibility(); - updateControlsGeometry(_wrap->size()); - }); - const auto editDraft = _history->draft(draftKey(DraftType::Edit)); const auto draft = editDraft ? editDraft : _history->draft(draftKey(DraftType::Normal)); + const auto editingId = (draft == editDraft) + ? FullMsgId{ _history->peer->id, draft ? draft->msgId : 0 } + : FullMsgId(); + + InvokeQueued(_autocomplete.get(), [=] { updateStickersByEmoji(); }); + const auto guard = gsl::finally([&] { + updateSendButtonType(); + updateReplaceMediaButton(); + updateControlsVisibility(); + updateControlsGeometry(_wrap->size()); + }); + + const auto hadFocus = Ui::InFocusChain(_field); if (!draft) { clearFieldText(0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } _header->editMessage({}); _header->replyToMessage({}); + _preview->refreshState(Data::PreviewState::Allowed, false); + _canReplaceMedia = false; + _photoEditMedia = nullptr; return; } _textUpdateEvents = 0; setFieldText(draft->textWithTags, 0, fieldHistoryAction); - _field->setFocus(); + if (hadFocus) { + _field->setFocus(); + } 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) { - _header->editMessage({ _history->peer->id, draft->msgId }); + 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 + && media->photo() + && !media->photo()->isNull()) + ? media->photo()->createMediaView() + : nullptr; + if (_photoEditMedia) { + _photoEditMedia->wanted( + Data::PhotoSize::Large, + item->fullId()); + } + _header->editMessage(editingId, _photoEditMedia != nullptr); + _preview->refreshState(_preview->state(), disablePreview); + return true; + } + _canReplaceMedia = false; + _photoEditMedia = nullptr; + _header->editMessage(editingId, false); + return false; + }; + if (!resolve()) { + const auto callback = crl::guard(_header.get(), [=] { + if (_header->editMsgId() == editingId + && resolve() + && updateReplaceMediaButton()) { + updateControlsVisibility(); + updateControlsGeometry(_wrap->size()); + } + }); + _history->session().api().requestMessageData( + _history->peer, + editingId.msg, + callback); + } _header->replyToMessage({}); } else { + _canReplaceMedia = false; + _photoEditMedia = nullptr; _header->replyToMessage({ _history->peer->id, draft->msgId }); if (_header->replyingToMessage()) { cancelForward(); @@ -1967,7 +2257,7 @@ rpl::producer ComposeControls::sendActionUpdates() const { } void ComposeControls::initTabbedSelector() { - if (_window->hasTabbedSelectorOwnership()) { + if (!_regularWindow || _regularWindow->hasTabbedSelectorOwnership()) { createTabbedPanel(); } else { setTabbedPanel(nullptr); @@ -1981,25 +2271,24 @@ void ComposeControls::initTabbedSelector() { } }); - const auto selector = _window->tabbedSelector(); const auto wrap = _wrap.get(); - base::install_event_filter(wrap, selector, [=](not_null e) { + base::install_event_filter(wrap, _selector, [=](not_null e) { if (_tabbedPanel && e->type() == QEvent::ParentChange) { setTabbedPanel(nullptr); } return base::EventFilterResult::Continue; }); - selector->emojiChosen( + _selector->emojiChosen( ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji); }, wrap->lifetime()); rpl::merge( - selector->fileChosen(), - selector->customEmojiChosen(), - _window->stickerOrEmojiChosen() + _selector->fileChosen(), + _selector->customEmojiChosen(), + _stickerOrEmojiChosen.events() ) | rpl::start_with_next([=](ChatHelpers::FileChosen &&data) { if (const auto info = data.document->sticker() ; info && info->setType == Data::StickersType::Emoji) { @@ -2018,18 +2307,18 @@ void ComposeControls::initTabbedSelector() { } }, wrap->lifetime()); - selector->photoChosen( + _selector->photoChosen( ) | rpl::start_to_stream(_photoChosen, wrap->lifetime()); - selector->inlineResultChosen( + _selector->inlineResultChosen( ) | rpl::start_to_stream(_inlineResultChosen, wrap->lifetime()); - selector->contextMenuRequested( + _selector->contextMenuRequested( ) | rpl::start_with_next([=] { - selector->showMenuWithType(sendMenuType()); + _selector->showMenuWithType(sendMenuType()); }, wrap->lifetime()); - selector->choosingStickerUpdated( + _selector->choosingStickerUpdated( ) | rpl::start_with_next([=](ChatHelpers::TabbedSelector::Action action) { _sendActionUpdates.fire({ .type = Api::SendProgressType::ChooseSticker, @@ -2063,7 +2352,8 @@ void ComposeControls::initSendButton() { _send.get(), [=] { return sendButtonMenuType(); }, SendMenu::DefaultSilentCallback(send), - SendMenu::DefaultScheduleCallback(_wrap.get(), sendMenuType(), send)); + SendMenu::DefaultScheduleCallback(_wrap.get(), sendMenuType(), send), + SendMenu::DefaultWhenOnlineCallback(send)); } void ComposeControls::initSendAsButton(not_null peer) { @@ -2081,7 +2371,7 @@ void ComposeControls::initSendAsButton(not_null peer) { updateControlsGeometry(_wrap->size()); orderControls(); } - }, _wrap->lifetime()); + }, _historyLifetime); updateSendAsButton(); } @@ -2143,16 +2433,31 @@ void ComposeControls::initWriteRestriction() { }, _wrap->lifetime()); } +void ComposeControls::changeFocusedControl() { + _focusRequests.fire({}); + if (_regularWindow) { + _regularWindow->widget()->setInnerFocus(); + } +} + void ComposeControls::initVoiceRecordBar() { _voiceRecordBar->recordingStateChanges( ) | rpl::start_with_next([=](bool active) { + if (active) { + _recording = true; + changeFocusedControl(); + } _field->setVisible(!active); + if (!active) { + changeFocusedControl(); + _recording = false; + } }, _wrap->lifetime()); _voiceRecordBar->setStartRecordingFilter([=] { const auto error = [&]() -> std::optional { const auto peer = _history ? _history->peer.get() : nullptr; - if (!peer) { + if (peer) { if (const auto error = Data::RestrictionError( peer, ChatRestriction::SendVoiceMessages)) { @@ -2162,7 +2467,7 @@ void ComposeControls::initVoiceRecordBar() { return std::nullopt; }(); if (error) { - _window->show(Ui::MakeInformBox(*error)); + _show->showToast(*error); return true; } else if (_showSlowmodeError && _showSlowmodeError()) { return true; @@ -2170,26 +2475,6 @@ void ComposeControls::initVoiceRecordBar() { return false; }); - { - auto geometry = rpl::merge( - _wrap->geometryValue(), - _send->geometryValue() - ) | rpl::map([=](QRect geometry) { - auto r = _send->geometry(); - r.setY(r.y() + _wrap->y()); - return r; - }); - _voiceRecordBar->setSendButtonGeometryValue(std::move(geometry)); - } - - { - auto bottom = _wrap->geometryValue( - ) | rpl::map([=](QRect geometry) { - return geometry.y() - st::historyRecordLockPosition.y(); - }); - _voiceRecordBar->setLockBottom(std::move(bottom)); - } - _voiceRecordBar->updateSendButtonTypeRequests( ) | rpl::start_with_next([=] { updateSendButtonType(); @@ -2250,7 +2535,7 @@ void ComposeControls::finishAnimating() { } void ComposeControls::updateControlsGeometry(QSize size) { - // _attachToggle (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel + // (_attachToggle|_replaceMedia) (_sendAs) -- _inlineResults ------ _tabbedPanel -- _fieldBarCancel // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_silent|_botCommandStart) _tabbedSelectorToggle _send const auto fieldWidth = size.width() @@ -2259,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); @@ -2275,6 +2561,9 @@ void ComposeControls::updateControlsGeometry(QSize size) { const auto buttonsTop = size.height() - _attachToggle->height(); auto left = st::historySendRight; + if (_replaceMedia) { + _replaceMedia->moveToLeft(left, buttonsTop); + } _attachToggle->moveToLeft(left, buttonsTop); left += _attachToggle->width(); if (_sendAs) { @@ -2295,9 +2584,17 @@ void ComposeControls::updateControlsGeometry(QSize size) { right += _send->width(); _tabbedSelectorToggle->moveToRight(right, buttonsTop); right += _tabbedSelectorToggle->width(); - _botCommandStart->moveToRight(right, buttonsTop); - if (_botCommandShown) { - right += _botCommandStart->width(); + if (_like) { + _like->moveToRight(right, buttonsTop); + if (_likeShown) { + right += _like->width(); + } + } + if (_botCommandStart) { + _botCommandStart->moveToRight(right, buttonsTop); + if (_botCommandShown) { + right += _botCommandStart->width(); + } } if (_silent) { _silent->moveToRight(right, buttonsTop); @@ -2314,19 +2611,40 @@ void ComposeControls::updateControlsGeometry(QSize size) { } void ComposeControls::updateControlsVisibility() { - _botCommandStart->setVisible(_botCommandShown); + if (_botCommandStart) { + _botCommandStart->setVisible(_botCommandShown); + } + if (_like) { + _like->setVisible(_likeShown); + } if (_ttlInfo) { _ttlInfo->show(); } if (_sendAs) { _sendAs->show(); } + if (_replaceMedia) { + _replaceMedia->show(); + _attachToggle->hide(); + } else { + _attachToggle->show(); + } +} + +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; - if (peer + if (_botCommandStart + && peer && ((peer->isChat() && peer->asChat()->botStatus > 0) || (peer->isMegagroup() && peer->asChannel()->mgInfo->botStatus > 0) || (peer->isUser() && peer->asUser()->isBot()))) { @@ -2356,7 +2674,9 @@ void ComposeControls::updateOuterGeometry(QRect rect) { void ComposeControls::updateMessagesTTLShown() { const auto peer = _history ? _history->peer.get() : nullptr; - const auto shown = peer && (peer->messagesTTL() > 0); + const auto shown = _features.ttlInfo + && peer + && (peer->messagesTTL() > 0); if (!shown && _ttlInfo) { _ttlInfo = nullptr; updateControlsVisibility(); @@ -2364,7 +2684,7 @@ void ComposeControls::updateMessagesTTLShown() { } else if (shown && !_ttlInfo) { _ttlInfo = std::make_unique( _wrap.get(), - std::make_shared(_window), + _show, peer); orderControls(); updateControlsVisibility(); @@ -2373,10 +2693,12 @@ void ComposeControls::updateMessagesTTLShown() { } bool ComposeControls::updateSendAsButton() { - Expects(_history != nullptr); - - const auto peer = _history->peer; - if (isEditingMessage() || !session().sendAsPeers().shouldChoose(peer)) { + const auto peer = _history ? _history->peer.get() : nullptr; + if (!_features.sendAs + || !peer + || !_regularWindow + || isEditingMessage() + || !session().sendAsPeers().shouldChoose(peer)) { if (!_sendAs) { return false; } @@ -2390,19 +2712,22 @@ bool ComposeControls::updateSendAsButton() { st::sendAsButton); Ui::SetupSendAsButton( _sendAs.get(), - rpl::single(peer.get()), - _window); + rpl::single(peer), + _regularWindow); return true; } void ComposeControls::updateAttachBotsMenu() { _attachBotsMenu = nullptr; - if (!_history || !_sendActionFactory) { + if (!_features.attachBotsMenu + || !_history + || !_sendActionFactory + || !_regularWindow) { return; } _attachBotsMenu = InlineBots::MakeAttachBotsMenu( _parent, - _window, + _regularWindow, _history->peer, _sendActionFactory, [=](bool compress) { _attachRequests.fire_copy(compress); }); @@ -2421,7 +2746,18 @@ void ComposeControls::updateAttachBotsMenu() { void ComposeControls::paintBackground(QRect clip) { Painter p(_wrap.get()); - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + //p.setCompositionMode(QPainter::CompositionMode_Source); + //p.fillRect(clip, Qt::transparent); + //p.setCompositionMode(QPainter::CompositionMode_SourceOver); + //_backgroundRect->paint(p, _wrap->rect()); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(_st.bg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(_wrap->rect(), _st.radius, _st.radius); + } else { + p.fillRect(clip, _st.bg); + } } void ComposeControls::escape() { @@ -2435,7 +2771,7 @@ void ComposeControls::escape() { bool ComposeControls::pushTabbedSelectorToThirdSection( not_null thread, const Window::SectionShow ¶ms) { - if (!_tabbedPanel) { + if (!_tabbedPanel || !_regularWindow) { return true; //} else if (!_canSendMessages) { // Core::App().settings().setTabbedReplacedWithInfo(true); @@ -2447,8 +2783,8 @@ bool ComposeControls::pushTabbedSelectorToThirdSection( &st::historyAttachEmojiActive, &st::historyRecordVoiceFgActive, &st::historyRecordVoiceRippleBgActive); - _window->resizeForThirdSection(); - _window->showSection( + _regularWindow->resizeForThirdSection(); + _regularWindow->showSection( std::make_shared(), params.withThirdColumn()); return true; @@ -2461,10 +2797,21 @@ bool ComposeControls::returnTabbedSelector() { } void ComposeControls::createTabbedPanel() { - setTabbedPanel(std::make_unique( + using namespace ChatHelpers; + auto descriptor = TabbedPanelDescriptor{ + .regularWindow = _regularWindow, + .ownedSelector = (_ownedSelector + ? object_ptr::fromRaw(_ownedSelector.release()) + : object_ptr(nullptr)), + .nonOwnedSelector = _ownedSelector ? nullptr : _selector.get(), + }; + setTabbedPanel(std::make_unique( _parent, - _window, - _window->tabbedSelector())); + std::move(descriptor))); + _tabbedPanel->setDesiredHeightValues( + st::emojiPanHeightRatio, + _st.tabbedHeightMin, + _st.tabbedHeightMax); } void ComposeControls::setTabbedPanel( @@ -2482,12 +2829,12 @@ void ComposeControls::setTabbedPanel( } void ComposeControls::toggleTabbedSelectorMode() { - if (!_history) { + if (!_history || !_regularWindow) { return; } if (_tabbedPanel) { - if (_window->canShowThirdSection() - && !_window->adaptive().isOneColumn()) { + if (_regularWindow->canShowThirdSection() + && !_regularWindow->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); const auto topic = _history->peer->forumTopicFor( @@ -2499,7 +2846,7 @@ void ComposeControls::toggleTabbedSelectorMode() { _tabbedPanel->toggleAnimated(); } } else { - _window->closeThirdSection(); + _regularWindow->closeThirdSection(); } } @@ -2523,7 +2870,7 @@ void ComposeControls::editMessage(not_null item) { Expects(draftKeyCurrent() != Data::DraftKey::None()); if (_voiceRecordBar->isActive()) { - _window->show(Ui::MakeInformBox(tr::lng_edit_caption_voice())); + _show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice())); return; } @@ -2555,12 +2902,37 @@ void ComposeControls::editMessage(not_null item) { cursor, previewState)); applyDraft(); + if (updateReplaceMediaButton()) { + updateControlsVisibility(); + updateControlsGeometry(_wrap->size()); + } if (_autocomplete) { InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); }); } } +bool ComposeControls::updateReplaceMediaButton() { + if (!_canReplaceMedia || !_regularWindow) { + const auto result = (_replaceMedia != nullptr); + _replaceMedia = nullptr; + return result; + } else if (_replaceMedia) { + return false; + } + _replaceMedia = std::make_unique( + _wrap.get(), + st::historyReplaceMedia); + _replaceMedia->setClickedCallback([=] { + EditCaptionBox::StartMediaReplace( + _regularWindow, + _editingId, + _field->getTextWithTags(), + crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); + }); + return true; +} + void ComposeControls::cancelEditMessage() { Expects(_history != nullptr); Expects(draftKeyCurrent() != Data::DraftKey::None()); @@ -2573,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()); @@ -2608,24 +3000,24 @@ void ComposeControls::replyToMessage(FullMsgId id) { } void ComposeControls::cancelReplyMessage() { - Expects(_history != nullptr); - const auto wasReply = replyingToMessage(); _header->replyToMessage({}); - const auto key = draftKey(DraftType::Normal); - if (const auto localDraft = _history->draft(key)) { - if (localDraft->msgId) { - if (localDraft->textWithTags.text.isEmpty()) { - _history->clearDraft(key); - } else { - localDraft->msgId = 0; + if (_history) { + const auto key = draftKey(DraftType::Normal); + if (const auto localDraft = _history->draft(key)) { + if (localDraft->msgId) { + if (localDraft->textWithTags.text.isEmpty()) { + _history->clearDraft(key); + } else { + localDraft->msgId = 0; + } } } - } - if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + if (wasReply) { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); + } } } @@ -2648,7 +3040,7 @@ bool ComposeControls::handleCancelRequest() { _autocomplete->hideAnimated(); return true; } else if (isEditingMessage()) { - cancelEditMessage(); + maybeCancelEditMessage(); return true; } else if (readyToForward()) { cancelForward(); @@ -2660,16 +3052,26 @@ bool ComposeControls::handleCancelRequest() { return false; } -void ComposeControls::initWebpageProcess() { - Expects(_history); +void ComposeControls::tryProcessKeyInput(not_null e) { + if (_field->isVisible() && !e->text().isEmpty()) { + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + } +} + +void ComposeControls::initWebpageProcess() { + if (!_history) { + _preview = nullptr; + _header->previewUnregister(); + return; + } - auto &lifetime = _wrap->lifetime(); _preview = std::make_unique(_history, _field); _preview->paintRequests( ) | rpl::start_with_next(crl::guard(_header.get(), [=] { _header->update(); - }), lifetime); + }), _historyLifetime); session().changes().peerUpdates( Data::PeerUpdate::Flag::Rights @@ -2698,7 +3100,7 @@ void ComposeControls::initWebpageProcess() { updateControlsGeometry(_wrap->size()); } } - }, lifetime); + }, _historyLifetime); _header->previewRequested( _preview->titleChanges(), @@ -2760,10 +3162,39 @@ rpl::producer> ComposeControls::viewportEvents() const { return _voiceRecordBar->lockViewportEvents(); } +rpl::producer<> ComposeControls::likeToggled() const { + return _likeToggled.events(); +} + bool ComposeControls::isRecording() const { return _voiceRecordBar->isRecording(); } +bool ComposeControls::isRecordingPressed() const { + return !_voiceRecordBar->isRecordingLocked() + && (!_voiceRecordBar->isHidden() + || (_send->type() == Ui::SendButton::Type::Record + && _send->isDown())); +} + +rpl::producer ComposeControls::recordingActiveValue() const { + return _voiceRecordBar->shownValue(); +} + +rpl::producer ComposeControls::hasSendTextValue() const { + return _hasSendText.value(); +} + +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)); @@ -2773,7 +3204,7 @@ bool ComposeControls::preventsClose(Fn &&continueCallback) const { } bool ComposeControls::hasSilentBroadcastToggle() const { - if (!_history) { + if (!_features.silentBroadcastToggle || !_history) { return false; } const auto &peer = _history->peer; @@ -2784,7 +3215,7 @@ bool ComposeControls::hasSilentBroadcastToggle() const { } void ComposeControls::updateInlineBotQuery() { - if (!_history) { + if (!_history || !_regularWindow) { return; } const auto query = ParseInlineBotQuery(&session(), _field); @@ -2848,6 +3279,8 @@ void ComposeControls::updateInlineBotQuery() { void ComposeControls::applyInlineBotQuery( UserData *bot, const QString &query) { + Expects(_regularWindow != nullptr); + if (_history && bot) { if (_inlineBot != bot) { _inlineBot = bot; @@ -2857,15 +3290,15 @@ void ComposeControls::applyInlineBotQuery( if (!_inlineResults) { _inlineResults = std::make_unique( _parent, - _window); + _regularWindow); _inlineResults->setResultSelectedCallback([=]( InlineBots::ResultSelected result) { if (result.open) { const auto request = result.result->openRequest(); if (const auto photo = request.photo()) { - _window->openPhoto(photo, {}, {}); + _regularWindow->openPhoto(photo, {}); } else if (const auto document = request.document()) { - _window->openDocument(document, {}, {}); + _regularWindow->openDocument(document, false, {}); } } else { _inlineResultChosen.fire_copy(result); 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 25a92acfa..06d8ae9a8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -7,19 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "base/required.h" #include "api/api_common.h" +#include "base/required.h" #include "base/unique_qptr.h" #include "base/timer.h" +#include "chat_helpers/compose/compose_features.h" #include "dialogs/dialogs_key.h" #include "history/view/controls/compose_controls_common.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/widgets/input_fields.h" class History; +class DocumentData; class FieldAutocomplete; +namespace style { +struct ComposeControls; +} // namespace style + namespace SendMenu { enum class Type; } // namespace SendMenu @@ -29,6 +36,7 @@ class TabbedPanel; class TabbedSelector; struct FileChosen; struct PhotoChosen; +class Show; } // namespace ChatHelpers namespace Data { @@ -36,6 +44,7 @@ struct MessagePosition; struct Draft; class DraftKey; enum class PreviewState : char; +class PhotoMedia; } // namespace Data namespace InlineBots { @@ -54,6 +63,7 @@ class EmojiButton; class SendAsButton; class SilentToggle; class DropdownMenu; +struct PreparedList; } // namespace Ui namespace Main { @@ -61,8 +71,8 @@ class Session; } // namespace Main namespace Window { -class SessionController; struct SectionShow; +class SessionController; } // namespace Window namespace Api { @@ -79,6 +89,25 @@ class TTLButton; class FieldHeader; class WebpageProcessor; +enum class ComposeControlsMode { + Normal, + Scheduled, +}; + +struct ComposeControlsDescriptor { + const style::ComposeControls *stOverride = nullptr; + std::shared_ptr show; + Fn)> unavailableEmojiPasted; + ComposeControlsMode mode = ComposeControlsMode::Normal; + SendMenu::Type sendMenuType = {}; + Window::SessionController *regularWindow = nullptr; + rpl::producer stickerOrEmojiChosen; + rpl::producer customPlaceholder; + QString voiceCustomCancelText; + bool voiceLockFromBottom = false; + ChatHelpers::ComposeFeatures features; +}; + class ComposeControls final { public: using FileChosen = ChatHelpers::FileChosen; @@ -91,18 +120,17 @@ public: using SetHistoryArgs = Controls::SetHistoryArgs; using ReplyNextRequest = Controls::ReplyNextRequest; using FieldHistoryAction = Ui::InputField::HistoryAction; - - enum class Mode { - Normal, - Scheduled, - }; + using Mode = ComposeControlsMode; ComposeControls( not_null parent, - not_null window, + not_null controller, Fn)> unavailableEmojiPasted, Mode mode, SendMenu::Type sendMenuType); + ComposeControls( + not_null parent, + ComposeControlsDescriptor descriptor); ~ComposeControls(); [[nodiscard]] Main::Session &session() const; @@ -119,6 +147,9 @@ 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; [[nodiscard]] rpl::producer sendRequests() const; [[nodiscard]] rpl::producer sendVoiceRequests() const; @@ -131,17 +162,20 @@ 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 -> rpl::producer>; [[nodiscard]] auto replyNextRequests() const -> rpl::producer; + [[nodiscard]] rpl::producer<> focusRequests() const; using MimeDataHook = Fn data, Ui::InputField::MimeAction action)>; void setMimeDataHook(MimeDataHook hook); + bool confirmMediaEdit(Ui::PreparedList &list); bool pushTabbedSelectorToThirdSection( not_null thread, @@ -162,6 +196,7 @@ public: void editMessage(FullMsgId id); void cancelEditMessage(); + void maybeCancelEditMessage(); // Confirm if changed and cancel. void replyToMessage(FullMsgId id); void cancelReplyMessage(); @@ -170,6 +205,7 @@ public: void cancelForward(); bool handleCancelRequest(); + void tryProcessKeyInput(not_null e); [[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const; [[nodiscard]] WebPageId webPageId() const; @@ -184,6 +220,11 @@ public: [[nodiscard]] rpl::producer lockShowStarts() const; [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isRecording() const; + [[nodiscard]] bool isRecordingPressed() const; + [[nodiscard]] rpl::producer recordingActiveValue() const; + [[nodiscard]] rpl::producer hasSendTextValue() const; + [[nodiscard]] rpl::producer fieldMenuShownValue() const; + [[nodiscard]] not_null likeAnimationTarget() const; void applyCloudDraft(); void applyDraft( @@ -227,6 +268,7 @@ private: void updateWrappingVisibility(); void updateControlsVisibility(); void updateControlsGeometry(QSize size); + bool updateReplaceMediaButton(); void updateOuterGeometry(QRect rect); void paintBackground(QRect clip); @@ -234,8 +276,6 @@ private: [[nodiscard]] SendMenu::Type sendMenuType() const; [[nodiscard]] SendMenu::Type sendButtonMenuType() const; - void sendSilent(); - void sendScheduled(); [[nodiscard]] auto sendContentRequests( SendRequestType requestType = SendRequestType::Text) const; @@ -256,6 +296,7 @@ private: bool showRecordButton() const; void drawRestrictedWrite(QPainter &p, const QString &error); bool updateBotCommandShown(); + bool updateLikeShown(); void cancelInlineBot(); void clearInlineBot(); @@ -290,14 +331,25 @@ private: void unregisterDraftSources(); void registerDraftSource(); + void changeFocusedControl(); + const style::ComposeControls &_st; + const ChatHelpers::ComposeFeatures _features; const not_null _parent; - const not_null _window; + const std::shared_ptr _show; + const not_null _session; + + Window::SessionController * const _regularWindow = nullptr; + std::unique_ptr _ownedSelector; + const not_null _selector; + rpl::event_stream _stickerOrEmojiChosen; + History *_history = nullptr; Fn _showSlowmodeError; Fn _sendActionFactory; rpl::variable _slowmodeSecondsLeft; rpl::variable _sendDisabledBySlowmode; + rpl::variable _liked; rpl::variable> _writeRestriction; rpl::variable _hidden; Mode _mode = Mode::Normal; @@ -305,11 +357,16 @@ private: const std::unique_ptr _wrap; const std::unique_ptr _writeRestricted; + 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; + rpl::producer _fieldCustomPlaceholder; const not_null _field; - const not_null _botCommandStart; + Ui::IconButton * const _botCommandStart = nullptr; std::unique_ptr _sendAs; std::unique_ptr _silent; std::unique_ptr _ttlInfo; @@ -336,7 +393,11 @@ 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; + rpl::variable _hasSendText; TextUpdateEvents _textUpdateEvents = TextUpdateEvents() | TextUpdateEvent::SaveDraft @@ -354,12 +415,19 @@ private: mtpRequestId _inlineBotResolveRequestId = 0; bool _isInlineBot = false; bool _botCommandShown = false; + bool _likeShown = false; + + FullMsgId _editingId; + std::shared_ptr _photoEditMedia; + bool _canReplaceMedia = false; std::unique_ptr _preview; - rpl::lifetime _uploaderSubscriptions; - Fn _raiseEmojiSuggestions; + rpl::event_stream _focusChanges; + + rpl::lifetime _historyLifetime; + rpl::lifetime _uploaderSubscriptions; }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp index b7024d55d..0786b0745 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_search.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_search.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_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" @@ -818,12 +819,12 @@ ComposeSearch::Inner::Inner( auto box = Dialogs::SearchFromBox( peer, crl::guard(_bottomBar.get(), [=](not_null from) { - Window::Show(_window).hideLayer(); + _window->hideLayer(); _topBar->setFrom(from); }), crl::guard(_bottomBar.get(), [=] { setInnerFocus(); })); - Window::Show(_window).showBox(std::move(box)); + _window->show(std::move(box)); }, _bottomBar->lifetime()); _bottomBar->showListRequests( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index efb85e7bc..b00624867 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Controls { namespace { @@ -218,8 +219,7 @@ bool ForwardPanel::empty() const { return _data.items.empty(); } -void ForwardPanel::editOptions( - not_null controller) { +void ForwardPanel::editOptions(std::shared_ptr show) { using Options = Data::ForwardOptions; const auto now = _data.options; const auto count = _data.items.size(); @@ -258,7 +258,7 @@ void ForwardPanel::editOptions( } auto data = base::take(_data); _to->owningHistory()->setForwardDraft(_to->topicRootId(), {}); - Window::ShowForwardMessagesBox(controller, { + Window::ShowForwardMessagesBox(show, { .ids = _to->owner().itemsToIds(data.items), .options = data.options, }); @@ -287,7 +287,7 @@ void ForwardPanel::editOptions( _repaint(); } }); - controller->show(Box( + show->showBox(Box( Ui::ForwardOptionsBox, count, Ui::ForwardOptions{ diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index af052ec8a..b178e27d0 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -26,6 +26,10 @@ namespace Window { class SessionController; } // namespace Window +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace HistoryView::Controls { class ForwardPanel final : public base::has_weak_ptr { @@ -42,7 +46,7 @@ public: [[nodiscard]] rpl::producer<> itemsUpdated() const; - void editOptions(not_null controller); + void editOptions(std::shared_ptr show); [[nodiscard]] const HistoryItemsList &items() const; [[nodiscard]] bool empty() const; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp index f0b2eda2f..f7e15f5e5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "menu/menu_ttl_validator.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "styles/style_chat_helpers.h" #include "styles/style_chat.h" namespace HistoryView::Controls { @@ -49,6 +50,14 @@ void TTLButton::hide() { _button.hide(); } +void TTLButton::setVisible(bool visible) { + _button.setVisible(visible); +} + +bool TTLButton::isVisible() const { + return _button.isVisible(); +} + void TTLButton::move(int x, int y) { _button.move(x, y); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.h b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.h index dbdc50a9f..16098aa6e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_ttl_button.h @@ -28,6 +28,8 @@ public: void show(); void hide(); + void setVisible(bool visible); + [[nodiscard]] bool isVisible() const; void move(int x, int y); [[nodiscard]] int width() const; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 3b65bc18a..cba6d2e7c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/random.h" #include "base/unixtime.h" #include "ui/boxes/confirm_box.h" +#include "chat_helpers/compose/compose_show.h" #include "core/application.h" #include "data/data_document.h" #include "data/data_document_media.h" @@ -26,18 +27,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/player/media_player_button.h" #include "media/player/media_player_instance.h" -#include "styles/style_chat.h" -#include "styles/style_layers.h" -#include "styles/style_media_player.h" #include "ui/controls/send_button.h" #include "ui/effects/animation_value.h" #include "ui/effects/ripple_animation.h" #include "ui/text/format_values.h" #include "ui/painter.h" -#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" +#include "styles/style_media_player.h" namespace HistoryView::Controls { - namespace { using SendActionUpdate = VoiceRecordBar::SendActionUpdate; @@ -72,8 +72,8 @@ enum class FilterType { return std::clamp(float64(low) / high, 0., 1.); } -[[nodiscard]] auto Duration(int samples) { - return samples / ::Media::Player::kDefaultFrequency; +[[nodiscard]] crl::time Duration(int samples) { + return samples * crl::time(1000) / ::Media::Player::kDefaultFrequency; } [[nodiscard]] auto FormatVoiceDuration(int samples) { @@ -90,7 +90,6 @@ enum class FilterType { [[nodiscard]] std::unique_ptr ProcessCaptureResult( const ::Media::Capture::Result &data) { auto voiceData = std::make_unique(); - voiceData->duration = Duration(data.samples); voiceData->waveform = data.waveform; voiceData->wavemax = voiceData->waveform.empty() ? uchar(0) @@ -206,7 +205,8 @@ class ListenWrap final { public: ListenWrap( not_null parent, - not_null controller, + const style::RecordBar &st, + not_null session, ::Media::Capture::Result &&data, const style::font &font); @@ -231,12 +231,12 @@ private: not_null _parent; - const not_null _controller; + const style::RecordBar &_st; + const not_null _session; const not_null _document; const std::unique_ptr _voiceData; const std::shared_ptr _mediaView; const std::unique_ptr<::Media::Capture::Result> _data; - const style::IconButton &_stDelete; const base::unique_qptr _delete; const style::font &_durationFont; const QString _duration; @@ -264,17 +264,18 @@ private: ListenWrap::ListenWrap( not_null parent, - not_null controller, + const style::RecordBar &st, + not_null session, ::Media::Capture::Result &&data, const style::font &font) : _parent(parent) -, _controller(controller) -, _document(DummyDocument(&_controller->session().data())) +, _st(st) +, _session(session) +, _document(DummyDocument(&session->data())) , _voiceData(ProcessCaptureResult(data)) , _mediaView(_document->createMediaView()) , _data(std::make_unique<::Media::Capture::Result>(std::move(data))) -, _stDelete(st::historyRecordDelete) -, _delete(base::make_unique_q(parent, _stDelete)) +, _delete(base::make_unique_q(parent, _st.remove)) , _durationFont(font) , _duration(Ui::FormatDurationText( float64(_data->samples) / ::Media::Player::kDefaultFrequency)) @@ -299,7 +300,7 @@ void ListenWrap::init() { _waveformBgRect = QRect({ 0, 0 }, size) .marginsRemoved(st::historyRecordWaveformBgMargins); { - const auto m = _stDelete.width + _waveformBgRect.height() / 2; + const auto m = _st.remove.width + _waveformBgRect.height() / 2; _waveformBgFinalCenterRect = _waveformBgRect.marginsRemoved( style::margins(m, 0, m, 0)); } @@ -319,22 +320,23 @@ void ListenWrap::init() { PainterHighQualityEnabler hq(p); const auto progress = _showProgress.current(); p.setOpacity(progress); + const auto &remove = _st.remove; if (progress > 0. && progress < 1.) { - _stDelete.icon.paint(p, _stDelete.iconPosition, _parent->width()); + remove.icon.paint(p, remove.iconPosition, _parent->width()); } { const auto hideOffset = _isShowAnimation ? 0 : anim::interpolate(kHideWaveformBgOffset, 0, progress); - const auto deleteIconLeft = _stDelete.iconPosition.x(); + const auto deleteIconLeft = remove.iconPosition.x(); const auto bgRectRight = anim::interpolate( deleteIconLeft, - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectLeft = anim::interpolate( _parent->width() - deleteIconLeft - _waveformBgRect.height(), - _stDelete.width, + remove.width, _isShowAnimation ? progress : 1.); const auto bgRectMargins = style::margins( bgRectLeft - hideOffset, @@ -357,7 +359,7 @@ void ListenWrap::init() { p.setOpacity(progress); } p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordCancelActive); + p.setBrush(_st.cancelActive); QPainterPath path; path.setFillRule(Qt::WindingFill); path.addEllipse(bgLeftCircleRect); @@ -456,13 +458,6 @@ void ListenWrap::initPlayButton() { ) | rpl::start_with_next([=] { *showPause = false; }, _lifetime); - - const auto weak = Ui::MakeWeak(_controller->content().get()); - _lifetime.add([=] { - if (weak && isInPlayer()) { - weak->stopAndClosePlayer(); - } - }); } void ListenWrap::initPlayProgress() { @@ -612,10 +607,13 @@ rpl::lifetime &ListenWrap::lifetime() { class RecordLock final : public Ui::RippleButton { public: - RecordLock(not_null parent); + RecordLock( + not_null parent, + const style::RecordBarLock &st); void requestPaintProgress(float64 progress); void requestPaintLockToStopProgress(float64 progress); + void setVisibleTopPart(int part); [[nodiscard]] rpl::producer<> locks() const; [[nodiscard]] bool isLocked() const; @@ -634,6 +632,7 @@ private: void setProgress(float64 progress); void startLockingAnimation(float64 to); + const style::RecordBarLock &_st; const QRect _rippleRect; const QPen _arcPen; @@ -641,10 +640,15 @@ private: float64 _lockToStopProgress = 0.; rpl::variable _progress = 0.; + int _visibleTopPart = -1; + }; -RecordLock::RecordLock(not_null parent) -: RippleButton(parent, st::defaultRippleAnimation) +RecordLock::RecordLock( + not_null parent, + const style::RecordBarLock &st) +: RippleButton(parent, st.ripple) +, _st(st) , _rippleRect(QRect( 0, 0, @@ -660,6 +664,10 @@ RecordLock::RecordLock(not_null parent) init(); } +void RecordLock::setVisibleTopPart(int part) { + _visibleTopPart = part; +} + void RecordLock::init() { shownValue( ) | rpl::start_with_next([=](bool shown) { @@ -677,7 +685,13 @@ void RecordLock::init() { paintRequest( ) | rpl::start_with_next([=](const QRect &clip) { + if (!_visibleTopPart) { + return; + } Painter p(this); + if (_visibleTopPart > 0 && _visibleTopPart < height()) { + p.setClipRect(0, 0, width(), _visibleTopPart); + } if (isLocked()) { const auto top = anim::interpolate( 0, @@ -694,12 +708,12 @@ void RecordLock::init() { void RecordLock::drawProgress(Painter &p) { const auto progress = _progress.current(); - const auto &originTop = st::historyRecordLockTop; - const auto &originBottom = st::historyRecordLockBottom; - const auto &originBody = st::historyRecordLockBody; - const auto &shadowTop = st::historyRecordLockTopShadow; - const auto &shadowBottom = st::historyRecordLockBottomShadow; - const auto &shadowBody = st::historyRecordLockBodyShadow; + const auto &originTop = _st.originTop; + const auto &originBottom = _st.originBottom; + const auto &originBody = _st.originBody; + const auto &shadowTop = _st.shadowTop; + const auto &shadowBottom = _st.shadowBottom; + const auto &shadowBody = _st.shadowBody; const auto &shadowMargins = st::historyRecordLockMargin; const auto bottomMargin = anim::interpolate( @@ -746,7 +760,7 @@ void RecordLock::drawProgress(Painter &p) { originBody.fill(p, content); } { - const auto &arrow = st::historyRecordLockArrow; + const auto &arrow = _st.arrow; const auto arrowRect = QRect( inner.x(), content.y() + content.height() - arrow.height() / 2, @@ -798,7 +812,7 @@ void RecordLock::drawProgress(Painter &p) { PainterHighQualityEnabler hq(p); p.translate(inner.topLeft() + lockTranslation); p.setPen(Qt::NoPen); - p.setBrush(st::historyRecordLockIconFg); + p.setBrush(_st.fg); p.drawRoundedRect(blockRect, xRadius, 3); } else { // Paint an animation frame. @@ -851,7 +865,7 @@ void RecordLock::drawProgress(Painter &p) { p.drawImage( inner.topLeft(), - style::colorizeImage(frame, st::historyRecordLockIconFg)); + style::colorizeImage(frame, _st.fg)); } } } @@ -921,7 +935,10 @@ QPoint RecordLock::prepareRippleStartPosition() const { class CancelButton final : public Ui::RippleButton { public: - CancelButton(not_null parent, int height); + CancelButton( + not_null parent, + const style::RecordBar &st, + int height); void requestPaintProgress(float64 progress); @@ -932,6 +949,7 @@ protected: private: void init(); + const style::RecordBar &_st; const int _width; const QRect _rippleRect; @@ -941,8 +959,12 @@ private: }; -CancelButton::CancelButton(not_null parent, int height) -: Ui::RippleButton(parent, st::defaultLightButton.ripple) +CancelButton::CancelButton( + not_null parent, + const style::RecordBar &st, + int height) +: Ui::RippleButton(parent, st.cancelRipple) +, _st(st) , _width(st::historyRecordCancelButtonWidth) , _rippleRect(QRect(0, (height - _width) / 2, _width, _width)) , _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now)) { @@ -965,7 +987,7 @@ void CancelButton::init() { paintRipple(p, _rippleRect.x(), _rippleRect.y()); - p.setPen(st::historyRecordCancelButtonFg); + p.setPen(_st.cancelActive); _text.draw( p, 0, @@ -990,36 +1012,40 @@ void CancelButton::requestPaintProgress(float64 progress) { VoiceRecordBar::VoiceRecordBar( not_null parent, - not_null sectionWidget, - not_null controller, - std::shared_ptr send, - int recorderHeight) + VoiceRecordBarDescriptor &&descriptor) : RpWidget(parent) -, _sectionWidget(sectionWidget) -, _controller(controller) -, _send(send) -, _lock(std::make_unique(sectionWidget)) -, _level(std::make_unique( - sectionWidget, - _controller->widget()->leaveEvents())) -, _cancel(std::make_unique(this, recorderHeight)) +, _st(descriptor.stOverride ? *descriptor.stOverride : st::defaultRecordBar) +, _outerContainer(descriptor.outerContainer) +, _show(std::move(descriptor.show)) +, _send(std::move(descriptor.send)) +, _lock(std::make_unique(_outerContainer, _st.lock)) +, _level(std::make_unique(_outerContainer, _st)) +, _cancel(std::make_unique(this, _st, descriptor.recorderHeight)) , _startTimer([=] { startRecording(); }) , _message( st::historyRecordTextStyle, - tr::lng_record_cancel(tr::now), + (!descriptor.customCancelText.isEmpty() + ? descriptor.customCancelText + : tr::lng_record_cancel(tr::now)), TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto }) +, _lockFromBottom(descriptor.lockFromBottom) , _cancelFont(st::historyRecordFont) { - resize(QSize(parent->width(), recorderHeight)); + resize(QSize(parent->width(), descriptor.recorderHeight)); init(); hideFast(); } VoiceRecordBar::VoiceRecordBar( not_null parent, - not_null controller, + std::shared_ptr show, std::shared_ptr send, int recorderHeight) -: VoiceRecordBar(parent, parent, controller, send, recorderHeight) { +: VoiceRecordBar(parent, { + .outerContainer = parent, + .show = std::move(show), + .send = std::move(send), + .recorderHeight = recorderHeight, +}) { } VoiceRecordBar::~VoiceRecordBar() { @@ -1049,14 +1075,32 @@ void VoiceRecordBar::updateMessageGeometry() { } void VoiceRecordBar::updateLockGeometry() { - const auto right = anim::interpolate( - -_lock->width(), - st::historyRecordLockPosition.x(), - _showLockAnimation.value(_lockShowing.current() ? 1. : 0.)); - _lock->moveToRight(right, _lock->y()); + const auto parent = parentWidget(); + const auto me = Ui::MapFrom(_outerContainer, parent, geometry()); + const auto finalTop = me.y() + - st::historyRecordLockPosition.y() + - _lock->height(); + const auto finalRight = _outerContainer->width() + - (me.x() + me.width()) + + st::historyRecordLockPosition.x(); + const auto progress = _showLockAnimation.value( + _lockShowing.current() ? 1. : 0.); + if (_lockFromBottom) { + const auto top = anim::interpolate(me.y(), finalTop, progress); + _lock->moveToRight(finalRight, top); + _lock->setVisibleTopPart(me.y() - top); + } else { + const auto from = -_lock->width(); + const auto right = anim::interpolate(from, finalRight, progress); + _lock->moveToRight(right, finalTop); + } } void VoiceRecordBar::init() { + if (_st.radius > 0) { + _backgroundRect.emplace(_st.radius, _st.bg); + } + // Keep VoiceRecordBar behind SendButton. rpl::single( ) | rpl::then( @@ -1096,7 +1140,6 @@ void VoiceRecordBar::init() { } _cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0); updateMessageGeometry(); - updateLockGeometry(); }, lifetime()); paintRequest( @@ -1105,7 +1148,11 @@ void VoiceRecordBar::init() { if (_showAnimation.animating()) { p.setOpacity(showAnimationRatio()); } - p.fillRect(clip, st::historyComposeAreaBg); + if (_backgroundRect) { + _backgroundRect->paint(p, rect()); + } else { + p.fillRect(clip, _st.bg); + } p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio())); const auto opacity = p.opacity(); @@ -1246,6 +1293,9 @@ void VoiceRecordBar::init() { _cancel->setClickedCallback([=] { hideAnimated(); }); + + initLockGeometry(); + initLevelGeometry(); } void VoiceRecordBar::activeAnimate(bool active) { @@ -1287,22 +1337,28 @@ void VoiceRecordBar::setStartRecordingFilter(Fn &&callback) { _startRecordingFilter = std::move(callback); } -void VoiceRecordBar::setLockBottom(rpl::producer &&bottom) { +void VoiceRecordBar::initLockGeometry() { rpl::combine( - std::move(bottom), - _lock->sizeValue() | rpl::map_to(true) // Dummy value. - ) | rpl::start_with_next([=](int value, bool dummy) { - _lock->moveToLeft(_lock->x(), value - _lock->height()); + _lock->heightValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=] { + updateLockGeometry(); }, lifetime()); } -void VoiceRecordBar::setSendButtonGeometryValue( - rpl::producer &&geometry) { - std::move( - geometry - ) | rpl::start_with_next([=](QRect r) { - const auto center = (r.width() - _level->width()) / 2; - _level->moveToLeft(r.x() + center, r.y() + center); +void VoiceRecordBar::initLevelGeometry() { + rpl::combine( + _send->geometryValue(), + geometryValue(), + static_cast(parentWidget())->geometryValue() + ) | rpl::start_with_next([=](QRect send, auto, auto) { + const auto mapped = Ui::MapFrom( + _outerContainer, + _send->parentWidget(), + send); + const auto center = (send.width() - _level->width()) / 2; + _level->moveToLeft(mapped.x() + center, mapped.y() + center); }, lifetime()); } @@ -1325,7 +1381,6 @@ void VoiceRecordBar::startRecording() { startRedCircleAnimation(); _recording = true; - _controller->widget()->setInnerFocus(); instance()->start(); instance()->updated( ) | rpl::start_with_next_error([=](const Update &update) { @@ -1410,7 +1465,6 @@ void VoiceRecordBar::finish() { _listen = nullptr; _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); - _controller->widget()->setInnerFocus(); } void VoiceRecordBar::hideFast() { @@ -1434,14 +1488,16 @@ void VoiceRecordBar::stopRecording(StopType type) { return; } - Window::ActivateWindow(_controller); + window()->raise(); + window()->activateWindow(); const auto duration = Duration(data.samples); if (type == StopType::Send) { _sendVoiceRequests.fire({ data.bytes, data.waveform, duration }); } else if (type == StopType::Listen) { _listen = std::make_unique( this, - _controller, + _st, + &_show->session(), std::move(data), _cancelFont); _listenChanges.fire({}); @@ -1454,7 +1510,7 @@ void VoiceRecordBar::stopRecording(StopType type) { void VoiceRecordBar::drawDuration(Painter &p) { const auto duration = FormatVoiceDuration(_recordingSamples); p.setFont(_cancelFont); - p.setPen(st::historyRecordDurationFg); + p.setPen(_st.durationFg); p.drawText(_durationRect, style::al_left, duration); } @@ -1488,11 +1544,7 @@ void VoiceRecordBar::drawRedCircle(Painter &p) { } void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) { - p.setPen( - anim::pen( - st::historyRecordCancel, - st::historyRecordCancelActive, - 1. - recordActive)); + p.setPen(anim::pen(_st.cancel, _st.cancelActive, 1. - recordActive)); const auto opacity = p.opacity(); p.setOpacity(opacity * (1. - _lock->lockToStopProgress())); @@ -1534,6 +1586,10 @@ bool VoiceRecordBar::isRecording() const { return _recording.current(); } +bool VoiceRecordBar::isRecordingLocked() const { + return isRecording() && _lock->isLocked(); +} + bool VoiceRecordBar::isActive() const { return isRecording() || isListenState(); } @@ -1631,8 +1687,8 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) { void VoiceRecordBar::orderControls() { stackUnder(_send.get()); - _level->raise(); _lock->raise(); + _level->raise(); } void VoiceRecordBar::installListenStateFilter() { @@ -1690,7 +1746,7 @@ void VoiceRecordBar::showDiscardBox( callback(); } }; - _controller->show(Ui::MakeConfirmBox({ + _show->showBox(Ui::MakeConfirmBox({ .text = (isListenState() ? tr::lng_record_listen_cancel_sure : tr::lng_record_lock_cancel_sure)(), diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index 6dcb19da9..09c491678 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -11,10 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "history/view/controls/compose_controls_common.h" #include "ui/effects/animations.h" +#include "ui/round_rect.h" #include "ui/rp_widget.h" struct VoiceData; +namespace style { +struct RecordBar; +} // namespace style + namespace Ui { class SendButton; } // namespace Ui @@ -23,6 +28,10 @@ namespace Window { class SessionController; } // namespace Window +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace HistoryView::Controls { class VoiceRecordButton; @@ -30,6 +39,16 @@ class ListenWrap; class RecordLock; class CancelButton; +struct VoiceRecordBarDescriptor { + not_null outerContainer; + std::shared_ptr show; + std::shared_ptr send; + QString customCancelText; + const style::RecordBar *stOverride = nullptr; + int recorderHeight = 0; + bool lockFromBottom = false; +}; + class VoiceRecordBar final : public Ui::RpWidget { public: using SendActionUpdate = Controls::SendActionUpdate; @@ -37,13 +56,10 @@ public: VoiceRecordBar( not_null parent, - not_null sectionWidget, - not_null controller, - std::shared_ptr send, - int recorderHeight); + VoiceRecordBarDescriptor &&descriptor); VoiceRecordBar( not_null parent, - not_null controller, + std::shared_ptr show, std::shared_ptr send, int recorderHeight); ~VoiceRecordBar(); @@ -71,11 +87,10 @@ public: void requestToSendWithOptions(Api::SendOptions options); - void setLockBottom(rpl::producer &&bottom); - void setSendButtonGeometryValue(rpl::producer &&geometry); void setStartRecordingFilter(Fn &&callback); [[nodiscard]] bool isRecording() const; + [[nodiscard]] bool isRecordingLocked() const; [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isListenState() const; [[nodiscard]] bool isActive() const; @@ -89,6 +104,8 @@ private: }; void init(); + void initLockGeometry(); + void initLevelGeometry(); void updateMessageGeometry(); void updateLockGeometry(); @@ -121,8 +138,9 @@ private: void computeAndSetLockProgress(QPoint globalPos); - const not_null _sectionWidget; - const not_null _controller; + const style::RecordBar &_st; + const not_null _outerContainer; + const std::shared_ptr _show; const std::shared_ptr _send; const std::unique_ptr _lock; const std::unique_ptr _level; @@ -155,11 +173,13 @@ private: rpl::event_stream<> _recordingTipRequests; bool _recordingTipRequired = false; + bool _lockFromBottom = false; const style::font &_cancelFont; rpl::lifetime _recordingLifetime; + std::optional _backgroundRect; Ui::Animations::Simple _showLockAnimation; Ui::Animations::Simple _lockToStopAnimation; Ui::Animations::Simple _showListenAnimation; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index 15dd53517..4dbc3cd10 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" namespace HistoryView::Controls { @@ -49,7 +50,7 @@ auto Blobs() { VoiceRecordButton::VoiceRecordButton( not_null parent, - rpl::producer<> leaveWindowEventProducer) + const style::RecordBar &st) : AbstractButton(parent) , _blobs(std::make_unique( Blobs(), @@ -57,11 +58,6 @@ VoiceRecordButton::VoiceRecordButton( kMaxLevel)) , _center(_blobs->maxRadius()) { resize(_center * 2, _center * 2); - std::move( - leaveWindowEventProducer - ) | rpl::start_with_next([=] { - _inCircle = false; - }, lifetime()); init(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index 97c3d5072..bd3f0150c 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -11,11 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/rp_widget.h" -namespace Ui { -namespace Paint { +namespace style { +struct RecordBar; +} // namespace style + +namespace Ui::Paint { class Blobs; -} // namespace Paint -} // namespace Ui +} // namespace Ui::Paint namespace HistoryView::Controls { @@ -23,7 +25,7 @@ class VoiceRecordButton final : public Ui::AbstractButton { public: VoiceRecordButton( not_null parent, - rpl::producer<> leaveWindowEventProducer); + const style::RecordBar &st); ~VoiceRecordButton(); enum class Type { @@ -53,7 +55,6 @@ private: rpl::variable _showProgress = 0.; float64 _colorProgress = 0.; - rpl::variable _inCircle = false; rpl::variable _state = Type::Record; // This can animate for a very long time (like in music playing), diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index fb6687eac..dffcdde95 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/peers/edit_contact_box.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" @@ -683,22 +684,17 @@ void ContactStatus::setupBlockHandler(not_null user) { void ContactStatus::setupShareHandler(not_null user) { _inner->shareClicks( ) | rpl::start_with_next([=] { - const auto show = std::make_shared(_controller); + const auto show = _controller->uiShow(); const auto share = [=](Fn &&close) { user->setSettings(0); user->session().api().request(MTPcontacts_AcceptContact( user->inputUser )).done([=](const MTPUpdates &result) { user->session().api().applyUpdates(result); - - if (show->valid()) { - Ui::Toast::Show( - show->toastParent(), - tr::lng_new_contact_share_done( - tr::now, - lt_user, - user->shortName())); - } + show->showToast(tr::lng_new_contact_share_done( + tr::now, + lt_user, + user->shortName())); }).send(); close(); }; @@ -735,8 +731,8 @@ void ContactStatus::setupReportHandler(not_null peer) { _inner->reportClicks( ) | rpl::start_with_next([=] { Expects(!peer->isUser()); - const auto show = std::make_shared(_controller); + const auto show = _controller->uiShow(); const auto callback = crl::guard(_inner, [=](Fn &&close) { close(); @@ -751,11 +747,7 @@ void ContactStatus::setupReportHandler(not_null peer) { peer->session().api().deleteConversation(peer, false); }); - if (show->valid()) { - Ui::Toast::Show( - show->toastParent(), - tr::lng_report_spam_done(tr::now)); - } + show->showToast(tr::lng_report_spam_done(tr::now)); // Destroys _bar. _controller->showBackFromStack(); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 58a0722bb..409ae264d 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -48,11 +48,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_types.h" #include "data/data_forum_topic.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_groups.h" #include "data/data_channel.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" -#include "data/data_scheduled_messages.h" #include "data/data_message_reactions.h" #include "data/stickers/data_custom_emoji.h" #include "core/file_utilities.h" @@ -69,6 +69,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "spellcheck/spellcheck_types.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include @@ -131,14 +132,14 @@ void CopyImage(not_null photo) { void ShowStickerPackInfo( not_null document, not_null list) { - StickerSetBox::Show(list->controller(), document); + StickerSetBox::Show(list->controller()->uiShow(), document); } void ToggleFavedSticker( not_null controller, not_null document, FullMsgId contextId) { - Api::ToggleFavedSticker(controller, document, contextId); + Api::ToggleFavedSticker(controller->uiShow(), document, contextId); } void AddPhotoActions( @@ -182,7 +183,7 @@ void SaveGif( if (const auto media = item->media()) { if (const auto document = media->document()) { Api::ToggleSavedGif( - controller, + controller->uiShow(), document, item->fullId(), true); @@ -555,9 +556,8 @@ bool AddRescheduleAction( ? SendMenu::Type::ScheduledToUser : SendMenu::Type::Scheduled; - using S = Data::ScheduledMessages; const auto itemDate = firstItem->date(); - const auto date = (itemDate == S::kScheduledUntilOnlineTimestamp) + const auto date = (itemDate == Api::kScheduledUntilOnlineTimestamp) ? HistoryView::DefaultScheduleTime() : itemDate + 600; @@ -566,8 +566,7 @@ bool AddRescheduleAction( &request.navigation->session(), sendMenuType, callback, - date), - Ui::LayerOption::KeepOther); + date)); owner->itemRemoved( ) | rpl::start_with_next([=](not_null item) { @@ -996,7 +995,9 @@ base::unique_qptr FillContextMenu( list, st::popupMenuWithIcons); - if (request.overSelection && !list->hasCopyRestrictionForSelected()) { + if (request.overSelection + && !list->hasCopyRestrictionForSelected() + && !list->getSelectedText().empty()) { const auto text = request.selectedItems.empty() ? tr::lng_context_copy_selected(tr::now) : tr::lng_context_copy_selected_items(tr::now); @@ -1128,11 +1129,23 @@ void CopyPostLink( return channel->hasUsername(); }(); - Ui::Toast::Show( - Window::Show(controller).toastParent(), - isPublicLink - ? tr::lng_channel_public_link_copied(tr::now) - : tr::lng_context_about_private_link(tr::now)); + controller->showToast(isPublicLink + ? tr::lng_channel_public_link_copied(tr::now) + : tr::lng_context_about_private_link(tr::now)); +} + +void CopyStoryLink( + std::shared_ptr show, + FullStoryId storyId) { + const auto session = &show->session(); + const auto maybeStory = session->data().stories().lookup(storyId); + if (!maybeStory) { + return; + } + const auto story = *maybeStory; + QGuiApplication::clipboard()->setText( + session->api().exportDirectStoryLink(story)); + show->showToast(tr::lng_channel_public_link_copied(tr::now)); } void AddPollActions( @@ -1150,7 +1163,7 @@ void AddPollActions( } if (!Ui::SkipTranslate({ text })) { menu->addAction(tr::lng_context_translate(tr::now), [=] { - Window::Show(controller).showBox(Box( + controller->show(Box( Ui::TranslateBox, item->history()->peer, MsgId(), @@ -1186,7 +1199,7 @@ void AddPollActions( .confirmText = tr::lng_polls_stop_sure(), .cancelText = tr::lng_cancel(), })); - }, &st::menuIconStopPoll); + }, &st::menuIconRemove); } } @@ -1203,26 +1216,23 @@ void AddSaveSoundForNotifications( } else if (int(ringtones.list().size()) >= ringtones.maxSavedCount()) { return; } else if (const auto song = document->song()) { - if (song->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else if (const auto voice = document->voice()) { - if (voice->duration > ringtones.maxDuration()) { + if (document->duration() > ringtones.maxDuration()) { return; } } else { return; } - const auto toastParent = Window::Show(controller).toastParent(); + const auto show = controller->uiShow(); menu->addAction(tr::lng_context_save_custom_sound(tr::now), [=] { Api::ToggleSavedRingtone( document, item->fullId(), - crl::guard(toastParent, [=] { - Ui::Toast::Show( - toastParent, - tr::lng_ringtones_toast_added(tr::now)); - }), + [=] { show->showToast( + tr::lng_ringtones_toast_added(tr::now)); }, true); }, &st::menuIconSoundAdd); } @@ -1453,17 +1463,14 @@ void AddEmojiPacksAction( if (!strong) { return; } else if (packIds.size() > 1) { - strong->show(Box(strong, packIds)); + strong->show(Box(strong->uiShow(), packIds)); return; } // Single used emoji pack. - strong->show( - Box( - strong, - packIds.front(), - Data::StickersType::Emoji), - Ui::LayerOption::KeepOther); - + strong->show(Box( + strong->uiShow(), + packIds.front(), + Data::StickersType::Emoji)); }); menu->addAction(std::move(button)); } diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 84e443d41..d534d9c24 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -15,6 +15,7 @@ struct ReactionId; namespace Main { class Session; +class SessionShow; } // namespace Main namespace Ui { @@ -58,6 +59,9 @@ void CopyPostLink( not_null controller, FullMsgId itemId, Context context); +void CopyStoryLink( + std::shared_ptr show, + FullStoryId storyId); void AddPollActions( not_null menu, not_null poll, diff --git a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp index 5bd763c48..0ea99bc50 100644 --- a/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp +++ b/Telegram/SourceFiles/history/view/history_view_corner_buttons.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/toast/toast.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 8f13011cd..0503f04ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_settings.h" #include "core/click_handler_types.h" +#include "core/file_utilities.h" #include "core/ui_integration.h" #include "main/main_session.h" #include "main/main_domain.h" @@ -36,7 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/reaction_fly_animation.h" #include "ui/chat/chat_style.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/item_text_options.h" @@ -249,6 +249,7 @@ TextSelection ShiftItemSelection( QString DateTooltipText(not_null view) { const auto locale = QLocale(); const auto format = QLocale::LongFormat; + const auto item = view->data(); auto dateText = locale.toString(view->dateTime(), format); if (const auto editedDate = view->displayedEditDate()) { dateText += '\n' + tr::lng_edited_date( @@ -256,18 +257,22 @@ QString DateTooltipText(not_null view) { lt_date, locale.toString(base::unixtime::parse(editedDate), format)); } - if (const auto forwarded = view->data()->Get()) { - dateText += '\n' + tr::lng_forwarded_date( - tr::now, - lt_date, - locale.toString(base::unixtime::parse(forwarded->originalDate), format)); - if (forwarded->imported) { - dateText = tr::lng_forwarded_imported(tr::now) - + "\n\n" + dateText; + if (const auto forwarded = item->Get()) { + if (!forwarded->story && forwarded->psaType.isEmpty()) { + dateText += '\n' + tr::lng_forwarded_date( + tr::now, + lt_date, + locale.toString( + base::unixtime::parse(forwarded->originalDate), + format)); + if (forwarded->imported) { + dateText = tr::lng_forwarded_imported(tr::now) + + "\n\n" + dateText; + } } } if (view->isSignedAuthorElided()) { - if (const auto msgsigned = view->data()->Get()) { + if (const auto msgsigned = item->Get()) { dateText += '\n' + tr::lng_signed_author(tr::now, lt_user, msgsigned->author); } @@ -835,14 +840,20 @@ auto Element::contextDependentServiceText() -> TextWithLinks { void Element::validateText() { const auto item = data(); const auto &text = item->_text; - if (_text.isEmpty() == text.empty()) { - return; + const auto media = item->media(); + const auto storyMention = media && media->storyMention(); + if (media && media->storyExpired()) { + _media = nullptr; + if (!storyMention) { + if (_text.isEmpty()) { + setTextWithLinks(Ui::Text::Italic( + tr::lng_forwarded_story_expired(tr::now))); + } + return; + } } - const auto context = Core::MarkedTextContext{ - .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; - if (_flags & Flag::ServiceMessage) { + if (_text.isEmpty() == text.empty()) { + } else if (_flags & Flag::ServiceMessage) { const auto contextDependentText = contextDependentServiceText(); const auto &markedText = contextDependentText.text.empty() ? text @@ -850,28 +861,33 @@ void Element::validateText() { const auto &customLinks = contextDependentText.text.empty() ? item->customTextLinks() : contextDependentText.links; - _text.setMarkedText( - st::serviceTextStyle, - markedText, - Ui::ItemTextServiceOptions(), - context); + setTextWithLinks(markedText, customLinks); + } else { + setTextWithLinks(item->translatedTextWithLocalEntities()); + } +} + +void Element::setTextWithLinks( + const TextWithEntities &text, + const std::vector &links) { + const auto context = Core::MarkedTextContext{ + .session = &history()->session(), + .customEmojiRepaint = [=] { customEmojiRepaint(); }, + }; + if (_flags & Flag::ServiceMessage) { + const auto &options = Ui::ItemTextServiceOptions(); + _text.setMarkedText(st::serviceTextStyle, text, options, context); auto linkIndex = 0; - for (const auto &link : customLinks) { + for (const auto &link : links) { // Link indices start with 1. _text.setLink(++linkIndex, link); } } else { + const auto item = data(); + const auto &options = Ui::ItemTextOptions(item); clearSpecialOnlyEmoji(); - const auto context = Core::MarkedTextContext{ - .session = &history()->session(), - .customEmojiRepaint = [=] { customEmojiRepaint(); }, - }; - _text.setMarkedText( - st::messageTextStyle, - item->translatedTextWithLocalEntities(), - Ui::ItemTextOptions(item), - context); - if (!text.empty() && _text.isEmpty()) { + _text.setMarkedText(st::messageTextStyle, text, options, context); + if (!item->_text.empty() && _text.isEmpty()){ // If server has allowed some text that we've trim-ed entirely, // just replace it with something so that UI won't look buggy. _text.setMarkedText( @@ -982,7 +998,9 @@ ClickHandlerPtr Element::fromLink() const { auto &sponsored = session->data().sponsoredMessages(); const auto itemId = my.itemId ? my.itemId : item->fullId(); const auto details = sponsored.lookupDetails(itemId); - if (const auto &hash = details.hash) { + if (!details.externalLink.isEmpty()) { + File::OpenUrl(details.externalLink); + } else if (const auto &hash = details.hash) { Api::CheckChatInvite(window, *hash); } else if (const auto peer = details.peer) { window->showPeerInfo(peer); @@ -1012,10 +1030,7 @@ ClickHandlerPtr Element::fromLink() const { const auto my = context.other.value(); const auto weak = my.sessionWindow; if (const auto strong = weak.get()) { - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(strong).toastParent(), - .text = { tr::lng_forwarded_imported(tr::now) }, - }); + strong->showToast(tr::lng_forwarded_imported(tr::now)); } }); return imported; @@ -1497,6 +1512,7 @@ auto Element::takeReactionAnimations() Element::~Element() { // Delete media while owner still exists. + clearSpecialOnlyEmoji(); base::take(_media); if (_heavyCustomEmoji) { _heavyCustomEmoji = false; diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index a2b9cb049..3faaa355b 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -536,6 +536,9 @@ private: virtual QSize performCountCurrentSize(int newWidth) = 0; void refreshMedia(Element *replacing); + void setTextWithLinks( + const TextWithEntities &text, + const std::vector &links = {}); struct TextWithLinks { TextWithEntities text; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp index 0e5176c12..7fc19532a 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_bar.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_instance.h" #include "core/application.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { @@ -70,8 +71,10 @@ rpl::producer GroupCallBarContentByCall( std::vector userpics; Ui::GroupCallBarContent current; base::has_weak_ptr guard; + uint64 ownerId = 0; bool someUserpicsNotLoaded = false; bool pushScheduled = false; + bool noUserpics = false; }; // speaking DESC, std::max(date, lastActive) DESC @@ -244,6 +247,8 @@ rpl::producer GroupCallBarContentByCall( return [=](auto consumer) { auto lifetime = rpl::lifetime(); auto state = lifetime.make_state(); + state->noUserpics = call->listenersHidden(); + state->ownerId = call->peer()->id.value; state->current.shown = true; state->current.livestream = call->peer()->isBroadcast(); @@ -254,7 +259,18 @@ rpl::producer GroupCallBarContentByCall( state->pushScheduled = true; crl::on_main(&state->guard, [=] { state->pushScheduled = false; - consumer.put_next_copy(state->current); + auto copy = state->current; + if (state->noUserpics && copy.count > 0) { + const auto i = ranges::find( + copy.users, + state->ownerId, + &Ui::GroupCallUser::id); + if (i != end(copy.users)) { + copy.users.erase(i); + --copy.count; + } + } + consumer.put_next(std::move(copy)); }); }; diff --git a/Telegram/SourceFiles/history/view/history_view_item_preview.h b/Telegram/SourceFiles/history/view/history_view_item_preview.h index 4348743b3..7ac0a2b18 100644 --- a/Telegram/SourceFiles/history/view/history_view_item_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_item_preview.h @@ -37,6 +37,7 @@ struct ToPreviewOptions { bool generateImages = true; bool ignoreGroup = false; bool ignoreTopic = true; + bool spoilerLoginCode = false; bool translated = false; }; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 639c35ce5..3de0cb20e 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -44,7 +44,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/toast/toast.h" -#include "ui/toasts/common_toasts.h" #include "ui/inactive_press.h" #include "ui/effects/message_sending_animation_controller.h" #include "ui/effects/path_shift_gradient.h" @@ -934,7 +933,7 @@ Element *ListWidget::viewByPosition(Data::MessagePosition position) const { const auto result = (index < 0) ? nullptr : _items[index].get(); return (position == Data::MinMessagePosition || position == Data::MaxMessagePosition - || result->data()->position() == position) + || (result && result->data()->position() == position)) ? result : nullptr; } @@ -1303,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 { @@ -1443,12 +1434,9 @@ bool ListWidget::showCopyRestriction(HistoryItem *item) { if (type == CopyRestrictionType::None) { return false; } - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(_controller).toastParent(), - .text = { (type == CopyRestrictionType::Channel) - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now) }, - }); + _controller->showToast((type == CopyRestrictionType::Channel) + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -1457,12 +1445,9 @@ bool ListWidget::showCopyMediaRestriction(not_null item) { if (type == CopyRestrictionType::None) { return false; } - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(_controller).toastParent(), - .text = { (type == CopyRestrictionType::Channel) - ? tr::lng_error_nocopy_channel(tr::now) - : tr::lng_error_nocopy_group(tr::now) }, - }); + _controller->showToast((type == CopyRestrictionType::Channel) + ? tr::lng_error_nocopy_channel(tr::now) + : tr::lng_error_nocopy_group(tr::now)); return true; } @@ -2066,12 +2051,13 @@ void ListWidget::paintEvent(QPaintEvent *e) { } auto readTill = (HistoryItem*)nullptr; auto readContents = base::flat_set>(); + const auto markingAsViewed = markingMessagesRead(); const auto guard = gsl::finally([&] { if (_translateTracker) { _delegate->listAddTranslatedItems(_translateTracker.get()); _translateTracker->finishBunch(); } - if (readTill && markingMessagesRead()) { + if (markingAsViewed && readTill) { _delegate->listMarkReadTill(readTill); } if (!readContents.empty() && markingContentsRead()) { @@ -2143,7 +2129,7 @@ void ListWidget::paintEvent(QPaintEvent *e) { } else if (isUnread) { readTill = item; } - if (item->hasViews()) { + if (markingAsViewed && item->hasViews()) { session->api().views().scheduleIncrement(item); } if (withReaction) { @@ -2467,6 +2453,9 @@ void ListWidget::keyPressEvent(QKeyEvent *e) { #endif // Q_OS_MAC } else if (e == QKeySequence::Delete) { _delegate->listDeleteRequest(); + } else if (!(e->modifiers() & ~Qt::ShiftModifier) + && e->key() != Qt::Key_Shift) { + _delegate->listTryProcessKeyInput(e); } else { e->ignore(); } @@ -3628,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()); } } @@ -3763,7 +3751,7 @@ void ListWidget::refreshItem(not_null view) { }(); const auto [i, ok] = _views.emplace( item, - item->createView(this)); + item->createView(this, was.get())); const auto now = i->second.get(); _items[index] = now; diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index ad3517723..704fb1685 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -93,6 +93,7 @@ public: virtual bool listScrollTo(int top, bool syntetic = true) = 0; virtual void listCancelRequest() = 0; virtual void listDeleteRequest() = 0; + virtual void listTryProcessKeyInput(not_null e) = 0; virtual rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 01d654cd1..e6f7fbcad 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -21,11 +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/toasts/common_toasts.h" +#include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/text/text_entity.h" #include "ui/cached_round_corners.h" @@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_dialogs.h" namespace HistoryView { @@ -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; @@ -1777,9 +1956,8 @@ void Message::unloadHeavyPart() { bool Message::showForwardsFromSender( not_null forwarded) const { const auto peer = data()->history()->peer; - return peer->isSelf() - || peer->isRepliesChat() - || forwarded->imported; + return !forwarded->story + && (peer->isSelf() || peer->isRepliesChat() || forwarded->imported); } bool Message::hasFromPhoto() const { @@ -1871,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; @@ -2061,11 +2243,8 @@ ClickHandlerPtr Message::createGoToCommentsLink() const { const auto history = item->history(); if (const auto channel = history->peer->asChannel()) { if (channel->invitePeekExpires()) { - const auto show = Window::Show(controller); - Ui::ShowMultilineToast({ - .parentOverride = show.toastParent(), - .text = { tr::lng_channel_invite_private(tr::now) }, - }); + controller->showToast( + tr::lng_channel_invite_private(tr::now)); return; } } @@ -2272,8 +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) { - if (reply->replyToMsg && QRect(trect.x(), trect.y() + st::msgReplyPadding.top(), trect.width(), st::msgReplyBarSize.height()).contains(point)) { + const auto g = QRect( + trect.x(), + trect.y() + st::msgReplyPadding.top(), + trect.width(), + st::msgReplyBarSize.height()); + if ((reply->replyToMsg || reply->replyToStory) + && g.contains(point)) { outResult->link = reply->replyToLink(); + reply->ripple.lastPoint = point - g.topLeft(); } return true; } @@ -2879,7 +3065,9 @@ bool Message::displayFromName() const { bool Message::displayForwardedFrom() const { const auto item = data(); if (const auto forwarded = item->Get()) { - if (showForwardsFromSender(forwarded)) { + if (forwarded->story) { + return true; + } else if (showForwardsFromSender(forwarded)) { return false; } if (const auto sender = item->discussionPostOriginalSender()) { @@ -3603,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(); } @@ -3635,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()) { @@ -3687,6 +3880,9 @@ bool Message::needInfoDisplay() const { bool Message::hasVisibleText() const { if (data()->emptyText()) { + if (const auto media = data()->media()) { + return media->storyExpired(); + } return false; } const auto media = this->media(); @@ -3715,6 +3911,9 @@ void Message::refreshInfoSkipBlock() { const auto media = this->media(); const auto hasTextSkipBlock = [&] { if (item->_text.empty()) { + if (const auto media = data()->media()) { + return media->storyExpired(); + } return false; } else if (item->Has()) { return false; 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_pinned_bar.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp index 7ec3b365c..2b82db6f2 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_bar.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/weak_ptr.h" #include "apiwrap.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView { namespace { @@ -54,7 +55,9 @@ namespace { result.preview = kEmpty; result.spoilerRepaint = nullptr; } else { - result.preview = preview->original(); + result.preview = Images::Round( + preview->original(), + ImageRoundRadius::Small); result.spoilerRepaint = spoiler ? repaint : nullptr; } return result; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 22ca0af7a..0fbaf0349 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -24,7 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/ui_utility.h" -#include "ui/toasts/common_toasts.h" #include "base/timer_rpl.h" #include "apiwrap.h" #include "window/window_adaptive.h" @@ -47,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -497,6 +497,9 @@ void PinnedWidget::listDeleteRequest() { confirmDeleteSelected(); } +void PinnedWidget::listTryProcessKeyInput(not_null e) { +} + rpl::producer PinnedWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -643,14 +646,14 @@ void PinnedWidget::listShowPremiumToast(not_null document) { void PinnedWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, MsgId()); + controller()->openPhoto(photo, { context }); } void PinnedWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, MsgId(), showInMediaView); + controller()->openDocument(document, showInMediaView, { context }); } void PinnedWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 1ee0a9b7d..7125a1f18 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -82,6 +82,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 40b891335..7b878ec52 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -97,17 +98,6 @@ namespace { constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200); -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - rpl::producer RootViewContent( not_null history, MsgId rootId, @@ -321,11 +311,7 @@ RepliesWidget::RepliesWidget( }) | rpl::start_with_next([=](auto fullId) { if (const auto item = session().data().message(fullId)) { const auto media = item->media(); - if (media && !media->webpage()) { - if (media->allowsEditCaption()) { - controller->show(Box(controller, item)); - } - } else { + if (!media || media->webpage() || media->allowsEditCaption()) { _composeControls->editMessage(fullId); } } @@ -823,7 +809,7 @@ void RepliesWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, @@ -865,7 +851,7 @@ void RepliesWidget::chooseAttach( std::optional overrideSendImagesAsPhotos) { _choosingAttach = false; if (const auto error = Data::AnyFileRestrictionError(_history->peer)) { - controller()->showToast({ *error }); + controller()->showToast(*error); return; } else if (showSlowmodeError()) { return; @@ -941,7 +927,9 @@ bool RepliesWidget::confirmSendingFiles( bool RepliesWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { - if (showSendingFilesError(list)) { + if (_composeControls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { return false; } @@ -1014,7 +1002,7 @@ void RepliesWidget::sendingFilesConfirmed( album, action); } - if (_composeControls->replyingToMessage().msg == action.replyTo) { + if (_composeControls->replyingToMessage().msg == action.replyTo.msgId) { _composeControls->cancelReplyMessage(); refreshTopBarActiveChat(); } @@ -1056,7 +1044,7 @@ bool RepliesWidget::showSlowmodeError() { if (text.isEmpty()) { return false; } - controller()->showToast({ text }); + controller()->showToast(text); return true; } @@ -1125,19 +1113,19 @@ bool RepliesWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } - controller()->showToast({ text }); + controller()->showToast(text); return true; } Api::SendAction RepliesWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); - result.replyTo = replyToId(); - result.topicRootId = _rootId; + result.replyTo = { .msgId = replyToId(), .topicRootId = _rootId }; result.options.sendAs = _composeControls->sendAsPeer(); return result; } @@ -1178,7 +1166,7 @@ void RepliesWidget::send(Api::SendOptions options) { const auto webPageId = _composeControls->webPageId(); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -1191,7 +1179,7 @@ void RepliesWidget::send(Api::SendOptions options) { .ignoreSlowmodeCountdown = (options.scheduled != 0), }); if (!error.isEmpty()) { - controller()->showToast({ error }); + controller()->showToast(error); return; } @@ -1228,7 +1216,10 @@ void RepliesWidget::edit( TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); - if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { + if (!TextUtilities::CutPart(sending, left, MaxMessageSize) + && (!item + || !item->media() + || !item->media()->allowsEditCaption())) { if (item) { controller()->show(Box(item, false)); } else { @@ -1237,9 +1228,8 @@ void RepliesWidget::edit( return; } else if (!left.text.isEmpty()) { const auto remove = left.text.size(); - controller()->showToast({ - tr::lng_edit_limit_reached(tr::now, lt_count, remove), - }); + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); return; } @@ -1263,13 +1253,13 @@ void RepliesWidget::edit( } if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { _composeControls->cancelEditMessage(); } else if (error == u"MESSAGE_EMPTY"_q) { doSetInnerFocus(); } else { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } update(); return true; @@ -1350,7 +1340,7 @@ bool RepliesWidget::sendExistingDocument( _history->peer, ChatRestriction::SendStickers); if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } else if (showSlowmodeError() || ShowSendPremiumError(controller(), document)) { @@ -1385,7 +1375,7 @@ bool RepliesWidget::sendExistingPhoto( _history->peer, ChatRestriction::SendPhotos); if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } else if (showSlowmodeError()) { return false; @@ -1405,7 +1395,7 @@ void RepliesWidget::sendInlineResult( not_null bot) { const auto errorText = result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - controller()->showToast({ errorText }); + controller()->showToast(errorText); return; } sendInlineResult(result, bot, {}, std::nullopt); @@ -2327,6 +2317,9 @@ void RepliesWidget::showAnimatedHook( void RepliesWidget::showFinishedHook() { _topBar->setAnimatingMode(false); if (_joinGroup) { + if (Ui::InFocusChain(this)) { + _inner->setFocus(); + } _composeControls->hide(); } else { _composeControls->showFinished(); @@ -2390,6 +2383,10 @@ void RepliesWidget::listDeleteRequest() { confirmDeleteSelected(); } +void RepliesWidget::listTryProcessKeyInput(not_null e) { + _composeControls->tryProcessKeyInput(e); +} + rpl::producer RepliesWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -2435,6 +2432,9 @@ void RepliesWidget::listSelectionChanged(SelectedItems &&items) { } } _topBar->showSelected(state); + if (items.empty()) { + doSetInnerFocus(); + } } void RepliesWidget::listMarkReadTill(not_null item) { @@ -2511,8 +2511,7 @@ void RepliesWidget::listSendBotCommand( _history->peer, command, context); - auto message = ApiWrap::MessageToSend( - prepareSendAction({})); + auto message = Api::MessageToSend(prepareSendAction({})); message.textWithTags = { text }; session().api().sendMessage(std::move(message)); finishSending(); @@ -2558,14 +2557,17 @@ void RepliesWidget::listShowPremiumToast(not_null document) { void RepliesWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, _rootId); + controller()->openPhoto(photo, { context, _rootId }); } void RepliesWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, _rootId, showInMediaView); + controller()->openDocument( + document, + showInMediaView, + { context, _rootId }); } void RepliesWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index a8645a5c5..b048dca36 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -126,6 +126,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index 39a9a8738..95c7c6270 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "data/data_peer.h" #include "data/data_user.h" -#include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp #include "lang/lang_keys.h" #include "base/event_filter.h" #include "base/qt/qt_key_modifiers.h" @@ -98,12 +97,12 @@ void ScheduleBox( descriptor.submit.data(), [=] { return SendMenu::Type::SilentOnly; }, [=] { save(true, descriptor.collect()); }, + nullptr, nullptr); if (type == SendMenu::Type::ScheduledToUser) { const auto sendUntilOnline = box->addTopButton(*style.topButtonStyle); - const auto timestamp - = Data::ScheduledMessages::kScheduledUntilOnlineTimestamp; + const auto timestamp = Api::kScheduledUntilOnlineTimestamp; FillSendUntilOnlineMenu( sendUntilOnline.data(), [=] { save(false, timestamp); }, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 33710b711..d8db92e6d 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_window.h" #include "styles/style_info.h" #include "styles/style_boxes.h" @@ -65,20 +66,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace HistoryView { -namespace { - -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - -} // namespace object_ptr ScheduledMemento::createWidget( QWidget *parent, @@ -174,11 +161,7 @@ ScheduledWidget::ScheduledWidget( ) | rpl::start_with_next([=](auto fullId) { if (const auto item = session().data().message(fullId)) { const auto media = item->media(); - if (media && !media->webpage()) { - if (media->allowsEditCaption()) { - controller->show(Box(controller, item)); - } - } else { + if (!media || media->webpage() || media->allowsEditCaption()) { _composeControls->editMessage(fullId); } } @@ -312,7 +295,7 @@ void ScheduledWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, @@ -336,7 +319,7 @@ void ScheduledWidget::setupComposeControls() { void ScheduledWidget::chooseAttach() { if (const auto error = Data::AnyFileRestrictionError(_history->peer)) { - controller()->showToast({ *error }); + controller()->showToast(*error); return; } @@ -406,7 +389,9 @@ bool ScheduledWidget::confirmSendingFiles( bool ScheduledWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { - if (showSendingFilesError(list)) { + if (_composeControls->confirmMediaEdit(list)) { + return true; + } else if (showSendingFilesError(list)) { return false; } @@ -528,9 +513,7 @@ void ScheduledWidget::uploadFile( type, prepareSendAction(options)); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } bool ScheduledWidget::showSendingFilesError( @@ -563,11 +546,12 @@ bool ScheduledWidget::showSendingFilesError( return false; } else if (text == u"(toolarge)"_q) { const auto fileSize = list.files.back().size; - controller()->show(Box(FileSizeLimitBox, &session(), fileSize)); + controller()->show( + Box(FileSizeLimitBox, &session(), fileSize, nullptr)); return true; } - controller()->showToast({ text }); + controller()->showToast(text); return true; } @@ -593,19 +577,17 @@ void ScheduledWidget::send() { .ignoreSlowmodeCountdown = true, }); if (!error.isEmpty()) { - controller()->showToast({ error }); + controller()->showToast(error); return; } const auto callback = [=](Api::SendOptions options) { send(options); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } void ScheduledWidget::send(Api::SendOptions options) { const auto webPageId = _composeControls->webPageId(); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = _composeControls->getTextWithAppliedMarkdown(); message.webPageId = webPageId; @@ -625,19 +607,17 @@ void ScheduledWidget::send(Api::SendOptions options) { void ScheduledWidget::sendVoice( QByteArray bytes, VoiceWaveform waveform, - int duration) { + crl::time duration) { const auto callback = [=](Api::SendOptions options) { sendVoice(bytes, waveform, duration, options); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } void ScheduledWidget::sendVoice( QByteArray bytes, VoiceWaveform waveform, - int duration, + crl::time duration, Api::SendOptions options) { session().api().sendVoiceMessage( bytes, @@ -664,7 +644,10 @@ void ScheduledWidget::edit( TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); - if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { + if (!TextUtilities::CutPart(sending, left, MaxMessageSize) + && (!item + || !item->media() + || !item->media()->allowsEditCaption())) { if (item) { controller()->show(Box(item, false)); } else { @@ -673,9 +656,8 @@ void ScheduledWidget::edit( return; } else if (!left.text.isEmpty()) { const auto remove = left.text.size(); - controller()->showToast({ - tr::lng_edit_limit_reached(tr::now, lt_count, remove) - }); + controller()->showToast( + tr::lng_edit_limit_reached(tr::now, lt_count, remove)); return; } @@ -699,13 +681,13 @@ void ScheduledWidget::edit( } if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { _composeControls->cancelEditMessage(); } else if (error == u"MESSAGE_EMPTY"_q) { _composeControls->focus(); } else { - controller()->showToast({ tr::lng_edit_error(tr::now) }); + controller()->showToast(tr::lng_edit_error(tr::now)); } update(); return true; @@ -727,9 +709,7 @@ void ScheduledWidget::sendExistingDocument( const auto callback = [=](Api::SendOptions options) { sendExistingDocument(document, options); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } bool ScheduledWidget::sendExistingDocument( @@ -739,7 +719,7 @@ bool ScheduledWidget::sendExistingDocument( _history->peer, ChatRestriction::SendStickers); if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } else if (ShowSendPremiumError(controller(), document)) { return false; @@ -758,9 +738,7 @@ void ScheduledWidget::sendExistingPhoto(not_null photo) { const auto callback = [=](Api::SendOptions options) { sendExistingPhoto(photo, options); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } bool ScheduledWidget::sendExistingPhoto( @@ -770,7 +748,7 @@ bool ScheduledWidget::sendExistingPhoto( _history->peer, ChatRestriction::SendPhotos); if (error) { - controller()->showToast({ *error }); + controller()->showToast(*error); return false; } @@ -788,15 +766,13 @@ void ScheduledWidget::sendInlineResult( not_null bot) { const auto errorText = result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - controller()->showToast({ errorText }); + controller()->showToast(errorText); return; } const auto callback = [=](Api::SendOptions options) { sendInlineResult(result, bot, options); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } void ScheduledWidget::sendInlineResult( @@ -1102,6 +1078,10 @@ void ScheduledWidget::listDeleteRequest() { confirmDeleteSelected(); } +void ScheduledWidget::listTryProcessKeyInput(not_null e) { + _composeControls->tryProcessKeyInput(e); +} + rpl::producer ScheduledWidget::listSource( Data::MessagePosition aroundId, int limitBefore, @@ -1173,6 +1153,9 @@ void ScheduledWidget::listSelectionChanged(SelectedItems &&items) { } } _topBar->showSelected(state); + if (items.empty()) { + doSetInnerFocus(); + } } void ScheduledWidget::listMarkReadTill(not_null item) { @@ -1256,13 +1239,11 @@ void ScheduledWidget::listSendBotCommand( _history->peer, command, context); - auto message = ApiWrap::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(prepareSendAction(options)); message.textWithTags = { text }; session().api().sendMessage(std::move(message)); }; - controller()->show( - PrepareScheduleBox(this, sendMenuType(), callback), - Ui::LayerOption::KeepOther); + controller()->show(PrepareScheduleBox(this, sendMenuType(), callback)); } void ScheduledWidget::listHandleViaClick(not_null bot) { @@ -1313,14 +1294,14 @@ void ScheduledWidget::listShowPremiumToast( void ScheduledWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, context, MsgId()); + controller()->openPhoto(photo, { context }); } void ScheduledWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { - controller()->openDocument(document, context, MsgId(), showInMediaView); + controller()->openDocument(document, showInMediaView, { context }); } void ScheduledWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index acbec32d8..1c715e17e 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -104,6 +104,7 @@ public: bool listScrollTo(int top, bool syntetic = true) override; void listCancelRequest() override; void listDeleteRequest() override; + void listTryProcessKeyInput(not_null e) override; rpl::producer listSource( Data::MessagePosition aroundId, int limitBefore, @@ -196,11 +197,14 @@ private: Api::SendOptions options) const; void send(); void send(Api::SendOptions options); - void sendVoice(QByteArray bytes, VoiceWaveform waveform, int duration); void sendVoice( QByteArray bytes, VoiceWaveform waveform, - int duration, + crl::time duration); + void sendVoice( + QByteArray bytes, + VoiceWaveform waveform, + crl::time duration, Api::SendOptions options); void edit( not_null item, diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 2697ad7c4..d1d7953a6 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -429,7 +429,7 @@ QSize Service::performCountCurrentSize(int newWidth) { return { newWidth, newHeight }; } const auto media = this->media(); - if (media && data()->isUserpicSuggestion()) { + if (media && media->hideServiceText()) { newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(newWidth) + st::msgServiceMargin.bottom(); @@ -461,7 +461,7 @@ QSize Service::performCountOptimalSize() { if (const auto media = this->media()) { media->initDimensions(); - if (data()->isUserpicSuggestion()) { + if (media->hideServiceText()) { return { media->maxWidth(), media->minHeight() }; } } @@ -527,7 +527,7 @@ void Service::draw(Painter &p, const PaintContext &context) const { p.setTextPalette(st->serviceTextPalette()); const auto media = this->media(); - const auto onlyMedia = (media && data()->isUserpicSuggestion()); + const auto onlyMedia = (media && media->hideServiceText()); if (!onlyMedia) { if (media) { @@ -602,7 +602,7 @@ PointState Service::pointState(QPoint point) const { TextState Service::textState(QPoint point, StateRequest request) const { const auto item = data(); const auto media = this->media(); - const auto onlyMedia = (media && data()->isUserpicSuggestion()); + const auto onlyMedia = (media && media->hideServiceText()); auto result = TextState(item); @@ -652,6 +652,8 @@ TextState Service::textState(QPoint point, StateRequest request) const { if (TTLMenu::TTLValidator(nullptr, history()->peer).can()) { result.link = ttl->link; } + } else if (const auto same = item->Get()) { + result.link = same->lnk; } } } else if (media) { diff --git a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp index 5cf22f446..f10d8e328 100644 --- a/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sticker_toast.cpp @@ -165,7 +165,7 @@ void StickerToast::showWithTitle(const QString &title) { _weak = Ui::Toast::Show(_parent, Ui::Toast::Config{ .text = text, .st = &_st, - .durationMs = kPremiumToastDuration, + .duration = kPremiumToastDuration, .multiline = true, .dark = true, .slideSide = RectPart::Bottom, @@ -241,7 +241,7 @@ void StickerToast::showWithTitle(const QString &title) { PremiumPreview::AnimatedEmoji); } else { _controller->show(Box( - _controller, + _controller->uiShow(), _for->sticker()->set, setType)); } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 808c26e5d..fa0d6ed7f 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -31,7 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/effects/radial_animation.h" -#include "ui/toasts/common_toasts.h" #include "ui/boxes/report_box.h" // Ui::ReportReason #include "ui/text/text.h" #include "ui/text/text_options.h" diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index adf6b75e5..92b4a4744 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -284,7 +284,7 @@ void TranslateBar::setup(not_null history) { button->paintRequest( ) | rpl::start_with_next([=](QRect clip) { - QPainter(button).fillRect(clip, st::historyComposeButton.bgColor); + QPainter(button).fillRect(clip, st::historyComposeButtonBg); }, button->lifetime()); button->setClickedCallback([=] { @@ -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; }; @@ -524,7 +524,7 @@ void TranslateBar::showToast( const auto weak = Ui::Toast::Show(_wrap.window(), Ui::Toast::Config{ .text = std::move(text), .st = st.get(), - .durationMs = kToastDuration, + .duration = kToastDuration, .multiline = true, .dark = true, .slideSide = RectPart::Bottom, diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp index 1a2db4d6b..71b258ed9 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_invite.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "core/file_utilities.h" #include "data/data_cloud_themes.h" #include "data/data_session.h" #include "data/data_sponsored_messages.h" @@ -42,6 +43,8 @@ inline auto SponsoredPhrase(SponsoredType type) { case SponsoredType::Broadcast: return tr::lng_view_button_channel; case SponsoredType::Post: return tr::lng_view_button_message; case SponsoredType::Bot: return tr::lng_view_button_bot; + case SponsoredType::ExternalLink: + return tr::lng_view_button_external_link; } Unexpected("SponsoredType in SponsoredPhrase."); }(); @@ -52,6 +55,8 @@ inline auto WebPageToPhrase(not_null webpage) { const auto type = webpage->type; return Ui::Text::Upper((type == WebPageType::Theme) ? tr::lng_view_button_theme(tr::now) + : (type == WebPageType::Story) + ? tr::lng_view_button_story(tr::now) : (type == WebPageType::Message) ? tr::lng_view_button_message(tr::now) : (type == WebPageType::Group) @@ -113,6 +118,7 @@ struct ViewButton::Inner { const ClickHandlerPtr link; const Fn updateCallback; bool belowInfo = true; + bool externalLink = false; int lastWidth = 0; QPoint lastPoint; std::unique_ptr ripple; @@ -139,6 +145,8 @@ bool ViewButton::MediaHasViewButton( || ((type == WebPageType::Theme) && webpage->document && webpage->document->isTheme()) + || ((type == WebPageType::Story) + && (webpage->photo || webpage->document)) || ((type == WebPageType::WallPaper) && webpage->document && webpage->document->isWallPaper()); @@ -154,7 +162,9 @@ ViewButton::Inner::Inner( const auto &data = controller->session().data(); const auto itemId = my.itemId; const auto details = data.sponsoredMessages().lookupDetails(itemId); - if (details.hash) { + if (!details.externalLink.isEmpty()) { + File::OpenUrl(details.externalLink); + } else if (details.hash) { Api::CheckChatInvite(controller, *details.hash); } else if (details.peer) { controller->showPeerHistory( @@ -165,6 +175,7 @@ ViewButton::Inner::Inner( } })) , updateCallback(std::move(updateCallback)) +, externalLink(sponsored->type == SponsoredType::ExternalLink) , text(st::historyViewButtonTextStyle, SponsoredPhrase(sponsored->type)) { } @@ -256,6 +267,17 @@ void ViewButton::draw( r.width(), 1, style::al_center); + + if (_inner->externalLink) { + const auto &icon = st::msgBotKbUrlIcon; + const auto padding = st::msgBotKbIconPadding; + icon.paint( + p, + r.left() + r.width() - icon.width() - padding, + r.top() + padding, + r.width(), + stm->fwdTextPalette.linkFg->c); + } } p.restore(); if (_inner->lastWidth != r.width()) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 19832fc23..d1ddb2b96 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -172,7 +172,7 @@ void PaintWaveform( accumulate_max(result, st::normalFont->width(text)); }; add(FormatDownloadText(document->size, document->size)); - const auto duration = document->getDuration(); + const auto duration = document->duration() / 1000; if (const auto song = document->song()) { add(FormatPlayedText(duration, duration)); add(FormatDurationAndSizeText(duration, document->size)); @@ -1212,14 +1212,16 @@ bool Document::uploading() const { } void Document::setStatusSize(int64 newSize, TimeId realDuration) const { - TimeId duration = _data->isSong() - ? _data->song()->duration - : (_data->isVoiceMessage() - ? _data->voice()->duration - : _transcribedRound - ? _data->round()->duration - : -1); - File::setStatusSize(newSize, _data->size, duration, realDuration); + const auto duration = (_data->isSong() + || _data->isVoiceMessage() + || _transcribedRound) + ? _data->duration() + : -1; + File::setStatusSize( + newSize, + _data->size, + (duration >= 0) ? duration / 1000 : -1, + realDuration); if (auto thumbed = Get()) { if (_statusSize == Ui::FileStatusSizeReady) { thumbed->link = tr::lng_media_download(tr::now).toUpper(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index a4e8a372d..074f89dab 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/effects/spoiler_mess.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_document.h" #include "data/data_file_click_handler.h" @@ -87,7 +88,11 @@ Gif::Gif( bool spoiler) : File(parent, realParent) , _data(document) -, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) +, _storyId(realParent->media() + ? realParent->media()->storyId() + : FullStoryId()) +, _caption( + st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) , _downloadSize(Ui::FormatSizeText(_data->size)) { setDocumentLinks(_data, realParent, [=] { @@ -129,6 +134,7 @@ Gif::~Gif() { _parent->checkHeavyPart(); } } + togglePollingStory(false); } bool Gif::CanPlayInline(not_null document) { @@ -536,6 +542,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { if (radial || (!streamingMode && ((!loaded && !_data->loading()) || !autoplay))) { + const auto radialRevealed = 1.; const auto opacity = (item->isSending() || _data->uploading()) ? 1. : streamedForWaiting @@ -543,7 +550,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { : (radial && loaded) ? _animation->radial.opacity() : 1.; - const auto radialOpacity = opacity * revealed; + const auto radialOpacity = opacity * radialRevealed; const auto innerSize = st::msgFileLayout.thumbSize; auto inner = QRect(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize); p.setPen(Qt::NoPen); @@ -581,7 +588,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const { if (icon) { icon->paintInCenter(p, inner); } - p.setOpacity(revealed); + p.setOpacity(radialRevealed); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); if (streamedForWaiting && !_data->uploading()) { @@ -1057,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; } } @@ -1295,6 +1312,7 @@ void Gif::drawGrouped( if (radial || (!streamingMode && ((!loaded && !_data->loading()) || !autoplay))) { + const auto radialRevealed = 1.; const auto opacity = (item->isSending() || _data->uploading()) ? 1. : streamedForWaiting @@ -1302,7 +1320,7 @@ void Gif::drawGrouped( : (radial && loaded) ? _animation->radial.opacity() : 1.; - const auto radialOpacity = opacity * revealed; + const auto radialOpacity = opacity * radialRevealed; const auto radialSize = st::historyGroupRadialSize; const auto inner = QRect( geometry.x() + (geometry.width() - radialSize) / 2, @@ -1353,7 +1371,7 @@ void Gif::drawGrouped( icon->paintInCenter(p, inner); } } - p.setOpacity(revealed); + p.setOpacity(radialRevealed); if (radial) { const auto line = st::historyGroupRadialLine; const auto rinner = inner.marginsRemoved({ line, line, line, line }); @@ -1425,6 +1443,21 @@ void Gif::dataMediaCreated() const { _dataMedia->videoThumbnailWanted(_realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); +} + +void Gif::togglePollingStory(bool enabled) const { + if (!_storyId || _pollingStory == enabled) { + return; + } + const auto polling = Data::Stories::Polling::Chat; + if (!enabled) { + _data->owner().stories().unregisterPolling(_storyId, polling); + } else if ( + !_data->owner().stories().registerPolling(_storyId, polling)) { + return; + } + _pollingStory = enabled; } bool Gif::uploading() const { @@ -1439,7 +1472,9 @@ void Gif::hideSpoilers() { } bool Gif::needsBubble() const { - if (_data->isVideoMessage()) { + if (_storyId) { + return true; + } else if (_data->isVideoMessage()) { return false; } else if (!_caption.isEmpty()) { return true; @@ -1598,12 +1633,12 @@ void Gif::setStatusSize(int64 newSize) const { _statusText = Ui::FormatDurationText(-newSize - 1); } else if (_data->isVideoMessage()) { _statusSize = newSize; - _statusText = Ui::FormatDurationText(_data->getDuration()); + _statusText = Ui::FormatDurationText(_data->duration() / 1000); } else { File::setStatusSize( newSize, _data->size, - _data->isVideoFile() ? _data->getDuration() : -2, + _data->isVideoFile() ? (_data->duration() / 1000) : -2, 0); } } @@ -1636,7 +1671,7 @@ void Gif::updateStatusText() const { } statusSize = -1 - int((state.length - position) / state.frequency + 1); } else { - statusSize = -1 - _data->getDuration(); + statusSize = -1 - (_data->duration() / 1000); } } if (statusSize != _statusSize) { @@ -1677,6 +1712,7 @@ void Gif::unloadHeavyPart() { _thumbCache = QImage(); _videoThumbnailFrame = nullptr; _caption.unloadPersistentAnimation(); + togglePollingStory(false); } void Gif::refreshParentId(not_null realParent) { @@ -1806,6 +1842,7 @@ void Gif::setStreamed(std::unique_ptr value) { _streamed = std::move(value); if (set) { history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } else if (removed) { _parent->checkHeavyPart(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 7900f9e46..c737d2391 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -208,7 +208,10 @@ private: StateRequest request, QPoint position) const; + void togglePollingStory(bool enabled) const; + const not_null _data; + const FullStoryId _storyId; Ui::Text::String _caption; std::unique_ptr _streamed; const std::unique_ptr _spoiler; @@ -219,8 +222,9 @@ private: mutable QImage _thumbCache; mutable QImage _roundingMask; mutable std::optional _thumbCacheRounding; - mutable bool _thumbCacheBlurred = false; - mutable bool _thumbIsEllipse = false; + mutable bool _thumbCacheBlurred : 1 = false; + mutable bool _thumbIsEllipse : 1 = false; + mutable bool _pollingStory : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index da0f56871..22b5ef296 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -56,7 +56,7 @@ TimeId DurationForTimestampLinks(not_null document) { && !document->isVoiceMessage()) { return TimeId(0); } - return std::max(document->getDuration(), TimeId(0)); + return std::max(document->duration(), crl::time(0)) / 1000; } QString TimestampLinkBase( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 346d1e685..768107f65 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -98,6 +98,9 @@ public: [[nodiscard]] virtual bool hideMessageText() const { return true; } + [[nodiscard]] virtual bool hideServiceText() const { + return false; + } [[nodiscard]] virtual bool allowsFastShare() const { return false; } @@ -123,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_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 78c4be6f3..a5cf0f41c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_photo.h" #include "data/data_photo_media.h" @@ -40,6 +41,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { +constexpr auto kStoryWidth = 720; +constexpr auto kStoryHeight = 1280; + using Data::PhotoSize; } // namespace @@ -65,6 +69,9 @@ Photo::Photo( bool spoiler) : File(parent, realParent) , _data(photo) +, _storyId(realParent->media() + ? realParent->media()->storyId() + : FullStoryId()) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) { _caption = createCaption(realParent); @@ -93,6 +100,7 @@ Photo::~Photo() { _parent->checkHeavyPart(); } } + togglePollingStory(false); } void Photo::create(FullMsgId contextId, PeerData *chat) { @@ -137,6 +145,7 @@ void Photo::dataMediaCreated() const { _dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } bool Photo::hasHeavyPart() const { @@ -152,11 +161,27 @@ void Photo::unloadHeavyPart() { } _imageCache = QImage(); _caption.unloadPersistentAnimation(); + togglePollingStory(false); +} + +void Photo::togglePollingStory(bool enabled) const { + const auto pollingStory = (enabled ? 1 : 0); + if (!_storyId || _pollingStory == pollingStory) { + return; + } + const auto polling = Data::Stories::Polling::Chat; + if (!enabled) { + _data->owner().stories().unregisterPolling(_storyId, polling); + } else if ( + !_data->owner().stories().registerPolling(_storyId, polling)) { + return; + } + _pollingStory = pollingStory; } QSize Photo::countOptimalSize() { if (_serviceWidth > 0) { - return { _serviceWidth, _serviceWidth }; + return { int(_serviceWidth), int(_serviceWidth) }; } if (_parent->media() != this) { @@ -167,7 +192,7 @@ QSize Photo::countOptimalSize() { _parent->skipBlockHeight()); } - const auto dimensions = QSize(_data->width(), _data->height()); + const auto dimensions = photoSize(); const auto scaled = CountDesiredMediaSize(dimensions); const auto minWidth = std::clamp( _parent->minWidthForMedia(), @@ -201,7 +226,7 @@ QSize Photo::countOptimalSize() { QSize Photo::countCurrentSize(int newWidth) { if (_serviceWidth) { - return { _serviceWidth, _serviceWidth }; + return { int(_serviceWidth), int(_serviceWidth) }; } const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize); const auto minWidth = std::clamp( @@ -210,7 +235,7 @@ QSize Photo::countCurrentSize(int newWidth) { ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), thumbMaxWidth); - const auto dimensions = QSize(_data->width(), _data->height()); + const auto dimensions = photoSize(); auto pix = CountPhotoMediaSize( CountDesiredMediaSize(dimensions), newWidth, @@ -255,7 +280,11 @@ int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const { } void Photo::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; + } else if (_storyId && _data->isNull()) { + return; + } ensureDataMediaCreated(); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); @@ -590,11 +619,20 @@ void Photo::paintUserpicFrame( } } +QSize Photo::photoSize() const { + if (_storyId) { + return { kStoryWidth, kStoryHeight }; + } + return QSize(_data->width(), _data->height()); +} + TextState Photo::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; + } else if (_storyId && _data->isNull()) { + return result; } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); @@ -657,9 +695,8 @@ TextState Photo::textState(QPoint point, StateRequest request) const { } QSize Photo::sizeForGroupingOptimal(int maxWidth) const { - const auto width = _data->width(); - const auto height = _data->height(); - return { std::max(width, 1), std::max(height, 1) }; + const auto size = photoSize(); + return { std::max(size.width(), 1), std::max(size.height(), 1)}; } QSize Photo::sizeForGrouping(int width) const { @@ -848,8 +885,9 @@ void Photo::validateGroupedCache( return; } - const auto originalWidth = style::ConvertScale(_data->width()); - const auto originalHeight = style::ConvertScale(_data->height()); + const auto unscaled = photoSize(); + const auto originalWidth = style::ConvertScale(unscaled.width()); + const auto originalHeight = style::ConvertScale(unscaled.height()); const auto pixSize = Ui::GetImageScaleSizeForGeometry( { originalWidth, originalHeight }, { width, height }); @@ -905,6 +943,7 @@ void Photo::setStreamed(std::unique_ptr value) { _streamed = std::move(value); if (set) { history()->owner().registerHeavyViewPart(_parent); + togglePollingStory(true); } else if (removed) { _parent->checkHeavyPart(); } @@ -1012,7 +1051,7 @@ void Photo::hideSpoilers() { } bool Photo::needsBubble() const { - if (!_caption.isEmpty()) { + if (_storyId || !_caption.isEmpty()) { return true; } const auto item = _parent->data(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 9440d1421..cfe9e9627 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -158,16 +158,22 @@ private: const PaintContext &context, QPoint photoPosition) const; + [[nodiscard]] QSize photoSize() const; + + void togglePollingStory(bool enabled) const; + const not_null _data; + const FullStoryId _storyId; Ui::Text::String _caption; mutable std::shared_ptr _dataMedia; mutable std::unique_ptr _streamed; const std::unique_ptr _spoiler; mutable QImage _imageCache; mutable std::optional _imageCacheRounding; - int _serviceWidth : 30 = 0; - mutable int _imageCacheForum : 1 = 0; - mutable int _imageCacheBlurred : 1 = 0; + uint32 _serviceWidth : 28 = 0; + mutable uint32 _imageCacheForum : 1 = 0; + mutable uint32 _imageCacheBlurred : 1 = 0; + mutable uint32 _pollingStory : 1 = 0; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index b991e911f..c08d8fde5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -182,7 +182,7 @@ struct Poll::CloseInformation { }; struct Poll::RecentVoter { - not_null user; + not_null peer; mutable Ui::PeerUserpicView userpic; }; @@ -487,20 +487,20 @@ void Poll::updateRecentVoters() { _recentVoters, sliced, ranges::equal_to(), - &RecentVoter::user); + &RecentVoter::peer); if (changed) { auto updated = ranges::views::all( sliced - ) | ranges::views::transform([](not_null user) { - return RecentVoter{ user }; + ) | ranges::views::transform([](not_null peer) { + return RecentVoter{ peer }; }) | ranges::to_vector; const auto has = hasHeavyPart(); if (has) { for (auto &voter : updated) { const auto i = ranges::find( _recentVoters, - voter.user, - &RecentVoter::user); + voter.peer, + &RecentVoter::peer); if (i != end(_recentVoters)) { voter.userpic = std::move(i->userpic); } @@ -892,7 +892,7 @@ void Poll::paintRecentVoters( auto created = false; for (auto &recent : _recentVoters) { const auto was = !recent.userpic.null(); - recent.user->paintUserpic(p, recent.userpic, x, y, size); + recent.peer->paintUserpic(p, recent.userpic, x, y, size); if (!was && !recent.userpic.null()) { created = true; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 252b1e621..7df009fde 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -54,8 +54,8 @@ QString PremiumGift::title() { return tr::lng_premium_summary_title(tr::now); } -QString PremiumGift::subtitle() { - return FormatGiftMonths(_gift->months()); +TextWithEntities PremiumGift::subtitle() { + return { FormatGiftMonths(_gift->months()) }; } QString PremiumGift::button() { @@ -76,6 +76,10 @@ ClickHandlerPtr PremiumGift::createViewLink() { }); } +int PremiumGift::buttonSkip() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + void PremiumGift::draw( Painter &p, const PaintContext &context, diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index a548abe84..f3839291d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -26,14 +26,19 @@ public: int top() override; QSize size() override; QString title() override; - QString subtitle() override; + TextWithEntities subtitle() override; QString button() override; + int buttonSkip() override; void draw( Painter &p, const PaintContext &context, const QRect &geometry) override; ClickHandlerPtr createViewLink() override; + bool hideServiceText() override { + return false; + } + void stickerClearLoopPlayed() override; std::unique_ptr stickerTakePlayer( not_null data, diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 149998353..57369d1fa 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/effects/ripple_animation.h" +#include "ui/text/text_utilities.h" #include "ui/painter.h" #include "styles/style_chat.h" #include "styles/style_premium.h" @@ -27,6 +28,12 @@ ServiceBox::ServiceBox( , _content(std::move(content)) , _button([&] { auto result = Button(); + result.link = _content->createViewLink(); + + const auto text = _content->button(); + if (text.isEmpty()) { + return result; + } result.repaint = [=] { repaint(); }; result.text.setText(st::semiboldTextStyle, _content->button()); @@ -39,8 +46,6 @@ ServiceBox::ServiceBox( + padding.right(), height); - result.link = _content->createViewLink(); - return result; }()) , _maxWidth(st::msgServiceGiftBoxSize.width() @@ -53,8 +58,15 @@ ServiceBox::ServiceBox( _maxWidth) , _subtitle( st::premiumPreviewAbout.style, - _content->subtitle(), - kDefaultTextOptions, + Ui::Text::Filtered( + _content->subtitle(), + { + EntityType::Bold, + EntityType::StrikeOut, + EntityType::Underline, + EntityType::Italic, + }), + kMarkupTextOptions, _maxWidth) , _size( st::msgServiceGiftBoxSize.width(), @@ -67,8 +79,9 @@ ServiceBox::ServiceBox( : (_title.countHeight(_maxWidth) + st::msgServiceGiftBoxTitlePadding.bottom())) + _subtitle.countHeight(_maxWidth) - + st::msgServiceGiftBoxButtonMargins.top() - + _button.size.height() + + (_button.empty() + ? 0 + : (_content->buttonSkip() + _button.size.height())) + st::msgServiceGiftBoxButtonMargins.bottom())) , _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) { } @@ -106,7 +119,7 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const { top += _subtitle.countHeight(_maxWidth) + padding.bottom(); } - { + if (!_button.empty()) { const auto position = buttonRect().topLeft(); p.translate(position); @@ -142,11 +155,20 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const { TextState ServiceBox::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - { + if (_button.empty()) { + if (QRect(QPoint(), _innerSize).contains(point)) { + result.link = _button.link; + } + } else { const auto rect = buttonRect(); if (rect.contains(point)) { result.link = _button.link; _button.lastPoint = point - rect.topLeft(); + } else if (contentRect().contains(point)) { + if (!_contentLink) { + _contentLink = _content->createViewLink(); + } + result.link = _contentLink; } } return result; @@ -214,7 +236,9 @@ QRect ServiceBox::contentRect() const { } void ServiceBox::Button::toggleRipple(bool pressed) { - if (pressed) { + if (empty()) { + return; + } else if (pressed) { const auto linkWidth = size.width(); const auto linkHeight = size.height(); if (!ripple) { @@ -234,6 +258,10 @@ void ServiceBox::Button::toggleRipple(bool pressed) { } } +bool ServiceBox::Button::empty() const { + return text.isEmpty(); +} + void ServiceBox::Button::drawBg(QPainter &p) const { const auto radius = size.height() / 2.; p.drawRoundedRect(0, 0, size.width(), size.height(), radius, radius); diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index 79178994a..1ae0c30ea 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -22,7 +22,10 @@ public: [[nodiscard]] virtual int top() = 0; [[nodiscard]] virtual QSize size() = 0; [[nodiscard]] virtual QString title() = 0; - [[nodiscard]] virtual QString subtitle() = 0; + [[nodiscard]] virtual TextWithEntities subtitle() = 0; + [[nodiscard]] virtual int buttonSkip() { + return top(); + } [[nodiscard]] virtual QString button() = 0; virtual void draw( Painter &p, @@ -30,6 +33,8 @@ public: const QRect &geometry) = 0; [[nodiscard]] virtual ClickHandlerPtr createViewLink() = 0; + [[nodiscard]] virtual bool hideServiceText() = 0; + virtual void stickerClearLoopPlayed() = 0; [[nodiscard]] virtual std::unique_ptr stickerTakePlayer( not_null data, @@ -69,6 +74,10 @@ public: [[nodiscard]] bool needsBubble() const override; [[nodiscard]] bool customInfoLayout() const override; + [[nodiscard]] bool hideServiceText() const override { + return _content->hideServiceText(); + } + bool hasHeavyPart() const override; void unloadHeavyPart() override; @@ -78,10 +87,12 @@ private: const not_null _parent; const std::unique_ptr _content; + mutable ClickHandlerPtr _contentLink; struct Button { void drawBg(QPainter &p) const; void toggleRipple(bool pressed); + [[nodiscard]] bool empty() const; Fn repaint; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index b4000725b..b3ff9b069 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -412,7 +412,7 @@ ClickHandlerPtr Sticker::ShowSetHandler(not_null document) { return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto window = my.sessionWindow.get()) { - StickerSetBox::Show(window, document); + StickerSetBox::Show(window->uiShow(), document); } }); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp new file mode 100644 index 000000000..07a469097 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.cpp @@ -0,0 +1,187 @@ +/* +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 "history/view/media/history_view_story_mention.h" + +#include "core/click_handler_types.h" // ClickHandlerContext +#include "data/data_document.h" +#include "data/data_photo.h" +#include "data/data_user.h" +#include "data/data_photo_media.h" +#include "data/data_file_click_handler.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "editor/photo_editor_common.h" +#include "editor/photo_editor_layer_widget.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/view/media/history_view_sticker_player_abstract.h" +#include "history/view/history_view_element.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "window/window_session_controller.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/outline_segments.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/painter.h" +#include "mainwidget.h" +#include "apiwrap.h" +#include "api/api_peer_photo.h" +#include "settings/settings_information.h" // UpdatePhotoLocally +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +constexpr auto kReadOutlineAlpha = 0.5; + +} // namespace + +StoryMention::StoryMention( + not_null parent, + not_null story) +: _parent(parent) +, _story(story) +, _unread(story->owner().stories().isUnread(story) ? 1 : 0) { +} + +StoryMention::~StoryMention() { + if (_subscribed) { + changeSubscribedTo(0); + _parent->checkHeavyPart(); + } +} + +int StoryMention::top() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +QSize StoryMention::size() { + return { st::msgServicePhotoWidth, st::msgServicePhotoWidth }; +} + +QString StoryMention::title() { + return QString(); +} + +int StoryMention::buttonSkip() { + return st::storyMentionButtonSkip; +} + +QString StoryMention::button() { + return tr::lng_action_story_mention_button(tr::now); +} + +TextWithEntities StoryMention::subtitle() { + return _parent->data()->notificationText(); +} + +ClickHandlerPtr StoryMention::createViewLink() { + const auto itemId = _parent->data()->fullId(); + return std::make_shared(crl::guard(this, [=]( + ClickContext) { + if (const auto photo = _story->photo()) { + _parent->delegate()->elementOpenPhoto(photo, itemId); + } else if (const auto video = _story->document()) { + _parent->delegate()->elementOpenDocument(video, itemId); + } + })); +} + +void StoryMention::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + const auto showStory = _story->forbidsForward() ? 0 : 1; + if (!_thumbnail || _thumbnailFromStory != showStory) { + using namespace Dialogs::Stories; + const auto item = _parent->data(); + const auto history = item->history(); + _thumbnail = showStory + ? MakeStoryThumbnail(_story) + : MakeUserpicThumbnail(item->out() + ? history->session().user() + : history->peer); + _thumbnailFromStory = showStory; + changeSubscribedTo(0); + } + if (changeSubscribedTo(1)) { + _thumbnail->subscribeToUpdates([=] { + _parent->data()->history()->owner().requestViewRepaint(_parent); + }); + } + + const auto padding = (geometry.width() - st::storyMentionSize) / 2; + const auto size = geometry.width() - 2 * padding; + p.drawImage( + geometry.topLeft() + QPoint(padding, padding), + _thumbnail->image(size)); + + const auto thumbnail = QRectF(geometry.marginsRemoved( + QMargins(padding, padding, padding, padding))); + const auto added = 0.5 * (_unread + ? st::storyMentionUnreadSkipTwice + : st::storyMentionReadSkipTwice); + const auto outline = thumbnail.marginsAdded( + QMarginsF(added, added, added, added)); + if (_unread && _paletteVersion != style::PaletteVersion()) { + _paletteVersion = style::PaletteVersion(); + _unreadBrush = QBrush(Ui::UnreadStoryOutlineGradient(outline)); + } + auto readColor = context.st->msgServiceFg()->c; + readColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha)); + p.setPen(QPen( + _unread ? _unreadBrush : QBrush(readColor), + 0.5 * (_unread + ? st::storyMentionUnreadStrokeTwice + : st::storyMentionReadStrokeTwice))); + p.setBrush(Qt::NoBrush); + auto hq = PainterHighQualityEnabler(p); + p.drawEllipse(outline); +} + +void StoryMention::stickerClearLoopPlayed() { +} + +std::unique_ptr StoryMention::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return nullptr; +} + +bool StoryMention::hasHeavyPart() { + return _subscribed != 0; +} + +void StoryMention::unloadHeavyPart() { + if (changeSubscribedTo(0)) { + _thumbnail->subscribeToUpdates(nullptr); + } +} + +bool StoryMention::changeSubscribedTo(uint32 value) { + Expects(value == 0 || value == 1); + + if (_subscribed == value) { + return false; + } + _subscribed = value; + const auto stories = &_parent->history()->owner().stories(); + if (value) { + _parent->history()->owner().registerHeavyViewPart(_parent); + stories->registerPolling(_story, Data::Stories::Polling::Chat); + } else { + stories->unregisterPolling(_story, Data::Stories::Polling::Chat); + } + return true; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_story_mention.h b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h new file mode 100644 index 000000000..51704d440 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_story_mention.h @@ -0,0 +1,71 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_media_unwrapped.h" +#include "history/view/media/history_view_service_box.h" + +namespace Data { +class Story; +} // namespace Data + +namespace Dialogs::Stories { +class Thumbnail; +} // namespace Dialogs::Stories + +namespace HistoryView { + +class StoryMention final + : public ServiceBoxContent + , public base::has_weak_ptr { +public: + StoryMention(not_null parent, not_null story); + ~StoryMention(); + + int top() override; + QSize size() override; + QString title() override; + TextWithEntities subtitle() override; + int buttonSkip() override; + QString button() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override { + return true; + } + + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + using Thumbnail = Dialogs::Stories::Thumbnail; + + bool changeSubscribedTo(uint32 value); + + const not_null _parent; + const not_null _story; + std::shared_ptr _thumbnail; + QBrush _unreadBrush; + uint32 _paletteVersion : 29 = 0; + uint32 _thumbnailFromStory : 1 = 0; + uint32 _subscribed : 1 = 0; + uint32 _unread : 1 = 0; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index bc000ea1e..ad5dfcac1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -7,10 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_theme_document.h" +#include "boxes/background_preview_box.h" #include "history/history.h" #include "history/history_item.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" +#include "history/view/media/history_view_sticker_player_abstract.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_document_media.h" @@ -18,13 +20,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_wall_paper.h" #include "base/qthelp_url.h" +#include "core/click_handler_types.h" #include "core/local_url_handlers.h" +#include "lang/lang_keys.h" #include "ui/text/format_values.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/ui_utility.h" +#include "window/window_session_controller.h" +#include "window/themes/window_theme.h" #include "styles/style_chat.h" namespace HistoryView { @@ -32,21 +38,29 @@ namespace HistoryView { ThemeDocument::ThemeDocument( not_null parent, DocumentData *document) -: ThemeDocument(parent, document, std::nullopt) { +: ThemeDocument(parent, document, std::nullopt, 0) { } ThemeDocument::ThemeDocument( not_null parent, DocumentData *document, - const std::optional ¶ms) + const std::optional ¶ms, + int serviceWidth) : File(parent, parent->data()) -, _data(document) { +, _data(document) +, _serviceWidth(serviceWidth) { Expects(params.has_value() || _data->hasThumbnail() || _data->isTheme()); if (params) { _background = params->backgroundColors(); _patternOpacity = params->patternOpacity(); _gradientRotation = params->gradientRotation(); + _blurredWallPaper = params->isBlurred(); + _dimmingIntensity = (!params->document() + || params->isPattern() + || !_serviceWidth) + ? 0 + : std::max(params->patternIntensity(), 0); } const auto fullId = _parent->data()->fullId(); if (_data) { @@ -100,6 +114,10 @@ std::optional ThemeDocument::ParamsFromUrl( } QSize ThemeDocument::countOptimalSize() { + if (_serviceWidth > 0) { + return { _serviceWidth, _serviceWidth }; + } + if (!_data) { return { st::maxWallPaperWidth, st::maxWallPaperHeight }; } else if (_data->isTheme()) { @@ -123,6 +141,10 @@ QSize ThemeDocument::countOptimalSize() { } QSize ThemeDocument::countCurrentSize(int newWidth) { + if (_serviceWidth) { + _pixw = _pixh = _serviceWidth; + return { _serviceWidth, _serviceWidth }; + } if (!_data) { _pixw = st::maxWallPaperWidth; _pixh = st::maxWallPaperHeight; @@ -187,14 +209,16 @@ void ThemeDocument::draw(Painter &p, const PaintContext &context) const { } if (_data) { - auto statusX = paintx + st::msgDateImgDelta + st::msgDateImgPadding.x(); - auto statusY = painty + st::msgDateImgDelta + st::msgDateImgPadding.y(); - auto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x(); - auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); - Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgDateImgBg, sti->msgDateImgBgCorners); - p.setFont(st::normalFont); - p.setPen(st->msgDateImgFg()); - p.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x()); + if (!_serviceWidth) { + auto statusX = paintx + st::msgDateImgDelta + st::msgDateImgPadding.x(); + auto statusY = painty + st::msgDateImgDelta + st::msgDateImgPadding.y(); + auto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x(); + auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); + Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgDateImgBg, sti->msgDateImgBgCorners); + p.setFont(st::normalFont); + p.setPen(st->msgDateImgFg()); + p.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x()); + } if (radial || (!loaded && !_data->loading())) { const auto radialOpacity = (radial && loaded && !_data->uploading()) ? _animation->radial.opacity() : @@ -250,6 +274,11 @@ bool ThemeDocument::checkGoodThumbnail() const { } void ThemeDocument::validateThumbnail() const { + const auto isDark = Window::Theme::IsNightMode(); + if (_isDark != isDark) { + _isDark = isDark; + _thumbnailGood = -1; + } if (checkGoodThumbnail()) { if (_thumbnailGood > 0) { return; @@ -277,12 +306,30 @@ void ThemeDocument::validateThumbnail() const { } } +QImage ThemeDocument::finishServiceThumbnail(QImage image) const { + if (!_serviceWidth) { + return image; + } else if (_isDark && _dimmingIntensity > 0) { + image.setDevicePixelRatio(cIntRetinaFactor()); + auto p = QPainter(&image); + const auto alpha = 255 * _dimmingIntensity / 100; + p.fillRect(0, 0, _pixw, _pixh, QColor(0, 0, 0, alpha)); + } + if (_blurredWallPaper) { + constexpr auto kRadius = 16; + image = Images::BlurLargeImage(std::move(image), kRadius); + } + return Images::Circle(std::move(image)); +} + void ThemeDocument::generateThumbnail() const { - _thumbnail = Ui::PixmapFromImage(Ui::GenerateBackgroundImage( + auto image = Ui::GenerateBackgroundImage( QSize(_pixw, _pixh) * cIntRetinaFactor(), _background, _gradientRotation, - _patternOpacity)); + _patternOpacity); + _thumbnail = Ui::PixmapFromImage( + finishServiceThumbnail(std::move(image))); _thumbnail.setDevicePixelRatio(cRetinaFactor()); _thumbnailGood = 1; } @@ -307,9 +354,12 @@ void ThemeDocument::prepareThumbnailFrom( tw = th = 1; } const auto ratio = style::DevicePixelRatio(); + const auto resizeTo = _serviceWidth + ? QSize(tw, th).scaled(_pixw, _pixh, Qt::KeepAspectRatioByExpanding) + : QSize(_pixw, (_pixw * th) / tw); original = Images::Prepare( std::move(original), - QSize(_pixw, (_pixw * th) / tw) * ratio, + resizeTo * ratio, { .options = options, .outer = { _pixw, _pixh } }); if (isPattern) { original = Ui::PreparePatternImage( @@ -319,7 +369,8 @@ void ThemeDocument::prepareThumbnailFrom( _patternOpacity); original.setDevicePixelRatio(ratio); } - _thumbnail = Ui::PixmapFromImage(std::move(original)); + _thumbnail = Ui::PixmapFromImage( + finishServiceThumbnail(std::move(original))); _thumbnailGood = good; } @@ -382,4 +433,89 @@ void ThemeDocument::unloadHeavyPart() { _dataMedia = nullptr; } +ThemeDocumentBox::ThemeDocumentBox( + not_null parent, + const Data::WallPaper &paper) +: _parent(parent) +, _preview( + parent, + paper.document(), + paper, + st::msgServicePhotoWidth) { + _preview.initDimensions(); + _preview.resizeGetHeight(_preview.maxWidth()); +} + +ThemeDocumentBox::~ThemeDocumentBox() = default; + +int ThemeDocumentBox::top() { + return st::msgServiceGiftBoxButtonMargins.top(); +} + +QSize ThemeDocumentBox::size() { + return { _preview.maxWidth(), _preview.minHeight() }; +} + +QString ThemeDocumentBox::title() { + return QString(); +} + +TextWithEntities ThemeDocumentBox::subtitle() { + return _parent->data()->notificationText(); +} + +QString ThemeDocumentBox::button() { + return _parent->data()->out() + ? QString() + : tr::lng_action_set_wallpaper_button(tr::now); +} + +ClickHandlerPtr ThemeDocumentBox::createViewLink() { + const auto out = _parent->data()->out(); + const auto to = _parent->history()->peer; + const auto media = _parent->data()->media(); + const auto paper = media ? media->paper() : nullptr; + const auto maybe = paper ? *paper : std::optional(); + const auto itemId = _parent->data()->fullId(); + return std::make_shared([=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + if (out) { + controller->toggleChooseChatTheme(to); + } else if (maybe) { + controller->show(Box( + controller, + *maybe, + BackgroundPreviewArgs{ to, itemId })); + } + } + }); +} + +void ThemeDocumentBox::draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) { + p.translate(geometry.topLeft()); + _preview.draw(p, context); + p.translate(-geometry.topLeft()); +} + +void ThemeDocumentBox::stickerClearLoopPlayed() { +} + +std::unique_ptr ThemeDocumentBox::stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) { + return nullptr; +} + +bool ThemeDocumentBox::hasHeavyPart() { + return _preview.hasHeavyPart(); +} + +void ThemeDocumentBox::unloadHeavyPart() { + _preview.unloadHeavyPart(); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index 66dc68ad5..94549e951 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "history/view/media/history_view_file.h" +#include "history/view/media/history_view_service_box.h" class Image; @@ -24,7 +25,8 @@ public: ThemeDocument( not_null parent, DocumentData *document, - const std::optional ¶ms); + const std::optional ¶ms, + int serviceWidth = 0); ~ThemeDocument(); void draw(Painter &p, const PaintContext &context) const override; @@ -61,16 +63,17 @@ private: QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; - void fillPatternFieldsFrom(const QString &url); [[nodiscard]] bool checkGoodThumbnail() const; void validateThumbnail() const; void prepareThumbnailFrom(not_null image, int good) const; void generateThumbnail() const; void ensureDataMediaCreated() const; + [[nodiscard]] QImage finishServiceThumbnail(QImage image) const; DocumentData *_data = nullptr; int _pixw = 1; int _pixh = 1; + const int _serviceWidth = 0; mutable QPixmap _thumbnail; mutable int _thumbnailGood = -1; // -1 inline, 0 thumbnail, 1 good mutable std::shared_ptr _dataMedia; @@ -80,6 +83,46 @@ private: float64 _patternOpacity = 0.; int _gradientRotation = 0; + mutable bool _isDark = false; + int _dimmingIntensity = 0; + bool _blurredWallPaper = false; + +}; + +class ThemeDocumentBox final : public ServiceBoxContent { +public: + ThemeDocumentBox( + not_null parent, + const Data::WallPaper &paper); + ~ThemeDocumentBox(); + + int top() override; + QSize size() override; + QString title() override; + TextWithEntities subtitle() override; + QString button() override; + void draw( + Painter &p, + const PaintContext &context, + const QRect &geometry) override; + ClickHandlerPtr createViewLink() override; + + bool hideServiceText() override { + return true; + } + + void stickerClearLoopPlayed() override; + std::unique_ptr stickerTakePlayer( + not_null data, + const Lottie::ColorReplacements *replacements) override; + + bool hasHeavyPart() override; + void unloadHeavyPart() override; + +private: + const not_null _parent; + ThemeDocument _preview; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp index 1d4603a14..5c37ccb04 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.cpp @@ -143,11 +143,10 @@ void ShowSetToast( st->padding.setLeft(skip + size + skip); st->palette.linkFg = st->palette.selectLinkFg = st::mediaviewTextLinkFg; - const auto parent = Window::Show(controller).toastParent(); - const auto weak = Ui::Toast::Show(parent, { + const auto weak = controller->showToast({ .text = text, .st = st.get(), - .durationMs = kToastDuration, + .duration = kToastDuration, .multiline = true, .dark = true, .slideSide = RectPart::Bottom, @@ -212,8 +211,8 @@ QString UserpicSuggestion::button() { : tr::lng_action_suggested_photo_button(tr::now); } -QString UserpicSuggestion::subtitle() { - return _photo.parent()->data()->notificationText().text; +TextWithEntities UserpicSuggestion::subtitle() { + return _photo.parent()->data()->notificationText(); } ClickHandlerPtr UserpicSuggestion::createViewLink() { diff --git a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h index 083a19c83..42ad65f44 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h +++ b/Telegram/SourceFiles/history/view/media/history_view_userpic_suggestion.h @@ -30,7 +30,7 @@ public: int top() override; QSize size() override; QString title() override; - QString subtitle() override; + TextWithEntities subtitle() override; QString button() override; void draw( Painter &p, @@ -38,6 +38,10 @@ public: const QRect &geometry) override; ClickHandlerPtr createViewLink() override; + bool hideServiceText() override { + return true; + } + void stickerClearLoopPlayed() override; std::unique_ptr stickerTakePlayer( not_null data, 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 3b909485f..4fb1cef59 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -168,6 +168,7 @@ QSize WebPage::countOptimalSize() { && _data->photo && _data->type != WebPageType::Photo && _data->type != WebPageType::Document + && _data->type != WebPageType::Story && _data->type != WebPageType::Video) { if (_data->type == WebPageType::Profile) { _asArticle = true; @@ -828,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.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index ef4679764..911472560 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Reactions { namespace { @@ -324,6 +325,7 @@ void InlineList::paint( const QRect &clip) const { struct SingleAnimation { not_null animation; + QColor textColor; QRect target; }; std::vector animations; @@ -396,6 +398,7 @@ void InlineList::paint( button.id, ::Data::Reactions::ImageSize::InlineList); } + const auto textFg = !inbubble ? (chosen ? QPen(AdaptChosenServiceFg(st->msgServiceBg()->c)) @@ -427,6 +430,7 @@ void InlineList::paint( if (animating) { animations.push_back({ .animation = button.animation.get(), + .textColor = textFg.color(), .target = image, }); } @@ -465,7 +469,7 @@ void InlineList::paint( p, QPoint(), single.target, - QColor(255, 255, 255, 0), // Colored, for emoji status. + single.textColor, QRect(), // Clip, for emoji status. now); result = result.isEmpty() ? area : result.united(area); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp index 0117ad4a5..345512ea1 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "base/event_filter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" namespace HistoryView::Reactions { @@ -318,6 +319,7 @@ Manager::Manager( : _outer(CountOuterSize()) , _inner(QRect({}, st::reactionCornerSize)) , _strip( + st::reactPanelEmojiPan, _inner, st::reactionCornerImage, crl::guard(this, [=] { updateCurrentButton(); }), 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 aed682aeb..e24577962 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "history/history_item.h" #include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "main/main_session.h" @@ -105,48 +106,59 @@ bool StripEmoji::readyInDefaultState() { Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, - Fn close) + Fn close, + bool child) : Selector( parent, - parentController, + st, + std::move(show), reactions, (reactions.customAllowed ? ChatHelpers::EmojiListMode::FullReactions : ChatHelpers::EmojiListMode::RecentReactions), {}, iconFactory, - close) { + close, + child) { } Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, ChatHelpers::EmojiListMode mode, std::vector recent, - Fn close) + Fn close, + bool child) : Selector( parent, - parentController, + st, + std::move(show), { .customAllowed = true }, mode, std::move(recent), nullptr, - close) { + close, + child) { } Selector::Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, ChatHelpers::EmojiListMode mode, std::vector recent, IconFactory iconFactory, - Fn close) + Fn close, + bool child) : RpWidget(parent) -, _parentController(parentController.get()) +, _st(st) +, _show(std::move(show)) , _reactions(reactions) , _recent(std::move(recent)) , _listMode(mode) @@ -157,6 +169,7 @@ Selector::Selector( st::reactStripHeight) , _strip(iconFactory ? std::make_unique( + _st, QRect(0, 0, st::reactStripSize, st::reactStripSize), st::reactStripImage, crl::guard(this, [=] { update(_inner); }), @@ -167,12 +180,7 @@ Selector::Selector( , _skipy((st::reactStripHeight - st::reactStripSize) / 2) { setMouseTracking(true); - _useTransparency = Ui::Platform::TranslucentWindowsSupported(); - - parentController->content()->alive( - ) | rpl::start_with_done([=] { - close(true); - }, lifetime()); + _useTransparency = child || Ui::Platform::TranslucentWindowsSupported(); } bool Selector::useTransparency() const { @@ -241,14 +249,14 @@ QMargins Selector::extentsForShadow() const { } int Selector::extendTopForCategories() const { - return _reactions.customAllowed ? st::reactPanelEmojiPan.footer : 0; + return _reactions.customAllowed ? _st.footer : 0; } int Selector::minimalHeight() const { return _skipy + (_recentRows * _size) + st::emojiPanRadius - + st::reactPanelEmojiPan.padding.bottom(); + + _st.padding.bottom(); } void Selector::setSpecialExpandTopSkip(int skip) { @@ -269,7 +277,7 @@ void Selector::initGeometry(int innerTop) { ? (extendTopForCategories() + _specialExpandTopSkip) : 0; const auto top = innerTop - extents.top() - _collapsedTopSkip; - const auto add = st::reactStripBubble.height() - extents.bottom(); + const auto add = _st.icons.stripBubble.height() - extents.bottom(); _outer = QRect(0, _collapsedTopSkip, width, height); _outerWithBubble = _outer.marginsAdded({ 0, 0, 0, add }); setGeometry(_outerWithBubble.marginsAdded( @@ -288,6 +296,10 @@ void Selector::beforeDestroy() { } } +rpl::producer<> Selector::escapes() const { + return _escapes.events(); +} + void Selector::updateShowState( float64 progress, float64 opacity, @@ -316,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); @@ -325,13 +341,10 @@ void Selector::paintAppearing(QPainter &p) { if (_paintBuffer.size() != _outerWithBubble.size() * factor) { _paintBuffer = _cachedRound.PrepareImage(_outerWithBubble.size()); } - _paintBuffer.fill(st::defaultPopupMenu.menu.itemBg->c); + _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()); @@ -344,7 +357,7 @@ void Selector::paintAppearing(QPainter &p) { 1., false); - _cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c); + _cachedRound.setBackgroundColor(_st.bg->c); _cachedRound.setShadowColor(st::shadowFg->c); q.translate(QPoint(0, _collapsedTopSkip) - _inner.topLeft()); const auto radius = st::reactStripHeight / 2; @@ -379,7 +392,7 @@ void Selector::paintBackgroundToBuffer() { } _paintBuffer.fill(Qt::transparent); - _cachedRound.setBackgroundColor(st::defaultPopupMenu.menu.itemBg->c); + _cachedRound.setBackgroundColor(_st.bg->c); _cachedRound.setShadowColor(st::shadowFg->c); auto p = QPainter(&_paintBuffer); @@ -399,7 +412,7 @@ void Selector::paintCollapsed(QPainter &p) { } p.drawImage(_outer.topLeft(), _paintBuffer); } else { - p.fillRect(_inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(_inner, _st.bg); } _strip->paint( p, @@ -422,8 +435,9 @@ void Selector::paintExpanding(Painter &p, float64 progress) { } _list->paintExpanding( p, - rects.list.marginsRemoved(st::reactPanelEmojiPan.margin), + rects.list.marginsRemoved(_st.margin), rects.finalBottom, + rects.expanding, progress, RectPart::TopRight); paintFadingExpandIcon(p, progress); @@ -453,11 +467,11 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress) const auto pattern = _cachedRound.validateFrame(frame, 1., radius); const auto fill = _cachedRound.FillWithImage(p, outer, pattern); if (!fill.isEmpty()) { - p.fillRect(fill, st::defaultPopupMenu.menu.itemBg); + p.fillRect(fill, _st.bg); } } else { const auto inner = outer.marginsRemoved(extentsForShadow()); - p.fillRect(inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(inner, _st.bg); p.fillRect( inner.x(), inner.y() + inner.height(), @@ -479,6 +493,7 @@ auto Selector::paintExpandingBg(QPainter &p, float64 progress) .categories = QRect(inner.x(), inner.y(), inner.width(), categories), .list = inner.marginsRemoved({ 0, categories, 0, 0 }), .radius = radius, + .expanding = expanding, .finalBottom = height() - extents.bottom(), }; } @@ -507,7 +522,7 @@ void Selector::paintExpanded(QPainter &p) { p.drawImage(0, 0, _paintBuffer); } else { const auto inner = rect().marginsRemoved(extentsForShadow()); - p.fillRect(inner, st::defaultPopupMenu.menu.itemBg); + p.fillRect(inner, _st.bg); p.fillRect( inner.x(), inner.y() + inner.height(), @@ -530,7 +545,7 @@ void Selector::finishExpand() { st::emojiPanRadius); const auto fill = _cachedRound.FillWithImage(q, rect(), pattern); if (!fill.isEmpty()) { - q.fillRect(fill, st::defaultPopupMenu.menu.itemBg); + q.fillRect(fill, _st.bg); } } if (_footer) { @@ -538,14 +553,11 @@ void Selector::finishExpand() { } _scroll->show(); _list->afterShown(); - - if (const auto controller = _parentController.get()) { - controller->session().api().updateCustomEmoji(); - } + _show->session().api().updateCustomEmoji(); } void Selector::paintBubble(QPainter &p, int innerWidth) { - const auto &bubble = st::reactStripBubble; + const auto &bubble = _st.icons.stripBubble; const auto bubbleRight = std::min( st::reactStripBubbleRight, (innerWidth - bubble.width()) / 2); @@ -657,11 +669,30 @@ ChosenReaction Selector::lookupChosen(const Data::ReactionId &id) const { return result; } +void Selector::preloadAllRecentsAnimations() { + const auto preload = [&](DocumentData *document) { + const auto view = document + ? document->activeMediaView() + : nullptr; + if (view) { + view->checkStickerLarge(); + } + }; + for (const auto &reaction : _reactions.recent) { + if (!reaction.id.custom()) { + preload(reaction.centerIcon); + } + preload(reaction.aroundAnimation); + } +} + void Selector::expand() { if (_expandScheduled) { return; } _expandScheduled = true; + _willExpand.fire({}); + preloadAllRecentsAnimations(); const auto parent = parentWidget()->geometry(); const auto extents = extentsForShadow(); const auto heightLimit = _reactions.customAllowed @@ -672,15 +703,14 @@ void Selector::expand() { extents.top() + heightLimit + extents.bottom()); const auto additionalBottom = willBeHeight - height(); const auto additional = _specialExpandTopSkip + additionalBottom; - const auto strong = _parentController.get(); - if (additionalBottom < 0 || additional <= 0 || !strong) { + if (additionalBottom < 0 || additional <= 0) { return; } else if (additionalBottom > 0) { resize(width(), height() + additionalBottom); raise(); } - createList(strong); + createList(); cacheExpandIcon(); [[maybe_unused]] const auto grabbed = Ui::GrabWidget(_scroll); @@ -705,7 +735,7 @@ void Selector::cacheExpandIcon() { _strip->paintOne(q, _strip->count() - 1, { 0, 0 }, 1.); } -void Selector::createList(not_null controller) { +void Selector::createList() { using namespace ChatHelpers; auto recent = _recent; auto defaultReactionIds = base::flat_map(); @@ -725,7 +755,7 @@ void Selector::createList(not_null controller) { } }; } - const auto manager = &controller->session().data().customEmojiManager(); + const auto manager = &_show->session().data().customEmojiManager(); _stripPaintOneShift = [&] { // See EmojiListWidget custom emoji position resolving. const auto area = st::emojiPanArea; @@ -769,17 +799,15 @@ void Selector::createList(not_null controller) { _scroll = Ui::CreateChild(this, st::reactPanelScroll); _scroll->hide(); - const auto st = lifetime().make_state( - st::reactPanelEmojiPan); + const auto st = lifetime().make_state(_st); st->padding.setTop(_skipy); if (!_reactions.customAllowed) { st->bg = st::transparent; } _list = _scroll->setOwnedWidget( object_ptr(_scroll, EmojiListDescriptor{ - .session = &controller->session(), + .show = _show, .mode = _listMode, - .controller = controller, .paused = [] { return false; }, .customRecentList = std::move(recent), .customRecentFactory = std::move(factory), @@ -787,6 +815,8 @@ void Selector::createList(not_null controller) { }) ).data(); + _list->escapes() | rpl::start_to_stream(_escapes, _list->lifetime()); + _list->customChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { const auto id = DocumentId{ data.document->id }; @@ -831,8 +861,7 @@ void Selector::createList(not_null controller) { }, _shadow->lifetime()); _shadow->show(); } - const auto geometry = inner.marginsRemoved( - st::reactPanelEmojiPan.margin); + const auto geometry = inner.marginsRemoved(_st.margin); _list->move(0, 0); _list->resizeToWidth(geometry.width()); _list->refreshEmoji(); @@ -853,7 +882,7 @@ void Selector::createList(not_null controller) { }, _list->lifetime()); _scroll->setGeometry(inner.marginsRemoved({ - st::reactPanelEmojiPan.margin.left(), + _st.margin.left(), _footer ? _footer->height() : 0, 0, 0, @@ -938,10 +967,12 @@ AttachSelectorResult MakeJustSelectorMenu( Fn chosen) { const auto selector = Ui::CreateChild( menu.get(), - controller, + st::reactPanelEmojiPan, + controller->uiShow(), mode, std::move(recent), - [=](bool fast) { menu->hideMenu(fast); }); + [=](bool fast) { menu->hideMenu(fast); }, + false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { return AttachSelectorResult::Failed; } @@ -1005,19 +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(), - controller, + st, + std::move(show), std::move(reactions), std::move(iconFactory), - [=](bool fast) { menu->hideMenu(fast); }); + [=](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); @@ -1034,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) { @@ -1067,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 6e6d119cd..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" @@ -19,6 +20,7 @@ struct ReactionId; } // namespace Data namespace ChatHelpers { +class Show; class TabbedPanel; class EmojiListWidget; class StickersListFooter; @@ -41,16 +43,20 @@ class Selector final : public Ui::RpWidget { public: Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, IconFactory iconFactory, - Fn close); + Fn close, + bool child = false); Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, ChatHelpers::EmojiListMode mode, std::vector recent, - Fn close); + Fn close, + bool child = false); [[nodiscard]] bool useTransparency() const; @@ -58,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(); @@ -68,6 +75,10 @@ public: [[nodiscard]] rpl::producer<> premiumPromoChosen() const { return _premiumPromoChosen.events(); } + [[nodiscard]] rpl::producer<> willExpand() const { + return _willExpand.events(); + } + [[nodiscard]] rpl::producer<> escapes() const; void updateShowState( float64 progress, @@ -82,17 +93,20 @@ private: QRect categories; QRect list; float64 radius = 0.; + float64 expanding = 0.; int finalBottom = 0; }; Selector( not_null parent, - not_null parentController, + const style::EmojiPan &st, + std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, ChatHelpers::EmojiListMode mode, std::vector recent, IconFactory iconFactory, - Fn close); + Fn close, + bool child); void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; @@ -116,11 +130,13 @@ private: void expand(); void cacheExpandIcon(); - void createList(not_null controller); + void createList(); void finishExpand(); ChosenReaction lookupChosen(const Data::ReactionId &id) const; + void preloadAllRecentsAnimations(); - const base::weak_ptr _parentController; + const style::EmojiPan &_st; + const std::shared_ptr _show; const Data::PossibleItemReactions _reactions; const std::vector _recent; const ChatHelpers::EmojiListMode _listMode; @@ -133,6 +149,8 @@ private: rpl::event_stream _chosen; rpl::event_stream<> _premiumPromoChosen; + rpl::event_stream<> _willExpand; + rpl::event_stream<> _escapes; Ui::ScrollArea *_scroll = nullptr; ChatHelpers::EmojiListWidget *_list = nullptr; @@ -193,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/history/view/reactions/history_view_reactions_strip.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp index 865f9293c..f9c27b15c 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/animated_icon.h" #include "ui/painter.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" namespace HistoryView::Reactions { namespace { @@ -37,20 +38,38 @@ constexpr auto kHoverScale = 1.24; return std::make_shared(Ui::AnimatedIconDescriptor{ .generator = DocumentIconFrameGenerator(media), .sizeOverride = QSize(size, size), + .colorized = media->owner()->emojiUsesTextColor(), }); } } // namespace Strip::Strip( + const style::EmojiPan &st, QRect inner, int size, Fn update, IconFactory iconFactory) -: _iconFactory(std::move(iconFactory)) +: _st(st) +, _iconFactory(std::move(iconFactory)) , _inner(inner) , _finalSize(size) , _update(std::move(update)) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + invalidateMainReactionImage(); + }, _lifetime); +} + +void Strip::invalidateMainReactionImage() { + if (_mainReactionImage.isNull() + && !ranges::contains(_validEmoji, true)) { + return; + } + const auto was = base::take(_mainReactionMedia); + _mainReactionImage = QImage(); + ranges::fill(_validEmoji, false); + resolveMainReactionIcon(); } void Strip::applyList( @@ -157,7 +176,11 @@ void Strip::paintOne( } else { const auto paintFrame = [&](not_null animation) { const auto size = int(std::floor(target.width() + 0.01)); - const auto frame = animation->frame({ size, size }, _update); + const auto &textColor = _st.textFg->c; + const auto frame = animation->frame( + textColor, + { size, size }, + _update); p.drawImage(target, frame.image); }; @@ -218,9 +241,9 @@ int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const { } const auto &icon = *i; if (const auto &appear = icon.appear; appear && appear->animating()) { - chosen.icon = appear->frame(); + chosen.icon = appear->frame(_st.textFg->c); } else if (const auto &select = icon.select; select && select->valid()) { - chosen.icon = select->frame(); + chosen.icon = select->frame(_st.textFg->c); } return (i - begin(_icons)); } @@ -243,7 +266,7 @@ void Strip::paintPremiumIcon( p.translate(-target.center()); } auto hq = PainterHighQualityEnabler(p); - st::reactionPremiumLocked.paintInCenter(p, to); + _st.icons.stripPremiumLocked.paintInCenter(p, to); if (scale != 1.) { p.restore(); } @@ -268,8 +291,8 @@ void Strip::paintExpandIcon( } auto hq = PainterHighQualityEnabler(p); ((_finalSize == st::reactionCornerImage) - ? st::reactionsExpandDropdown - : st::reactionExpandPanel).paintInCenter(p, to); + ? _st.icons.stripExpandDropdown + : _st.icons.stripExpandPanel).paintInCenter(p, to); if (scale != 1.) { p.restore(); } @@ -475,7 +498,7 @@ void Strip::setMainReactionIcon() { if (i != end(_loadCache) && i->second.icon) { const auto &icon = i->second.icon; if (!icon->frameIndex() && icon->width() == MainReactionSize()) { - _mainReactionImage = i->second.icon->frame(); + _mainReactionImage = i->second.icon->frame(_st.textFg->c); return; } } @@ -522,7 +545,8 @@ Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) { p.fillRect(QRect(position, result.rect.size() / ratio), Qt::transparent); if (_mainReactionImage.isNull() && _mainReactionIcon) { - _mainReactionImage = base::take(_mainReactionIcon)->frame(); + _mainReactionImage = base::take(_mainReactionIcon)->frame( + _st.textFg->c); } if (!_mainReactionImage.isNull()) { const auto target = QRect( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h index 56726ae88..e8a98fd49 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_strip.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/round_area_with_shadow.h" #include "data/data_message_reaction_id.h" +namespace style { +struct EmojiPan; +} // namespace style + class HistoryItem; namespace Data { @@ -44,7 +48,12 @@ class Strip final { public: using ReactionId = Data::ReactionId; - Strip(QRect inner, int size, Fn update, IconFactory iconFactory); + Strip( + const style::EmojiPan &st, + QRect inner, + int size, + Fn update, + IconFactory iconFactory); enum class AddedButton : uchar { None, @@ -117,9 +126,11 @@ private: [[nodiscard]] Fn resolveCountTargetMethod( float64 scale) const; + void invalidateMainReactionImage(); void resolveMainReactionIcon(); void setMainReactionIcon(); + const style::EmojiPan &_st; const IconFactory _iconFactory; const QRect _inner; const int _finalSize = 0; @@ -141,6 +152,8 @@ private: QImage _emojiParts; std::array _validEmoji = { { false } }; + rpl::lifetime _lifetime; + }; class CachedIconFactory final { diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp index fb8e5f9a0..fed48653b 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.cpp @@ -172,7 +172,7 @@ InnerWidget::InnerWidget( not_null controller, not_null user) : RpWidget(parent) -, _show(std::make_unique(controller->parentController())) +, _show(controller->uiShow()) , _controller(controller) , _user(user) , _listController(std::make_unique(controller, _user)) @@ -291,8 +291,8 @@ void InnerWidget::peerListHideLayer() { _show->hideLayer(); } -not_null InnerWidget::peerListToastParent() { - return _show->toastParent(); +std::shared_ptr InnerWidget::peerListUiShow() { + return _show; } } // namespace CommonGroups diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h index dc8573355..91d906782 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_inner_widget.h @@ -11,9 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "boxes/peer_list_box.h" -namespace Window { +namespace Ui { class Show; -} // namespace Window +} // namespace Ui namespace Info { @@ -68,13 +68,13 @@ private: object_ptr content, Ui::LayerOptions options = Ui::LayerOption::KeepOther) override; void peerListHideLayer() override; - not_null peerListToastParent() override; + std::shared_ptr peerListUiShow() override; object_ptr setupList( RpWidget *parent, not_null controller) const; - std::unique_ptr _show; + std::shared_ptr _show; not_null _controller; not_null _user; std::unique_ptr _listController; diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h index 2d2047f5e..65b55f4d0 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h +++ b/Telegram/SourceFiles/info/downloads/info_downloads_inner_widget.h @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" namespace Ui { -class SettingsSlider; class VerticalLayout; class SearchFieldController; } // namespace Ui diff --git a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp index 26a46d22a..6efd528f6 100644 --- a/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp +++ b/Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null controller) } Memento::Memento(not_null self) -: ContentMemento(Downloads::Tag{}) +: ContentMemento(Tag{}) , _media(self, 0, Media::Type::File) { } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index dfafc3ef6..ffd146b89 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -28,6 +28,25 @@ InfoPeerBadge { sizeTag: int; } +InfoTopBar { + height: pixels; + back: IconButton; + title: FlatLabel; + titlePosition: point; + bg: color; + mediaCancel: IconButton; + mediaActionsSkip: pixels; + mediaForward: IconButton; + mediaDelete: IconButton; + storiesSave: IconButton; + storiesArchive: IconButton; + search: IconButton; + searchRow: SearchFieldRow; + highlightBg: color; + highlightDuration: int; + radius: pixels; +} + infoMediaHeaderFg: windowFg; infoToggle: InfoToggle { @@ -156,6 +175,14 @@ infoTopBarDelete: IconButton(infoTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoTopBarSaveStories: IconButton(infoTopBarForward) { + icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }}; +} +infoTopBarArchiveStories: IconButton(infoTopBarForward) { + icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; +} infoTopBar: InfoTopBar { height: infoTopBarHeight; back: infoTopBarBack; @@ -166,6 +193,8 @@ infoTopBar: InfoTopBar { mediaActionsSkip: 4px; mediaForward: infoTopBarForward; mediaDelete: infoTopBarDelete; + storiesSave: infoTopBarSaveStories; + storiesArchive: infoTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; highlightBg: windowBgOver; @@ -221,6 +250,14 @@ infoLayerTopBarDelete: IconButton(infoLayerTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "info/info_stories_to_profile", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_profile", boxTitleCloseFgOver }}; +} +infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "info/info_stories_to_archive", boxTitleCloseFg }}; + iconOver: icon {{ "info/info_stories_to_archive", boxTitleCloseFgOver }}; +} infoLayerTopBar: InfoTopBar(infoTopBar) { height: infoLayerTopBarHeight; back: infoLayerTopBarBack; @@ -231,6 +268,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { mediaActionsSkip: 6px; mediaForward: infoLayerTopBarForward; mediaDelete: infoLayerTopBarDelete; + storiesSave: infoLayerTopBarSaveStories; + storiesArchive: infoLayerTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; radius: boxRadius; @@ -367,15 +406,9 @@ infoIconMediaAudio: icon {{ "info/info_media_audio", infoIconFg }}; infoIconMediaLink: icon {{ "info/info_media_link", infoIconFg }}; infoIconMediaGroup: icon {{ "info/info_common_groups", infoIconFg }}; infoIconMediaVoice: icon {{ "info/info_media_voice", 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 }}; +infoIconMediaStories: icon {{ "info/info_media_stories", infoIconFg }}; +infoIconMediaStoriesArchive: icon {{ "info/info_stories_archive", infoIconFg }}; +infoIconMediaStoriesRecent: icon {{ "info/info_stories_recent", infoIconFg }}; infoIconShare: icon {{ "info/info_share", infoIconFg }}; infoIconEdit: icon {{ "info/info_edit", infoIconFg }}; @@ -389,22 +422,6 @@ infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaButtonIconPosition: point(20px, 3px); infoGroupMembersIconPosition: point(20px, 10px); infoChannelMembersIconPosition: point(20px, 19px); -infoLabeledOneLine: FlatLabel(defaultFlatLabel) { - maxHeight: 20px; - style: TextStyle(defaultTextStyle) { - lineHeight: 19px; - } - margin: margins(5px, 5px, 5px, 5px); -} -infoLabelSkip: 2px; -infoLabeled: FlatLabel(infoLabeledOneLine) { - minWidth: 180px; - maxHeight: 0px; - margin: margins(5px, 5px, 5px, 5px); -} -infoLabel: FlatLabel(infoLabeled) { - textFg: windowSubTextFg; -} infoBlockHeaderLabel: FlatLabel(infoProfileStatus) { textFg: windowBoldFg; @@ -447,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; @@ -462,6 +481,15 @@ infoMembersList: PeerList(defaultPeerList) { photoPosition: point(18px, 6px); namePosition: point(79px, 11px); statusPosition: point(79px, 31px); + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectExtendTwice: 1px; + imageRadius: 21px; + imageSmallRadius: 19px; + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; } } infoMembersButtonPosition: point(12px, 0px); @@ -509,7 +537,8 @@ infoMediaHeaderStyle: TextStyle(semiboldTextStyle) { } infoMediaHeaderHeight: 28px; infoMediaHeaderPosition: point(14px, 6px); -infoMediaSkip: 5px; +infoMediaSkip: 2px; +infoMediaLeft: 3px; infoMediaMargin: margins(0px, 6px, 0px, 2px); infoMediaMinGridSize: 90px; @@ -571,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; @@ -589,6 +623,7 @@ infoEmptyAudio: icon {{ "info/info_media_audio_empty", infoEmptyFg }}; infoEmptyFile: icon {{ "info/info_media_file_empty", infoEmptyFg }}; infoEmptyVoice: icon {{ "info/info_media_voice_empty", infoEmptyFg }}; infoEmptyLink: icon {{ "info/info_media_link_empty", infoEmptyFg }}; +infoEmptyStories: icon {{ "info/info_media_story_empty", infoEmptyFg }}; infoEmptyIconTop: 120px; infoEmptyLabelTop: 40px; infoEmptyLabelSkip: 20px; @@ -597,23 +632,45 @@ infoEmptyLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } +infoStoriesAboutArchive: FlatLabel(defaultFlatLabel) { + minWidth: 245px; + align: align(top); + textFg: windowSubTextFg; + style: defaultTextStyle; +} +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) { @@ -800,6 +857,9 @@ inviteLinkList: PeerList(defaultPeerList) { item: inviteLinkListItem; padding: margins(0px, 4px, 0px, 0px); } +inviteLinkChatList: PeerList(peerListBox) { + padding: margins(0px, 4px, 0px, 6px); +} inviteLinkAdminsList: PeerList(inviteLinkList) { item: PeerListItem(inviteLinkListItem) { photoPosition: point(16px, 9px); @@ -885,10 +945,4 @@ shortInfoCover: ShortInfoCover { } } -reportReasonTopSkip: 8px; -reportReasonButton: SettingsButton(infoProfileButton) { - style: boxTextStyle; - padding: margins(62px, 7px, 8px, 7px); -} - permissionsExpandIcon: icon{{ "info/edit/expand_arrow_small", windowBoldFg }}; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index fcd405a5e..e96ba3cfc 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -273,6 +273,11 @@ void ContentWidget::setViewport( }, _scroll->lifetime()); } +auto ContentWidget::titleStories() +-> rpl::producer { + return nullptr; +} + void ContentWidget::saveChanges(FnMut done) { done(); } @@ -332,6 +337,8 @@ Key ContentMemento::key() const { return Key(poll, pollContextId()); } else if (const auto self = settingsSelf()) { return Settings::Tag{ self }; + } else if (const auto peer = storiesPeer()) { + return Stories::Tag{ peer, storiesTab() }; } else { return Downloads::Tag(); } @@ -363,4 +370,9 @@ ContentMemento::ContentMemento(Settings::Tag settings) ContentMemento::ContentMemento(Downloads::Tag downloads) { } +ContentMemento::ContentMemento(Stories::Tag stories) +: _storiesPeer(stories.peer) +, _storiesTab(stories.tab) { +} + } // namespace Info diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 44f4e2d8c..6a6f75bbd 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rp_widget.h" #include "info/info_wrap_widget.h" +namespace Dialogs::Stories { +struct Content; +} // namespace Dialogs::Stories + namespace Storage { enum class SharedMediaType : signed char; } // namespace Storage @@ -24,14 +28,20 @@ template class PaddingWrap; } // namespace Ui -namespace Info { -namespace Settings { +namespace Info::Settings { struct Tag; -} // namespace Settings +} // namespace Info::Settings -namespace Downloads { +namespace Info::Downloads { struct Tag; -} // namespace Downloads +} // namespace Info::Downloads + +namespace Info::Stories { +struct Tag; +enum class Tab; +} // namespace Info::Stories + +namespace Info { class ContentMemento; class Controller; @@ -82,6 +92,8 @@ public: } [[nodiscard]] virtual rpl::producer title() = 0; + [[nodiscard]] virtual auto titleStories() + -> rpl::producer; virtual void saveChanges(FnMut done); @@ -150,6 +162,7 @@ public: PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); + explicit ContentMemento(Stories::Tag stories); ContentMemento(not_null poll, FullMsgId contextId) : _poll(poll) , _pollContextId(contextId) { @@ -172,6 +185,12 @@ public: UserData *settingsSelf() const { return _settingsSelf; } + PeerData *storiesPeer() const { + return _storiesPeer; + } + Stories::Tab storiesTab() const { + return _storiesTab; + } PollData *poll() const { return _poll; } @@ -214,6 +233,8 @@ private: const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; UserData * const _settingsSelf = nullptr; + PeerData * const _storiesPeer = nullptr; + Stories::Tab _storiesTab = {}; PollData * const _poll = nullptr; const FullMsgId _pollContextId; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index 58dda92fe..d7beb981e 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -40,6 +40,9 @@ Key::Key(Settings::Tag settings) : _value(settings) { Key::Key(Downloads::Tag downloads) : _value(downloads) { } +Key::Key(Stories::Tag stories) : _value(stories) { +} + Key::Key(not_null poll, FullMsgId contextId) : _value(PollKey{ poll, contextId }) { } @@ -72,6 +75,20 @@ bool Key::isDownloads() const { return v::is(_value); } +PeerData *Key::storiesPeer() const { + if (const auto tag = std::get_if(&_value)) { + return tag->peer; + } + return nullptr; +} + +Stories::Tab Key::storiesTab() const { + if (const auto tag = std::get_if(&_value)) { + return tag->tab; + } + return Stories::Tab(); +} + PollData *Key::poll() const { if (const auto data = std::get_if(&_value)) { return data->poll; @@ -249,7 +266,8 @@ bool Controller::validateMementoPeer( not_null memento) const { return memento->peer() == peer() && memento->migratedPeerId() == migratedPeerId() - && memento->settingsSelf() == settingsSelf(); + && memento->settingsSelf() == settingsSelf() + && memento->storiesPeer() == storiesPeer(); } void Controller::setSection(not_null memento) { diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index cdbf53396..82eb6f8eb 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -36,6 +36,25 @@ struct Tag { } // namespace Info::Downloads +namespace Info::Stories { + +enum class Tab { + Saved, + Archive, +}; + +struct Tag { + explicit Tag(not_null peer, Tab tab = {}) + : peer(peer) + , tab(tab) { + } + + not_null peer; + Tab tab = {}; +}; + +} // namespace Info::Stories + namespace Info { class Key { @@ -44,12 +63,15 @@ public: explicit Key(not_null topic); Key(Settings::Tag settings); Key(Downloads::Tag downloads); + Key(Stories::Tag stories); Key(not_null poll, FullMsgId contextId); PeerData *peer() const; Data::ForumTopic *topic() const; UserData *settingsSelf() const; bool isDownloads() const; + PeerData *storiesPeer() const; + Stories::Tab storiesTab() const; PollData *poll() const; FullMsgId pollContextId() const; @@ -63,6 +85,7 @@ private: not_null, Settings::Tag, Downloads::Tag, + Stories::Tag, PollKey> _value; }; @@ -81,6 +104,7 @@ public: Members, Settings, Downloads, + Stories, PollResults, }; using SettingsType = ::Settings::Type; @@ -123,23 +147,29 @@ class AbstractController : public Window::SessionNavigation { public: AbstractController(not_null parent); - virtual Key key() const = 0; - virtual PeerData *migrated() const = 0; - virtual Section section() const = 0; + [[nodiscard]] virtual Key key() const = 0; + [[nodiscard]] virtual PeerData *migrated() const = 0; + [[nodiscard]] virtual Section section() const = 0; - PeerData *peer() const; - PeerId migratedPeerId() const; - Data::ForumTopic *topic() const { + [[nodiscard]] PeerData *peer() const; + [[nodiscard]] PeerId migratedPeerId() const; + [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } - UserData *settingsSelf() const { + [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } - bool isDownloads() const { + [[nodiscard]] bool isDownloads() const { return key().isDownloads(); } - PollData *poll() const; - FullMsgId pollContextId() const { + [[nodiscard]] PeerData *storiesPeer() const { + return key().storiesPeer(); + } + [[nodiscard]] Stories::Tab storiesTab() const { + return key().storiesTab(); + } + [[nodiscard]] PollData *poll() const; + [[nodiscard]] FullMsgId pollContextId() const { return key().pollContextId(); } diff --git a/Telegram/SourceFiles/info/info_section_widget.h b/Telegram/SourceFiles/info/info_section_widget.h index 8ff1dbcfa..f53ed505f 100644 --- a/Telegram/SourceFiles/info/info_section_widget.h +++ b/Telegram/SourceFiles/info/info_section_widget.h @@ -10,10 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include "window/section_widget.h" -namespace Ui { -class SettingsSlider; -} // namespace Ui - namespace Window { class ConnectionState; } // namespace Window diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 37dfe5529..f15a6b549 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" @@ -30,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_user.h" +#include "styles/style_dialogs.h" #include "styles/style_info.h" namespace Info { @@ -88,9 +91,11 @@ void TopBar::setTitle(rpl::producer &&title) { object_ptr(this, std::move(title), _st.title), st::infoTopBarScale); _title->setDuration(st::infoTopBarDuration); - _title->toggle(!selectionMode(), anim::type::instant); + _title->toggle( + !selectionMode() && !storiesTitle(), + anim::type::instant); registerToggleControlCallback(_title.data(), [=] { - return !selectionMode() && !searchMode(); + return !selectionMode() && !storiesTitle() && !searchMode(); }); if (_back) { @@ -119,6 +124,9 @@ void TopBar::enableBackButton() { if (_title) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); } + if (_storiesWrap) { + _storiesWrap->raise(); + } updateControlsGeometry(width()); } @@ -309,12 +317,15 @@ int TopBar::resizeGetHeight(int newWidth) { void TopBar::updateControlsGeometry(int newWidth) { updateDefaultControlsGeometry(newWidth); updateSelectionControlsGeometry(newWidth); + updateStoriesGeometry(newWidth); } void TopBar::updateDefaultControlsGeometry(int newWidth) { auto right = 0; for (auto &button : _buttons) { - if (!button) continue; + if (!button) { + continue; + } button->moveToRight(right, 0, newWidth); right += button->width(); } @@ -344,6 +355,10 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) { _delete->moveToRight(right, 0, newWidth); right += _delete->width(); } + if (_canToggleStoryPin) { + _toggleStoryPin->moveToRight(right, 0, newWidth); + right += _toggleStoryPin->width(); + } if (_canForward) { _forward->moveToRight(right, 0, newWidth); right += _forward->width(); @@ -362,6 +377,31 @@ void TopBar::updateSelectionControlsGeometry(int newWidth) { newWidth); } +void TopBar::updateStoriesGeometry(int newWidth) { + if (!_stories) { + return; + } + + auto right = 0; + for (auto &button : _buttons) { + if (!button) { + continue; + } + button->moveToRight(right, 0, newWidth); + right += button->width(); + } + const auto &small = st::dialogsStories; + const auto wrapLeft = (_back ? _st.back.width : 0); + const auto left = _back + ? 0 + : (_st.titlePosition.x() - small.left - small.photoLeft); + const auto height = small.photo + 2 * small.photoTop; + const auto top = _st.titlePosition.y() + + (_st.title.style.font->height - height) / 2; + _stories->setLayoutConstraints({ left, top }, style::al_left); + _storiesWrap->move(wrapLeft, 0); +} + void TopBar::paintEvent(QPaintEvent *e) { auto p = QPainter(this); @@ -412,6 +452,99 @@ void TopBar::updateControlsVisibility(anim::type animated) { } } +void TopBar::setStories(rpl::producer content) { + _storiesLifetime.destroy(); + delete _storiesWrap.data(); + if (content) { + using namespace Dialogs::Stories; + + auto last = std::move( + content + ) | rpl::start_spawning(_storiesLifetime); + + _storiesWrap = _storiesLifetime.make_state< + Ui::FadeWrap + >(this, object_ptr(this), st::infoTopBarScale); + registerToggleControlCallback( + _storiesWrap.data(), + [this] { return _storiesCount > 0; }); + _storiesWrap->toggle(false, anim::type::instant); + _storiesWrap->setDuration(st::infoTopBarDuration); + + const auto button = _storiesWrap->entity(); + const auto stories = Ui::CreateChild( + button, + st::dialogsStoriesListInfo, + rpl::duplicate( + last + ) | rpl::filter([](const Content &content) { + return !content.elements.empty(); + })); + const auto label = Ui::CreateChild( + button, + QString(), + _st.title); + stories->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + stories->geometryValue( + ) | rpl::start_with_next([=](QRect geometry) { + const auto skip = _st.title.style.font->spacew; + label->move( + geometry.x() + geometry.width() + skip, + _st.titlePosition.y()); + }, label->lifetime()); + rpl::combine( + _storiesWrap->positionValue(), + label->geometryValue() + ) | rpl::start_with_next([=] { + button->resize( + label->x() + label->width() + _st.titlePosition.x(), + _st.height); + }, button->lifetime()); + + _stories = stories; + _stories->clicks( + ) | rpl::start_to_stream(_storyClicks, _stories->lifetime()); + + button->setClickedCallback([=] { + _storyClicks.fire({}); + }); + + rpl::duplicate( + last + ) | rpl::start_with_next([=](const Content &content) { + const auto count = int(content.elements.size()); + if (_storiesCount != count) { + const auto was = (_storiesCount > 0); + _storiesCount = count; + const auto now = (_storiesCount > 0); + if (was != now) { + updateControlsVisibility(anim::type::normal); + } + if (now) { + label->setText( + tr::lng_contacts_stories_status( + tr::now, + lt_count, + _storiesCount)); + } + updateControlsGeometry(width()); + } + }, _storiesLifetime); + + _storiesLifetime.add([weak = QPointer(label)] { + delete weak.data(); + }); + } else { + _storiesCount = 0; + } + updateControlsVisibility(anim::type::instant); +} + +void TopBar::setStoriesArchive(bool archive) { + _storiesArchive = archive; +} + void TopBar::setSelectedItems(SelectedItems &&items) { auto wasSelectionMode = selectionMode(); _selectedItems = std::move(items); @@ -439,13 +572,14 @@ rpl::producer TopBar::selectionActionRequests() const { } void TopBar::updateSelectionState() { - Expects(_selectionText && _delete && _forward); + Expects(_selectionText && _delete && _forward && _toggleStoryPin); _canDelete = computeCanDelete(); _canForward = computeCanForward(); _selectionText->entity()->setValue(generateSelectedText()); _delete->toggle(_canDelete, anim::type::instant); _forward->toggle(_canForward, anim::type::instant); + _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); updateSelectionControlsGeometry(width()); } @@ -460,6 +594,7 @@ void TopBar::createSelectionControls() { }; _canDelete = computeCanDelete(); _canForward = computeCanForward(); + _canToggleStoryPin = computeCanToggleStoryPin(); _cancelSelection = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaCancel), @@ -511,6 +646,24 @@ void TopBar::createSelectionControls() { _selectionActionRequests, _cancelSelection->lifetime()); _delete->entity()->setVisible(_canDelete); + const auto archive = + _toggleStoryPin = wrap(Ui::CreateChild>( + this, + object_ptr( + this, + _storiesArchive ? _st.storiesSave : _st.storiesArchive), + st::infoTopBarScale)); + registerToggleControlCallback( + _toggleStoryPin.data(), + [this] { return selectionMode() && _canToggleStoryPin; }); + _toggleStoryPin->setDuration(st::infoTopBarDuration); + _toggleStoryPin->entity()->clicks( + ) | rpl::map_to( + SelectionAction::ToggleStoryPin + ) | rpl::start_to_stream( + _selectionActionRequests, + _cancelSelection->lifetime()); + _toggleStoryPin->entity()->setVisible(_canToggleStoryPin); updateControlsGeometry(width()); } @@ -523,6 +676,12 @@ bool TopBar::computeCanForward() const { return ranges::all_of(_selectedItems.list, &SelectedItem::canForward); } +bool TopBar::computeCanToggleStoryPin() const { + return ranges::all_of( + _selectedItems.list, + &SelectedItem::canToggleStoryPin); +} + Ui::StringWithNumbers TopBar::generateSelectedText() const { using Type = Storage::SharedMediaType; const auto phrase = [&] { @@ -534,6 +693,7 @@ Ui::StringWithNumbers TopBar::generateSelectedText() const { case Type::MusicFile: return tr::lng_media_selected_song; case Type::Link: return tr::lng_media_selected_link; case Type::RoundVoiceFile: return tr::lng_media_selected_audio; + case Type::PhotoVideo: return tr::lng_stories_row_count; } Unexpected("Type in TopBar::generateSelectedText()"); }(); @@ -548,6 +708,10 @@ bool TopBar::selectionMode() const { return !_selectedItems.list.empty(); } +bool TopBar::storiesTitle() const { + return _storiesCount > 0; +} + bool TopBar::searchMode() const { return _searchModeAvailable && _searchModeEnabled; } diff --git a/Telegram/SourceFiles/info/info_top_bar.h b/Telegram/SourceFiles/info/info_top_bar.h index a2ed052c5..496166954 100644 --- a/Telegram/SourceFiles/info/info_top_bar.h +++ b/Telegram/SourceFiles/info/info_top_bar.h @@ -18,11 +18,17 @@ namespace style { struct InfoTopBar; } // namespace style +namespace Dialogs::Stories { +class List; +struct Content; +} // namespace Dialogs::Stories + namespace Window { class SessionNavigation; } // namespace Window namespace Ui { +class AbstractButton; class IconButton; class FlatLabel; class InputField; @@ -43,11 +49,16 @@ public: const style::InfoTopBar &st, SelectedItems &&items); - auto backRequest() const { + [[nodiscard]] auto backRequest() const { return _backClicks.events(); } + [[nodiscard]] auto storyClicks() const { + return _storyClicks.events(); + } void setTitle(rpl::producer &&title); + void setStories(rpl::producer content); + void setStoriesArchive(bool archive); void enableBackButton(); void highlight(); @@ -95,6 +106,7 @@ private: void updateControlsGeometry(int newWidth); void updateDefaultControlsGeometry(int newWidth); void updateSelectionControlsGeometry(int newWidth); + void updateStoriesGeometry(int newWidth); Ui::FadeWrap *pushButton( base::unique_qptr button); void forceButtonVisibility( @@ -104,16 +116,19 @@ private: void startHighlightAnimation(); void updateControlsVisibility(anim::type animated); - bool selectionMode() const; - bool searchMode() const; - Ui::StringWithNumbers generateSelectedText() const; + [[nodiscard]] bool selectionMode() const; + [[nodiscard]] bool storiesTitle() const; + [[nodiscard]] bool searchMode() const; + [[nodiscard]] Ui::StringWithNumbers generateSelectedText() const; [[nodiscard]] bool computeCanDelete() const; [[nodiscard]] bool computeCanForward() const; + [[nodiscard]] bool computeCanToggleStoryPin() const; void updateSelectionState(); void createSelectionControls(); void performForward(); void performDelete(); + void performToggleStoryPin(); void setSearchField( base::unique_qptr field, @@ -147,16 +162,25 @@ private: QPointer _searchField; rpl::event_stream<> _backClicks; + rpl::event_stream _storyClicks; SelectedItems _selectedItems; bool _canDelete = false; bool _canForward = false; + bool _canToggleStoryPin = false; + bool _storiesArchive = false; QPointer> _cancelSelection; QPointer> _selectionText; QPointer> _forward; QPointer> _delete; + QPointer> _toggleStoryPin; rpl::event_stream _selectionActionRequests; + QPointer> _storiesWrap; + QPointer _stories; + rpl::lifetime _storiesLifetime; + int _storiesCount = 0; + using UpdateCallback = Fn; std::map _updateControlCallbacks; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 2d53f0316..214e191fb 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -244,6 +244,12 @@ Dialogs::RowDescriptor WrapWidget::activeChat() const { return Dialogs::RowDescriptor( peer->owner().history(peer), FullMsgId()); + } else if (const auto storiesPeer = key().storiesPeer()) { + return (key().storiesTab() == Stories::Tab::Saved) + ? Dialogs::RowDescriptor( + storiesPeer->owner().history(storiesPeer), + FullMsgId()) + : Dialogs::RowDescriptor(); } else if (key().settingsSelf() || key().isDownloads() || key().poll()) { return Dialogs::RowDescriptor(); } @@ -297,6 +303,11 @@ void WrapWidget::createTopBar() { _controller->parentController()->closeThirdSection(); }); } + _topBar->storyClicks() | rpl::start_with_next([=] { + if (const auto peer = _controller->key().peer()) { + _controller->parentController()->openPeerStories(peer->id); + } + }, _topBar->lifetime()); if (wrapValue == Wrap::Layer) { auto close = _topBar->addButton( base::make_unique_q( @@ -573,6 +584,9 @@ void WrapWidget::finishShowContent() { _content->setIsStackBottom(!hasStackHistory()); if (_topBar) { _topBar->setTitle(_content->title()); + _topBar->setStories(_content->titleStories()); + _topBar->setStoriesArchive( + _controller->key().storiesTab() == Stories::Tab::Archive); } _desiredHeights.fire(desiredHeightForContent()); _desiredShadowVisibilities.fire(_content->desiredShadowVisibility()); @@ -759,9 +773,9 @@ bool WrapWidget::returnToFirstStackFrame( void WrapWidget::showNewContent( not_null memento, const Window::SectionShow ¶ms) { - auto saveToStack = (_content != nullptr) + const auto saveToStack = (_content != nullptr) && (params.way == Window::SectionShow::Way::Forward); - auto needAnimation = (_content != nullptr) + const auto needAnimation = (_content != nullptr) && (params.animated != anim::type::instant); auto animationParams = SectionSlideParams(); auto newController = createController( @@ -771,8 +785,12 @@ void WrapWidget::showNewContent( newController->takeStepData(_controller.get()); } auto newContent = object_ptr(nullptr); - if (needAnimation) { + const auto enableBackButton = hasBackButton(); + const auto createInAdvance = needAnimation || enableBackButton; + if (createInAdvance) { newContent = createContent(memento, newController.get()); + } + if (needAnimation) { animationParams.withTopBarShadow = hasTopBarShadow() && newContent->hasTopBarShadow(); animationParams.oldContentCache = grabForShowAnimation( @@ -783,7 +801,6 @@ void WrapWidget::showNewContent( if (HasCustomTopBar(_controller.get()) || HasCustomTopBar(newController.get())) { - const auto s = QSize( newContent->width(), animationParams.topSkip); @@ -803,11 +820,10 @@ void WrapWidget::showNewContent( _historyStack.clear(); } - { - if (hasBackButton()) { - newContent->enableBackButton(); - } + if (enableBackButton) { + newContent->enableBackButton(); } + { // Let old controller outlive old content widget. const auto oldController = std::exchange( diff --git a/Telegram/SourceFiles/info/info_wrap_widget.h b/Telegram/SourceFiles/info/info_wrap_widget.h index a04c4d89c..5445185e1 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.h +++ b/Telegram/SourceFiles/info/info_wrap_widget.h @@ -15,7 +15,6 @@ enum class SharedMediaType : signed char; } // namespace Storage namespace Ui { -class SettingsSlider; class FadeShadow; class PlainShadow; class PopupMenu; @@ -58,6 +57,7 @@ struct SelectedItem { GlobalMsgId globalId; bool canDelete = false; bool canForward = false; + bool canToggleStoryPin = false; }; struct SelectedItems { @@ -73,6 +73,7 @@ enum class SelectionAction { Clear, Forward, Delete, + ToggleStoryPin, }; class WrapWidget final : public Window::SectionWidget { diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index 1780d741a..9d3693280 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -10,10 +10,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include "lang/lang_keys.h" +#include "data/data_stories_ids.h" #include "storage/storage_shared_media.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" +#include "info/stories/info_stories_widget.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/buttons.h" @@ -124,4 +126,29 @@ inline auto AddCommonGroupsButton( return result; }; +inline auto AddStoriesButton( + Ui::VerticalLayout *parent, + not_null navigation, + not_null user, + Ui::MultiSlideTracker &tracker) { + auto count = rpl::single(0) | rpl::then(Data::SavedStoriesIds( + user, + ServerMaxStoryId - 1, + 0 + ) | rpl::map([](const Data::StoriesIdsSlice &slice) { + return slice.fullCount().value_or(0); + })); + auto result = AddCountedButton( + parent, + std::move(count), + [](int count) { + return tr::lng_profile_saved_stories(tr::now, lt_count, count); + }, + tracker)->entity(); + result->addClickHandler([=] { + navigation->showSection(Info::Stories::Make(user)); + }); + return result; +}; + } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_common.h b/Telegram/SourceFiles/info/media/info_media_common.h index 20c3051ca..27e8d4b9f 100644 --- a/Telegram/SourceFiles/info/media/info_media_common.h +++ b/Telegram/SourceFiles/info/media/info_media_common.h @@ -30,15 +30,12 @@ struct ListItemSelectionData { TextSelection text; bool canDelete = false; bool canForward = false; -}; + bool canToggleStoryPin = false; -inline bool operator==( - ListItemSelectionData a, - ListItemSelectionData b) { - return (a.text == b.text) - && (a.canDelete == b.canDelete) - && (a.canForward == b.canForward); -} + friend inline bool operator==( + ListItemSelectionData, + ListItemSelectionData) = default; +}; using ListSelectedMap = base::flat_map< not_null, diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.h b/Telegram/SourceFiles/info/media/info_media_inner_widget.h index a7a2ce61d..a3e485960 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.h @@ -14,16 +14,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_list_widget.h" namespace Ui { -class SettingsSlider; class VerticalLayout; class SearchFieldController; } // namespace Ui namespace Info { - class Controller; +} // namespace Info -namespace Media { +namespace Info::Media { class Memento; class ListWidget; @@ -86,5 +85,4 @@ private: }; -} // namespace Media -} // namespace Info +} // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.cpp b/Telegram/SourceFiles/info/media/info_media_list_section.cpp index d956d4d76..2bf957192 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_section.cpp @@ -338,15 +338,19 @@ void ListSection::resizeToWidth(int newWidth) { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: case Type::RoundFile: { - _itemsLeft = st::infoMediaSkip; + const auto skip = st::infoMediaSkip; + _itemsLeft = st::infoMediaLeft; _itemsTop = st::infoMediaSkip; - _itemsInRow = (newWidth - _itemsLeft) - / (st::infoMediaMinGridSize + st::infoMediaSkip); - _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow) + _itemsInRow = (newWidth - _itemsLeft * 2 + skip) + / (st::infoMediaMinGridSize + skip); + _itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow) - st::infoMediaSkip; + _itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip) + / 2; for (auto &item : _items) { - item->resizeGetHeight(_itemWidth); + _itemHeight = item->resizeGetHeight(_itemWidth); } } break; @@ -375,8 +379,9 @@ int ListSection::recountHeight() { switch (_type) { case Type::Photo: case Type::Video: + case Type::PhotoVideo: case Type::RoundFile: { - auto itemHeight = _itemWidth + st::infoMediaSkip; + auto itemHeight = _itemHeight + st::infoMediaSkip; auto index = 0; result += _itemsTop; for (auto &item : _items) { diff --git a/Telegram/SourceFiles/info/media/info_media_list_section.h b/Telegram/SourceFiles/info/media/info_media_list_section.h index 77666a31a..358712a75 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_section.h +++ b/Telegram/SourceFiles/info/media/info_media_list_section.h @@ -82,6 +82,7 @@ private: int _itemsLeft = 0; int _itemsTop = 0; int _itemWidth = 0; + int _itemHeight = 0; int _itemsInRow = 1; mutable int _rowsCount = 0; int _top = 0; diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 48e00a41b..5bc2155cb 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/media/info_media_provider.h" #include "info/media/info_media_list_section.h" #include "info/downloads/info_downloads_provider.h" +#include "info/stories/info_stories_provider.h" #include "info/info_controller.h" #include "layout/layout_mosaic.h" #include "layout/layout_selection.h" @@ -21,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer_values.h" #include "data/data_document.h" #include "data/data_session.h" +#include "data/data_stories.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_download_manager.h" @@ -30,6 +32,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_service_message.h" +#include "media/stories/media_stories_controller.h" // ...TogglePinnedToast. +#include "media/stories/media_stories_share.h" // PrepareShareBox. #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "ui/widgets/popup_menu.h" @@ -53,6 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "core/file_utilities.h" #include "core/application.h" +#include "ui/toast/toast.h" #include "styles/style_overview.h" #include "styles/style_info.h" #include "styles/style_layers.h" @@ -88,6 +93,8 @@ struct ListWidget::DateBadge { not_null controller) { if (controller->isDownloads()) { return std::make_unique(controller); + } else if (controller->storiesPeer()) { + return std::make_unique(controller); } return std::make_unique(controller); } @@ -126,6 +133,7 @@ ListWidget::DateBadge::DateBadge( , hideTimer(std::move(hideCallback)) , goodType(type == Type::Photo || type == Type::Video + || type == Type::PhotoVideo || type == Type::GIF) { } @@ -171,6 +179,9 @@ void ListWidget::start() { ) | rpl::start_with_next([this](QString &&query) { _provider->setSearchQuery(std::move(query)); }, lifetime()); + } else if (_controller->storiesPeer()) { + trackSession(&session()); + restart(); } else { trackSession(&session()); @@ -250,6 +261,7 @@ void ListWidget::selectionAction(SelectionAction action) { case SelectionAction::Clear: clearSelected(); return; case SelectionAction::Forward: forwardSelected(); return; case SelectionAction::Delete: deleteSelected(); return; + case SelectionAction::ToggleStoryPin: toggleStoryPinSelected(); return; } } @@ -327,6 +339,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { auto result = SelectedItem(item->globalId()); result.canDelete = selection.canDelete; result.canForward = selection.canForward; + result.canToggleStoryPin = selection.canToggleStoryPin; return result; }; auto transformation = [&](const auto &item) { @@ -341,6 +354,12 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems { std::back_inserter(items.list), transformation); } + if (_controller->storiesPeer() && items.list.size() > 1) { + // Don't allow forwarding more than one story. + for (auto &entry : items.list) { + entry.canForward = false; + } + } return items; } @@ -469,18 +488,31 @@ bool ListWidget::tooltipWindowActive() const { } void ListWidget::openPhoto(not_null photo, FullMsgId id) { - _controller->parentController()->openPhoto(photo, id, topicRootId()); + using namespace Data; + + const auto tab = _controller->storiesTab(); + const auto context = (tab == Stories::Tab::Archive) + ? Data::StoriesContext{ Data::StoriesContextArchive() } + : Data::StoriesContext{ Data::StoriesContextSaved() }; + _controller->parentController()->openPhoto( + photo, + { id, topicRootId() }, + _controller->storiesPeer() ? &context : nullptr); } void ListWidget::openDocument( not_null document, FullMsgId id, bool showInMediaView) { + const auto tab = _controller->storiesTab(); + const auto context = (tab == Stories::Tab::Archive) + ? Data::StoriesContext{ Data::StoriesContextArchive() } + : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openDocument( document, - id, - topicRootId(), - showInMediaView); + showInMediaView, + { id, topicRootId() }, + _controller->storiesPeer() ? &context : nullptr); } void ListWidget::trackSession(not_null session) { @@ -516,6 +548,7 @@ void ListWidget::refreshRows() { resizeToWidth(width()); restoreScrollState(); mouseActionUpdate(); + update(); } bool ListWidget::preventAutoHide() const { @@ -883,6 +916,11 @@ void ListWidget::showContextMenu( auto canForwardAll = [&] { return ranges::none_of(_selected, [](auto &&item) { return !item.second.canForward; + }) && (!_controller->key().storiesPeer() || _selected.size() == 1); + }; + auto canToggleStoryPinAll = [&] { + return ranges::none_of(_selected, [](auto &&item) { + return !item.second.canToggleStoryPin; }); }; @@ -984,6 +1022,18 @@ void ListWidget::showContextMenu( } } if (overSelected == SelectionState::OverSelectedItems) { + if (canToggleStoryPinAll()) { + const auto tab = _controller->key().storiesTab(); + const auto pin = (tab == Stories::Tab::Archive); + _contextMenu->addAction( + (pin + ? tr::lng_mediaview_save_to_profile + : tr::lng_archived_add)(tr::now), + crl::guard(this, [this] { toggleStoryPinSelected(); }), + (pin + ? &st::menuIconStoriesSave + : &st::menuIconStoriesArchive)); + } if (canForwardAll()) { _contextMenu->addAction( tr::lng_context_forward_selected(tr::now), @@ -1013,6 +1063,20 @@ void ListWidget::showContextMenu( const auto selectionData = _provider->computeSelectionData( item, FullSelection); + if (selectionData.canToggleStoryPin) { + const auto tab = _controller->key().storiesTab(); + const auto pin = (tab == Stories::Tab::Archive); + _contextMenu->addAction( + (pin + ? tr::lng_mediaview_save_to_profile + : tr::lng_mediaview_archive_story)(tr::now), + crl::guard(this, [=] { + toggleStoryPin({ 1, globalId.itemId }); + }), + (pin + ? &st::menuIconStoriesSave + : &st::menuIconStoriesArchive)); + } if (selectionData.canForward) { _contextMenu->addAction( tr::lng_context_forward_msg(tr::now), @@ -1034,6 +1098,20 @@ void ListWidget::showContextMenu( } } } + if (const auto peer = _controller->key().storiesPeer()) { + if (!peer->isSelf() && IsStoryMsgId(globalId.itemId.msg)) { + const auto storyId = FullStoryId{ + globalId.itemId.peer, + StoryIdFromMsgId(globalId.itemId.msg), + }; + _contextMenu->addAction( + tr::lng_profile_report(tr::now), + [=] { ::Media::Stories::ReportRequested( + _controller->uiShow(), + storyId); }, + &st::menuIconReport); + } + } if (!_provider->hasSelectRestriction()) { _contextMenu->addAction( tr::lng_context_select_msg(tr::now), @@ -1088,15 +1166,25 @@ void ListWidget::forwardItem(GlobalMsgId globalId) { } void ListWidget::forwardItems(MessageIdsList &&items) { - auto callback = [weak = Ui::MakeWeak(this)] { - if (const auto strong = weak.data()) { - strong->clearSelected(); + if (_controller->storiesPeer()) { + if (items.size() == 1 && IsStoryMsgId(items.front().msg)) { + const auto id = items.front(); + _controller->parentController()->show( + ::Media::Stories::PrepareShareBox( + _controller->parentController()->uiShow(), + { id.peer, StoryIdFromMsgId(id.msg) })); } - }; - setActionBoxWeak(Window::ShowForwardMessagesBox( - _controller, - std::move(items), - std::move(callback))); + } else { + auto callback = [weak = Ui::MakeWeak(this)] { + if (const auto strong = weak.data()) { + strong->clearSelected(); + } + }; + setActionBoxWeak(Window::ShowForwardMessagesBox( + _controller, + std::move(items), + std::move(callback))); + } } void ListWidget::deleteSelected() { @@ -1105,6 +1193,48 @@ void ListWidget::deleteSelected() { })); } +void ListWidget::toggleStoryPinSelected() { + toggleStoryPin(collectSelectedIds(), crl::guard(this, [=] { + clearSelected(); + })); +} + +void ListWidget::toggleStoryPin( + MessageIdsList &&items, + Fn confirmed) { + auto list = std::vector(); + for (const auto &id : items) { + if (IsStoryMsgId(id.msg)) { + list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); + } + } + const auto count = int(list.size()); + const auto pin = (_controller->storiesTab() == Stories::Tab::Archive); + const auto controller = _controller; + const auto sure = [=](Fn close) { + controller->session().data().stories().togglePinnedList(list, pin); + controller->showToast( + ::Media::Stories::PrepareTogglePinnedToast(count, pin)); + close(); + if (confirmed) { + confirmed(); + } + }; + const auto onePhrase = pin + ? tr::lng_stories_save_sure + : tr::lng_stories_archive_sure; + const auto manyPhrase = pin + ? tr::lng_stories_save_sure_many + : tr::lng_stories_archive_sure_many; + _controller->parentController()->show(Ui::MakeConfirmBox({ + .text = (count == 1 + ? onePhrase() + : manyPhrase(lt_count, rpl::single(count) | tr::to_count())), + .confirmed = sure, + .confirmText = tr::lng_box_ok(), + })); +} + void ListWidget::deleteItem(GlobalMsgId globalId) { if (const auto item = MessageByGlobalId(globalId)) { auto items = SelectedItems(_provider->type()); @@ -1113,7 +1243,6 @@ void ListWidget::deleteItem(GlobalMsgId globalId) { item, FullSelection); items.list.back().canDelete = selectionData.canDelete; - items.list.back().canForward = selectionData.canForward; deleteItems(std::move(items)); } } @@ -1159,6 +1288,33 @@ void ListWidget::deleteItems(SelectedItems &&items, Fn confirmed) { .confirmText = tr::lng_box_delete(tr::now), .confirmStyle = &st::attentionBoxButton, }))); + } else if (_controller->storiesPeer()) { + auto list = std::vector(); + for (const auto &item : items.list) { + const auto id = item.globalId.itemId; + if (IsStoryMsgId(id.msg)) { + list.push_back({ id.peer, StoryIdFromMsgId(id.msg) }); + } + } + const auto session = &_controller->session(); + const auto sure = [=](Fn close) { + session->data().stories().deleteList(list); + close(); + if (confirmed) { + confirmed(); + } + }; + const auto count = int(list.size()); + window->show(Ui::MakeConfirmBox({ + .text = (count == 1 + ? tr::lng_stories_delete_one_sure() + : tr::lng_stories_delete_sure( + lt_count, + rpl::single(count) | tr::to_count())), + .confirmed = sure, + .confirmText = tr::lng_selected_delete(), + .confirmStyle = &st::attentionBoxButton, + })); } else if (auto list = collectSelectedIds(items); !list.empty()) { auto box = Box( &_controller->session(), @@ -1659,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; // } @@ -1787,6 +1943,7 @@ void ListWidget::applyDragSelection(SelectedMap &applyTo) const { void ListWidget::refreshHeight() { resize(width(), recountHeight()); + update(); } int ListWidget::recountHeight() { diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index e95d5842b..10984f27e 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -169,7 +169,6 @@ private: void itemRemoved(not_null item); void itemLayoutChanged(not_null item); - void refreshViewer(); void refreshRows(); void trackSession(not_null session); @@ -190,8 +189,12 @@ private: void forwardItem(GlobalMsgId globalId); void forwardItems(MessageIdsList &&items); void deleteSelected(); + void toggleStoryPinSelected(); void deleteItem(GlobalMsgId globalId); void deleteItems(SelectedItems &&items, Fn confirmed = nullptr); + void toggleStoryPin( + MessageIdsList &&items, + Fn confirmed = nullptr); void applyItemSelection( HistoryItem *item, TextSelection selection); @@ -295,7 +298,6 @@ private: bool _wasSelectedText = false; // was some text selected in current drag action const std::unique_ptr _dateBadge; - base::flat_map, rpl::lifetime> _trackedSessions; base::unique_qptr _contextMenu; rpl::event_stream<> _checkForHide; @@ -305,6 +307,8 @@ private: QPoint _trippleClickPoint; crl::time _trippleClickStartTime = 0; + base::flat_map, rpl::lifetime> _trackedSessions; + }; } // namespace Media diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index f2d8dcde7..3e75975fa 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -421,19 +421,21 @@ std::unique_ptr Provider::createLayout( } return nullptr; }; - const auto spoiler = [&] { - if (const auto media = item->media()) { - return media->hasSpoiler(); - } - return false; - }; const auto &songSt = st::overviewFileLayout; using namespace Overview::Layout; + const auto options = [&] { + const auto media = item->media(); + return MediaOptions{ .spoiler = media && media->hasSpoiler() }; + }; switch (type) { case Type::Photo: if (const auto photo = getPhoto()) { - return std::make_unique(delegate, item, photo, spoiler()); + return std::make_unique( + delegate, + item, + photo, + options()); } return nullptr; case Type::GIF: @@ -443,7 +445,7 @@ std::unique_ptr Provider::createLayout( return nullptr; case Type::Video: if (const auto file = getFile()) { - return std::make_unique