diff --git a/.github/stale.yml b/.github/stale.yml index afc525a07..40a568cf2 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -3,7 +3,7 @@ daysUntilStale: 180 # Number of days of inactivity before a stale issue is closed daysUntilClose: 30 # Issues with these labels will never be considered stale -exemptLabels: [] +exemptLabels: [ "enhancement" ] # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 59e64bcea..9e6d68ee2 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -212,6 +212,16 @@ jobs: cd $LibrariesPath sudo cp -R opus-cache/. / + - name: Rnnoise. + run: | + cd $LibrariesPath + + git clone $GIT/desktop-app/rnnoise.git + mkdir -p rnnoise/out/Debug + cd rnnoise/out/Debug + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ../.. + ninja + - name: Libiconv cache. id: cache-libiconv uses: actions/cache@v2 @@ -247,7 +257,7 @@ jobs: git clone $GIT/FFmpeg/FFmpeg.git ffmpeg cd ffmpeg - git checkout release/4.2 + git checkout release/4.4 CFLAGS=`freetype-config --cflags` LDFLAGS=`freetype-config --libs` PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig:/usr/X11/lib/pkgconfig @@ -324,7 +334,6 @@ jobs: --enable-decoder=pcm_u32be \ --enable-decoder=pcm_u32le \ --enable-decoder=pcm_u8 \ - --enable-decoder=pcm_zork \ --enable-decoder=vorbis \ --enable-decoder=wavpack \ --enable-decoder=wmalossless \ @@ -497,6 +506,7 @@ jobs: cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Debug \ -DTG_OWT_SPECIAL_TARGET=mac \ + -DTG_OWT_BUILD_AUDIO_BACKENDS=OFF \ -DTG_OWT_LIBJPEG_INCLUDE_PATH=$PREFIX/include \ -DTG_OWT_OPENSSL_INCLUDE_PATH=`pwd`/../../../openssl_$OPENSSL_VER/include \ -DTG_OWT_OPUS_INCLUDE_PATH=$PREFIX/include/opus \ diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 52e3de1d6..47775ecbd 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -291,9 +291,16 @@ jobs: msbuild -m opus.sln /property:Configuration=Debug /property:Platform="Win32" msbuild -m opus.sln /property:Configuration=Release /property:Platform="Win32" - echo "Workaround for FFmpeg." - copy Win32\Release\m.lib Win32\Release\ssp.lib - copy Win32\Release\m.lib Win32\Debug\ssp.lib + - name: Rnnoise. + shell: cmd + run: | + %VC% + + git clone %GIT%/desktop-app/rnnoise.git + mkdir rnnoise\out + cd rnnoise\out + cmake -A Win32 .. + cmake --build . --config Debug - name: FFmpeg cache. id: cache-ffmpeg @@ -309,7 +316,7 @@ jobs: git clone %GIT%/FFmpeg/FFmpeg.git ffmpeg cd ffmpeg - git checkout release/4.2 + git checkout release/4.4 set CHERE_INVOKING=enabled_from_arguments set MSYS2_PATH_TYPE=inherit call c:\tools\msys64\usr\bin\bash --login ../patches/build_ffmpeg_win.sh @@ -348,7 +355,7 @@ jobs: -confirm-license ^ -static ^ -static-runtime -I "%SSL%\include" ^ - -no-opengl ^ + -opengl dynamic ^ -openssl-linked ^ OPENSSL_LIBS_DEBUG="%SSL%\out32.dbg\libssl.lib %SSL%\out32.dbg\%LIBS%" ^ OPENSSL_LIBS_RELEASE="%SSL%\out32\libssl.lib %SSL%\out32\%LIBS%" ^ @@ -388,6 +395,7 @@ jobs: cmake -G Ninja ^ -DCMAKE_BUILD_TYPE=Debug ^ -DTG_OWT_SPECIAL_TARGET=win ^ + -DTG_OWT_BUILD_AUDIO_BACKENDS=OFF ^ -DTG_OWT_LIBJPEG_INCLUDE_PATH=%cd%/../../../mozjpeg ^ -DTG_OWT_OPENSSL_INCLUDE_PATH=%cd%/../../../openssl_%OPENSSL_VER%/include ^ -DTG_OWT_OPUS_INCLUDE_PATH=%cd%/../../../opus/include ^ @@ -416,6 +424,9 @@ jobs: fi echo "TDESKTOP_BUILD_DEFINE=$DEFINE" >> $GITHUB_ENV + - name: Free up some disk space. + run: del /S *.pdb + - name: Kotatogram Desktop build. if: env.ONLY_CACHE == 'false' run: | diff --git a/.gitmodules b/.gitmodules index beda934f6..b13197e9c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -91,3 +91,9 @@ [submodule "Telegram/lib_webview"] path = Telegram/lib_webview url = https://github.com/kotatogram/lib_webview.git +[submodule "Telegram/ThirdParty/mallocng"] + path = Telegram/ThirdParty/mallocng + url = https://github.com/desktop-app/mallocng.git +[submodule "Telegram/lib_waylandshells"] + path = Telegram/lib_waylandshells + url = https://github.com/desktop-app/lib_waylandshells.git diff --git a/CMakeLists.txt b/CMakeLists.txt index cce0c8a45..fef703fc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ 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) include(cmake/options.cmake) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index fe177acf6..e7b8f53cd 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -18,6 +18,9 @@ endif() add_subdirectory(lib_storage) add_subdirectory(lib_lottie) add_subdirectory(lib_qr) +if (LINUX AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) + add_subdirectory(lib_waylandshells) +endif() add_subdirectory(lib_webrtc) add_subdirectory(lib_webview) add_subdirectory(codegen) @@ -79,16 +82,12 @@ PRIVATE desktop-app::external_xxhash ) -if (WIN32) - target_link_libraries(Telegram - PRIVATE - desktop-app::lib_webview_winrt - ) -elseif (LINUX) +if (LINUX) target_link_libraries(Telegram PRIVATE desktop-app::external_glibmm desktop-app::external_glib + desktop-app::external_mallocng ) if (NOT DESKTOP_APP_DISABLE_DBUS_INTEGRATION) @@ -109,6 +108,7 @@ elseif (LINUX) if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) target_link_libraries(Telegram PRIVATE + desktop-app::lib_waylandshells desktop-app::external_kwayland ) endif() @@ -125,7 +125,7 @@ elseif (LINUX) target_link_libraries(Telegram PRIVATE PkgConfig::X11) endif() else() - pkg_search_module(GTK REQUIRED gtk+-3.0 gtk+-2.0) + pkg_check_modules(GTK REQUIRED gtk+-3.0) target_include_directories(Telegram PRIVATE ${GTK_INCLUDE_DIRS}) if (NOT DESKTOP_APP_DISABLE_X11_INTEGRATION) @@ -271,23 +271,39 @@ PRIVATE boxes/url_auth_box.h boxes/username_box.cpp boxes/username_box.h + calls/group/calls_choose_join_as.cpp + calls/group/calls_choose_join_as.h + calls/group/calls_group_call.cpp + calls/group/calls_group_call.h + calls/group/calls_group_common.h + calls/group/calls_group_invite_controller.cpp + calls/group/calls_group_invite_controller.h + calls/group/calls_group_members.cpp + calls/group/calls_group_members.h + calls/group/calls_group_members_row.cpp + calls/group/calls_group_members_row.h + calls/group/calls_group_menu.cpp + calls/group/calls_group_menu.h + calls/group/calls_group_panel.cpp + calls/group/calls_group_panel.h + calls/group/calls_group_settings.cpp + calls/group/calls_group_settings.h + calls/group/calls_group_toasts.cpp + calls/group/calls_group_toasts.h + calls/group/calls_group_viewport.cpp + calls/group/calls_group_viewport.h + calls/group/calls_group_viewport_opengl.cpp + calls/group/calls_group_viewport_opengl.h + calls/group/calls_group_viewport_raster.cpp + calls/group/calls_group_viewport_raster.h + calls/group/calls_group_viewport_tile.cpp + calls/group/calls_group_viewport_tile.h + calls/group/calls_volume_item.cpp + calls/group/calls_volume_item.h calls/calls_box_controller.cpp calls/calls_box_controller.h calls/calls_call.cpp calls/calls_call.h - calls/calls_choose_join_as.cpp - calls/calls_choose_join_as.h - calls/calls_group_call.cpp - calls/calls_group_call.h - calls/calls_group_common.h - calls/calls_group_members.cpp - calls/calls_group_members.h - calls/calls_group_menu.cpp - calls/calls_group_menu.h - calls/calls_group_panel.cpp - calls/calls_group_panel.h - calls/calls_group_settings.cpp - calls/calls_group_settings.h calls/calls_emoji_fingerprint.cpp calls/calls_emoji_fingerprint.h calls/calls_instance.cpp @@ -302,8 +318,8 @@ PRIVATE calls/calls_userpic.h calls/calls_video_bubble.cpp calls/calls_video_bubble.h - calls/calls_volume_item.cpp - calls/calls_volume_item.h + calls/calls_video_incoming.cpp + calls/calls_video_incoming.h chat_helpers/bot_keyboard.cpp chat_helpers/bot_keyboard.h chat_helpers/emoji_keywords.cpp @@ -352,6 +368,8 @@ PRIVATE core/core_cloud_password.h core/core_settings.cpp core/core_settings.h + core/core_settings_proxy.cpp + core/core_settings_proxy.h core/crash_report_window.cpp core/crash_report_window.h core/crash_reports.cpp @@ -399,10 +417,14 @@ PRIVATE data/data_document.h data/data_document_media.cpp data/data_document_media.h + data/data_document_resolver.cpp + data/data_document_resolver.h data/data_drafts.cpp data/data_drafts.h data/data_folder.cpp data/data_folder.h + data/data_file_click_handler.cpp + data/data_file_click_handler.h data/data_file_origin.cpp data/data_file_origin.h data/data_flags.h @@ -723,6 +745,8 @@ PRIVATE main/main_session.h main/main_session_settings.cpp main/main_session_settings.h + media/system_media_controls_manager.h + media/system_media_controls_manager.cpp media/audio/media_audio.cpp media/audio/media_audio.h media/audio/media_audio_capture.cpp @@ -777,14 +801,25 @@ PRIVATE media/streaming/media_streaming_video_track.h media/view/media_view_group_thumbs.cpp media/view/media_view_group_thumbs.h + media/view/media_view_overlay_opengl.cpp + media/view/media_view_overlay_opengl.h + media/view/media_view_overlay_raster.cpp + media/view/media_view_overlay_raster.h + media/view/media_view_overlay_renderer.h media/view/media_view_overlay_widget.cpp media/view/media_view_overlay_widget.h media/view/media_view_pip.cpp media/view/media_view_pip.h + media/view/media_view_pip_opengl.cpp + media/view/media_view_pip_opengl.h + media/view/media_view_pip_raster.cpp + media/view/media_view_pip_raster.h + media/view/media_view_pip_renderer.h media/view/media_view_playback_controls.cpp media/view/media_view_playback_controls.h media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h + media/view/media_view_open_common.h mtproto/config_loader.cpp mtproto/config_loader.h mtproto/connection_abstract.cpp @@ -845,15 +880,15 @@ PRIVATE platform/linux/linux_gsd_media_keys.h platform/linux/linux_gtk_file_dialog.cpp platform/linux/linux_gtk_file_dialog.h + platform/linux/linux_gtk_integration_dummy.cpp platform/linux/linux_gtk_integration_p.h platform/linux/linux_gtk_integration.cpp platform/linux/linux_gtk_integration.h platform/linux/linux_gtk_open_with_dialog.cpp platform/linux/linux_gtk_open_with_dialog.h - platform/linux/linux_mpris_support.cpp - platform/linux/linux_mpris_support.h platform/linux/linux_notification_service_watcher.cpp platform/linux/linux_notification_service_watcher.h + platform/linux/linux_wayland_integration_dummy.cpp platform/linux/linux_wayland_integration.cpp platform/linux/linux_wayland_integration.h platform/linux/linux_xdp_file_dialog.cpp @@ -866,6 +901,7 @@ PRIVATE platform/linux/launcher_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/specific_linux.cpp @@ -875,6 +911,7 @@ PRIVATE platform/mac/file_utilities_mac.h platform/mac/launcher_mac.mm platform/mac/launcher_mac.h + platform/mac/mac_iconv_helper.c platform/mac/main_window_mac.mm platform/mac/main_window_mac.h platform/mac/notifications_manager_mac.mm @@ -1051,6 +1088,8 @@ PRIVATE ui/search_field_controller.h ui/special_buttons.cpp ui/special_buttons.h + ui/text/format_song_document_name.cpp + ui/text/format_song_document_name.h ui/unread_badge.cpp ui/unread_badge.h window/main_window.cpp @@ -1064,6 +1103,8 @@ PRIVATE window/section_memento.h window/section_widget.cpp window/section_widget.h + window/window_adaptive.cpp + window/window_adaptive.h window/window_connecting_widget.cpp window/window_connecting_widget.h window/window_controller.cpp @@ -1150,16 +1191,20 @@ if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION) platform/linux/linux_xdp_open_with_dialog.h platform/linux/notifications_manager_linux.cpp ) - - nice_target_sources(Telegram ${src_loc} - PRIVATE +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) - nice_target_sources(Telegram ${src_loc} PRIVATE platform/linux/linux_wayland_integration_dummy.cpp) + remove_target_sources(Telegram ${src_loc} + platform/linux/linux_wayland_integration.cpp + ) +else() + remove_target_sources(Telegram ${src_loc} + platform/linux/linux_wayland_integration_dummy.cpp + ) endif() if (DESKTOP_APP_DISABLE_GTK_INTEGRATION) @@ -1173,15 +1218,16 @@ if (DESKTOP_APP_DISABLE_GTK_INTEGRATION) platform/linux/linux_gtk_open_with_dialog.cpp platform/linux/linux_gtk_open_with_dialog.h ) - - nice_target_sources(Telegram ${src_loc} - PRIVATE +else() + remove_target_sources(Telegram ${src_loc} platform/linux/linux_gtk_integration_dummy.cpp ) endif() -if (NOT DESKTOP_APP_USE_PACKAGED) - nice_target_sources(Telegram ${src_loc} PRIVATE platform/mac/mac_iconv_helper.c) +if (DESKTOP_APP_USE_PACKAGED) + remove_target_sources(Telegram ${src_loc} + platform/mac/mac_iconv_helper.c + ) endif() nice_target_sources(Telegram ${res_loc} @@ -1217,8 +1263,6 @@ if (WIN32) # $ # ) elseif (APPLE) - target_link_libraries(Telegram PRIVATE desktop-app::external_sp_media_key_tap) - if (NOT DESKTOP_APP_USE_PACKAGED) target_link_libraries(Telegram PRIVATE desktop-app::external_iconv) endif() diff --git a/Telegram/Resources/icons/calls/call_settings.png b/Telegram/Resources/icons/calls/call_settings.png deleted file mode 100644 index f16037f3c..000000000 Binary files a/Telegram/Resources/icons/calls/call_settings.png and /dev/null differ diff --git a/Telegram/Resources/icons/calls/call_settings@2x.png b/Telegram/Resources/icons/calls/call_settings@2x.png deleted file mode 100644 index 21d18e24f..000000000 Binary files a/Telegram/Resources/icons/calls/call_settings@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/calls/call_settings@3x.png b/Telegram/Resources/icons/calls/call_settings@3x.png deleted file mode 100644 index d86c793a0..000000000 Binary files a/Telegram/Resources/icons/calls/call_settings@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/calls/calls_more.png b/Telegram/Resources/icons/calls/calls_more.png new file mode 100644 index 000000000..1bb29b885 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_more.png differ diff --git a/Telegram/Resources/icons/calls/calls_more@2x.png b/Telegram/Resources/icons/calls/calls_more@2x.png new file mode 100644 index 000000000..d7003fb82 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_more@2x.png differ diff --git a/Telegram/Resources/icons/calls/calls_more@3x.png b/Telegram/Resources/icons/calls/calls_more@3x.png new file mode 100644 index 000000000..e400631f4 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_more@3x.png differ diff --git a/Telegram/Resources/icons/calls/calls_present.png b/Telegram/Resources/icons/calls/calls_present.png new file mode 100644 index 000000000..5e0ba0950 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_present.png differ diff --git a/Telegram/Resources/icons/calls/calls_present@2x.png b/Telegram/Resources/icons/calls/calls_present@2x.png new file mode 100644 index 000000000..5950f1678 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_present@2x.png differ diff --git a/Telegram/Resources/icons/calls/calls_present@3x.png b/Telegram/Resources/icons/calls/calls_present@3x.png new file mode 100644 index 000000000..ae36d18bd Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_present@3x.png differ diff --git a/Telegram/Resources/icons/calls/calls_settings.png b/Telegram/Resources/icons/calls/calls_settings.png new file mode 100644 index 000000000..d965e0a0b Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_settings.png differ diff --git a/Telegram/Resources/icons/calls/calls_settings@2x.png b/Telegram/Resources/icons/calls/calls_settings@2x.png new file mode 100644 index 000000000..57fd5eb97 Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_settings@2x.png differ diff --git a/Telegram/Resources/icons/calls/calls_settings@3x.png b/Telegram/Resources/icons/calls/calls_settings@3x.png new file mode 100644 index 000000000..c9f788eab Binary files /dev/null and b/Telegram/Resources/icons/calls/calls_settings@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_back.png b/Telegram/Resources/icons/calls/video_back.png new file mode 100644 index 000000000..77da5ba03 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_back.png differ diff --git a/Telegram/Resources/icons/calls/video_back@2x.png b/Telegram/Resources/icons/calls/video_back@2x.png new file mode 100644 index 000000000..be90e5810 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_back@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_back@3x.png b/Telegram/Resources/icons/calls/video_back@3x.png new file mode 100644 index 000000000..9013645fc Binary files /dev/null and b/Telegram/Resources/icons/calls/video_back@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_large_paused.png b/Telegram/Resources/icons/calls/video_large_paused.png new file mode 100644 index 000000000..acf2553a8 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_large_paused.png differ diff --git a/Telegram/Resources/icons/calls/video_large_paused@2x.png b/Telegram/Resources/icons/calls/video_large_paused@2x.png new file mode 100644 index 000000000..c159bbdaa Binary files /dev/null and b/Telegram/Resources/icons/calls/video_large_paused@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_large_paused@3x.png b/Telegram/Resources/icons/calls/video_large_paused@3x.png new file mode 100644 index 000000000..00159c4d7 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_large_paused@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_mute.png b/Telegram/Resources/icons/calls/video_mini_mute.png new file mode 100644 index 000000000..c96446d3d Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_mute.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_mute@2x.png b/Telegram/Resources/icons/calls/video_mini_mute@2x.png new file mode 100644 index 000000000..358fdbfdd Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_mute@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_mute@3x.png b/Telegram/Resources/icons/calls/video_mini_mute@3x.png new file mode 100644 index 000000000..4be74a8d7 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_mute@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_screencast.png b/Telegram/Resources/icons/calls/video_mini_screencast.png new file mode 100644 index 000000000..9955d0439 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_screencast.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_screencast@2x.png b/Telegram/Resources/icons/calls/video_mini_screencast@2x.png new file mode 100644 index 000000000..f77427386 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_screencast@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_screencast@3x.png b/Telegram/Resources/icons/calls/video_mini_screencast@3x.png new file mode 100644 index 000000000..082e48f02 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_screencast@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_speak.png b/Telegram/Resources/icons/calls/video_mini_speak.png new file mode 100644 index 000000000..af740e57e Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_speak.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_speak@2x.png b/Telegram/Resources/icons/calls/video_mini_speak@2x.png new file mode 100644 index 000000000..aa3fe7a09 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_speak@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_speak@3x.png b/Telegram/Resources/icons/calls/video_mini_speak@3x.png new file mode 100644 index 000000000..ef2f53ed0 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_speak@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_video.png b/Telegram/Resources/icons/calls/video_mini_video.png new file mode 100644 index 000000000..7fb85a579 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_video.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_video@2x.png b/Telegram/Resources/icons/calls/video_mini_video@2x.png new file mode 100644 index 000000000..6db72fcd6 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_video@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_mini_video@3x.png b/Telegram/Resources/icons/calls/video_mini_video@3x.png new file mode 100644 index 000000000..34f1f3f97 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_mini_video@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_over_mute.png b/Telegram/Resources/icons/calls/video_over_mute.png new file mode 100644 index 000000000..349ad3c17 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_mute.png differ diff --git a/Telegram/Resources/icons/calls/video_over_mute@2x.png b/Telegram/Resources/icons/calls/video_over_mute@2x.png new file mode 100644 index 000000000..3d65f89b4 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_mute@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_over_mute@3x.png b/Telegram/Resources/icons/calls/video_over_mute@3x.png new file mode 100644 index 000000000..2ba38a81c Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_mute@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_over_pin.png b/Telegram/Resources/icons/calls/video_over_pin.png new file mode 100644 index 000000000..478030357 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_pin.png differ diff --git a/Telegram/Resources/icons/calls/video_over_pin@2x.png b/Telegram/Resources/icons/calls/video_over_pin@2x.png new file mode 100644 index 000000000..801ffdda6 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_pin@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_over_pin@3x.png b/Telegram/Resources/icons/calls/video_over_pin@3x.png new file mode 100644 index 000000000..cc1bfafa2 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_over_pin@3x.png differ diff --git a/Telegram/Resources/icons/calls/video_tooltip.png b/Telegram/Resources/icons/calls/video_tooltip.png new file mode 100644 index 000000000..60ecf2cd7 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip.png differ diff --git a/Telegram/Resources/icons/calls/video_tooltip@2x.png b/Telegram/Resources/icons/calls/video_tooltip@2x.png new file mode 100644 index 000000000..37aee8b75 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_tooltip@3x.png b/Telegram/Resources/icons/calls/video_tooltip@3x.png new file mode 100644 index 000000000..13a66723a Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip@3x.png differ diff --git a/Telegram/Resources/icons/player_volume_off@2x.png b/Telegram/Resources/icons/player_volume_off@2x.png index c4df75d9c..267bcc6f7 100644 Binary files a/Telegram/Resources/icons/player_volume_off@2x.png and b/Telegram/Resources/icons/player_volume_off@2x.png differ diff --git a/Telegram/Resources/icons/player_volume_off@3x.png b/Telegram/Resources/icons/player_volume_off@3x.png index b31bad377..9556ac795 100644 Binary files a/Telegram/Resources/icons/player_volume_off@3x.png and b/Telegram/Resources/icons/player_volume_off@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 720a97e4f..671e1e39d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -451,6 +451,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_system_integration" = "System integration"; "lng_settings_performance" = "Performance"; "lng_settings_enable_animations" = "Enable animations"; +"lng_settings_enable_opengl" = "Enable OpenGL rendering for media"; "lng_settings_sensitive_title" = "Sensitive content"; "lng_settings_sensitive_disable_filtering" = "Disable filtering"; "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices."; @@ -496,6 +497,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_background_text1" = "Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff!"; "lng_background_text2" = "I can't even take you seriously right now."; "lng_background_bad_link" = "This background link appears to be invalid."; +"lng_background_gradient_unsupported" = "Telegram Desktop doesn't support gradient backgrounds yet."; "lng_background_apply" = "Apply"; "lng_background_share" = "Share"; "lng_background_link_copied" = "Link copied to clipboard"; @@ -1005,6 +1007,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_report_group_title" = "Report group"; "lng_report_bot_title" = "Report bot"; "lng_report_message_title" = "Report message"; +"lng_report_please_select_messages" = "Please select messages to report."; "lng_report_select_messages" = "Select messages"; "lng_report_messages_none" = "Select Messages"; "lng_report_messages_count#one" = "Report {count} Message"; @@ -1112,6 +1115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_group_call_scheduled_group" = "{from} scheduled a voice chat for {date}"; "lng_action_group_call_scheduled_channel" = "Voice chat scheduled for {date}"; "lng_action_group_call_finished" = "Voice chat finished ({duration})"; +"lng_action_group_call_finished_group" = "{from} ended the voice chat ({duration})"; "lng_action_add_user" = "{from} added {user}"; "lng_action_add_users_many" = "{from} added {users}"; "lng_action_add_users_and_one" = "{accumulated}, {user}"; @@ -1401,6 +1405,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stickers_search_sets" = "Search sticker sets"; "lng_stickers_nothing_found" = "No stickers found"; "lng_stickers_remove_pack_confirm" = "Remove"; +"lng_stickers_archive_pack" = "Archive Stickers"; +"lng_stickers_has_been_archived" = "Sticker pack has been archived."; "lng_in_dlg_photo" = "Photo"; "lng_in_dlg_album" = "Album"; @@ -1997,8 +2003,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_raised_hand_status" = "wants to speak"; "lng_group_call_settings" = "Settings"; "lng_group_call_share_button" = "Share"; +"lng_group_call_video" = "Video"; +"lng_group_call_screen_share_start" = "Share Screen"; +"lng_group_call_screen_share_stop" = "Stop Sharing"; +"lng_group_call_screen_title" = "Screen {index}"; +"lng_group_call_unmute_small" = "Unmute"; +"lng_group_call_more" = "More"; "lng_group_call_unmute" = "Unmute"; -"lng_group_call_unmute_sub" = "or hold spacebar to talk"; +"lng_group_call_unmute_sub" = "Hold space bar to temporarily unmute."; "lng_group_call_you_are_live" = "You are Live"; "lng_group_call_force_muted" = "Muted by admin"; "lng_group_call_force_muted_sub" = "You are in Listen Only mode"; @@ -2016,6 +2028,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?"; "lng_group_call_create_sure_channel" = "Are you sure you want to start a voice chat in this channel as your personal account?"; "lng_group_call_join_sure_personal" = "Are you sure you want to join this voice chat as your personal account?"; +"lng_group_call_muted_no_camera" = "You can't turn on video while you're muted by admin."; +"lng_group_call_muted_no_screen" = "You can't share your screen while you're muted by admin."; +"lng_group_call_chat_no_camera" = "You can't turn on video in this chat."; +"lng_group_call_chat_no_screen" = "You can't share your screen in this chat."; +"lng_group_call_failed_screen" = "An error occured. Screencast has stopped."; +"lng_group_call_failed_camera" = "Could not enable camera. Perhaps another app is using the camera already. Try closing other apps."; +"lng_group_call_tooltip_screen" = "Share screen"; +"lng_group_call_tooltip_camera" = "Your camera is off. Click here to enable camera."; +"lng_group_call_tooltip_microphone" = "You are on mute. Click here to speak."; +"lng_group_call_tooltip_camera_off" = "Disable camera"; +"lng_group_call_tooltip_force_muted" = "Muted by admin. Click if you want to speak."; +"lng_group_call_tooltip_raised_hand" = "You asked to speak. We let the speakers know."; "lng_group_call_also_end" = "End voice chat"; "lng_group_call_settings_title" = "Settings"; "lng_group_call_invite" = "Invite Member"; @@ -2038,6 +2062,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_ptt_delay_s" = "{amount}s"; "lng_group_call_ptt_delay" = "Push to Talk release delay: {delay}"; "lng_group_call_share" = "Share Invite Link"; +"lng_group_call_noise_suppression" = "Enable Noise Suppression"; +"lng_group_call_limit#one" = "Video is only available\nfor the first {count} member"; +"lng_group_call_limit#other" = "Video is only available\nfor the first {count} members"; +"lng_group_call_video_paused" = "Video is paused"; "lng_group_call_share_speaker" = "Users with this link can speak"; "lng_group_call_copy_speaker_link" = "Copy Speaker Link"; "lng_group_call_copy_listener_link" = "Copy Listener Link"; @@ -2058,6 +2086,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_context_remove_hand" = "Cancel request to speak"; "lng_group_call_context_mute_for_me" = "Mute for me"; "lng_group_call_context_unmute_for_me" = "Unmute for me"; +"lng_group_call_context_pin_camera" = "Pin video"; +"lng_group_call_context_unpin_camera" = "Unpin video"; +"lng_group_call_context_pin_screen" = "Pin screencast"; +"lng_group_call_context_unpin_screen" = "Unpin screencast"; "lng_group_call_context_remove" = "Remove"; "lng_group_call_remove_channel" = "Remove {channel} from the voice chat?"; "lng_group_call_duration_days#one" = "{count} day"; @@ -2071,6 +2103,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_mac_access" = "Telegram Desktop does not have access to system wide keyboard input required for Push to Talk."; "lng_group_call_mac_input" = "Please allow **Input Monitoring** for Telegram in Privacy Settings."; "lng_group_call_mac_accessibility" = "Please allow **Accessibility** for Telegram in Privacy Settings.\n\nApp restart may be required."; +"lng_group_call_mac_screencast_access" = "Telegram Desktop does not have access to screen recording required for Screen Sharing."; +"lng_group_call_mac_recording" = "Please allow **Screen Recording** for Telegram in Privacy Settings."; "lng_group_call_mac_settings" = "Open Settings"; "lng_group_call_start_as_header" = "Start Voice Chat as..."; @@ -2107,6 +2141,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_recording_started" = "Voice chat recording started."; "lng_group_call_recording_stopped" = "Voice chat recording stopped."; "lng_group_call_recording_saved" = "Audio saved to Saved Messages."; +"lng_group_call_pinned_camera_me" = "Your video is pinned."; +"lng_group_call_pinned_screen_me" = "Your screencast is pinned."; +"lng_group_call_pinned_camera" = "{user}'s video is pinned."; +"lng_group_call_pinned_screen" = "{user}'s screencast is pinned."; +"lng_group_call_unpinned_camera_me" = "Your video is unpinned."; +"lng_group_call_unpinned_screen_me" = "Your screencast is unpinned."; +"lng_group_call_unpinned_camera" = "{user}'s video is unpinned."; +"lng_group_call_unpinned_screen" = "{user}'s screencast is unpinned."; +"lng_group_call_sure_screencast" = "{user} is screensharing. This action will make your screencast pinned for all participants."; "lng_group_call_recording_start_sure" = "Do you want to start recording this chat and save the result into an audio file?\n\nOther members will see the chat is being recorded."; "lng_group_call_recording_stop_sure" = "Do you want to stop recording this chat?"; "lng_group_call_recording_start_field" = "Recording Title"; @@ -2773,6 +2816,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mac_menu_show" = "Show Telegram"; "lng_mac_menu_emoji_and_symbols" = "Emoji & Symbols"; +"lng_mac_menu_player_pause" = "Pause"; +"lng_mac_menu_player_resume" = "Resume"; +"lng_mac_menu_player_next" = "Next"; +"lng_mac_menu_player_previous" = "Previous"; +"lng_mac_menu_player_stop" = "Stop"; + "lng_mac_touchbar_favorite_stickers" = "Favorite stickers"; // Kotatogram keys diff --git a/Telegram/Resources/night-green.tdesktop-theme b/Telegram/Resources/night-green.tdesktop-theme index 6158491ac..4c29a2d9f 100644 Binary files a/Telegram/Resources/night-green.tdesktop-theme and b/Telegram/Resources/night-green.tdesktop-theme differ diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 93d30d008..1db76ca10 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -68,7 +68,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#f4e096c3 flags:# multiple_allowed:flags.1?true can_forward:flags.2?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; +inputMediaInvoice#d9799874 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -223,7 +223,7 @@ peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bo peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true geo_distance:flags.6?int = PeerSettings; wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; -wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; +wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; inputReportReasonSpam#58dbcab8 = ReportReason; inputReportReasonViolence#1e22c78d = ReportReason; @@ -356,7 +356,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; -updateMessagePollVote#42f88f2c poll_id:long user_id:int options:Vector = Update; +updateMessagePollVote#37f69f0b poll_id:long user_id:int options:Vector qts:int = Update; updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; updateDialogFilterOrder#a5d72105 order:Vector = Update; updateDialogFilters#3504914f = Update; @@ -375,6 +375,7 @@ updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update; updateChatParticipant#f3b3781f flags:# chat_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateChannelParticipant#7fecb1ec flags:# channel_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateBotStopped#7f9488a user_id:int date:int stopped:Bool qts:int = Update; +updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -405,7 +406,7 @@ config#330b4067 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:fla nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; -help.appUpdate#1da7158f flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string = help.AppUpdate; +help.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate; help.noAppUpdate#c45a6536 = help.AppUpdate; help.inviteText#18cb9f78 message:string = help.InviteText; @@ -649,7 +650,7 @@ inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:f inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaInvoice#d5348d85 flags:# multiple_allowed:flags.1?true can_forward:flags.3?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; @@ -1069,14 +1070,14 @@ chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; -inputWallPaperNoFile#8427bbac = InputWallPaper; +inputWallPaperNoFile#967a462e id:long = InputWallPaper; account.wallPapersNotModified#1c199183 = account.WallPapers; account.wallPapers#702b65a9 hash:int wallpapers:Vector = account.WallPapers; codeSettings#debebe83 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true = CodeSettings; -wallPaperSettings#5086cf8 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; +wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; autoDownloadSettings#e04232f3 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:int file_size_max:int video_upload_maxbitrate:int = AutoDownloadSettings; @@ -1204,11 +1205,11 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#c95c6654 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true id:long access_hash:long participants_count:int params:flags.0?DataJSON title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall; +groupCall#653dbaad flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; -groupCallParticipant#b96b25ee flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long params:flags.6?DataJSON = GroupCallParticipant; +groupCallParticipant#eba636fe flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo = GroupCallParticipant; phone.groupCall#9e727aad call:GroupCall participants:Vector participants_next_offset:string chats:Vector users:Vector = phone.GroupCall; @@ -1245,6 +1246,10 @@ phone.joinAsPeers#afe5623f peers:Vector chats:Vector users:Vector = GroupCallParticipantVideoSourceGroup; + +groupCallParticipantVideo#78e41663 flags:# paused:flags.0?true endpoint:string source_groups:Vector = GroupCallParticipantVideo; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1617,22 +1622,24 @@ phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhon phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; phone.createGroupCall#48cdc6d8 flags:# peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; -phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; +phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates; phone.getGroupCall#c7cb017 call:InputGroupCall = phone.GroupCall; phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; -phone.checkGroupCall#b74a7bea call:InputGroupCall source:int = Bool; +phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector = Vector; phone.toggleGroupCallRecord#c02a66d7 flags:# start:flags.0?true call:InputGroupCall title:flags.1?string = Updates; -phone.editGroupCallParticipant#d975eb80 flags:# muted:flags.0?true call:InputGroupCall participant:InputPeer volume:flags.1?int raise_hand:flags.2?Bool = Updates; +phone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates; phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates; phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers; phone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite; phone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates; phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates; phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool; +phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates; +phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; @@ -1649,4 +1656,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 128 +// LAYER 129 diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 877faddbe..fb608fe1d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -9,7 +9,7 @@ + Version="2.7.10.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index e1d53f467..cffdb2fe1 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 2,7,4,0 - PRODUCTVERSION 2,7,4,0 + FILEVERSION 2,7,10,0 + PRODUCTVERSION 2,7,10,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "2.7.4.0" + VALUE "FileVersion", "2.7.10.0" VALUE "LegalCopyright", "Copyright (C) 2014-2021" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "2.7.4.0" + VALUE "ProductVersion", "2.7.10.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 911963678..9c4998e52 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 2,7,4,0 - PRODUCTVERSION 2,7,4,0 + FILEVERSION 2,7,10,0 + PRODUCTVERSION 2,7,10,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", "2.7.4.0" + VALUE "FileVersion", "2.7.10.0" VALUE "LegalCopyright", "Copyright (C) 2014-2021" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "2.7.4.0" + VALUE "ProductVersion", "2.7.10.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/_other/packer.cpp b/Telegram/SourceFiles/_other/packer.cpp index b3e001cf5..86fba4786 100644 --- a/Telegram/SourceFiles/_other/packer.cpp +++ b/Telegram/SourceFiles/_other/packer.cpp @@ -47,6 +47,18 @@ typedef signed int int32; namespace{ +struct BIODeleter { + void operator()(BIO *value) { + BIO_free(value); + } +}; + +inline auto makeBIO(const void *buf, int len) { + return std::unique_ptr{ + BIO_new_mem_buf(buf, len), + }; +} + inline uint32 sha1Shift(uint32 v, uint32 shift) { return ((v << shift) | (v >> (32 - shift))); } @@ -430,7 +442,15 @@ int main(int argc, char *argv[]) uint32 siglen = 0; cout << "Signing..\n"; - RSA *prKey = PEM_read_bio_RSAPrivateKey(BIO_new_mem_buf(const_cast((BetaChannel || AlphaVersion) ? PrivateBetaKey : PrivateKey), -1), 0, 0, 0); + RSA *prKey = [] { + const auto bio = makeBIO( + const_cast( + (BetaChannel || AlphaVersion) + ? PrivateBetaKey + : PrivateKey), + -1); + return PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0); + }(); if (!prKey) { cout << "Could not read RSA private key!\n"; return -1; @@ -453,7 +473,15 @@ int main(int argc, char *argv[]) } cout << "Checking signature..\n"; - RSA *pbKey = PEM_read_bio_RSAPublicKey(BIO_new_mem_buf(const_cast((BetaChannel || AlphaVersion) ? PublicBetaKey : PublicKey), -1), 0, 0, 0); + RSA *pbKey = [] { + const auto bio = makeBIO( + const_cast( + (BetaChannel || AlphaVersion) + ? PublicBetaKey + : PublicKey), + -1); + return PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0); + }(); if (!pbKey) { cout << "Could not read RSA public key!\n"; return -1; @@ -510,7 +538,12 @@ QString countAlphaVersionSignature(quint64 version) { // duplicated in autoupdat uint32 siglen = 0; - RSA *prKey = PEM_read_bio_RSAPrivateKey(BIO_new_mem_buf(const_cast(cAlphaPrivateKey.constData()), -1), 0, 0, 0); + RSA *prKey = [&] { + const auto bio = makeBIO( + const_cast(cAlphaPrivateKey.constData()), + -1); + return PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0); + }(); if (!prKey) { cout << "Error: Could not read alpha private key!\n"; return QString(); diff --git a/Telegram/SourceFiles/_other/updater_linux.cpp b/Telegram/SourceFiles/_other/updater_linux.cpp index 4aa70dc7f..7bbd13328 100644 --- a/Telegram/SourceFiles/_other/updater_linux.cpp +++ b/Telegram/SourceFiles/_other/updater_linux.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include #include #include #include @@ -87,7 +88,7 @@ void writeLog(const char *format, ...) { va_end(args); } -bool copyFile(const char *from, const char *to) { +bool copyFile(const char *from, const char *to, bool writeprotected) { FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb"); if (!ffrom) { if (fto) fclose(fto); @@ -97,11 +98,6 @@ bool copyFile(const char *from, const char *to) { fclose(ffrom); return false; } - static const int BufSize = 65536; - char buf[BufSize]; - while (size_t size = fread(buf, 1, BufSize, ffrom)) { - fwrite(buf, 1, size, fto); - } struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c //let's say this wont fail since you already worked OK on that fp @@ -110,8 +106,35 @@ bool copyFile(const char *from, const char *to) { fclose(fto); return false; } + + ssize_t copied = sendfile( + fileno(fto), + fileno(ffrom), + nullptr, + fst.st_size); + + if (copied == -1) { + writeLog( + "Copy by sendfile '%s' to '%s' failed, error: %d, fallback now.", + from, + to, + int(errno)); + static const int BufSize = 65536; + char buf[BufSize]; + while (size_t size = fread(buf, 1, BufSize, ffrom)) { + fwrite(buf, 1, size, fto); + } + } else { + writeLog( + "Copy by sendfile '%s' to '%s' done, size: %d, result: %d.", + from, + to, + int(fst.st_size), + int(copied)); + } + //update to the same uid/gid - if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) { + if (!writeprotected && fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) { fclose(ffrom); fclose(fto); return false; @@ -210,7 +233,7 @@ void delFolder() { rmdir(delFolder.c_str()); } -bool update() { +bool update(bool writeprotected) { writeLog("Update started.."); string updDir = workDir + "tupdates/temp", readyFilePath = workDir + "tupdates/temp/ready", tdataDir = workDir + "tupdates/temp/tdata"; @@ -323,7 +346,7 @@ bool update() { writeLog("Copying file '%s' to '%s'..", fname.c_str(), tofname.c_str()); int copyTries = 0, triesLimit = 30; do { - if (!copyFile(fname.c_str(), tofname.c_str())) { + if (!copyFile(fname.c_str(), tofname.c_str(), writeprotected)) { ++copyTries; usleep(100000); } else { @@ -358,6 +381,7 @@ int main(int argc, char *argv[]) { bool needupdate = true; bool autostart = false; bool debug = false; + bool writeprotected = false; bool tosettings = false; bool startintray = false; bool testmode = false; @@ -386,6 +410,8 @@ int main(int argc, char *argv[]) { tosettings = true; } else if (equal(argv[i], "-workdir_custom")) { customWorkingDir = true; + } else if (equal(argv[i], "-writeprotected")) { + writeprotected = true; } else if (equal(argv[i], "-key") && ++i < argc) { key = argv[i]; } else if (equal(argv[i], "-workpath") && ++i < argc) { @@ -413,6 +439,7 @@ int main(int argc, char *argv[]) { } if (needupdate) writeLog("Need to update!"); if (autostart) writeLog("From autostart!"); + if (writeprotected) writeLog("Write Protected folder!"); updaterName = CurrentExecutablePath(argc, argv); writeLog("Updater binary full path is: %s", updaterName.c_str()); @@ -465,7 +492,7 @@ int main(int argc, char *argv[]) { } else { writeLog("Passed workpath is '%s'", workDir.c_str()); } - update(); + update(writeprotected); } } else { writeLog("Error: bad exe name!"); @@ -513,14 +540,17 @@ int main(int argc, char *argv[]) { } args.push_back(nullptr); - pid_t pid = fork(); - switch (pid) { - case -1: - writeLog("fork() failed!"); - return 1; - case 0: - execv(path, args.data()); - return 1; + // let the parent launch instead + if (!writeprotected) { + pid_t pid = fork(); + switch (pid) { + case -1: + writeLog("fork() failed!"); + return 1; + case 0: + execv(args[0], args.data()); + return 1; + } } writeLog("Executed Kotatogram, closing log and quitting.."); diff --git a/Telegram/SourceFiles/api/api_attached_stickers.cpp b/Telegram/SourceFiles/api/api_attached_stickers.cpp index 65cf9efd9..480e9674b 100644 --- a/Telegram/SourceFiles/api/api_attached_stickers.cpp +++ b/Telegram/SourceFiles/api/api_attached_stickers.cpp @@ -36,10 +36,12 @@ void AttachedStickers::request( return; } if (result.v.isEmpty()) { - Ui::show(Box(tr::lng_stickers_not_found(tr::now))); + strongController->show( + Box(tr::lng_stickers_not_found(tr::now))); return; } else if (result.v.size() > 1) { - Ui::show(Box(strongController, result)); + strongController->show( + Box(strongController, result)); return; } // Single attached sticker pack. @@ -52,12 +54,15 @@ void AttachedStickers::request( const auto setId = (setData->vid().v && setData->vaccess_hash().v) ? MTP_inputStickerSetID(setData->vid(), setData->vaccess_hash()) : MTP_inputStickerSetShortName(setData->vshort_name()); - Ui::show( + strongController->show( Box(strongController, setId), Ui::LayerOption::KeepOther); }).fail([=](const MTP::Error &error) { _requestId = 0; - Ui::show(Box(tr::lng_stickers_not_found(tr::now))); + if (const auto strongController = weak.get()) { + strongController->show( + Box(tr::lng_stickers_not_found(tr::now))); + } }).send(); } diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index 183864479..facc08489 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -34,7 +34,11 @@ void CheckChatInvite( session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) { Core::App().hideMediaView(); result.match([=](const MTPDchatInvite &data) { - const auto box = Ui::show(Box( + const auto strongController = weak.get(); + if (!strongController) { + return; + } + const auto box = strongController->show(Box( session, data, invitePeekChannel, @@ -80,7 +84,10 @@ void CheckChatInvite( return; } Core::App().hideMediaView(); - Ui::show(Box(tr::lng_group_invite_bad_link(tr::now))); + if (const auto strong = weak.get()) { + strong->show( + Box(tr::lng_group_invite_bad_link(tr::now))); + } }); } diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 021811c23..02ff76013 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -862,26 +862,35 @@ int32 Updates::pts() const { return _ptsWaiter.current(); } -void Updates::updateOnline() { - updateOnline(false); +void Updates::updateOnline(crl::time lastNonIdleTime) { + updateOnline(lastNonIdleTime, false); } bool Updates::isIdle() const { - return _isIdle; + return _isIdle.current(); } -void Updates::updateOnline(bool gotOtherOffline) { - crl::on_main(&session(), [] { Core::App().checkAutoLock(); }); +rpl::producer Updates::isIdleValue() const { + return _isIdle.value(); +} + +void Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) { + if (!lastNonIdleTime) { + lastNonIdleTime = Core::App().lastNonIdleTime(); + } + crl::on_main(&session(), [=] { + Core::App().checkAutoLock(lastNonIdleTime); + }); const auto &config = _session->serverConfig(); bool isOnline = Core::App().hasActiveWindow(&session()); int updateIn = config.onlineUpdatePeriod; Assert(updateIn >= 0); if (isOnline) { - const auto idle = crl::now() - Core::App().lastNonIdleTime(); + const auto idle = crl::now() - lastNonIdleTime; if (idle >= config.offlineIdleTimeout) { isOnline = false; - if (!_isIdle) { + if (!isIdle()) { _isIdle = true; _idleFinishTimer.callOnce(900); } @@ -929,13 +938,15 @@ void Updates::updateOnline(bool gotOtherOffline) { _onlineTimer.callOnce(updateIn); } -void Updates::checkIdleFinish() { - if (crl::now() - Core::App().lastNonIdleTime() +void Updates::checkIdleFinish(crl::time lastNonIdleTime) { + if (!lastNonIdleTime) { + lastNonIdleTime = Core::App().lastNonIdleTime(); + } + if (crl::now() - lastNonIdleTime < _session->serverConfig().offlineIdleTimeout) { + updateOnline(lastNonIdleTime); _idleFinishTimer.cancel(); _isIdle = false; - updateOnline(); - App::wnd()->checkHistoryActivation(); } else { _idleFinishTimer.callOnce(900); } @@ -954,9 +965,10 @@ bool Updates::isQuitPrevent() { return false; } LOG(("Api::Updates prevents quit, sending offline status...")); - updateOnline(); + updateOnline(crl::now()); return true; } + void Updates::handleSendActionUpdate( PeerId peerId, MsgId rootId, @@ -1747,7 +1759,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { if (UserId(d.vuser_id()) == session().userId()) { if (d.vstatus().type() == mtpc_userStatusOffline || d.vstatus().type() == mtpc_userStatusEmpty) { - updateOnline(true); + updateOnline(Core::App().lastNonIdleTime(), true); if (d.vstatus().type() == mtpc_userStatusOffline) { cSetOtherOnline( d.vstatus().c_userStatusOffline().vwas_online().v); @@ -1878,6 +1890,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updatePhoneCall: case mtpc_updatePhoneCallSignalingData: case mtpc_updateGroupCallParticipants: + case mtpc_updateGroupCallConnection: case mtpc_updateGroupCall: { Core::App().calls().handleUpdate(&session(), update); } break; diff --git a/Telegram/SourceFiles/api/api_updates.h b/Telegram/SourceFiles/api/api_updates.h index dc6c451f7..91635592e 100644 --- a/Telegram/SourceFiles/api/api_updates.h +++ b/Telegram/SourceFiles/api/api_updates.h @@ -38,9 +38,10 @@ public: [[nodiscard]] int32 pts() const; - void updateOnline(); + void updateOnline(crl::time lastNonIdleTime = 0); [[nodiscard]] bool isIdle() const; - void checkIdleFinish(); + [[nodiscard]] rpl::producer isIdleValue() const; + void checkIdleFinish(crl::time lastNonIdleTime = 0); bool lastWasOnline() const; crl::time lastSetOnline() const; bool isQuitPrevent(); @@ -86,7 +87,7 @@ private: MsgRange range, const MTPupdates_ChannelDifference &result); - void updateOnline(bool gotOtherOffline); + void updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline); void sendPing(); void getDifferenceByPts(); void getDifferenceAfterFail(); @@ -185,7 +186,7 @@ private: base::Timer _idleFinishTimer; crl::time _lastSetOnline = 0; bool _lastWasOnline = false; - bool _isIdle = false; + rpl::variable _isIdle = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 160d4dd59..ee74c801b 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -212,6 +212,11 @@ ApiWrap::ApiWrap(not_null session) }, _session->lifetime()); setupSupportMode(); + + Core::App().settings().proxy().connectionTypeValue( + ) | rpl::start_with_next([=] { + refreshTopPromotion(); + }, _session->lifetime()); }); } @@ -261,10 +266,10 @@ void ApiWrap::refreshTopPromotion() { return; } const auto key = [&]() -> std::pair { - if (Global::ProxySettings() != MTP::ProxyData::Settings::Enabled) { + if (!Core::App().settings().proxy().isEnabled()) { return {}; } - const auto &proxy = Global::SelectedProxy(); + const auto &proxy = Core::App().settings().proxy().selected(); if (proxy.type != MTP::ProxyData::Type::Mtproto) { return {}; } diff --git a/Telegram/SourceFiles/boxes/auto_lock_box.cpp b/Telegram/SourceFiles/boxes/auto_lock_box.cpp index 94cf9da88..77ef64b2f 100644 --- a/Telegram/SourceFiles/boxes/auto_lock_box.cpp +++ b/Telegram/SourceFiles/boxes/auto_lock_box.cpp @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "mainwindow.h" #include "ui/widgets/checkbox.h" -#include "facades.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -45,8 +44,7 @@ void AutoLockBox::prepare() { void AutoLockBox::durationChanged(int seconds) { Core::App().settings().setAutoLock(seconds); Core::App().saveSettingsDelayed(); - Global::RefLocalPasscodeChanged().notify(); - Core::App().checkAutoLock(); + Core::App().checkAutoLock(crl::now()); closeBox(); } diff --git a/Telegram/SourceFiles/boxes/background_box.cpp b/Telegram/SourceFiles/boxes/background_box.cpp index 3df6df80a..6c8c7946a 100644 --- a/Telegram/SourceFiles/boxes/background_box.cpp +++ b/Telegram/SourceFiles/boxes/background_box.cpp @@ -151,7 +151,7 @@ void BackgroundBox::prepare() { _inner->chooseEvents( ) | rpl::start_with_next([=](const Data::WallPaper &paper) { - Ui::show( + _controller->show( Box(_controller, paper), Ui::LayerOption::KeepOther); }, _inner->lifetime()); @@ -176,7 +176,7 @@ void BackgroundBox::removePaper(const Data::WallPaper &paper) { paper.mtpSettings() )).send(); }; - Ui::show( + _controller->show( Box( tr::lng_background_sure_delete(tr::now), tr::lng_selected_delete(tr::now), diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 3877a9057..cf1af6627 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_document_resolver.h" #include "data/data_file_origin.h" #include "base/unixtime.h" #include "boxes/confirm_box.h" @@ -769,23 +770,25 @@ bool BackgroundPreviewBox::Start( const QString &slug, const QMap ¶ms) { if (const auto paper = Data::WallPaper::FromColorSlug(slug)) { - Ui::show(Box( + controller->show(Box( controller, paper->withUrlParams(params))); return true; } if (!IsValidWallPaperSlug(slug)) { - Ui::show(Box(tr::lng_background_bad_link(tr::now))); + controller->show( + Box(tr::lng_background_bad_link(tr::now))); return false; } controller->session().api().requestWallPaper(slug, crl::guard(controller, [=]( const Data::WallPaper &result) { - Ui::show(Box( + controller->show(Box( controller, result.withUrlParams(params))); - }), [](const MTP::Error &error) { - Ui::show(Box(tr::lng_background_bad_link(tr::now))); - }); + }), crl::guard(controller, [=](const MTP::Error &error) { + controller->show( + Box(tr::lng_background_bad_link(tr::now))); + })); return true; } diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index b02077e2b..f99767337 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -433,7 +433,7 @@ editMediaButtonFileSkipRight: 1px; editMediaButtonFileSkipTop: 7px; editMediaButtonIconFile: icon {{ "settings_edit", menuIconFg }}; -editMediaButtonIconPhoto: icon {{ "settings_edit", msgServiceFg }}; +editMediaButtonIconPhoto: icon {{ "settings_edit", roundedFg }}; editMediaButton: IconButton { width: editMediaButtonSize; height: editMediaButtonSize; @@ -464,8 +464,8 @@ sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "history_file_cancel", menuIconFg, point(6px, 6px) }}; -sendBoxAlbumGroupButtonMediaEdit: icon {{ "settings_edit", msgServiceFg, point(3px, -2px) }}; -sendBoxAlbumGroupButtonMediaDelete: icon {{ "history_file_cancel", msgServiceFg, point(2px, 5px) }}; +sendBoxAlbumGroupButtonMediaEdit: icon {{ "settings_edit", roundedFg, point(3px, -2px) }}; +sendBoxAlbumGroupButtonMediaDelete: icon {{ "history_file_cancel", roundedFg, point(2px, 5px) }}; sendBoxAlbumGroupButtonMedia: IconButton { width: sendBoxAlbumGroupHeight; height: sendBoxAlbumGroupHeight; @@ -729,10 +729,6 @@ termsAgePadding: margins(22px, 16px, 16px, 0px); themesSmallSkip: 10px; themesBackgroundSize: 120px; -themesScroll: ScrollArea(defaultScrollArea) { - bottomsh: 0px; - topsh: 0px; -} themesMenuToggle: IconButton(defaultIconButton) { width: 44px; height: 44px; diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 708f738cc..db9b281c7 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_url.h" #include "base/call_delayed.h" #include "core/application.h" +#include "core/core_settings.h" #include "main/main_account.h" #include "mtproto/facade.h" #include "ui/widgets/checkbox.h" @@ -26,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" #include "ui/text/text_options.h" -#include "facades.h" +#include "ui/basic_click_handlers.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" @@ -185,7 +186,10 @@ class ProxiesBox : public Ui::BoxContent { public: using View = ProxiesBoxController::ItemView; - ProxiesBox(QWidget*, not_null controller); + ProxiesBox( + QWidget*, + not_null controller, + Core::SettingsProxy &settings); protected: void prepare() override; @@ -200,6 +204,7 @@ private: void refreshProxyForCalls(); not_null _controller; + Core::SettingsProxy &_settings; QPointer _tryIPv6; std::shared_ptr> _proxySettings; QPointer> _proxyForCalls; @@ -565,8 +570,10 @@ void ProxyRow::showMenu() { ProxiesBox::ProxiesBox( QWidget*, - not_null controller) + not_null controller, + Core::SettingsProxy &settings) : _controller(controller) +, _settings(settings) , _initialWrap(this) { _controller->views( ) | rpl::start_with_next([=](View &&view) { @@ -590,11 +597,11 @@ void ProxiesBox::setupContent() { object_ptr( inner, tr::lng_connection_try_ipv6(tr::now), - Global::TryIPv6()), + _settings.tryIPv6()), st::proxyTryIPv6Padding); _proxySettings = std::make_shared>( - Global::ProxySettings()); + _settings.settings()); inner->add( object_ptr>( inner, @@ -622,7 +629,7 @@ void ProxiesBox::setupContent() { object_ptr( inner, tr::lng_proxy_use_for_calls(tr::now), - Global::UseProxyForCalls()), + _settings.useProxyForCalls()), style::margins( 0, st::proxyUsePadding.top(), @@ -651,7 +658,7 @@ void ProxiesBox::setupContent() { _proxySettings->setChangedCallback([=](ProxyData::Settings value) { if (!_controller->setProxySettings(value)) { - _proxySettings->setValue(Global::ProxySettings()); + _proxySettings->setValue(_settings.settings()); addNewProxy(); } refreshProxyForCalls(); @@ -1050,20 +1057,22 @@ void ProxyBox::addLabel( ProxiesBoxController::ProxiesBoxController(not_null account) : _account(account) +, _settings(Core::App().settings().proxy()) , _saveTimer([] { Local::writeSettings(); }) { _list = ranges::views::all( - Global::ProxiesList() + _settings.list() ) | ranges::views::transform([&](const ProxyData &proxy) { return Item{ ++_idCounter, proxy }; }) | ranges::to_vector; - subscribe(Global::RefConnectionTypeChanged(), [=] { - _proxySettingsChanges.fire_copy(Global::ProxySettings()); - const auto i = findByProxy(Global::SelectedProxy()); + _settings.connectionTypeChanges( + ) | rpl::start_with_next([=] { + _proxySettingsChanges.fire_copy(_settings.settings()); + const auto i = findByProxy(_settings.selected()); if (i != end(_list)) { updateView(*i); } - }); + }, _lifetime); for (auto &item : _list) { refreshChecker(item); @@ -1086,17 +1095,32 @@ void ProxiesBoxController::ShowApplyConfirmation( proxy.password = fields.value(qsl("secret")); } if (proxy) { + const auto displayed = "https://" + server + "/"; + const auto parsed = QUrl::fromUserInput(displayed); + const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed) + ? displayed + : parsed.isValid() + ? QString::fromUtf8(parsed.toEncoded()) + : UrlClickHandler::ShowEncoded(displayed); + const auto displayServer = QString( + displayUrl + ).replace( + QRegularExpression( + "^https://", + QRegularExpression::CaseInsensitiveOption), + QString() + ).replace(QRegularExpression("/$"), QString()); const auto text = tr::lng_sure_enable_socks( tr::now, lt_server, - server, + displayServer, lt_port, QString::number(port)) + (proxy.type == Type::Mtproto ? "\n\n" + tr::lng_proxy_sponsor_warning(tr::now) : QString()); auto callback = [=](Fn &&close) { - auto &proxies = Global::RefProxiesList(); + auto &proxies = Core::App().settings().proxy().list(); if (!ranges::contains(proxies, proxy)) { proxies.push_back(proxy); } @@ -1123,7 +1147,7 @@ void ProxiesBoxController::ShowApplyConfirmation( auto ProxiesBoxController::proxySettingsValue() const -> rpl::producer { return _proxySettingsChanges.events_starting_with_copy( - Global::ProxySettings() + _settings.settings() ) | rpl::distinct_until_changed(); } @@ -1134,6 +1158,7 @@ void ProxiesBoxController::refreshChecker(Item &item) { : Variants::Tcp; const auto mtproto = &_account->mtp(); const auto dcId = mtproto->mainDcId(); + const auto forFiles = false; item.state = ItemState::Checking; const auto setup = [&](Checker &checker, const bytes::vector &secret) { @@ -1152,7 +1177,8 @@ void ProxiesBoxController::refreshChecker(Item &item) { item.data.host, item.data.port, secret, - dcId); + dcId, + forFiles); item.checkerv6 = nullptr; } else { const auto options = mtproto->dcOptions().lookup( @@ -1164,7 +1190,8 @@ void ProxiesBoxController::refreshChecker(Item &item) { Variants::Address address) { const auto &list = options.data[address][type]; if (list.empty() - || (address == Variants::IPv6 && !Global::TryIPv6())) { + || ((address == Variants::IPv6) + && !Core::App().settings().proxy().tryIPv6())) { checker = nullptr; return; } @@ -1174,7 +1201,8 @@ void ProxiesBoxController::refreshChecker(Item &item) { QString::fromStdString(endpoint.ip), endpoint.port, endpoint.secret, - dcId); + dcId, + forFiles); }; connect(item.checker, Variants::IPv4); connect(item.checkerv6, Variants::IPv6); @@ -1225,7 +1253,7 @@ object_ptr ProxiesBoxController::CreateOwningBox( } object_ptr ProxiesBoxController::create() { - auto result = Box(this); + auto result = Box(this, _settings); for (const auto &item : _list) { updateView(item); } @@ -1263,14 +1291,13 @@ void ProxiesBoxController::shareItem(int id) { void ProxiesBoxController::applyItem(int id) { auto item = findById(id); - if ((Global::ProxySettings() == ProxyData::Settings::Enabled) - && Global::SelectedProxy() == item->data) { + if (_settings.isEnabled() && (_settings.selected() == item->data)) { return; } else if (item->deleted) { return; } - auto j = findByProxy(Global::SelectedProxy()); + auto j = findByProxy(_settings.selected()); Core::App().setCurrentProxy( item->data, @@ -1288,12 +1315,13 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) { item->deleted = deleted; if (deleted) { - auto &proxies = Global::RefProxiesList(); + auto &proxies = _settings.list(); proxies.erase(ranges::remove(proxies, item->data), end(proxies)); - if (item->data == Global::SelectedProxy()) { - _lastSelectedProxy = base::take(Global::RefSelectedProxy()); - if (Global::ProxySettings() == ProxyData::Settings::Enabled) { + if (item->data == _settings.selected()) { + _lastSelectedProxy = _settings.selected(); + _settings.setSelected(MTP::ProxyData()); + if (_settings.isEnabled()) { _lastSelectedProxyUsed = true; Core::App().setCurrentProxy( ProxyData(), @@ -1304,7 +1332,7 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) { } } } else { - auto &proxies = Global::RefProxiesList(); + auto &proxies = _settings.list(); if (ranges::find(proxies, item->data) == end(proxies)) { auto insertBefore = item + 1; while (insertBefore != end(_list) && insertBefore->deleted) { @@ -1316,15 +1344,15 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) { proxies.insert(insertBeforeIt, item->data); } - if (!Global::SelectedProxy() && _lastSelectedProxy == item->data) { - Assert(Global::ProxySettings() != ProxyData::Settings::Enabled); + if (!_settings.selected() && _lastSelectedProxy == item->data) { + Assert(!_settings.isEnabled()); if (base::take(_lastSelectedProxyUsed)) { Core::App().setCurrentProxy( base::take(_lastSelectedProxy), ProxyData::Settings::Enabled); } else { - Global::SetSelectedProxy(base::take(_lastSelectedProxy)); + _settings.setSelected(base::take(_lastSelectedProxy)); } } } @@ -1352,7 +1380,7 @@ object_ptr ProxiesBoxController::editItemBox(int id) { void ProxiesBoxController::replaceItemWith( std::vector::iterator which, std::vector::iterator with) { - auto &proxies = Global::RefProxiesList(); + auto &proxies = _settings.list(); proxies.erase(ranges::remove(proxies, which->data), end(proxies)); _views.fire({ which->id }); @@ -1372,7 +1400,7 @@ void ProxiesBoxController::replaceItemValue( restoreItem(which->id); } - auto &proxies = Global::RefProxiesList(); + auto &proxies = _settings.list(); const auto i = ranges::find(proxies, which->data); Assert(i != end(proxies)); *i = proxy; @@ -1403,7 +1431,7 @@ object_ptr ProxiesBoxController::addNewItemBox() { } void ProxiesBoxController::addNewItem(const ProxyData &proxy) { - auto &proxies = Global::RefProxiesList(); + auto &proxies = _settings.list(); proxies.push_back(proxy); _list.push_back({ ++_idCounter, proxy }); @@ -1412,43 +1440,42 @@ void ProxiesBoxController::addNewItem(const ProxyData &proxy) { } bool ProxiesBoxController::setProxySettings(ProxyData::Settings value) { - if (Global::ProxySettings() == value) { + if (_settings.settings() == value) { return true; } else if (value == ProxyData::Settings::Enabled) { - if (Global::ProxiesList().empty()) { + if (_settings.list().empty()) { return false; - } else if (!Global::SelectedProxy()) { - Global::SetSelectedProxy(Global::ProxiesList().back()); - auto j = findByProxy(Global::SelectedProxy()); + } else if (!_settings.selected()) { + _settings.setSelected(_settings.list().back()); + auto j = findByProxy(_settings.selected()); if (j != end(_list)) { updateView(*j); } } } - Core::App().setCurrentProxy(Global::SelectedProxy(), value); + Core::App().setCurrentProxy(_settings.selected(), value); saveDelayed(); return true; } void ProxiesBoxController::setProxyForCalls(bool enabled) { - if (Global::UseProxyForCalls() == enabled) { + if (_settings.useProxyForCalls() == enabled) { return; } - Global::SetUseProxyForCalls(enabled); - if ((Global::ProxySettings() == ProxyData::Settings::Enabled) - && Global::SelectedProxy().supportsCalls()) { - Global::RefConnectionTypeChanged().notify(); + _settings.setUseProxyForCalls(enabled); + if (_settings.isEnabled() && _settings.selected().supportsCalls()) { + _settings.connectionTypeChangesNotify(); } saveDelayed(); } void ProxiesBoxController::setTryIPv6(bool enabled) { - if (Global::TryIPv6() == enabled) { + if (Core::App().settings().proxy().tryIPv6() == enabled) { return; } - Global::SetTryIPv6(enabled); + Core::App().settings().proxy().setTryIPv6(enabled); _account->mtp().restart(); - Global::RefConnectionTypeChanged().notify(); + _settings.connectionTypeChangesNotify(); saveDelayed(); } @@ -1461,7 +1488,7 @@ auto ProxiesBoxController::views() const -> rpl::producer { } void ProxiesBoxController::updateView(const Item &item) { - const auto selected = (Global::SelectedProxy() == item.data); + const auto selected = (_settings.selected() == item.data); const auto deleted = item.deleted; const auto type = [&] { switch (item.data.type) { @@ -1475,8 +1502,7 @@ void ProxiesBoxController::updateView(const Item &item) { Unexpected("Proxy type in ProxiesBoxController::updateView."); }(); const auto state = [&] { - if (!selected - || (Global::ProxySettings() != ProxyData::Settings::Enabled)) { + if (!selected || !_settings.isEnabled()) { return item.state; } else if (_account->mtp().dcstate() == MTP::ConnectedState) { return ItemState::Online; diff --git a/Telegram/SourceFiles/boxes/connection_box.h b/Telegram/SourceFiles/boxes/connection_box.h index 8a7709f9c..26244fe50 100644 --- a/Telegram/SourceFiles/boxes/connection_box.h +++ b/Telegram/SourceFiles/boxes/connection_box.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/object_ptr.h" +#include "core/core_settings.h" #include "mtproto/connection_abstract.h" #include "mtproto/mtproto_proxy_data.h" @@ -28,7 +29,7 @@ namespace Main { class Account; } // namespace Main -class ProxiesBoxController : public base::Subscriber { +class ProxiesBoxController { public: using ProxyData = MTP::ProxyData; using Type = ProxyData::Type; @@ -110,6 +111,7 @@ private: void addNewItem(const ProxyData &proxy); const not_null _account; + Core::SettingsProxy &_settings; int _idCounter = 0; std::vector _list; rpl::event_stream _views; @@ -119,4 +121,6 @@ private: ProxyData _lastSelectedProxy; bool _lastSelectedProxyUsed = false; + rpl::lifetime _lifetime; + }; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index c72618cea..82e37f248 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -1085,7 +1085,7 @@ object_ptr CreatePollBox::setupContent() { send({ .silent = true }); }; const auto sendScheduled = [=] { - Ui::show( + _controller->show( HistoryView::PrepareScheduleBox( this, SendMenu::Type::Scheduled, diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 1a517ca21..b7b20aa91 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h" +#include "ui/text/format_song_document_name.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/chat/attach/attach_prepare.h" @@ -220,7 +221,7 @@ EditCaptionBox::EditCaptionBox( const auto document = _documentMedia->owner(); const auto nameString = document->isVoiceMessage() ? tr::lng_media_audio(tr::now) - : document->composeNameString(); + : Ui::Text::FormatSongNameFor(document).string(); setName(nameString, document->size); _isImage = document->isImage(); _isAudio = document->isVoiceMessage() @@ -486,13 +487,13 @@ void EditCaptionBox::handleStreamingError(Error &&error) { } void EditCaptionBox::streamingReady(Information &&info) { - const auto calculateGifDimensions = [&]() { + const auto calculateGifDimensions = [&] { const auto scaled = QSize( info.video.size.width(), info.video.size.height() ).scaled( - st::sendMediaPreviewSize * cIntRetinaFactor(), - st::confirmMaxHeight * cIntRetinaFactor(), + st::sendMediaPreviewSize, + st::confirmMaxHeight, Qt::KeepAspectRatio); _thumbw = _gifw = scaled.width(); _thumbh = _gifh = scaled.height(); @@ -549,10 +550,10 @@ void EditCaptionBox::updateEditPreview() { if (shouldAsDoc) { auto nameString = filename; if (const auto song = std::get_if(fileMedia)) { - nameString = Ui::ComposeNameString( + nameString = Ui::Text::FormatSongName( filename, song->title, - song->performer); + song->performer).string(); _isAudio = true; if (auto cover = song->cover; !cover.isNull()) { @@ -620,7 +621,7 @@ void EditCaptionBox::updateEditMediaButton() { const auto icon = _doc ? &st::editMediaButtonIconFile : &st::editMediaButtonIconPhoto; - const auto color = _doc ? &st::windowBgRipple : &st::callFingerprintBg; + const auto color = _doc ? &st::windowBgRipple : &st::roundedBg; _editMedia->setIconOverride(icon); _editMedia->setRippleColorOverride(color); _editMedia->setForceRippled(!_doc, anim::type::instant); diff --git a/Telegram/SourceFiles/boxes/edit_color_box.cpp b/Telegram/SourceFiles/boxes/edit_color_box.cpp index 8889db904..3c78af799 100644 --- a/Telegram/SourceFiles/boxes/edit_color_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_color_box.cpp @@ -27,8 +27,8 @@ public: return _y; } - base::Observable &changed() { - return _changed; + rpl::producer<> changed() const { + return _changed.events(); } void setHSB(HSB hsb); void setRGB(int red, int green, int blue); @@ -61,7 +61,7 @@ private: float64 _y = 0.; bool _choosing = false; - base::Observable _changed; + rpl::event_stream<> _changed; }; @@ -234,7 +234,7 @@ void EditColorBox::Picker::updateCurrentPoint(QPoint localPosition) { _x = x; _y = y; update(); - _changed.notify(); + _changed.fire({}); } } @@ -284,8 +284,8 @@ public: }; Slider(QWidget *parent, Direction direction, Type type, QColor color); - base::Observable &changed() { - return _changed; + rpl::producer<> changed() const { + return _changed.events(); } float64 value() const { return _value; @@ -335,7 +335,7 @@ private: QBrush _transparent; bool _choosing = false; - base::Observable _changed; + rpl::event_stream<> _changed; }; @@ -349,7 +349,9 @@ EditColorBox::Slider::Slider( , _type(type) , _color(color.red(), color.green(), color.blue()) , _value(valueFromColor(color)) -, _transparent((_type == Type::Opacity) ? style::transparentPlaceholderBrush() : QBrush()) { +, _transparent((_type == Type::Opacity) + ? style::TransparentPlaceholder() + : QBrush()) { prepareMinSize(); } @@ -538,7 +540,7 @@ void EditColorBox::Slider::updateCurrentPoint(QPoint localPosition) { if (_value != value) { _value = value; update(); - _changed.notify(); + _changed.fire({}); } } @@ -758,7 +760,7 @@ EditColorBox::EditColorBox( , _greenField(this, st::colorValueInput, "G", 255) , _blueField(this, st::colorValueInput, "B", 255) , _result(this, st::colorResultInput) -, _transparent(style::transparentPlaceholderBrush()) +, _transparent(style::TransparentPlaceholder()) , _current(current) , _new(current) { if (_mode == Mode::RGBA) { @@ -824,16 +826,14 @@ void EditColorBox::prepare() { auto height = st::colorEditSkip + st::colorPickerSize + st::colorEditSkip + st::colorSliderWidth + st::colorEditSkip; setDimensions(st::colorEditWidth, height); - subscribe(_picker->changed(), [=] { updateFromControls(); }); - if (_hueSlider) { - subscribe(_hueSlider->changed(), [=] { updateFromControls(); }); - } - if (_opacitySlider) { - subscribe(_opacitySlider->changed(), [=] { updateFromControls(); }); - } - if (_lightnessSlider) { - subscribe(_lightnessSlider->changed(), [=] { updateFromControls(); }); - } + rpl::merge( + _picker->changed(), + (_hueSlider ? _hueSlider->changed() : rpl::never<>()), + (_opacitySlider ? _opacitySlider->changed() : rpl::never<>()), + (_lightnessSlider ? _lightnessSlider->changed() : rpl::never<>()) + ) | rpl::start_with_next([=] { + updateFromControls(); + }, lifetime()); boxClosing() | rpl::start_with_next([=] { if (_cancelCallback) { diff --git a/Telegram/SourceFiles/boxes/edit_color_box.h b/Telegram/SourceFiles/boxes/edit_color_box.h index 5c2e7170a..c77616f5a 100644 --- a/Telegram/SourceFiles/boxes/edit_color_box.h +++ b/Telegram/SourceFiles/boxes/edit_color_box.h @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/abstract_box.h" -class EditColorBox : public Ui::BoxContent, private base::Subscriber { +class EditColorBox : public Ui::BoxContent { public: enum class Mode { RGBA, diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index d6e2a26c7..1102f5fa1 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -165,7 +165,7 @@ void EditPrivacyBox::editExceptions( })); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); }; - Ui::show( + _window->show( Box(std::move(controller), std::move(initBox)), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index d42145837..e7917c763 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -1166,15 +1166,19 @@ base::binary_guard LanguageBox::Show() { if (manager.languageList().empty()) { auto guard = std::make_shared( result.make_guard()); - auto alive = std::make_shared>( - std::make_unique()); - **alive = manager.languageListChanged().add_subscription([=] { + auto lifetime = std::make_shared(); + manager.languageListChanged( + ) | rpl::take( + 1 + ) | rpl::start_with_next([=]() mutable { const auto show = guard->alive(); - *alive = nullptr; + if (lifetime) { + base::take(lifetime)->destroy(); + } if (show) { Ui::show(Box()); } - }); + }, *lifetime); } else { Ui::show(Box()); } diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index a6ce44819..0d33661a0 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -27,7 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_encryption.h" #include "passport/passport_panel_edit_contact.h" #include "settings/settings_privacy_security.h" -#include "facades.h" #include "styles/style_layers.h" #include "styles/style_passport.h" #include "styles/style_boxes.h" @@ -119,7 +118,12 @@ PasscodeBox::PasscodeBox( , _turningOff(turningOff) , _about(st::boxWidth - st::boxPadding.left() * 1.5) , _oldPasscode(this, st::defaultInputField, tr::lng_passcode_enter_old()) -, _newPasscode(this, st::defaultInputField, Global::LocalPasscode() ? tr::lng_passcode_enter_new() : tr::lng_passcode_enter_first()) +, _newPasscode( + this, + st::defaultInputField, + session->domain().local().hasLocalPasscode() + ? tr::lng_passcode_enter_new() + : tr::lng_passcode_enter_first()) , _reenterPasscode(this, st::defaultInputField, tr::lng_passcode_confirm_new()) , _passwordHint(this, st::defaultInputField, tr::lng_cloud_password_hint()) , _recoverEmail(this, st::defaultInputField, tr::lng_cloud_password_email()) @@ -164,7 +168,9 @@ rpl::producer<> PasscodeBox::clearUnconfirmedPassword() const { } bool PasscodeBox::currentlyHave() const { - return _cloudPwd ? (!!_cloudFields.curRequest) : Global::LocalPasscode(); + return _cloudPwd + ? (!!_cloudFields.curRequest) + : _session->domain().local().hasLocalPasscode(); } bool PasscodeBox::onlyCheckCurrent() const { @@ -520,7 +526,7 @@ void PasscodeBox::save(bool force) { return; } - if (Core::App().domain().local().checkPasscode(old.toUtf8())) { + if (_session->domain().local().checkPasscode(old.toUtf8())) { cSetPasscodeBadTries(0); if (_turningOff) pwd = conf = QString(); } else { @@ -588,7 +594,7 @@ void PasscodeBox::save(bool force) { closeReplacedBy(); const auto weak = Ui::MakeWeak(this); cSetPasscodeBadTries(0); - Core::App().domain().local().setPasscode(pwd.toUtf8()); + _session->domain().local().setPasscode(pwd.toUtf8()); Core::App().localPasscodeChanged(); if (weak) { closeBox(); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index b78e4ead5..b2defa384 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -437,6 +437,7 @@ PeerListRow::PeerListRow(not_null peer) PeerListRow::PeerListRow(not_null peer, PeerListRowId id) : _id(id) , _peer(peer) +, _hidden(false) , _initialized(false) , _isSearchResult(false) , _isSavedMessagesChat(false) @@ -445,6 +446,7 @@ PeerListRow::PeerListRow(not_null peer, PeerListRowId id) PeerListRow::PeerListRow(PeerListRowId id) : _id(id) +, _hidden(false) , _initialized(false) , _isSearchResult(false) , _isSavedMessagesChat(false) @@ -542,7 +544,7 @@ QString PeerListRow::generateShortName() { : peer()->shortName(); } -std::shared_ptr PeerListRow::ensureUserpicView() { +std::shared_ptr &PeerListRow::ensureUserpicView() { if (!_userpic) { _userpic = peer()->createUserpicView(); } @@ -669,11 +671,14 @@ bool PeerListRow::hasAction() { return true; } -template -void PeerListRow::addRipple(const style::PeerListItem &st, QSize size, QPoint point, UpdateCallback updateCallback) { +template +void PeerListRow::addRipple(const style::PeerListItem &st, MaskGenerator &&maskGenerator, QPoint point, UpdateCallback &&updateCallback) { if (!_ripple) { - auto mask = Ui::RippleAnimation::rectMask(size); - _ripple = std::make_unique(st.button.ripple, std::move(mask), std::move(updateCallback)); + auto mask = maskGenerator(); + if (mask.isNull()) { + return; + } + _ripple = std::make_unique(st.button.ripple, std::move(mask), std::forward(updateCallback)); } _ripple->add(point); } @@ -855,12 +860,42 @@ PeerListContent::PeerListContent( _repaintByStatus.setCallback([this] { update(); }); } +void PeerListContent::setMode(Mode mode) { + if (mode == Mode::Default && _mode == Mode::Default) { + return; + } + _mode = mode; + switch (_mode) { + case Mode::Default: + _rowHeight = _st.item.height; + break; + case Mode::Custom: + _rowHeight = _controller->customRowHeight(); + break; + } + const auto wasMouseSelection = _mouseSelection; + const auto wasLastMousePosition = _lastMousePosition; + _contextMenu = nullptr; + if (wasMouseSelection) { + setSelected(Selected()); + } + setPressed(Selected()); + refreshRows(); + if (wasMouseSelection && wasLastMousePosition) { + selectByMouse(*wasLastMousePosition); + } +} + void PeerListContent::appendRow(std::unique_ptr row) { Expects(row != nullptr); if (_rowsById.find(row->id()) == _rowsById.cend()) { row->setAbsoluteIndex(_rows.size()); addRowEntry(row.get()); + if (!_hiddenRows.empty()) { + Assert(!row->hidden()); + _filterResults.push_back(row.get()); + } _rows.push_back(std::move(row)); } } @@ -898,6 +933,17 @@ void PeerListContent::changeCheckState( [=] { updateRow(row); }); } +void PeerListContent::setRowHidden(not_null row, bool hidden) { + Expects(!row->isSearchResult()); + + row->setHidden(hidden); + if (hidden) { + _hiddenRows.emplace(row); + } else { + _hiddenRows.remove(row); + } +} + void PeerListContent::addRowEntry(not_null row) { if (_controller->respectSavedMessagesChat() && !row->special()) { if (row->peer()->isSelf()) { @@ -964,6 +1010,10 @@ void PeerListContent::prependRow(std::unique_ptr row) { if (_rowsById.find(row->id()) == _rowsById.cend()) { addRowEntry(row.get()); + if (!_hiddenRows.empty()) { + Assert(!row->hidden()); + _filterResults.insert(_filterResults.begin(), row.get()); + } _rows.insert(_rows.begin(), std::move(row)); refreshIndices(); } @@ -979,6 +1029,10 @@ void PeerListContent::prependRowFromSearchResult(not_null row) { Assert(_searchRows[index].get() == row); row->setIsSearchResult(false); + if (!_hiddenRows.empty()) { + Assert(!row->hidden()); + _filterResults.insert(_filterResults.begin(), row); + } _rows.insert(_rows.begin(), std::move(_searchRows[index])); refreshIndices(); removeRowAtIndex(_searchRows, index); @@ -1032,6 +1086,7 @@ void PeerListContent::removeRow(not_null row) { _filterResults.erase( ranges::remove(_filterResults, row), end(_filterResults)); + _hiddenRows.remove(row); removeRowAtIndex(eraseFrom, index); restoreSelection(); @@ -1069,7 +1124,9 @@ void PeerListContent::convertRowToSearchResult(not_null row) { removeFromSearchIndex(row); row->setIsSearchResult(true); + row->setHidden(false); row->setAbsoluteIndex(_searchRows.size()); + _hiddenRows.remove(row); _searchRows.push_back(std::move(_rows[index])); removeRowAtIndex(_rows, index); } @@ -1154,6 +1211,14 @@ int PeerListContent::labelHeight() const { } void PeerListContent::refreshRows() { + if (!_hiddenRows.empty()) { + _filterResults.clear(); + for (const auto &row : _rows) { + if (!row->hidden()) { + _filterResults.push_back(row.get()); + } + } + } resizeToWidth(width()); if (_visibleBottom > 0) { checkScrollForPreload(); @@ -1167,7 +1232,7 @@ void PeerListContent::refreshRows() { void PeerListContent::setSearchMode(PeerListSearchMode mode) { if (_searchMode != mode) { if (!addingToSearchIndex()) { - for_const (auto &row, _rows) { + for (const auto &row : _rows) { addToSearchIndex(row.get()); } } @@ -1194,25 +1259,27 @@ void PeerListContent::clearSearchRows() { void PeerListContent::paintEvent(QPaintEvent *e) { Painter p(this); - auto clip = e->rect(); - p.fillRect(clip, _st.item.button.textBg); + const auto clip = e->rect(); + if (_mode != Mode::Custom) { + p.fillRect(clip, _st.item.button.textBg); + } - auto repaintByStatusAfter = _repaintByStatus.remainingTime(); + const auto repaintByStatusAfter = _repaintByStatus.remainingTime(); auto repaintAfterMin = repaintByStatusAfter; - auto rowsTopCached = rowsTop(); - auto ms = crl::now(); - auto yFrom = clip.y() - rowsTopCached; - auto yTo = clip.y() + clip.height() - rowsTopCached; + const auto rowsTopCached = rowsTop(); + const auto now = crl::now(); + const auto yFrom = clip.y() - rowsTopCached; + const auto yTo = clip.y() + clip.height() - rowsTopCached; p.translate(0, rowsTopCached); - auto count = shownRowsCount(); + const auto count = shownRowsCount(); if (count > 0) { - auto from = floorclamp(yFrom, _rowHeight, 0, count); - auto to = ceilclamp(yTo, _rowHeight, 0, count); + const auto from = floorclamp(yFrom, _rowHeight, 0, count); + const auto to = ceilclamp(yTo, _rowHeight, 0, count); p.translate(0, from * _rowHeight); for (auto index = from; index != to; ++index) { - auto repaintAfter = paintRow(p, ms, RowIndex(index)); - if (repaintAfter >= 0 + const auto repaintAfter = paintRow(p, now, RowIndex(index)); + if (repaintAfter > 0 && (repaintAfterMin < 0 || repaintAfterMin > repaintAfter)) { repaintAfterMin = repaintAfter; @@ -1327,9 +1394,16 @@ void PeerListContent::mousePressEvent(QMouseEvent *e) { row->addActionRipple(point, std::move(updateCallback)); } } else { - auto size = QSize(width(), _rowHeight); auto point = mapFromGlobal(QCursor::pos()) - QPoint(0, getRowTop(_selected.index)); - row->addRipple(_st.item, size, point, std::move(updateCallback)); + if (_mode == Mode::Custom) { + row->addRipple(_st.item, _controller->customRowRippleMaskGenerator(), point, std::move(updateCallback)); + } else { + const auto maskGenerator = [&] { + return Ui::RippleAnimation::rectMask( + QSize(width(), _rowHeight)); + }; + row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback)); + } } } if (anim::Disabled()) { @@ -1360,13 +1434,22 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) { void PeerListContent::showRowMenu( not_null row, + bool highlightRow, Fn)> destroyed) { - showRowMenu(findRowIndex(row), QCursor::pos(), std::move(destroyed)); + const auto index = findRowIndex(row); + showRowMenu( + index, + row, + QCursor::pos(), + highlightRow, + std::move(destroyed)); } bool PeerListContent::showRowMenu( RowIndex index, + PeerListRow *row, QPoint globalPos, + bool highlightRow, Fn)> destroyed) { if (_contextMenu) { _contextMenu->setDestroyedCallback(nullptr); @@ -1377,7 +1460,9 @@ bool PeerListContent::showRowMenu( mousePressReleased(_pressButton); } - const auto row = getRow(index); + if (highlightRow) { + row = getRow(index); + } if (!row) { return false; } @@ -1388,11 +1473,15 @@ bool PeerListContent::showRowMenu( return false; } - setContexted({ index, false }); + if (highlightRow) { + setContexted({ index, false }); + } raw->setDestroyedCallback(crl::guard( this, [=] { - setContexted(Selected()); + if (highlightRow) { + setContexted(Selected()); + } handleMouseMove(QCursor::pos()); if (destroyed) { destroyed(raw); @@ -1406,7 +1495,7 @@ void PeerListContent::contextMenuEvent(QContextMenuEvent *e) { if (e->reason() == QContextMenuEvent::Mouse) { handleMouseMove(e->globalPos()); } - if (showRowMenu(_selected.index, e->globalPos())) { + if (showRowMenu(_selected.index, nullptr, e->globalPos(), true)) { e->accept(); } } @@ -1421,7 +1510,7 @@ void PeerListContent::setPressed(Selected pressed) { crl::time PeerListContent::paintRow( Painter &p, - crl::time ms, + crl::time now, RowIndex index) { const auto row = getRow(index); Assert(row != nullptr); @@ -1429,13 +1518,15 @@ crl::time PeerListContent::paintRow( row->lazyInitialize(_st.item); auto refreshStatusAt = row->refreshStatusTime(); - if (refreshStatusAt >= 0 && ms >= refreshStatusAt) { + if (refreshStatusAt > 0 && now >= refreshStatusAt) { row->refreshStatus(); refreshStatusAt = row->refreshStatusTime(); } + const auto refreshStatusIn = (refreshStatusAt > 0) + ? std::max(refreshStatusAt - now, crl::time(1)) + : 0; const auto peer = row->special() ? nullptr : row->peer().get(); - const auto user = peer ? peer->asUser() : nullptr; const auto active = (_contexted.index.value >= 0) ? _contexted : (_pressed.index.value >= 0) @@ -1444,6 +1535,11 @@ crl::time PeerListContent::paintRow( const auto selected = (active.index == index); const auto actionSelected = (selected && active.action); + if (_mode == Mode::Custom) { + _controller->customRowPaint(p, now, row, selected); + return refreshStatusIn; + } + const auto &bg = selected ? _st.item.button.textBgOver : _st.item.button.textBg; @@ -1541,7 +1637,7 @@ crl::time PeerListContent::paintRow( } else { row->paintStatusText(p, _st.item, _st.item.statusPosition.x(), _st.item.statusPosition.y(), statusw, width(), selected); } - return (refreshStatusAt - ms); + return refreshStatusIn; } PeerListContent::SkipResult PeerListContent::selectSkip(int direction) { @@ -1691,6 +1787,8 @@ void PeerListContent::searchQueryChanged(QString query) { if (_normalizedSearchQuery != normalizedQuery) { setSearchQuery(query, normalizedQuery); if (_controller->searchInLocal() && !searchWordsList.isEmpty()) { + Assert(_hiddenRows.empty()); + auto minimalList = (const std::vector>*)nullptr; for (const auto &searchWord : searchWordsList) { auto searchWordStart = searchWord[0].toLower(); @@ -1740,15 +1838,17 @@ void PeerListContent::searchQueryChanged(QString query) { } std::unique_ptr PeerListContent::saveState() const { + Expects(_hiddenRows.empty()); + auto result = std::make_unique(); result->controllerState = std::make_unique(); result->list.reserve(_rows.size()); - for (auto &row : _rows) { + for (const auto &row : _rows) { result->list.push_back(row->peer()); } result->filterResults.reserve(_filterResults.size()); - for (auto &row : _filterResults) { + for (const auto &row : _filterResults) { result->filterResults.push_back(row->peer()); } result->searchQuery = _searchQuery; @@ -1869,15 +1969,25 @@ void PeerListContent::selectByMouse(QPoint globalPosition) { _mouseSelection = true; _lastMousePosition = globalPosition; const auto point = mapFromGlobal(globalPosition); + const auto customMode = (_mode == Mode::Custom); auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(globalPosition)); auto selected = Selected(); auto rowsPointY = point.y() - rowsTop(); - selected.index.value = (in && rowsPointY >= 0 && rowsPointY < shownRowsCount() * _rowHeight) ? (rowsPointY / _rowHeight) : -1; + selected.index.value = (in + && rowsPointY >= 0 + && rowsPointY < shownRowsCount() * _rowHeight) + ? (rowsPointY / _rowHeight) + : -1; if (selected.index.value >= 0) { - auto row = getRow(selected.index); - if (row->disabled()) { + const auto row = getRow(selected.index); + if (row->disabled() + || (customMode + && !_controller->customRowSelectionPoint( + row, + point.x(), + rowsPointY - (selected.index.value * _rowHeight)))) { selected = Selected(); - } else { + } else if (!customMode) { if (row->hasAction() && getActiveActionRect(row, selected.index).contains(point)) { selected.action = true; } @@ -1918,8 +2028,7 @@ void PeerListContent::updateRow(RowIndex index) { if (index.value < 0) { return; } - auto row = getRow(index); - if (row->disabled()) { + if (const auto row = getRow(index); row && row->disabled()) { if (index == _selected.index) { setSelected(Selected()); } @@ -2014,3 +2123,10 @@ PeerListContent::~PeerListContent() { _contextMenu->setDestroyedCallback(nullptr); } } + +void PeerListContentDelegate::peerListShowRowMenu( + not_null row, + bool highlightRow, + Fn)> destroyed) { + _content->showRowMenu(row, highlightRow, std::move(destroyed)); +} diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 0d6cbe9af..7f16c25af 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -82,7 +82,7 @@ public: return _id; } - [[nodiscard]] std::shared_ptr ensureUserpicView(); + [[nodiscard]] std::shared_ptr &ensureUserpicView(); [[nodiscard]] virtual QString generateName(); [[nodiscard]] virtual QString generateShortName(); @@ -133,7 +133,7 @@ public: bool actionSelected) { } - void refreshName(const style::PeerListItem &st); + virtual void refreshName(const style::PeerListItem &st); const Ui::Text::String &name() const { return _name; } @@ -172,7 +172,7 @@ public: template void setChecked( bool checked, - const style::RoundImageCheckbox &st, + const style::RoundImageCheckbox &st, anim::type animated, UpdateCallback callback) { if (checked && !_checkbox) { @@ -180,15 +180,21 @@ public: } setCheckedInternal(checked, animated); } + void setHidden(bool hidden) { + _hidden = hidden; + } + [[nodiscard]] bool hidden() const { + return _hidden; + } void finishCheckedAnimation(); void invalidatePixmapsCache(); - template + template void addRipple( const style::PeerListItem &st, - QSize size, + MaskGenerator &&maskGenerator, QPoint point, - UpdateCallback updateCallback); + UpdateCallback &&updateCallback); void stopLastRipple(); void paintRipple(Painter &p, int x, int y, int outerWidth); void paintUserpic( @@ -250,6 +256,7 @@ private: base::flat_set _nameFirstLetters; int _absoluteIndex = -1; State _disabledState = State::Active; + bool _hidden : 1; bool _initialized : 1; bool _isSearchResult : 1; bool _isSavedMessagesChat : 1; @@ -287,6 +294,7 @@ public: virtual void peerListConvertRowToSearchResult(not_null row) = 0; virtual bool peerListIsRowChecked(not_null row) = 0; virtual void peerListSetRowChecked(not_null row, bool checked) = 0; + virtual void peerListSetRowHidden(not_null row, bool hidden) = 0; virtual void peerListSetForeignRowChecked( not_null row, bool checked, @@ -317,6 +325,7 @@ public: virtual void peerListShowRowMenu( not_null row, + bool highlightRow, Fn)> destroyed = nullptr) = 0; virtual int peerListSelectedRowsCount() = 0; virtual std::unique_ptr peerListSaveState() const = 0; @@ -463,6 +472,25 @@ public: [[nodiscard]] virtual bool respectSavedMessagesChat() const { return false; } + [[nodiscard]] virtual int customRowHeight() { + Unexpected("PeerListController::customRowHeight."); + } + virtual void customRowPaint( + Painter &p, + crl::time now, + not_null row, + bool selected) { + Unexpected("PeerListController::customRowPaint."); + } + [[nodiscard]] virtual bool customRowSelectionPoint( + not_null row, + int x, + int y) { + Unexpected("PeerListController::customRowSelectionPoint."); + } + [[nodiscard]] virtual Fn customRowRippleMaskGenerator() { + Unexpected("PeerListController::customRowRippleMaskGenerator."); + } [[nodiscard]] virtual rpl::producer onlineCountValue() const; @@ -527,6 +555,12 @@ public: SkipResult selectSkip(int direction); void selectSkipPage(int height, int direction); + enum class Mode { + Default, + Custom, + }; + void setMode(Mode mode); + [[nodiscard]] rpl::producer selectedIndexValue() const; [[nodiscard]] bool hasSelection() const; [[nodiscard]] bool hasPressed() const; @@ -565,6 +599,9 @@ public: not_null row, bool checked, anim::type animated); + void setRowHidden( + not_null row, + bool hidden); template void reorderRows(ReorderCallback &&callback) { @@ -573,6 +610,9 @@ public: callback(searchEntity.second.begin(), searchEntity.second.end()); } refreshIndices(); + if (!_hiddenRows.empty()) { + callback(_filterResults.begin(), _filterResults.end()); + } update(); } @@ -581,6 +621,7 @@ public: void showRowMenu( not_null row, + bool highlightRow, Fn)> destroyed); auto scrollToRequests() const { @@ -668,10 +709,12 @@ private: bool showRowMenu( RowIndex index, + PeerListRow *row, QPoint globalPos, + bool highlightRow, Fn)> destroyed = nullptr); - crl::time paintRow(Painter &p, crl::time ms, RowIndex index); + crl::time paintRow(Painter &p, crl::time now, RowIndex index); void addRowEntry(not_null row); void addToSearchIndex(not_null row); @@ -679,7 +722,7 @@ private: void removeFromSearchIndex(not_null row); void setSearchQuery(const QString &query, const QString &normalizedQuery); bool showingSearch() const { - return !_searchQuery.isEmpty(); + return !_hiddenRows.empty() || !_searchQuery.isEmpty(); } int shownRowsCount() const { return showingSearch() ? _filterResults.size() : _rows.size(); @@ -701,6 +744,7 @@ private: not_null _controller; PeerListSearchMode _searchMode = PeerListSearchMode::Disabled; + Mode _mode = Mode::Default; int _rowHeight = 0; int _visibleTop = 0; int _visibleBottom = 0; @@ -724,6 +768,7 @@ private: QString _normalizedSearchQuery; QString _mentionHighlight; std::vector> _filterResults; + base::flat_set> _hiddenRows; int _aboveHeight = 0; int _belowHeight = 0; @@ -788,6 +833,11 @@ public: bool checked) override { _content->changeCheckState(row, checked, anim::type::normal); } + void peerListSetRowHidden( + not_null row, + bool hidden) override { + _content->setRowHidden(row, hidden); + } void peerListSetForeignRowChecked( not_null row, bool checked, @@ -858,10 +908,9 @@ public: _content->restoreState(std::move(state)); } void peerListShowRowMenu( - not_null row, - Fn)> destroyed = nullptr) override { - _content->showRowMenu(row, std::move(destroyed)); - } + not_null row, + bool highlightRow, + Fn)> destroyed = nullptr) override; protected: not_null content() const { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index bece3bebe..2af8a51d2 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -203,8 +203,10 @@ void SaveSlowmodeSeconds( void ShowEditPermissions( not_null navigation, not_null peer) { - const auto box = Ui::show( - Box(navigation, peer), + auto content = Box(navigation, peer); + const auto box = QPointer(content.data()); + navigation->parentController()->show( + std::move(content), Ui::LayerOption::KeepOther); const auto saving = box->lifetime().make_state(0); const auto save = [=]( @@ -246,8 +248,10 @@ void ShowEditPermissions( void ShowEditInviteLinks( not_null navigation, not_null peer) { - const auto box = Ui::show( - Box(navigation, peer), + auto content = Box(navigation, peer); + const auto box = QPointer(content.data()); + navigation->parentController()->show( + std::move(content), Ui::LayerOption::KeepOther); const auto saving = box->lifetime().make_state(0); const auto save = [=]( @@ -612,7 +616,7 @@ object_ptr Controller::createStickersEdit() { tr::lng_group_stickers_add(tr::now), st::editPeerInviteLinkButton) )->addClickHandler([=] { - Ui::show( + _navigation->parentController()->show( Box(_navigation->parentController(), channel), Ui::LayerOption::KeepOther); }); @@ -649,7 +653,7 @@ void Controller::showEditPeerTypeBox( _usernameSavedValue = publicLink; refreshHistoryVisibility(); }); - Ui::show( + _navigation->parentController()->show( Box( _peer, _channelHasLocationOriginalValue, @@ -681,7 +685,7 @@ void Controller::showEditLinkedChatBox() { || channel->canEditPreHistoryHidden())); if (const auto chat = *_linkedChatSavedValue) { - *box = Ui::show( + *box = _navigation->parentController()->show( EditLinkedChatBox( _navigation, channel, @@ -709,7 +713,7 @@ void Controller::showEditLinkedChatBox() { for (const auto &item : list) { chats.emplace_back(_peer->owner().processChat(item)); } - *box = Ui::show( + *box = _navigation->parentController()->show( EditLinkedChatBox( _navigation, channel, @@ -858,7 +862,7 @@ void Controller::fillHistoryVisibilityButton() { _historyVisibilitySavedValue = checked; }); const auto buttonCallback = [=] { - Ui::show( + _navigation->parentController()->show( Box( _peer, boxCallback, @@ -1023,9 +1027,15 @@ void Controller::fillManageSection() { wrap->entity(), tr::lng_manage_peer_invite_links(), rpl::duplicate(count) | ToPositiveNumberString(), - [=] { Ui::show( - Box(ManageInviteLinksBox, _peer, _peer->session().user(), 0, 0), - Ui::LayerOption::KeepOther); + [=] { + _navigation->parentController()->show( + Box( + ManageInviteLinksBox, + _peer, + _peer->session().user(), + 0, + 0), + Ui::LayerOption::KeepOther); }, st::infoIconInviteLinks); @@ -1520,7 +1530,7 @@ void Controller::deleteWithConfirmation() { const auto deleteCallback = crl::guard(this, [=] { deleteChannel(); }); - Ui::show( + _navigation->parentController()->show( Box( text, tr::lng_box_delete(tr::now), diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 8558fd126..001c62434 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -133,8 +133,8 @@ QImage QrExact(const Qr::Data &data, int pixel, QColor color) { skip, skip, Intro::details::TelegramLogoImage().scaled( - logoSize, - logoSize, + logoSize * cIntRetinaFactor(), + logoSize * cIntRetinaFactor(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 5d2bb6ef9..093c5c5ba 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -533,7 +533,7 @@ void LinksController::rowClicked(not_null row) { } void LinksController::rowActionClicked(not_null row) { - delegate()->peerListShowRowMenu(row, nullptr); + delegate()->peerListShowRowMenu(row, true); } base::unique_qptr LinksController::rowContextMenu( diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index cc4662345..f5fc45e5e 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1002,7 +1002,7 @@ void SendFilesBox::sendScheduled() { ? SendMenu::Type::ScheduledToUser : _sendMenuType; const auto callback = [=](Api::SendOptions options) { send(options); }; - Ui::show( + _controller->show( HistoryView::PrepareScheduleBox(this, type, callback), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 663f92a16..b1a2dd732 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -65,12 +65,14 @@ public: bool loaded() const; bool notInstalled() const; bool official() const; - rpl::producer title() const; - QString shortName() const; + [[nodiscard]] rpl::producer title() const; + [[nodiscard]] QString shortName() const; void install(); - rpl::producer setInstalled() const; - rpl::producer<> updateControls() const; + [[nodiscard]] rpl::producer setInstalled() const; + [[nodiscard]] rpl::producer<> updateControls() const; + + [[nodiscard]] rpl::producer errors() const; ~Inner(); @@ -139,6 +141,7 @@ private: rpl::event_stream _setInstalled; rpl::event_stream<> _updateControls; + rpl::event_stream _errors; }; @@ -155,7 +158,7 @@ QPointer StickerSetBox::Show( not_null document) { if (const auto sticker = document->sticker()) { if (sticker->set.type() != mtpc_inputStickerSetEmpty) { - return Ui::show( + return controller->show( Box(controller, sticker->set), Ui::LayerOption::KeepOther).data(); } @@ -188,6 +191,11 @@ void StickerSetBox::prepare() { _controller->session().api().stickerSetInstalled(setId); closeBox(); }, lifetime()); + + _inner->errors( + ) | rpl::start_with_next([=](Error error) { + handleError(error); + }, lifetime()); } void StickerSetBox::addStickers() { @@ -208,6 +216,52 @@ void StickerSetBox::copyTitle() { }, lifetime()); } +void StickerSetBox::handleError(Error error) { + const auto guard = gsl::finally(crl::guard(this, [=] { + closeBox(); + })); + + switch (error) { + case Error::NotFound: + _controller->show( + Box(tr::lng_stickers_not_found(tr::now))); + break; + default: Unexpected("Error in StickerSetBox::handleError."); + } +} + +void StickerSetBox::archiveStickers() { + const auto weak = base::make_weak(_controller.get()); + const auto setId = _set.c_inputStickerSetID().vid().v; + _controller->session().api().request(MTPmessages_InstallStickerSet( + _set, + MTP_boolTrue() + )).done([=](const MTPmessages_StickerSetInstallResult &result) { + const auto controller = weak.get(); + if (!controller) { + return; + } + if (result.type() == mtpc_messages_stickerSetInstallResultSuccess) { + Ui::Toast::Show(tr::lng_stickers_has_been_archived(tr::now)); + + const auto &session = controller->session(); + auto &order = session.data().stickers().setsOrderRef(); + const auto index = order.indexOf(setId); + if (index == -1) { + return; + } + order.removeAt(index); + + session.local().writeInstalledStickers(); + session.local().writeArchivedStickers(); + + session.data().stickers().notifyUpdated(); + } + }).fail([](const MTP::Error &error) { + Ui::Toast::Show(Lang::Hard::ServerError()); + }).send(); +} + void StickerSetBox::updateTitleAndButtons() { setTitle(_inner->title()); updateButtons(); @@ -252,6 +306,26 @@ void StickerSetBox::updateButtons() { }; addButton(tr::lng_stickers_share_pack(), std::move(share)); addButton(tr::lng_cancel(), [=] { closeBox(); }); + + /* + if (!_inner->shortName().isEmpty()) { + const auto top = addTopButton(st::infoTopBarMenu); + const auto archive = [=] { + archiveStickers(); + closeBox(); + }; + const auto menu = + std::make_shared>(); + top->setClickedCallback([=] { + *menu = base::make_unique_q(top); + (*menu)->addAction( + tr::lng_stickers_archive_pack(tr::now), + archive); + (*menu)->popup(QCursor::pos()); + return true; + }); + } + */ } } else { addButton(tr::lng_cancel(), [=] { closeBox(); }); @@ -291,6 +365,14 @@ bool StickerSetBox::showMenu(not_null button) { _menu->addAction(tr::lng_stickers_share_pack(tr::now), [=] { copyStickersLink(); }); } + if (!_inner->notInstalled()) { + const auto archive = [=] { + archiveStickers(); + closeBox(); + }; + _menu->addAction(tr::lng_stickers_archive_pack(tr::now), archive); + } + const auto parentTopLeft = window()->mapToGlobal({ 0, 0 }); const auto buttonTopLeft = button->mapToGlobal({ 0, 0 }); const auto parentRect = QRect(parentTopLeft, window()->size()); @@ -333,7 +415,7 @@ StickerSetBox::Inner::Inner( gotSet(result); }).fail([=](const MTP::Error &error) { _loaded = true; - Ui::show(Box(tr::lng_stickers_not_found(tr::now))); + _errors.fire(Error::NotFound); }).send(); _controller->session().api().updateStickers(); @@ -428,7 +510,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { }); if (_pack.isEmpty()) { - Ui::show(Box(tr::lng_stickers_not_found(tr::now))); + _errors.fire(Error::NotFound); return; } else { int32 rows = _pack.size() / kStickersPanelPerRow + ((_pack.size() % kStickersPanelPerRow) ? 1 : 0); @@ -448,6 +530,10 @@ rpl::producer<> StickerSetBox::Inner::updateControls() const { return _updateControls.events(); } +rpl::producer StickerSetBox::Inner::errors() const { + return _errors.events(); +} + void StickerSetBox::Inner::installDone( const MTPmessages_StickerSetInstallResult &result) { auto &sets = _controller->session().data().stickers().setsRef(); @@ -804,7 +890,7 @@ QString StickerSetBox::Inner::shortName() const { void StickerSetBox::Inner::install() { if (isMasksSet()) { - Ui::show( + _controller->show( Box(tr::lng_stickers_masks_pack(tr::now)), Ui::LayerOption::KeepOther); return; @@ -817,7 +903,7 @@ void StickerSetBox::Inner::install() { )).done([=](const MTPmessages_StickerSetInstallResult &result) { installDone(result); }).fail([=](const MTP::Error &error) { - Ui::show(Box(tr::lng_stickers_not_found(tr::now))); + _errors.fire(Error::NotFound); }).send(); } diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index c0c3e5ac8..6c15316ae 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -39,12 +39,18 @@ protected: void resizeEvent(QResizeEvent *e) override; private: + enum class Error { + NotFound, + }; + void updateTitleAndButtons(); void updateButtons(); bool showMenu(not_null button); void addStickers(); void copyStickersLink(); void copyTitle(); + void archiveStickers(); + void handleError(Error error); const not_null _controller; MTPInputStickerSet _set; diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 6100e2f62..27294ebc8 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -68,9 +68,7 @@ private: }; // This class is hold in header because it requires Qt preprocessing. -class StickersBox::Inner - : public Ui::RpWidget - , private base::Subscriber { +class StickersBox::Inner : public Ui::RpWidget { public: using Section = StickersBox::Section; @@ -85,7 +83,9 @@ public: [[nodiscard]] Main::Session &session() const; - base::Observable scrollToY; + rpl::producer scrollsToY() const { + return _scrollsToY.events(); + } void setInnerFocus(); void saveGroupSet(); @@ -276,6 +276,8 @@ private: int _above = -1; rpl::event_stream _draggingScrollDelta; + rpl::event_stream _scrollsToY; + int _minHeight = 0; int _scrollbar = 0; @@ -387,9 +389,10 @@ StickersBox::StickersBox( , _section(Section::Installed) , _installed(0, this, controller, megagroup) , _megagroupSet(megagroup) { - subscribe(_installed.widget()->scrollToY, [=](int y) { + _installed.widget()->scrollsToY( + ) | rpl::start_with_next([=](int y) { onScrollToY(y); - }); + }, lifetime()); } StickersBox::StickersBox( @@ -1589,7 +1592,7 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { }(); const auto showSetByRow = [&](const Row &row) { setSelected(SelectedRow()); - Ui::show( + _controller->show( Box(_controller, row.set->mtpInput()), Ui::LayerOption::KeepOther); }; @@ -1898,7 +1901,7 @@ void StickersBox::Inner::rebuild() { void StickersBox::Inner::setMegagroupSelectedSet(const MTPInputStickerSet &set) { _megagroupSetInput = set; rebuild(); - scrollToY.notify(0, true); + _scrollsToY.fire(0); updateSelected(); } diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index b2201c1f9..19a4fca8e 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -175,6 +175,87 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) { } callBottomShadowSize: 124px; +CallMuteButton { + active: CallButton; + muted: CallButton; + labelAdditional: pixels; + sublabel: FlatLabel; + labelsSkip: pixels; + sublabelSkip: pixels; + lottieSize: size; + lottieTop: pixels; +} + +callMuteButtonLabel: FlatLabel(defaultFlatLabel) { + textFg: groupCallMembersFg; + style: TextStyle(defaultTextStyle) { + font: font(14px); + linkFont: font(14px); + linkFontOver: font(14px underline); + } +} +callMuteButtonActiveInner: IconButton { + width: 112px; + height: 138px; +} +callMuteButtonSmallActiveInner: IconButton { + width: 68px; + height: 68px; +} +callMuteButtonActive: CallButton { + button: callMuteButtonActiveInner; + bg: groupCallLive1; + bgSize: 77px; + bgPosition: point(18px, 18px); + outerRadius: 18px; + outerBg: callAnswerBgOuter; + label: callMuteButtonLabel; +} +callMuteButton: CallMuteButton { + active: callMuteButtonActive; + muted: CallButton(callMuteButtonActive) { + bg: groupCallMuted1; + label: callMuteButtonLabel; + } + labelAdditional: 5px; + sublabel: FlatLabel(defaultFlatLabel) { + textFg: groupCallMemberNotJoinedStatus; + } + labelsSkip: 8px; + sublabelSkip: 14px; + lottieSize: size(54px, 54px); + lottieTop: 31px; +} +callMuteButtonSmallActive: CallButton(callMuteButtonActive) { + button: callMuteButtonSmallActiveInner; + bgSize: 42px; + bgPosition: point(13px, 13px); + outerRadius: 13px; + label: callButtonLabel; +} +callMuteButtonSmall: CallMuteButton(callMuteButton) { + active: callMuteButtonSmallActive; + muted: CallButton(callMuteButtonSmallActive) { + bg: groupCallMuted1; + label: callButtonLabel; + } + labelsSkip: 0px; + sublabelSkip: 0px; + lottieSize: size(36px, 36px); + lottieTop: 17px; +} + +callMuteMinorBlobMinRadius: 64px; +callMuteMinorBlobMaxRadius: 74px; +callMuteMajorBlobMinRadius: 67px; +callMuteMajorBlobMaxRadius: 77px; +callMuteBlobRadiusForDiameter: 100px; + +callConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { + color: lightButtonFg; + thickness: 4px; +} + callName: FlatLabel(defaultFlatLabel) { minWidth: 260px; maxHeight: 30px; @@ -208,55 +289,6 @@ callRemoteAudioMute: FlatLabel(callStatus) { } callRemoteAudioMuteSkip: 12px; -callMuteMainBlobMinRadius: 57px; -callMuteMainBlobMaxRadius: 63px; -callMuteMinorBlobMinRadius: 64px; -callMuteMinorBlobMaxRadius: 74px; -callMuteMajorBlobMinRadius: 67px; -callMuteMajorBlobMaxRadius: 77px; - -callMuteButtonActiveInner: IconButton { - width: 136px; - height: 165px; -} -callMuteButtonLabel: FlatLabel(defaultFlatLabel) { - textFg: groupCallMembersFg; - style: TextStyle(defaultTextStyle) { - font: font(14px); - linkFont: font(14px); - linkFontOver: font(14px underline); - } -} -callMuteButtonSublabel: FlatLabel(defaultFlatLabel) { - textFg: groupCallMemberNotJoinedStatus; -} -callMuteButtonLabelsSkip: 5px; -callMuteButtonSublabelSkip: 19px; -callMuteButtonActive: CallButton { - button: callMuteButtonActiveInner; - bg: groupCallLive1; - bgSize: 100px; - bgPosition: point(18px, 18px); - outerRadius: 18px; - outerBg: callAnswerBgOuter; - label: callMuteButtonLabel; -} -callMuteButtonMuted: CallButton(callMuteButtonActive) { - bg: groupCallMuted1; - label: callMuteButtonLabel; -} -callMuteButtonConnecting: CallButton(callMuteButtonMuted) { - bg: callIconBg; - label: callMuteButtonLabel; -} -callMuteButtonLabelAdditional: 5px; - -callConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) { - color: lightButtonFg; - thickness: 4px; - size: size(100px, 100px); -} - callBarHeight: 38px; callBarMuteToggle: IconButton { width: 41px; @@ -433,7 +465,8 @@ callTitle: WindowTitle(defaultWindowTitle) { closeIconActive: callTitleCloseIcon; closeIconActiveOver: callTitleCloseIconOver; } -callTitleShadow: icon {{ "calls/calls_shadow_controls", windowShadowFg }}; +callTitleShadowRight: icon {{ "calls/calls_shadow_controls", windowShadowFg }}; +callTitleShadowLeft: icon {{ "calls/calls_shadow_controls-flip_horizontal", windowShadowFg }}; callErrorToast: Toast(defaultToast) { minWidth: 240px; @@ -442,8 +475,6 @@ callErrorToast: Toast(defaultToast) { groupCallWidth: 380px; groupCallHeight: 580px; -groupCallMuteButtonIconSize: size(67px, 67px); -groupCallMuteButtonIconTop: 35px; groupCallRipple: RippleAnimation(defaultRippleAnimation) { color: groupCallMembersBgRipple; } @@ -459,6 +490,7 @@ groupCallMenu: Menu(defaultMenu) { itemFgShortcutDisabled: groupCallMemberNotJoinedStatus; separatorFg: groupCallMenuBgOver; + separatorPadding: margins(0px, 4px, 0px, 4px); arrow: icon {{ "dropdown_submenu_arrow", groupCallMemberNotJoinedStatus }}; @@ -482,6 +514,16 @@ groupCallPopupMenu: PopupMenu(defaultPopupMenu) { menu: groupCallMenu; animation: groupCallPanelAnimation; } +groupCallPopupMenuWithVolume: PopupMenu(groupCallPopupMenu) { + scrollPadding: margins(0px, 3px, 0px, 8px); + menu: Menu(groupCallMenu) { + widthMin: 210px; + } +} +groupCallPopupVolumeMenu: Menu(groupCallMenu) { + widthMin: 210px; + itemBgOver: groupCallMenuBg; +} groupCallRecordingTimerPadding: margins(0px, 4px, 0px, 4px); groupCallRecordingTimerFont: font(12px); @@ -532,6 +574,11 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) { statusFgOver: groupCallMemberInactiveStatus; statusFgActive: groupCallMemberActiveStatus; } +groupCallNarrowMembersListItem: PeerListItem(groupCallMembersListItem) { + statusFg: groupCallMemberNotJoinedStatus; + statusFgOver: groupCallMemberNotJoinedStatus; + statusFgActive: groupCallMemberActiveStatus; +} groupCallMembersList: PeerList(defaultPeerList) { bg: groupCallMembersBg; about: FlatLabel(defaultPeerListAbout) { @@ -648,8 +695,8 @@ groupCallShareBoxList: PeerList(groupCallMembersList) { groupCallMembersTop: 51px; groupCallTitleTop: 8px; groupCallSubtitleTop: 26px; +groupCallWideVideoTop: 24px; -groupCallMembersMargin: margins(16px, 16px, 16px, 28px); groupCallAddMember: SettingsButton(defaultSettingsButton) { textFg: groupCallMemberNotJoinedStatus; textFgOver: groupCallMemberNotJoinedStatus; @@ -678,7 +725,7 @@ groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) { } } groupCallAddButtonPosition: point(10px, 7px); -groupCallMembersWidthMax: 360px; +groupCallMembersWidthMax: 480px; groupCallRecordingMark: 6px; groupCallRecordingMarkSkip: 4px; groupCallRecordingMarkTop: 8px; @@ -704,6 +751,7 @@ groupCallJoinAsToggle: UserpicButton(defaultUserpicButton) { photoPosition: point(3px, 3px); } groupCallMenuPosition: point(-1px, 29px); +groupCallWideMenuPosition: point(-2px, 28px); groupCallActiveButton: IconButton { width: 36px; @@ -736,33 +784,106 @@ groupCallMemberRaisedHand: icon {{ "calls/group_calls_raised_hand", groupCallMem groupCallSettingsInner: IconButton(callButton) { iconPosition: point(-1px, 22px); - icon: icon {{ "calls/call_settings", groupCallIconFg }}; + icon: icon {{ "calls/calls_settings", groupCallIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callMuteRipple; } } +groupCallShareInner: IconButton(groupCallSettingsInner) { + icon: icon {{ "calls/group_calls_share", groupCallIconFg }}; +} +groupCallVideoInner: IconButton(groupCallSettingsInner) { + icon: icon {{ "calls/call_camera_muted", groupCallIconFg }}; + iconPosition: point(-1px, 16px); +} +groupCallHangupInner: IconButton(callButton) { + icon: icon {{ "calls/call_discard", groupCallIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: groupCallLeaveBgRipple; + } +} groupCallSettings: CallButton(callMicrophoneMute) { button: groupCallSettingsInner; } groupCallShare: CallButton(groupCallSettings) { - button: IconButton(groupCallSettingsInner) { - icon: icon {{ "calls/group_calls_share", groupCallIconFg }}; - } + button: groupCallShareInner; +} +groupCallVideo: CallButton(groupCallSettings) { + button: groupCallVideoInner; +} +groupCallVideoInnerActive: IconButton(groupCallVideoInner) { + icon: icon {{ "calls/call_camera_active", groupCallIconFg }}; +} +groupCallVideoActive: CallButton(groupCallVideo) { + button: groupCallVideoInnerActive; } groupCallHangup: CallButton(callHangup) { - button: IconButton(callButton) { - icon: icon {{ "calls/call_discard", groupCallIconFg }}; - ripple: RippleAnimation(defaultRippleAnimation) { - color: groupCallLeaveBgRipple; - } - } + button: groupCallHangupInner; bg: groupCallLeaveBg; outerBg: groupCallLeaveBg; label: callButtonLabel; } -groupCallButtonSkip: 43px; -groupCallButtonBottomSkip: 145px; -groupCallMuteBottomSkip: 160px; +groupCallSettingsSmall: CallButton(groupCallSettings) { + button: IconButton(groupCallSettingsInner) { + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } + bgPosition: point(8px, 12px); +} +groupCallHangupSmall: CallButton(groupCallHangup) { + button: IconButton(groupCallHangupInner) { + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } + bgPosition: point(8px, 12px); +} +groupCallVideoSmall: CallButton(groupCallSettingsSmall) { + button: IconButton(groupCallVideoInner) { + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } +} +groupCallVideoActiveSmall: CallButton(groupCallVideoSmall) { + button: IconButton(groupCallVideoInnerActive) { + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } +} +groupCallScreenShareSmall: CallButton(groupCallSettingsSmall) { + button: IconButton(groupCallSettingsInner) { + icon: icon {{ "calls/calls_present", groupCallIconFg }}; + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } +} +groupCallMenuToggleSmall: CallButton(groupCallSettingsSmall) { + button: IconButton(groupCallSettingsInner) { + icon: icon {{ "calls/calls_more", groupCallIconFg }}; + width: 60px; + height: 68px; + rippleAreaPosition: point(8px, 12px); + } +} +groupCallButtonSkip: 40px; +groupCallButtonSkipSmall: 5px; +groupCallButtonBottomSkip: 113px; +groupCallButtonBottomSkipSmall: 95px; +groupCallButtonBottomSkipWide: 108px; +groupCallControlsBackMargin: margins(10px, 0px, 10px, 0px); +groupCallControlsBackRadius: 12px; +groupCallMuteBottomSkip: 116px; + +groupCallMembersMargin: margins(16px, 16px, 16px, 60px); +groupCallMembersTopSkip: 6px; +groupCallMembersBottomSkip: 80px; +groupCallMembersShadowHeight: 160px; +groupCallMembersFadeSkip: 10px; +groupCallMembersFadeHeight: 100px; groupCallTopBarUserpics: GroupCallUserpics { size: 28px; @@ -780,17 +901,18 @@ groupCallTopBarOpen: RoundButton(groupCallTopBarJoin) { color: shadowFg; } } -groupCallBox: Box(defaultBox) { - button: RoundButton(defaultBoxButton) { - textFg: groupCallActiveFg; - textFgOver: groupCallActiveFg; - numbersTextFg: groupCallActiveFg; - numbersTextFgOver: groupCallActiveFg; - textBg: groupCallMembersBg; - textBgOver: groupCallMembersBgOver; +groupCallBoxButton: RoundButton(defaultBoxButton) { + textFg: groupCallActiveFg; + textFgOver: groupCallActiveFg; + numbersTextFg: groupCallActiveFg; + numbersTextFgOver: groupCallActiveFg; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; - ripple: groupCallRipple; - } + ripple: groupCallRipple; +} +groupCallBox: Box(defaultBox) { + button: groupCallBoxButton; margin: margins(0px, 56px, 0px, 10px); bg: groupCallMembersBg; title: FlatLabel(boxTitle) { @@ -807,7 +929,7 @@ groupCallLevelMeter: LevelMeter(defaultLevelMeter) { lineSpacing: 5px; lineCount: 44; activeFg: groupCallActiveFg; - inactiveFg: groupCallMemberNotJoinedStatus; + inactiveFg: groupCallMembersBgRipple; } groupCallCheckboxIcon: icon {{ "default_checkbox_check", groupCallMembersFg, point(4px, 7px) }}; groupCallCheck: Check(defaultCheck) { @@ -960,13 +1082,17 @@ groupCallMuteCrossLine: CrossLineAnimation { groupCallMenuSpeakerArcsSkip: 1px; groupCallMenuVolumeSkip: 5px; +groupCallMenuVolumePadding: margins(17px, 6px, 17px, 5px); +groupCallMenuVolumeMargin: margins(55px, 0px, 15px, 0px); groupCallMenuVolumeSlider: MediaSlider(defaultContinuousSlider) { activeFg: groupCallMembersFg; - inactiveFg: groupCallMemberInactiveIcon; + inactiveFg: groupCallMembersBgOver; activeFgOver: groupCallMembersFg; - inactiveFgOver: groupCallMemberInactiveIcon; - activeFgDisabled: groupCallMemberInactiveIcon; - receivedTillFg: groupCallMemberInactiveIcon; + inactiveFgOver: groupCallMembersBgOver; + activeFgDisabled: groupCallMembersBgOver; + receivedTillFg: groupCallMembersBgOver; + width: 7px; + seekSize: size(7px, 7px); } groupCallSpeakerArcsAnimation: ArcsAnimation { @@ -1015,3 +1141,141 @@ groupCallStartsInTop: 10px; groupCallStartsWhenTop: 160px; groupCallCountdownFont: font(64px semibold); groupCallCountdownTop: 52px; + +desktopCaptureMargins: margins(12px, 8px, 12px, 6px); +desktopCaptureSourceSize: size(235px, 165px); +desktopCaptureSourceSkips: size(2px, 10px); +desktopCaptureSourceTitle: WindowTitle(groupCallTitle) { + bg: groupCallMembersBgOver; + bgActive: groupCallMembersBgOver; + height: 21px; +} +desktopCapturePadding: margins(7px, 7px, 7px, 33px); +desktopCaptureLabelBottom: 7px; +desktopCaptureLabel: FlatLabel(defaultFlatLabel) { + minWidth: 200px; + maxHeight: 20px; + textFg: groupCallMembersFg; + style: semiboldTextStyle; +} +desktopCaptureCancel: RoundButton(defaultBoxButton) { + textFg: groupCallActiveFg; + textFgOver: groupCallActiveFg; + numbersTextFg: groupCallActiveFg; + numbersTextFgOver: groupCallActiveFg; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; + + ripple: groupCallRipple; +} +desktopCaptureFinish: RoundButton(desktopCaptureCancel) { + textFg: groupCallMemberMutedIcon; + textFgOver: groupCallMemberMutedIcon; +} +desktopCaptureSubmit: RoundButton(desktopCaptureCancel) { + textFg: groupCallIconFg; + textFgOver: groupCallIconFg; + numbersTextFg: groupCallIconFg; + numbersTextFgOver: groupCallIconFg; + textBg: groupCallMuted1; + textBgOver: groupCallMuted1; + + ripple: RippleAnimation(groupCallRipple) { + color: shadowFg; + } +} + +groupCallNarrowSkip: 9px; +groupCallNarrowMembersWidth: 204px; +groupCallNarrowVideoHeight: 120px; +groupCallWideModeWidthMin: 600px; +groupCallWideModeSize: size(960px, 580px); +groupCallNarrowInactiveCrossLine: CrossLineAnimation { + fg: groupCallMemberNotJoinedStatus; + icon: icon {{ "calls/video_mini_mute", groupCallMemberNotJoinedStatus }}; + startPosition: point(3px, 0px); + endPosition: point(13px, 12px); + stroke: 3px; + strokeDenominator: 2; +} +groupCallNarrowColoredCrossLine: CrossLineAnimation(groupCallNarrowInactiveCrossLine) { + fg: groupCallMemberNotJoinedStatus; + icon: icon {{ "calls/video_mini_mute", groupCallMemberActiveStatus }}; +} +groupCallNarrowRaisedHand: icon {{ "calls/video_mini_speak", groupCallMemberInactiveStatus }}; +groupCallNarrowCameraIcon: icon {{ "calls/video_mini_video", groupCallMemberNotJoinedStatus }}; +groupCallNarrowScreenIcon: icon {{ "calls/video_mini_screencast", groupCallMemberNotJoinedStatus }}; +groupCallNarrowIconPosition: point(-4px, 2px); +groupCallNarrowIconSkip: 15px; +groupCallOutline: 2px; +groupCallVideoCrossLine: CrossLineAnimation(groupCallMemberColoredCrossLine) { + fg: groupCallVideoTextFg; + icon: icon {{ "calls/video_over_mute", groupCallVideoTextFg }}; +} + +GroupCallVideoTile { + shadowHeight: pixels; + namePosition: point; + pin: CrossLineAnimation; + pinPosition: point; + pinPadding: margins; + pinTextPosition: point; + back: icon; + iconPosition: point; +} + +groupCallVideoTile: GroupCallVideoTile { + shadowHeight: 40px; + namePosition: point(15px, 8px); + pin: CrossLineAnimation { + fg: groupCallVideoTextFg; + icon: icon {{ "calls/video_over_pin", groupCallVideoTextFg }}; + startPosition: point(7px, 4px); + endPosition: point(17px, 14px); + stroke: 3px; + strokeDenominator: 2; + } + pinPosition: point(18px, 18px); + pinPadding: margins(6px, 2px, 12px, 1px); + pinTextPosition: point(1px, 3px); + back: icon {{ "calls/video_back", groupCallVideoTextFg }}; + iconPosition: point(10px, 5px); +} + +groupCallVideoSmallSkip: 4px; +groupCallVideoLargeSkip: 6px; +groupCallVideoPlaceholderHeight: 212px; +groupCallVideoPlaceholderIconTop: 50px; +groupCallVideoPlaceholderTextTop: 120px; + +groupCallTooltip: Tooltip(defaultTooltip) { + textBg: groupCallMembersBg; + textFg: groupCallMembersFg; + textBorder: groupCallMembersBgOver; +} +groupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) { + bg: importantTooltipBg; + padding: margins(10px, 3px, 10px, 5px); + radius: 4px; + arrow: 4px; +} +groupCallNiceTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { + style: TextStyle(defaultTextStyle) { + font: font(11px); + linkFont: font(11px); + linkFontOver: font(11px underline); + } +} +groupCallStickedTooltip: ImportantTooltip(groupCallNiceTooltip) { + padding: margins(10px, 1px, 6px, 3px); +} +groupCallStickedTooltipClose: IconButton(defaultIconButton) { + width: 20px; + height: 20px; + iconPosition: point(4px, 3px); + icon: icon {{ "calls/video_tooltip", importantTooltipFg }}; + iconOver: icon {{ "calls/video_tooltip", importantTooltipFg }}; + ripple: emptyRippleAnimation; +} +groupCallNiceTooltipTop: 4px; +groupCallPaused: icon {{ "calls/video_large_paused", groupCallVideoTextFg }}; diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index 19e0c6a7b..6ff9075e9 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -357,7 +357,7 @@ base::unique_qptr BoxController::rowContextMenu( auto result = base::make_unique_q(parent); result->addAction(tr::lng_context_delete_selected(tr::now), [=] { - Ui::show( + _window->show( Box(session, base::duplicate(ids)), Ui::LayerOption::KeepOther); }); diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index cbf41b004..41b5beb92 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -28,7 +28,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webrtc/webrtc_create_adm.h" #include "data/data_user.h" #include "data/data_session.h" -#include "facades.h" #include #include @@ -161,8 +160,12 @@ Call::Call( , _user(user) , _api(&_user->session().mtp()) , _type(type) -, _videoIncoming(std::make_unique(StartVideoState(video))) -, _videoOutgoing(std::make_unique(StartVideoState(video))) { +, _videoIncoming( + std::make_unique( + StartVideoState(video))) +, _videoOutgoing( + std::make_unique( + StartVideoState(video))) { _discardByTimeoutTimer.setCallback([=] { hangup(); }); if (_type == Type::Outgoing) { @@ -380,7 +383,7 @@ void Call::setupOutgoingVideo() { // Paused not supported right now. Assert(state == Webrtc::VideoState::Active); if (!_videoCapture) { - _videoCapture = _delegate->getVideoCapture(); + _videoCapture = _delegate->callGetVideoCapture(); _videoCapture->setOutput(_videoOutgoing->sink()); } if (_instance) { @@ -801,16 +804,19 @@ void Call::createAndStartController(const MTPDphoneCall &call) { AppendServer(descriptor.rtcServers, connection); } - if (Global::UseProxyForCalls() - && (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled)) { - const auto &selected = Global::SelectedProxy(); - if (selected.supportsCalls() && !selected.host.isEmpty()) { - Assert(selected.type == MTP::ProxyData::Type::Socks5); - descriptor.proxy = std::make_unique(); - descriptor.proxy->host = selected.host.toStdString(); - descriptor.proxy->port = selected.port; - descriptor.proxy->login = selected.user.toStdString(); - descriptor.proxy->password = selected.password.toStdString(); + { + auto &settingsProxy = Core::App().settings().proxy(); + using ProxyData = MTP::ProxyData; + if (settingsProxy.useProxyForCalls() && settingsProxy.isEnabled()) { + const auto &selected = settingsProxy.selected(); + if (selected.supportsCalls() && !selected.host.isEmpty()) { + Assert(selected.type == ProxyData::Type::Socks5); + descriptor.proxy = std::make_unique(); + descriptor.proxy->host = selected.host.toStdString(); + descriptor.proxy->port = selected.port; + descriptor.proxy->login = selected.user.toStdString(); + descriptor.proxy->password = selected.password.toStdString(); + } } } diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index b84f5c51f..fec90fc4a 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -53,6 +53,11 @@ struct Error { QString details; }; +enum class CallType { + Incoming, + Outgoing, +}; + class Call : public base::has_weak_ptr { public: class Delegate { @@ -72,7 +77,7 @@ public: Fn onSuccess, bool video) = 0; - virtual auto getVideoCapture() + virtual auto callGetVideoCapture() -> std::shared_ptr = 0; virtual ~Delegate() = default; @@ -81,11 +86,12 @@ public: static constexpr auto kSoundSampleMs = 100; - enum class Type { - Incoming, - Outgoing, - }; - Call(not_null delegate, not_null user, Type type, bool video); + using Type = CallType; + Call( + not_null delegate, + not_null user, + Type type, + bool video); [[nodiscard]] Type type() const { return _type; diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp deleted file mode 100644 index 8edb10426..000000000 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ /dev/null @@ -1,1792 +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 "calls/calls_group_call.h" - -#include "calls/calls_group_common.h" -#include "main/main_session.h" -#include "api/api_send_progress.h" -#include "api/api_updates.h" -#include "apiwrap.h" -#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 "base/unixtime.h" -#include "core/application.h" -#include "core/core_settings.h" -#include "data/data_changes.h" -#include "data/data_user.h" -#include "data/data_chat.h" -#include "data/data_channel.h" -#include "data/data_group_call.h" -#include "data/data_session.h" -#include "base/global_shortcuts.h" -#include "base/openssl_help.h" -#include "webrtc/webrtc_media_devices.h" -#include "webrtc/webrtc_create_adm.h" - -#include -#include - -#include -#include -#include - -namespace Calls { -namespace { - -constexpr auto kMaxInvitePerSlice = 10; -constexpr auto kCheckLastSpokeInterval = crl::time(1000); -constexpr auto kCheckJoinedTimeout = 4 * crl::time(1000); -constexpr auto kUpdateSendActionEach = crl::time(500); -constexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000); - -[[nodiscard]] std::unique_ptr CreateMediaDevices() { - const auto &settings = Core::App().settings(); - return Webrtc::CreateMediaDevices( - settings.callInputDeviceId(), - settings.callOutputDeviceId(), - settings.callVideoInputDeviceId()); -} - -[[nodiscard]] const Data::GroupCall::Participant *LookupParticipant( - not_null peer, - uint64 id, - not_null participantPeer) { - const auto call = peer->groupCall(); - if (!id || !call || call->id() != id) { - return nullptr; - } - const auto &participants = call->participants(); - const auto i = ranges::find( - participants, - participantPeer, - &Data::GroupCall::Participant::peer); - return (i != end(participants)) ? &*i : nullptr; -} - -[[nodiscard]] double TimestampFromMsgId(mtpMsgId msgId) { - return msgId / double(1ULL << 32); -} - -} // namespace - -class GroupCall::LoadPartTask final : public tgcalls::BroadcastPartTask { -public: - LoadPartTask( - base::weak_ptr call, - int64 time, - int64 period, - Fn done); - - [[nodiscard]] int64 time() const { - return _time; - } - [[nodiscard]] int32 scale() const { - return _scale; - } - - void done(tgcalls::BroadcastPart &&part); - void cancel() override; - -private: - const base::weak_ptr _call; - const int64 _time = 0; - const int32 _scale = 0; - Fn _done; - QMutex _mutex; - -}; - -[[nodiscard]] bool IsGroupCallAdmin( - not_null peer, - not_null participantPeer) { - const auto user = participantPeer->asUser(); - if (!user) { - return false; - } - if (const auto chat = peer->asChat()) { - return chat->admins.contains(user) - || (chat->creator == peerToUser(user->id)); - } else if (const auto group = peer->asChannel()) { - if (const auto mgInfo = group->mgInfo.get()) { - if (mgInfo->creator == user) { - return true; - } - const auto i = mgInfo->lastAdmins.find(user); - if (i == mgInfo->lastAdmins.end()) { - return false; - } - const auto &rights = i->second.rights; - return rights.c_chatAdminRights().is_manage_call(); - } - } - return false; -} - -GroupCall::LoadPartTask::LoadPartTask( - base::weak_ptr call, - int64 time, - int64 period, - Fn done) -: _call(std::move(call)) -, _time(time ? time : (base::unixtime::now() * int64(1000))) -, _scale([&] { - switch (period) { - case 1000: return 0; - case 500: return 1; - case 250: return 2; - case 125: return 3; - } - Unexpected("Period in LoadPartTask."); -}()) -, _done(std::move(done)) { -} - -void GroupCall::LoadPartTask::done(tgcalls::BroadcastPart &&part) { - QMutexLocker lock(&_mutex); - if (_done) { - base::take(_done)(std::move(part)); - } -} - -void GroupCall::LoadPartTask::cancel() { - QMutexLocker lock(&_mutex); - if (!_done) { - return; - } - _done = nullptr; - lock.unlock(); - - if (_call) { - const auto that = this; - crl::on_main(_call, [weak = _call, that] { - if (const auto strong = weak.get()) { - strong->broadcastPartCancel(that); - } - }); - } -} - -GroupCall::GroupCall( - not_null delegate, - Group::JoinInfo info, - const MTPInputGroupCall &inputCall) -: _delegate(delegate) -, _peer(info.peer) -, _history(_peer->owner().history(_peer)) -, _api(&_peer->session().mtp()) -, _joinAs(info.joinAs) -, _possibleJoinAs(std::move(info.possibleJoinAs)) -, _joinHash(info.joinHash) -, _id(inputCall.c_inputGroupCall().vid().v) -, _scheduleDate(info.scheduleDate) -, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) -, _checkJoinedTimer([=] { checkJoined(); }) -, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) -, _connectingSoundTimer([=] { playConnectingSoundOnce(); }) -, _mediaDevices(CreateMediaDevices()) { - _muted.value( - ) | rpl::combine_previous( - ) | rpl::start_with_next([=](MuteState previous, MuteState state) { - if (_instance) { - updateInstanceMuteState(); - } - if (_mySsrc - && (!_initialMuteStateSent || state == MuteState::Active)) { - _initialMuteStateSent = true; - maybeSendMutedUpdate(previous); - } - }, _lifetime); - - _instanceState.value( - ) | rpl::filter([=] { - return _hadJoinedState; - }) | rpl::start_with_next([=](InstanceState state) { - if (state == InstanceState::Disconnected) { - playConnectingSound(); - } else { - stopConnectingSound(); - } - }, _lifetime); - - checkGlobalShortcutAvailability(); - - if (const auto real = lookupReal()) { - subscribeToReal(real); - if (!_peer->canManageGroupCall() && real->joinMuted()) { - _muted = MuteState::ForceMuted; - } - } else { - _peer->session().changes().peerFlagsValue( - _peer, - Data::PeerUpdate::Flag::GroupCall - ) | rpl::map([=] { - return lookupReal(); - }) | rpl::filter([](Data::GroupCall *real) { - return real != nullptr; - }) | rpl::map([](Data::GroupCall *real) { - return not_null{ real }; - }) | rpl::take( - 1 - ) | rpl::start_with_next([=](not_null real) { - subscribeToReal(real); - _realChanges.fire_copy(real); - }, _lifetime); - } - if (_id) { - join(inputCall); - } else { - start(info.scheduleDate); - } - if (_scheduleDate) { - saveDefaultJoinAs(_joinAs); - } - - _mediaDevices->audioInputId( - ) | rpl::start_with_next([=](QString id) { - _audioInputId = id; - if (_instance) { - _instance->setAudioInputDevice(id.toStdString()); - } - }, _lifetime); - - _mediaDevices->audioOutputId( - ) | rpl::start_with_next([=](QString id) { - _audioOutputId = id; - if (_instance) { - _instance->setAudioOutputDevice(id.toStdString()); - } - }, _lifetime); -} - -GroupCall::~GroupCall() { - destroyController(); -} - -void GroupCall::setScheduledDate(TimeId date) { - const auto was = _scheduleDate; - _scheduleDate = date; - if (was && !date) { - join(inputCall()); - } -} - -void GroupCall::subscribeToReal(not_null real) { - real->scheduleDateValue( - ) | rpl::start_with_next([=](TimeId date) { - setScheduledDate(date); - }, _lifetime); -} - -void GroupCall::checkGlobalShortcutAvailability() { - auto &settings = Core::App().settings(); - if (!settings.groupCallPushToTalk()) { - return; - } else if (!base::GlobalShortcutsAllowed()) { - settings.setGroupCallPushToTalk(false); - Core::App().saveSettingsDelayed(); - } -} - -void GroupCall::setState(State state) { - if (_state.current() == State::Failed) { - return; - } else if (_state.current() == State::FailedHangingUp - && state != State::Failed) { - return; - } - if (_state.current() == state) { - return; - } - _state = state; - - if (state == State::Joined) { - stopConnectingSound(); - if (const auto call = _peer->groupCall(); call && call->id() == _id) { - call->setInCall(); - } - } - - if (false - || state == State::Ended - || state == State::Failed) { - // Destroy controller before destroying Call Panel, - // so that the panel hide animation is smooth. - destroyController(); - } - switch (state) { - case State::HangingUp: - case State::FailedHangingUp: - _delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended); - break; - case State::Ended: - _delegate->groupCallFinished(this); - break; - case State::Failed: - _delegate->groupCallFailed(this); - break; - case State::Connecting: - if (!_checkJoinedTimer.isActive()) { - _checkJoinedTimer.callOnce(kCheckJoinedTimeout); - } - break; - } -} - -void GroupCall::playConnectingSound() { - if (_connectingSoundTimer.isActive()) { - return; - } - playConnectingSoundOnce(); - _connectingSoundTimer.callEach(kPlayConnectingEach); -} - -void GroupCall::stopConnectingSound() { - _connectingSoundTimer.cancel(); -} - -void GroupCall::playConnectingSoundOnce() { - _delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting); -} - -bool GroupCall::showChooseJoinAs() const { - return (_possibleJoinAs.size() > 1) - || (_possibleJoinAs.size() == 1 - && !_possibleJoinAs.front()->isSelf()); -} - -bool GroupCall::scheduleStartSubscribed() const { - if (const auto real = lookupReal()) { - return real->scheduleStartSubscribed(); - } - return false; -} - -Data::GroupCall *GroupCall::lookupReal() const { - const auto real = _peer->groupCall(); - return (real && real->id() == _id) ? real : nullptr; -} - -rpl::producer> GroupCall::real() const { - if (const auto real = lookupReal()) { - return rpl::single(not_null{ real }); - } - return _realChanges.events(); -} - -void GroupCall::start(TimeId scheduleDate) { - using Flag = MTPphone_CreateGroupCall::Flag; - _createRequestId = _api.request(MTPphone_CreateGroupCall( - MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag(0)), - _peer->input, - MTP_int(openssl::RandomValue()), - MTPstring(), // title - MTP_int(scheduleDate) - )).done([=](const MTPUpdates &result) { - _acceptFields = true; - _peer->session().api().applyUpdates(result); - _acceptFields = false; - }).fail([=](const MTP::Error &error) { - LOG(("Call Error: Could not create, error: %1" - ).arg(error.type())); - hangup(); - if (error.type() == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q) { - Ui::ShowMultilineToast({ - .text = { tr::lng_group_call_no_anonymous(tr::now) }, - }); - } - }).send(); -} - -void GroupCall::join(const MTPInputGroupCall &inputCall) { - inputCall.match([&](const MTPDinputGroupCall &data) { - _id = data.vid().v; - _accessHash = data.vaccess_hash().v; - }); - setState(_scheduleDate ? State::Waiting : State::Joining); - - if (_scheduleDate) { - return; - } - rejoin(); - - using Update = Data::GroupCall::ParticipantUpdate; - _peer->groupCall()->participantUpdated( - ) | rpl::filter([=](const Update &update) { - return (_instance != nullptr); - }) | rpl::start_with_next([=](const Update &update) { - if (!update.now) { - _instance->removeSsrcs({ update.was->ssrc }); - } else { - const auto &now = *update.now; - const auto &was = update.was; - const auto volumeChanged = was - ? (was->volume != now.volume || was->mutedByMe != now.mutedByMe) - : (now.volume != Group::kDefaultVolume || now.mutedByMe); - if (volumeChanged) { - _instance->setVolume( - now.ssrc, - (now.mutedByMe - ? 0. - : (now.volume - / float64(Group::kDefaultVolume)))); - } - } - }, _lifetime); - - addParticipantsToInstance(); - - _peer->session().updates().addActiveChat( - _peerStream.events_starting_with_copy(_peer)); - SubscribeToMigration(_peer, _lifetime, [=](not_null group) { - _peer = group; - _peerStream.fire_copy(group); - }); -} - -void GroupCall::rejoin() { - rejoin(_joinAs); -} - -void GroupCall::rejoinWithHash(const QString &hash) { - if (!hash.isEmpty() - && (muted() == MuteState::ForceMuted - || muted() == MuteState::RaisedHand)) { - _joinHash = hash; - rejoin(); - } -} - -void GroupCall::setJoinAs(not_null as) { - _joinAs = as; - if (const auto chat = _peer->asChat()) { - chat->setGroupCallDefaultJoinAs(_joinAs->id); - } else if (const auto channel = _peer->asChannel()) { - channel->setGroupCallDefaultJoinAs(_joinAs->id); - } -} - -void GroupCall::saveDefaultJoinAs(not_null as) { - setJoinAs(as); - _api.request(MTPphone_SaveDefaultGroupCallJoinAs( - _peer->input, - _joinAs->input - )).send(); -} - -void GroupCall::rejoin(not_null as) { - if (state() != State::Joining - && state() != State::Joined - && state() != State::Connecting) { - return; - } - - _mySsrc = 0; - _initialMuteStateSent = false; - setState(State::Joining); - ensureControllerCreated(); - setInstanceMode(InstanceMode::None); - applyMeInCallLocally(); - LOG(("Call Info: Requesting join payload.")); - - setJoinAs(as); - - const auto weak = base::make_weak(this); - _instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { - crl::on_main(weak, [=, payload = std::move(payload)]{ - auto fingerprints = QJsonArray(); - for (const auto &print : payload.fingerprints) { - auto object = QJsonObject(); - object.insert("hash", QString::fromStdString(print.hash)); - object.insert("setup", QString::fromStdString(print.setup)); - object.insert( - "fingerprint", - QString::fromStdString(print.fingerprint)); - fingerprints.push_back(object); - } - - auto root = QJsonObject(); - const auto ssrc = payload.ssrc; - root.insert("ufrag", QString::fromStdString(payload.ufrag)); - root.insert("pwd", QString::fromStdString(payload.pwd)); - root.insert("fingerprints", fingerprints); - root.insert("ssrc", double(payload.ssrc)); - - LOG(("Call Info: Join payload received, joining with ssrc: %1." - ).arg(ssrc)); - - const auto json = QJsonDocument(root).toJson( - QJsonDocument::Compact); - const auto wasMuteState = muted(); - using Flag = MTPphone_JoinGroupCall::Flag; - _api.request(MTPphone_JoinGroupCall( - MTP_flags((wasMuteState != MuteState::Active - ? Flag::f_muted - : Flag(0)) | (_joinHash.isEmpty() - ? Flag(0) - : Flag::f_invite_hash)), - inputCall(), - _joinAs->input, - MTP_string(_joinHash), - MTP_dataJSON(MTP_bytes(json)) - )).done([=](const MTPUpdates &updates) { - _mySsrc = ssrc; - _mySsrcs.emplace(ssrc); - setState((_instanceState.current() - == InstanceState::Disconnected) - ? State::Connecting - : State::Joined); - applyMeInCallLocally(); - maybeSendMutedUpdate(wasMuteState); - _peer->session().api().applyUpdates(updates); - applyQueuedSelfUpdates(); - checkFirstTimeJoined(); - }).fail([=](const MTP::Error &error) { - const auto type = error.type(); - LOG(("Call Error: Could not join, error: %1").arg(type)); - - if (type == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { - rejoin(); - return; - } - - hangup(); - Ui::ShowMultilineToast({ - .text = { type == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q - ? tr::lng_group_call_no_anonymous(tr::now) - : type == u"GROUPCALL_PARTICIPANTS_TOO_MUCH"_q - ? tr::lng_group_call_too_many(tr::now) - : type == u"GROUPCALL_FORBIDDEN"_q - ? tr::lng_group_not_accessible(tr::now) - : Lang::Hard::ServerError() }, - }); - }).send(); - }); - }); -} - -[[nodiscard]] uint64 FindLocalRaisedHandRating( - const std::vector &list) { - const auto i = ranges::max_element( - list, - ranges::less(), - &Data::GroupCallParticipant::raisedHandRating); - return (i == end(list)) ? 1 : (i->raisedHandRating + 1); -} - -void GroupCall::applyMeInCallLocally() { - const auto call = _peer->groupCall(); - if (!call || call->id() != _id) { - return; - } - using Flag = MTPDgroupCallParticipant::Flag; - const auto &participants = call->participants(); - const auto i = ranges::find( - participants, - _joinAs, - &Data::GroupCall::Participant::peer); - const auto date = (i != end(participants)) - ? i->date - : base::unixtime::now(); - const auto lastActive = (i != end(participants)) - ? i->lastActive - : TimeId(0); - const auto volume = (i != end(participants)) - ? i->volume - : Group::kDefaultVolume; - const auto canSelfUnmute = (muted() != MuteState::ForceMuted) - && (muted() != MuteState::RaisedHand); - const auto raisedHandRating = (muted() != MuteState::RaisedHand) - ? uint64(0) - : (i != end(participants)) - ? i->raisedHandRating - : FindLocalRaisedHandRating(participants); - const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0)) - | (lastActive ? Flag::f_active_date : Flag(0)) - | (_mySsrc ? Flag(0) : Flag::f_left) - | Flag::f_self - | Flag::f_volume // Without flag the volume is reset to 100%. - | Flag::f_volume_by_admin // Self volume can only be set by admin. - | ((muted() != MuteState::Active) ? Flag::f_muted : Flag(0)) - | (raisedHandRating > 0 ? Flag::f_raise_hand_rating : Flag(0)); - call->applyLocalUpdate( - MTP_updateGroupCallParticipants( - inputCall(), - MTP_vector( - 1, - MTP_groupCallParticipant( - MTP_flags(flags), - peerToMTP(_joinAs->id), - MTP_int(date), - MTP_int(lastActive), - MTP_int(_mySsrc), - MTP_int(volume), - MTPstring(), // Don't update about text in local updates. - MTP_long(raisedHandRating), - MTPDataJSON())), - MTP_int(0)).c_updateGroupCallParticipants()); -} - -void GroupCall::applyParticipantLocally( - not_null participantPeer, - bool mute, - std::optional volume) { - const auto participant = LookupParticipant(_peer, _id, participantPeer); - if (!participant || !participant->ssrc) { - return; - } - const auto canManageCall = _peer->canManageGroupCall(); - const auto isMuted = participant->muted || (mute && canManageCall); - const auto canSelfUnmute = !canManageCall - ? participant->canSelfUnmute - : (!mute || IsGroupCallAdmin(_peer, participantPeer)); - const auto isMutedByYou = mute && !canManageCall; - const auto mutedCount = 0/*participant->mutedCount*/; - using Flag = MTPDgroupCallParticipant::Flag; - const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0)) - | Flag::f_volume // Without flag the volume is reset to 100%. - | ((participant->applyVolumeFromMin && !volume) - ? Flag::f_volume_by_admin - : Flag(0)) - | (participant->lastActive ? Flag::f_active_date : Flag(0)) - | (isMuted ? Flag::f_muted : Flag(0)) - | (isMutedByYou ? Flag::f_muted_by_you : Flag(0)) - | (participantPeer == _joinAs ? Flag::f_self : Flag(0)) - | (participant->raisedHandRating - ? Flag::f_raise_hand_rating - : Flag(0)); - _peer->groupCall()->applyLocalUpdate( - MTP_updateGroupCallParticipants( - inputCall(), - MTP_vector( - 1, - MTP_groupCallParticipant( - MTP_flags(flags), - peerToMTP(participantPeer->id), - MTP_int(participant->date), - MTP_int(participant->lastActive), - MTP_int(participant->ssrc), - MTP_int(volume.value_or(participant->volume)), - MTPstring(), // Don't update about text in local updates. - MTP_long(participant->raisedHandRating), - MTPDataJSON())), - MTP_int(0)).c_updateGroupCallParticipants()); -} - -void GroupCall::hangup() { - finish(FinishType::Ended); -} - -void GroupCall::discard() { - if (!_id) { - _api.request(_createRequestId).cancel(); - hangup(); - return; - } - _api.request(MTPphone_DiscardGroupCall( - inputCall() - )).done([=](const MTPUpdates &result) { - // Here 'this' could be destroyed by updates, so we set Ended after - // updates being handled, but in a guarded way. - crl::on_main(this, [=] { hangup(); }); - _peer->session().api().applyUpdates(result); - }).fail([=](const MTP::Error &error) { - hangup(); - }).send(); -} - -void GroupCall::rejoinAs(Group::JoinInfo info) { - _possibleJoinAs = std::move(info.possibleJoinAs); - if (info.joinAs == _joinAs) { - return; - } - const auto event = Group::RejoinEvent{ - .wasJoinAs = _joinAs, - .nowJoinAs = info.joinAs, - }; - if (_scheduleDate) { - saveDefaultJoinAs(info.joinAs); - } else { - setState(State::Joining); - rejoin(info.joinAs); - } - _rejoinEvents.fire_copy(event); -} - -void GroupCall::finish(FinishType type) { - Expects(type != FinishType::None); - - const auto finalState = (type == FinishType::Ended) - ? State::Ended - : State::Failed; - const auto hangupState = (type == FinishType::Ended) - ? State::HangingUp - : State::FailedHangingUp; - const auto state = _state.current(); - if (state == State::HangingUp - || state == State::FailedHangingUp - || state == State::Ended - || state == State::Failed) { - return; - } - if (!_mySsrc) { - setState(finalState); - return; - } - - setState(hangupState); - - // We want to leave request still being sent and processed even if - // the call is already destroyed. - const auto session = &_peer->session(); - const auto weak = base::make_weak(this); - session->api().request(MTPphone_LeaveGroupCall( - inputCall(), - MTP_int(_mySsrc) - )).done([=](const MTPUpdates &result) { - // Here 'this' could be destroyed by updates, so we set Ended after - // updates being handled, but in a guarded way. - crl::on_main(weak, [=] { setState(finalState); }); - session->api().applyUpdates(result); - }).fail(crl::guard(weak, [=](const MTP::Error &error) { - setState(finalState); - })).send(); -} - -void GroupCall::startScheduledNow() { - if (!lookupReal()) { - return; - } - _api.request(MTPphone_StartScheduledGroupCall( - inputCall() - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - }).send(); -} - -void GroupCall::toggleScheduleStartSubscribed(bool subscribed) { - if (!lookupReal()) { - return; - } - _api.request(MTPphone_ToggleGroupCallStartSubscription( - inputCall(), - MTP_bool(subscribed) - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - }).send(); -} - -void GroupCall::setMuted(MuteState mute) { - const auto set = [=] { - const auto wasMuted = (muted() == MuteState::Muted) - || (muted() == MuteState::PushToTalk); - const auto wasRaiseHand = (muted() == MuteState::RaisedHand); - _muted = mute; - const auto nowMuted = (muted() == MuteState::Muted) - || (muted() == MuteState::PushToTalk); - const auto nowRaiseHand = (muted() == MuteState::RaisedHand); - if (wasMuted != nowMuted || wasRaiseHand != nowRaiseHand) { - applyMeInCallLocally(); - } - }; - if (mute == MuteState::Active || mute == MuteState::PushToTalk) { - _delegate->groupCallRequestPermissionsOrFail(crl::guard(this, set)); - } else { - set(); - } -} - -void GroupCall::setMutedAndUpdate(MuteState mute) { - const auto was = muted(); - - // Active state is sent from _muted changes, - // because it may be set delayed, after permissions request, not now. - const auto send = _initialMuteStateSent && (mute != MuteState::Active); - setMuted(mute); - if (send) { - maybeSendMutedUpdate(was); - } -} - -void GroupCall::handlePossibleCreateOrJoinResponse( - const MTPDupdateGroupCall &data) { - data.vcall().match([&](const MTPDgroupCall &data) { - handlePossibleCreateOrJoinResponse(data); - }, [&](const MTPDgroupCallDiscarded &data) { - handlePossibleDiscarded(data); - }); -} - -void GroupCall::handlePossibleCreateOrJoinResponse( - const MTPDgroupCall &data) { - setScheduledDate(data.vschedule_date().value_or_empty()); - if (_acceptFields) { - if (!_instance && !_id) { - const auto input = MTP_inputGroupCall( - data.vid(), - data.vaccess_hash()); - const auto scheduleDate = data.vschedule_date().value_or_empty(); - if (const auto chat = _peer->asChat()) { - chat->setGroupCall(input, scheduleDate); - } else if (const auto group = _peer->asChannel()) { - group->setGroupCall(input, scheduleDate); - } else { - Unexpected("Peer type in GroupCall::join."); - } - join(input); - } - return; - } else if (_id != data.vid().v || !_instance) { - return; - } - const auto streamDcId = MTP::BareDcId( - data.vstream_dc_id().value_or_empty()); - const auto params = data.vparams(); - if (!params) { - return; - } - params->match([&](const MTPDdataJSON &data) { - auto error = QJsonParseError{ 0, QJsonParseError::NoError }; - const auto document = QJsonDocument::fromJson( - data.vdata().v, - &error); - if (error.error != QJsonParseError::NoError) { - LOG(("API Error: " - "Failed to parse group call params, error: %1." - ).arg(error.errorString())); - return; - } else if (!document.isObject()) { - LOG(("API Error: " - "Not an object received in group call params.")); - return; - } - - const auto guard = gsl::finally([&] { - addParticipantsToInstance(); - }); - - if (document.object().value("stream").toBool()) { - if (!streamDcId) { - LOG(("Api Error: Empty stream_dc_id in groupCall.")); - } - _broadcastDcId = streamDcId - ? streamDcId - : _peer->session().mtp().mainDcId(); - setInstanceMode(InstanceMode::Stream); - return; - } - - const auto readString = []( - const QJsonObject &object, - const char *key) { - return object.value(key).toString().toStdString(); - }; - const auto root = document.object().value("transport").toObject(); - auto payload = tgcalls::GroupJoinResponsePayload(); - payload.ufrag = readString(root, "ufrag"); - payload.pwd = readString(root, "pwd"); - const auto prints = root.value("fingerprints").toArray(); - const auto candidates = root.value("candidates").toArray(); - for (const auto &print : prints) { - const auto object = print.toObject(); - payload.fingerprints.push_back(tgcalls::GroupJoinPayloadFingerprint{ - .hash = readString(object, "hash"), - .setup = readString(object, "setup"), - .fingerprint = readString(object, "fingerprint"), - }); - } - for (const auto &candidate : candidates) { - const auto object = candidate.toObject(); - payload.candidates.push_back(tgcalls::GroupJoinResponseCandidate{ - .port = readString(object, "port"), - .protocol = readString(object, "protocol"), - .network = readString(object, "network"), - .generation = readString(object, "generation"), - .id = readString(object, "id"), - .component = readString(object, "component"), - .foundation = readString(object, "foundation"), - .priority = readString(object, "priority"), - .ip = readString(object, "ip"), - .type = readString(object, "type"), - .tcpType = readString(object, "tcpType"), - .relAddr = readString(object, "relAddr"), - .relPort = readString(object, "relPort"), - }); - } - setInstanceMode(InstanceMode::Rtc); - _instance->setJoinResponsePayload(payload, {}); - }); -} - -void GroupCall::handlePossibleDiscarded(const MTPDgroupCallDiscarded &data) { - if (data.vid().v == _id) { - LOG(("Call Info: Hangup after groupCallDiscarded.")); - _mySsrc = 0; - hangup(); - } -} - -void GroupCall::addParticipantsToInstance() { - const auto real = lookupReal(); - if (!real || (_instanceMode == InstanceMode::None)) { - return; - } - for (const auto &participant : real->participants()) { - prepareParticipantForAdding(participant); - } - addPreparedParticipants(); -} - -void GroupCall::prepareParticipantForAdding( - const Data::GroupCallParticipant &participant) { - _preparedParticipants.push_back(tgcalls::GroupParticipantDescription()); - auto &added = _preparedParticipants.back(); - added.audioSsrc = participant.ssrc; - _unresolvedSsrcs.remove(added.audioSsrc); -} - -void GroupCall::addPreparedParticipants() { - _addPreparedParticipantsScheduled = false; - if (!_preparedParticipants.empty()) { - _instance->addParticipants(base::take(_preparedParticipants)); - } - if (const auto real = lookupReal()) { - if (!_unresolvedSsrcs.empty()) { - real->resolveParticipants(base::take(_unresolvedSsrcs)); - } - } -} - -void GroupCall::addPreparedParticipantsDelayed() { - if (_addPreparedParticipantsScheduled) { - return; - } - _addPreparedParticipantsScheduled = true; - crl::on_main(this, [=] { addPreparedParticipants(); }); -} - -void GroupCall::handleUpdate(const MTPUpdate &update) { - update.match([&](const MTPDupdateGroupCall &data) { - handleUpdate(data); - }, [&](const MTPDupdateGroupCallParticipants &data) { - handleUpdate(data); - }, [](const auto &) { - Unexpected("Type in Instance::applyGroupCallUpdateChecked."); - }); -} - -void GroupCall::handleUpdate(const MTPDupdateGroupCall &data) { - data.vcall().match([](const MTPDgroupCall &) { - }, [&](const MTPDgroupCallDiscarded &data) { - handlePossibleDiscarded(data); - }); -} - -void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) { - const auto callId = data.vcall().match([](const auto &data) { - return data.vid().v; - }); - if (_id != callId) { - return; - } - const auto state = _state.current(); - const auto joined = (state == State::Joined) - || (state == State::Connecting); - for (const auto &participant : data.vparticipants().v) { - participant.match([&](const MTPDgroupCallParticipant &data) { - const auto isSelf = data.is_self() - || (data.is_min() - && peerFromMTP(data.vpeer()) == _joinAs->id); - if (!isSelf) { - applyOtherParticipantUpdate(data); - } else if (joined) { - applySelfUpdate(data); - } else { - _queuedSelfUpdates.push_back(participant); - } - }); - } -} - -void GroupCall::applyQueuedSelfUpdates() { - const auto weak = base::make_weak(this); - while (weak - && !_queuedSelfUpdates.empty() - && (_state.current() == State::Joined - || _state.current() == State::Connecting)) { - const auto update = _queuedSelfUpdates.front(); - _queuedSelfUpdates.erase(_queuedSelfUpdates.begin()); - update.match([&](const MTPDgroupCallParticipant &data) { - applySelfUpdate(data); - }); - } -} - -void GroupCall::applySelfUpdate(const MTPDgroupCallParticipant &data) { - if (data.is_left()) { - if (data.vsource().v == _mySsrc) { - // I was removed from the call, rejoin. - LOG(("Call Info: " - "Rejoin after got 'left' with my ssrc.")); - setState(State::Joining); - rejoin(); - } - return; - } else if (data.vsource().v != _mySsrc) { - if (!_mySsrcs.contains(data.vsource().v)) { - // I joined from another device, hangup. - LOG(("Call Info: " - "Hangup after '!left' with ssrc %1, my %2." - ).arg(data.vsource().v - ).arg(_mySsrc)); - _mySsrc = 0; - hangup(); - } else { - LOG(("Call Info: " - "Some old 'self' with '!left' and ssrc %1, my %2." - ).arg(data.vsource().v - ).arg(_mySsrc)); - } - return; - } - if (data.is_muted() && !data.is_can_self_unmute()) { - setMuted(data.vraise_hand_rating().value_or_empty() - ? MuteState::RaisedHand - : MuteState::ForceMuted); - } else if (_instanceMode == InstanceMode::Stream) { - LOG(("Call Info: Rejoin after unforcemute in stream mode.")); - setState(State::Joining); - rejoin(); - } else if (muted() == MuteState::ForceMuted - || muted() == MuteState::RaisedHand) { - setMuted(MuteState::Muted); - if (!_instanceTransitioning) { - notifyAboutAllowedToSpeak(); - } - } else if (data.is_muted() && muted() != MuteState::Muted) { - setMuted(MuteState::Muted); - } -} - -void GroupCall::applyOtherParticipantUpdate( - const MTPDgroupCallParticipant &data) { - if (data.is_min()) { - // No real information about mutedByMe or my custom volume. - return; - } - const auto participantPeer = _peer->owner().peer( - peerFromMTP(data.vpeer())); - if (!LookupParticipant(_peer, _id, participantPeer)) { - return; - } - _otherParticipantStateValue.fire(Group::ParticipantState{ - .peer = participantPeer, - .volume = data.vvolume().value_or_empty(), - .mutedByMe = data.is_muted_by_you(), - }); -} - -void GroupCall::changeTitle(const QString &title) { - const auto real = lookupReal(); - if (!real || real->title() == title) { - return; - } - - _api.request(MTPphone_EditGroupCallTitle( - inputCall(), - MTP_string(title) - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - _titleChanged.fire({}); - }).fail([=](const MTP::Error &error) { - }).send(); -} - -void GroupCall::toggleRecording(bool enabled, const QString &title) { - const auto real = lookupReal(); - if (!real) { - return; - } - - const auto already = (real->recordStartDate() != 0); - if (already == enabled) { - return; - } - - if (!enabled) { - _recordingStoppedByMe = true; - } - using Flag = MTPphone_ToggleGroupCallRecord::Flag; - _api.request(MTPphone_ToggleGroupCallRecord( - MTP_flags((enabled ? Flag::f_start : Flag(0)) - | (title.isEmpty() ? Flag(0) : Flag::f_title)), - inputCall(), - MTP_string(title) - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - _recordingStoppedByMe = false; - }).fail([=](const MTP::Error &error) { - _recordingStoppedByMe = false; - }).send(); -} - -void GroupCall::ensureControllerCreated() { - if (_instance) { - return; - } - const auto &settings = Core::App().settings(); - - const auto weak = base::make_weak(this); - const auto myLevel = std::make_shared(); - tgcalls::GroupInstanceDescriptor descriptor = { - .threads = tgcalls::StaticThreads::getThreads(), - .config = tgcalls::GroupConfig{ - }, - .networkStateUpdated = [=](tgcalls::GroupNetworkState networkState) { - crl::on_main(weak, [=] { setInstanceConnected(networkState); }); - }, - .audioLevelsUpdated = [=](const tgcalls::GroupLevelsUpdate &data) { - const auto &updates = data.updates; - if (updates.empty()) { - return; - } else if (updates.size() == 1 && !updates.front().ssrc) { - const auto &value = updates.front().value; - // Don't send many 0 while we're muted. - if (myLevel->level == value.level - && myLevel->voice == value.voice) { - return; - } - *myLevel = updates.front().value; - } - crl::on_main(weak, [=] { audioLevelsUpdated(data); }); - }, - .initialInputDeviceId = _audioInputId.toStdString(), - .initialOutputDeviceId = _audioOutputId.toStdString(), - .createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator( - settings.callAudioBackend()), - .participantDescriptionsRequired = [=]( - const std::vector &ssrcs) { - crl::on_main(weak, [=] { - requestParticipantsInformation(ssrcs); - }); - }, - .requestBroadcastPart = [=]( - int64_t time, - int64_t period, - std::function done) { - auto result = std::make_shared( - weak, - time, - period, - std::move(done)); - crl::on_main(weak, [=]() mutable { - broadcastPartStart(std::move(result)); - }); - return result; - } - }; - if (Logs::DebugEnabled()) { - auto callLogFolder = cWorkingDir() + qsl("DebugLogs"); - auto callLogPath = callLogFolder + qsl("/last_group_call_log.txt"); - auto callLogNative = QDir::toNativeSeparators(callLogPath); -#ifdef Q_OS_WIN - descriptor.config.logPath.data = callLogNative.toStdWString(); -#else // Q_OS_WIN - const auto callLogUtf = QFile::encodeName(callLogNative); - descriptor.config.logPath.data.resize(callLogUtf.size()); - ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); -#endif // Q_OS_WIN - QFile(callLogPath).remove(); - QDir().mkpath(callLogFolder); - } - - LOG(("Call Info: Creating group instance")); - _instance = std::make_unique( - std::move(descriptor)); - - updateInstanceMuteState(); - updateInstanceVolumes(); - - //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); -} - -void GroupCall::broadcastPartStart(std::shared_ptr task) { - const auto raw = task.get(); - const auto time = raw->time(); - const auto scale = raw->scale(); - const auto finish = [=](tgcalls::BroadcastPart &&part) { - raw->done(std::move(part)); - _broadcastParts.erase(raw); - }; - using Status = tgcalls::BroadcastPart::Status; - const auto requestId = _api.request(MTPupload_GetFile( - MTP_flags(0), - MTP_inputGroupCallStream( - inputCall(), - MTP_long(time), - MTP_int(scale)), - MTP_int(0), - MTP_int(128 * 1024) - )).done([=]( - const MTPupload_File &result, - const MTP::Response &response) { - result.match([&](const MTPDupload_file &data) { - const auto size = data.vbytes().v.size(); - auto bytes = std::vector(size); - memcpy(bytes.data(), data.vbytes().v.constData(), size); - finish({ - .timestampMilliseconds = time, - .responseTimestamp = TimestampFromMsgId(response.outerMsgId), - .status = Status::Success, - .oggData = std::move(bytes), - }); - }, [&](const MTPDupload_fileCdnRedirect &data) { - LOG(("Voice Chat Stream Error: fileCdnRedirect received.")); - finish({ - .timestampMilliseconds = time, - .responseTimestamp = TimestampFromMsgId(response.outerMsgId), - .status = Status::ResyncNeeded, - }); - }); - }).fail([=](const MTP::Error &error, const MTP::Response &response) { - if (error.type() == u"GROUPCALL_JOIN_MISSING"_q - || error.type() == u"GROUPCALL_FORBIDDEN"_q) { - for (const auto &[task, part] : _broadcastParts) { - _api.request(part.requestId).cancel(); - } - setState(State::Joining); - rejoin(); - return; - } - const auto status = (MTP::IsFloodError(error) - || error.type() == u"TIME_TOO_BIG"_q) - ? Status::NotReady - : Status::ResyncNeeded; - finish({ - .timestampMilliseconds = time, - .responseTimestamp = TimestampFromMsgId(response.outerMsgId), - .status = status, - }); - }).handleAllErrors().toDC( - MTP::groupCallStreamDcId(_broadcastDcId) - ).send(); - _broadcastParts.emplace(raw, LoadingPart{ std::move(task), requestId }); -} - -void GroupCall::broadcastPartCancel(not_null task) { - const auto i = _broadcastParts.find(task); - if (i != _broadcastParts.end()) { - _api.request(i->second.requestId).cancel(); - _broadcastParts.erase(i); - } -} - -void GroupCall::requestParticipantsInformation( - const std::vector &ssrcs) { - const auto real = lookupReal(); - if (!real || (_instanceMode == InstanceMode::None)) { - for (const auto ssrc : ssrcs) { - _unresolvedSsrcs.emplace(ssrc); - } - return; - } - - const auto &existing = real->participants(); - for (const auto ssrc : ssrcs) { - const auto participantPeer = real->participantPeerBySsrc(ssrc); - if (!participantPeer) { - _unresolvedSsrcs.emplace(ssrc); - continue; - } - const auto i = ranges::find( - existing, - not_null{ participantPeer }, - &Data::GroupCall::Participant::peer); - Assert(i != end(existing)); - - prepareParticipantForAdding(*i); - } - addPreparedParticipants(); -} - -void GroupCall::updateInstanceMuteState() { - Expects(_instance != nullptr); - - const auto state = muted(); - _instance->setIsMuted(state != MuteState::Active - && state != MuteState::PushToTalk); -} - -void GroupCall::updateInstanceVolumes() { - const auto real = lookupReal(); - if (!real) { - return; - } - - const auto &participants = real->participants(); - for (const auto &participant : participants) { - const auto setVolume = participant.mutedByMe - || (participant.volume != Group::kDefaultVolume); - if (setVolume && participant.ssrc) { - _instance->setVolume( - participant.ssrc, - (participant.mutedByMe - ? 0. - : (participant.volume / float64(Group::kDefaultVolume)))); - } - } -} - -void GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) { - Expects(!data.updates.empty()); - - auto check = false; - auto checkNow = false; - const auto now = crl::now(); - for (const auto &[ssrcOrZero, value] : data.updates) { - const auto ssrc = ssrcOrZero ? ssrcOrZero : _mySsrc; - const auto level = value.level; - const auto voice = value.voice; - const auto me = (ssrc == _mySsrc); - _levelUpdates.fire(LevelUpdate{ - .ssrc = ssrc, - .value = level, - .voice = voice, - .me = me - }); - if (level <= kSpeakLevelThreshold) { - continue; - } - if (me - && voice - && (!_lastSendProgressUpdate - || _lastSendProgressUpdate + kUpdateSendActionEach < now)) { - _lastSendProgressUpdate = now; - _peer->session().sendProgressManager().update( - _history, - Api::SendProgressType::Speaking); - } - - check = true; - const auto i = _lastSpoke.find(ssrc); - if (i == _lastSpoke.end()) { - _lastSpoke.emplace(ssrc, Data::LastSpokeTimes{ - .anything = now, - .voice = voice ? now : 0, - }); - checkNow = true; - } else { - if ((i->second.anything + kCheckLastSpokeInterval / 3 <= now) - || (voice - && i->second.voice + kCheckLastSpokeInterval / 3 <= now)) { - checkNow = true; - } - i->second.anything = now; - if (voice) { - i->second.voice = now; - } - } - } - if (checkNow) { - checkLastSpoke(); - } else if (check && !_lastSpokeCheckTimer.isActive()) { - _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2); - } -} - -void GroupCall::checkLastSpoke() { - const auto real = lookupReal(); - if (!real) { - return; - } - - auto hasRecent = false; - const auto now = crl::now(); - auto list = base::take(_lastSpoke); - for (auto i = list.begin(); i != list.end();) { - const auto [ssrc, when] = *i; - if (when.anything + kCheckLastSpokeInterval >= now) { - hasRecent = true; - ++i; - } else { - i = list.erase(i); - } - real->applyLastSpoke(ssrc, when, now); - } - _lastSpoke = std::move(list); - - if (!hasRecent) { - _lastSpokeCheckTimer.cancel(); - } else if (!_lastSpokeCheckTimer.isActive()) { - _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3); - } -} - -void GroupCall::checkJoined() { - if (state() != State::Connecting || !_id || !_mySsrc) { - return; - } - _api.request(MTPphone_CheckGroupCall( - inputCall(), - MTP_int(_mySsrc) - )).done([=](const MTPBool &result) { - if (!mtpIsTrue(result)) { - LOG(("Call Info: Rejoin after FALSE in checkGroupCall.")); - rejoin(); - } else if (state() == State::Connecting) { - _checkJoinedTimer.callOnce(kCheckJoinedTimeout); - } - }).fail([=](const MTP::Error &error) { - LOG(("Call Info: Rejoin after error '%1' in checkGroupCall." - ).arg(error.type())); - rejoin(); - }).send(); -} - -void GroupCall::setInstanceConnected( - tgcalls::GroupNetworkState networkState) { - const auto inTransit = networkState.isTransitioningFromBroadcastToRtc; - const auto instanceState = !networkState.isConnected - ? InstanceState::Disconnected - : inTransit - ? InstanceState::TransitionToRtc - : InstanceState::Connected; - const auto connected = (instanceState != InstanceState::Disconnected); - if (_instanceState.current() == instanceState - && _instanceTransitioning == inTransit) { - return; - } - const auto nowCanSpeak = connected - && _instanceTransitioning - && !inTransit - && (muted() == MuteState::Muted); - _instanceTransitioning = inTransit; - _instanceState = instanceState; - if (state() == State::Connecting && connected) { - setState(State::Joined); - } else if (state() == State::Joined && !connected) { - setState(State::Connecting); - } - if (nowCanSpeak) { - notifyAboutAllowedToSpeak(); - } - if (!_hadJoinedState && state() == State::Joined) { - checkFirstTimeJoined(); - } -} - -void GroupCall::checkFirstTimeJoined() { - if (_hadJoinedState || state() != State::Joined) { - return; - } - _hadJoinedState = true; - applyGlobalShortcutChanges(); - _delegate->groupCallPlaySound(Delegate::GroupCallSound::Started); -} - -void GroupCall::notifyAboutAllowedToSpeak() { - if (!_hadJoinedState) { - return; - } - _delegate->groupCallPlaySound( - Delegate::GroupCallSound::AllowedToSpeak); - _allowedToSpeakNotifications.fire({}); -} - -void GroupCall::setInstanceMode(InstanceMode mode) { - Expects(_instance != nullptr); - - _instanceMode = mode; - - using Mode = tgcalls::GroupConnectionMode; - _instance->setConnectionMode([&] { - switch (_instanceMode) { - case InstanceMode::None: return Mode::GroupConnectionModeNone; - case InstanceMode::Rtc: return Mode::GroupConnectionModeRtc; - case InstanceMode::Stream: return Mode::GroupConnectionModeBroadcast; - } - Unexpected("Mode in GroupCall::setInstanceMode."); - }(), true); -} - -void GroupCall::maybeSendMutedUpdate(MuteState previous) { - // Send Active <-> !Active or ForceMuted <-> RaisedHand changes. - const auto now = muted(); - if ((previous == MuteState::Active && now == MuteState::Muted) - || (now == MuteState::Active - && (previous == MuteState::Muted - || previous == MuteState::PushToTalk))) { - sendSelfUpdate(SendUpdateType::Mute); - } else if ((now == MuteState::ForceMuted - && previous == MuteState::RaisedHand) - || (now == MuteState::RaisedHand - && previous == MuteState::ForceMuted)) { - sendSelfUpdate(SendUpdateType::RaiseHand); - } -} - -void GroupCall::sendSelfUpdate(SendUpdateType type) { - _api.request(_updateMuteRequestId).cancel(); - using Flag = MTPphone_EditGroupCallParticipant::Flag; - _updateMuteRequestId = _api.request(MTPphone_EditGroupCallParticipant( - MTP_flags((type == SendUpdateType::RaiseHand) - ? Flag::f_raise_hand - : (muted() != MuteState::Active) - ? Flag::f_muted - : Flag(0)), - inputCall(), - _joinAs->input, - MTP_int(100000), // volume - MTP_bool(muted() == MuteState::RaisedHand) - )).done([=](const MTPUpdates &result) { - _updateMuteRequestId = 0; - _peer->session().api().applyUpdates(result); - }).fail([=](const MTP::Error &error) { - _updateMuteRequestId = 0; - if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { - LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." - ).arg(error.type())); - rejoin(); - } - }).send(); -} - -void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { - if (input) { - _mediaDevices->switchToAudioInput(deviceId); - } else { - _mediaDevices->switchToAudioOutput(deviceId); - } -} - -void GroupCall::toggleMute(const Group::MuteRequest &data) { - if (data.locallyOnly) { - applyParticipantLocally(data.peer, data.mute, std::nullopt); - } else { - editParticipant(data.peer, data.mute, std::nullopt); - } -} - -void GroupCall::changeVolume(const Group::VolumeRequest &data) { - if (data.locallyOnly) { - applyParticipantLocally(data.peer, false, data.volume); - } else { - editParticipant(data.peer, false, data.volume); - } -} - -void GroupCall::editParticipant( - not_null participantPeer, - bool mute, - std::optional volume) { - const auto participant = LookupParticipant(_peer, _id, participantPeer); - if (!participant) { - return; - } - applyParticipantLocally(participantPeer, mute, volume); - - using Flag = MTPphone_EditGroupCallParticipant::Flag; - const auto flags = (mute ? Flag::f_muted : Flag(0)) - | (volume.has_value() ? Flag::f_volume : Flag(0)); - _api.request(MTPphone_EditGroupCallParticipant( - MTP_flags(flags), - inputCall(), - participantPeer->input, - MTP_int(std::clamp(volume.value_or(0), 1, Group::kMaxVolume)), - MTPBool() - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - }).fail([=](const MTP::Error &error) { - if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { - LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." - ).arg(error.type())); - rejoin(); - } - }).send(); -} - -std::variant> GroupCall::inviteUsers( - const std::vector> &users) { - const auto real = lookupReal(); - if (!real) { - return 0; - } - const auto owner = &_peer->owner(); - const auto &invited = owner->invitedToCallUsers(_id); - const auto &participants = real->participants(); - auto &&toInvite = users | ranges::views::filter([&]( - not_null user) { - return !invited.contains(user) && !ranges::contains( - participants, - user, - &Data::GroupCall::Participant::peer); - }); - - auto count = 0; - auto slice = QVector(); - auto result = std::variant>(0); - slice.reserve(kMaxInvitePerSlice); - const auto sendSlice = [&] { - count += slice.size(); - _api.request(MTPphone_InviteToGroupCall( - inputCall(), - MTP_vector(slice) - )).done([=](const MTPUpdates &result) { - _peer->session().api().applyUpdates(result); - }).send(); - slice.clear(); - }; - for (const auto user : users) { - if (!count && slice.empty()) { - result = user; - } - owner->registerInvitedToCallUser(_id, _peer, user); - slice.push_back(user->inputUser); - if (slice.size() == kMaxInvitePerSlice) { - sendSlice(); - } - } - if (count != 0 || slice.size() != 1) { - result = int(count + slice.size()); - } - if (!slice.empty()) { - sendSlice(); - } - return result; -} - -auto GroupCall::ensureGlobalShortcutManager() --> std::shared_ptr { - if (!_shortcutManager) { - _shortcutManager = base::CreateGlobalShortcutManager(); - } - return _shortcutManager; -} - -void GroupCall::applyGlobalShortcutChanges() { - auto &settings = Core::App().settings(); - if (!settings.groupCallPushToTalk() - || settings.groupCallPushToTalkShortcut().isEmpty() - || !base::GlobalShortcutsAvailable() - || !base::GlobalShortcutsAllowed()) { - _shortcutManager = nullptr; - _pushToTalk = nullptr; - return; - } - ensureGlobalShortcutManager(); - const auto shortcut = _shortcutManager->shortcutFromSerialized( - settings.groupCallPushToTalkShortcut()); - if (!shortcut) { - settings.setGroupCallPushToTalkShortcut(QByteArray()); - settings.setGroupCallPushToTalk(false); - Core::App().saveSettingsDelayed(); - _shortcutManager = nullptr; - _pushToTalk = nullptr; - return; - } - if (_pushToTalk) { - if (shortcut->serialize() == _pushToTalk->serialize()) { - return; - } - _shortcutManager->stopWatching(_pushToTalk); - } - _pushToTalk = shortcut; - _shortcutManager->startWatching(_pushToTalk, [=](bool pressed) { - pushToTalk( - pressed, - Core::App().settings().groupCallPushToTalkDelay()); - }); -} - -void GroupCall::pushToTalk(bool pressed, crl::time delay) { - if (muted() == MuteState::ForceMuted - || muted() == MuteState::RaisedHand - || muted() == MuteState::Active) { - return; - } else if (pressed) { - _pushToTalkCancelTimer.cancel(); - setMuted(MuteState::PushToTalk); - } else if (delay) { - _pushToTalkCancelTimer.callOnce(delay); - } else { - pushToTalkCancel(); - } -} - -void GroupCall::pushToTalkCancel() { - _pushToTalkCancelTimer.cancel(); - if (muted() == MuteState::PushToTalk) { - setMuted(MuteState::Muted); - } -} - -auto GroupCall::otherParticipantStateValue() const --> rpl::producer { - return _otherParticipantStateValue.events(); -} - -//void GroupCall::setAudioVolume(bool input, float level) { -// if (_instance) { -// if (input) { -// _instance->setInputVolume(level); -// } else { -// _instance->setOutputVolume(level); -// } -// } -//} - -void GroupCall::setAudioDuckingEnabled(bool enabled) { - if (_instance) { - //_instance->setAudioOutputDuckingEnabled(enabled); - } -} - -void GroupCall::handleRequestError(const MTP::Error &error) { - //if (error.type() == qstr("USER_PRIVACY_RESTRICTED")) { - // Ui::show(Box(tr::lng_call_error_not_available(tr::now, lt_user, _user->name))); - //} else if (error.type() == qstr("PARTICIPANT_VERSION_OUTDATED")) { - // Ui::show(Box(tr::lng_call_error_outdated(tr::now, lt_user, _user->name))); - //} else if (error.type() == qstr("CALL_PROTOCOL_LAYER_INVALID")) { - // Ui::show(Box(Lang::Hard::CallErrorIncompatible().replace("{user}", _user->name))); - //} - //finish(FinishType::Failed); -} - -void GroupCall::handleControllerError(const QString &error) { - if (error == u"ERROR_INCOMPATIBLE"_q) { - //Ui::show(Box( - // Lang::Hard::CallErrorIncompatible().replace( - // "{user}", - // _user->name))); - } else if (error == u"ERROR_AUDIO_IO"_q) { - //Ui::show(Box(tr::lng_call_error_audio_io(tr::now))); - } - //finish(FinishType::Failed); -} - -MTPInputGroupCall GroupCall::inputCall() const { - Expects(_id != 0); - - return MTP_inputGroupCall( - MTP_long(_id), - MTP_long(_accessHash)); -} - -void GroupCall::destroyController() { - if (_instance) { - //_instance->stop([](tgcalls::FinalState) { - //}); - - DEBUG_LOG(("Call Info: Destroying call controller..")); - _instance.reset(); - DEBUG_LOG(("Call Info: Call controller destroyed.")); - } -} - -} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h deleted file mode 100644 index 90f0f8f9f..000000000 --- a/Telegram/SourceFiles/calls/calls_group_call.h +++ /dev/null @@ -1,363 +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 -*/ -#pragma once - -#include "base/weak_ptr.h" -#include "base/timer.h" -#include "base/bytes.h" -#include "mtproto/sender.h" -#include "mtproto/mtproto_auth_key.h" - -class History; - -namespace tgcalls { -class GroupInstanceCustomImpl; -struct GroupLevelsUpdate; -struct GroupNetworkState; -struct GroupParticipantDescription; -} // namespace tgcalls - -namespace base { -class GlobalShortcutManager; -class GlobalShortcutValue; -} // namespace base - -namespace Webrtc { -class MediaDevices; -} // namespace Webrtc - -namespace Data { -struct LastSpokeTimes; -struct GroupCallParticipant; -class GroupCall; -} // namespace Data - -namespace Calls { - -namespace Group { -struct MuteRequest; -struct VolumeRequest; -struct ParticipantState; -struct JoinInfo; -struct RejoinEvent; -} // namespace Group - -enum class MuteState { - Active, - PushToTalk, - Muted, - ForceMuted, - RaisedHand, -}; - -[[nodiscard]] inline auto MapPushToTalkToActive() { - return rpl::map([=](MuteState state) { - return (state == MuteState::PushToTalk) ? MuteState::Active : state; - }); -} - -[[nodiscard]] bool IsGroupCallAdmin( - not_null peer, - not_null participantPeer); - -struct LevelUpdate { - uint32 ssrc = 0; - float value = 0.; - bool voice = false; - bool me = false; -}; - -class GroupCall final : public base::has_weak_ptr { -public: - class Delegate { - public: - virtual ~Delegate() = default; - - virtual void groupCallFinished(not_null call) = 0; - virtual void groupCallFailed(not_null call) = 0; - virtual void groupCallRequestPermissionsOrFail( - Fn onSuccess) = 0; - - enum class GroupCallSound { - Started, - Connecting, - AllowedToSpeak, - Ended, - }; - virtual void groupCallPlaySound(GroupCallSound sound) = 0; - }; - - using GlobalShortcutManager = base::GlobalShortcutManager; - - GroupCall( - not_null delegate, - Group::JoinInfo info, - const MTPInputGroupCall &inputCall); - ~GroupCall(); - - [[nodiscard]] uint64 id() const { - return _id; - } - [[nodiscard]] not_null peer() const { - return _peer; - } - [[nodiscard]] not_null joinAs() const { - return _joinAs; - } - [[nodiscard]] bool showChooseJoinAs() const; - [[nodiscard]] TimeId scheduleDate() const { - return _scheduleDate; - } - [[nodiscard]] bool scheduleStartSubscribed() const; - - [[nodiscard]] Data::GroupCall *lookupReal() const; - [[nodiscard]] rpl::producer> real() const; - - void start(TimeId scheduleDate); - void hangup(); - void discard(); - void rejoinAs(Group::JoinInfo info); - void rejoinWithHash(const QString &hash); - void join(const MTPInputGroupCall &inputCall); - void handleUpdate(const MTPUpdate &update); - void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data); - void changeTitle(const QString &title); - void toggleRecording(bool enabled, const QString &title); - [[nodiscard]] bool recordingStoppedByMe() const { - return _recordingStoppedByMe; - } - void startScheduledNow(); - void toggleScheduleStartSubscribed(bool subscribed); - - void setMuted(MuteState mute); - void setMutedAndUpdate(MuteState mute); - [[nodiscard]] MuteState muted() const { - return _muted.current(); - } - [[nodiscard]] rpl::producer mutedValue() const { - return _muted.value(); - } - - [[nodiscard]] auto otherParticipantStateValue() const - -> rpl::producer; - - enum State { - Creating, - Waiting, - Joining, - Connecting, - Joined, - FailedHangingUp, - Failed, - HangingUp, - Ended, - }; - [[nodiscard]] State state() const { - return _state.current(); - } - [[nodiscard]] rpl::producer stateValue() const { - return _state.value(); - } - - enum class InstanceState { - Disconnected, - TransitionToRtc, - Connected, - }; - [[nodiscard]] InstanceState instanceState() const { - return _instanceState.current(); - } - [[nodiscard]] rpl::producer instanceStateValue() const { - return _instanceState.value(); - } - - [[nodiscard]] rpl::producer levelUpdates() const { - return _levelUpdates.events(); - } - [[nodiscard]] rpl::producer rejoinEvents() const { - return _rejoinEvents.events(); - } - [[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const { - return _allowedToSpeakNotifications.events(); - } - [[nodiscard]] rpl::producer<> titleChanged() const { - return _titleChanged.events(); - } - static constexpr auto kSpeakLevelThreshold = 0.2; - - void setCurrentAudioDevice(bool input, const QString &deviceId); - //void setAudioVolume(bool input, float level); - void setAudioDuckingEnabled(bool enabled); - - void toggleMute(const Group::MuteRequest &data); - void changeVolume(const Group::VolumeRequest &data); - std::variant> inviteUsers( - const std::vector> &users); - - std::shared_ptr ensureGlobalShortcutManager(); - void applyGlobalShortcutChanges(); - - void pushToTalk(bool pressed, crl::time delay); - - [[nodiscard]] rpl::lifetime &lifetime() { - return _lifetime; - } - -private: - class LoadPartTask; - -public: - void broadcastPartStart(std::shared_ptr task); - void broadcastPartCancel(not_null task); - -private: - using GlobalShortcutValue = base::GlobalShortcutValue; - - struct LoadingPart { - std::shared_ptr task; - mtpRequestId requestId = 0; - }; - - enum class FinishType { - None, - Ended, - Failed, - }; - enum class InstanceMode { - None, - Rtc, - Stream, - }; - enum class SendUpdateType { - Mute, - RaiseHand, - }; - - void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data); - void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data); - void handleUpdate(const MTPDupdateGroupCall &data); - void handleUpdate(const MTPDupdateGroupCallParticipants &data); - void handleRequestError(const MTP::Error &error); - void handleControllerError(const QString &error); - void ensureControllerCreated(); - void destroyController(); - - void setState(State state); - void finish(FinishType type); - void maybeSendMutedUpdate(MuteState previous); - void sendSelfUpdate(SendUpdateType type); - void updateInstanceMuteState(); - void updateInstanceVolumes(); - void applyMeInCallLocally(); - void rejoin(); - void rejoin(not_null as); - void setJoinAs(not_null as); - void saveDefaultJoinAs(not_null as); - void subscribeToReal(not_null real); - void setScheduledDate(TimeId date); - - void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data); - void setInstanceConnected(tgcalls::GroupNetworkState networkState); - void setInstanceMode(InstanceMode mode); - void checkLastSpoke(); - void pushToTalkCancel(); - - void checkGlobalShortcutAvailability(); - void checkJoined(); - void checkFirstTimeJoined(); - void notifyAboutAllowedToSpeak(); - - void playConnectingSound(); - void stopConnectingSound(); - void playConnectingSoundOnce(); - - void requestParticipantsInformation(const std::vector &ssrcs); - void addParticipantsToInstance(); - void prepareParticipantForAdding( - const Data::GroupCallParticipant &participant); - void addPreparedParticipants(); - void addPreparedParticipantsDelayed(); - - void editParticipant( - not_null participantPeer, - bool mute, - std::optional volume); - void applyParticipantLocally( - not_null participantPeer, - bool mute, - std::optional volume); - void applyQueuedSelfUpdates(); - void applySelfUpdate(const MTPDgroupCallParticipant &data); - void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data); - - [[nodiscard]] MTPInputGroupCall inputCall() const; - - const not_null _delegate; - not_null _peer; // Can change in legacy group migration. - rpl::event_stream _peerStream; - not_null _history; // Can change in legacy group migration. - MTP::Sender _api; - rpl::event_stream> _realChanges; - rpl::variable _state = State::Creating; - rpl::variable _instanceState - = InstanceState::Disconnected; - bool _instanceTransitioning = false; - InstanceMode _instanceMode = InstanceMode::None; - base::flat_set _unresolvedSsrcs; - std::vector _preparedParticipants; - bool _addPreparedParticipantsScheduled = false; - bool _recordingStoppedByMe = false; - - MTP::DcId _broadcastDcId = 0; - base::flat_map, LoadingPart> _broadcastParts; - - not_null _joinAs; - std::vector> _possibleJoinAs; - QString _joinHash; - - rpl::variable _muted = MuteState::Muted; - bool _initialMuteStateSent = false; - bool _acceptFields = false; - - rpl::event_stream _otherParticipantStateValue; - std::vector _queuedSelfUpdates; - - uint64 _id = 0; - uint64 _accessHash = 0; - uint32 _mySsrc = 0; - TimeId _scheduleDate = 0; - base::flat_set _mySsrcs; - mtpRequestId _createRequestId = 0; - mtpRequestId _updateMuteRequestId = 0; - - std::unique_ptr _instance; - rpl::event_stream _levelUpdates; - base::flat_map _lastSpoke; - rpl::event_stream _rejoinEvents; - rpl::event_stream<> _allowedToSpeakNotifications; - rpl::event_stream<> _titleChanged; - base::Timer _lastSpokeCheckTimer; - base::Timer _checkJoinedTimer; - - crl::time _lastSendProgressUpdate = 0; - - std::shared_ptr _shortcutManager; - std::shared_ptr _pushToTalk; - base::Timer _pushToTalkCancelTimer; - base::Timer _connectingSoundTimer; - bool _hadJoinedState = false; - - std::unique_ptr _mediaDevices; - QString _audioInputId; - QString _audioOutputId; - - rpl::lifetime _lifetime; - -}; - -} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp deleted file mode 100644 index caa700953..000000000 --- a/Telegram/SourceFiles/calls/calls_group_panel.cpp +++ /dev/null @@ -1,1564 +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 "calls/calls_group_panel.h" - -#include "calls/calls_group_common.h" -#include "calls/calls_group_members.h" -#include "calls/calls_group_settings.h" -#include "calls/calls_group_menu.h" -#include "ui/platform/ui_platform_window_title.h" -#include "ui/platform/ui_platform_utility.h" -#include "ui/controls/call_mute_button.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/window.h" -#include "ui/widgets/call_button.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/dropdown_menu.h" -#include "ui/widgets/input_fields.h" -#include "ui/chat/group_call_bar.h" -#include "ui/layers/layer_manager.h" -#include "ui/layers/generic_box.h" -#include "ui/text/text_utilities.h" -#include "ui/toasts/common_toasts.h" -#include "ui/special_buttons.h" -#include "info/profile/info_profile_values.h" // Info::Profile::Value. -#include "core/application.h" -#include "lang/lang_keys.h" -#include "data/data_channel.h" -#include "data/data_chat.h" -#include "data/data_user.h" -#include "data/data_group_call.h" -#include "data/data_session.h" -#include "data/data_changes.h" -#include "data/data_peer_values.h" -#include "main/main_session.h" -#include "base/event_filter.h" -#include "boxes/peers/edit_participants_box.h" -#include "boxes/peers/add_participants_box.h" -#include "boxes/peer_lists_box.h" -#include "boxes/confirm_box.h" -#include "base/unixtime.h" -#include "base/timer_rpl.h" -#include "app.h" -#include "apiwrap.h" // api().kickParticipant. -#include "styles/style_calls.h" -#include "styles/style_layers.h" - -#include -#include -#include - -namespace Calls::Group { -namespace { - -constexpr auto kSpacePushToTalkDelay = crl::time(250); -constexpr auto kRecordingAnimationDuration = crl::time(1200); -constexpr auto kRecordingOpacity = 0.6; -constexpr auto kStartNoConfirmation = TimeId(10); - -class InviteController final : public ParticipantsBoxController { -public: - InviteController( - not_null peer, - base::flat_set> alreadyIn); - - void prepare() override; - - void rowClicked(not_null row) override; - base::unique_qptr rowContextMenu( - QWidget *parent, - not_null row) override; - - void itemDeselectedHook(not_null peer) override; - - [[nodiscard]] auto peersWithRows() const - -> not_null>*>; - [[nodiscard]] rpl::producer> rowAdded() const; - - [[nodiscard]] bool hasRowFor(not_null peer) const; - -private: - [[nodiscard]] bool isAlreadyIn(not_null user) const; - - std::unique_ptr createRow( - not_null participant) const override; - - not_null _peer; - const base::flat_set> _alreadyIn; - mutable base::flat_set> _inGroup; - rpl::event_stream> _rowAdded; - -}; - -class InviteContactsController final : public AddParticipantsBoxController { -public: - InviteContactsController( - not_null peer, - base::flat_set> alreadyIn, - not_null>*> inGroup, - rpl::producer> discoveredInGroup); - -private: - void prepareViewHook() override; - - std::unique_ptr createRow( - not_null user) override; - - bool needsInviteLinkButton() override { - return false; - } - - const not_null>*> _inGroup; - rpl::producer> _discoveredInGroup; - - rpl::lifetime _lifetime; - -}; - -[[nodiscard]] rpl::producer StartsWhenText( - rpl::producer date) { - return std::move( - date - ) | rpl::map([](TimeId date) -> rpl::producer { - const auto parsedDate = base::unixtime::parse(date); - const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0)); - const auto previousDay = QDateTime( - parsedDate.date().addDays(-1), - QTime(0, 0)); - const auto now = QDateTime::currentDateTime(); - const auto kDay = int64(24 * 60 * 60); - const auto tillTomorrow = int64(now.secsTo(previousDay)); - const auto tillToday = tillTomorrow + kDay; - const auto tillAfter = tillToday + kDay; - - const auto time = parsedDate.time().toString( - QLocale::system().timeFormat(QLocale::ShortFormat)); - auto exact = tr::lng_group_call_starts_short_date( - lt_date, - rpl::single(langDayOfMonthFull(dateDay.date())), - lt_time, - rpl::single(time) - ) | rpl::type_erased(); - auto tomorrow = tr::lng_group_call_starts_short_tomorrow( - lt_time, - rpl::single(time)); - auto today = tr::lng_group_call_starts_short_today( - lt_time, - rpl::single(time)); - - auto todayAndAfter = rpl::single( - std::move(today) - ) | rpl::then(base::timer_once( - std::min(tillAfter, kDay) * crl::time(1000) - ) | rpl::map([=] { - return rpl::duplicate(exact); - })) | rpl::flatten_latest() | rpl::type_erased(); - - auto tomorrowAndAfter = rpl::single( - std::move(tomorrow) - ) | rpl::then(base::timer_once( - std::min(tillToday, kDay) * crl::time(1000) - ) | rpl::map([=] { - return rpl::duplicate(todayAndAfter); - })) | rpl::flatten_latest() | rpl::type_erased(); - - auto full = rpl::single( - rpl::duplicate(exact) - ) | rpl::then(base::timer_once( - tillTomorrow * crl::time(1000) - ) | rpl::map([=] { - return rpl::duplicate(tomorrowAndAfter); - })) | rpl::flatten_latest() | rpl::type_erased(); - - if (tillTomorrow > 0) { - return full; - } else if (tillToday > 0) { - return tomorrowAndAfter; - } else if (tillAfter > 0) { - return todayAndAfter; - } else { - return exact; - } - }) | rpl::flatten_latest(); -} - -[[nodiscard]] object_ptr CreateGradientLabel( - QWidget *parent, - rpl::producer text) { - struct State { - QBrush brush; - QPainterPath path; - }; - auto result = object_ptr(parent); - const auto raw = result.data(); - const auto state = raw->lifetime().make_state(); - - std::move( - text - ) | rpl::start_with_next([=](const QString &text) { - state->path = QPainterPath(); - const auto &font = st::groupCallCountdownFont; - state->path.addText(0, font->ascent, font->f, text); - const auto width = font->width(text); - raw->resize(width, font->height); - auto gradient = QLinearGradient(QPoint(width, 0), QPoint()); - gradient.setStops(QGradientStops{ - { 0.0, st::groupCallForceMutedBar1->c }, - { .7, st::groupCallForceMutedBar2->c }, - { 1.0, st::groupCallForceMutedBar3->c } - }); - state->brush = QBrush(std::move(gradient)); - raw->update(); - }, raw->lifetime()); - - raw->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(raw); - auto hq = PainterHighQualityEnabler(p); - const auto skip = st::groupCallWidth / 20; - const auto available = parent->width() - 2 * skip; - const auto full = raw->width(); - if (available > 0 && full > available) { - const auto scale = available / float64(full); - const auto shift = raw->rect().center(); - p.translate(shift); - p.scale(scale, scale); - p.translate(-shift); - } - p.setPen(Qt::NoPen); - p.setBrush(state->brush); - p.drawPath(state->path); - }, raw->lifetime()); - return result; -} - -[[nodiscard]] object_ptr CreateSectionSubtitle( - QWidget *parent, - rpl::producer text) { - auto result = object_ptr( - parent, - st::searchedBarHeight); - - const auto raw = result.data(); - raw->paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(raw); - p.fillRect(clip, st::groupCallMembersBgOver); - }, raw->lifetime()); - - const auto label = Ui::CreateChild( - raw, - std::move(text), - st::groupCallBoxLabel); - raw->widthValue( - ) | rpl::start_with_next([=](int width) { - const auto padding = st::groupCallInviteDividerPadding; - const auto available = width - padding.left() - padding.right(); - label->resizeToNaturalWidth(available); - label->moveToLeft(padding.left(), padding.top(), width); - }, label->lifetime()); - - return result; -} - -InviteController::InviteController( - not_null peer, - base::flat_set> alreadyIn) -: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members) -, _peer(peer) -, _alreadyIn(std::move(alreadyIn)) { - SubscribeToMigration( - _peer, - lifetime(), - [=](not_null channel) { _peer = channel; }); -} - -void InviteController::prepare() { - delegate()->peerListSetHideEmpty(true); - ParticipantsBoxController::prepare(); - delegate()->peerListSetAboveWidget(CreateSectionSubtitle( - nullptr, - tr::lng_group_call_invite_members())); - delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( - nullptr, - tr::lng_group_call_invite_members())); -} - -void InviteController::rowClicked(not_null row) { - delegate()->peerListSetRowChecked(row, !row->checked()); -} - -base::unique_qptr InviteController::rowContextMenu( - QWidget *parent, - not_null row) { - return nullptr; -} - -void InviteController::itemDeselectedHook(not_null peer) { -} - -bool InviteController::hasRowFor(not_null peer) const { - return (delegate()->peerListFindRow(peer->id.value) != nullptr); -} - -bool InviteController::isAlreadyIn(not_null user) const { - return _alreadyIn.contains(user); -} - -std::unique_ptr InviteController::createRow( - not_null participant) const { - const auto user = participant->asUser(); - if (!user || user->isSelf() || user->isBot()) { - return nullptr; - } - auto result = std::make_unique(user); - _rowAdded.fire_copy(user); - _inGroup.emplace(user); - if (isAlreadyIn(user)) { - result->setDisabledState(PeerListRow::State::DisabledChecked); - } - return result; -} - -auto InviteController::peersWithRows() const --> not_null>*> { - return &_inGroup; -} - -rpl::producer> InviteController::rowAdded() const { - return _rowAdded.events(); -} - -InviteContactsController::InviteContactsController( - not_null peer, - base::flat_set> alreadyIn, - not_null>*> inGroup, - rpl::producer> discoveredInGroup) -: AddParticipantsBoxController(peer, std::move(alreadyIn)) -, _inGroup(inGroup) -, _discoveredInGroup(std::move(discoveredInGroup)) { -} - -void InviteContactsController::prepareViewHook() { - AddParticipantsBoxController::prepareViewHook(); - - delegate()->peerListSetAboveWidget(CreateSectionSubtitle( - nullptr, - tr::lng_contacts_header())); - delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( - nullptr, - tr::lng_group_call_invite_search_results())); - - std::move( - _discoveredInGroup - ) | rpl::start_with_next([=](not_null user) { - if (auto row = delegate()->peerListFindRow(user->id.value)) { - delegate()->peerListRemoveRow(row); - } - }, _lifetime); -} - -std::unique_ptr InviteContactsController::createRow( - not_null user) { - return _inGroup->contains(user) - ? nullptr - : AddParticipantsBoxController::createRow(user); -} - -} // namespace - -Panel::Panel(not_null call) -: _call(call) -, _peer(call->peer()) -, _window(std::make_unique()) -, _layerBg(std::make_unique(_window->body())) -#ifndef Q_OS_MAC -, _controls(std::make_unique( - _window->body(), - st::groupCallTitle)) -#endif // !Q_OS_MAC -, _mute(std::make_unique( - widget(), - Core::App().appDeactivatedValue(), - Ui::CallMuteButtonState{ - .text = (_call->scheduleDate() - ? tr::lng_group_call_start_now(tr::now) - : tr::lng_group_call_connecting(tr::now)), - .type = (!_call->scheduleDate() - ? Ui::CallMuteButtonType::Connecting - : _peer->canManageGroupCall() - ? Ui::CallMuteButtonType::ScheduledCanStart - : _call->scheduleStartSubscribed() - ? Ui::CallMuteButtonType::ScheduledNotify - : Ui::CallMuteButtonType::ScheduledSilent), - })) -, _hangup(widget(), st::groupCallHangup) { - _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); - _layerBg->setHideByBackgroundClick(true); - - SubscribeToMigration( - _peer, - _window->lifetime(), - [=](not_null channel) { migrate(channel); }); - setupRealCallViewers(); - - initWindow(); - initWidget(); - initControls(); - initLayout(); - showAndActivate(); - setupJoinAsChangedToasts(); - setupTitleChangedToasts(); - setupAllowedToSpeakToasts(); -} - -Panel::~Panel() { - if (_menu) { - _menu.destroy(); - } -} - -void Panel::setupRealCallViewers() { - _call->real( - ) | rpl::start_with_next([=](not_null real) { - subscribeToChanges(real); - }, _window->lifetime()); -} - -bool Panel::isActive() const { - return _window->isActiveWindow() - && _window->isVisible() - && !(_window->windowState() & Qt::WindowMinimized); -} - -void Panel::minimize() { - _window->setWindowState(_window->windowState() | Qt::WindowMinimized); -} - -void Panel::close() { - _window->close(); -} - -void Panel::showAndActivate() { - if (_window->isHidden()) { - _window->show(); - } - const auto state = _window->windowState(); - if (state & Qt::WindowMinimized) { - _window->setWindowState(state & ~Qt::WindowMinimized); - } - _window->raise(); - _window->activateWindow(); - _window->setFocus(); -} - -void Panel::migrate(not_null channel) { - _peer = channel; - _peerLifetime.destroy(); - subscribeToPeerChanges(); - _title.destroy(); - refreshTitle(); -} - -void Panel::subscribeToPeerChanges() { - Info::Profile::NameValue( - _peer - ) | rpl::start_with_next([=](const TextWithEntities &name) { - _window->setTitle(name.text); - }, _peerLifetime); -} - -void Panel::initWindow() { - _window->setAttribute(Qt::WA_OpaquePaintEvent); - _window->setAttribute(Qt::WA_NoSystemBackground); - _window->setWindowIcon( - QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); - _window->setTitleStyle(st::groupCallTitle); - - subscribeToPeerChanges(); - - base::install_event_filter(_window.get(), [=](not_null e) { - if (e->type() == QEvent::Close && handleClose()) { - e->ignore(); - return base::EventFilterResult::Cancel; - } else if (e->type() == QEvent::KeyPress - || e->type() == QEvent::KeyRelease) { - if (static_cast(e.get())->key() == Qt::Key_Space) { - _call->pushToTalk( - e->type() == QEvent::KeyPress, - kSpacePushToTalkDelay); - } - } - return base::EventFilterResult::Continue; - }); - - _window->setBodyTitleArea([=](QPoint widgetPoint) { - using Flag = Ui::WindowTitleHitTestFlag; - const auto titleRect = QRect( - 0, - 0, - widget()->width(), - st::groupCallMembersTop); - return (titleRect.contains(widgetPoint) - && (!_menuToggle || !_menuToggle->geometry().contains(widgetPoint)) - && (!_menu || !_menu->geometry().contains(widgetPoint)) - && (!_recordingMark || !_recordingMark->geometry().contains(widgetPoint)) - && (!_joinAsToggle || !_joinAsToggle->geometry().contains(widgetPoint))) - ? (Flag::Move | Flag::Maximize) - : Flag::None; - }); -} - -void Panel::initWidget() { - widget()->setMouseTracking(true); - - widget()->paintRequest( - ) | rpl::start_with_next([=](QRect clip) { - paint(clip); - }, widget()->lifetime()); - - widget()->sizeValue( - ) | rpl::skip(1) | rpl::start_with_next([=] { - updateControlsGeometry(); - - // title geometry depends on _controls->geometry, - // which is not updated here yet. - crl::on_main(widget(), [=] { refreshTitle(); }); - }, widget()->lifetime()); -} - -void Panel::endCall() { - if (!_call->peer()->canManageGroupCall()) { - _call->hangup(); - return; - } - _layerBg->showBox(Box( - LeaveBox, - _call, - false, - BoxContext::GroupCallPanel)); -} - -void Panel::startScheduledNow() { - const auto date = _call->scheduleDate(); - const auto now = base::unixtime::now(); - if (!date) { - return; - } else if (now + kStartNoConfirmation >= date) { - _call->startScheduledNow(); - } else { - const auto box = std::make_shared>(); - const auto done = [=] { - if (*box) { - (*box)->closeBox(); - } - _call->startScheduledNow(); - }; - auto owned = ConfirmBox({ - .text = { tr::lng_group_call_start_now_sure(tr::now) }, - .button = tr::lng_group_call_start_now(), - .callback = done, - }); - *box = owned.data(); - _layerBg->showBox(std::move(owned)); - } -} - -void Panel::initControls() { - _mute->clicks( - ) | rpl::filter([=](Qt::MouseButton button) { - return (button == Qt::LeftButton); - }) | rpl::start_with_next([=] { - if (_call->scheduleDate()) { - if (_peer->canManageGroupCall()) { - startScheduledNow(); - } else if (const auto real = _call->lookupReal()) { - _call->toggleScheduleStartSubscribed( - !real->scheduleStartSubscribed()); - } - return; - } - const auto oldState = _call->muted(); - const auto newState = (oldState == MuteState::ForceMuted) - ? MuteState::RaisedHand - : (oldState == MuteState::RaisedHand) - ? MuteState::RaisedHand - : (oldState == MuteState::Muted) - ? MuteState::Active - : MuteState::Muted; - _call->setMutedAndUpdate(newState); - }, _mute->lifetime()); - - initShareAction(); - refreshLeftButton(); - - _hangup->setClickedCallback([=] { endCall(); }); - - const auto scheduleDate = _call->scheduleDate(); - _hangup->setText(scheduleDate - ? tr::lng_group_call_close() - : tr::lng_group_call_leave()); - if (scheduleDate) { - auto changes = _call->real( - ) | rpl::map([=](not_null real) { - return real->scheduleDateValue(); - }) | rpl::flatten_latest(); - - setupScheduledLabels(rpl::single( - scheduleDate - ) | rpl::then(rpl::duplicate(changes))); - - auto started = std::move(changes) | rpl::filter([](TimeId date) { - return (date == 0); - }) | rpl::take(1); - - rpl::merge( - rpl::duplicate(started) | rpl::to_empty, - _peer->session().changes().peerFlagsValue( - _peer, - Data::PeerUpdate::Flag::Username - ) | rpl::skip(1) | rpl::to_empty - ) | rpl::start_with_next([=] { - refreshLeftButton(); - updateControlsGeometry(); - }, _callLifetime); - - std::move(started) | rpl::start_with_next([=] { - _hangup->setText(tr::lng_group_call_leave()); - setupMembers(); - }, _callLifetime); - } - - _call->stateValue( - ) | rpl::filter([](State state) { - return (state == State::HangingUp) - || (state == State::Ended) - || (state == State::FailedHangingUp) - || (state == State::Failed); - }) | rpl::start_with_next([=] { - closeBeforeDestroy(); - }, _callLifetime); - - _call->levelUpdates( - ) | rpl::filter([=](const LevelUpdate &update) { - return update.me; - }) | rpl::start_with_next([=](const LevelUpdate &update) { - _mute->setLevel(update.value); - }, _callLifetime); - - _call->real( - ) | rpl::start_with_next([=](not_null real) { - setupRealMuteButtonState(real); - }, _callLifetime); -} - -void Panel::refreshLeftButton() { - const auto share = _call->scheduleDate() - && _peer->isBroadcast() - && _peer->asChannel()->hasUsername(); - if ((share && _share) || (!share && _settings)) { - return; - } - if (share) { - _settings.destroy(); - _share.create(widget(), st::groupCallShare); - _share->setClickedCallback(_shareLinkCallback); - _share->setText(tr::lng_group_call_share_button()); - } else { - _share.destroy(); - _settings.create(widget(), st::groupCallSettings); - _settings->setClickedCallback([=] { - _layerBg->showBox(Box(SettingsBox, _call)); - }); - _settings->setText(tr::lng_group_call_settings()); - } - const auto raw = _share ? _share.data() : _settings.data(); - raw->show(); - raw->setColorOverrides(_mute->colorOverrides()); -} - -void Panel::initShareAction() { - const auto showBox = [=](object_ptr next) { - _layerBg->showBox(std::move(next)); - }; - const auto showToast = [=](QString text) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = { text }, - }); - }; - auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction( - _peer, - showBox, - showToast); - _shareLinkCallback = [=, callback = std::move(shareLinkCallback)] { - if (_call->lookupReal()) { - callback(); - } - }; - widget()->lifetime().add(std::move(shareLinkLifetime)); -} - -void Panel::setupRealMuteButtonState(not_null real) { - using namespace rpl::mappers; - rpl::combine( - _call->mutedValue() | MapPushToTalkToActive(), - _call->instanceStateValue(), - real->scheduleDateValue(), - real->scheduleStartSubscribedValue(), - Data::CanManageGroupCallValue(_peer) - ) | rpl::distinct_until_changed( - ) | rpl::filter( - _2 != GroupCall::InstanceState::TransitionToRtc - ) | rpl::start_with_next([=]( - MuteState mute, - GroupCall::InstanceState state, - TimeId scheduleDate, - bool scheduleStartSubscribed, - bool canManage) { - using Type = Ui::CallMuteButtonType; - _mute->setState(Ui::CallMuteButtonState{ - .text = (scheduleDate - ? (canManage - ? tr::lng_group_call_start_now(tr::now) - : scheduleStartSubscribed - ? tr::lng_group_call_cancel_reminder(tr::now) - : tr::lng_group_call_set_reminder(tr::now)) - : state == GroupCall::InstanceState::Disconnected - ? tr::lng_group_call_connecting(tr::now) - : mute == MuteState::ForceMuted - ? tr::lng_group_call_force_muted(tr::now) - : mute == MuteState::RaisedHand - ? tr::lng_group_call_raised_hand(tr::now) - : mute == MuteState::Muted - ? tr::lng_group_call_unmute(tr::now) - : tr::lng_group_call_you_are_live(tr::now)), - .subtext = (scheduleDate - ? QString() - : state == GroupCall::InstanceState::Disconnected - ? QString() - : mute == MuteState::ForceMuted - ? tr::lng_group_call_raise_hand_tip(tr::now) - : mute == MuteState::RaisedHand - ? tr::lng_group_call_raised_hand_sub(tr::now) - : mute == MuteState::Muted - ? tr::lng_group_call_unmute_sub(tr::now) - : QString()), - .type = (scheduleDate - ? (canManage - ? Type::ScheduledCanStart - : scheduleStartSubscribed - ? Type::ScheduledNotify - : Type::ScheduledSilent) - : state == GroupCall::InstanceState::Disconnected - ? Type::Connecting - : mute == MuteState::ForceMuted - ? Type::ForceMuted - : mute == MuteState::RaisedHand - ? Type::RaisedHand - : mute == MuteState::Muted - ? Type::Muted - : Type::Active), - }); - }, _callLifetime); -} - -void Panel::setupScheduledLabels(rpl::producer date) { - using namespace rpl::mappers; - date = std::move(date) | rpl::take_while(_1 != 0); - _startsWhen.create( - widget(), - StartsWhenText(rpl::duplicate(date)), - st::groupCallStartsWhen); - auto countdownCreated = std::move( - date - ) | rpl::map([=](TimeId date) { - _countdownData = std::make_shared(date); - return rpl::empty_value(); - }) | rpl::start_spawning(widget()->lifetime()); - - _countdown = CreateGradientLabel(widget(), rpl::duplicate( - countdownCreated - ) | rpl::map([=] { - return _countdownData->text( - Ui::GroupCallScheduledLeft::Negative::Ignore); - }) | rpl::flatten_latest()); - - _startsIn.create( - widget(), - rpl::conditional( - std::move( - countdownCreated - ) | rpl::map( - [=] { return _countdownData->late(); } - ) | rpl::flatten_latest(), - tr::lng_group_call_late_by(), - tr::lng_group_call_starts_in()), - st::groupCallStartsIn); - - const auto top = [=] { - const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; - const auto membersTop = st::groupCallMembersTop; - const auto height = st::groupCallScheduledBodyHeight; - return (membersTop + (muteTop - membersTop - height) / 2); - }; - rpl::combine( - widget()->sizeValue(), - _startsIn->widthValue() - ) | rpl::start_with_next([=](QSize size, int width) { - _startsIn->move( - (size.width() - width) / 2, - top() + st::groupCallStartsInTop); - }, _startsIn->lifetime()); - - rpl::combine( - widget()->sizeValue(), - _startsWhen->widthValue() - ) | rpl::start_with_next([=](QSize size, int width) { - _startsWhen->move( - (size.width() - width) / 2, - top() + st::groupCallStartsWhenTop); - }, _startsWhen->lifetime()); - - rpl::combine( - widget()->sizeValue(), - _countdown->widthValue() - ) | rpl::start_with_next([=](QSize size, int width) { - _countdown->move( - (size.width() - width) / 2, - top() + st::groupCallCountdownTop); - }, _startsWhen->lifetime()); -} - -void Panel::setupMembers() { - if (_members) { - return; - } - - _startsIn.destroy(); - _countdown.destroy(); - _startsWhen.destroy(); - - _members.create(widget(), _call); - _members->show(); - - _members->desiredHeightValue( - ) | rpl::start_with_next([=] { - updateMembersGeometry(); - }, _members->lifetime()); - - _members->toggleMuteRequests( - ) | rpl::start_with_next([=](MuteRequest request) { - if (_call) { - _call->toggleMute(request); - } - }, _callLifetime); - - _members->changeVolumeRequests( - ) | rpl::start_with_next([=](VolumeRequest request) { - if (_call) { - _call->changeVolume(request); - } - }, _callLifetime); - - _members->kickParticipantRequests( - ) | rpl::start_with_next([=](not_null participantPeer) { - kickParticipant(participantPeer); - }, _callLifetime); - - _members->addMembersRequests( - ) | rpl::start_with_next([=] { - if (_peer->isBroadcast() && _peer->asChannel()->hasUsername()) { - _shareLinkCallback(); - } else { - addMembers(); - } - }, _callLifetime); -} - -void Panel::setupJoinAsChangedToasts() { - _call->rejoinEvents( - ) | rpl::filter([](RejoinEvent event) { - return (event.wasJoinAs != event.nowJoinAs); - }) | rpl::map([=] { - return _call->stateValue() | rpl::filter([](State state) { - return (state == State::Joined); - }) | rpl::take(1); - }) | rpl::flatten_latest() | rpl::start_with_next([=] { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = tr::lng_group_call_join_as_changed( - tr::now, - lt_name, - Ui::Text::Bold(_call->joinAs()->name), - Ui::Text::WithEntities), - }); - }, widget()->lifetime()); -} - -void Panel::setupTitleChangedToasts() { - _call->titleChanged( - ) | rpl::filter([=] { - return (_call->lookupReal() != nullptr); - }) | rpl::map([=] { - return _peer->groupCall()->title().isEmpty() - ? _peer->name - : _peer->groupCall()->title(); - }) | rpl::start_with_next([=](const QString &title) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = tr::lng_group_call_title_changed( - tr::now, - lt_title, - Ui::Text::Bold(title), - Ui::Text::WithEntities), - }); - }, widget()->lifetime()); -} - -void Panel::setupAllowedToSpeakToasts() { - _call->allowedToSpeakNotifications( - ) | rpl::start_with_next([=] { - if (isActive()) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = { tr::lng_group_call_can_speak_here(tr::now) }, - }); - } else { - const auto real = _call->lookupReal(); - const auto name = (real && !real->title().isEmpty()) - ? real->title() - : _peer->name; - Ui::ShowMultilineToast({ - .text = tr::lng_group_call_can_speak( - tr::now, - lt_chat, - Ui::Text::Bold(name), - Ui::Text::WithEntities), - }); - } - }, widget()->lifetime()); -} - -void Panel::subscribeToChanges(not_null real) { - const auto validateRecordingMark = [=](bool recording) { - if (!recording && _recordingMark) { - _recordingMark.destroy(); - } else if (recording && !_recordingMark) { - struct State { - Ui::Animations::Simple animation; - base::Timer timer; - bool opaque = true; - }; - _recordingMark.create(widget()); - _recordingMark->show(); - const auto state = _recordingMark->lifetime().make_state(); - const auto size = st::groupCallRecordingMark; - const auto skip = st::groupCallRecordingMarkSkip; - _recordingMark->resize(size + 2 * skip, size + 2 * skip); - _recordingMark->setClickedCallback([=] { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = { tr::lng_group_call_is_recorded(tr::now) }, - }); - }); - const auto animate = [=] { - const auto opaque = state->opaque; - state->opaque = !opaque; - state->animation.start( - [=] { _recordingMark->update(); }, - opaque ? 1. : kRecordingOpacity, - opaque ? kRecordingOpacity : 1., - kRecordingAnimationDuration); - }; - state->timer.setCallback(animate); - state->timer.callEach(kRecordingAnimationDuration); - animate(); - - _recordingMark->paintRequest( - ) | rpl::start_with_next([=] { - auto p = QPainter(_recordingMark.data()); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::groupCallMemberMutedIcon); - p.setOpacity(state->animation.value( - state->opaque ? 1. : kRecordingOpacity)); - p.drawEllipse(skip, skip, size, size); - }, _recordingMark->lifetime()); - } - refreshTitleGeometry(); - }; - - using namespace rpl::mappers; - real->recordStartDateChanges( - ) | rpl::map( - _1 != 0 - ) | rpl::distinct_until_changed( - ) | rpl::start_with_next([=](bool recorded) { - validateRecordingMark(recorded); - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = (recorded - ? tr::lng_group_call_recording_started - : _call->recordingStoppedByMe() - ? tr::lng_group_call_recording_saved - : tr::lng_group_call_recording_stopped)( - tr::now, - Ui::Text::RichLangValue), - }); - }, widget()->lifetime()); - validateRecordingMark(real->recordStartDate() != 0); - - const auto showMenu = _peer->canManageGroupCall(); - const auto showUserpic = !showMenu && _call->showChooseJoinAs(); - if (showMenu) { - _joinAsToggle.destroy(); - if (!_menuToggle) { - _menuToggle.create(widget(), st::groupCallMenuToggle); - _menuToggle->show(); - _menuToggle->setClickedCallback([=] { showMainMenu(); }); - } - } else if (showUserpic) { - _menuToggle.destroy(); - rpl::single( - _call->joinAs() - ) | rpl::then(_call->rejoinEvents( - ) | rpl::map([](const RejoinEvent &event) { - return event.nowJoinAs; - })) | rpl::start_with_next([=](not_null joinAs) { - auto joinAsToggle = object_ptr( - widget(), - joinAs, - Ui::UserpicButton::Role::Custom, - st::groupCallJoinAsToggle); - _joinAsToggle.destroy(); - _joinAsToggle = std::move(joinAsToggle); - _joinAsToggle->show(); - _joinAsToggle->setClickedCallback([=] { - chooseJoinAs(); - }); - updateControlsGeometry(); - }, widget()->lifetime()); - } else { - _menuToggle.destroy(); - _joinAsToggle.destroy(); - } - updateControlsGeometry(); -} - -void Panel::chooseJoinAs() { - const auto context = ChooseJoinAsProcess::Context::Switch; - const auto callback = [=](JoinInfo info) { - _call->rejoinAs(info); - }; - const auto showBox = [=](object_ptr next) { - _layerBg->showBox(std::move(next)); - }; - const auto showToast = [=](QString text) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = { text }, - }); - }; - _joinAsProcess.start( - _peer, - context, - showBox, - showToast, - callback, - _call->joinAs()); -} - -void Panel::showMainMenu() { - if (_menu) { - return; - } - _menu.create(widget(), st::groupCallDropdownMenu); - FillMenu( - _menu.data(), - _peer, - _call, - [=] { chooseJoinAs(); }, - [=](auto box) { _layerBg->showBox(std::move(box)); }); - if (_menu->empty()) { - _menu.destroy(); - return; - } - - const auto raw = _menu.data(); - raw->setHiddenCallback([=] { - raw->deleteLater(); - if (_menu == raw) { - _menu = nullptr; - _menuToggle->setForceRippled(false); - } - }); - raw->setShowStartCallback([=] { - if (_menu == raw) { - _menuToggle->setForceRippled(true); - } - }); - raw->setHideStartCallback([=] { - if (_menu == raw) { - _menuToggle->setForceRippled(false); - } - }); - _menuToggle->installEventFilter(_menu); - - const auto x = st::groupCallMenuPosition.x(); - const auto y = st::groupCallMenuPosition.y(); - if (_menuToggle->x() > widget()->width() / 2) { - _menu->moveToRight(x, y); - _menu->showAnimated(Ui::PanelAnimation::Origin::TopRight); - } else { - _menu->moveToLeft(x, y); - _menu->showAnimated(Ui::PanelAnimation::Origin::TopLeft); - } -} - -void Panel::addMembers() { - const auto real = _call->lookupReal(); - if (!real) { - return; - } - auto alreadyIn = _peer->owner().invitedToCallUsers(real->id()); - for (const auto &participant : real->participants()) { - if (const auto user = participant.peer->asUser()) { - alreadyIn.emplace(user); - } - } - alreadyIn.emplace(_peer->session().user()); - auto controller = std::make_unique( - _peer, - alreadyIn); - controller->setStyleOverrides( - &st::groupCallInviteMembersList, - &st::groupCallMultiSelect); - - auto contactsController = std::make_unique( - _peer, - std::move(alreadyIn), - controller->peersWithRows(), - controller->rowAdded()); - contactsController->setStyleOverrides( - &st::groupCallInviteMembersList, - &st::groupCallMultiSelect); - - const auto weak = base::make_weak(_call.get()); - const auto invite = [=](const std::vector> &users) { - const auto call = weak.get(); - if (!call) { - return; - } - const auto result = call->inviteUsers(users); - if (const auto user = std::get_if>(&result)) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = tr::lng_group_call_invite_done_user( - tr::now, - lt_user, - Ui::Text::Bold((*user)->firstName), - Ui::Text::WithEntities), - }); - } else if (const auto count = std::get_if(&result)) { - if (*count > 0) { - Ui::ShowMultilineToast({ - .parentOverride = widget(), - .text = tr::lng_group_call_invite_done_many( - tr::now, - lt_count, - *count, - Ui::Text::RichLangValue), - }); - } - } else { - Unexpected("Result in GroupCall::inviteUsers."); - } - }; - const auto inviteWithAdd = [=]( - const std::vector> &users, - const std::vector> &nonMembers, - Fn finish) { - _peer->session().api().addChatParticipants( - _peer, - nonMembers, - [=](bool) { invite(users); finish(); }); - }; - const auto inviteWithConfirmation = [=]( - const std::vector> &users, - const std::vector> &nonMembers, - Fn finish) { - if (nonMembers.empty()) { - invite(users); - finish(); - return; - } - const auto name = _peer->name; - const auto text = (nonMembers.size() == 1) - ? tr::lng_group_call_add_to_group_one( - tr::now, - lt_user, - nonMembers.front()->shortName(), - lt_group, - name) - : (nonMembers.size() < users.size()) - ? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name) - : tr::lng_group_call_add_to_group_all(tr::now, lt_group, name); - const auto shared = std::make_shared>(); - const auto finishWithConfirm = [=] { - if (*shared) { - (*shared)->closeBox(); - } - finish(); - }; - const auto done = [=] { - inviteWithAdd(users, nonMembers, finishWithConfirm); - }; - auto box = ConfirmBox({ - .text = { text }, - .button = tr::lng_participant_invite(), - .callback = done, - }); - *shared = box.data(); - _layerBg->showBox(std::move(box)); - }; - auto initBox = [=, controller = controller.get()]( - not_null box) { - box->setTitle(tr::lng_group_call_invite_title()); - box->addButton(tr::lng_group_call_invite_button(), [=] { - const auto rows = box->collectSelectedRows(); - - const auto users = ranges::views::all( - rows - ) | ranges::views::transform([](not_null peer) { - return not_null(peer->asUser()); - }) | ranges::to_vector; - - const auto nonMembers = ranges::views::all( - users - ) | ranges::views::filter([&](not_null user) { - return !controller->hasRowFor(user); - }) | ranges::to_vector; - - const auto finish = [box = Ui::MakeWeak(box)]() { - if (box) { - box->closeBox(); - } - }; - inviteWithConfirmation(users, nonMembers, finish); - }); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - }; - - auto controllers = std::vector>(); - controllers.push_back(std::move(controller)); - controllers.push_back(std::move(contactsController)); - _layerBg->showBox(Box(std::move(controllers), initBox)); -} - -void Panel::kickParticipant(not_null participantPeer) { - _layerBg->showBox(Box([=](not_null box) { - box->addRow( - object_ptr( - box.get(), - (!participantPeer->isUser() - ? tr::lng_group_call_remove_channel( - tr::now, - lt_channel, - participantPeer->name) - : (_peer->isBroadcast() - ? tr::lng_profile_sure_kick_channel - : tr::lng_profile_sure_kick)( - tr::now, - lt_user, - participantPeer->asUser()->firstName)), - st::groupCallBoxLabel), - style::margins( - st::boxRowPadding.left(), - st::boxPadding.top(), - st::boxRowPadding.right(), - st::boxPadding.bottom())); - box->addButton(tr::lng_box_remove(), [=] { - box->closeBox(); - kickParticipantSure(participantPeer); - }); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); - })); -} - -void Panel::kickParticipantSure(not_null participantPeer) { - if (const auto chat = _peer->asChat()) { - chat->session().api().kickParticipant(chat, participantPeer); - } else if (const auto channel = _peer->asChannel()) { - const auto currentRestrictedRights = [&] { - const auto user = participantPeer->asUser(); - if (!channel->mgInfo || !user) { - return ChannelData::EmptyRestrictedRights(participantPeer); - } - const auto i = channel->mgInfo->lastRestricted.find(user); - return (i != channel->mgInfo->lastRestricted.cend()) - ? i->second.rights - : ChannelData::EmptyRestrictedRights(participantPeer); - }(); - channel->session().api().kickParticipant( - channel, - participantPeer, - currentRestrictedRights); - } -} - -void Panel::initLayout() { - initGeometry(); - -#ifndef Q_OS_MAC - _controls->raise(); - - Ui::Platform::TitleControlsLayoutChanged( - ) | rpl::start_with_next([=] { - // _menuToggle geometry depends on _controls arrangement. - crl::on_main(widget(), [=] { updateControlsGeometry(); }); - }, widget()->lifetime()); - -#endif // !Q_OS_MAC -} - -void Panel::showControls() { - Expects(_call != nullptr); - - widget()->showChildren(); -} - -void Panel::closeBeforeDestroy() { - _window->close(); - _callLifetime.destroy(); -} - -void Panel::initGeometry() { - const auto center = Core::App().getPointForCallPanelCenter(); - const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight); - _window->setGeometry(rect.translated(center - rect.center())); - _window->setMinimumSize(rect.size()); - _window->show(); - updateControlsGeometry(); -} - -QRect Panel::computeTitleRect() const { - const auto skip = st::groupCallTitleTop; - const auto remove = skip + (_menuToggle - ? (_menuToggle->width() + st::groupCallMenuTogglePosition.x()) - : 0) + (_joinAsToggle - ? (_joinAsToggle->width() + st::groupCallMenuTogglePosition.x()) - : 0); - const auto width = widget()->width(); -#ifdef Q_OS_MAC - return QRect(70, 0, width - remove - 70, 28); -#else // Q_OS_MAC - const auto controls = _controls->geometry(); - const auto right = controls.x() + controls.width() + skip; - return (controls.center().x() < width / 2) - ? QRect(right, 0, width - right - remove, controls.height()) - : QRect(remove, 0, controls.x() - skip - remove, controls.height()); -#endif // !Q_OS_MAC -} - -void Panel::updateControlsGeometry() { - if (widget()->size().isEmpty() || (!_settings && !_share)) { - return; - } - const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; - const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip; - const auto muteSize = _mute->innerSize().width(); - const auto fullWidth = muteSize - + 2 * (_settings ? _settings : _share)->width() - + 2 * st::groupCallButtonSkip; - _mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop }); - const auto leftButtonLeft = (widget()->width() - fullWidth) / 2; - if (_settings) { - _settings->moveToLeft(leftButtonLeft, buttonsTop); - } - if (_share) { - _share->moveToLeft(leftButtonLeft, buttonsTop); - } - _hangup->moveToRight(leftButtonLeft, buttonsTop); - - updateMembersGeometry(); - refreshTitle(); - -#ifdef Q_OS_MAC - const auto controlsOnTheLeft = true; -#else // Q_OS_MAC - const auto controlsOnTheLeft = _controls->geometry().center().x() - < widget()->width() / 2; -#endif // Q_OS_MAC - const auto menux = st::groupCallMenuTogglePosition.x(); - const auto menuy = st::groupCallMenuTogglePosition.y(); - if (controlsOnTheLeft) { - if (_menuToggle) { - _menuToggle->moveToRight(menux, menuy); - } else if (_joinAsToggle) { - _joinAsToggle->moveToRight(menux, menuy); - } - } else { - if (_menuToggle) { - _menuToggle->moveToLeft(menux, menuy); - } else if (_joinAsToggle) { - _joinAsToggle->moveToLeft(menux, menuy); - } - } -} - -void Panel::updateMembersGeometry() { - if (!_members) { - return; - } - const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; - const auto membersTop = st::groupCallMembersTop; - const auto availableHeight = muteTop - - membersTop - - st::groupCallMembersMargin.bottom(); - const auto desiredHeight = _members->desiredHeight(); - const auto membersWidthAvailable = widget()->width() - - st::groupCallMembersMargin.left() - - st::groupCallMembersMargin.right(); - const auto membersWidthMin = st::groupCallWidth - - st::groupCallMembersMargin.left() - - st::groupCallMembersMargin.right(); - const auto membersWidth = std::clamp( - membersWidthAvailable, - membersWidthMin, - st::groupCallMembersWidthMax); - _members->setGeometry( - (widget()->width() - membersWidth) / 2, - membersTop, - membersWidth, - std::min(desiredHeight, availableHeight)); -} - -void Panel::refreshTitle() { - if (!_title) { - auto text = rpl::combine( - Info::Profile::NameValue(_peer), - rpl::single( - QString() - ) | rpl::then(_call->real( - ) | rpl::map([=](not_null real) { - return real->titleValue(); - }) | rpl::flatten_latest()) - ) | rpl::map([=]( - const TextWithEntities &name, - const QString &title) { - return title.isEmpty() ? name.text : title; - }) | rpl::after_next([=] { - refreshTitleGeometry(); - }); - _title.create( - widget(), - rpl::duplicate(text), - st::groupCallTitleLabel); - _title->show(); - _title->setAttribute(Qt::WA_TransparentForMouseEvents); - } - refreshTitleGeometry(); - if (!_subtitle) { - _subtitle.create( - widget(), - rpl::single( - _call->scheduleDate() - ) | rpl::then( - _call->real( - ) | rpl::map([=](not_null real) { - return real->scheduleDateValue(); - }) | rpl::flatten_latest() - ) | rpl::map([=](TimeId scheduleDate) { - if (scheduleDate) { - return tr::lng_group_call_scheduled_status(); - } else if (!_members) { - setupMembers(); - } - return tr::lng_group_call_members( - lt_count_decimal, - _members->fullCountValue() | rpl::map([](int value) { - return (value > 0) ? float64(value) : 1.; - })); - }) | rpl::flatten_latest(), - st::groupCallSubtitleLabel); - _subtitle->show(); - _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); - } - const auto middle = _title - ? (_title->x() + _title->width() / 2) - : (widget()->width() / 2); - const auto top = _title - ? st::groupCallSubtitleTop - : st::groupCallTitleTop; - _subtitle->moveToLeft( - (widget()->width() - _subtitle->width()) / 2, - top); -} - -void Panel::refreshTitleGeometry() { - if (!_title) { - return; - } - const auto fullRect = computeTitleRect(); - const auto recordingWidth = 2 * st::groupCallRecordingMarkSkip - + st::groupCallRecordingMark; - const auto titleRect = _recordingMark - ? QRect( - fullRect.x(), - fullRect.y(), - fullRect.width() - _recordingMark->width(), - fullRect.height()) - : fullRect; - const auto best = _title->naturalWidth(); - const auto from = (widget()->width() - best) / 2; - const auto top = st::groupCallTitleTop; - const auto left = titleRect.x(); - if (from >= left && from + best <= left + titleRect.width()) { - _title->resizeToWidth(best); - _title->moveToLeft(from, top); - } else if (titleRect.width() < best) { - _title->resizeToWidth(titleRect.width()); - _title->moveToLeft(left, top); - } else if (from < left) { - _title->resizeToWidth(best); - _title->moveToLeft(left, top); - } else { - _title->resizeToWidth(best); - _title->moveToLeft(left + titleRect.width() - best, top); - } - if (_recordingMark) { - const auto markTop = top + st::groupCallRecordingMarkTop; - _recordingMark->move( - _title->x() + _title->width(), - markTop - st::groupCallRecordingMarkSkip); - } -} - -void Panel::paint(QRect clip) { - Painter p(widget()); - - auto region = QRegion(clip); - for (const auto rect : region) { - p.fillRect(rect, st::groupCallBg); - } -} - -bool Panel::handleClose() { - if (_call) { - _window->hide(); - return true; - } - return false; -} - -not_null Panel::widget() const { - return _window->body(); -} - -} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h deleted file mode 100644 index 71d68042c..000000000 --- a/Telegram/SourceFiles/calls/calls_group_panel.h +++ /dev/null @@ -1,146 +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 -*/ -#pragma once - -#include "base/weak_ptr.h" -#include "base/timer.h" -#include "base/object_ptr.h" -#include "calls/calls_group_call.h" -#include "calls/calls_choose_join_as.h" -#include "ui/effects/animations.h" -#include "ui/rp_widget.h" - -class Image; - -namespace Data { -class PhotoMedia; -class CloudImageView; -class GroupCall; -} // namespace Data - -namespace Ui { -class AbstractButton; -class DropdownMenu; -class CallButton; -class CallMuteButton; -class IconButton; -class FlatLabel; -template -class FadeWrap; -template -class PaddingWrap; -class Window; -class ScrollArea; -class GenericBox; -class LayerManager; -class GroupCallScheduledLeft; -namespace Platform { -class TitleControls; -} // namespace Platform -} // namespace Ui - -namespace style { -struct CallSignalBars; -struct CallBodyLayout; -} // namespace style - -namespace Calls::Group { - -class Members; - -class Panel final { -public: - Panel(not_null call); - ~Panel(); - - [[nodiscard]] bool isActive() const; - void minimize(); - void close(); - void showAndActivate(); - void closeBeforeDestroy(); - -private: - using State = GroupCall::State; - - [[nodiscard]] not_null widget() const; - - void paint(QRect clip); - - void initWindow(); - void initWidget(); - void initControls(); - void initShareAction(); - void initLayout(); - void initGeometry(); - void setupScheduledLabels(rpl::producer date); - void setupMembers(); - void setupJoinAsChangedToasts(); - void setupTitleChangedToasts(); - void setupAllowedToSpeakToasts(); - void setupRealMuteButtonState(not_null real); - - bool handleClose(); - void startScheduledNow(); - - void updateControlsGeometry(); - void updateMembersGeometry(); - void showControls(); - void refreshLeftButton(); - - void endCall(); - - void showMainMenu(); - void chooseJoinAs(); - void addMembers(); - void kickParticipant(not_null participantPeer); - void kickParticipantSure(not_null participantPeer); - [[nodiscard]] QRect computeTitleRect() const; - void refreshTitle(); - void refreshTitleGeometry(); - void setupRealCallViewers(); - void subscribeToChanges(not_null real); - - void migrate(not_null channel); - void subscribeToPeerChanges(); - - const not_null _call; - not_null _peer; - - const std::unique_ptr _window; - const std::unique_ptr _layerBg; - -#ifndef Q_OS_MAC - std::unique_ptr _controls; -#endif // !Q_OS_MAC - - rpl::lifetime _callLifetime; - - object_ptr _title = { nullptr }; - object_ptr _subtitle = { nullptr }; - object_ptr _recordingMark = { nullptr }; - object_ptr _menuToggle = { nullptr }; - object_ptr _menu = { nullptr }; - object_ptr _joinAsToggle = { nullptr }; - object_ptr _members = { nullptr }; - object_ptr _startsIn = { nullptr }; - object_ptr _countdown = { nullptr }; - std::shared_ptr _countdownData; - object_ptr _startsWhen = { nullptr }; - ChooseJoinAsProcess _joinAsProcess; - - object_ptr _settings = { nullptr }; - object_ptr _share = { nullptr }; - std::unique_ptr _mute; - object_ptr _hangup; - Fn _shareLinkCallback; - - rpl::lifetime _peerLifetime; - -}; - -} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 8e1826a95..c32ea6b1f 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/calls_instance.h" -#include "calls/calls_group_common.h" +#include "calls/calls_call.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_choose_join_as.h" +#include "calls/group/calls_group_call.h" #include "mtproto/mtproto_dh_utils.h" #include "core/application.h" #include "main/main_session.h" @@ -15,10 +18,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "lang/lang_keys.h" #include "boxes/confirm_box.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_panel.h" #include "calls/calls_call.h" -#include "calls/calls_group_call.h" #include "calls/calls_panel.h" -#include "calls/calls_group_panel.h" #include "data/data_user.h" #include "data/data_group_call.h" #include "data/data_channel.h" @@ -41,11 +44,142 @@ namespace { constexpr auto kServerConfigUpdateTimeoutMs = 24 * 3600 * crl::time(1000); +using CallSound = Call::Delegate::CallSound; +using GroupCallSound = GroupCall::Delegate::GroupCallSound; + } // namespace -Instance::Instance() = default; +class Instance::Delegate final + : public Call::Delegate + , public GroupCall::Delegate { +public: + explicit Delegate(not_null instance); -Instance::~Instance() = default; + DhConfig getDhConfig() const override; + + void callFinished(not_null call) override; + void callFailed(not_null call) override; + void callRedial(not_null call) override; + void callRequestPermissionsOrFail( + Fn onSuccess, + bool video) override; + void callPlaySound(CallSound sound) override; + auto callGetVideoCapture() + -> std::shared_ptr override; + + void groupCallFinished(not_null call) override; + void groupCallFailed(not_null call) override; + void groupCallRequestPermissionsOrFail(Fn onSuccess) override; + void groupCallPlaySound(GroupCallSound sound) override; + auto groupCallGetVideoCapture(const QString &deviceId) + -> std::shared_ptr override; + FnMut groupCallAddAsyncWaiter() override; + +private: + const not_null _instance; + +}; + +Instance::Delegate::Delegate(not_null instance) +: _instance(instance) { +} + +DhConfig Instance::Delegate::getDhConfig() const { + return *_instance->_cachedDhConfig; +} + +void Instance::Delegate::callFinished(not_null call) { + crl::on_main(call, [=] { + _instance->destroyCall(call); + }); +} + +void Instance::Delegate::callFailed(not_null call) { + crl::on_main(call, [=] { + _instance->destroyCall(call); + }); +} + +void Instance::Delegate::callRedial(not_null call) { + if (_instance->_currentCall.get() == call) { + _instance->refreshDhConfig(); + } +} + +void Instance::Delegate::callRequestPermissionsOrFail( + Fn onSuccess, + bool video) { + _instance->requestPermissionsOrFail(std::move(onSuccess), video); +} + +void Instance::Delegate::callPlaySound(CallSound sound) { + _instance->playSoundOnce([&] { + switch (sound) { + case CallSound::Busy: return "call_busy"; + case CallSound::Ended: return "call_end"; + case CallSound::Connecting: return "call_connect"; + } + Unexpected("CallSound in Instance::callPlaySound."); + }()); +} + +auto Instance::Delegate::callGetVideoCapture() +-> std::shared_ptr { + return _instance->getVideoCapture(); +} + +void Instance::Delegate::groupCallFinished(not_null call) { + crl::on_main(call, [=] { + _instance->destroyGroupCall(call); + }); +} + +void Instance::Delegate::groupCallFailed(not_null call) { + crl::on_main(call, [=] { + _instance->destroyGroupCall(call); + }); +} + +void Instance::Delegate::groupCallRequestPermissionsOrFail( + Fn onSuccess) { + _instance->requestPermissionsOrFail(std::move(onSuccess), false); +} + +void Instance::Delegate::groupCallPlaySound(GroupCallSound sound) { + _instance->playSoundOnce([&] { + switch (sound) { + case GroupCallSound::Started: return "group_call_start"; + case GroupCallSound::Ended: return "group_call_end"; + case GroupCallSound::AllowedToSpeak: return "group_call_allowed"; + case GroupCallSound::Connecting: return "group_call_connect"; + } + Unexpected("GroupCallSound in Instance::groupCallPlaySound."); + }()); +} + +auto Instance::Delegate::groupCallGetVideoCapture(const QString &deviceId) +-> std::shared_ptr { + return _instance->getVideoCapture(deviceId); +} + +FnMut Instance::Delegate::groupCallAddAsyncWaiter() { + return _instance->addAsyncWaiter(); +} + +Instance::Instance() +: _delegate(std::make_unique(this)) +, _cachedDhConfig(std::make_unique()) +, _chooseJoinAs(std::make_unique()) { +} + +Instance::~Instance() { + destroyCurrentCall(); + + while (!_asyncWaiters.empty()) { + _asyncWaiters.front()->acquire(); + _asyncWaiters.erase(_asyncWaiters.begin()); + } +} void Instance::startOutgoingCall(not_null user, bool video) { if (activateCurrentCall()) { @@ -72,7 +206,7 @@ void Instance::startOrJoinGroupCall( : peer->groupCall() ? Group::ChooseJoinAsProcess::Context::Join : Group::ChooseJoinAsProcess::Context::Create; - _chooseJoinAs.start(peer, context, [=](object_ptr box) { + _chooseJoinAs->start(peer, context, [=](object_ptr box) { Ui::show(std::move(box), Ui::LayerOption::KeepOther); }, [=](QString text) { Ui::Toast::Show(text); @@ -85,36 +219,6 @@ void Instance::startOrJoinGroupCall( }); } -void Instance::callFinished(not_null call) { - crl::on_main(call, [=] { - destroyCall(call); - }); -} - -void Instance::callFailed(not_null call) { - crl::on_main(call, [=] { - destroyCall(call); - }); -} - -void Instance::callRedial(not_null call) { - if (_currentCall.get() == call) { - refreshDhConfig(); - } -} - -void Instance::groupCallFinished(not_null call) { - crl::on_main(call, [=] { - destroyGroupCall(call); - }); -} - -void Instance::groupCallFailed(not_null call) { - crl::on_main(call, [=] { - destroyGroupCall(call); - }); -} - not_null Instance::ensureSoundLoaded( const QString &key) { const auto i = _tracks.find(key); @@ -132,31 +236,6 @@ void Instance::playSoundOnce(const QString &key) { ensureSoundLoaded(key)->playOnce(); } -void Instance::callPlaySound(CallSound sound) { - playSoundOnce([&] { - switch (sound) { - case CallSound::Busy: return "call_busy"; - case CallSound::Ended: return "call_end"; - case CallSound::Connecting: return "call_connect"; - } - Unexpected("CallSound in Instance::callPlaySound."); - return ""; - }()); -} - -void Instance::groupCallPlaySound(GroupCallSound sound) { - playSoundOnce([&] { - switch (sound) { - case GroupCallSound::Started: return "group_call_start"; - case GroupCallSound::Ended: return "group_call_end"; - case GroupCallSound::AllowedToSpeak: return "group_call_allowed"; - case GroupCallSound::Connecting: return "group_call_connect"; - } - Unexpected("GroupCallSound in Instance::groupCallPlaySound."); - return ""; - }()); -} - void Instance::destroyCall(not_null call) { if (_currentCall.get() == call) { _currentCallPanel->closeBeforeDestroy(); @@ -174,7 +253,7 @@ void Instance::destroyCall(not_null call) { } void Instance::createCall(not_null user, Call::Type type, bool video) { - auto call = std::make_unique(getCallDelegate(), user, type, video); + auto call = std::make_unique(_delegate.get(), user, type, video); const auto raw = call.get(); user->session().account().sessionChanges( @@ -217,7 +296,7 @@ void Instance::createGroupCall( destroyCurrentCall(); auto call = std::make_unique( - getGroupCallDelegate(), + _delegate.get(), std::move(info), inputCall); const auto raw = call.get(); @@ -237,7 +316,7 @@ void Instance::refreshDhConfig() { const auto weak = base::make_weak(_currentCall); _currentCall->user()->session().api().request(MTPmessages_GetDhConfig( - MTP_int(_dhConfig.version), + MTP_int(_cachedDhConfig->version), MTP_int(MTP::ModExpFirst::kRandomPowerSize) )).done([=](const MTPmessages_DhConfig &result) { const auto call = weak.get(); @@ -249,14 +328,14 @@ void Instance::refreshDhConfig() { Assert(random.size() == MTP::ModExpFirst::kRandomPowerSize); call->start(random); } else { - callFailed(call); + _delegate->callFailed(call); } }).fail([=](const MTP::Error &error) { const auto call = weak.get(); if (!call) { return; } - callFailed(call); + _delegate->callFailed(call); }).send(); } @@ -277,13 +356,13 @@ bytes::const_span Instance::updateDhConfig( } else if (!validRandom(data.vrandom().v)) { return {}; } - _dhConfig.g = data.vg().v; - _dhConfig.p = std::move(primeBytes); - _dhConfig.version = data.vversion().v; + _cachedDhConfig->g = data.vg().v; + _cachedDhConfig->p = std::move(primeBytes); + _cachedDhConfig->version = data.vversion().v; return bytes::make_span(data.vrandom().v); }, [&](const MTPDmessages_dhConfigNotModified &data) -> bytes::const_span { - if (!_dhConfig.g || _dhConfig.p.empty()) { + if (!_cachedDhConfig->g || _cachedDhConfig->p.empty()) { LOG(("API Error: dhConfigNotModified on zero version.")); return {}; } else if (!validRandom(data.vrandom().v)) { @@ -324,6 +403,8 @@ void Instance::handleUpdate( handleSignalingData(session, data); }, [&](const MTPDupdateGroupCall &data) { handleGroupCallUpdate(session, update); + }, [&](const MTPDupdateGroupCallConnection &data) { + handleGroupCallUpdate(session, update); }, [&](const MTPDupdateGroupCallParticipants &data) { handleGroupCallUpdate(session, update); }, [](const auto &) { @@ -357,6 +438,26 @@ void Instance::setCurrentAudioDevice(bool input, const QString &deviceId) { } } +FnMut Instance::addAsyncWaiter() { + auto semaphore = std::make_unique(); + const auto raw = semaphore.get(); + const auto weak = base::make_weak(this); + _asyncWaiters.emplace(std::move(semaphore)); + return [raw, weak] { + raw->release(); + crl::on_main(weak, [raw, weak] { + auto &waiters = weak->_asyncWaiters; + auto wrapped = std::unique_ptr(raw); + const auto i = waiters.find(wrapped); + wrapped.release(); + + if (i != end(waiters)) { + waiters.erase(i); + } + }); + }; +} + bool Instance::isQuitPrevent() { if (!_currentCall || _currentCall->isIncomingWaiting()) { return false; @@ -415,10 +516,15 @@ void Instance::handleGroupCallUpdate( && (&_currentGroupCall->peer()->session() == session)) { update.match([&](const MTPDupdateGroupCall &data) { _currentGroupCall->handlePossibleCreateOrJoinResponse(data); + }, [&](const MTPDupdateGroupCallConnection &data) { + _currentGroupCall->handlePossibleCreateOrJoinResponse(data); }, [](const auto &) { }); } + if (update.type() == mtpc_updateGroupCallConnection) { + return; + } const auto callId = update.match([](const MTPDupdateGroupCall &data) { return data.vcall().match([](const auto &data) { return data.vid().v; @@ -592,14 +698,19 @@ void Instance::requestPermissionOrFail(Platform::PermissionType type, Fn } } -std::shared_ptr Instance::getVideoCapture() { +std::shared_ptr Instance::getVideoCapture( + QString deviceId) { + if (deviceId.isEmpty()) { + deviceId = Core::App().settings().callVideoInputDeviceId(); + } if (auto result = _videoCapture.lock()) { + result->switchToDevice(deviceId.toStdString()); return result; } auto result = std::shared_ptr( tgcalls::VideoCaptureInterface::Create( tgcalls::StaticThreads::getThreads(), - Core::App().settings().callVideoInputDeviceId().toStdString())); + deviceId.toStdString())); _videoCapture = result; return result; } diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 96034fcaf..ff1f51c9c 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -8,9 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "mtproto/sender.h" -#include "calls/calls_call.h" -#include "calls/calls_group_call.h" -#include "calls/calls_choose_join_as.h" + +namespace crl { +class semaphore; +} // namespace crl namespace Platform { enum class PermissionType; @@ -27,17 +28,22 @@ class Session; namespace Calls::Group { struct JoinInfo; class Panel; +class ChooseJoinAsProcess; } // namespace Calls::Group +namespace tgcalls { +class VideoCaptureInterface; +} // namespace tgcalls + namespace Calls { +class Call; +enum class CallType; +class GroupCall; class Panel; +struct DhConfig; -class Instance - : private Call::Delegate - , private GroupCall::Delegate - , private base::Subscriber - , public base::has_weak_ptr { +class Instance : private base::Subscriber, public base::has_weak_ptr { public: Instance(); ~Instance(); @@ -69,49 +75,24 @@ public: bool activateCurrentCall(const QString &joinHash = QString()); bool minimizeCurrentActiveCall(); bool closeCurrentActiveCall(); - auto getVideoCapture() - -> std::shared_ptr override; + [[nodiscard]] auto getVideoCapture(QString deviceId = QString()) + -> std::shared_ptr; void requestPermissionsOrFail(Fn onSuccess, bool video = true); void setCurrentAudioDevice(bool input, const QString &deviceId); + [[nodiscard]] FnMut addAsyncWaiter(); + [[nodiscard]] bool isQuitPrevent(); private: - using CallSound = Call::Delegate::CallSound; - using GroupCallSound = GroupCall::Delegate::GroupCallSound; - - [[nodiscard]] not_null getCallDelegate() { - return static_cast(this); - } - [[nodiscard]] not_null getGroupCallDelegate() { - return static_cast(this); - } - [[nodiscard]] DhConfig getDhConfig() const override { - return _dhConfig; - } + class Delegate; + friend class Delegate; not_null ensureSoundLoaded(const QString &key); void playSoundOnce(const QString &key); - void callFinished(not_null call) override; - void callFailed(not_null call) override; - void callRedial(not_null call) override; - void callRequestPermissionsOrFail( - Fn onSuccess, - bool video) override { - requestPermissionsOrFail(std::move(onSuccess), video); - } - void callPlaySound(CallSound sound) override; - - void groupCallFinished(not_null call) override; - void groupCallFailed(not_null call) override; - void groupCallRequestPermissionsOrFail(Fn onSuccess) override { - requestPermissionsOrFail(std::move(onSuccess), false); - } - void groupCallPlaySound(GroupCallSound sound) override; - - void createCall(not_null user, Call::Type type, bool video); + void createCall(not_null user, CallType type, bool video); void destroyCall(not_null call); void createGroupCall( @@ -138,7 +119,8 @@ private: not_null session, const MTPUpdate &update); - DhConfig _dhConfig; + const std::unique_ptr _delegate; + const std::unique_ptr _cachedDhConfig; crl::time _lastServerConfigUpdateTime = 0; base::weak_ptr _serverConfigRequestSession; @@ -154,7 +136,9 @@ private: base::flat_map> _tracks; - Group::ChooseJoinAsProcess _chooseJoinAs; + const std::unique_ptr _chooseJoinAs; + + base::flat_set> _asyncWaiters; }; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index b3812ca2a..698a894bd 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_signal_bars.h" #include "calls/calls_userpic.h" #include "calls/calls_video_bubble.h" +#include "calls/calls_video_incoming.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/widgets/call_button.h" #include "ui/widgets/buttons.h" @@ -29,6 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/platform/ui_platform_utility.h" +#include "ui/gl/gl_surface.h" +#include "ui/gl/gl_shader.h" #include "ui/toast/toast.h" #include "ui/empty_userpic.h" #include "ui/emoji_config.h" @@ -39,7 +42,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "base/platform/base_platform_info.h" #include "window/main_window.h" -#include "media/view/media_view_pip.h" // Utilities for frame rotation. #include "app.h" #include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" @@ -48,146 +50,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include namespace Calls { -namespace { - -#if defined Q_OS_MAC && !defined OS_MAC_OLD -#define USE_OPENGL_OVERLAY_WIDGET -#endif // Q_OS_MAC && !OS_MAC_OLD - -#ifdef USE_OPENGL_OVERLAY_WIDGET -using IncomingParent = Ui::RpWidgetWrap; -#else // USE_OPENGL_OVERLAY_WIDGET -using IncomingParent = Ui::RpWidget; -#endif // USE_OPENGL_OVERLAY_WIDGET - -} // namespace - -class Panel::Incoming final : public IncomingParent { -public: - Incoming( - not_null parent, - not_null track); - -private: - void paintEvent(QPaintEvent *e) override; - - void initBottomShadow(); - void fillTopShadow(QPainter &p); - void fillBottomShadow(QPainter &p); - - const not_null _track; - QPixmap _bottomShadow; - -}; - -Panel::Incoming::Incoming( - not_null parent, - not_null track) -: IncomingParent(parent) -, _track(track) { - initBottomShadow(); - setAttribute(Qt::WA_OpaquePaintEvent); - setAttribute(Qt::WA_TransparentForMouseEvents); -} - -void Panel::Incoming::paintEvent(QPaintEvent *e) { - QPainter p(this); - - const auto [image, rotation] = _track->frameOriginalWithRotation(); - if (image.isNull()) { - p.fillRect(e->rect(), Qt::black); - } else { - using namespace Media::View; - auto hq = PainterHighQualityEnabler(p); - if (UsePainterRotation(rotation)) { - if (rotation) { - p.save(); - p.rotate(rotation); - } - p.drawImage(RotatedRect(rect(), rotation), image); - if (rotation) { - p.restore(); - } - } else if (rotation) { - p.drawImage(rect(), RotateFrameImage(image, rotation)); - } else { - p.drawImage(rect(), image); - } - fillBottomShadow(p); - fillTopShadow(p); - } - _track->markFrameShown(); -} - -void Panel::Incoming::initBottomShadow() { - auto image = QImage( - QSize(1, st::callBottomShadowSize) * cIntRetinaFactor(), - QImage::Format_ARGB32_Premultiplied); - const auto colorFrom = uint32(0); - const auto colorTill = uint32(74); - const auto rows = image.height(); - const auto step = (uint64(colorTill - colorFrom) << 32) / rows; - auto accumulated = uint64(); - auto bytes = image.bits(); - for (auto y = 0; y != rows; ++y) { - accumulated += step; - const auto color = (colorFrom + uint32(accumulated >> 32)) << 24; - for (auto x = 0; x != image.width(); ++x) { - *(reinterpret_cast(bytes) + x) = color; - } - bytes += image.bytesPerLine(); - } - _bottomShadow = Images::PixmapFast(std::move(image)); -} - -void Panel::Incoming::fillTopShadow(QPainter &p) { -#ifdef Q_OS_WIN - const auto width = parentWidget()->width(); - const auto position = QPoint(width - st::callTitleShadow.width(), 0); - const auto shadowArea = QRect( - position, - st::callTitleShadow.size()); - const auto fill = shadowArea.intersected(geometry()).translated(-pos()); - if (fill.isEmpty()) { - return; - } - p.save(); - p.setClipRect(fill); - st::callTitleShadow.paint(p, position - pos(), width); - p.restore(); -#endif // Q_OS_WIN -} - -void Panel::Incoming::fillBottomShadow(QPainter &p) { - const auto shadowArea = QRect( - 0, - parentWidget()->height() - st::callBottomShadowSize, - parentWidget()->width(), - st::callBottomShadowSize); - const auto fill = shadowArea.intersected(geometry()).translated(-pos()); - if (fill.isEmpty()) { - return; - } - const auto factor = cIntRetinaFactor(); - p.drawPixmap( - fill, - _bottomShadow, - QRect( - 0, - factor * (fill.y() - shadowArea.translated(-pos()).y()), - factor, - factor * fill.height())); -} Panel::Panel(not_null call) : _call(call) , _user(call->user()) -, _window(std::make_unique()) #ifndef Q_OS_MAC , _controls(std::make_unique( - _window->body(), + widget(), st::callTitle, [=](bool maximized) { toggleFullScreen(maximized); })) #endif // !Q_OS_MAC @@ -214,26 +86,26 @@ Panel::Panel(not_null call) Panel::~Panel() = default; bool Panel::isActive() const { - return _window->isActiveWindow() - && _window->isVisible() - && !(_window->windowState() & Qt::WindowMinimized); + return window()->isActiveWindow() + && window()->isVisible() + && !(window()->windowState() & Qt::WindowMinimized); } void Panel::showAndActivate() { - if (_window->isHidden()) { - _window->show(); + if (window()->isHidden()) { + window()->show(); } - const auto state = _window->windowState(); + const auto state = window()->windowState(); if (state & Qt::WindowMinimized) { - _window->setWindowState(state & ~Qt::WindowMinimized); + window()->setWindowState(state & ~Qt::WindowMinimized); } - _window->raise(); - _window->activateWindow(); - _window->setFocus(); + window()->raise(); + window()->activateWindow(); + window()->setFocus(); } void Panel::minimize() { - _window->setWindowState(_window->windowState() | Qt::WindowMinimized); + window()->setWindowState(window()->windowState() | Qt::WindowMinimized); } void Panel::replaceCall(not_null call) { @@ -242,26 +114,26 @@ void Panel::replaceCall(not_null call) { } void Panel::initWindow() { - _window->setAttribute(Qt::WA_OpaquePaintEvent); - _window->setAttribute(Qt::WA_NoSystemBackground); - _window->setWindowIcon( + window()->setAttribute(Qt::WA_OpaquePaintEvent); + window()->setAttribute(Qt::WA_NoSystemBackground); + window()->setWindowIcon( QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); - _window->setTitle(u" "_q); - _window->setTitleStyle(st::callTitle); + window()->setTitle(u" "_q); + window()->setTitleStyle(st::callTitle); - _window->events( + window()->events( ) | rpl::start_with_next([=](not_null e) { if (e->type() == QEvent::Close) { handleClose(); } else if (e->type() == QEvent::KeyPress) { if ((static_cast(e.get())->key() == Qt::Key_Escape) - && _window->isFullScreen()) { - _window->showNormal(); + && window()->isFullScreen()) { + window()->showNormal(); } } - }, _window->lifetime()); + }, window()->lifetime()); - _window->setBodyTitleArea([=](QPoint widgetPoint) { + window()->setBodyTitleArea([=](QPoint widgetPoint) { using Flag = Ui::WindowTitleHitTestFlag; if (!widget()->rect().contains(widgetPoint)) { return Flag::None | Flag(0); @@ -287,28 +159,31 @@ void Panel::initWindow() { : (Flag::Move | Flag::FullScreen); }); -#ifdef Q_OS_WIN - // On Windows we replace snap-to-top maximizing with fullscreen. - // - // We have to switch first to showNormal, so that showFullScreen - // will remember correct normal window geometry and next showNormal - // will show it instead of a moving maximized window. - // - // We have to do it in InvokeQueued, otherwise it still captures - // the maximized window geometry and saves it. - // - // I couldn't find a less glitchy way to do that *sigh*. - const auto object = _window->windowHandle(); - const auto signal = &QWindow::windowStateChanged; - QObject::connect(object, signal, [=](Qt::WindowState state) { - if (state == Qt::WindowMaximized) { - InvokeQueued(object, [=] { - _window->showNormal(); - _window->showFullScreen(); - }); - } - }); -#endif // Q_OS_WIN + // Don't do that, it looks awful :( +//#ifdef Q_OS_WIN +// // On Windows we replace snap-to-top maximizing with fullscreen. +// // +// // We have to switch first to showNormal, so that showFullScreen +// // will remember correct normal window geometry and next showNormal +// // will show it instead of a moving maximized window. +// // +// // We have to do it in InvokeQueued, otherwise it still captures +// // the maximized window geometry and saves it. +// // +// // I couldn't find a less glitchy way to do that *sigh*. +// const auto object = window()->windowHandle(); +// const auto signal = &QWindow::windowStateChanged; +// QObject::connect(object, signal, [=](Qt::WindowState state) { +// if (state == Qt::WindowMaximized) { +// InvokeQueued(object, [=] { +// window()->showNormal(); +// InvokeQueued(object, [=] { +// window()->showFullScreen(); +// }); +// }); +// } +// }); +//#endif // Q_OS_WIN } void Panel::initWidget() { @@ -392,7 +267,7 @@ void Panel::refreshIncomingGeometry() { Expects(_incoming != nullptr); if (_incomingFrameSize.isEmpty()) { - _incoming->hide(); + _incoming->widget()->hide(); return; } const auto to = widget()->size(); @@ -401,7 +276,7 @@ void Panel::refreshIncomingGeometry() { to, Qt::KeepAspectRatioByExpanding); - // If we cut out no more than 0.33 of the original, let's use expanding. + // If we cut out no more than 0.25 of the original, let's use expanding. const auto use = ((big.width() * 3 <= to.width() * 4) && (big.height() * 3 <= to.height() * 4)) ? big @@ -409,8 +284,8 @@ void Panel::refreshIncomingGeometry() { const auto pos = QPoint( (to.width() - use.width()) / 2, (to.height() - use.height()) / 2); - _incoming->setGeometry(QRect(pos, use)); - _incoming->show(); + _incoming->widget()->setGeometry(QRect(pos, use)); + _incoming->widget()->show(); } void Panel::reinitWithCall(Call *call) { @@ -446,8 +321,9 @@ void Panel::reinitWithCall(Call *call) { _call->videoOutgoing()); _incoming = std::make_unique( widget(), - _call->videoIncoming()); - _incoming->hide(); + _call->videoIncoming(), + _window.backend()); + _incoming->widget()->hide(); _call->mutedValue( ) | rpl::start_with_next([=](bool mute) { @@ -474,28 +350,34 @@ void Panel::reinitWithCall(Call *call) { _call->videoIncoming()->renderNextFrame( ) | rpl::start_with_next([=] { const auto track = _call->videoIncoming(); - const auto [frame, rotation] = track->frameOriginalWithRotation(); - setIncomingSize((rotation == 90 || rotation == 270) - ? QSize(frame.height(), frame.width()) - : frame.size()); - if (_incoming->isHidden()) { + setIncomingSize(track->state() == Webrtc::VideoState::Active + ? track->frameSize() + : QSize()); + if (_incoming->widget()->isHidden()) { return; } const auto incoming = incomingFrameGeometry(); const auto outgoing = outgoingFrameGeometry(); - _incoming->update(); + _incoming->widget()->update(); if (incoming.intersects(outgoing)) { widget()->update(outgoing); } }, _callLifetime); + _call->videoIncoming()->stateValue( + ) | rpl::start_with_next([=](Webrtc::VideoState state) { + setIncomingSize((state == Webrtc::VideoState::Active) + ? _call->videoIncoming()->frameSize() + : QSize()); + }, _callLifetime); + _call->videoOutgoing()->renderNextFrame( ) | rpl::start_with_next([=] { const auto incoming = incomingFrameGeometry(); const auto outgoing = outgoingFrameGeometry(); widget()->update(outgoing); if (incoming.intersects(outgoing)) { - _incoming->update(); + _incoming->widget()->update(); } }, _callLifetime); @@ -539,7 +421,13 @@ void Panel::reinitWithCall(Call *call) { _name->setText(_user->name); updateStatusText(_call->state()); - _incoming->lower(); + _answerHangupRedial->raise(); + _decline->raise(); + _cancel->raise(); + _camera->raise(); + _mute->raise(); + + _incoming->widget()->lower(); } void Panel::createRemoteAudioMute() { @@ -604,7 +492,7 @@ void Panel::showControls() { _cancel->setVisible(_cancel->toggled()); const auto shown = !_incomingFrameSize.isEmpty(); - _incoming->setVisible(shown); + _incoming->widget()->setVisible(shown); _name->setVisible(!shown); _status->setVisible(!shown); _userpic->setVisible(!shown); @@ -614,16 +502,20 @@ void Panel::showControls() { } void Panel::closeBeforeDestroy() { - _window->close(); + window()->close(); reinitWithCall(nullptr); } +rpl::lifetime &Panel::lifetime() { + return window()->lifetime(); +} + void Panel::initGeometry() { const auto center = Core::App().getPointForCallPanelCenter(); const auto initRect = QRect(0, 0, st::callWidth, st::callHeight); - _window->setGeometry(initRect.translated(center - initRect.center())); - _window->setMinimumSize({ st::callWidthMin, st::callHeightMin }); - _window->show(); + window()->setGeometry(initRect.translated(center - initRect.center())); + window()->setMinimumSize({ st::callWidthMin, st::callHeightMin }); + window()->show(); updateControlsGeometry(); } @@ -641,16 +533,16 @@ void Panel::refreshOutgoingPreviewInBody(State state) { void Panel::toggleFullScreen(bool fullscreen) { if (fullscreen) { - _window->showFullScreen(); + window()->showFullScreen(); } else { - _window->showNormal(); + window()->showNormal(); } } QRect Panel::incomingFrameGeometry() const { - return (!_incoming || _incoming->isHidden()) + return (!_incoming || _incoming->widget()->isHidden()) ? QRect() - : _incoming->geometry(); + : _incoming->widget()->geometry(); } QRect Panel::outgoingFrameGeometry() const { @@ -666,15 +558,31 @@ void Panel::updateControlsGeometry() { } if (_fingerprint) { #ifndef Q_OS_MAC - const auto minRight = _controls->geometry().width() - + st::callFingerprintTop; + const auto controlsGeometry = _controls->geometry(); + const auto halfWidth = widget()->width() / 2; + const auto minLeft = (controlsGeometry.center().x() < halfWidth) + ? (controlsGeometry.width() + st::callFingerprintTop) + : 0; + const auto minRight = (controlsGeometry.center().x() >= halfWidth) + ? (controlsGeometry.width() + st::callFingerprintTop) + : 0; + _incoming->setControlsAlignment(minLeft + ? style::al_left + : style::al_right); #else // !Q_OS_MAC + const auto minLeft = 0; const auto minRight = 0; #endif // _controls const auto desired = (widget()->width() - _fingerprint->width()) / 2; - _fingerprint->moveToRight( - std::max(desired, minRight), - st::callFingerprintTop); + if (minLeft) { + _fingerprint->moveToLeft( + std::max(desired, minLeft), + st::callFingerprintTop); + } else { + _fingerprint->moveToRight( + std::max(desired, minRight), + st::callFingerprintTop); + } } const auto innerHeight = std::max(widget()->height(), st::callHeightMin); const auto innerWidth = widget()->width() - 2 * st::callInnerPadding; @@ -786,13 +694,13 @@ void Panel::paint(QRect clip) { Painter p(widget()); auto region = QRegion(clip); - if (!_incoming->isHidden()) { - region = region.subtracted(QRegion(_incoming->geometry())); + if (!_incoming->widget()->isHidden()) { + region = region.subtracted(QRegion(_incoming->widget()->geometry())); } for (const auto rect : region) { p.fillRect(rect, st::callBgOpaque); } - if (_incoming && _incoming->isHidden()) { + if (_incoming && _incoming->widget()->isHidden()) { _call->videoIncoming()->markFrameShown(); } } @@ -803,8 +711,12 @@ void Panel::handleClose() { } } +not_null Panel::window() const { + return _window.window(); +} + not_null Panel::widget() const { - return _window->body(); + return _window.widget(); } void Panel::stateChanged(State state) { @@ -820,7 +732,7 @@ void Panel::stateChanged(State state) { auto toggleButton = [&](auto &&button, bool visible) { button->toggle( visible, - _window->isHidden() + window()->isHidden() ? anim::type::instant : anim::type::normal); }; diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 4c6e13b81..e7f7861d9 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "calls/calls_call.h" #include "ui/effects/animations.h" +#include "ui/gl/gl_window.h" #include "ui/rp_widget.h" class Image; @@ -30,6 +31,9 @@ class FadeWrap; template class PaddingWrap; class Window; +namespace GL { +enum class Backend; +} // namespace GL namespace Platform { class TitleControls; } // namespace Platform @@ -57,6 +61,8 @@ public: void replaceCall(not_null call); void closeBeforeDestroy(); + rpl::lifetime &lifetime(); + private: class Incoming; using State = Call::State; @@ -67,6 +73,7 @@ private: Redial, }; + [[nodiscard]] not_null window() const; [[nodiscard]] not_null widget() const; void paint(QRect clip); @@ -80,9 +87,6 @@ private: void handleClose(); - QRect signalBarsRect() const; - void paintSignalBarsBg(Painter &p); - void updateControlsGeometry(); void updateHangupGeometry(); void updateStatusGeometry(); @@ -105,7 +109,7 @@ private: Call *_call = nullptr; not_null _user; - const std::unique_ptr _window; + Ui::GL::Window _window; std::unique_ptr _incoming; #ifndef Q_OS_MAC diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index f598bb20b..99c3ea21b 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -22,7 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_signal_bars.h" -#include "calls/calls_group_menu.h" // Group::LeaveBox. +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_menu.h" // Group::LeaveBox. #include "history/view/history_view_group_call_tracker.h" // ContentByCall. #include "data/data_user.h" #include "data/data_group_call.h" @@ -279,8 +280,7 @@ void TopBar::initControls() { if (const auto call = _call.get()) { call->setMuted(!call->muted()); } else if (const auto group = _groupCall.get()) { - if (group->muted() == MuteState::ForceMuted - || group->muted() == MuteState::RaisedHand) { + if (group->mutedByAdmin()) { Ui::Toast::Show(tr::lng_group_call_force_muted_sub(tr::now)); } else { group->setMuted((group->muted() == MuteState::Muted) diff --git a/Telegram/SourceFiles/calls/calls_video_incoming.cpp b/Telegram/SourceFiles/calls/calls_video_incoming.cpp new file mode 100644 index 000000000..4edec9b2c --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_video_incoming.cpp @@ -0,0 +1,576 @@ +/* +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 "calls/calls_video_incoming.h" + +#include "ui/gl/gl_surface.h" +#include "ui/gl/gl_shader.h" +#include "ui/gl/gl_image.h" +#include "ui/gl/gl_primitives.h" +#include "media/view/media_view_pip.h" +#include "webrtc/webrtc_video_track.h" +#include "styles/style_calls.h" + +#include +#include + +namespace Calls { +namespace { + +constexpr auto kBottomShadowAlphaMax = 74; + +using namespace Ui::GL; + +[[nodiscard]] ShaderPart FragmentBottomShadow() { + return { + .header = R"( +uniform vec3 shadow; // fullHeight, shadowTop, maxOpacity +)", + .body = R"( + float shadowCoord = shadow.y - gl_FragCoord.y; + float shadowValue = clamp(shadowCoord / shadow.x, 0., 1.); + float shadowShown = shadowValue * shadow.z; + result = vec4(min(result.rgb, vec3(1.)) * (1. - shadowShown), result.a); +)", + }; +} + +} // namespace + +class Panel::Incoming::RendererGL final : public Ui::GL::Renderer { +public: + explicit RendererGL(not_null owner); + + void init( + not_null widget, + QOpenGLFunctions &f) override; + + void deinit( + not_null widget, + QOpenGLFunctions &f) override; + + void paint( + not_null widget, + QOpenGLFunctions &f) override; + +private: + void uploadTexture( + QOpenGLFunctions &f, + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const; + void validateShadowImage(); + + const not_null _owner; + + QSize _viewport; + float _factor = 1.; + QVector2D _uniformViewport; + + std::optional _contentBuffer; + std::optional _argb32Program; + QOpenGLShader *_texturedVertexShader = nullptr; + std::optional _yuv420Program; + std::optional _imageProgram; + Ui::GL::Textures<4> _textures; + QSize _rgbaSize; + QSize _lumaSize; + QSize _chromaSize; + int _trackFrameIndex = 0; + + Ui::GL::Image _controlsShadowImage; + QRect _controlsShadowLeft; + QRect _controlsShadowRight; + + rpl::lifetime _lifetime; + +}; + +class Panel::Incoming::RendererSW final : public Ui::GL::Renderer { +public: + explicit RendererSW(not_null owner); + + void paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) override; + +private: + void initBottomShadow(); + void fillTopShadow(QPainter &p); + void fillBottomShadow(QPainter &p); + + const not_null _owner; + + QImage _bottomShadow; + +}; + +Panel::Incoming::RendererGL::RendererGL(not_null owner) +: _owner(owner) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _controlsShadowImage.invalidate(); + }, _lifetime); +} + +void Panel::Incoming::RendererGL::init( + not_null widget, + QOpenGLFunctions &f) { + constexpr auto kQuads = 2; + constexpr auto kQuadVertices = kQuads * 4; + constexpr auto kQuadValues = kQuadVertices * 4; + + _contentBuffer.emplace(); + _contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); + _contentBuffer->create(); + _contentBuffer->bind(); + _contentBuffer->allocate(kQuadValues * sizeof(GLfloat)); + + _textures.ensureCreated(f); + + _imageProgram.emplace(); + _texturedVertexShader = LinkProgram( + &*_imageProgram, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + })).vertex; + + _argb32Program.emplace(); + LinkProgram( + &*_argb32Program, + _texturedVertexShader, + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentBottomShadow(), + })); + + _yuv420Program.emplace(); + LinkProgram( + &*_yuv420Program, + _texturedVertexShader, + FragmentShader({ + FragmentSampleYUV420Texture(), + FragmentBottomShadow(), + })); +} + +void Panel::Incoming::RendererGL::deinit( + not_null widget, + QOpenGLFunctions &f) { + _textures.destroy(f); + _imageProgram = std::nullopt; + _texturedVertexShader = nullptr; + _argb32Program = std::nullopt; + _yuv420Program = std::nullopt; + _contentBuffer = std::nullopt; +} + +void Panel::Incoming::RendererGL::paint( + not_null widget, + QOpenGLFunctions &f) { + const auto markGuard = gsl::finally([&] { + _owner->_track->markFrameShown(); + }); + const auto data = _owner->_track->frameWithInfo(false); + if (data.format == Webrtc::FrameFormat::None) { + return; + } + + const auto factor = widget->devicePixelRatio(); + if (_factor != factor) { + _factor = factor; + _controlsShadowImage.invalidate(); + } + _viewport = widget->size(); + _uniformViewport = QVector2D( + _viewport.width() * _factor, + _viewport.height() * _factor); + + const auto rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32); + const auto upload = (_trackFrameIndex != data.index); + _trackFrameIndex = data.index; + auto &program = rgbaFrame ? _argb32Program : _yuv420Program; + program->bind(); + if (rgbaFrame) { + Assert(!data.original.isNull()); + f.glActiveTexture(GL_TEXTURE0); + _textures.bind(f, 0); + if (upload) { + uploadTexture( + f, + GL_RGBA, + GL_RGBA, + data.original.size(), + _rgbaSize, + data.original.bytesPerLine() / 4, + data.original.constBits()); + _rgbaSize = data.original.size(); + } + program->setUniformValue("s_texture", GLint(0)); + } else { + Assert(data.format == Webrtc::FrameFormat::YUV420); + Assert(!data.yuv420->size.isEmpty()); + const auto yuv = data.yuv420; + + f.glActiveTexture(GL_TEXTURE0); + _textures.bind(f, 1); + if (upload) { + f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->size, + _lumaSize, + yuv->y.stride, + yuv->y.data); + _lumaSize = yuv->size; + } + f.glActiveTexture(GL_TEXTURE1); + _textures.bind(f, 2); + if (upload) { + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->u.stride, + yuv->u.data); + } + f.glActiveTexture(GL_TEXTURE2); + _textures.bind(f, 3); + if (upload) { + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->v.stride, + yuv->v.data); + _chromaSize = yuv->chromaSize; + f.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + program->setUniformValue("y_texture", GLint(0)); + program->setUniformValue("u_texture", GLint(1)); + program->setUniformValue("v_texture", GLint(2)); + } + const auto rect = TransformRect( + widget->rect(), + _viewport, + _factor); + std::array, 4> texcoords = { { + { { 0.f, 1.f } }, + { { 1.f, 1.f } }, + { { 1.f, 0.f } }, + { { 0.f, 0.f } }, + } }; + if (const auto shift = (data.rotation / 90); shift != 0) { + std::rotate( + begin(texcoords), + begin(texcoords) + shift, + end(texcoords)); + } + + const auto width = widget->parentWidget()->width(); + const auto left = (_owner->_topControlsAlignment == style::al_left); + validateShadowImage(); + const auto position = left + ? QPoint() + : QPoint(width - st::callTitleShadowRight.width(), 0); + const auto translated = position - widget->pos(); + const auto shadowArea = QRect(translated, st::callTitleShadowLeft.size()); + const auto shadow = _controlsShadowImage.texturedRect( + shadowArea, + (left ? _controlsShadowLeft : _controlsShadowRight), + widget->rect()); + const auto shadowRect = TransformRect( + shadow.geometry, + _viewport, + _factor); + + const GLfloat coords[] = { + rect.left(), rect.top(), + texcoords[0][0], texcoords[0][1], + + rect.right(), rect.top(), + texcoords[1][0], texcoords[1][1], + + rect.right(), rect.bottom(), + texcoords[2][0], texcoords[2][1], + + rect.left(), rect.bottom(), + texcoords[3][0], texcoords[3][1], + + shadowRect.left(), shadowRect.top(), + shadow.texture.left(), shadow.texture.bottom(), + + shadowRect.right(), shadowRect.top(), + shadow.texture.right(), shadow.texture.bottom(), + + shadowRect.right(), shadowRect.bottom(), + shadow.texture.right(), shadow.texture.top(), + + shadowRect.left(), shadowRect.bottom(), + shadow.texture.left(), shadow.texture.top(), + }; + + _contentBuffer->write(0, coords, sizeof(coords)); + + const auto bottomShadowArea = QRect( + 0, + widget->parentWidget()->height() - st::callBottomShadowSize, + widget->parentWidget()->width(), + st::callBottomShadowSize); + const auto bottomShadowFill = bottomShadowArea.intersected( + widget->geometry()).translated(-widget->pos()); + const auto shadowHeight = bottomShadowFill.height(); + const auto shadowAlpha = (shadowHeight * kBottomShadowAlphaMax) + / (st::callBottomShadowSize * 255.); + + program->setUniformValue("viewport", _uniformViewport); + program->setUniformValue("shadow", QVector3D( + shadowHeight * _factor, + TransformRect(bottomShadowFill, _viewport, _factor).bottom(), + shadowAlpha)); + + FillTexturedRectangle(f, &*program); + +#ifndef Q_OS_MAC + if (!shadowRect.empty()) { + f.glEnable(GL_BLEND); + f.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + const auto guard = gsl::finally([&] { + f.glDisable(GL_BLEND); + }); + + _imageProgram->bind(); + _imageProgram->setUniformValue("viewport", _uniformViewport); + _imageProgram->setUniformValue("s_texture", GLint(0)); + + f.glActiveTexture(GL_TEXTURE0); + _controlsShadowImage.bind(f); + + FillTexturedRectangle(f, &*_imageProgram, 4); + } +#endif // Q_OS_MAC +} + +void Panel::Incoming::RendererGL::validateShadowImage() { + if (_controlsShadowImage) { + return; + } + const auto size = st::callTitleShadowLeft.size(); + const auto full = QSize(size.width(), 2 * size.height()) * int(_factor); + auto image = QImage(full, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(_factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + st::callTitleShadowLeft.paint(p, 0, 0, size.width()); + _controlsShadowLeft = QRect(0, 0, full.width(), full.height() / 2); + st::callTitleShadowRight.paint(p, 0, size.height(), size.width()); + _controlsShadowRight = QRect( + 0, + full.height() / 2, + full.width(), + full.height() / 2); + } + _controlsShadowImage.setImage(std::move(image)); +} + +void Panel::Incoming::RendererGL::uploadTexture( + QOpenGLFunctions &f, + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const { + f.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); + if (hasSize != size) { + f.glTexImage2D( + GL_TEXTURE_2D, + 0, + internalformat, + size.width(), + size.height(), + 0, + format, + GL_UNSIGNED_BYTE, + data); + } else { + f.glTexSubImage2D( + GL_TEXTURE_2D, + 0, + 0, + 0, + size.width(), + size.height(), + format, + GL_UNSIGNED_BYTE, + data); + } + f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + +Panel::Incoming::RendererSW::RendererSW(not_null owner) +: _owner(owner) { + initBottomShadow(); +} + +void Panel::Incoming::RendererSW::paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { + const auto markGuard = gsl::finally([&] { + _owner->_track->markFrameShown(); + }); + const auto data = _owner->_track->frameWithInfo(true); + const auto &image = data.original; + const auto rotation = data.rotation; + if (image.isNull()) { + p.fillRect(clip.boundingRect(), Qt::black); + } else { + const auto rect = _owner->widget()->rect(); + using namespace Media::View; + auto hq = PainterHighQualityEnabler(p); + if (UsePainterRotation(rotation)) { + if (rotation) { + p.save(); + p.rotate(rotation); + } + p.drawImage(RotatedRect(rect, rotation), image); + if (rotation) { + p.restore(); + } + } else if (rotation) { + p.drawImage(rect, RotateFrameImage(image, rotation)); + } else { + p.drawImage(rect, image); + } + fillBottomShadow(p); + fillTopShadow(p); + } +} + +void Panel::Incoming::RendererSW::initBottomShadow() { + auto image = QImage( + QSize(1, st::callBottomShadowSize) * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); + const auto colorFrom = uint32(0); + const auto colorTill = uint32(kBottomShadowAlphaMax); + const auto rows = image.height(); + const auto step = (uint64(colorTill - colorFrom) << 32) / rows; + auto accumulated = uint64(); + auto bytes = image.bits(); + for (auto y = 0; y != rows; ++y) { + accumulated += step; + const auto color = (colorFrom + uint32(accumulated >> 32)) << 24; + for (auto x = 0; x != image.width(); ++x) { + *(reinterpret_cast(bytes) + x) = color; + } + bytes += image.bytesPerLine(); + } + _bottomShadow = std::move(image); +} + +void Panel::Incoming::RendererSW::fillTopShadow(QPainter &p) { +#ifndef Q_OS_MAC + const auto widget = _owner->widget(); + const auto width = widget->parentWidget()->width(); + const auto left = (_owner->_topControlsAlignment == style::al_left); + const auto &icon = left + ? st::callTitleShadowLeft + : st::callTitleShadowRight; + const auto position = left + ? QPoint() + : QPoint(width - icon.width(), 0); + const auto shadowArea = QRect(position, icon.size()); + const auto fill = shadowArea.intersected( + widget->geometry()).translated(-widget->pos()); + if (fill.isEmpty()) { + return; + } + p.save(); + p.setClipRect(fill); + icon.paint(p, position - widget->pos(), width); + p.restore(); +#endif // Q_OS_MAC +} + +void Panel::Incoming::RendererSW::fillBottomShadow(QPainter &p) { + const auto widget = _owner->widget(); + const auto shadowArea = QRect( + 0, + widget->parentWidget()->height() - st::callBottomShadowSize, + widget->parentWidget()->width(), + st::callBottomShadowSize); + const auto fill = shadowArea.intersected( + widget->geometry()).translated(-widget->pos()); + if (fill.isEmpty()) { + return; + } + const auto factor = cIntRetinaFactor(); + p.drawImage( + fill, + _bottomShadow, + QRect( + 0, + (factor + * (fill.y() - shadowArea.translated(-widget->pos()).y())), + factor, + factor * fill.height())); +} + +Panel::Incoming::Incoming( + not_null parent, + not_null track, + Ui::GL::Backend backend) +: _surface(Ui::GL::CreateSurface(parent, chooseRenderer(backend))) +, _track(track) { + widget()->setAttribute(Qt::WA_OpaquePaintEvent); + widget()->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +not_null Panel::Incoming::widget() const { + return _surface->rpWidget(); +} + +not_null Panel::Incoming::rp() const { + return _surface.get(); +} + +void Panel::Incoming::setControlsAlignment(style::align align) { + if (_topControlsAlignment != align) { + _topControlsAlignment = align; + widget()->update(); + } +} + +Ui::GL::ChosenRenderer Panel::Incoming::chooseRenderer( + Ui::GL::Backend backend) { + _opengl = (backend == Ui::GL::Backend::OpenGL); + return { + .renderer = (_opengl + ? std::unique_ptr( + std::make_unique(this)) + : std::make_unique(this)), + .backend = backend, + }; +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_video_incoming.h b/Telegram/SourceFiles/calls/calls_video_incoming.h new file mode 100644 index 000000000..65da54620 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_video_incoming.h @@ -0,0 +1,45 @@ +/* +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 "calls/calls_panel.h" + +namespace Ui::GL { +enum class Backend; +struct ChosenRenderer; +} // namespace Ui::GL + +namespace Calls { + +class Panel::Incoming final { +public: + Incoming( + not_null parent, + not_null track, + Ui::GL::Backend backend); + + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null rp() const; + + void setControlsAlignment(style::align align); + +private: + class RendererGL; + class RendererSW; + + [[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer( + Ui::GL::Backend backend); + + const std::unique_ptr _surface; + const not_null _track; + style::align _topControlsAlignment = style::al_left; + bool _opengl = false; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_choose_join_as.cpp b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp similarity index 98% rename from Telegram/SourceFiles/calls/calls_choose_join_as.cpp rename to Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp index d58850b4b..5f7bec0e3 100644 --- a/Telegram/SourceFiles/calls/calls_choose_join_as.cpp +++ b/Telegram/SourceFiles/calls/group/calls_choose_join_as.cpp @@ -5,10 +5,10 @@ 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 "calls/calls_choose_join_as.h" +#include "calls/group/calls_choose_join_as.h" -#include "calls/calls_group_common.h" -#include "calls/calls_group_menu.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_menu.h" #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_channel.h" diff --git a/Telegram/SourceFiles/calls/calls_choose_join_as.h b/Telegram/SourceFiles/calls/group/calls_choose_join_as.h similarity index 100% rename from Telegram/SourceFiles/calls/calls_choose_join_as.h rename to Telegram/SourceFiles/calls/group/calls_choose_join_as.h diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp new file mode 100644 index 000000000..dae5d4255 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -0,0 +1,3238 @@ +/* +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 "calls/group/calls_group_call.h" + +#include "calls/group/calls_group_common.h" +#include "main/main_session.h" +#include "api/api_send_progress.h" +#include "api/api_updates.h" +#include "apiwrap.h" +#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 "base/unixtime.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_changes.h" +#include "data/data_user.h" +#include "data/data_chat.h" +#include "data/data_channel.h" +#include "data/data_group_call.h" +#include "data/data_peer_values.h" +#include "data/data_session.h" +#include "base/global_shortcuts.h" +#include "base/openssl_help.h" +#include "webrtc/webrtc_video_track.h" +#include "webrtc/webrtc_media_devices.h" +#include "webrtc/webrtc_create_adm.h" + +#include +#include +#include +#include +#include +#include + +namespace Calls { +namespace { + +constexpr auto kMaxInvitePerSlice = 10; +constexpr auto kCheckLastSpokeInterval = crl::time(1000); +constexpr auto kCheckJoinedTimeout = 4 * crl::time(1000); +constexpr auto kUpdateSendActionEach = crl::time(500); +constexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000); +constexpr auto kFixManualLargeVideoDuration = 5 * crl::time(1000); +constexpr auto kFixSpeakingLargeVideoDuration = 3 * crl::time(1000); +constexpr auto kFullAsMediumsCount = 4; // 1 Full is like 4 Mediums. +constexpr auto kMaxMediumQualities = 16; // 4 Fulls or 16 Mediums. + +[[nodiscard]] std::unique_ptr CreateMediaDevices() { + const auto &settings = Core::App().settings(); + return Webrtc::CreateMediaDevices( + settings.callInputDeviceId(), + settings.callOutputDeviceId(), + settings.callVideoInputDeviceId()); +} + +[[nodiscard]] const Data::GroupCallParticipant *LookupParticipant( + not_null peer, + uint64 id, + not_null participantPeer) { + const auto call = peer->groupCall(); + return (id && call && call->id() == id) + ? call->participantByPeer(participantPeer) + : nullptr; +} + +[[nodiscard]] double TimestampFromMsgId(mtpMsgId msgId) { + return msgId / double(1ULL << 32); +} + +[[nodiscard]] std::string ReadJsonString( + const QJsonObject &object, + const char *key) { + return object.value(key).toString().toStdString(); +} + +[[nodiscard]] uint64 FindLocalRaisedHandRating( + const std::vector &list) { + const auto i = ranges::max_element( + list, + ranges::less(), + &Data::GroupCallParticipant::raisedHandRating); + return (i == end(list)) ? 1 : (i->raisedHandRating + 1); +} + +struct JoinVideoEndpoint { + std::string id; +}; + +struct JoinBroadcastStream { +}; + +using JoinClientFields = std::variant< + v::null_t, + JoinVideoEndpoint, + JoinBroadcastStream>; + +[[nodiscard]] JoinClientFields ParseJoinResponse(const QByteArray &json) { + auto error = QJsonParseError{ 0, QJsonParseError::NoError }; + const auto document = QJsonDocument::fromJson(json, &error); + if (error.error != QJsonParseError::NoError) { + LOG(("API Error: " + "Failed to parse join response params, error: %1." + ).arg(error.errorString())); + return {}; + } else if (!document.isObject()) { + LOG(("API Error: " + "Not an object received in join response params.")); + return {}; + } + if (document.object().value("stream").toBool()) { + return JoinBroadcastStream{}; + } + const auto video = document.object().value("video").toObject(); + return JoinVideoEndpoint{ + video.value("endpoint").toString().toStdString(), + }; +} + +[[nodiscard]] const std::string &EmptyString() { + static const auto result = std::string(); + return result; +} + +} // namespace + +class GroupCall::LoadPartTask final : public tgcalls::BroadcastPartTask { +public: + LoadPartTask( + base::weak_ptr call, + int64 time, + int64 period, + Fn done); + + [[nodiscard]] int64 time() const { + return _time; + } + [[nodiscard]] int32 scale() const { + return _scale; + } + + void done(tgcalls::BroadcastPart &&part); + void cancel() override; + +private: + const base::weak_ptr _call; + const int64 _time = 0; + const int32 _scale = 0; + Fn _done; + QMutex _mutex; + +}; + +class GroupCall::MediaChannelDescriptionsTask final + : public tgcalls::RequestMediaChannelDescriptionTask { +public: + MediaChannelDescriptionsTask( + base::weak_ptr call, + const std::vector &ssrcs, + Fn&&)> done); + + [[nodiscard]] base::flat_set ssrcs() const; + + [[nodiscard]] bool finishWithAdding( + uint32 ssrc, + std::optional description, + bool screen = false); + + void cancel() override; + +private: + const base::weak_ptr _call; + base::flat_set _ssrcs; + base::flat_set _cameraAdded; + base::flat_set _screenAdded; + std::vector _result; + Fn&&)> _done; + QMutex _mutex; + +}; + +struct GroupCall::SinkPointer { + std::weak_ptr data; +}; + +struct GroupCall::VideoTrack { + VideoTrack(bool paused, bool requireARGB32, not_null peer); + + Webrtc::VideoTrack track; + rpl::variable trackSize; + not_null peer; + rpl::lifetime lifetime; + Group::VideoQuality quality = Group::VideoQuality(); + bool shown = false; +}; + +GroupCall::VideoTrack::VideoTrack( + bool paused, + bool requireARGB32, + not_null peer) +: track((paused + ? Webrtc::VideoState::Paused + : Webrtc::VideoState::Active), + requireARGB32) +, peer(peer) { +} + +[[nodiscard]] bool IsGroupCallAdmin( + not_null peer, + not_null participantPeer) { + const auto user = participantPeer->asUser(); + if (!user) { + return (peer == participantPeer); + } + if (const auto chat = peer->asChat()) { + return chat->admins.contains(user) + || (chat->creator == peerToUser(user->id)); + } else if (const auto group = peer->asChannel()) { + if (const auto mgInfo = group->mgInfo.get()) { + if (mgInfo->creator == user) { + return true; + } + const auto i = mgInfo->lastAdmins.find(user); + if (i == mgInfo->lastAdmins.end()) { + return false; + } + const auto &rights = i->second.rights; + return rights.c_chatAdminRights().is_manage_call(); + } + } + return false; +} + +struct VideoParams { + std::string endpointId; + std::vector ssrcGroups; + bool paused = false; + + [[nodiscard]] bool empty() const { + return endpointId.empty() || ssrcGroups.empty(); + } + [[nodiscard]] explicit operator bool() const { + return !empty(); + } +}; + +struct ParticipantVideoParams { + VideoParams camera; + VideoParams screen; +}; + +[[nodiscard]] bool VideoParamsAreEqual( + const VideoParams &was, + const tl::conditional &now) { + if (!now) { + return !was; + } + return now->match([&](const MTPDgroupCallParticipantVideo &data) { + if (data.is_paused() != was.paused) { + return false; + } + if (gsl::make_span(data.vendpoint().v) + != gsl::make_span(was.endpointId)) { + return false; + } + const auto &list = data.vsource_groups().v; + if (list.size() != was.ssrcGroups.size()) { + return false; + } + auto index = 0; + for (const auto &group : list) { + const auto equal = group.match([&]( + const MTPDgroupCallParticipantVideoSourceGroup &data) { + const auto &group = was.ssrcGroups[index++]; + if (gsl::make_span(data.vsemantics().v) + != gsl::make_span(group.semantics)) { + return false; + } + const auto list = data.vsources().v; + if (list.size() != group.ssrcs.size()) { + return false; + } + auto i = 0; + for (const auto &ssrc : list) { + if (ssrc.v != group.ssrcs[i++]) { + return false; + } + } + return true; + }); + if (!equal) { + return false; + } + } + return true; + }); +} + +[[nodiscard]] VideoParams ParseVideoParams( + const tl::conditional ¶ms) { + if (!params) { + return VideoParams(); + } + auto result = VideoParams(); + params->match([&](const MTPDgroupCallParticipantVideo &data) { + result.paused = data.is_paused(); + result.endpointId = data.vendpoint().v.toStdString(); + const auto &list = data.vsource_groups().v; + result.ssrcGroups.reserve(list.size()); + for (const auto &group : list) { + group.match([&]( + const MTPDgroupCallParticipantVideoSourceGroup &data) { + const auto &list = data.vsources().v; + auto ssrcs = std::vector(); + ssrcs.reserve(list.size()); + for (const auto &ssrc : list) { + ssrcs.push_back(ssrc.v); + } + result.ssrcGroups.push_back({ + .semantics = data.vsemantics().v.toStdString(), + .ssrcs = std::move(ssrcs), + }); + }); + } + }); + return result; +} + +const std::string &GetCameraEndpoint( + const std::shared_ptr ¶ms) { + return params ? params->camera.endpointId : EmptyString(); +} + +const std::string &GetScreenEndpoint( + const std::shared_ptr ¶ms) { + return params ? params->screen.endpointId : EmptyString(); +} + +bool IsCameraPaused(const std::shared_ptr ¶ms) { + return params && params->camera.paused; +} + +bool IsScreenPaused(const std::shared_ptr ¶ms) { + return params && params->screen.paused; +} + +std::shared_ptr ParseVideoParams( + const tl::conditional &camera, + const tl::conditional &screen, + const std::shared_ptr &existing) { + using namespace tgcalls; + + if (!camera && !screen) { + return nullptr; + } + if (existing + && VideoParamsAreEqual(existing->camera, camera) + && VideoParamsAreEqual(existing->screen, screen)) { + return existing; + } + // We don't reuse existing pointer, that way we can compare pointers + // to see if anything was changed in video params. + const auto data = /*existing + ? existing + : */std::make_shared(); + data->camera = ParseVideoParams(camera); + data->screen = ParseVideoParams(screen); + return data; +} + +GroupCall::LoadPartTask::LoadPartTask( + base::weak_ptr call, + int64 time, + int64 period, + Fn done) +: _call(std::move(call)) +, _time(time ? time : (base::unixtime::now() * int64(1000))) +, _scale([&] { + switch (period) { + case 1000: return 0; + case 500: return 1; + case 250: return 2; + case 125: return 3; + } + Unexpected("Period in LoadPartTask."); +}()) +, _done(std::move(done)) { +} + +void GroupCall::LoadPartTask::done(tgcalls::BroadcastPart &&part) { + QMutexLocker lock(&_mutex); + if (_done) { + base::take(_done)(std::move(part)); + } +} + +void GroupCall::LoadPartTask::cancel() { + QMutexLocker lock(&_mutex); + if (!_done) { + return; + } + _done = nullptr; + lock.unlock(); + + if (_call) { + const auto that = this; + crl::on_main(_call, [weak = _call, that] { + if (const auto strong = weak.get()) { + strong->broadcastPartCancel(that); + } + }); + } +} + +GroupCall::MediaChannelDescriptionsTask::MediaChannelDescriptionsTask( + base::weak_ptr call, + const std::vector &ssrcs, + Fn&&)> done) +: _call(std::move(call)) +, _ssrcs(ssrcs.begin(), ssrcs.end()) +, _done(std::move(done)) { +} + +auto GroupCall::MediaChannelDescriptionsTask::ssrcs() const +-> base::flat_set { + return _ssrcs; +} + +bool GroupCall::MediaChannelDescriptionsTask::finishWithAdding( + uint32 ssrc, + std::optional description, + bool screen) { + Expects(_ssrcs.contains(ssrc)); + + using Type = tgcalls::MediaChannelDescription::Type; + _ssrcs.remove(ssrc); + if (!description) { + } else if (description->type == Type::Audio + || (!screen && _cameraAdded.emplace(description->audioSsrc).second) + || (screen && _screenAdded.emplace(description->audioSsrc).second)) { + _result.push_back(std::move(*description)); + } + + if (!_ssrcs.empty()) { + return false; + } + QMutexLocker lock(&_mutex); + if (_done) { + base::take(_done)(std::move(_result)); + } + return true; +} + +void GroupCall::MediaChannelDescriptionsTask::cancel() { + QMutexLocker lock(&_mutex); + if (!_done) { + return; + } + _done = nullptr; + lock.unlock(); + + if (_call) { + const auto that = this; + crl::on_main(_call, [weak = _call, that] { + if (const auto strong = weak.get()) { + strong->mediaChannelDescriptionsCancel(that); + } + }); + } +} + +not_null GroupCall::TrackPeer( + const std::unique_ptr &track) { + return track->peer; +} + +not_null GroupCall::TrackPointer( + const std::unique_ptr &track) { + return &track->track; +} + +rpl::producer GroupCall::TrackSizeValue( + const std::unique_ptr &track) { + return track->trackSize.value(); +} + +GroupCall::GroupCall( + not_null delegate, + Group::JoinInfo info, + const MTPInputGroupCall &inputCall) +: _delegate(delegate) +, _peer(info.peer) +, _history(_peer->owner().history(_peer)) +, _api(&_peer->session().mtp()) +, _joinAs(info.joinAs) +, _possibleJoinAs(std::move(info.possibleJoinAs)) +, _joinHash(info.joinHash) +, _canManage(Data::CanManageGroupCallValue(_peer)) +, _id(inputCall.c_inputGroupCall().vid().v) +, _scheduleDate(info.scheduleDate) +, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) +, _checkJoinedTimer([=] { checkJoined(); }) +, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) +, _connectingSoundTimer([=] { playConnectingSoundOnce(); }) +, _mediaDevices(CreateMediaDevices()) { + _muted.value( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](MuteState previous, MuteState state) { + if (_instance) { + updateInstanceMuteState(); + } + if (_joinState.ssrc + && (!_initialMuteStateSent || state == MuteState::Active)) { + _initialMuteStateSent = true; + maybeSendMutedUpdate(previous); + } + }, _lifetime); + + _instanceState.value( + ) | rpl::filter([=] { + return _hadJoinedState; + }) | rpl::start_with_next([=](InstanceState state) { + if (state == InstanceState::Disconnected) { + playConnectingSound(); + } else { + stopConnectingSound(); + } + }, _lifetime); + + checkGlobalShortcutAvailability(); + + if (const auto real = lookupReal()) { + subscribeToReal(real); + if (!canManage() && real->joinMuted()) { + _muted = MuteState::ForceMuted; + } + } else { + _peer->session().changes().peerFlagsValue( + _peer, + Data::PeerUpdate::Flag::GroupCall + ) | rpl::map([=] { + return lookupReal(); + }) | rpl::filter([](Data::GroupCall *real) { + return real != nullptr; + }) | rpl::map([](Data::GroupCall *real) { + return not_null{ real }; + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](not_null real) { + subscribeToReal(real); + _realChanges.fire_copy(real); + }, _lifetime); + } + + setupMediaDevices(); + setupOutgoingVideo(); + + if (_id) { + join(inputCall); + } else { + start(info.scheduleDate); + } + if (_scheduleDate) { + saveDefaultJoinAs(_joinAs); + } +} + +GroupCall::~GroupCall() { + destroyScreencast(); + destroyController(); +} + +bool GroupCall::isSharingScreen() const { + return _isSharingScreen.current(); +} + +rpl::producer GroupCall::isSharingScreenValue() const { + return _isSharingScreen.value(); +} + +bool GroupCall::isScreenPaused() const { + return (_screenState.current() == Webrtc::VideoState::Paused); +} + +const std::string &GroupCall::screenSharingEndpoint() const { + return isSharingScreen() ? _screenEndpoint : EmptyString(); +} + +bool GroupCall::isSharingCamera() const { + return _isSharingCamera.current(); +} + +rpl::producer GroupCall::isSharingCameraValue() const { + return _isSharingCamera.value(); +} + +bool GroupCall::isCameraPaused() const { + return (_cameraState.current() == Webrtc::VideoState::Paused); +} + +const std::string &GroupCall::cameraSharingEndpoint() const { + return isSharingCamera() ? _cameraEndpoint : EmptyString(); +} + +QString GroupCall::screenSharingDeviceId() const { + return isSharingScreen() ? _screenDeviceId : QString(); +} + +bool GroupCall::mutedByAdmin() const { + const auto mute = muted(); + return mute == MuteState::ForceMuted || mute == MuteState::RaisedHand; +} + +bool GroupCall::canManage() const { + return _canManage.current(); +} + +rpl::producer GroupCall::canManageValue() const { + return _canManage.value(); +} + +void GroupCall::toggleVideo(bool active) { + if (!_instance || !_id) { + return; + } + _cameraState = active + ? Webrtc::VideoState::Active + : Webrtc::VideoState::Inactive; +} + +void GroupCall::toggleScreenSharing(std::optional uniqueId) { + if (!_instance || !_id) { + return; + } else if (!uniqueId) { + _screenState = Webrtc::VideoState::Inactive; + return; + } + const auto changed = (_screenDeviceId != *uniqueId); + const auto wasSharing = isSharingScreen(); + _screenDeviceId = *uniqueId; + _screenState = Webrtc::VideoState::Active; + if (changed && wasSharing && isSharingScreen()) { + _screenCapture->switchToDevice(uniqueId->toStdString()); + } +} + +bool GroupCall::hasVideoWithFrames() const { + return !_shownVideoTracks.empty(); +} + +rpl::producer GroupCall::hasVideoWithFramesValue() const { + return _videoStreamShownUpdates.events_starting_with( + VideoStateToggle() + ) | rpl::map([=] { + return hasVideoWithFrames(); + }) | rpl::distinct_until_changed(); +} + +void GroupCall::setScheduledDate(TimeId date) { + const auto was = _scheduleDate; + _scheduleDate = date; + if (was && !date) { + join(inputCall()); + } +} + +void GroupCall::subscribeToReal(not_null real) { + real->scheduleDateValue( + ) | rpl::start_with_next([=](TimeId date) { + setScheduledDate(date); + }, _lifetime); + + // If we joined before you could start video and then you can, + // you have to rejoin so that the server knows your video params. + //real->canStartVideoValue( // ignore can_start_video after call start. + //) | rpl::combine_previous( + //) | rpl::start_with_next([=](bool could, bool can) { + // if (could || !can) { + // return; + // } if (_joinState.action == JoinAction::None) { + // rejoin(); + // } else { + // _joinState.nextActionPending = true; + // } + //}, _lifetime); + + // Postpone creating video tracks, so that we know if Panel + // supports OpenGL and we don't need ARGB32 frames at all. + Ui::PostponeCall(this, [=] { + if (const auto real = lookupReal()) { + real->participantsReloaded( + ) | rpl::start_with_next([=] { + fillActiveVideoEndpoints(); + }, _lifetime); + fillActiveVideoEndpoints(); + } + }); + + using Update = Data::GroupCall::ParticipantUpdate; + real->participantUpdated( + ) | rpl::start_with_next([=](const Update &data) { + const auto regularEndpoint = [&](const std::string &endpoint) + -> const std::string & { + return (endpoint.empty() + || endpoint == _cameraEndpoint + || endpoint == _screenEndpoint) + ? EmptyString() + : endpoint; + }; + + const auto peer = data.was ? data.was->peer : data.now->peer; + if (peer == _joinAs) { + const auto working = data.now && data.now->videoJoined; + if (videoIsWorking() != working) { + fillActiveVideoEndpoints(); + } + return; + } + const auto &wasCameraEndpoint = data.was + ? regularEndpoint(GetCameraEndpoint(data.was->videoParams)) + : EmptyString(); + const auto &nowCameraEndpoint = data.now + ? regularEndpoint(GetCameraEndpoint(data.now->videoParams)) + : EmptyString(); + const auto wasCameraPaused = !wasCameraEndpoint.empty() + && IsCameraPaused(data.was->videoParams); + const auto nowCameraPaused = !nowCameraEndpoint.empty() + && IsCameraPaused(data.now->videoParams); + if (wasCameraEndpoint != nowCameraEndpoint) { + markEndpointActive({ + VideoEndpointType::Camera, + peer, + nowCameraEndpoint, + }, true, nowCameraPaused); + markEndpointActive({ + VideoEndpointType::Camera, + peer, + wasCameraEndpoint, + }, false, false); + } else if (wasCameraPaused != nowCameraPaused) { + markTrackPaused({ + VideoEndpointType::Camera, + peer, + nowCameraEndpoint, + }, nowCameraPaused); + } + const auto &wasScreenEndpoint = data.was + ? regularEndpoint(data.was->screenEndpoint()) + : EmptyString(); + const auto &nowScreenEndpoint = data.now + ? regularEndpoint(data.now->screenEndpoint()) + : EmptyString(); + const auto wasScreenPaused = !wasScreenEndpoint.empty() + && IsScreenPaused(data.was->videoParams); + const auto nowScreenPaused = !nowScreenEndpoint.empty() + && IsScreenPaused(data.now->videoParams); + if (wasScreenEndpoint != nowScreenEndpoint) { + markEndpointActive({ + VideoEndpointType::Screen, + peer, + nowScreenEndpoint, + }, true, nowScreenPaused); + markEndpointActive({ + VideoEndpointType::Screen, + peer, + wasScreenEndpoint, + }, false, false); + } else if (wasScreenPaused != nowScreenPaused) { + markTrackPaused({ + VideoEndpointType::Screen, + peer, + wasScreenEndpoint, + }, nowScreenPaused); + } + }, _lifetime); + + real->participantsResolved( + ) | rpl::start_with_next([=]( + not_null*> ssrcs) { + checkMediaChannelDescriptions([&](uint32 ssrc) { + return ssrcs->contains(ssrc); + }); + }, _lifetime); + + real->participantSpeaking( + ) | rpl::filter([=] { + return _videoEndpointLarge.current(); + }) | rpl::start_with_next([=](not_null p) { + const auto now = crl::now(); + if (_videoEndpointLarge.current().peer == p->peer) { + _videoLargeTillTime = std::max( + _videoLargeTillTime, + now + kFixSpeakingLargeVideoDuration); + return; + } else if (videoEndpointPinned() || _videoLargeTillTime > now) { + return; + } + using Type = VideoEndpointType; + const auto ¶ms = p->videoParams; + if (GetCameraEndpoint(params).empty() + && GetScreenEndpoint(params).empty()) { + return; + } + const auto tryEndpoint = [&](Type type, const std::string &id) { + if (id.empty()) { + return false; + } + const auto endpoint = VideoEndpoint{ type, p->peer, id }; + if (!shownVideoTracks().contains(endpoint)) { + return false; + } + setVideoEndpointLarge(endpoint); + return true; + }; + if (tryEndpoint(Type::Screen, GetScreenEndpoint(params)) + || tryEndpoint(Type::Camera, GetCameraEndpoint(params))) { + _videoLargeTillTime = now + kFixSpeakingLargeVideoDuration; + } + }, _lifetime); +} + +void GroupCall::checkGlobalShortcutAvailability() { + auto &settings = Core::App().settings(); + if (!settings.groupCallPushToTalk()) { + return; + } else if (!base::GlobalShortcutsAllowed()) { + settings.setGroupCallPushToTalk(false); + Core::App().saveSettingsDelayed(); + } +} + +void GroupCall::setState(State state) { + const auto current = _state.current(); + if (current == State::Failed) { + return; + } else if (current == State::Ended && state != State::Failed) { + return; + } else if (current == State::FailedHangingUp && state != State::Failed) { + return; + } else if (current == State::HangingUp + && state != State::Ended + && state != State::Failed) { + return; + } + if (current == state) { + return; + } + _state = state; + + if (state == State::Joined) { + stopConnectingSound(); + if (const auto call = _peer->groupCall(); call && call->id() == _id) { + call->setInCall(); + } + if (!videoIsWorking()) { + refreshHasNotShownVideo(); + } + } + + if (false + || state == State::Ended + || state == State::Failed) { + // Destroy controller before destroying Call Panel, + // so that the panel hide animation is smooth. + destroyScreencast(); + destroyController(); + } + switch (state) { + case State::HangingUp: + case State::FailedHangingUp: + stopConnectingSound(); + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended); + break; + case State::Ended: + stopConnectingSound(); + _delegate->groupCallFinished(this); + break; + case State::Failed: + stopConnectingSound(); + _delegate->groupCallFailed(this); + break; + case State::Connecting: + if (!_checkJoinedTimer.isActive()) { + _checkJoinedTimer.callOnce(kCheckJoinedTimeout); + } + break; + } +} + +void GroupCall::playConnectingSound() { + const auto state = _state.current(); + if (_connectingSoundTimer.isActive() + || state == State::HangingUp + || state == State::FailedHangingUp + || state == State::Ended + || state == State::Failed) { + return; + } + playConnectingSoundOnce(); + _connectingSoundTimer.callEach(kPlayConnectingEach); +} + +void GroupCall::stopConnectingSound() { + _connectingSoundTimer.cancel(); +} + +void GroupCall::playConnectingSoundOnce() { + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting); +} + +bool GroupCall::showChooseJoinAs() const { + return (_possibleJoinAs.size() > 1) + || (_possibleJoinAs.size() == 1 + && !_possibleJoinAs.front()->isSelf()); +} + +bool GroupCall::scheduleStartSubscribed() const { + if (const auto real = lookupReal()) { + return real->scheduleStartSubscribed(); + } + return false; +} + +Data::GroupCall *GroupCall::lookupReal() const { + const auto real = _peer->groupCall(); + return (real && real->id() == _id) ? real : nullptr; +} + +rpl::producer> GroupCall::real() const { + if (const auto real = lookupReal()) { + return rpl::single(not_null{ real }); + } + return _realChanges.events(); +} + +void GroupCall::start(TimeId scheduleDate) { + using Flag = MTPphone_CreateGroupCall::Flag; + _createRequestId = _api.request(MTPphone_CreateGroupCall( + MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag(0)), + _peer->input, + MTP_int(openssl::RandomValue()), + MTPstring(), // title + MTP_int(scheduleDate) + )).done([=](const MTPUpdates &result) { + _acceptFields = true; + _peer->session().api().applyUpdates(result); + _acceptFields = false; + }).fail([=](const MTP::Error &error) { + LOG(("Call Error: Could not create, error: %1" + ).arg(error.type())); + hangup(); + if (error.type() == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q) { + Ui::ShowMultilineToast({ + .text = { tr::lng_group_call_no_anonymous(tr::now) }, + }); + } + }).send(); +} + +void GroupCall::join(const MTPInputGroupCall &inputCall) { + inputCall.match([&](const MTPDinputGroupCall &data) { + _id = data.vid().v; + _accessHash = data.vaccess_hash().v; + }); + setState(_scheduleDate ? State::Waiting : State::Joining); + + if (_scheduleDate) { + return; + } + rejoin(); + + using Update = Data::GroupCall::ParticipantUpdate; + const auto real = lookupReal(); + Assert(real != nullptr); + real->participantUpdated( + ) | rpl::filter([=](const Update &update) { + return (_instance != nullptr); + }) | rpl::start_with_next([=](const Update &update) { + if (!update.now) { + _instance->removeSsrcs({ update.was->ssrc }); + } else { + const auto &now = *update.now; + const auto &was = update.was; + const auto volumeChanged = was + ? (was->volume != now.volume + || was->mutedByMe != now.mutedByMe) + : (now.volume != Group::kDefaultVolume || now.mutedByMe); + if (volumeChanged) { + _instance->setVolume( + now.ssrc, + (now.mutedByMe + ? 0. + : (now.volume + / float64(Group::kDefaultVolume)))); + } + } + }, _lifetime); + + _peer->session().updates().addActiveChat( + _peerStream.events_starting_with_copy(_peer)); + SubscribeToMigration(_peer, _lifetime, [=](not_null group) { + _peer = group; + _canManage = Data::CanManageGroupCallValue(_peer); + _peerStream.fire_copy(group); + }); +} + +void GroupCall::setScreenEndpoint(std::string endpoint) { + if (_screenEndpoint == endpoint) { + return; + } + if (!_screenEndpoint.empty()) { + markEndpointActive({ + VideoEndpointType::Screen, + _joinAs, + _screenEndpoint + }, false, false); + } + _screenEndpoint = std::move(endpoint); + if (_screenEndpoint.empty()) { + return; + } + if (isSharingScreen()) { + markEndpointActive({ + VideoEndpointType::Screen, + _joinAs, + _screenEndpoint + }, true, isScreenPaused()); + } +} + +void GroupCall::setCameraEndpoint(std::string endpoint) { + if (_cameraEndpoint == endpoint) { + return; + } + if (!_cameraEndpoint.empty()) { + markEndpointActive({ + VideoEndpointType::Camera, + _joinAs, + _cameraEndpoint + }, false, false); + } + _cameraEndpoint = std::move(endpoint); + if (_cameraEndpoint.empty()) { + return; + } + if (isSharingCamera()) { + markEndpointActive({ + VideoEndpointType::Camera, + _joinAs, + _cameraEndpoint + }, true, isCameraPaused()); + } +} + +void GroupCall::addVideoOutput( + const std::string &endpoint, + SinkPointer sink) { + if (_cameraEndpoint == endpoint) { + if (auto strong = sink.data.lock()) { + _cameraCapture->setOutput(std::move(strong)); + } + } else if (_screenEndpoint == endpoint) { + if (auto strong = sink.data.lock()) { + _screenCapture->setOutput(std::move(strong)); + } + } else if (_instance) { + _instance->addIncomingVideoOutput(endpoint, std::move(sink.data)); + } else { + _pendingVideoOutputs.emplace(endpoint, std::move(sink)); + } +} + +void GroupCall::markEndpointActive( + VideoEndpoint endpoint, + bool active, + bool paused) { + if (!endpoint) { + return; + } else if (active && !videoIsWorking()) { + refreshHasNotShownVideo(); + return; + } + const auto i = _activeVideoTracks.find(endpoint); + const auto changed = active + ? (i == end(_activeVideoTracks)) + : (i != end(_activeVideoTracks)); + if (!changed) { + if (active) { + markTrackPaused(endpoint, paused); + } + return; + } + auto shown = false; + if (active) { + const auto i = _activeVideoTracks.emplace( + endpoint, + std::make_unique( + paused, + _requireARGB32, + endpoint.peer)).first; + const auto track = &i->second->track; + + track->renderNextFrame( + ) | rpl::start_with_next([=] { + const auto activeTrack = _activeVideoTracks[endpoint].get(); + const auto size = track->frameSize(); + if (size.isEmpty()) { + track->markFrameShown(); + } else if (!activeTrack->shown) { + activeTrack->shown = true; + markTrackShown(endpoint, true); + } + activeTrack->trackSize = size; + }, i->second->lifetime); + + const auto size = track->frameSize(); + i->second->trackSize = size; + if (!size.isEmpty() || paused) { + i->second->shown = true; + shown = true; + } else { + track->stateValue( + ) | rpl::filter([=](Webrtc::VideoState state) { + return (state == Webrtc::VideoState::Paused) + && !_activeVideoTracks[endpoint]->shown; + }) | rpl::start_with_next([=] { + _activeVideoTracks[endpoint]->shown = true; + markTrackShown(endpoint, true); + }, i->second->lifetime); + } + addVideoOutput(i->first.id, { track->sink() }); + } else { + if (_videoEndpointLarge.current() == endpoint) { + setVideoEndpointLarge({}); + } + markTrackShown(endpoint, false); + markTrackPaused(endpoint, false); + _activeVideoTracks.erase(i); + } + updateRequestedVideoChannelsDelayed(); + _videoStreamActiveUpdates.fire({ endpoint, active }); + if (active) { + markTrackShown(endpoint, shown); + markTrackPaused(endpoint, paused); + } +} + +void GroupCall::markTrackShown(const VideoEndpoint &endpoint, bool shown) { + const auto changed = shown + ? _shownVideoTracks.emplace(endpoint).second + : _shownVideoTracks.remove(endpoint); + if (!changed) { + return; + } + _videoStreamShownUpdates.fire_copy({ endpoint, shown }); + if (shown && endpoint.type == VideoEndpointType::Screen) { + crl::on_main(this, [=] { + if (_shownVideoTracks.contains(endpoint)) { + pinVideoEndpoint(endpoint); + } + }); + } +} + +void GroupCall::markTrackPaused(const VideoEndpoint &endpoint, bool paused) { + if (!endpoint) { + return; + } + + const auto i = _activeVideoTracks.find(endpoint); + Assert(i != end(_activeVideoTracks)); + + i->second->track.setState(paused + ? Webrtc::VideoState::Paused + : Webrtc::VideoState::Active); +} + +void GroupCall::rejoin() { + rejoin(_joinAs); +} + +void GroupCall::rejoinWithHash(const QString &hash) { + if (!hash.isEmpty() && mutedByAdmin()) { + _joinHash = hash; + rejoin(); + } +} + +void GroupCall::setJoinAs(not_null as) { + _joinAs = as; + if (const auto chat = _peer->asChat()) { + chat->setGroupCallDefaultJoinAs(_joinAs->id); + } else if (const auto channel = _peer->asChannel()) { + channel->setGroupCallDefaultJoinAs(_joinAs->id); + } +} + +void GroupCall::saveDefaultJoinAs(not_null as) { + setJoinAs(as); + _api.request(MTPphone_SaveDefaultGroupCallJoinAs( + _peer->input, + _joinAs->input + )).send(); +} + +void GroupCall::rejoin(not_null as) { + if (state() != State::Joining + && state() != State::Joined + && state() != State::Connecting) { + return; + } else if (_joinState.action != JoinAction::None) { + return; + } + + if (_joinAs != as) { + toggleVideo(false); + toggleScreenSharing(std::nullopt); + } + + _joinState.action = JoinAction::Joining; + _joinState.ssrc = 0; + _initialMuteStateSent = false; + setState(State::Joining); + if (!tryCreateController()) { + setInstanceMode(InstanceMode::None); + } + applyMeInCallLocally(); + LOG(("Call Info: Requesting join payload.")); + + setJoinAs(as); + + const auto weak = base::make_weak(&_instanceGuard); + _instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { + crl::on_main(weak, [=, payload = std::move(payload)] { + if (state() != State::Joining) { + _joinState.finish(); + checkNextJoinAction(); + return; + } + const auto ssrc = payload.audioSsrc; + LOG(("Call Info: Join payload received, joining with ssrc: %1." + ).arg(ssrc)); + + const auto json = QByteArray::fromStdString(payload.json); + const auto wasMuteState = muted(); + const auto wasVideoStopped = !isSharingCamera(); + using Flag = MTPphone_JoinGroupCall::Flag; + const auto flags = (wasMuteState != MuteState::Active + ? Flag::f_muted + : Flag(0)) + | (_joinHash.isEmpty() + ? Flag(0) + : Flag::f_invite_hash) + | (wasVideoStopped + ? Flag::f_video_stopped + : Flag(0)); + _api.request(MTPphone_JoinGroupCall( + MTP_flags(flags), + inputCall(), + _joinAs->input, + MTP_string(_joinHash), + MTP_dataJSON(MTP_bytes(json)) + )).done([=](const MTPUpdates &updates) { + _joinState.finish(ssrc); + _mySsrcs.emplace(ssrc); + + setState((_instanceState.current() + == InstanceState::Disconnected) + ? State::Connecting + : State::Joined); + applyMeInCallLocally(); + maybeSendMutedUpdate(wasMuteState); + _peer->session().api().applyUpdates(updates); + applyQueuedSelfUpdates(); + checkFirstTimeJoined(); + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + if (wasVideoStopped == isSharingCamera()) { + sendSelfUpdate(SendUpdateType::CameraStopped); + } + if (isCameraPaused()) { + sendSelfUpdate(SendUpdateType::CameraPaused); + } + sendPendingSelfUpdates(); + }).fail([=](const MTP::Error &error) { + _joinState.finish(); + + const auto type = error.type(); + LOG(("Call Error: Could not join, error: %1").arg(type)); + + if (type == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { + rejoin(); + return; + } + + hangup(); + Ui::ShowMultilineToast({ + .text = { type == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q + ? tr::lng_group_call_no_anonymous(tr::now) + : type == u"GROUPCALL_PARTICIPANTS_TOO_MUCH"_q + ? tr::lng_group_call_too_many(tr::now) + : type == u"GROUPCALL_FORBIDDEN"_q + ? tr::lng_group_not_accessible(tr::now) + : Lang::Hard::ServerError() }, + }); + }).send(); + }); + }); +} + +void GroupCall::checkNextJoinAction() { + if (_joinState.action != JoinAction::None) { + return; + } else if (_joinState.nextActionPending) { + _joinState.nextActionPending = false; + const auto state = _state.current(); + if (state != State::HangingUp && state != State::FailedHangingUp) { + rejoin(); + } else { + leave(); + } + } else if (!_joinState.ssrc) { + rejoin(); + } else if (_screenJoinState.action != JoinAction::None + || !_screenJoinState.nextActionPending) { + return; + } else { + _screenJoinState.nextActionPending = false; + if (isSharingScreen()) { + rejoinPresentation(); + } else { + leavePresentation(); + } + } +} + +void GroupCall::rejoinPresentation() { + if (!_joinState.ssrc + || _screenJoinState.action == JoinAction::Joining + || !isSharingScreen()) { + return; + } else if (_screenJoinState.action != JoinAction::None) { + _screenJoinState.nextActionPending = true; + return; + } + + _screenJoinState.action = JoinAction::Joining; + _screenJoinState.ssrc = 0; + if (!tryCreateScreencast()) { + setScreenInstanceMode(InstanceMode::None); + } + LOG(("Call Info: Requesting join screen payload.")); + + const auto weak = base::make_weak(&_screenInstanceGuard); + _screenInstance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { + crl::on_main(weak, [=, payload = std::move(payload)]{ + if (!isSharingScreen() || !_joinState.ssrc) { + _screenJoinState.finish(); + checkNextJoinAction(); + return; + } + const auto withMainSsrc = _joinState.ssrc; + const auto ssrc = payload.audioSsrc; + LOG(("Call Info: Join screen payload received, ssrc: %1." + ).arg(ssrc)); + + const auto json = QByteArray::fromStdString(payload.json); + _api.request( + MTPphone_JoinGroupCallPresentation( + inputCall(), + MTP_dataJSON(MTP_bytes(json))) + ).done([=](const MTPUpdates &updates) { + _screenJoinState.finish(ssrc); + _mySsrcs.emplace(ssrc); + + _peer->session().api().applyUpdates(updates); + checkNextJoinAction(); + if (isScreenPaused()) { + sendSelfUpdate(SendUpdateType::ScreenPaused); + } + sendPendingSelfUpdates(); + }).fail([=](const MTP::Error &error) { + _screenJoinState.finish(); + + const auto type = error.type(); + if (type == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + } else if (type == u"GROUPCALL_JOIN_MISSING"_q + || type == u"GROUPCALL_FORBIDDEN"_q) { + if (_joinState.ssrc != withMainSsrc) { + // We've rejoined, rejoin presentation again. + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + } + } else { + LOG(("Call Error: " + "Could not screen join, error: %1").arg(type)); + _screenState = Webrtc::VideoState::Inactive; + _errors.fire_copy(mutedByAdmin() + ? Error::MutedNoScreen + : Error::ScreenFailed); + } + }).send(); + }); + }); +} + +void GroupCall::leavePresentation() { + destroyScreencast(); + if (!_screenJoinState.ssrc) { + setScreenEndpoint(std::string()); + return; + } else if (_screenJoinState.action == JoinAction::Leaving) { + return; + } else if (_screenJoinState.action != JoinAction::None) { + _screenJoinState.nextActionPending = true; + return; + } + _api.request( + MTPphone_LeaveGroupCallPresentation(inputCall()) + ).done([=](const MTPUpdates &updates) { + _screenJoinState.finish(); + + _peer->session().api().applyUpdates(updates); + setScreenEndpoint(std::string()); + checkNextJoinAction(); + }).fail([=](const MTP::Error &error) { + _screenJoinState.finish(); + + const auto type = error.type(); + LOG(("Call Error: " + "Could not screen leave, error: %1").arg(type)); + setScreenEndpoint(std::string()); + checkNextJoinAction(); + }).send(); +} + +void GroupCall::applyMeInCallLocally() { + const auto real = lookupReal(); + if (!real) { + return; + } + using Flag = MTPDgroupCallParticipant::Flag; + const auto participant = real->participantByPeer(_joinAs); + const auto date = participant + ? participant->date + : base::unixtime::now(); + const auto lastActive = participant + ? participant->lastActive + : TimeId(0); + const auto volume = participant + ? participant->volume + : Group::kDefaultVolume; + const auto canSelfUnmute = !mutedByAdmin(); + const auto raisedHandRating = (muted() != MuteState::RaisedHand) + ? uint64(0) + : participant + ? participant->raisedHandRating + : FindLocalRaisedHandRating(real->participants()); + const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0)) + | (lastActive ? Flag::f_active_date : Flag(0)) + | (_joinState.ssrc ? Flag(0) : Flag::f_left) + | (_videoIsWorking.current() ? Flag::f_video_joined : Flag(0)) + | Flag::f_self + | Flag::f_volume // Without flag the volume is reset to 100%. + | Flag::f_volume_by_admin // Self volume can only be set by admin. + | ((muted() != MuteState::Active) ? Flag::f_muted : Flag(0)) + | (raisedHandRating > 0 ? Flag::f_raise_hand_rating : Flag(0)); + real->applyLocalUpdate( + MTP_updateGroupCallParticipants( + inputCall(), + MTP_vector( + 1, + MTP_groupCallParticipant( + MTP_flags(flags), + peerToMTP(_joinAs->id), + MTP_int(date), + MTP_int(lastActive), + MTP_int(_joinState.ssrc), + MTP_int(volume), + MTPstring(), // Don't update about text in local updates. + MTP_long(raisedHandRating), + MTPGroupCallParticipantVideo(), + MTPGroupCallParticipantVideo())), + MTP_int(0)).c_updateGroupCallParticipants()); +} + +void GroupCall::applyParticipantLocally( + not_null participantPeer, + bool mute, + std::optional volume) { + const auto participant = LookupParticipant(_peer, _id, participantPeer); + if (!participant || !participant->ssrc) { + return; + } + const auto canManageCall = canManage(); + const auto isMuted = participant->muted || (mute && canManageCall); + const auto canSelfUnmute = !canManageCall + ? participant->canSelfUnmute + : (!mute || IsGroupCallAdmin(_peer, participantPeer)); + const auto isMutedByYou = mute && !canManageCall; + const auto mutedCount = 0/*participant->mutedCount*/; + using Flag = MTPDgroupCallParticipant::Flag; + const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0)) + | Flag::f_volume // Without flag the volume is reset to 100%. + | ((participant->applyVolumeFromMin && !volume) + ? Flag::f_volume_by_admin + : Flag(0)) + | (participant->videoJoined ? Flag::f_video_joined : Flag(0)) + | (participant->lastActive ? Flag::f_active_date : Flag(0)) + | (isMuted ? Flag::f_muted : Flag(0)) + | (isMutedByYou ? Flag::f_muted_by_you : Flag(0)) + | (participantPeer == _joinAs ? Flag::f_self : Flag(0)) + | (participant->raisedHandRating + ? Flag::f_raise_hand_rating + : Flag(0)); + _peer->groupCall()->applyLocalUpdate( + MTP_updateGroupCallParticipants( + inputCall(), + MTP_vector( + 1, + MTP_groupCallParticipant( + MTP_flags(flags), + peerToMTP(participantPeer->id), + MTP_int(participant->date), + MTP_int(participant->lastActive), + MTP_int(participant->ssrc), + MTP_int(volume.value_or(participant->volume)), + MTPstring(), // Don't update about text in local updates. + MTP_long(participant->raisedHandRating), + MTPGroupCallParticipantVideo(), + MTPGroupCallParticipantVideo())), + MTP_int(0)).c_updateGroupCallParticipants()); +} + +void GroupCall::hangup() { + finish(FinishType::Ended); +} + +void GroupCall::discard() { + if (!_id) { + _api.request(_createRequestId).cancel(); + hangup(); + return; + } + _api.request(MTPphone_DiscardGroupCall( + inputCall() + )).done([=](const MTPUpdates &result) { + // Here 'this' could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + crl::on_main(this, [=] { hangup(); }); + _peer->session().api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + hangup(); + }).send(); +} + +void GroupCall::rejoinAs(Group::JoinInfo info) { + _possibleJoinAs = std::move(info.possibleJoinAs); + if (info.joinAs == _joinAs) { + return; + } + const auto event = Group::RejoinEvent{ + .wasJoinAs = _joinAs, + .nowJoinAs = info.joinAs, + }; + if (_scheduleDate) { + saveDefaultJoinAs(info.joinAs); + } else { + setState(State::Joining); + rejoin(info.joinAs); + } + _rejoinEvents.fire_copy(event); +} + +void GroupCall::finish(FinishType type) { + Expects(type != FinishType::None); + + const auto finalState = (type == FinishType::Ended) + ? State::Ended + : State::Failed; + const auto hangupState = (type == FinishType::Ended) + ? State::HangingUp + : State::FailedHangingUp; + const auto state = _state.current(); + if (state == State::HangingUp + || state == State::FailedHangingUp + || state == State::Ended + || state == State::Failed) { + return; + } else if (_joinState.action == JoinAction::None && !_joinState.ssrc) { + setState(finalState); + return; + } + setState(hangupState); + _joinState.nextActionPending = true; + checkNextJoinAction(); +} + +void GroupCall::leave() { + Expects(_joinState.action == JoinAction::None); + + _joinState.action = JoinAction::Leaving; + + const auto finalState = (_state.current() == State::HangingUp) + ? State::Ended + : State::Failed; + + // We want to leave request still being sent and processed even if + // the call is already destroyed. + const auto session = &_peer->session(); + const auto weak = base::make_weak(this); + session->api().request(MTPphone_LeaveGroupCall( + inputCall(), + MTP_int(base::take(_joinState.ssrc)) + )).done([=](const MTPUpdates &result) { + // Here 'this' could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + crl::on_main(weak, [=] { setState(finalState); }); + session->api().applyUpdates(result); + }).fail(crl::guard(weak, [=](const MTP::Error &error) { + setState(finalState); + })).send(); +} + +void GroupCall::startScheduledNow() { + if (!lookupReal()) { + return; + } + _api.request(MTPphone_StartScheduledGroupCall( + inputCall() + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).send(); +} + +void GroupCall::toggleScheduleStartSubscribed(bool subscribed) { + if (!lookupReal()) { + return; + } + _api.request(MTPphone_ToggleGroupCallStartSubscription( + inputCall(), + MTP_bool(subscribed) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).send(); +} + +void GroupCall::setNoiseSuppression(bool enabled) { + if (_instance) { + _instance->setIsNoiseSuppressionEnabled(enabled); + } +} + +void GroupCall::addVideoOutput( + const std::string &endpoint, + not_null track) { + addVideoOutput(endpoint, { track->sink() }); +} + +void GroupCall::setMuted(MuteState mute) { + const auto set = [=] { + const auto was = muted(); + const auto wasSpeaking = (was == MuteState::Active) + || (was == MuteState::PushToTalk); + const auto wasMuted = (was == MuteState::Muted) + || (was == MuteState::PushToTalk); + const auto wasRaiseHand = (was == MuteState::RaisedHand); + _muted = mute; + const auto now = muted(); + const auto nowSpeaking = (now == MuteState::Active) + || (now == MuteState::PushToTalk); + const auto nowMuted = (now == MuteState::Muted) + || (now == MuteState::PushToTalk); + const auto nowRaiseHand = (now == MuteState::RaisedHand); + if (wasMuted != nowMuted || wasRaiseHand != nowRaiseHand) { + applyMeInCallLocally(); + } + if (mutedByAdmin()) { + toggleVideo(false); + toggleScreenSharing(std::nullopt); + } + if (wasSpeaking && !nowSpeaking && _joinState.ssrc) { + _levelUpdates.fire(LevelUpdate{ + .ssrc = _joinState.ssrc, + .value = 0.f, + .voice = false, + .me = true, + }); + } + }; + if (mute == MuteState::Active || mute == MuteState::PushToTalk) { + _delegate->groupCallRequestPermissionsOrFail(crl::guard(this, set)); + } else { + set(); + } +} + +void GroupCall::setMutedAndUpdate(MuteState mute) { + const auto was = muted(); + + // Active state is sent from _muted changes, + // because it may be set delayed, after permissions request, not now. + const auto send = _initialMuteStateSent && (mute != MuteState::Active); + setMuted(mute); + if (send) { + maybeSendMutedUpdate(was); + } +} + +void GroupCall::handlePossibleCreateOrJoinResponse( + const MTPDupdateGroupCall &data) { + data.vcall().match([&](const MTPDgroupCall &data) { + handlePossibleCreateOrJoinResponse(data); + }, [&](const MTPDgroupCallDiscarded &data) { + handlePossibleDiscarded(data); + }); +} + +void GroupCall::handlePossibleCreateOrJoinResponse( + const MTPDgroupCall &data) { + setScheduledDate(data.vschedule_date().value_or_empty()); + if (_acceptFields) { + if (!_instance && !_id) { + const auto input = MTP_inputGroupCall( + data.vid(), + data.vaccess_hash()); + const auto scheduleDate = data.vschedule_date().value_or_empty(); + if (const auto chat = _peer->asChat()) { + chat->setGroupCall(input, scheduleDate); + } else if (const auto group = _peer->asChannel()) { + group->setGroupCall(input, scheduleDate); + } else { + Unexpected("Peer type in GroupCall::join."); + } + join(input); + } + return; + } else if (_id != data.vid().v || !_instance) { + return; + } + if (const auto streamDcId = data.vstream_dc_id()) { + _broadcastDcId = MTP::BareDcId(streamDcId->v); + } +} + +void GroupCall::handlePossibleCreateOrJoinResponse( + const MTPDupdateGroupCallConnection &data) { + if (data.is_presentation()) { + if (!_screenInstance) { + return; + } + setScreenInstanceMode(InstanceMode::Rtc); + data.vparams().match([&](const MTPDdataJSON &data) { + const auto json = data.vdata().v; + const auto response = ParseJoinResponse(json); + const auto endpoint = std::get_if(&response); + if (endpoint) { + setScreenEndpoint(endpoint->id); + } else { + LOG(("Call Error: Bad response for 'presentation' flag.")); + } + _screenInstance->setJoinResponsePayload(json.toStdString()); + }); + } else { + if (!_instance) { + return; + } + data.vparams().match([&](const MTPDdataJSON &data) { + const auto json = data.vdata().v; + const auto response = ParseJoinResponse(json); + const auto endpoint = std::get_if(&response); + if (v::is(response)) { + if (!_broadcastDcId) { + LOG(("Api Error: Empty stream_dc_id in groupCall.")); + _broadcastDcId = _peer->session().mtp().mainDcId(); + } + setInstanceMode(InstanceMode::Stream); + } else { + setInstanceMode(InstanceMode::Rtc); + setCameraEndpoint(endpoint ? endpoint->id : std::string()); + _instance->setJoinResponsePayload(json.toStdString()); + } + updateRequestedVideoChannels(); + checkMediaChannelDescriptions(); + }); + } +} + +void GroupCall::handlePossibleDiscarded(const MTPDgroupCallDiscarded &data) { + if (data.vid().v == _id) { + LOG(("Call Info: Hangup after groupCallDiscarded.")); + _joinState.finish(); + hangup(); + } +} + +void GroupCall::checkMediaChannelDescriptions( + Fn resolved) { + const auto real = lookupReal(); + if (!real || (_instanceMode == InstanceMode::None)) { + return; + } + for (auto i = begin(_mediaChannelDescriptionses) + ; i != end(_mediaChannelDescriptionses);) { + if (mediaChannelDescriptionsFill(i->get(), resolved)) { + i = _mediaChannelDescriptionses.erase(i); + } else { + ++i; + } + } + if (!_unresolvedSsrcs.empty()) { + real->resolveParticipants(base::take(_unresolvedSsrcs)); + } +} + +void GroupCall::handleUpdate(const MTPUpdate &update) { + update.match([&](const MTPDupdateGroupCall &data) { + handleUpdate(data); + }, [&](const MTPDupdateGroupCallParticipants &data) { + handleUpdate(data); + }, [](const auto &) { + Unexpected("Type in Instance::applyGroupCallUpdateChecked."); + }); +} + +void GroupCall::handleUpdate(const MTPDupdateGroupCall &data) { + data.vcall().match([](const MTPDgroupCall &) { + }, [&](const MTPDgroupCallDiscarded &data) { + handlePossibleDiscarded(data); + }); +} + +void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) { + const auto callId = data.vcall().match([](const auto &data) { + return data.vid().v; + }); + if (_id != callId) { + return; + } + const auto state = _state.current(); + const auto joined = (state == State::Joined) + || (state == State::Connecting); + for (const auto &participant : data.vparticipants().v) { + participant.match([&](const MTPDgroupCallParticipant &data) { + const auto isSelf = data.is_self() + || (data.is_min() + && peerFromMTP(data.vpeer()) == _joinAs->id); + if (!isSelf) { + applyOtherParticipantUpdate(data); + } else if (joined) { + applySelfUpdate(data); + } else { + _queuedSelfUpdates.push_back(participant); + } + }); + } +} + +void GroupCall::applyQueuedSelfUpdates() { + const auto weak = base::make_weak(this); + while (weak + && !_queuedSelfUpdates.empty() + && (_state.current() == State::Joined + || _state.current() == State::Connecting)) { + const auto update = _queuedSelfUpdates.front(); + _queuedSelfUpdates.erase(_queuedSelfUpdates.begin()); + update.match([&](const MTPDgroupCallParticipant &data) { + applySelfUpdate(data); + }); + } +} + +void GroupCall::applySelfUpdate(const MTPDgroupCallParticipant &data) { + if (data.is_left()) { + if (data.vsource().v == _joinState.ssrc) { + // I was removed from the call, rejoin. + LOG(("Call Info: " + "Rejoin after got 'left' with my ssrc.")); + setState(State::Joining); + rejoin(); + } + return; + } else if (data.vsource().v != _joinState.ssrc) { + if (!_mySsrcs.contains(data.vsource().v)) { + // I joined from another device, hangup. + LOG(("Call Info: " + "Hangup after '!left' with ssrc %1, my %2." + ).arg(data.vsource().v + ).arg(_joinState.ssrc)); + _joinState.finish(); + hangup(); + } else { + LOG(("Call Info: " + "Some old 'self' with '!left' and ssrc %1, my %2." + ).arg(data.vsource().v + ).arg(_joinState.ssrc)); + } + return; + } + if (data.is_muted() && !data.is_can_self_unmute()) { + setMuted(data.vraise_hand_rating().value_or_empty() + ? MuteState::RaisedHand + : MuteState::ForceMuted); + } else if (_instanceMode == InstanceMode::Stream) { + LOG(("Call Info: Rejoin after unforcemute in stream mode.")); + setState(State::Joining); + rejoin(); + } else if (mutedByAdmin()) { + setMuted(MuteState::Muted); + if (!_instanceTransitioning) { + notifyAboutAllowedToSpeak(); + } + } else if (data.is_muted() && muted() != MuteState::Muted) { + setMuted(MuteState::Muted); + } +} + +void GroupCall::applyOtherParticipantUpdate( + const MTPDgroupCallParticipant &data) { + if (data.is_min()) { + // No real information about mutedByMe or my custom volume. + return; + } + const auto participantPeer = _peer->owner().peer( + peerFromMTP(data.vpeer())); + if (!LookupParticipant(_peer, _id, participantPeer)) { + return; + } + _otherParticipantStateValue.fire(Group::ParticipantState{ + .peer = participantPeer, + .volume = data.vvolume().value_or_empty(), + .mutedByMe = data.is_muted_by_you(), + }); +} + +void GroupCall::setupMediaDevices() { + _mediaDevices->audioInputId( + ) | rpl::start_with_next([=](QString id) { + _audioInputId = id; + if (_instance) { + _instance->setAudioInputDevice(id.toStdString()); + } + }, _lifetime); + + _mediaDevices->audioOutputId( + ) | rpl::start_with_next([=](QString id) { + _audioOutputId = id; + if (_instance) { + _instance->setAudioOutputDevice(id.toStdString()); + } + }, _lifetime); + + _mediaDevices->videoInputId( + ) | rpl::start_with_next([=](QString id) { + _cameraInputId = id; + if (_cameraCapture) { + _cameraCapture->switchToDevice(id.toStdString()); + } + }, _lifetime); +} + +bool GroupCall::emitShareCameraError() { + const auto emitError = [=](Error error) { + emitShareCameraError(error); + return true; + }; + /*if (const auto real = lookupReal(); real && !real->canStartVideo()) { + return emitError(Error::DisabledNoCamera); + } else */if (!videoIsWorking()) { + return emitError(Error::DisabledNoCamera); + } else if (mutedByAdmin()) { + return emitError(Error::MutedNoCamera); + } else if (Webrtc::GetVideoInputList().empty()) { + return emitError(Error::NoCamera); + } + return false; +} + +void GroupCall::emitShareCameraError(Error error) { + _cameraState = Webrtc::VideoState::Inactive; + if (error == Error::CameraFailed + && Webrtc::GetVideoInputList().empty()) { + error = Error::NoCamera; + } + _errors.fire_copy(error); +} + +bool GroupCall::emitShareScreenError() { + const auto emitError = [=](Error error) { + emitShareScreenError(error); + return true; + }; + /*if (const auto real = lookupReal(); real && !real->canStartVideo()) { + return emitError(Error::DisabledNoScreen); + } else */if (!videoIsWorking()) { + return emitError(Error::DisabledNoScreen); + } else if (mutedByAdmin()) { + return emitError(Error::MutedNoScreen); + } + return false; +} + +void GroupCall::emitShareScreenError(Error error) { + _screenState = Webrtc::VideoState::Inactive; + _errors.fire_copy(error); +} + +void GroupCall::setupOutgoingVideo() { + using Webrtc::VideoState; + + _cameraState.value( + ) | rpl::combine_previous( + ) | rpl::filter([=](VideoState previous, VideoState state) { + // Recursive entrance may happen if error happens when activating. + return (previous != state); + }) | rpl::start_with_next([=](VideoState previous, VideoState state) { + const auto wasPaused = (previous == VideoState::Paused); + const auto wasActive = (previous != VideoState::Inactive); + const auto nowPaused = (state == VideoState::Paused); + const auto nowActive = (state != VideoState::Inactive); + if (wasActive == nowActive) { + Assert(wasActive && nowActive); + sendSelfUpdate(SendUpdateType::CameraPaused); + markTrackPaused({ + VideoEndpointType::Camera, + _joinAs, + _cameraEndpoint + }, nowPaused); + return; + } + if (nowActive) { + if (emitShareCameraError()) { + return; + } else if (!_cameraCapture) { + _cameraCapture = _delegate->groupCallGetVideoCapture( + _cameraInputId); + if (!_cameraCapture) { + return emitShareCameraError(Error::CameraFailed); + } + const auto weak = base::make_weak(this); + _cameraCapture->setOnFatalError([=] { + crl::on_main(weak, [=] { + emitShareCameraError(Error::CameraFailed); + }); + }); + } else { + _cameraCapture->switchToDevice(_cameraInputId.toStdString()); + } + if (_instance) { + _instance->setVideoCapture(_cameraCapture); + } + _cameraCapture->setState(tgcalls::VideoState::Active); + } else if (_cameraCapture) { + _cameraCapture->setState(tgcalls::VideoState::Inactive); + } + _isSharingCamera = nowActive; + markEndpointActive({ + VideoEndpointType::Camera, + _joinAs, + _cameraEndpoint + }, nowActive, nowPaused); + sendSelfUpdate(SendUpdateType::CameraStopped); + applyMeInCallLocally(); + }, _lifetime); + + _screenState.value( + ) | rpl::combine_previous( + ) | rpl::filter([=](VideoState previous, VideoState state) { + // Recursive entrance may happen if error happens when activating. + return (previous != state); + }) | rpl::start_with_next([=](VideoState previous, VideoState state) { + const auto wasPaused = (previous == VideoState::Paused); + const auto wasActive = (previous != VideoState::Inactive); + const auto nowPaused = (state == VideoState::Paused); + const auto nowActive = (state != VideoState::Inactive); + if (wasActive == nowActive) { + Assert(wasActive && nowActive); + sendSelfUpdate(SendUpdateType::ScreenPaused); + markTrackPaused({ + VideoEndpointType::Screen, + _joinAs, + _screenEndpoint + }, nowPaused); + return; + } + if (nowActive) { + if (emitShareScreenError()) { + return; + } else if (!_screenCapture) { + _screenCapture = std::shared_ptr< + tgcalls::VideoCaptureInterface + >(tgcalls::VideoCaptureInterface::Create( + tgcalls::StaticThreads::getThreads(), + _screenDeviceId.toStdString())); + if (!_screenCapture) { + return emitShareScreenError(Error::ScreenFailed); + } + const auto weak = base::make_weak(this); + _screenCapture->setOnFatalError([=] { + crl::on_main(weak, [=] { + emitShareScreenError(Error::ScreenFailed); + }); + }); + _screenCapture->setOnPause([=](bool paused) { + crl::on_main(weak, [=] { + if (isSharingScreen()) { + _screenState = paused + ? VideoState::Paused + : VideoState::Active; + } + }); + }); + } else { + _screenCapture->switchToDevice( + _screenDeviceId.toStdString()); + } + if (_screenInstance) { + _screenInstance->setVideoCapture(_screenCapture); + } + _screenCapture->setState(tgcalls::VideoState::Active); + } else if (_screenCapture) { + _screenCapture->setState(tgcalls::VideoState::Inactive); + } + _isSharingScreen = nowActive; + markEndpointActive({ + VideoEndpointType::Screen, + _joinAs, + _screenEndpoint + }, nowActive, nowPaused); + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + }, _lifetime); +} + +void GroupCall::changeTitle(const QString &title) { + const auto real = lookupReal(); + if (!real || real->title() == title) { + return; + } + + _api.request(MTPphone_EditGroupCallTitle( + inputCall(), + MTP_string(title) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + _titleChanged.fire({}); + }).fail([=](const MTP::Error &error) { + }).send(); +} + +void GroupCall::toggleRecording(bool enabled, const QString &title) { + const auto real = lookupReal(); + if (!real) { + return; + } + + const auto already = (real->recordStartDate() != 0); + if (already == enabled) { + return; + } + + if (!enabled) { + _recordingStoppedByMe = true; + } + using Flag = MTPphone_ToggleGroupCallRecord::Flag; + _api.request(MTPphone_ToggleGroupCallRecord( + MTP_flags((enabled ? Flag::f_start : Flag(0)) + | (title.isEmpty() ? Flag(0) : Flag::f_title)), + inputCall(), + MTP_string(title) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + _recordingStoppedByMe = false; + }).fail([=](const MTP::Error &error) { + _recordingStoppedByMe = false; + }).send(); +} + +bool GroupCall::tryCreateController() { + if (_instance) { + return false; + } + const auto &settings = Core::App().settings(); + + const auto weak = base::make_weak(&_instanceGuard); + const auto myLevel = std::make_shared(); + tgcalls::GroupInstanceDescriptor descriptor = { + .threads = tgcalls::StaticThreads::getThreads(), + .config = tgcalls::GroupConfig{ + }, + .networkStateUpdated = [=](tgcalls::GroupNetworkState networkState) { + crl::on_main(weak, [=] { setInstanceConnected(networkState); }); + }, + .audioLevelsUpdated = [=](const tgcalls::GroupLevelsUpdate &data) { + const auto &updates = data.updates; + if (updates.empty()) { + return; + } else if (updates.size() == 1 && !updates.front().ssrc) { + const auto &value = updates.front().value; + // Don't send many 0 while we're muted. + if (myLevel->level == value.level + && myLevel->voice == value.voice) { + return; + } + *myLevel = updates.front().value; + } + crl::on_main(weak, [=] { audioLevelsUpdated(data); }); + }, + .initialInputDeviceId = _audioInputId.toStdString(), + .initialOutputDeviceId = _audioOutputId.toStdString(), + .createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator( + settings.callAudioBackend()), + .videoCapture = _cameraCapture, + .requestBroadcastPart = [=, call = base::make_weak(this)]( + int64_t time, + int64_t period, + std::function done) { + auto result = std::make_shared( + call, + time, + period, + std::move(done)); + crl::on_main(weak, [=]() mutable { + broadcastPartStart(std::move(result)); + }); + return result; + }, + .videoContentType = tgcalls::VideoContentType::Generic, + .initialEnableNoiseSuppression + = settings.groupCallNoiseSuppression(), + .requestMediaChannelDescriptions = [=, call = base::make_weak(this)]( + const std::vector &ssrcs, + std::function &&)> done) { + auto result = std::make_shared( + call, + ssrcs, + std::move(done)); + crl::on_main(weak, [=]() mutable { + mediaChannelDescriptionsStart(std::move(result)); + }); + return result; + }, + }; + if (Logs::DebugEnabled()) { + auto callLogFolder = cWorkingDir() + qsl("DebugLogs"); + auto callLogPath = callLogFolder + qsl("/last_group_call_log.txt"); + auto callLogNative = QDir::toNativeSeparators(callLogPath); +#ifdef Q_OS_WIN + descriptor.config.logPath.data = callLogNative.toStdWString(); +#else // Q_OS_WIN + const auto callLogUtf = QFile::encodeName(callLogNative); + descriptor.config.logPath.data.resize(callLogUtf.size()); + ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); +#endif // Q_OS_WIN + QFile(callLogPath).remove(); + QDir().mkpath(callLogFolder); + } + + LOG(("Call Info: Creating group instance")); + _instance = std::make_unique( + std::move(descriptor)); + + updateInstanceMuteState(); + updateInstanceVolumes(); + for (auto &[endpoint, sink] : base::take(_pendingVideoOutputs)) { + _instance->addIncomingVideoOutput(endpoint, std::move(sink.data)); + } + //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); + return true; +} + +bool GroupCall::tryCreateScreencast() { + if (_screenInstance) { + return false; + } + + const auto weak = base::make_weak(&_screenInstanceGuard); + tgcalls::GroupInstanceDescriptor descriptor = { + .threads = tgcalls::StaticThreads::getThreads(), + .config = tgcalls::GroupConfig{ + }, + .networkStateUpdated = [=](tgcalls::GroupNetworkState networkState) { + crl::on_main(weak, [=] { + setScreenInstanceConnected(networkState); + }); + }, + .videoCapture = _screenCapture, + .videoContentType = tgcalls::VideoContentType::Screencast, + }; + + LOG(("Call Info: Creating group screen instance")); + _screenInstance = std::make_unique( + std::move(descriptor)); + return true; +} + +void GroupCall::broadcastPartStart(std::shared_ptr task) { + const auto raw = task.get(); + const auto time = raw->time(); + const auto scale = raw->scale(); + const auto finish = [=](tgcalls::BroadcastPart &&part) { + raw->done(std::move(part)); + _broadcastParts.erase(raw); + }; + using Status = tgcalls::BroadcastPart::Status; + const auto requestId = _api.request(MTPupload_GetFile( + MTP_flags(0), + MTP_inputGroupCallStream( + inputCall(), + MTP_long(time), + MTP_int(scale)), + MTP_int(0), + MTP_int(128 * 1024) + )).done([=]( + const MTPupload_File &result, + const MTP::Response &response) { + result.match([&](const MTPDupload_file &data) { + const auto size = data.vbytes().v.size(); + auto bytes = std::vector(size); + memcpy(bytes.data(), data.vbytes().v.constData(), size); + finish({ + .timestampMilliseconds = time, + .responseTimestamp = TimestampFromMsgId(response.outerMsgId), + .status = Status::Success, + .oggData = std::move(bytes), + }); + }, [&](const MTPDupload_fileCdnRedirect &data) { + LOG(("Voice Chat Stream Error: fileCdnRedirect received.")); + finish({ + .timestampMilliseconds = time, + .responseTimestamp = TimestampFromMsgId(response.outerMsgId), + .status = Status::ResyncNeeded, + }); + }); + }).fail([=](const MTP::Error &error, const MTP::Response &response) { + if (error.type() == u"GROUPCALL_JOIN_MISSING"_q + || error.type() == u"GROUPCALL_FORBIDDEN"_q) { + for (const auto &[task, part] : _broadcastParts) { + _api.request(part.requestId).cancel(); + } + setState(State::Joining); + rejoin(); + return; + } + const auto status = (MTP::IsFloodError(error) + || error.type() == u"TIME_TOO_BIG"_q) + ? Status::NotReady + : Status::ResyncNeeded; + finish({ + .timestampMilliseconds = time, + .responseTimestamp = TimestampFromMsgId(response.outerMsgId), + .status = status, + }); + }).handleAllErrors().toDC( + MTP::groupCallStreamDcId(_broadcastDcId) + ).send(); + _broadcastParts.emplace(raw, LoadingPart{ std::move(task), requestId }); +} + +void GroupCall::broadcastPartCancel(not_null task) { + const auto i = _broadcastParts.find(task); + if (i != end(_broadcastParts)) { + _api.request(i->second.requestId).cancel(); + _broadcastParts.erase(i); + } +} + +void GroupCall::mediaChannelDescriptionsStart( + std::shared_ptr task) { + const auto raw = task.get(); + + const auto real = lookupReal(); + if (!real || (_instanceMode == InstanceMode::None)) { + for (const auto ssrc : task->ssrcs()) { + _unresolvedSsrcs.emplace(ssrc); + } + _mediaChannelDescriptionses.emplace(std::move(task)); + return; + } + if (!mediaChannelDescriptionsFill(task.get())) { + _mediaChannelDescriptionses.emplace(std::move(task)); + Assert(!_unresolvedSsrcs.empty()); + } + if (!_unresolvedSsrcs.empty()) { + real->resolveParticipants(base::take(_unresolvedSsrcs)); + } +} + +bool GroupCall::mediaChannelDescriptionsFill( + not_null task, + Fn resolved) { + using Channel = tgcalls::MediaChannelDescription; + auto result = false; + const auto real = lookupReal(); + Assert(real != nullptr); + const auto &existing = real->participants(); + for (const auto ssrc : task->ssrcs()) { + const auto add = [&]( + std::optional channel, + bool screen = false) { + if (task->finishWithAdding(ssrc, std::move(channel), screen)) { + result = true; + } + }; + if (const auto byAudio = real->participantPeerByAudioSsrc(ssrc)) { + add(Channel{ + .type = Channel::Type::Audio, + .audioSsrc = ssrc, + }); + } else if (!resolved) { + _unresolvedSsrcs.emplace(ssrc); + } else if (resolved(ssrc)) { + add(std::nullopt); + } + } + return result; +} + +void GroupCall::mediaChannelDescriptionsCancel( + not_null task) { + const auto i = _mediaChannelDescriptionses.find(task.get()); + if (i != end(_mediaChannelDescriptionses)) { + _mediaChannelDescriptionses.erase(i); + } +} + +void GroupCall::updateRequestedVideoChannels() { + _requestedVideoChannelsUpdateScheduled = false; + const auto real = lookupReal(); + if (!real || !_instance) { + return; + } + auto channels = std::vector(); + using Quality = tgcalls::VideoChannelDescription::Quality; + channels.reserve(_activeVideoTracks.size()); + const auto &camera = cameraSharingEndpoint(); + const auto &screen = screenSharingEndpoint(); + auto mediums = 0; + auto fullcameras = 0; + auto fullscreencasts = 0; + for (const auto &[endpoint, video] : _activeVideoTracks) { + const auto &endpointId = endpoint.id; + if (endpointId == camera || endpointId == screen) { + continue; + } + const auto participant = real->participantByEndpoint(endpointId); + const auto params = (participant && participant->ssrc) + ? participant->videoParams.get() + : nullptr; + if (!params) { + continue; + } + const auto min = (video->quality == Group::VideoQuality::Full + && endpoint.type == VideoEndpointType::Screen) + ? Quality::Full + : Quality::Thumbnail; + const auto max = (video->quality == Group::VideoQuality::Full) + ? Quality::Full + : (video->quality == Group::VideoQuality::Medium + && endpoint.type != VideoEndpointType::Screen) + ? Quality::Medium + : Quality::Thumbnail; + if (max == Quality::Full) { + if (endpoint.type == VideoEndpointType::Screen) { + ++fullscreencasts; + } else { + ++fullcameras; + } + } else if (max == Quality::Medium) { + ++mediums; + } + channels.push_back({ + .audioSsrc = participant->ssrc, + .endpointId = endpointId, + .ssrcGroups = (params->camera.endpointId == endpointId + ? params->camera.ssrcGroups + : params->screen.ssrcGroups), + .minQuality = min, + .maxQuality = max, + }); + } + + // We limit `count(Full) * kFullAsMediumsCount + count(medium)`. + // + // Try to preserve all qualities; If not + // Try to preserve all screencasts as Full and cameras as Medium; If not + // Try to preserve all screencasts as Full; If not + // Try to preserve all cameras as Medium; + const auto mediumsCount = mediums + + (fullcameras + fullscreencasts) * kFullAsMediumsCount; + const auto downgradeSome = (mediumsCount > kMaxMediumQualities); + const auto downgradeAll = (fullscreencasts * kFullAsMediumsCount) + > kMaxMediumQualities; + if (downgradeSome) { + for (auto &channel : channels) { + if (channel.maxQuality == Quality::Full) { + const auto camera = (channel.minQuality != Quality::Full); + if (camera) { + channel.maxQuality = Quality::Medium; + } else if (downgradeAll) { + channel.maxQuality + = channel.minQuality + = Quality::Thumbnail; + --fullscreencasts; + } + } + } + mediums += fullcameras; + fullcameras = 0; + if (downgradeAll) { + fullscreencasts = 0; + } + } + if (mediums > kMaxMediumQualities) { + for (auto &channel : channels) { + if (channel.maxQuality == Quality::Medium) { + channel.maxQuality = Quality::Thumbnail; + } + } + } + _instance->setRequestedVideoChannels(std::move(channels)); +} + +void GroupCall::updateRequestedVideoChannelsDelayed() { + if (_requestedVideoChannelsUpdateScheduled) { + return; + } + _requestedVideoChannelsUpdateScheduled = true; + crl::on_main(this, [=] { + if (_requestedVideoChannelsUpdateScheduled) { + updateRequestedVideoChannels(); + } + }); +} + +void GroupCall::refreshHasNotShownVideo() { + if (!_joinState.ssrc || hasNotShownVideo()) { + return; + } + const auto real = lookupReal(); + Assert(real != nullptr); + + const auto hasVideo = [&](const Data::GroupCallParticipant &data) { + return (data.peer != _joinAs) + && (!GetCameraEndpoint(data.videoParams).empty() + || !GetScreenEndpoint(data.videoParams).empty()); + }; + _hasNotShownVideo = _joinState.ssrc + && ranges::any_of(real->participants(), hasVideo); +} + +void GroupCall::fillActiveVideoEndpoints() { + const auto real = lookupReal(); + Assert(real != nullptr); + + const auto me = real->participantByPeer(_joinAs); + if (me && me->videoJoined) { + _videoIsWorking = true; + _hasNotShownVideo = false; + } else { + refreshHasNotShownVideo(); + _videoIsWorking = false; + toggleVideo(false); + toggleScreenSharing(std::nullopt); + } + + const auto &large = _videoEndpointLarge.current(); + auto largeFound = false; + auto endpoints = _activeVideoTracks | ranges::views::transform([]( + const auto &pair) { + return pair.first; + }); + auto removed = base::flat_set( + begin(endpoints), + end(endpoints)); + const auto feedOne = [&](VideoEndpoint endpoint, bool paused) { + if (endpoint.empty()) { + return; + } else if (endpoint == large) { + largeFound = true; + } + if (removed.remove(endpoint)) { + markTrackPaused(endpoint, paused); + } else { + markEndpointActive(std::move(endpoint), true, paused); + } + }; + using Type = VideoEndpointType; + if (_videoIsWorking.current()) { + for (const auto &participant : real->participants()) { + const auto camera = GetCameraEndpoint(participant.videoParams); + if (camera != _cameraEndpoint + && camera != _screenEndpoint + && participant.peer != _joinAs) { + const auto paused = IsCameraPaused(participant.videoParams); + feedOne({ Type::Camera, participant.peer, camera }, paused); + } + const auto screen = GetScreenEndpoint(participant.videoParams); + if (screen != _cameraEndpoint + && screen != _screenEndpoint + && participant.peer != _joinAs) { + const auto paused = IsScreenPaused(participant.videoParams); + feedOne({ Type::Screen, participant.peer, screen }, paused); + } + } + feedOne( + { Type::Camera, _joinAs, cameraSharingEndpoint() }, + isCameraPaused()); + feedOne( + { Type::Screen, _joinAs, screenSharingEndpoint() }, + isScreenPaused()); + } + if (large && !largeFound) { + setVideoEndpointLarge({}); + } + for (const auto &endpoint : removed) { + markEndpointActive(endpoint, false, false); + } + updateRequestedVideoChannels(); +} + +void GroupCall::updateInstanceMuteState() { + Expects(_instance != nullptr); + + const auto state = muted(); + _instance->setIsMuted(state != MuteState::Active + && state != MuteState::PushToTalk); +} + +void GroupCall::updateInstanceVolumes() { + const auto real = lookupReal(); + if (!real) { + return; + } + + const auto &participants = real->participants(); + for (const auto &participant : participants) { + const auto setVolume = participant.mutedByMe + || (participant.volume != Group::kDefaultVolume); + if (setVolume && participant.ssrc) { + _instance->setVolume( + participant.ssrc, + (participant.mutedByMe + ? 0. + : (participant.volume / float64(Group::kDefaultVolume)))); + } + } +} + +void GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) { + Expects(!data.updates.empty()); + + auto check = false; + auto checkNow = false; + const auto now = crl::now(); + const auto meMuted = [&] { + const auto state = muted(); + return (state != MuteState::Active) + && (state != MuteState::PushToTalk); + }; + for (const auto &[ssrcOrZero, value] : data.updates) { + const auto ssrc = ssrcOrZero ? ssrcOrZero : _joinState.ssrc; + if (!ssrc) { + continue; + } + const auto level = value.level; + const auto voice = value.voice; + const auto me = (ssrc == _joinState.ssrc); + const auto ignore = me && meMuted(); + _levelUpdates.fire(LevelUpdate{ + .ssrc = ssrc, + .value = ignore ? 0.f : level, + .voice = (!ignore && voice), + .me = me, + }); + if (level <= kSpeakLevelThreshold) { + continue; + } + if (me + && voice + && (!_lastSendProgressUpdate + || _lastSendProgressUpdate + kUpdateSendActionEach < now)) { + _lastSendProgressUpdate = now; + _peer->session().sendProgressManager().update( + _history, + Api::SendProgressType::Speaking); + } + + check = true; + const auto i = _lastSpoke.find(ssrc); + if (i == _lastSpoke.end()) { + _lastSpoke.emplace(ssrc, Data::LastSpokeTimes{ + .anything = now, + .voice = voice ? now : 0, + }); + checkNow = true; + } else { + if ((i->second.anything + kCheckLastSpokeInterval / 3 <= now) + || (voice + && i->second.voice + kCheckLastSpokeInterval / 3 <= now)) { + checkNow = true; + } + i->second.anything = now; + if (voice) { + i->second.voice = now; + } + } + } + if (checkNow) { + checkLastSpoke(); + } else if (check && !_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2); + } +} + +void GroupCall::checkLastSpoke() { + const auto real = lookupReal(); + if (!real) { + return; + } + + constexpr auto kKeepInListFor = kCheckLastSpokeInterval * 2; + static_assert(Data::GroupCall::kSoundStatusKeptFor + <= kKeepInListFor - (kCheckLastSpokeInterval / 3)); + + auto hasRecent = false; + const auto now = crl::now(); + auto list = base::take(_lastSpoke); + for (auto i = list.begin(); i != list.end();) { + const auto [ssrc, when] = *i; + if (when.anything + kKeepInListFor >= now) { + hasRecent = true; + ++i; + } else { + i = list.erase(i); + } + + // Ignore my levels from microphone if I'm already muted. + if (ssrc != _joinState.ssrc + || muted() == MuteState::Active + || muted() == MuteState::PushToTalk) { + real->applyLastSpoke(ssrc, when, now); + } + } + _lastSpoke = std::move(list); + + if (!hasRecent) { + _lastSpokeCheckTimer.cancel(); + } else if (!_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3); + } +} + +void GroupCall::checkJoined() { + if (state() != State::Connecting || !_id || !_joinState.ssrc) { + return; + } + auto sources = QVector(1, MTP_int(_joinState.ssrc)); + if (_screenJoinState.ssrc) { + sources.push_back(MTP_int(_screenJoinState.ssrc)); + } + _api.request(MTPphone_CheckGroupCall( + inputCall(), + MTP_vector(std::move(sources)) + )).done([=](const MTPVector &result) { + if (!ranges::contains(result.v, MTP_int(_joinState.ssrc))) { + LOG(("Call Info: Rejoin after no _mySsrc in checkGroupCall.")); + _joinState.nextActionPending = true; + checkNextJoinAction(); + } else { + if (state() == State::Connecting) { + _checkJoinedTimer.callOnce(kCheckJoinedTimeout); + } + if (_screenJoinState.ssrc + && !ranges::contains( + result.v, + MTP_int(_screenJoinState.ssrc))) { + LOG(("Call Info: " + "Screen rejoin after _screenSsrc not found.")); + _screenJoinState.nextActionPending = true; + checkNextJoinAction(); + } + } + }).fail([=](const MTP::Error &error) { + LOG(("Call Info: Full rejoin after error '%1' in checkGroupCall." + ).arg(error.type())); + rejoin(); + }).send(); +} + +void GroupCall::setInstanceConnected( + tgcalls::GroupNetworkState networkState) { + const auto inTransit = networkState.isTransitioningFromBroadcastToRtc; + const auto instanceState = !networkState.isConnected + ? InstanceState::Disconnected + : inTransit + ? InstanceState::TransitionToRtc + : InstanceState::Connected; + const auto connected = (instanceState != InstanceState::Disconnected); + if (_instanceState.current() == instanceState + && _instanceTransitioning == inTransit) { + return; + } + const auto nowCanSpeak = connected + && _instanceTransitioning + && !inTransit + && (muted() == MuteState::Muted); + _instanceTransitioning = inTransit; + _instanceState = instanceState; + if (state() == State::Connecting && connected) { + setState(State::Joined); + } else if (state() == State::Joined && !connected) { + setState(State::Connecting); + } + if (nowCanSpeak) { + notifyAboutAllowedToSpeak(); + } + if (!_hadJoinedState && state() == State::Joined) { + checkFirstTimeJoined(); + } +} + +void GroupCall::setScreenInstanceConnected( + tgcalls::GroupNetworkState networkState) { + const auto inTransit = networkState.isTransitioningFromBroadcastToRtc; + const auto screenInstanceState = !networkState.isConnected + ? InstanceState::Disconnected + : inTransit + ? InstanceState::TransitionToRtc + : InstanceState::Connected; + const auto connected = (screenInstanceState + != InstanceState::Disconnected); + if (_screenInstanceState.current() == screenInstanceState) { + return; + } + _screenInstanceState = screenInstanceState; +} + +void GroupCall::checkFirstTimeJoined() { + if (_hadJoinedState || state() != State::Joined) { + return; + } + _hadJoinedState = true; + applyGlobalShortcutChanges(); + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Started); +} + +void GroupCall::notifyAboutAllowedToSpeak() { + if (!_hadJoinedState) { + return; + } + _delegate->groupCallPlaySound( + Delegate::GroupCallSound::AllowedToSpeak); + _allowedToSpeakNotifications.fire({}); +} + +void GroupCall::setInstanceMode(InstanceMode mode) { + Expects(_instance != nullptr); + + _instanceMode = mode; + + using Mode = tgcalls::GroupConnectionMode; + _instance->setConnectionMode([&] { + switch (_instanceMode) { + case InstanceMode::None: return Mode::GroupConnectionModeNone; + case InstanceMode::Rtc: return Mode::GroupConnectionModeRtc; + case InstanceMode::Stream: return Mode::GroupConnectionModeBroadcast; + } + Unexpected("Mode in GroupCall::setInstanceMode."); + }(), true); +} + +void GroupCall::setScreenInstanceMode(InstanceMode mode) { + Expects(_screenInstance != nullptr); + + _screenInstanceMode = mode; + + using Mode = tgcalls::GroupConnectionMode; + _screenInstance->setConnectionMode([&] { + switch (_screenInstanceMode) { + case InstanceMode::None: return Mode::GroupConnectionModeNone; + case InstanceMode::Rtc: return Mode::GroupConnectionModeRtc; + case InstanceMode::Stream: return Mode::GroupConnectionModeBroadcast; + } + Unexpected("Mode in GroupCall::setInstanceMode."); + }(), true); +} + +void GroupCall::maybeSendMutedUpdate(MuteState previous) { + // Send Active <-> !Active or ForceMuted <-> RaisedHand changes. + const auto now = muted(); + if ((previous == MuteState::Active && now == MuteState::Muted) + || (now == MuteState::Active + && (previous == MuteState::Muted + || previous == MuteState::PushToTalk))) { + sendSelfUpdate(SendUpdateType::Mute); + } else if ((now == MuteState::ForceMuted + && previous == MuteState::RaisedHand) + || (now == MuteState::RaisedHand + && previous == MuteState::ForceMuted)) { + sendSelfUpdate(SendUpdateType::RaiseHand); + } +} + +void GroupCall::sendPendingSelfUpdates() { + if ((state() != State::Connecting && state() != State::Joined) + || _selfUpdateRequestId) { + return; + } + const auto updates = { + SendUpdateType::Mute, + SendUpdateType::RaiseHand, + SendUpdateType::CameraStopped, + SendUpdateType::CameraPaused, + SendUpdateType::ScreenPaused, + }; + for (const auto type : updates) { + if (type == SendUpdateType::ScreenPaused + && _screenJoinState.action != JoinAction::None) { + continue; + } + if (_pendingSelfUpdates & type) { + _pendingSelfUpdates &= ~type; + sendSelfUpdate(type); + return; + } + } +} + +void GroupCall::sendSelfUpdate(SendUpdateType type) { + if ((state() != State::Connecting && state() != State::Joined) + || _selfUpdateRequestId) { + _pendingSelfUpdates |= type; + return; + } + using Flag = MTPphone_EditGroupCallParticipant::Flag; + _selfUpdateRequestId = _api.request(MTPphone_EditGroupCallParticipant( + MTP_flags((type == SendUpdateType::RaiseHand) + ? Flag::f_raise_hand + : (type == SendUpdateType::CameraStopped) + ? Flag::f_video_stopped + : (type == SendUpdateType::CameraPaused) + ? Flag::f_video_paused + : (type == SendUpdateType::ScreenPaused) + ? Flag::f_presentation_paused + : Flag::f_muted), + inputCall(), + _joinAs->input, + MTP_bool(muted() != MuteState::Active), + MTP_int(100000), // volume + MTP_bool(muted() == MuteState::RaisedHand), + MTP_bool(!isSharingCamera()), + MTP_bool(isCameraPaused()), + MTP_bool(isScreenPaused()) + )).done([=](const MTPUpdates &result) { + _selfUpdateRequestId = 0; + _peer->session().api().applyUpdates(result); + sendPendingSelfUpdates(); + }).fail([=](const MTP::Error &error) { + _selfUpdateRequestId = 0; + if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." + ).arg(error.type())); + rejoin(); + } + }).send(); +} + +void GroupCall::pinVideoEndpoint(VideoEndpoint endpoint) { + _videoEndpointPinned = false; + if (endpoint) { + setVideoEndpointLarge(std::move(endpoint)); + _videoEndpointPinned = true; + } +} + +void GroupCall::showVideoEndpointLarge(VideoEndpoint endpoint) { + if (_videoEndpointLarge.current() == endpoint) { + return; + } + _videoEndpointPinned = false; + setVideoEndpointLarge(std::move(endpoint)); + _videoLargeTillTime = crl::now() + kFixManualLargeVideoDuration; +} + +void GroupCall::setVideoEndpointLarge(VideoEndpoint endpoint) { + if (!endpoint) { + _videoEndpointPinned = false; + } + _videoEndpointLarge = endpoint; +} + +void GroupCall::requestVideoQuality( + const VideoEndpoint &endpoint, + Group::VideoQuality quality) { + if (!endpoint) { + return; + } + const auto i = _activeVideoTracks.find(endpoint); + if (i == end(_activeVideoTracks) || i->second->quality == quality) { + return; + } + i->second->quality = quality; + updateRequestedVideoChannelsDelayed(); +} + +void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { + if (input) { + _mediaDevices->switchToAudioInput(deviceId); + } else { + _mediaDevices->switchToAudioOutput(deviceId); + } +} + +void GroupCall::setCurrentVideoDevice(const QString &deviceId) { + _mediaDevices->switchToVideoInput(deviceId); +} + +void GroupCall::toggleMute(const Group::MuteRequest &data) { + if (data.locallyOnly) { + applyParticipantLocally(data.peer, data.mute, std::nullopt); + } else { + editParticipant(data.peer, data.mute, std::nullopt); + } +} + +void GroupCall::changeVolume(const Group::VolumeRequest &data) { + if (data.locallyOnly) { + applyParticipantLocally(data.peer, false, data.volume); + } else { + editParticipant(data.peer, false, data.volume); + } +} + +void GroupCall::editParticipant( + not_null participantPeer, + bool mute, + std::optional volume) { + const auto participant = LookupParticipant(_peer, _id, participantPeer); + if (!participant) { + return; + } + applyParticipantLocally(participantPeer, mute, volume); + + using Flag = MTPphone_EditGroupCallParticipant::Flag; + const auto flags = Flag::f_muted + | (volume.has_value() ? Flag::f_volume : Flag(0)); + _api.request(MTPphone_EditGroupCallParticipant( + MTP_flags(flags), + inputCall(), + participantPeer->input, + MTP_bool(mute), + MTP_int(std::clamp(volume.value_or(0), 1, Group::kMaxVolume)), + MTPBool(), // raise_hand + MTPBool(), // video_muted + MTPBool(), // video_paused + MTPBool() // presentation_paused + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." + ).arg(error.type())); + rejoin(); + } + }).send(); +} + +std::variant> GroupCall::inviteUsers( + const std::vector> &users) { + const auto real = lookupReal(); + if (!real) { + return 0; + } + const auto owner = &_peer->owner(); + const auto &invited = owner->invitedToCallUsers(_id); + auto &&toInvite = users | ranges::views::filter([&]( + not_null user) { + return !invited.contains(user) && !real->participantByPeer(user); + }); + + auto count = 0; + auto slice = QVector(); + auto result = std::variant>(0); + slice.reserve(kMaxInvitePerSlice); + const auto sendSlice = [&] { + count += slice.size(); + _api.request(MTPphone_InviteToGroupCall( + inputCall(), + MTP_vector(slice) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).send(); + slice.clear(); + }; + for (const auto user : users) { + if (!count && slice.empty()) { + result = user; + } + owner->registerInvitedToCallUser(_id, _peer, user); + slice.push_back(user->inputUser); + if (slice.size() == kMaxInvitePerSlice) { + sendSlice(); + } + } + if (count != 0 || slice.size() != 1) { + result = int(count + slice.size()); + } + if (!slice.empty()) { + sendSlice(); + } + return result; +} + +auto GroupCall::ensureGlobalShortcutManager() +-> std::shared_ptr { + if (!_shortcutManager) { + _shortcutManager = base::CreateGlobalShortcutManager(); + } + return _shortcutManager; +} + +void GroupCall::applyGlobalShortcutChanges() { + auto &settings = Core::App().settings(); + if (!settings.groupCallPushToTalk() + || settings.groupCallPushToTalkShortcut().isEmpty() + || !base::GlobalShortcutsAvailable() + || !base::GlobalShortcutsAllowed()) { + _shortcutManager = nullptr; + _pushToTalk = nullptr; + return; + } + ensureGlobalShortcutManager(); + const auto shortcut = _shortcutManager->shortcutFromSerialized( + settings.groupCallPushToTalkShortcut()); + if (!shortcut) { + settings.setGroupCallPushToTalkShortcut(QByteArray()); + settings.setGroupCallPushToTalk(false); + Core::App().saveSettingsDelayed(); + _shortcutManager = nullptr; + _pushToTalk = nullptr; + return; + } + if (_pushToTalk) { + if (shortcut->serialize() == _pushToTalk->serialize()) { + return; + } + _shortcutManager->stopWatching(_pushToTalk); + } + _pushToTalk = shortcut; + _shortcutManager->startWatching(_pushToTalk, [=](bool pressed) { + pushToTalk( + pressed, + Core::App().settings().groupCallPushToTalkDelay()); + }); +} + +void GroupCall::pushToTalk(bool pressed, crl::time delay) { + if (mutedByAdmin() || muted() == MuteState::Active) { + return; + } else if (pressed) { + _pushToTalkCancelTimer.cancel(); + setMuted(MuteState::PushToTalk); + } else if (delay) { + _pushToTalkCancelTimer.callOnce(delay); + } else { + pushToTalkCancel(); + } +} + +void GroupCall::pushToTalkCancel() { + _pushToTalkCancelTimer.cancel(); + if (muted() == MuteState::PushToTalk) { + setMuted(MuteState::Muted); + } +} + +void GroupCall::setNotRequireARGB32() { + _requireARGB32 = false; +} + +auto GroupCall::otherParticipantStateValue() const +-> rpl::producer { + return _otherParticipantStateValue.events(); +} + +MTPInputGroupCall GroupCall::inputCall() const { + Expects(_id != 0); + + return MTP_inputGroupCall( + MTP_long(_id), + MTP_long(_accessHash)); +} + +void GroupCall::destroyController() { + if (_instance) { + DEBUG_LOG(("Call Info: Destroying call controller..")); + invalidate_weak_ptrs(&_instanceGuard); + + crl::async([ + instance = base::take(_instance), + done = _delegate->groupCallAddAsyncWaiter() + ]() mutable { + instance = nullptr; + DEBUG_LOG(("Call Info: Call controller destroyed.")); + done(); + }); + } +} + +void GroupCall::destroyScreencast() { + if (_screenInstance) { + DEBUG_LOG(("Call Info: Destroying call screen controller..")); + invalidate_weak_ptrs(&_screenInstanceGuard); + crl::async([ + instance = base::take(_screenInstance), + done = _delegate->groupCallAddAsyncWaiter() + ]() mutable { + instance = nullptr; + DEBUG_LOG(("Call Info: Call screen controller destroyed.")); + done(); + }); + } +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h new file mode 100644 index 000000000..6a4611050 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -0,0 +1,651 @@ +/* +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 "base/timer.h" +#include "base/bytes.h" +#include "mtproto/sender.h" +#include "mtproto/mtproto_auth_key.h" + +class History; + +namespace tgcalls { +class GroupInstanceCustomImpl; +struct GroupLevelsUpdate; +struct GroupNetworkState; +struct GroupParticipantDescription; +class VideoCaptureInterface; +} // namespace tgcalls + +namespace base { +class GlobalShortcutManager; +class GlobalShortcutValue; +} // namespace base + +namespace Webrtc { +class MediaDevices; +class VideoTrack; +enum class VideoState; +} // namespace Webrtc + +namespace Data { +struct LastSpokeTimes; +struct GroupCallParticipant; +class GroupCall; +} // namespace Data + +namespace Calls { + +namespace Group { +struct MuteRequest; +struct VolumeRequest; +struct ParticipantState; +struct JoinInfo; +struct RejoinEvent; +enum class VideoQuality; +enum class Error; +} // namespace Group + +enum class MuteState { + Active, + PushToTalk, + Muted, + ForceMuted, + RaisedHand, +}; + +[[nodiscard]] inline auto MapPushToTalkToActive() { + return rpl::map([=](MuteState state) { + return (state == MuteState::PushToTalk) ? MuteState::Active : state; + }); +} + +[[nodiscard]] bool IsGroupCallAdmin( + not_null peer, + not_null participantPeer); + +struct LevelUpdate { + uint32 ssrc = 0; + float value = 0.; + bool voice = false; + bool me = false; +}; + +enum class VideoEndpointType { + Camera, + Screen, +}; + +struct VideoEndpoint { + VideoEndpoint() = default; + VideoEndpoint( + VideoEndpointType type, + not_null peer, + std::string id) + : type(type) + , peer(peer) + , id(std::move(id)) { + } + + VideoEndpointType type = VideoEndpointType::Camera; + PeerData *peer = nullptr; + std::string id; + + [[nodiscard]] bool empty() const noexcept { + Expects(id.empty() || peer != nullptr); + + return id.empty(); + } + [[nodiscard]] explicit operator bool() const noexcept { + return !empty(); + } +}; + +inline bool operator==( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return (a.id == b.id); +} + +inline bool operator!=( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return !(a == b); +} + +inline bool operator<( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return (a.peer < b.peer) + || (a.peer == b.peer && a.id < b.id); +} + +inline bool operator>( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return (b < a); +} + +inline bool operator<=( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return !(b < a); +} + +inline bool operator>=( + const VideoEndpoint &a, + const VideoEndpoint &b) noexcept { + return !(a < b); +} + +struct VideoStateToggle { + VideoEndpoint endpoint; + bool value = false; +}; + +struct VideoQualityRequest { + VideoEndpoint endpoint; + Group::VideoQuality quality = Group::VideoQuality(); +}; + +struct ParticipantVideoParams; + +[[nodiscard]] std::shared_ptr ParseVideoParams( + const tl::conditional &camera, + const tl::conditional &screen, + const std::shared_ptr &existing); + +[[nodiscard]] const std::string &GetCameraEndpoint( + const std::shared_ptr ¶ms); +[[nodiscard]] const std::string &GetScreenEndpoint( + const std::shared_ptr ¶ms); +[[nodiscard]] bool IsCameraPaused( + const std::shared_ptr ¶ms); +[[nodiscard]] bool IsScreenPaused( + const std::shared_ptr ¶ms); + +class GroupCall final : public base::has_weak_ptr { +public: + class Delegate { + public: + virtual ~Delegate() = default; + + virtual void groupCallFinished(not_null call) = 0; + virtual void groupCallFailed(not_null call) = 0; + virtual void groupCallRequestPermissionsOrFail( + Fn onSuccess) = 0; + + enum class GroupCallSound { + Started, + Connecting, + AllowedToSpeak, + Ended, + }; + virtual void groupCallPlaySound(GroupCallSound sound) = 0; + virtual auto groupCallGetVideoCapture(const QString &deviceId) + -> std::shared_ptr = 0; + + [[nodiscard]] virtual FnMut groupCallAddAsyncWaiter() = 0; + }; + + using GlobalShortcutManager = base::GlobalShortcutManager; + + struct VideoTrack; + + [[nodiscard]] static not_null TrackPeer( + const std::unique_ptr &track); + [[nodiscard]] static not_null TrackPointer( + const std::unique_ptr &track); + [[nodiscard]] static rpl::producer TrackSizeValue( + const std::unique_ptr &track); + + GroupCall( + not_null delegate, + Group::JoinInfo info, + const MTPInputGroupCall &inputCall); + ~GroupCall(); + + [[nodiscard]] uint64 id() const { + return _id; + } + [[nodiscard]] not_null peer() const { + return _peer; + } + [[nodiscard]] not_null joinAs() const { + return _joinAs; + } + [[nodiscard]] bool showChooseJoinAs() const; + [[nodiscard]] TimeId scheduleDate() const { + return _scheduleDate; + } + [[nodiscard]] bool scheduleStartSubscribed() const; + + [[nodiscard]] Data::GroupCall *lookupReal() const; + [[nodiscard]] rpl::producer> real() const; + + void start(TimeId scheduleDate); + void hangup(); + void discard(); + void rejoinAs(Group::JoinInfo info); + void rejoinWithHash(const QString &hash); + void join(const MTPInputGroupCall &inputCall); + void handleUpdate(const MTPUpdate &update); + void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data); + void handlePossibleCreateOrJoinResponse( + const MTPDupdateGroupCallConnection &data); + void changeTitle(const QString &title); + void toggleRecording(bool enabled, const QString &title); + [[nodiscard]] bool recordingStoppedByMe() const { + return _recordingStoppedByMe; + } + void startScheduledNow(); + void toggleScheduleStartSubscribed(bool subscribed); + void setNoiseSuppression(bool enabled); + + bool emitShareScreenError(); + bool emitShareCameraError(); + + [[nodiscard]] rpl::producer errors() const { + return _errors.events(); + } + + void addVideoOutput( + const std::string &endpoint, + not_null track); + + void setMuted(MuteState mute); + void setMutedAndUpdate(MuteState mute); + [[nodiscard]] MuteState muted() const { + return _muted.current(); + } + [[nodiscard]] rpl::producer mutedValue() const { + return _muted.value(); + } + + [[nodiscard]] auto otherParticipantStateValue() const + -> rpl::producer; + + enum State { + Creating, + Waiting, + Joining, + Connecting, + Joined, + FailedHangingUp, + Failed, + HangingUp, + Ended, + }; + [[nodiscard]] State state() const { + return _state.current(); + } + [[nodiscard]] rpl::producer stateValue() const { + return _state.value(); + } + + enum class InstanceState { + Disconnected, + TransitionToRtc, + Connected, + }; + [[nodiscard]] InstanceState instanceState() const { + return _instanceState.current(); + } + [[nodiscard]] rpl::producer instanceStateValue() const { + return _instanceState.value(); + } + + [[nodiscard]] rpl::producer levelUpdates() const { + return _levelUpdates.events(); + } + [[nodiscard]] auto videoStreamActiveUpdates() const + -> rpl::producer { + return _videoStreamActiveUpdates.events(); + } + [[nodiscard]] auto videoStreamShownUpdates() const + -> rpl::producer { + return _videoStreamShownUpdates.events(); + } + void requestVideoQuality( + const VideoEndpoint &endpoint, + Group::VideoQuality quality); + + [[nodiscard]] bool videoEndpointPinned() const { + return _videoEndpointPinned.current(); + } + [[nodiscard]] rpl::producer videoEndpointPinnedValue() const { + return _videoEndpointPinned.value(); + } + void pinVideoEndpoint(VideoEndpoint endpoint); + + void showVideoEndpointLarge(VideoEndpoint endpoint); + [[nodiscard]] const VideoEndpoint &videoEndpointLarge() const { + return _videoEndpointLarge.current(); + } + [[nodiscard]] auto videoEndpointLargeValue() const + -> rpl::producer { + return _videoEndpointLarge.value(); + } + [[nodiscard]] auto activeVideoTracks() const + -> const base::flat_map> & { + return _activeVideoTracks; + } + [[nodiscard]] auto shownVideoTracks() const + -> const base::flat_set & { + return _shownVideoTracks; + } + [[nodiscard]] rpl::producer rejoinEvents() const { + return _rejoinEvents.events(); + } + [[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const { + return _allowedToSpeakNotifications.events(); + } + [[nodiscard]] rpl::producer<> titleChanged() const { + return _titleChanged.events(); + } + static constexpr auto kSpeakLevelThreshold = 0.2; + + [[nodiscard]] bool mutedByAdmin() const; + [[nodiscard]] bool canManage() const; + [[nodiscard]] rpl::producer canManageValue() const; + [[nodiscard]] bool videoIsWorking() const { + return _videoIsWorking.current(); + } + [[nodiscard]] rpl::producer videoIsWorkingValue() const { + return _videoIsWorking.value(); + } + [[nodiscard]] bool hasNotShownVideo() const { + return _hasNotShownVideo.current(); + } + [[nodiscard]] rpl::producer hasNotShownVideoValue() const { + return _hasNotShownVideo.value(); + } + + void setCurrentAudioDevice(bool input, const QString &deviceId); + void setCurrentVideoDevice(const QString &deviceId); + [[nodiscard]] bool isSharingScreen() const; + [[nodiscard]] rpl::producer isSharingScreenValue() const; + [[nodiscard]] bool isScreenPaused() const; + [[nodiscard]] const std::string &screenSharingEndpoint() const; + [[nodiscard]] bool isSharingCamera() const; + [[nodiscard]] rpl::producer isSharingCameraValue() const; + [[nodiscard]] bool isCameraPaused() const; + [[nodiscard]] const std::string &cameraSharingEndpoint() const; + [[nodiscard]] QString screenSharingDeviceId() const; + void toggleVideo(bool active); + void toggleScreenSharing(std::optional uniqueId); + [[nodiscard]] bool hasVideoWithFrames() const; + [[nodiscard]] rpl::producer hasVideoWithFramesValue() const; + + void toggleMute(const Group::MuteRequest &data); + void changeVolume(const Group::VolumeRequest &data); + std::variant> inviteUsers( + const std::vector> &users); + + std::shared_ptr ensureGlobalShortcutManager(); + void applyGlobalShortcutChanges(); + + void pushToTalk(bool pressed, crl::time delay); + void setNotRequireARGB32(); + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + class LoadPartTask; + class MediaChannelDescriptionsTask; + +public: + void broadcastPartStart(std::shared_ptr task); + void broadcastPartCancel(not_null task); + void mediaChannelDescriptionsStart( + std::shared_ptr task); + void mediaChannelDescriptionsCancel( + not_null task); + +private: + using GlobalShortcutValue = base::GlobalShortcutValue; + using Error = Group::Error; + struct SinkPointer; + + static constexpr uint32 kDisabledSsrc = uint32(-1); + + struct LoadingPart { + std::shared_ptr task; + mtpRequestId requestId = 0; + }; + + enum class FinishType { + None, + Ended, + Failed, + }; + enum class InstanceMode { + None, + Rtc, + Stream, + }; + enum class SendUpdateType { + Mute = 0x01, + RaiseHand = 0x02, + CameraStopped = 0x04, + CameraPaused = 0x08, + ScreenPaused = 0x10, + }; + enum class JoinAction { + None, + Joining, + Leaving, + }; + struct JoinState { + uint32 ssrc = 0; + JoinAction action = JoinAction::None; + bool nextActionPending = false; + + void finish(uint32 updatedSsrc = 0) { + action = JoinAction::None; + ssrc = updatedSsrc; + } + }; + + friend inline constexpr bool is_flag_type(SendUpdateType) { + return true; + } + + [[nodiscard]] bool mediaChannelDescriptionsFill( + not_null task, + Fn resolved = nullptr); + void checkMediaChannelDescriptions(Fn resolved = nullptr); + + void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data); + void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data); + void handleUpdate(const MTPDupdateGroupCall &data); + void handleUpdate(const MTPDupdateGroupCallParticipants &data); + bool tryCreateController(); + void destroyController(); + bool tryCreateScreencast(); + void destroyScreencast(); + + void emitShareCameraError(Error error); + void emitShareScreenError(Error error); + + void setState(State state); + void finish(FinishType type); + void maybeSendMutedUpdate(MuteState previous); + void sendSelfUpdate(SendUpdateType type); + void updateInstanceMuteState(); + void updateInstanceVolumes(); + void applyMeInCallLocally(); + void rejoin(); + void leave(); + void rejoin(not_null as); + void setJoinAs(not_null as); + void saveDefaultJoinAs(not_null as); + void subscribeToReal(not_null real); + void setScheduledDate(TimeId date); + void rejoinPresentation(); + void leavePresentation(); + void checkNextJoinAction(); + + void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data); + void setInstanceConnected(tgcalls::GroupNetworkState networkState); + void setInstanceMode(InstanceMode mode); + void setScreenInstanceConnected(tgcalls::GroupNetworkState networkState); + void setScreenInstanceMode(InstanceMode mode); + void checkLastSpoke(); + void pushToTalkCancel(); + + void checkGlobalShortcutAvailability(); + void checkJoined(); + void checkFirstTimeJoined(); + void notifyAboutAllowedToSpeak(); + + void playConnectingSound(); + void stopConnectingSound(); + void playConnectingSoundOnce(); + + void updateRequestedVideoChannels(); + void updateRequestedVideoChannelsDelayed(); + void fillActiveVideoEndpoints(); + void refreshHasNotShownVideo(); + + void editParticipant( + not_null participantPeer, + bool mute, + std::optional volume); + void applyParticipantLocally( + not_null participantPeer, + bool mute, + std::optional volume); + void applyQueuedSelfUpdates(); + void sendPendingSelfUpdates(); + void applySelfUpdate(const MTPDgroupCallParticipant &data); + void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data); + + void setupMediaDevices(); + void setupOutgoingVideo(); + void setScreenEndpoint(std::string endpoint); + void setCameraEndpoint(std::string endpoint); + void addVideoOutput(const std::string &endpoint, SinkPointer sink); + void setVideoEndpointLarge(VideoEndpoint endpoint); + + void markEndpointActive( + VideoEndpoint endpoint, + bool active, + bool paused); + void markTrackPaused(const VideoEndpoint &endpoint, bool paused); + void markTrackShown(const VideoEndpoint &endpoint, bool shown); + + [[nodiscard]] MTPInputGroupCall inputCall() const; + + const not_null _delegate; + not_null _peer; // Can change in legacy group migration. + rpl::event_stream _peerStream; + not_null _history; // Can change in legacy group migration. + MTP::Sender _api; + rpl::event_stream> _realChanges; + rpl::variable _state = State::Creating; + base::flat_set _unresolvedSsrcs; + rpl::event_stream _errors; + bool _recordingStoppedByMe = false; + bool _requestedVideoChannelsUpdateScheduled = false; + + MTP::DcId _broadcastDcId = 0; + base::flat_map, LoadingPart> _broadcastParts; + base::flat_set< + std::shared_ptr< + MediaChannelDescriptionsTask>, + base::pointer_comparator> _mediaChannelDescriptionses; + + not_null _joinAs; + std::vector> _possibleJoinAs; + QString _joinHash; + + rpl::variable _muted = MuteState::Muted; + rpl::variable _canManage = false; + rpl::variable _videoIsWorking = false; + rpl::variable _hasNotShownVideo = false; + bool _initialMuteStateSent = false; + bool _acceptFields = false; + + rpl::event_stream _otherParticipantStateValue; + std::vector _queuedSelfUpdates; + + uint64 _id = 0; + uint64 _accessHash = 0; + JoinState _joinState; + JoinState _screenJoinState; + std::string _cameraEndpoint; + std::string _screenEndpoint; + TimeId _scheduleDate = 0; + base::flat_set _mySsrcs; + mtpRequestId _createRequestId = 0; + mtpRequestId _selfUpdateRequestId = 0; + + rpl::variable _instanceState + = InstanceState::Disconnected; + bool _instanceTransitioning = false; + InstanceMode _instanceMode = InstanceMode::None; + std::unique_ptr _instance; + base::has_weak_ptr _instanceGuard; + std::shared_ptr _cameraCapture; + rpl::variable _cameraState; + rpl::variable _isSharingCamera = false; + base::flat_map _pendingVideoOutputs; + + rpl::variable _screenInstanceState + = InstanceState::Disconnected; + InstanceMode _screenInstanceMode = InstanceMode::None; + std::unique_ptr _screenInstance; + base::has_weak_ptr _screenInstanceGuard; + std::shared_ptr _screenCapture; + rpl::variable _screenState; + rpl::variable _isSharingScreen = false; + QString _screenDeviceId; + + base::flags _pendingSelfUpdates; + bool _requireARGB32 = true; + + rpl::event_stream _levelUpdates; + rpl::event_stream _videoStreamActiveUpdates; + rpl::event_stream _videoStreamPausedUpdates; + rpl::event_stream _videoStreamShownUpdates; + base::flat_map< + VideoEndpoint, + std::unique_ptr> _activeVideoTracks; + base::flat_set _shownVideoTracks; + rpl::variable _videoEndpointLarge; + rpl::variable _videoEndpointPinned = false; + crl::time _videoLargeTillTime = 0; + base::flat_map _lastSpoke; + rpl::event_stream _rejoinEvents; + rpl::event_stream<> _allowedToSpeakNotifications; + rpl::event_stream<> _titleChanged; + base::Timer _lastSpokeCheckTimer; + base::Timer _checkJoinedTimer; + + crl::time _lastSendProgressUpdate = 0; + + std::shared_ptr _shortcutManager; + std::shared_ptr _pushToTalk; + base::Timer _pushToTalkCancelTimer; + base::Timer _connectingSoundTimer; + bool _hadJoinedState = false; + + std::unique_ptr _mediaDevices; + QString _audioInputId; + QString _audioOutputId; + QString _cameraInputId; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h similarity index 68% rename from Telegram/SourceFiles/calls/calls_group_common.h rename to Telegram/SourceFiles/calls/group/calls_group_common.h index 0cfa4d9fe..ce84f94ef 100644 --- a/Telegram/SourceFiles/calls/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -13,6 +13,7 @@ namespace Calls::Group { constexpr auto kDefaultVolume = 10000; constexpr auto kMaxVolume = 20000; +constexpr auto kBlobsEnterDuration = crl::time(250); struct MuteRequest { not_null peer; @@ -47,4 +48,34 @@ struct JoinInfo { TimeId scheduleDate = 0; }; +enum class PanelMode { + Default, + Wide, +}; + +enum class VideoQuality { + Thumbnail, + Medium, + Full, +}; + +enum class Error { + NoCamera, + CameraFailed, + ScreenFailed, + MutedNoCamera, + MutedNoScreen, + DisabledNoCamera, + DisabledNoScreen, +}; + +enum class StickedTooltip { + Camera = 0x01, + Microphone = 0x02, +}; +constexpr inline bool is_flag_type(StickedTooltip) { + return true; +} +using StickedTooltips = base::flags; + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp new file mode 100644 index 000000000..6ad1c9efc --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -0,0 +1,305 @@ +/* +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 "calls/group/calls_group_invite_controller.h" + +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_menu.h" +#include "boxes/peer_lists_box.h" +#include "data/data_user.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "data/data_group_call.h" +#include "main/main_session.h" +#include "ui/text/text_utilities.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/labels.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" + +namespace Calls::Group { +namespace { + +[[nodiscard]] object_ptr CreateSectionSubtitle( + QWidget *parent, + rpl::producer text) { + auto result = object_ptr( + parent, + st::searchedBarHeight); + + const auto raw = result.data(); + raw->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(raw); + p.fillRect(clip, st::groupCallMembersBgOver); + }, raw->lifetime()); + + const auto label = Ui::CreateChild( + raw, + std::move(text), + st::groupCallBoxLabel); + raw->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto padding = st::groupCallInviteDividerPadding; + const auto available = width - padding.left() - padding.right(); + label->resizeToNaturalWidth(available); + label->moveToLeft(padding.left(), padding.top(), width); + }, label->lifetime()); + + return result; +} + +} // namespace + +InviteController::InviteController( + not_null peer, + base::flat_set> alreadyIn) +: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members) +, _peer(peer) +, _alreadyIn(std::move(alreadyIn)) { + SubscribeToMigration( + _peer, + lifetime(), + [=](not_null channel) { _peer = channel; }); +} + +void InviteController::prepare() { + delegate()->peerListSetHideEmpty(true); + ParticipantsBoxController::prepare(); + delegate()->peerListSetAboveWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_members())); + delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_members())); +} + +void InviteController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked(row, !row->checked()); +} + +base::unique_qptr InviteController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +void InviteController::itemDeselectedHook(not_null peer) { +} + +bool InviteController::hasRowFor(not_null peer) const { + return (delegate()->peerListFindRow(peer->id.value) != nullptr); +} + +bool InviteController::isAlreadyIn(not_null user) const { + return _alreadyIn.contains(user); +} + +std::unique_ptr InviteController::createRow( + not_null participant) const { + const auto user = participant->asUser(); + if (!user + || user->isSelf() + || user->isBot() + || (user->flags() & MTPDuser::Flag::f_deleted)) { + return nullptr; + } + auto result = std::make_unique(user); + _rowAdded.fire_copy(user); + _inGroup.emplace(user); + if (isAlreadyIn(user)) { + result->setDisabledState(PeerListRow::State::DisabledChecked); + } + return result; +} + +auto InviteController::peersWithRows() const +-> not_null>*> { + return &_inGroup; +} + +rpl::producer> InviteController::rowAdded() const { + return _rowAdded.events(); +} + +InviteContactsController::InviteContactsController( + not_null peer, + base::flat_set> alreadyIn, + not_null>*> inGroup, + rpl::producer> discoveredInGroup) +: AddParticipantsBoxController(peer, std::move(alreadyIn)) +, _inGroup(inGroup) +, _discoveredInGroup(std::move(discoveredInGroup)) { +} + +void InviteContactsController::prepareViewHook() { + AddParticipantsBoxController::prepareViewHook(); + + delegate()->peerListSetAboveWidget(CreateSectionSubtitle( + nullptr, + tr::lng_contacts_header())); + delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_search_results())); + + std::move( + _discoveredInGroup + ) | rpl::start_with_next([=](not_null user) { + if (auto row = delegate()->peerListFindRow(user->id.value)) { + delegate()->peerListRemoveRow(row); + } + }, _lifetime); +} + +std::unique_ptr InviteContactsController::createRow( + not_null user) { + return _inGroup->contains(user) + ? nullptr + : AddParticipantsBoxController::createRow(user); +} + +object_ptr PrepareInviteBox( + not_null call, + Fn showToast) { + const auto real = call->lookupReal(); + if (!real) { + return nullptr; + } + const auto peer = call->peer(); + auto alreadyIn = peer->owner().invitedToCallUsers(real->id()); + for (const auto &participant : real->participants()) { + if (const auto user = participant.peer->asUser()) { + alreadyIn.emplace(user); + } + } + alreadyIn.emplace(peer->session().user()); + auto controller = std::make_unique(peer, alreadyIn); + controller->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + + auto contactsController = std::make_unique( + peer, + std::move(alreadyIn), + controller->peersWithRows(), + controller->rowAdded()); + contactsController->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + + const auto weak = base::make_weak(call.get()); + const auto invite = [=](const std::vector> &users) { + const auto call = weak.get(); + if (!call) { + return; + } + const auto result = call->inviteUsers(users); + if (const auto user = std::get_if>(&result)) { + showToast(tr::lng_group_call_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold((*user)->firstName), + Ui::Text::WithEntities)); + } else if (const auto count = std::get_if(&result)) { + if (*count > 0) { + showToast(tr::lng_group_call_invite_done_many( + tr::now, + lt_count, + *count, + Ui::Text::RichLangValue)); + } + } else { + Unexpected("Result in GroupCall::inviteUsers."); + } + }; + const auto inviteWithAdd = [=]( + const std::vector> &users, + const std::vector> &nonMembers, + Fn finish) { + peer->session().api().addChatParticipants( + peer, + nonMembers, + [=](bool) { invite(users); finish(); }); + }; + const auto inviteWithConfirmation = [=]( + not_null parentBox, + const std::vector> &users, + const std::vector> &nonMembers, + Fn finish) { + if (nonMembers.empty()) { + invite(users); + finish(); + return; + } + const auto name = peer->name; + const auto text = (nonMembers.size() == 1) + ? tr::lng_group_call_add_to_group_one( + tr::now, + lt_user, + nonMembers.front()->shortName(), + lt_group, + name) + : (nonMembers.size() < users.size()) + ? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name) + : tr::lng_group_call_add_to_group_all(tr::now, lt_group, name); + const auto shared = std::make_shared>(); + const auto finishWithConfirm = [=] { + if (*shared) { + (*shared)->closeBox(); + } + finish(); + }; + const auto done = [=] { + inviteWithAdd(users, nonMembers, finishWithConfirm); + }; + auto box = ConfirmBox({ + .text = { text }, + .button = tr::lng_participant_invite(), + .callback = done, + }); + *shared = box.data(); + parentBox->getDelegate()->showBox( + std::move(box), + Ui::LayerOption::KeepOther, + anim::type::normal); + }; + auto initBox = [=, controller = controller.get()]( + not_null box) { + box->setTitle(tr::lng_group_call_invite_title()); + box->addButton(tr::lng_group_call_invite_button(), [=] { + const auto rows = box->collectSelectedRows(); + + const auto users = ranges::views::all( + rows + ) | ranges::views::transform([](not_null peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + + const auto nonMembers = ranges::views::all( + users + ) | ranges::views::filter([&](not_null user) { + return !controller->hasRowFor(user); + }) | ranges::to_vector; + + const auto finish = [box = Ui::MakeWeak(box)]() { + if (box) { + box->closeBox(); + } + }; + inviteWithConfirmation(box, users, nonMembers, finish); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + + auto controllers = std::vector>(); + controllers.push_back(std::move(controller)); + controllers.push_back(std::move(contactsController)); + return Box(std::move(controllers), initBox); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h new file mode 100644 index 000000000..e1e10ff4e --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.h @@ -0,0 +1,82 @@ +/* +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 "boxes/peers/edit_participants_box.h" +#include "boxes/peers/add_participants_box.h" + +namespace Calls { +class GroupCall; +} // namespace Calls + +namespace Calls::Group { + +class InviteController final : public ParticipantsBoxController { +public: + InviteController( + not_null peer, + base::flat_set> alreadyIn); + + void prepare() override; + + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + void itemDeselectedHook(not_null peer) override; + + [[nodiscard]] auto peersWithRows() const + -> not_null>*>; + [[nodiscard]] rpl::producer> rowAdded() const; + + [[nodiscard]] bool hasRowFor(not_null peer) const; + +private: + [[nodiscard]] bool isAlreadyIn(not_null user) const; + + std::unique_ptr createRow( + not_null participant) const override; + + not_null _peer; + const base::flat_set> _alreadyIn; + mutable base::flat_set> _inGroup; + rpl::event_stream> _rowAdded; + +}; + +class InviteContactsController final : public AddParticipantsBoxController { +public: + InviteContactsController( + not_null peer, + base::flat_set> alreadyIn, + not_null>*> inGroup, + rpl::producer> discoveredInGroup); + +private: + void prepareViewHook() override; + + std::unique_ptr createRow( + not_null user) override; + + bool needsInviteLinkButton() override { + return false; + } + + const not_null>*> _inGroup; + rpl::producer> _discoveredInGroup; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] object_ptr PrepareInviteBox( + not_null call, + Fn showToast); + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp similarity index 53% rename from Telegram/SourceFiles/calls/calls_group_members.cpp rename to Telegram/SourceFiles/calls/group/calls_group_members.cpp index 5ad91316f..bae760f09 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -5,269 +5,159 @@ 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 "calls/calls_group_members.h" +#include "calls/group/calls_group_members.h" -#include "calls/calls_group_call.h" -#include "calls/calls_group_common.h" -#include "calls/calls_group_menu.h" -#include "calls/calls_volume_item.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_menu.h" +#include "calls/group/calls_volume_item.h" +#include "calls/group/calls_group_members_row.h" +#include "calls/group/calls_group_viewport.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" +#include "data/data_peer.h" #include "data/data_changes.h" #include "data/data_group_call.h" #include "data/data_peer_values.h" // Data::CanWriteValue. #include "data/data_session.h" // Data::Session::invitedToCallUsers. #include "settings/settings_common.h" // Settings::CreateButton. -#include "info/profile/info_profile_values.h" // Info::Profile::AboutValue. -#include "ui/paint/arcs.h" -#include "ui/paint/blobs.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/popup_menu.h" -#include "ui/text/text_utilities.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/cross_line.h" -#include "core/application.h" // Core::App().domain, Core::App().activeWindow. +#include "core/application.h" // Core::App().domain, .activeWindow. #include "main/main_domain.h" // Core::App().domain().activate. #include "main/main_session.h" -#include "base/timer.h" +#include "main/main_account.h" // account().appConfig(). +#include "main/main_app_config.h" // appConfig().get(). #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. -#include "lang/lang_keys.h" #include "window/window_controller.h" // Controller::sessionController. #include "window/window_session_controller.h" +#include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" namespace Calls::Group { namespace { -constexpr auto kBlobsEnterDuration = crl::time(250); -constexpr auto kLevelDuration = 100. + 500. * 0.23; -constexpr auto kBlobScale = 0.605; -constexpr auto kMinorBlobFactor = 0.9f; -constexpr auto kUserpicMinScale = 0.8; -constexpr auto kMaxLevel = 1.; -constexpr auto kWideScale = 5; constexpr auto kKeepRaisedHandStatusDuration = 3 * crl::time(1000); +constexpr auto kShadowMaxAlpha = 74; +constexpr auto kUserpicSizeForBlur = 40; +constexpr auto kUserpicBlurRadius = 8; -const auto kSpeakerThreshold = std::vector{ - Group::kDefaultVolume * 0.1f / Group::kMaxVolume, - Group::kDefaultVolume * 0.9f / Group::kMaxVolume }; +using Row = MembersRow; -constexpr auto kArcsStrokeRatio = 0.8; - -auto RowBlobs() -> std::array { - return { { +void SetupVideoPlaceholder( + not_null widget, + not_null chat) { + struct State { + QImage blurred; + QImage rounded; + InMemoryKey key = {}; + std::shared_ptr view; + qint64 blurredCacheKey = 0; + }; + const auto state = widget->lifetime().make_state(); + const auto refreshBlurred = [=] { + const auto key = chat->userpicUniqueKey(state->view); + if (state->key == key && !state->blurred.isNull()) { + return; + } + constexpr auto size = kUserpicSizeForBlur; + state->key = key; + state->blurred = QImage( + QSize(size, size), + QImage::Format_ARGB32_Premultiplied); { - .segmentsCount = 6, - .minScale = kBlobScale * kMinorBlobFactor, - .minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor, - .maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor, - .speedScale = 1., - .alpha = .5, - }, + auto p = Painter(&state->blurred); + auto hq = PainterHighQualityEnabler(p); + chat->paintUserpicSquare(p, state->view, 0, 0, size); + } + state->blurred = Images::BlurLargeImage( + std::move(state->blurred), + kUserpicBlurRadius); + widget->update(); + }; + const auto refreshRounded = [=](QSize size) { + refreshBlurred(); + const auto key = state->blurred.cacheKey(); + if (state->rounded.size() == size && state->blurredCacheKey == key) { + return; + } + state->blurredCacheKey = key; + state->rounded = Images::prepare( + state->blurred, + size.width(), + size.width(), // Square + Images::Option::Smooth, + size.width(), + size.height()); { - .segmentsCount = 8, - .minScale = kBlobScale, - .minRadius = (float)st::groupCallRowBlobMinRadius, - .maxRadius = (float)st::groupCallRowBlobMaxRadius, - .speedScale = 1., - .alpha = .2, - }, - } }; + auto p = QPainter(&state->rounded); + p.fillRect( + 0, + 0, + size.width(), + size.height(), + QColor(0, 0, 0, Viewport::kShadowMaxAlpha)); + } + state->rounded = Images::prepare( + std::move(state->rounded), + size.width(), + size.height(), + (Images::Option::RoundedLarge | Images::Option::RoundedAll), + size.width(), + size.height()); + }; + chat->loadUserpic(); + refreshBlurred(); + + widget->paintRequest( + ) | rpl::start_with_next([=] { + const auto size = QSize( + widget->width(), + widget->height() - st::groupCallVideoSmallSkip); + refreshRounded(size * cIntRetinaFactor()); + + auto p = QPainter(widget); + const auto inner = QRect(QPoint(), size); + p.drawImage(inner, state->rounded); + st::groupCallPaused.paint( + p, + (size.width() - st::groupCallPaused.width()) / 2, + st::groupCallVideoPlaceholderIconTop, + size.width()); + + const auto skip = st::groupCallVideoLargeSkip; + const auto limit = chat->session().account().appConfig().get( + "groupcall_video_participants_max", + 30.); + p.setPen(st::groupCallVideoTextFg); + const auto text = QRect( + skip, + st::groupCallVideoPlaceholderTextTop, + (size.width() - 2 * skip), + size.height() - st::groupCallVideoPlaceholderTextTop); + p.setFont(st::semiboldFont); + p.drawText( + text, + tr::lng_group_call_limit(tr::now, lt_count, int(limit)), + style::al_top); + }, widget->lifetime()); } -class Row; +} // namespace -class RowDelegate { -public: - struct IconState { - float64 speaking = 0.; - float64 active = 0.; - float64 muted = 0.; - bool mutedByMe = false; - bool raisedHand = false; - }; - virtual bool rowIsMe(not_null participantPeer) = 0; - virtual bool rowCanMuteMembers() = 0; - virtual void rowUpdateRow(not_null row) = 0; - virtual void rowScheduleRaisedHandStatusRemove(not_null row) = 0; - virtual void rowPaintIcon( - Painter &p, - QRect rect, - IconState state) = 0; -}; - -class Row final : public PeerListRow { -public: - Row( - not_null delegate, - not_null participantPeer); - - enum class State { - Active, - Inactive, - Muted, - RaisedHand, - MutedByMe, - Invited, - }; - - void setAbout(const QString &about); - void setSkipLevelUpdate(bool value); - void updateState(const Data::GroupCall::Participant *participant); - void updateLevel(float level); - void updateBlobAnimation(crl::time now); - void clearRaisedHandStatus(); - [[nodiscard]] State state() const { - return _state; - } - [[nodiscard]] uint32 ssrc() const { - return _ssrc; - } - [[nodiscard]] bool sounding() const { - return _sounding; - } - [[nodiscard]] bool speaking() const { - return _speaking; - } - [[nodiscard]] crl::time speakingLastTime() const { - return _speakingLastTime; - } - [[nodiscard]] int volume() const { - return _volume; - } - [[nodiscard]] uint64 raisedHandRating() const { - return _raisedHandRating; - } - - void addActionRipple(QPoint point, Fn updateCallback) override; - void stopLastActionRipple() override; - - QSize actionSize() const override { - return QSize( - st::groupCallActiveButton.width, - st::groupCallActiveButton.height); - } - bool actionDisabled() const override { - return _delegate->rowIsMe(peer()) - || (_state == State::Invited) - || !_delegate->rowCanMuteMembers(); - } - QMargins actionMargins() const override { - return QMargins( - 0, - 0, - st::groupCallMemberButtonSkip, - 0); - } - void paintAction( - Painter &p, - int x, - int y, - int outerWidth, - bool selected, - bool actionSelected) override; - - auto generatePaintUserpicCallback() -> PaintRoundImageCallback override; - - void paintStatusText( - Painter &p, - const style::PeerListItem &st, - int x, - int y, - int availableWidth, - int outerWidth, - bool selected) override; - -private: - struct BlobsAnimation { - BlobsAnimation( - std::vector blobDatas, - float levelDuration, - float maxLevel) - : blobs(std::move(blobDatas), levelDuration, maxLevel) { - style::PaletteChanged( - ) | rpl::start_with_next([=] { - userpicCache = QImage(); - }, lifetime); - } - - Ui::Paint::Blobs blobs; - crl::time lastTime = 0; - crl::time lastSoundingUpdateTime = 0; - float64 enter = 0.; - - QImage userpicCache; - InMemoryKey userpicKey; - - rpl::lifetime lifetime; - }; - - struct StatusIcon { - StatusIcon(bool shown, float volume); - - const style::icon &speaker; - Ui::Paint::ArcsAnimation arcs; - Ui::Animations::Simple arcsAnimation; - Ui::Animations::Simple shownAnimation; - QString percent; - int percentWidth = 0; - int arcsWidth = 0; - int wasArcsWidth = 0; - bool shown = true; - - rpl::lifetime lifetime; - }; - - int statusIconWidth() const; - int statusIconHeight() const; - void paintStatusIcon( - Painter &p, - const style::PeerListItem &st, - const style::font &font, - bool selected); - - void refreshStatus() override; - void setSounding(bool sounding); - void setSpeaking(bool speaking); - void setState(State state); - void setSsrc(uint32 ssrc); - void setVolume(int volume); - - void ensureUserpicCache( - std::shared_ptr &view, - int size); - - const not_null _delegate; - State _state = State::Inactive; - std::unique_ptr _actionRipple; - std::unique_ptr _blobsAnimation; - std::unique_ptr _statusIcon; - Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon. - Ui::Animations::Simple _mutedAnimation; // For gray/red icon. - Ui::Animations::Simple _activeAnimation; // For icon cross animation. - QString _aboutText; - crl::time _speakingLastTime = 0; - uint64 _raisedHandRating = 0; - uint32 _ssrc = 0; - int _volume = Group::kDefaultVolume; - bool _sounding = false; - bool _speaking = false; - bool _raisedHandStatus = false; - bool _skipLevelUpdate = false; - -}; - -class MembersController final +class Members::Controller final : public PeerListController - , public RowDelegate + , public MembersRowDelegate , public base::has_weak_ptr { public: - MembersController( + Controller( not_null call, - not_null menuParent); - ~MembersController(); + not_null menuParent, + PanelMode mode); + ~Controller(); using MuteRequest = Group::MuteRequest; using VolumeRequest = Group::VolumeRequest; @@ -289,6 +179,9 @@ public: [[nodiscard]] auto kickParticipantRequests() const -> rpl::producer>; + Row *findRow(not_null participantPeer) const; + void setMode(PanelMode mode); + bool rowIsMe(not_null participantPeer) override; bool rowCanMuteMembers() override; void rowUpdateRow(not_null row) override; @@ -296,18 +189,26 @@ public: void rowPaintIcon( Painter &p, QRect rect, - IconState state) override; + const IconState &state) override; + int rowPaintStatusIcon( + Painter &p, + int x, + int y, + int outerWidth, + not_null row, + const IconState &state) override; + bool rowIsNarrow() override; + void rowShowContextMenu(not_null row) override; private: [[nodiscard]] std::unique_ptr createRowForMe(); [[nodiscard]] std::unique_ptr createRow( - const Data::GroupCall::Participant &participant); + const Data::GroupCallParticipant &participant); [[nodiscard]] std::unique_ptr createInvitedRow( not_null participantPeer); [[nodiscard]] bool isMe(not_null participantPeer) const; void prepareRows(not_null real); - //void repaintByTimer(); [[nodiscard]] base::unique_qptr createRowContextMenu( QWidget *parent, @@ -320,11 +221,11 @@ private: void setupListChangeViewers(); void subscribeToChanges(not_null real); void updateRow( - const std::optional &was, - const Data::GroupCall::Participant &now); + const std::optional &was, + const Data::GroupCallParticipant &now); void updateRow( not_null row, - const Data::GroupCall::Participant *participant); + const Data::GroupCallParticipant *participant); void removeRow(not_null row); void updateRowLevel(not_null row, float level); void checkRowPosition(not_null row); @@ -333,13 +234,29 @@ private: [[nodiscard]] bool allRowsAboveMoreImportantThanHand( not_null row, uint64 raiseHandRating) const; - Row *findRow(not_null participantPeer) const; + [[nodiscard]] const Data::GroupCallParticipant *findParticipant( + const std::string &endpoint) const; + [[nodiscard]] const std::string &computeScreenEndpoint( + not_null participant) const; + [[nodiscard]] const std::string &computeCameraEndpoint( + not_null participant) const; + void showRowMenu(not_null row, bool highlightRow); + + void toggleVideoEndpointActive( + const VideoEndpoint &endpoint, + bool active); void appendInvitedUsers(); void scheduleRaisedHandStatusRemove(); + void hideRowsWithVideoExcept(const VideoEndpoint &large); + void showAllHiddenRows(); + void hideRowWithVideo(const VideoEndpoint &endpoint); + void showRowWithVideo(const VideoEndpoint &endpoint); + const not_null _call; not_null _peer; + std::string _largeEndpoint; bool _prepared = false; rpl::event_stream _toggleMuteRequests; @@ -355,561 +272,51 @@ private: base::Timer _raisedHandStatusRemoveTimer; base::flat_map> _soundingRowBySsrc; + base::flat_set> _cameraActive; + base::flat_set> _screenActive; Ui::Animations::Basic _soundingAnimation; crl::time _soundingAnimationHideLastTime = 0; bool _skipRowLevelUpdate = false; + PanelMode _mode = PanelMode::Default; Ui::CrossLineAnimation _inactiveCrossLine; Ui::CrossLineAnimation _coloredCrossLine; + Ui::CrossLineAnimation _inactiveNarrowCrossLine; + Ui::CrossLineAnimation _coloredNarrowCrossLine; + Ui::CrossLineAnimation _videoCrossLine; + Ui::RoundRect _narrowRoundRectSelected; + Ui::RoundRect _narrowRoundRect; + QImage _narrowShadow; rpl::lifetime _lifetime; }; -[[nodiscard]] QString StatusPercentString(float volume) { - return QString::number(int(std::round(volume * 200))) + '%'; -} - -[[nodiscard]] int StatusPercentWidth(const QString &percent) { - return st::normalFont->width(percent); -} - -Row::StatusIcon::StatusIcon(bool shown, float volume) -: speaker(st::groupCallStatusSpeakerIcon) -, arcs( - st::groupCallStatusSpeakerArcsAnimation, - kSpeakerThreshold, - volume, - Ui::Paint::ArcsAnimation::Direction::Right) -, percent(StatusPercentString(volume)) -, percentWidth(StatusPercentWidth(percent)) -, shown(shown) { -} - -Row::Row( - not_null delegate, - not_null participantPeer) -: PeerListRow(participantPeer) -, _delegate(delegate) { - refreshStatus(); - _aboutText = participantPeer->about(); -} - -void Row::setSkipLevelUpdate(bool value) { - _skipLevelUpdate = value; -} - -void Row::updateState(const Data::GroupCall::Participant *participant) { - setSsrc(participant ? participant->ssrc : 0); - setVolume(participant - ? participant->volume - : Group::kDefaultVolume); - if (!participant) { - setState(State::Invited); - setSounding(false); - setSpeaking(false); - _raisedHandRating = 0; - } else if (!participant->muted - || (participant->sounding && participant->ssrc != 0)) { - setState(participant->mutedByMe ? State::MutedByMe : State::Active); - setSounding(participant->sounding && participant->ssrc != 0); - setSpeaking(participant->speaking && participant->ssrc != 0); - _raisedHandRating = 0; - } else if (participant->canSelfUnmute) { - setState(participant->mutedByMe - ? State::MutedByMe - : State::Inactive); - setSounding(false); - setSpeaking(false); - _raisedHandRating = 0; - } else { - _raisedHandRating = participant->raisedHandRating; - setState(_raisedHandRating ? State::RaisedHand : State::Muted); - setSounding(false); - setSpeaking(false); - } - refreshStatus(); -} - -void Row::setSpeaking(bool speaking) { - if (_speaking == speaking) { - return; - } - _speaking = speaking; - _speakingAnimation.start( - [=] { _delegate->rowUpdateRow(this); }, - _speaking ? 0. : 1., - _speaking ? 1. : 0., - st::widgetFadeDuration); - - if (!_speaking - || (_state == State::MutedByMe) - || (_state == State::Muted) - || (_state == State::RaisedHand)) { - if (_statusIcon) { - _statusIcon = nullptr; - _delegate->rowUpdateRow(this); - } - } else if (!_statusIcon) { - _statusIcon = std::make_unique( - (_volume != Group::kDefaultVolume), - (float)_volume / Group::kMaxVolume); - _statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio); - _statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth(); - _statusIcon->arcs.startUpdateRequests( - ) | rpl::start_with_next([=] { - if (!_statusIcon->arcsAnimation.animating()) { - _statusIcon->wasArcsWidth = _statusIcon->arcsWidth; - } - auto callback = [=](float64 value) { - _statusIcon->arcs.update(crl::now()); - _statusIcon->arcsWidth = anim::interpolate( - _statusIcon->wasArcsWidth, - _statusIcon->arcs.finishedWidth(), - value); - _delegate->rowUpdateRow(this); - }; - _statusIcon->arcsAnimation.start( - std::move(callback), - 0., - 1., - st::groupCallSpeakerArcsAnimation.duration); - }, _statusIcon->lifetime); - } -} - -void Row::setSounding(bool sounding) { - if (_sounding == sounding) { - return; - } - _sounding = sounding; - if (!_sounding) { - _blobsAnimation = nullptr; - } else if (!_blobsAnimation) { - _blobsAnimation = std::make_unique( - RowBlobs() | ranges::to_vector, - kLevelDuration, - kMaxLevel); - _blobsAnimation->lastTime = crl::now(); - updateLevel(GroupCall::kSpeakLevelThreshold); - } -} - -void Row::clearRaisedHandStatus() { - if (!_raisedHandStatus) { - return; - } - _raisedHandStatus = false; - refreshStatus(); - _delegate->rowUpdateRow(this); -} - -void Row::setState(State state) { - if (_state == state) { - return; - } - const auto wasActive = (_state == State::Active); - const auto wasMuted = (_state == State::Muted) - || (_state == State::RaisedHand); - const auto wasRaisedHand = (_state == State::RaisedHand); - _state = state; - const auto nowActive = (_state == State::Active); - const auto nowMuted = (_state == State::Muted) - || (_state == State::RaisedHand); - const auto nowRaisedHand = (_state == State::RaisedHand); - if (!wasRaisedHand && nowRaisedHand) { - _raisedHandStatus = true; - _delegate->rowScheduleRaisedHandStatusRemove(this); - } - if (nowActive != wasActive) { - _activeAnimation.start( - [=] { _delegate->rowUpdateRow(this); }, - nowActive ? 0. : 1., - nowActive ? 1. : 0., - st::widgetFadeDuration); - } - if (nowMuted != wasMuted) { - _mutedAnimation.start( - [=] { _delegate->rowUpdateRow(this); }, - nowMuted ? 0. : 1., - nowMuted ? 1. : 0., - st::widgetFadeDuration); - } -} - -void Row::setSsrc(uint32 ssrc) { - _ssrc = ssrc; -} - -void Row::setVolume(int volume) { - _volume = volume; - if (_statusIcon) { - const auto floatVolume = (float)volume / Group::kMaxVolume; - _statusIcon->arcs.setValue(floatVolume); - _statusIcon->percent = StatusPercentString(floatVolume); - _statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent); - - const auto shown = (volume != Group::kDefaultVolume); - if (_statusIcon->shown != shown) { - _statusIcon->shown = shown; - _statusIcon->shownAnimation.start( - [=] { _delegate->rowUpdateRow(this); }, - shown ? 0. : 1., - shown ? 1. : 0., - st::groupCallSpeakerArcsAnimation.duration); - } - } -} - -void Row::updateLevel(float level) { - Expects(_blobsAnimation != nullptr); - - const auto spoke = (level >= GroupCall::kSpeakLevelThreshold) - ? crl::now() - : crl::time(); - if (spoke && _speaking) { - _speakingLastTime = spoke; - } - - if (_skipLevelUpdate) { - return; - } - - if (spoke) { - _blobsAnimation->lastSoundingUpdateTime = spoke; - } - _blobsAnimation->blobs.setLevel(level); -} - -void Row::updateBlobAnimation(crl::time now) { - Expects(_blobsAnimation != nullptr); - - const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime - + Data::GroupCall::kSoundStatusKeptFor; - const auto soundingStartsFinishing = soundingFinishesAt - - kBlobsEnterDuration; - const auto soundingFinishes = (soundingStartsFinishing < now); - if (soundingFinishes) { - _blobsAnimation->enter = std::clamp( - (soundingFinishesAt - now) / float64(kBlobsEnterDuration), - 0., - 1.); - } else if (_blobsAnimation->enter < 1.) { - _blobsAnimation->enter = std::clamp( - (_blobsAnimation->enter - + ((now - _blobsAnimation->lastTime) - / float64(kBlobsEnterDuration))), - 0., - 1.); - } - _blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime); - _blobsAnimation->lastTime = now; -} - -void Row::ensureUserpicCache( - std::shared_ptr &view, - int size) { - Expects(_blobsAnimation != nullptr); - - const auto user = peer(); - const auto key = user->userpicUniqueKey(view); - const auto full = QSize(size, size) * kWideScale * cIntRetinaFactor(); - auto &cache = _blobsAnimation->userpicCache; - if (cache.isNull()) { - cache = QImage(full, QImage::Format_ARGB32_Premultiplied); - cache.setDevicePixelRatio(cRetinaFactor()); - } else if (_blobsAnimation->userpicKey == key - && cache.size() == full) { - return; - } - _blobsAnimation->userpicKey = key; - cache.fill(Qt::transparent); - { - Painter p(&cache); - const auto skip = (kWideScale - 1) / 2 * size; - user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size); - } -} - -auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback { - auto userpic = ensureUserpicView(); - return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { - if (_blobsAnimation) { - const auto mutedByMe = (_state == State::MutedByMe); - const auto shift = QPointF(x + size / 2., y + size / 2.); - auto hq = PainterHighQualityEnabler(p); - p.translate(shift); - const auto brush = mutedByMe - ? st::groupCallMemberMutedIcon->b - : anim::brush( - st::groupCallMemberInactiveStatus, - st::groupCallMemberActiveStatus, - _speakingAnimation.value(_speaking ? 1. : 0.)); - _blobsAnimation->blobs.paint(p, brush); - p.translate(-shift); - p.setOpacity(1.); - - const auto enter = _blobsAnimation->enter; - const auto &minScale = kUserpicMinScale; - const auto scaleUserpic = minScale - + (1. - minScale) * _blobsAnimation->blobs.currentLevel(); - const auto scale = scaleUserpic * enter + 1. * (1. - enter); - if (scale == 1.) { - peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); - } else { - ensureUserpicCache(userpic, size); - - PainterHighQualityEnabler hq(p); - - auto target = QRect( - x + (1 - kWideScale) / 2 * size, - y + (1 - kWideScale) / 2 * size, - kWideScale * size, - kWideScale * size); - auto shrink = anim::interpolate( - (1 - kWideScale) / 2 * size, - 0, - scale); - auto margins = QMargins(shrink, shrink, shrink, shrink); - p.drawImage( - target.marginsAdded(margins), - _blobsAnimation->userpicCache); - } - } else { - peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); - } - }; -} - -int Row::statusIconWidth() const { - if (!_statusIcon || !_speaking) { - return 0; - } - const auto shown = _statusIcon->shownAnimation.value( - _statusIcon->shown ? 1. : 0.); - const auto full = _statusIcon->speaker.width() - + _statusIcon->arcsWidth - + _statusIcon->percentWidth - + st::normalFont->spacew; - return int(std::round(shown * full)); -} - -int Row::statusIconHeight() const { - return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0; -} - -void Row::paintStatusIcon( - Painter &p, - const style::PeerListItem &st, - const style::font &font, - bool selected) { - if (!_statusIcon) { - return; - } - const auto shown = _statusIcon->shownAnimation.value( - _statusIcon->shown ? 1. : 0.); - if (shown == 0.) { - return; - } - - p.setFont(font); - const auto color = (_speaking - ? st.statusFgActive - : (selected ? st.statusFgOver : st.statusFg))->c; - p.setPen(color); - - const auto speakerRect = QRect( - st.statusPosition - + QPoint(0, (font->height - statusIconHeight()) / 2), - _statusIcon->speaker.size()); - const auto arcPosition = speakerRect.topLeft() - + QPoint( - speakerRect.width() - st::groupCallStatusSpeakerArcsSkip, - speakerRect.height() / 2); - const auto fullWidth = speakerRect.width() - + _statusIcon->arcsWidth - + _statusIcon->percentWidth - + st::normalFont->spacew; - - p.save(); - if (shown < 1.) { - const auto centerx = speakerRect.x() + fullWidth / 2; - const auto centery = speakerRect.y() + speakerRect.height() / 2; - p.translate(centerx, centery); - p.scale(shown, shown); - p.translate(-centerx, -centery); - } - _statusIcon->speaker.paint( - p, - speakerRect.topLeft(), - speakerRect.width(), - color); - p.translate(arcPosition); - _statusIcon->arcs.paint(p, color); - p.translate(-arcPosition); - p.setFont(st::normalFont); - p.setPen(st.statusFgActive); - p.drawTextLeft( - st.statusPosition.x() + speakerRect.width() + _statusIcon->arcsWidth, - st.statusPosition.y(), - fullWidth, - _statusIcon->percent); - p.restore(); -} - -void Row::setAbout(const QString &about) { - if (_aboutText == about) { - return; - } - _aboutText = about; - _delegate->rowUpdateRow(this); -} - -void Row::paintStatusText( - Painter &p, - const style::PeerListItem &st, - int x, - int y, - int availableWidth, - int outerWidth, - bool selected) { - const auto &font = st::normalFont; - const auto about = (_state == State::Inactive - || _state == State::Muted - || (_state == State::RaisedHand && !_raisedHandStatus)) - ? _aboutText - : QString(); - if (about.isEmpty() - && _state != State::Invited - && _state != State::MutedByMe) { - paintStatusIcon(p, st, font, selected); - - const auto translatedWidth = statusIconWidth(); - p.translate(translatedWidth, 0); - const auto guard = gsl::finally([&] { - p.translate(-translatedWidth, 0); - }); - - PeerListRow::paintStatusText( - p, - st, - x, - y, - availableWidth - translatedWidth, - outerWidth, - selected); - return; - } - p.setFont(font); - if (_state == State::MutedByMe) { - p.setPen(st::groupCallMemberMutedIcon); - } else { - p.setPen(st::groupCallMemberNotJoinedStatus); - } - p.drawTextLeft( - x, - y, - outerWidth, - (_state == State::MutedByMe - ? tr::lng_group_call_muted_by_me_status(tr::now) - : !about.isEmpty() - ? font->m.elidedText(about, Qt::ElideRight, availableWidth) - : _delegate->rowIsMe(peer()) - ? tr::lng_status_connecting(tr::now) - : tr::lng_group_call_invited_status(tr::now))); -} - -void Row::paintAction( - Painter &p, - int x, - int y, - int outerWidth, - bool selected, - bool actionSelected) { - auto size = actionSize(); - const auto iconRect = style::rtlrect( - x, - y, - size.width(), - size.height(), - outerWidth); - if (_state == State::Invited) { - _actionRipple = nullptr; - st::groupCallMemberInvited.paint( - p, - QPoint(x, y) + st::groupCallMemberInvitedPosition, - outerWidth); - return; - } - if (_actionRipple) { - _actionRipple->paint( - p, - x + st::groupCallActiveButton.rippleAreaPosition.x(), - y + st::groupCallActiveButton.rippleAreaPosition.y(), - outerWidth); - if (_actionRipple->empty()) { - _actionRipple.reset(); - } - } - const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.); - const auto active = _activeAnimation.value((_state == State::Active) ? 1. : 0.); - const auto muted = _mutedAnimation.value( - (_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.); - const auto mutedByMe = (_state == State::MutedByMe); - _delegate->rowPaintIcon(p, iconRect, { - .speaking = speaking, - .active = active, - .muted = muted, - .mutedByMe = (_state == State::MutedByMe), - .raisedHand = (_state == State::RaisedHand), - }); -} - -void Row::refreshStatus() { - setCustomStatus( - (_speaking - ? tr::lng_group_call_active(tr::now) - : _raisedHandStatus - ? tr::lng_group_call_raised_hand_status(tr::now) - : tr::lng_group_call_inactive(tr::now)), - _speaking); -} - -void Row::addActionRipple(QPoint point, Fn updateCallback) { - if (!_actionRipple) { - auto mask = Ui::RippleAnimation::ellipseMask(QSize( - st::groupCallActiveButton.rippleAreaSize, - st::groupCallActiveButton.rippleAreaSize)); - _actionRipple = std::make_unique( - st::groupCallActiveButton.ripple, - std::move(mask), - std::move(updateCallback)); - } - _actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition); -} - -void Row::stopLastActionRipple() { - if (_actionRipple) { - _actionRipple->lastStop(); - } -} - -MembersController::MembersController( +Members::Controller::Controller( not_null call, - not_null menuParent) + not_null menuParent, + PanelMode mode) : _call(call) , _peer(call->peer()) , _menuParent(menuParent) , _raisedHandStatusRemoveTimer([=] { scheduleRaisedHandStatusRemove(); }) +, _mode(mode) , _inactiveCrossLine(st::groupCallMemberInactiveCrossLine) -, _coloredCrossLine(st::groupCallMemberColoredCrossLine) { - setupListChangeViewers(); - +, _coloredCrossLine(st::groupCallMemberColoredCrossLine) +, _inactiveNarrowCrossLine(st::groupCallNarrowInactiveCrossLine) +, _coloredNarrowCrossLine(st::groupCallNarrowColoredCrossLine) +, _videoCrossLine(st::groupCallVideoCrossLine) +, _narrowRoundRectSelected( + ImageRoundRadius::Large, + st::groupCallMembersBgOver) +, _narrowRoundRect(ImageRoundRadius::Large, st::groupCallMembersBg) { style::PaletteChanged( ) | rpl::start_with_next([=] { _inactiveCrossLine.invalidate(); _coloredCrossLine.invalidate(); + _inactiveNarrowCrossLine.invalidate(); + _coloredNarrowCrossLine.invalidate(); }, _lifetime); rpl::combine( @@ -955,23 +362,16 @@ MembersController::MembersController( }, _lifetime); } -MembersController::~MembersController() { +Members::Controller::~Controller() { base::take(_menu); } -void MembersController::setupListChangeViewers() { +void Members::Controller::setupListChangeViewers() { _call->real( ) | rpl::start_with_next([=](not_null real) { subscribeToChanges(real); }, _lifetime); - _call->stateValue( - ) | rpl::start_with_next([=] { - if (const auto real = _call->lookupReal()) { - //updateRow(channel->session().user()); - } - }, _lifetime); - _call->levelUpdates( ) | rpl::start_with_next([=](const LevelUpdate &update) { const auto i = _soundingRowBySsrc.find(update.ssrc); @@ -980,6 +380,27 @@ void MembersController::setupListChangeViewers() { } }, _lifetime); + _call->videoEndpointLargeValue( + ) | rpl::start_with_next([=](const VideoEndpoint &large) { + if (large) { + hideRowsWithVideoExcept(large); + } else { + showAllHiddenRows(); + } + }, _lifetime); + + _call->videoStreamShownUpdates( + ) | rpl::filter([=](const VideoStateToggle &update) { + const auto &large = _call->videoEndpointLarge(); + return large && (update.endpoint != large); + }) | rpl::start_with_next([=](const VideoStateToggle &update) { + if (update.value) { + hideRowWithVideo(update.endpoint); + } else { + showRowWithVideo(update.endpoint); + } + }, _lifetime); + _call->rejoinEvents( ) | rpl::start_with_next([=](const Group::RejoinEvent &event) { const auto guard = gsl::finally([&] { @@ -996,10 +417,71 @@ void MembersController::setupListChangeViewers() { }, _lifetime); } -void MembersController::subscribeToChanges(not_null real) { +void Members::Controller::hideRowsWithVideoExcept( + const VideoEndpoint &large) { + auto changed = false; + auto showLargeRow = true; + for (const auto &endpoint : _call->shownVideoTracks()) { + if (endpoint != large) { + if (const auto row = findRow(endpoint.peer)) { + if (endpoint.peer == large.peer) { + showLargeRow = false; + } + delegate()->peerListSetRowHidden(row, true); + changed = true; + } + } + } + if (const auto row = showLargeRow ? findRow(large.peer) : nullptr) { + delegate()->peerListSetRowHidden(row, false); + changed = true; + } + if (changed) { + delegate()->peerListRefreshRows(); + } +} + +void Members::Controller::showAllHiddenRows() { + auto shown = false; + for (const auto &endpoint : _call->shownVideoTracks()) { + if (const auto row = findRow(endpoint.peer)) { + delegate()->peerListSetRowHidden(row, false); + shown = true; + } + } + if (shown) { + delegate()->peerListRefreshRows(); + } +} + +void Members::Controller::hideRowWithVideo(const VideoEndpoint &endpoint) { + if (const auto row = findRow(endpoint.peer)) { + delegate()->peerListSetRowHidden(row, true); + delegate()->peerListRefreshRows(); + } +} + +void Members::Controller::showRowWithVideo(const VideoEndpoint &endpoint) { + const auto peer = endpoint.peer; + const auto &large = _call->videoEndpointLarge(); + if (large) { + for (const auto &endpoint : _call->shownVideoTracks()) { + if (endpoint != large && endpoint.peer == peer) { + // Still hidden with another video. + return; + } + } + } + if (const auto row = findRow(endpoint.peer)) { + delegate()->peerListSetRowHidden(row, false); + delegate()->peerListRefreshRows(); + } +} + +void Members::Controller::subscribeToChanges(not_null real) { _fullCount = real->fullCountValue(); - real->participantsSliceAdded( + real->participantsReloaded( ) | rpl::start_with_next([=] { prepareRows(real); }, _lifetime); @@ -1027,12 +509,64 @@ void MembersController::subscribeToChanges(not_null real) { } }, _lifetime); + for (const auto &[endpoint, track] : _call->activeVideoTracks()) { + toggleVideoEndpointActive(endpoint, true); + } + _call->videoStreamActiveUpdates( + ) | rpl::start_with_next([=](const VideoStateToggle &update) { + toggleVideoEndpointActive(update.endpoint, update.value); + }, _lifetime); + if (_prepared) { appendInvitedUsers(); } } -void MembersController::appendInvitedUsers() { +void Members::Controller::toggleVideoEndpointActive( + const VideoEndpoint &endpoint, + bool active) { + const auto toggleOne = [=]( + base::flat_set> &set, + not_null participantPeer, + bool active) { + if ((active && set.emplace(participantPeer).second) + || (!active && set.remove(participantPeer))) { + if (_mode == PanelMode::Wide) { + if (const auto row = findRow(participantPeer)) { + delegate()->peerListUpdateRow(row); + } + } + } + }; + const auto &id = endpoint.id; + const auto participantPeer = endpoint.peer; + const auto real = _call->lookupReal(); + if (active) { + if (const auto participant = findParticipant(id)) { + if (computeCameraEndpoint(participant) == id) { + toggleOne(_cameraActive, participantPeer, true); + } else if (computeScreenEndpoint(participant) == id) { + toggleOne(_screenActive, participantPeer, true); + } + } + } else if (const auto participant = real->participantByPeer( + participantPeer)) { + const auto &camera = computeCameraEndpoint(participant); + const auto &screen = computeScreenEndpoint(participant); + if (camera == id || camera.empty()) { + toggleOne(_cameraActive, participantPeer, false); + } + if (screen == id || screen.empty()) { + toggleOne(_screenActive, participantPeer, false); + } + } else { + toggleOne(_cameraActive, participantPeer, false); + toggleOne(_screenActive, participantPeer, false); + } + +} + +void Members::Controller::appendInvitedUsers() { if (const auto id = _call->id()) { for (const auto user : _peer->owner().invitedToCallUsers(id)) { if (auto row = createInvitedRow(user)) { @@ -1054,9 +588,9 @@ void MembersController::appendInvitedUsers() { }, _lifetime); } -void MembersController::updateRow( - const std::optional &was, - const Data::GroupCall::Participant &now) { +void Members::Controller::updateRow( + const std::optional &was, + const Data::GroupCallParticipant &now) { auto reorderIfInvitedBefore = 0; auto checkPosition = (Row*)nullptr; auto addedToBottom = (Row*)nullptr; @@ -1121,7 +655,7 @@ void MembersController::updateRow( } } -bool MembersController::allRowsAboveAreSpeaking(not_null row) const { +bool Members::Controller::allRowsAboveAreSpeaking(not_null row) const { const auto count = delegate()->peerListFullRowsCount(); for (auto i = 0; i != count; ++i) { const auto above = delegate()->peerListRowAt(i); @@ -1135,7 +669,7 @@ bool MembersController::allRowsAboveAreSpeaking(not_null row) const { return false; } -bool MembersController::allRowsAboveMoreImportantThanHand( +bool Members::Controller::allRowsAboveMoreImportantThanHand( not_null row, uint64 raiseHandRating) const { Expects(raiseHandRating > 0); @@ -1158,7 +692,7 @@ bool MembersController::allRowsAboveMoreImportantThanHand( return false; } -bool MembersController::needToReorder(not_null row) const { +bool Members::Controller::needToReorder(not_null row) const { // All reorder cases: // - bring speaking up // - bring raised hand up @@ -1194,7 +728,7 @@ bool MembersController::needToReorder(not_null row) const { return false; } -void MembersController::checkRowPosition(not_null row) { +void Members::Controller::checkRowPosition(not_null row) { if (_menu) { // Don't reorder rows while we show the popup menu. _menuCheckRowsAfterHidden.emplace(row->peer()); @@ -1240,9 +774,9 @@ void MembersController::checkRowPosition(not_null row) { : makeComparator(projForOther)); } -void MembersController::updateRow( +void Members::Controller::updateRow( not_null row, - const Data::GroupCall::Participant *participant) { + const Data::GroupCallParticipant *participant) { const auto wasSounding = row->sounding(); const auto wasSsrc = row->ssrc(); const auto wasInChat = (row->state() != Row::State::Invited); @@ -1277,12 +811,12 @@ void MembersController::updateRow( delegate()->peerListUpdateRow(row); } -void MembersController::removeRow(not_null row) { +void Members::Controller::removeRow(not_null row) { _soundingRowBySsrc.remove(row->ssrc()); delegate()->peerListRemoveRow(row); } -void MembersController::updateRowLevel( +void Members::Controller::updateRowLevel( not_null row, float level) { if (_skipRowLevelUpdate) { @@ -1291,20 +825,54 @@ void MembersController::updateRowLevel( row->updateLevel(level); } -Row *MembersController::findRow(not_null participantPeer) const { +Row *Members::Controller::findRow( + not_null participantPeer) const { return static_cast( delegate()->peerListFindRow(participantPeer->id.value)); } -Main::Session &MembersController::session() const { +void Members::Controller::setMode(PanelMode mode) { + _mode = mode; +} + +const Data::GroupCallParticipant *Members::Controller::findParticipant( + const std::string &endpoint) const { + if (endpoint.empty()) { + return nullptr; + } + const auto real = _call->lookupReal(); + if (!real) { + return nullptr; + } else if (endpoint == _call->screenSharingEndpoint() + || endpoint == _call->cameraSharingEndpoint()) { + return real->participantByPeer(_call->joinAs()); + } else { + return real->participantByEndpoint(endpoint); + } +} + +const std::string &Members::Controller::computeScreenEndpoint( + not_null participant) const { + return (participant->peer == _call->joinAs()) + ? _call->screenSharingEndpoint() + : participant->screenEndpoint(); +} + +const std::string &Members::Controller::computeCameraEndpoint( + not_null participant) const { + return (participant->peer == _call->joinAs()) + ? _call->cameraSharingEndpoint() + : participant->cameraEndpoint(); +} + +Main::Session &Members::Controller::session() const { return _call->peer()->session(); } -void MembersController::prepare() { +void Members::Controller::prepare() { delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled); - //delegate()->peerListSetTitle(std::move(title)); - setDescriptionText(tr::lng_contacts_loading(tr::now)); - setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); + setDescription(nullptr); + setSearchNoResults(nullptr); if (const auto real = _call->lookupReal()) { prepareRows(real); @@ -1316,16 +884,17 @@ void MembersController::prepare() { loadMoreRows(); appendInvitedUsers(); _prepared = true; + + setupListChangeViewers(); } -bool MembersController::isMe(not_null participantPeer) const { +bool Members::Controller::isMe(not_null participantPeer) const { return (_call->joinAs() == participantPeer); } -void MembersController::prepareRows(not_null real) { +void Members::Controller::prepareRows(not_null real) { auto foundMe = false; auto changed = false; - const auto &participants = real->participants(); auto count = delegate()->peerListFullRowsCount(); for (auto i = 0; i != count;) { auto row = delegate()->peerListRowAt(i); @@ -1335,11 +904,7 @@ void MembersController::prepareRows(not_null real) { ++i; continue; } - const auto contains = ranges::contains( - participants, - participantPeer, - &Data::GroupCall::Participant::peer); - if (contains) { + if (real->participantByPeer(participantPeer)) { ++i; } else { changed = true; @@ -1349,19 +914,16 @@ void MembersController::prepareRows(not_null real) { } if (!foundMe) { const auto me = _call->joinAs(); - const auto i = ranges::find( - participants, - me, - &Data::GroupCall::Participant::peer); - auto row = (i != end(participants)) - ? createRow(*i) + const auto participant = real->participantByPeer(me); + auto row = participant + ? createRow(*participant) : createRowForMe(); if (row) { changed = true; delegate()->peerListAppendRow(std::move(row)); } } - for (const auto &participant : participants) { + for (const auto &participant : real->participants()) { if (auto row = createRow(participant)) { changed = true; delegate()->peerListAppendRow(std::move(row)); @@ -1372,35 +934,35 @@ void MembersController::prepareRows(not_null real) { } } -void MembersController::loadMoreRows() { +void Members::Controller::loadMoreRows() { if (const auto real = _call->lookupReal()) { real->requestParticipants(); } } -auto MembersController::toggleMuteRequests() const +auto Members::Controller::toggleMuteRequests() const -> rpl::producer { return _toggleMuteRequests.events(); } -auto MembersController::changeVolumeRequests() const +auto Members::Controller::changeVolumeRequests() const -> rpl::producer { return _changeVolumeRequests.events(); } -bool MembersController::rowIsMe(not_null participantPeer) { +bool Members::Controller::rowIsMe(not_null participantPeer) { return isMe(participantPeer); } -bool MembersController::rowCanMuteMembers() { +bool Members::Controller::rowCanMuteMembers() { return _peer->canManageGroupCall(); } -void MembersController::rowUpdateRow(not_null row) { +void Members::Controller::rowUpdateRow(not_null row) { delegate()->peerListUpdateRow(row); } -void MembersController::rowScheduleRaisedHandStatusRemove( +void Members::Controller::rowScheduleRaisedHandStatusRemove( not_null row) { const auto id = row->id(); const auto when = crl::now() + kKeepRaisedHandStatusDuration; @@ -1413,7 +975,7 @@ void MembersController::rowScheduleRaisedHandStatusRemove( scheduleRaisedHandStatusRemove(); } -void MembersController::scheduleRaisedHandStatusRemove() { +void Members::Controller::scheduleRaisedHandStatusRemove() { auto waiting = crl::time(0); const auto now = crl::now(); for (auto i = begin(_raisedHandStatusRemoveAt) @@ -1438,46 +1000,88 @@ void MembersController::scheduleRaisedHandStatusRemove() { } } -void MembersController::rowPaintIcon( +void Members::Controller::rowPaintIcon( Painter &p, QRect rect, - IconState state) { - const auto &greenIcon = st::groupCallMemberColoredCrossLine.icon; + const IconState &state) { + if (_mode == PanelMode::Wide + && state.style == MembersRowStyle::Default) { + return; + } + const auto narrow = (state.style == MembersRowStyle::Narrow); + if (!narrow && state.invited) { + st::groupCallMemberInvited.paintInCenter( + p, + QRect( + rect.topLeft() + st::groupCallMemberInvitedPosition, + st::groupCallMemberInvited.size())); + return; + } + const auto video = (state.style == MembersRowStyle::Video); + const auto &greenIcon = video + ? st::groupCallVideoCrossLine.icon + : narrow + ? st::groupCallNarrowColoredCrossLine.icon + : st::groupCallMemberColoredCrossLine.icon; const auto left = rect.x() + (rect.width() - greenIcon.width()) / 2; const auto top = rect.y() + (rect.height() - greenIcon.height()) / 2; if (state.speaking == 1. && !state.mutedByMe) { // Just green icon, no cross, no coloring. greenIcon.paintInCenter(p, rect); return; - } else if (state.speaking == 0.) { + } else if (state.speaking == 0. && (!narrow || !state.mutedByMe)) { if (state.active == 1.) { // Just gray icon, no cross, no coloring. - st::groupCallMemberInactiveCrossLine.icon.paintInCenter(p, rect); + const auto &grayIcon = video + ? st::groupCallVideoCrossLine.icon + : narrow + ? st::groupCallNarrowInactiveCrossLine.icon + : st::groupCallMemberInactiveCrossLine.icon; + grayIcon.paintInCenter(p, rect); return; } else if (state.active == 0.) { if (state.muted == 1.) { if (state.raisedHand) { - st::groupCallMemberRaisedHand.paintInCenter(p, rect); + (narrow + ? st::groupCallNarrowRaisedHand + : st::groupCallMemberRaisedHand).paintInCenter(p, rect); return; } // Red crossed icon, colorized once, cached as last frame. - _coloredCrossLine.paint( + auto &line = video + ? _videoCrossLine + : narrow + ? _coloredNarrowCrossLine + : _coloredCrossLine; + const auto color = video + ? std::nullopt + : std::make_optional(st::groupCallMemberMutedIcon->c); + line.paint( p, left, top, 1., - st::groupCallMemberMutedIcon->c); + color); return; } else if (state.muted == 0.) { // Gray crossed icon, no coloring, cached as last frame. - _inactiveCrossLine.paint(p, left, top, 1.); + auto &line = video + ? _videoCrossLine + : narrow + ? _inactiveNarrowCrossLine + : _inactiveCrossLine; + line.paint(p, left, top, 1.); return; } } } const auto activeInactiveColor = anim::color( - st::groupCallMemberInactiveIcon, - (state.mutedByMe + (narrow + ? st::groupCallMemberNotJoinedStatus + : st::groupCallMemberInactiveIcon), + (narrow + ? st::groupCallMemberActiveStatus + : state.mutedByMe ? st::groupCallMemberMutedIcon : st::groupCallMemberActiveIcon), state.speaking); @@ -1485,20 +1089,111 @@ void MembersController::rowPaintIcon( activeInactiveColor, st::groupCallMemberMutedIcon, state.muted); + const auto color = video + ? std::nullopt + : std::make_optional((narrow && state.mutedByMe) + ? st::groupCallMemberMutedIcon->c + : (narrow && state.raisedHand) + ? st::groupCallMemberInactiveStatus->c + : iconColor); // Don't use caching of the last frame, // because 'muted' may animate color. const auto crossProgress = std::min(1. - state.active, 0.9999); - _inactiveCrossLine.paint(p, left, top, crossProgress, iconColor); + auto &line = video + ? _videoCrossLine + : narrow + ? _inactiveNarrowCrossLine + : _inactiveCrossLine; + line.paint(p, left, top, crossProgress, color); } -auto MembersController::kickParticipantRequests() const +int Members::Controller::rowPaintStatusIcon( + Painter &p, + int x, + int y, + int outerWidth, + not_null row, + const IconState &state) { + Expects(state.style == MembersRowStyle::Narrow); + + if (_mode != PanelMode::Wide) { + return 0; + } + const auto &icon = st::groupCallNarrowColoredCrossLine.icon; + x += st::groupCallNarrowIconPosition.x(); + y += st::groupCallNarrowIconPosition.y(); + const auto rect = QRect(x, y, icon.width(), icon.height()); + rowPaintIcon(p, rect, state); + x += icon.width(); + auto result = st::groupCallNarrowIconSkip; + const auto participantPeer = row->peer(); + const auto camera = _cameraActive.contains(participantPeer); + const auto screen = _screenActive.contains(participantPeer); + if (camera || screen) { + const auto activeInactiveColor = anim::color( + st::groupCallMemberNotJoinedStatus, + st::groupCallMemberActiveStatus, + state.speaking); + const auto iconColor = anim::color( + activeInactiveColor, + st::groupCallMemberNotJoinedStatus, + state.muted); + const auto other = state.mutedByMe + ? st::groupCallMemberMutedIcon->c + : state.raisedHand + ? st::groupCallMemberInactiveStatus->c + : iconColor; + const auto color = (state.speaking == 1. && !state.mutedByMe) + ? st::groupCallMemberActiveStatus->c + : (state.speaking == 0. + ? (state.active == 1. + ? st::groupCallMemberNotJoinedStatus->c + : (state.active == 0. + ? (state.muted == 1. + ? (state.raisedHand + ? st::groupCallMemberInactiveStatus->c + : st::groupCallMemberNotJoinedStatus->c) + : (state.muted == 0. + ? st::groupCallMemberNotJoinedStatus->c + : other)) + : other)) + : other); + if (camera) { + st::groupCallNarrowCameraIcon.paint(p, x, y, outerWidth, other); + x += st::groupCallNarrowCameraIcon.width(); + result += st::groupCallNarrowCameraIcon.width(); + } + if (screen) { + st::groupCallNarrowScreenIcon.paint(p, x, y, outerWidth, other); + x += st::groupCallNarrowScreenIcon.width(); + result += st::groupCallNarrowScreenIcon.width(); + } + } + return result; +} + +bool Members::Controller::rowIsNarrow() { + return (_mode == PanelMode::Wide); +} + +void Members::Controller::rowShowContextMenu(not_null row) { + showRowMenu(row, false); +} + +auto Members::Controller::kickParticipantRequests() const -> rpl::producer>{ return _kickParticipantRequests.events(); } -void MembersController::rowClicked(not_null row) { - delegate()->peerListShowRowMenu(row, [=](not_null menu) { +void Members::Controller::rowClicked(not_null row) { + showRowMenu(row, true); +} + +void Members::Controller::showRowMenu( + not_null row, + bool highlightRow) { + const auto cleanup = [=](not_null menu) { if (!_menu || _menu.get() != menu) { return; } @@ -1509,15 +1204,16 @@ void MembersController::rowClicked(not_null row) { } } _menu = std::move(saved); - }); + }; + delegate()->peerListShowRowMenu(row, highlightRow, cleanup); } -void MembersController::rowActionClicked( +void Members::Controller::rowActionClicked( not_null row) { - rowClicked(row); + showRowMenu(row, true); } -base::unique_qptr MembersController::rowContextMenu( +base::unique_qptr Members::Controller::rowContextMenu( QWidget *parent, not_null row) { auto result = createRowContextMenu(parent, row); @@ -1534,17 +1230,15 @@ base::unique_qptr MembersController::rowContextMenu( return result; } -base::unique_qptr MembersController::createRowContextMenu( +base::unique_qptr Members::Controller::createRowContextMenu( QWidget *parent, not_null row) { const auto participantPeer = row->peer(); const auto real = static_cast(row.get()); - - auto result = base::make_unique_q( - parent, - st::groupCallPopupMenu); - const auto muteState = real->state(); + const auto muted = (muteState == Row::State::Muted) + || (muteState == Row::State::RaisedHand); + const auto addVolumeItem = !muted || isMe(participantPeer); const auto admin = IsGroupCallAdmin(_peer, participantPeer); const auto session = &_peer->session(); const auto getCurrentWindow = [=]() -> Window::SessionController* { @@ -1565,15 +1259,22 @@ base::unique_qptr MembersController::createRowContextMenu( } return getCurrentWindow(); }; + + auto result = base::make_unique_q( + parent, + (addVolumeItem + ? st::groupCallPopupMenuWithVolume + : st::groupCallPopupMenu)); + const auto weakMenu = Ui::MakeWeak(result.get()); const auto performOnMainWindow = [=](auto callback) { if (const auto window = getWindow()) { - if (_menu) { - _menu->discardParentReActivate(); + if (const auto menu = weakMenu.data()) { + menu->discardParentReActivate(); // We must hide PopupMenu before we activate the MainWindow, // otherwise we set focus in field inside MainWindow and then // PopupMenu::hide activates back the group call panel :( - _menu = nullptr; + delete weakMenu; } callback(window); window->widget()->activate(); @@ -1595,6 +1296,58 @@ base::unique_qptr MembersController::createRowContextMenu( _kickParticipantRequests.fire_copy(participantPeer); }); + if (const auto real = _call->lookupReal()) { + auto oneFound = false; + auto hasTwoOrMore = false; + const auto &shown = _call->shownVideoTracks(); + for (const auto &[endpoint, track] : _call->activeVideoTracks()) { + if (shown.contains(endpoint)) { + if (oneFound) { + hasTwoOrMore = true; + break; + } + oneFound = true; + } + } + const auto participant = real->participantByPeer(participantPeer); + if (participant && hasTwoOrMore) { + const auto &large = _call->videoEndpointLarge(); + const auto pinned = _call->videoEndpointPinned(); + const auto camera = VideoEndpoint{ + VideoEndpointType::Camera, + participantPeer, + computeCameraEndpoint(participant), + }; + const auto screen = VideoEndpoint{ + VideoEndpointType::Screen, + participantPeer, + computeScreenEndpoint(participant), + }; + if (shown.contains(camera)) { + if (pinned && large == camera) { + result->addAction( + tr::lng_group_call_context_unpin_camera(tr::now), + [=] { _call->pinVideoEndpoint({}); }); + } else { + result->addAction( + tr::lng_group_call_context_pin_camera(tr::now), + [=] { _call->pinVideoEndpoint(camera); }); + } + } + if (shown.contains(screen)) { + if (pinned && large == screen) { + result->addAction( + tr::lng_group_call_context_unpin_screen(tr::now), + [=] { _call->pinVideoEndpoint({}); }); + } else { + result->addAction( + tr::lng_group_call_context_pin_screen(tr::now), + [=] { _call->pinVideoEndpoint(screen); }); + } + } + } + } + if (real->ssrc() != 0 && (!isMe(participantPeer) || _peer->canManageGroupCall())) { addMuteActionsToContextMenu(result, participantPeer, admin, real); @@ -1635,7 +1388,8 @@ base::unique_qptr MembersController::createRowContextMenu( && chat->canBanMembers() && !chat->admins.contains(user)); } else if (const auto channel = _peer->asChannel()) { - return channel->canRestrictParticipant(participantPeer); + return !participantPeer->isMegagroup() // That's the creator. + && channel->canRestrictParticipant(participantPeer); } return false; }(); @@ -1652,21 +1406,19 @@ base::unique_qptr MembersController::createRowContextMenu( return result; } -void MembersController::addMuteActionsToContextMenu( +void Members::Controller::addMuteActionsToContextMenu( not_null menu, not_null participantPeer, bool participantIsCallAdmin, not_null row) { - const auto muteString = [=] { - return (_peer->canManageGroupCall() - ? tr::lng_group_call_context_mute - : tr::lng_group_call_context_mute_for_me)(tr::now); - }; - - const auto unmuteString = [=] { - return (_peer->canManageGroupCall() - ? tr::lng_group_call_context_unmute - : tr::lng_group_call_context_unmute_for_me)(tr::now); + const auto muteUnmuteString = [=](bool muted, bool mutedByMe) { + return (muted && _peer->canManageGroupCall()) + ? tr::lng_group_call_context_unmute(tr::now) + : mutedByMe + ? tr::lng_group_call_context_unmute_for_me(tr::now) + : _peer->canManageGroupCall() + ? tr::lng_group_call_context_mute(tr::now) + : tr::lng_group_call_context_mute_for_me(tr::now); }; const auto toggleMute = crl::guard(this, [=](bool mute, bool local) { @@ -1687,13 +1439,14 @@ void MembersController::addMuteActionsToContextMenu( }); const auto muteState = row->state(); - const auto isMuted = (muteState == Row::State::Muted) - || (muteState == Row::State::RaisedHand) - || (muteState == Row::State::MutedByMe); + const auto muted = (muteState == Row::State::Muted) + || (muteState == Row::State::RaisedHand); + const auto mutedByMe = row->mutedByMe(); auto mutesFromVolume = rpl::never() | rpl::type_erased(); - if (!isMuted || _call->joinAs() == participantPeer) { + const auto addVolumeItem = !muted || isMe(participantPeer); + if (addVolumeItem) { auto otherParticipantStateValue = _call->otherParticipantStateValue( ) | rpl::filter([=](const Group::ParticipantState &data) { @@ -1702,11 +1455,11 @@ void MembersController::addMuteActionsToContextMenu( auto volumeItem = base::make_unique_q( menu->menu(), - st::groupCallPopupMenu.menu, + st::groupCallPopupVolumeMenu, otherParticipantStateValue, row->volume(), Group::kMaxVolume, - isMuted); + muted); mutesFromVolume = volumeItem->toggleMuteRequests(); @@ -1741,7 +1494,15 @@ void MembersController::addMuteActionsToContextMenu( } }, volumeItem->lifetime()); + if (!menu->empty()) { + menu->addSeparator(); + } + menu->addAction(std::move(volumeItem)); + + if (!isMe(participantPeer)) { + menu->addSeparator(); + } }; const auto muteAction = [&]() -> QAction* { @@ -1749,47 +1510,56 @@ void MembersController::addMuteActionsToContextMenu( || isMe(participantPeer) || (muteState == Row::State::Inactive && participantIsCallAdmin - && _peer->canManageGroupCall()) - || (isMuted - && !_peer->canManageGroupCall() - && muteState != Row::State::MutedByMe)) { + && _peer->canManageGroupCall())) { return nullptr; } auto callback = [=] { const auto state = row->state(); const auto muted = (state == Row::State::Muted) - || (state == Row::State::RaisedHand) - || (state == Row::State::MutedByMe); - toggleMute(!muted, false); + || (state == Row::State::RaisedHand); + const auto mutedByMe = row->mutedByMe(); + toggleMute(!mutedByMe && (!_call->canManage() || !muted), false); }; return menu->addAction( - isMuted ? unmuteString() : muteString(), + muteUnmuteString(muted, mutedByMe), std::move(callback)); }(); if (muteAction) { std::move( mutesFromVolume - ) | rpl::start_with_next([=](bool muted) { - muteAction->setText(muted ? unmuteString() : muteString()); + ) | rpl::start_with_next([=](bool mutedFromVolume) { + const auto state = _call->canManage() + ? (mutedFromVolume + ? (row->raisedHandRating() + ? Row::State::RaisedHand + : Row::State::Muted) + : Row::State::Inactive) + : row->state(); + const auto muted = (state == Row::State::Muted) + || (state == Row::State::RaisedHand); + const auto mutedByMe = _call->canManage() + ? false + : mutedFromVolume; + muteAction->setText(muteUnmuteString(muted, mutedByMe)); }, menu->lifetime()); } } -std::unique_ptr MembersController::createRowForMe() { +std::unique_ptr Members::Controller::createRowForMe() { auto result = std::make_unique(this, _call->joinAs()); updateRow(result.get(), nullptr); return result; } -std::unique_ptr MembersController::createRow( - const Data::GroupCall::Participant &participant) { +std::unique_ptr Members::Controller::createRow( + const Data::GroupCallParticipant &participant) { auto result = std::make_unique(this, participant.peer); updateRow(result.get(), &participant); return result; } -std::unique_ptr MembersController::createInvitedRow( +std::unique_ptr Members::Controller::createInvitedRow( not_null participantPeer) { if (findRow(participantPeer)) { return nullptr; @@ -1799,61 +1569,77 @@ std::unique_ptr MembersController::createInvitedRow( return result; } -} // namespace - Members::Members( not_null parent, - not_null call) + not_null call, + PanelMode mode, + Ui::GL::Backend backend) : RpWidget(parent) , _call(call) -, _scroll(this, st::defaultSolidScroll) -, _listController(std::make_unique(call, parent)) { - setupAddMember(call); +, _mode(mode) +, _scroll(this) +, _listController(std::make_unique(call, parent, mode)) +, _layout(_scroll->setOwnedWidget( + object_ptr(_scroll.data()))) +, _videoWrap(_layout->add(object_ptr(_layout.get()))) +, _videoPlaceholder(std::make_unique(_videoWrap.get())) +, _viewport( + std::make_unique( + _videoWrap.get(), + PanelMode::Default, + backend)) { setupList(); + setupAddMember(call); setContent(_list); setupFakeRoundCorners(); _listController->setDelegate(static_cast(this)); + trackViewportGeometry(); +} + +Members::~Members() { + _viewport = nullptr; } auto Members::toggleMuteRequests() const -> rpl::producer { - return static_cast( - _listController.get())->toggleMuteRequests(); + return _listController->toggleMuteRequests(); } auto Members::changeVolumeRequests() const -> rpl::producer { - return static_cast( - _listController.get())->changeVolumeRequests(); + return _listController->changeVolumeRequests(); } auto Members::kickParticipantRequests() const -> rpl::producer> { - return static_cast( - _listController.get())->kickParticipantRequests(); + return _listController->kickParticipantRequests(); +} + +not_null Members::viewport() const { + return _viewport.get(); } int Members::desiredHeight() const { - const auto top = _addMember ? _addMember->height() : 0; - auto count = [&] { + const auto count = [&] { if (const auto real = _call->lookupReal()) { return real->fullCount(); } return 0; }(); const auto use = std::max(count, _list->fullRowsCount()); - return top - + (use * st::groupCallMembersList.item.height) + const auto single = st::groupCallMembersList.item.height; + const auto desired = (_layout->height() - _list->height()) + + (use * single) + (use ? st::lineWidth : 0); + return std::max(height(), desired); } rpl::producer Members::desiredHeightValue() const { - const auto controller = static_cast( - _listController.get()); return rpl::combine( heightValue(), _addMemberButton.value(), - controller->fullCountValue() + _listController->fullCountValue(), + _mode.value() ) | rpl::map([=] { return desiredHeight(); }); @@ -1882,55 +1668,180 @@ void Members::setupAddMember(not_null call) { }); } - _canAddMembers.value( - ) | rpl::start_with_next([=](bool can) { + rpl::combine( + _canAddMembers.value(), + _mode.value() + ) | rpl::start_with_next([=](bool can, PanelMode mode) { if (!can) { - _addMemberButton = nullptr; - _addMember.destroy(); - updateControlsGeometry(); + if (const auto old = _addMemberButton.current()) { + delete old; + _addMemberButton = nullptr; + updateControlsGeometry(); + } return; } - _addMember = Settings::CreateButton( - this, + auto addMember = Settings::CreateButton( + _layout.get(), tr::lng_group_call_invite(), st::groupCallAddMember, &st::groupCallAddMemberIcon, - st::groupCallAddMemberIconLeft); - _addMember->show(); - - _addMember->addClickHandler([=] { // TODO throttle(ripple duration) - _addMemberRequests.fire({}); - }); - _addMemberButton = _addMember.data(); - - resizeToList(); + st::groupCallAddMemberIconLeft, + &st::groupCallMemberInactiveIcon); + addMember->clicks( + ) | rpl::to_empty | rpl::start_to_stream( + _addMemberRequests, + addMember->lifetime()); + addMember->show(); + addMember->resizeToWidth(_layout->width()); + delete _addMemberButton.current(); + _addMemberButton = addMember.data(); + _layout->insert(3, std::move(addMember)); }, lifetime()); + + updateControlsGeometry(); +} + +Row *Members::lookupRow(not_null peer) const { + return _listController->findRow(peer); +} + +void Members::setMode(PanelMode mode) { + if (_mode.current() == mode) { + return; + } + _mode = mode; + _listController->setMode(mode); +} + +QRect Members::getInnerGeometry() const { + const auto addMembers = _addMemberButton.current(); + const auto add = addMembers ? addMembers->height() : 0; + return QRect( + 0, + -_scroll->scrollTop(), + width(), + _list->y() + _list->height() + _bottomSkip->height() + add); } rpl::producer Members::fullCountValue() const { - return static_cast( - _listController.get())->fullCountValue(); + return _listController->fullCountValue(); } void Members::setupList() { _listController->setStyleOverrides(&st::groupCallMembersList); - _list = _scroll->setOwnedWidget(object_ptr( - this, - _listController.get())); + const auto addSkip = [&] { + const auto result = _layout->add( + object_ptr( + _layout.get(), + st::groupCallMembersTopSkip)); + result->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(result).fillRect(clip, st::groupCallMembersBg); + }, result->lifetime()); + return result; + }; + _topSkip = addSkip(); + _list = _layout->add( + object_ptr( + _layout.get(), + _listController.get())); + _bottomSkip = addSkip(); - _list->heightValue( + using namespace rpl::mappers; + rpl::combine( + _list->heightValue() | rpl::map(_1 > 0), + _addMemberButton.value() | rpl::map(_1 != nullptr) + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool hasList, bool hasAddMembers) { + _topSkip->resize( + _topSkip->width(), + hasList ? st::groupCallMembersTopSkip : 0); + _bottomSkip->resize( + _bottomSkip->width(), + (hasList && !hasAddMembers) ? st::groupCallMembersTopSkip : 0); + }, _list->lifetime()); + + const auto skip = _layout->add(object_ptr(_layout.get())); + _mode.value( + ) | rpl::start_with_next([=](PanelMode mode) { + skip->resize(skip->width(), (mode == PanelMode::Default) + ? st::groupCallMembersBottomSkip + : 0); + }, skip->lifetime()); + + rpl::combine( + _mode.value(), + _layout->heightValue() ) | rpl::start_with_next([=] { resizeToList(); - }, _list->lifetime()); + }, _layout->lifetime()); rpl::combine( _scroll->scrollTopValue(), _scroll->heightValue() ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { - _list->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight); + _layout->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight); }, _scroll->lifetime()); +} - updateControlsGeometry(); +void Members::trackViewportGeometry() { + _call->videoEndpointLargeValue( + ) | rpl::start_with_next([=](const VideoEndpoint &large) { + _viewport->showLarge(large); + }, _viewport->lifetime()); + + const auto move = [=] { + const auto maxTop = _viewport->fullHeight() + - _viewport->widget()->height(); + if (maxTop < 0) { + return; + } + const auto scrollTop = _scroll->scrollTop(); + const auto shift = std::min(scrollTop, maxTop); + _viewport->setScrollTop(shift); + if (_viewport->widget()->y() != shift) { + _viewport->widget()->move(0, shift); + } + }; + const auto resize = [=] { + _viewport->widget()->resize( + _layout->width(), + std::min(_scroll->height(), _viewport->fullHeight())); + }; + _layout->widthValue( + ) | rpl::start_with_next([=](int width) { + _viewport->resizeToWidth(width); + resize(); + }, _viewport->lifetime()); + + _scroll->heightValue( + ) | rpl::skip(1) | rpl::start_with_next(resize, _viewport->lifetime()); + + _scroll->scrollTopValue( + ) | rpl::skip(1) | rpl::start_with_next(move, _viewport->lifetime()); + + rpl::combine( + _layout->widthValue(), + _call->hasNotShownVideoValue() + ) | rpl::start_with_next([=](int width, bool has) { + const auto height = has ? st::groupCallVideoPlaceholderHeight : 0; + _videoPlaceholder->setGeometry(0, 0, width, height); + }, _videoPlaceholder->lifetime()); + + SetupVideoPlaceholder(_videoPlaceholder.get(), _call->peer()); + + rpl::combine( + _videoPlaceholder->heightValue(), + _viewport->fullHeightValue() + ) | rpl::start_with_next([=](int placeholder, int viewport) { + _videoWrap->resize( + _videoWrap->width(), + std::max(placeholder, viewport)); + if (viewport > 0) { + move(); + resize(); + } + }, _viewport->lifetime()); } void Members::resizeEvent(QResizeEvent *e) { @@ -1941,11 +1852,8 @@ void Members::resizeToList() { if (!_list) { return; } - const auto listHeight = _list->height(); - const auto newHeight = (listHeight > 0) - ? ((_addMember ? _addMember->height() : 0) - + listHeight - + st::lineWidth) + const auto newHeight = (_list->height() > 0) + ? (_layout->height() + st::lineWidth) : 0; if (height() == newHeight) { updateControlsGeometry(); @@ -1955,17 +1863,8 @@ void Members::resizeToList() { } void Members::updateControlsGeometry() { - if (!_list) { - return; - } - auto topSkip = 0; - if (_addMember) { - _addMember->resizeToWidth(width()); - _addMember->move(0, 0); - topSkip = _addMember->height(); - } - _scroll->setGeometry(0, topSkip, width(), height() - topSkip); - _list->resizeToWidth(width()); + _scroll->setGeometry(rect()); + _layout->resizeToWidth(width()); } void Members::setupFakeRoundCorners() { @@ -1990,7 +1889,7 @@ void Members::setupFakeRoundCorners() { }; const auto create = [&](QPoint imagePartOrigin) { - const auto result = Ui::CreateChild(this); + const auto result = Ui::CreateChild(_layout.get()); result->show(); result->resize(size, size); result->setAttribute(Qt::WA_TransparentForMouseEvents); @@ -2010,14 +1909,29 @@ void Members::setupFakeRoundCorners() { const auto bottomleft = create({ 0, shift }); const auto bottomright = create({ shift, shift }); - sizeValue( - ) | rpl::start_with_next([=](QSize size) { - topleft->move(0, 0); - topright->move(size.width() - topright->width(), 0); - bottomleft->move(0, size.height() - bottomleft->height()); - bottomright->move( - size.width() - bottomright->width(), - size.height() - bottomright->height()); + rpl::combine( + _list->geometryValue(), + _addMemberButton.value() | rpl::map([=](Ui::RpWidget *widget) { + topleft->raise(); + topright->raise(); + bottomleft->raise(); + bottomright->raise(); + return widget ? widget->heightValue() : rpl::single(0); + }) | rpl::flatten_latest() + ) | rpl::start_with_next([=](QRect list, int addMembers) { + const auto left = list.x(); + const auto top = list.y() - _topSkip->height(); + const auto right = left + list.width() - topright->width(); + const auto bottom = top + + _topSkip->height() + + list.height() + + _bottomSkip->height() + + addMembers + - bottomleft->height(); + topleft->move(left, top); + topright->move(right, top); + bottomleft->move(left, bottom); + bottomright->move(right, bottom); }, lifetime()); refreshImage(); diff --git a/Telegram/SourceFiles/calls/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h similarity index 71% rename from Telegram/SourceFiles/calls/calls_group_members.h rename to Telegram/SourceFiles/calls/group/calls_group_members.h index 20064879c..198fdcaad 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -10,8 +10,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_box.h" namespace Ui { +class RpWidget; class ScrollArea; +class VerticalLayout; class SettingsButton; +namespace GL { +enum class Backend; +} // namespace GL } // namespace Ui namespace Data { @@ -24,8 +29,11 @@ class GroupCall; namespace Calls::Group { +class Viewport; +class MembersRow; struct VolumeRequest; struct MuteRequest; +enum class PanelMode; class Members final : public Ui::RpWidget @@ -33,8 +41,12 @@ class Members final public: Members( not_null parent, - not_null call); + not_null call, + PanelMode mode, + Ui::GL::Backend backend); + ~Members(); + [[nodiscard]] not_null viewport() const; [[nodiscard]] int desiredHeight() const; [[nodiscard]] rpl::producer desiredHeightValue() const override; [[nodiscard]] rpl::producer fullCountValue() const; @@ -48,7 +60,14 @@ public: return _addMemberRequests.events(); } + [[nodiscard]] MembersRow *lookupRow(not_null peer) const; + + void setMode(PanelMode mode); + [[nodiscard]] QRect getInnerGeometry() const; + private: + class Controller; + struct VideoTile; using ListWidget = PeerListContent; void resizeEvent(QResizeEvent *e) override; @@ -73,14 +92,21 @@ private: void setupList(); void setupFakeRoundCorners(); + void trackViewportGeometry(); void updateControlsGeometry(); const not_null _call; + rpl::variable _mode = PanelMode(); object_ptr _scroll; - std::unique_ptr _listController; - object_ptr _addMember = { nullptr }; - rpl::variable _addMemberButton = nullptr; - ListWidget *_list = { nullptr }; + std::unique_ptr _listController; + not_null _layout; + const not_null _videoWrap; + const std::unique_ptr _videoPlaceholder; + std::unique_ptr _viewport; + rpl::variable _addMemberButton = nullptr; + RpWidget *_topSkip = nullptr; + RpWidget *_bottomSkip = nullptr; + ListWidget *_list = nullptr; rpl::event_stream<> _addMemberRequests; rpl::variable _canAddMembers; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp new file mode 100644 index 000000000..21743fd03 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp @@ -0,0 +1,773 @@ +/* +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 "calls/group/calls_group_members_row.h" + +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_common.h" +#include "data/data_peer.h" +#include "data/data_group_call.h" +#include "ui/paint/arcs.h" +#include "ui/paint/blobs.h" +#include "ui/text/text_options.h" +#include "ui/effects/ripple_animation.h" +#include "lang/lang_keys.h" +#include "webrtc/webrtc_video_track.h" +#include "styles/style_calls.h" + +namespace Calls::Group { +namespace { + +constexpr auto kLevelDuration = 100. + 500. * 0.23; +constexpr auto kBlobScale = 0.605; +constexpr auto kMinorBlobFactor = 0.9f; +constexpr auto kUserpicMinScale = 0.8; +constexpr auto kMaxLevel = 1.; +constexpr auto kWideScale = 5; + +constexpr auto kArcsStrokeRatio = 0.8; + +const auto kSpeakerThreshold = std::vector{ + Group::kDefaultVolume * 0.1f / Group::kMaxVolume, + Group::kDefaultVolume * 0.9f / Group::kMaxVolume }; + +auto RowBlobs() -> std::array { + return { { + { + .segmentsCount = 6, + .minScale = kBlobScale * kMinorBlobFactor, + .minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor, + .maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor, + .speedScale = 1., + .alpha = .5, + }, + { + .segmentsCount = 8, + .minScale = kBlobScale, + .minRadius = (float)st::groupCallRowBlobMinRadius, + .maxRadius = (float)st::groupCallRowBlobMaxRadius, + .speedScale = 1., + .alpha = .2, + }, + } }; +} + +[[nodiscard]] QString StatusPercentString(float volume) { + return QString::number(int(std::round(volume * 200))) + '%'; +} + +[[nodiscard]] int StatusPercentWidth(const QString &percent) { + return st::normalFont->width(percent); +} + +} // namespace + +struct MembersRow::BlobsAnimation { + BlobsAnimation( + std::vector blobDatas, + float levelDuration, + float maxLevel); + + Ui::Paint::Blobs blobs; + crl::time lastTime = 0; + crl::time lastSoundingUpdateTime = 0; + float64 enter = 0.; + + QImage userpicCache; + InMemoryKey userpicKey; + + rpl::lifetime lifetime; +}; + +struct MembersRow::StatusIcon { + StatusIcon(bool shown, float volume); + + const style::icon &speaker; + Ui::Paint::ArcsAnimation arcs; + Ui::Animations::Simple arcsAnimation; + Ui::Animations::Simple shownAnimation; + QString percent; + int percentWidth = 0; + int arcsWidth = 0; + int wasArcsWidth = 0; + bool shown = true; + + rpl::lifetime lifetime; +}; + +MembersRow::BlobsAnimation::BlobsAnimation( + std::vector blobDatas, + float levelDuration, + float maxLevel) +: blobs(std::move(blobDatas), levelDuration, maxLevel) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + userpicCache = QImage(); + }, lifetime); +} + +MembersRow::StatusIcon::StatusIcon(bool shown, float volume) +: speaker(st::groupCallStatusSpeakerIcon) +, arcs( + st::groupCallStatusSpeakerArcsAnimation, + kSpeakerThreshold, + volume, + Ui::Paint::ArcsAnimation::Direction::Right) +, percent(StatusPercentString(volume)) +, percentWidth(StatusPercentWidth(percent)) +, shown(shown) { +} + +MembersRow::MembersRow( + not_null delegate, + not_null participantPeer) +: PeerListRow(participantPeer) +, _delegate(delegate) +, _sounding(false) +, _speaking(false) +, _raisedHandStatus(false) +, _skipLevelUpdate(false) +, _mutedByMe(false) { + refreshStatus(); + _aboutText = participantPeer->about(); +} + +MembersRow::~MembersRow() = default; + +void MembersRow::setSkipLevelUpdate(bool value) { + _skipLevelUpdate = value; +} + +void MembersRow::updateState( + const Data::GroupCallParticipant *participant) { + setSsrc(participant ? participant->ssrc : 0); + setVolume(participant + ? participant->volume + : Group::kDefaultVolume); + if (!participant) { + setState(State::Invited); + setSounding(false); + setSpeaking(false); + _mutedByMe = false; + _raisedHandRating = 0; + } else if (!participant->muted + || (participant->sounding && participant->ssrc != 0)) { + setState(State::Active); + setSounding(participant->sounding && participant->ssrc != 0); + setSpeaking(participant->speaking && participant->ssrc != 0); + _mutedByMe = participant->mutedByMe; + _raisedHandRating = 0; + } else if (participant->canSelfUnmute) { + setState(State::Inactive); + setSounding(false); + setSpeaking(false); + _mutedByMe = participant->mutedByMe; + _raisedHandRating = 0; + } else { + setSounding(false); + setSpeaking(false); + _mutedByMe = participant->mutedByMe; + _raisedHandRating = participant->raisedHandRating; + setState(_raisedHandRating ? State::RaisedHand : State::Muted); + } + refreshStatus(); +} + +void MembersRow::setSpeaking(bool speaking) { + if (_speaking == speaking) { + return; + } + _speaking = speaking; + _speakingAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + _speaking ? 0. : 1., + _speaking ? 1. : 0., + st::widgetFadeDuration); + + if (!_speaking + || _mutedByMe + || (_state == State::Muted) + || (_state == State::RaisedHand)) { + if (_statusIcon) { + _statusIcon = nullptr; + _delegate->rowUpdateRow(this); + } + } else if (!_statusIcon) { + _statusIcon = std::make_unique( + (_volume != Group::kDefaultVolume), + (float)_volume / Group::kMaxVolume); + _statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio); + _statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth(); + _statusIcon->arcs.startUpdateRequests( + ) | rpl::start_with_next([=] { + if (!_statusIcon->arcsAnimation.animating()) { + _statusIcon->wasArcsWidth = _statusIcon->arcsWidth; + } + auto callback = [=](float64 value) { + _statusIcon->arcs.update(crl::now()); + _statusIcon->arcsWidth = anim::interpolate( + _statusIcon->wasArcsWidth, + _statusIcon->arcs.finishedWidth(), + value); + _delegate->rowUpdateRow(this); + }; + _statusIcon->arcsAnimation.start( + std::move(callback), + 0., + 1., + st::groupCallSpeakerArcsAnimation.duration); + }, _statusIcon->lifetime); + } +} + +void MembersRow::setSounding(bool sounding) { + if (_sounding == sounding) { + return; + } + _sounding = sounding; + if (!_sounding) { + _blobsAnimation = nullptr; + } else if (!_blobsAnimation) { + _blobsAnimation = std::make_unique( + RowBlobs() | ranges::to_vector, + kLevelDuration, + kMaxLevel); + _blobsAnimation->lastTime = crl::now(); + updateLevel(GroupCall::kSpeakLevelThreshold); + } +} + +void MembersRow::clearRaisedHandStatus() { + if (!_raisedHandStatus) { + return; + } + _raisedHandStatus = false; + refreshStatus(); + _delegate->rowUpdateRow(this); +} + +void MembersRow::setState(State state) { + if (_state == state) { + return; + } + const auto wasActive = (_state == State::Active); + const auto wasMuted = (_state == State::Muted) + || (_state == State::RaisedHand); + const auto wasRaisedHand = (_state == State::RaisedHand); + _state = state; + const auto nowActive = (_state == State::Active); + const auto nowMuted = (_state == State::Muted) + || (_state == State::RaisedHand); + const auto nowRaisedHand = (_state == State::RaisedHand); + if (!wasRaisedHand && nowRaisedHand) { + _raisedHandStatus = true; + _delegate->rowScheduleRaisedHandStatusRemove(this); + } + if (nowActive != wasActive) { + _activeAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + nowActive ? 0. : 1., + nowActive ? 1. : 0., + st::widgetFadeDuration); + } + if (nowMuted != wasMuted) { + _mutedAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + nowMuted ? 0. : 1., + nowMuted ? 1. : 0., + st::widgetFadeDuration); + } +} + +void MembersRow::setSsrc(uint32 ssrc) { + _ssrc = ssrc; +} + +void MembersRow::setVolume(int volume) { + _volume = volume; + if (_statusIcon) { + const auto floatVolume = (float)volume / Group::kMaxVolume; + _statusIcon->arcs.setValue(floatVolume); + _statusIcon->percent = StatusPercentString(floatVolume); + _statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent); + + const auto shown = (volume != Group::kDefaultVolume); + if (_statusIcon->shown != shown) { + _statusIcon->shown = shown; + _statusIcon->shownAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + shown ? 0. : 1., + shown ? 1. : 0., + st::groupCallSpeakerArcsAnimation.duration); + } + } +} + +void MembersRow::updateLevel(float level) { + Expects(_blobsAnimation != nullptr); + + const auto spoke = (level >= GroupCall::kSpeakLevelThreshold) + ? crl::now() + : crl::time(); + if (spoke && _speaking) { + _speakingLastTime = spoke; + } + + if (_skipLevelUpdate) { + return; + } + + if (spoke) { + _blobsAnimation->lastSoundingUpdateTime = spoke; + } + _blobsAnimation->blobs.setLevel(level); +} + +void MembersRow::updateBlobAnimation(crl::time now) { + Expects(_blobsAnimation != nullptr); + + const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime + + Data::GroupCall::kSoundStatusKeptFor; + const auto soundingStartsFinishing = soundingFinishesAt + - kBlobsEnterDuration; + const auto soundingFinishes = (soundingStartsFinishing < now); + if (soundingFinishes) { + _blobsAnimation->enter = std::clamp( + (soundingFinishesAt - now) / float64(kBlobsEnterDuration), + 0., + 1.); + } else if (_blobsAnimation->enter < 1.) { + _blobsAnimation->enter = std::clamp( + (_blobsAnimation->enter + + ((now - _blobsAnimation->lastTime) + / float64(kBlobsEnterDuration))), + 0., + 1.); + } + _blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime); + _blobsAnimation->lastTime = now; +} + +void MembersRow::ensureUserpicCache( + std::shared_ptr &view, + int size) { + Expects(_blobsAnimation != nullptr); + + const auto user = peer(); + const auto key = user->userpicUniqueKey(view); + const auto full = QSize(size, size) * kWideScale * cIntRetinaFactor(); + auto &cache = _blobsAnimation->userpicCache; + if (cache.isNull()) { + cache = QImage(full, QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(cRetinaFactor()); + } else if (_blobsAnimation->userpicKey == key + && cache.size() == full) { + return; + } + _blobsAnimation->userpicKey = key; + cache.fill(Qt::transparent); + { + Painter p(&cache); + const auto skip = (kWideScale - 1) / 2 * size; + user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size); + } +} + +void MembersRow::paintBlobs( + Painter &p, + int x, + int y, + int sizew, + int sizeh, + PanelMode mode) { + if (!_blobsAnimation) { + return; + } + auto size = sizew; + const auto shift = QPointF(x + size / 2., y + size / 2.); + auto hq = PainterHighQualityEnabler(p); + p.translate(shift); + const auto brush = _mutedByMe + ? st::groupCallMemberMutedIcon->b + : anim::brush( + st::groupCallMemberInactiveStatus, + st::groupCallMemberActiveStatus, + _speakingAnimation.value(_speaking ? 1. : 0.)); + _blobsAnimation->blobs.paint(p, brush); + p.translate(-shift); + p.setOpacity(1.); +} + +void MembersRow::paintScaledUserpic( + Painter &p, + std::shared_ptr &userpic, + int x, + int y, + int outerWidth, + int sizew, + int sizeh, + PanelMode mode) { + auto size = sizew; + if (!_blobsAnimation) { + peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + return; + } + const auto enter = _blobsAnimation->enter; + const auto &minScale = kUserpicMinScale; + const auto scaleUserpic = minScale + + (1. - minScale) * _blobsAnimation->blobs.currentLevel(); + const auto scale = scaleUserpic * enter + 1. * (1. - enter); + if (scale == 1.) { + peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + return; + } + ensureUserpicCache(userpic, size); + + PainterHighQualityEnabler hq(p); + + auto target = QRect( + x + (1 - kWideScale) / 2 * size, + y + (1 - kWideScale) / 2 * size, + kWideScale * size, + kWideScale * size); + auto shrink = anim::interpolate( + (1 - kWideScale) / 2 * size, + 0, + scale); + auto margins = QMargins(shrink, shrink, shrink, shrink); + p.drawImage( + target.marginsAdded(margins), + _blobsAnimation->userpicCache); +} + +void MembersRow::paintMuteIcon( + Painter &p, + QRect iconRect, + MembersRowStyle style) { + _delegate->rowPaintIcon(p, iconRect, computeIconState(style)); +} + +auto MembersRow::generatePaintUserpicCallback() -> PaintRoundImageCallback { + return [=](Painter &p, int x, int y, int outerWidth, int size) { + const auto outer = outerWidth; + paintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default); + }; +} + +void MembersRow::paintComplexUserpic( + Painter &p, + int x, + int y, + int outerWidth, + int sizew, + int sizeh, + PanelMode mode, + bool selected) { + paintBlobs(p, x, y, sizew, sizeh, mode); + paintScaledUserpic( + p, + ensureUserpicView(), + x, + y, + outerWidth, + sizew, + sizeh, + mode); +} + +int MembersRow::statusIconWidth(bool skipIcon) const { + if (!_statusIcon || !_speaking) { + return 0; + } + const auto shown = _statusIcon->shownAnimation.value( + _statusIcon->shown ? 1. : 0.); + const auto iconWidth = skipIcon + ? 0 + : (_statusIcon->speaker.width() + _statusIcon->arcsWidth); + const auto full = iconWidth + + _statusIcon->percentWidth + + st::normalFont->spacew; + return int(std::round(shown * full)); +} + +int MembersRow::statusIconHeight() const { + return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0; +} + +void MembersRow::paintStatusIcon( + Painter &p, + int x, + int y, + const style::PeerListItem &st, + const style::font &font, + bool selected, + bool skipIcon) { + if (!_statusIcon) { + return; + } + const auto shown = _statusIcon->shownAnimation.value( + _statusIcon->shown ? 1. : 0.); + if (shown == 0.) { + return; + } + + p.setFont(font); + const auto color = (_speaking + ? st.statusFgActive + : (selected ? st.statusFgOver : st.statusFg))->c; + p.setPen(color); + + const auto speakerRect = QRect( + QPoint(x, y + (font->height - statusIconHeight()) / 2), + _statusIcon->speaker.size()); + const auto arcPosition = speakerRect.topLeft() + + QPoint( + speakerRect.width() - st::groupCallStatusSpeakerArcsSkip, + speakerRect.height() / 2); + const auto iconWidth = skipIcon + ? 0 + : (speakerRect.width() + _statusIcon->arcsWidth); + const auto fullWidth = iconWidth + + _statusIcon->percentWidth + + st::normalFont->spacew; + + p.save(); + if (shown < 1.) { + const auto centerx = speakerRect.x() + fullWidth / 2; + const auto centery = speakerRect.y() + speakerRect.height() / 2; + p.translate(centerx, centery); + p.scale(shown, shown); + p.translate(-centerx, -centery); + } + if (!skipIcon) { + _statusIcon->speaker.paint( + p, + speakerRect.topLeft(), + speakerRect.width(), + color); + p.translate(arcPosition); + _statusIcon->arcs.paint(p, color); + p.translate(-arcPosition); + } + p.setFont(st::normalFont); + p.setPen(st.statusFgActive); + p.drawTextLeft( + x + iconWidth, + y, + fullWidth, + _statusIcon->percent); + p.restore(); +} + +void MembersRow::setAbout(const QString &about) { + if (_aboutText == about) { + return; + } + _aboutText = about; + _delegate->rowUpdateRow(this); +} + +void MembersRow::paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) { + paintComplexStatusText( + p, + st, + x, + y, + availableWidth, + outerWidth, + selected, + MembersRowStyle::Default); +} + +void MembersRow::paintComplexStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected, + MembersRowStyle style) { + const auto skip = (style == MembersRowStyle::Default) + ? _delegate->rowPaintStatusIcon( + p, + x, + y, + outerWidth, + this, + computeIconState(MembersRowStyle::Narrow)) + : 0; + const auto narrowMode = (skip > 0); + x += skip; + availableWidth -= skip; + const auto &font = st::normalFont; + const auto about = (style == MembersRowStyle::Video) + ? QString() + : ((_state == State::RaisedHand && !_raisedHandStatus) + || (_state != State::RaisedHand && !_speaking)) + ? _aboutText + : QString(); + if (about.isEmpty() + && _state != State::Invited + && !_mutedByMe) { + paintStatusIcon(p, x, y, st, font, selected, narrowMode); + + const auto translatedWidth = statusIconWidth(narrowMode); + p.translate(translatedWidth, 0); + const auto guard = gsl::finally([&] { + p.translate(-translatedWidth, 0); + }); + + const auto &style = (!narrowMode + || (_state == State::RaisedHand && _raisedHandStatus)) + ? st + : st::groupCallNarrowMembersListItem; + PeerListRow::paintStatusText( + p, + style, + x, + y, + availableWidth - translatedWidth, + outerWidth, + selected); + return; + } + p.setFont(font); + if (style == MembersRowStyle::Video) { + p.setPen(st::groupCallVideoSubTextFg); + } else if (_mutedByMe) { + p.setPen(st::groupCallMemberMutedIcon); + } else { + p.setPen(st::groupCallMemberNotJoinedStatus); + } + p.drawTextLeft( + x, + y, + outerWidth, + (_mutedByMe + ? tr::lng_group_call_muted_by_me_status(tr::now) + : !about.isEmpty() + ? font->m.elidedText(about, Qt::ElideRight, availableWidth) + : _delegate->rowIsMe(peer()) + ? tr::lng_status_connecting(tr::now) + : tr::lng_group_call_invited_status(tr::now))); +} + +QSize MembersRow::actionSize() const { + return _delegate->rowIsNarrow() ? QSize() : QSize( + st::groupCallActiveButton.width, + st::groupCallActiveButton.height); +} + +bool MembersRow::actionDisabled() const { + return _delegate->rowIsMe(peer()) + || (_state == State::Invited) + || !_delegate->rowCanMuteMembers(); +} + +QMargins MembersRow::actionMargins() const { + return QMargins( + 0, + 0, + st::groupCallMemberButtonSkip, + 0); +} + +void MembersRow::paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + auto size = actionSize(); + const auto iconRect = style::rtlrect( + x, + y, + size.width(), + size.height(), + outerWidth); + if (_state == State::Invited) { + _actionRipple = nullptr; + } + if (_actionRipple) { + _actionRipple->paint( + p, + x + st::groupCallActiveButton.rippleAreaPosition.x(), + y + st::groupCallActiveButton.rippleAreaPosition.y(), + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + paintMuteIcon(p, iconRect); +} + +MembersRowDelegate::IconState MembersRow::computeIconState( + MembersRowStyle style) const { + const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.); + const auto active = _activeAnimation.value( + (_state == State::Active) ? 1. : 0.); + const auto muted = _mutedAnimation.value( + (_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.); + return { + .speaking = speaking, + .active = active, + .muted = muted, + .mutedByMe = _mutedByMe, + .raisedHand = (_state == State::RaisedHand), + .invited = (_state == State::Invited), + .style = style, + }; +} + +void MembersRow::showContextMenu() { + return _delegate->rowShowContextMenu(this); +} + +void MembersRow::refreshStatus() { + setCustomStatus( + (_speaking + ? tr::lng_group_call_active(tr::now) + : _raisedHandStatus + ? tr::lng_group_call_raised_hand_status(tr::now) + : tr::lng_group_call_inactive(tr::now)), + _speaking); +} + +void MembersRow::addActionRipple(QPoint point, Fn updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::ellipseMask(QSize( + st::groupCallActiveButton.rippleAreaSize, + st::groupCallActiveButton.rippleAreaSize)); + _actionRipple = std::make_unique( + st::groupCallActiveButton.ripple, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition); +} + +void MembersRow::refreshName(const style::PeerListItem &st) { + PeerListRow::refreshName(st); + //_narrowName = Ui::Text::String(); +} + +void MembersRow::stopLastActionRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.h b/Telegram/SourceFiles/calls/group/calls_group_members_row.h new file mode 100644 index 000000000..314b86703 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.h @@ -0,0 +1,225 @@ +/* +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 "boxes/peer_list_box.h" +#include "calls/group/calls_group_common.h" + +class PeerData; +class Painter; + +namespace Data { +struct GroupCallParticipant; +} // namespace Data + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace Calls::Group { + +enum class MembersRowStyle { + Default, + Narrow, + Video, +}; + +class MembersRow; +class MembersRowDelegate { +public: + struct IconState { + float64 speaking = 0.; + float64 active = 0.; + float64 muted = 0.; + bool mutedByMe = false; + bool raisedHand = false; + bool invited = false; + MembersRowStyle style = MembersRowStyle::Default; + }; + virtual bool rowIsMe(not_null participantPeer) = 0; + virtual bool rowCanMuteMembers() = 0; + virtual void rowUpdateRow(not_null row) = 0; + virtual void rowScheduleRaisedHandStatusRemove( + not_null row) = 0; + virtual void rowPaintIcon( + Painter &p, + QRect rect, + const IconState &state) = 0; + virtual int rowPaintStatusIcon( + Painter &p, + int x, + int y, + int outerWidth, + not_null row, + const IconState &state) = 0; + virtual bool rowIsNarrow() = 0; + virtual void rowShowContextMenu(not_null row) = 0; +}; + +class MembersRow final : public PeerListRow { +public: + MembersRow( + not_null delegate, + not_null participantPeer); + ~MembersRow(); + + enum class State { + Active, + Inactive, + Muted, + RaisedHand, + Invited, + }; + + void setAbout(const QString &about); + void setSkipLevelUpdate(bool value); + void updateState(const Data::GroupCallParticipant *participant); + void updateLevel(float level); + void updateBlobAnimation(crl::time now); + void clearRaisedHandStatus(); + [[nodiscard]] State state() const { + return _state; + } + [[nodiscard]] uint32 ssrc() const { + return _ssrc; + } + [[nodiscard]] bool sounding() const { + return _sounding; + } + [[nodiscard]] bool speaking() const { + return _speaking; + } + [[nodiscard]] bool mutedByMe() const { + return _mutedByMe; + } + [[nodiscard]] crl::time speakingLastTime() const { + return _speakingLastTime; + } + [[nodiscard]] int volume() const { + return _volume; + } + [[nodiscard]] uint64 raisedHandRating() const { + return _raisedHandRating; + } + + void addActionRipple(QPoint point, Fn updateCallback) override; + void stopLastActionRipple() override; + + void refreshName(const style::PeerListItem &st) override; + + QSize actionSize() const override; + bool actionDisabled() const override; + QMargins actionMargins() const override; + void paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + + PaintRoundImageCallback generatePaintUserpicCallback() override; + void paintComplexUserpic( + Painter &p, + int x, + int y, + int outerWidth, + int sizew, + int sizeh, + PanelMode mode, + bool selected = false); + + void paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) override; + void paintComplexStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected, + MembersRowStyle style); + void paintMuteIcon( + Painter &p, + QRect iconRect, + MembersRowStyle style = MembersRowStyle::Default); + [[nodiscard]] MembersRowDelegate::IconState computeIconState( + MembersRowStyle style = MembersRowStyle::Default) const; + + void showContextMenu(); + +private: + struct BlobsAnimation; + struct StatusIcon; + + int statusIconWidth(bool skipIcon) const; + int statusIconHeight() const; + void paintStatusIcon( + Painter &p, + int x, + int y, + const style::PeerListItem &st, + const style::font &font, + bool selected, + bool skipIcon); + + void refreshStatus() override; + void setSounding(bool sounding); + void setSpeaking(bool speaking); + void setState(State state); + void setSsrc(uint32 ssrc); + void setVolume(int volume); + + void ensureUserpicCache( + std::shared_ptr &view, + int size); + void paintBlobs( + Painter &p, + int x, + int y, + int sizew, + int sizeh, PanelMode mode); + void paintScaledUserpic( + Painter &p, + std::shared_ptr &userpic, + int x, + int y, + int outerWidth, + int sizew, + int sizeh, + PanelMode mode); + + const not_null _delegate; + State _state = State::Inactive; + std::unique_ptr _actionRipple; + std::unique_ptr _blobsAnimation; + std::unique_ptr _statusIcon; + Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon. + Ui::Animations::Simple _mutedAnimation; // For gray/red icon. + Ui::Animations::Simple _activeAnimation; // For icon cross animation. + QString _aboutText; + crl::time _speakingLastTime = 0; + uint64 _raisedHandRating = 0; + uint32 _ssrc = 0; + int _volume = Group::kDefaultVolume; + bool _sounding : 1; + bool _speaking : 1; + bool _raisedHandStatus : 1; + bool _skipLevelUpdate : 1; + bool _mutedByMe : 1; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_group_menu.cpp b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp similarity index 94% rename from Telegram/SourceFiles/calls/calls_group_menu.cpp rename to Telegram/SourceFiles/calls/group/calls_group_menu.cpp index 3887503bc..2310b1c9d 100644 --- a/Telegram/SourceFiles/calls/calls_group_menu.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_menu.cpp @@ -5,11 +5,11 @@ 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 "calls/calls_group_menu.h" +#include "calls/group/calls_group_menu.h" -#include "calls/calls_group_call.h" -#include "calls/calls_group_settings.h" -#include "calls/calls_group_panel.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_settings.h" +#include "calls/group/calls_group_panel.h" #include "data/data_peer.h" #include "data/data_group_call.h" #include "info/profile/info_profile_values.h" // Info::Profile::NameValue. @@ -78,10 +78,6 @@ void StartGroupCallRecordingBox( }); box->addButton(tr::lng_group_call_recording_start_button(), [=] { const auto result = input->getLastText().trimmed(); - if (result.isEmpty()) { - input->showError(); - return; - } box->closeBox(); done(result); }); @@ -590,7 +586,9 @@ void FillMenu( not_null menu, not_null peer, not_null call, + bool wide, Fn chooseJoinAs, + Fn chooseShareScreenSource, Fn)> showBox) { const auto weak = base::make_weak(call.get()); const auto resolveReal = [=] { @@ -606,8 +604,10 @@ void FillMenu( } const auto addEditJoinAs = call->showChooseJoinAs(); - const auto addEditTitle = peer->canManageGroupCall(); - const auto addEditRecording = peer->canManageGroupCall() + const auto addEditTitle = call->canManage(); + const auto addEditRecording = call->canManage() && !real->scheduleDate(); + const auto addScreenCast = !wide + && call->videoIsWorking() && !real->scheduleDate(); if (addEditJoinAs) { menu->addAction(MakeJoinAsAction( @@ -660,6 +660,23 @@ void FillMenu( real->recordStartDateValue(), handler)); } + if (addScreenCast) { + const auto sharing = call->isSharingScreen(); + const auto toggle = [=] { + if (const auto strong = weak.get()) { + if (sharing) { + strong->toggleScreenSharing(std::nullopt); + } else { + chooseShareScreenSource(); + } + } + }; + menu->addAction( + (call->isSharingScreen() + ? tr::lng_group_call_screen_share_stop(tr::now) + : tr::lng_group_call_screen_share_start(tr::now)), + toggle); + } menu->addAction(tr::lng_group_call_settings(tr::now), [=] { if (const auto strong = weak.get()) { showBox(Box(SettingsBox, strong)); @@ -677,8 +694,12 @@ void FillMenu( menu->addAction(MakeAttentionAction( menu->menu(), (real->scheduleDate() - ? tr::lng_group_call_cancel(tr::now) - : tr::lng_group_call_end(tr::now)), + ? (call->canManage() + ? tr::lng_group_call_cancel(tr::now) + : tr::lng_group_call_leave(tr::now)) + : (call->canManage() + ? tr::lng_group_call_end(tr::now) + : tr::lng_group_call_leave(tr::now))), finish)); } diff --git a/Telegram/SourceFiles/calls/calls_group_menu.h b/Telegram/SourceFiles/calls/group/calls_group_menu.h similarity index 96% rename from Telegram/SourceFiles/calls/calls_group_menu.h rename to Telegram/SourceFiles/calls/group/calls_group_menu.h index 484974493..0cb70f3ee 100644 --- a/Telegram/SourceFiles/calls/calls_group_menu.h +++ b/Telegram/SourceFiles/calls/group/calls_group_menu.h @@ -61,7 +61,9 @@ void FillMenu( not_null menu, not_null peer, not_null call, + bool wide, Fn chooseJoinAs, + Fn chooseShareScreenSource, Fn)> showBox); [[nodiscard]] base::unique_qptr MakeAttentionAction( diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp new file mode 100644 index 000000000..600c2c913 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -0,0 +1,2228 @@ +/* +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 "calls/group/calls_group_panel.h" + +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_members.h" +#include "calls/group/calls_group_settings.h" +#include "calls/group/calls_group_menu.h" +#include "calls/group/calls_group_viewport.h" +#include "calls/group/calls_group_toasts.h" +#include "calls/group/calls_group_invite_controller.h" +#include "calls/group/ui/calls_group_scheduled_labels.h" +#include "calls/group/ui/desktop_capture_choose_source.h" +#include "ui/platform/ui_platform_window_title.h" +#include "ui/platform/ui_platform_utility.h" +#include "ui/controls/call_mute_button.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/call_button.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/input_fields.h" +#include "ui/widgets/tooltip.h" +#include "ui/widgets/window.h" +#include "ui/chat/group_call_bar.h" +#include "ui/layers/layer_manager.h" +#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/round_rect.h" +#include "ui/special_buttons.h" +#include "info/profile/info_profile_values.h" // Info::Profile::Value. +#include "core/application.h" +#include "lang/lang_keys.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_user.h" +#include "data/data_group_call.h" +#include "data/data_session.h" +#include "data/data_changes.h" +#include "main/main_session.h" +#include "base/event_filter.h" +#include "base/unixtime.h" +#include "base/qt_signal_producer.h" +#include "base/timer_rpl.h" +#include "app.h" +#include "apiwrap.h" // api().kickParticipant. +#include "webrtc/webrtc_video_track.h" +#include "webrtc/webrtc_media_devices.h" // UniqueDesktopCaptureSource. +#include "webrtc/webrtc_audio_input_tester.h" +#include "styles/style_calls.h" +#include "styles/style_layers.h" + +#include +#include +#include +#include + +namespace Calls::Group { +namespace { + +constexpr auto kSpacePushToTalkDelay = crl::time(250); +constexpr auto kRecordingAnimationDuration = crl::time(1200); +constexpr auto kRecordingOpacity = 0.6; +constexpr auto kStartNoConfirmation = TimeId(10); +constexpr auto kControlsBackgroundOpacity = 0.8; +constexpr auto kOverrideActiveColorBgAlpha = 172; +constexpr auto kMicrophoneTooltipAfterLoudCount = 3; +constexpr auto kDropLoudAfterQuietCount = 5; +constexpr auto kMicrophoneTooltipLevelThreshold = 0.2; +constexpr auto kMicrophoneTooltipCheckInterval = crl::time(500); + +} // namespace + +struct Panel::ControlsBackgroundNarrow { + explicit ControlsBackgroundNarrow(not_null parent) + : shadow(parent) + , blocker(parent) { + } + + Ui::RpWidget shadow; + Ui::RpWidget blocker; +}; + +class Panel::MicLevelTester final { +public: + explicit MicLevelTester(Fn show); + + [[nodiscard]] bool showTooltip() const; + +private: + void check(); + + Fn _show; + base::Timer _timer; + Webrtc::AudioInputTester _tester; + int _loudCount = 0; + int _quietCount = 0; + +}; + +Panel::MicLevelTester::MicLevelTester(Fn show) +: _show(std::move(show)) +, _timer([=] { check(); }) +, _tester( + Core::App().settings().callAudioBackend(), + Core::App().settings().callInputDeviceId()) { + _timer.callEach(kMicrophoneTooltipCheckInterval); +} + +bool Panel::MicLevelTester::showTooltip() const { + return (_loudCount >= kMicrophoneTooltipAfterLoudCount); +} + +void Panel::MicLevelTester::check() { + const auto level = _tester.getAndResetLevel(); + if (level >= kMicrophoneTooltipLevelThreshold) { + _quietCount = 0; + if (++_loudCount >= kMicrophoneTooltipAfterLoudCount) { + _show(); + } + } else if (_loudCount > 0 && ++_quietCount >= kDropLoudAfterQuietCount) { + _quietCount = 0; + _loudCount = 0; + } +} + +Panel::Panel(not_null call) +: _call(call) +, _peer(call->peer()) +, _layerBg(std::make_unique(widget())) +#ifndef Q_OS_MAC +, _controls(std::make_unique( + widget(), + st::groupCallTitle)) +#endif // !Q_OS_MAC +, _viewport( + std::make_unique(widget(), PanelMode::Wide, _window.backend())) +, _mute(std::make_unique( + widget(), + st::callMuteButton, + Core::App().appDeactivatedValue(), + Ui::CallMuteButtonState{ + .text = (_call->scheduleDate() + ? tr::lng_group_call_start_now(tr::now) + : tr::lng_group_call_connecting(tr::now)), + .type = (!_call->scheduleDate() + ? Ui::CallMuteButtonType::Connecting + : _peer->canManageGroupCall() + ? Ui::CallMuteButtonType::ScheduledCanStart + : _call->scheduleStartSubscribed() + ? Ui::CallMuteButtonType::ScheduledNotify + : Ui::CallMuteButtonType::ScheduledSilent), + })) +, _hangup(widget(), st::groupCallHangup) +, _stickedTooltipsShown(Core::App().settings().hiddenGroupCallTooltips() + & ~StickedTooltip::Microphone) // Always show tooltip about mic. +, _toasts(std::make_unique(this)) { + _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); + _layerBg->setHideByBackgroundClick(true); + + _viewport->widget()->hide(); + if (!_viewport->requireARGB32()) { + _call->setNotRequireARGB32(); + } + + SubscribeToMigration( + _peer, + lifetime(), + [=](not_null channel) { migrate(channel); }); + setupRealCallViewers(); + + initWindow(); + initWidget(); + initControls(); + initLayout(); + showAndActivate(); +} + +Panel::~Panel() { + _menu.destroy(); + _viewport = nullptr; +} + +void Panel::setupRealCallViewers() { + _call->real( + ) | rpl::start_with_next([=](not_null real) { + subscribeToChanges(real); + }, lifetime()); +} + +not_null Panel::call() const { + return _call; +} + +bool Panel::isActive() const { + return window()->isActiveWindow() + && window()->isVisible() + && !(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, + }); +} + +void Panel::minimize() { + window()->setWindowState(window()->windowState() | Qt::WindowMinimized); +} + +void Panel::close() { + window()->close(); +} + +void Panel::showAndActivate() { + if (window()->isHidden()) { + window()->show(); + } + const auto state = window()->windowState(); + if (state & Qt::WindowMinimized) { + window()->setWindowState(state & ~Qt::WindowMinimized); + } + window()->raise(); + window()->activateWindow(); + window()->setFocus(); +} + +void Panel::migrate(not_null channel) { + _peer = channel; + _peerLifetime.destroy(); + subscribeToPeerChanges(); + _title.destroy(); + refreshTitle(); +} + +void Panel::subscribeToPeerChanges() { + Info::Profile::NameValue( + _peer + ) | rpl::start_with_next([=](const TextWithEntities &name) { + window()->setTitle(name.text); + }, _peerLifetime); +} + +QWidget *Panel::chooseSourceParent() { + return window().get(); +} + +QString Panel::chooseSourceActiveDeviceId() { + return _call->screenSharingDeviceId(); +} + +rpl::lifetime &Panel::chooseSourceInstanceLifetime() { + return lifetime(); +} + +void Panel::chooseSourceAccepted(const QString &deviceId) { + _call->toggleScreenSharing(deviceId); +} + +void Panel::chooseSourceStop() { + _call->toggleScreenSharing(std::nullopt); +} + +void Panel::initWindow() { + window()->setAttribute(Qt::WA_OpaquePaintEvent); + window()->setAttribute(Qt::WA_NoSystemBackground); + window()->setWindowIcon( + QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); + window()->setTitleStyle(st::groupCallTitle); + + subscribeToPeerChanges(); + + base::install_event_filter(window().get(), [=](not_null e) { + if (e->type() == QEvent::Close && handleClose()) { + e->ignore(); + return base::EventFilterResult::Cancel; + } else if (e->type() == QEvent::KeyPress + || e->type() == QEvent::KeyRelease) { + if (static_cast(e.get())->key() == Qt::Key_Space) { + _call->pushToTalk( + e->type() == QEvent::KeyPress, + kSpacePushToTalkDelay); + } + } + return base::EventFilterResult::Continue; + }); + + window()->setBodyTitleArea([=](QPoint widgetPoint) { + using Flag = Ui::WindowTitleHitTestFlag; + const auto titleRect = QRect( + 0, + 0, + widget()->width(), + st::groupCallMembersTop); + return (titleRect.contains(widgetPoint) + && (!_menuToggle || !_menuToggle->geometry().contains(widgetPoint)) + && (!_menu || !_menu->geometry().contains(widgetPoint)) + && (!_recordingMark || !_recordingMark->geometry().contains(widgetPoint)) + && (!_joinAsToggle || !_joinAsToggle->geometry().contains(widgetPoint))) + ? (Flag::Move | Flag::Maximize) + : Flag::None; + }); + + _call->hasVideoWithFramesValue( + ) | rpl::start_with_next([=] { + updateMode(); + }, lifetime()); +} + +void Panel::initWidget() { + widget()->setMouseTracking(true); + + widget()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + paint(clip); + }, lifetime()); + + widget()->sizeValue( + ) | rpl::skip(1) | rpl::start_with_next([=](QSize size) { + if (!updateMode()) { + updateControlsGeometry(); + } + + // title geometry depends on _controls->geometry, + // which is not updated here yet. + crl::on_main(widget(), [=] { refreshTitle(); }); + }, lifetime()); +} + +void Panel::endCall() { + if (!_call->canManage()) { + _call->hangup(); + return; + } + _layerBg->showBox(Box( + LeaveBox, + _call, + false, + BoxContext::GroupCallPanel)); +} + +void Panel::startScheduledNow() { + const auto date = _call->scheduleDate(); + const auto now = base::unixtime::now(); + if (!date) { + return; + } else if (now + kStartNoConfirmation >= date) { + _call->startScheduledNow(); + } else { + const auto box = std::make_shared>(); + const auto done = [=] { + if (*box) { + (*box)->closeBox(); + } + _call->startScheduledNow(); + }; + auto owned = ConfirmBox({ + .text = { tr::lng_group_call_start_now_sure(tr::now) }, + .button = tr::lng_group_call_start_now(), + .callback = done, + }); + *box = owned.data(); + _layerBg->showBox(std::move(owned)); + } +} + +void Panel::initControls() { + _mute->clicks( + ) | rpl::filter([=](Qt::MouseButton button) { + return (button == Qt::LeftButton); + }) | rpl::start_with_next([=] { + if (_call->scheduleDate()) { + if (_call->canManage()) { + startScheduledNow(); + } else if (const auto real = _call->lookupReal()) { + _call->toggleScheduleStartSubscribed( + !real->scheduleStartSubscribed()); + } + return; + } + const auto oldState = _call->muted(); + const auto newState = (oldState == MuteState::ForceMuted) + ? MuteState::RaisedHand + : (oldState == MuteState::RaisedHand) + ? MuteState::RaisedHand + : (oldState == MuteState::Muted) + ? MuteState::Active + : MuteState::Muted; + _call->setMutedAndUpdate(newState); + }, _mute->lifetime()); + + initShareAction(); + refreshLeftButton(); + refreshVideoButtons(); + + rpl::combine( + _mode.value(), + _call->canManageValue() + ) | rpl::start_with_next([=] { + refreshTopButton(); + }, lifetime()); + + _hangup->setClickedCallback([=] { endCall(); }); + + const auto scheduleDate = _call->scheduleDate(); + if (scheduleDate) { + auto changes = _call->real( + ) | rpl::map([=](not_null real) { + return real->scheduleDateValue(); + }) | rpl::flatten_latest(); + + setupScheduledLabels(rpl::single( + scheduleDate + ) | rpl::then(rpl::duplicate(changes))); + + auto started = std::move(changes) | rpl::filter([](TimeId date) { + return (date == 0); + }) | rpl::take(1); + + rpl::merge( + rpl::duplicate(started) | rpl::to_empty, + _peer->session().changes().peerFlagsValue( + _peer, + Data::PeerUpdate::Flag::Username + ) | rpl::skip(1) | rpl::to_empty + ) | rpl::start_with_next([=] { + refreshLeftButton(); + updateControlsGeometry(); + }, _callLifetime); + + std::move(started) | rpl::start_with_next([=] { + refreshVideoButtons(); + updateButtonsStyles(); + setupMembers(); + }, _callLifetime); + } + + _call->stateValue( + ) | rpl::before_next([=] { + showStickedTooltip(); + }) | rpl::filter([](State state) { + return (state == State::HangingUp) + || (state == State::Ended) + || (state == State::FailedHangingUp) + || (state == State::Failed); + }) | rpl::start_with_next([=] { + closeBeforeDestroy(); + }, _callLifetime); + + _call->levelUpdates( + ) | rpl::filter([=](const LevelUpdate &update) { + return update.me; + }) | rpl::start_with_next([=](const LevelUpdate &update) { + _mute->setLevel(update.value); + }, _callLifetime); + + _call->real( + ) | rpl::start_with_next([=](not_null real) { + setupRealMuteButtonState(real); + }, _callLifetime); + + refreshControlsBackground(); +} + +void Panel::refreshLeftButton() { + const auto share = _call->scheduleDate() + && _peer->isBroadcast() + && _peer->asChannel()->hasUsername(); + if ((share && _callShare) || (!share && _settings)) { + return; + } + if (share) { + _settings.destroy(); + _callShare.create(widget(), st::groupCallShare); + _callShare->setClickedCallback(_callShareLinkCallback); + } else { + _callShare.destroy(); + _settings.create(widget(), st::groupCallSettings); + _settings->setClickedCallback([=] { + _layerBg->showBox(Box(SettingsBox, _call)); + }); + } + const auto raw = _callShare ? _callShare.data() : _settings.data(); + raw->show(); + raw->setColorOverrides(_mute->colorOverrides()); + updateButtonsStyles(); +} + +void Panel::refreshVideoButtons(std::optional overrideWideMode) { + const auto real = _call->lookupReal(); + const auto create = overrideWideMode.value_or(mode() == PanelMode::Wide) + || (!_call->scheduleDate() && _call->videoIsWorking()); + const auto created = _video && _screenShare; + if (created == create) { + return; + } else if (created) { + _video.destroy(); + _screenShare.destroy(); + if (!overrideWideMode) { + updateButtonsGeometry(); + } + return; + } + auto toggleableOverrides = [&](rpl::producer active) { + return rpl::combine( + std::move(active), + _mute->colorOverrides() + ) | rpl::map([](bool active, Ui::CallButtonColors colors) { + if (active && colors.bg) { + colors.bg->setAlpha(kOverrideActiveColorBgAlpha); + } + return colors; + }); + }; + if (!_video) { + _video.create( + widget(), + st::groupCallVideoSmall, + &st::groupCallVideoActiveSmall); + _video->show(); + _video->setClickedCallback([=] { + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Activated); + _call->toggleVideo(!_call->isSharingCamera()); + }); + _video->setColorOverrides( + toggleableOverrides(_call->isSharingCameraValue())); + _call->isSharingCameraValue( + ) | rpl::start_with_next([=](bool sharing) { + if (sharing) { + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Activated); + } + _video->setProgress(sharing ? 1. : 0.); + }, _video->lifetime()); + } + if (!_screenShare) { + _screenShare.create(widget(), st::groupCallScreenShareSmall); + _screenShare->show(); + _screenShare->setClickedCallback([=] { + chooseShareScreenSource(); + }); + _screenShare->setColorOverrides( + toggleableOverrides(_call->isSharingScreenValue())); + _call->isSharingScreenValue( + ) | rpl::start_with_next([=](bool sharing) { + _screenShare->setProgress(sharing ? 1. : 0.); + }, _screenShare->lifetime()); + } + if (!_wideMenu) { + _wideMenu.create(widget(), st::groupCallMenuToggleSmall); + _wideMenu->show(); + _wideMenu->setClickedCallback([=] { showMainMenu(); }); + _wideMenu->setColorOverrides( + toggleableOverrides(_wideMenuShown.value())); + } + updateButtonsStyles(); + updateButtonsGeometry(); +} + +void Panel::hideStickedTooltip(StickedTooltipHide hide) { + if (!_stickedTooltipClose || !_niceTooltipControl) { + return; + } + if (_niceTooltipControl.data() == _video.data()) { + hideStickedTooltip(StickedTooltip::Camera, hide); + } else if (_niceTooltipControl.data() == _mute->outer().get()) { + hideStickedTooltip(StickedTooltip::Microphone, hide); + } +} + +void Panel::hideStickedTooltip( + StickedTooltip type, + StickedTooltipHide hide) { + if (hide != StickedTooltipHide::Unavailable) { + _stickedTooltipsShown |= type; + if (hide == StickedTooltipHide::Discarded) { + Core::App().settings().setHiddenGroupCallTooltip(type); + Core::App().saveSettingsDelayed(); + } + } + const auto control = (type == StickedTooltip::Camera) + ? _video.data() + : (type == StickedTooltip::Microphone) + ? _mute->outer().get() + : nullptr; + if (_niceTooltipControl.data() == control) { + hideNiceTooltip(); + } +} + +void Panel::hideNiceTooltip() { + if (!_niceTooltip) { + return; + } + _stickedTooltipClose = nullptr; + _niceTooltip.release()->toggleAnimated(false); + _niceTooltipControl = nullptr; +} + +void Panel::initShareAction() { + const auto showBoxCallback = [=](object_ptr next) { + _layerBg->showBox(std::move(next)); + }; + const auto showToastCallback = [=](QString text) { + showToast({ text }); + }; + auto [shareLinkCallback, shareLinkLifetime] = ShareInviteLinkAction( + _peer, + showBoxCallback, + showToastCallback); + _callShareLinkCallback = [=, callback = std::move(shareLinkCallback)] { + if (_call->lookupReal()) { + callback(); + } + }; + lifetime().add(std::move(shareLinkLifetime)); +} + +void Panel::setupRealMuteButtonState(not_null real) { + using namespace rpl::mappers; + rpl::combine( + _call->mutedValue() | MapPushToTalkToActive(), + _call->instanceStateValue(), + real->scheduleDateValue(), + real->scheduleStartSubscribedValue(), + _call->canManageValue(), + _mode.value() + ) | rpl::distinct_until_changed( + ) | rpl::filter( + _2 != GroupCall::InstanceState::TransitionToRtc + ) | rpl::start_with_next([=]( + MuteState mute, + GroupCall::InstanceState state, + TimeId scheduleDate, + bool scheduleStartSubscribed, + bool canManage, + PanelMode mode) { + const auto wide = (mode == PanelMode::Wide); + using Type = Ui::CallMuteButtonType; + _mute->setState(Ui::CallMuteButtonState{ + .text = (wide + ? QString() + : scheduleDate + ? (canManage + ? tr::lng_group_call_start_now(tr::now) + : scheduleStartSubscribed + ? tr::lng_group_call_cancel_reminder(tr::now) + : tr::lng_group_call_set_reminder(tr::now)) + : state == GroupCall::InstanceState::Disconnected + ? tr::lng_group_call_connecting(tr::now) + : mute == MuteState::ForceMuted + ? tr::lng_group_call_force_muted(tr::now) + : mute == MuteState::RaisedHand + ? tr::lng_group_call_raised_hand(tr::now) + : mute == MuteState::Muted + ? tr::lng_group_call_unmute(tr::now) + : tr::lng_group_call_you_are_live(tr::now)), + .tooltip = ((!scheduleDate && mute == MuteState::Muted) + ? tr::lng_group_call_unmute_sub(tr::now) + : QString()), + .type = (scheduleDate + ? (canManage + ? Type::ScheduledCanStart + : scheduleStartSubscribed + ? Type::ScheduledNotify + : Type::ScheduledSilent) + : state == GroupCall::InstanceState::Disconnected + ? Type::Connecting + : mute == MuteState::ForceMuted + ? Type::ForceMuted + : mute == MuteState::RaisedHand + ? Type::RaisedHand + : mute == MuteState::Muted + ? Type::Muted + : Type::Active), + }); + }, _callLifetime); +} + +void Panel::setupScheduledLabels(rpl::producer date) { + using namespace rpl::mappers; + date = std::move(date) | rpl::take_while(_1 != 0); + _startsWhen.create( + widget(), + Ui::StartsWhenText(rpl::duplicate(date)), + st::groupCallStartsWhen); + auto countdownCreated = std::move( + date + ) | rpl::map([=](TimeId date) { + _countdownData = std::make_shared(date); + return rpl::empty_value(); + }) | rpl::start_spawning(lifetime()); + + _countdown = Ui::CreateGradientLabel(widget(), rpl::duplicate( + countdownCreated + ) | rpl::map([=] { + return _countdownData->text( + Ui::GroupCallScheduledLeft::Negative::Ignore); + }) | rpl::flatten_latest()); + + _startsIn.create( + widget(), + rpl::conditional( + std::move( + countdownCreated + ) | rpl::map( + [=] { return _countdownData->late(); } + ) | rpl::flatten_latest(), + tr::lng_group_call_late_by(), + tr::lng_group_call_starts_in()), + st::groupCallStartsIn); + + const auto top = [=] { + const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; + const auto membersTop = st::groupCallMembersTop; + const auto height = st::groupCallScheduledBodyHeight; + return (membersTop + (muteTop - membersTop - height) / 2); + }; + rpl::combine( + widget()->sizeValue(), + _startsIn->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _startsIn->move( + (size.width() - width) / 2, + top() + st::groupCallStartsInTop); + }, _startsIn->lifetime()); + + rpl::combine( + widget()->sizeValue(), + _startsWhen->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _startsWhen->move( + (size.width() - width) / 2, + top() + st::groupCallStartsWhenTop); + }, _startsWhen->lifetime()); + + rpl::combine( + widget()->sizeValue(), + _countdown->widthValue() + ) | rpl::start_with_next([=](QSize size, int width) { + _countdown->move( + (size.width() - width) / 2, + top() + st::groupCallCountdownTop); + }, _startsWhen->lifetime()); +} + +PanelMode Panel::mode() const { + return _mode.current(); +} + +void Panel::setupMembers() { + if (_members) { + return; + } + + _startsIn.destroy(); + _countdown.destroy(); + _startsWhen.destroy(); + + _members.create(widget(), _call, mode(), _window.backend()); + + setupVideo(_viewport.get()); + setupVideo(_members->viewport()); + _viewport->mouseInsideValue( + ) | rpl::start_with_next([=](bool inside) { + toggleWideControls(inside); + }, _viewport->lifetime()); + + _members->show(); + + refreshControlsBackground(); + raiseControls(); + + _members->desiredHeightValue( + ) | rpl::start_with_next([=] { + updateMembersGeometry(); + }, _members->lifetime()); + + _members->toggleMuteRequests( + ) | rpl::start_with_next([=](MuteRequest request) { + if (_call) { + _call->toggleMute(request); + } + }, _callLifetime); + + _members->changeVolumeRequests( + ) | rpl::start_with_next([=](VolumeRequest request) { + if (_call) { + _call->changeVolume(request); + } + }, _callLifetime); + + _members->kickParticipantRequests( + ) | rpl::start_with_next([=](not_null participantPeer) { + kickParticipant(participantPeer); + }, _callLifetime); + + _members->addMembersRequests( + ) | rpl::start_with_next([=] { + if (_peer->isBroadcast() && _peer->asChannel()->hasUsername()) { + _callShareLinkCallback(); + } else { + addMembers(); + } + }, _callLifetime); + + _call->videoEndpointLargeValue( + ) | rpl::start_with_next([=](const VideoEndpoint &large) { + if (large && mode() != PanelMode::Wide) { + enlargeVideo(); + } + _viewport->showLarge(large); + }, _callLifetime); +} + +void Panel::enlargeVideo() { + _lastSmallGeometry = window()->geometry(); + + const auto available = window()->screen()->availableGeometry(); + const auto width = std::max( + window()->width(), + std::max( + std::min(available.width(), st::groupCallWideModeSize.width()), + st::groupCallWideModeWidthMin)); + const auto height = std::max( + window()->height(), + std::min(available.height(), st::groupCallWideModeSize.height())); + auto geometry = QRect(window()->pos(), QSize(width, height)); + if (geometry.x() < available.x()) { + geometry.moveLeft(std::min(available.x(), window()->x())); + } + if (geometry.x() + geometry.width() + > available.x() + available.width()) { + geometry.moveLeft(std::max( + available.x() + available.width(), + window()->x() + window()->width()) - geometry.width()); + } + if (geometry.y() < available.y()) { + geometry.moveTop(std::min(available.y(), window()->y())); + } + if (geometry.y() + geometry.height() > available.y() + available.height()) { + geometry.moveTop(std::max( + available.y() + available.height(), + window()->y() + window()->height()) - geometry.height()); + } + if (_lastLargeMaximized) { + window()->setWindowState( + window()->windowState() | Qt::WindowMaximized); + } else { + window()->setGeometry((_lastLargeGeometry + && available.intersects(*_lastLargeGeometry)) + ? *_lastLargeGeometry + : geometry); + } +} + +void Panel::minimizeVideo() { + if (window()->windowState() & Qt::WindowMaximized) { + _lastLargeMaximized = true; + window()->setWindowState( + window()->windowState() & ~Qt::WindowMaximized); + } else { + _lastLargeMaximized = false; + _lastLargeGeometry = window()->geometry(); + } + const auto available = window()->screen()->availableGeometry(); + const auto width = st::groupCallWidth; + const auto height = st::groupCallHeight; + auto geometry = QRect( + window()->x() + (window()->width() - width) / 2, + window()->y() + (window()->height() - height) / 2, + width, + height); + window()->setGeometry((_lastSmallGeometry + && available.intersects(*_lastSmallGeometry)) + ? *_lastSmallGeometry + : geometry); +} + +void Panel::raiseControls() { + if (_controlsBackgroundWide) { + _controlsBackgroundWide->raise(); + } + if (_controlsBackgroundNarrow) { + _controlsBackgroundNarrow->shadow.raise(); + _controlsBackgroundNarrow->blocker.raise(); + } + const auto buttons = { + &_settings, + &_callShare, + &_screenShare, + &_wideMenu, + &_video, + &_hangup + }; + for (const auto button : buttons) { + if (const auto raw = button->data()) { + raw->raise(); + } + } + _mute->raise(); + if (_niceTooltip) { + _niceTooltip->raise(); + } +} + +void Panel::setupVideo(not_null viewport) { + const auto setupTile = [=]( + const VideoEndpoint &endpoint, + const std::unique_ptr &track) { + using namespace rpl::mappers; + const auto row = _members->lookupRow(GroupCall::TrackPeer(track)); + Assert(row != nullptr); + auto pinned = rpl::combine( + _call->videoEndpointLargeValue(), + _call->videoEndpointPinnedValue() + ) | rpl::map(_1 == endpoint && _2); + viewport->add( + endpoint, + VideoTileTrack{ GroupCall::TrackPointer(track), row }, + GroupCall::TrackSizeValue(track), + std::move(pinned)); + }; + for (const auto &[endpoint, track] : _call->activeVideoTracks()) { + setupTile(endpoint, track); + } + _call->videoStreamActiveUpdates( + ) | rpl::start_with_next([=](const VideoStateToggle &update) { + if (update.value) { + // Add async (=> the participant row is definitely in Members). + const auto endpoint = update.endpoint; + crl::on_main(viewport->widget(), [=] { + const auto &tracks = _call->activeVideoTracks(); + const auto i = tracks.find(endpoint); + if (i != end(tracks)) { + setupTile(endpoint, i->second); + } + }); + } else { + // Remove sync. + viewport->remove(update.endpoint); + } + }, viewport->lifetime()); + + viewport->pinToggled( + ) | rpl::start_with_next([=](bool pinned) { + _call->pinVideoEndpoint(pinned + ? _call->videoEndpointLarge() + : VideoEndpoint{}); + }, viewport->lifetime()); + + viewport->clicks( + ) | rpl::start_with_next([=](VideoEndpoint &&endpoint) { + if (_call->videoEndpointLarge() == endpoint) { + _call->showVideoEndpointLarge({}); + } else if (_call->videoEndpointPinned()) { + _call->pinVideoEndpoint(std::move(endpoint)); + } else { + _call->showVideoEndpointLarge(std::move(endpoint)); + } + }, viewport->lifetime()); + + viewport->qualityRequests( + ) | rpl::start_with_next([=](const VideoQualityRequest &request) { + _call->requestVideoQuality(request.endpoint, request.quality); + }, viewport->lifetime()); +} + +void Panel::toggleWideControls(bool shown) { + if (_showWideControls == shown) { + return; + } + _showWideControls = shown; + crl::on_main(widget(), [=] { + updateWideControlsVisibility(); + }); +} + +void Panel::updateWideControlsVisibility() { + const auto shown = _showWideControls + || (_stickedTooltipClose != nullptr); + if (_wideControlsShown == shown) { + return; + } + _wideControlsShown = shown; + _wideControlsAnimation.start( + [=] { updateButtonsGeometry(); }, + _wideControlsShown ? 0. : 1., + _wideControlsShown ? 1. : 0., + st::slideWrapDuration); +} + +void Panel::subscribeToChanges(not_null real) { + const auto validateRecordingMark = [=](bool recording) { + if (!recording && _recordingMark) { + _recordingMark.destroy(); + } else if (recording && !_recordingMark) { + struct State { + Ui::Animations::Simple animation; + base::Timer timer; + bool opaque = true; + }; + _recordingMark.create(widget()); + _recordingMark->show(); + const auto state = _recordingMark->lifetime().make_state(); + const auto size = st::groupCallRecordingMark; + const auto skip = st::groupCallRecordingMarkSkip; + _recordingMark->resize(size + 2 * skip, size + 2 * skip); + _recordingMark->setClickedCallback([=] { + showToast({ tr::lng_group_call_is_recorded(tr::now) }); + }); + const auto animate = [=] { + const auto opaque = state->opaque; + state->opaque = !opaque; + state->animation.start( + [=] { _recordingMark->update(); }, + opaque ? 1. : kRecordingOpacity, + opaque ? kRecordingOpacity : 1., + kRecordingAnimationDuration); + }; + state->timer.setCallback(animate); + state->timer.callEach(kRecordingAnimationDuration); + animate(); + + _recordingMark->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(_recordingMark.data()); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::groupCallMemberMutedIcon); + p.setOpacity(state->animation.value( + state->opaque ? 1. : kRecordingOpacity)); + p.drawEllipse(skip, skip, size, size); + }, _recordingMark->lifetime()); + } + refreshTitleGeometry(); + }; + + using namespace rpl::mappers; + real->recordStartDateChanges( + ) | rpl::map( + _1 != 0 + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool recorded) { + validateRecordingMark(recorded); + showToast((recorded + ? tr::lng_group_call_recording_started + : _call->recordingStoppedByMe() + ? tr::lng_group_call_recording_saved + : tr::lng_group_call_recording_stopped)( + tr::now, + Ui::Text::RichLangValue)); + }, lifetime()); + validateRecordingMark(real->recordStartDate() != 0); + + rpl::combine( + _call->videoIsWorkingValue(), + _call->isSharingCameraValue() + ) | rpl::start_with_next([=] { + refreshVideoButtons(); + showStickedTooltip(); + }, lifetime()); + + rpl::combine( + _call->videoIsWorkingValue(), + _call->isSharingScreenValue() + ) | rpl::start_with_next([=] { + refreshTopButton(); + }, lifetime()); + + _call->mutedValue( + ) | rpl::skip(1) | rpl::start_with_next([=](MuteState state) { + updateButtonsGeometry(); + if (state == MuteState::Active + || state == MuteState::PushToTalk) { + hideStickedTooltip( + StickedTooltip::Microphone, + StickedTooltipHide::Activated); + } + showStickedTooltip(); + }, lifetime()); + + updateControlsGeometry(); +} + +void Panel::refreshTopButton() { + if (_mode.current() == PanelMode::Wide) { + _menuToggle.destroy(); + _joinAsToggle.destroy(); + updateButtonsGeometry(); // _wideMenu <-> _settings + return; + } + const auto real = _call->lookupReal(); + const auto hasJoinAs = _call->showChooseJoinAs(); + const auto wide = (_mode.current() == PanelMode::Wide); + const auto showNarrowMenu = _call->canManage() + || _call->videoIsWorking(); + const auto showNarrowUserpic = !showNarrowMenu && hasJoinAs; + if (showNarrowMenu) { + _joinAsToggle.destroy(); + if (!_menuToggle) { + _menuToggle.create(widget(), st::groupCallMenuToggle); + _menuToggle->show(); + _menuToggle->setClickedCallback([=] { showMainMenu(); }); + updateControlsGeometry(); + } + } else if (showNarrowUserpic) { + _menuToggle.destroy(); + rpl::single( + _call->joinAs() + ) | rpl::then(_call->rejoinEvents( + ) | rpl::map([](const RejoinEvent &event) { + return event.nowJoinAs; + })) | rpl::start_with_next([=](not_null joinAs) { + auto joinAsToggle = object_ptr( + widget(), + joinAs, + Ui::UserpicButton::Role::Custom, + st::groupCallJoinAsToggle); + _joinAsToggle.destroy(); + _joinAsToggle = std::move(joinAsToggle); + _joinAsToggle->show(); + _joinAsToggle->setClickedCallback([=] { + chooseJoinAs(); + }); + updateControlsGeometry(); + }, lifetime()); + } else { + _menuToggle.destroy(); + _joinAsToggle.destroy(); + } +} + +void Panel::screenSharingPrivacyRequest() { +#ifdef Q_OS_MAC + if (!Platform::IsMac10_15OrGreater()) { + return; + } + const auto requestInputMonitoring = Platform::IsMac10_15OrGreater(); + _layerBg->showBox(Box([=](not_null box) { + box->addRow( + object_ptr( + box.get(), + rpl::combine( + tr::lng_group_call_mac_screencast_access(), + tr::lng_group_call_mac_recording() + ) | rpl::map([](QString a, QString b) { + auto result = Ui::Text::RichLangValue(a); + result.append("\n\n").append(Ui::Text::RichLangValue(b)); + return result; + }), + st::groupCallBoxLabel), + style::margins( + st::boxRowPadding.left(), + st::boxPadding.top(), + st::boxRowPadding.right(), + st::boxPadding.bottom())); + box->addButton(tr::lng_group_call_mac_settings(), [=] { + Platform::OpenDesktopCapturePrivacySettings(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +#endif // Q_OS_MAC +} + +void Panel::chooseShareScreenSource() { + if (_call->emitShareScreenError()) { + return; + } + const auto choose = [=] { + if (!Webrtc::DesktopCaptureAllowed()) { + screenSharingPrivacyRequest(); + } else if (const auto source = Webrtc::UniqueDesktopCaptureSource()) { + if (_call->isSharingScreen()) { + _call->toggleScreenSharing(std::nullopt); + } else { + chooseSourceAccepted(*source); + } + } else { + Ui::DesktopCapture::ChooseSource(this); + } + }; + const auto screencastFromPeer = [&]() -> PeerData* { + for (const auto &[endpoint, track] : _call->activeVideoTracks()) { + if (endpoint.type == VideoEndpointType::Screen) { + return endpoint.peer; + } + } + return nullptr; + }(); + if (!screencastFromPeer || _call->isSharingScreen()) { + choose(); + return; + } + const auto text = tr::lng_group_call_sure_screencast( + tr::now, + lt_user, + screencastFromPeer->shortName()); + const auto shared = std::make_shared>(); + const auto done = [=] { + if (*shared) { + base::take(*shared)->closeBox(); + } + choose(); + }; + auto box = ConfirmBox({ + .text = { text }, + .button = tr::lng_continue(), + .callback = done, + }); + *shared = box.data(); + _layerBg->showBox(std::move(box)); +} + +void Panel::chooseJoinAs() { + const auto context = ChooseJoinAsProcess::Context::Switch; + const auto callback = [=](JoinInfo info) { + _call->rejoinAs(info); + }; + const auto showBoxCallback = [=](object_ptr next) { + _layerBg->showBox(std::move(next)); + }; + const auto showToastCallback = [=](QString text) { + showToast({ text }); + }; + _joinAsProcess.start( + _peer, + context, + showBoxCallback, + showToastCallback, + callback, + _call->joinAs()); +} + +void Panel::showMainMenu() { + if (_menu) { + return; + } + const auto wide = (_mode.current() == PanelMode::Wide) && _wideMenu; + if (!wide && !_menuToggle) { + return; + } + _menu.create(widget(), st::groupCallDropdownMenu); + FillMenu( + _menu.data(), + _peer, + _call, + wide, + [=] { chooseJoinAs(); }, + [=] { chooseShareScreenSource(); }, + [=](auto box) { _layerBg->showBox(std::move(box)); }); + if (_menu->empty()) { + _wideMenuShown = false; + _menu.destroy(); + return; + } + + const auto raw = _menu.data(); + raw->setHiddenCallback([=] { + raw->deleteLater(); + if (_menu == raw) { + _menu = nullptr; + _wideMenuShown = false; + _trackControlsMenuLifetime.destroy(); + if (_menuToggle) { + _menuToggle->setForceRippled(false); + } + } + }); + raw->setShowStartCallback([=] { + if (_menu == raw) { + if (wide) { + _wideMenuShown = true; + } else if (_menuToggle) { + _menuToggle->setForceRippled(true); + } + } + }); + raw->setHideStartCallback([=] { + if (_menu == raw) { + _wideMenuShown = false; + if (_menuToggle) { + _menuToggle->setForceRippled(false); + } + } + }); + + if (wide) { + _wideMenu->installEventFilter(_menu); + const auto x = st::groupCallWideMenuPosition.x(); + const auto y = st::groupCallWideMenuPosition.y(); + _menu->moveToLeft( + _wideMenu->x() + x, + _wideMenu->y() - _menu->height() + y); + _menu->showAnimated(Ui::PanelAnimation::Origin::BottomLeft); + trackControl(_menu, _trackControlsMenuLifetime); + } else { + _menuToggle->installEventFilter(_menu); + const auto x = st::groupCallMenuPosition.x(); + const auto y = st::groupCallMenuPosition.y(); + if (_menuToggle->x() > widget()->width() / 2) { + _menu->moveToRight(x, y); + _menu->showAnimated(Ui::PanelAnimation::Origin::TopRight); + } else { + _menu->moveToLeft(x, y); + _menu->showAnimated(Ui::PanelAnimation::Origin::TopLeft); + } + } +} + +void Panel::addMembers() { + const auto showToastCallback = [=](TextWithEntities &&text) { + showToast(std::move(text)); + }; + if (auto box = PrepareInviteBox(_call, showToastCallback)) { + _layerBg->showBox(std::move(box)); + } +} + +void Panel::kickParticipant(not_null participantPeer) { + _layerBg->showBox(Box([=](not_null box) { + box->addRow( + object_ptr( + box.get(), + (!participantPeer->isUser() + ? tr::lng_group_call_remove_channel( + tr::now, + lt_channel, + participantPeer->name) + : (_peer->isBroadcast() + ? tr::lng_profile_sure_kick_channel + : tr::lng_profile_sure_kick)( + tr::now, + lt_user, + participantPeer->asUser()->firstName)), + st::groupCallBoxLabel), + style::margins( + st::boxRowPadding.left(), + st::boxPadding.top(), + st::boxRowPadding.right(), + st::boxPadding.bottom())); + box->addButton(tr::lng_box_remove(), [=] { + box->closeBox(); + kickParticipantSure(participantPeer); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +} + +void Panel::kickParticipantSure(not_null participantPeer) { + if (const auto chat = _peer->asChat()) { + chat->session().api().kickParticipant(chat, participantPeer); + } else if (const auto channel = _peer->asChannel()) { + const auto currentRestrictedRights = [&] { + const auto user = participantPeer->asUser(); + if (!channel->mgInfo || !user) { + return ChannelData::EmptyRestrictedRights(participantPeer); + } + const auto i = channel->mgInfo->lastRestricted.find(user); + return (i != channel->mgInfo->lastRestricted.cend()) + ? i->second.rights + : ChannelData::EmptyRestrictedRights(participantPeer); + }(); + channel->session().api().kickParticipant( + channel, + participantPeer, + currentRestrictedRights); + } +} + +void Panel::initLayout() { + initGeometry(); + +#ifndef Q_OS_MAC + _controls->raise(); + + Ui::Platform::TitleControlsLayoutChanged( + ) | rpl::start_with_next([=] { + // _menuToggle geometry depends on _controls arrangement. + crl::on_main(widget(), [=] { updateControlsGeometry(); }); + }, lifetime()); + +#endif // !Q_OS_MAC +} + +void Panel::showControls() { + Expects(_call != nullptr); + + widget()->showChildren(); +} + +void Panel::closeBeforeDestroy() { + window()->close(); + _callLifetime.destroy(); +} + +rpl::lifetime &Panel::lifetime() { + return window()->lifetime(); +} + +void Panel::initGeometry() { + const auto center = Core::App().getPointForCallPanelCenter(); + const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight); + window()->setGeometry(rect.translated(center - rect.center())); + window()->setMinimumSize(rect.size()); + window()->show(); +} + +QRect Panel::computeTitleRect() const { + const auto skip = st::groupCallTitleTop; + const auto remove = skip + (_menuToggle + ? (_menuToggle->width() + st::groupCallMenuTogglePosition.x()) + : 0) + (_joinAsToggle + ? (_joinAsToggle->width() + st::groupCallMenuTogglePosition.x()) + : 0); + const auto width = widget()->width(); +#ifdef Q_OS_MAC + return QRect(70, 0, width - remove - 70, 28); +#else // Q_OS_MAC + const auto controls = _controls->geometry(); + const auto right = controls.x() + controls.width() + skip; + return (controls.center().x() < width / 2) + ? QRect(right, 0, width - right - remove, controls.height()) + : QRect(remove, 0, controls.x() - skip - remove, controls.height()); +#endif // !Q_OS_MAC +} + +bool Panel::updateMode() { + if (!_viewport) { + return false; + } + const auto wide = _call->hasVideoWithFrames() + && (widget()->width() >= st::groupCallWideModeWidthMin); + const auto mode = wide ? PanelMode::Wide : PanelMode::Default; + if (_mode.current() == mode) { + return false; + } + if (!wide && _call->videoEndpointLarge()) { + _call->showVideoEndpointLarge({}); + } + refreshVideoButtons(wide); + if (!_stickedTooltipClose + || _niceTooltipControl.data() != _mute->outer().get()) { + _niceTooltip.destroy(); + } + _mode = mode; + if (_title) { + _title->setTextColorOverride(wide + ? std::make_optional(st::groupCallMemberNotJoinedStatus->c) + : std::nullopt); + } + if (wide && _subtitle) { + _subtitle.destroy(); + } else if (!wide && !_subtitle) { + refreshTitle(); + } + _wideControlsShown = _showWideControls = true; + _wideControlsAnimation.stop(); + _viewport->widget()->setVisible(wide); + if (_members) { + _members->setMode(mode); + } + updateButtonsStyles(); + refreshControlsBackground(); + updateControlsGeometry(); + showStickedTooltip(); + return true; +} + +void Panel::updateButtonsStyles() { + const auto wide = (_mode.current() == PanelMode::Wide); + _mute->setStyle(wide ? st::callMuteButtonSmall : st::callMuteButton); + if (_video) { + _video->setStyle( + wide ? st::groupCallVideoSmall : st::groupCallVideo, + (wide + ? &st::groupCallVideoActiveSmall + : &st::groupCallVideoActive)); + _video->setText(wide + ? rpl::single(QString()) + : tr::lng_group_call_video()); + } + if (_settings) { + _settings->setText(wide + ? rpl::single(QString()) + : tr::lng_group_call_settings()); + _settings->setStyle(wide + ? st::groupCallSettingsSmall + : st::groupCallSettings); + } + _hangup->setText(wide + ? rpl::single(QString()) + : _call->scheduleDate() + ? tr::lng_group_call_close() + : tr::lng_group_call_leave()); + _hangup->setStyle(wide + ? st::groupCallHangupSmall + : st::groupCallHangup); +} + +void Panel::refreshControlsBackground() { + if (!_members) { + return; + } + if (mode() == PanelMode::Default) { + trackControls(false); + _controlsBackgroundWide.destroy(); + if (_controlsBackgroundNarrow) { + return; + } + setupControlsBackgroundNarrow(); + } else { + _controlsBackgroundNarrow = nullptr; + if (_controlsBackgroundWide) { + return; + } + setupControlsBackgroundWide(); + } + raiseControls(); + updateButtonsGeometry(); +} + +void Panel::setupControlsBackgroundNarrow() { + _controlsBackgroundNarrow = std::make_unique( + widget()); + _controlsBackgroundNarrow->shadow.show(); + _controlsBackgroundNarrow->blocker.show(); + auto &lifetime = _controlsBackgroundNarrow->shadow.lifetime(); + + const auto factor = cIntRetinaFactor(); + const auto height = std::max( + st::groupCallMembersShadowHeight, + st::groupCallMembersFadeSkip + st::groupCallMembersFadeHeight); + const auto full = lifetime.make_state( + QSize(1, height * factor), + QImage::Format_ARGB32_Premultiplied); + rpl::single( + rpl::empty_value() + ) | rpl::then( + style::PaletteChanged() + ) | rpl::start_with_next([=] { + full->fill(Qt::transparent); + + auto p = QPainter(full); + const auto bottom = (height - st::groupCallMembersFadeSkip) * factor; + p.fillRect( + 0, + bottom, + full->width(), + st::groupCallMembersFadeSkip * factor, + st::groupCallMembersBg); + p.drawImage( + QRect( + 0, + bottom - (st::groupCallMembersFadeHeight * factor), + full->width(), + st::groupCallMembersFadeHeight * factor), + GenerateShadow( + st::groupCallMembersFadeHeight, + 0, + 255, + st::groupCallMembersBg->c)); + p.drawImage( + QRect( + 0, + (height - st::groupCallMembersShadowHeight) * factor, + full->width(), + st::groupCallMembersShadowHeight * factor), + GenerateShadow( + st::groupCallMembersShadowHeight, + 0, + 255, + st::groupCallBg->c)); + }, lifetime); + + _controlsBackgroundNarrow->shadow.resize( + (widget()->width() + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right()), + height); + _controlsBackgroundNarrow->shadow.paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(&_controlsBackgroundNarrow->shadow); + clip = clip.intersected(_controlsBackgroundNarrow->shadow.rect()); + const auto inner = _members->getInnerGeometry().translated( + _members->x() - _controlsBackgroundNarrow->shadow.x(), + _members->y() - _controlsBackgroundNarrow->shadow.y()); + const auto faded = clip.intersected(inner); + if (!faded.isEmpty()) { + const auto factor = cIntRetinaFactor(); + p.drawImage( + faded, + *full, + QRect( + 0, + faded.y() * factor, + full->width(), + faded.height() * factor)); + } + const auto bottom = inner.y() + inner.height(); + const auto after = clip.intersected(QRect( + 0, + bottom, + inner.width(), + _controlsBackgroundNarrow->shadow.height() - bottom)); + if (!after.isEmpty()) { + p.fillRect(after, st::groupCallBg); + } + }, lifetime); + _controlsBackgroundNarrow->shadow.setAttribute( + Qt::WA_TransparentForMouseEvents); + _controlsBackgroundNarrow->blocker.setUpdatesEnabled(false); +} + +void Panel::setupControlsBackgroundWide() { + _controlsBackgroundWide.create(widget()); + _controlsBackgroundWide->show(); + auto &lifetime = _controlsBackgroundWide->lifetime(); + const auto color = lifetime.make_state([] { + auto result = st::groupCallBg->c; + result.setAlphaF(kControlsBackgroundOpacity); + return result; + }); + const auto corners = lifetime.make_state( + st::groupCallControlsBackRadius, + color->color()); + _controlsBackgroundWide->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(_controlsBackgroundWide.data()); + corners->paint(p, _controlsBackgroundWide->rect()); + }, lifetime); + + trackControls(true); +} + +void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) { + if (!widget) { + return; + } + widget->events( + ) | rpl::start_with_next([=](not_null e) { + if (e->type() == QEvent::Enter) { + trackControlOver(widget, true); + } else if (e->type() == QEvent::Leave) { + trackControlOver(widget, false); + } + }, lifetime); +} + +void Panel::trackControlOver(not_null control, bool over) { + if (_stickedTooltipClose) { + if (!over) { + return; + } + } else { + hideNiceTooltip(); + } + if (over) { + Ui::Integration::Instance().registerLeaveSubscription(control); + showNiceTooltip(control); + } else { + Ui::Integration::Instance().unregisterLeaveSubscription(control); + } + toggleWideControls(over); +} + +void Panel::showStickedTooltip() { + static const auto kHasCamera = !Webrtc::GetVideoInputList().empty(); + const auto callReady = (_call->state() == State::Joined + || _call->state() == State::Connecting); + if (!(_stickedTooltipsShown & StickedTooltip::Camera) + && callReady + && (_mode.current() == PanelMode::Wide) + && _video + && _call->videoIsWorking() + && !_call->mutedByAdmin() + && kHasCamera) { // Don't recount this every time for now. + showNiceTooltip(_video, NiceTooltipType::Sticked); + return; + } + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Unavailable); + + if (!(_stickedTooltipsShown & StickedTooltip::Microphone) + && callReady + && _mute + && !_call->mutedByAdmin()) { + if (_stickedTooltipClose) { + // Showing already. + return; + } else if (!_micLevelTester) { + // Check if there is incoming sound. + _micLevelTester = std::make_unique([=] { + showStickedTooltip(); + }); + } + if (_micLevelTester->showTooltip()) { + _micLevelTester = nullptr; + showNiceTooltip(_mute->outer(), NiceTooltipType::Sticked); + } + return; + } + _micLevelTester = nullptr; + hideStickedTooltip( + StickedTooltip::Microphone, + StickedTooltipHide::Unavailable); +} + +void Panel::showNiceTooltip( + not_null control, + NiceTooltipType type) { + auto text = [&]() -> rpl::producer { + if (control == _screenShare.data()) { + if (_call->mutedByAdmin()) { + return nullptr; + } + return tr::lng_group_call_tooltip_screen(); + } else if (control == _video.data()) { + if (_call->mutedByAdmin()) { + return nullptr; + } + return _call->isSharingCameraValue( + ) | rpl::map([=](bool sharing) { + return sharing + ? tr::lng_group_call_tooltip_camera_off() + : tr::lng_group_call_tooltip_camera(); + }) | rpl::flatten_latest(); + } else if (control == _settings.data()) { + return tr::lng_group_call_settings(); + } else if (control == _mute->outer()) { + return MuteButtonTooltip(_call); + } else if (control == _hangup.data()) { + return tr::lng_group_call_leave(); + } + return rpl::producer(); + }(); + if (!text || _stickedTooltipClose) { + return; + } else if (_wideControlsAnimation.animating() || !_wideControlsShown) { + if (type == NiceTooltipType::Normal) { + return; + } + } + const auto inner = [&]() -> Ui::RpWidget* { + const auto normal = (type == NiceTooltipType::Normal); + auto container = normal + ? nullptr + : Ui::CreateChild(widget().get()); + const auto label = Ui::CreateChild( + (normal ? widget().get() : container), + std::move(text), + st::groupCallNiceTooltipLabel); + if (normal) { + return label; + } + const auto button = Ui::CreateChild( + container, + st::groupCallStickedTooltipClose); + rpl::combine( + label->sizeValue(), + button->sizeValue() + ) | rpl::start_with_next([=](QSize text, QSize close) { + const auto height = std::max(text.height(), close.height()); + container->resize(text.width() + close.width(), height); + label->move(0, (height - text.height()) / 2); + button->move(text.width(), (height - close.height()) / 2); + }, container->lifetime()); + button->setClickedCallback([=] { + hideStickedTooltip(StickedTooltipHide::Discarded); + }); + _stickedTooltipClose = button; + updateWideControlsVisibility(); + return container; + }(); + _niceTooltip.create( + widget().get(), + object_ptr::fromRaw(inner), + (type == NiceTooltipType::Sticked + ? st::groupCallStickedTooltip + : st::groupCallNiceTooltip)); + const auto tooltip = _niceTooltip.data(); + const auto weak = QPointer(tooltip); + const auto destroy = [=] { + delete weak.data(); + }; + if (type != NiceTooltipType::Sticked) { + tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); + } + tooltip->setHiddenCallback(destroy); + base::qt_signal_producer( + control.get(), + &QObject::destroyed + ) | rpl::start_with_next(destroy, tooltip->lifetime()); + + _niceTooltipControl = control; + updateTooltipGeometry(); + tooltip->toggleAnimated(true); +} + +void Panel::updateTooltipGeometry() { + if (!_niceTooltip) { + return; + } else if (!_niceTooltipControl) { + hideNiceTooltip(); + return; + } + const auto geometry = _niceTooltipControl->geometry(); + const auto weak = QPointer(_niceTooltip); + const auto countPosition = [=](QSize size) { + const auto strong = weak.data(); + const auto wide = (_mode.current() == PanelMode::Wide); + const auto top = geometry.y() + - (wide ? st::groupCallNiceTooltipTop : 0) + - size.height(); + const auto middle = geometry.center().x(); + if (!strong) { + return QPoint(); + } else if (!wide) { + return QPoint( + std::max( + std::min( + middle - size.width() / 2, + (widget()->width() + - st::groupCallMembersMargin.right() + - size.width())), + st::groupCallMembersMargin.left()), + top); + } + const auto back = _controlsBackgroundWide.data(); + if (size.width() >= _viewport->widget()->width()) { + return QPoint(_viewport->widget()->x(), top); + } else if (back && size.width() >= back->width()) { + return QPoint( + back->x() - (size.width() - back->width()) / 2, + top); + } else if (back && (middle - back->x() < size.width() / 2)) { + return QPoint(back->x(), top); + } else if (back + && (back->x() + back->width() - middle < size.width() / 2)) { + return QPoint(back->x() + back->width() - size.width(), top); + } else { + return QPoint(middle - size.width() / 2, top); + } + }; + _niceTooltip->pointAt(geometry, RectPart::Top, countPosition); +} + +void Panel::trackControls(bool track) { + if (_trackControls == track) { + return; + } + _trackControls = track; + if (!track) { + _trackControlsLifetime.destroy(); + _trackControlsOverStateLifetime.destroy(); + _trackControlsMenuLifetime.destroy(); + toggleWideControls(true); + if (_wideControlsAnimation.animating()) { + _wideControlsAnimation.stop(); + updateButtonsGeometry(); + } + return; + } + + const auto trackOne = [=](auto &&widget) { + trackControl(widget, _trackControlsOverStateLifetime); + }; + trackOne(_mute->outer()); + trackOne(_video); + trackOne(_screenShare); + trackOne(_wideMenu); + trackOne(_settings); + trackOne(_hangup); + trackOne(_controlsBackgroundWide); + trackControl(_menu, _trackControlsMenuLifetime); +} + +void Panel::updateControlsGeometry() { + if (widget()->size().isEmpty() || (!_settings && !_callShare)) { + return; + } + updateButtonsGeometry(); + updateMembersGeometry(); + refreshTitle(); + +#ifdef Q_OS_MAC + const auto controlsOnTheLeft = true; +#else // Q_OS_MAC + const auto controlsOnTheLeft = _controls->geometry().center().x() + < widget()->width() / 2; +#endif // Q_OS_MAC + const auto menux = st::groupCallMenuTogglePosition.x(); + const auto menuy = st::groupCallMenuTogglePosition.y(); + if (controlsOnTheLeft) { + if (_menuToggle) { + _menuToggle->moveToRight(menux, menuy); + } else if (_joinAsToggle) { + _joinAsToggle->moveToRight(menux, menuy); + } + } else { + if (_menuToggle) { + _menuToggle->moveToLeft(menux, menuy); + } else if (_joinAsToggle) { + _joinAsToggle->moveToLeft(menux, menuy); + } + } +} + +void Panel::updateButtonsGeometry() { + if (widget()->size().isEmpty() || (!_settings && !_callShare)) { + return; + } + const auto toggle = [](auto &widget, bool shown) { + if (widget && widget->isHidden() == shown) { + widget->setVisible(shown); + } + }; + if (mode() == PanelMode::Wide) { + Assert(_video != nullptr); + Assert(_screenShare != nullptr); + Assert(_wideMenu != nullptr); + Assert(_settings != nullptr); + Assert(_callShare == nullptr); + + const auto shown = _wideControlsAnimation.value( + _wideControlsShown ? 1. : 0.); + const auto hidden = (shown == 0.); + + if (_viewport) { + _viewport->setControlsShown(shown); + } + + const auto buttonsTop = widget()->height() - anim::interpolate( + 0, + st::groupCallButtonBottomSkipWide, + shown); + const auto addSkip = st::callMuteButtonSmall.active.outerRadius; + const auto muteSize = _mute->innerSize().width() + 2 * addSkip; + const auto skip = st::groupCallButtonSkipSmall; + const auto fullWidth = (_video->width() + skip) + + (_screenShare->width() + skip) + + (muteSize + skip) + + (_settings ->width() + skip) + + _hangup->width(); + const auto membersSkip = st::groupCallNarrowSkip; + const auto membersWidth = st::groupCallNarrowMembersWidth + + 2 * membersSkip; + auto left = membersSkip + (widget()->width() + - membersWidth + - membersSkip + - fullWidth) / 2; + toggle(_screenShare, !hidden); + _screenShare->moveToLeft(left, buttonsTop); + left += _screenShare->width() + skip; + toggle(_video, !hidden); + _video->moveToLeft(left, buttonsTop); + left += _video->width() + skip; + toggle(_mute, !hidden); + _mute->moveInner({ left + addSkip, buttonsTop + addSkip }); + left += muteSize + skip; + const auto wideMenuShown = _call->canManage() + || _call->showChooseJoinAs(); + toggle(_settings, !hidden && !wideMenuShown); + toggle(_wideMenu, !hidden && wideMenuShown); + _wideMenu->moveToLeft(left, buttonsTop); + _settings->moveToLeft(left, buttonsTop); + left += _settings->width() + skip; + toggle(_hangup, !hidden); + _hangup->moveToLeft(left, buttonsTop); + left += _hangup->width(); + if (_controlsBackgroundWide) { + const auto rect = QRect( + left - fullWidth, + buttonsTop, + fullWidth, + _hangup->height()); + _controlsBackgroundWide->setGeometry( + rect.marginsAdded(st::groupCallControlsBackMargin)); + } + } else { + const auto muteTop = widget()->height() + - st::groupCallMuteBottomSkip; + const auto buttonsTop = widget()->height() + - st::groupCallButtonBottomSkip; + const auto muteSize = _mute->innerSize().width(); + const auto fullWidth = muteSize + + 2 * (_settings ? _settings : _callShare)->width() + + 2 * st::groupCallButtonSkip; + toggle(_mute, true); + _mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop }); + const auto leftButtonLeft = (widget()->width() - fullWidth) / 2; + toggle(_screenShare, false); + toggle(_wideMenu, false); + toggle(_callShare, true); + if (_callShare) { + _callShare->moveToLeft(leftButtonLeft, buttonsTop); + } + const auto showVideoButton = videoButtonInNarrowMode(); + toggle(_video, !_callShare && showVideoButton); + if (_video) { + _video->setStyle(st::groupCallVideo, &st::groupCallVideoActive); + _video->moveToLeft(leftButtonLeft, buttonsTop); + } + toggle(_settings, !_callShare && !showVideoButton); + if (_settings) { + _settings->moveToLeft(leftButtonLeft, buttonsTop); + } + toggle(_hangup, true); + _hangup->moveToRight(leftButtonLeft, buttonsTop); + } + if (_controlsBackgroundNarrow) { + const auto left = st::groupCallMembersMargin.left(); + const auto width = (widget()->width() + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right()); + _controlsBackgroundNarrow->shadow.setGeometry( + left, + (widget()->height() + - st::groupCallMembersMargin.bottom() + - _controlsBackgroundNarrow->shadow.height()), + width, + _controlsBackgroundNarrow->shadow.height()); + _controlsBackgroundNarrow->blocker.setGeometry( + left, + (widget()->height() + - st::groupCallMembersMargin.bottom() + - st::groupCallMembersBottomSkip), + width, + st::groupCallMembersBottomSkip); + } + updateTooltipGeometry(); +} + +bool Panel::videoButtonInNarrowMode() const { + return (_video != nullptr) && !_call->mutedByAdmin(); +} + +void Panel::updateMembersGeometry() { + if (!_members) { + return; + } + const auto desiredHeight = _members->desiredHeight(); + if (mode() == PanelMode::Wide) { + const auto skip = st::groupCallNarrowSkip; + const auto membersWidth = st::groupCallNarrowMembersWidth; + const auto top = st::groupCallWideVideoTop; + _members->setGeometry( + widget()->width() - skip - membersWidth, + top, + membersWidth, + std::min(desiredHeight, widget()->height() - top - skip)); + _viewport->setGeometry({ + skip, + top, + widget()->width() - membersWidth - 3 * skip, + widget()->height() - top - skip, + }); + } else { + const auto membersBottom = widget()->height(); + const auto membersTop = st::groupCallMembersTop; + const auto availableHeight = membersBottom + - st::groupCallMembersMargin.bottom() + - membersTop; + const auto membersWidthAvailable = widget()->width() + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right(); + const auto membersWidthMin = st::groupCallWidth + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right(); + const auto membersWidth = std::clamp( + membersWidthAvailable, + membersWidthMin, + st::groupCallMembersWidthMax); + _members->setGeometry( + (widget()->width() - membersWidth) / 2, + membersTop, + membersWidth, + std::min(desiredHeight, availableHeight)); + } +} + +void Panel::refreshTitle() { + if (!_title) { + auto text = rpl::combine( + Info::Profile::NameValue(_peer), + rpl::single( + QString() + ) | rpl::then(_call->real( + ) | rpl::map([=](not_null real) { + return real->titleValue(); + }) | rpl::flatten_latest()) + ) | rpl::map([=]( + const TextWithEntities &name, + const QString &title) { + return title.isEmpty() ? name.text : title; + }) | rpl::after_next([=] { + refreshTitleGeometry(); + }); + _title.create( + widget(), + rpl::duplicate(text), + st::groupCallTitleLabel); + _title->show(); + _title->setAttribute(Qt::WA_TransparentForMouseEvents); + } + refreshTitleGeometry(); + if (!_subtitle && mode() == PanelMode::Default) { + _subtitle.create( + widget(), + rpl::single( + _call->scheduleDate() + ) | rpl::then( + _call->real( + ) | rpl::map([=](not_null real) { + return real->scheduleDateValue(); + }) | rpl::flatten_latest() + ) | rpl::map([=](TimeId scheduleDate) { + if (scheduleDate) { + return tr::lng_group_call_scheduled_status(); + } else if (!_members) { + setupMembers(); + } + return tr::lng_group_call_members( + lt_count_decimal, + _members->fullCountValue() | rpl::map([](int value) { + return (value > 0) ? float64(value) : 1.; + })); + }) | rpl::flatten_latest(), + st::groupCallSubtitleLabel); + _subtitle->show(); + _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + } + if (_subtitle) { + const auto middle = _title + ? (_title->x() + _title->width() / 2) + : (widget()->width() / 2); + const auto top = _title + ? st::groupCallSubtitleTop + : st::groupCallTitleTop; + _subtitle->moveToLeft( + (widget()->width() - _subtitle->width()) / 2, + top); + } +} + +void Panel::refreshTitleGeometry() { + if (!_title) { + return; + } + const auto fullRect = computeTitleRect(); + const auto recordingWidth = 2 * st::groupCallRecordingMarkSkip + + st::groupCallRecordingMark; + const auto titleRect = _recordingMark + ? QRect( + fullRect.x(), + fullRect.y(), + fullRect.width() - _recordingMark->width(), + fullRect.height()) + : fullRect; + const auto best = _title->naturalWidth(); + const auto from = (widget()->width() - best) / 2; + const auto top = (mode() == PanelMode::Default) + ? st::groupCallTitleTop + : (st::groupCallWideVideoTop + - st::groupCallTitleLabel.style.font->height) / 2; + const auto left = titleRect.x(); + if (from >= left && from + best <= left + titleRect.width()) { + _title->resizeToWidth(best); + _title->moveToLeft(from, top); + } else if (titleRect.width() < best) { + _title->resizeToWidth(titleRect.width()); + _title->moveToLeft(left, top); + } else if (from < left) { + _title->resizeToWidth(best); + _title->moveToLeft(left, top); + } else { + _title->resizeToWidth(best); + _title->moveToLeft(left + titleRect.width() - best, top); + } + if (_recordingMark) { + const auto markTop = top + st::groupCallRecordingMarkTop; + _recordingMark->move( + _title->x() + _title->width(), + markTop - st::groupCallRecordingMarkSkip); + } +} + +void Panel::paint(QRect clip) { + Painter p(widget()); + + auto region = QRegion(clip); + for (const auto rect : region) { + p.fillRect(rect, st::groupCallBg); + } +} + +bool Panel::handleClose() { + if (_call) { + window()->hide(); + return true; + } + return false; +} + +not_null Panel::window() const { + return _window.window(); +} + +not_null Panel::widget() const { + return _window.widget(); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h new file mode 100644 index 000000000..e1cd8dad3 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -0,0 +1,238 @@ +/* +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 "base/timer.h" +#include "base/object_ptr.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_choose_join_as.h" +#include "calls/group/ui/desktop_capture_choose_source.h" +#include "ui/effects/animations.h" +#include "ui/gl/gl_window.h" +#include "ui/rp_widget.h" + +class Image; + +namespace Data { +class PhotoMedia; +class CloudImageView; +class GroupCall; +} // namespace Data + +namespace Ui { +class AbstractButton; +class ImportantTooltip; +class DropdownMenu; +class CallButton; +class CallMuteButton; +class IconButton; +class FlatLabel; +class RpWidget; +template +class FadeWrap; +template +class PaddingWrap; +class ScrollArea; +class GenericBox; +class LayerManager; +class GroupCallScheduledLeft; +namespace Toast { +class Instance; +} // namespace Toast +namespace Platform { +class TitleControls; +} // namespace Platform +} // namespace Ui + +namespace style { +struct CallSignalBars; +struct CallBodyLayout; +} // namespace style + +namespace Calls::Group { + +class Toasts; +class Members; +class Viewport; +enum class PanelMode; +enum class StickedTooltip; + +class Panel final : private Ui::DesktopCapture::ChooseSourceDelegate { +public: + Panel(not_null call); + ~Panel(); + + [[nodiscard]] not_null call() const; + [[nodiscard]] bool isActive() const; + + void showToast(TextWithEntities &&text, crl::time duration = 0); + + void minimize(); + void close(); + void showAndActivate(); + void closeBeforeDestroy(); + + rpl::lifetime &lifetime(); + +private: + using State = GroupCall::State; + struct ControlsBackgroundNarrow; + + enum class NiceTooltipType { + Normal, + Sticked, + }; + enum class StickedTooltipHide { + Unavailable, + Activated, + Discarded, + }; + class MicLevelTester; + + [[nodiscard]] not_null window() const; + [[nodiscard]] not_null widget() const; + + [[nodiscard]] PanelMode mode() const; + + void paint(QRect clip); + + void initWindow(); + void initWidget(); + void initControls(); + void initShareAction(); + void initLayout(); + void initGeometry(); + void setupScheduledLabels(rpl::producer date); + void setupMembers(); + void setupVideo(not_null viewport); + void setupRealMuteButtonState(not_null real); + + bool handleClose(); + void startScheduledNow(); + void trackControls(bool track); + void raiseControls(); + void enlargeVideo(); + void minimizeVideo(); + + void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime); + void trackControlOver(not_null control, bool over); + void showNiceTooltip( + not_null control, + NiceTooltipType type = NiceTooltipType::Normal); + void showStickedTooltip(); + void hideStickedTooltip(StickedTooltipHide hide); + void hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide); + void hideNiceTooltip(); + + bool updateMode(); + void updateControlsGeometry(); + void updateButtonsGeometry(); + void updateTooltipGeometry(); + void updateButtonsStyles(); + void updateMembersGeometry(); + void refreshControlsBackground(); + void setupControlsBackgroundWide(); + void setupControlsBackgroundNarrow(); + void showControls(); + void refreshLeftButton(); + void refreshVideoButtons( + std::optional overrideWideMode = std::nullopt); + void refreshTopButton(); + void toggleWideControls(bool shown); + void updateWideControlsVisibility(); + [[nodiscard]] bool videoButtonInNarrowMode() const; + + void endCall(); + + void showMainMenu(); + void chooseJoinAs(); + void chooseShareScreenSource(); + void screenSharingPrivacyRequest(); + void addMembers(); + void kickParticipant(not_null participantPeer); + void kickParticipantSure(not_null participantPeer); + [[nodiscard]] QRect computeTitleRect() const; + void refreshTitle(); + void refreshTitleGeometry(); + void setupRealCallViewers(); + void subscribeToChanges(not_null real); + + void migrate(not_null channel); + void subscribeToPeerChanges(); + + QWidget *chooseSourceParent() override; + QString chooseSourceActiveDeviceId() override; + rpl::lifetime &chooseSourceInstanceLifetime() override; + void chooseSourceAccepted(const QString &deviceId) override; + void chooseSourceStop() override; + + const not_null _call; + not_null _peer; + + Ui::GL::Window _window; + const std::unique_ptr _layerBg; + rpl::variable _mode; + +#ifndef Q_OS_MAC + std::unique_ptr _controls; +#endif // !Q_OS_MAC + + rpl::lifetime _callLifetime; + + object_ptr _title = { nullptr }; + object_ptr _subtitle = { nullptr }; + object_ptr _recordingMark = { nullptr }; + object_ptr _menuToggle = { nullptr }; + object_ptr _menu = { nullptr }; + rpl::variable _wideMenuShown = false; + object_ptr _joinAsToggle = { nullptr }; + object_ptr _members = { nullptr }; + std::unique_ptr _viewport; + rpl::lifetime _trackControlsLifetime; + rpl::lifetime _trackControlsOverStateLifetime; + rpl::lifetime _trackControlsMenuLifetime; + object_ptr _startsIn = { nullptr }; + object_ptr _countdown = { nullptr }; + std::shared_ptr _countdownData; + object_ptr _startsWhen = { nullptr }; + ChooseJoinAsProcess _joinAsProcess; + std::optional _lastSmallGeometry; + std::optional _lastLargeGeometry; + bool _lastLargeMaximized = false; + bool _showWideControls = false; + bool _trackControls = false; + bool _wideControlsShown = false; + Ui::Animations::Simple _wideControlsAnimation; + + object_ptr _controlsBackgroundWide = { nullptr }; + std::unique_ptr _controlsBackgroundNarrow; + object_ptr _settings = { nullptr }; + object_ptr _wideMenu = { nullptr }; + object_ptr _callShare = { nullptr }; + object_ptr _video = { nullptr }; + object_ptr _screenShare = { nullptr }; + std::unique_ptr _mute; + object_ptr _hangup; + object_ptr _niceTooltip = { nullptr }; + QPointer _stickedTooltipClose; + QPointer _niceTooltipControl; + StickedTooltips _stickedTooltipsShown; + Fn _callShareLinkCallback; + + const std::unique_ptr _toasts; + base::weak_ptr _lastToast; + + std::unique_ptr _micLevelTester; + + rpl::lifetime _peerLifetime; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_group_settings.cpp b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp similarity index 96% rename from Telegram/SourceFiles/calls/calls_group_settings.cpp rename to Telegram/SourceFiles/calls/group/calls_group_settings.cpp index 9ae8bbcae..66087e309 100644 --- a/Telegram/SourceFiles/calls/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_settings.cpp @@ -5,13 +5,13 @@ 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 "calls/calls_group_settings.h" +#include "calls/group/calls_group_settings.h" -#include "calls/calls_group_call.h" -#include "calls/calls_group_menu.h" // LeaveBox. -#include "calls/calls_group_common.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_menu.h" // LeaveBox. +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_choose_join_as.h" #include "calls/calls_instance.h" -#include "calls/calls_choose_join_as.h" #include "ui/widgets/level_meter.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/buttons.h" @@ -306,6 +306,20 @@ void SettingsBox( //AddDivider(layout); //AddSkip(layout); + AddButton( + layout, + tr::lng_group_call_noise_suppression(), + st::groupCallSettingsButton + )->toggleOn(rpl::single( + settings.groupCallNoiseSuppression() + ))->toggledChanges( + ) | rpl::start_with_next([=](bool enabled) { + Core::App().settings().setGroupCallNoiseSuppression(enabled); + call->setNoiseSuppression(enabled); + Core::App().saveSettingsDelayed(); + }, layout->lifetime()); + + using GlobalShortcut = base::GlobalShortcut; struct PushToTalkState { rpl::variable recordText = tr::lng_group_call_ptt_shortcut(); @@ -479,8 +493,10 @@ void SettingsBox( tr::now, lt_delay, FormatDelay(delay))); - Core::App().settings().setGroupCallPushToTalkDelay(delay); - applyAndSave(); + if (Core::App().settings().groupCallPushToTalkDelay() != delay) { + Core::App().settings().setGroupCallPushToTalkDelay(delay); + applyAndSave(); + } }; callback(value); const auto slider = pushToTalkInner->add( diff --git a/Telegram/SourceFiles/calls/calls_group_settings.h b/Telegram/SourceFiles/calls/group/calls_group_settings.h similarity index 100% rename from Telegram/SourceFiles/calls/calls_group_settings.h rename to Telegram/SourceFiles/calls/group/calls_group_settings.h diff --git a/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp new file mode 100644 index 000000000..cf27a9a81 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_toasts.cpp @@ -0,0 +1,179 @@ +/* +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 "calls/group/calls_group_toasts.h" + +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_panel.h" +#include "data/data_peer.h" +#include "data/data_group_call.h" +#include "ui/text/text_utilities.h" +#include "ui/toasts/common_toasts.h" +#include "lang/lang_keys.h" + +namespace Calls::Group { +namespace { + +constexpr auto kErrorDuration = 2 * crl::time(1000); + +using State = GroupCall::State; + +} // namespace + +Toasts::Toasts(not_null panel) +: _panel(panel) +, _call(panel->call()) { + setup(); +} + +void Toasts::setup() { + setupJoinAsChanged(); + setupTitleChanged(); + setupRequestedToSpeak(); + setupAllowedToSpeak(); + setupPinnedVideo(); + setupError(); +} + +void Toasts::setupJoinAsChanged() { + _call->rejoinEvents( + ) | rpl::filter([](RejoinEvent event) { + return (event.wasJoinAs != event.nowJoinAs); + }) | rpl::map([=] { + return _call->stateValue() | rpl::filter([](State state) { + return (state == State::Joined); + }) | rpl::take(1); + }) | rpl::flatten_latest() | rpl::start_with_next([=] { + _panel->showToast(tr::lng_group_call_join_as_changed( + tr::now, + lt_name, + Ui::Text::Bold(_call->joinAs()->name), + Ui::Text::WithEntities)); + }, _lifetime); +} + +void Toasts::setupTitleChanged() { + _call->titleChanged( + ) | rpl::filter([=] { + return (_call->lookupReal() != nullptr); + }) | rpl::map([=] { + const auto peer = _call->peer(); + return peer->groupCall()->title().isEmpty() + ? peer->name + : peer->groupCall()->title(); + }) | rpl::start_with_next([=](const QString &title) { + _panel->showToast(tr::lng_group_call_title_changed( + tr::now, + lt_title, + Ui::Text::Bold(title), + Ui::Text::WithEntities)); + }, _lifetime); +} + +void Toasts::setupAllowedToSpeak() { + _call->allowedToSpeakNotifications( + ) | rpl::start_with_next([=] { + if (_panel->isActive()) { + _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({ + .text = tr::lng_group_call_can_speak( + tr::now, + lt_chat, + Ui::Text::Bold(name), + Ui::Text::WithEntities), + }); + } + }, _lifetime); +} + +void Toasts::setupPinnedVideo() { + _call->videoEndpointPinnedValue( + ) | rpl::map([=](bool pinned) { + return pinned + ? _call->videoEndpointLargeValue() + : rpl::single(_call->videoEndpointLarge()); + }) | rpl::flatten_latest( + ) | rpl::filter([=] { + return (_call->shownVideoTracks().size() > 1); + }) | rpl::start_with_next([=](const VideoEndpoint &endpoint) { + const auto pinned = _call->videoEndpointPinned(); + const auto peer = endpoint.peer; + if (!peer) { + return; + } + const auto text = [&] { + const auto me = (peer == _call->joinAs()); + const auto camera = (endpoint.type == VideoEndpointType::Camera); + if (me) { + const auto key = camera + ? (pinned + ? tr::lng_group_call_pinned_camera_me + : tr::lng_group_call_unpinned_camera_me) + : (pinned + ? tr::lng_group_call_pinned_screen_me + : tr::lng_group_call_unpinned_screen_me); + return key(tr::now); + } + const auto key = camera + ? (pinned + ? tr::lng_group_call_pinned_camera + : tr::lng_group_call_unpinned_camera) + : (pinned + ? tr::lng_group_call_pinned_screen + : tr::lng_group_call_unpinned_screen); + return key(tr::now, lt_user, peer->shortName()); + }(); + _panel->showToast({ text }); + }, _lifetime); +} + +void Toasts::setupRequestedToSpeak() { + _call->mutedValue( + ) | 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), + }); + } + }, _lifetime); +} + +void Toasts::setupError() { + _call->errors( + ) | rpl::start_with_next([=](Error error) { + const auto key = [&] { + switch (error) { + case Error::NoCamera: return tr::lng_call_error_no_camera; + case Error::CameraFailed: + return tr::lng_group_call_failed_camera; + case Error::ScreenFailed: + return tr::lng_group_call_failed_screen; + case Error::MutedNoCamera: + return tr::lng_group_call_muted_no_camera; + case Error::MutedNoScreen: + return tr::lng_group_call_muted_no_screen; + case Error::DisabledNoCamera: + return tr::lng_group_call_chat_no_camera; + case Error::DisabledNoScreen: + return tr::lng_group_call_chat_no_screen; + } + Unexpected("Error in Calls::Group::Toasts::setupErrorToasts."); + }(); + _panel->showToast({ key(tr::now) }, kErrorDuration); + }, _lifetime); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_toasts.h b/Telegram/SourceFiles/calls/group/calls_group_toasts.h new file mode 100644 index 000000000..32e214a99 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_toasts.h @@ -0,0 +1,38 @@ +/* +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 Calls { +class GroupCall; +} // namespace Calls + +namespace Calls::Group { + +class Panel; + +class Toasts final { +public: + explicit Toasts(not_null panel); + +private: + void setup(); + void setupJoinAsChanged(); + void setupTitleChanged(); + void setupRequestedToSpeak(); + void setupAllowedToSpeak(); + void setupPinnedVideo(); + void setupError(); + + const not_null _panel; + const not_null _call; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp new file mode 100644 index 000000000..c14aa0d77 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp @@ -0,0 +1,945 @@ +/* +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 "calls/group/calls_group_viewport.h" + +#include "calls/group/calls_group_viewport_tile.h" +#include "calls/group/calls_group_viewport_opengl.h" +#include "calls/group/calls_group_viewport_raster.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_members_row.h" +#include "media/view/media_view_pip.h" +#include "base/platform/base_platform_info.h" +#include "webrtc/webrtc_video_track.h" +#include "ui/painter.h" +#include "ui/abstract_button.h" +#include "ui/gl/gl_surface.h" +#include "ui/effects/animations.h" +#include "ui/effects/cross_line.h" +#include "data/data_group_call.h" // MuteButtonTooltip. +#include "lang/lang_keys.h" +#include "styles/style_calls.h" + +#include +#include + +namespace Calls::Group { +namespace { + +[[nodiscard]] QRect InterpolateRect(QRect a, QRect b, float64 ratio) { + const auto left = anim::interpolate(a.x(), b.x(), ratio); + const auto top = anim::interpolate(a.y(), b.y(), ratio); + const auto right = anim::interpolate( + a.x() + a.width(), + b.x() + b.width(), + ratio); + const auto bottom = anim::interpolate( + a.y() + a.height(), + b.y() + b.height(), + ratio); + return { left, top, right - left, bottom - top }; +} + +} // namespace + +Viewport::Viewport( + not_null parent, + PanelMode mode, + Ui::GL::Backend backend) +: _mode(mode) +, _content(Ui::GL::CreateSurface(parent, chooseRenderer(backend))) { + setup(); +} + +Viewport::~Viewport() = default; + +not_null Viewport::widget() const { + return _content->rpWidget(); +} + +not_null Viewport::rp() const { + return _content.get(); +} + +void Viewport::setup() { + const auto raw = widget(); + + raw->resize(0, 0); + raw->setAttribute(Qt::WA_OpaquePaintEvent); + raw->setMouseTracking(true); + + _content->sizeValue( + ) | rpl::filter([=] { + return wide(); + }) | rpl::start_with_next([=] { + updateTilesGeometry(); + }, lifetime()); + + _content->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Enter) { + Ui::Integration::Instance().registerLeaveSubscription(raw); + _mouseInside = true; + } else if (type == QEvent::Leave) { + Ui::Integration::Instance().unregisterLeaveSubscription(raw); + setSelected({}); + _mouseInside = false; + } else if (type == QEvent::MouseButtonPress) { + handleMousePress( + static_cast(e.get())->pos(), + static_cast(e.get())->button()); + } else if (type == QEvent::MouseButtonRelease) { + handleMouseRelease( + static_cast(e.get())->pos(), + static_cast(e.get())->button()); + } else if (type == QEvent::MouseMove) { + handleMouseMove(static_cast(e.get())->pos()); + } + }, lifetime()); +} + +void Viewport::setGeometry(QRect geometry) { + Expects(wide()); + + if (widget()->geometry() != geometry) { + _geometryStaleAfterModeChange = false; + widget()->setGeometry(geometry); + } else if (_geometryStaleAfterModeChange) { + _geometryStaleAfterModeChange = false; + updateTilesGeometry(); + } +} + +void Viewport::resizeToWidth(int width) { + Expects(!wide()); + + updateTilesGeometry(width); +} + +void Viewport::setScrollTop(int scrollTop) { + if (_scrollTop == scrollTop) { + return; + } + _scrollTop = scrollTop; + updateTilesGeometry(); +} + +bool Viewport::wide() const { + return (_mode == PanelMode::Wide); +} + +void Viewport::setMode(PanelMode mode, not_null parent) { + if (_mode == mode && widget()->parent() == parent) { + return; + } + _mode = mode; + _scrollTop = 0; + setControlsShown(1.); + if (widget()->parent() != parent) { + const auto hidden = widget()->isHidden(); + widget()->setParent(parent); + if (!hidden) { + widget()->show(); + } + } + if (!wide()) { + for (const auto &tile : _tiles) { + tile->toggleTopControlsShown(false); + } + } else if (_selected.tile) { + _selected.tile->toggleTopControlsShown(true); + } +} + +void Viewport::handleMousePress(QPoint position, Qt::MouseButton button) { + handleMouseMove(position); + setPressed(_selected); +} + +void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) { + handleMouseMove(position); + const auto pressed = _pressed; + setPressed({}); + if (const auto tile = pressed.tile) { + if (pressed == _selected) { + if (button == Qt::RightButton) { + tile->row()->showContextMenu(); + } else if (!wide() + || (_hasTwoOrMore && !_large) + || pressed.element != Selection::Element::PinButton) { + _clicks.fire_copy(tile->endpoint()); + } else if (pressed.element == Selection::Element::PinButton) { + _pinToggles.fire(!tile->pinned()); + } + } + } +} + +void Viewport::handleMouseMove(QPoint position) { + updateSelected(position); +} + +void Viewport::updateSelected(QPoint position) { + if (!widget()->rect().contains(position)) { + setSelected({}); + return; + } + for (const auto &tile : _tiles) { + const auto geometry = tile->visible() + ? tile->geometry() + : QRect(); + if (geometry.contains(position)) { + const auto pin = wide() + && tile->pinOuter().contains(position - geometry.topLeft()); + const auto back = wide() + && tile->backOuter().contains(position - geometry.topLeft()); + setSelected({ + .tile = tile.get(), + .element = (pin + ? Selection::Element::PinButton + : back + ? Selection::Element::BackButton + : Selection::Element::Tile), + }); + return; + } + } + setSelected({}); +} + +void Viewport::updateSelected() { + updateSelected(widget()->mapFromGlobal(QCursor::pos())); +} + +void Viewport::setControlsShown(float64 shown) { + _controlsShownRatio = shown; + widget()->update(); +} + +void Viewport::add( + const VideoEndpoint &endpoint, + VideoTileTrack track, + rpl::producer trackSize, + rpl::producer pinned) { + _tiles.push_back(std::make_unique( + endpoint, + track, + std::move(trackSize), + std::move(pinned), + [=] { widget()->update(); })); + + _tiles.back()->trackSizeValue( + ) | rpl::filter([](QSize size) { + return !size.isEmpty(); + }) | rpl::start_with_next([=] { + updateTilesGeometry(); + }, _tiles.back()->lifetime()); + + _tiles.back()->track()->stateValue( + ) | rpl::start_with_next([=] { + updateTilesGeometry(); + }, _tiles.back()->lifetime()); +} + +void Viewport::remove(const VideoEndpoint &endpoint) { + const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint); + if (i == end(_tiles)) { + return; + } + const auto removing = i->get(); + const auto largeRemoved = (_large == removing); + if (largeRemoved) { + prepareLargeChangeAnimation(); + _large = nullptr; + } + if (_selected.tile == removing) { + setSelected({}); + } + if (_pressed.tile == removing) { + setPressed({}); + } + for (auto &geometry : _startTilesLayout.list) { + if (geometry.tile == removing) { + geometry.tile = nullptr; + } + } + for (auto &geometry : _finishTilesLayout.list) { + if (geometry.tile == removing) { + geometry.tile = nullptr; + } + } + _tiles.erase(i); + if (largeRemoved) { + startLargeChangeAnimation(); + } else { + updateTilesGeometry(); + } +} + +void Viewport::prepareLargeChangeAnimation() { + if (!wide()) { + return; + } else if (_largeChangeAnimation.animating()) { + updateTilesAnimated(); + const auto field = _finishTilesLayout.useColumns + ? &Geometry::columns + : &Geometry::rows; + for (auto &finish : _finishTilesLayout.list) { + const auto tile = finish.tile; + if (!tile) { + continue; + } + finish.*field = tile->geometry(); + } + _startTilesLayout = std::move(_finishTilesLayout); + _largeChangeAnimation.stop(); + + _startTilesLayout.list.erase( + ranges::remove(_startTilesLayout.list, nullptr, &Geometry::tile), + end(_startTilesLayout.list)); + } else { + _startTilesLayout = applyLarge(std::move(_startTilesLayout)); + } +} + +void Viewport::startLargeChangeAnimation() { + Expects(!_largeChangeAnimation.animating()); + + if (!wide() + || anim::Disabled() + || (_startTilesLayout.list.size() < 2) + || !_opengl + || widget()->size().isEmpty()) { + updateTilesGeometry(); + return; + } + _finishTilesLayout = applyLarge( + countWide(widget()->width(), widget()->height())); + if (_finishTilesLayout.list.empty() + || _finishTilesLayout.outer != _startTilesLayout.outer) { + updateTilesGeometry(); + return; + } + _largeChangeAnimation.start( + [=] { updateTilesAnimated(); }, + 0., + 1., + st::slideDuration); +} + +Viewport::Layout Viewport::applyLarge(Layout layout) const { + auto &list = layout.list; + if (!_large) { + return layout; + } + const auto i = ranges::find(list, _large, &Geometry::tile); + if (i == end(list)) { + return layout; + } + const auto field = layout.useColumns + ? &Geometry::columns + : &Geometry::rows; + const auto fullWidth = layout.outer.width(); + const auto fullHeight = layout.outer.height(); + const auto largeRect = (*i).*field; + const auto largeLeft = largeRect.x(); + const auto largeTop = largeRect.y(); + const auto largeRight = largeLeft + largeRect.width(); + const auto largeBottom = largeTop + largeRect.height(); + const auto largeCenter = largeRect.center(); + for (auto &geometry : list) { + if (geometry.tile == _large) { + geometry.*field = { QPoint(), layout.outer }; + } else if (layout.useColumns) { + auto &rect = geometry.columns; + const auto center = rect.center(); + if (center.x() < largeLeft) { + rect = rect.translated(-largeLeft, 0); + } else if (center.x() > largeRight) { + rect = rect.translated(fullWidth - largeRight, 0); + } else if (center.y() < largeTop) { + rect = QRect( + 0, + rect.y() - largeTop, + fullWidth, + rect.height()); + } else if (center.y() > largeBottom) { + rect = QRect( + 0, + rect.y() + (fullHeight - largeBottom), + fullWidth, + rect.height()); + } + } else { + auto &rect = geometry.rows; + const auto center = rect.center(); + if (center.y() < largeTop) { + rect = rect.translated(0, -largeTop); + } else if (center.y() > largeBottom) { + rect = rect.translated(0, fullHeight - largeBottom); + } else if (center.x() < largeLeft) { + rect = QRect( + rect.x() - largeLeft, + 0, + rect.width(), + fullHeight); + } else { + rect = QRect( + rect.x() + (fullWidth - largeRight), + 0, + rect.width(), + fullHeight); + } + } + } + return layout; +} + +void Viewport::updateTilesAnimated() { + if (!_largeChangeAnimation.animating()) { + updateTilesGeometry(); + return; + } + const auto ratio = _largeChangeAnimation.value(1.); + const auto field = _finishTilesLayout.useColumns + ? &Geometry::columns + : &Geometry::rows; + for (const auto &finish : _finishTilesLayout.list) { + const auto tile = finish.tile; + if (!tile) { + continue; + } + const auto i = ranges::find( + _startTilesLayout.list, + tile, + &Geometry::tile); + if (i == end(_startTilesLayout.list)) { + LOG(("Tiles Animation Error 1!")); + _largeChangeAnimation.stop(); + updateTilesGeometry(); + return; + } + const auto from = (*i).*field; + const auto to = finish.*field; + tile->setGeometry( + InterpolateRect(from, to, ratio), + TileAnimation{ from.size(), to.size(), ratio }); + } + widget()->update(); +} + +Viewport::Layout Viewport::countWide(int outerWidth, int outerHeight) const { + auto result = Layout{ .outer = QSize(outerWidth, outerHeight) }; + auto &sizes = result.list; + sizes.reserve(_tiles.size()); + for (const auto &tile : _tiles) { + const auto video = tile.get(); + const auto size = video->trackOrUserpicSize(); + if (!size.isEmpty()) { + sizes.push_back(Geometry{ video, size }); + } + } + if (sizes.empty()) { + return result; + } else if (sizes.size() == 1) { + sizes.front().rows = { 0, 0, outerWidth, outerHeight }; + return result; + } + + auto columnsBlack = uint64(); + auto rowsBlack = uint64(); + const auto count = int(sizes.size()); + const auto skip = st::groupCallVideoLargeSkip; + const auto slices = int(std::ceil(std::sqrt(float64(count)))); + { + auto index = 0; + const auto columns = slices; + const auto sizew = (outerWidth + skip) / float64(columns); + for (auto column = 0; column != columns; ++column) { + const auto left = int(std::round(column * sizew)); + const auto width = int(std::round(column * sizew + sizew - skip)) + - left; + const auto rows = int(std::round((count - index) + / float64(columns - column))); + const auto sizeh = (outerHeight + skip) / float64(rows); + for (auto row = 0; row != rows; ++row) { + const auto top = int(std::round(row * sizeh)); + const auto height = int(std::round( + row * sizeh + sizeh - skip)) - top; + auto &geometry = sizes[index]; + geometry.columns = { + left, + top, + width, + height }; + const auto scaled = geometry.size.scaled( + width, + height, + Qt::KeepAspectRatio); + columnsBlack += (scaled.width() < width) + ? (width - scaled.width()) * height + : (height - scaled.height()) * width; + ++index; + } + } + } + { + auto index = 0; + const auto rows = slices; + const auto sizeh = (outerHeight + skip) / float64(rows); + for (auto row = 0; row != rows; ++row) { + const auto top = int(std::round(row * sizeh)); + const auto height = int(std::round(row * sizeh + sizeh - skip)) + - top; + const auto columns = int(std::round((count - index) + / float64(rows - row))); + const auto sizew = (outerWidth + skip) / float64(columns); + for (auto column = 0; column != columns; ++column) { + const auto left = int(std::round(column * sizew)); + const auto width = int(std::round( + column * sizew + sizew - skip)) - left; + auto &geometry = sizes[index]; + geometry.rows = { + left, + top, + width, + height }; + const auto scaled = geometry.size.scaled( + width, + height, + Qt::KeepAspectRatio); + rowsBlack += (scaled.width() < width) + ? (width - scaled.width()) * height + : (height - scaled.height()) * width; + ++index; + } + } + } + result.useColumns = (columnsBlack < rowsBlack); + return result; +} + +void Viewport::showLarge(const VideoEndpoint &endpoint) { + // If a video get's switched off, GroupCall first unpins it, + // then removes it from Large endpoint, then removes from active tracks. + // + // If we want to animate large video removal properly, we need to + // delay this update and start animation directly from removing of the + // track from the active list. Otherwise final state won't be correct. + _updateLargeScheduled = [=] { + const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint); + const auto large = (i != end(_tiles)) ? i->get() : nullptr; + if (_large != large) { + prepareLargeChangeAnimation(); + _large = large; + updateTopControlsVisibility(); + startLargeChangeAnimation(); + } + + Ensures(!_large || !_large->trackOrUserpicSize().isEmpty()); + }; + crl::on_main(widget(), [=] { + if (!_updateLargeScheduled) { + return; + } + base::take(_updateLargeScheduled)(); + }); +} + +void Viewport::updateTilesGeometry() { + updateTilesGeometry(widget()->width()); +} + +void Viewport::updateTilesGeometry(int outerWidth) { + const auto mouseInside = _mouseInside.current(); + const auto guard = gsl::finally([&] { + if (mouseInside) { + updateSelected(); + } + widget()->update(); + }); + + const auto outerHeight = widget()->height(); + if (_tiles.empty() || !outerWidth) { + _fullHeight = 0; + return; + } + + if (wide()) { + updateTilesGeometryWide(outerWidth, outerHeight); + refreshHasTwoOrMore(); + _fullHeight = 0; + } else { + updateTilesGeometryNarrow(outerWidth); + } +} + +void Viewport::refreshHasTwoOrMore() { + auto hasTwoOrMore = false; + auto oneFound = false; + for (const auto &tile : _tiles) { + if (!tile->trackOrUserpicSize().isEmpty()) { + if (oneFound) { + hasTwoOrMore = true; + break; + } + oneFound = true; + } + } + if (_hasTwoOrMore == hasTwoOrMore) { + return; + } + _hasTwoOrMore = hasTwoOrMore; + updateCursor(); + updateTopControlsVisibility(); +} + +void Viewport::updateTopControlsVisibility() { + if (_selected.tile) { + _selected.tile->toggleTopControlsShown( + _hasTwoOrMore && wide() && _large && _large == _selected.tile); + } +} + +void Viewport::updateTilesGeometryWide(int outerWidth, int outerHeight) { + if (!outerHeight) { + return; + } else if (_largeChangeAnimation.animating()) { + if (_startTilesLayout.outer == QSize(outerWidth, outerHeight)) { + return; + } + _largeChangeAnimation.stop(); + } + + _startTilesLayout = countWide(outerWidth, outerHeight); + if (_large && !_large->trackOrUserpicSize().isEmpty()) { + for (const auto &geometry : _startTilesLayout.list) { + if (geometry.tile == _large) { + setTileGeometry(_large, { 0, 0, outerWidth, outerHeight }); + } else { + geometry.tile->hide(); + } + } + } else { + const auto field = _startTilesLayout.useColumns + ? &Geometry::columns + : &Geometry::rows; + for (const auto &geometry : _startTilesLayout.list) { + if (const auto video = geometry.tile) { + setTileGeometry(video, geometry.*field); + } + } + } +} + +void Viewport::updateTilesGeometryNarrow(int outerWidth) { + if (outerWidth <= st::groupCallNarrowMembersWidth) { + updateTilesGeometryColumn(outerWidth); + return; + } + + const auto y = -_scrollTop; + auto sizes = base::flat_map, QSize>(); + sizes.reserve(_tiles.size()); + for (const auto &tile : _tiles) { + const auto video = tile.get(); + const auto size = video->trackOrUserpicSize(); + if (size.isEmpty()) { + video->hide(); + } else { + sizes.emplace(video, size); + } + } + if (sizes.empty()) { + _fullHeight = 0; + return; + } else if (sizes.size() == 1) { + const auto size = sizes.front().second; + const auto heightMin = (outerWidth * 9) / 16; + const auto heightMax = (outerWidth * 3) / 4; + const auto scaled = size.scaled( + QSize(outerWidth, heightMax), + Qt::KeepAspectRatio); + const auto height = std::max(scaled.height(), heightMin); + const auto skip = st::groupCallVideoSmallSkip; + setTileGeometry(sizes.front().first, { 0, y, outerWidth, height }); + _fullHeight = height + skip; + return; + } + const auto min = (st::groupCallWidth + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right() + - st::groupCallVideoSmallSkip) / 2; + const auto square = (outerWidth - st::groupCallVideoSmallSkip) / 2; + const auto skip = (outerWidth - 2 * square); + const auto put = [&](not_null tile, int column, int row) { + setTileGeometry(tile, { + (column == 2) ? 0 : column ? (outerWidth - square) : 0, + y + row * (min + skip), + (column == 2) ? outerWidth : square, + min, + }); + }; + const auto rows = (sizes.size() + 1) / 2; + if (sizes.size() == 3) { + put(sizes.front().first, 2, 0); + put((sizes.begin() + 1)->first, 0, 1); + put((sizes.begin() + 2)->first, 1, 1); + } else { + auto row = 0; + auto column = 0; + for (const auto &[video, endpoint] : sizes) { + put(video, column, row); + if (column) { + ++row; + column = (row + 1 == rows && sizes.size() % 2) ? 2 : 0; + } else { + column = 1; + } + } + } + _fullHeight = rows * (min + skip); +} + +void Viewport::updateTilesGeometryColumn(int outerWidth) { + const auto y = -_scrollTop; + auto top = 0; + const auto layoutNext = [&](not_null tile) { + const auto size = tile->trackOrUserpicSize(); + const auto shown = !size.isEmpty() && _large && tile != _large; + const auto height = st::groupCallNarrowVideoHeight; + if (!shown) { + tile->hide(); + } else { + setTileGeometry(tile, { 0, y + top, outerWidth, height }); + top += height + st::groupCallVideoSmallSkip; + } + }; + const auto topPeer = _large ? _large->row()->peer().get() : nullptr; + const auto reorderNeeded = [&] { + if (!_large) { + return false; + } + for (const auto &tile : _tiles) { + if (tile.get() != _large && tile->row()->peer() == topPeer) { + return (tile.get() != _tiles.front().get()) + && !tile->trackOrUserpicSize().isEmpty(); + } + } + return false; + }(); + if (reorderNeeded) { + _tilesForOrder.clear(); + _tilesForOrder.reserve(_tiles.size()); + for (const auto &tile : _tiles) { + _tilesForOrder.push_back(tile.get()); + } + ranges::stable_partition( + _tilesForOrder, + [&](not_null tile) { + return (tile->row()->peer() == topPeer); + }); + for (const auto &tile : _tilesForOrder) { + layoutNext(tile); + } + } else { + for (const auto &tile : _tiles) { + layoutNext(tile.get()); + } + } + _fullHeight = top; +} + +void Viewport::setTileGeometry(not_null tile, QRect geometry) { + tile->setGeometry(geometry); + + const auto min = std::min(geometry.width(), geometry.height()); + const auto kMedium = style::ConvertScale(540); + const auto kSmall = style::ConvertScale(240); + const auto &endpoint = tile->endpoint(); + const auto forceThumbnailQuality = !wide() + && (ranges::count(_tiles, false, &VideoTile::hidden) > 1); + const auto forceFullQuality = wide() && (tile.get() == _large); + const auto quality = forceThumbnailQuality + ? VideoQuality::Thumbnail + : (forceFullQuality || min >= kMedium) + ? VideoQuality::Full + : (min >= kSmall) + ? VideoQuality::Medium + : VideoQuality::Thumbnail; + if (tile->updateRequestedQuality(quality)) { + _qualityRequests.fire(VideoQualityRequest{ + .endpoint = endpoint, + .quality = quality, + }); + } +} + +void Viewport::setSelected(Selection value) { + if (_selected == value) { + return; + } + if (_selected.tile) { + _selected.tile->toggleTopControlsShown(false); + } + _selected = value; + updateTopControlsVisibility(); + updateCursor(); +} + +void Viewport::updateCursor() { + const auto pointer = _selected.tile && (!wide() || _hasTwoOrMore); + widget()->setCursor(pointer ? style::cur_pointer : style::cur_default); +} + +void Viewport::setPressed(Selection value) { + if (_pressed == value) { + return; + } + _pressed = value; +} + +Ui::GL::ChosenRenderer Viewport::chooseRenderer(Ui::GL::Backend backend) { + _opengl = (backend == Ui::GL::Backend::OpenGL); + return { + .renderer = (_opengl + ? std::unique_ptr( + std::make_unique(this)) + : std::make_unique(this)), + .backend = backend, + }; +} + +bool Viewport::requireARGB32() const { + return !_opengl; +} + +int Viewport::fullHeight() const { + return _fullHeight.current(); +} + +rpl::producer Viewport::fullHeightValue() const { + return _fullHeight.value(); +} + +rpl::producer Viewport::pinToggled() const { + return _pinToggles.events(); +} + +rpl::producer Viewport::clicks() const { + return _clicks.events(); +} + +rpl::producer Viewport::qualityRequests() const { + return _qualityRequests.events(); +} + +rpl::producer Viewport::mouseInsideValue() const { + return _mouseInside.value(); +} + +rpl::lifetime &Viewport::lifetime() { + return _content->lifetime(); +} + +QImage GenerateShadow( + int height, + int topAlpha, + int bottomAlpha, + QColor color) { + Expects(topAlpha >= 0 && topAlpha < 256); + Expects(bottomAlpha >= 0 && bottomAlpha < 256); + Expects(height * style::DevicePixelRatio() < 65536); + + const auto base = (uint32(color.red()) << 16) + | (uint32(color.green()) << 8) + | uint32(color.blue()); + const auto premultiplied = (topAlpha == bottomAlpha) || !base; + auto result = QImage( + QSize(1, height * style::DevicePixelRatio()), + (premultiplied + ? QImage::Format_ARGB32_Premultiplied + : QImage::Format_ARGB32)); + if (topAlpha == bottomAlpha) { + color.setAlpha(topAlpha); + result.fill(color); + return result; + } + constexpr auto kShift = 16; + constexpr auto kMultiply = (1U << kShift); + const auto values = std::abs(topAlpha - bottomAlpha); + const auto rows = uint32(result.height()); + const auto step = (values * kMultiply) / (rows - 1); + const auto till = rows * uint32(step); + Assert(result.bytesPerLine() == sizeof(uint32)); + auto ints = reinterpret_cast(result.bits()); + if (topAlpha < bottomAlpha) { + for (auto i = uint32(0); i != till; i += step) { + *ints++ = base | ((topAlpha + (i >> kShift)) << 24); + } + } else { + for (auto i = uint32(0); i != till; i += step) { + *ints++ = base | ((topAlpha - (i >> kShift)) << 24); + } + } + if (!premultiplied) { + result = std::move(result).convertToFormat( + QImage::Format_ARGB32_Premultiplied); + } + return result; +} + +rpl::producer MuteButtonTooltip(not_null call) { + //return rpl::single(std::make_tuple( + // (Data::GroupCall*)nullptr, + // call->scheduleDate() + //)) | rpl::then(call->real( + //) | rpl::map([](not_null real) { + // using namespace rpl::mappers; + // return real->scheduleDateValue( + // ) | rpl::map([=](TimeId scheduleDate) { + // return std::make_tuple(real.get(), scheduleDate); + // }); + //}) | rpl::flatten_latest( + //)) | rpl::map([=]( + // Data::GroupCall *real, + // TimeId scheduleDate) -> rpl::producer { + // if (scheduleDate) { + // return rpl::combine( + // call->canManageValue(), + // (real + // ? real->scheduleStartSubscribedValue() + // : rpl::single(false)) + // ) | rpl::map([](bool canManage, bool subscribed) { + // return canManage + // ? tr::lng_group_call_start_now() + // : subscribed + // ? tr::lng_group_call_cancel_reminder() + // : tr::lng_group_call_set_reminder(); + // }) | rpl::flatten_latest(); + // } + return call->mutedValue( + ) | rpl::map([](MuteState muted) { + switch (muted) { + case MuteState::Active: + case MuteState::PushToTalk: + return tr::lng_group_call_you_are_live(); + case MuteState::ForceMuted: + return tr::lng_group_call_tooltip_force_muted(); + case MuteState::RaisedHand: + return tr::lng_group_call_tooltip_raised_hand(); + case MuteState::Muted: + return tr::lng_group_call_tooltip_microphone(); + } + Unexpected("Value in MuteState in showNiceTooltip."); + }) | rpl::flatten_latest(); + //}) | rpl::flatten_latest(); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.h b/Telegram/SourceFiles/calls/group/calls_group_viewport.h new file mode 100644 index 000000000..a26e0a811 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.h @@ -0,0 +1,204 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" +#include "ui/effects/animations.h" + +namespace Ui { +class AbstractButton; +class RpWidgetWrap; +namespace GL { +enum class Backend; +struct Capabilities; +struct ChosenRenderer; +} // namespace GL +} // namespace Ui + +namespace Calls { +class GroupCall; +struct VideoEndpoint; +struct VideoQualityRequest; +} // namespace Calls + +namespace Webrtc { +class VideoTrack; +} // namespace Webrtc + +namespace Calls::Group { + +class MembersRow; +enum class PanelMode; +enum class VideoQuality; + +struct VideoTileTrack { + Webrtc::VideoTrack *track = nullptr; + MembersRow *row = nullptr; + rpl::variable trackSize; + + [[nodiscard]] explicit operator bool() const { + return track != nullptr; + } +}; + +[[nodiscard]] inline bool operator==( + VideoTileTrack a, + VideoTileTrack b) noexcept { + return (a.track == b.track) && (a.row == b.row); +} + +[[nodiscard]] inline bool operator!=( + VideoTileTrack a, + VideoTileTrack b) noexcept { + return !(a == b); +} + +class Viewport final { +public: + Viewport( + not_null parent, + PanelMode mode, + Ui::GL::Backend backend); + ~Viewport(); + + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null rp() const; + + void setMode(PanelMode mode, not_null parent); + void setControlsShown(float64 shown); + void setGeometry(QRect geometry); + void resizeToWidth(int newWidth); + void setScrollTop(int scrollTop); + + void add( + const VideoEndpoint &endpoint, + VideoTileTrack track, + rpl::producer trackSize, + rpl::producer pinned); + void remove(const VideoEndpoint &endpoint); + void showLarge(const VideoEndpoint &endpoint); + + [[nodiscard]] bool requireARGB32() const; + [[nodiscard]] int fullHeight() const; + [[nodiscard]] rpl::producer fullHeightValue() const; + [[nodiscard]] rpl::producer pinToggled() const; + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer qualityRequests() const; + [[nodiscard]] rpl::producer mouseInsideValue() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + + static constexpr auto kShadowMaxAlpha = 80; + +private: + struct Textures; + class VideoTile; + class RendererSW; + class RendererGL; + using TileId = quintptr; + + struct Geometry { + VideoTile *tile = nullptr; + QSize size; + QRect rows; + QRect columns; + }; + + struct Layout { + std::vector list; + QSize outer; + bool useColumns = false; + }; + + struct TileAnimation { + QSize from; + QSize to; + float64 ratio = -1.; + }; + + struct Selection { + enum class Element { + None, + Tile, + PinButton, + BackButton, + }; + VideoTile *tile = nullptr; + Element element = Element::None; + + inline bool operator==(Selection other) const { + return (tile == other.tile) && (element == other.element); + } + }; + + void setup(); + [[nodiscard]] bool wide() const; + + void updateCursor(); + void updateTilesGeometry(); + void updateTilesGeometry(int outerWidth); + void updateTilesGeometryWide(int outerWidth, int outerHeight); + void updateTilesGeometryNarrow(int outerWidth); + void updateTilesGeometryColumn(int outerWidth); + void setTileGeometry(not_null tile, QRect geometry); + void refreshHasTwoOrMore(); + void updateTopControlsVisibility(); + + void prepareLargeChangeAnimation(); + void startLargeChangeAnimation(); + void updateTilesAnimated(); + [[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const; + [[nodiscard]] Layout applyLarge(Layout layout) const; + + void setSelected(Selection value); + void setPressed(Selection value); + + void handleMousePress(QPoint position, Qt::MouseButton button); + void handleMouseRelease(QPoint position, Qt::MouseButton button); + void handleMouseMove(QPoint position); + void updateSelected(QPoint position); + void updateSelected(); + + [[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer( + Ui::GL::Backend backend); + + PanelMode _mode = PanelMode(); + bool _opengl = false; + bool _geometryStaleAfterModeChange = false; + const std::unique_ptr _content; + std::vector> _tiles; + std::vector> _tilesForOrder; + rpl::variable _fullHeight = 0; + bool _hasTwoOrMore = false; + int _scrollTop = 0; + QImage _shadow; + rpl::event_stream _clicks; + rpl::event_stream _pinToggles; + rpl::event_stream _qualityRequests; + float64 _controlsShownRatio = 1.; + VideoTile *_large = nullptr; + Fn _updateLargeScheduled; + Ui::Animations::Simple _largeChangeAnimation; + Layout _startTilesLayout; + Layout _finishTilesLayout; + Selection _selected; + Selection _pressed; + rpl::variable _mouseInside = false; + +}; + +[[nodiscard]] QImage GenerateShadow( + int height, + int topAlpha, + int bottomAlpha, + QColor color = QColor(0, 0, 0)); + +[[nodiscard]] rpl::producer MuteButtonTooltip( + not_null call); + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp new file mode 100644 index 000000000..f2cf335ba --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -0,0 +1,1465 @@ +/* +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 "calls/group/calls_group_viewport_opengl.h" + +#include "calls/group/calls_group_viewport_tile.h" +#include "webrtc/webrtc_video_track.h" +#include "media/view/media_view_pip.h" +#include "calls/group/calls_group_members_row.h" +#include "lang/lang_keys.h" +#include "ui/gl/gl_shader.h" +#include "data/data_peer.h" +#include "styles/style_calls.h" + +#include + +namespace Calls::Group { +namespace { + +using namespace Ui::GL; + +constexpr auto kScaleForBlurTextureIndex = 3; +constexpr auto kFirstBlurPassTextureIndex = 4; +constexpr auto kNoiseTextureSize = 256; + +// The more the scale - more blurred the image. +constexpr auto kBlurTextureSizeFactor = 4.; +constexpr auto kBlurOpacity = 0.65; +constexpr auto kDitherNoiseAmount = 0.002; +constexpr auto kMinCameraVisiblePart = 0.75; + +constexpr auto kQuads = 9; +constexpr auto kQuadVertices = kQuads * 4; +constexpr auto kQuadValues = kQuadVertices * 4; +constexpr auto kValues = kQuadValues + 8; // Blur texture coordinates. + +[[nodiscard]] ShaderPart FragmentBlurTexture( + bool vertical, + char prefix = 'v') { + const auto offsets = (vertical ? QString("0, 1") : QString("1, 0")); + const auto name = prefix + QString("_texcoord"); + return { + .header = R"( +varying vec2 )" + name + R"(; +uniform sampler2D b_texture; +uniform float texelOffset; +const vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722); +const vec2 offsets = vec2()" + offsets + R"(); +const int radius = 15; +const int diameter = 2 * radius + 1; +)", + .body = R"( + vec4 accumulated = vec4(0.); + for (int i = 0; i != diameter; i++) { + float stepOffset = float(i - radius) * texelOffset; + vec2 offset = vec2(stepOffset) * offsets; + vec4 sampled = vec4(texture2D(b_texture, )" + name + R"( + offset)); + float fradius = float(radius); + float boxWeight = fradius + 1.0 - abs(float(i) - fradius); + accumulated += sampled * boxWeight; + } + vec3 blurred = accumulated.rgb / accumulated.a; + float satLuminance = dot(blurred, satLuminanceWeighting); + vec3 mixinColor = vec3(satLuminance); + result = vec4(clamp(mix(mixinColor, blurred, 1.1), 0.0, 1.0), 1.0); +)", + }; +} + +[[nodiscard]] ShaderPart FragmentGenerateNoise() { + const auto size = QString::number(kNoiseTextureSize); + return { + .header = R"( +const float permTexUnit = 1.0 / )" + size + R"(.0; +const float permTexUnitHalf = 0.5 / )" + size + R"(.0; +const float grainsize = 1.3; +const float noiseCoordRotation = 1.425; +const vec2 dimensions = vec2()" + size + ", " + size + R"(); + +vec4 rnm(vec2 tc) { + float noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453; + return vec4( + fract(noise), + fract(noise * 1.2154), + fract(noise * 1.3453), + fract(noise * 1.3647) + ) * 2.0 - 1.0; +} + +float fade(float t) { + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + +float pnoise3D(vec3 p) { + vec3 pi = permTexUnit * floor(p) + permTexUnitHalf; + vec3 pf = fract(p); + float perm = rnm(pi.xy).a; + float n000 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf); + float n001 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 0.0, 1.0)); + perm = rnm(pi.xy + vec2(0.0, permTexUnit)).a; + float n010 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 1.0, 0.0)); + float n011 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 1.0, 1.0)); + perm = rnm(pi.xy + vec2(permTexUnit, 0.0)).a; + float n100 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 0.0, 0.0)); + float n101 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 0.0, 1.0)); + perm = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a; + float n110 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 1.0, 0.0)); + float n111 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 1.0, 1.0)); + vec4 n_x = mix( + vec4(n000, n001, n010, n011), + vec4(n100, n101, n110, n111), + fade(pf.x)); + vec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y)); + return mix(n_xy.x, n_xy.y, fade(pf.z)); +} + +vec2 rotateTexCoords(in lowp vec2 tc, in lowp float angle) { + float cosa = cos(angle); + float sina = sin(angle); + return vec2( + ((tc.x * 2.0 - 1.0) * cosa - (tc.y * 2.0 - 1.0) * sina) * 0.5 + 0.5, + ((tc.y * 2.0 - 1.0) * cosa + (tc.x * 2.0 - 1.0) * sina) * 0.5 + 0.5); +} +)", + .body = R"( + vec2 rotatedCoords = rotateTexCoords( + gl_FragCoord.xy / dimensions.xy, + noiseCoordRotation); + float intensity = pnoise3D(vec3( + rotatedCoords.x * dimensions.x / grainsize, + rotatedCoords.y * dimensions.y / grainsize, + 0.0)); + + // Looks like intensity is almost always in [-2, 2] range. + float clamped = clamp((intensity + 2.) * 0.25, 0., 1.); + result = vec4(clamped, 0., 0., 1.); +)", + }; +} + +[[nodiscard]] ShaderPart FragmentDitherNoise() { + const auto size = QString::number(kNoiseTextureSize); + return { + .header = R"( +uniform sampler2D n_texture; +)", + .body = R"( + vec2 noiseTextureCoord = gl_FragCoord.xy / )" + size + R"(.; + float noiseClamped = texture2D(n_texture, noiseTextureCoord).r; + float noiseIntensity = (noiseClamped * 4.) - 2.; + + vec3 lumcoeff = vec3(0.299, 0.587, 0.114); + float luminance = dot(result.rgb, lumcoeff); + float lum = smoothstep(0.2, 0.0, luminance) + luminance; + vec3 noiseColor = mix(vec3(noiseIntensity), vec3(0.0), pow(lum, 4.0)); + + result.rgb = result.rgb + noiseColor * noiseGrain; +)", + }; +} + +// Depends on FragmentSampleTexture(). +[[nodiscard]] ShaderPart FragmentFrameColor() { + const auto round = FragmentRoundCorners(); + const auto blur = FragmentBlurTexture(true, 'b'); + const auto noise = FragmentDitherNoise(); + return { + .header = R"( +uniform vec4 frameBg; +uniform vec3 shadow; // fullHeight, shown, maxOpacity +uniform float paused; // 0. <-> 1. + +)" + blur.header + round.header + noise.header + R"( + +const float backgroundOpacity = )" + QString::number(kBlurOpacity) + R"(; +const float noiseGrain = )" + QString::number(kDitherNoiseAmount) + R"(; + +float insideTexture() { + vec2 textureHalf = vec2(0.5, 0.5); + vec2 fromTextureCenter = abs(v_texcoord - textureHalf); + vec2 fromTextureEdge = max(fromTextureCenter, textureHalf) - textureHalf; + float outsideCheck = dot(fromTextureEdge, fromTextureEdge); + return step(outsideCheck, 0.); +} + +vec4 background() { + vec4 result; + +)" + blur.body + noise.body + R"( + + return result; +} +)", + .body = R"( + float inside = insideTexture() * (1. - paused); + result = result * inside + + (1. - inside) * (backgroundOpacity * background() + + (1. - backgroundOpacity) * frameBg); + + float shadowCoord = gl_FragCoord.y - roundRect.y; + float shadowValue = max(1. - (shadowCoord / shadow.x), 0.); + float shadowShown = max(shadowValue * shadow.y, paused) * shadow.z; + result = vec4(result.rgb * (1. - shadowShown), result.a); +)" + round.body, + }; +} + +[[nodiscard]] bool UseExpandForCamera(QSize original, QSize viewport) { + const auto big = original.scaled( + viewport, + Qt::KeepAspectRatioByExpanding); + + // If we cut out no more than 0.25 of the original, let's use expanding. + return (big.width() * kMinCameraVisiblePart <= viewport.width()) + && (big.height() * kMinCameraVisiblePart <= viewport.height()); +} + +[[nodiscard]] QSize NonEmpty(QSize size) { + return QSize(std::max(size.width(), 1), std::max(size.height(), 1)); +} + +[[nodiscard]] QSize CountBlurredSize( + QSize unscaled, + QSize outer, + float factor) { + factor *= kBlurTextureSizeFactor; + const auto area = outer / int(std::round(factor * cScale() / 100)); + const auto scaled = unscaled.scaled(area, Qt::KeepAspectRatio); + return (scaled.width() > unscaled.width() + || scaled.height() > unscaled.height()) + ? unscaled + : NonEmpty(scaled); +} + +[[nodiscard]] QSize InterpolateScaledSize( + QSize unscaled, + QSize size, + float64 ratio) { + if (ratio == 0.) { + return NonEmpty(unscaled.scaled( + size, + Qt::KeepAspectRatio)); + } else if (ratio == 1.) { + return NonEmpty(unscaled.scaled( + size, + Qt::KeepAspectRatioByExpanding)); + } + const auto notExpanded = NonEmpty(unscaled.scaled( + size, + Qt::KeepAspectRatio)); + const auto expanded = NonEmpty(unscaled.scaled( + size, + Qt::KeepAspectRatioByExpanding)); + return QSize( + anim::interpolate(notExpanded.width(), expanded.width(), ratio), + anim::interpolate(notExpanded.height(), expanded.height(), ratio)); +} + +[[nodiscard]] std::array, 4> CountTexCoords( + QSize unscaled, + QSize size, + float64 expandRatio, + bool swap = false) { + const auto scaled = InterpolateScaledSize(unscaled, size, expandRatio); + const auto left = (size.width() - scaled.width()) / 2; + const auto top = (size.height() - scaled.height()) / 2; + const auto right = left + scaled.width(); + const auto bottom = top + scaled.height(); + auto dleft = float(left) / scaled.width(); + auto dright = float(size.width() - left) / scaled.width(); + auto dtop = float(top) / scaled.height(); + auto dbottom = float(size.height() - top) / scaled.height(); + if (swap) { + std::swap(dleft, dtop); + std::swap(dright, dbottom); + } + return { { + { { -dleft, 1.f + dtop } }, + { { dright, 1.f + dtop } }, + { { dright, 1.f - dbottom } }, + { { -dleft, 1.f - dbottom } }, + } }; +} + +} // namespace + +Viewport::RendererGL::RendererGL(not_null owner) +: _owner(owner) +, _pinIcon(st::groupCallVideoTile.pin) +, _muteIcon(st::groupCallVideoCrossLine) +, _pinBackground( + (st::groupCallVideoTile.pinPadding.top() + + st::groupCallVideoTile.pin.icon.height() + + st::groupCallVideoTile.pinPadding.bottom()) / 2, + st::radialBg) { + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _buttons.invalidate(); + }, _lifetime); +} + +void Viewport::RendererGL::init( + not_null widget, + QOpenGLFunctions &f) { + _frameBuffer.emplace(); + _frameBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); + _frameBuffer->create(); + _frameBuffer->bind(); + _frameBuffer->allocate(kValues * sizeof(GLfloat)); + _downscaleProgram.yuv420.emplace(); + const auto downscaleVertexSource = VertexShader({ + VertexPassTextureCoord(), + }); + _downscaleVertexShader = LinkProgram( + &*_downscaleProgram.yuv420, + VertexShader({ + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleYUV420Texture(), + })).vertex; + if (!_downscaleProgram.yuv420->isLinked()) { + //... + } + _blurProgram.emplace(); + LinkProgram( + &*_blurProgram, + _downscaleVertexShader, + FragmentShader({ + FragmentBlurTexture(false), + })); + _frameProgram.yuv420.emplace(); + _frameVertexShader = LinkProgram( + &*_frameProgram.yuv420, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + VertexPassTextureCoord('b'), + }), + FragmentShader({ + FragmentSampleYUV420Texture(), + FragmentFrameColor(), + })).vertex; + + _imageProgram.emplace(); + LinkProgram( + &*_imageProgram, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentGlobalOpacity(), + })); + + validateNoiseTexture(f, 0); +} + +void Viewport::RendererGL::ensureARGB32Program() { + Expects(_downscaleVertexShader != nullptr); + Expects(_frameVertexShader != nullptr); + + _downscaleProgram.argb32.emplace(); + LinkProgram( + &*_downscaleProgram.argb32, + _downscaleVertexShader, + FragmentShader({ + FragmentSampleARGB32Texture(), + })); + + _frameProgram.argb32.emplace(); + LinkProgram( + &*_frameProgram.argb32, + _frameVertexShader, + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentFrameColor(), + })); +} + +void Viewport::RendererGL::deinit( + not_null widget, + QOpenGLFunctions &f) { + _frameBuffer = std::nullopt; + _frameVertexShader = nullptr; + _imageProgram = std::nullopt; + _downscaleProgram.argb32 = std::nullopt; + _downscaleProgram.yuv420 = std::nullopt; + _blurProgram = std::nullopt; + _frameProgram.argb32 = std::nullopt; + _frameProgram.yuv420 = std::nullopt; + _noiseTexture.destroy(f); + _noiseFramebuffer.destroy(f); + for (auto &data : _tileData) { + data.textures.destroy(f); + } + _tileData.clear(); + _tileDataIndices.clear(); + _buttons.destroy(f); +} + +void Viewport::RendererGL::setDefaultViewport(QOpenGLFunctions &f) { + const auto size = _viewport * _factor; + f.glViewport(0, 0, size.width(), size.height()); +} + +void Viewport::RendererGL::paint( + not_null widget, + QOpenGLFunctions &f) { + const auto factor = widget->devicePixelRatio(); + if (_factor != factor) { + _factor = factor; + _buttons.invalidate(); + } + _viewport = widget->size(); + + const auto defaultFramebufferObject = widget->defaultFramebufferObject(); + + validateDatas(); + auto index = 0; + for (const auto &tile : _owner->_tiles) { + if (!tile->visible()) { + index++; + continue; + } + paintTile( + f, + defaultFramebufferObject, + tile.get(), + _tileData[_tileDataIndices[index++]]); + } +} + +std::optional Viewport::RendererGL::clearColor() { + return st::groupCallBg->c; +} + +void Viewport::RendererGL::validateUserpicFrame( + not_null tile, + TileData &tileData) { + if (!_userpicFrame) { + tileData.userpicFrame = QImage(); + return; + } else if (!tileData.userpicFrame.isNull()) { + return; + } + tileData.userpicFrame = QImage( + tile->trackOrUserpicSize(), + QImage::Format_ARGB32_Premultiplied); + tileData.userpicFrame.fill(Qt::black); + { + auto p = Painter(&tileData.userpicFrame); + tile->row()->peer()->paintUserpicSquare( + p, + tile->row()->ensureUserpicView(), + 0, + 0, + tileData.userpicFrame.width()); + } +} + +void Viewport::RendererGL::paintTile( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject, + not_null tile, + TileData &tileData) { + const auto track = tile->track(); + const auto markGuard = gsl::finally([&] { + tile->track()->markFrameShown(); + }); + const auto data = track->frameWithInfo(false); + _userpicFrame = (data.format == Webrtc::FrameFormat::None); + validateUserpicFrame(tile, tileData); + const auto frameSize = _userpicFrame + ? tileData.userpicFrame.size() + : data.yuv420->size; + const auto frameRotation = _userpicFrame + ? 0 + : data.rotation; + Assert(!frameSize.isEmpty()); + + _rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32) + || _userpicFrame; + const auto geometry = tile->geometry(); + const auto x = geometry.x(); + const auto y = geometry.y(); + const auto width = geometry.width(); + const auto height = geometry.height(); + const auto &st = st::groupCallVideoTile; + const auto shown = _owner->_controlsShownRatio; + const auto fullNameShift = st.namePosition.y() + st::normalFont->height; + const auto nameShift = anim::interpolate(fullNameShift, 0, shown); + const auto row = tile->row(); + const auto style = row->computeIconState(MembersRowStyle::Video); + + validateOutlineAnimation(tile, tileData); + validatePausedAnimation(tile, tileData); + const auto outline = tileData.outlined.value(tileData.outline ? 1. : 0.); + const auto paused = tileData.paused.value(tileData.pause ? 1. : 0.); + + ensureButtonsImage(); + + // Frame. + const auto unscaled = Media::View::FlipSizeByRotation( + frameSize, + frameRotation); + const auto tileSize = geometry.size(); + const auto swap = (((frameRotation / 90) % 2) == 1); + const auto expand = isExpanded(tile, unscaled, tileSize); + const auto animation = tile->animation(); + const auto expandRatio = (animation.ratio >= 0.) + ? countExpandRatio(tile, unscaled, animation) + : expand + ? 1. + : 0.; + auto texCoords = CountTexCoords(unscaled, tileSize, expandRatio, swap); + auto blurTexCoords = (expandRatio == 1. && !swap) + ? texCoords + : CountTexCoords(unscaled, tileSize, 1.); + const auto rect = transformRect(geometry); + auto toBlurTexCoords = std::array, 4> { { + { { 0.f, 1.f } }, + { { 1.f, 1.f } }, + { { 1.f, 0.f } }, + { { 0.f, 0.f } }, + } }; + if (const auto shift = (frameRotation / 90); shift > 0) { + std::rotate( + toBlurTexCoords.begin(), + toBlurTexCoords.begin() + shift, + toBlurTexCoords.end()); + std::rotate( + texCoords.begin(), + texCoords.begin() + shift, + texCoords.end()); + } + + const auto nameTop = y + (height + - st.namePosition.y() + - st::semiboldFont->height); + + // Paused icon and text. + const auto middle = (st::groupCallVideoPlaceholderHeight + - st::groupCallPaused.height()) / 2; + const auto pausedSpace = (nameTop - y) + - st::groupCallPaused.height() + - st::semiboldFont->height; + const auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop; + const auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop + - st::groupCallVideoPlaceholderIconTop; + const auto pauseIconTop = !_owner->wide() + ? (y + (height - st::groupCallPaused.height()) / 2) + : (pausedSpace < 3 * st::semiboldFont->height) + ? (pausedSpace / 3) + : std::min( + y + (height / 2) - pauseIconSkip, + (nameTop + - st::semiboldFont->height * 3 + - st::groupCallPaused.height())); + const auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height) + ? (nameTop - (pausedSpace / 3) - st::semiboldFont->height) + : std::min( + pauseIconTop + pauseTextSkip, + nameTop - st::semiboldFont->height * 2); + + const auto pauseIcon = _buttons.texturedRect( + QRect( + x + (width - st::groupCallPaused.width()) / 2, + pauseIconTop, + st::groupCallPaused.width(), + st::groupCallPaused.height()), + _paused); + const auto pauseRect = transformRect(pauseIcon.geometry); + + const auto pausedPosition = QPoint( + x + (width - (_pausedTextRect.width() / cIntRetinaFactor())) / 2, + pauseTextTop); + const auto pausedText = _names.texturedRect( + QRect(pausedPosition, _pausedTextRect.size() / cIntRetinaFactor()), + _pausedTextRect); + const auto pausedRect = transformRect(pausedText.geometry); + + // Pin. + const auto pin = _buttons.texturedRect( + tile->pinInner().translated(x, y), + tile->pinned() ? _pinOn : _pinOff, + geometry); + const auto pinRect = transformRect(pin.geometry); + + // Back. + const auto back = _buttons.texturedRect( + tile->backInner().translated(x, y), + _back, + geometry); + const auto backRect = transformRect(back.geometry); + + // Mute. + const auto &icon = st::groupCallVideoCrossLine.icon; + const auto iconLeft = x + width - st.iconPosition.x() - icon.width(); + const auto iconTop = y + (height + - st.iconPosition.y() + - icon.height() + + nameShift); + const auto mute = _buttons.texturedRect( + QRect(iconLeft, iconTop, icon.width(), icon.height()), + (row->state() == MembersRow::State::Active + ? _muteOff + : _muteOn), + geometry); + const auto muteRect = transformRect(mute.geometry); + + // Name. + const auto namePosition = QPoint( + x + st.namePosition.x(), + nameTop + nameShift); + const auto name = _names.texturedRect( + QRect(namePosition, tileData.nameRect.size() / cIntRetinaFactor()), + tileData.nameRect, + geometry); + const auto nameRect = transformRect(name.geometry); + + const GLfloat coords[] = { + // YUV -> RGB-for-blur quad. + -1.f, 1.f, + toBlurTexCoords[0][0], toBlurTexCoords[0][1], + + 1.f, 1.f, + toBlurTexCoords[1][0], toBlurTexCoords[1][1], + + 1.f, -1.f, + toBlurTexCoords[2][0], toBlurTexCoords[2][1], + + -1.f, -1.f, + toBlurTexCoords[3][0], toBlurTexCoords[3][1], + + // First RGB -> RGB blur pass. + -1.f, 1.f, + 0.f, 1.f, + + 1.f, 1.f, + 1.f, 1.f, + + 1.f, -1.f, + 1.f, 0.f, + + -1.f, -1.f, + 0.f, 0.f, + + // Second blur pass + paint final frame. + rect.left(), rect.top(), + texCoords[0][0], texCoords[0][1], + + rect.right(), rect.top(), + texCoords[1][0], texCoords[1][1], + + rect.right(), rect.bottom(), + texCoords[2][0], texCoords[2][1], + + rect.left(), rect.bottom(), + texCoords[3][0], texCoords[3][1], + + // Additional blurred background texture coordinates. + blurTexCoords[0][0], blurTexCoords[0][1], + blurTexCoords[1][0], blurTexCoords[1][1], + blurTexCoords[2][0], blurTexCoords[2][1], + blurTexCoords[3][0], blurTexCoords[3][1], + + // Pin button. + pinRect.left(), pinRect.top(), + pin.texture.left(), pin.texture.bottom(), + + pinRect.right(), pinRect.top(), + pin.texture.right(), pin.texture.bottom(), + + pinRect.right(), pinRect.bottom(), + pin.texture.right(), pin.texture.top(), + + pinRect.left(), pinRect.bottom(), + pin.texture.left(), pin.texture.top(), + + // Back button. + backRect.left(), backRect.top(), + back.texture.left(), back.texture.bottom(), + + backRect.right(), backRect.top(), + back.texture.right(), back.texture.bottom(), + + backRect.right(), backRect.bottom(), + back.texture.right(), back.texture.top(), + + backRect.left(), backRect.bottom(), + back.texture.left(), back.texture.top(), + + // Mute icon. + muteRect.left(), muteRect.top(), + mute.texture.left(), mute.texture.bottom(), + + muteRect.right(), muteRect.top(), + mute.texture.right(), mute.texture.bottom(), + + muteRect.right(), muteRect.bottom(), + mute.texture.right(), mute.texture.top(), + + muteRect.left(), muteRect.bottom(), + mute.texture.left(), mute.texture.top(), + + // Name. + nameRect.left(), nameRect.top(), + name.texture.left(), name.texture.bottom(), + + nameRect.right(), nameRect.top(), + name.texture.right(), name.texture.bottom(), + + nameRect.right(), nameRect.bottom(), + name.texture.right(), name.texture.top(), + + nameRect.left(), nameRect.bottom(), + name.texture.left(), name.texture.top(), + + // Paused icon. + pauseRect.left(), pauseRect.top(), + pauseIcon.texture.left(), pauseIcon.texture.bottom(), + + pauseRect.right(), pauseRect.top(), + pauseIcon.texture.right(), pauseIcon.texture.bottom(), + + pauseRect.right(), pauseRect.bottom(), + pauseIcon.texture.right(), pauseIcon.texture.top(), + + pauseRect.left(), pauseRect.bottom(), + pauseIcon.texture.left(), pauseIcon.texture.top(), + + // Paused text. + pausedRect.left(), pausedRect.top(), + pausedText.texture.left(), pausedText.texture.bottom(), + + pausedRect.right(), pausedRect.top(), + pausedText.texture.right(), pausedText.texture.bottom(), + + pausedRect.right(), pausedRect.bottom(), + pausedText.texture.right(), pausedText.texture.top(), + + pausedRect.left(), pausedRect.bottom(), + pausedText.texture.left(), pausedText.texture.top(), + }; + + _frameBuffer->bind(); + _frameBuffer->write(0, coords, sizeof(coords)); + + const auto blurSize = CountBlurredSize( + unscaled, + geometry.size(), + _factor); + prepareObjects(f, tileData, blurSize); + f.glViewport(0, 0, blurSize.width(), blurSize.height()); + + bindFrame(f, data, tileData, _downscaleProgram); + + drawDownscalePass(f, tileData); + drawFirstBlurPass(f, tileData, blurSize); + + f.glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject); + setDefaultViewport(f); + + bindFrame(f, data, tileData, _frameProgram); + + const auto program = _rgbaFrame + ? &*_frameProgram.argb32 + : &*_frameProgram.yuv420; + const auto uniformViewport = QSizeF(_viewport * _factor); + + program->setUniformValue("viewport", uniformViewport); + program->setUniformValue("frameBg", st::groupCallBg->c); + program->setUniformValue("radiusOutline", QVector2D( + GLfloat(st::roundRadiusLarge * _factor), + (outline > 0) ? (st::groupCallOutline * _factor) : 0.f)); + program->setUniformValue("roundRect", Uniform(rect)); + program->setUniformValue("roundBg", st::groupCallBg->c); + program->setUniformValue("outlineFg", QVector4D( + st::groupCallMemberActiveIcon->c.redF(), + st::groupCallMemberActiveIcon->c.greenF(), + st::groupCallMemberActiveIcon->c.blueF(), + st::groupCallMemberActiveIcon->c.alphaF() * outline)); + + const auto shadowHeight = st.shadowHeight * _factor; + const auto shadowAlpha = kShadowMaxAlpha / 255.f; + program->setUniformValue( + "shadow", + QVector3D(shadowHeight, shown, shadowAlpha)); + program->setUniformValue("paused", GLfloat(paused)); + + f.glActiveTexture(_rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3); + tileData.textures.bind(f, kFirstBlurPassTextureIndex); + program->setUniformValue("b_texture", GLint(_rgbaFrame ? 1 : 3)); + f.glActiveTexture(_rgbaFrame ? GL_TEXTURE2 : GL_TEXTURE5); + _noiseTexture.bind(f, 0); + program->setUniformValue("n_texture", GLint(_rgbaFrame ? 2 : 5)); + program->setUniformValue( + "texelOffset", + GLfloat(1.f / blurSize.height())); + GLint blurTexcoord = program->attributeLocation("b_texcoordIn"); + f.glVertexAttribPointer( + blurTexcoord, + 2, + GL_FLOAT, + GL_FALSE, + 2 * sizeof(GLfloat), + reinterpret_cast(48 * sizeof(GLfloat))); + f.glEnableVertexAttribArray(blurTexcoord); + FillTexturedRectangle(f, program, 8); + f.glDisableVertexAttribArray(blurTexcoord); + + const auto pinVisible = _owner->wide() + && (pin.geometry.bottom() > y); + const auto nameVisible = (nameShift != fullNameShift); + const auto pausedVisible = (paused > 0.); + if (!nameVisible && !pinVisible && !pausedVisible) { + return; + } + + f.glEnable(GL_BLEND); + f.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + const auto guard = gsl::finally([&] { + f.glDisable(GL_BLEND); + }); + + _imageProgram->bind(); + _imageProgram->setUniformValue("viewport", uniformViewport); + _imageProgram->setUniformValue("s_texture", GLint(0)); + + f.glActiveTexture(GL_TEXTURE0); + _buttons.bind(f); + + // Paused icon. + if (pausedVisible) { + _imageProgram->setUniformValue("g_opacity", GLfloat(paused)); + FillTexturedRectangle(f, &*_imageProgram, 30); + } + _imageProgram->setUniformValue("g_opacity", GLfloat(1.f)); + + // Pin. + if (pinVisible) { + FillTexturedRectangle(f, &*_imageProgram, 14); + FillTexturedRectangle(f, &*_imageProgram, 18); + } + + // Mute. + if (nameVisible && !muteRect.empty()) { + FillTexturedRectangle(f, &*_imageProgram, 22); + } + + if (!nameVisible && !pausedVisible) { + return; + } + + _names.bind(f); + + // Name. + if (nameVisible && !nameRect.empty()) { + FillTexturedRectangle(f, &*_imageProgram, 26); + } + + // Paused text. + if (pausedVisible && _owner->wide()) { + _imageProgram->setUniformValue("g_opacity", GLfloat(paused)); + FillTexturedRectangle(f, &*_imageProgram, 34); + } +} + +void Viewport::RendererGL::prepareObjects( + QOpenGLFunctions &f, + TileData &tileData, + QSize blurSize) { + if (!tileData.textures.created()) { + tileData.textures.ensureCreated(f); // All are GL_LINEAR, except.. + tileData.textures.bind(f, kScaleForBlurTextureIndex); + + // kScaleForBlurTextureIndex is attached to framebuffer 0, + // and is used to draw to framebuffer 1 of the same size. + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + tileData.framebuffers.ensureCreated(f); + + if (tileData.textureBlurSize == blurSize) { + return; + } + tileData.textureBlurSize = blurSize; + + const auto create = [&](int framebufferIndex, int index) { + tileData.textures.bind(f, index); + f.glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGB, + blurSize.width(), + blurSize.height(), + 0, + GL_RGB, + GL_UNSIGNED_BYTE, + nullptr); + + tileData.framebuffers.bind(f, framebufferIndex); + f.glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + tileData.textures.id(index), + 0); + }; + create(0, kScaleForBlurTextureIndex); + create(1, kFirstBlurPassTextureIndex); +} + +bool Viewport::RendererGL::isExpanded( + not_null tile, + QSize unscaled, + QSize tileSize) const { + return !tile->screencast() + && (!_owner->wide() || UseExpandForCamera(unscaled, tileSize)); +} + +float64 Viewport::RendererGL::countExpandRatio( + not_null tile, + QSize unscaled, + const TileAnimation &animation) const { + const auto expandedFrom = isExpanded(tile, unscaled, animation.from); + const auto expandedTo = isExpanded(tile, unscaled, animation.to); + return (expandedFrom && expandedTo) + ? 1. + : (!expandedFrom && !expandedTo) + ? 0. + : expandedFrom + ? (1. - animation.ratio) + : animation.ratio; +} + +void Viewport::RendererGL::bindFrame( + QOpenGLFunctions &f, + const Webrtc::FrameWithInfo &data, + TileData &tileData, + Program &program) { + const auto imageIndex = _userpicFrame ? 0 : (data.index + 1); + const auto upload = (tileData.trackIndex != imageIndex); + tileData.trackIndex = imageIndex; + if (_rgbaFrame) { + ensureARGB32Program(); + program.argb32->bind(); + f.glActiveTexture(GL_TEXTURE0); + tileData.textures.bind(f, 0); + if (upload) { + const auto &image = _userpicFrame + ? tileData.userpicFrame + : data.original; + const auto stride = image.bytesPerLine() / 4; + const auto data = image.constBits(); + uploadTexture( + f, + GL_RGBA, + GL_RGBA, + image.size(), + tileData.rgbaSize, + stride, + data); + tileData.rgbaSize = image.size(); + tileData.textureSize = QSize(); + } + program.argb32->setUniformValue("s_texture", GLint(0)); + } else { + const auto yuv = data.yuv420; + program.yuv420->bind(); + f.glActiveTexture(GL_TEXTURE0); + tileData.textures.bind(f, 0); + if (upload) { + f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->size, + tileData.textureSize, + yuv->y.stride, + yuv->y.data); + tileData.textureSize = yuv->size; + tileData.rgbaSize = QSize(); + } + f.glActiveTexture(GL_TEXTURE1); + tileData.textures.bind(f, 1); + if (upload) { + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->chromaSize, + tileData.textureChromaSize, + yuv->u.stride, + yuv->u.data); + } + f.glActiveTexture(GL_TEXTURE2); + tileData.textures.bind(f, 2); + if (upload) { + uploadTexture( + f, + GL_RED, + GL_RED, + yuv->chromaSize, + tileData.textureChromaSize, + yuv->v.stride, + yuv->v.data); + tileData.textureChromaSize = yuv->chromaSize; + f.glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + program.yuv420->setUniformValue("y_texture", GLint(0)); + program.yuv420->setUniformValue("u_texture", GLint(1)); + program.yuv420->setUniformValue("v_texture", GLint(2)); + } +} + +void Viewport::RendererGL::uploadTexture( + QOpenGLFunctions &f, + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const { + f.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); + if (hasSize != size) { + f.glTexImage2D( + GL_TEXTURE_2D, + 0, + internalformat, + size.width(), + size.height(), + 0, + format, + GL_UNSIGNED_BYTE, + data); + } else { + f.glTexSubImage2D( + GL_TEXTURE_2D, + 0, + 0, + 0, + size.width(), + size.height(), + format, + GL_UNSIGNED_BYTE, + data); + } + f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + +void Viewport::RendererGL::drawDownscalePass( + QOpenGLFunctions &f, + TileData &tileData) { + tileData.framebuffers.bind(f, 0); + + const auto program = _rgbaFrame + ? &*_downscaleProgram.argb32 + : &*_downscaleProgram.yuv420; + + FillTexturedRectangle(f, program); +} + +void Viewport::RendererGL::drawFirstBlurPass( + QOpenGLFunctions &f, + TileData &tileData, + QSize blurSize) { + tileData.framebuffers.bind(f, 1); + + _blurProgram->bind(); + f.glActiveTexture(GL_TEXTURE0); + tileData.textures.bind(f, kScaleForBlurTextureIndex); + + _blurProgram->setUniformValue("b_texture", GLint(0)); + _blurProgram->setUniformValue( + "texelOffset", + GLfloat(1.f / blurSize.width())); + + FillTexturedRectangle(f, &*_blurProgram, 4); +} + +Rect Viewport::RendererGL::transformRect(const Rect &raster) const { + return TransformRect(raster, _viewport, _factor); +} + +Rect Viewport::RendererGL::transformRect(const QRect &raster) const { + return TransformRect(Rect(raster), _viewport, _factor); +} + +void Viewport::RendererGL::ensureButtonsImage() { + if (_buttons) { + return; + } + const auto pinOnSize = VideoTile::PinInnerSize(true); + const auto pinOffSize = VideoTile::PinInnerSize(false); + const auto backSize = VideoTile::BackInnerSize(); + const auto muteSize = st::groupCallVideoCrossLine.icon.size(); + const auto pausedSize = st::groupCallPaused.size(); + + const auto fullSize = QSize( + std::max({ + pinOnSize.width(), + pinOffSize.width(), + backSize.width(), + 2 * muteSize.width(), + pausedSize.width(), + }), + (pinOnSize.height() + + pinOffSize.height() + + backSize.height() + + muteSize.height() + + pausedSize.height())); + const auto imageSize = fullSize * _factor; + auto image = _buttons.takeImage(); + if (image.size() != imageSize) { + image = QImage(imageSize, QImage::Format_ARGB32_Premultiplied); + } + image.fill(Qt::transparent); + image.setDevicePixelRatio(_factor); + { + auto p = Painter(&image); + auto hq = PainterHighQualityEnabler(p); + + _pinOn = QRect(QPoint(), pinOnSize * _factor); + VideoTile::PaintPinButton( + p, + true, + 0, + 0, + fullSize.width(), + &_pinBackground, + &_pinIcon); + + const auto pinOffTop = pinOnSize.height(); + _pinOff = QRect( + QPoint(0, pinOffTop) * _factor, + pinOffSize * _factor); + VideoTile::PaintPinButton( + p, + false, + 0, + pinOnSize.height(), + fullSize.width(), + &_pinBackground, + &_pinIcon); + + const auto backTop = pinOffTop + pinOffSize.height(); + _back = QRect(QPoint(0, backTop) * _factor, backSize * _factor); + VideoTile::PaintBackButton( + p, + 0, + pinOnSize.height() + pinOffSize.height(), + fullSize.width(), + &_pinBackground); + + const auto muteTop = backTop + backSize.height(); + _muteOn = QRect(QPoint(0, muteTop) * _factor, muteSize * _factor); + _muteIcon.paint(p, { 0, muteTop }, 1.); + + _muteOff = QRect( + QPoint(muteSize.width(), muteTop) * _factor, + muteSize * _factor); + _muteIcon.paint(p, { muteSize.width(), muteTop }, 0.); + + const auto pausedTop = muteTop + muteSize.height(); + _paused = QRect( + QPoint(0, pausedTop) * _factor, + pausedSize * _factor); + st::groupCallPaused.paint(p, 0, pausedTop, fullSize.width()); + } + _buttons.setImage(std::move(image)); +} + +void Viewport::RendererGL::validateDatas() { + const auto &tiles = _owner->_tiles; + const auto &st = st::groupCallVideoTile; + const auto count = int(tiles.size()); + const auto factor = cIntRetinaFactor(); + const auto nameHeight = st::semiboldFont->height * factor; + const auto pausedText = tr::lng_group_call_video_paused(tr::now); + const auto pausedBottom = nameHeight; + const auto pausedWidth = st::semiboldFont->width(pausedText) * factor; + struct Request { + int index = 0; + bool updating = false; + }; + auto requests = std::vector(); + auto available = std::max(_names.image().width(), pausedWidth); + for (auto &data : _tileData) { + data.stale = true; + } + _tileDataIndices.resize(count); + const auto nameWidth = [&](int i) { + const auto row = tiles[i]->row(); + const auto hasWidth = tiles[i]->geometry().width() + - st.iconPosition.x() + - st::groupCallVideoCrossLine.icon.width() + - st.namePosition.x(); + if (hasWidth < 1) { + return 0; + } + return std::clamp(row->name().maxWidth(), 1, hasWidth) * factor; + }; + for (auto i = 0; i != count; ++i) { + tiles[i]->row()->lazyInitialize(st::groupCallMembersListItem); + const auto width = nameWidth(i); + if (width > available) { + available = width; + } + const auto id = quintptr(tiles[i]->track().get()); + const auto j = ranges::find(_tileData, id, &TileData::id); + if (j != end(_tileData)) { + j->stale = false; + const auto index = (j - begin(_tileData)); + _tileDataIndices[i] = index; + const auto peer = tiles[i]->row()->peer(); + if (peer != j->peer + || peer->nameVersion != j->nameVersion + || width != j->nameRect.width()) { + const auto nameTop = pausedBottom + index * nameHeight; + j->nameRect = QRect(0, nameTop, width, nameHeight); + requests.push_back({ .index = i, .updating = true }); + } + } else { + _tileDataIndices[i] = -1; + requests.push_back({ .index = i, .updating = false }); + } + } + if (requests.empty()) { + return; + } + auto maybeStaleAfter = begin(_tileData); + auto maybeStaleEnd = end(_tileData); + for (auto &request : requests) { + const auto i = request.index; + if (_tileDataIndices[i] >= 0) { + continue; + } + const auto id = quintptr(tiles[i]->track().get()); + const auto peer = tiles[i]->row()->peer(); + const auto paused = (tiles[i]->track()->state() + == Webrtc::VideoState::Paused); + auto index = int(_tileData.size()); + maybeStaleAfter = ranges::find( + maybeStaleAfter, + maybeStaleEnd, + true, + &TileData::stale); + if (maybeStaleAfter != maybeStaleEnd) { + index = (maybeStaleAfter - begin(_tileData)); + maybeStaleAfter->id = id; + maybeStaleAfter->peer = peer; + maybeStaleAfter->stale = false; + maybeStaleAfter->pause = paused; + maybeStaleAfter->paused.stop(); + request.updating = true; + } else { + // This invalidates maybeStale*, but they're already equal. + _tileData.push_back({ + .id = id, + .peer = peer, + .pause = paused, + }); + } + const auto nameTop = pausedBottom + index * nameHeight; + _tileData[index].nameVersion = peer->nameVersion; + _tileData[index].nameRect = QRect( + 0, + nameTop, + nameWidth(i), + nameHeight); + _tileDataIndices[i] = index; + } + auto image = _names.takeImage(); + const auto imageSize = QSize( + available, + pausedBottom + _tileData.size() * nameHeight); + const auto allocate = (image.size() != imageSize); + auto paintToImage = allocate + ? QImage(imageSize, QImage::Format_ARGB32_Premultiplied) + : base::take(image); + paintToImage.setDevicePixelRatio(factor); + if (allocate && image.isNull()) { + paintToImage.fill(Qt::transparent); + } + { + auto p = Painter(&paintToImage); + p.setPen(st::groupCallVideoTextFg); + if (!image.isNull()) { + p.setCompositionMode(QPainter::CompositionMode_Source); + p.drawImage(0, 0, image); + if (paintToImage.width() > image.width()) { + p.fillRect( + image.width() / factor, + 0, + (paintToImage.width() - image.width()) / factor, + image.height() / factor, + Qt::transparent); + } + if (paintToImage.height() > image.height()) { + p.fillRect( + 0, + image.height() / factor, + paintToImage.width() / factor, + (paintToImage.height() - image.height()) / factor, + Qt::transparent); + } + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } else if (allocate) { + p.setFont(st::semiboldFont); + p.drawText(0, st::semiboldFont->ascent, pausedText); + _pausedTextRect = QRect(0, 0, pausedWidth, nameHeight); + } + for (const auto &request : requests) { + const auto i = request.index; + const auto index = _tileDataIndices[i]; + const auto &data = _tileData[_tileDataIndices[i]]; + if (data.nameRect.isEmpty()) { + continue; + } + const auto row = tiles[i]->row(); + if (request.updating) { + p.setCompositionMode(QPainter::CompositionMode_Source); + p.fillRect( + 0, + data.nameRect.y() / factor, + paintToImage.width() / factor, + nameHeight / factor, + Qt::transparent); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + row->name().drawLeftElided( + p, + 0, + data.nameRect.y() / factor, + data.nameRect.width() / factor, + paintToImage.width() / factor); + } + } + _names.setImage(std::move(paintToImage)); +} + +void Viewport::RendererGL::validateNoiseTexture( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject) { + if (_noiseTexture.created()) { + return; + } + _noiseTexture.ensureCreated(f, GL_NEAREST, GL_REPEAT); + _noiseTexture.bind(f, 0); + f.glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + kNoiseTextureSize, + kNoiseTextureSize, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + nullptr); + + _noiseFramebuffer.ensureCreated(f); + _noiseFramebuffer.bind(f, 0); + + f.glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + _noiseTexture.id(0), + 0); + + f.glViewport(0, 0, kNoiseTextureSize, kNoiseTextureSize); + + const GLfloat coords[] = { + -1, -1, + -1, 1, + 1, 1, + 1, -1, + }; + auto buffer = QOpenGLBuffer(); + buffer.setUsagePattern(QOpenGLBuffer::StaticDraw); + buffer.create(); + buffer.bind(); + buffer.allocate(coords, sizeof(coords)); + + auto program = QOpenGLShaderProgram(); + LinkProgram( + &program, + VertexShader({}), + FragmentShader({ FragmentGenerateNoise() })); + program.bind(); + + GLint position = program.attributeLocation("position"); + f.glVertexAttribPointer( + position, + 2, + GL_FLOAT, + GL_FALSE, + 2 * sizeof(GLfloat), + nullptr); + f.glEnableVertexAttribArray(position); + + f.glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + f.glDisableVertexAttribArray(position); + + f.glUseProgram(0); +} + +void Viewport::RendererGL::validateOutlineAnimation( + not_null tile, + TileData &data) { + const auto outline = tile->row()->speaking(); + if (data.outline == outline) { + return; + } + data.outline = outline; + data.outlined.start( + [=] { _owner->widget()->update(); }, + outline ? 0. : 1., + outline ? 1. : 0., + st::fadeWrapDuration); +} + +void Viewport::RendererGL::validatePausedAnimation( + not_null tile, + TileData &data) { + const auto paused = (_userpicFrame + && tile->track()->frameSize().isEmpty()) + || (tile->track()->state() == Webrtc::VideoState::Paused); + if (data.pause == paused) { + return; + } + data.pause = paused; + data.paused.start( + [=] { _owner->widget()->update(); }, + paused ? 0. : 1., + paused ? 1. : 0., + st::fadeWrapDuration); +} + + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h new file mode 100644 index 000000000..bcb733647 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h @@ -0,0 +1,168 @@ +/* +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 "calls/group/calls_group_viewport.h" +#include "ui/round_rect.h" +#include "ui/effects/animations.h" +#include "ui/effects/cross_line.h" +#include "ui/gl/gl_primitives.h" +#include "ui/gl/gl_surface.h" +#include "ui/gl/gl_image.h" + +#include +#include + +namespace Webrtc { +struct FrameWithInfo; +} // namespace Webrtc + +namespace Calls::Group { + +class Viewport::RendererGL final : public Ui::GL::Renderer { +public: + explicit RendererGL(not_null owner); + + void init( + not_null widget, + QOpenGLFunctions &f) override; + + void deinit( + not_null widget, + QOpenGLFunctions &f) override; + + void paint( + not_null widget, + QOpenGLFunctions &f) override; + + std::optional clearColor() override; + +private: + struct TileData { + quintptr id = 0; + not_null peer; + Ui::GL::Textures<5> textures; + Ui::GL::Framebuffers<2> framebuffers; + Ui::Animations::Simple outlined; + Ui::Animations::Simple paused; + QImage userpicFrame; + QRect nameRect; + int nameVersion = 0; + mutable int trackIndex = -1; + mutable QSize rgbaSize; + mutable QSize textureSize; + mutable QSize textureChromaSize; + mutable QSize textureBlurSize; + bool stale = false; + bool pause = false; + bool outline = false; + }; + struct Program { + std::optional argb32; + std::optional yuv420; + }; + + void setDefaultViewport(QOpenGLFunctions &f); + void paintTile( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject, + not_null tile, + TileData &nameData); + [[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect( + const Ui::GL::Rect &raster) const; + + void ensureARGB32Program(); + void ensureButtonsImage(); + void prepareObjects( + QOpenGLFunctions &f, + TileData &tileData, + QSize blurSize); + void bindFrame( + QOpenGLFunctions &f, + const Webrtc::FrameWithInfo &data, + TileData &tileData, + Program &program); + void drawDownscalePass( + QOpenGLFunctions &f, + TileData &tileData); + void drawFirstBlurPass( + QOpenGLFunctions &f, + TileData &tileData, + QSize blurSize); + void validateDatas(); + void validateNoiseTexture( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject); + void validateOutlineAnimation( + not_null tile, + TileData &data); + void validatePausedAnimation( + not_null tile, + TileData &data); + void validateUserpicFrame( + not_null tile, + TileData &tileData); + + void uploadTexture( + QOpenGLFunctions &f, + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const; + + [[nodiscard]] bool isExpanded( + not_null tile, + QSize unscaled, + QSize tileSize) const; + [[nodiscard]] float64 countExpandRatio( + not_null tile, + QSize unscaled, + const TileAnimation &animation) const; + + const not_null _owner; + + GLfloat _factor = 1.; + QSize _viewport; + bool _rgbaFrame = false; + bool _userpicFrame; + std::optional _frameBuffer; + Program _downscaleProgram; + std::optional _blurProgram; + Program _frameProgram; + std::optional _imageProgram; + Ui::GL::Textures<1> _noiseTexture; + Ui::GL::Framebuffers<1> _noiseFramebuffer; + QOpenGLShader *_downscaleVertexShader = nullptr; + QOpenGLShader *_frameVertexShader = nullptr; + + Ui::GL::Image _buttons; + QRect _pinOn; + QRect _pinOff; + QRect _back; + QRect _muteOn; + QRect _muteOff; + QRect _paused; + + Ui::GL::Image _names; + QRect _pausedTextRect; + std::vector _tileData; + std::vector _tileDataIndices; + + Ui::CrossLineAnimation _pinIcon; + Ui::CrossLineAnimation _muteIcon; + + Ui::RoundRect _pinBackground; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp new file mode 100644 index 000000000..b76bf0bcf --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp @@ -0,0 +1,340 @@ +/* +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 "calls/group/calls_group_viewport_raster.h" + +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_viewport_tile.h" +#include "calls/group/calls_group_members_row.h" +#include "data/data_peer.h" +#include "media/view/media_view_pip.h" +#include "webrtc/webrtc_video_track.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" +#include "styles/palette.h" + +namespace Calls::Group { +namespace { + +constexpr auto kBlurRadius = 15; + +} // namespace + +Viewport::RendererSW::RendererSW(not_null owner) +: _owner(owner) +, _pinIcon(st::groupCallVideoTile.pin) +, _pinBackground( + (st::groupCallVideoTile.pinPadding.top() + + st::groupCallVideoTile.pin.icon.height() + + st::groupCallVideoTile.pinPadding.bottom()) / 2, + st::radialBg) { +} + +void Viewport::RendererSW::paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { + auto bg = clip; + auto hq = PainterHighQualityEnabler(p); + const auto bounding = clip.boundingRect(); + for (auto &[tile, tileData] : _tileData) { + tileData.stale = true; + } + for (const auto &tile : _owner->_tiles) { + if (!tile->visible()) { + continue; + } + paintTile(p, tile.get(), bounding, bg); + } + for (const auto rect : bg) { + p.fillRect(rect, st::groupCallBg); + } + for (auto i = _tileData.begin(); i != _tileData.end();) { + if (i->second.stale) { + i = _tileData.erase(i); + } else { + ++i; + } + } +} + +void Viewport::RendererSW::validateUserpicFrame( + not_null tile, + TileData &data) { + if (!_userpicFrame) { + data.userpicFrame = QImage(); + return; + } else if (!data.userpicFrame.isNull()) { + return; + } + auto userpic = QImage( + tile->trackOrUserpicSize(), + QImage::Format_ARGB32_Premultiplied); + userpic.fill(Qt::black); + { + auto p = Painter(&userpic); + tile->row()->peer()->paintUserpicSquare( + p, + tile->row()->ensureUserpicView(), + 0, + 0, + userpic.width()); + } + data.userpicFrame = Images::BlurLargeImage( + std::move(userpic), + kBlurRadius); +} + +void Viewport::RendererSW::paintTile( + Painter &p, + not_null tile, + const QRect &clip, + QRegion &bg) { + const auto track = tile->track(); + const auto markGuard = gsl::finally([&] { + tile->track()->markFrameShown(); + }); + const auto data = track->frameWithInfo(true); + auto &tileData = _tileData[tile]; + tileData.stale = false; + _userpicFrame = (data.format == Webrtc::FrameFormat::None); + _pausedFrame = (track->state() == Webrtc::VideoState::Paused); + validateUserpicFrame(tile, tileData); + if (_userpicFrame || !_pausedFrame) { + tileData.blurredFrame = QImage(); + } else if (tileData.blurredFrame.isNull()) { + tileData.blurredFrame = Images::BlurLargeImage( + data.original.scaled( + VideoTile::PausedVideoSize(), + Qt::KeepAspectRatio), + kBlurRadius); + } + const auto &image = _userpicFrame + ? tileData.userpicFrame + : _pausedFrame + ? tileData.blurredFrame + : data.original; + const auto frameRotation = _userpicFrame ? 0 : data.rotation; + Assert(!image.isNull()); + + const auto fill = [&](QRect rect) { + const auto intersected = rect.intersected(clip); + if (!intersected.isEmpty()) { + p.fillRect(intersected, st::groupCallMembersBg); + bg -= intersected; + } + }; + + using namespace Media::View; + const auto geometry = tile->geometry(); + const auto x = geometry.x(); + const auto y = geometry.y(); + const auto width = geometry.width(); + const auto height = geometry.height(); + const auto scaled = FlipSizeByRotation( + image.size(), + frameRotation + ).scaled(QSize(width, height), Qt::KeepAspectRatio); + const auto left = (width - scaled.width()) / 2; + const auto top = (height - scaled.height()) / 2; + const auto target = QRect(QPoint(x + left, y + top), scaled); + if (UsePainterRotation(frameRotation)) { + if (frameRotation) { + p.save(); + p.rotate(frameRotation); + } + p.drawImage(RotatedRect(target, frameRotation), image); + if (frameRotation) { + p.restore(); + } + } else if (frameRotation) { + p.drawImage(target, RotateFrameImage(image, frameRotation)); + } else { + p.drawImage(target, image); + } + bg -= target; + + if (left > 0) { + fill({ x, y, left, height }); + } + if (const auto right = left + scaled.width(); right < width) { + fill({ x + right, y, width - right, height }); + } + if (top > 0) { + fill({ x, y, width, top }); + } + if (const auto bottom = top + scaled.height(); bottom < height) { + fill({ x, y + bottom, width, height - bottom }); + } + + paintTileControls(p, x, y, width, height, tile); + paintTileOutline(p, x, y, width, height, tile); +} + +void Viewport::RendererSW::paintTileOutline( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile) { + if (!tile->row()->speaking()) { + return; + } + const auto outline = st::groupCallOutline; + const auto &color = st::groupCallMemberActiveIcon; + p.setPen(Qt::NoPen); + p.fillRect(x, y, outline, height - outline, color); + p.fillRect(x + outline, y, width - outline, outline, color); + p.fillRect( + x + width - outline, + y + outline, + outline, + height - outline, + color); + p.fillRect(x, y + height - outline, width - outline, outline, color); +} + +void Viewport::RendererSW::paintTileControls( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile) { + p.setClipRect(x, y, width, height); + const auto guard = gsl::finally([&] { p.setClipping(false); }); + + const auto wide = _owner->wide(); + if (wide) { + // Pin. + const auto pinInner = tile->pinInner(); + VideoTile::PaintPinButton( + p, + tile->pinned(), + x + pinInner.x(), + y + pinInner.y(), + _owner->widget()->width(), + &_pinBackground, + &_pinIcon); + + // Back. + const auto backInner = tile->backInner(); + VideoTile::PaintBackButton( + p, + x + backInner.x(), + y + backInner.y(), + _owner->widget()->width(), + &_pinBackground); + } + + const auto &st = st::groupCallVideoTile; + const auto nameTop = y + (height + - st.namePosition.y() + - st::semiboldFont->height); + + if (_pausedFrame) { + p.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha)); + + const auto middle = (st::groupCallVideoPlaceholderHeight + - st::groupCallPaused.height()) / 2; + const auto pausedSpace = (nameTop - y) + - st::groupCallPaused.height() + - st::semiboldFont->height; + const auto pauseIconSkip = middle - st::groupCallVideoPlaceholderIconTop; + const auto pauseTextSkip = st::groupCallVideoPlaceholderTextTop + - st::groupCallVideoPlaceholderIconTop; + const auto pauseIconTop = !_owner->wide() + ? (y + (height - st::groupCallPaused.height()) / 2) + : (pausedSpace < 3 * st::semiboldFont->height) + ? (pausedSpace / 3) + : std::min( + y + (height / 2) - pauseIconSkip, + (nameTop + - st::semiboldFont->height * 3 + - st::groupCallPaused.height())); + const auto pauseTextTop = (pausedSpace < 3 * st::semiboldFont->height) + ? (nameTop - (pausedSpace / 3) - st::semiboldFont->height) + : std::min( + pauseIconTop + pauseTextSkip, + nameTop - st::semiboldFont->height * 2); + + st::groupCallPaused.paint( + p, + x + (width - st::groupCallPaused.width()) / 2, + pauseIconTop, + width); + if (_owner->wide()) { + p.drawText( + QRect(x, pauseTextTop, width, y + height - pauseTextTop), + tr::lng_group_call_video_paused(tr::now), + style::al_top); + } + } + + const auto shown = _owner->_controlsShownRatio; + if (shown == 0.) { + return; + } + + const auto fullShift = st.namePosition.y() + st::normalFont->height; + const auto shift = anim::interpolate(fullShift, 0, shown); + + // Shadow. + if (_shadow.isNull()) { + _shadow = GenerateShadow(st.shadowHeight, 0, kShadowMaxAlpha); + } + const auto shadowRect = QRect( + x, + y + (height - anim::interpolate(0, st.shadowHeight, shown)), + width, + st.shadowHeight); + const auto shadowFill = shadowRect.intersected({ x, y, width, height }); + if (shadowFill.isEmpty()) { + return; + } + const auto factor = style::DevicePixelRatio(); + if (!_pausedFrame) { + p.drawImage( + shadowFill, + _shadow, + QRect( + 0, + (shadowFill.y() - shadowRect.y()) * factor, + _shadow.width(), + shadowFill.height() * factor)); + } + const auto row = tile->row(); + row->lazyInitialize(st::groupCallMembersListItem); + + // Mute. + const auto &icon = st::groupCallVideoCrossLine.icon; + const auto iconLeft = x + width - st.iconPosition.x() - icon.width(); + const auto iconTop = y + (height + - st.iconPosition.y() + - icon.height() + + shift); + row->paintMuteIcon( + p, + { iconLeft, iconTop, icon.width(), icon.height() }, + MembersRowStyle::Video); + + // Name. + p.setPen(st::groupCallVideoTextFg); + const auto hasWidth = width + - st.iconPosition.x() - icon.width() + - st.namePosition.x(); + const auto nameLeft = x + st.namePosition.x(); + row->name().drawLeftElided( + p, + nameLeft, + nameTop + shift, + hasWidth, + width); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h new file mode 100644 index 000000000..595a09b7a --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h @@ -0,0 +1,67 @@ +/* +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 "calls/group/calls_group_viewport.h" +#include "ui/round_rect.h" +#include "ui/effects/cross_line.h" +#include "ui/gl/gl_surface.h" +#include "ui/text/text.h" + +namespace Calls::Group { + +class Viewport::RendererSW final : public Ui::GL::Renderer { +public: + explicit RendererSW(not_null owner); + + void paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) override; + +private: + struct TileData { + QImage userpicFrame; + QImage blurredFrame; + bool stale = false; + }; + void paintTile( + Painter &p, + not_null tile, + const QRect &clip, + QRegion &bg); + void paintTileOutline( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile); + void paintTileControls( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile); + void validateUserpicFrame( + not_null tile, + TileData &data); + + const not_null _owner; + + QImage _shadow; + bool _userpicFrame = false; + bool _pausedFrame = false; + base::flat_map, TileData> _tileData; + Ui::CrossLineAnimation _pinIcon; + Ui::RoundRect _pinBackground; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp new file mode 100644 index 000000000..5a438c14f --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp @@ -0,0 +1,264 @@ +/* +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 "calls/group/calls_group_viewport_tile.h" + +#include "webrtc/webrtc_video_track.h" +#include "lang/lang_keys.h" +#include "ui/round_rect.h" +#include "ui/effects/cross_line.h" +#include "styles/style_calls.h" + +#include + +namespace Calls::Group { +namespace { + +constexpr auto kPausedVideoSize = 90; + +} // namespace + +Viewport::VideoTile::VideoTile( + const VideoEndpoint &endpoint, + VideoTileTrack track, + rpl::producer trackSize, + rpl::producer pinned, + Fn update) +: _endpoint(endpoint) +, _update(std::move(update)) +, _track(track) +, _trackSize(std::move(trackSize)) { + Expects(track.track != nullptr); + Expects(track.row != nullptr); + + setup(std::move(pinned)); +} + +QRect Viewport::VideoTile::pinOuter() const { + return _pinOuter; +} + +QRect Viewport::VideoTile::pinInner() const { + return _pinInner.translated(0, -topControlsSlide()); +} + +QRect Viewport::VideoTile::backOuter() const { + return _backOuter; +} + +QRect Viewport::VideoTile::backInner() const { + return _backInner.translated(0, -topControlsSlide()); +} + +int Viewport::VideoTile::topControlsSlide() const { + return anim::interpolate( + st::groupCallVideoTile.pinPosition.y() + _pinInner.height(), + 0, + _topControlsShownAnimation.value(_topControlsShown ? 1. : 0.)); +} + +QSize Viewport::VideoTile::PausedVideoSize() { + return QSize(kPausedVideoSize, kPausedVideoSize); +} + +QSize Viewport::VideoTile::trackOrUserpicSize() const { + if (const auto size = trackSize(); !size.isEmpty()) { + return size; + } else if (_userpicSize.isEmpty() + && _track.track->state() == Webrtc::VideoState::Paused) { + _userpicSize = PausedVideoSize(); + } + return _userpicSize; +} + +bool Viewport::VideoTile::screencast() const { + return (_endpoint.type == VideoEndpointType::Screen); +} + +void Viewport::VideoTile::setGeometry( + QRect geometry, + TileAnimation animation) { + _hidden = false; + _geometry = geometry; + _animation = animation; + updateTopControlsPosition(); +} + +void Viewport::VideoTile::hide() { + _hidden = true; + _quality = std::nullopt; +} + +void Viewport::VideoTile::toggleTopControlsShown(bool shown) { + if (_topControlsShown == shown) { + return; + } + _topControlsShown = shown; + _topControlsShownAnimation.start( + _update, + shown ? 0. : 1., + shown ? 1. : 0., + st::slideWrapDuration); +} + +bool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) { + if (_hidden) { + _quality = std::nullopt; + return false; + } else if (_quality && *_quality == quality) { + return false; + } + _quality = quality; + return true; +} + +QSize Viewport::VideoTile::PinInnerSize(bool pinned) { + const auto &st = st::groupCallVideoTile; + const auto &icon = st::groupCallVideoTile.pin.icon; + const auto innerWidth = icon.width() + + st.pinTextPosition.x() + + st::semiboldFont->width(pinned + ? tr::lng_pinned_unpin(tr::now) + : tr::lng_pinned_pin(tr::now)); + const auto innerHeight = icon.height(); + const auto buttonWidth = st.pinPadding.left() + + innerWidth + + st.pinPadding.right(); + const auto buttonHeight = st.pinPadding.top() + + innerHeight + + st.pinPadding.bottom(); + return { buttonWidth, buttonHeight }; +} + +void Viewport::VideoTile::PaintPinButton( + Painter &p, + bool pinned, + int x, + int y, + int outerWidth, + not_null background, + not_null icon) { + const auto &st = st::groupCallVideoTile; + const auto rect = QRect(QPoint(x, y), PinInnerSize(pinned)); + background->paint(p, rect); + icon->paint( + p, + rect.marginsRemoved(st.pinPadding).topLeft(), + pinned ? 1. : 0.); + p.setPen(st::groupCallVideoTextFg); + p.setFont(st::semiboldFont); + p.drawTextLeft( + (x + + st.pinPadding.left() + + st::groupCallVideoTile.pin.icon.width() + + st.pinTextPosition.x()), + (y + + st.pinPadding.top() + + st.pinTextPosition.y()), + outerWidth, + (pinned + ? tr::lng_pinned_unpin(tr::now) + : tr::lng_pinned_pin(tr::now))); + +} + +QSize Viewport::VideoTile::BackInnerSize() { + const auto &st = st::groupCallVideoTile; + const auto &icon = st::groupCallVideoTile.back; + const auto innerWidth = icon.width() + + st.pinTextPosition.x() + + st::semiboldFont->width(tr::lng_create_group_back(tr::now)); + const auto innerHeight = icon.height(); + const auto buttonWidth = st.pinPadding.left() + + innerWidth + + st.pinPadding.right(); + const auto buttonHeight = st.pinPadding.top() + + innerHeight + + st.pinPadding.bottom(); + return { buttonWidth, buttonHeight }; +} + +void Viewport::VideoTile::PaintBackButton( + Painter &p, + int x, + int y, + int outerWidth, + not_null background) { + const auto &st = st::groupCallVideoTile; + const auto rect = QRect(QPoint(x, y), BackInnerSize()); + background->paint(p, rect); + st.back.paint( + p, + rect.marginsRemoved(st.pinPadding).topLeft(), + outerWidth); + p.setPen(st::groupCallVideoTextFg); + p.setFont(st::semiboldFont); + p.drawTextLeft( + (x + + st.pinPadding.left() + + st::groupCallVideoTile.pin.icon.width() + + st.pinTextPosition.x()), + (y + + st.pinPadding.top() + + st.pinTextPosition.y()), + outerWidth, + tr::lng_create_group_back(tr::now)); +} + +void Viewport::VideoTile::updateTopControlsSize() { + const auto &st = st::groupCallVideoTile; + + const auto pinSize = PinInnerSize(_pinned); + const auto pinWidth = st.pinPosition.x() * 2 + pinSize.width(); + const auto pinHeight = st.pinPosition.y() * 2 + pinSize.height(); + _pinInner = QRect(QPoint(), pinSize); + _pinOuter = QRect(0, 0, pinWidth, pinHeight); + + const auto backSize = BackInnerSize(); + const auto backWidth = st.pinPosition.x() * 2 + backSize.width(); + const auto backHeight = st.pinPosition.y() * 2 + backSize.height(); + _backInner = QRect(QPoint(), backSize); + _backOuter = QRect(0, 0, backWidth, backHeight); +} + +void Viewport::VideoTile::updateTopControlsPosition() { + const auto &st = st::groupCallVideoTile; + + _pinInner = QRect( + _geometry.width() - st.pinPosition.x() - _pinInner.width(), + st.pinPosition.y(), + _pinInner.width(), + _pinInner.height()); + _pinOuter = QRect( + _geometry.width() - _pinOuter.width(), + 0, + _pinOuter.width(), + _pinOuter.height()); + _backInner = QRect(st.pinPosition, _backInner.size()); +} + +void Viewport::VideoTile::setup(rpl::producer pinned) { + std::move( + pinned + ) | rpl::filter([=](bool pinned) { + return (_pinned != pinned); + }) | rpl::start_with_next([=](bool pinned) { + _pinned = pinned; + updateTopControlsSize(); + if (!_hidden) { + updateTopControlsPosition(); + _update(); + } + }, _lifetime); + + _track.track->renderNextFrame( + ) | rpl::start_with_next(_update, _lifetime); + + updateTopControlsSize(); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h new file mode 100644 index 000000000..3a2efa267 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h @@ -0,0 +1,128 @@ +/* +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 "calls/group/calls_group_viewport.h" +#include "calls/group/calls_group_call.h" +#include "ui/effects/animations.h" + +class Painter; +class QOpenGLFunctions; + +namespace Ui { +class CrossLineAnimation; +class RoundRect; +} // namespace Ui + +namespace Calls::Group { + +class Viewport::VideoTile final { +public: + VideoTile( + const VideoEndpoint &endpoint, + VideoTileTrack track, + rpl::producer trackSize, + rpl::producer pinned, + Fn update); + + [[nodiscard]] not_null track() const { + return _track.track; + } + [[nodiscard]] not_null row() const { + return _track.row; + } + [[nodiscard]] QRect geometry() const { + return _geometry; + } + [[nodiscard]] TileAnimation animation() const { + return _animation; + } + [[nodiscard]] bool pinned() const { + return _pinned; + } + [[nodiscard]] bool hidden() const { + return _hidden; + } + [[nodiscard]] bool visible() const { + return !_hidden && !_geometry.isEmpty(); + } + [[nodiscard]] QRect pinOuter() const; + [[nodiscard]] QRect pinInner() const; + [[nodiscard]] QRect backOuter() const; + [[nodiscard]] QRect backInner() const; + [[nodiscard]] const VideoEndpoint &endpoint() const { + return _endpoint; + } + [[nodiscard]] QSize trackSize() const { + return _trackSize.current(); + } + [[nodiscard]] rpl::producer trackSizeValue() const { + return _trackSize.value(); + } + [[nodiscard]] QSize trackOrUserpicSize() const; + [[nodiscard]] static QSize PausedVideoSize(); + + [[nodiscard]] bool screencast() const; + void setGeometry( + QRect geometry, + TileAnimation animation = TileAnimation()); + void hide(); + void toggleTopControlsShown(bool shown); + bool updateRequestedQuality(VideoQuality quality); + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + + [[nodiscard]] static QSize PinInnerSize(bool pinned); + static void PaintPinButton( + Painter &p, + bool pinned, + int x, + int y, + int outerWidth, + not_null background, + not_null icon); + + [[nodiscard]] static QSize BackInnerSize(); + static void PaintBackButton( + Painter &p, + int x, + int y, + int outerWidth, + not_null background); + +private: + void setup(rpl::producer pinned); + [[nodiscard]] int topControlsSlide() const; + void updateTopControlsSize(); + void updateTopControlsPosition(); + + const VideoEndpoint _endpoint; + const Fn _update; + + VideoTileTrack _track; + QRect _geometry; + TileAnimation _animation; + rpl::variable _trackSize; + mutable QSize _userpicSize; + QRect _pinOuter; + QRect _pinInner; + QRect _backOuter; + QRect _backInner; + Ui::Animations::Simple _topControlsShownAnimation; + bool _topControlsShown = false; + bool _pinned = false; + bool _hidden = true; + std::optional _quality; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/calls_volume_item.cpp b/Telegram/SourceFiles/calls/group/calls_volume_item.cpp similarity index 81% rename from Telegram/SourceFiles/calls/calls_volume_item.cpp rename to Telegram/SourceFiles/calls/group/calls_volume_item.cpp index 3d1cc249c..979beb6a4 100644 --- a/Telegram/SourceFiles/calls/calls_volume_item.cpp +++ b/Telegram/SourceFiles/calls/group/calls_volume_item.cpp @@ -5,9 +5,9 @@ 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 "calls/calls_volume_item.h" +#include "calls/group/calls_volume_item.h" -#include "calls/calls_group_common.h" +#include "calls/group/calls_group_common.h" #include "ui/effects/animation_value.h" #include "ui/effects/cross_line.h" #include "ui/widgets/continuous_sliders.h" @@ -30,16 +30,12 @@ constexpr auto kVolumeStickedValues = { 25. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 50. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 75. / kMaxVolumePercent, 2. / kMaxVolumePercent }, - { 100. / kMaxVolumePercent, 5. / kMaxVolumePercent }, + { 100. / kMaxVolumePercent, 10. / kMaxVolumePercent }, { 125. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 150. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 175. / kMaxVolumePercent, 2. / kMaxVolumePercent }, }}; -QString VolumeString(int volumePercent) { - return u"%1%"_q.arg(volumePercent); -} - } // namespace MenuVolumeItem::MenuVolumeItem( @@ -75,20 +71,21 @@ MenuVolumeItem::MenuVolumeItem( sizeValue( ) | rpl::start_with_next([=](const QSize &size) { const auto geometry = QRect(QPoint(), size); - _itemRect = geometry - _st.itemPadding; + _itemRect = geometry - st::groupCallMenuVolumePadding; _speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size()); _arcPosition = _speakerRect.center() + QPoint(0, st::groupCallMenuSpeakerArcsSkip); - _volumeRect = QRect( - _arcPosition.x() - + st::groupCallMenuVolumeSkip - + _arcs->finishedWidth(), + const auto sliderLeft = _arcPosition.x() + + st::groupCallMenuVolumeSkip + + _arcs->maxWidth() + + st::groupCallMenuVolumeSkip; + _slider->setGeometry( + st::groupCallMenuVolumeMargin.left(), _speakerRect.y(), - _st.itemStyle.font->width(VolumeString(kMaxVolumePercent)), + (geometry.width() + - st::groupCallMenuVolumeMargin.left() + - st::groupCallMenuVolumeMargin.right()), _speakerRect.height()); - - _slider->setGeometry(_itemRect - - style::margins(0, contentHeight() / 2, 0, 0)); }, lifetime()); setCloudVolume(startVolume); @@ -110,15 +107,12 @@ MenuVolumeItem::MenuVolumeItem( unmuteColor(), muteColor(), muteProgress); - p.setPen(mutePen); - p.setFont(_st.itemStyle.font); - p.drawText(_volumeRect, VolumeString(volume), style::al_left); _crossLineMute->paint( p, _speakerRect.topLeft(), muteProgress, - (!muteProgress) ? std::nullopt : std::optional(mutePen)); + (muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt); { p.translate(_arcPosition); @@ -133,7 +127,7 @@ MenuVolumeItem::MenuVolumeItem( _toggleMuteLocallyRequests.fire_copy(newMuted); _crossLineAnimation.start( - [=] { update(_speakerRect.united(_volumeRect)); }, + [=] { update(_speakerRect); }, _localMuted ? 0. : 1., _localMuted ? 1. : 0., st::callPanelDuration); @@ -141,8 +135,8 @@ MenuVolumeItem::MenuVolumeItem( if (value > 0) { _changeVolumeLocallyRequests.fire(value * _maxVolume); } - update(_volumeRect); _arcs->setValue(value); + updateSliderColor(value); }); const auto returnVolume = [=] { @@ -169,6 +163,7 @@ MenuVolumeItem::MenuVolumeItem( if (!_cloudMuted && !muted) { _changeVolumeRequests.fire_copy(newVolume); } + updateSliderColor(value); }); std::move( @@ -209,30 +204,15 @@ MenuVolumeItem::MenuVolumeItem( } void MenuVolumeItem::initArcsAnimation() { - const auto volumeLeftWas = lifetime().make_state(0); const auto lastTime = lifetime().make_state(0); _arcsAnimation.init([=](crl::time now) { _arcs->update(now); update(_speakerRect); - - const auto wasRect = _volumeRect; - _volumeRect.moveLeft(anim::interpolate( - *volumeLeftWas, - _arcPosition.x() - + st::groupCallMenuVolumeSkip - + _arcs->finishedWidth(), - std::clamp( - (now - (*lastTime)) - / float64(st::groupCallSpeakerArcsAnimation.duration), - 0., - 1.))); - update(_speakerRect.united(wasRect.united(_volumeRect))); }); _arcs->startUpdateRequests( ) | rpl::start_with_next([=] { if (!_arcsAnimation.animating()) { - *volumeLeftWas = _volumeRect.left(); *lastTime = crl::now(); _arcsAnimation.start(); } @@ -269,8 +249,30 @@ void MenuVolumeItem::setCloudVolume(int volume) { } void MenuVolumeItem::setSliderVolume(int volume) { - _slider->setValue(float64(volume) / _maxVolume); - update(_volumeRect); + const auto value = float64(volume) / _maxVolume; + _slider->setValue(value); + updateSliderColor(value); +} + +void MenuVolumeItem::updateSliderColor(float64 value) { + value = std::clamp(value, 0., 1.); + const auto color = [](int rgb) { + return QColor( + int((rgb & 0xFF0000) >> 16), + int((rgb & 0x00FF00) >> 8), + int(rgb & 0x0000FF)); + }; + const auto colors = std::array{ { + color(0xF66464), + color(0xD0B738), + color(0x24CD80), + color(0x3BBCEC), + } }; + _slider->setActiveFgOverride((value < 0.25) + ? anim::color(colors[0], colors[1], value / 0.25) + : (value < 0.5) + ? anim::color(colors[1], colors[2], (value - 0.25) / 0.25) + : anim::color(colors[2], colors[3], (value - 0.5) / 0.5)); } not_null MenuVolumeItem::action() const { @@ -282,9 +284,9 @@ bool MenuVolumeItem::isEnabled() const { } int MenuVolumeItem::contentHeight() const { - return _st.itemPadding.top() - + _st.itemPadding.bottom() - + _stCross.icon.height() * 2; + return st::groupCallMenuVolumePadding.top() + + st::groupCallMenuVolumePadding.bottom() + + _stCross.icon.height(); } rpl::producer MenuVolumeItem::toggleMuteRequests() const { diff --git a/Telegram/SourceFiles/calls/calls_volume_item.h b/Telegram/SourceFiles/calls/group/calls_volume_item.h similarity index 98% rename from Telegram/SourceFiles/calls/calls_volume_item.h rename to Telegram/SourceFiles/calls/group/calls_volume_item.h index 5ba925835..86eadd1f9 100644 --- a/Telegram/SourceFiles/calls/calls_volume_item.h +++ b/Telegram/SourceFiles/calls/group/calls_volume_item.h @@ -52,6 +52,7 @@ private: void setCloudVolume(int volume); void setSliderVolume(int volume); + void updateSliderColor(float64 value); QColor unmuteColor() const; QColor muteColor() const; @@ -64,7 +65,6 @@ private: QRect _itemRect; QRect _speakerRect; - QRect _volumeRect; QPoint _arcPosition; const base::unique_qptr _slider; diff --git a/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp new file mode 100644 index 000000000..aa793b6e6 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.cpp @@ -0,0 +1,136 @@ +/* +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 "calls/group/ui/calls_group_scheduled_labels.h" + +#include "ui/rp_widget.h" +#include "lang/lang_keys.h" +#include "base/unixtime.h" +#include "base/timer_rpl.h" +#include "styles/style_calls.h" + +#include + +namespace Calls::Group::Ui { + +rpl::producer StartsWhenText(rpl::producer date) { + return std::move( + date + ) | rpl::map([](TimeId date) -> rpl::producer { + const auto parsedDate = base::unixtime::parse(date); + const auto dateDay = QDateTime(parsedDate.date(), QTime(0, 0)); + const auto previousDay = QDateTime( + parsedDate.date().addDays(-1), + QTime(0, 0)); + const auto now = QDateTime::currentDateTime(); + const auto kDay = int64(24 * 60 * 60); + const auto tillTomorrow = int64(now.secsTo(previousDay)); + const auto tillToday = tillTomorrow + kDay; + const auto tillAfter = tillToday + kDay; + + const auto time = parsedDate.time().toString( + QLocale::system().timeFormat(QLocale::ShortFormat)); + auto exact = tr::lng_group_call_starts_short_date( + lt_date, + rpl::single(langDayOfMonthFull(dateDay.date())), + lt_time, + rpl::single(time) + ) | rpl::type_erased(); + auto tomorrow = tr::lng_group_call_starts_short_tomorrow( + lt_time, + rpl::single(time)); + auto today = tr::lng_group_call_starts_short_today( + lt_time, + rpl::single(time)); + + auto todayAndAfter = rpl::single( + std::move(today) + ) | rpl::then(base::timer_once( + std::min(tillAfter, kDay) * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(exact); + })) | rpl::flatten_latest() | rpl::type_erased(); + + auto tomorrowAndAfter = rpl::single( + std::move(tomorrow) + ) | rpl::then(base::timer_once( + std::min(tillToday, kDay) * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(todayAndAfter); + })) | rpl::flatten_latest() | rpl::type_erased(); + + auto full = rpl::single( + rpl::duplicate(exact) + ) | rpl::then(base::timer_once( + tillTomorrow * crl::time(1000) + ) | rpl::map([=] { + return rpl::duplicate(tomorrowAndAfter); + })) | rpl::flatten_latest() | rpl::type_erased(); + + if (tillTomorrow > 0) { + return full; + } else if (tillToday > 0) { + return tomorrowAndAfter; + } else if (tillAfter > 0) { + return todayAndAfter; + } else { + return exact; + } + }) | rpl::flatten_latest(); +} + +object_ptr CreateGradientLabel( + QWidget *parent, + rpl::producer text) { + struct State { + QBrush brush; + QPainterPath path; + }; + auto result = object_ptr(parent); + const auto raw = result.data(); + const auto state = raw->lifetime().make_state(); + + std::move( + text + ) | rpl::start_with_next([=](const QString &text) { + state->path = QPainterPath(); + const auto &font = st::groupCallCountdownFont; + state->path.addText(0, font->ascent, font->f, text); + const auto width = font->width(text); + raw->resize(width, font->height); + auto gradient = QLinearGradient(QPoint(width, 0), QPoint()); + gradient.setStops(QGradientStops{ + { 0.0, st::groupCallForceMutedBar1->c }, + { .7, st::groupCallForceMutedBar2->c }, + { 1.0, st::groupCallForceMutedBar3->c } + }); + state->brush = QBrush(std::move(gradient)); + raw->update(); + }, raw->lifetime()); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + const auto skip = st::groupCallWidth / 20; + const auto available = parent->width() - 2 * skip; + const auto full = raw->width(); + if (available > 0 && full > available) { + const auto scale = available / float64(full); + const auto shift = raw->rect().center(); + p.translate(shift); + p.scale(scale, scale); + p.translate(-shift); + } + p.setPen(Qt::NoPen); + p.setBrush(state->brush); + p.drawPath(state->path); + }, raw->lifetime()); + return result; +} + +} // namespace Calls::Group::Ui diff --git a/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.h b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.h new file mode 100644 index 000000000..d09a57b84 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/calls_group_scheduled_labels.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Calls::Group::Ui { + +using namespace ::Ui; + +[[nodiscard]] rpl::producer StartsWhenText( + rpl::producer date); + +[[nodiscard]] object_ptr CreateGradientLabel( + QWidget *parent, + rpl::producer text); + +} // namespace Calls::Group::Ui diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp new file mode 100644 index 000000000..2203195b0 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -0,0 +1,564 @@ +/* +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 "calls/group/ui/desktop_capture_choose_source.h" + +#include "ui/widgets/window.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/buttons.h" +#include "ui/effects/ripple_animation.h" +#include "ui/image/image.h" +#include "ui/platform/ui_platform_window_title.h" +#include "base/platform/base_platform_info.h" +#include "webrtc/webrtc_video_track.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" + +#include +#include +#include + +namespace Calls::Group::Ui::DesktopCapture { +namespace { + +constexpr auto kColumns = 3; +constexpr auto kRows = 2; + +struct Preview { + explicit Preview(tgcalls::DesktopCaptureSource source); + + tgcalls::DesktopCaptureSourceHelper helper; + Webrtc::VideoTrack track; + rpl::lifetime lifetime; +}; + +class SourceButton final : public RippleButton { +public: + using RippleButton::RippleButton; + +private: + QImage prepareRippleMask() const override; + +}; + +QImage SourceButton::prepareRippleMask() const { + return RippleAnimation::roundRectMask(size(), st::roundRadiusLarge); +} + +class Source final { +public: + Source( + not_null parent, + tgcalls::DesktopCaptureSource source, + const QString &title); + + void setGeometry(QRect geometry); + void clearHelper(); + + [[nodiscard]] rpl::producer<> activations() const; + void setActive(bool active); + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void paint(); + void setupPreview(); + + SourceButton _widget; + FlatLabel _label; + RoundRect _selectedRect; + RoundRect _activeRect; + tgcalls::DesktopCaptureSource _source; + std::unique_ptr _preview; + rpl::event_stream<> _activations; + QImage _frame; + bool _active = false; + +}; + +class ChooseSourceProcess final { +public: + static void Start(not_null delegate); + + explicit ChooseSourceProcess(not_null delegate); + + void activate(); + +private: + void setupPanel(); + void setupSources(); + void setupGeometryWithParent(not_null parent); + void fillSources(); + void setupSourcesGeometry(); + void destroy(); + + static base::flat_map< + not_null, + std::unique_ptr> &Map(); + + const not_null _delegate; + const std::unique_ptr _window; + const std::unique_ptr _scroll; + const not_null _inner; + const not_null _bottom; + const not_null _submit; + const not_null _finish; + + std::vector> _sources; + Source *_selected = nullptr; + QString _selectedId; + +}; + +[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() { + const auto factor = style::DevicePixelRatio(); + const auto size = st::desktopCaptureSourceSize * factor; + return { + .aspectSize = { size.width(), size.height() }, + .fps = 1, + .captureMouse = false, + }; +} + +Preview::Preview(tgcalls::DesktopCaptureSource source) +: helper(source, SourceData()) +, track(Webrtc::VideoState::Active) { + helper.setOutput(track.sink()); + helper.start(); +} + +Source::Source( + not_null parent, + tgcalls::DesktopCaptureSource source, + const QString &title) +: _widget(parent, st::groupCallRipple) +, _label(&_widget, title, st::desktopCaptureLabel) +, _selectedRect(ImageRoundRadius::Large, st::groupCallMembersBgOver) +, _activeRect(ImageRoundRadius::Large, st::groupCallMuted1) +, _source(source) { + _widget.paintRequest( + ) | rpl::start_with_next([=] { + paint(); + }, _widget.lifetime()); + + _label.setAttribute(Qt::WA_TransparentForMouseEvents); + + _widget.sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto padding = st::desktopCapturePadding; + _label.resizeToNaturalWidth( + size.width() - padding.left() - padding.right()); + _label.move( + (size.width() - _label.width()) / 2, + size.height() - _label.height() - st::desktopCaptureLabelBottom); + }, _label.lifetime()); + + _widget.setClickedCallback([=] { + setActive(true); + }); +} + +rpl::producer<> Source::activations() const { + return _activations.events(); +} + +void Source::setActive(bool active) { + if (_active != active) { + _active = active; + _widget.update(); + if (active) { + _activations.fire({}); + } + } +} + +void Source::setGeometry(QRect geometry) { + _widget.setGeometry(geometry); +} + +void Source::clearHelper() { + _preview = nullptr; +} + +void Source::paint() { + auto p = QPainter(&_widget); + + if (_frame.isNull() && !_preview) { + setupPreview(); + } + if (_active) { + _activeRect.paint(p, _widget.rect()); + } else if (_widget.isOver() || _widget.isDown()) { + _selectedRect.paint(p, _widget.rect()); + } + _widget.paintRipple( + p, + { 0, 0 }, + _active ? &st::shadowFg->c : nullptr); + + const auto size = _preview ? _preview->track.frameSize() : QSize(); + const auto factor = style::DevicePixelRatio(); + const auto padding = st::desktopCapturePadding; + const auto rect = _widget.rect(); + const auto inner = rect.marginsRemoved(padding); + if (!size.isEmpty()) { + const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio); + const auto request = Webrtc::FrameRequest{ + .resize = scaled * factor, + .outer = scaled * factor, + }; + _frame = _preview->track.frame(request); + _preview->track.markFrameShown(); + } + if (!_frame.isNull()) { + clearHelper(); + const auto size = _frame.size() / factor; + const auto x = inner.x() + (inner.width() - size.width()) / 2; + const auto y = inner.y() + (inner.height() - size.height()) / 2; + auto hq = PainterHighQualityEnabler(p); + p.drawImage(QRect(x, y, size.width(), size.height()), _frame); + } +} + +void Source::setupPreview() { + _preview = std::make_unique(_source); + _preview->track.renderNextFrame( + ) | rpl::start_with_next([=] { + if (_preview->track.frameSize().isEmpty()) { + _preview->track.markFrameShown(); + } + _widget.update(); + }, _preview->lifetime); +} + +rpl::lifetime &Source::lifetime() { + return _widget.lifetime(); +} + +ChooseSourceProcess::ChooseSourceProcess( + not_null delegate) +: _delegate(delegate) +, _window(std::make_unique()) +, _scroll(std::make_unique(_window->body())) +, _inner(_scroll->setOwnedWidget(object_ptr(_scroll.get()))) +, _bottom(CreateChild(_window->body().get())) +, _submit( + CreateChild( + _bottom.get(), + tr::lng_group_call_screen_share_start(), + st::desktopCaptureSubmit)) +, _finish( + CreateChild( + _bottom.get(), + tr::lng_group_call_screen_share_stop(), + st::desktopCaptureFinish)) { + setupPanel(); + setupSources(); + activate(); +} + +void ChooseSourceProcess::Start(not_null delegate) { + auto &map = Map(); + auto i = map.find(delegate); + if (i == end(map)) { + i = map.emplace(delegate, nullptr).first; + delegate->chooseSourceInstanceLifetime().add([=] { + Map().erase(delegate); + }); + } + if (!i->second) { + i->second = std::make_unique(delegate); + } else { + i->second->activate(); + } +} + +void ChooseSourceProcess::activate() { + if (_window->windowState() & Qt::WindowMinimized) { + _window->showNormal(); + } else { + _window->show(); + } + _window->raise(); + _window->activateWindow(); +} + +[[nodiscard]] base::flat_map< + not_null, + std::unique_ptr> &ChooseSourceProcess::Map() { + static auto result = base::flat_map< + not_null, + std::unique_ptr>(); + return result; +} + +void ChooseSourceProcess::setupPanel() { +#ifndef Q_OS_LINUX + //_window->setAttribute(Qt::WA_OpaquePaintEvent); +#endif // Q_OS_LINUX + //_window->setAttribute(Qt::WA_NoSystemBackground); + + _window->setWindowIcon(QIcon( + QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); + _window->setTitleStyle(st::desktopCaptureSourceTitle); + + const auto skips = st::desktopCaptureSourceSkips; + const auto margins = st::desktopCaptureMargins; + const auto padding = st::desktopCapturePadding; + const auto bottomSkip = margins.right() + padding.right(); + const auto bottomHeight = 2 * bottomSkip + + st::desktopCaptureCancel.height; + const auto width = margins.left() + + kColumns * st::desktopCaptureSourceSize.width() + + (kColumns - 1) * skips.width() + + margins.right(); + const auto height = margins.top() + + kRows * st::desktopCaptureSourceSize.height() + + (kRows - 1) * skips.height() + + (st::desktopCaptureSourceSize.height() / 2) + + bottomHeight; + _window->setFixedSize({ width, height }); + _window->setStaysOnTop(true); + + _window->body()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(_window->body()).fillRect(clip, st::groupCallMembersBg); + }, _window->lifetime()); + + _bottom->setGeometry(0, height - bottomHeight, width, bottomHeight); + + _submit->setClickedCallback([=] { + if (_selectedId.isEmpty()) { + return; + } + const auto weak = MakeWeak(_window.get()); + _delegate->chooseSourceAccepted(_selectedId); + if (const auto strong = weak.data()) { + strong->close(); + } + }); + _finish->setClickedCallback([=] { + const auto weak = MakeWeak(_window.get()); + _delegate->chooseSourceStop(); + if (const auto strong = weak.data()) { + strong->close(); + } + }); + const auto cancel = CreateChild( + _bottom.get(), + tr::lng_cancel(), + st::desktopCaptureCancel); + cancel->setClickedCallback([=] { + _window->close(); + }); + + rpl::combine( + _submit->widthValue(), + _submit->shownValue(), + _finish->widthValue(), + _finish->shownValue(), + cancel->widthValue() + ) | rpl::start_with_next([=]( + int submitWidth, + bool submitShown, + int finishWidth, + bool finishShown, + int cancelWidth) { + _finish->moveToRight(bottomSkip, bottomSkip); + _submit->moveToRight(bottomSkip, bottomSkip); + cancel->moveToRight( + bottomSkip * 2 + (submitShown ? submitWidth : finishWidth), + bottomSkip); + }, _bottom->lifetime()); + + const auto sharing = !_delegate->chooseSourceActiveDeviceId().isEmpty(); + _finish->setVisible(sharing); + _submit->setVisible(!sharing); + + _window->body()->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + _scroll->setGeometry( + 0, + 0, + size.width(), + size.height() - _bottom->height()); + }, _scroll->lifetime()); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto innerHeight = margins.top() + + rows * st::desktopCaptureSourceSize.height() + + (rows - 1) * skips.height() + + margins.bottom(); + _inner->resize(width, std::max(height, innerHeight)); + }, _inner->lifetime()); + + if (const auto parent = _delegate->chooseSourceParent()) { + setupGeometryWithParent(parent); + } + + _window->events( + ) | rpl::filter([=](not_null e) { + return e->type() == QEvent::Close; + }) | rpl::start_with_next([=] { + destroy(); + }, _window->lifetime()); +} + +void ChooseSourceProcess::setupSources() { + fillSources(); + setupSourcesGeometry(); +} + +void ChooseSourceProcess::fillSources() { + using Type = tgcalls::DesktopCaptureType; + auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen); + auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window); + + auto screenIndex = 0; + auto windowIndex = 0; + const auto active = _delegate->chooseSourceActiveDeviceId(); + const auto append = [&](const tgcalls::DesktopCaptureSource &source) { + const auto title = !source.isWindow() + ? tr::lng_group_call_screen_title( + tr::now, + lt_index, + QString::number(++screenIndex)) + : !source.title().empty() + ? QString::fromStdString(source.title()) + : "Window " + QString::number(++windowIndex); + const auto id = source.deviceIdKey(); + _sources.push_back(std::make_unique(_inner, source, title)); + + const auto raw = _sources.back().get(); + if (!active.isEmpty() && active.toStdString() == id) { + _selected = raw; + raw->setActive(true); + } + _sources.back()->activations( + ) | rpl::filter([=] { + return (_selected != raw); + }) | rpl::start_with_next([=]{ + if (_selected) { + _selected->setActive(false); + } + _selected = raw; + _selectedId = QString::fromStdString(id); + if (_selectedId == _delegate->chooseSourceActiveDeviceId()) { + _selectedId = QString(); + _finish->setVisible(true); + _submit->setVisible(false); + } else { + _finish->setVisible(false); + _submit->setVisible(true); + } + }, raw->lifetime()); + }; + for (const auto &source : screensManager.sources()) { + append(source); + } + for (const auto &source : windowsManager.sources()) { + append(source); + } +} + +void ChooseSourceProcess::setupSourcesGeometry() { + if (_sources.empty()) { + destroy(); + return; + } + _inner->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto margins = st::desktopCaptureMargins; + const auto skips = st::desktopCaptureSourceSkips; + const auto single = (width + - margins.left() + - margins.right() + - (kColumns - 1) * skips.width()) / kColumns; + const auto height = st::desktopCaptureSourceSize.height(); + auto top = margins.top(); + auto index = 0; + for (auto row = 0; row != rows; ++row) { + auto left = margins.left(); + for (auto column = 0; column != kColumns; ++column) { + _sources[index]->setGeometry({ left, top, single, height }); + if (++index == _sources.size()) { + break; + } + left += single + skips.width(); + } + if (index >= _sources.size()) { + break; + } + top += height + skips.height(); + } + }, _inner->lifetime()); + + rpl::combine( + _scroll->scrollTopValue(), + _scroll->heightValue() + ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto margins = st::desktopCaptureMargins; + const auto skips = st::desktopCaptureSourceSkips; + const auto height = st::desktopCaptureSourceSize.height(); + auto top = margins.top(); + auto index = 0; + for (auto row = 0; row != rows; ++row) { + const auto hidden = (top + height <= scrollTop) + || (top >= scrollTop + scrollHeight); + if (hidden) { + for (auto column = 0; column != kColumns; ++column) { + _sources[index]->clearHelper(); + if (++index == _sources.size()) { + break; + } + } + } else { + index += kColumns; + } + if (index >= _sources.size()) { + break; + } + top += height + skips.height(); + } + }, _inner->lifetime()); +} + +void ChooseSourceProcess::setupGeometryWithParent( + not_null parent) { + if (const auto handle = parent->windowHandle()) { + _window->createWinId(); + const auto parentScreen = handle->screen(); + const auto myScreen = _window->windowHandle()->screen(); + if (parentScreen && myScreen != parentScreen) { + _window->windowHandle()->setScreen(parentScreen); + } + } + _window->move( + parent->x() + (parent->width() - _window->width()) / 2, + parent->y() + (parent->height() - _window->height()) / 2); +} + +void ChooseSourceProcess::destroy() { + auto &map = Map(); + if (const auto i = map.find(_delegate); i != end(map)) { + if (i->second.get() == this) { + base::take(i->second); + } + } +} + +} // namespace + +void ChooseSource(not_null delegate) { + ChooseSourceProcess::Start(delegate); +} + +} // namespace Calls::Group::Ui::DesktopCapture diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h new file mode 100644 index 000000000..82f0fb9e5 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h @@ -0,0 +1,30 @@ +/* +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 Ui { +} // namespace Ui + +namespace Calls::Group::Ui { +using namespace ::Ui; +} // namespace Calls::Group::Ui + +namespace Calls::Group::Ui::DesktopCapture { + +class ChooseSourceDelegate { +public: + virtual QWidget *chooseSourceParent() = 0; + virtual QString chooseSourceActiveDeviceId() = 0; + virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0; + virtual void chooseSourceAccepted(const QString &deviceId) = 0; + virtual void chooseSourceStop() = 0; +}; + +void ChooseSource(not_null delegate); + +} // namespace Calls::Group::Ui::DesktopCapture diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 25ae77ad8..58880770d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -248,10 +248,6 @@ emojiSuggestionsPadding: margins(emojiColorsPadding, 0px, emojiColorsPadding, 0p emojiSuggestionsFadeAfter: 20px; mentionHeight: 40px; -mentionScroll: ScrollArea(defaultScrollArea) { - topsh: 0px; - bottomsh: 0px; -} mentionPadding: margins(8px, 5px, 8px, 5px); mentionTop: 11px; mentionFont: linkFont; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp index 692535903..565754665 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp @@ -529,8 +529,7 @@ void EmojiKeywords::apiChanged(ApiWrap *api) { _api = api; if (_api) { crl::on_main(&_api->session(), crl::guard(&_guard, [=] { - base::ObservableViewer( - Lang::CurrentCloudManager().firstLanguageSuggestion() + Lang::CurrentCloudManager().firstLanguageSuggestion( ) | rpl::filter([=] { // Refresh with the suggested language if we already were asked. return !_data.empty(); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index aa554e19c..f1e0e176e 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -526,14 +526,14 @@ SuggestionsController::SuggestionsController( setReplaceCallback(nullptr); const auto fieldCallback = [=](not_null event) { - return fieldFilter(event) + return (_container && fieldFilter(event)) ? base::EventFilterResult::Cancel : base::EventFilterResult::Continue; }; _fieldFilter.reset(base::install_event_filter(_field, fieldCallback)); const auto outerCallback = [=](not_null event) { - return outerFilter(event) + return (_container && outerFilter(event)) ? base::EventFilterResult::Cancel : base::EventFilterResult::Continue; }; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 8cb4151cc..b4892ab87 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -34,8 +34,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/cached_round_corners.h" #include "base/unixtime.h" #include "base/openssl_help.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" -#include "facades.h" #include "styles/style_chat.h" #include "styles/style_widgets.h" #include "styles/style_chat_helpers.h" @@ -123,6 +123,8 @@ private: bool _previewShown = false; + bool _isOneColumn = false; + Fn _sendMenuType; rpl::event_stream _mentionChosen; @@ -140,7 +142,7 @@ FieldAutocomplete::FieldAutocomplete( not_null controller) : RpWidget(parent) , _controller(controller) -, _scroll(this, st::mentionScroll) { +, _scroll(this) { hide(); _scroll->setGeometry(rect()); @@ -745,6 +747,12 @@ FieldAutocomplete::Inner::Inner( ) | rpl::start_with_next([=] { update(); }, lifetime()); + + controller->adaptive().value( + ) | rpl::start_with_next([=] { + _isOneColumn = controller->adaptive().isOneColumn(); + update(); + }, lifetime()); } void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { @@ -763,7 +771,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { auto htagwidth = width() - st::mentionPadding.right() - htagleft - - st::mentionScroll.width; + - st::defaultScrollArea.width; if (!_srows->empty()) { int32 rows = rowscount(_srows->size(), _stickersPerRow); @@ -941,9 +949,9 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) { } } } - p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); + p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); } - p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); + p.fillRect(_isOneColumn ? 0 : st::lineWidth, _parent->innerTop(), width() - (_isOneColumn ? 0 : st::lineWidth), st::lineWidth, st::shadowFg); } void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) { @@ -1017,7 +1025,7 @@ bool FieldAutocomplete::Inner::chooseAtIndex( FieldAutocomplete::ChooseMethod method, int index, Api::SendOptions options) const { - if (index < 0) { + if (index < 0 || (method == ChooseMethod::ByEnter && _mouseSelection)) { return false; } if (!_srows->empty()) { diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 5e1c93333..abfde7084 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -188,12 +188,13 @@ GifsListWidget::GifsListWidget( update(); }, lifetime()); - subscribe(controller->gifPauseLevelChanged(), [=] { + controller->gifPauseLevelChanged( + ) | rpl::start_with_next([=] { if (!controller->isGifPausedAtLeastFor( Window::GifPauseReason::SavedGifs)) { update(); } - }); + }, lifetime()); } rpl::producer GifsListWidget::fileChosen() const { diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h index 0f1bce421..279934ea6 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.h @@ -46,8 +46,7 @@ void AddGifAction( class GifsListWidget : public TabbedSelector::Inner - , public InlineBots::Layout::Context - , private base::Subscriber { + , public InlineBots::Layout::Context { public: using InlineChosen = TabbedSelector::InlineChosen; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index e4d70743f..b9f146da0 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -281,7 +281,7 @@ Fn(controller, text, link, [=]( + controller->show(Box(controller, text, link, [=]( const QString &text, const QString &link) { if (const auto strong = weak.data()) { @@ -321,7 +321,9 @@ void InitSpellchecker( Core::App().settings().spellcheckerEnabledValue(), Spellchecker::SpellingHighlighter::CustomContextMenuItem{ tr::lng_settings_manage_dictionaries(tr::now), - [=] { Ui::show(Box(controller)); } + [=] { + controller->show(Box(controller)); + } }); field->setExtendedContextMenu(s->contextMenuCreated()); #endif // TDESKTOP_DISABLE_SPELLCHECK diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 61a4f21e4..89ff1e421 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -533,7 +533,7 @@ void StickersListWidget::Footer::mousePressEvent(QMouseEvent *e) { updateSelected(); if (_iconOver == SpecialOver::Settings) { - Ui::show(Box( + _pan->controller()->show(Box( _pan->controller(), (hasOnlyFeaturedSets() ? StickersBox::Section::Featured @@ -908,7 +908,7 @@ StickersListWidget::StickersListWidget( setAttribute(Qt::WA_OpaquePaintEvent); _settings->addClickHandler([=] { - Ui::show( + controller->show( Box(controller, StickersBox::Section::Installed)); }); @@ -2189,7 +2189,7 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { removeSet(sets[button->section].id); } } else if (std::get_if(&pressed)) { - Ui::show(Box(controller(), _megagroupSet)); + controller()->show(Box(controller(), _megagroupSet)); } } } @@ -3010,7 +3010,7 @@ void StickersListWidget::displaySet(uint64 setId) { if (setId == Data::Stickers::MegagroupSetId) { if (_megagroupSet->canEditStickers()) { _displayingSet = true; - checkHideWithBox(Ui::show( + checkHideWithBox(controller()->show( Box(controller(), _megagroupSet), Ui::LayerOption::KeepOther).data()); return; @@ -3024,7 +3024,7 @@ void StickersListWidget::displaySet(uint64 setId) { auto it = sets.find(setId); if (it != sets.cend()) { _displayingSet = true; - checkHideWithBox(Ui::show( + checkHideWithBox(controller()->show( Box(controller(), it->second->mtpInput()), Ui::LayerOption::KeepOther).data()); } @@ -3088,7 +3088,7 @@ void StickersListWidget::removeMegagroupSet(bool locally) { return; } _removingSetId = Data::Stickers::MegagroupSetId; - Ui::show(Box(tr::lng_stickers_remove_group_set(tr::now), crl::guard(this, [this, group = _megagroupSet] { + controller()->show(Box(tr::lng_stickers_remove_group_set(tr::now), crl::guard(this, [this, group = _megagroupSet] { Expects(group->mgInfo != nullptr); if (group->mgInfo->stickerSet.type() != mtpc_inputStickerSetEmpty) { @@ -3110,7 +3110,7 @@ void StickersListWidget::removeSet(uint64 setId) { const auto set = it->second.get(); _removingSetId = set->id; auto text = tr::lng_stickers_remove_pack(tr::now, lt_sticker_pack, set->title); - Ui::show(Box(text, tr::lng_stickers_remove_pack_confirm(tr::now), crl::guard(this, [=] { + controller()->show(Box(text, tr::lng_stickers_remove_pack_confirm(tr::now), crl::guard(this, [=] { Ui::hideLayer(); const auto &sets = session().data().stickers().sets(); const auto it = sets.find(_removingSetId); diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 1c6b54568..c21ff6544 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_user.h" #include "base/timer.h" +#include "base/event_filter.h" #include "base/concurrent_timer.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" @@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/emoji_keywords.h" #include "chat_helpers/stickers_emoji_image_loader.h" #include "base/platform/base_platform_last_input.h" +#include "base/platform/base_platform_info.h" #include "platform/platform_specific.h" #include "mainwindow.h" #include "dialogs/dialogs_entry.h" @@ -42,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "media/view/media_view_overlay_widget.h" +#include "media/view/media_view_open_common.h" #include "mtproto/mtproto_dc_options.h" #include "mtproto/mtproto_config.h" #include "mtproto/mtp_instance.h" @@ -73,7 +76,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_phone_box.h" #include "boxes/confirm_box.h" #include "boxes/share_box.h" -#include "facades.h" #include "app.h" #include @@ -195,7 +197,6 @@ Application::~Application() { Media::Player::finish(_audio.get()); style::stopManager(); - Global::finish(); ThirdParty::finish(); Instance = nullptr; @@ -205,8 +206,7 @@ void Application::run() { style::internal::StartFonts(); ThirdParty::start(); - Global::start(); - refreshGlobalProxy(); // Depends on Global::start(). + refreshGlobalProxy(); // Depends on Core::IsAppLaunched(). // Depends on OpenSSL on macOS, so on ThirdParty::start(). // Depends on notifications settings. @@ -305,6 +305,43 @@ void Application::run() { for (const auto &error : Shortcuts::Errors()) { LOG(("Shortcuts Error: %1").arg(error)); } + + if (!Platform::IsMac() + && Ui::Integration::Instance().openglLastCheckFailed()) { + showOpenGLCrashNotification(); + } + + _window->openInMediaViewRequests( + ) | rpl::start_with_next([=](Media::View::OpenRequest &&request) { + if (_mediaView) { + _mediaView->show(std::move(request)); + } + }, _window->lifetime()); +} + +void Application::showOpenGLCrashNotification() { + const auto enable = [=] { + Ui::GL::ForceDisable(false); + Ui::Integration::Instance().openglCheckFinish(); + Core::App().settings().setDisableOpenGL(false); + Local::writeSettings(); + App::restart(); + }; + const auto keepDisabled = [=] { + Ui::GL::ForceDisable(true); + Ui::Integration::Instance().openglCheckFinish(); + Core::App().settings().setDisableOpenGL(true); + Local::writeSettings(); + }; + _window->show(Box( + "There may be a problem with your graphics drivers and OpenGL. " + "Try updating your drivers.\n\n" + "OpenGL has been disabled. You can try to enable it again " + "or keep it disabled if crashes continue.", + "Enable", + "Keep Disabled", + enable, + keepDisabled)); } void Application::startDomain() { @@ -314,8 +351,6 @@ void Application::startDomain() { startSettingsAndBackground(); } if (state != Storage::StartResult::Success) { - Global::SetLocalPasscode(true); - Global::RefLocalPasscodeChanged().notify(); lockByPasscode(); DEBUG_LOG(("Application Info: passcode needed...")); } @@ -380,57 +415,6 @@ bool Application::hideMediaView() { return false; } -void Application::showPhoto(not_null link) { - const auto photo = link->photo(); - const auto peer = link->peer(); - const auto item = photo->owner().message(link->context()); - return (!item && peer) - ? showPhoto(photo, peer) - : showPhoto(photo, item); -} - -void Application::showPhoto(not_null photo, HistoryItem *item) { - Expects(_mediaView != nullptr); - - _mediaView->showPhoto(photo, item); - _mediaView->activateWindow(); - _mediaView->setFocus(); -} - -void Application::showPhoto( - not_null photo, - not_null peer) { - Expects(_mediaView != nullptr); - - _mediaView->showPhoto(photo, peer); - _mediaView->activateWindow(); - _mediaView->setFocus(); -} - -void Application::showDocument(not_null document, HistoryItem *item) { - Expects(_mediaView != nullptr); - - if (cUseExternalVideoPlayer() - && document->isVideoFile() - && !document->filepath().isEmpty()) { - File::Launch(document->location(false).fname); - } else { - _mediaView->showDocument(document, item); - _mediaView->activateWindow(); - _mediaView->setFocus(); - } -} - -void Application::showTheme( - not_null document, - const Data::CloudTheme &cloud) { - Expects(_mediaView != nullptr); - - _mediaView->showTheme(document, cloud); - _mediaView->activateWindow(); - _mediaView->setFocus(); -} - PeerData *Application::ui_getPeerForMouseAction() { if (_mediaView && !_mediaView->isHidden()) { return _mediaView->ui_getPeerForMouseAction(); @@ -526,17 +510,17 @@ void Application::setCurrentProxy( const MTP::ProxyData &proxy, MTP::ProxyData::Settings settings) { const auto current = [&] { - return (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled) - ? Global::SelectedProxy() + return _settings.proxy().isEnabled() + ? _settings.proxy().selected() : MTP::ProxyData(); }; const auto was = current(); - Global::SetSelectedProxy(proxy); - Global::SetProxySettings(settings); + _settings.proxy().setSelected(proxy); + _settings.proxy().setSettings(settings); const auto now = current(); refreshGlobalProxy(); _proxyChanges.fire({ was, now }); - Global::RefConnectionTypeChanged().notify(); + _settings.proxy().connectionTypeChangesNotify(); } auto Application::proxyChanges() const -> rpl::producer { @@ -544,11 +528,10 @@ auto Application::proxyChanges() const -> rpl::producer { } void Application::badMtprotoConfigurationError() { - if (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled - && !_badProxyDisableBox) { + if (_settings.proxy().isEnabled() && !_badProxyDisableBox) { const auto disableCallback = [=] { setCurrentProxy( - Global::SelectedProxy(), + _settings.proxy().selected(), MTP::ProxyData::Settings::System); }; _badProxyDisableBox = Ui::show(Box( @@ -593,6 +576,14 @@ void Application::startEmojiImageLoader() { }, _lifetime); } +void Application::setScreenIsLocked(bool locked) { + _screenIsLocked = locked; +} + +bool Application::screenIsLocked() const { + return _screenIsLocked; +} + void Application::setDefaultFloatPlayerDelegate( not_null delegate) { Expects(!_defaultFloatPlayerDelegate == !_floatPlayers); @@ -883,7 +874,7 @@ bool Application::passcodeLocked() const { void Application::updateNonIdle() { _lastNonIdleTime = crl::now(); if (const auto session = maybeActiveSession()) { - session->updates().checkIdleFinish(); + session->updates().checkIdleFinish(_lastNonIdleTime); } } @@ -910,19 +901,21 @@ bool Application::someSessionExists() const { return false; } -void Application::checkAutoLock() { - if (!Global::LocalPasscode() +void Application::checkAutoLock(crl::time lastNonIdleTime) { + if (!_domain->local().hasLocalPasscode() || passcodeLocked() || !someSessionExists()) { _shouldLockAt = 0; _autoLockTimer.cancel(); return; + } else if (!lastNonIdleTime) { + lastNonIdleTime = this->lastNonIdleTime(); } checkLocalTime(); const auto now = crl::now(); const auto shouldLockInMs = _settings.autoLock() * 1000LL; - const auto checkTimeMs = now - lastNonIdleTime(); + const auto checkTimeMs = now - lastNonIdleTime; if (checkTimeMs >= shouldLockInMs || (_shouldLockAt > 0 && now > _shouldLockAt + kAutoLockTimeoutLateMs)) { _shouldLockAt = 0; _autoLockTimer.cancel(); @@ -944,7 +937,7 @@ void Application::checkAutoLockIn(crl::time time) { void Application::localPasscodeChanged() { _shouldLockAt = 0; _autoLockTimer.cancel(); - checkAutoLock(); + checkAutoLock(crl::now()); } bool Application::hasActiveWindow(not_null session) const { @@ -1001,10 +994,10 @@ bool Application::minimizeActiveWindow() { } QWidget *Application::getFileDialogParent() { - return (_mediaView && _mediaView->isVisible()) - ? (QWidget*)_mediaView.get() + return (_mediaView && !_mediaView->isHidden()) + ? static_cast(_mediaView->widget()) : activeWindow() - ? (QWidget*)activeWindow()->widget() + ? static_cast(activeWindow()->widget()) : nullptr; } @@ -1016,9 +1009,7 @@ void Application::notifyFileDialogShown(bool shown) { void Application::checkMediaViewActivation() { if (_mediaView && !_mediaView->isHidden()) { - _mediaView->activateWindow(); - QApplication::setActiveWindow(_mediaView.get()); - _mediaView->setFocus(); + _mediaView->activate(); } } @@ -1032,30 +1023,49 @@ QPoint Application::getPointForCallPanelCenter() const { // macOS Qt bug workaround, sometimes no leaveEvent() gets to the nested widgets. void Application::registerLeaveSubscription(not_null widget) { #ifdef Q_OS_MAC - if (const auto topLevel = widget->window()) { - if (topLevel == _window->widget()) { - auto weak = Ui::MakeWeak(widget); - auto subscription = _window->widget()->leaveEvents( - ) | rpl::start_with_next([weak] { - if (const auto window = weak.data()) { - QEvent ev(QEvent::Leave); - QGuiApplication::sendEvent(window, &ev); + if (const auto window = widget->window()) { + auto i = _leaveFilters.find(window); + if (i == end(_leaveFilters)) { + const auto check = [=](not_null e) { + if (e->type() == QEvent::Leave) { + if (const auto taken = _leaveFilters.take(window)) { + for (const auto weak : taken->registered) { + if (const auto widget = weak.data()) { + QEvent ev(QEvent::Leave); + QCoreApplication::sendEvent(widget, &ev); + } + } + delete taken->filter.data(); + } } + return base::EventFilterResult::Continue; + }; + const auto filter = base::install_event_filter(window, check); + QObject::connect(filter, &QObject::destroyed, [=] { + _leaveFilters.remove(window); }); - _leaveSubscriptions.emplace_back(weak, std::move(subscription)); + i = _leaveFilters.emplace( + window, + LeaveFilter{ .filter = filter.get() }).first; } + i->second.registered.push_back(widget.get()); } #endif // Q_OS_MAC } void Application::unregisterLeaveSubscription(not_null widget) { #ifdef Q_OS_MAC - _leaveSubscriptions = std::move( - _leaveSubscriptions - ) | ranges::actions::remove_if([&](const LeaveSubscription &subscription) { - auto pointer = subscription.pointer.data(); - return !pointer || (pointer == widget); - }); + if (const auto topLevel = widget->window()) { + const auto i = _leaveFilters.find(topLevel); + if (i != end(_leaveFilters)) { + i->second.registered = std::move( + i->second.registered + ) | ranges::actions::remove_if([&](QPointer widget) { + const auto pointer = widget.data(); + return !pointer || (pointer == widget); + }); + } + } #endif // Q_OS_MAC } @@ -1130,7 +1140,7 @@ void Application::startShortcuts() { return true; }); request->check(Command::Lock) && request->handle([=] { - if (!passcodeLocked() && Global::LocalPasscode()) { + if (!passcodeLocked() && _domain->local().hasLocalPasscode()) { lockByPasscode(); return true; } diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index 5e58f9b5e..c74064235 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "mtproto/mtproto_auth_key.h" #include "mtproto/mtproto_proxy_data.h" -#include "base/observer.h" #include "base/timer.h" class MainWindow; @@ -104,7 +103,7 @@ namespace Core { class Launcher; struct LocalUrlHandler; -class Application final : public QObject, private base::Subscriber { +class Application final : public QObject { public: struct ProxyChange { MTP::ProxyData was; @@ -144,13 +143,6 @@ public: // Media view interface. void checkMediaViewActivation(); bool hideMediaView(); - void showPhoto(not_null link); - void showPhoto(not_null photo, HistoryItem *item); - void showPhoto(not_null photo, not_null item); - void showDocument(not_null document, HistoryItem *item); - void showTheme( - not_null document, - const Data::CloudTheme &cloud); [[nodiscard]] PeerData *ui_getPeerForMouseAction(); [[nodiscard]] QPoint getPointForCallPanelCenter() const; @@ -269,7 +261,7 @@ public: rpl::producer passcodeLockChanges() const; rpl::producer passcodeLockValue() const; - void checkAutoLock(); + void checkAutoLock(crl::time lastNonIdleTime = 0); void checkAutoLockIn(crl::time time); void localPasscodeChanged(); @@ -297,6 +289,10 @@ public: void call_handleObservables(); + // Global runtime variables. + void setScreenIsLocked(bool locked); + bool screenIsLocked() const; + protected: bool eventFilter(QObject *object, QEvent *event) override; @@ -315,13 +311,12 @@ private: void startEmojiImageLoader(); void startSystemDarkModeViewer(); - void stateChanged(Qt::ApplicationState state); - friend void App::quit(); static void QuitAttempt(); void quitDelayed(); [[nodiscard]] bool readyToQuit(); + void showOpenGLCrashNotification(); void clearPasscodeLock(); bool openCustomUrl( @@ -368,7 +363,6 @@ private: const std::unique_ptr _langCloudManager; const std::unique_ptr _emojiKeywords; std::unique_ptr _translator; - base::Observable _passcodedChanged; QPointer _badProxyDisableBox; std::unique_ptr _floatPlayers; @@ -389,23 +383,18 @@ private: const QImage _logoNoMarginOld; rpl::variable _passcodeLock; + bool _screenIsLocked = false; crl::time _shouldLockAt = 0; base::Timer _autoLockTimer; std::optional _saveSettingsTimer; - struct LeaveSubscription { - LeaveSubscription( - QPointer pointer, - rpl::lifetime &&subscription) - : pointer(pointer), subscription(std::move(subscription)) { - } - - QPointer pointer; - rpl::lifetime subscription; + struct LeaveFilter { + std::vector> registered; + QPointer filter; }; - std::vector _leaveSubscriptions; + base::flat_map, LeaveFilter> _leaveFilters; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index f43b02e44..4ea3c65c7 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -23,60 +23,6 @@ namespace { std::map BetaLogs() { return { - { - 2004006, - "- Fix image compression option when sending files with drag-n-drop.\n" - - "- Fix caption text selection in media albums.\n" - - "- Fix drafts display in personal chats in the chats list.\n" - - "- Bug fixes and other minor improvements.\n" - }, - { - 2004008, - "- Upgrade several third party libraries to latest versions.\n" - }, - { - 2004010, - "- Use inline bots and sticker by emoji suggestions in channel comments.\n" - - "- Lock voice message recording, listen to your voice message before sending.\n" - }, - { - 2004011, - "- Improve locked voice message recording.\n" - - "- Fix main window closing to tray on Windows.\n" - - "- Fix crash in bot command sending.\n" - - "- Fix adding additional photos when sending an album to a group with enabled slow mode.\n" - }, - { - 2004012, - "- Voice chats in groups. (alpha version)\n" - }, - { - 2004014, - "- Create voice chats in legacy groups.\n" - - "- Fix sticker pack opening.\n" - - "- Fix group status display.\n" - - "- Fix group members display.\n" - }, - { - 2004015, - "- Improve design of voice chats.\n" - - "- Fix sending of voice messages as replies.\n" - - "- Fix 'Open With' menu position in macOS.\n" - - "- Fix freeze on secondary screen disconnect.\n" - }, { 2005002, "- Fix possible crash in video calls.\n" @@ -149,6 +95,48 @@ std::map BetaLogs() { "- MPRIS support on Linux.\n" }, + { + 2007005, + "- Add \"Voice chats\" filter in \"Recent actions\" for channels.\n" + + "- Write local drafts to disk on a background thread.\n" + + "- Support autoupdate for Telegram in write-protected folders on Linux.\n" + + "- Fix crash in native notifications on Linux.\n" + + "- Fix crash in file dialog on Linux.\n" + }, + { + 2007007, + "- Optimized video playback in media viewer and Picture-in-Picture mode.\n" + + "- Added integration with System Media Transport Controls on Windows 10.\n" + + "- Added \"Now Playing\" integration for music playback on macOS.\n" + + "- Added \"Archive Sticker\" into the \"...\" menu of the Sticker Set Box.\n" + + "- Fixed memory not being freed on Linux.\n" + + "- Several crash fixes.\n" + }, + { + 2007009, + "- Added \"Enable noise suppression\" option to group calls Settings.\n" + + "- Fix media viewer with Retina + Non-Retina dual monitor setup on macOS.\n" + + "- Several bug and crash fixes.\n" + }, + { + 2007010, + "- Added ability to mix together bold, italic and other formatting.\n" + + "- Fix voice chats and video calls OpenGL with some drivers on Windows.\n" + + "- Several bug fixes.\n" + }, }; }; @@ -291,7 +279,7 @@ void Changelogs::addBetaLog(int changeVersion, const char *changes) { return result.replace(simple, separator); }(); const auto version = FormatVersionDisplay(changeVersion); - const auto log = qsl("New in version %1:\n\n").arg(version) + text; + const auto log = qsl("New in version %1 beta:\n\n").arg(version) + text; addLocalLog(log); } diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index b90da5917..f17c2c6a6 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/section_widget.h" #include "base/platform/base_platform_info.h" #include "webrtc/webrtc_create_adm.h" +#include "ui/gl/gl_detection.h" +#include "calls/group/calls_group_common.h" #include "facades.h" namespace Core { @@ -76,6 +78,7 @@ Settings::Settings() QByteArray Settings::serialize() const { const auto themesAccentColors = _themesAccentColors.serialize(); const auto windowPosition = Serialize(_windowPosition); + const auto proxy = _proxy.serialize(); auto recentEmojiPreloadGenerated = std::vector(); if (_recentEmojiPreload.empty()) { @@ -92,22 +95,35 @@ QByteArray Settings::serialize() const { + sizeof(qint32) * 5 + Serialize::stringSize(_downloadPath.current()) + Serialize::bytearraySize(_downloadPathBookmark) - + sizeof(qint32) * 12 + + sizeof(qint32) * 9 + Serialize::stringSize(_callOutputDeviceId) + Serialize::stringSize(_callInputDeviceId) - + Serialize::stringSize(_callVideoInputDeviceId) + sizeof(qint32) * 5; for (const auto &[key, value] : _soundOverrides) { size += Serialize::stringSize(key) + Serialize::stringSize(value); } + size += sizeof(qint32) * 13 + + Serialize::bytearraySize(_videoPipGeometry) + + sizeof(qint32) + + (_dictionariesEnabled.current().size() * sizeof(quint64)) + + sizeof(qint32) * 12 + + Serialize::stringSize(_callVideoInputDeviceId) + + sizeof(qint32) * 2 + + Serialize::bytearraySize(_groupCallPushToTalkShortcut) + + sizeof(qint64) + + sizeof(qint32) * 2 + + Serialize::bytearraySize(windowPosition) + + sizeof(qint32); for (const auto &[id, rating] : recentEmojiPreloadData) { size += Serialize::stringSize(id) + sizeof(quint16); } + size += sizeof(qint32); for (const auto &[id, variant] : _emojiVariants) { size += Serialize::stringSize(id) + sizeof(quint8); } - size += Serialize::bytearraySize(_videoPipGeometry); - size += Serialize::bytearraySize(windowPosition); + size += sizeof(qint32) * 3 + + Serialize::bytearraySize(proxy) + + sizeof(qint32); auto result = QByteArray(); result.reserve(size); @@ -116,7 +132,7 @@ QByteArray Settings::serialize() const { stream.setVersion(QDataStream::Qt_5_1); stream << themesAccentColors - << qint32(_adaptiveForWide ? 1 : 0) + << qint32(_adaptiveForWide.current() ? 1 : 0) << qint32(_moderateModeEnabled ? 1 : 0) << qint32(qRound(_songVolume.current() * 1e6)) << qint32(qRound(_videoVolume.current() * 1e6)) @@ -194,6 +210,12 @@ QByteArray Settings::serialize() const { for (const auto &[id, variant] : _emojiVariants) { stream << id << quint8(variant); } + stream + << qint32(_disableOpenGL ? 1 : 0) + << qint32(_groupCallNoiseSuppression ? 1 : 0) + << qint32(_workMode.current()) + << proxy + << qint32(_hiddenGroupCallTooltips.value()); } return result; } @@ -207,7 +229,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { stream.setVersion(QDataStream::Qt_5_1); QByteArray themesAccentColors; - qint32 adaptiveForWide = _adaptiveForWide ? 1 : 0; + qint32 adaptiveForWide = _adaptiveForWide.current() ? 1 : 0; qint32 moderateModeEnabled = _moderateModeEnabled ? 1 : 0; qint32 songVolume = qint32(qRound(_songVolume.current() * 1e6)); qint32 videoVolume = qint32(qRound(_videoVolume.current() * 1e6)); @@ -269,6 +291,11 @@ void Settings::addFromSerialized(const QByteArray &serialized) { QByteArray windowPosition; std::vector recentEmojiPreload; base::flat_map emojiVariants; + qint32 disableOpenGL = _disableOpenGL ? 1 : 0; + qint32 groupCallNoiseSuppression = _groupCallNoiseSuppression ? 1 : 0; + qint32 workMode = static_cast(_workMode.current()); + QByteArray proxy; + qint32 hiddenGroupCallTooltips = qint32(_hiddenGroupCallTooltips.value()); stream >> themesAccentColors; if (!stream.atEnd()) { @@ -397,12 +424,29 @@ void Settings::addFromSerialized(const QByteArray &serialized) { } } } + if (!stream.atEnd()) { + stream >> disableOpenGL; + } + if (!stream.atEnd()) { + stream >> groupCallNoiseSuppression; + } + if (!stream.atEnd()) { + stream >> workMode; + } + if (!stream.atEnd()) { + stream >> proxy; + } + if (!stream.atEnd()) { + stream >> hiddenGroupCallTooltips; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); return; } else if (!_themesAccentColors.setFromSerialized(themesAccentColors)) { return; + } else if (!_proxy.setFromSerialized(proxy)) { + return; } _adaptiveForWide = (adaptiveForWide == 1); _moderateModeEnabled = (moderateModeEnabled == 1); @@ -415,11 +459,11 @@ void Settings::addFromSerialized(const QByteArray &serialized) { _soundNotify = (soundNotify == 1); _desktopNotify = (desktopNotify == 1); _flashBounceNotify = (flashBounceNotify == 1); - const auto uncheckedNotifyView = static_cast(notifyView); + const auto uncheckedNotifyView = static_cast(notifyView); switch (uncheckedNotifyView) { - case dbinvShowNothing: - case dbinvShowName: - case dbinvShowPreview: _notifyView = uncheckedNotifyView; break; + case NotifyView::ShowNothing: + case NotifyView::ShowName: + case NotifyView::ShowPreview: _notifyView = uncheckedNotifyView; break; } switch (nativeNotifications) { case 0: _nativeNotifications = std::nullopt; break; @@ -502,11 +546,28 @@ void Settings::addFromSerialized(const QByteArray &serialized) { } _recentEmojiPreload = std::move(recentEmojiPreload); _emojiVariants = std::move(emojiVariants); -} - -bool Settings::chatWide() const { - return _adaptiveForWide - && (Global::AdaptiveChatLayout() == Adaptive::ChatLayout::Wide); + _disableOpenGL = (disableOpenGL == 1); + if (!Platform::IsMac()) { + Ui::GL::ForceDisable(_disableOpenGL + || Ui::Integration::Instance().openglLastCheckFailed()); + } + _groupCallNoiseSuppression = (groupCallNoiseSuppression == 1); + const auto uncheckedWorkMode = static_cast(workMode); + switch (uncheckedWorkMode) { + case WorkMode::WindowAndTray: + case WorkMode::TrayOnly: + case WorkMode::WindowOnly: _workMode = uncheckedWorkMode; break; + } + _hiddenGroupCallTooltips = [&] { + using Tooltip = Calls::Group::StickedTooltip; + return Tooltip(0) + | ((hiddenGroupCallTooltips & int(Tooltip::Camera)) + ? Tooltip::Camera + : Tooltip(0)) + | ((hiddenGroupCallTooltips & int(Tooltip::Microphone)) + ? Tooltip::Microphone + : Tooltip(0)); + }(); } QString Settings::getSoundPath(const QString &key) const { @@ -708,7 +769,7 @@ void Settings::resetOnLastLogout() { _soundNotify = true; _desktopNotify = true; _flashBounceNotify = true; - _notifyView = dbinvShowPreview; + _notifyView = NotifyView::ShowPreview; //_nativeNotifications = std::nullopt; //_notificationsCount = 3; //_notificationsCorner = ScreenCorner::BottomRight; @@ -730,6 +791,8 @@ void Settings::resetOnLastLogout() { _groupCallPushToTalkShortcut = QByteArray(); _groupCallPushToTalkDelay = 20; + _groupCallNoiseSuppression = true; + //_themesAccentColors = Window::Theme::AccentColors(); _lastSeenWarningSeen = false; @@ -760,10 +823,13 @@ void Settings::resetOnLastLogout() { _notifyFromAll = true; _tabbedReplacedWithInfo = false; // per-window _systemDarkModeEnabled = false; + _hiddenGroupCallTooltips = 0; _recentEmojiPreload.clear(); _recentEmoji.clear(); _emojiVariants.clear(); + + _workMode = WorkMode::WindowAndTray; } bool Settings::ThirdColumnByDefault() { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index cb9f84e63..6f2c000ed 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/platform/base_platform_info.h" +#include "core/core_settings_proxy.h" #include "window/themes/window_themes_embedded.h" #include "ui/chat/attach/attach_send_files_way.h" #include "platform/platform_notifications_manager.h" +#include "base/flags.h" #include "emoji.h" enum class RectPart; @@ -27,6 +29,10 @@ namespace Webrtc { enum class Backend; } // namespace Webrtc +namespace Calls::Group { +enum class StickedTooltip; +} // namespace Calls::Group + namespace Core { struct WindowPosition { @@ -49,6 +55,16 @@ public: BottomRight = 2, BottomLeft = 3, }; + enum class NotifyView { + ShowPreview = 0, + ShowName = 1, + ShowNothing = 2, + }; + enum class WorkMode { + WindowAndTray = 0, + TrayOnly = 1, + WindowOnly = 2, + }; static constexpr auto kDefaultVolume = 0.9; @@ -58,6 +74,10 @@ public: return _saveDelayed.events(); } + [[nodiscard]] SettingsProxy &proxy() { + return _proxy; + } + [[nodiscard]] static bool IsLeftCorner(ScreenCorner corner) { return (corner == ScreenCorner::TopLeft) || (corner == ScreenCorner::BottomLeft); @@ -70,9 +90,14 @@ public: [[nodiscard]] QByteArray serialize() const; void addFromSerialized(const QByteArray &serialized); - [[nodiscard]] bool chatWide() const; [[nodiscard]] bool adaptiveForWide() const { - return _adaptiveForWide; + return _adaptiveForWide.current(); + } + [[nodiscard]] rpl::producer adaptiveForWideValue() const { + return _adaptiveForWide.value(); + } + [[nodiscard]] rpl::producer adaptiveForWideChanges() const { + return _adaptiveForWide.changes(); } void setAdaptiveForWide(bool value) { _adaptiveForWide = value; @@ -146,10 +171,10 @@ public: void setFlashBounceNotify(bool value) { _flashBounceNotify = value; } - [[nodiscard]] DBINotifyView notifyView() const { + [[nodiscard]] NotifyView notifyView() const { return _notifyView; } - void setNotifyView(DBINotifyView value) { + void setNotifyView(NotifyView value) { _notifyView = value; } [[nodiscard]] bool nativeNotifications() const { @@ -266,6 +291,12 @@ public: void setGroupCallPushToTalkDelay(crl::time delay) { _groupCallPushToTalkDelay = delay; } + [[nodiscard]] bool groupCallNoiseSuppression() const { + return _groupCallNoiseSuppression; + } + void setGroupCallNoiseSuppression(bool value) { + _groupCallNoiseSuppression = value; + } [[nodiscard]] Window::Theme::AccentColors &themesAccentColors() { return _themesAccentColors; } @@ -514,6 +545,18 @@ public: void setWindowPosition(const WindowPosition &position) { _windowPosition = position; } + void setWorkMode(WorkMode value) { + _workMode = value; + } + [[nodiscard]] WorkMode workMode() const { + return _workMode.current(); + } + [[nodiscard]] rpl::producer workModeValue() const { + return _workMode.value(); + } + [[nodiscard]] rpl::producer workModeChanges() const { + return _workMode.changes(); + } struct RecentEmoji { EmojiPtr emoji = nullptr; @@ -533,6 +576,20 @@ public: void saveEmojiVariant(EmojiPtr emoji); void setLegacyEmojiVariants(QMap data); + [[nodiscard]] bool disableOpenGL() const { + return _disableOpenGL; + } + void setDisableOpenGL(bool value) { + _disableOpenGL = value; + } + + [[nodiscard]] base::flags hiddenGroupCallTooltips() const { + return _hiddenGroupCallTooltips; + } + void setHiddenGroupCallTooltip(Calls::Group::StickedTooltip value) { + _hiddenGroupCallTooltips |= value; + } + [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] static float64 DefaultDialogsWidthRatio(); [[nodiscard]] static qint32 SerializePlaybackSpeed(float64 speed) { @@ -561,7 +618,9 @@ private: ushort rating = 0; }; - bool _adaptiveForWide = true; + SettingsProxy _proxy; + + rpl::variable _adaptiveForWide = true; bool _moderateModeEnabled = false; rpl::variable _songVolume = kDefaultVolume; rpl::variable _videoVolume = kDefaultVolume; @@ -572,7 +631,7 @@ private: bool _soundNotify = true; bool _desktopNotify = true; bool _flashBounceNotify = true; - DBINotifyView _notifyView = dbinvShowPreview; + NotifyView _notifyView = NotifyView::ShowPreview; std::optional _nativeNotifications; int _notificationsCount = 3; ScreenCorner _notificationsCorner = ScreenCorner::BottomRight; @@ -588,6 +647,7 @@ private: bool _callAudioDuckingEnabled = true; bool _disableCalls = false; bool _groupCallPushToTalk = false; + bool _groupCallNoiseSuppression = true; QByteArray _groupCallPushToTalkShortcut; crl::time _groupCallPushToTalkDelay = 20; Window::Theme::AccentColors _themesAccentColors; @@ -625,6 +685,9 @@ private: rpl::variable> _systemDarkMode = std::nullopt; rpl::variable _systemDarkModeEnabled = false; WindowPosition _windowPosition; // per-window + bool _disableOpenGL = false; + rpl::variable _workMode = WorkMode::WindowAndTray; + base::flags _hiddenGroupCallTooltips; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/core/core_settings_proxy.cpp b/Telegram/SourceFiles/core/core_settings_proxy.cpp new file mode 100644 index 000000000..b219f4fca --- /dev/null +++ b/Telegram/SourceFiles/core/core_settings_proxy.cpp @@ -0,0 +1,238 @@ +/* +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 "core/core_settings_proxy.h" + +#include "base/platform/base_platform_info.h" +#include "storage/serialize_common.h" + +namespace Core { +namespace { + +[[nodiscard]] qint32 ProxySettingsToInt(MTP::ProxyData::Settings settings) { + switch(settings) { + case MTP::ProxyData::Settings::System: return 0; + case MTP::ProxyData::Settings::Enabled: return 1; + case MTP::ProxyData::Settings::Disabled: return 2; + } + Unexpected("Bad type in ProxySettingsToInt"); +} + +[[nodiscard]] MTP::ProxyData::Settings IntToProxySettings(qint32 value) { + switch(value) { + case 0: return MTP::ProxyData::Settings::System; + case 1: return MTP::ProxyData::Settings::Enabled; + case 2: return MTP::ProxyData::Settings::Disabled; + } + Unexpected("Bad type in IntToProxySettings"); +} + +[[nodiscard]] MTP::ProxyData DeserializeProxyData(const QByteArray &data) { + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_1); + + qint32 proxyType, port; + MTP::ProxyData proxy; + stream + >> proxyType + >> proxy.host + >> port + >> proxy.user + >> proxy.password; + proxy.port = port; + proxy.type = [&] { + switch(proxyType) { + case 0: return MTP::ProxyData::Type::None; + case 1: return MTP::ProxyData::Type::Socks5; + case 2: return MTP::ProxyData::Type::Http; + case 3: return MTP::ProxyData::Type::Mtproto; + } + Unexpected("Bad type in DeserializeProxyData"); + }(); + return proxy; +} + +[[nodiscard]] QByteArray SerializeProxyData(const MTP::ProxyData &proxy) { + auto result = QByteArray(); + const auto size = 1 * sizeof(qint32) + + Serialize::stringSize(proxy.host) + + 1 * sizeof(qint32) + + Serialize::stringSize(proxy.user) + + Serialize::stringSize(proxy.password); + + result.reserve(size); + { + const auto proxyType = [&] { + switch(proxy.type) { + case MTP::ProxyData::Type::None: return 0; + case MTP::ProxyData::Type::Socks5: return 1; + case MTP::ProxyData::Type::Http: return 2; + case MTP::ProxyData::Type::Mtproto: return 3; + } + Unexpected("Bad type in SerializeProxyData"); + }(); + + QDataStream stream(&result, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_1); + stream + << qint32(proxyType) + << proxy.host + << qint32(proxy.port) + << proxy.user + << proxy.password; + } + return result; +} + +} // namespace + +SettingsProxy::SettingsProxy() +: _tryIPv6(!Platform::IsWindows()) { +} + +QByteArray SettingsProxy::serialize() const { + auto result = QByteArray(); + auto stream = QDataStream(&result, QIODevice::WriteOnly); + + const auto serializedSelected = SerializeProxyData(_selected); + const auto serializedList = ranges::views::all( + _list + ) | ranges::views::transform(SerializeProxyData) | ranges::to_vector; + + const auto size = 3 * sizeof(qint32) + + Serialize::bytearraySize(serializedSelected) + + 1 * sizeof(qint32) + + ranges::accumulate( + serializedList, + 0, + ranges::plus(), + &Serialize::bytearraySize); + result.reserve(size); + + stream.setVersion(QDataStream::Qt_5_1); + stream + << qint32(_tryIPv6 ? 1 : 0) + << qint32(_useProxyForCalls ? 1 : 0) + << ProxySettingsToInt(_settings) + << serializedSelected + << qint32(_list.size()); + for (const auto &i : serializedList) { + stream << i; + } + + stream.device()->close(); + return result; +} + +bool SettingsProxy::setFromSerialized(const QByteArray &serialized) { + if (serialized.isEmpty()) { + return true; + } + + auto stream = QDataStream(serialized); + + auto tryIPv6 = qint32(_tryIPv6 ? 1 : 0); + auto useProxyForCalls = qint32(_useProxyForCalls ? 1 : 0); + auto settings = ProxySettingsToInt(_settings); + auto listCount = qint32(_list.size()); + auto selectedProxy = QByteArray(); + + if (!stream.atEnd()) { + stream + >> tryIPv6 + >> useProxyForCalls + >> settings + >> selectedProxy + >> listCount; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != listCount; ++i) { + QByteArray data; + stream >> data; + _list.push_back(DeserializeProxyData(data)); + } + } + } + + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for Core::SettingsProxy::setFromSerialized()")); + return false; + } + + _tryIPv6 = (tryIPv6 == 1); + _useProxyForCalls = (useProxyForCalls == 1); + _settings = IntToProxySettings(settings); + _selected = DeserializeProxyData(selectedProxy); + + return true; +} + +bool SettingsProxy::isEnabled() const { + return _settings == MTP::ProxyData::Settings::Enabled; +} + +bool SettingsProxy::isSystem() const { + return _settings == MTP::ProxyData::Settings::System; +} + +bool SettingsProxy::isDisabled() const { + return _settings == MTP::ProxyData::Settings::Disabled; +} + +bool SettingsProxy::tryIPv6() const { + return _tryIPv6; +} + +void SettingsProxy::setTryIPv6(bool value) { + _tryIPv6 = value; +} + +bool SettingsProxy::useProxyForCalls() const { + return _useProxyForCalls; +} + +void SettingsProxy::setUseProxyForCalls(bool value) { + _useProxyForCalls = value; +} + +MTP::ProxyData::Settings SettingsProxy::settings() const { + return _settings; +} + +void SettingsProxy::setSettings(MTP::ProxyData::Settings value) { + _settings = value; +} + +MTP::ProxyData SettingsProxy::selected() const { + return _selected; +} + +void SettingsProxy::setSelected(MTP::ProxyData value) { + _selected = value; +} + +const std::vector &SettingsProxy::list() const { + return _list; +} + +std::vector &SettingsProxy::list() { + return _list; +} + +rpl::producer<> SettingsProxy::connectionTypeValue() const { + return _connectionTypeChanges.events_starting_with({}); +} + +rpl::producer<> SettingsProxy::connectionTypeChanges() const { + return _connectionTypeChanges.events(); +} + +void SettingsProxy::connectionTypeChangesNotify() { + _connectionTypeChanges.fire({}); +} + +} // namespace Core diff --git a/Telegram/SourceFiles/core/core_settings_proxy.h b/Telegram/SourceFiles/core/core_settings_proxy.h new file mode 100644 index 000000000..bd709c230 --- /dev/null +++ b/Telegram/SourceFiles/core/core_settings_proxy.h @@ -0,0 +1,56 @@ +/* +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/mtproto_proxy_data.h" + +namespace Core { + +class SettingsProxy final { +public: + SettingsProxy(); + + [[nodiscard]] bool isEnabled() const; + [[nodiscard]] bool isSystem() const; + [[nodiscard]] bool isDisabled() const; + + [[nodiscard]] rpl::producer<> connectionTypeChanges() const; + [[nodiscard]] rpl::producer<> connectionTypeValue() const; + void connectionTypeChangesNotify(); + + [[nodiscard]] bool tryIPv6() const; + void setTryIPv6(bool value); + + [[nodiscard]] bool useProxyForCalls() const; + void setUseProxyForCalls(bool value); + + [[nodiscard]] MTP::ProxyData::Settings settings() const; + void setSettings(MTP::ProxyData::Settings value); + + [[nodiscard]] MTP::ProxyData selected() const; + void setSelected(MTP::ProxyData value); + + [[nodiscard]] const std::vector &list() const; + [[nodiscard]] std::vector &list(); + + [[nodiscard]] QByteArray serialize() const; + bool setFromSerialized(const QByteArray &serialized); + +private: + bool _tryIPv6 = false; + bool _useProxyForCalls = false; + MTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System; + MTP::ProxyData _selected; + std::vector _list; + + rpl::event_stream<> _connectionTypeChanges; + +}; + +} // namespace Core + diff --git a/Telegram/SourceFiles/core/crash_report_window.cpp b/Telegram/SourceFiles/core/crash_report_window.cpp index 0f244c45a..0b4db9a40 100644 --- a/Telegram/SourceFiles/core/crash_report_window.cpp +++ b/Telegram/SourceFiles/core/crash_report_window.cpp @@ -62,6 +62,7 @@ void PreLaunchWindow::activate() { setWindowState(windowState() & ~Qt::WindowMinimized); setVisible(true); psActivateProcess(); + raise(); activateWindow(); } @@ -83,6 +84,7 @@ PreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) { QPalette p(palette()); p.setColor(QPalette::WindowText, QColor(0, 0, 0)); + p.setColor(QPalette::Text, QColor(0, 0, 0)); setPalette(p); show(); }; @@ -101,8 +103,11 @@ PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(paren QPalette p(palette()); p.setColor(QPalette::WindowText, QColor(0, 0, 0)); + p.setColor(QPalette::Text, QColor(0, 0, 0)); setPalette(p); + setStyleSheet("QLineEdit { background-color: white; }"); + QLineEdit::setTextMargins(0, 0, 0, 0); setContentsMargins(0, 0, 0, 0); if (password) { @@ -119,6 +124,7 @@ PreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) { QPalette p(palette()); p.setColor(QPalette::WindowText, QColor(96, 96, 96)); + p.setColor(QPalette::Text, QColor(96, 96, 96)); setPalette(p); setReadOnly(true); @@ -158,6 +164,11 @@ PreLaunchCheckbox::PreLaunchCheckbox(QWidget *parent) : QCheckBox(parent) { closeFont.setPixelSize(static_cast(parent)->basicSize()); setFont(closeFont); + QPalette p(palette()); + p.setColor(QPalette::WindowText, QColor(96, 96, 96)); + p.setColor(QPalette::Text, QColor(96, 96, 96)); + setPalette(p); + setCursor(Qt::PointingHandCursor); show(); }; @@ -176,7 +187,7 @@ NotStartedWindow::NotStartedWindow() _log.setPlainText(Logs::full()); - connect(&_close, SIGNAL(clicked()), this, SLOT(close())); + connect(&_close, &QPushButton::clicked, [=] { close(); }); _close.setText(qsl("CLOSE")); QRect scr(QApplication::primaryScreen()->availableGeometry()); @@ -201,6 +212,7 @@ void NotStartedWindow::updateControls() { void NotStartedWindow::closeEvent(QCloseEvent *e) { deleteLater(); + App::quit(); } void NotStartedWindow::resizeEvent(QResizeEvent *e) { @@ -220,7 +232,6 @@ LastCrashedWindow::LastCrashedWindow( const QByteArray &crashdump, Fn launch) : _dumpraw(crashdump) -, _port(kDefaultProxyPort) , _label(this) , _pleaseSendReport(this) , _yourReportName(this) @@ -245,7 +256,11 @@ LastCrashedWindow::LastCrashedWindow( , _launch(std::move(launch)) { excludeReportUsername(); - if (!cInstallBetaVersion() && !cAlphaVersion()) { // currently accept crash reports only from testers + if (!cInstallBetaVersion() && !cAlphaVersion()) { + // Currently accept crash reports only from testers. + _sendingState = SendingNoReport; + } else if (Core::OpenGLLastCheckFailed()) { + // Nothing we can do right now with graphics driver crashes in GL. _sendingState = SendingNoReport; } if (_sendingState != SendingNoReport) { @@ -305,7 +320,10 @@ LastCrashedWindow::LastCrashedWindow( } _networkSettings.setText(qsl("NETWORK SETTINGS")); - connect(&_networkSettings, SIGNAL(clicked()), this, SLOT(onNetworkSettings())); + connect( + &_networkSettings, + &QPushButton::clicked, + [=] { networkSettings(); }); if (_sendingState == SendingNoReport) { _label.setText(qsl("Last time Kotatogram Desktop was not closed properly.")); @@ -315,24 +333,53 @@ LastCrashedWindow::LastCrashedWindow( if (_updaterData) { _updaterData->check.setText(qsl("TRY AGAIN")); - connect(&_updaterData->check, SIGNAL(clicked()), this, SLOT(onUpdateRetry())); + connect( + &_updaterData->check, + &QPushButton::clicked, + [=] { updateRetry(); }); _updaterData->skip.setText(qsl("SKIP")); - connect(&_updaterData->skip, SIGNAL(clicked()), this, SLOT(onUpdateSkip())); + connect( + &_updaterData->skip, + &QPushButton::clicked, + [=] { updateSkip(); }); Core::UpdateChecker checker; using Progress = Core::UpdateChecker::Progress; checker.checking( - ) | rpl::start_with_next([=] { onUpdateChecking(); }, _lifetime); + ) | rpl::start_with_next([=] { + Assert(_updaterData != nullptr); + + setUpdatingState(UpdatingCheck); + }, _lifetime); + checker.isLatest( - ) | rpl::start_with_next([=] { onUpdateLatest(); }, _lifetime); + ) | rpl::start_with_next([=] { + Assert(_updaterData != nullptr); + + setUpdatingState(UpdatingLatest); + }, _lifetime); + checker.progress( ) | rpl::start_with_next([=](const Progress &result) { - onUpdateDownloading(result.already, result.size); + Assert(_updaterData != nullptr); + + setUpdatingState(UpdatingDownload); + setDownloadProgress(result.already, result.size); }, _lifetime); + checker.failed( - ) | rpl::start_with_next([=] { onUpdateFailed(); }, _lifetime); + ) | rpl::start_with_next([=] { + Assert(_updaterData != nullptr); + + setUpdatingState(UpdatingFail); + }, _lifetime); + checker.ready( - ) | rpl::start_with_next([=] { onUpdateReady(); }, _lifetime); + ) | rpl::start_with_next([=] { + Assert(_updaterData != nullptr); + + setUpdatingState(UpdatingReady); + }, _lifetime); switch (checker.state()) { case Core::UpdateChecker::State::Download: @@ -366,21 +413,26 @@ LastCrashedWindow::LastCrashedWindow( _report.setPlainText(_reportTextNoUsername); _showReport.setText(qsl("VIEW REPORT")); - connect(&_showReport, SIGNAL(clicked()), this, SLOT(onViewReport())); + connect(&_showReport, &QPushButton::clicked, [=] { + _reportShown = !_reportShown; + updateControls(); + }); _saveReport.setText(qsl("SAVE TO FILE")); - connect(&_saveReport, SIGNAL(clicked()), this, SLOT(onSaveReport())); - _getApp.setText(qsl("GET THE LATEST VERSION OF KOTATOGRAM DESKTOP")); - connect(&_getApp, SIGNAL(clicked()), this, SLOT(onGetApp())); + connect(&_saveReport, &QPushButton::clicked, [=] { saveReport(); }); + _getApp.setText(qsl("GET THE LATEST OFFICIAL VERSION OF KOTATOGRAM DESKTOP")); + connect(&_getApp, &QPushButton::clicked, [=] { + QDesktopServices::openUrl(qsl("https://kotatgram.github.io")); + }); /* _send.setText(qsl("SEND CRASH REPORT")); - connect(&_send, SIGNAL(clicked()), this, SLOT(onSendReport())); + connect(&_send, &QPushButton::clicked, [=] { sendReport(); }); */ _sendSkip.setText(qsl("CLOSE AND START APP")); - connect(&_sendSkip, SIGNAL(clicked()), this, SLOT(onContinue())); + connect(&_sendSkip, &QPushButton::clicked, [=] { processContinue(); }); _continue.setText(qsl("CONTINUE")); - connect(&_continue, SIGNAL(clicked()), this, SLOT(onContinue())); + connect(&_continue, &QPushButton::clicked, [=] { processContinue(); }); QRect scr(QApplication::primaryScreen()->availableGeometry()); move(scr.x() + (scr.width() / 6), scr.y() + (scr.height() / 6)); @@ -388,12 +440,7 @@ LastCrashedWindow::LastCrashedWindow( show(); } -void LastCrashedWindow::onViewReport() { - _reportShown = !_reportShown; - updateControls(); -} - -void LastCrashedWindow::onSaveReport() { +void LastCrashedWindow::saveReport() { QString to = QFileDialog::getSaveFileName(0, qsl("Telegram Crash Report"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + qsl("/report.telegramcrash"), qsl("Telegram crash report (*.telegramcrash)")); if (!to.isEmpty()) { QFile file(to); @@ -415,10 +462,6 @@ QByteArray LastCrashedWindow::getCrashReportRaw() const { return result; } -void LastCrashedWindow::onGetApp() { - QDesktopServices::openUrl(qsl("https://github.com/kotatogram/kotatogram-desktop")); -} - void LastCrashedWindow::excludeReportUsername() { QString prefix = qstr("Username:"); QStringList lines = _reportText.split('\n'); @@ -462,7 +505,7 @@ void LastCrashedWindow::addReportFieldPart(const QLatin1String &name, const QLat } } -void LastCrashedWindow::onSendReport() { +void LastCrashedWindow::sendReport() { if (_checkReply) { _checkReply->deleteLater(); _checkReply = nullptr; @@ -479,8 +522,14 @@ void LastCrashedWindow::onSendReport() { QString::number(minidumpFileName().isEmpty() ? 0 : 1), CrashReports::PlatformString()))); - connect(_checkReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onSendingError(QNetworkReply::NetworkError))); - connect(_checkReply, SIGNAL(finished()), this, SLOT(onCheckingFinished())); + connect( + _checkReply, + &QNetworkReply::errorOccurred, + [=](QNetworkReply::NetworkError code) { sendingError(code); }); + connect( + _checkReply, + &QNetworkReply::finished, + [=] { checkingFinished(); }); _pleaseSendReport.setText(qsl("Sending crash report...")); _sendingState = SendingProgress; @@ -497,7 +546,7 @@ QString LastCrashedWindow::minidumpFileName() { return QString(); } -void LastCrashedWindow::onCheckingFinished() { +void LastCrashedWindow::checkingFinished() { if (!_checkReply || _sendReply) return; QByteArray result = _checkReply->readAll().trimmed(); @@ -569,9 +618,18 @@ void LastCrashedWindow::onCheckingFinished() { _sendReply = _sendManager.post(QNetworkRequest(qsl("https://tdesktop.com/crash.php?act=report")), multipart); multipart->setParent(_sendReply); - connect(_sendReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onSendingError(QNetworkReply::NetworkError))); - connect(_sendReply, SIGNAL(finished()), this, SLOT(onSendingFinished())); - connect(_sendReply, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onSendingProgress(qint64,qint64))); + connect( + _sendReply, + &QNetworkReply::errorOccurred, + [=](QNetworkReply::NetworkError code) { sendingError(code); }); + connect( + _sendReply, + &QNetworkReply::finished, + [=] { sendingFinished(); }); + connect( + _sendReply, + &QNetworkReply::uploadProgress, + [=](qint64 sent, qint64 total) { sendingProgress(sent, total); }); updateControls(); } @@ -800,41 +858,23 @@ void LastCrashedWindow::updateControls() { } } -void LastCrashedWindow::onNetworkSettings() { +void LastCrashedWindow::networkSettings() { const auto &proxy = Core::Sandbox::Instance().sandboxProxy(); const auto box = new NetworkSettingsWindow( this, proxy.host, - proxy.port ? proxy.port : 80, + proxy.port ? proxy.port : kDefaultProxyPort, proxy.user, proxy.password); - connect( - box, - SIGNAL(saved(QString,quint32,QString,QString)), - this, - SLOT(onNetworkSettingsSaved(QString,quint32,QString,QString))); + box->saveRequests( + ) | rpl::start_with_next([=](MTP::ProxyData &&data) { + Assert(data.host.isEmpty() || data.port != 0); + _proxyChanges.fire(std::move(data)); + proxyUpdated(); + }, _lifetime); box->show(); } -void LastCrashedWindow::onNetworkSettingsSaved( - QString host, - quint32 port, - QString username, - QString password) { - Expects(host.isEmpty() || port != 0); - - auto proxy = MTP::ProxyData(); - proxy.type = host.isEmpty() - ? MTP::ProxyData::Type::None - : MTP::ProxyData::Type::Http; - proxy.host = host; - proxy.port = port; - proxy.user = username; - proxy.password = password; - _proxyChanges.fire(std::move(proxy)); - proxyUpdated(); -} - void LastCrashedWindow::proxyUpdated() { if (_updaterData && ((_updaterData->state == UpdatingCheck) @@ -847,7 +887,7 @@ void LastCrashedWindow::proxyUpdated() { checker.start(); } else if (_sendingState == SendingFail || _sendingState == SendingProgress) { - onSendReport(); + sendReport(); } activate(); } @@ -865,7 +905,7 @@ void LastCrashedWindow::setUpdatingState(UpdatingState state, bool force) { case UpdatingLatest: _updating.setText(qsl("Latest version is installed.")); if (_sendingState == SendingNoReport) { - QTimer::singleShot(0, this, SLOT(onContinue())); + InvokeQueued(this, [=] { processContinue(); }); } else { _sendingState = SendingNone; } @@ -905,7 +945,7 @@ void LastCrashedWindow::setDownloadProgress(qint64 ready, qint64 total) { } } -void LastCrashedWindow::onUpdateRetry() { +void LastCrashedWindow::updateRetry() { Expects(_updaterData != nullptr); cSetLastUpdateCheck(0); @@ -913,11 +953,11 @@ void LastCrashedWindow::onUpdateRetry() { checker.start(); } -void LastCrashedWindow::onUpdateSkip() { +void LastCrashedWindow::updateSkip() { Expects(_updaterData != nullptr); if (_sendingState == SendingNoReport) { - onContinue(); + processContinue(); } else { if (_updaterData->state == UpdatingCheck || _updaterData->state == UpdatingDownload) { @@ -930,47 +970,11 @@ void LastCrashedWindow::onUpdateSkip() { } } -void LastCrashedWindow::onUpdateChecking() { - Expects(_updaterData != nullptr); - - setUpdatingState(UpdatingCheck); -} - -void LastCrashedWindow::onUpdateLatest() { - Expects(_updaterData != nullptr); - - setUpdatingState(UpdatingLatest); -} - -void LastCrashedWindow::onUpdateDownloading(qint64 ready, qint64 total) { - Expects(_updaterData != nullptr); - - setUpdatingState(UpdatingDownload); - setDownloadProgress(ready, total); -} - -void LastCrashedWindow::onUpdateReady() { - Expects(_updaterData != nullptr); - - setUpdatingState(UpdatingReady); -} - -void LastCrashedWindow::onUpdateFailed() { - Expects(_updaterData != nullptr); - - setUpdatingState(UpdatingFail); -} - -void LastCrashedWindow::onContinue() { - if (CrashReports::Restart() == CrashReports::CantOpen) { - new NotStartedWindow(); - } else { - _launch(); - } +void LastCrashedWindow::processContinue() { close(); } -void LastCrashedWindow::onSendingError(QNetworkReply::NetworkError e) { +void LastCrashedWindow::sendingError(QNetworkReply::NetworkError e) { LOG(("Crash report sending error: %1").arg(e)); _pleaseSendReport.setText(qsl("Sending crash report failed :(")); @@ -986,7 +990,7 @@ void LastCrashedWindow::onSendingError(QNetworkReply::NetworkError e) { updateControls(); } -void LastCrashedWindow::onSendingFinished() { +void LastCrashedWindow::sendingFinished() { if (_sendReply) { QByteArray result = _sendReply->readAll(); LOG(("Crash report sending done, result: %1").arg(QString::fromUtf8(result))); @@ -1001,7 +1005,7 @@ void LastCrashedWindow::onSendingFinished() { } } -void LastCrashedWindow::onSendingProgress(qint64 uploaded, qint64 total) { +void LastCrashedWindow::sendingProgress(qint64 uploaded, qint64 total) { if (_sendingState != SendingProgress && _sendingState != SendingUploading) return; _sendingState = SendingUploading; @@ -1015,6 +1019,12 @@ void LastCrashedWindow::onSendingProgress(qint64 uploaded, qint64 total) { void LastCrashedWindow::closeEvent(QCloseEvent *e) { deleteLater(); + + if (CrashReports::Restart() == CrashReports::CantOpen) { + new NotStartedWindow(); + } else { + _launch(); + } } void LastCrashedWindow::resizeEvent(QResizeEvent *e) { @@ -1096,9 +1106,9 @@ NetworkSettingsWindow::NetworkSettingsWindow(QWidget *parent, QString host, quin _passwordLabel.setText(qsl("Password")); _save.setText(qsl("SAVE")); - connect(&_save, SIGNAL(clicked()), this, SLOT(onSave())); + connect(&_save, &QPushButton::clicked, [=] { save(); }); _cancel.setText(qsl("CANCEL")); - connect(&_cancel, SIGNAL(clicked()), this, SLOT(close())); + connect(&_cancel, &QPushButton::clicked, [=] { close(); }); _hostInput.setText(host); _portInput.setText(QString::number(port)); @@ -1129,7 +1139,7 @@ void NetworkSettingsWindow::resizeEvent(QResizeEvent *e) { _cancel.move(_save.x() - padding - _cancel.width(), _save.y()); } -void NetworkSettingsWindow::onSave() { +void NetworkSettingsWindow::save() { QString host = _hostInput.text().trimmed(), port = _portInput.text().trimmed(), username = _usernameInput.text().trimmed(), password = _passwordInput.text().trimmed(); if (!port.isEmpty() && !port.toUInt()) { _portInput.setFocus(); @@ -1138,11 +1148,24 @@ void NetworkSettingsWindow::onSave() { _portInput.setFocus(); return; } - saved(host, port.toUInt(), username, password); + _saveRequests.fire({ + .type = host.isEmpty() + ? MTP::ProxyData::Type::None + : MTP::ProxyData::Type::Http, + .host = host, + .port = port.toUInt(), + .user = username, + .password = password, + }); close(); } void NetworkSettingsWindow::closeEvent(QCloseEvent *e) { + deleteLater(); +} + +rpl::producer NetworkSettingsWindow::saveRequests() const { + return _saveRequests.events(); } void NetworkSettingsWindow::updateControls() { diff --git a/Telegram/SourceFiles/core/crash_report_window.h b/Telegram/SourceFiles/core/crash_report_window.h index f9c1cb2b1..75e17ba23 100644 --- a/Telegram/SourceFiles/core/crash_report_window.h +++ b/Telegram/SourceFiles/core/crash_report_window.h @@ -92,7 +92,6 @@ private: }; class LastCrashedWindow : public PreLaunchWindow { - Q_OBJECT public: LastCrashedWindow( @@ -106,29 +105,19 @@ public: return _lifetime; } -public Q_SLOTS: - void onViewReport(); - void onSaveReport(); - void onSendReport(); - void onGetApp(); + void saveReport(); + void sendReport(); - void onNetworkSettings(); - void onNetworkSettingsSaved(QString host, quint32 port, QString username, QString password); - void onContinue(); + void networkSettings(); + void processContinue(); - void onCheckingFinished(); - void onSendingError(QNetworkReply::NetworkError e); - void onSendingFinished(); - void onSendingProgress(qint64 uploaded, qint64 total); + void checkingFinished(); + void sendingError(QNetworkReply::NetworkError e); + void sendingFinished(); + void sendingProgress(qint64 uploaded, qint64 total); - void onUpdateRetry(); - void onUpdateSkip(); - - void onUpdateChecking(); - void onUpdateLatest(); - void onUpdateDownloading(qint64 ready, qint64 total); - void onUpdateReady(); - void onUpdateFailed(); + void updateRetry(); + void updateSkip(); protected: void closeEvent(QCloseEvent *e) override; @@ -146,9 +135,6 @@ private: QByteArray _dumpraw; - QString _host, _username, _password; - quint32 _port; - PreLaunchLabel _label, _pleaseSendReport, _yourReportName, _minidump; PreLaunchLog _report; PreLaunchButton _send, _sendSkip, _networkSettings, _continue, _showReport, _saveReport, _getApp; @@ -175,8 +161,6 @@ private: SendingState _sendingState; PreLaunchLabel _updating; - qint64 _sendingProgress = 0; - qint64 _sendingTotal = 0; QNetworkAccessManager _sendManager; QNetworkReply *_checkReply = nullptr; @@ -209,16 +193,12 @@ private: }; class NetworkSettingsWindow : public PreLaunchWindow { - Q_OBJECT public: NetworkSettingsWindow(QWidget *parent, QString host, quint32 port, QString username, QString password); -Q_SIGNALS: - void saved(QString host, quint32 port, QString username, QString password); - -public Q_SLOTS: - void onSave(); + [[nodiscard]] rpl::producer saveRequests() const; + void save(); protected: void closeEvent(QCloseEvent *e); @@ -233,17 +213,6 @@ private: QWidget *_parent; -}; - -class ShowCrashReportWindow : public PreLaunchWindow { -public: - ShowCrashReportWindow(const QString &text); - -protected: - void resizeEvent(QResizeEvent *e); - void closeEvent(QCloseEvent *e); - -private: - PreLaunchLog _log; + rpl::event_stream _saveRequests; }; diff --git a/Telegram/SourceFiles/core/crash_reports.cpp b/Telegram/SourceFiles/core/crash_reports.cpp index 06ec930ce..4355f79a5 100644 --- a/Telegram/SourceFiles/core/crash_reports.cpp +++ b/Telegram/SourceFiles/core/crash_reports.cpp @@ -16,8 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS - -// see https://blog.inventic.eu/2012/08/qt-and-google-breakpad/ #ifdef Q_OS_WIN #pragma warning(push) @@ -25,11 +23,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "client/windows/handler/exception_handler.h" #pragma warning(pop) -#elif defined Q_OS_MAC // Q_OS_WIN +#elif defined Q_OS_UNIX // Q_OS_WIN #include -#include #include + +#ifdef Q_OS_MAC + #include #include @@ -39,16 +39,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "client/crashpad_client.h" #endif // else for MAC_USE_BREAKPAD -#elif defined Q_OS_UNIX // Q_OS_MAC - -#include -#include -#include +#else // Q_OS_MAC #include "client/linux/handler/exception_handler.h" -#endif // Q_OS_UNIX +#endif // Q_OS_MAC +#endif // Q_OS_WIN #endif // !DESKTOP_APP_DISABLE_CRASH_REPORTS namespace CrashReports { @@ -131,20 +128,98 @@ void InstallQtMessageHandler() { }); } -Qt::HANDLE ReportingThreadId = nullptr; -bool ReportingHeaderWritten = false; -QMutex ReportingMutex; +std::atomic ReportingThreadId/* = nullptr*/; +bool ReportingHeaderWritten/* = false*/; +const char *BreakpadDumpPath/* = nullptr*/; +const wchar_t *BreakpadDumpPathW/* = nullptr*/; -const char *BreakpadDumpPath = nullptr; -const wchar_t *BreakpadDumpPathW = nullptr; +void WriteReportHeader() { + if (ReportingHeaderWritten) { + return; + } + ReportingHeaderWritten = true; + const auto dec2hex = [](int value) -> char { + if (value >= 0 && value < 10) { + return '0' + value; + } else if (value >= 10 && value < 16) { + return 'a' + (value - 10); + } + return '#'; + }; + for (const auto &i : ProcessAnnotationRefs) { + QByteArray utf8 = i.second->toUtf8(); + std::string wrapped; + wrapped.reserve(4 * utf8.size()); + for (auto ch : utf8) { + auto uch = static_cast(ch); + wrapped.append("\\x", 2).append(1, dec2hex(uch >> 4)).append(1, dec2hex(uch & 0x0F)); + } + ProcessAnnotations[i.first] = wrapped; + } + for (const auto &i : ProcessAnnotations) { + dump() << i.first.c_str() << ": " << i.second.c_str() << "\n"; + } + Platform::WriteCrashDumpDetails(); + dump() << "\n"; +} + +void WriteReportInfo(int signum, const char *name) { + WriteReportHeader(); + + const auto thread = ReportingThreadId.load(); + if (name) { + dump() << "Caught signal " << signum << " (" << name << ") in thread " << uint64(thread) << "\n"; + } else if (signum == -1) { + dump() << "Google Breakpad caught a crash, minidump written in thread " << uint64(thread) << "\n"; + if (BreakpadDumpPath) { + dump() << "Minidump: " << BreakpadDumpPath << "\n"; + } else if (BreakpadDumpPathW) { + dump() << "Minidump: " << BreakpadDumpPathW << "\n"; + } + } else { + dump() << "Caught signal " << signum << " in thread " << uint64(thread) << "\n"; + } + + dump() << "\nBacktrace omitted.\n"; + dump() << "\n"; +} + +const int HandledSignals[] = { + SIGSEGV, + SIGABRT, + SIGFPE, + SIGILL, +#ifdef Q_OS_UNIX + SIGBUS, + SIGTRAP, +#endif // Q_OS_UNIX +}; #ifdef Q_OS_UNIX -struct sigaction SIG_def[32]; +struct sigaction OldSigActions[32]/* = { 0 }*/; + +void RestoreSignalHandlers() { + for (const auto signum : HandledSignals) { + sigaction(signum, &OldSigActions[signum], nullptr); + } +} + +void InvokeOldSignalHandler(int signum, siginfo_t *info, void *ucontext) { + if (signum < 0 || signum > 31) { + return; + } else if (OldSigActions[signum].sa_flags & SA_SIGINFO) { + if (OldSigActions[signum].sa_sigaction) { + OldSigActions[signum].sa_sigaction(signum, info, ucontext); + } + } else { + if (OldSigActions[signum].sa_handler) { + OldSigActions[signum].sa_handler(signum); + } + } +} void SignalHandler(int signum, siginfo_t *info, void *ucontext) { - if (signum > 0) { - sigaction(signum, &SIG_def[signum], 0); - } + RestoreSignalHandlers(); #else // Q_OS_UNIX void SignalHandler(int signum) { @@ -162,125 +237,17 @@ void SignalHandler(int signum) { #endif // !Q_OS_WIN } - Qt::HANDLE thread = QThread::currentThreadId(); - if (thread == ReportingThreadId) return; + auto expected = Qt::HANDLE(nullptr); + const auto thread = QThread::currentThreadId(); - QMutexLocker lock(&ReportingMutex); - ReportingThreadId = thread; - - if (!ReportingHeaderWritten) { - ReportingHeaderWritten = true; - auto dec2hex = [](int value) -> char { - if (value >= 0 && value < 10) { - return '0' + value; - } else if (value >= 10 && value < 16) { - return 'a' + (value - 10); - } - return '#'; - }; - - for (const auto &i : ProcessAnnotationRefs) { - QByteArray utf8 = i.second->toUtf8(); - std::string wrapped; - wrapped.reserve(4 * utf8.size()); - for (auto ch : utf8) { - auto uch = static_cast(ch); - wrapped.append("\\x", 2).append(1, dec2hex(uch >> 4)).append(1, dec2hex(uch & 0x0F)); - } - ProcessAnnotations[i.first] = wrapped; - } - - for (const auto &i : ProcessAnnotations) { - dump() << i.first.c_str() << ": " << i.second.c_str() << "\n"; - } - psWriteDump(); - dump() << "\n"; - } - if (name) { - dump() << "Caught signal " << signum << " (" << name << ") in thread " << uint64(thread) << "\n"; - } else if (signum == -1) { - dump() << "Google Breakpad caught a crash, minidump written in thread " << uint64(thread) << "\n"; - if (BreakpadDumpPath) { - dump() << "Minidump: " << BreakpadDumpPath << "\n"; - } else if (BreakpadDumpPathW) { - dump() << "Minidump: " << BreakpadDumpPathW << "\n"; - } - } else { - dump() << "Caught signal " << signum << " in thread " << uint64(thread) << "\n"; + if (ReportingThreadId.compare_exchange_strong(expected, thread)) { + WriteReportInfo(signum, name); + ReportingThreadId = nullptr; } - // see https://github.com/benbjohnson/bandicoot #ifdef Q_OS_UNIX - ucontext_t *uc = (ucontext_t*)ucontext; - - void *caller = 0; - if (uc) { -#if defined(__APPLE__) && !defined(MAC_OS_X_VERSION_10_6) - /* OSX < 10.6 */ -#if defined(__x86_64__) - caller = (void*)uc->uc_mcontext->__ss.__rip; -#elif defined(__i386__) - caller = (void*)uc->uc_mcontext->__ss.__eip; -#else - caller = (void*)uc->uc_mcontext->__ss.__srr0; -#endif -#elif defined(__APPLE__) && defined(MAC_OS_X_VERSION_10_6) - /* OSX >= 10.6 */ -#if defined(_STRUCT_X86_THREAD_STATE64) && !defined(__i386__) - caller = (void*)uc->uc_mcontext->__ss.__rip; -#else - caller = (void*)uc->uc_mcontext->__ss.__eip; -#endif -#elif defined(__linux__) - /* Linux */ -#if defined(__i386__) - caller = (void*)uc->uc_mcontext.gregs[14]; /* Linux 32 */ -#elif defined(__X86_64__) || defined(__x86_64__) - caller = (void*)uc->uc_mcontext.gregs[16]; /* Linux 64 */ -#elif defined(__ia64__) /* Linux IA64 */ - caller = (void*)uc->uc_mcontext.sc_ip; -#endif - -#endif - } - - void *addresses[132] = { 0 }; - size_t size = backtrace(addresses, 128); - - /* overwrite sigaction with caller's address */ - if (caller) { - for (int i = size; i > 1; --i) { - addresses[i + 3] = addresses[i]; - } - addresses[2] = (void*)0x1; - addresses[3] = caller; - addresses[4] = (void*)0x1; - } - -#ifdef Q_OS_MAC - dump() << "\nBase image addresses:\n"; - for (size_t i = 0; i < size; ++i) { - Dl_info info; - dump() << i << " "; - if (dladdr(addresses[i], &info)) { - dump() << uint64(info.dli_fbase) << " (" << info.dli_fname << ")\n"; - } else { - dump() << "_unknown_module_\n"; - } - } -#endif // Q_OS_MAC - - dump() << "\nBacktrace:\n"; - - backtrace_symbols_fd(addresses, size, ReportFileNo); - -#else // Q_OS_UNIX - dump() << "\nBacktrace omitted.\n"; -#endif // else for Q_OS_UNIX - - dump() << "\n"; - - ReportingThreadId = nullptr; + InvokeOldSignalHandler(signum, info, ucontext); +#endif // Q_OS_UNIX } bool SetSignalHandlers = true; @@ -475,17 +442,13 @@ Status Restart() { sigemptyset(&sigact.sa_mask); sigact.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO; - sigaction(SIGABRT, &sigact, &SIG_def[SIGABRT]); - sigaction(SIGSEGV, &sigact, &SIG_def[SIGSEGV]); - sigaction(SIGILL, &sigact, &SIG_def[SIGILL]); - sigaction(SIGFPE, &sigact, &SIG_def[SIGFPE]); - sigaction(SIGBUS, &sigact, &SIG_def[SIGBUS]); - sigaction(SIGSYS, &sigact, &SIG_def[SIGSYS]); + for (const auto signum : HandledSignals) { + sigaction(signum, &sigact, &OldSigActions[signum]); + } #else // !Q_OS_WIN - signal(SIGABRT, SignalHandler); - signal(SIGSEGV, SignalHandler); - signal(SIGILL, SignalHandler); - signal(SIGFPE, SignalHandler); + for (const auto signum : HandledSignals) { + signal(signum, SignalHandler); + } #endif // else for !Q_OS_WIN } diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index a23982767..97cecbcb4 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -12,13 +12,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "base/platform/base_platform_file_utilities.h" #include "ui/main_queue_processor.h" -#include "ui/ui_utility.h" #include "core/crash_reports.h" #include "core/update_checker.h" #include "core/sandbox.h" #include "base/concurrent_timer.h" #include "kotato/json_settings.h" +#include + namespace Core { namespace { @@ -102,6 +103,9 @@ void ComputeDebugMode() { if (cDebugMode()) { Logs::SetDebugEnabled(true); } + if (Logs::DebugEnabled()) { + QLoggingCategory::setFilterRules("qt.qpa.gl.debug=true"); + } } void ComputeExternalUpdater() { @@ -339,10 +343,6 @@ int Launcher::exec() { // Must be started before Sandbox is created. Platform::start(); - if (!cQtScale()) { - Ui::DisableCustomScaling(); - } - if (cUseEnvApi() && qEnvironmentVariableIsSet(kApiIdVarName.utf8().constData()) && qEnvironmentVariableIsSet(kApiHashVarName.utf8().constData())) { @@ -441,7 +441,6 @@ void Launcher::prepareSettings() { void Launcher::initQtMessageLogging() { static QtMessageHandler OriginalMessageHandler = nullptr; - static bool WritingQtMessage = false; OriginalMessageHandler = qInstallMessageHandler([]( QtMsgType type, const QMessageLogContext &context, @@ -450,10 +449,9 @@ void Launcher::initQtMessageLogging() { OriginalMessageHandler(type, context, msg); } if (Logs::DebugEnabled() || !Logs::started()) { - if (!WritingQtMessage) { - WritingQtMessage = true; + if (!Logs::WritingEntry()) { + // Sometimes Qt logs something inside our own logging. LOG((msg)); - WritingQtMessage = false; } } }); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 5ef33749a..95d8e7225 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/local_url_handlers.h" +#include "api/api_authorizations.h" #include "api/api_text_entities.h" #include "api/api_chat_invite.h" #include "base/qthelp_regex.h" @@ -68,7 +69,7 @@ bool ShowStickerSet( return false; } Core::App().hideMediaView(); - Ui::show(Box( + controller->show(Box( controller, MTP_inputStickerSetShortName(MTP_string(match->captured(1))))); return true; @@ -84,6 +85,7 @@ bool ShowTheme( const auto fromMessageId = context.value().itemId; Core::App().hideMediaView(); controller->session().data().cloudThemes().resolve( + &controller->window(), match->captured(1), fromMessageId); return true; @@ -231,9 +233,15 @@ bool ShowWallPaper( const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); + if (!params.value("gradient").isEmpty()) { + Ui::show(Box( + tr::lng_background_gradient_unsupported(tr::now))); + return false; + } + const auto color = params.value("color"); return BackgroundPreviewBox::Start( controller, - params.value(qsl("slug")), + (color.isEmpty() ? params.value(qsl("slug")) : color), params); } @@ -366,7 +374,8 @@ bool ResolveSettings( return true; } if (section == qstr("devices")) { - Ui::show(Box(&controller->session())); + controller->session().api().authorizations().reload(); + controller->show(Box(&controller->session())); return true; } else if (section == qstr("language")) { ShowLanguagesBox(); @@ -401,12 +410,12 @@ bool HandleUnknown( Core::UpdateApplication(); close(); }; - Ui::show(Box( + controller->show(Box( text, tr::lng_menu_update(tr::now), callback)); } else { - Ui::show(Box(text)); + controller->show(Box(text)); } }); controller->session().api().requestDeepLinkInfo(request, callback); @@ -437,9 +446,7 @@ bool OpenMediaTimestamp( documentId, time * crl::time(1000)); if (document->isVideoFile()) { - Core::App().showDocument( - document, - session->data().message(itemId)); + controller->openDocument(document, itemId, true); } else if (document->isSong() || document->isVoiceMessage()) { Media::Player::instance()->play({ document, itemId }); } @@ -584,9 +591,16 @@ QString TryConvertUrlToLocal(QString url) { return qsl("tg://socks?") + socksMatch->captured(1); } else if (auto proxyMatch = regex_match(qsl("^proxy/?\\?(.+)(#|$)"), query, matchOptions)) { return qsl("tg://proxy?") + proxyMatch->captured(1); - } else if (auto bgMatch = regex_match(qsl("^bg/([a-zA-Z0-9\\.\\_\\-]+)(\\?(.+)?)?$"), query, matchOptions)) { + } else if (auto bgMatch = regex_match(qsl("^bg/([a-zA-Z0-9\\.\\_\\-\\~]+)(\\?(.+)?)?$"), query, matchOptions)) { const auto params = bgMatch->captured(3); - return qsl("tg://bg?slug=") + bgMatch->captured(1) + (params.isEmpty() ? QString() : '&' + params); + const auto bg = bgMatch->captured(1); + const auto type = regex_match(qsl("^[a-fA-F0-9]{6}^"), bg) + ? "color" + : (regex_match(qsl("^[a-fA-F0-9]{6}\\-[a-fA-F0-9]{6}$"), bg) + || regex_match(qsl("^[a-fA-F0-9]{6}(\\~[a-fA-F0-9]{6}){1,3}$"), bg)) + ? "gradient" + : "slug"; + return qsl("tg://bg?") + type + '=' + bg + (params.isEmpty() ? QString() : '&' + params); } else if (auto postMatch = regex_match(qsl("^c/(\\-?\\d+)/(\\d+)(/?\\?|/?$)"), query, matchOptions)) { auto params = query.mid(postMatch->captured(0).size()).toString(); return qsl("tg://privatepost?channel=%1&post=%2").arg(postMatch->captured(1), postMatch->captured(2)) + (params.isEmpty() ? QString() : '&' + params); diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index d1002b892..90e20d121 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -25,8 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_url.h" #include "base/qthelp_regex.h" #include "base/qt_adapters.h" +#include "ui/ui_utility.h" #include "ui/effects/animations.h" -#include "facades.h" #include "app.h" #include @@ -313,6 +313,10 @@ void Sandbox::singleInstanceChecked() { Logs::multipleInstances(); } + if (!cQtScale()) { + Ui::DisableCustomScaling(); + } + refreshGlobalProxy(); if (!Logs::started() || (!cManyInstance() && !Logs::instanceChecked())) { new NotStartedWindow(); @@ -450,17 +454,17 @@ void Sandbox::checkForQuit() { } void Sandbox::refreshGlobalProxy() { - const auto proxy = !Global::started() + const auto proxy = !Core::IsAppLaunched() ? _sandboxProxy - : (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled) - ? Global::SelectedProxy() + : Core::App().settings().proxy().isEnabled() + ? Core::App().settings().proxy().selected() : MTP::ProxyData(); if (proxy.type == MTP::ProxyData::Type::Socks5 || proxy.type == MTP::ProxyData::Type::Http) { QNetworkProxy::setApplicationProxy( MTP::ToNetworkProxy(MTP::ToDirectIpProxy(proxy))); - } else if (!Global::started() - || Global::ProxySettings() == MTP::ProxyData::Settings::System) { + } else if (!Core::IsAppLaunched() + || Core::App().settings().proxy().isSystem()) { QNetworkProxyFactory::setUseSystemConfiguration(true); } else { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); diff --git a/Telegram/SourceFiles/core/shortcuts.cpp b/Telegram/SourceFiles/core/shortcuts.cpp index 33da82349..ec46ed3a4 100644 --- a/Telegram/SourceFiles/core/shortcuts.cpp +++ b/Telegram/SourceFiles/core/shortcuts.cpp @@ -15,7 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "platform/platform_specific.h" #include "base/parse_helper.h" -#include "facades.h" #include #include @@ -586,8 +585,6 @@ rpl::producer> Requests() { } void Start() { - Assert(Global::started()); - Data.fill(); } diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 6bc5e4910..c5e209b66 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -22,7 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_app_config.h" #include "mainwindow.h" -#include "facades.h" // Global::ScreenIsLocked. namespace Core { namespace { @@ -86,6 +85,10 @@ const auto kBadPrefix = u"http://"_q; return true; } +[[nodiscard]] QString OpenGLCheckFilePath() { + return cWorkingDir() + "tdata/opengl_crash_check"; +} + } // namespace void UiIntegration::postponeCall(FnMut &&callable) { @@ -104,6 +107,22 @@ QString UiIntegration::emojiCacheFolder() { return cWorkingDir() + "tdata/emoji"; } +void UiIntegration::openglCheckStart() { + auto f = QFile(OpenGLCheckFilePath()); + if (f.open(QIODevice::WriteOnly)) { + f.write("1", 1); + f.close(); + } +} + +void UiIntegration::openglCheckFinish() { + QFile::remove(OpenGLCheckFilePath()); +} + +bool UiIntegration::openglLastCheckFailed() { + return OpenGLLastCheckFailed(); +} + void UiIntegration::textActionsUpdated() { if (const auto window = App::wnd()) { window->updateGlobalMenu(); @@ -127,7 +146,7 @@ style::CustomFontSettings UiIntegration::fontSettings() { } bool UiIntegration::screenIsLocked() { - return Global::ScreenIsLocked(); + return Core::App().screenIsLocked(); } QString UiIntegration::timeFormat() { @@ -313,4 +332,8 @@ QString UiIntegration::phraseFormattingMonospace() { return tr::lng_menu_formatting_monospace(tr::now); } +bool OpenGLLastCheckFailed() { + return QFile::exists(OpenGLCheckFilePath()); +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/ui_integration.h b/Telegram/SourceFiles/core/ui_integration.h index 811aa6903..eec01315a 100644 --- a/Telegram/SourceFiles/core/ui_integration.h +++ b/Telegram/SourceFiles/core/ui_integration.h @@ -38,6 +38,10 @@ public: QString emojiCacheFolder() override; + void openglCheckStart() override; + void openglCheckFinish() override; + bool openglLastCheckFailed() override; + void textActionsUpdated() override; void activationFromTopPanel() override; @@ -72,4 +76,6 @@ public: }; +[[nodiscard]] bool OpenGLLastCheckFailed(); + } // namespace Core diff --git a/Telegram/SourceFiles/core/update_checker.cpp b/Telegram/SourceFiles/core/update_checker.cpp index 927cb927d..5edcc42d8 100644 --- a/Telegram/SourceFiles/core/update_checker.cpp +++ b/Telegram/SourceFiles/core/update_checker.cpp @@ -45,6 +45,10 @@ extern "C" { #include #endif // else of Q_OS_WIN && !DESKTOP_APP_USE_PACKAGED +#ifdef Q_OS_UNIX +#include +#endif // Q_OS_UNIX + namespace Core { namespace { @@ -72,6 +76,18 @@ using VersionChar = wchar_t; using Loader = MTP::AbstractDedicatedLoader; +struct BIODeleter { + void operator()(BIO *value) { + BIO_free(value); + } +}; + +inline auto MakeBIO(const void *buf, int len) { + return std::unique_ptr{ + BIO_new_mem_buf(buf, len), + }; +} + class Checker : public base::has_weak_ptr { public: Checker(bool testing); @@ -285,7 +301,15 @@ bool UnpackUpdate(const QString &filepath) { return false; } - RSA *pbKey = PEM_read_bio_RSAPublicKey(BIO_new_mem_buf(const_cast(AppBetaVersion ? UpdatesPublicBetaKey : UpdatesPublicKey), -1), 0, 0, 0); + RSA *pbKey = [] { + const auto bio = MakeBIO( + const_cast( + AppBetaVersion + ? UpdatesPublicBetaKey + : UpdatesPublicKey), + -1); + return PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0); + }(); if (!pbKey) { LOG(("Update Error: cant read public rsa key!")); return false; @@ -294,7 +318,15 @@ bool UnpackUpdate(const QString &filepath) { RSA_free(pbKey); // try other public key, if we update from beta to stable or vice versa - pbKey = PEM_read_bio_RSAPublicKey(BIO_new_mem_buf(const_cast(AppBetaVersion ? UpdatesPublicKey : UpdatesPublicBetaKey), -1), 0, 0, 0); + pbKey = [] { + const auto bio = MakeBIO( + const_cast( + AppBetaVersion + ? UpdatesPublicKey + : UpdatesPublicBetaKey), + -1); + return PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0); + }(); if (!pbKey) { LOG(("Update Error: cant read public rsa key!")); return false; @@ -1549,9 +1581,32 @@ bool checkReadyUpdate() { return false; } #elif defined Q_OS_UNIX // Q_OS_MAC + // if the files in the directory are owned by user, while the directory is not, + // update will still fail since it's not possible to remove files + if (QFile::exists(curUpdater) + && unlink(QFile::encodeName(curUpdater).constData())) { + if (errno == EACCES) { + DEBUG_LOG(("Update Info: " + "could not unlink current Updater, access denied.")); + cSetWriteProtected(true); + return true; + } else { + DEBUG_LOG(("Update Error: could not unlink current Updater.")); + ClearAll(); + return false; + } + } if (!linuxMoveFile(QFile::encodeName(updater.absoluteFilePath()).constData(), QFile::encodeName(curUpdater).constData())) { - ClearAll(); - return false; + if (errno == EACCES) { + DEBUG_LOG(("Update Info: " + "could not copy new Updater, access denied.")); + cSetWriteProtected(true); + return true; + } else { + DEBUG_LOG(("Update Error: could not copy new Updater.")); + ClearAll(); + return false; + } } #endif // Q_OS_UNIX @@ -1618,7 +1673,12 @@ QString countAlphaVersionSignature(uint64 version) { // duplicated in packer.cpp uint32 siglen = 0; - RSA *prKey = PEM_read_bio_RSAPrivateKey(BIO_new_mem_buf(const_cast(cAlphaPrivateKey().constData()), -1), 0, 0, 0); + RSA *prKey = [] { + const auto bio = MakeBIO( + const_cast(cAlphaPrivateKey().constData()), + -1); + return PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0); + }(); if (!prKey) { LOG(("Error: Could not read alpha private key!")); return QString(); diff --git a/Telegram/SourceFiles/core/utils.h b/Telegram/SourceFiles/core/utils.h index f2d47c438..3f932644e 100644 --- a/Telegram/SourceFiles/core/utils.h +++ b/Telegram/SourceFiles/core/utils.h @@ -121,18 +121,6 @@ void memset_rand(void *data, uint32 len); QString translitRusEng(const QString &rus); QString rusKeyboardLayoutSwitch(const QString &from); -enum DBINotifyView { - dbinvShowPreview = 0, - dbinvShowName = 1, - dbinvShowNothing = 2, -}; - -enum DBIWorkMode { - dbiwmWindowAndTray = 0, - dbiwmTrayOnly = 1, - dbiwmWindowOnly = 2, -}; - static const int MatrixRowShift = 40000; inline int rowscount(int fullCount, int countPerRow) { diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 4468ba310..e2a1b8a3a 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -23,7 +23,7 @@ constexpr auto AppId = "{C4A4AE8F-B9F7-4CC7-8A6C-BF7EEE87ACA5}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Kotatogram Desktop"_cs; constexpr auto AppFile = "Kotatogram"_cs; -constexpr auto AppVersion = 2007004; -constexpr auto AppVersionStr = "2.7.4"; +constexpr auto AppVersion = 2007010; +constexpr auto AppVersionStr = "2.7.10"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_cloud_themes.cpp b/Telegram/SourceFiles/data/data_cloud_themes.cpp index 4f3ecc3ee..d66d64348 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.cpp +++ b/Telegram/SourceFiles/data/data_cloud_themes.cpp @@ -17,11 +17,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "main/main_session.h" #include "boxes/confirm_box.h" -#include "core/application.h" // Core::App().showTheme. +#include "media/view/media_view_open_common.h" #include "lang/lang_keys.h" #include "apiwrap.h" -#include "app.h" -#include "mainwindow.h" namespace Data { namespace { @@ -140,6 +138,7 @@ void CloudThemes::applyUpdate(const MTPTheme &theme) { } void CloudThemes::resolve( + not_null controller, const QString &slug, const FullMsgId &clickFromMessageId) { _session->api().request(_resolveRequestId).cancel(); @@ -148,31 +147,35 @@ void CloudThemes::resolve( MTP_inputThemeSlug(MTP_string(slug)), MTP_long(0) )).done([=](const MTPTheme &result) { - showPreview(result); + showPreview(controller, result); }).fail([=](const MTP::Error &error) { if (error.type() == qstr("THEME_FORMAT_INVALID")) { - Ui::show(Box( + controller->show(Box( tr::ktg_theme_no_desktop(tr::now))); } }).send(); } -void CloudThemes::showPreview(const MTPTheme &data) { +void CloudThemes::showPreview( + not_null controller, + const MTPTheme &data) { data.match([&](const MTPDtheme &data) { - showPreview(CloudTheme::Parse(_session, data)); + showPreview(controller, CloudTheme::Parse(_session, data)); }); } -void CloudThemes::showPreview(const CloudTheme &cloud) { +void CloudThemes::showPreview( + not_null controller, + const CloudTheme &cloud) { if (const auto documentId = cloud.documentId) { - previewFromDocument(cloud); + previewFromDocument(controller, cloud); } else if (cloud.createdBy == _session->userId()) { - Ui::show(Box( + controller->show(Box( Window::Theme::CreateForExistingBox, - &App::wnd()->controller(), + controller, cloud)); } else { - Ui::show(Box( + controller->show(Box( tr::ktg_theme_no_desktop(tr::now))); } } @@ -193,12 +196,19 @@ void CloudThemes::applyFromDocument(const CloudTheme &cloud) { }); } -void CloudThemes::previewFromDocument(const CloudTheme &cloud) { +void CloudThemes::previewFromDocument( + not_null controller, + const CloudTheme &cloud) { + const auto sessionController = controller->sessionController(); + if (!sessionController) { + return; + } const auto document = _session->data().document(cloud.documentId); loadDocumentAndInvoke(_previewFrom, cloud, document, [=]( std::shared_ptr media) { const auto document = media->owner(); - Core::App().showTheme(document, cloud); + using Open = Media::View::OpenRequest; + controller->openInMediaView(Open(sessionController, document, cloud)); }); } diff --git a/Telegram/SourceFiles/data/data_cloud_themes.h b/Telegram/SourceFiles/data/data_cloud_themes.h index 2e3e2af9f..0a32db306 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.h +++ b/Telegram/SourceFiles/data/data_cloud_themes.h @@ -15,6 +15,10 @@ namespace Main { class Session; } // namespace Main +namespace Window { +class Controller; +} // namespace Window + namespace Data { class DocumentMedia; @@ -47,9 +51,16 @@ public: void applyUpdate(const MTPTheme &theme); - void resolve(const QString &slug, const FullMsgId &clickFromMessageId); - void showPreview(const MTPTheme &data); - void showPreview(const CloudTheme &cloud); + void resolve( + not_null controller, + const QString &slug, + const FullMsgId &clickFromMessageId); + void showPreview( + not_null controller, + const MTPTheme &data); + void showPreview( + not_null controller, + const CloudTheme &cloud); void applyFromDocument(const CloudTheme &cloud); private: @@ -69,7 +80,9 @@ private: [[nodiscard]] bool needReload() const; void scheduleReload(); void reloadCurrent(); - void previewFromDocument(const CloudTheme &cloud); + void previewFromDocument( + not_null controller, + const CloudTheme &cloud); void loadDocumentAndInvoke( LoadingDocument &value, const CloudTheme &cloud, diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp index 523c73681..1a1b718a7 100644 --- a/Telegram/SourceFiles/data/data_document.cpp +++ b/Telegram/SourceFiles/data/data_document.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_document.h" +#include "data/data_document_resolver.h" #include "data/data_session.h" #include "data/data_streaming.h" #include "data/data_document_media.h" @@ -40,7 +41,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "ui/image/image.h" #include "ui/text/text_utilities.h" -#include "ui/text/format_values.h" #include "base/base_file_utilities.h" #include "mainwindow.h" #include "core/application.h" @@ -76,59 +76,6 @@ QString JoinStringList(const QStringList &list, const QString &separator) { return result; } -void LaunchWithWarning( - not_null session, - const QString &name, - HistoryItem *item) { - const auto isExecutable = Data::IsExecutableName(name); - const auto isIpReveal = Data::IsIpRevealingName(name); - auto &app = Core::App(); - const auto warn = [&] { - if (item && item->history()->peer->isVerified()) { - return false; - } - return (isExecutable && app.settings().exeLaunchWarning()) - || (isIpReveal && app.settings().ipRevealWarning()); - }(); - const auto extension = '.' + Data::FileExtension(name); - if (Platform::IsWindows() && extension == u"."_q) { - // If you launch a file without extension, like "test", in case - // there is an executable file with the same name in this folder, - // like "test.bat", the executable file will be launched. - // - // Now we always force an Open With dialog box for such files. - crl::on_main([=] { - Platform::File::UnsafeShowOpenWith(name); - }); - return; - } else if (!warn) { - File::Launch(name); - return; - } - const auto callback = [=, &app](bool checked) { - if (checked) { - if (isExecutable) { - app.settings().setExeLaunchWarning(false); - } else if (isIpReveal) { - app.settings().setIpRevealWarning(false); - } - app.saveSettingsDelayed(); - } - File::Launch(name); - }; - auto text = isExecutable - ? tr::lng_launch_exe_warning( - lt_extension, - rpl::single(Ui::Text::Bold(extension)), - Ui::Text::WithEntities) - : tr::lng_launch_svg_warning(Ui::Text::WithEntities); - Ui::show(Box( - std::move(text), - tr::lng_launch_exe_dont_ask(tr::now), - (isExecutable ? tr::lng_launch_exe_sure : tr::lng_continue)(), - callback)); -} - } // namespace QString FileNameUnsafe( @@ -309,162 +256,6 @@ QString DocumentFileNameForSave( dir); } -DocumentClickHandler::DocumentClickHandler( - not_null document, - FullMsgId context) -: FileClickHandler(&document->session(), context) -, _document(document) { -} - -void DocumentOpenClickHandler::Open( - Data::FileOrigin origin, - not_null data, - HistoryItem *context) { - if (!data->date) { - return; - } - - const auto media = data->createMediaView(); - const auto openImageInApp = [&] { - if (data->size >= App::kImageSizeLimit) { - return false; - } - const auto &location = data->location(true); - if (!location.isEmpty() && location.accessEnable()) { - const auto guard = gsl::finally([&] { - location.accessDisable(); - }); - const auto path = location.name(); - if (Core::MimeTypeForFile(path).name().startsWith("image/") - && QImageReader(path).canRead()) { - Core::App().showDocument(data, context); - return true; - } - } else if (data->mimeString().startsWith("image/") - && !media->bytes().isEmpty()) { - auto bytes = media->bytes(); - auto buffer = QBuffer(&bytes); - if (QImageReader(&buffer).canRead()) { - Core::App().showDocument(data, context); - return true; - } - } - return false; - }; - const auto &location = data->location(true); - if (data->isTheme() && media->loaded(true)) { - Core::App().showDocument(data, context); - location.accessDisable(); - } else if (media->canBePlayed()) { - if (data->isAudioFile() - || data->isVoiceMessage() - || data->isVideoMessage()) { - const auto msgId = context ? context->fullId() : FullMsgId(); - Media::Player::instance()->playPause({ data, msgId }); - /* - } else if (context - && data->isAnimation() - && HistoryView::Gif::CanPlayInline(data)) { - data->owner().requestAnimationPlayInline(context); - */ - } else { - Core::App().showDocument(data, context); - } - } else { - data->saveFromDataSilent(); - if (!openImageInApp()) { - if (!data->filepath(true).isEmpty()) { - LaunchWithWarning(&data->session(), location.name(), context); - } else if (data->status == FileReady - || data->status == FileDownloadFailed) { - DocumentSaveClickHandler::Save(origin, data); - } - } - } -} - -void DocumentOpenClickHandler::onClickImpl() const { - Open(context(), document(), getActionItem()); -} - -void DocumentSaveClickHandler::Save( - Data::FileOrigin origin, - not_null data, - Mode mode) { - if (!data->date) { - return; - } - - auto savename = QString(); - if (mode != Mode::ToCacheOrFile || !data->saveToCache()) { - if (mode != Mode::ToNewFile && data->saveFromData()) { - return; - } - const auto filepath = data->filepath(true); - const auto fileinfo = QFileInfo( - ); - const auto filedir = filepath.isEmpty() - ? QDir() - : fileinfo.dir(); - const auto filename = filepath.isEmpty() - ? QString() - : fileinfo.fileName(); - savename = DocumentFileNameForSave( - data, - (mode == Mode::ToNewFile), - filename, - filedir); - if (savename.isEmpty()) { - return; - } - } - data->save(origin, savename); -} - -void DocumentSaveClickHandler::onClickImpl() const { - Save(context(), document()); -} - -void DocumentCancelClickHandler::onClickImpl() const { - const auto data = document(); - if (!data->date) { - return; - } else if (data->uploading()) { - if (const auto item = data->owner().message(context())) { - if (const auto m = App::main()) { // multi good - if (&m->session() == &data->session()) { - m->cancelUploadLayer(item); - } - } - } - } else { - data->cancel(); - } -} - -void DocumentOpenWithClickHandler::Open( - Data::FileOrigin origin, - not_null data) { - if (!data->date) { - return; - } - - data->saveFromDataSilent(); - const auto path = data->filepath(true); - if (!path.isEmpty()) { - File::OpenWith(path, QCursor::pos()); - } else { - DocumentSaveClickHandler::Save( - origin, - data, - DocumentSaveClickHandler::Mode::ToFile); - } -} - -void DocumentOpenWithClickHandler::onClickImpl() const { - Open(context(), document()); -} - Data::FileOrigin StickerData::setOrigin() const { return set.match([&](const MTPDinputStickerSetID &data) { return Data::FileOrigin( @@ -1466,16 +1257,6 @@ uint8 DocumentData::cacheTag() const { return 0; } -QString DocumentData::composeNameString() const { - if (auto songData = song()) { - return Ui::ComposeNameString( - _filename, - songData->title, - songData->performer); - } - return Ui::ComposeNameString(_filename, QString(), QString()); -} - LocationType DocumentData::locationType() const { return isVoiceMessage() ? AudioFileLocation @@ -1647,126 +1428,3 @@ void DocumentData::collectLocalData(not_null local) { session().local().writeFileLocation(mediaKey(), _location); } } - -namespace Data { - -QString FileExtension(const QString &filepath) { - const auto reversed = ranges::views::reverse(filepath); - const auto last = ranges::find_first_of(reversed, ".\\/"); - if (last == reversed.end() || *last != '.') { - return QString(); - } - return QString(last.base(), last - reversed.begin()); -} - -bool IsValidMediaFile(const QString &filepath) { - static const auto kExtensions = [] { - const auto list = qsl("\ -16svx 2sf 3g2 3gp 8svx aac aaf aif aifc aiff amr amv ape asf ast au aup \ -avchd avi brstm bwf cam cdda cust dat divx drc dsh dsf dts dtshd dtsma \ -dvr-ms dwd evo f4a f4b f4p f4v fla flac flr flv gif gifv gsf gsm gym iff \ -ifo it jam la ly m1v m2p m2ts m2v m4a m4p m4v mcf mid mk3d mka mks mkv mng \ -mov mp1 mp2 mp3 mp4 minipsf mod mpc mpe mpeg mpg mpv mscz mt2 mus mxf mxl \ -niff nsf nsv off ofr ofs ogg ogv opus ots pac ps psf psf2 psflib ptb qsf \ -qt ra raw rka rm rmj rmvb roq s3m shn sib sid smi smp sol spc spx ssf svi \ -swa swf tak ts tta txm usf vgm vob voc vox vqf wav webm wma wmv wrap wtv \ -wv xm xml ym yuv").split(' '); - return base::flat_set(list.begin(), list.end()); - }(); - - return ranges::binary_search( - kExtensions, - FileExtension(filepath).toLower()); -} - -bool IsExecutableName(const QString &filepath) { - static const auto kExtensions = [] { - const auto joined = -#ifdef Q_OS_MAC - qsl("\ -applescript action app bin command csh osx workflow terminal url caction \ -mpkg pkg scpt scptd xhtm webarchive"); -#elif defined Q_OS_UNIX // Q_OS_MAC - qsl("bin csh deb desktop ksh out pet pkg pup rpm run sh shar \ -slp zsh"); -#else // Q_OS_MAC || Q_OS_UNIX - qsl("\ -ad ade adp app application appref-ms asp asx bas bat bin cab cdxml cer cfg \ -chi chm cmd cnt com cpl crt csh der diagcab dll drv eml exe fon fxp gadget \ -grp hlp hpj hta htt inf ini ins inx isp isu its jar jnlp job js jse key ksh \ -lnk local lua mad maf mag mam manifest maq mar mas mat mau mav maw mcf mda \ -mdb mde mdt mdw mdz mht mhtml mjs mmc mof msc msg msh msh1 msh2 msh1xml \ -msh2xml mshxml msi msp mst ops osd paf pcd phar php php3 php4 php5 php7 phps \ -php-s pht phtml pif pl plg pm pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 \ -psd1 psm1 pssc pst py py3 pyc pyd pyi pyo pyw pywz pyz rb reg rgs scf scr \ -sct search-ms settingcontent-ms sh shb shs slk sys t tmp u3p url vb vbe vbp \ -vbs vbscript vdx vsmacros vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx \ -vtx website ws wsc wsf wsh xbap xll xnk xs"); -#endif // !Q_OS_MAC && !Q_OS_UNIX - const auto list = joined.split(' '); - return base::flat_set(list.begin(), list.end()); - }(); - - return ranges::binary_search( - kExtensions, - FileExtension(filepath).toLower()); -} - -bool IsIpRevealingName(const QString &filepath) { - static const auto kExtensions = [] { - const auto joined = u"htm html svg"_q; - const auto list = joined.split(' '); - return base::flat_set(list.begin(), list.end()); - }(); - static const auto kMimeTypes = [] { - const auto joined = u"text/html image/svg+xml"_q; - const auto list = joined.split(' '); - return base::flat_set(list.begin(), list.end()); - }(); - - return ranges::binary_search( - kExtensions, - FileExtension(filepath).toLower() - ) || ranges::binary_search( - kMimeTypes, - QMimeDatabase().mimeTypeForFile(QFileInfo(filepath)).name() - ); -} - -base::binary_guard ReadImageAsync( - not_null media, - FnMut postprocess, - FnMut done) { - auto result = base::binary_guard(); - crl::async([ - bytes = media->bytes(), - path = media->owner()->filepath(), - postprocess = std::move(postprocess), - guard = result.make_guard(), - callback = std::move(done) - ]() mutable { - auto format = QByteArray(); - if (bytes.isEmpty()) { - QFile f(path); - if (f.size() <= App::kImageSizeLimit - && f.open(QIODevice::ReadOnly)) { - bytes = f.readAll(); - } - } - auto image = bytes.isEmpty() - ? QImage() - : App::readImage(bytes, &format, false, nullptr); - if (postprocess) { - image = postprocess(std::move(image)); - } - crl::on_main(std::move(guard), [ - image = std::move(image), - callback = std::move(callback) - ]() mutable { - callback(std::move(image)); - }); - }); - return result; -} - -} // namespace Data diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h index 7ee523c0d..706f04743 100644 --- a/Telegram/SourceFiles/data/data_document.h +++ b/Telegram/SourceFiles/data/data_document.h @@ -228,8 +228,6 @@ public: [[nodiscard]] Storage::Cache::Key cacheKey() const; [[nodiscard]] uint8 cacheTag() const; - [[nodiscard]] QString composeNameString() const; - [[nodiscard]] bool canBeStreamed() const; [[nodiscard]] auto createStreamingLoader( Data::FileOrigin origin, @@ -329,103 +327,6 @@ private: VoiceWaveform documentWaveformDecode(const QByteArray &encoded5bit); QByteArray documentWaveformEncode5bit(const VoiceWaveform &waveform); -class DocumentClickHandler : public FileClickHandler { -public: - DocumentClickHandler( - not_null document, - FullMsgId context = FullMsgId()); - - [[nodiscard]] not_null document() const { - return _document; - } - -private: - const not_null _document; - -}; - -class DocumentSaveClickHandler : public DocumentClickHandler { -public: - enum class Mode { - ToCacheOrFile, - ToFile, - ToNewFile, - }; - using DocumentClickHandler::DocumentClickHandler; - static void Save( - Data::FileOrigin origin, - not_null document, - Mode mode = Mode::ToCacheOrFile); - -protected: - void onClickImpl() const override; - -}; - -class DocumentOpenClickHandler : public DocumentClickHandler { -public: - using DocumentClickHandler::DocumentClickHandler; - static void Open( - Data::FileOrigin origin, - not_null document, - HistoryItem *context); - -protected: - void onClickImpl() const override; - -}; - -class DocumentCancelClickHandler : public DocumentClickHandler { -public: - using DocumentClickHandler::DocumentClickHandler; - -protected: - void onClickImpl() const override; - -}; - -class DocumentOpenWithClickHandler : public DocumentClickHandler { -public: - using DocumentClickHandler::DocumentClickHandler; - static void Open( - Data::FileOrigin origin, - not_null document); - -protected: - void onClickImpl() const override; - -}; - -class VoiceSeekClickHandler : public DocumentOpenClickHandler { -public: - using DocumentOpenClickHandler::DocumentOpenClickHandler; - -protected: - void onClickImpl() const override { - } - -}; - -class DocumentWrappedClickHandler : public DocumentClickHandler { -public: - DocumentWrappedClickHandler( - ClickHandlerPtr wrapped, - not_null document, - FullMsgId context = FullMsgId()) - : DocumentClickHandler(document, context) - , _wrapped(wrapped) { - } - -protected: - void onClickImpl() const override { - _wrapped->onClick({ Qt::LeftButton }); - } - -private: - ClickHandlerPtr _wrapped; - -}; - QString FileNameForSave( not_null session, const QString &title, @@ -440,16 +341,3 @@ QString DocumentFileNameForSave( bool forceSavingAs = false, const QString &already = QString(), const QDir &dir = QDir()); - -namespace Data { - -[[nodiscard]] QString FileExtension(const QString &filepath); -[[nodiscard]] bool IsValidMediaFile(const QString &filepath); -[[nodiscard]] bool IsExecutableName(const QString &filepath); -[[nodiscard]] bool IsIpRevealingName(const QString &filepath); -base::binary_guard ReadImageAsync( - not_null media, - FnMut postprocess, - FnMut done); - -} // namespace Data diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 2e2f1c787..6a2824e29 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document_media.h" #include "data/data_document.h" +#include "data/data_document_resolver.h" #include "data/data_session.h" #include "data/data_cloud_themes.h" #include "data/data_file_origin.h" diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp new file mode 100644 index 000000000..12cba6c43 --- /dev/null +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -0,0 +1,290 @@ +/* +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_document_resolver.h" + +#include "app.h" +#include "facades.h" +#include "base/platform/base_platform_info.h" +#include "boxes/confirm_box.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "core/mime_type.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_click_handler.h" +#include "data/data_file_origin.h" +#include "data/data_session.h" +#include "history/view/media/history_view_gif.h" +#include "history/history.h" +#include "history/history_item.h" +#include "media/player/media_player_instance.h" +#include "platform/platform_file_utilities.h" +#include "ui/text/text_utilities.h" +#include "window/window_session_controller.h" + +#include +#include +#include + +namespace Data { +namespace { + +void LaunchWithWarning( + // not_null controller, + const QString &name, + HistoryItem *item) { + const auto isExecutable = Data::IsExecutableName(name); + const auto isIpReveal = Data::IsIpRevealingName(name); + auto &app = Core::App(); + const auto warn = [&] { + if (item && item->history()->peer->isVerified()) { + return false; + } + return (isExecutable && app.settings().exeLaunchWarning()) + || (isIpReveal && app.settings().ipRevealWarning()); + }(); + const auto extension = '.' + Data::FileExtension(name); + if (Platform::IsWindows() && extension == u"."_q) { + // If you launch a file without extension, like "test", in case + // there is an executable file with the same name in this folder, + // like "test.bat", the executable file will be launched. + // + // Now we always force an Open With dialog box for such files. + crl::on_main([=] { + Platform::File::UnsafeShowOpenWith(name); + }); + return; + } else if (!warn) { + File::Launch(name); + return; + } + const auto callback = [=, &app](bool checked) { + if (checked) { + if (isExecutable) { + app.settings().setExeLaunchWarning(false); + } else if (isIpReveal) { + app.settings().setIpRevealWarning(false); + } + app.saveSettingsDelayed(); + } + File::Launch(name); + }; + auto text = isExecutable + ? tr::lng_launch_exe_warning( + lt_extension, + rpl::single(Ui::Text::Bold(extension)), + Ui::Text::WithEntities) + : tr::lng_launch_svg_warning(Ui::Text::WithEntities); + Ui::show(Box( + std::move(text), + tr::lng_launch_exe_dont_ask(tr::now), + (isExecutable ? tr::lng_launch_exe_sure : tr::lng_continue)(), + callback)); +} + +} // namespace + +QString FileExtension(const QString &filepath) { + const auto reversed = ranges::views::reverse(filepath); + const auto last = ranges::find_first_of(reversed, ".\\/"); + if (last == reversed.end() || *last != '.') { + return QString(); + } + return QString(last.base(), last - reversed.begin()); +} + +// bool IsValidMediaFile(const QString &filepath) { +// static const auto kExtensions = [] { +// const auto list = qsl("\ +// 16svx 2sf 3g2 3gp 8svx aac aaf aif aifc aiff amr amv ape asf ast au aup \ +// avchd avi brstm bwf cam cdda cust dat divx drc dsh dsf dts dtshd dtsma \ +// dvr-ms dwd evo f4a f4b f4p f4v fla flac flr flv gif gifv gsf gsm gym iff \ +// ifo it jam la ly m1v m2p m2ts m2v m4a m4p m4v mcf mid mk3d mka mks mkv mng \ +// mov mp1 mp2 mp3 mp4 minipsf mod mpc mpe mpeg mpg mpv mscz mt2 mus mxf mxl \ +// niff nsf nsv off ofr ofs ogg ogv opus ots pac ps psf psf2 psflib ptb qsf \ +// qt ra raw rka rm rmj rmvb roq s3m shn sib sid smi smp sol spc spx ssf svi \ +// swa swf tak ts tta txm usf vgm vob voc vox vqf wav webm wma wmv wrap wtv \ +// wv xm xml ym yuv").split(' '); +// return base::flat_set(list.begin(), list.end()); +// }(); + +// return ranges::binary_search( +// kExtensions, +// FileExtension(filepath).toLower()); +// } + +bool IsExecutableName(const QString &filepath) { + static const auto kExtensions = [] { + const auto joined = +#ifdef Q_OS_MAC + qsl("\ +applescript action app bin command csh osx workflow terminal url caction \ +mpkg pkg scpt scptd xhtm webarchive"); +#elif defined Q_OS_UNIX // Q_OS_MAC + qsl("bin csh deb desktop ksh out pet pkg pup rpm run sh shar \ +slp zsh"); +#else // Q_OS_MAC || Q_OS_UNIX + qsl("\ +ad ade adp app application appref-ms asp asx bas bat bin cab cdxml cer cfg \ +chi chm cmd cnt com cpl crt csh der diagcab dll drv eml exe fon fxp gadget \ +grp hlp hpj hta htt inf ini ins inx isp isu its jar jnlp job js jse key ksh \ +lnk local lua mad maf mag mam manifest maq mar mas mat mau mav maw mcf mda \ +mdb mde mdt mdw mdz mht mhtml mjs mmc mof msc msg msh msh1 msh2 msh1xml \ +msh2xml mshxml msi msp mst ops osd paf pcd phar php php3 php4 php5 php7 phps \ +php-s pht phtml pif pl plg pm pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 \ +psd1 psm1 pssc pst py py3 pyc pyd pyi pyo pyw pywz pyz rb reg rgs scf scr \ +sct search-ms settingcontent-ms sh shb shs slk sys t tmp u3p url vb vbe vbp \ +vbs vbscript vdx vsmacros vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx \ +vtx website ws wsc wsf wsh xbap xll xnk xs"); +#endif // !Q_OS_MAC && !Q_OS_UNIX + const auto list = joined.split(' '); + return base::flat_set(list.begin(), list.end()); + }(); + + return ranges::binary_search( + kExtensions, + FileExtension(filepath).toLower()); +} + +bool IsIpRevealingName(const QString &filepath) { + static const auto kExtensions = [] { + const auto joined = u"htm html svg"_q; + const auto list = joined.split(' '); + return base::flat_set(list.begin(), list.end()); + }(); + static const auto kMimeTypes = [] { + const auto joined = u"text/html image/svg+xml"_q; + const auto list = joined.split(' '); + return base::flat_set(list.begin(), list.end()); + }(); + + return ranges::binary_search( + kExtensions, + FileExtension(filepath).toLower() + ) || ranges::binary_search( + kMimeTypes, + QMimeDatabase().mimeTypeForFile(QFileInfo(filepath)).name() + ); +} + +base::binary_guard ReadImageAsync( + not_null media, + FnMut postprocess, + FnMut done) { + auto result = base::binary_guard(); + crl::async([ + bytes = media->bytes(), + path = media->owner()->filepath(), + postprocess = std::move(postprocess), + guard = result.make_guard(), + callback = std::move(done) + ]() mutable { + auto format = QByteArray(); + if (bytes.isEmpty()) { + QFile f(path); + if (f.size() <= App::kImageSizeLimit + && f.open(QIODevice::ReadOnly)) { + bytes = f.readAll(); + } + } + auto image = bytes.isEmpty() + ? QImage() + : App::readImage(bytes, &format, false, nullptr); + if (postprocess) { + image = postprocess(std::move(image)); + } + crl::on_main(std::move(guard), [ + image = std::move(image), + callback = std::move(callback) + ]() mutable { + callback(std::move(image)); + }); + }); + return result; +} + +void ResolveDocument( + Window::SessionController *controller, + not_null document, + HistoryItem *item) { + if (!document->date) { + return; + } + const auto msgId = item ? item->fullId() : FullMsgId(); + + const auto showDocument = [&] { + if (cUseExternalVideoPlayer() + && document->isVideoFile() + && !document->filepath().isEmpty()) { + File::Launch(document->location(false).fname); + } else if (controller) { + controller->openDocument(document, msgId, true); + } + }; + + const auto media = document->createMediaView(); + const auto openImageInApp = [&] { + if (document->size >= App::kImageSizeLimit) { + return false; + } + const auto &location = document->location(true); + if (!location.isEmpty() && location.accessEnable()) { + const auto guard = gsl::finally([&] { + location.accessDisable(); + }); + const auto path = location.name(); + if (Core::MimeTypeForFile(path).name().startsWith("image/") + && QImageReader(path).canRead()) { + showDocument(); + return true; + } + } else if (document->mimeString().startsWith("image/") + && !media->bytes().isEmpty()) { + auto bytes = media->bytes(); + auto buffer = QBuffer(&bytes); + if (QImageReader(&buffer).canRead()) { + showDocument(); + return true; + } + } + return false; + }; + const auto &location = document->location(true); + if (document->isTheme() && media->loaded(true)) { + showDocument(); + location.accessDisable(); + } else if (media->canBePlayed()) { + if (document->isAudioFile() + || document->isVoiceMessage() + || document->isVideoMessage()) { + ::Media::Player::instance()->playPause({ document, msgId }); + /* + } else if (item + && document->isAnimation() + && HistoryView::Gif::CanPlayInline(document)) { + document->owner().requestAnimationPlayInline(item); + */ + } else { + showDocument(); + } + } else { + document->saveFromDataSilent(); + if (!openImageInApp()) { + if (!document->filepath(true).isEmpty()) { + LaunchWithWarning(location.name(), item); + } else if (document->status == FileReady + || document->status == FileDownloadFailed) { + DocumentSaveClickHandler::Save( + item ? item->fullId() : Data::FileOrigin(), + document); + } + } + } +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_document_resolver.h b/Telegram/SourceFiles/data/data_document_resolver.h new file mode 100644 index 000000000..b1cf745a4 --- /dev/null +++ b/Telegram/SourceFiles/data/data_document_resolver.h @@ -0,0 +1,37 @@ +/* +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/binary_guard.h" + +class DocumentData; +class HistoryItem; + +namespace Window { +class SessionController; +} // namespace Window + +namespace Data { + +class DocumentMedia; + +[[nodiscard]] QString FileExtension(const QString &filepath); +// [[nodiscard]] bool IsValidMediaFile(const QString &filepath); +[[nodiscard]] bool IsExecutableName(const QString &filepath); +[[nodiscard]] bool IsIpRevealingName(const QString &filepath); +base::binary_guard ReadImageAsync( + not_null media, + FnMut postprocess, + FnMut done); + +void ResolveDocument( + Window::SessionController *controller, + not_null document, + HistoryItem *item); + +} // namespace Data diff --git a/Telegram/SourceFiles/data/data_file_click_handler.cpp b/Telegram/SourceFiles/data/data_file_click_handler.cpp new file mode 100644 index 000000000..e4a9ad934 --- /dev/null +++ b/Telegram/SourceFiles/data/data_file_click_handler.cpp @@ -0,0 +1,199 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_file_click_handler.h" + +#include "core/file_utilities.h" +#include "data/data_document.h" +#include "data/data_photo.h" + +FileClickHandler::FileClickHandler(FullMsgId context) +: _context(context) { +} + +void FileClickHandler::setMessageId(FullMsgId context) { + _context = context; +} + +FullMsgId FileClickHandler::context() const { + return _context; +} + +not_null DocumentClickHandler::document() const { + return _document; +} + +DocumentWrappedClickHandler::DocumentWrappedClickHandler( + ClickHandlerPtr wrapped, + not_null document, + FullMsgId context) +: DocumentClickHandler(document, context) +, _wrapped(wrapped) { +} + +void DocumentWrappedClickHandler::onClickImpl() const { + _wrapped->onClick({ Qt::LeftButton }); +} + +DocumentClickHandler::DocumentClickHandler( + not_null document, + FullMsgId context) +: FileClickHandler(context) +, _document(document) { +} + +DocumentOpenClickHandler::DocumentOpenClickHandler( + not_null document, + Fn &&callback, + FullMsgId context) +: DocumentClickHandler(document, context) +, _handler(std::move(callback)) { + Expects(_handler != nullptr); +} + +void DocumentOpenClickHandler::onClickImpl() const { + _handler(context()); +} + +void DocumentSaveClickHandler::Save( + Data::FileOrigin origin, + not_null data, + Mode mode) { + if (!data->date) { + return; + } + + auto savename = QString(); + if (mode != Mode::ToCacheOrFile || !data->saveToCache()) { + if (mode != Mode::ToNewFile && data->saveFromData()) { + return; + } + const auto filepath = data->filepath(true); + const auto fileinfo = QFileInfo( + ); + const auto filedir = filepath.isEmpty() + ? QDir() + : fileinfo.dir(); + const auto filename = filepath.isEmpty() + ? QString() + : fileinfo.fileName(); + savename = DocumentFileNameForSave( + data, + (mode == Mode::ToNewFile), + filename, + filedir); + if (savename.isEmpty()) { + return; + } + } + data->save(origin, savename); +} + +void DocumentSaveClickHandler::onClickImpl() const { + Save(context(), document()); +} + +DocumentCancelClickHandler::DocumentCancelClickHandler( + not_null document, + Fn &&callback, + FullMsgId context) +: DocumentClickHandler(document, context) +, _handler(std::move(callback)) { +} + +void DocumentCancelClickHandler::onClickImpl() const { + const auto data = document(); + if (!data->date) { + return; + } else if (data->uploading() && _handler) { + _handler(context()); + } else { + data->cancel(); + } +} + +void DocumentOpenWithClickHandler::Open( + Data::FileOrigin origin, + not_null data) { + if (!data->date) { + return; + } + + data->saveFromDataSilent(); + const auto path = data->filepath(true); + if (!path.isEmpty()) { + File::OpenWith(path, QCursor::pos()); + } else { + DocumentSaveClickHandler::Save( + origin, + data, + DocumentSaveClickHandler::Mode::ToFile); + } +} + +void DocumentOpenWithClickHandler::onClickImpl() const { + Open(context(), document()); +} + +PhotoClickHandler::PhotoClickHandler( + not_null photo, + FullMsgId context, + PeerData *peer) +: FileClickHandler(context) +, _photo(photo) +, _peer(peer) { +} + +not_null PhotoClickHandler::photo() const { + return _photo; +} + +PeerData *PhotoClickHandler::peer() const { + return _peer; +} + +PhotoOpenClickHandler::PhotoOpenClickHandler( + not_null photo, + Fn &&callback, + FullMsgId context) +: PhotoClickHandler(photo, context) +, _handler(std::move(callback)) { + Expects(_handler != nullptr); +} + +void PhotoOpenClickHandler::onClickImpl() const { + _handler(context()); +} + +void PhotoSaveClickHandler::onClickImpl() const { + const auto data = photo(); + if (!data->date) { + return; + } else { + data->clearFailed(Data::PhotoSize::Large); + data->load(context()); + } +} + +PhotoCancelClickHandler::PhotoCancelClickHandler( + not_null photo, + Fn &&callback, + FullMsgId context) +: PhotoClickHandler(photo, context) +, _handler(std::move(callback)) { +} + +void PhotoCancelClickHandler::onClickImpl() const { + const auto data = photo(); + if (!data->date) { + return; + } else if (data->uploading() && _handler) { + _handler(context()); + } else { + data->cancel(); + } +} diff --git a/Telegram/SourceFiles/data/data_file_click_handler.h b/Telegram/SourceFiles/data/data_file_click_handler.h new file mode 100644 index 000000000..995307d1a --- /dev/null +++ b/Telegram/SourceFiles/data/data_file_click_handler.h @@ -0,0 +1,181 @@ +/* +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_file_origin.h" +#include "ui/basic_click_handlers.h" + +class DocumentData; +class HistoryItem; +class PhotoData; + +class FileClickHandler : public LeftButtonClickHandler { +public: + FileClickHandler(FullMsgId context); + + void setMessageId(FullMsgId context); + + [[nodiscard]] FullMsgId context() const; + +private: + FullMsgId _context; + +}; + +class DocumentClickHandler : public FileClickHandler { +public: + DocumentClickHandler( + not_null document, + FullMsgId context = FullMsgId()); + + [[nodiscard]] not_null document() const; + +private: + const not_null _document; + +}; + +class DocumentSaveClickHandler : public DocumentClickHandler { +public: + enum class Mode { + ToCacheOrFile, + ToFile, + ToNewFile, + }; + using DocumentClickHandler::DocumentClickHandler; + static void Save( + Data::FileOrigin origin, + not_null document, + Mode mode = Mode::ToCacheOrFile); + +protected: + void onClickImpl() const override; + +}; + +class DocumentOpenClickHandler : public DocumentClickHandler { +public: + DocumentOpenClickHandler( + not_null document, + Fn &&callback, + FullMsgId context = FullMsgId()); + +protected: + void onClickImpl() const override; + +private: + const Fn _handler; + +}; + +class DocumentCancelClickHandler : public DocumentClickHandler { +public: + DocumentCancelClickHandler( + not_null document, + Fn &&callback, + FullMsgId context = FullMsgId()); + +protected: + void onClickImpl() const override; + +private: + const Fn _handler; + +}; + +class DocumentOpenWithClickHandler : public DocumentClickHandler { +public: + using DocumentClickHandler::DocumentClickHandler; + static void Open( + Data::FileOrigin origin, + not_null document); + +protected: + void onClickImpl() const override; + +}; + +class VoiceSeekClickHandler : public DocumentOpenClickHandler { +public: + using DocumentOpenClickHandler::DocumentOpenClickHandler; + +protected: + void onClickImpl() const override { + } + +}; + +class DocumentWrappedClickHandler : public DocumentClickHandler { +public: + DocumentWrappedClickHandler( + ClickHandlerPtr wrapped, + not_null document, + FullMsgId context = FullMsgId()); + +protected: + void onClickImpl() const override; + +private: + ClickHandlerPtr _wrapped; + +}; + +class PhotoClickHandler : public FileClickHandler { +public: + PhotoClickHandler( + not_null photo, + FullMsgId context = FullMsgId(), + PeerData *peer = nullptr); + + [[nodiscard]] not_null photo() const; + [[nodiscard]] PeerData *peer() const; + +private: + const not_null _photo; + PeerData * const _peer = nullptr; + +}; + +class PhotoOpenClickHandler : public PhotoClickHandler { +public: + PhotoOpenClickHandler( + not_null photo, + Fn &&callback, + FullMsgId context = FullMsgId()); + +protected: + void onClickImpl() const override; + +private: + const Fn _handler; + +}; + +class PhotoSaveClickHandler : public PhotoClickHandler { +public: + using PhotoClickHandler::PhotoClickHandler; + +protected: + void onClickImpl() const override; + +}; + +class PhotoCancelClickHandler : public PhotoClickHandler { +public: + PhotoCancelClickHandler( + not_null photo, + Fn &&callback, + FullMsgId context = FullMsgId()); + +protected: + void onClickImpl() const override; + +private: + const Fn _handler; + +}; diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 1ec22213c..06d222de5 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -14,8 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "main/main_session.h" #include "calls/calls_instance.h" -#include "calls/calls_group_call.h" -#include "calls/calls_group_common.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_common.h" #include "core/application.h" #include "apiwrap.h" @@ -33,8 +33,29 @@ constexpr auto kWaitForUpdatesTimeout = 3 * crl::time(1000); }); } +[[nodiscard]] const std::string &EmptyEndpoint() { + static const auto result = std::string(); + return result; +} + } // namespace +const std::string &GroupCallParticipant::cameraEndpoint() const { + return GetCameraEndpoint(videoParams); +} + +const std::string &GroupCallParticipant::screenEndpoint() const { + return GetScreenEndpoint(videoParams); +} + +bool GroupCallParticipant::cameraPaused() const { + return IsCameraPaused(videoParams); +} + +bool GroupCallParticipant::screenPaused() const { + return IsScreenPaused(videoParams); +} + GroupCall::GroupCall( not_null peer, uint64 id, @@ -99,42 +120,50 @@ void GroupCall::requestParticipants() { : _nextOffset), MTP_int(kRequestPerPage) )).done([=](const MTPphone_GroupParticipants &result) { - _participantsRequestId = 0; - processSavedFullCall(); result.match([&](const MTPDphone_groupParticipants &data) { + _participantsRequestId = 0; + const auto reloaded = processSavedFullCall(); _nextOffset = qs(data.vnext_offset()); _peer->owner().processUsers(data.vusers()); _peer->owner().processChats(data.vchats()); applyParticipantsSlice( data.vparticipants().v, - ApplySliceSource::SliceLoaded); + (reloaded + ? ApplySliceSource::FullReloaded + : ApplySliceSource::SliceLoaded)); setServerParticipantsCount(data.vcount().v); if (data.vparticipants().v.isEmpty()) { _allParticipantsLoaded = true; } finishParticipantsSliceRequest(); + if (reloaded) { + _participantsReloaded.fire({}); + } }); }).fail([=](const MTP::Error &error) { _participantsRequestId = 0; - processSavedFullCall(); + const auto reloaded = processSavedFullCall(); setServerParticipantsCount(_participants.size()); _allParticipantsLoaded = true; finishParticipantsSliceRequest(); + if (reloaded) { + _participantsReloaded.fire({}); + } }).send(); } -void GroupCall::processSavedFullCall() { +bool GroupCall::processSavedFullCall() { if (!_savedFull) { - return; + return false; } _reloadRequestId = 0; processFullCallFields(*base::take(_savedFull)); + return true; } void GroupCall::finishParticipantsSliceRequest() { computeParticipantsCount(); processQueuedUpdates(); - _participantsSliceAdded.fire({}); } void GroupCall::setServerParticipantsCount(int count) { @@ -186,13 +215,40 @@ bool GroupCall::participantsLoaded() const { return _allParticipantsLoaded; } -PeerData *GroupCall::participantPeerBySsrc(uint32 ssrc) const { - const auto i = _participantPeerBySsrc.find(ssrc); - return (i != end(_participantPeerBySsrc)) ? i->second.get() : nullptr; +PeerData *GroupCall::participantPeerByAudioSsrc(uint32 ssrc) const { + const auto i = _participantPeerByAudioSsrc.find(ssrc); + return (i != end(_participantPeerByAudioSsrc)) + ? i->second.get() + : nullptr; } -rpl::producer<> GroupCall::participantsSliceAdded() { - return _participantsSliceAdded.events(); +const GroupCallParticipant *GroupCall::participantByPeer( + not_null peer) const { + return const_cast(this)->findParticipant(peer); +} + +GroupCallParticipant *GroupCall::findParticipant( + not_null peer) { + const auto i = ranges::find(_participants, peer, &Participant::peer); + return (i != end(_participants)) ? &*i : nullptr; +} + +const GroupCallParticipant *GroupCall::participantByEndpoint( + const std::string &endpoint) const { + if (endpoint.empty()) { + return nullptr; + } + for (const auto &participant : _participants) { + if (GetCameraEndpoint(participant.videoParams) == endpoint + || GetScreenEndpoint(participant.videoParams) == endpoint) { + return &participant; + } + } + return nullptr; +} + +rpl::producer<> GroupCall::participantsReloaded() { + return _participantsReloaded.events(); } auto GroupCall::participantUpdated() const @@ -200,6 +256,11 @@ auto GroupCall::participantUpdated() const return _participantUpdates.events(); } +auto GroupCall::participantSpeaking() const +-> rpl::producer> { + return _participantSpeaking.events(); +} + void GroupCall::enqueueUpdate(const MTPUpdate &update) { update.match([&](const MTPDupdateGroupCall &updateData) { updateData.vcall().match([&](const MTPDgroupCall &data) { @@ -295,12 +356,12 @@ void GroupCall::processFullCallFields(const MTPphone_GroupCall &call) { data.vcall().match([&](const MTPDgroupCall &data) { _participants.clear(); _speakingByActiveFinishes.clear(); - _participantPeerBySsrc.clear(); + _participantPeerByAudioSsrc.clear(); _allParticipantsLoaded = false; applyParticipantsSlice( participants, - ApplySliceSource::SliceLoaded); + ApplySliceSource::FullReloaded); _nextOffset = nextOffset; applyCallFields(data); @@ -314,6 +375,7 @@ void GroupCall::processFullCall(const MTPphone_GroupCall &call) { processFullCallUsersChats(call); processFullCallFields(call); finishParticipantsSliceRequest(); + _participantsReloaded.fire({}); } void GroupCall::applyCallFields(const MTPDgroupCall &data) { @@ -335,6 +397,7 @@ void GroupCall::applyCallFields(const MTPDgroupCall &data) { _recordStartDate = data.vrecord_start_date().value_or_empty(); _scheduleDate = data.vschedule_date().value_or_empty(); _scheduleStartSubscribed = data.is_schedule_start_subscribed(); + _canStartVideo = data.is_can_start_video(); _allParticipantsLoaded = (_serverParticipantsCount == _participants.size()); } @@ -343,7 +406,7 @@ void GroupCall::applyLocalUpdate( const MTPDupdateGroupCallParticipants &update) { applyParticipantsSlice( update.vparticipants().v, - ApplySliceSource::UpdateReceived); + ApplySliceSource::UpdateConstructed); } void GroupCall::applyEnqueuedUpdate(const MTPUpdate &update) { @@ -488,10 +551,10 @@ void GroupCall::applyParticipantsSlice( auto update = ParticipantUpdate{ .was = *i, }; - _participantPeerBySsrc.erase(i->ssrc); + _participantPeerByAudioSsrc.erase(i->ssrc); _speakingByActiveFinishes.remove(participantPeer); _participants.erase(i); - if (sliceSource != ApplySliceSource::SliceLoaded) { + if (sliceSource != ApplySliceSource::FullReloaded) { _participantUpdates.fire(std::move(update)); } } @@ -527,10 +590,23 @@ void GroupCall::applyParticipantsSlice( : data.is_muted_by_you(); const auto onlyMinLoaded = data.is_min() && (!was || was->onlyMinLoaded); + const auto videoJoined = data.is_video_joined(); const auto raisedHandRating = data.vraise_hand_rating().value_or_empty(); + const auto localUpdate = (sliceSource + == ApplySliceSource::UpdateConstructed); + const auto existingVideoParams = (i != end(_participants)) + ? i->videoParams + : nullptr; + auto videoParams = localUpdate + ? existingVideoParams + : Calls::ParseVideoParams( + data.vvideo(), + data.vpresentation(), + existingVideoParams); const auto value = Participant{ .peer = participantPeer, + .videoParams = std::move(videoParams), .date = data.vdate().v, .lastActive = lastActive, .raisedHandRating = raisedHandRating, @@ -542,17 +618,20 @@ void GroupCall::applyParticipantsSlice( .mutedByMe = mutedByMe, .canSelfUnmute = canSelfUnmute, .onlyMinLoaded = onlyMinLoaded, + .videoJoined = videoJoined, }; if (i == end(_participants)) { - _participantPeerBySsrc.emplace(value.ssrc, participantPeer); + _participantPeerByAudioSsrc.emplace( + value.ssrc, + participantPeer); _participants.push_back(value); if (const auto user = participantPeer->asUser()) { _peer->owner().unregisterInvitedToCallUser(_id, user); } } else { if (i->ssrc != value.ssrc) { - _participantPeerBySsrc.erase(i->ssrc); - _participantPeerBySsrc.emplace( + _participantPeerByAudioSsrc.erase(i->ssrc); + _participantPeerByAudioSsrc.emplace( value.ssrc, participantPeer); } @@ -561,7 +640,7 @@ void GroupCall::applyParticipantsSlice( if (data.is_just_joined()) { ++_serverParticipantsCount; } - if (sliceSource != ApplySliceSource::SliceLoaded) { + if (sliceSource != ApplySliceSource::FullReloaded) { _participantUpdates.fire({ .was = was, .now = value, @@ -579,30 +658,31 @@ void GroupCall::applyLastSpoke( uint32 ssrc, LastSpokeTimes when, crl::time now) { - const auto i = _participantPeerBySsrc.find(ssrc); - if (i == end(_participantPeerBySsrc)) { + const auto i = _participantPeerByAudioSsrc.find(ssrc); + if (i == end(_participantPeerByAudioSsrc)) { _unknownSpokenSsrcs[ssrc] = when; requestUnknownParticipants(); return; } - const auto j = ranges::find( - _participants, - i->second, - &Participant::peer); - Assert(j != end(_participants)); + const auto participant = findParticipant(i->second); + Assert(participant != nullptr); - _speakingByActiveFinishes.remove(j->peer); + _speakingByActiveFinishes.remove(participant->peer); const auto sounding = (when.anything + kSoundStatusKeptFor >= now) - && j->canSelfUnmute; + && participant->canSelfUnmute; const auto speaking = sounding && (when.voice + kSoundStatusKeptFor >= now); - if (j->sounding != sounding || j->speaking != speaking) { - const auto was = *j; - j->sounding = sounding; - j->speaking = speaking; + if (speaking) { + _participantSpeaking.fire({ participant }); + } + if (participant->sounding != sounding + || participant->speaking != speaking) { + const auto was = *participant; + participant->sounding = sounding; + participant->speaking = speaking; _participantUpdates.fire({ .was = was, - .now = *j, + .now = *participant, }); } } @@ -624,41 +704,37 @@ void GroupCall::applyActiveUpdate( if (inCall()) { return; } - const auto i = participantPeerLoaded - ? ranges::find( - _participants, - not_null{ participantPeerLoaded }, - &Participant::peer) - : _participants.end(); - const auto notFound = (i == end(_participants)); - const auto loadByUserId = notFound || i->onlyMinLoaded; + const auto participant = participantPeerLoaded + ? findParticipant(participantPeerLoaded) + : nullptr; + const auto loadByUserId = !participant || participant->onlyMinLoaded; if (loadByUserId) { _unknownSpokenPeerIds[participantPeerId] = when; requestUnknownParticipants(); } - if (notFound || !i->canSelfUnmute) { + if (!participant || !participant->canSelfUnmute) { return; } - const auto was = std::make_optional(*i); + const auto was = std::make_optional(*participant); const auto now = crl::now(); const auto elapsed = TimeId((now - when.anything) / crl::time(1000)); const auto lastActive = base::unixtime::now() - elapsed; const auto finishes = when.anything + kSpeakingAfterActive; - if (lastActive <= i->lastActive || finishes <= now) { + if (lastActive <= participant->lastActive || finishes <= now) { return; } - _speakingByActiveFinishes[i->peer] = finishes; + _speakingByActiveFinishes[participant->peer] = finishes; if (!_speakingByActiveFinishTimer.isActive()) { _speakingByActiveFinishTimer.callOnce(finishes - now); } - i->lastActive = lastActive; - i->speaking = true; - i->canSelfUnmute = true; + participant->lastActive = lastActive; + participant->speaking = true; + participant->canSelfUnmute = true; if (!was->speaking || !was->canSelfUnmute) { _participantUpdates.fire({ .was = was, - .now = *i, + .now = *participant, }); } } @@ -681,16 +757,14 @@ void GroupCall::checkFinishSpeakingByActive() { } } for (const auto participantPeer : stop) { - const auto i = ranges::find( - _participants, - participantPeer, - &Participant::peer); - if (i->speaking) { - const auto was = *i; - i->speaking = false; + const auto participant = findParticipant(participantPeer); + Assert(participant != nullptr); + if (participant->speaking) { + const auto was = *participant; + participant->speaking = false; _participantUpdates.fire({ .was = was, - .now = *i, + .now = *participant, }); } } @@ -788,6 +862,9 @@ void GroupCall::requestUnknownParticipants() { } _unknownSpokenPeerIds.remove(id); } + if (!ssrcs.empty()) { + _participantsResolved.fire(&ssrcs); + } requestUnknownParticipants(); }).fail([=](const MTP::Error &error) { _unknownParticipantPeersRequestId = 0; diff --git a/Telegram/SourceFiles/data/data_group_call.h b/Telegram/SourceFiles/data/data_group_call.h index 078f5634e..9b38e05b0 100644 --- a/Telegram/SourceFiles/data/data_group_call.h +++ b/Telegram/SourceFiles/data/data_group_call.h @@ -13,6 +13,10 @@ class PeerData; class ApiWrap; +namespace Calls { +struct ParticipantVideoParams; +} // namespace Calls + namespace Data { struct LastSpokeTimes { @@ -22,6 +26,7 @@ struct LastSpokeTimes { struct GroupCallParticipant { not_null peer; + std::shared_ptr videoParams; TimeId date = 0; TimeId lastActive = 0; uint64 raisedHandRating = 0; @@ -34,6 +39,12 @@ struct GroupCallParticipant { bool mutedByMe = false; bool canSelfUnmute = false; bool onlyMinLoaded = false; + bool videoJoined = false; + + [[nodiscard]] const std::string &cameraEndpoint() const; + [[nodiscard]] const std::string &screenEndpoint() const; + [[nodiscard]] bool cameraPaused() const; + [[nodiscard]] bool screenPaused() const; }; class GroupCall final { @@ -82,6 +93,12 @@ public: [[nodiscard]] rpl::producer scheduleStartSubscribedValue() const { return _scheduleStartSubscribed.value(); } + [[nodiscard]] bool canStartVideo() const { + return _canStartVideo.current(); + } + [[nodiscard]] rpl::producer canStartVideoValue() const { + return _canStartVideo.value(); + } void setPeer(not_null peer); @@ -91,16 +108,23 @@ public: std::optional now; }; - static constexpr auto kSoundStatusKeptFor = crl::time(350); + static constexpr auto kSoundStatusKeptFor = crl::time(1500); [[nodiscard]] auto participants() const -> const std::vector &; void requestParticipants(); [[nodiscard]] bool participantsLoaded() const; - [[nodiscard]] PeerData *participantPeerBySsrc(uint32 ssrc) const; + [[nodiscard]] PeerData *participantPeerByAudioSsrc(uint32 ssrc) const; + [[nodiscard]] const Participant *participantByPeer( + not_null peer) const; + [[nodiscard]] const Participant *participantByEndpoint( + const std::string &endpoint) const; - [[nodiscard]] rpl::producer<> participantsSliceAdded(); - [[nodiscard]] rpl::producer participantUpdated() const; + [[nodiscard]] rpl::producer<> participantsReloaded(); + [[nodiscard]] auto participantUpdated() const + -> rpl::producer; + [[nodiscard]] auto participantSpeaking() const + -> rpl::producer>; void enqueueUpdate(const MTPUpdate &update); void applyLocalUpdate( @@ -113,6 +137,12 @@ public: PeerData *participantPeerLoaded); void resolveParticipants(const base::flat_set &ssrcs); + [[nodiscard]] rpl::producer< + not_null*>> participantsResolved() const { + return _participantsResolved.events(); + } [[nodiscard]] int fullCount() const; [[nodiscard]] rpl::producer fullCountValue() const; @@ -128,9 +158,11 @@ public: private: enum class ApplySliceSource { + FullReloaded, SliceLoaded, UnknownLoaded, UpdateReceived, + UpdateConstructed, }; enum class QueuedType : uint8 { VersionedParticipant, @@ -156,8 +188,9 @@ private: void processFullCallFields(const MTPphone_GroupCall &call); [[nodiscard]] bool requestParticipantsAfterReload( const MTPphone_GroupCall &call) const; - void processSavedFullCall(); + [[nodiscard]] bool processSavedFullCall(); void finishParticipantsSliceRequest(); + [[nodiscard]] Participant *findParticipant(not_null peer); const uint64 _id = 0; const uint64 _accessHash = 0; @@ -175,7 +208,7 @@ private: std::optional _savedFull; std::vector _participants; - base::flat_map> _participantPeerBySsrc; + base::flat_map> _participantPeerByAudioSsrc; base::flat_map, crl::time> _speakingByActiveFinishes; base::Timer _speakingByActiveFinishTimer; QString _nextOffset; @@ -184,13 +217,19 @@ private: rpl::variable _recordStartDate = 0; rpl::variable _scheduleDate = 0; rpl::variable _scheduleStartSubscribed = false; + rpl::variable _canStartVideo = false; base::flat_map _unknownSpokenSsrcs; base::flat_map _unknownSpokenPeerIds; + rpl::event_stream< + not_null*>> _participantsResolved; mtpRequestId _unknownParticipantPeersRequestId = 0; rpl::event_stream _participantUpdates; - rpl::event_stream<> _participantsSliceAdded; + rpl::event_stream> _participantSpeaking; + rpl::event_stream<> _participantsReloaded; bool _joinMuted = false; bool _canChangeJoinMuted = true; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index ce9dadcaf..b99734ad5 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -26,6 +26,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 "ui/image/image.h" +#include "ui/text/format_song_document_name.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" @@ -510,6 +511,7 @@ QString MediaFile::chatListText() const { return Media::chatListText(); } const auto type = [&] { + using namespace Ui::Text; if (_document->isVideoMessage()) { return tr::lng_in_dlg_video_message(tr::now); } else if (_document->isAnimation()) { @@ -518,7 +520,7 @@ QString MediaFile::chatListText() const { return tr::lng_in_dlg_video(tr::now); } else if (_document->isVoiceMessage()) { return tr::lng_in_dlg_audio(tr::now); - } else if (const auto name = _document->composeNameString(); + } else if (const auto name = FormatSongNameFor(_document).string(); !name.isEmpty()) { return name; } else if (_document->isAudioFile()) { @@ -580,7 +582,7 @@ QString MediaFile::pinnedTextSubstring() const { TextForMimeData MediaFile::clipboardText() const { const auto attachType = [&] { - const auto name = _document->composeNameString(); + const auto name = Ui::Text::FormatSongNameFor(_document).string(); const auto addName = !name.isEmpty() ? qstr(" : ") + name : QString(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 7a9fe2256..569982776 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -312,6 +312,8 @@ public: [[nodiscard]] ImageLocation userpicLocation() const { return _userpic.location(); } + + static constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL); [[nodiscard]] bool userpicPhotoUnknown() const { return (_userpicPhotoId == kUnknownPhotoId); } @@ -423,8 +425,6 @@ private: void setUserpicChecked(PhotoId photoId, const ImageLocation &location); - static constexpr auto kUnknownPhotoId = PhotoId(0xFFFFFFFFFFFFFFFFULL); - const not_null _owner; mutable Data::CloudImage _userpic; diff --git a/Telegram/SourceFiles/data/data_photo.cpp b/Telegram/SourceFiles/data/data_photo.cpp index c382511f8..f93d6fb67 100644 --- a/Telegram/SourceFiles/data/data_photo.cpp +++ b/Telegram/SourceFiles/data/data_photo.cpp @@ -468,43 +468,3 @@ auto PhotoData::createStreamingLoader( origin) : nullptr; } - -PhotoClickHandler::PhotoClickHandler( - not_null photo, - FullMsgId context, - PeerData *peer) -: FileClickHandler(&photo->session(), context) -, _photo(photo) -, _peer(peer) { -} - -void PhotoOpenClickHandler::onClickImpl() const { - Core::App().showPhoto(this); -} - -void PhotoSaveClickHandler::onClickImpl() const { - const auto data = photo(); - if (!data->date) { - return; - } else { - data->clearFailed(PhotoSize::Large); - data->load(context()); - } -} - -void PhotoCancelClickHandler::onClickImpl() const { - const auto data = photo(); - if (!data->date) { - return; - } else if (data->uploading()) { - if (const auto item = data->owner().message(context())) { - if (const auto m = App::main()) { // multi good - if (&m->session() == &data->session()) { - m->cancelUploadLayer(item); - } - } - } - } else { - data->cancel(); - } -} diff --git a/Telegram/SourceFiles/data/data_photo.h b/Telegram/SourceFiles/data/data_photo.h index bcd3f9ccf..4922b28d3 100644 --- a/Telegram/SourceFiles/data/data_photo.h +++ b/Telegram/SourceFiles/data/data_photo.h @@ -175,50 +175,3 @@ private: not_null _owner; }; - -class PhotoClickHandler : public FileClickHandler { -public: - PhotoClickHandler( - not_null photo, - FullMsgId context = FullMsgId(), - PeerData *peer = nullptr); - - [[nodiscard]] not_null photo() const { - return _photo; - } - [[nodiscard]] PeerData *peer() const { - return _peer; - } - -private: - const not_null _photo; - PeerData * const _peer = nullptr; - -}; - -class PhotoOpenClickHandler : public PhotoClickHandler { -public: - using PhotoClickHandler::PhotoClickHandler; - -protected: - void onClickImpl() const override; - -}; - -class PhotoSaveClickHandler : public PhotoClickHandler { -public: - using PhotoClickHandler::PhotoClickHandler; - -protected: - void onClickImpl() const override; - -}; - -class PhotoCancelClickHandler : public PhotoClickHandler { -public: - using PhotoClickHandler::PhotoClickHandler; - -protected: - void onClickImpl() const override; - -}; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 1bdd340cf..f8cdf59b0 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -834,7 +834,7 @@ void Session::registerInvitedToCallUser( const auto inCall = ranges::contains( call->participants(), user, - &Data::GroupCall::Participant::peer); + &Data::GroupCallParticipant::peer); if (inCall) { return; } diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 644caebe6..6f3a86532 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -133,10 +133,6 @@ void MessageCursor::applyTo(not_null field) { field->scrollTo(scroll); } -HistoryItem *FileClickHandler::getActionItem() const { - return _session->data().message(context()); -} - PeerId PeerFromMessage(const MTPmessage &message) { return message.match([](const MTPDmessageEmpty &) { return PeerId(0); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 2009e1a6f..b3cd44bf2 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -193,16 +193,6 @@ struct GameData; struct PollData; class AudioMsgId; -class PhotoClickHandler; -class PhotoOpenClickHandler; -class PhotoSaveClickHandler; -class PhotoCancelClickHandler; -class DocumentClickHandler; -class DocumentSaveClickHandler; -class DocumentOpenClickHandler; -class DocumentCancelClickHandler; -class DocumentWrappedClickHandler; -class VoiceSeekClickHandler; using PhotoId = uint64; using VideoId = uint64; @@ -361,33 +351,3 @@ inline bool operator!=( const MessageCursor &b) { return !(a == b); } - -class FileClickHandler : public LeftButtonClickHandler { -public: - FileClickHandler( - not_null session, - FullMsgId context) - : _session(session) - , _context(context) { - } - - [[nodiscard]] Main::Session &session() const { - return *_session; - } - - void setMessageId(FullMsgId context) { - _context = context; - } - - [[nodiscard]] FullMsgId context() const { - return _context; - } - -protected: - HistoryItem *getActionItem() const; - -private: - const not_null _session; - FullMsgId _context; - -}; diff --git a/Telegram/SourceFiles/data/data_wall_paper.cpp b/Telegram/SourceFiles/data/data_wall_paper.cpp index e5d228fc8..49391bf7a 100644 --- a/Telegram/SourceFiles/data/data_wall_paper.cpp +++ b/Telegram/SourceFiles/data/data_wall_paper.cpp @@ -216,6 +216,8 @@ MTPWallPaperSettings WallPaper::mtpSettings() const { ? MTP_int(SerializeMaybeColor(_backgroundColor)) : MTP_int(0)), MTP_int(0), // second_background_color + MTP_int(0), // third_background_color + MTP_int(0), // fourth_background_color MTP_int(_intensity), MTP_int(0) // rotation ); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 8c4f13fe4..db603b46a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -51,10 +51,6 @@ dialogsSkip: 8px; dialogsWidthDuration: 120; dialogsTextWidthMin: 150px; -dialogsScroll: ScrollArea(defaultScrollArea) { - topsh: 0px; - bottomsh: 0px; -} dialogsTextPalette: TextPalette(defaultTextPalette) { linkFg: dialogsTextFgService; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index c8dd1e1f3..f7462d611 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -134,9 +134,6 @@ InnerWidget::InnerWidget( _cancelSearchInChat->setClickedCallback([=] { cancelSearchInChat(); }); _cancelSearchInChat->hide(); - _cancelSearchFromUser->setClickedCallback([=] { - searchFromUserChanged.notify(nullptr); - }); _cancelSearchFromUser->hide(); session().downloaderTaskFinished( @@ -144,13 +141,13 @@ InnerWidget::InnerWidget( update(); }, lifetime()); - subscribe(Core::App().notifications().settingsChanged(), [=]( - Window::Notifications::ChangeType change) { + Core::App().notifications().settingsChanged( + ) | rpl::start_with_next([=](Window::Notifications::ChangeType change) { if (change == Window::Notifications::ChangeType::CountMessages) { // Folder rows change their unread badge with this setting. update(); } - }); + }, lifetime()); session().data().contactsLoaded().changes( ) | rpl::start_with_next([=] { @@ -1988,6 +1985,10 @@ rpl::producer<> InnerWidget::listBottomReached() const { return _listBottomReached.events(); } +rpl::producer<> InnerWidget::cancelSearchFromUserRequests() const { + return _cancelSearchFromUser->clicks() | rpl::to_empty; +} + void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { @@ -2168,7 +2169,7 @@ void InnerWidget::peerSearchReceived( } else { LOG(("API Error: " "user %1 was not loaded in InnerWidget::peopleReceived()" - ).arg(peer->id.value)); + ).arg(peerFromMTP(mtpPeer).value)); } } for (const auto &mtpPeer : result) { @@ -2183,7 +2184,7 @@ void InnerWidget::peerSearchReceived( } else { LOG(("API Error: " "user %1 was not loaded in InnerWidget::peopleReceived()" - ).arg(peer->id.value)); + ).arg(peerFromMTP(mtpPeer).value)); } } refresh(); @@ -3222,7 +3223,7 @@ void InnerWidget::setupShortcuts() { }); request->check(Command::ShowContacts) && request->handle([=] { - Ui::show(PrepareContactsBox(_controller)); + _controller->show(PrepareContactsBox(_controller)); return true; }); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 1127035a9..c9e0fd0a3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -120,9 +120,7 @@ public: void setLoadMoreCallback(Fn callback); [[nodiscard]] rpl::producer<> listBottomReached() const; - - base::Observable searchFromUserChanged; - + [[nodiscard]] rpl::producer<> cancelSearchFromUserRequests() const; [[nodiscard]] rpl::producer chosenRow() const; [[nodiscard]] rpl::producer<> updated() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 5b8a6bf16..50b26a1a2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "mainwindow.h" #include "mainwidget.h" +#include "main/main_domain.h" #include "main/main_session.h" #include "main/main_account.h" #include "main/main_session_settings.h" @@ -29,12 +30,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/update_checker.h" #include "boxes/peer_list_box.h" #include "boxes/peers/edit_participants_box.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" #include "window/window_connecting_widget.h" #include "window/window_main_menu.h" #include "storage/storage_media_prepare.h" #include "storage/storage_account.h" +#include "storage/storage_domain.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -175,7 +178,7 @@ Widget::Widget( object_ptr(this, st::dialogsCalendar)) , _cancelSearch(_searchControls, st::dialogsCancelSearch) , _lockUnlock(_searchControls, st::dialogsLock) -, _scroll(this, st::dialogsScroll) +, _scroll(this) , _scrollToTop(_scroll, st::dialogsToUp) , _singleMessageSearch(&controller->session()) { _inner = _scroll->setOwnedWidget(object_ptr(this, controller)); @@ -208,10 +211,11 @@ Widget::Widget( connect(_inner, SIGNAL(completeHashtag(QString)), this, SLOT(onCompleteHashtag(QString))); connect(_inner, SIGNAL(refreshHashtags()), this, SLOT(onFilterCursorMoved())); connect(_inner, SIGNAL(cancelSearchInChat()), this, SLOT(onCancelSearchInChat())); - subscribe(_inner->searchFromUserChanged, [this](PeerData *from) { - setSearchInChat(_searchInChat, from); + _inner->cancelSearchFromUserRequests( + ) | rpl::start_with_next([=] { + setSearchInChat(_searchInChat, nullptr); applyFilterUpdate(true); - }); + }, lifetime()); _inner->chosenRow( ) | rpl::start_with_next([=](const ChosenRow &row) { const auto openSearchResult = !controller->selectingPeer() @@ -263,13 +267,21 @@ Widget::Widget( }, lifetime()); } - subscribe(Adaptive::Changed(), [this] { updateForwardBar(); }); + controller->adaptive().changes( + ) | rpl::start_with_next([=] { + updateForwardBar(); + }, lifetime()); _cancelSearch->setClickedCallback([this] { onCancelSearch(); }); _jumpToDate->entity()->setClickedCallback([this] { showJumpToDate(); }); _chooseFromUser->entity()->setClickedCallback([this] { showSearchFrom(); }); - _lockUnlock->setVisible(Global::LocalPasscode()); - subscribe(Global::RefLocalPasscodeChanged(), [this] { updateLockUnlockVisibility(); }); + rpl::single( + rpl::empty_value() + ) | rpl::then( + session().domain().local().localPasscodeChanged() + ) | rpl::start_with_next([=] { + updateLockUnlockVisibility(); + }, lifetime()); _lockUnlock->setClickedCallback([this] { _lockUnlock->setIconOverride(&st::dialogsUnlockIcon, &st::dialogsUnlockIconOver); Core::App().lockByPasscode(); @@ -407,7 +419,7 @@ void Widget::setupConnectingWidget() { _connecting = std::make_unique( this, &session().account(), - Window::AdaptiveIsOneColumn()); + controller()->adaptive().oneColumnValue()); } void Widget::setupSupportMode() { @@ -1278,7 +1290,7 @@ void Widget::dragEnterEvent(QDragEnterEvent *e) { const auto data = e->mimeData(); _dragInScroll = false; - _dragForward = Adaptive::OneColumn() + _dragForward = controller()->adaptive().isOneColumn() ? false : data->hasFormat(qsl("application/x-td-forward")); if (_dragForward) { @@ -1341,6 +1353,7 @@ void Widget::dropEvent(QDropEvent *e) { controller()->content()->onFilesOrForwardDrop( peer->id, e->mimeData()); + controller()->widget()->raise(); controller()->widget()->activateWindow(); } } @@ -1509,7 +1522,7 @@ void Widget::updateLockUnlockVisibility() { if (_a_show.animating()) { return; } - const auto hidden = !Global::LocalPasscode(); + const auto hidden = !session().domain().local().hasLocalPasscode(); if (_lockUnlock->isHidden() != hidden) { _lockUnlock->setVisible(!hidden); updateControlsGeometry(); @@ -1567,7 +1580,7 @@ void Widget::updateControlsGeometry() { auto smallLayoutWidth = (st::dialogsPadding.x() + (DialogListLines() == 1 ? st::dialogsUnreadHeight : st::dialogsPhotoSize) + st::dialogsPadding.x()); auto smallLayoutRatio = (width() < st::columnMinimalWidthLeft) ? (st::columnMinimalWidthLeft - width()) / float64(st::columnMinimalWidthLeft - smallLayoutWidth) : 0.; auto filterLeft = (controller()->filtersWidth() ? st::dialogsFilterSkip : st::dialogsFilterPadding.x() + _mainMenuToggle->width()) + st::dialogsFilterPadding.x(); - auto filterRight = (Global::LocalPasscode() ? (st::dialogsFilterPadding.x() + _lockUnlock->width()) : st::dialogsFilterSkip) + st::dialogsFilterPadding.x(); + auto filterRight = (session().domain().local().hasLocalPasscode() ? (st::dialogsFilterPadding.x() + _lockUnlock->width()) : st::dialogsFilterSkip) + st::dialogsFilterPadding.x(); auto filterWidth = qMax(width(), st::columnMinimalWidthLeft) - filterLeft - filterRight; auto filterAreaHeight = st::topBarHeight; _searchControls->setGeometry(0, filterAreaTop, width(), filterAreaHeight); @@ -1624,16 +1637,21 @@ void Widget::updateControlsGeometry() { } } +rpl::producer<> Widget::closeForwardBarRequests() const { + return _closeForwardBarRequests.events(); +} + void Widget::updateForwardBar() { auto selecting = controller()->selectingPeer(); - auto oneColumnSelecting = (Adaptive::OneColumn() && selecting); + auto oneColumnSelecting = (controller()->adaptive().isOneColumn() + && selecting); if (!oneColumnSelecting == !_forwardCancel) { return; } if (oneColumnSelecting) { _forwardCancel.create(this, st::dialogsForwardCancel); - _forwardCancel->setClickedCallback([] { - Global::RefPeerChooseCancel().notify(true); + _forwardCancel->setClickedCallback([=] { + _closeForwardBarRequests.fire({}); }); if (!_a_show.animating()) _forwardCancel->show(); } else { @@ -1746,7 +1764,7 @@ bool Widget::onCancelSearch() { bool clearing = !_filter->getLastText().isEmpty(); cancelSearchRequest(); if (_searchInChat && !clearing) { - if (Adaptive::OneColumn()) { + if (controller()->adaptive().isOneColumn()) { if (const auto peer = _searchInChat.peer()) { Ui::showPeerHistory(peer, ShowAtUnreadMsgId); } else { @@ -1765,8 +1783,9 @@ bool Widget::onCancelSearch() { void Widget::onCancelSearchInChat() { cancelSearchRequest(); + const auto isOneColumn = controller()->adaptive().isOneColumn(); if (_searchInChat) { - if (Adaptive::OneColumn() + if (isOneColumn && !controller()->selectingPeer() && _filter->getLastText().trimmed().isEmpty()) { if (const auto peer = _searchInChat.peer()) { @@ -1778,7 +1797,7 @@ void Widget::onCancelSearchInChat() { setSearchInChat(Key()); } applyFilterUpdate(true); - if (!Adaptive::OneColumn() && !controller()->selectingPeer()) { + if (!isOneColumn && !controller()->selectingPeer()) { cancelled(); } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 2df492fce..1f90b375a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -85,6 +85,10 @@ public: void searchMessages(const QString &query, Key inChat = {}, UserData *from = nullptr); void onSearchMore(); + void updateForwardBar(); + + [[nodiscard]] rpl::producer<> closeForwardBarRequests() const; + // Float player interface. bool floatPlayerHandleWheelEvent(QEvent *e) override; QRect floatPlayerAvailableRect() override; @@ -155,7 +159,6 @@ private: void updateSearchFromVisibility(bool fast = false); void updateControlsGeometry(); void refreshFolderTopBar(); - void updateForwardBar(); void checkUpdateStatus(); void changeOpenedFolder(Data::Folder *folder, anim::type animated); QPixmap grabForFolderSlideAnimation(); @@ -244,6 +247,8 @@ private: int _topDelta = 0; + rpl::event_stream<> _closeForwardBarRequests; + }; } // namespace Dialogs diff --git a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp index 30c53872d..b61218f90 100644 --- a/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp +++ b/Telegram/SourceFiles/export/view/export_view_panel_controller.cpp @@ -153,11 +153,15 @@ PanelController::~PanelController() { if (_saveSettingsTimer.isActive()) { saveSettings(); } - _panel->destroyLayer(); + if (_panel) { + _panel->destroyLayer(); + } } void PanelController::activatePanel() { - _panel->showAndActivate(); + if (_panel) { + _panel->showAndActivate(); + } } void PanelController::createPanel() { diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index 631dadb1b..a9dd1cea7 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -270,10 +270,6 @@ void showChatsList(not_null session) { } } -void showPeerHistoryAtItem(not_null item) { - showPeerHistory(item->history()->peer, item->id); -} - void showPeerHistory(not_null history, MsgId msgId) { showPeerHistory(history->peer, msgId); } @@ -323,91 +319,3 @@ bool switchInlineBotButtonReceived( } } // namespace Notify - -#define DefineReadOnlyVar(Namespace, Type, Name) const Type &Name() { \ - AssertCustom(Namespace##Data != nullptr, #Namespace "Data != nullptr in " #Namespace "::" #Name); \ - return Namespace##Data->Name; \ -} -#define DefineRefVar(Namespace, Type, Name) DefineReadOnlyVar(Namespace, Type, Name) \ -Type &Ref##Name() { \ - AssertCustom(Namespace##Data != nullptr, #Namespace "Data != nullptr in " #Namespace "::Ref" #Name); \ - return Namespace##Data->Name; \ -} -#define DefineVar(Namespace, Type, Name) DefineRefVar(Namespace, Type, Name) \ -void Set##Name(const Type &Name) { \ - AssertCustom(Namespace##Data != nullptr, #Namespace "Data != nullptr in " #Namespace "::Set" #Name); \ - Namespace##Data->Name = Name; \ -} - -namespace Global { -namespace internal { - -struct Data { - bool ScreenIsLocked = false; - Adaptive::WindowLayout AdaptiveWindowLayout = Adaptive::WindowLayout::Normal; - Adaptive::ChatLayout AdaptiveChatLayout = Adaptive::ChatLayout::Normal; - base::Observable AdaptiveChanged; - - bool NotificationsDemoIsShown = false; - - bool TryIPv6 = !Platform::IsWindows(); - std::vector ProxiesList; - MTP::ProxyData SelectedProxy; - MTP::ProxyData::Settings ProxySettings = MTP::ProxyData::Settings::System; - bool UseProxyForCalls = false; - base::Observable ConnectionTypeChanged; - - bool LocalPasscode = false; - base::Observable LocalPasscodeChanged; - - base::Variable WorkMode = { dbiwmWindowAndTray }; - - base::Observable PeerChooseCancel; - - base::Observable ChatIDFormatChanged; -}; - -} // namespace internal -} // namespace Global - -Global::internal::Data *GlobalData = nullptr; - -namespace Global { - -bool started() { - return GlobalData != nullptr; -} - -void start() { - GlobalData = new internal::Data(); -} - -void finish() { - delete GlobalData; - GlobalData = nullptr; -} - -DefineVar(Global, bool, ScreenIsLocked); -DefineVar(Global, Adaptive::WindowLayout, AdaptiveWindowLayout); -DefineVar(Global, Adaptive::ChatLayout, AdaptiveChatLayout); -DefineRefVar(Global, base::Observable, AdaptiveChanged); - -DefineVar(Global, bool, NotificationsDemoIsShown); - -DefineVar(Global, bool, TryIPv6); -DefineVar(Global, std::vector, ProxiesList); -DefineVar(Global, MTP::ProxyData, SelectedProxy); -DefineVar(Global, MTP::ProxyData::Settings, ProxySettings); -DefineVar(Global, bool, UseProxyForCalls); -DefineRefVar(Global, base::Observable, ConnectionTypeChanged); - -DefineVar(Global, bool, LocalPasscode); -DefineRefVar(Global, base::Observable, LocalPasscodeChanged); - -DefineRefVar(Global, base::Variable, WorkMode); - -DefineRefVar(Global, base::Observable, PeerChooseCancel); - -DefineRefVar(Global, base::Observable, ChatIDFormatChanged); - -} // namespace Global diff --git a/Telegram/SourceFiles/facades.h b/Telegram/SourceFiles/facades.h index 6ad05c328..8f0f5d42a 100644 --- a/Telegram/SourceFiles/facades.h +++ b/Telegram/SourceFiles/facades.h @@ -58,7 +58,6 @@ namespace Ui { void showPeerProfile(not_null peer); void showPeerProfile(not_null history); -void showPeerHistoryAtItem(not_null item); void showPeerHistory(not_null peer, MsgId msgId); void showPeerHistory(not_null history, MsgId msgId); void showChatsList(not_null session); @@ -83,75 +82,3 @@ bool switchInlineBotButtonReceived( MsgId samePeerReplyTo = 0); } // namespace Notify - -#define DeclareReadOnlyVar(Type, Name) const Type &Name(); -#define DeclareRefVar(Type, Name) DeclareReadOnlyVar(Type, Name) \ - Type &Ref##Name(); -#define DeclareVar(Type, Name) DeclareRefVar(Type, Name) \ - void Set##Name(const Type &Name); - -namespace Adaptive { - -enum class WindowLayout { - OneColumn, - Normal, - ThreeColumn, -}; - -enum class ChatLayout { - Normal, - Wide, -}; - -} // namespace Adaptive - -namespace Global { - -bool started(); -void start(); -void finish(); - -DeclareVar(bool, ScreenIsLocked); -DeclareVar(Adaptive::ChatLayout, AdaptiveChatLayout); -DeclareVar(Adaptive::WindowLayout, AdaptiveWindowLayout); -DeclareRefVar(base::Observable, AdaptiveChanged); - -DeclareVar(bool, NotificationsDemoIsShown); - -DeclareVar(bool, TryIPv6); -DeclareVar(std::vector, ProxiesList); -DeclareVar(MTP::ProxyData, SelectedProxy); -DeclareVar(MTP::ProxyData::Settings, ProxySettings); -DeclareVar(bool, UseProxyForCalls); -DeclareRefVar(base::Observable, ConnectionTypeChanged); - -DeclareVar(bool, LocalPasscode); -DeclareRefVar(base::Observable, LocalPasscodeChanged); - -DeclareRefVar(base::Variable, WorkMode); - -DeclareRefVar(base::Observable, PeerChooseCancel); - -DeclareRefVar(base::Observable, ChatIDFormatChanged); - -} // namespace Global - -namespace Adaptive { - -inline base::Observable &Changed() { - return Global::RefAdaptiveChanged(); -} - -inline bool OneColumn() { - return Global::AdaptiveWindowLayout() == WindowLayout::OneColumn; -} - -inline bool Normal() { - return Global::AdaptiveWindowLayout() == WindowLayout::Normal; -} - -inline bool ThreeColumn() { - return Global::AdaptiveWindowLayout() == WindowLayout::ThreeColumn; -} - -} // namespace Adaptive diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h index 4be81913c..dd4675b25 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/bytes.h" +#include "base/algorithm.h" #include @@ -61,55 +62,42 @@ private: class Packet { public: - Packet() { - setEmpty(); - } - Packet(const AVPacket &data) { - bytes::copy(_data, bytes::object_as_span(&data)); - } - Packet(Packet &&other) { - bytes::copy(_data, other._data); - if (!other.empty()) { - other.release(); - } + Packet() = default; + Packet(Packet &&other) : _data(base::take(other._data)) { } Packet &operator=(Packet &&other) { if (this != &other) { - av_packet_unref(&fields()); - bytes::copy(_data, other._data); - if (!other.empty()) { - other.release(); - } + release(); + _data = base::take(other._data); } return *this; } ~Packet() { - av_packet_unref(&fields()); + release(); } [[nodiscard]] AVPacket &fields() { - return *reinterpret_cast(_data); + if (!_data) { + _data = av_packet_alloc(); + } + return *_data; } [[nodiscard]] const AVPacket &fields() const { - return *reinterpret_cast(_data); + if (!_data) { + _data = av_packet_alloc(); + } + return *_data; } [[nodiscard]] bool empty() const { - return !fields().data; + return !_data || !fields().data; } void release() { - setEmpty(); + av_packet_free(&_data); } private: - void setEmpty() { - auto &native = fields(); - av_init_packet(&native); - native.data = nullptr; - native.size = 0; - } - - alignas(alignof(AVPacket)) bytes::type _data[sizeof(AVPacket)]; + mutable AVPacket *_data = nullptr; }; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp index e754e01f8..7f1c688b9 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_filter.cpp @@ -282,8 +282,8 @@ void FilterBox::Inner::createActionsCheckboxes(const FilterValue &filter) { addFlag(Flag::f_edit, tr::lng_admin_log_filter_messages_edited(tr::now)); if (isGroup) { addFlag(Flag::f_pinned, tr::lng_admin_log_filter_messages_pinned(tr::now)); - addFlag(Flag::f_group_call, tr::lng_admin_log_filter_voice_chats(tr::now)); } + addFlag(Flag::f_group_call, tr::lng_admin_log_filter_voice_chats(tr::now)); addFlag(Flag::f_invites, tr::lng_admin_log_filter_invite_links(tr::now)); addFlag(Flag::f_leave, tr::lng_admin_log_filter_members_removed(tr::now)); } 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 66ed5d6e9..743a57675 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_document.h" #include "data/data_media_types.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_cloud_file.h" #include "data/data_channel.h" @@ -479,7 +480,8 @@ void InnerWidget::showFilter(Fn callback) { if (_admins.empty()) { _showFilterCallback = std::move(callback); } else { - Ui::show(Box(_channel, _admins, _filter, std::move(callback))); + _controller->show( + Box(_channel, _admins, _filter, std::move(callback))); } } @@ -602,6 +604,25 @@ void InnerWidget::elementShowPollResults( FullMsgId context) { } +void InnerWidget::elementOpenPhoto( + not_null photo, + FullMsgId context) { + _controller->openPhoto(photo, context); +} + +void InnerWidget::elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, context, showInMediaView); +} + +void InnerWidget::elementCancelUpload(const FullMsgId &context) { + if (const auto item = session().data().message(context)) { + _controller->cancelUploadLayer(item); + } +} + void InnerWidget::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { @@ -627,6 +648,10 @@ void InnerWidget::elementSendBotCommand( void InnerWidget::elementHandleViaClick(not_null bot) { } +bool InnerWidget::elementIsChatWide() { + return _controller->adaptive().isChatWide(); +} + void InnerWidget::saveState(not_null memento) { memento->setFilter(std::move(_filter)); memento->setAdmins(std::move(_admins)); @@ -942,14 +967,17 @@ void InnerWidget::paintEvent(QPaintEvent *e) { p.setOpacity(opacity); const auto dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top()); const auto width = view->width(); + const auto chatWide = + _controller->adaptive().isChatWide(); if (const auto date = view->Get()) { - date->paint(p, dateY, width); + date->paint(p, dateY, width, chatWide); } else { HistoryView::ServiceMessagePainter::paintDate( p, view->dateTime(), dateY, - width); + width, + chatWide); } } } @@ -1273,7 +1301,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()) { - Core::App().showDocument(document, item); + _controller->openDocument(document, itemId, true); } } } @@ -1311,7 +1339,8 @@ void InnerWidget::suggestRestrictUser(not_null user) { (*weakBox)->closeBox(); } }); - *weakBox = Ui::show( + *weakBox = QPointer(box.data()); + _controller->show( std::move(box), Ui::LayerOption::KeepOther); }; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index b3af9bb0b..85b24efff 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -108,6 +108,14 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementOpenPhoto( + not_null photo, + FullMsgId context) override; + void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false) override; + void elementCancelUpload(const FullMsgId &context) override; void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) override; @@ -120,6 +128,7 @@ public: const QString &command, const FullMsgId &context) override; void elementHandleViaClick(not_null bot) override; + bool elementIsChatWide() override; ~InnerWidget(); 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 a1f0c7503..56086db7b 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_section.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "apiwrap.h" #include "window/themes/window_theme.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "boxes/confirm_box.h" #include "base/timer.h" @@ -34,16 +35,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace AdminLog { -class FixedBar final : public TWidget, private base::Subscriber { +class FixedBar final : public TWidget { public: FixedBar( QWidget *parent, not_null controller, not_null channel); - base::Observable showFilterSignal; - base::Observable searchCancelledSignal; - base::Observable searchSignal; + [[nodiscard]] rpl::producer<> showFilterRequests() const; + [[nodiscard]] rpl::producer<> searchCancelRequests() const; + [[nodiscard]] rpl::producer searchRequests() const; // When animating mode is enabled the content is hidden and the // whole fixed bar acts like a back button. @@ -85,6 +86,9 @@ private: bool _animatingMode = false; base::Timer _searchTimer; + rpl::event_stream<> _searchCancelRequests; + rpl::event_stream _searchRequests; + }; object_ptr SectionMemento::createWidget( @@ -110,13 +114,13 @@ FixedBar::FixedBar( , _backButton( this, &controller->session(), - tr::lng_admin_log_title_all(tr::now)) + tr::lng_admin_log_title_all(tr::now), + controller->adaptive().oneColumnValue()) , _search(this, st::topBarSearch) , _cancel(this, st::historyAdminLogCancelSearch) , _filter(this, tr::lng_admin_log_filter(), st::topBarButton) { _backButton->moveToLeft(0, 0); _backButton->setClickedCallback([=] { goBack(); }); - _filter->setClickedCallback([=] { showFilterSignal.notify(); }); _search->setClickedCallback([=] { showSearch(); }); _cancel->setClickedCallback([=] { cancelSearch(); }); _field->hide(); @@ -158,7 +162,7 @@ void FixedBar::toggleSearch() { _field->show(); _field->setFocus(); } else { - searchCancelledSignal.notify(true); + _searchCancelRequests.fire({}); } } @@ -198,7 +202,7 @@ void FixedBar::searchUpdated() { } void FixedBar::applySearch() { - searchSignal.notify(_field->getLastText()); + _searchRequests.fire_copy(_field->getLastText()); } int FixedBar::resizeGetHeight(int newWidth) { @@ -223,6 +227,18 @@ int FixedBar::resizeGetHeight(int newWidth) { return newHeight; } +rpl::producer<> FixedBar::showFilterRequests() const { + return _filter->clicks() | rpl::to_empty; +} + +rpl::producer<> FixedBar::searchCancelRequests() const { + return _searchCancelRequests.events(); +} + +rpl::producer FixedBar::searchRequests() const { + return _searchRequests.events(); +} + void FixedBar::setAnimatingMode(bool enabled) { if (_animatingMode != enabled) { _animatingMode = enabled; @@ -266,14 +282,26 @@ Widget::Widget( , _whatIsThis(this, tr::lng_admin_log_about(tr::now).toUpper(), st::historyComposeButton) { _fixedBar->move(0, 0); _fixedBar->resizeToWidth(width()); - subscribe(_fixedBar->showFilterSignal, [this] { showFilter(); }); - subscribe(_fixedBar->searchCancelledSignal, [this] { setInnerFocus(); }); - subscribe(_fixedBar->searchSignal, [this](const QString &query) { _inner->applySearch(query); }); + _fixedBar->showFilterRequests( + ) | rpl::start_with_next([=] { + showFilter(); + }, lifetime()); + _fixedBar->searchCancelRequests( + ) | rpl::start_with_next([=] { + setInnerFocus(); + }, lifetime()); + _fixedBar->searchRequests( + ) | rpl::start_with_next([=](const QString &query) { + _inner->applySearch(query); + }, lifetime()); _fixedBar->show(); _fixedBarShadow->raise(); - updateAdaptiveLayout(); - subscribe(Adaptive::Changed(), [this] { updateAdaptiveLayout(); }); + + controller->adaptive().value( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); _inner = _scroll->setOwnedWidget(object_ptr(this, controller, channel)); _inner->showSearchSignal( @@ -295,7 +323,7 @@ Widget::Widget( connect(_scroll, &Ui::ScrollArea::scrolled, this, [this] { onScroll(); }); _whatIsThis->setClickedCallback([=] { - Ui::show(Box(channel->isMegagroup() + controller->show(Box(channel->isMegagroup() ? tr::lng_admin_log_about_text(tr::now) : tr::lng_admin_log_about_text_channel(tr::now))); }); @@ -311,7 +339,11 @@ void Widget::showFilter() { } void Widget::updateAdaptiveLayout() { - _fixedBarShadow->moveToLeft(Adaptive::OneColumn() ? 0 : st::lineWidth, _fixedBar->height()); + _fixedBarShadow->moveToLeft( + controller()->adaptive().isOneColumn() + ? 0 + : st::lineWidth, + _fixedBar->height()); } not_null Widget::channel() const { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index e852a7923..e5bfe4a7c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "ui/inactive_press.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "window/window_controller.h" @@ -60,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_user.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/data_changes.h" @@ -169,14 +171,13 @@ HistoryInner::HistoryInner( notifyIsBotChanged(); setMouseTracking(true); - subscribe(_controller->gifPauseLevelChanged(), [this] { - if (!_controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any)) { + _controller->gifPauseLevelChanged( + ) | rpl::start_with_next([=] { + if (!_controller->isGifPausedAtLeastFor( + Window::GifPauseReason::Any)) { update(); } - }); - subscribe(_controller->widget()->dragFinished(), [this] { - mouseActionUpdate(QCursor::pos()); - }); + }, lifetime()); session().data().itemRemoved( ) | rpl::start_with_next( [this](auto item) { itemRemoved(item); }, @@ -209,6 +210,11 @@ HistoryInner::HistoryInner( ) | rpl::start_with_next([=] { update(); }, lifetime()); + + controller->adaptive().chatWideValue( + ) | rpl::start_with_next([=](bool wide) { + _isChatWide = wide; + }, lifetime()); } Main::Session &HistoryInner::session() const { @@ -358,7 +364,7 @@ bool HistoryInner::canHaveFromUserpics() const { if (_peer->isUser() && !_peer->isSelf() && !_peer->isRepliesChat() - && !Core::App().settings().chatWide()) { + && !_isChatWide) { return false; } else if (_peer->isChannel() && !_peer->isMegagroup()) { return false; @@ -772,13 +778,14 @@ void HistoryInner::paintEvent(QPaintEvent *e) { ? itemtop : (dateTop - st::msgServiceMargin.top()); if (const auto date = view->Get()) { - date->paint(p, dateY, _contentWidth); + date->paint(p, dateY, _contentWidth, _isChatWide); } else { HistoryView::ServiceMessagePainter::paintDate( p, view->dateTime(), dateY, - _contentWidth); + _contentWidth, + _isChatWide); } } } @@ -1211,7 +1218,7 @@ std::unique_ptr HistoryInner::prepareDrag() { _widget->noSelectingScroll(); if (!urls.isEmpty()) mimeData->setUrls(urls); - if (uponSelected && !Adaptive::OneColumn()) { + if (uponSelected && !_controller->adaptive().isOneColumn()) { auto selectedState = getSelectionState(); if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) { session().data().setMimeForwardIds(getSelectedItems()); @@ -1259,7 +1266,9 @@ std::unique_ptr HistoryInner::prepareDrag() { void HistoryInner::performDrag() { if (auto mimeData = prepareDrag()) { // This call enters event loop and can destroy any QObject. - _controller->widget()->launchDrag(std::move(mimeData)); + _controller->widget()->launchDrag( + std::move(mimeData), + crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); })); } } @@ -1955,7 +1964,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()) { - Core::App().showDocument(document, item); + _controller->openDocument(document, itemId, true); } } } @@ -2167,7 +2176,7 @@ void HistoryInner::recountHistoryGeometry() { : (st::msgNameFont->height + st::botDescSkip); int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom(); int32 descMaxWidth = _scroll->width(); - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (_isChatWide && !AdaptiveBubbles()) { descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left(); @@ -2374,7 +2383,7 @@ void HistoryInner::updateSize() { : (st::msgNameFont->height + st::botDescSkip); int32 descH = st::msgMargin.top() + st::msgPadding.top() + descriptionHeight + _botAbout->height + st::msgPadding.bottom() + st::msgMargin.bottom(); int32 descMaxWidth = _scroll->width(); - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (_isChatWide && !AdaptiveBubbles()) { descMaxWidth = qMin(descMaxWidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } int32 descAtX = (descMaxWidth - _botAbout->width) / 2 - st::msgPadding.left(); @@ -2565,6 +2574,25 @@ void HistoryInner::elementShowPollResults( _controller->showPollResults(poll, context); } +void HistoryInner::elementOpenPhoto( + not_null photo, + FullMsgId context) { + _controller->openPhoto(photo, context); +} + +void HistoryInner::elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, context, showInMediaView); +} + +void HistoryInner::elementCancelUpload(const FullMsgId &context) { + if (const auto item = session().data().message(context)) { + _controller->cancelUploadLayer(item); + } +} + void HistoryInner::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { @@ -2597,6 +2625,10 @@ void HistoryInner::elementHandleViaClick(not_null bot) { App::insertBotCommand('@' + bot->username); } +bool HistoryInner::elementIsChatWide() { + return _isChatWide; +} + auto HistoryInner::getSelectionState() const -> HistoryView::TopBarWidget::SelectedState { auto result = HistoryView::TopBarWidget::SelectedState {}; @@ -2753,7 +2785,7 @@ void HistoryInner::mouseActionUpdate() { dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right(); auto dateLeft = st::msgServiceMargin.left(); auto maxwidth = _contentWidth; - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (_isChatWide && !AdaptiveBubbles()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); @@ -3215,12 +3247,12 @@ void HistoryInner::deleteItem(FullMsgId itemId) { void HistoryInner::deleteItem(not_null item) { if (auto message = item->toHistoryMessage()) { if (message->uploading()) { - _controller->content()->cancelUploadLayer(item); + _controller->cancelUploadLayer(item); return; } } const auto suggestModerateActions = true; - Ui::show(Box(item, suggestModerateActions)); + _controller->show(Box(item, suggestModerateActions)); } bool HistoryInner::hasPendingResizedItems() const { @@ -3234,7 +3266,7 @@ void HistoryInner::deleteAsGroup(FullMsgId itemId) { if (!group) { return deleteItem(item); } - Ui::show(Box( + _controller->show(Box( &session(), session().data().itemsToIds(group->items))); } @@ -3257,7 +3289,7 @@ void HistoryInner::reportAsGroup(FullMsgId itemId) { void HistoryInner::blockSenderItem(FullMsgId itemId) { if (const auto item = session().data().message(itemId)) { - Ui::show(Box( + _controller->show(Box( Window::BlockSenderFromRepliesBox, _controller, itemId)); @@ -3439,6 +3471,29 @@ not_null HistoryInner::ElementDelegate() { Instance->elementShowPollResults(poll, context); } } + void elementOpenPhoto( + not_null photo, + FullMsgId context) override { + if (Instance) { + Instance->elementOpenPhoto(photo, context); + } + } + void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false) override { + if (Instance) { + Instance->elementOpenDocument( + document, + context, + showInMediaView); + } + } + void elementCancelUpload(const FullMsgId &context) override { + if (Instance) { + Instance->elementCancelUpload(context); + } + } void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) override { @@ -3467,6 +3522,11 @@ not_null HistoryInner::ElementDelegate() { Instance->elementHandleViaClick(bot); } } + bool elementIsChatWide() override { + return Instance + ? Instance->elementIsChatWide() + : false; + } }; static Result result; diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index bf3a11c11..39952148d 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -41,8 +41,7 @@ enum class ReportReason; class HistoryWidget; class HistoryInner : public Ui::RpWidget - , public Ui::AbstractTooltipShower - , private base::Subscriber { + , public Ui::AbstractTooltipShower { // The Q_OBJECT meta info is used for qobject_cast! Q_OBJECT @@ -89,6 +88,14 @@ public: void elementShowPollResults( not_null poll, FullMsgId context); + void elementOpenPhoto( + not_null photo, + FullMsgId context); + void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false); + void elementCancelUpload(const FullMsgId &context); void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback); @@ -97,6 +104,7 @@ public: const QString &command, const FullMsgId &context); void elementHandleViaClick(not_null bot); + bool elementIsChatWide(); void updateBotInfo(bool recount = true); @@ -357,6 +365,8 @@ private: SelectedItems _selected; std::optional _chooseForReportReason; + bool _isChatWide = false; + base::flat_set> _animatedStickersPlayed; base::flat_map< not_null, diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index be06f6ea0..7631968a0 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -878,7 +878,7 @@ bool HistoryItem::unread() const { return false; } if (const auto user = history()->peer->asUser()) { - if (user->isBot()) { + if (user->isBot() && !user->isSupport()) { return false; } } else if (const auto channel = history()->peer->asChannel()) { diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 90ab65500..c3f611284 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_file_origin.h" #include "data/data_document.h" +#include "data/data_file_click_handler.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "facades.h" diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 2061c4230..b60a8d051 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 "ui/effects/animations.h" struct WebPageData; +class VoiceSeekClickHandler; namespace Data { class Session; diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index ba5b5e798..de8a396da 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -348,6 +348,7 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { }; auto prepareGroupCall = [this](const MTPDmessageActionGroupCall &action) { + auto result = PreparedText{}; if (const auto duration = action.vduration()) { const auto seconds = duration->v; const auto days = seconds / 86400; @@ -360,9 +361,22 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) { : (minutes > 1) ? tr::lng_group_call_duration_minutes(tr::now, lt_count, minutes) : tr::lng_group_call_duration_seconds(tr::now, lt_count, seconds); - return PreparedText{ tr::lng_action_group_call_finished(tr::now, lt_duration, text) }; + if (history()->peer->isBroadcast()) { + result.text = tr::lng_action_group_call_finished( + tr::now, + lt_duration, + text); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_group_call_finished_group( + tr::now, + lt_from, + fromLinkText(), + lt_duration, + text); + } + return result; } - auto result = PreparedText{}; if (history()->peer->isBroadcast()) { result.text = tr::lng_action_group_call_started_channel(tr::now); } else { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index bd6023ec6..41f5a0174 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -115,6 +115,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" #include "window/themes/window_theme.h" #include "window/notifications_manager.h" +#include "window/window_adaptive.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_slide_animation.h" @@ -512,7 +513,8 @@ HistoryWidget::HistoryWidget( Window::ActivateWindow(controller); }); - subscribe(Adaptive::Changed(), [=] { + controller->adaptive().changes( + ) | rpl::start_with_next([=] { if (_history) { _history->forceFullResize(); if (_migrated) { @@ -521,7 +523,7 @@ HistoryWidget::HistoryWidget( updateHistoryGeometry(); update(); } - }); + }, lifetime()); session().data().unreadItemAdded( ) | rpl::start_with_next([=](not_null item) { @@ -781,7 +783,7 @@ HistoryWidget::HistoryWidget( const auto unavailable = _peer->computeUnavailableReason(); if (!unavailable.isEmpty()) { controller->showBackFromStack(); - Ui::show(Box(unavailable)); + controller->show(Box(unavailable)); return; } } @@ -945,7 +947,7 @@ void HistoryWidget::initVoiceRecordBar() { ? Data::RestrictionError(_peer, ChatRestriction::f_send_media) : std::nullopt; if (error) { - Ui::show(Box(*error)); + controller()->show(Box(*error)); return true; } else if (showSlowmodeError()) { return true; @@ -1110,7 +1112,7 @@ void HistoryWidget::supportShareContact(Support::Contact contact) { contact.lastName, action); }; - const auto box = Ui::show(Box( + const auto box = controller()->show(Box( controller(), _history, contact, @@ -1430,7 +1432,16 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { _inlineResults.create(this, controller()); _inlineResults->setResultSelectedCallback([=]( InlineBots::ResultSelected result) { - sendInlineResult(result); + if (result.open) { + const auto request = result.result->openRequest(); + if (const auto photo = request.photo()) { + controller()->openPhoto(photo, FullMsgId()); + } else if (const auto document = request.document()) { + controller()->openDocument(document, FullMsgId()); + } + } else { + sendInlineResult(result); + } }); _inlineResults->setCurrentDialogsEntryState( computeDialogsEntryState()); @@ -3294,10 +3305,11 @@ void HistoryWidget::saveEditMsg() { if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { const auto suggestModerateActions = false; - Ui::show(Box(item, suggestModerateActions)); + controller()->show( + Box(item, suggestModerateActions)); return; } else if (!left.text.isEmpty()) { - Ui::show(Box(tr::lng_edit_too_long(tr::now))); + controller()->show(Box(tr::lng_edit_too_long(tr::now))); return; } @@ -3331,14 +3343,16 @@ void HistoryWidget::saveEditMsg() { } const auto &err = error.type(); if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show( + Box(tr::lng_edit_error(tr::now))); } else if (err == u"MESSAGE_NOT_MODIFIED"_q) { cancelEdit(); } else if (err == u"MESSAGE_EMPTY"_q) { _field->selectAll(); _field->setFocus(); } else { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show( + Box(tr::lng_edit_error(tr::now))); } update(); })(); @@ -3461,7 +3475,7 @@ void HistoryWidget::sendScheduled() { return; } const auto callback = [=](Api::SendOptions options) { send(options); }; - Ui::show( + controller()->show( HistoryView::PrepareScheduleBox(_list, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -3691,7 +3705,7 @@ void HistoryWidget::checkSuggestToGigagroup() { group->input, MTP_string("convert_to_gigagroup") )).send(); - Ui::show(Box([=](not_null box) { + controller()->show(Box([=](not_null box) { box->setTitle(tr::lng_gigagroup_suggest_title()); box->addRow( object_ptr( @@ -3729,7 +3743,8 @@ void HistoryWidget::unreadMentionsAnimationFinish() { void HistoryWidget::chooseAttach() { if (_editMsgId) { - Ui::show(Box(tr::lng_edit_caption_attach(tr::now))); + controller()->show( + Box(tr::lng_edit_caption_attach(tr::now))); return; } @@ -4293,7 +4308,8 @@ void HistoryWidget::toggleTabbedSelectorMode() { return; } if (_tabbedPanel) { - if (controller()->canShowThirdSection() && !Adaptive::OneColumn()) { + if (controller()->canShowThirdSection() + && !controller()->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); pushTabbedSelectorToThirdSection( @@ -4308,13 +4324,10 @@ void HistoryWidget::toggleTabbedSelectorMode() { } void HistoryWidget::recountChatWidth() { - auto layout = (width() < st::adaptiveChatWideWidth) - ? Adaptive::ChatLayout::Normal - : Adaptive::ChatLayout::Wide; - if (layout != Global::AdaptiveChatLayout()) { - Global::SetAdaptiveChatLayout(layout); - Adaptive::Changed().notify(true); - } + const auto layout = (width() < st::adaptiveChatWideWidth) + ? Window::Adaptive::ChatLayout::Normal + : Window::Adaptive::ChatLayout::Wide; + controller()->adaptive().setChatLayout(layout); } void HistoryWidget::moveFieldControls() { @@ -4584,7 +4597,8 @@ bool HistoryWidget::confirmSendingFiles( return false; } if (_editMsgId) { - Ui::show(Box(tr::lng_edit_caption_attach(tr::now))); + controller()->show( + Box(tr::lng_edit_caption_attach(tr::now))); return false; } @@ -4628,7 +4642,7 @@ bool HistoryWidget::confirmSendingFiles( })); Window::ActivateWindow(controller()); - const auto shown = Ui::show(std::move(box)); + const auto shown = controller()->show(std::move(box)); shown->setCloseByOutsideClick(false); return true; @@ -4890,8 +4904,14 @@ void HistoryWidget::updateControlsGeometry() { _membersDropdown->setMaxHeight(countMembersDropdownHeightMax()); } - auto topShadowLeft = (Adaptive::OneColumn() || _inGrab) ? 0 : st::lineWidth; - auto topShadowRight = (Adaptive::ThreeColumn() && !_inGrab && _peer) ? st::lineWidth : 0; + const auto isOneColumn = controller()->adaptive().isOneColumn(); + const auto isThreeColumn = controller()->adaptive().isThreeColumn(); + const auto topShadowLeft = (isOneColumn || _inGrab) + ? 0 + : st::lineWidth; + const auto topShadowRight = (isThreeColumn && !_inGrab && _peer) + ? st::lineWidth + : 0; _topShadow->setGeometryToLeft( topShadowLeft, _topBar->bottomNoMargins(), @@ -5586,14 +5606,14 @@ bool HistoryWidget::replyToPreviousMessage() { if (const auto view = item->mainView()) { if (const auto previousView = view->previousDisplayedInBlocks()) { const auto previous = previousView->data(); - Ui::showPeerHistoryAtItem(previous); + controller()->showPeerHistoryAtItem(previous); replyToMessage(previous); return true; } } } else if (const auto previousView = _history->findLastDisplayed()) { const auto previous = previousView->data(); - Ui::showPeerHistoryAtItem(previous); + controller()->showPeerHistoryAtItem(previous); replyToMessage(previous); return true; } @@ -5611,7 +5631,7 @@ bool HistoryWidget::replyToNextMessage() { if (const auto view = item->mainView()) { if (const auto nextView = view->nextDisplayedInBlocks()) { const auto next = nextView->data(); - Ui::showPeerHistoryAtItem(next); + controller()->showPeerHistoryAtItem(next); replyToMessage(next); } else { clearHighlightMessages(); @@ -5667,7 +5687,7 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { auto errorText = result.result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - Ui::show(Box(errorText)); + controller()->show(Box(errorText)); return; } @@ -5871,13 +5891,8 @@ void HistoryWidget::checkPinnedBarState() { refreshPinnedBarButton(many); }, _pinnedBar->lifetime()); - rpl::single( - rpl::empty_value() - ) | rpl::then( - base::ObservableViewer(Adaptive::Changed()) - ) | rpl::map([] { - return Adaptive::OneColumn(); - }) | rpl::start_with_next([=](bool one) { + controller()->adaptive().oneColumnValue( + ) | rpl::start_with_next([=](bool one) { _pinnedBar->setShadowGeometryPostprocess([=](QRect geometry) { if (!one) { geometry.setLeft(geometry.left() + st::lineWidth); @@ -6004,13 +6019,8 @@ void HistoryWidget::setupGroupCallTracker() { _groupCallTracker->content(), Core::App().appDeactivatedValue()); - rpl::single( - rpl::empty_value() - ) | rpl::then( - base::ObservableViewer(Adaptive::Changed()) - ) | rpl::map([] { - return Adaptive::OneColumn(); - }) | rpl::start_with_next([=](bool one) { + controller()->adaptive().oneColumnValue( + ) | rpl::start_with_next([=](bool one) { _groupCallBar->setShadowGeometryPostprocess([=](QRect geometry) { if (!one) { geometry.setLeft(geometry.left() + st::lineWidth); @@ -6066,7 +6076,9 @@ bool HistoryWidget::sendExistingDocument( ? Data::RestrictionError(_peer, ChatRestriction::f_send_stickers) : Data::RestrictionError(_peer, ChatRestriction::f_send_gifs); if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } else if (!_peer || !_peer->canWrite()) { return false; @@ -6100,7 +6112,9 @@ bool HistoryWidget::sendExistingPhoto( ? Data::RestrictionError(_peer, ChatRestriction::f_send_media) : std::nullopt; if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } else if (!_peer || !_peer->canWrite()) { return false; @@ -6203,14 +6217,18 @@ void HistoryWidget::replyToMessage(not_null item) { } if (item->history() == _migrated) { if (item->serviceMsg()) { - Ui::show(Box(tr::lng_reply_cant(tr::now))); + controller()->show(Box(tr::lng_reply_cant(tr::now))); } else { const auto itemId = item->fullId(); - Ui::show(Box(tr::lng_reply_cant_forward(tr::now), tr::lng_selected_forward(tr::now), crl::guard(this, [=] { - controller()->content()->setForwardDraft( - _peer->id, - { 1, itemId }); - }))); + controller()->show( + Box( + tr::lng_reply_cant_forward(tr::now), + tr::lng_selected_forward(tr::now), + crl::guard(this, [=] { + controller()->content()->setForwardDraft( + _peer->id, + { 1, itemId }); + }))); } return; } @@ -6253,12 +6271,13 @@ void HistoryWidget::editMessage(FullMsgId itemId) { void HistoryWidget::editMessage(not_null item) { if (_voiceRecordBar->isActive()) { - Ui::show(Box(tr::lng_edit_caption_voice(tr::now))); + controller()->show( + Box(tr::lng_edit_caption_voice(tr::now))); return; } if (const auto media = item->media()) { if (media->allowsEditCaption()) { - Ui::show(Box(controller(), item)); + controller()->show(Box(controller(), item)); return; } } @@ -6707,14 +6726,13 @@ void HistoryWidget::confirmDeleteSelected() { return; } const auto weak = Ui::MakeWeak(this); - const auto box = Ui::show(Box( - &session(), - std::move(items))); + auto box = Box(&session(), std::move(items)); box->setDeleteConfirmedCallback([=] { if (const auto strong = weak.data()) { strong->clearSelected(); } }); + controller()->show(std::move(box)); } void HistoryWidget::escape() { @@ -6727,7 +6745,7 @@ void HistoryWidget::escape() { } else if (_editMsgId) { if (_replyEditMsg && PrepareEditText(_replyEditMsg) != _field->getTextWithTags()) { - Ui::show(Box( + controller()->show(Box( tr::lng_cancel_edit_post_sure(tr::now), tr::lng_cancel_edit_post_yes(tr::now), tr::lng_cancel_edit_post_no(tr::now), 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 275e13d39..92d363a37 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -30,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_stickers.h" #include "data/data_web_page.h" #include "storage/storage_account.h" -#include "facades.h" #include "apiwrap.h" #include "boxes/confirm_box.h" #include "history/history.h" @@ -52,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/emoji_button.h" #include "ui/controls/send_button.h" #include "ui/special_buttons.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "mainwindow.h" @@ -795,7 +795,8 @@ rpl::producer<> ComposeControls::attachRequests() const { _attachRequests.events() ) | rpl::filter([=] { if (isEditingMessage()) { - Ui::show(Box(tr::lng_edit_caption_attach(tr::now))); + _window->show( + Box(tr::lng_edit_caption_attach(tr::now))); return false; } return true; @@ -1092,20 +1093,22 @@ void ComposeControls::initKeyHandler() { auto keyEvent = static_cast(e.get()); const auto key = keyEvent->key(); const auto isCtrl = keyEvent->modifiers() == Qt::ControlModifier; + const auto hasModifiers = keyEvent->modifiers() != Qt::NoModifier; if (key == Qt::Key_O && isCtrl) { _attachRequests.fire({}); return; } - if (key == Qt::Key_Up) { + if (key == Qt::Key_Up && !hasModifiers) { if (!isEditingMessage()) { _editLastMessageRequests.fire(std::move(keyEvent)); return; } } - if ((key == Qt::Key_Up) - || (key == Qt::Key_Down) - || (key == Qt::Key_PageUp) - || (key == Qt::Key_PageDown)) { + if (!hasModifiers + && ((key == Qt::Key_Up) + || (key == Qt::Key_Down) + || (key == Qt::Key_PageUp) + || (key == Qt::Key_PageDown))) { _scrollKeyEvents.fire(std::move(keyEvent)); } }, _wrap->lifetime()); @@ -1673,7 +1676,7 @@ void ComposeControls::initVoiceRecordBar() { ChatRestriction::f_send_media) : std::nullopt; if (error) { - Ui::show(Box(*error)); + _window->show(Box(*error)); return true; } else if (_showSlowmodeError && _showSlowmodeError()) { return true; @@ -1939,7 +1942,8 @@ void ComposeControls::toggleTabbedSelectorMode() { return; } if (_tabbedPanel) { - if (_window->canShowThirdSection() && !Adaptive::OneColumn()) { + if (_window->canShowThirdSection() + && !_window->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); pushTabbedSelectorToThirdSection( @@ -1973,7 +1977,7 @@ void ComposeControls::editMessage(not_null item) { Expects(draftKeyCurrent() != Data::DraftKey::None()); if (_voiceRecordBar->isActive()) { - Ui::show(Box(tr::lng_edit_caption_voice(tr::now))); + _window->show(Box(tr::lng_edit_caption_voice(tr::now))); return; } @@ -2412,7 +2416,16 @@ void ComposeControls::applyInlineBotQuery( _currentDialogsEntryState); _inlineResults->setResultSelectedCallback([=]( InlineBots::ResultSelected result) { - _inlineResultChosen.fire_copy(result); + if (result.open) { + const auto request = result.result->openRequest(); + if (const auto photo = request.photo()) { + _window->openPhoto(photo, FullMsgId()); + } else if (const auto document = request.document()) { + _window->openDocument(document, FullMsgId()); + } + } else { + _inlineResultChosen.fire_copy(result); + } }); _inlineResults->setSendMenuType([=] { return sendMenuType(); }); _inlineResults->requesting( 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 3796889c6..1c21b9968 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 @@ -1638,7 +1638,7 @@ void VoiceRecordBar::showDiscardBox( callback(); } }; - Ui::show(Box( + _controller->show(Box( (isListenState() ? tr::lng_record_listen_cancel_sure : tr::lng_record_lock_cancel_sure)(tr::now), diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index b640a696c..301d10f9b 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.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 "core/file_utilities.h" @@ -157,11 +158,13 @@ void AddPhotoActions( } } -void OpenGif(not_null session, FullMsgId itemId) { - if (const auto item = session->data().message(itemId)) { +void OpenGif( + not_null controller, + FullMsgId itemId) { + if (const auto item = controller->session().data().message(itemId)) { if (const auto media = item->media()) { if (const auto document = media->document()) { - Core::App().showDocument(document, item); + controller->openDocument(document, itemId, true); } } } @@ -223,7 +226,7 @@ void AddDocumentActions( }(); if (notAutoplayedGif) { menu->addAction(tr::lng_context_open_gif(tr::now), [=] { - OpenGif(session, contextId); + OpenGif(list->controller(), contextId); }); } } @@ -481,7 +484,7 @@ bool AddRescheduleAction( list->cancelSelection(); for (const auto &id : ids) { const auto item = owner->message(id); - if (!item && !item->isScheduled()) { + if (!item || !item->isScheduled()) { continue; } if (!item->media() || !item->media()->webpage()) { @@ -508,7 +511,7 @@ bool AddRescheduleAction( ? HistoryView::DefaultScheduleTime() : itemDate + 600; - const auto box = Ui::show( + const auto box = request.navigation->parentController()->show( HistoryView::PrepareScheduleBox( &request.navigation->session(), sendMenuType, @@ -676,14 +679,15 @@ bool AddDeleteSelectedAction( menu->addAction(tr::lng_context_delete_selected(tr::now), [=] { const auto weak = Ui::MakeWeak(list); auto items = ExtractIdsList(request.selectedItems); - const auto box = Ui::show(Box( + auto box = Box( &request.navigation->session(), - std::move(items))); + std::move(items)); box->setDeleteConfirmedCallback([=] { if (const auto strong = weak.data()) { strong->cancelSelection(); } }); + request.navigation->parentController()->show(std::move(box)); }); return true; } @@ -716,7 +720,7 @@ bool AddDeleteMessageAction( if (const auto item = owner->message(itemId)) { if (asGroup) { if (const auto group = owner->groups().find(item)) { - Ui::show(Box( + controller->show(Box( &owner->session(), owner->itemsToIds(group->items))); return; @@ -724,12 +728,13 @@ bool AddDeleteMessageAction( } if (const auto message = item->toHistoryMessage()) { if (message->uploading()) { - controller->content()->cancelUploadLayer(item); + controller->cancelUploadLayer(item); return; } } const auto suggestModerateActions = true; - Ui::show(Box(item, suggestModerateActions)); + controller->show( + Box(item, suggestModerateActions)); } }); if (const auto message = item->toHistoryMessage()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index ede5b7b51..d53666628 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -107,6 +107,20 @@ void SimpleElementDelegate::elementShowPollResults( FullMsgId context) { } +void SimpleElementDelegate::elementOpenPhoto( + not_null photo, + FullMsgId context) { +} + +void SimpleElementDelegate::elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { +} + +void SimpleElementDelegate::elementCancelUpload(const FullMsgId &context) { +} + void SimpleElementDelegate::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { @@ -133,6 +147,10 @@ void SimpleElementDelegate::elementSendBotCommand( void SimpleElementDelegate::elementHandleViaClick(not_null bot) { } +bool SimpleElementDelegate::elementIsChatWide() { + return false; +} + TextSelection UnshiftItemSelection( TextSelection selection, uint16 byLength) { @@ -223,7 +241,7 @@ int UnreadBar::marginTop() { return st::lineWidth + st::historyUnreadBarMargin; } -void UnreadBar::paint(Painter &p, int y, int w) const { +void UnreadBar::paint(Painter &p, int y, int w, bool chatWide) const { const auto bottom = y + height(); y += marginTop(); p.fillRect( @@ -243,7 +261,7 @@ void UnreadBar::paint(Painter &p, int y, int w) const { int left = st::msgServiceMargin.left(); int maxwidth = w; - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (chatWide && !AdaptiveBubbles()) { maxwidth = qMin( maxwidth, st::msgMaxWidth @@ -275,8 +293,8 @@ int DateBadge::height() const { + st::msgServiceMargin.bottom(); } -void DateBadge::paint(Painter &p, int y, int w) const { - ServiceMessagePainter::paintDate(p, text, width, y, w); +void DateBadge::paint(Painter &p, int y, int w, bool chatWide) const { + ServiceMessagePainter::paintDate(p, text, width, y, w, chatWide); } Element::Element( diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 27f6ee521..7425324a0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -60,6 +60,14 @@ public: virtual void elementShowPollResults( not_null poll, FullMsgId context) = 0; + virtual void elementOpenPhoto( + not_null photo, + FullMsgId context) = 0; + virtual void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false) = 0; + virtual void elementCancelUpload(const FullMsgId &context) = 0; virtual void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) = 0; @@ -70,6 +78,7 @@ public: const QString &command, const FullMsgId &context) = 0; virtual void elementHandleViaClick(not_null bot) = 0; + virtual bool elementIsChatWide() = 0; }; @@ -96,6 +105,14 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementOpenPhoto( + not_null photo, + FullMsgId context) override; + void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false) override; + void elementCancelUpload(const FullMsgId &context) override; void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) override; @@ -106,6 +123,7 @@ public: const QString &command, const FullMsgId &context) override; void elementHandleViaClick(not_null bot) override; + bool elementIsChatWide() override; private: const not_null _controller; @@ -135,7 +153,7 @@ struct UnreadBar : public RuntimeComponent { static int height(); static int marginTop(); - void paint(Painter &p, int y, int w) const; + void paint(Painter &p, int y, int w, bool chatWide) const; QString text; int width = 0; @@ -149,7 +167,7 @@ struct DateBadge : public RuntimeComponent { void init(const QString &date); int height() const; - void paint(Painter &p, int y, int w) const; + void paint(Painter &p, int y, int w, bool chatWide) const; QString text; int width = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp index f3fc904f4..ee35f3acb 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/group_call_bar.h" #include "ui/chat/group_call_userpics.h" #include "ui/painter.h" -#include "calls/calls_group_call.h" +#include "calls/group/calls_group_call.h" #include "calls/calls_instance.h" #include "core/application.h" #include "styles/style_chat.h" @@ -100,7 +100,7 @@ rpl::producer GroupCallTracker::ContentByCall( }; // speaking DESC, std::max(date, lastActive) DESC - static const auto SortKey = [](const Data::GroupCall::Participant &p) { + static const auto SortKey = [](const Data::GroupCallParticipant &p) { const auto result = (p.speaking ? uint64(0x100000000ULL) : uint64(0)) | uint64(std::max(p.lastActive, p.date)); return (~uint64(0)) - result; // sorting with less(), so invert. @@ -115,7 +115,7 @@ rpl::producer GroupCallTracker::ContentByCall( if (already >= kLimit || participants.size() <= already) { return false; } - std::array adding{ + std::array adding{ { nullptr } }; for (const auto &participant : call->participants()) { @@ -222,7 +222,7 @@ rpl::producer GroupCallTracker::ContentByCall( const auto j = ranges::find( participants, i->peer, - &Data::GroupCall::Participant::peer); + &Data::GroupCallParticipant::peer); if (j == end(participants) || !j->speaking) { // Found a non-speaking one, put the new speaking one here. break; @@ -253,7 +253,7 @@ rpl::producer GroupCallTracker::ContentByCall( const auto j = ranges::find( participants, i->peer, - &Data::GroupCall::Participant::peer); + &Data::GroupCallParticipant::peer); if (j == end(participants) || !j->speaking) { // Found a non-speaking one, remove. state->userpics.erase(i); @@ -331,7 +331,7 @@ rpl::producer GroupCallTracker::ContentByCall( } }, lifetime); - call->participantsSliceAdded( + call->participantsReloaded( ) | rpl::filter([=] { return RegenerateUserpics(state, call, userpicSize); }) | rpl::start_with_next(pushNext, lifetime); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 77f3bb1b3..af5f9fa0d 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -21,10 +21,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" #include "mainwindow.h" #include "mainwidget.h" -#include "core/application.h" #include "core/click_handler_types.h" #include "apiwrap.h" #include "layout.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "main/main_session.h" @@ -42,6 +42,7 @@ 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_click_handler.h" #include "facades.h" #include "styles/style_chat.h" @@ -51,7 +52,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -constexpr auto kScrollDateHideTimeout = 1000; constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadIfLessThanScreens = 2; constexpr auto kPreloadedScreensCountFull @@ -312,6 +312,11 @@ ListWidget::ListWidget( } } }); + + controller->adaptive().chatWideValue( + ) | rpl::start_with_next([=](bool wide) { + _isChatWide = wide; + }, lifetime()); } Main::Session &ListWidget::session() const { @@ -743,7 +748,7 @@ void ListWidget::scrollDateCheck() { } _scrollDateLastItem = _visibleTopItem; _scrollDateLastItemTop = _visibleTopFromItem; - _scrollDateHideTimer.callOnce(kScrollDateHideTimeout); + _scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout); } } @@ -766,7 +771,7 @@ void ListWidget::keepScrollDateForNow() { && _scrollDateOpacity.animating()) { toggleScrollDateShown(); } - _scrollDateHideTimer.callOnce(kScrollDateHideTimeout); + _scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout); } void ListWidget::toggleScrollDateShown() { @@ -1296,6 +1301,25 @@ void ListWidget::elementShowPollResults( _controller->showPollResults(poll, context); } +void ListWidget::elementOpenPhoto( + not_null photo, + FullMsgId context) { + _controller->openPhoto(photo, context); +} + +void ListWidget::elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView) { + _controller->openDocument(document, context, showInMediaView); +} + +void ListWidget::elementCancelUpload(const FullMsgId &context) { + if (const auto item = session().data().message(context)) { + _controller->cancelUploadLayer(item); + } +} + void ListWidget::elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) { @@ -1323,6 +1347,10 @@ void ListWidget::elementHandleViaClick(not_null bot) { _delegate->listHandleViaClick(bot); } +bool ListWidget::elementIsChatWide() { + return _isChatWide; +} + void ListWidget::saveState(not_null memento) { memento->setAroundPosition(_aroundPosition); auto state = countScrollState(); @@ -1558,13 +1586,14 @@ void ListWidget::paintEvent(QPaintEvent *e) { int dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top()); int width = view->width(); if (const auto date = view->Get()) { - date->paint(p, dateY, width); + date->paint(p, dateY, width, _isChatWide); } else { ServiceMessagePainter::paintDate( p, ItemDateText(view->data(), IsItemScheduledUntilOnline(view->data())), dateY, - width); + width, + _isChatWide); } } } @@ -2348,7 +2377,7 @@ void ListWidget::mouseActionUpdate() { dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right(); auto dateLeft = st::msgServiceMargin.left(); auto maxwidth = view->width(); - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (_isChatWide && !AdaptiveBubbles()) { maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); @@ -2505,7 +2534,7 @@ std::unique_ptr ListWidget::prepareDrag() { if (!urls.isEmpty()) { mimeData->setUrls(urls); } - if (uponSelected && !Adaptive::OneColumn()) { + if (uponSelected && !_controller->adaptive().isOneColumn()) { const auto canForwardAll = [&] { for (const auto &[itemId, data] : _selected) { if (!data.canForward) { @@ -2568,7 +2597,9 @@ std::unique_ptr ListWidget::prepareDrag() { void ListWidget::performDrag() { if (auto mimeData = prepareDrag()) { // This call enters event loop and can destroy any QObject. - _controller->widget()->launchDrag(std::move(mimeData)); + _controller->widget()->launchDrag( + std::move(mimeData), + crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));; } } @@ -2815,14 +2846,15 @@ void ConfirmDeleteSelectedItems(not_null widget) { } } const auto weak = Ui::MakeWeak(widget); - const auto box = Ui::show(Box( + auto box = Box( &widget->controller()->session(), - widget->getSelectedIds())); + widget->getSelectedIds()); box->setDeleteConfirmedCallback([=] { if (const auto strong = weak.data()) { strong->cancelSelection(); } }); + widget->controller()->show(std::move(box)); } void ConfirmForwardSelectedItems(not_null widget) { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 8d6cf9169..a9a04bf43 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -234,6 +234,14 @@ public: void elementShowPollResults( not_null poll, FullMsgId context) override; + void elementOpenPhoto( + not_null photo, + FullMsgId context) override; + void elementOpenDocument( + not_null document, + FullMsgId context, + bool showInMediaView = false) override; + void elementCancelUpload(const FullMsgId &context) override; void elementShowTooltip( const TextWithEntities &text, Fn hiddenCallback) override; @@ -244,6 +252,7 @@ public: const QString &command, const FullMsgId &context) override; void elementHandleViaClick(not_null bot) override; + bool elementIsChatWide() override; ~ListWidget(); @@ -546,6 +555,8 @@ private: bool _wasSelectedText = false; Qt::CursorShape _cursor = style::cur_default; + bool _isChatWide = false; + base::unique_qptr _menu; QPoint _trippleClickPoint; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index dda5077e1..dc8a6f917 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -16,8 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "ui/effects/ripple_animation.h" #include "base/unixtime.h" -#include "core/application.h" -#include "core/core_settings.h" #include "ui/toast/toast.h" #include "ui/text/text_utilities.h" #include "ui/text/text_entity.h" @@ -580,7 +578,7 @@ void Message::draw( auto unreadbarh = bar->height(); if (clip.intersects(QRect(0, dateh, width(), unreadbarh))) { p.translate(0, dateh); - bar->paint(p, 0, width()); + bar->paint(p, 0, width(), delegate()->elementIsChatWide()); p.translate(0, -dateh); } } @@ -660,7 +658,7 @@ void Message::draw( || (context() == Context::Replies && data()->isDiscussionPost()); auto displayTail = skipTail ? RectPart::None - : (outbg && !Core::App().settings().chatWide()) + : (outbg && !delegate()->elementIsChatWide()) ? RectPart::Right : RectPart::Left; PaintBubble( @@ -1223,7 +1221,7 @@ bool Message::hasFromPhoto() const { || item->isEmpty() || (context() == Context::Replies && item->isDiscussionPost())) { return false; - } else if (Core::App().settings().chatWide()) { + } else if (delegate()->elementIsChatWide()) { return true; } else if (const auto forwarded = item->Get()) { const auto peer = item->history()->peer; @@ -2533,7 +2531,7 @@ QRect Message::countGeometry() const { const auto availableWidth = width() - st::msgMargin.left() - (commentsRoot ? st::msgMargin.left() : st::msgMargin.right()); - auto contentLeft = (outbg && !Core::App().settings().chatWide()) + auto contentLeft = (outbg && !delegate()->elementIsChatWide()) ? st::msgMargin.right() : st::msgMargin.left(); auto contentWidth = availableWidth; @@ -2559,7 +2557,7 @@ QRect Message::countGeometry() const { } } if (contentWidth < availableWidth - && (!Core::App().settings().chatWide() + && (!delegate()->elementIsChatWide() || (commentsRoot && AdaptiveBubbles()))) { if (outbg) { contentLeft += availableWidth - contentWidth; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index b347da9f7..d5a2fc370 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toasts/common_toasts.h" #include "base/timer_rpl.h" #include "apiwrap.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "base/event_filter.h" @@ -127,8 +128,10 @@ PinnedWidget::PinnedWidget( }, _topBar->lifetime()); _topBarShadow->raise(); - updateAdaptiveLayout(); - subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); + controller->adaptive().value( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); _inner = _scroll->setOwnedWidget(object_ptr( this, @@ -297,7 +300,7 @@ void PinnedWidget::scrollDownAnimationFinish() { void PinnedWidget::updateAdaptiveLayout() { _topBarShadow->moveToLeft( - Adaptive::OneColumn() ? 0 : st::lineWidth, + controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, _topBar->height()); } @@ -387,12 +390,9 @@ void PinnedWidget::resizeEvent(QResizeEvent *e) { void PinnedWidget::recountChatWidth() { auto layout = (width() < st::adaptiveChatWideWidth) - ? Adaptive::ChatLayout::Normal - : Adaptive::ChatLayout::Wide; - if (layout != Global::AdaptiveChatLayout()) { - Global::SetAdaptiveChatLayout(layout); - Adaptive::Changed().notify(true); - } + ? Window::Adaptive::ChatLayout::Normal + : Window::Adaptive::ChatLayout::Wide; + controller()->adaptive().setChatLayout(layout); } void PinnedWidget::setMessagesCount(int count) { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index c1f16596d..468bb1382 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "boxes/edit_caption_box.h" #include "boxes/send_files_box.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "base/event_filter.h" @@ -191,8 +192,11 @@ RepliesWidget::RepliesWidget( _rootView->raise(); _topBarShadow->raise(); - updateAdaptiveLayout(); - subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); + + controller->adaptive().value( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); _inner = _scroll->setOwnedWidget(object_ptr( this, @@ -208,7 +212,7 @@ RepliesWidget::RepliesWidget( const auto media = item->media(); if (media && !media->webpage()) { if (media->allowsEditCaption()) { - Ui::show(Box(controller, item)); + controller->show(Box(controller, item)); } } else { _composeControls->editMessage(fullId); @@ -327,13 +331,8 @@ void RepliesWidget::setupRootView() { }); _rootView = std::make_unique(this, std::move(content)); - rpl::single( - rpl::empty_value() - ) | rpl::then( - base::ObservableViewer(Adaptive::Changed()) - ) | rpl::map([] { - return Adaptive::OneColumn(); - }) | rpl::start_with_next([=](bool one) { + controller()->adaptive().oneColumnValue( + ) | rpl::start_with_next([=](bool one) { _rootView->setShadowGeometryPostprocess([=](QRect geometry) { if (!one) { geometry.setLeft(geometry.left() + st::lineWidth); @@ -653,7 +652,7 @@ bool RepliesWidget::confirmSendingFiles( insertTextOnCancel)); //ActivateWindow(controller()); - const auto shown = Ui::show(std::move(box)); + const auto shown = controller()->show(std::move(box)); shown->setCloseByOutsideClick(false); return true; @@ -943,13 +942,13 @@ void RepliesWidget::edit( if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { if (item) { - Ui::show(Box(item, false)); + controller()->show(Box(item, false)); } else { doSetInnerFocus(); } return; } else if (!left.text.isEmpty()) { - Ui::show(Box(tr::lng_edit_too_long(tr::now))); + controller()->show(Box(tr::lng_edit_too_long(tr::now))); return; } @@ -974,13 +973,13 @@ void RepliesWidget::edit( const auto &err = error.type(); if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show(Box(tr::lng_edit_error(tr::now))); } else if (err == u"MESSAGE_NOT_MODIFIED"_q) { _composeControls->cancelEditMessage(); } else if (err == u"MESSAGE_EMPTY"_q) { doSetInnerFocus(); } else { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show(Box(tr::lng_edit_error(tr::now))); } update(); return true; @@ -1016,7 +1015,9 @@ bool RepliesWidget::sendExistingDocument( _history->peer, ChatRestriction::f_send_stickers); if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } else if (showSlowmodeError()) { return false; @@ -1050,7 +1051,9 @@ bool RepliesWidget::sendExistingPhoto( _history->peer, ChatRestriction::f_send_media); if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } else if (showSlowmodeError()) { return false; @@ -1071,7 +1074,7 @@ void RepliesWidget::sendInlineResult( not_null bot) { const auto errorText = result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - Ui::show(Box(errorText)); + controller()->show(Box(errorText)); return; } sendInlineResult(result, bot, Api::SendOptions()); @@ -1284,7 +1287,7 @@ void RepliesWidget::scrollDownAnimationFinish() { void RepliesWidget::updateAdaptiveLayout() { _topBarShadow->moveToLeft( - Adaptive::OneColumn() ? 0 : st::lineWidth, + controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, _topBar->height()); } @@ -1462,12 +1465,9 @@ void RepliesWidget::resizeEvent(QResizeEvent *e) { void RepliesWidget::recountChatWidth() { auto layout = (width() < st::adaptiveChatWideWidth) - ? Adaptive::ChatLayout::Normal - : Adaptive::ChatLayout::Wide; - if (layout != Global::AdaptiveChatLayout()) { - Global::SetAdaptiveChatLayout(layout); - Adaptive::Changed().notify(true); - } + ? Window::Adaptive::ChatLayout::Normal + : Window::Adaptive::ChatLayout::Wide; + controller()->adaptive().setChatLayout(layout); } void RepliesWidget::updateControlsGeometry() { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 7d0fb43dd..dac95d8bc 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "boxes/edit_caption_box.h" #include "boxes/send_files_box.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "base/event_filter.h" @@ -128,8 +129,10 @@ ScheduledWidget::ScheduledWidget( }, _topBar->lifetime()); _topBarShadow->raise(); - updateAdaptiveLayout(); - subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); + controller->adaptive().value( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); _inner = _scroll->setOwnedWidget(object_ptr( this, @@ -145,7 +148,7 @@ ScheduledWidget::ScheduledWidget( const auto media = item->media(); if (media && !media->webpage()) { if (media->allowsEditCaption()) { - Ui::show(Box(controller, item)); + controller->show(Box(controller, item)); } } else { _composeControls->editMessage(fullId); @@ -381,7 +384,7 @@ bool ScheduledWidget::confirmSendingFiles( insertTextOnCancel)); //ActivateWindow(controller()); - const auto shown = Ui::show(std::move(box)); + const auto shown = controller()->show(std::move(box)); shown->setCloseByOutsideClick(false); return true; @@ -451,7 +454,7 @@ void ScheduledWidget::uploadFile( action.options = options; session().api().sendFile(fileContent, type, action); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -496,7 +499,7 @@ void ScheduledWidget::send() { return; } const auto callback = [=](Api::SendOptions options) { send(options); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -541,7 +544,7 @@ void ScheduledWidget::sendVoice( const auto callback = [=](Api::SendOptions options) { sendVoice(bytes, waveform, duration, options); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -576,13 +579,13 @@ void ScheduledWidget::edit( if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { if (item) { - Ui::show(Box(item, false)); + controller()->show(Box(item, false)); } else { _composeControls->focus(); } return; } else if (!left.text.isEmpty()) { - Ui::show(Box(tr::lng_edit_too_long(tr::now))); + controller()->show(Box(tr::lng_edit_too_long(tr::now))); return; } @@ -607,13 +610,13 @@ void ScheduledWidget::edit( const auto &err = error.type(); if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show(Box(tr::lng_edit_error(tr::now))); } else if (err == u"MESSAGE_NOT_MODIFIED"_q) { _composeControls->cancelEditMessage(); } else if (err == u"MESSAGE_EMPTY"_q) { _composeControls->focus(); } else { - Ui::show(Box(tr::lng_edit_error(tr::now))); + controller()->show(Box(tr::lng_edit_error(tr::now))); } update(); return true; @@ -635,7 +638,7 @@ void ScheduledWidget::sendExistingDocument( const auto callback = [=](Api::SendOptions options) { sendExistingDocument(document, options); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -647,7 +650,9 @@ bool ScheduledWidget::sendExistingDocument( _history->peer, ChatRestriction::f_send_stickers); if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } @@ -665,7 +670,7 @@ void ScheduledWidget::sendExistingPhoto(not_null photo) { const auto callback = [=](Api::SendOptions options) { sendExistingPhoto(photo, options); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -677,7 +682,9 @@ bool ScheduledWidget::sendExistingPhoto( _history->peer, ChatRestriction::f_send_media); if (error) { - Ui::show(Box(*error), Ui::LayerOption::KeepOther); + controller()->show( + Box(*error), + Ui::LayerOption::KeepOther); return false; } @@ -696,13 +703,13 @@ void ScheduledWidget::sendInlineResult( not_null bot) { const auto errorText = result->getErrorOnSend(_history); if (!errorText.isEmpty()) { - Ui::show(Box(errorText)); + controller()->show(Box(errorText)); return; } const auto callback = [=](Api::SendOptions options) { sendInlineResult(result, bot, options); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } @@ -858,7 +865,7 @@ void ScheduledWidget::scrollDownAnimationFinish() { void ScheduledWidget::updateAdaptiveLayout() { _topBarShadow->moveToLeft( - Adaptive::OneColumn() ? 0 : st::lineWidth, + controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, _topBar->height()); } @@ -1171,7 +1178,7 @@ void ScheduledWidget::listSendBotCommand( message.action.options = options; session().api().sendMessage(std::move(message)); }; - Ui::show( + controller()->show( PrepareScheduleBox(this, sendMenuType(), callback), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 42a25d144..3df78755c 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -16,8 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_channel.h" #include "ui/text/text_options.h" -#include "core/core_settings.h" -#include "core/application.h" #include "mainwidget.h" #include "layout.h" #include "lang/lang_keys.h" @@ -50,6 +48,8 @@ public: // corners[(CircleMask value) * MaskMultiplier | (CornerVerticalSide value) | (CornerHorizontalSide value)] QPixmap corners[8]; + + base::flat_map, QPixmap> overridenCorners; }; Data::GlobalStructurePointer serviceMessageStyle; @@ -86,10 +86,20 @@ void createCircleMasks() { serviceMessageStyle->circle[InvertedMask] = style::createInvertedCircleMask(sizeInverted); } -QPixmap circleCorner(int corner) { - if (serviceMessageStyle->corners[corner].isNull()) { +uint32 ColorToUint(const style::color &bg) { + const auto &c = bg->c; + return c.red() << 24 | c.green() << 16 | c.blue() << 8 | c.alpha(); +} + +QPixmap circleCorner(int corner, const style::color &bg) { + auto ¤tCorner = (bg == st::msgServiceBg) + ? serviceMessageStyle->corners[corner] + : serviceMessageStyle->overridenCorners[{ corner, ColorToUint(bg) }]; + if (currentCorner.isNull()) { int maskType = corner / MaskMultiplier; - int radius = (maskType == NormalMask ? historyServiceMsgRadius() : historyServiceMsgInvertedRadius()); + int radius = (maskType == NormalMask + ? historyServiceMsgRadius() + : historyServiceMsgInvertedRadius()); int size = radius * cIntRetinaFactor(); int xoffset = 0, yoffset = 0; @@ -100,11 +110,14 @@ QPixmap circleCorner(int corner) { yoffset = size; } auto part = QRect(xoffset, yoffset, size, size); - auto result = style::colorizeImage(serviceMessageStyle->circle[maskType], st::msgServiceBg, part); + auto result = style::colorizeImage( + serviceMessageStyle->circle[maskType], + bg, + part); result.setDevicePixelRatio(cRetinaFactor()); - serviceMessageStyle->corners[corner] = App::pixmapFromImageInPlace(std::move(result)); + currentCorner = App::pixmapFromImageInPlace(std::move(result)); } - return serviceMessageStyle->corners[corner]; + return currentCorner; } enum class SideStyle { @@ -114,38 +127,63 @@ enum class SideStyle { }; // Returns amount of pixels already painted vertically (so you can skip them in the complex rect shape). -int paintBubbleSide(Painter &p, int x, int y, int width, SideStyle style, CornerVerticalSide side) { +int paintBubbleSide( + Painter &p, + int x, + int y, + int width, + SideStyle style, + CornerVerticalSide side, + const style::color &bg) { if (style == SideStyle::Rounded) { - auto left = circleCorner((NormalMask * MaskMultiplier) | side | CornerLeft); + const auto corner = (NormalMask * MaskMultiplier) | side; + auto left = circleCorner(corner | CornerLeft, bg); int leftWidth = left.width() / cIntRetinaFactor(); p.drawPixmap(x, y, left); - auto right = circleCorner((NormalMask * MaskMultiplier) | side | CornerRight); + auto right = circleCorner(corner | CornerRight, bg); int rightWidth = right.width() / cIntRetinaFactor(); p.drawPixmap(x + width - rightWidth, y, right); int cornerHeight = left.height() / cIntRetinaFactor(); - p.fillRect(x + leftWidth, y, width - leftWidth - rightWidth, cornerHeight, st::msgServiceBg); + p.fillRect( + x + leftWidth, + y, + width - leftWidth - rightWidth, + cornerHeight, + bg); return cornerHeight; } else if (style == SideStyle::Inverted) { // CornerLeft and CornerRight are inverted for SideStyle::Inverted sprites. - auto left = circleCorner((InvertedMask * MaskMultiplier) | side | CornerRight); + const auto corner = (InvertedMask * MaskMultiplier) | side; + auto left = circleCorner(corner | CornerRight, bg); int leftWidth = left.width() / cIntRetinaFactor(); p.drawPixmap(x - leftWidth, y, left); - auto right = circleCorner((InvertedMask * MaskMultiplier) | side | CornerLeft); + auto right = circleCorner(corner | CornerLeft, bg); p.drawPixmap(x + width, y, right); } return 0; } -void paintBubblePart(Painter &p, int x, int y, int width, int height, SideStyle topStyle, SideStyle bottomStyle, bool forceShrink = false) { - if (topStyle == SideStyle::Inverted || bottomStyle == SideStyle::Inverted || forceShrink) { +void paintBubblePart( + Painter &p, + int x, + int y, + int width, + int height, + SideStyle topStyle, + SideStyle bottomStyle, + const style::color &bg, + bool forceShrink = false) { + if ((topStyle == SideStyle::Inverted) + || (bottomStyle == SideStyle::Inverted) + || forceShrink) { width -= historyServiceMsgInvertedShrink() * 2; x += historyServiceMsgInvertedShrink(); } - if (int skip = paintBubbleSide(p, x, y, width, topStyle, CornerTop)) { + if (int skip = paintBubbleSide(p, x, y, width, topStyle, CornerTop, bg)) { y += skip; height -= skip; } @@ -155,27 +193,50 @@ void paintBubblePart(Painter &p, int x, int y, int width, int height, SideStyle } else if (bottomStyle == SideStyle::Inverted) { bottomSize = historyServiceMsgInvertedRadius(); } - if (int skip = paintBubbleSide(p, x, y + height - bottomSize, width, bottomStyle, CornerBottom)) { + const auto skip = paintBubbleSide( + p, + x, + y + height - bottomSize, + width, + bottomStyle, + CornerBottom, + bg); + if (skip) { height -= skip; } - p.fillRect(x, y, width, height, st::msgServiceBg); + p.fillRect(x, y, width, height, bg); } -void paintPreparedDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) { +void paintPreparedDate( + Painter &p, + const QString &dateText, + int dateTextWidth, + int y, + int w, + bool chatWide, + const style::color &bg, + const style::color &fg) { int left = st::msgServiceMargin.left(); - int maxwidth = w; - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { - maxwidth = qMin(maxwidth, WideChatWidth()); - } + const auto maxwidth = (chatWide && !AdaptiveBubbles()) + ? std::min(w, WideChatWidth()) + : w; w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); left += (w - dateTextWidth - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2; int height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); - ServiceMessagePainter::paintBubble(p, left, y + st::msgServiceMargin.top(), dateTextWidth + st::msgServicePadding.left() + st::msgServicePadding.left(), height); + ServiceMessagePainter::paintBubble( + p, + left, + y + st::msgServiceMargin.top(), + dateTextWidth + + st::msgServicePadding.left() + + st::msgServicePadding.left(), + height, + bg); p.setFont(st::msgServiceFont); - p.setPen(st::msgServiceFg); + p.setPen(fg); p.drawText(left + st::msgServicePadding.left(), y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent, dateText); } @@ -194,27 +255,77 @@ int WideChatWidth() { return st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left(); } -void ServiceMessagePainter::paintDate(Painter &p, const QDateTime &date, int y, int w) { - auto dateText = langDayOfMonthFull(date.date()); - auto dateTextWidth = st::msgServiceFont->width(dateText); - paintPreparedDate(p, dateText, dateTextWidth, y, w); +void ServiceMessagePainter::paintDate( + Painter &p, + const QDateTime &date, + int y, + int w, + bool chatWide, + const style::color &bg, + const style::color &fg) { + const auto dateText = langDayOfMonthFull(date.date()); + const auto dateTextWidth = st::msgServiceFont->width(dateText); + paintPreparedDate(p, dateText, dateTextWidth, y, w, chatWide, bg, fg); } -void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int y, int w) { - paintPreparedDate(p, dateText, st::msgServiceFont->width(dateText), y, w); +void ServiceMessagePainter::paintDate( + Painter &p, + const QString &dateText, + int y, + int w, + bool chatWide, + const style::color &bg, + const style::color &fg) { + paintPreparedDate( + p, + dateText, + st::msgServiceFont->width(dateText), + y, + w, + chatWide, + bg, + fg); } -void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) { - paintPreparedDate(p, dateText, dateTextWidth, y, w); +void ServiceMessagePainter::paintDate( + Painter &p, + const QString &dateText, + int dateTextWidth, + int y, + int w, + bool chatWide, + const style::color &bg, + const style::color &fg) { + paintPreparedDate(p, dateText, dateTextWidth, y, w, chatWide, bg, fg); } -void ServiceMessagePainter::paintBubble(Painter &p, int x, int y, int w, int h) { +void ServiceMessagePainter::paintBubble( + Painter &p, + int x, + int y, + int w, + int h, + const style::color &bg) { createCircleMasks(); - paintBubblePart(p, x, y, w, h, SideStyle::Rounded, SideStyle::Rounded); + paintBubblePart( + p, + x, + y, + w, + h, + SideStyle::Rounded, + SideStyle::Rounded, + bg); } -void ServiceMessagePainter::paintComplexBubble(Painter &p, int left, int width, const Ui::Text::String &text, const QRect &textRect) { +void ServiceMessagePainter::paintComplexBubble( + Painter &p, + int left, + int width, + const Ui::Text::String &text, + const QRect &textRect, + const style::color &bg) { createCircleMasks(); auto lineWidths = countLineWidths(text, textRect); @@ -250,7 +361,16 @@ void ServiceMessagePainter::paintComplexBubble(Painter &p, int left, int width, richHeight -= st::msgServicePadding.top(); } forceShrink = previousShrink && (richWidth == previousRichWidth); - paintBubblePart(p, left + ((width - richWidth) / 2), y, richWidth, richHeight, topStyle, bottomStyle, forceShrink); + paintBubblePart( + p, + left + ((width - richWidth) / 2), + y, + richWidth, + richHeight, + topStyle, + bottomStyle, + bg, + forceShrink); y += richHeight; previousShrink = forceShrink || (topStyle == SideStyle::Inverted) || (bottomStyle == SideStyle::Inverted); @@ -321,7 +441,7 @@ not_null Service::message() const { QRect Service::countGeometry() const { auto result = QRect(0, 0, width(), height()); - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (delegate()->elementIsChatWide() && !AdaptiveBubbles()) { result.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } return result.marginsRemoved(st::msgServiceMargin); @@ -344,7 +464,7 @@ QSize Service::performCountCurrentSize(int newWidth) { item->_textHeight = 0; } else { auto contentWidth = newWidth; - if (Core::App().settings().chatWide() && !AdaptiveBubbles()) { + if (delegate()->elementIsChatWide() && !AdaptiveBubbles()) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins @@ -418,7 +538,7 @@ void Service::draw( if (const auto bar = Get()) { unreadbarh = bar->height(); if (clip.intersects(QRect(0, 0, width(), unreadbarh))) { - bar->paint(p, 0, width()); + bar->paint(p, 0, width(), delegate()->elementIsChatWide()); } p.translate(0, unreadbarh); clip.translate(0, -unreadbarh); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index 8b86ca1ad..1295ae4e5 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -63,13 +63,47 @@ struct PaintContext { class ServiceMessagePainter { public: - static void paintDate(Painter &p, const QDateTime &date, int y, int w); - static void paintDate(Painter &p, const QString &dateText, int y, int w); - static void paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w); + static void paintDate( + Painter &p, + const QDateTime &date, + int y, + int w, + bool chatWide, + const style::color &bg = st::msgServiceBg, + const style::color &fg = st::msgServiceFg); + static void paintDate( + Painter &p, + const QString &dateText, + int y, + int w, + bool chatWide, + const style::color &bg = st::msgServiceBg, + const style::color &fg = st::msgServiceFg); + static void paintDate( + Painter &p, + const QString &dateText, + int dateTextWidth, + int y, + int w, + bool chatWide, + const style::color &bg = st::msgServiceBg, + const style::color &fg = st::msgServiceFg); - static void paintBubble(Painter &p, int x, int y, int w, int h); + static void paintBubble( + Painter &p, + int x, + int y, + int w, + int h, + const style::color &bg = st::msgServiceBg); - static void paintComplexBubble(Painter &p, int left, int width, const Ui::Text::String &text, const QRect &textRect); + static void paintComplexBubble( + Painter &p, + int left, + int width, + const Ui::Text::String &text, + const QRect &textRect, + const style::color &bg = st::msgServiceBg); private: static QVector countLineWidths(const Ui::Text::String &text, const QRect &textRect); 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 4adcf7614..77ea5019c 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/unread_badge.h" #include "ui/ui_utility.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "calls/calls_instance.h" @@ -47,7 +48,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "support/support_helper.h" #include "apiwrap.h" -#include "facades.h" #include "styles/style_window.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" @@ -118,7 +118,11 @@ TopBarWidget::TopBarWidget( _search->setForceRippled(searchInActiveChat, animated); }, lifetime()); - subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); + controller->adaptive().changes( + ) | rpl::start_with_next([=] { + updateAdaptiveLayout(); + }, lifetime()); + refreshUnreadBadge(); { using AnimationUpdate = Data::Session::SendActionAnimationUpdate; @@ -168,8 +172,7 @@ TopBarWidget::TopBarWidget( updateInfoToggleActive(); }, lifetime()); - rpl::single(rpl::empty_value()) | rpl::then( - base::ObservableViewer(Global::RefConnectionTypeChanged()) + Core::App().settings().proxy().connectionTypeValue( ) | rpl::start_with_next([=] { updateConnectingState(); }, lifetime()); @@ -305,7 +308,8 @@ void TopBarWidget::showMenu() { } void TopBarWidget::toggleInfoSection() { - if (Adaptive::ThreeColumn() + const auto isThreeColumn = _controller->adaptive().isThreeColumn(); + if (isThreeColumn && (Core::App().settings().thirdSectionInfoEnabled() || Core::App().settings().tabbedReplacedWithInfo())) { _controller->closeThirdSection(); @@ -313,7 +317,7 @@ void TopBarWidget::toggleInfoSection() { if (_controller->canShowThirdSection()) { Core::App().settings().setThirdSectionInfoEnabled(true); Core::App().saveSettingsDelayed(); - if (Adaptive::ThreeColumn()) { + if (isThreeColumn) { _controller->showSection( Info::Memento::Default(_activeChat.key.peer()), Window::SectionShow().withThirdColumn()); @@ -681,7 +685,8 @@ void TopBarWidget::updateControlsGeometry() { auto hasSelected = showSelectedActions(); auto selectedButtonsTop = countSelectedButtonsTop(_selectedShown.value(hasSelected ? 1. : 0.)); auto otherButtonsTop = selectedButtonsTop + st::topBarHeight; - auto buttonsLeft = st::topBarActionSkip + (Adaptive::OneColumn() ? 0 : st::lineWidth); + auto buttonsLeft = st::topBarActionSkip + + (_controller->adaptive().isOneColumn() ? 0 : st::lineWidth); auto buttonsWidth = (_forward->isHidden() ? 0 : _forward->contentWidth()) + (_sendNow->isHidden() ? 0 : _sendNow->contentWidth()) + (_delete->isHidden() ? 0 : _delete->contentWidth()) @@ -783,13 +788,14 @@ void TopBarWidget::updateControlsVisibility() { _forward->setVisible(_canForward); _sendNow->setVisible(_canSendNow); - auto backVisible = Adaptive::OneColumn() + const auto isOneColumn = _controller->adaptive().isOneColumn(); + auto backVisible = isOneColumn || !_controller->content()->stackIsEmpty() || _activeChat.key.folder(); _back->setVisible(backVisible && !_chooseForReportReason); _cancelChoose->setVisible(_chooseForReportReason.has_value()); if (_info) { - _info->setVisible(cShowTopBarUserpic() || (Adaptive::OneColumn() && !_chooseForReportReason)); + _info->setVisible(cShowTopBarUserpic() || (isOneColumn && !_chooseForReportReason)); } if (_unreadBadge) { _unreadBadge->setVisible(!_chooseForReportReason); @@ -806,7 +812,7 @@ void TopBarWidget::updateControlsVisibility() { _menuToggle->setVisible(hasMenu && !_chooseForReportReason); _infoToggle->setVisible(historyMode && !_activeChat.key.folder() - && !Adaptive::OneColumn() + && !isOneColumn && _controller->canShowThirdSection() && !_chooseForReportReason); const auto callsEnabled = [&] { @@ -937,7 +943,7 @@ void TopBarWidget::updateAdaptiveLayout() { } void TopBarWidget::refreshUnreadBadge() { - if (!Adaptive::OneColumn() && !_activeChat.key.folder()) { + if (!_controller->adaptive().isOneColumn() && !_activeChat.key.folder()) { _unreadBadge.destroy(); return; } else if (_unreadBadge) { @@ -981,7 +987,7 @@ void TopBarWidget::updateUnreadBadge() { } void TopBarWidget::updateInfoToggleActive() { - auto infoThirdActive = Adaptive::ThreeColumn() + auto infoThirdActive = _controller->adaptive().isThreeColumn() && (Core::App().settings().thirdSectionInfoEnabled() || Core::App().settings().tabbedReplacedWithInfo()); auto iconOverride = infoThirdActive diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 895f1a481..f1e11e5f6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -18,13 +18,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_common.h" #include "ui/image/image.h" #include "ui/text/format_values.h" +#include "ui/text/format_song_document_name.h" #include "ui/cached_round_corners.h" #include "ui/ui_utility.h" #include "layout.h" // FullSelection #include "data/data_session.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_document_resolver.h" #include "data/data_media_types.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "styles/style_chat.h" @@ -215,18 +218,21 @@ void Document::createComponents(bool caption) { _realParent->fullId()); thumbed->_linkcancell = std::make_shared( _data, + crl::guard(this, [=](FullMsgId id) { + _parent->delegate()->elementCancelUpload(id); + }), _realParent->fullId()); } if (const auto voice = Get()) { voice->_seekl = std::make_shared( _data, - _realParent->fullId()); + [](FullMsgId) {}); } } void Document::fillNamedFromData(HistoryDocumentNamed *named) { const auto nameString = named->_name = CleanTagSymbols( - _data->composeNameString()); + Ui::Text::FormatSongNameFor(_data).string()); named->_namew = st::semiboldFont->width(nameString); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_file.cpp b/Telegram/SourceFiles/history/view/media/history_view_file.cpp index a32d63dc2..45cc23e05 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_file.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_file.cpp @@ -11,12 +11,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "history/history_item.h" #include "history/history.h" +#include "history/view/history_view_element.h" #include "data/data_document.h" +#include "data/data_file_click_handler.h" #include "data/data_session.h" #include "styles/style_chat.h" namespace HistoryView { +bool File::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const { + return p == _openl || p == _savel || p == _cancell; +} +bool File::dragItemByHandler(const ClickHandlerPtr &p) const { + return p == _openl || p == _savel || p == _cancell; +} + void File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (p == _savel || p == _cancell) { if (active && !dataLoaded()) { @@ -104,9 +113,19 @@ void File::setDocumentLinks( not_null realParent) { const auto context = realParent->fullId(); setLinks( - std::make_shared(document, context), + std::make_shared( + document, + crl::guard(this, [=](FullMsgId id) { + _parent->delegate()->elementOpenDocument(document, id); + }), + context), std::make_shared(document, context), - std::make_shared(document, context)); + std::make_shared( + document, + crl::guard(this, [=](FullMsgId id) { + _parent->delegate()->elementCancelUpload(id); + }), + context)); } File::~File() = default; diff --git a/Telegram/SourceFiles/history/view/media/history_view_file.h b/Telegram/SourceFiles/history/view/media/history_view_file.h index 0e6574f1a..a425d7305 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_file.h +++ b/Telegram/SourceFiles/history/view/media/history_view_file.h @@ -11,9 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" +class FileClickHandler; + namespace HistoryView { -class File : public Media { +class File : public Media, public base::has_weak_ptr { public: File( not_null parent, @@ -22,19 +24,17 @@ public: , _realParent(realParent) { } - bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { - return p == _openl || p == _savel || p == _cancell; - } - bool dragItemByHandler(const ClickHandlerPtr &p) const override { - return p == _openl || p == _savel || p == _cancell; - } + [[nodiscard]] bool toggleSelectionByHandlerClick( + const ClickHandlerPtr &p) const override; + [[nodiscard]] bool dragItemByHandler( + const ClickHandlerPtr &p) const override; void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override; void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; void refreshParentId(not_null realParent) override; - bool allowsFastShare() const override { + [[nodiscard]] bool allowsFastShare() const override { return true; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index fcf93ccf2..4d2b894b6 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_streaming.h" #include "data/data_document.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_document_media.h" #include "layout.h" // FullSelection @@ -408,7 +409,8 @@ void Gif::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms } p.drawImage(rthumb, activeOwnPlaying->frozenFrame); } else { - if (activeOwnPlaying) { + if (activeOwnPlaying + && !activeOwnPlaying->frozenFrame.isNull()) { activeOwnPlaying->frozenFrame = QImage(); activeOwnPlaying->frozenStatusText = QString(); } @@ -1405,7 +1407,10 @@ void Gif::playAnimation(bool autoplay) { return; } else if ((_streamed && autoplayEnabled()) || (!autoplay && _data->isVideoFile())) { - Core::App().showDocument(_data, _parent->data()); + _parent->delegate()->elementOpenDocument( + _data, + _parent->data()->fullId(), + true); return; } if (_streamed) { 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 56fabbb52..b1f965356 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -14,8 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_components.h" #include "lottie/lottie_single_player.h" -#include "core/application.h" -#include "core/core_settings.h" #include "data/data_session.h" #include "ui/cached_round_corners.h" #include "layout.h" @@ -106,7 +104,8 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) { } } auto newHeight = minHeight(); - if (_parent->hasOutLayout() && !Core::App().settings().chatWide()) { + if (_parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide()) { // Add some height to isolated emoji for the timestamp info. const auto infoHeight = st::msgDateImgPadding.y() * 2 + st::msgDateFont->height; @@ -128,7 +127,8 @@ void UnwrappedMedia::draw( } bool selected = (selection == FullSelection); - const auto rightAligned = _parent->hasOutLayout() && !Core::App().settings().chatWide(); + const auto rightAligned = _parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide(); const auto inWebPage = (_parent->media() != this); const auto item = _parent->data(); const auto via = inWebPage ? nullptr : item->Get(); @@ -196,7 +196,8 @@ void UnwrappedMedia::drawSurrounding( const HistoryMessageVia *via, const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const { - const auto rightAligned = _parent->hasOutLayout() && !Core::App().settings().chatWide(); + const auto rightAligned = _parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide(); const auto rightActionSize = _parent->rightActionSize(); const auto fullRight = calculateFullRight(inner); auto fullBottom = height(); @@ -256,7 +257,8 @@ PointState UnwrappedMedia::pointState(QPoint point) const { return PointState::Outside; } - const auto rightAligned = _parent->hasOutLayout() && !Core::App().settings().chatWide(); + const auto rightAligned = _parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide(); const auto inWebPage = (_parent->media() != this); const auto item = _parent->data(); const auto via = inWebPage ? nullptr : item->Get(); @@ -296,7 +298,8 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { return result; } - const auto rightAligned = _parent->hasOutLayout() && !Core::App().settings().chatWide(); + const auto rightAligned = _parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide(); const auto inWebPage = (_parent->media() != this); const auto item = _parent->data(); const auto via = inWebPage ? nullptr : item->Get(); @@ -409,7 +412,8 @@ std::unique_ptr UnwrappedMedia::stickerTakeLottie( } int UnwrappedMedia::calculateFullRight(const QRect &inner) const { - const auto rightAligned = _parent->hasOutLayout() && !Core::App().settings().chatWide(); + const auto rightAligned = _parent->hasOutLayout() + && !_parent->delegate()->elementIsChatWide(); const auto infoWidth = _parent->infoWidth() + st::msgDateImgPadding.x() * 2 + st::msgReplyPadding.left(); @@ -455,7 +459,7 @@ bool UnwrappedMedia::needInfoDisplay() const { || (_parent->rightActionSize()) || (_parent->isLastAndSelfMessage()) || (_parent->hasOutLayout() - && !Core::App().settings().chatWide() + && !_parent->delegate()->elementIsChatWide() && _content->alwaysShowOutTimestamp()); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 6a51f6efd..79e65a481 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_streaming.h" #include "data/data_photo.h" #include "data/data_photo_media.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_auto_download.h" #include "core/application.h" @@ -86,9 +87,17 @@ Photo::~Photo() { void Photo::create(FullMsgId contextId, PeerData *chat) { setLinks( - std::make_shared(_data, contextId, chat), + std::make_shared( + _data, + crl::guard(this, [=](FullMsgId id) { showPhoto(id); }), + contextId), std::make_shared(_data, contextId, chat), - std::make_shared(_data, contextId, chat)); + std::make_shared( + _data, + crl::guard(this, [=](FullMsgId id) { + _parent->delegate()->elementCancelUpload(id); + }), + contextId)); if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); } else if (_data->inlineThumbnailBytes().isEmpty() @@ -795,7 +804,7 @@ void Photo::playAnimation(bool autoplay) { if (_streamed && autoplay) { return; } else if (_streamed && videoAutoplayEnabled()) { - Core::App().showPhoto(_data, _parent->data()); + showPhoto(_parent->data()->fullId()); return; } if (_streamed) { @@ -868,4 +877,8 @@ void Photo::parentTextUpdated() { history()->owner().requestViewResize(_parent); } +void Photo::showPhoto(FullMsgId id) { + _parent->delegate()->elementOpenPhoto(_data, id); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index b0acad447..04dc9726b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -102,6 +102,8 @@ protected: private: struct Streamed; + void showPhoto(FullMsgId id); + void create(FullMsgId contextId, PeerData *chat = nullptr); void playAnimation(bool autoplay) override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index 0ee8bc829..9beb87907 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "lottie/lottie_single_player.h" #include "chat_helpers/stickers_lottie.h" @@ -289,6 +290,9 @@ void Sticker::refreshLink() { // .webp image and we allow to open it in media viewer. _link = std::make_shared( _data, + crl::guard(this, [=](FullMsgId id) { + _parent->delegate()->elementOpenDocument(_data, id); + }), _parent->data()->fullId()); } } 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 43938b0c4..2d97754c3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_session.h" #include "data/data_document_media.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "base/qthelp_url.h" #include "ui/text/format_values.h" 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 72edb19cb..15c74958b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_photo.h" #include "data/data_photo_media.h" +#include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "styles/style_chat.h" diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 6683458d7..59e2389ea 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -37,11 +37,6 @@ infoToggle: InfoToggle { rippleAreaPadding: 8px; } -infoScroll: ScrollArea(defaultScrollArea) { - bottomsh: 0px; - topsh: 0px; -} - infoMediaSearch: SearchFieldRow { height: 44px; padding: margins(8px, 6px, 8px, 6px); @@ -960,6 +955,9 @@ infoAboutGigagroup: FlatLabel(defaultFlatLabel) { minWidth: 274px; } +infoScrollDateHideTimeout: historyScrollDateHideTimeout; +infoDateFadeDuration: historyDateFadeDuration; + ktgHistoryTopBarBack: IconButton(historyTopBarBack) { icon: icon {{ "info_back", ktgTopBarBackIconFg }}; iconOver: icon {{ "info_back", ktgTopBarBackIconFgOver }}; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index c52cb2fb8..c9789093c 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -37,7 +37,7 @@ ContentWidget::ContentWidget( not_null controller) : RpWidget(parent) , _controller(controller) -, _scroll(this, st::infoScroll) { +, _scroll(this) { using namespace rpl::mappers; setAttribute(Qt::WA_OpaquePaintEvent); diff --git a/Telegram/SourceFiles/info/info_layer_widget.cpp b/Telegram/SourceFiles/info/info_layer_widget.cpp index 750e30856..4c2178a2a 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.cpp +++ b/Telegram/SourceFiles/info/info_layer_widget.cpp @@ -73,6 +73,11 @@ bool LayerWidget::floatPlayerIsVisible(not_null item) { return false; } +void LayerWidget::floatPlayerDoubleClickEvent( + not_null item) { + _controller->showPeerHistoryAtItem(item); +} + void LayerWidget::setupHeightConsumers() { Expects(_content != nullptr); diff --git a/Telegram/SourceFiles/info/info_layer_widget.h b/Telegram/SourceFiles/info/info_layer_widget.h index 79b4d247f..4499a8d22 100644 --- a/Telegram/SourceFiles/info/info_layer_widget.h +++ b/Telegram/SourceFiles/info/info_layer_widget.h @@ -64,6 +64,8 @@ private: not_null<::Media::Player::FloatSectionDelegate*> widget, Window::Column widgetColumn)> callback) override; bool floatPlayerIsVisible(not_null item) override; + void floatPlayerDoubleClickEvent( + not_null item) override; void setupHeightConsumers(); diff --git a/Telegram/SourceFiles/info/info_section_widget.cpp b/Telegram/SourceFiles/info/info_section_widget.cpp index 99b289bf9..188432308 100644 --- a/Telegram/SourceFiles/info/info_section_widget.cpp +++ b/Telegram/SourceFiles/info/info_section_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/info_section_widget.h" +#include "window/window_adaptive.h" #include "window/window_connecting_widget.h" #include "window/window_session_controller.h" #include "main/main_session.h" @@ -51,7 +52,7 @@ void SectionWidget::init() { _connecting = std::make_unique( _content.data(), &controller()->session().account(), - Window::AdaptiveIsOneColumn()); + controller()->adaptive().oneColumnValue()); _content->contentChanged( ) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index e23720e6a..8f841dc37 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -541,14 +541,15 @@ void TopBar::performDelete() { if (items.empty()) { _cancelSelectionClicks.fire({}); } else { - const auto box = Ui::show(Box( + auto box = Box( &_navigation->session(), - std::move(items))); + std::move(items)); box->setDeleteConfirmedCallback([weak = Ui::MakeWeak(this)] { if (weak) { weak->_cancelSelectionClicks.fire({}); } }); + _navigation->parentController()->show(std::move(box)); } } diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 0378461ce..2277d8582 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -13,10 +13,12 @@ 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_file_click_handler.h" #include "data/data_file_origin.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_service_message.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" @@ -46,6 +48,7 @@ namespace Info { namespace Media { namespace { +constexpr auto kFloatingHeaderAlpha = 0.9; constexpr auto kPreloadedScreensCount = 4; constexpr auto kPreloadIfLessThanScreens = 2; constexpr auto kPreloadedScreensCountFull @@ -66,6 +69,21 @@ UniversalMsgId GetUniversalId(not_null layout) { return GetUniversalId(layout->getItem()->fullId()); } +bool HasFloatingHeader(Type type) { + switch (type) { + case Type::Photo: + case Type::Video: + case Type::RoundFile: + case Type::RoundVoiceFile: + case Type::MusicFile: + return false; + case Type::File: + case Type::Link: + return true; + } + Unexpected("Type in HasFloatingHeader()"); +} + } // namespace struct ListWidget::Context { @@ -77,7 +95,9 @@ struct ListWidget::Context { class ListWidget::Section { public: - Section(Type type) : _type(type) { + Section(Type type) + : _type(type) + , _hasFloatingHeader(HasFloatingHeader(type)) { } bool addItem(not_null item); @@ -122,6 +142,8 @@ public: QRect clip, int outerWidth) const; + void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth); + static int MinItemHeight(Type type, int width); private: @@ -150,6 +172,7 @@ private: void refreshHeight(); Type _type = Type::Photo; + bool _hasFloatingHeader = false; Ui::Text::String _header; Items _items; int _itemsLeft = 0; @@ -426,6 +449,37 @@ void ListWidget::Section::paint( } } +void ListWidget::Section::paintFloatingHeader( + Painter &p, + int visibleTop, + int outerWidth) { + if (!_hasFloatingHeader) { + return; + } + const auto headerTop = st::infoMediaHeaderPosition.y() / 2; + if (visibleTop <= (_top + headerTop)) { + return; + } + const auto header = headerHeight(); + const auto headerLeft = st::infoMediaHeaderPosition.x(); + const auto floatingTop = std::min( + visibleTop, + bottom() - header + headerTop); + p.save(); + p.resetTransform(); + p.setOpacity(kFloatingHeaderAlpha); + p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg); + p.setOpacity(1.0); + p.setPen(st::infoMediaHeaderFg); + _header.drawLeftElided( + p, + headerLeft, + floatingTop + headerTop, + outerWidth - 2 * headerLeft, + outerWidth); + p.restore(); +} + TextSelection ListWidget::Section::itemSelection( not_null item, const Context &context) const { @@ -573,7 +627,12 @@ ListWidget::ListWidget( , _peer(_controller->key().peer()) , _migrated(_controller->migrated()) , _type(_controller->section().mediaType()) -, _slice(sliceKey(_universalAroundId)) { +, _slice(sliceKey(_universalAroundId)) +, _dateBadge(DateBadge{ + .check = SingleQueuedInvokation([=] { scrollDateCheck(); }), + .hideTimer = base::Timer([=] { scrollDateHide(); }), + .goodType = (_type == Type::Photo || _type == Type::Video), +}) { setMouseTracking(true); start(); } @@ -834,6 +893,16 @@ void ListWidget::unregisterHeavyItem(not_null item) { } } +void ListWidget::openPhoto(not_null photo, FullMsgId id) { + _controller->parentController()->openPhoto(photo, id); +} + +void ListWidget::openDocument( + not_null document, + FullMsgId id) { + _controller->parentController()->openDocument(document, id); +} + SparseIdsMergedSlice::Key ListWidget::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; @@ -1076,6 +1145,55 @@ void ListWidget::visibleTopBottomUpdated( checkMoveToOtherViewer(); clearHeavyItems(); + + if (_dateBadge.goodType) { + updateDateBadgeFor(_visibleTop); + if (!_visibleTop) { + if (_dateBadge.shown) { + scrollDateHide(); + } else { + update(_dateBadge.rect); + } + } else { + _dateBadge.check.call(); + } + } +} + +void ListWidget::updateDateBadgeFor(int top) { + if (_sections.empty()) { + return; + } + const auto layout = findItemByPoint({ st::infoMediaSkip, top }).layout; + const auto rectHeight = st::msgServiceMargin.top() + + st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom(); + + _dateBadge.text = ItemDateText(layout->getItem(), false); + _dateBadge.rect = QRect(0, top, width(), rectHeight); +} + +void ListWidget::scrollDateCheck() { + if (!_dateBadge.shown) { + toggleScrollDateShown(); + } + _dateBadge.hideTimer.callOnce(st::infoScrollDateHideTimeout); +} + +void ListWidget::scrollDateHide() { + if (_dateBadge.shown) { + toggleScrollDateShown(); + } +} + +void ListWidget::toggleScrollDateShown() { + _dateBadge.shown = !_dateBadge.shown; + _dateBadge.opacity.start( + [=] { update(_dateBadge.rect); }, + _dateBadge.shown ? 0. : 1., + _dateBadge.shown ? 1. : 0., + st::infoDateFadeDuration); } void ListWidget::checkMoveToOtherViewer() { @@ -1223,6 +1341,25 @@ void ListWidget::paintEvent(QPaintEvent *e) { it->paint(p, context, clip.translated(0, -top), outerWidth); p.translate(0, -top); } + if (fromSectionIt != _sections.end()) { + fromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth); + } + + if (_dateBadge.goodType && clip.intersects(_dateBadge.rect)) { + const auto scrollDateOpacity = + _dateBadge.opacity.value(_dateBadge.shown ? 1. : 0.); + if (scrollDateOpacity > 0.) { + p.setOpacity(scrollDateOpacity); + HistoryView::ServiceMessagePainter::paintDate( + p, + _dateBadge.text, + _visibleTop, + outerWidth, + false, + st::roundedBg, + st::roundedFg); + } + } } void ListWidget::mousePressEvent(QMouseEvent *e) { @@ -1315,7 +1452,7 @@ void ListWidget::showContextMenu( tr::lng_context_to_msg(tr::now), [=] { if (const auto item = owner->message(itemFullId)) { - Ui::showPeerHistoryAtItem(item); + _controller->parentController()->showPeerHistoryAtItem(item); } }); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 5713cf159..8d05a9066 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -79,6 +79,11 @@ public: void registerHeavyItem(not_null item) override; void unregisterHeavyItem(not_null item) override; + void openPhoto(not_null photo, FullMsgId id) override; + void openDocument( + not_null document, + FullMsgId id) override; + private: struct Context; class Section; @@ -271,6 +276,11 @@ private: void updateDragSelection(); void clearDragSelection(); + void updateDateBadgeFor(int top); + void scrollDateCheck(); + void scrollDateHide(); + void toggleScrollDateShown(); + void trySwitchToWordSelection(); void switchToWordSelection(); void validateTrippleClickStartTime(); @@ -282,7 +292,7 @@ private: const not_null _controller; const not_null _peer; PeerData * const _migrated = nullptr; - Type _type = Type::Photo; + const Type _type = Type::Photo; static constexpr auto kMinimalIdsLimit = 16; static constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1); @@ -317,6 +327,16 @@ private: DragSelectAction _dragSelectAction = DragSelectAction::None; bool _wasSelectedText = false; // was some text selected in current drag action + struct DateBadge { + SingleQueuedInvokation check; + base::Timer hideTimer; + Ui::Animations::Simple opacity; + bool goodType = false; + bool shown = false; + QString text; + QRect rect; + } _dateBadge; + base::unique_qptr _contextMenu; rpl::event_stream<> _checkForHide; QPointer _actionBoxWeak; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index d537fe1e3..a850df70f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -269,7 +269,7 @@ object_ptr DetailsFiller::setupInfo() { return result; }; if (const auto user = _peer->asUser()) { - if (cShowChatId() != 0) { + if (ShowChatId() != 0) { auto idDrawableText = IDValue( user ) | rpl::map([](TextWithEntities &&text) { @@ -364,7 +364,7 @@ object_ptr DetailsFiller::setupInfo() { [=] { controller->window().show(Box(EditContactBox, controller, user)); }, tracker); } else { - if (cShowChatId() != 0) { + if (ShowChatId() != 0) { auto idDrawableText = IDValue( _peer ) | rpl::map([](TextWithEntities &&text) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 30f21f49c..b77ba9c9e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -79,7 +79,7 @@ QString IDString(not_null peer) { ? peerToChannel(peer->id).bare : peer->id.value); - if (cShowChatId() == 2) { + if (ShowChatId() == 2) { if (peer->isChannel()) { resultId = QString::number(peerToChannel(peer->id).bare - kMaxChannelId).prepend("-"); } else if (peer->isChat()) { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp index ca2af7d37..0eb54d3d0 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.cpp @@ -812,10 +812,6 @@ void Video::prepareThumbnail(QSize size) const { } } -void OpenFileClickHandler::onClickImpl() const { - _result->openFile(); -} - void CancelFileClickHandler::onClickImpl() const { _result->cancelFile(); } @@ -824,7 +820,6 @@ File::File(not_null context, not_null result) : FileBase(context, result) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineFileSize - st::inlineThumbSkip) -, _open(std::make_shared(result)) , _cancel(std::make_shared(result)) , _document(getShownDocument()) { Expects(getResultDocument() != nullptr); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h index 4cdda986f..e9ab8b87b 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_internal.h @@ -246,19 +246,6 @@ private: }; -class OpenFileClickHandler : public LeftButtonClickHandler { -public: - OpenFileClickHandler(not_null result) : _result(result) { - } - -protected: - void onClickImpl() const override; - -private: - not_null _result; - -}; - class CancelFileClickHandler : public LeftButtonClickHandler { public: CancelFileClickHandler(not_null result) : _result(result) { @@ -328,7 +315,7 @@ private: mutable std::unique_ptr _animation; Ui::Text::String _title, _description; - ClickHandlerPtr _open, _cancel; + ClickHandlerPtr _cancel; // >= 0 will contain download / upload string, _statusSize = loaded bytes // < 0 will contain played string, _statusSize = -(seconds + 1) played diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp index 30b656e3c..84593fa81 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.cpp @@ -199,10 +199,9 @@ ClickHandlerPtr ItemBase::getResultPreviewHandler() const { false); } else if (const auto document = _result->_document && _result->_document->createMediaView()->canBePlayed()) { - return std::make_shared( - _result->_document); + return std::make_shared(); } else if (_result->_photo) { - return std::make_shared(_result->_photo); + return std::make_shared(); } return ClickHandlerPtr(); } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h index 2295af96f..a2ada30ff 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_layout_item.h @@ -42,6 +42,12 @@ public: } }; +class OpenFileClickHandler : public ClickHandler { +public: + void onClick(ClickContext context) const override { + } +}; + class Context { public: virtual void inlineItemLayoutChanged(const ItemBase *layout) = 0; @@ -131,6 +137,7 @@ protected: PhotoData *_photo = nullptr; ClickHandlerPtr _send = ClickHandlerPtr{ new SendClickHandler() }; + ClickHandlerPtr _open = ClickHandlerPtr{ new OpenFileClickHandler() }; int _position = 0; // < 0 means removed from layout diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index a605a7122..f3c593d1e 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -12,6 +12,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_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_photo_media.h" #include "data/data_document_media.h" @@ -331,19 +332,20 @@ bool Result::onChoose(Layout::ItemBase *layout) { return true; } -void Result::openFile() { +Media::View::OpenRequest Result::openRequest() { if (_document) { - DocumentOpenClickHandler(_document).onClick({}); + return Media::View::OpenRequest(nullptr, _document, nullptr); } else if (_photo) { - PhotoOpenClickHandler(_photo).onClick({}); + return Media::View::OpenRequest(nullptr, _photo, nullptr); } + return {}; } void Result::cancelFile() { if (_document) { - DocumentCancelClickHandler(_document).onClick({}); + DocumentCancelClickHandler(_document, nullptr).onClick({}); } else if (_photo) { - PhotoCancelClickHandler(_photo).onClick({}); + PhotoCancelClickHandler(_photo, nullptr).onClick({}); } } diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.h b/Telegram/SourceFiles/inline_bots/inline_bot_result.h index 4969959dd..5d243f49d 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.h +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_cloud_file.h" #include "api/api_common.h" +#include "media/view/media_view_open_common.h" class FileLoader; class History; @@ -54,7 +55,7 @@ public: // inline bot result. If it returns true you need to send this result. bool onChoose(Layout::ItemBase *layout); - void openFile(); + Media::View::OpenRequest openRequest(); void cancelFile(); bool hasThumbDisplay() const; @@ -131,6 +132,8 @@ struct ResultSelected { not_null result; not_null bot; Api::SendOptions options; + // Open in OverlayWidget; + bool open = false; }; } // namespace InlineBots diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp index 48b77fe2e..9055f493a 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.cpp @@ -48,11 +48,13 @@ Inner::Inner( update(); }, lifetime()); - subscribe(controller->gifPauseLevelChanged(), [this] { - if (!_controller->isGifPausedAtLeastFor(Window::GifPauseReason::InlineResults)) { + controller->gifPauseLevelChanged( + ) | rpl::start_with_next([=] { + if (!_controller->isGifPausedAtLeastFor( + Window::GifPauseReason::InlineResults)) { update(); } - }); + }, lifetime()); _controller->session().changes().peerUpdates( Data::PeerUpdate::Flag::Rights @@ -234,22 +236,22 @@ void Inner::mouseReleaseEvent(QMouseEvent *e) { return; } - if (dynamic_cast(activated.get())) { - int row = _selected / MatrixRowShift, column = _selected % MatrixRowShift; - selectInlineResult(row, column); + using namespace InlineBots::Layout; + const auto open = dynamic_cast(activated.get()); + if (dynamic_cast(activated.get()) || open) { + const auto row = int(_selected / MatrixRowShift); + const auto column = int(_selected % MatrixRowShift); + selectInlineResult(row, column, {}, !!open); } else { ActivateClickHandler(window(), activated, e->button()); } } -void Inner::selectInlineResult(int row, int column) { - selectInlineResult(row, column, Api::SendOptions()); -} - void Inner::selectInlineResult( int row, int column, - Api::SendOptions options) { + Api::SendOptions options, + bool open) { if (row >= _rows.size() || column >= _rows.at(row).items.size()) { return; } @@ -260,7 +262,8 @@ void Inner::selectInlineResult( _resultSelectedCallback({ .result = inlineResult, .bot = _inlineBot, - .options = std::move(options) + .options = std::move(options), + .open = open, }); } } @@ -298,7 +301,7 @@ void Inner::contextMenuEvent(QContextMenuEvent *e) { _menu = base::make_unique_q(this); const auto send = [=](Api::SendOptions options) { - selectInlineResult(row, column, options); + selectInlineResult(row, column, options, false); }; SendMenu::FillSendMenu( _menu, diff --git a/Telegram/SourceFiles/inline_bots/inline_results_inner.h b/Telegram/SourceFiles/inline_bots/inline_results_inner.h index bcf58a49d..cf54bf8a7 100644 --- a/Telegram/SourceFiles/inline_bots/inline_results_inner.h +++ b/Telegram/SourceFiles/inline_bots/inline_results_inner.h @@ -61,8 +61,7 @@ struct CacheEntry { class Inner : public Ui::RpWidget , public Ui::AbstractTooltipShower - , public Context - , private base::Subscriber { + , public Context { public: Inner(QWidget *parent, not_null controller); @@ -148,8 +147,11 @@ private: void deleteUnusedInlineLayouts(); int validateExistingInlineRows(const Results &results); - void selectInlineResult(int row, int column); - void selectInlineResult(int row, int column, Api::SendOptions options); + void selectInlineResult( + int row, + int column, + Api::SendOptions options, + bool open); not_null _controller; diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 6eac34d2a..075f394e2 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -53,13 +53,13 @@ PhoneWidget::PhoneWidget( connect(_country, &CountryInput::codeChanged, [=](const QString &code) { _code->codeSelected(code); + _phone->chooseCode(code); }); _code->codeChanged( ) | rpl::start_with_next([=](const QString &code) { _country->onChooseCode(code); _phone->chooseCode(code); }, _code->lifetime()); - connect(_country, SIGNAL(codeChanged(const QString &)), _phone, SLOT(onChooseCode(const QString &))); _code->addedToNumber( ) | rpl::start_with_next([=](const QString &added) { _phone->addedToNumber(added); @@ -69,7 +69,10 @@ PhoneWidget::PhoneWidget( setTitleText(tr::lng_phone_title()); setDescriptionText(tr::lng_phone_desc()); - subscribe(getData()->updated, [=] { countryChanged(); }); + getData()->updated.events( + ) | rpl::start_with_next([=] { + countryChanged(); + }, lifetime()); setErrorCentered(true); setupQrLogin(); diff --git a/Telegram/SourceFiles/intro/intro_step.cpp b/Telegram/SourceFiles/intro/intro_step.cpp index 787db231b..bc0347003 100644 --- a/Telegram/SourceFiles/intro/intro_step.cpp +++ b/Telegram/SourceFiles/intro/intro_step.cpp @@ -75,15 +75,16 @@ Step::Step( ? st::introCoverDescription : st::introDescription)) { hide(); - subscribe(Window::Theme::Background(), [this]( - const Window::Theme::BackgroundUpdate &update) { - if (update.paletteChanged()) { - if (!_coverMask.isNull()) { - _coverMask = QPixmap(); - prepareCoverMask(); - } + base::ObservableViewer( + *Window::Theme::Background() + ) | rpl::filter([](const Window::Theme::BackgroundUpdate &update) { + return update.paletteChanged(); + }) | rpl::start_with_next([=] { + if (!_coverMask.isNull()) { + _coverMask = QPixmap(); + prepareCoverMask(); } - }); + }, lifetime()); _errorText.value( ) | rpl::start_with_next([=](const QString &text) { @@ -157,6 +158,7 @@ void Step::finish(const MTPUser &user, QImage &&photo) { _account->logOut(); crl::on_main(raw, [=] { Core::App().domain().activate(raw); + Local::sync(); }); return; } @@ -203,6 +205,7 @@ void Step::createSession( if (session.supportMode()) { PrepareSupportMode(&session); } + Local::sync(); } void Step::paintEvent(QPaintEvent *e) { diff --git a/Telegram/SourceFiles/intro/intro_step.h b/Telegram/SourceFiles/intro/intro_step.h index b98349a44..4a590a93e 100644 --- a/Telegram/SourceFiles/intro/intro_step.h +++ b/Telegram/SourceFiles/intro/intro_step.h @@ -31,7 +31,7 @@ struct Data; enum class StackAction; enum class Animate; -class Step : public Ui::RpWidget, protected base::Subscriber { +class Step : public Ui::RpWidget { public: Step( QWidget *parent, diff --git a/Telegram/SourceFiles/intro/intro_widget.cpp b/Telegram/SourceFiles/intro/intro_widget.cpp index 60069279b..f1e012e76 100644 --- a/Telegram/SourceFiles/intro/intro_widget.cpp +++ b/Telegram/SourceFiles/intro/intro_widget.cpp @@ -21,6 +21,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "mainwindow.h" +#include "history/history.h" +#include "history/history_item.h" #include "data/data_user.h" #include "data/data_countries.h" #include "boxes/confirm_box.h" @@ -33,6 +35,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_dc_options.h" #include "window/window_slide_animation.h" #include "window/window_connecting_widget.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" #include "window/section_widget.h" #include "base/platform/base_platform_info.h" #include "api/api_text_entities.h" @@ -63,10 +67,12 @@ using namespace ::Intro::details; Widget::Widget( QWidget *parent, + not_null controller, not_null account, EnterPoint point) : RpWidget(parent) , _account(account) +, _data(details::Data{ .controller = controller }) , _back(this, object_ptr(this, st::introBackButton)) , _settings( this, @@ -107,9 +113,10 @@ Widget::Widget( fixOrder(); - subscribe(Lang::CurrentCloudManager().firstLanguageSuggestion(), [=] { + Lang::CurrentCloudManager().firstLanguageSuggestion( + ) | rpl::start_with_next([=] { createLanguageLink(); - }); + }, lifetime()); _account->mtpUpdates( ) | rpl::start_with_next([=](const MTPUpdates &updates) { @@ -183,6 +190,14 @@ bool Widget::floatPlayerIsVisible(not_null item) { return false; } +void Widget::floatPlayerDoubleClickEvent(not_null item) { + getData()->controller->invokeForSessionController( + &item->history()->peer->session().account(), + [=](not_null controller) { + controller->showPeerHistoryAtItem(item); + }); +} + QRect Widget::floatPlayerAvailableRect() { return mapToGlobal(rect()); } @@ -566,7 +581,7 @@ void Widget::getNearestDC() { const auto nearestCountry = qs(nearest.vcountry()); if (getData()->country != nearestCountry) { getData()->country = nearestCountry; - getData()->updated.notify(); + getData()->updated.fire({}); } }).send(); } diff --git a/Telegram/SourceFiles/intro/intro_widget.h b/Telegram/SourceFiles/intro/intro_widget.h index 59be97a2e..5f30c2206 100644 --- a/Telegram/SourceFiles/intro/intro_widget.h +++ b/Telegram/SourceFiles/intro/intro_widget.h @@ -29,6 +29,7 @@ class FadeWrap; namespace Window { class ConnectionState; +class Controller; } // namespace Window namespace Intro { @@ -42,6 +43,9 @@ enum class CallStatus { }; struct Data { + // Required for the UserpicButton. + const not_null controller; + QString country; QString phone; QByteArray phoneHash; @@ -59,7 +63,7 @@ struct Data { Window::TermsLock termsLock; - base::Observable updated; + rpl::event_stream<> updated; }; @@ -87,11 +91,11 @@ enum class EnterPoint : uchar { class Widget : public Ui::RpWidget , private Media::Player::FloatDelegate - , private Media::Player::FloatSectionDelegate - , private base::Subscriber { + , private Media::Player::FloatSectionDelegate { public: Widget( QWidget *parent, + not_null controller, not_null account, EnterPoint point); @@ -162,6 +166,8 @@ private: not_null widget, Window::Column widgetColumn)> callback) override; bool floatPlayerIsVisible(not_null item) override; + void floatPlayerDoubleClickEvent( + not_null item) override; // FloatSectionDelegate QRect floatPlayerAvailableRect() override; diff --git a/Telegram/SourceFiles/kotato/json_settings.cpp b/Telegram/SourceFiles/kotato/json_settings.cpp index 8cb3a0765..46e9bdec5 100644 --- a/Telegram/SourceFiles/kotato/json_settings.cpp +++ b/Telegram/SourceFiles/kotato/json_settings.cpp @@ -366,7 +366,7 @@ QByteArray GenerateSettingsJson(bool areDefault = false) { settings.insert(qsl("adaptive_bubbles"), AdaptiveBubbles()); settings.insert(qsl("big_emoji_outline"), BigEmojiOutline()); settings.insert(qsl("always_show_scheduled"), cAlwaysShowScheduled()); - settings.insert(qsl("show_chat_id"), cShowChatId()); + settings.insert(qsl("show_chat_id"), ShowChatId()); settings.insert(qsl("show_phone_in_drawer"), cShowPhoneInDrawer()); settings.insert(qsl("chat_list_lines"), DialogListLines()); settings.insert(qsl("disable_up_edit"), cDisableUpEdit()); @@ -529,13 +529,13 @@ bool Manager::readCustomFile() { auto isShowChatIdSet = ReadIntOption(settings, "show_chat_id", [&](auto v) { if (v >= 0 && v <= 2) { - cSetShowChatId(v); + SetShowChatId(v); } }); if (!isShowChatIdSet) { ReadBoolOption(settings, "show_chat_id", [&](auto v) { - cSetShowChatId(v ? 1 : 0); + SetShowChatId(v ? 1 : 0); }); } diff --git a/Telegram/SourceFiles/kotato/settings.cpp b/Telegram/SourceFiles/kotato/settings.cpp index eb4c51db9..11501f842 100644 --- a/Telegram/SourceFiles/kotato/settings.cpp +++ b/Telegram/SourceFiles/kotato/settings.cpp @@ -80,7 +80,19 @@ rpl::producer MonospaceLargeBubblesChanges() { } bool gAlwaysShowScheduled = false; -int gShowChatId = 2; + +rpl::variable gShowChatId = 2; +void SetShowChatId(int chatIdType) { + if (chatIdType >= 0 && chatIdType <= 2) { + gShowChatId = chatIdType; + } +} +int ShowChatId() { + return gShowChatId.current(); +} +rpl::producer ShowChatIdChanges() { + return gShowChatId.changes(); +} int gNetSpeedBoost = 0; int gNetRequestsCount = 2; diff --git a/Telegram/SourceFiles/kotato/settings.h b/Telegram/SourceFiles/kotato/settings.h index aaa5ca8e2..928cdf240 100644 --- a/Telegram/SourceFiles/kotato/settings.h +++ b/Telegram/SourceFiles/kotato/settings.h @@ -63,7 +63,10 @@ void SetMonospaceLargeBubbles(bool enabled); [[nodiscard]] rpl::producer MonospaceLargeBubblesChanges(); DeclareSetting(bool, AlwaysShowScheduled); -DeclareSetting(int, ShowChatId); + +void SetShowChatId(int chatIdType); +[[nodiscard]] int ShowChatId(); +[[nodiscard]] rpl::producer ShowChatIdChanges(); DeclareSetting(int, NetSpeedBoost); DeclareSetting(int, NetRequestsCount); diff --git a/Telegram/SourceFiles/kotato/settings_menu.cpp b/Telegram/SourceFiles/kotato/settings_menu.cpp index be71a8861..6974810d0 100644 --- a/Telegram/SourceFiles/kotato/settings_menu.cpp +++ b/Telegram/SourceFiles/kotato/settings_menu.cpp @@ -632,12 +632,13 @@ void SetupKotatoOther(not_null container) { tr::ktg_settings_chat_id(), st::settingsButton)); auto chatIdText = rpl::single( - rpl::empty_value() - ) | rpl::then(base::ObservableViewer( - Global::RefChatIDFormatChanged() - )) | rpl::map([] { - return ChatIdLabel(cShowChatId()); - }); + ChatIdLabel(ShowChatId()) + ) | rpl::then( + ShowChatIdChanges( + ) | rpl::map([] (int chatIdType) { + return ChatIdLabel(chatIdType); + }) + ); CreateRightLabel( chatIdButton, std::move(chatIdText), @@ -647,13 +648,12 @@ void SetupKotatoOther(not_null container) { Ui::show(Box<::Kotato::RadioBox>( tr::ktg_settings_chat_id(tr::now), tr::ktg_settings_chat_id_desc(tr::now), - cShowChatId(), + ShowChatId(), 3, ChatIdLabel, [=] (int value) { - cSetShowChatId(value); + SetShowChatId(value); ::Kotato::JsonSettings::Write(); - Global::RefChatIDFormatChanged().notify(); })); }); diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp index ede2d1c2c..7707aae89 100644 --- a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp +++ b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp @@ -182,6 +182,14 @@ Pack CloudManager::packTypeFromId(const QString &id) const { return Pack::None; } +rpl::producer<> CloudManager::languageListChanged() const { + return _languageListChanged.events(); +} + +rpl::producer<> CloudManager::firstLanguageSuggestion() const { + return _firstLanguageSuggestion.events(); +} + void CloudManager::requestLangPackDifference(const QString &langId) { Expects(!langId.isEmpty()); @@ -251,7 +259,7 @@ void CloudManager::setSuggestedLanguage(const QString &langCode) { if (!_languageWasSuggested) { _languageWasSuggested = true; - _firstLanguageSuggestion.notify(); + _firstLanguageSuggestion.fire({}); if (Core::App().offerLegacyLangPackSwitch() && _langpack.id().isEmpty() @@ -311,7 +319,7 @@ void CloudManager::requestLanguageList() { } if (_languages != languages) { _languages = languages; - _languagesChanged.notify(); + _languageListChanged.fire({}); } _languagesRequestId = 0; }).fail([=](const MTP::Error &error) { @@ -324,9 +332,10 @@ void CloudManager::offerSwitchLangPack() { Expects(_offerSwitchToId != DefaultLanguageId()); if (!showOfferSwitchBox()) { - subscribe(languageListChanged(), [this] { + languageListChanged( + ) | rpl::start_with_next([=] { showOfferSwitchBox(); - }); + }, _lifetime); requestLanguageList(); } } diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.h b/Telegram/SourceFiles/lang/lang_cloud_manager.h index d8b53deb8..4d1a10097 100644 --- a/Telegram/SourceFiles/lang/lang_cloud_manager.h +++ b/Telegram/SourceFiles/lang/lang_cloud_manager.h @@ -22,7 +22,7 @@ struct Language; Language ParseLanguage(const MTPLangPackLanguage &data); -class CloudManager : public base::has_weak_ptr, private base::Subscriber { +class CloudManager : public base::has_weak_ptr { public: explicit CloudManager(Instance &langpack); @@ -32,9 +32,8 @@ public: const Languages &languageList() const { return _languages; } - base::Observable &languageListChanged() { - return _languagesChanged; - } + [[nodiscard]] rpl::producer<> languageListChanged() const; + [[nodiscard]] rpl::producer<> firstLanguageSuggestion() const; void requestLangPackDifference(const QString &langId); void applyLangPackDifference(const MTPLangPackDifference &difference); void setCurrentVersions(int version, int baseVersion); @@ -48,9 +47,6 @@ public: QString suggestedLanguage() const { return _suggestedLanguage; } - base::Observable &firstLanguageSuggestion() { - return _firstLanguageSuggestion; - } private: mtpRequestId &packRequestId(Pack pack); @@ -78,7 +74,6 @@ private: std::optional _api; Instance &_langpack; Languages _languages; - base::Observable _languagesChanged; mtpRequestId _langPackRequestId = 0; mtpRequestId _langPackBaseRequestId = 0; mtpRequestId _languagesRequestId = 0; @@ -88,7 +83,6 @@ private: QString _suggestedLanguage; bool _languageWasSuggested = false; - base::Observable _firstLanguageSuggestion; mtpRequestId _switchingToLanguageRequest = 0; QString _switchingToLanguageId; @@ -96,6 +90,9 @@ private: mtpRequestId _getKeysForSwitchRequestId = 0; + rpl::event_stream<> _languageListChanged; + rpl::event_stream<> _firstLanguageSuggestion; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/logs.cpp b/Telegram/SourceFiles/logs.cpp index 7ad8bbbda..37789816d 100644 --- a/Telegram/SourceFiles/logs.cpp +++ b/Telegram/SourceFiles/logs.cpp @@ -14,6 +14,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { std::atomic ThreadCounter/* = 0*/; +thread_local bool WritingEntryFlag/* = false*/; + +class WritingEntryScope final { +public: + WritingEntryScope() { + WritingEntryFlag = true; + } + ~WritingEntryScope() { + WritingEntryFlag = false; + } +}; } // namespace @@ -73,6 +84,8 @@ public: void closeMain() { QMutexLocker lock(_logsMutex(LogDataMain)); + WritingEntryScope scope; + const auto file = files[LogDataMain].get(); if (file && file->isOpen()) { file->close(); @@ -85,7 +98,7 @@ public: QString full() { const auto file = files[LogDataMain].get(); - if (!!file || !file->isOpen()) { + if (!file || !file->isOpen()) { return QString(); } @@ -98,6 +111,8 @@ public: void write(LogDataType type, const QString &msg) { QMutexLocker lock(_logsMutex(type)); + WritingEntryScope scope; + if (type != LogDataMain) { reopenDebug(); } @@ -323,6 +338,10 @@ bool DebugEnabled() { #endif } +bool WritingEntry() { + return WritingEntryFlag; +} + void start(not_null launcher) { Assert(LogsData == nullptr); diff --git a/Telegram/SourceFiles/logs.h b/Telegram/SourceFiles/logs.h index e42bf8b22..f45b07658 100644 --- a/Telegram/SourceFiles/logs.h +++ b/Telegram/SourceFiles/logs.h @@ -19,6 +19,7 @@ namespace Logs { void SetDebugEnabled(bool enabled); bool DebugEnabled(); +[[nodiscard]] bool WritingEntry(); void start(not_null launcher); bool started(); diff --git a/Telegram/SourceFiles/main/main_account.cpp b/Telegram/SourceFiles/main/main_account.cpp index 8f4569165..d60b5cb5d 100644 --- a/Telegram/SourceFiles/main/main_account.cpp +++ b/Telegram/SourceFiles/main/main_account.cpp @@ -31,7 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session_settings.h" #include "kotato/json_settings.h" -#include "facades.h" namespace Main { namespace { @@ -464,7 +463,7 @@ void Account::startMtp(std::unique_ptr config) { }); _mtp->setStateChangedHandler([=](MTP::ShiftedDcId dc, int32 state) { if (dc == _mtp->mainDcId()) { - Global::RefConnectionTypeChanged().notify(); + Core::App().settings().proxy().connectionTypeChangesNotify(); } }); _mtp->setSessionResetHandler([=](MTP::ShiftedDcId shiftedDcId) { diff --git a/Telegram/SourceFiles/main/main_domain.cpp b/Telegram/SourceFiles/main/main_domain.cpp index 672da38dc..055f3873e 100644 --- a/Telegram/SourceFiles/main/main_domain.cpp +++ b/Telegram/SourceFiles/main/main_domain.cpp @@ -340,7 +340,7 @@ bool Domain::removePasscodeIfEmpty() { return false; } Local::reset(); - if (!Global::LocalPasscode()) { + if (!_local->hasLocalPasscode()) { return false; } // We completely logged out, remove the passcode if it was there. diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 62e7584dc..ef71eb359 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_updates.h" #include "api/api_send_progress.h" -#include "core/application.h" #include "main/main_account.h" #include "main/main_domain.h" #include "main/main_session_settings.h" @@ -34,7 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "calls/calls_instance.h" #include "support/support_helper.h" -#include "facades.h" #ifndef TDESKTOP_DISABLE_SPELLCHECK #include "chat_helpers/spellchecker_common.h" @@ -88,10 +86,6 @@ Session::Session( , _saveSettingsTimer([=] { saveSettings(); }) { Expects(_settings != nullptr); - subscribe(Global::RefConnectionTypeChanged(), [=] { - _api->refreshTopPromotion(); - }); - _api->refreshTopPromotion(); _api->requestTermsUpdate(); _api->requestFullPeer(_user); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 3e89b7cb6..07cac9363 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_document_media.h" +#include "data/data_document_resolver.h" #include "data/data_web_page.h" #include "data/data_game.h" #include "data/data_peer_values.h" @@ -34,7 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/special_buttons.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" -#include "ui/toast/toast.h" +#include "ui/toasts/common_toasts.h" #include "ui/widgets/dropdown_menu.h" #include "ui/image/image.h" #include "ui/focus_persister.h" @@ -68,7 +69,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "lang/lang_cloud_manager.h" #include "boxes/add_contact_box.h" -#include "storage/file_upload.h" #include "mainwindow.h" #include "inline_bots/inline_bot_layout_item.h" #include "boxes/confirm_box.h" @@ -94,8 +94,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/changelogs.h" #include "base/unixtime.h" +#include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_top_bar.h" +#include "calls/group/calls_group_call.h" #include "export/export_settings.h" #include "export/export_manager.h" #include "export/view/export_view_top_bar.h" @@ -379,10 +381,13 @@ MainWidget::MainWidget( } }); - subscribe(Adaptive::Changed(), [this]() { handleAdaptiveLayoutUpdate(); }); + _controller->adaptive().changes( + ) | rpl::start_with_next([=] { + handleAdaptiveLayoutUpdate(); + }, lifetime()); _dialogs->show(); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { _history->hide(); } else { _history->show(); @@ -415,7 +420,7 @@ void MainWidget::setupConnectingWidget() { _connecting = std::make_unique( this, &session().account(), - Window::AdaptiveIsOneColumn() | rpl::map(!_1)); + _controller->adaptive().oneColumnValue() | rpl::map(!_1)); } not_null MainWidget::floatPlayerDelegate() { @@ -428,7 +433,7 @@ not_null MainWidget::floatPlayerWidget() { auto MainWidget::floatPlayerGetSection(Window::Column column) -> not_null { - if (Adaptive::ThreeColumn()) { + if (isThreeColumn()) { if (column == Window::Column::First) { return _dialogs; } else if (column == Window::Column::Second @@ -439,7 +444,7 @@ auto MainWidget::floatPlayerGetSection(Window::Column column) return _history; } return _thirdSection; - } else if (Adaptive::Normal()) { + } else if (isNormalColumn()) { if (column == Window::Column::First) { return _dialogs; } else if (_mainSection) { @@ -447,11 +452,11 @@ auto MainWidget::floatPlayerGetSection(Window::Column column) } return _history; } - if (Adaptive::OneColumn() && selectingPeer()) { + if (isOneColumn() && selectingPeer()) { return _dialogs; } else if (_mainSection) { return _mainSection; - } else if (!Adaptive::OneColumn() || _history->peer()) { + } else if (!isOneColumn() || _history->peer()) { return _history; } return _dialogs; @@ -460,7 +465,7 @@ auto MainWidget::floatPlayerGetSection(Window::Column column) void MainWidget::floatPlayerEnumerateSections(Fn widget, Window::Column widgetColumn)> callback) { - if (Adaptive::ThreeColumn()) { + if (isThreeColumn()) { callback(_dialogs, Window::Column::First); if (_mainSection) { callback(_mainSection, Window::Column::Second); @@ -470,7 +475,7 @@ void MainWidget::floatPlayerEnumerateSections(Fnpeer()) { + } else if (!isOneColumn() || _history->peer()) { callback(_history, Window::Column::Second); } else { callback(_dialogs, Window::Column::First); @@ -506,6 +511,11 @@ void MainWidget::floatPlayerClosed(FullMsgId itemId) { } } +void MainWidget::floatPlayerDoubleClickEvent( + not_null item) { + _controller->showPeerHistoryAtItem(item); +} + bool MainWidget::setForwardDraft(PeerId peerId, MessageIdsList &&items) { Expects(peerId != 0); @@ -648,7 +658,7 @@ void MainWidget::clearHider(not_null instance) { _hider.release(); controller()->setSelectingPeer(false); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { if (_mainSection || (_history->peer() && _history->peer()->id)) { auto animationParams = ([=] { if (_mainSection) { @@ -663,6 +673,8 @@ void MainWidget::clearHider(not_null instance) { _history->showAnimated(Window::SlideDirection::FromRight, animationParams); } floatPlayerCheckVisibility(); + } else { + _dialogs->updateForwardBar(); } } } @@ -675,6 +687,11 @@ void MainWidget::hiderLayer(base::unique_qptr hider) { _hider = std::move(hider); controller()->setSelectingPeer(true); + _dialogs->closeForwardBarRequests( + ) | rpl::start_with_next([=] { + _hider->startHide(); + }, _hider->lifetime()); + _hider->setParent(this); _hider->hidden( @@ -689,7 +706,7 @@ void MainWidget::hiderLayer(base::unique_qptr hider) { _dialogs->onCancelSearch(); }, _hider->lifetime()); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { dialogsToUp(); _hider->hide(); @@ -720,14 +737,16 @@ void MainWidget::showForwardLayer(MessageIdsList &&items) { hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), - std::move(callback))); + std::move(callback), + _controller->adaptive().oneColumnValue())); } void MainWidget::showSendPathsLayer() { hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), - [=](PeerId peer) { return sendPaths(peer); })); + [=](PeerId peer) { return sendPaths(peer); }, + _controller->adaptive().oneColumnValue())); if (_hider) { connect(_hider, &QObject::destroyed, [] { cSetSendPaths(QStringList()); @@ -735,36 +754,6 @@ void MainWidget::showSendPathsLayer() { } } -void MainWidget::cancelUploadLayer(not_null item) { - const auto itemId = item->fullId(); - session().uploader().pause(itemId); - const auto stopUpload = [=] { - Ui::hideLayer(); - auto &data = session().data(); - if (const auto item = data.message(itemId)) { - if (!item->isEditingMedia()) { - const auto history = item->history(); - item->destroy(); - history->requestChatListMessage(); - } else { - item->returnSavedMedia(); - session().uploader().cancel(item->fullId()); - } - data.sendHistoryChangeNotifications(); - } - session().uploader().unpause(); - }; - const auto continueUpload = [=] { - session().uploader().unpause(); - }; - Ui::show(Box( - tr::lng_selected_cancel_sure_this(tr::now), - tr::lng_selected_upload_stop(tr::now), - tr::lng_continue(tr::now), - stopUpload, - continueUpload)); -} - void MainWidget::deletePhotoLayer(PhotoData *photo) { if (!photo) return; Ui::show(Box(tr::lng_delete_photo_sure(tr::now), tr::lng_box_delete(tr::now), crl::guard(this, [=] { @@ -784,7 +773,8 @@ void MainWidget::shareUrlLayer(const QString &url, const QString &text) { hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), - std::move(callback))); + std::move(callback), + _controller->adaptive().oneColumnValue())); } void MainWidget::inlineSwitchLayer(const QString &botAndQuery) { @@ -794,7 +784,8 @@ void MainWidget::inlineSwitchLayer(const QString &botAndQuery) { hiderLayer(base::make_unique_q( this, tr::lng_inline_switch_choose(tr::now), - std::move(callback))); + std::move(callback), + _controller->adaptive().oneColumnValue())); } bool MainWidget::selectingPeer() const { @@ -865,7 +856,7 @@ bool MainWidget::insertBotCommand(const QString &cmd) { void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat, UserData *from) { _dialogs->searchMessages(query, inChat, from); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { Ui::showChatsList(&session()); } else { _dialogs->setInnerFocus(); @@ -884,10 +875,12 @@ void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) { if (const auto item = session().data().message(state.id.contextId())) { session().data().requestItemRepaint(item); } - if (const auto items = InlineBots::Layout::documentItems()) { - if (const auto i = items->find(document); i != items->end()) { - for (const auto item : i->second) { - item->update(); + if (document) { + if (const auto items = InlineBots::Layout::documentItems()) { + if (const auto i = items->find(document); i != items->end()) { + for (const auto item : i->second) { + item->update(); + } } } } @@ -916,7 +909,8 @@ void MainWidget::createPlayer() { if (!_player) { _player.create( this, - object_ptr(this, &session())); + object_ptr(this, &session()), + _controller->adaptive().oneColumnValue()); rpl::merge( _player->heightValue() | rpl::map_to(true), _player->shownValue() @@ -924,6 +918,10 @@ void MainWidget::createPlayer() { [this] { playerHeightUpdated(); }, _player->lifetime()); _player->entity()->setCloseCallback([=] { closeBothPlayers(); }); + _player->entity()->setShowItemCallback([=]( + not_null item) { + _controller->showPeerHistoryAtItem(item); + }); _playerVolume.create(this, _controller); _player->entity()->volumeWidgetCreated(_playerVolume); orderWidgets(); @@ -1082,7 +1080,8 @@ void MainWidget::setCurrentExportView(Export::View::PanelController *view) { void MainWidget::createExportTopBar(Export::View::Content &&data) { _exportTopBar.create( this, - object_ptr(this, std::move(data))); + object_ptr(this, std::move(data)), + _controller->adaptive().oneColumnValue()); _exportTopBar->entity()->clicks( ) | rpl::start_with_next([=] { if (_currentExportView) { @@ -1429,6 +1428,9 @@ void MainWidget::showChooseReportMessages( peer->id, SectionShow::Way::Forward, ShowForChooseMessagesMsgId); + Ui::ShowMultilineToast({ + .text = { tr::lng_report_please_select_messages(tr::now) }, + }); } void MainWidget::clearChooseReportMessages() { @@ -1529,18 +1531,18 @@ void MainWidget::ui_showPeerHistory( return false; } if (!peerId) { - if (Adaptive::OneColumn()) { + if (isOneColumn()) { return _dialogs->isHidden(); } else { return false; } } if (_history->isHidden()) { - if (!Adaptive::OneColumn() && way == Way::ClearStack) { + if (!isOneColumn() && way == Way::ClearStack) { return false; } return (_mainSection != nullptr) - || (Adaptive::OneColumn() && !_dialogs->isHidden()); + || (isOneColumn() && !_dialogs->isHidden()); } if (back || way == Way::Forward) { return true; @@ -1561,7 +1563,7 @@ void MainWidget::ui_showPeerHistory( _history->showHistory(peerId, showAtMsgId); auto noPeer = !_history->peer(); - auto onlyDialogs = noPeer && Adaptive::OneColumn(); + auto onlyDialogs = noPeer && isOneColumn(); _mainSection.destroy(); updateControlsGeometry(); @@ -1585,7 +1587,7 @@ void MainWidget::ui_showPeerHistory( if (nowActivePeer && nowActivePeer != wasActivePeer) { _viewsIncremented.remove(nowActivePeer); } - if (Adaptive::OneColumn() && !_dialogs->isHidden()) { + if (isOneColumn() && !_dialogs->isHidden()) { _dialogs->hide(); } if (!_a_show.animating()) { @@ -1699,7 +1701,7 @@ Window::SectionSlideParams MainWidget::prepareShowAnimation( bool willHaveTopBarShadow) { Window::SectionSlideParams result; result.withTopBarShadow = willHaveTopBarShadow; - if (selectingPeer() && Adaptive::OneColumn()) { + if (selectingPeer() && isOneColumn()) { result.withTopBarShadow = false; } else if (_mainSection) { if (!_mainSection->hasTopBarShadow()) { @@ -1723,7 +1725,7 @@ Window::SectionSlideParams MainWidget::prepareShowAnimation( } auto sectionTop = getMainSectionTop(); - if (selectingPeer() && Adaptive::OneColumn()) { + if (selectingPeer() && isOneColumn()) { result.oldContentCache = Ui::GrabWidget(this, QRect( 0, sectionTop, @@ -1731,7 +1733,7 @@ Window::SectionSlideParams MainWidget::prepareShowAnimation( height() - sectionTop)); } else if (_mainSection) { result.oldContentCache = _mainSection->grabForShowAnimation(result); - } else if (!Adaptive::OneColumn() || !_history->isHidden()) { + } else if (!isOneColumn() || !_history->isHidden()) { result.oldContentCache = _history->grabForShowAnimation(result); } else { result.oldContentCache = Ui::GrabWidget(this, QRect( @@ -1779,7 +1781,7 @@ void MainWidget::showNewSection( thirdSectionTop, st::columnMinimalWidthThird, height() - thirdSectionTop); - auto newThirdSection = (Adaptive::ThreeColumn() && params.thirdColumn) + auto newThirdSection = (isThreeColumn() && params.thirdColumn) ? memento->createWidget( this, _controller, @@ -1815,7 +1817,7 @@ void MainWidget::showNewSection( : memento->createWidget( this, _controller, - Adaptive::OneColumn() ? Column::First : Column::Second, + isOneColumn() ? Column::First : Column::Second, newMainGeometry); Assert(newMainSection || newThirdSection); @@ -1826,9 +1828,9 @@ void MainWidget::showNewSection( || memento->instant()) { return false; } - if (!Adaptive::OneColumn() && params.way == SectionShow::Way::ClearStack) { + if (!isOneColumn() && params.way == SectionShow::Way::ClearStack) { return false; - } else if (Adaptive::OneColumn() + } else if (isOneColumn() || (newThirdSection && _thirdSection) || (newMainSection && isMainSectionShown())) { return true; @@ -1864,7 +1866,7 @@ void MainWidget::showNewSection( _history->finishAnimating(); _history->showHistory(0, 0); _history->hide(); - if (Adaptive::OneColumn()) _dialogs->hide(); + if (isOneColumn()) _dialogs->hide(); } if (animationParams) { @@ -1872,7 +1874,7 @@ void MainWidget::showNewSection( auto direction = (back || settingSection->forceAnimateBack()) ? Window::SlideDirection::FromLeft : Window::SlideDirection::FromRight; - if (Adaptive::OneColumn()) { + if (isOneColumn()) { _controller->removeLayerBlackout(); } settingSection->showAnimated(direction, animationParams); @@ -2047,7 +2049,7 @@ QPixmap MainWidget::grabForShowAnimation(const Window::SectionSlideParams ¶m } auto sectionTop = getMainSectionTop(); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { result = Ui::GrabWidget(this, QRect( 0, sectionTop, @@ -2184,7 +2186,7 @@ void MainWidget::showAll() { cSetPasswordRecovered(false); Ui::show(Box(tr::lng_signin_password_removed(tr::now))); } - if (Adaptive::OneColumn()) { + if (isOneColumn()) { _sideShadow->hide(); if (_hider) { _hider->hide(); @@ -2259,7 +2261,7 @@ void MainWidget::updateControlsGeometry() { if (!_a_dialogsWidth.animating()) { _dialogs->stopWidthAnimation(); } - if (Adaptive::ThreeColumn()) { + if (isThreeColumn()) { if (!_thirdSection && !_controller->takeThirdSectionFromLayer()) { auto params = Window::SectionShow( @@ -2287,7 +2289,7 @@ void MainWidget::updateControlsGeometry() { } auto mainSectionTop = getMainSectionTop(); auto dialogsWidth = qRound(_a_dialogsWidth.value(_dialogsWidth)); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { if (_callTopBar) { _callTopBar->resizeToWidth(dialogsWidth); _callTopBar->moveToLeft(0, 0); @@ -2368,7 +2370,7 @@ void MainWidget::updateControlsGeometry() { } void MainWidget::refreshResizeAreas() { - if (!Adaptive::OneColumn()) { + if (!isOneColumn()) { ensureFirstColumnResizeAreaCreated(); _firstColumnResizeArea->setGeometryToLeft( _history->x(), @@ -2379,7 +2381,7 @@ void MainWidget::refreshResizeAreas() { _firstColumnResizeArea.destroy(); } - if (Adaptive::ThreeColumn() && _thirdSection) { + if (isThreeColumn() && _thirdSection) { ensureThirdColumnResizeAreaCreated(); _thirdColumnResizeArea->setGeometryToLeft( _thirdSection->x(), @@ -2417,7 +2419,7 @@ void MainWidget::ensureFirstColumnResizeAreaCreated() { Core::App().settings().setDialogsWidthRatio(newRatio); }; auto moveFinishedCallback = [=] { - if (Adaptive::OneColumn()) { + if (isOneColumn()) { return; } if (Core::App().settings().dialogsWidthRatio() > 0) { @@ -2441,7 +2443,7 @@ void MainWidget::ensureThirdColumnResizeAreaCreated() { Core::App().settings().setThirdColumnWidth(newWidth); }; auto moveFinishedCallback = [=] { - if (!Adaptive::ThreeColumn() || !_thirdSection) { + if (!isThreeColumn() || !_thirdSection) { return; } Core::App().settings().setThirdColumnWidth(std::clamp( @@ -2516,7 +2518,7 @@ void MainWidget::updateThirdColumnToCurrentChat( // // Like in _controller->showPeerInfo() // - if (Adaptive::ThreeColumn() + if (isThreeColumn() && !settings.thirdSectionInfoEnabled()) { settings.setThirdSectionInfoEnabled(true); Core::App().saveSettingsDelayed(); @@ -2532,7 +2534,7 @@ void MainWidget::updateThirdColumnToCurrentChat( ? _mainSection->pushTabbedSelectorToThirdSection(peer, params) : _history->pushTabbedSelectorToThirdSection(peer, params); }; - if (Adaptive::ThreeColumn() + if (isThreeColumn() && settings.tabbedSelectorSectionEnabled() && key) { if (!canWrite) { @@ -2552,7 +2554,7 @@ void MainWidget::updateThirdColumnToCurrentChat( _thirdShadow.destroy(); updateControlsGeometry(); } - } else if (Adaptive::ThreeColumn() + } else if (isThreeColumn() && settings.thirdSectionInfoEnabled()) { switchInfoFast(); } @@ -2619,7 +2621,7 @@ bool MainWidget::eventFilter(QObject *o, QEvent *e) { void MainWidget::handleAdaptiveLayoutUpdate() { showAll(); - _sideShadow->setVisible(!Adaptive::OneColumn()); + _sideShadow->setVisible(!isOneColumn()); if (_player) { _player->updateAdaptiveLayout(); } @@ -2646,7 +2648,7 @@ void MainWidget::updateWindowAdaptiveLayout() { // Check if we are in a single-column layout in a wide enough window // for the normal layout. If so, switch to the normal layout. - if (layout.windowLayout == Adaptive::WindowLayout::OneColumn) { + if (layout.windowLayout == Window::Adaptive::WindowLayout::OneColumn) { auto chatWidth = layout.chatWidth; //if (session().settings().tabbedSelectorSectionEnabled() // && chatWidth >= _history->minimalWidthForTabbedSelectorSection()) { @@ -2656,7 +2658,7 @@ void MainWidget::updateWindowAdaptiveLayout() { + st::columnMinimalWidthMain; if (chatWidth >= minimalNormalWidth) { // Switch layout back to normal in a wide enough window. - layout.windowLayout = Adaptive::WindowLayout::Normal; + layout.windowLayout = Window::Adaptive::WindowLayout::Normal; layout.dialogsWidth = st::columnMinimalWidthLeft; layout.chatWidth = layout.bodyWidth - layout.dialogsWidth; dialogsWidthRatio = float64(layout.dialogsWidth) / layout.bodyWidth; @@ -2666,7 +2668,7 @@ void MainWidget::updateWindowAdaptiveLayout() { // Check if we are going to create the third column and shrink the // dialogs widget to provide a wide enough chat history column. // Don't shrink the column on the first call, when window is inited. - if (layout.windowLayout == Adaptive::WindowLayout::ThreeColumn + if (layout.windowLayout == Window::Adaptive::WindowLayout::ThreeColumn && _controller->widget()->positionInited()) { //auto chatWidth = layout.chatWidth; //if (_history->willSwitchToTabbedSelectorWithWidth(chatWidth)) { @@ -2687,17 +2689,14 @@ void MainWidget::updateWindowAdaptiveLayout() { Core::App().settings().setDialogsWidthRatio(dialogsWidthRatio); - auto useSmallColumnWidth = !Adaptive::OneColumn() + auto useSmallColumnWidth = !isOneColumn() && !dialogsWidthRatio && !_controller->forceWideDialogs(); _dialogsWidth = useSmallColumnWidth ? _controller->dialogsSmallColumnWidth() : layout.dialogsWidth; _thirdColumnWidth = layout.thirdWidth; - if (layout.windowLayout != Global::AdaptiveWindowLayout()) { - Global::SetAdaptiveWindowLayout(layout.windowLayout); - Adaptive::Changed().notify(true); - } + _controller->adaptive().setWindowLayout(layout.windowLayout); } int MainWidget::backgroundFromY() const { @@ -2709,7 +2708,7 @@ void MainWidget::searchInChat(Dialogs::Key chat) { _controller->closeFolder(); } _dialogs->searchInChat(chat); - if (Adaptive::OneColumn()) { + if (isOneColumn()) { Ui::showChatsList(&session()); } else { _dialogs->setInnerFocus(); @@ -2778,6 +2777,18 @@ void MainWidget::saveFieldToHistoryLocalDraft() { _history->saveFieldToHistoryLocalDraft(); } +bool MainWidget::isOneColumn() const { + return _controller->adaptive().isOneColumn(); +} + +bool MainWidget::isNormalColumn() const { + return _controller->adaptive().isNormal(); +} + +bool MainWidget::isThreeColumn() const { + return _controller->adaptive().isThreeColumn(); +} + namespace App { MainWidget *main() { diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index df9ef6950..66ab6325c 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -160,7 +160,6 @@ public: void showForwardLayer(MessageIdsList &&items); void showSendPathsLayer(); - void cancelUploadLayer(not_null item); void shareUrlLayer(const QString &url, const QString &text); void inlineSwitchLayer(const QString &botAndQuery); void hiderLayer(base::unique_qptr h); @@ -324,6 +323,8 @@ private: Window::Column widgetColumn)> callback) override; bool floatPlayerIsVisible(not_null item) override; void floatPlayerClosed(FullMsgId itemId); + void floatPlayerDoubleClickEvent( + not_null item) override; void viewsIncrementDone( QVector ids, @@ -349,6 +350,10 @@ private: void handleHistoryBack(); + bool isOneColumn() const; + bool isNormalColumn() const; + bool isThreeColumn() const; + const not_null _controller; MTP::Sender _api; diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp index d36d499d4..6d56608cb 100644 --- a/Telegram/SourceFiles/mainwindow.cpp +++ b/Telegram/SourceFiles/mainwindow.cpp @@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" // Account::sessionValue. #include "main/main_domain.h" #include "mainwidget.h" +#include "media/system_media_controls_manager.h" #include "boxes/confirm_box.h" #include "boxes/connection_box.h" #include "storage/storage_account.h" @@ -82,6 +83,7 @@ void FeedLangTestingKey(int key) { MainWindow::MainWindow(not_null controller) : Platform::MainWindow(controller) { + updateIconCache(); resize(st::windowDefaultWidth, st::windowDefaultHeight); @@ -122,6 +124,11 @@ void MainWindow::initHook() { this, [=] { checkHistoryActivation(); }, Qt::QueuedConnection); + + if (Media::SystemMediaControlsManager::Supported()) { + using MediaManager = Media::SystemMediaControlsManager; + _mediaControlsManager = std::make_unique(&controller()); + } } void MainWindow::createTrayIconMenu() { @@ -166,7 +173,8 @@ void MainWindow::createTrayIconMenu() { } void MainWindow::applyInitialWorkMode() { - Global::RefWorkMode().setForced(Global::WorkMode().value(), true); + const auto workMode = Core::App().settings().workMode(); + workmodeUpdated(workMode); if (Core::App().settings().windowPosition().maximized) { DEBUG_LOG(("Window Pos: First show, setting maximized.")); @@ -179,8 +187,8 @@ void MainWindow::applyInitialWorkMode() { const auto minimizeAndHide = [=] { DEBUG_LOG(("Window Pos: First show, setting minimized after.")); setWindowState(windowState() | Qt::WindowMinimized); - if (Global::WorkMode().value() == dbiwmTrayOnly - || Global::WorkMode().value() == dbiwmWindowAndTray) { + if (workMode == Core::Settings::WorkMode::TrayOnly + || workMode == Core::Settings::WorkMode::WindowAndTray) { hide(); } }; @@ -285,7 +293,11 @@ void MainWindow::setupIntro(Intro::EnterPoint point) { auto bg = animated ? grabInner() : QPixmap(); destroyLayer(); - auto created = object_ptr(bodyWidget(), &account(), point); + auto created = object_ptr( + bodyWidget(), + &controller(), + &account(), + point); created->showSettingsRequested( ) | rpl::start_with_next([=] { showSettings(); @@ -763,13 +775,13 @@ void MainWindow::toggleDisplayNotifyFromTray() { } account().session().saveSettings(); using Change = Window::Notifications::ChangeType; - auto &changes = Core::App().notifications().settingsChanged(); - changes.notify(Change::DesktopEnabled); + auto ¬ifications = Core::App().notifications(); + notifications.notifySettingsChanged(Change::DesktopEnabled); if (soundNotifyChanged) { - changes.notify(Change::SoundEnabled); + notifications.notifySettingsChanged(Change::SoundEnabled); } if (flashBounceNotifyChanged) { - changes.notify(Change::FlashBounceEnabled); + notifications.notifySettingsChanged(Change::FlashBounceEnabled); } } @@ -787,8 +799,8 @@ void MainWindow::toggleSoundNotifyFromTray() { settings.setSoundNotify(!settings.soundNotify()); account().session().saveSettings(); using Change = Window::Notifications::ChangeType; - auto &changes = Core::App().notifications().settingsChanged(); - changes.notify(Change::SoundEnabled); + auto ¬ifications = Core::App().notifications(); + notifications.notifySettingsChanged(Change::SoundEnabled); } void MainWindow::closeEvent(QCloseEvent *e) { @@ -991,7 +1003,7 @@ void MainWindow::sendPaths() { } } -void MainWindow::updateIsActiveHook() { +void MainWindow::activeChangedHook() { if (const auto controller = sessionController()) { controller->session().updates().updateOnline(); } diff --git a/Telegram/SourceFiles/mainwindow.h b/Telegram/SourceFiles/mainwindow.h index d117e2b66..c48b2daba 100644 --- a/Telegram/SourceFiles/mainwindow.h +++ b/Telegram/SourceFiles/mainwindow.h @@ -19,6 +19,10 @@ class Widget; enum class EnterPoint : uchar; } // namespace Intro +namespace Media { +class SystemMediaControlsManager; +} // namespace Media + namespace Window { class MediaPreviewWidget; class SectionMemento; @@ -110,7 +114,7 @@ protected: void closeEvent(QCloseEvent *e) override; void initHook() override; - void updateIsActiveHook() override; + void activeChangedHook() override; void clearWidgetsHook() override; private: @@ -132,6 +136,8 @@ private: QPixmap grabInner(); + std::unique_ptr _mediaControlsManager; + QImage icon16, icon32, icon64, iconbig16, iconbig32, iconbig64; int _customIconId = 0; diff --git a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp index a5965e0d1..5e3d8a58d 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_capture.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_capture.cpp @@ -47,17 +47,17 @@ public: void start(Fn updated, Fn error); void stop(Fn callback = nullptr); - void timeout(); - private: - void processFrame(int32 offset, int32 framesize); + void process(); + + [[nodiscard]] bool processFrame(int32 offset, int32 framesize); void fail(); - void writeFrame(AVFrame *frame); + [[nodiscard]] bool writeFrame(AVFrame *frame); // Writes the packets till EAGAIN is got from av_receive_packet() // Returns number of packets written or -1 on error - int writePackets(); + [[nodiscard]] int writePackets(); Fn _updated; Fn _error; @@ -150,6 +150,7 @@ struct Instance::Inner::Private { AVCodec *codec = nullptr; AVCodecContext *codecContext = nullptr; bool opened = false; + bool processing = false; int srcSamples = 0; int dstSamples = 0; @@ -217,7 +218,7 @@ struct Instance::Inner::Private { Instance::Inner::Inner(QThread *thread) : d(std::make_unique()) -, _timer(thread, [=] { timeout(); }) { +, _timer(thread, [=] { process(); }) { moveToThread(thread); } @@ -226,10 +227,10 @@ Instance::Inner::~Inner() { } void Instance::Inner::fail() { - Expects(_error != nullptr); - stop(); - _error(); + if (const auto error = base::take(_error)) { + InvokeQueued(this, error); + } } void Instance::Inner::start(Fn updated, Fn error) { @@ -384,13 +385,21 @@ void Instance::Inner::stop(Fn callback) { } _timer.cancel(); - if (d->device) { + const auto needResult = (callback != nullptr); + const auto hadDevice = (d->device != nullptr); + if (hadDevice) { alcCaptureStop(d->device); - timeout(); // get last data + if (d->processing) { + Assert(!needResult); // stop in the middle of processing - error. + } else { + process(); // get last data + } + alcCaptureCloseDevice(d->device); + d->device = nullptr; } // Write what is left - if (!_captured.isEmpty()) { + if (needResult && !_captured.isEmpty()) { auto fadeSamples = kCaptureFadeInDuration * kCaptureFrequency / 1000; auto capturedSamples = static_cast(_captured.size() / sizeof(short)); if ((_captured.size() % sizeof(short)) || (d->fullSamples + capturedSamples < kCaptureFrequency) || (capturedSamples < fadeSamples)) { @@ -414,11 +423,13 @@ void Instance::Inner::stop(Fn callback) { int32 framesize = d->srcSamples * d->codecContext->channels * sizeof(short), encoded = 0; while (_captured.size() >= encoded + framesize) { - processFrame(encoded, framesize); + if (!processFrame(encoded, framesize)) { + break; + } encoded += framesize; } - writeFrame(nullptr); // drain the codec - if (encoded != _captured.size()) { + // Drain the codec. + if (!writeFrame(nullptr) || encoded != _captured.size()) { d->fullSamples = 0; d->dataPos = 0; d->data.clear(); @@ -436,14 +447,14 @@ void Instance::Inner::stop(Fn callback) { _captured = QByteArray(); // Finish stream - if (d->device) { + if (needResult && hadDevice) { av_write_trailer(d->fmtContext); } QByteArray result = d->fullSamples ? d->data : QByteArray(); VoiceWaveform waveform; qint32 samples = d->fullSamples; - if (samples && !d->waveform.isEmpty()) { + if (needResult && samples && !d->waveform.isEmpty()) { int64 count = d->waveform.size(), sum = 0; if (count >= Player::kWaveformSamplesCount) { QVector peaks; @@ -472,11 +483,7 @@ void Instance::Inner::stop(Fn callback) { } } } - if (d->device) { - alcCaptureStop(d->device); - alcCaptureCloseDevice(d->device); - d->device = nullptr; - + if (hadDevice) { if (d->codecContext) { avcodec_free_context(&d->codecContext); d->codecContext = nullptr; @@ -528,12 +535,17 @@ void Instance::Inner::stop(Fn callback) { d->waveform.clear(); } - if (callback) { + if (needResult) { callback({ result, waveform, samples }); } } -void Instance::Inner::timeout() { +void Instance::Inner::process() { + Expects(!d->processing); + + d->processing = true; + const auto guard = gsl::finally([&] { d->processing = false; }); + if (!d->device) { _timer.cancel(); return; @@ -582,7 +594,9 @@ void Instance::Inner::timeout() { // Write frames int32 framesize = d->srcSamples * d->codecContext->channels * sizeof(short), encoded = 0; while (uint32(_captured.size()) >= encoded + framesize + fadeSamples * sizeof(short)) { - processFrame(encoded, framesize); + if (!processFrame(encoded, framesize)) { + return; + } encoded += framesize; } @@ -597,13 +611,13 @@ void Instance::Inner::timeout() { } } -void Instance::Inner::processFrame(int32 offset, int32 framesize) { +bool Instance::Inner::processFrame(int32 offset, int32 framesize) { // Prepare audio frame if (framesize % sizeof(short)) { // in the middle of a sample LOG(("Audio Error: Bad framesize in writeFrame() for capture, framesize %1, %2").arg(framesize)); fail(); - return; + return false; } auto samplesCnt = static_cast(framesize / sizeof(short)); @@ -650,7 +664,7 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) { if ((res = av_samples_alloc(d->dstSamplesData, 0, d->codecContext->channels, d->dstSamples, d->codecContext->sample_fmt, 1)) < 0) { LOG(("Audio Error: Unable to av_samples_alloc for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); fail(); - return; + return false; } d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0); } @@ -658,70 +672,79 @@ void Instance::Inner::processFrame(int32 offset, int32 framesize) { if ((res = swr_convert(d->swrContext, d->dstSamplesData, d->dstSamples, (const uint8_t **)srcSamplesData, d->srcSamples)) < 0) { LOG(("Audio Error: Unable to swr_convert for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); fail(); - return; + return false; } // Write audio frame AVFrame *frame = av_frame_alloc(); + frame->format = d->codecContext->sample_fmt; + frame->channels = d->codecContext->channels; + frame->channel_layout = d->codecContext->channel_layout; + frame->sample_rate = d->codecContext->sample_rate; frame->nb_samples = d->dstSamples; frame->pts = av_rescale_q(d->fullSamples, AVRational { 1, d->codecContext->sample_rate }, d->codecContext->time_base); avcodec_fill_audio_frame(frame, d->codecContext->channels, d->codecContext->sample_fmt, d->dstSamplesData[0], d->dstSamplesSize, 0); - writeFrame(frame); + if (!writeFrame(frame)) { + return false; + } d->fullSamples += samplesCnt; av_frame_free(&frame); + return true; } -void Instance::Inner::writeFrame(AVFrame *frame) { +bool Instance::Inner::writeFrame(AVFrame *frame) { int res = 0; char err[AV_ERROR_MAX_STRING_SIZE] = { 0 }; res = avcodec_send_frame(d->codecContext, frame); if (res == AVERROR(EAGAIN)) { - int packetsWritten = writePackets(); + const auto packetsWritten = writePackets(); if (packetsWritten < 0) { if (frame && packetsWritten == AVERROR_EOF) { LOG(("Audio Error: EOF in packets received when EAGAIN was got in avcodec_send_frame()")); fail(); + return false; } - return; + return true; } else if (!packetsWritten) { LOG(("Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()")); fail(); - return; + return false; } res = avcodec_send_frame(d->codecContext, frame); } if (res < 0) { LOG(("Audio Error: Unable to avcodec_send_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); fail(); - return; + return false; } if (!frame) { // drain if ((res = writePackets()) != AVERROR_EOF) { LOG(("Audio Error: not EOF in packets received when draining the codec, result %1").arg(res)); fail(); + return false; } } + return true; } int Instance::Inner::writePackets() { - AVPacket pkt; - memset(&pkt, 0, sizeof(pkt)); // data and size must be 0; + AVPacket *pkt = av_packet_alloc(); + const auto guard = gsl::finally([&] { av_packet_free(&pkt); }); int res = 0; char err[AV_ERROR_MAX_STRING_SIZE] = { 0 }; int written = 0; do { - av_init_packet(&pkt); - if ((res = avcodec_receive_packet(d->codecContext, &pkt)) < 0) { + if ((res = avcodec_receive_packet(d->codecContext, pkt)) < 0) { if (res == AVERROR(EAGAIN)) { return written; } else if (res == AVERROR_EOF) { @@ -732,16 +755,16 @@ int Instance::Inner::writePackets() { return res; } - av_packet_rescale_ts(&pkt, d->codecContext->time_base, d->stream->time_base); - pkt.stream_index = d->stream->index; - if ((res = av_interleaved_write_frame(d->fmtContext, &pkt)) < 0) { + av_packet_rescale_ts(pkt, d->codecContext->time_base, d->stream->time_base); + pkt->stream_index = d->stream->index; + if ((res = av_interleaved_write_frame(d->fmtContext, pkt)) < 0) { LOG(("Audio Error: Unable to av_interleaved_write_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res))); fail(); return -1; } ++written; - av_packet_unref(&pkt); + av_packet_unref(pkt); } while (true); return written; } diff --git a/Telegram/SourceFiles/media/audio/media_audio_track.cpp b/Telegram/SourceFiles/media/audio/media_audio_track.cpp index 034aff421..fb9ccb48a 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_track.cpp +++ b/Telegram/SourceFiles/media/audio/media_audio_track.cpp @@ -292,9 +292,6 @@ void Instance::trackFinished(Track *track) { _updateTimer.cancel(); scheduleDetachIfNotUsed(); } - if (track->isLooping()) { - trackFinished().notify(track, true); - } } void Instance::detachTracks() { diff --git a/Telegram/SourceFiles/media/audio/media_audio_track.h b/Telegram/SourceFiles/media/audio/media_audio_track.h index b24b50b28..ca11ff7b0 100644 --- a/Telegram/SourceFiles/media/audio/media_audio_track.h +++ b/Telegram/SourceFiles/media/audio/media_audio_track.h @@ -96,10 +96,6 @@ public: std::unique_ptr createTrack(); - base::Observable &trackFinished() { - return _trackFinished; - } - void detachTracks(); void reattachTracks(); bool hasActiveTracks() const; @@ -119,7 +115,6 @@ private: private: std::set _tracks; - base::Observable _trackFinished; base::Timer _updateTimer; diff --git a/Telegram/SourceFiles/media/player/media_player_float.cpp b/Telegram/SourceFiles/media/player/media_player_float.cpp index 79de6f242..fea2b9012 100644 --- a/Telegram/SourceFiles/media/player/media_player_float.cpp +++ b/Telegram/SourceFiles/media/player/media_player_float.cpp @@ -35,15 +35,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Media { namespace Player { +using DoubleClickedCallback = Fn)>; + Float::Float( QWidget *parent, not_null item, Fn toggleCallback, - Fn draggedCallback) + Fn draggedCallback, + DoubleClickedCallback doubleClickedCallback) : RpWidget(parent) , _item(item) , _toggleCallback(std::move(toggleCallback)) -, _draggedCallback(std::move(draggedCallback)) { +, _draggedCallback(std::move(draggedCallback)) +, _doubleClickedCallback(std::move(doubleClickedCallback)) { auto media = _item->media(); Assert(media != nullptr); @@ -131,10 +135,10 @@ void Float::finishDrag(bool closed) { } void Float::mouseDoubleClickEvent(QMouseEvent *e) { - if (_item) { + if (_item && _doubleClickedCallback) { // Handle second click. pauseResume(); - Ui::showPeerHistoryAtItem(_item); + _doubleClickedCallback(_item); } } @@ -275,7 +279,8 @@ FloatController::Item::Item( not_null parent, not_null item, ToggleCallback toggle, - DraggedCallback dragged) + DraggedCallback dragged, + DoubleClickedCallback doubleClicked) : animationSide(RectPart::Right) , column(Window::Column::Second) , corner(RectPart::TopRight) @@ -287,7 +292,8 @@ FloatController::Item::Item( }, [=, dragged = std::move(dragged)](bool closed) { dragged(this, closed); - }) { + }, + std::move(doubleClicked)) { } FloatController::FloatController(not_null delegate) @@ -394,6 +400,9 @@ void FloatController::create(not_null item) { }, [=](not_null instance, bool closed) { finishDrag(instance, closed); + }, + [=](not_null item) { + _delegate->floatPlayerDoubleClickEvent(item); })); current()->column = Core::App().settings().floatPlayerColumn(); current()->corner = Core::App().settings().floatPlayerCorner(); diff --git a/Telegram/SourceFiles/media/player/media_player_float.h b/Telegram/SourceFiles/media/player/media_player_float.h index 731e54bb7..9edb4af41 100644 --- a/Telegram/SourceFiles/media/player/media_player_float.h +++ b/Telegram/SourceFiles/media/player/media_player_float.h @@ -38,7 +38,8 @@ public: QWidget *parent, not_null item, Fn toggleCallback, - Fn draggedCallback); + Fn draggedCallback, + Fn)> doubleClickedCallback); [[nodiscard]] HistoryItem *item() const { return _item; @@ -101,6 +102,7 @@ private: bool _drag = false; QPoint _dragLocalPoint; Fn _draggedCallback; + Fn)> _doubleClickedCallback; }; @@ -138,6 +140,9 @@ public: virtual rpl::producer<> floatPlayerAreaUpdates() { return _areaUpdates.events(); } + virtual void floatPlayerDoubleClickEvent( + not_null item) { + } struct FloatPlayerFilterWheelEventRequest { not_null object; @@ -205,7 +210,8 @@ private: not_null parent, not_null item, ToggleCallback toggle, - DraggedCallback dragged); + DraggedCallback dragged, + Fn)> doubleClicked); bool hiddenByWidget = false; bool hiddenByHistory = false; diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index f483ce0ec..46c848749 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_session.h" #include "data/data_streaming.h" +#include "data/data_file_click_handler.h" #include "media/audio/media_audio.h" #include "media/audio/media_audio_capture.h" #include "media/streaming/media_streaming_instance.h" @@ -411,6 +412,16 @@ rpl::producer<> Media::Player::Instance::startsPlay( }) | rpl::to_empty; } +auto Media::Player::Instance::seekingChanges(AudioMsgId::Type type) const +-> rpl::producer { + return _seekingChanges.events( + ) | rpl::filter([=](SeekingChanges data) { + return data.type == type; + }) | rpl::map([](SeekingChanges data) { + return data.seeking; + }); +} + not_null instance() { Expects(SingleInstance != nullptr); return SingleInstance; @@ -625,6 +636,7 @@ void Instance::startSeeking(AudioMsgId::Type type) { } pause(type); emitUpdate(type); + _seekingChanges.fire({ .seeking = Seeking::Start, .type = type }); } void Instance::finishSeeking(AudioMsgId::Type type, float64 progress) { @@ -643,6 +655,7 @@ void Instance::finishSeeking(AudioMsgId::Type type, float64 progress) { } } cancelSeeking(type); + _seekingChanges.fire({ .seeking = Seeking::Finish, .type = type }); } void Instance::cancelSeeking(AudioMsgId::Type type) { @@ -650,6 +663,7 @@ void Instance::cancelSeeking(AudioMsgId::Type type) { data->seeking = AudioMsgId(); } emitUpdate(type); + _seekingChanges.fire({ .seeking = Seeking::Cancel, .type = type }); } void Instance::updateVoicePlaybackSpeed() { diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index 07cba2a5b..c88054eda 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -51,6 +51,12 @@ not_null instance(); class Instance : private base::Subscriber { public: + enum class Seeking { + Start, + Finish, + Cancel, + }; + void play(AudioMsgId::Type type); void pause(AudioMsgId::Type type); void stop(AudioMsgId::Type type); @@ -155,6 +161,8 @@ public: rpl::producer<> stops(AudioMsgId::Type type) const; rpl::producer<> startsPlay(AudioMsgId::Type type) const; + rpl::producer seekingChanges(AudioMsgId::Type type) const; + bool pauseGifByRoundVideo() const; void documentLoadProgress(DocumentData *document); @@ -189,6 +197,11 @@ private: std::unique_ptr streamed; }; + struct SeekingChanges { + Seeking seeking; + AudioMsgId::Type type; + }; + Instance(); ~Instance(); @@ -269,6 +282,7 @@ private: rpl::event_stream _playerStopped; rpl::event_stream _playerStartedPlay; rpl::event_stream _updatedNotifier; + rpl::event_stream _seekingChanges; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/media/player/media_player_widget.cpp b/Telegram/SourceFiles/media/player/media_player_widget.cpp index 093e4154f..f95653626 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.cpp +++ b/Telegram/SourceFiles/media/player/media_player_widget.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/effects/ripple_animation.h" #include "ui/text/format_values.h" +#include "ui/text/format_song_document_name.h" #include "lang/lang_keys.h" #include "media/audio/media_audio.h" #include "media/view/media_view_playback_progress.h" @@ -205,6 +206,11 @@ void Widget::setCloseCallback(Fn callback) { _close->setClickedCallback([this] { stopAndClose(); }); } +void Widget::setShowItemCallback( + Fn)> callback) { + _showItemCallback = std::move(callback); +} + void Widget::stopAndClose() { _voiceIsActive = false; if (_type == AudioMsgId::Type::Voice) { @@ -310,15 +316,16 @@ void Widget::mousePressEvent(QMouseEvent *e) { void Widget::mouseReleaseEvent(QMouseEvent *e) { if (auto downLabels = base::take(_labelsDown)) { - if (_labelsOver == downLabels) { - if (_type == AudioMsgId::Type::Voice) { - const auto current = instance()->current(_type); - const auto document = current.audio(); - const auto context = current.contextId(); - if (document && context) { - if (const auto item = document->owner().message(context)) { - Ui::showPeerHistoryAtItem(item); - } + if (_labelsOver != downLabels) { + return; + } + if (_type == AudioMsgId::Type::Voice) { + const auto current = instance()->current(_type); + const auto document = current.audio(); + const auto context = current.contextId(); + if (document && context && _showItemCallback) { + if (const auto item = document->owner().message(context)) { + _showItemCallback(item); } } } @@ -565,26 +572,8 @@ void Widget::handleSongChange() { textWithEntities.text = tr::lng_media_audio(tr::now); } } else { - const auto song = document->song(); - if (!song || song->performer.isEmpty()) { - textWithEntities.text = (!song || song->title.isEmpty()) - ? (document->filename().isEmpty() - ? qsl("Unknown Track") - : document->filename()) - : song->title; - } else { - auto title = song->title.isEmpty() - ? qsl("Unknown Track") - : TextUtilities::Clean(song->title); - auto dash = QString::fromUtf8(" \xe2\x80\x93 "); - textWithEntities.text = song->performer + dash + title; - textWithEntities.entities.append({ - EntityType::Semibold, - 0, - song->performer.size(), - QString() - }); - } + textWithEntities = Ui::Text::FormatSongNameFor(document) + .textWithEntities(true); } _nameLabel->setMarkedText(textWithEntities); diff --git a/Telegram/SourceFiles/media/player/media_player_widget.h b/Telegram/SourceFiles/media/player/media_player_widget.h index 7e5dc8b8d..80f67cf6a 100644 --- a/Telegram/SourceFiles/media/player/media_player_widget.h +++ b/Telegram/SourceFiles/media/player/media_player_widget.h @@ -42,6 +42,7 @@ public: Widget(QWidget *parent, not_null session); void setCloseCallback(Fn callback); + void setShowItemCallback(Fn)> callback); void stopAndClose(); void setShadowGeometryToLeft(int x, int y, int w, int h); void showShadow(); @@ -103,6 +104,7 @@ private: AudioMsgId _lastSongId; bool _voiceIsActive = false; Fn _closeCallback; + Fn)> _showItemCallback; bool _labelsOver = false; bool _labelsDown = false; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_common.h b/Telegram/SourceFiles/media/streaming/media_streaming_common.h index fd564e02b..d7a92c4ce 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_common.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_common.h @@ -119,6 +119,7 @@ struct FrameRequest { QSize outer; ImageRoundRadius radius = ImageRoundRadius(); RectParts corners = RectPart::AllCorners; + bool requireARGB32 = true; bool strict = true; static FrameRequest NonStrict() { @@ -135,16 +136,44 @@ struct FrameRequest { return (resize == other.resize) && (outer == other.outer) && (radius == other.radius) - && (corners == other.corners); + && (corners == other.corners) + && (requireARGB32 == other.requireARGB32); } [[nodiscard]] bool operator!=(const FrameRequest &other) const { return !(*this == other); } [[nodiscard]] bool goodFor(const FrameRequest &other) const { - return (*this == other) || (strict && !other.strict); + return (requireARGB32 == other.requireARGB32) + && ((*this == other) || (strict && !other.strict)); } }; +enum class FrameFormat { + None, + ARGB32, + YUV420, +}; + +struct FrameChannel { + const void *data = nullptr; + int stride = 0; +}; + +struct FrameYUV420 { + QSize size; + QSize chromaSize; + FrameChannel y; + FrameChannel u; + FrameChannel v; +}; + +struct FrameWithInfo { + QImage original; + FrameYUV420 *yuv420 = nullptr; + FrameFormat format = FrameFormat::None; + int index = -1; +}; + } // namespace Streaming } // namespace Media diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp index e6cf39e2a..c8e805d3d 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_document.cpp @@ -91,12 +91,8 @@ void Document::play(const PlaybackOptions &options) { } void Document::saveFrameToCover() { - auto request = Streaming::FrameRequest(); - //request.radius = (_doc && _doc->isVideoMessage()) - // ? ImageRoundRadius::Ellipse - // : ImageRoundRadius::None; _info.video.cover = _player.ready() - ? _player.frame(request) + ? _player.currentFrameImage() : _info.video.cover; } diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp index 782d7c40a..d2071f833 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.cpp @@ -172,7 +172,11 @@ QImage Instance::frame(const FrameRequest &request) const { return player().frame(request, this); } -bool Instance::markFrameShown() { +FrameWithInfo Instance::frameWithInfo() const { + return player().frameWithInfo(this); +} + +bool Instance::markFrameShown() const { Expects(_shared != nullptr); return _shared->player().markFrameShown(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h index a92b0c87e..63e2ec743 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_instance.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_instance.h @@ -69,7 +69,8 @@ public: void callWaitingCallback(); [[nodiscard]] QImage frame(const FrameRequest &request) const; - bool markFrameShown(); + [[nodiscard]] FrameWithInfo frameWithInfo() const; + bool markFrameShown() const; void lockPlayer(); void unlockPlayer(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp index 9a58d368d..fc6d036d6 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.cpp @@ -881,6 +881,18 @@ QImage Player::frame( return _video->frame(request, instance); } +FrameWithInfo Player::frameWithInfo(const Instance *instance) const { + Expects(_video != nullptr); + + return _video->frameWithInfo(instance); +} + +QImage Player::currentFrameImage() const { + Expects(_video != nullptr); + + return _video->currentFrameImage(); +} + void Player::unregisterInstance(not_null instance) { if (_video) { _video->unregisterInstance(instance); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_player.h b/Telegram/SourceFiles/media/streaming/media_streaming_player.h index cd4c15a9b..45fb6f07c 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_player.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_player.h @@ -64,6 +64,12 @@ public: [[nodiscard]] QImage frame( const FrameRequest &request, const Instance *instance = nullptr) const; + + [[nodiscard]] FrameWithInfo frameWithInfo( + const Instance *instance = nullptr) const; // !requireARGB32 + + [[nodiscard]] QImage currentFrameImage() const; // Converts if needed. + void unregisterInstance(not_null instance); bool markFrameShown(); diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp index 5cde41782..a183779c2 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.cpp @@ -91,7 +91,9 @@ bool GoodForRequest( const QImage &image, int rotation, const FrameRequest &request) { - if (request.resize.isEmpty()) { + if (image.isNull()) { + return false; + } else if (request.resize.isEmpty()) { return true; } else if (rotation != 0) { return false; @@ -174,6 +176,19 @@ QImage ConvertFrame( return storage; } +FrameYUV420 ExtractYUV420(Stream &stream, AVFrame *frame) { + return { + .size = { frame->width, frame->height }, + .chromaSize = { + AV_CEIL_RSHIFT(frame->width, 1), // SWScale does that. + AV_CEIL_RSHIFT(frame->height, 1) + }, + .y = { .data = frame->data[0], .stride = frame->linesize[0] }, + .u = { .data = frame->data[1], .stride = frame->linesize[1] }, + .v = { .data = frame->data[2], .stride = frame->linesize[2] }, + }; +} + void PaintFrameOuter(QPainter &p, const QRect &inner, QSize outer) { const auto left = inner.x(); const auto right = outer.width() - inner.width() - left; diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h index 3a39eb7f9..6a0769c26 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_utility.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_utility.h @@ -58,6 +58,7 @@ struct Stream { AVFrame *frame, QSize resize, QImage storage); +[[nodiscard]] FrameYUV420 ExtractYUV420(Stream &stream, AVFrame *frame); [[nodiscard]] QImage PrepareByRequest( const QImage &original, bool alpha, diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp index ab3126932..94518b1f3 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.cpp @@ -7,8 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/streaming/media_streaming_video_track.h" +#include "ffmpeg/ffmpeg_utility.h" #include "media/audio/media_audio.h" #include "base/concurrent_timer.h" +#include "core/crash_reports.h" namespace Media { namespace Streaming { @@ -19,6 +21,55 @@ constexpr auto kDisplaySkipped = crl::time(-1); constexpr auto kFinishedPosition = std::numeric_limits::max(); static_assert(kDisplaySkipped != kTimeUnknown); +[[nodiscard]] QImage ConvertToARGB32(const FrameYUV420 &data) { + Expects(data.y.data != nullptr); + Expects(data.u.data != nullptr); + Expects(data.v.data != nullptr); + Expects(!data.size.isEmpty()); + + //if (FFmpeg::RotationSwapWidthHeight(stream.rotation)) { + // resize.transpose(); + //} + + auto result = FFmpeg::CreateFrameStorage(data.size); + const auto format = AV_PIX_FMT_BGRA; + const auto swscale = FFmpeg::MakeSwscalePointer( + data.size, + AV_PIX_FMT_YUV420P, + data.size, + AV_PIX_FMT_BGRA); + if (!swscale) { + return QImage(); + } + + // AV_NUM_DATA_POINTERS defined in AVFrame struct + const uint8_t *srcData[AV_NUM_DATA_POINTERS] = { + static_cast(data.y.data), + static_cast(data.u.data), + static_cast(data.v.data), + nullptr, + }; + int srcLinesize[AV_NUM_DATA_POINTERS] = { + data.y.stride, + data.u.stride, + data.v.stride, + 0, + }; + uint8_t *dstData[AV_NUM_DATA_POINTERS] = { result.bits(), nullptr }; + int dstLinesize[AV_NUM_DATA_POINTERS] = { result.bytesPerLine(), 0 }; + + sws_scale( + swscale.get(), + srcData, + srcLinesize, + 0, + data.size.height(), + dstData, + dstLinesize); + + return result; +} + } // namespace class VideoTrackObject final { @@ -53,6 +104,7 @@ public: void removeFrameRequest(const Instance *instance); void rasterizeFrame(not_null frame); + [[nodiscard]] bool requireARGB32() const; private: enum class FrameResult { @@ -237,6 +289,7 @@ void VideoTrackObject::readFrames() { } }, [&](Shared::PrepareNextCheck delay) { Expects(delay == kTimeUnknown || delay > 0); + if (delay != kTimeUnknown) { queueReadFrames(delay); } @@ -252,7 +305,8 @@ auto VideoTrackObject::readEnoughFrames(crl::time trackTime) -> ReadEnoughState { const auto dropStaleFrames = !_options.waitForMarkAsShown; const auto state = _shared->prepareState(trackTime, dropStaleFrames); - return v::match(state, [&](Shared::PrepareFrame frame) -> ReadEnoughState { + return v::match(state, [&](Shared::PrepareFrame frame) + -> ReadEnoughState { while (true) { const auto result = readFrame(frame); if (result != FrameResult::Done) { @@ -263,6 +317,8 @@ auto VideoTrackObject::readEnoughFrames(crl::time trackTime) } } }, [&](Shared::PrepareNextCheck delay) -> ReadEnoughState { + Expects(delay == kTimeUnknown || delay > 0); // Debugging crash. + return delay; }, [&](v::null_t) -> ReadEnoughState { return FrameResult::Done; @@ -357,20 +413,56 @@ QSize VideoTrackObject::chooseOriginalResize() const { return chosen; } +bool VideoTrackObject::requireARGB32() const { + for (const auto &[_, request] : _requests) { + if (!request.requireARGB32) { + return false; + } + } + return true; +} + void VideoTrackObject::rasterizeFrame(not_null frame) { Expects(frame->position != kFinishedPosition); fillRequests(frame); - frame->alpha = (frame->decoded->format == AV_PIX_FMT_BGRA); - frame->original = ConvertFrame( - _stream, - frame->decoded.get(), - chooseOriginalResize(), - std::move(frame->original)); - if (frame->original.isNull()) { - frame->prepared.clear(); - fail(Error::InvalidData); - return; + frame->format = FrameFormat::None; + if (frame->decoded->format == AV_PIX_FMT_YUV420P && !requireARGB32()) { + frame->alpha = false; + frame->yuv420 = ExtractYUV420(_stream, frame->decoded.get()); + if (frame->yuv420.size.isEmpty() + || frame->yuv420.chromaSize.isEmpty() + || !frame->yuv420.y.data + || !frame->yuv420.u.data + || !frame->yuv420.v.data) { + frame->prepared.clear(); + fail(Error::InvalidData); + return; + } + if (!frame->original.isNull()) { + frame->original = QImage(); + for (auto &[_, prepared] : frame->prepared) { + prepared.image = QImage(); + } + } + frame->format = FrameFormat::YUV420; + } else { + frame->alpha = (frame->decoded->format == AV_PIX_FMT_BGRA); + frame->yuv420.size = { + frame->decoded->width, + frame->decoded->height + }; + frame->original = ConvertFrame( + _stream, + frame->decoded.get(), + chooseOriginalResize(), + std::move(frame->original)); + if (frame->original.isNull()) { + frame->prepared.clear(); + fail(Error::InvalidData); + return; + } + frame->format = FrameFormat::ARGB32; } VideoTrack::PrepareFrameByRequests(frame, _stream.rotation); @@ -474,7 +566,7 @@ void VideoTrackObject::addTimelineDelay(crl::time delayed) { void VideoTrackObject::updateFrameRequest( const Instance *instance, const FrameRequest &request) { - _requests.emplace(instance, request); + _requests[instance] = request; } void VideoTrackObject::removeFrameRequest(const Instance *instance) { @@ -613,6 +705,7 @@ void VideoTrack::Shared::init(QImage &&cover, crl::time position) { _frames[0].original = std::move(cover); _frames[0].position = position; + _frames[0].format = FrameFormat::ARGB32; // Usually main thread sets displayed time before _counter increment. // But in this case we update _counter, so we set a fake displayed time. @@ -664,6 +757,16 @@ auto VideoTrack::Shared::prepareState( next->displayed = kDisplaySkipped; return next; } else { + if (frame->position - trackTime + 1 <= 0) { // Debugging crash. + CrashReports::SetAnnotation( + "DelayValues", + (QString::number(frame->position) + + " + 1 <= " + + QString::number(trackTime))); + } + Assert(frame->position >= trackTime); + Assert(frame->position - trackTime + 1 > 0); + return PrepareNextCheck(frame->position - trackTime + 1); } }; @@ -722,7 +825,7 @@ auto VideoTrack::Shared::presentFrame( // Release this frame to the main thread for rendering. _counter.store( - (counter + 1) % (2 * kFramesCount), + counter + 1, std::memory_order_release); return { position, crl::time(0), addedWorldTimeDelay }; }; @@ -847,6 +950,9 @@ bool VideoTrack::Shared::markFrameShown() { if (frame->displayed == kTimeUnknown) { return false; } + if (counter == 2 * kFramesCount - 1) { + ++_counterCycle; + } _counter.store( next, std::memory_order_release); @@ -867,12 +973,20 @@ bool VideoTrack::Shared::markFrameShown() { } not_null VideoTrack::Shared::frameForPaint() { - const auto result = getFrame(counter() / 2); - Assert(!result->original.isNull()); - Assert(result->position != kTimeUnknown); - Assert(result->displayed != kTimeUnknown); + return frameForPaintWithIndex().frame; +} + +VideoTrack::FrameWithIndex VideoTrack::Shared::frameForPaintWithIndex() { + const auto index = counter() / 2; + const auto frame = getFrame(index); + Assert(frame->format != FrameFormat::None); + Assert(frame->position != kTimeUnknown); + Assert(frame->displayed != kTimeUnknown); + return { + .frame = frame, + .index = (_counterCycle * 2 * kFramesCount) + index, + }; - return result; } VideoTrack::VideoTrack( @@ -990,6 +1104,10 @@ QImage VideoTrack::frame( unwrapped.updateFrameRequest(instance, useRequest); }); } + if (frame->original.isNull() + && frame->format == FrameFormat::YUV420) { + frame->original = ConvertToARGB32(frame->yuv420); + } if (!frame->alpha && GoodForRequest(frame->original, _streamRotation, useRequest)) { return frame->original; @@ -1020,6 +1138,33 @@ QImage VideoTrack::frame( return i->second.image; } +FrameWithInfo VideoTrack::frameWithInfo(const Instance *instance) { + const auto data = _shared->frameForPaintWithIndex(); + const auto i = data.frame->prepared.find(instance); + const auto none = (i == data.frame->prepared.end()); + if (none || i->second.request.requireARGB32) { + _wrapped.with([=](Implementation &unwrapped) { + unwrapped.updateFrameRequest( + instance, + { .requireARGB32 = false }); + }); + } + return { + .original = data.frame->original, + .yuv420 = &data.frame->yuv420, + .format = data.frame->format, + .index = data.index, + }; +} + +QImage VideoTrack::currentFrameImage() { + const auto frame = _shared->frameForPaint(); + if (frame->original.isNull() && frame->format == FrameFormat::YUV420) { + frame->original = ConvertToARGB32(frame->yuv420); + } + return frame->original; +} + void VideoTrack::unregisterInstance(not_null instance) { _wrapped.with([=](Implementation &unwrapped) { unwrapped.removeFrameRequest(instance); @@ -1029,7 +1174,12 @@ void VideoTrack::unregisterInstance(not_null instance) { void VideoTrack::PrepareFrameByRequests( not_null frame, int rotation) { - Expects(!frame->original.isNull()); + Expects(frame->format != FrameFormat::ARGB32 + || !frame->original.isNull()); + + if (frame->format != FrameFormat::ARGB32) { + return; + } const auto begin = frame->prepared.begin(); const auto end = frame->prepared.end(); @@ -1063,7 +1213,8 @@ bool VideoTrack::IsDecoded(not_null frame) { bool VideoTrack::IsRasterized(not_null frame) { return IsDecoded(frame) - && !frame->original.isNull(); + && (!frame->original.isNull() + || frame->format == FrameFormat::YUV420); } bool VideoTrack::IsStale(not_null frame, crl::time trackTime) { diff --git a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h index 880bb0acc..707ff297d 100644 --- a/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h +++ b/Telegram/SourceFiles/media/streaming/media_streaming_video_track.h @@ -58,6 +58,8 @@ public: [[nodiscard]] QImage frame( const FrameRequest &request, const Instance *instance); + [[nodiscard]] FrameWithInfo frameWithInfo(const Instance *instance); + [[nodiscard]] QImage currentFrameImage(); void unregisterInstance(not_null instance); [[nodiscard]] rpl::producer<> checkNextFrame() const; [[nodiscard]] rpl::producer<> waitingForData() const; @@ -78,14 +80,20 @@ private: struct Frame { FFmpeg::FramePointer decoded = FFmpeg::MakeFramePointer(); QImage original; + FrameYUV420 yuv420; crl::time position = kTimeUnknown; crl::time displayed = kTimeUnknown; crl::time display = kTimeUnknown; + FrameFormat format = FrameFormat::None; base::flat_map prepared; bool alpha = false; }; + struct FrameWithIndex { + not_null frame; + int index = -1; + }; class Shared { public: @@ -123,6 +131,7 @@ private: bool markFrameShown(); [[nodiscard]] crl::time nextFrameDisplayTime() const; [[nodiscard]] not_null frameForPaint(); + [[nodiscard]] FrameWithIndex frameForPaintWithIndex(); private: [[nodiscard]] not_null getFrame(int index); @@ -132,6 +141,9 @@ private: static constexpr auto kCounterUninitialized = -1; std::atomic _counter = kCounterUninitialized; + // Main thread. + int _counterCycle = 0; + static constexpr auto kFramesCount = 4; std::array _frames; diff --git a/Telegram/SourceFiles/media/system_media_controls_manager.cpp b/Telegram/SourceFiles/media/system_media_controls_manager.cpp new file mode 100644 index 000000000..1335ca06f --- /dev/null +++ b/Telegram/SourceFiles/media/system_media_controls_manager.cpp @@ -0,0 +1,256 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/system_media_controls_manager.h" + +#include "base/observer.h" +#include "base/platform/base_platform_system_media_controls.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "mainwidget.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "media/audio/media_audio.h" +#include "media/player/media_player_instance.h" +#include "media/streaming/media_streaming_instance.h" +#include "media/streaming/media_streaming_player.h" +#include "ui/text/format_song_document_name.h" +#include "window/window_controller.h" + +namespace Media { + +bool SystemMediaControlsManager::Supported() { + return base::Platform::SystemMediaControls::Supported(); +} + +SystemMediaControlsManager::SystemMediaControlsManager( + not_null controller) +: _controls(std::make_unique()) { + + using PlaybackStatus = + base::Platform::SystemMediaControls::PlaybackStatus; + using Command = base::Platform::SystemMediaControls::Command; + + _controls->setApplicationName(AppName.utf16()); + const auto inited = _controls->init(controller->widget()); + if (!inited) { + LOG(("SystemMediaControlsManager failed to init.")); + return; + } + const auto type = AudioMsgId::Type::Song; + + using TrackState = Media::Player::TrackState; + const auto mediaPlayer = Media::Player::instance(); + + auto trackFilter = rpl::filter([=](const TrackState &state) { + return (state.id.type() == type); + }); + + mediaPlayer->updatedNotifier( + ) | trackFilter | rpl::map([=](const TrackState &state) { + using namespace Media::Player; + if (_streamed) { + const auto &player = _streamed->player(); + if (player.buffering() || !player.playing()) { + return PlaybackStatus::Paused; + } + } + if (IsStoppedOrStopping(state.state)) { + return PlaybackStatus::Stopped; + } else if (IsPausedOrPausing(state.state)) { + return PlaybackStatus::Paused; + } + return PlaybackStatus::Playing; + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](PlaybackStatus status) { + _controls->setPlaybackStatus(status); + }, _lifetime); + + rpl::merge( + mediaPlayer->stops(type) | rpl::map_to(false), + mediaPlayer->startsPlay(type) | rpl::map_to(true) + ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool audio) { + _controls->setEnabled(audio); + if (audio) { + _controls->setIsNextEnabled(mediaPlayer->nextAvailable(type)); + _controls->setIsPreviousEnabled( + mediaPlayer->previousAvailable(type)); + _controls->setIsPlayPauseEnabled(true); + _controls->setIsStopEnabled(true); + _controls->setPlaybackStatus(PlaybackStatus::Playing); + _controls->updateDisplay(); + } else { + _cachedMediaView.clear(); + _streamed = nullptr; + _controls->clearMetadata(); + } + _lifetimeDownload.destroy(); + }, _lifetime); + + auto trackChanged = base::ObservableViewer( + mediaPlayer->trackChangedNotifier() + ) | rpl::filter([=](AudioMsgId::Type audioType) { + return audioType == type; + }); + + auto unlocked = Core::App().passcodeLockChanges( + ) | rpl::filter([=](bool locked) { + return !locked && (mediaPlayer->current(type)); + }) | rpl::map([=] { + return type; + }) | rpl::before_next([=] { + _controls->setEnabled(true); + _controls->updateDisplay(); + }); + + rpl::merge( + std::move(trackChanged), + std::move(unlocked) + ) | rpl::start_with_next([=](AudioMsgId::Type audioType) { + _lifetimeDownload.destroy(); + + const auto current = mediaPlayer->current(audioType); + if (!current) { + return; + } + if ((_lastAudioMsgId.contextId() == current.contextId()) + && (_lastAudioMsgId.audio() == current.audio()) + && (_lastAudioMsgId.type() == current.type())) { + return; + } + const auto document = current.audio(); + + const auto &[title, performer] = Ui::Text::FormatSongNameFor(document) + .composedName(); + + _controls->setArtist(performer); + _controls->setTitle(title); + + if (_controls->seekingSupported()) { + const auto state = mediaPlayer->getState(audioType); + _controls->setDuration(state.length); + // macOS NowPlaying and Linux MPRIS update the track position + // according to the rate property + // while the playback status is "playing", + // so we should change the track position only when + // the track is changed + // or when the position is changed by the user. + _controls->setPosition(state.position); + + _streamed = std::make_unique( + document, + current.contextId(), + nullptr); + } + + // Setting a thumbnail can take a long time, + // so we need to update the display before that. + _controls->updateDisplay(); + + if (document && document->isSongWithCover()) { + const auto view = document->createMediaView(); + view->thumbnailWanted(current.contextId()); + _cachedMediaView.push_back(view); + if (const auto imagePtr = view->thumbnail()) { + _controls->setThumbnail(imagePtr->original()); + } else { + document->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + if (const auto imagePtr = view->thumbnail()) { + _controls->setThumbnail(imagePtr->original()); + _lifetimeDownload.destroy(); + } + }, _lifetimeDownload); + _controls->clearThumbnail(); + } + } else { + _controls->clearThumbnail(); + } + + _lastAudioMsgId = current; + }, _lifetime); + + mediaPlayer->playlistChanges( + type + ) | rpl::start_with_next([=] { + _controls->setIsNextEnabled(mediaPlayer->nextAvailable(type)); + _controls->setIsPreviousEnabled(mediaPlayer->previousAvailable(type)); + }, _lifetime); + + _controls->commandRequests( + ) | rpl::start_with_next([=](Command command) { + switch (command) { + case Command::PlayPause: mediaPlayer->playPause(type); break; + case Command::Play: mediaPlayer->play(type); break; + case Command::Pause: mediaPlayer->pause(type); break; + case Command::Next: mediaPlayer->next(type); break; + case Command::Previous: mediaPlayer->previous(type); break; + case Command::Stop: mediaPlayer->stop(type); break; + case Command::Raise: controller->widget()->showFromTray(); break; + case Command::Quit: { + if (const auto main = controller->widget()->sessionContent()) { + main->closeBothPlayers(); + } + break; + } + } + }, _lifetime); + + if (_controls->seekingSupported()) { + mediaPlayer->seekingChanges( + type + ) | rpl::filter([](Media::Player::Instance::Seeking seeking) { + return (seeking == Media::Player::Instance::Seeking::Finish); + }) | rpl::map([=] { + return mediaPlayer->getState(type).position; + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](int position) { + _controls->setPosition(position); + _controls->updateDisplay(); + }, _lifetime); + + _controls->seekRequests( + ) | rpl::start_with_next([=](float64 progress) { + mediaPlayer->finishSeeking(type, progress); + }, _lifetime); + + _controls->updatePositionRequests( + ) | rpl::start_with_next([=] { + _controls->setPosition(mediaPlayer->getState(type).position); + }, _lifetime); + } + + Core::App().passcodeLockValue( + ) | rpl::filter([=](bool locked) { + return locked && Core::App().maybeActiveSession(); + }) | rpl::start_with_next([=] { + _controls->setEnabled(false); + }, _lifetime); + + if (_controls->volumeSupported()) { + rpl::single( + Core::App().settings().songVolume() + ) | rpl::then( + Core::App().settings().songVolumeChanges() + ) | rpl::start_with_next([=](float64 volume) { + _controls->setVolume(volume); + }, _lifetime); + + _controls->volumeChangeRequests( + ) | rpl::start_with_next([](float64 volume) { + Core::App().settings().setSongVolume(volume); + }, _lifetime); + } + +} + +SystemMediaControlsManager::~SystemMediaControlsManager() = default; + +} // namespace Media diff --git a/Telegram/SourceFiles/media/system_media_controls_manager.h b/Telegram/SourceFiles/media/system_media_controls_manager.h new file mode 100644 index 000000000..0f6738119 --- /dev/null +++ b/Telegram/SourceFiles/media/system_media_controls_manager.h @@ -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 +*/ +#pragma once + +namespace base::Platform { +class SystemMediaControls; +} // namespace base::Platform + +namespace Data { +class DocumentMedia; +} // namespace Data + +namespace Window { +class Controller; +} // namespace Window + +namespace Media::Streaming { +class Instance; +} // namespace Media::Streaming + +namespace Media { + +class SystemMediaControlsManager { +public: + SystemMediaControlsManager(not_null controller); + ~SystemMediaControlsManager(); + + static bool Supported(); + +private: + const std::unique_ptr _controls; + + std::vector> _cachedMediaView; + std::unique_ptr _streamed; + AudioMsgId _lastAudioMsgId; + + rpl::lifetime _lifetimeDownload; + rpl::lifetime _lifetime; +}; + +} // namespace Media diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 40efafa80..81591e511 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -237,7 +237,7 @@ mediaviewTextOpacity: 0.5; mediaviewTextOverOpacity: 1; mediaviewIconOpacity: 0.45; -mediaviewIconOverOpacity: 1; +mediaviewIconOverOpacity: 1.; mediaviewControlBgOpacity: 0.3; mediaviewControlMargin: 0px; mediaviewControlSize: 90px; @@ -297,9 +297,15 @@ pipPlayIcon: icon {{ "player_pip_play", mediaviewPipControlsFg }}; pipPlayIconOver: icon {{ "player_pip_play", mediaviewPipControlsFgOver }}; pipPauseIcon: icon {{ "player_pip_pause", mediaviewPipControlsFg }}; pipPauseIconOver: icon {{ "player_pip_pause", mediaviewPipControlsFgOver }}; -pipCloseIcon: icon {{ "player_pip_close", mediaviewPlaybackIconFg }}; -pipCloseIconOver: icon {{ "player_pip_close", mediaviewPlaybackIconFgOver }}; -pipEnlargeIcon: icon {{ "player_pip_enlarge", mediaviewPlaybackIconFg }}; -pipEnlargeIconOver: icon {{ "player_pip_enlarge", mediaviewPlaybackIconFgOver }}; +pipCloseIcon: icon {{ "player_pip_close", mediaviewPipControlsFg }}; +pipCloseIconOver: icon {{ "player_pip_close", mediaviewPipControlsFgOver }}; +pipEnlargeIcon: icon {{ "player_pip_enlarge", mediaviewPipControlsFg }}; +pipEnlargeIconOver: icon {{ "player_pip_enlarge", mediaviewPipControlsFgOver }}; +pipVolumeIcon0: icon {{ "player_volume_off", mediaviewPipControlsFg }}; +pipVolumeIcon0Over: icon {{ "player_volume_off", mediaviewPipControlsFgOver }}; +pipVolumeIcon1: icon {{ "player_volume_small", mediaviewPipControlsFg }}; +pipVolumeIcon1Over: icon {{ "player_volume_small", mediaviewPipControlsFgOver }}; +pipVolumeIcon2: icon {{ "player_volume_on", mediaviewPipControlsFg }}; +pipVolumeIcon2Over: icon {{ "player_volume_on", mediaviewPipControlsFgOver }}; speedSliderDividerSize: size(2px, 8px); diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h new file mode 100644 index 000000000..320f6d240 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -0,0 +1,103 @@ +/* +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_cloud_themes.h" + +class DocumentData; +class PeerData; +class PhotoData; +class HistoryItem; + +namespace Window { +class SessionController; +} // namespace Window + +namespace Media::View { + +struct OpenRequest { +public: + OpenRequest() { + } + + OpenRequest( + Window::SessionController *controller, + not_null photo, + HistoryItem *item) + : _controller(controller) + , _photo(photo) + , _item(item) { + } + OpenRequest( + Window::SessionController *controller, + not_null photo, + not_null peer) + : _controller(controller) + , _photo(photo) + , _peer(peer) { + } + + OpenRequest( + Window::SessionController *controller, + not_null document, + HistoryItem *item, + bool continueStreaming = false) + : _controller(controller) + , _document(document) + , _item(item) + , _continueStreaming(continueStreaming) { + } + OpenRequest( + Window::SessionController *controller, + not_null document, + const Data::CloudTheme &cloudTheme) + : _controller(controller) + , _document(document) + , _cloudTheme(cloudTheme) { + } + + PeerData *peer() const { + return _peer; + } + + PhotoData *photo() const { + return _photo; + } + + HistoryItem *item() const { + return _item; + } + + DocumentData *document() const { + return _document; + } + + std::optional cloudTheme() const { + return _cloudTheme; + } + + Window::SessionController *controller() const { + return _controller; + } + + bool continueStreaming() const { + return _continueStreaming; + } + +private: + Window::SessionController *_controller = nullptr; + DocumentData *_document = nullptr; + PhotoData *_photo = nullptr; + PeerData *_peer = nullptr; + HistoryItem *_item = nullptr; + std::optional _cloudTheme = std::nullopt; + bool _continueStreaming = false; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp new file mode 100644 index 000000000..07e18f5a4 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp @@ -0,0 +1,667 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/view/media_view_overlay_opengl.h" + +#include "ui/gl/gl_shader.h" +#include "media/streaming/media_streaming_common.h" +#include "base/platform/base_platform_info.h" +#include "styles/style_media_view.h" + +namespace Media::View { +namespace { + +using namespace Ui::GL; + +constexpr auto kRadialLoadingOffset = 4; +constexpr auto kThemePreviewOffset = kRadialLoadingOffset + 4; +constexpr auto kDocumentBubbleOffset = kThemePreviewOffset + 4; +constexpr auto kSaveMsgOffset = kDocumentBubbleOffset + 4; +constexpr auto kFooterOffset = kSaveMsgOffset + 4; +constexpr auto kCaptionOffset = kFooterOffset + 4; +constexpr auto kGroupThumbsOffset = kCaptionOffset + 4; +constexpr auto kControlsOffset = kGroupThumbsOffset + 4; +constexpr auto kControlValues = 2 * 4 + 4 * 4; + +[[nodiscard]] ShaderPart FragmentPlaceOnTransparentBackground() { + return { + .header = R"( +uniform vec4 transparentBg; +uniform vec4 transparentFg; +uniform float transparentSize; +)", + .body = R"( + vec2 checkboardLadder = floor(gl_FragCoord.xy / transparentSize); + float checkboard = mod(checkboardLadder.x + checkboardLadder.y, 2.0); + vec4 checkboardColor = checkboard * transparentBg + + (1. - checkboard) * transparentFg; + result += checkboardColor * (1. - result.a); +)", + }; +} + +} // namespace + +OverlayWidget::RendererGL::RendererGL(not_null owner) +: _owner(owner) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _radialImage.invalidate(); + _documentBubbleImage.invalidate(); + _themePreviewImage.invalidate(); + _saveMsgImage.invalidate(); + _footerImage.invalidate(); + _captionImage.invalidate(); + invalidateControls(); + }, _lifetime); +} + +void OverlayWidget::RendererGL::init( + not_null widget, + QOpenGLFunctions &f) { + constexpr auto kQuads = 8; + constexpr auto kQuadVertices = kQuads * 4; + constexpr auto kQuadValues = kQuadVertices * 4; + constexpr auto kControlsValues = kControlsCount * kControlValues; + constexpr auto kValues = kQuadValues + kControlsValues; + + _contentBuffer.emplace(); + _contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); + _contentBuffer->create(); + _contentBuffer->bind(); + _contentBuffer->allocate(kValues * sizeof(GLfloat)); + + _textures.ensureCreated(f); + + _imageProgram.emplace(); + _texturedVertexShader = LinkProgram( + &*_imageProgram, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + })).vertex; + + _withTransparencyProgram.emplace(); + LinkProgram( + &*_withTransparencyProgram, + _texturedVertexShader, + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentPlaceOnTransparentBackground(), + })); + + _yuv420Program.emplace(); + LinkProgram( + &*_yuv420Program, + _texturedVertexShader, + FragmentShader({ + FragmentSampleYUV420Texture(), + })); + + _fillProgram.emplace(); + LinkProgram( + &*_fillProgram, + VertexShader({ VertexViewportTransform() }), + FragmentShader({ FragmentStaticColor() })); + + _controlsProgram.emplace(); + LinkProgram( + &*_controlsProgram, + _texturedVertexShader, + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentGlobalOpacity(), + })); +} + +void OverlayWidget::RendererGL::deinit( + not_null widget, + QOpenGLFunctions &f) { + _textures.destroy(f); + _imageProgram = std::nullopt; + _texturedVertexShader = nullptr; + _withTransparencyProgram = std::nullopt; + _yuv420Program = std::nullopt; + _fillProgram = std::nullopt; + _controlsProgram = std::nullopt; + _contentBuffer = std::nullopt; +} + +void OverlayWidget::RendererGL::paint( + not_null widget, + QOpenGLFunctions &f) { + if (handleHideWorkaround(f)) { + return; + } + const auto factor = widget->devicePixelRatio(); + if (_factor != factor) { + _factor = factor; + _controlsImage.invalidate(); + } + _blendingEnabled = false; + _viewport = widget->size(); + _uniformViewport = QVector2D( + _viewport.width() * _factor, + _viewport.height() * _factor); + _f = &f; + _owner->paint(this); + _f = nullptr; +} + +std::optional OverlayWidget::RendererGL::clearColor() { + if (Platform::IsWindows() && _owner->_hideWorkaround) { + return QColor(0, 0, 0, 0); + } else if (_owner->_fullScreenVideo) { + return st::mediaviewVideoBg->c; + } else { + return st::mediaviewBg->c; + } +} + +bool OverlayWidget::RendererGL::handleHideWorkaround(QOpenGLFunctions &f) { + // This is needed on Windows, + // because on reopen it blinks with the last shown content. + return Platform::IsWindows() && _owner->_hideWorkaround; +} + +void OverlayWidget::RendererGL::paintBackground() { + _contentBuffer->bind(); +} + +void OverlayWidget::RendererGL::paintTransformedVideoFrame( + ContentGeometry geometry) { + const auto data = _owner->videoFrameWithInfo(); + if (data.format == Streaming::FrameFormat::None) { + return; + } + if (data.format == Streaming::FrameFormat::ARGB32) { + Assert(!data.original.isNull()); + paintTransformedStaticContent(data.original, geometry, false, false); + return; + } + Assert(data.format == Streaming::FrameFormat::YUV420); + Assert(!data.yuv420->size.isEmpty()); + const auto yuv = data.yuv420; + _yuv420Program->bind(); + + const auto upload = (_trackFrameIndex != data.index) + || (_streamedIndex != _owner->streamedIndex()); + _trackFrameIndex = data.index; + _streamedIndex = _owner->streamedIndex(); + + _f->glActiveTexture(GL_TEXTURE0); + _textures.bind(*_f, 1); + if (upload) { + _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + uploadTexture( + GL_RED, + GL_RED, + yuv->size, + _lumaSize, + yuv->y.stride, + yuv->y.data); + _lumaSize = yuv->size; + } + _f->glActiveTexture(GL_TEXTURE1); + _textures.bind(*_f, 2); + if (upload) { + uploadTexture( + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->u.stride, + yuv->u.data); + } + _f->glActiveTexture(GL_TEXTURE2); + _textures.bind(*_f, 3); + if (upload) { + uploadTexture( + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->v.stride, + yuv->v.data); + _chromaSize = yuv->chromaSize; + _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + _yuv420Program->setUniformValue("y_texture", GLint(0)); + _yuv420Program->setUniformValue("u_texture", GLint(1)); + _yuv420Program->setUniformValue("v_texture", GLint(2)); + + toggleBlending(false); + paintTransformedContent(&*_yuv420Program, geometry); +} + +void OverlayWidget::RendererGL::paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground) { + Expects(image.isNull() + || image.format() == QImage::Format_RGB32 + || image.format() == QImage::Format_ARGB32_Premultiplied); + + if (geometry.rect.isEmpty()) { + return; + } + + auto &program = fillTransparentBackground + ? _withTransparencyProgram + : _imageProgram; + program->bind(); + if (fillTransparentBackground) { + program->setUniformValue( + "transparentBg", + st::mediaviewTransparentBg->c); + program->setUniformValue( + "transparentFg", + st::mediaviewTransparentFg->c); + program->setUniformValue( + "transparentSize", + st::transparentPlaceholderSize * _factor); + } + + _f->glActiveTexture(GL_TEXTURE0); + _textures.bind(*_f, 0); + const auto cacheKey = image.isNull() ? qint64(-1) : image.cacheKey(); + const auto upload = (_cacheKey != cacheKey); + if (upload) { + _cacheKey = cacheKey; + if (image.isNull()) { + // Upload transparent 2x2 texture. + const auto stride = 2; + const uint32_t data[4] = { 0 }; + uploadTexture( + GL_RGBA, + GL_RGBA, + QSize(2, 2), + _rgbaSize, + stride, + data); + } else { + const auto stride = image.bytesPerLine() / 4; + const auto data = image.constBits(); + uploadTexture( + GL_RGBA, + GL_RGBA, + image.size(), + _rgbaSize, + stride, + data); + _rgbaSize = image.size(); + } + } + program->setUniformValue("s_texture", GLint(0)); + + toggleBlending(semiTransparent && !fillTransparentBackground); + paintTransformedContent(&*program, geometry); +} + +void OverlayWidget::RendererGL::paintTransformedContent( + not_null program, + ContentGeometry geometry) { + const auto rect = transformRect(geometry.rect); + const auto centerx = rect.x() + rect.width() / 2; + const auto centery = rect.y() + rect.height() / 2; + const auto rsin = float(std::sin(geometry.rotation * M_PI / 180.)); + const auto rcos = float(std::cos(geometry.rotation * M_PI / 180.)); + const auto rotated = [&](float x, float y) -> std::array { + x -= centerx; + y -= centery; + return std::array{ + centerx + (x * rcos + y * rsin), + centery + (y * rcos - x * rsin) + }; + }; + const auto topleft = rotated(rect.left(), rect.top()); + const auto topright = rotated(rect.right(), rect.top()); + const auto bottomright = rotated(rect.right(), rect.bottom()); + const auto bottomleft = rotated(rect.left(), rect.bottom()); + const GLfloat coords[] = { + topleft[0], topleft[1], + 0.f, 1.f, + + topright[0], topright[1], + 1.f, 1.f, + + bottomright[0], bottomright[1], + 1.f, 0.f, + + bottomleft[0], bottomleft[1], + 0.f, 0.f, + }; + + _contentBuffer->write(0, coords, sizeof(coords)); + + program->setUniformValue("viewport", _uniformViewport); + + FillTexturedRectangle(*_f, &*program); +} + +void OverlayWidget::RendererGL::uploadTexture( + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const { + _f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); + if (hasSize != size) { + _f->glTexImage2D( + GL_TEXTURE_2D, + 0, + internalformat, + size.width(), + size.height(), + 0, + format, + GL_UNSIGNED_BYTE, + data); + } else { + _f->glTexSubImage2D( + GL_TEXTURE_2D, + 0, + 0, + 0, + size.width(), + size.height(), + format, + GL_UNSIGNED_BYTE, + data); + } + _f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + +void OverlayWidget::RendererGL::paintRadialLoading( + QRect inner, + bool radial, + float64 radialOpacity) { + paintUsingRaster(_radialImage, inner, [&](Painter &&p) { + const auto newInner = QRect(QPoint(), inner.size()); + _owner->paintRadialLoadingContent(p, newInner, radial, radialOpacity); + }, kRadialLoadingOffset, true); +} + +void OverlayWidget::RendererGL::paintThemePreview(QRect outer) { + paintUsingRaster(_themePreviewImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintThemePreviewContent(p, newOuter, newOuter); + }, kThemePreviewOffset); +} + +void OverlayWidget::RendererGL::paintDocumentBubble( + QRect outer, + QRect icon) { + paintUsingRaster(_documentBubbleImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + const auto newIcon = icon.translated(-outer.topLeft()); + _owner->paintDocumentBubbleContent(p, newOuter, newIcon, newOuter); + }, kDocumentBubbleOffset); + _owner->paintRadialLoading(this); +} + +void OverlayWidget::RendererGL::paintSaveMsg(QRect outer) { + paintUsingRaster(_saveMsgImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintSaveMsgContent(p, newOuter, newOuter); + }, kSaveMsgOffset, true); +} + +void OverlayWidget::RendererGL::paintControlsStart() { + validateControls(); + _f->glActiveTexture(GL_TEXTURE0); + _controlsImage.bind(*_f); + toggleBlending(true); +} + +void OverlayWidget::RendererGL::paintControl( + OverState control, + QRect outer, + float64 outerOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) { + const auto meta = ControlMeta(control); + Assert(meta.icon == &icon); + + const auto &bg = st::mediaviewControlBg->c; + const auto bgAlpha = int(std::round(bg.alpha() * outerOpacity)); + const auto offset = kControlsOffset + (meta.index * kControlValues) / 4; + const auto fgOffset = offset + 2; + const auto bgRect = transformRect(outer); + const auto iconRect = _controlsImage.texturedRect( + inner, + _controlsTextures[meta.index]); + const auto iconGeometry = transformRect(iconRect.geometry); + const GLfloat coords[] = { + bgRect.left(), bgRect.top(), + bgRect.right(), bgRect.top(), + bgRect.right(), bgRect.bottom(), + bgRect.left(), bgRect.bottom(), + + iconGeometry.left(), iconGeometry.top(), + iconRect.texture.left(), iconRect.texture.bottom(), + + iconGeometry.right(), iconGeometry.top(), + iconRect.texture.right(), iconRect.texture.bottom(), + + iconGeometry.right(), iconGeometry.bottom(), + iconRect.texture.right(), iconRect.texture.top(), + + iconGeometry.left(), iconGeometry.bottom(), + iconRect.texture.left(), iconRect.texture.top(), + }; + if (!outer.isEmpty() && bgAlpha > 0) { + _contentBuffer->write( + offset * 4 * sizeof(GLfloat), + coords, + sizeof(coords)); + _fillProgram->bind(); + _fillProgram->setUniformValue("viewport", _uniformViewport); + FillRectangle( + *_f, + &*_fillProgram, + offset, + QColor(bg.red(), bg.green(), bg.blue(), bgAlpha)); + } else { + _contentBuffer->write( + fgOffset * 4 * sizeof(GLfloat), + coords + (fgOffset - offset) * 4, + sizeof(coords) - (fgOffset - offset) * 4 * sizeof(GLfloat)); + } + _controlsProgram->bind(); + _controlsProgram->setUniformValue("g_opacity", GLfloat(innerOpacity)); + _controlsProgram->setUniformValue("viewport", _uniformViewport); + FillTexturedRectangle(*_f, &*_controlsProgram, fgOffset); +} + +auto OverlayWidget::RendererGL::ControlMeta(OverState control) +-> Control { + switch (control) { + case OverLeftNav: return { 0, &st::mediaviewLeft }; + case OverRightNav: return { 1, &st::mediaviewRight }; + case OverClose: return { 2, &st::mediaviewClose }; + case OverSave: return { 3, &st::mediaviewSave }; + case OverRotate: return { 4, &st::mediaviewRotate }; + case OverMore: return { 5, &st::mediaviewMore }; + } + Unexpected("Control value in OverlayWidget::RendererGL::ControlIndex."); +} + +void OverlayWidget::RendererGL::validateControls() { + if (!_controlsImage.image().isNull()) { + return; + } + const auto metas = { + ControlMeta(OverLeftNav), + ControlMeta(OverRightNav), + ControlMeta(OverClose), + ControlMeta(OverSave), + ControlMeta(OverRotate), + ControlMeta(OverMore), + }; + auto maxWidth = 0; + auto fullHeight = 0; + for (const auto meta : metas) { + maxWidth = std::max(meta.icon->width(), maxWidth); + fullHeight += meta.icon->height(); + } + auto image = QImage( + QSize(maxWidth, fullHeight) * _factor, + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + image.setDevicePixelRatio(_factor); + { + auto p = QPainter(&image); + auto index = 0; + auto height = 0; + for (const auto meta : metas) { + meta.icon->paint(p, 0, height, maxWidth); + _controlsTextures[index++] = QRect( + QPoint(0, height) * _factor, + meta.icon->size() * _factor); + height += meta.icon->height(); + } + } + _controlsImage.setImage(std::move(image)); +} + +void OverlayWidget::RendererGL::invalidateControls() { + _controlsImage.invalidate(); + ranges::fill(_controlsTextures, QRect()); +} + +void OverlayWidget::RendererGL::paintFooter(QRect outer, float64 opacity) { + paintUsingRaster(_footerImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintFooterContent(p, newOuter, newOuter, opacity); + }, kFooterOffset, true); +} + +void OverlayWidget::RendererGL::paintCaption(QRect outer, float64 opacity) { + paintUsingRaster(_captionImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintCaptionContent(p, newOuter, newOuter, opacity); + }, kCaptionOffset, true); +} + +void OverlayWidget::RendererGL::paintGroupThumbs( + QRect outer, + float64 opacity) { + paintUsingRaster(_groupThumbsImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintGroupThumbsContent(p, newOuter, newOuter, opacity); + }, kGroupThumbsOffset, true); +} + +void OverlayWidget::RendererGL::invalidate() { + _trackFrameIndex = -1; + _streamedIndex = -1; + const auto images = { + &_radialImage, + &_documentBubbleImage, + &_themePreviewImage, + &_saveMsgImage, + &_footerImage, + &_captionImage, + &_groupThumbsImage, + &_controlsImage, + }; + for (const auto image : images) { + image->setImage(QImage()); + } + invalidateControls(); +} + +void OverlayWidget::RendererGL::paintUsingRaster( + Ui::GL::Image &image, + QRect rect, + Fn method, + int bufferOffset, + bool transparent) { + auto raster = image.takeImage(); + const auto size = rect.size() * _factor; + if (raster.width() < size.width() || raster.height() < size.height()) { + raster = QImage(size, QImage::Format_ARGB32_Premultiplied); + raster.setDevicePixelRatio(_factor); + if (!transparent + && (raster.width() > size.width() + || raster.height() > size.height())) { + raster.fill(Qt::transparent); + } + } else if (raster.devicePixelRatio() != _factor) { + raster.setDevicePixelRatio(_factor); + } + + if (transparent) { + raster.fill(Qt::transparent); + } + method(Painter(&raster)); + + _f->glActiveTexture(GL_TEXTURE0); + + image.setImage(std::move(raster), size); + image.bind(*_f); + + const auto textured = image.texturedRect(rect, QRect(QPoint(), size)); + const auto geometry = transformRect(textured.geometry); + const GLfloat coords[] = { + geometry.left(), geometry.top(), + textured.texture.left(), textured.texture.bottom(), + + geometry.right(), geometry.top(), + textured.texture.right(), textured.texture.bottom(), + + geometry.right(), geometry.bottom(), + textured.texture.right(), textured.texture.top(), + + geometry.left(), geometry.bottom(), + textured.texture.left(), textured.texture.top(), + }; + _contentBuffer->write( + bufferOffset * 4 * sizeof(GLfloat), + coords, + sizeof(coords)); + + _imageProgram->bind(); + _imageProgram->setUniformValue("viewport", _uniformViewport); + _imageProgram->setUniformValue("s_texture", GLint(0)); + + toggleBlending(transparent); + FillTexturedRectangle(*_f, &*_imageProgram, bufferOffset); +} + +void OverlayWidget::RendererGL::toggleBlending(bool enabled) { + if (_blendingEnabled == enabled) { + return; + } else if (enabled) { + _f->glEnable(GL_BLEND); + _f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } else { + _f->glDisable(GL_BLEND); + } + _blendingEnabled = enabled; +} + +Rect OverlayWidget::RendererGL::transformRect(const Rect &raster) const { + return TransformRect(raster, _viewport, _factor); +} + +Rect OverlayWidget::RendererGL::transformRect(const QRectF &raster) const { + return TransformRect(raster, _viewport, _factor); +} + +Rect OverlayWidget::RendererGL::transformRect(const QRect &raster) const { + return TransformRect(Rect(raster), _viewport, _factor); +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h new file mode 100644 index 000000000..dca170f5c --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_opengl.h @@ -0,0 +1,139 @@ +/* +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 "media/view/media_view_overlay_renderer.h" +#include "ui/gl/gl_image.h" +#include "ui/gl/gl_primitives.h" + +#include + +namespace Media::View { + +class OverlayWidget::RendererGL final : public OverlayWidget::Renderer { +public: + explicit RendererGL(not_null owner); + + void init( + not_null widget, + QOpenGLFunctions &f) override; + + void deinit( + not_null widget, + QOpenGLFunctions &f) override; + + void paint( + not_null widget, + QOpenGLFunctions &f) override; + + std::optional clearColor() override; + +private: + struct Control { + int index = -1; + not_null icon; + }; + bool handleHideWorkaround(QOpenGLFunctions &f); + + void paintBackground() override; + void paintTransformedVideoFrame(ContentGeometry geometry) override; + void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground) override; + void paintTransformedContent( + not_null program, + ContentGeometry geometry); + void paintRadialLoading( + QRect inner, + bool radial, + float64 radialOpacity) override; + void paintThemePreview(QRect outer) override; + void paintDocumentBubble(QRect outer, QRect icon) override; + void paintSaveMsg(QRect outer) override; + void paintControlsStart() override; + void paintControl( + OverState control, + QRect outer, + float64 outerOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) override; + void paintFooter(QRect outer, float64 opacity) override; + void paintCaption(QRect outer, float64 opacity) override; + void paintGroupThumbs(QRect outer, float64 opacity) override; + + void invalidate(); + + void paintUsingRaster( + Ui::GL::Image &image, + QRect rect, + Fn method, + int bufferOffset, + bool transparent = false); + + void validateControls(); + void invalidateControls(); + void toggleBlending(bool enabled); + + [[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect( + const Ui::GL::Rect &raster) const; + + void uploadTexture( + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const; + + const not_null _owner; + + QOpenGLFunctions *_f = nullptr; + QSize _viewport; + float _factor = 1.; + QVector2D _uniformViewport; + + std::optional _contentBuffer; + std::optional _imageProgram; + QOpenGLShader *_texturedVertexShader = nullptr; + std::optional _withTransparencyProgram; + std::optional _yuv420Program; + std::optional _fillProgram; + std::optional _controlsProgram; + Ui::GL::Textures<4> _textures; + QSize _rgbaSize; + QSize _lumaSize; + QSize _chromaSize; + qint64 _cacheKey = 0; + int _trackFrameIndex = 0; + int _streamedIndex = 0; + + Ui::GL::Image _radialImage; + Ui::GL::Image _documentBubbleImage; + Ui::GL::Image _themePreviewImage; + Ui::GL::Image _saveMsgImage; + Ui::GL::Image _footerImage; + Ui::GL::Image _captionImage; + Ui::GL::Image _groupThumbsImage; + Ui::GL::Image _controlsImage; + + static constexpr auto kControlsCount = 6; + [[nodiscard]] static Control ControlMeta(OverState control); + std::array _controlsTextures; + + bool _blendingEnabled = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp new file mode 100644 index 000000000..7f60b9a60 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp @@ -0,0 +1,192 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/view/media_view_overlay_raster.h" + +#include "media/view/media_view_pip.h" + +namespace Media::View { + +OverlayWidget::RendererSW::RendererSW(not_null owner) +: _owner(owner) +, _transparentBrush(style::TransparentPlaceholder()) { +} + +void OverlayWidget::RendererSW::paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { + _p = &p; + _clip = &clip; + _clipOuter = clip.boundingRect(); + _owner->paint(this); + _p = nullptr; + _clip = nullptr; +} + +void OverlayWidget::RendererSW::paintBackground() { + const auto region = _owner->opaqueContentShown() + ? (*_clip - _owner->finalContentRect()) + : *_clip; + + const auto m = _p->compositionMode(); + _p->setCompositionMode(QPainter::CompositionMode_Source); + const auto &bg = _owner->_fullScreenVideo + ? st::mediaviewVideoBg + : st::mediaviewBg; + for (const auto rect : region) { + _p->fillRect(rect, bg); + } + _p->setCompositionMode(m); +} + +QRect OverlayWidget::RendererSW::TransformRect( + QRectF geometry, + int rotation) { + const auto center = geometry.center(); + const auto rect = ((rotation % 180) == 90) + ? QRectF( + center.x() - geometry.height() / 2., + center.y() - geometry.width() / 2., + geometry.height(), + geometry.width()) + : geometry; + return QRect( + int(rect.x()), + int(rect.y()), + int(rect.width()), + int(rect.height())); +} + +void OverlayWidget::RendererSW::paintTransformedVideoFrame( + ContentGeometry geometry) { + Expects(_owner->_streamed != nullptr); + + const auto rotation = int(geometry.rotation); + const auto rect = TransformRect(geometry.rect, rotation); + if (!rect.intersects(_clipOuter)) { + return; + } + paintTransformedImage(_owner->videoFrame(), rect, rotation); +} + +void OverlayWidget::RendererSW::paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground) { + const auto rotation = int(geometry.rotation); + const auto rect = TransformRect(geometry.rect, rotation); + if (!rect.intersects(_clipOuter)) { + return; + } + + if (fillTransparentBackground) { + _p->fillRect(rect, _transparentBrush); + } + if (image.isNull()) { + return; + } + paintTransformedImage(image, rect, rotation); +} + +void OverlayWidget::RendererSW::paintTransformedImage( + const QImage &image, + QRect rect, + int rotation) { + PainterHighQualityEnabler hq(*_p); + if (UsePainterRotation(rotation)) { + if (rotation) { + _p->save(); + _p->rotate(rotation); + } + _p->drawImage(RotatedRect(rect, rotation), image); + if (rotation) { + _p->restore(); + } + } else { + _p->drawImage(rect, _owner->transformShownContent(image, rotation)); + } +} + +void OverlayWidget::RendererSW::paintRadialLoading( + QRect inner, + bool radial, + float64 radialOpacity) { + _owner->paintRadialLoadingContent(*_p, inner, radial, radialOpacity); +} + +void OverlayWidget::RendererSW::paintThemePreview(QRect outer) { + _owner->paintThemePreviewContent(*_p, outer, _clipOuter); +} + +void OverlayWidget::RendererSW::paintDocumentBubble( + QRect outer, + QRect icon) { + if (outer.intersects(_clipOuter)) { + _owner->paintDocumentBubbleContent(*_p, outer, icon, _clipOuter); + if (icon.intersects(_clipOuter)) { + _owner->paintRadialLoading(this); + } + } +} + +void OverlayWidget::RendererSW::paintSaveMsg(QRect outer) { + if (outer.intersects(_clipOuter)) { + _owner->paintSaveMsgContent(*_p, outer, _clipOuter); + } +} + +void OverlayWidget::RendererSW::paintControlsStart() { +} + +void OverlayWidget::RendererSW::paintControl( + OverState control, + QRect outer, + float64 outerOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) { + if (!outer.isEmpty() && !outer.intersects(_clipOuter)) { + return; + } + if (!outer.isEmpty() && outerOpacity > 0) { + _p->setOpacity(outerOpacity); + for (const auto &rect : *_clip) { + const auto fill = outer.intersected(rect); + if (!fill.isEmpty()) { + _p->fillRect(fill, st::mediaviewControlBg); + } + } + } + if (inner.intersects(_clipOuter)) { + _p->setOpacity(innerOpacity); + icon.paintInCenter(*_p, inner); + } +} + +void OverlayWidget::RendererSW::paintFooter(QRect outer, float64 opacity) { + if (outer.intersects(_clipOuter)) { + _owner->paintFooterContent(*_p, outer, _clipOuter, opacity); + } +} + +void OverlayWidget::RendererSW::paintCaption(QRect outer, float64 opacity) { + if (outer.intersects(_clipOuter)) { + _owner->paintCaptionContent(*_p, outer, _clipOuter, opacity); + } +} + +void OverlayWidget::RendererSW::paintGroupThumbs( + QRect outer, + float64 opacity) { + if (outer.intersects(_clipOuter)) { + _owner->paintGroupThumbsContent(*_p, outer, _clipOuter, opacity); + } +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_raster.h b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h new file mode 100644 index 000000000..365eb1c42 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_raster.h @@ -0,0 +1,65 @@ +/* +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 "media/view/media_view_overlay_renderer.h" + +namespace Media::View { + +class OverlayWidget::RendererSW final : public OverlayWidget::Renderer { +public: + explicit RendererSW(not_null owner); + + void paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) override; + +private: + void paintBackground() override; + void paintTransformedVideoFrame(ContentGeometry geometry) override; + void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground) override; + void paintTransformedImage( + const QImage &image, + QRect rect, + int rotation); + void paintRadialLoading( + QRect inner, + bool radial, + float64 radialOpacity) override; + void paintThemePreview(QRect outer) override; + void paintDocumentBubble(QRect outer, QRect icon) override; + void paintSaveMsg(QRect outer) override; + void paintControlsStart() override; + void paintControl( + OverState control, + QRect outer, + float64 outerOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) override; + void paintFooter(QRect outer, float64 opacity) override; + void paintCaption(QRect outer, float64 opacity) override; + void paintGroupThumbs(QRect outer, float64 opacity) override; + + [[nodiscard]] static QRect TransformRect(QRectF geometry, int rotation); + + const not_null _owner; + QBrush _transparentBrush; + + Painter *_p = nullptr; + const QRegion *_clip = nullptr; + QRect _clipOuter; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.h new file mode 100644 index 000000000..6b58abdcc --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_overlay_renderer.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 + +#include "media/view/media_view_overlay_widget.h" + +namespace Media::View { + +class OverlayWidget::Renderer : public Ui::GL::Renderer { +public: + virtual void paintBackground() = 0; + virtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0; + virtual void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry, + bool semiTransparent, + bool fillTransparentBackground) = 0; + virtual void paintRadialLoading( + QRect inner, + bool radial, + float64 radialOpacity) = 0; + virtual void paintThemePreview(QRect outer) = 0; + virtual void paintDocumentBubble(QRect outer, QRect icon) = 0; + virtual void paintSaveMsg(QRect outer) = 0; + virtual void paintControlsStart() = 0; + virtual void paintControl( + OverState control, + QRect outer, + float64 outerOpacity, + QRect inner, + float64 innerOpacity, + const style::icon &icon) = 0; + virtual void paintFooter(QRect outer, float64 opacity) = 0; + virtual void paintCaption(QRect outer, float64 opacity) = 0; + virtual void paintGroupThumbs(QRect outer, float64 opacity) = 0; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 40a9564a7..c239c72ab 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -26,11 +26,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/item_text_options.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" +#include "ui/gl/gl_surface.h" #include "boxes/confirm_box.h" #include "media/audio/media_audio.h" #include "media/view/media_view_playback_controls.h" #include "media/view/media_view_group_thumbs.h" #include "media/view/media_view_pip.h" +#include "media/view/media_view_overlay_raster.h" +#include "media/view/media_view_overlay_opengl.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/player/media_player_instance.h" @@ -46,6 +49,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_media_rotation.h" #include "data/data_photo_media.h" #include "data/data_document_media.h" +#include "data/data_document_resolver.h" +#include "data/data_file_click_handler.h" #include "window/themes/window_theme_preview.h" #include "window/window_peer_menu.h" #include "window/window_session_controller.h" @@ -53,6 +58,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "base/openssl_help.h" #include "base/unixtime.h" +#include "base/qt_signal_producer.h" +#include "base/event_filter.h" #include "main/main_account.h" #include "main/main_domain.h" // Domain::activeSessionValue. #include "main/main_session.h" @@ -134,71 +141,14 @@ QWidget *PipDelegate::pipParentWidget() { return _parent; } -Images::Options VideoThumbOptions(DocumentData *document) { +[[nodiscard]] Images::Options VideoThumbOptions(DocumentData *document) { const auto result = Images::Option::Smooth | Images::Option::Blurred; return (document && document->isVideoMessage()) ? (result | Images::Option::Circled) : result; } -void PaintImageProfile(QPainter &p, const QImage &image, QRect rect, QRect fill) { - const auto argb = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); - const auto rgb = image.convertToFormat(QImage::Format_RGB32); - const auto argbp = QPixmap::fromImage(argb); - const auto rgbp = QPixmap::fromImage(rgb); - const auto width = image.width(); - const auto height = image.height(); - const auto xcopies = (fill.width() + width - 1) / width; - const auto ycopies = (fill.height() + height - 1) / height; - const auto copies = xcopies * ycopies; - auto times = QStringList(); - const auto bench = [&](QString label, auto &&paint) { - const auto single = [&](QString label) { - auto now = crl::now(); - const auto push = [&] { - times.push_back(QString("%1").arg(crl::now() - now, 4, 10, QChar(' '))); - now = crl::now(); - }; - paint(rect); - push(); - { - PainterHighQualityEnabler hq(p); - paint(rect); - } - push(); - for (auto i = 0; i < xcopies; ++i) { - for (auto j = 0; j < ycopies; ++j) { - paint(QRect( - fill.topLeft() + QPoint(i * width, j * height), - QSize(width, height))); - } - } - push(); - LOG(("FRAME (%1): %2 (copies: %3)").arg(label, times.join(' ')).arg(copies)); - times = QStringList(); - now = crl::now(); - }; - p.setCompositionMode(QPainter::CompositionMode_Source); - single(label + " S"); - p.setCompositionMode(QPainter::CompositionMode_SourceOver); - single(label + " O"); - }; - bench("ARGB I", [&](QRect rect) { - p.drawImage(rect, argb); - }); - bench("RGB I", [&](QRect rect) { - p.drawImage(rect, rgb); - }); - bench("ARGB P", [&](QRect rect) { - p.drawPixmap(rect, argbp); - }); - bench("RGB P", [&](QRect rect) { - p.drawPixmap(rect, rgbp); - }); -} - -QPixmap PrepareStaticImage(QImage image) { -#if defined Q_OS_MAC && !defined OS_MAC_OLD +[[nodiscard]] QImage PrepareStaticImage(QImage image) { if (image.width() > kMaxDisplayImageSize || image.height() > kMaxDisplayImageSize) { image = image.scaled( @@ -207,18 +157,38 @@ QPixmap PrepareStaticImage(QImage image) { Qt::KeepAspectRatio, Qt::SmoothTransformation); } -#endif // Q_OS_MAC && !OS_MAC_OLD - return App::pixmapFromImageInPlace(std::move(image)); + return image; } -QPixmap PrepareStaticImage(const QString &path) { +[[nodiscard]] QImage PrepareStaticImage(const QString &path) { return PrepareStaticImage(App::readImage(path, nullptr, false)); } -QPixmap PrepareStaticImage(const QByteArray &bytes) { +[[nodiscard]] QImage PrepareStaticImage(const QByteArray &bytes) { return PrepareStaticImage(App::readImage(bytes, nullptr, false)); } +[[nodiscard]] bool IsSemitransparent(const QImage &image) { + if (image.isNull()) { + return true; + } else if (!image.hasAlphaChannel()) { + return false; + } + Assert(image.format() == QImage::Format_ARGB32_Premultiplied); + constexpr auto kAlphaMask = 0xFF000000; + auto ints = reinterpret_cast(image.bits()); + const auto add = (image.bytesPerLine() / 4) - image.width(); + for (auto y = 0; y != image.height(); ++y) { + for (auto till = ints + image.width(); ints != till; ++ints) { + if ((*ints & kAlphaMask) != kAlphaMask) { + return true; + } + } + ints += add; + } + return false; +} + } // namespace struct OverlayWidget::SharedMedia { @@ -248,21 +218,19 @@ struct OverlayWidget::Streamed { Streamed( not_null document, Data::FileOrigin origin, - QWidget *controlsParent, + not_null controlsParent, not_null controlsDelegate, Fn waitingCallback); Streamed( not_null photo, Data::FileOrigin origin, - QWidget *controlsParent, + not_null controlsParent, not_null controlsDelegate, Fn waitingCallback); Streaming::Instance instance; PlaybackControls controls; - QImage frameForDirectPaint; - bool withSound = false; bool pausedBySeek = false; bool resumeOnCallEnd = false; @@ -287,7 +255,7 @@ struct OverlayWidget::PipWrap { OverlayWidget::Streamed::Streamed( not_null document, Data::FileOrigin origin, - QWidget *controlsParent, + not_null controlsParent, not_null controlsDelegate, Fn waitingCallback) : instance(document, origin, std::move(waitingCallback)) @@ -297,7 +265,7 @@ OverlayWidget::Streamed::Streamed( OverlayWidget::Streamed::Streamed( not_null photo, Data::FileOrigin origin, - QWidget *controlsParent, + not_null controlsParent, not_null controlsDelegate, Fn waitingCallback) : instance(photo, origin, std::move(waitingCallback)) @@ -322,15 +290,18 @@ OverlayWidget::PipWrap::PipWrap( } OverlayWidget::OverlayWidget() -: OverlayParent(nullptr) -, _transparentBrush(style::transparentPlaceholderBrush()) -, _docDownload(this, tr::lng_media_download(tr::now), st::mediaviewFileLink) -, _docSaveAs(this, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) -, _docCancel(this, tr::lng_cancel(tr::now), st::mediaviewFileLink) +: _surface(Ui::GL::CreateSurface( + [=](Ui::GL::Capabilities capabilities) { + return chooseRenderer(capabilities); + })) +, _widget(_surface->rpWidget()) +, _docDownload(_widget, tr::lng_media_download(tr::now), st::mediaviewFileLink) +, _docSaveAs(_widget, tr::lng_mediaview_save_as(tr::now), st::mediaviewFileLink) +, _docCancel(_widget, tr::lng_cancel(tr::now), st::mediaviewFileLink) , _radial([=](crl::time now) { return radialAnimationCallback(now); }) , _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction) , _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); }) -, _dropdown(this, st::mediaviewDropdownMenu) { +, _dropdown(_widget, st::mediaviewDropdownMenu) { Lang::Updated( ) | rpl::start_with_next([=] { refreshLang(); @@ -340,7 +311,7 @@ OverlayWidget::OverlayWidget() ? Core::App().settings().videoVolume() : Core::Settings::kDefaultVolume; - setWindowTitle(qsl("Media viewer")); + _widget->setWindowTitle(qsl("Media viewer")); const auto text = tr::lng_mediaview_saved_to( tr::now, @@ -351,51 +322,120 @@ OverlayWidget::OverlayWidget() Ui::Text::WithEntities); _saveMsgText.setMarkedText(st::mediaviewSaveMsgStyle, text, Ui::DialogTextOptions()); _saveMsg = QRect(0, 0, _saveMsgText.maxWidth() + st::mediaviewSaveMsgPadding.left() + st::mediaviewSaveMsgPadding.right(), st::mediaviewSaveMsgStyle.font->height + st::mediaviewSaveMsgPadding.top() + st::mediaviewSaveMsgPadding.bottom()); + _saveMsgImage = QImage( + _saveMsg.size() * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); - connect(QApplication::desktop(), SIGNAL(resized(int)), this, SLOT(onScreenResized(int))); + _docRectImage = QImage( + st::mediaviewFileSize * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); + _docRectImage.setDevicePixelRatio(cIntRetinaFactor()); + + _surface->shownValue( + ) | rpl::start_with_next([=](bool shown) { + toggleApplicationEventFilter(shown); + if (shown) { + const auto screenList = QGuiApplication::screens(); + DEBUG_LOG(("Viewer Pos: Shown, screen number: %1") + .arg(screenList.indexOf(window()->screen()))); + moveToScreen(); + } else { + clearAfterHide(); + } + }, lifetime()); + + const auto mousePosition = [](not_null e) { + return static_cast(e.get())->pos(); + }; + const auto mouseButton = [](not_null e) { + return static_cast(e.get())->button(); + }; + base::install_event_filter(_widget, [=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Move) { + const auto position = static_cast(e.get())->pos(); + DEBUG_LOG(("Viewer Pos: Moved to %1, %2") + .arg(position.x()) + .arg(position.y())); + } else if (type == QEvent::Resize) { + const auto size = static_cast(e.get())->size(); + DEBUG_LOG(("Viewer Pos: Resized to %1, %2") + .arg(size.width()) + .arg(size.height())); + updateControlsGeometry(); + } else if (type == QEvent::MouseButtonPress) { + handleMousePress(mousePosition(e), mouseButton(e)); + } else if (type == QEvent::MouseButtonRelease) { + handleMouseRelease(mousePosition(e), mouseButton(e)); + } else if (type == QEvent::MouseMove) { + handleMouseMove(mousePosition(e)); + } else if (type == QEvent::KeyPress) { + handleKeyPress(static_cast(e.get())); + } else if (type == QEvent::ContextMenu) { + const auto event = static_cast(e.get()); + const auto mouse = (event->reason() == QContextMenuEvent::Mouse); + const auto position = mouse + ? std::make_optional(event->pos()) + : std::nullopt; + if (handleContextMenu(position)) { + return base::EventFilterResult::Cancel; + } + } else if (type == QEvent::MouseButtonDblClick) { + if (handleDoubleClick(mousePosition(e), mouseButton(e))) { + return base::EventFilterResult::Cancel; + } else { + handleMousePress(mousePosition(e), mouseButton(e)); + } + } else if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd + || type == QEvent::TouchCancel) { + if (handleTouchEvent(static_cast(e.get()))) { + return base::EventFilterResult::Cancel;; + } + } else if (type == QEvent::Wheel) { + handleWheelEvent(static_cast(e.get())); + } + return base::EventFilterResult::Continue; + }); if (Platform::IsLinux()) { - setWindowFlags(Qt::FramelessWindowHint + _widget->setWindowFlags(Qt::FramelessWindowHint | Qt::MaximizeUsingFullscreenGeometryHint); } else if (Platform::IsMac()) { // Without Qt::Tool starting with Qt 5.15.1 this widget // when being opened from a fullscreen main window was // opening not as overlay over the main window, but as // a separate fullscreen window with a separate space. - setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + _widget->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); } else { - setWindowFlags(Qt::FramelessWindowHint); + _widget->setWindowFlags(Qt::FramelessWindowHint); } - updateGeometry(); - updateControlsGeometry(); - setAttribute(Qt::WA_NoSystemBackground, true); - setAttribute(Qt::WA_TranslucentBackground, true); - setMouseTracking(true); + _widget->setAttribute(Qt::WA_NoSystemBackground, true); + _widget->setAttribute(Qt::WA_TranslucentBackground, true); + _widget->setMouseTracking(true); hide(); - createWinId(); + _widget->createWinId(); if (Platform::IsLinux()) { - windowHandle()->setTransientParent(App::wnd()->windowHandle()); - setWindowModality(Qt::WindowModal); + window()->setTransientParent(App::wnd()->windowHandle()); + _widget->setWindowModality(Qt::WindowModal); } if (!Platform::IsMac()) { - setWindowState(Qt::WindowFullScreen); + _widget->setWindowState(Qt::WindowFullScreen); } - connect( - windowHandle(), - &QWindow::visibleChanged, - this, - [=](bool visible) { handleVisibleChanged(visible); }); - connect( - windowHandle(), + QObject::connect( + window(), &QWindow::screenChanged, - this, [=](QScreen *screen) { handleScreenChanged(screen); }); + subscribeToScreenGeometry(); + updateGeometry(); + updateControlsGeometry(); #if defined Q_OS_MAC && !defined OS_OSX TouchBar::SetupMediaViewTouchBar( - winId(), + _widget->winId(), static_cast(this), _touchbarTrackState.events(), _touchbarDisplay.events(), @@ -419,45 +459,43 @@ OverlayWidget::OverlayWidget() _saveMsgUpdater.setCallback([=] { updateImage(); }); - setAttribute(Qt::WA_AcceptTouchEvents); - _touchTimer.setCallback([=] { onTouchTimer(); }); + _widget->setAttribute(Qt::WA_AcceptTouchEvents); + _touchTimer.setCallback([=] { handleTouchTimer(); }); - _controlsHideTimer.setCallback([=] { onHideControls(); }); + _controlsHideTimer.setCallback([=] { hideControls(); }); - _docDownload->addClickHandler([=] { onDownload(); }); - _docSaveAs->addClickHandler([=] { onSaveAs(); }); - _docCancel->addClickHandler([=] { onSaveCancel(); }); + _docDownload->addClickHandler([=] { downloadMedia(); }); + _docSaveAs->addClickHandler([=] { saveAs(); }); + _docCancel->addClickHandler([=] { saveCancel(); }); _dropdown->setHiddenCallback([this] { dropdownHidden(); }); - _dropdownShowTimer.setCallback([=] { onDropdown(); }); + _dropdownShowTimer.setCallback([=] { showDropdown(); }); } void OverlayWidget::refreshLang() { - InvokeQueued(this, [this] { updateThemePreviewGeometry(); }); + InvokeQueued(_widget, [=] { updateThemePreviewGeometry(); }); } void OverlayWidget::moveToScreen() { - Expects(windowHandle()); - const auto widgetScreen = [&](auto &&widget) -> QScreen* { if (auto handle = widget ? widget->windowHandle() : nullptr) { return handle->screen(); } return nullptr; }; - const auto window = Core::App().activeWindow() + const auto applicationWindow = Core::App().activeWindow() ? Core::App().activeWindow()->widget().get() : nullptr; - const auto activeWindowScreen = widgetScreen(window); - const auto myScreen = widgetScreen(this); + const auto activeWindowScreen = widgetScreen(applicationWindow); + const auto myScreen = widgetScreen(_widget); if (activeWindowScreen && myScreen != activeWindowScreen) { const auto screenList = QGuiApplication::screens(); DEBUG_LOG(("Viewer Pos: Currently on screen %1, moving to screen %2") .arg(screenList.indexOf(myScreen)) .arg(screenList.indexOf(activeWindowScreen))); - windowHandle()->setScreen(activeWindowScreen); + window()->setScreen(activeWindowScreen); DEBUG_LOG(("Viewer Pos: New actual screen: %1") - .arg(screenList.indexOf(windowHandle()->screen()))); + .arg(screenList.indexOf(window()->screen()))); } updateGeometry(); } @@ -466,45 +504,37 @@ void OverlayWidget::updateGeometry() { if (Platform::IsWayland()) { return; } - const auto screen = windowHandle() && windowHandle()->screen() - ? windowHandle()->screen() + const auto screen = window()->screen() + ? window()->screen() : QApplication::primaryScreen(); const auto available = screen->geometry(); - if (geometry() == available) { + const auto useSizeHack = _opengl && Platform::IsWindows(); + const auto use = available.marginsAdded({ 0, 0, 0, 1 }); + const auto mask = useSizeHack + ? QRegion(QRect(QPoint(), available.size())) + : QRegion(); + if ((_widget->geometry() == use) + && (!useSizeHack || window()->mask() == mask)) { return; } DEBUG_LOG(("Viewer Pos: Setting %1, %2, %3, %4") - .arg(available.x()) - .arg(available.y()) - .arg(available.width()) - .arg(available.height())); - setGeometry(available); -} - -void OverlayWidget::moveEvent(QMoveEvent *e) { - const auto newPos = e->pos(); - DEBUG_LOG(("Viewer Pos: Moved to %1, %2") - .arg(newPos.x()) - .arg(newPos.y())); - OverlayParent::moveEvent(e); -} - -void OverlayWidget::resizeEvent(QResizeEvent *e) { - const auto newSize = e->size(); - DEBUG_LOG(("Viewer Pos: Resized to %1, %2") - .arg(newSize.width()) - .arg(newSize.height())); - updateControlsGeometry(); - OverlayParent::resizeEvent(e); + .arg(use.x()) + .arg(use.y()) + .arg(use.width()) + .arg(use.height())); + _widget->setGeometry(use); + if (useSizeHack) { + window()->setMask(mask); + } } void OverlayWidget::updateControlsGeometry() { auto navSkip = 2 * st::mediaviewControlMargin + st::mediaviewControlSize; - _closeNav = myrtlrect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, st::mediaviewControlMargin, st::mediaviewControlSize, st::mediaviewControlSize); + _closeNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, st::mediaviewControlMargin, st::mediaviewControlSize, st::mediaviewControlSize); _closeNavIcon = style::centerrect(_closeNav, st::mediaviewClose); - _leftNav = myrtlrect(st::mediaviewControlMargin, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); + _leftNav = QRect(st::mediaviewControlMargin, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); _leftNavIcon = style::centerrect(_leftNav, st::mediaviewLeft); - _rightNav = myrtlrect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); + _rightNav = QRect(width() - st::mediaviewControlMargin - st::mediaviewControlSize, navSkip, st::mediaviewControlSize, height() - 2 * navSkip); _rightNavIcon = style::centerrect(_rightNav, st::mediaviewRight); _saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2); @@ -547,43 +577,26 @@ QImage OverlayWidget::videoFrame() const { : _streamed->instance.info().video.cover; } -QImage OverlayWidget::videoFrameForDirectPaint() const { - Expects(_streamed != nullptr); +Streaming::FrameWithInfo OverlayWidget::videoFrameWithInfo() const { + Expects(videoShown()); - const auto result = videoFrame(); + return _streamed->instance.player().ready() + ? _streamed->instance.frameWithInfo() + : Streaming::FrameWithInfo{ + .original = _streamed->instance.info().video.cover, + .format = Streaming::FrameFormat::ARGB32, + .index = -2, + }; +} -#ifdef USE_OPENGL_OVERLAY_WIDGET - const auto bytesPerLine = result.bytesPerLine(); - if (bytesPerLine == result.width() * 4) { - return result; - } +QImage OverlayWidget::currentVideoFrameImage() const { + return _streamed->instance.player().ready() + ? _streamed->instance.player().currentFrameImage() + : _streamed->instance.info().video.cover; +} - // On macOS 10.8+ we use QOpenGLWidget as OverlayWidget base class. - // The OpenGL painter can't paint textures where byte data is with strides. - // So in that case we prepare a compact copy of the frame to render. - // - // See Qt commit ed557c037847e343caa010562952b398f806adcd - // - auto &cache = _streamed->frameForDirectPaint; - if (cache.size() != result.size()) { - cache = QImage(result.size(), result.format()); - } - const auto height = result.height(); - const auto line = cache.bytesPerLine(); - Assert(line == result.width() * 4); - Assert(line < bytesPerLine); - - auto from = result.bits(); - auto to = cache.bits(); - for (auto y = 0; y != height; ++y) { - memcpy(to, from, line); - to += line; - from += bytesPerLine; - } - return cache; -#endif // USE_OPENGL_OVERLAY_WIDGET - - return result; +int OverlayWidget::streamedIndex() const { + return _streamedCreated; } bool OverlayWidget::documentContentShown() const { @@ -598,6 +611,29 @@ bool OverlayWidget::documentBubbleShown() const { && _staticContent.isNull()); } +void OverlayWidget::setStaticContent(QImage image) { + constexpr auto kGood = QImage::Format_ARGB32_Premultiplied; + if (!image.isNull() + && image.format() != kGood + && image.format() != QImage::Format_RGB32) { + image = std::move(image).convertToFormat(kGood); + } + image.setDevicePixelRatio(cRetinaFactor()); + _staticContent = std::move(image); + _staticContentTransparent = IsSemitransparent(_staticContent); +} + +bool OverlayWidget::contentShown() const { + return _photo || documentContentShown(); +} + +bool OverlayWidget::opaqueContentShown() const { + return contentShown() + && (!_staticContentTransparent + || !_document + || (!_document->isVideoMessage() && !_document->sticker())); +} + void OverlayWidget::clearStreaming(bool savePosition) { if (_streamed && _document && savePosition) { Media::Player::SaveLastPlaybackPosition( @@ -615,7 +651,7 @@ void OverlayWidget::documentUpdated(DocumentData *doc) { updateControls(); } else if (_document->loading()) { updateDocSize(); - update(_docRect); + _widget->update(_docRect); } } else if (_streamed) { const auto ready = _documentMedia->loaded() @@ -687,10 +723,10 @@ void OverlayWidget::checkForSaveLoaded() { return; } else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::QuickSave) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; - onDownload(); + downloadMedia(); } else if (_savePhotoVideoWhenLoaded == SavePhotoVideo::SaveAs) { _savePhotoVideoWhenLoaded = SavePhotoVideo::None; - onSaveAs(); + saveAs(); } else { Unexpected("SavePhotoVideo in OverlayWidget::checkForSaveLoaded."); } @@ -730,7 +766,7 @@ void OverlayWidget::updateControls() { _saveVisible = contentCanBeSaved(); _rotateVisible = !_themePreviewShown; const auto navRect = [&](int i) { - return myrtlrect(width() - st::mediaviewIconSize.width() * i, + return QRect(width() - st::mediaviewIconSize.width() * i, height() - st::mediaviewIconSize.height(), st::mediaviewIconSize.width(), st::mediaviewIconSize.height()); @@ -758,17 +794,17 @@ void OverlayWidget::updateControls() { _dateText = Ui::FormatDateTime(d, cTimeFormat()); if (!_fromName.isEmpty()) { _fromNameLabel.setText(st::mediaviewTextStyle, _fromName, Ui::NameTextOptions()); - _nameNav = myrtlrect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height); - _dateNav = myrtlrect(st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); + _nameNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height); + _dateNav = QRect(st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); } else { _nameNav = QRect(); - _dateNav = myrtlrect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); + _dateNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); } updateHeader(); refreshNavVisibility(); resizeCenteredControls(); - updateOver(mapFromGlobal(QCursor::pos())); + updateOver(_widget->mapFromGlobal(QCursor::pos())); update(); } @@ -827,35 +863,28 @@ void OverlayWidget::refreshCaptionGeometry() { void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { if (_document && _document->loading()) { - addAction(tr::lng_cancel(tr::now), [=] { onSaveCancel(); }); + addAction(tr::lng_cancel(tr::now), [=] { saveCancel(); }); } if (IsServerMsgId(_msgid.msg)) { - addAction(tr::lng_context_to_msg(tr::now), [=] { onToMessage(); }); + addAction(tr::lng_context_to_msg(tr::now), [=] { toMessage(); }); } if (_document && !_document->filepath(true).isEmpty()) { const auto text = Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now); - addAction(text, [=] { onShowInFolder(); }); + addAction(text, [=] { showInFolder(); }); } if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) { - addAction(tr::lng_mediaview_copy(tr::now), [=] { onCopy(); }); + addAction(tr::lng_mediaview_copy(tr::now), [=] { copyMedia(); }); } if ((_photo && _photo->hasAttachedStickers()) || (_document && _document->hasAttachedStickers())) { - auto callback = [=] { - if (_photo) { - onPhotoAttachedStickers(); - } else if (_document) { - onDocumentAttachedStickers(); - } - }; addAction( tr::lng_context_attached_stickers(tr::now), - std::move(callback)); + [=] { showAttachedStickers(); }); } if (_canForwardItem) { - addAction(tr::lng_mediaview_forward(tr::now), [=] { onForward(); }); + addAction(tr::lng_mediaview_forward(tr::now), [=] { forwardMedia(); }); } const auto canDelete = [&] { if (_canDeleteItem) { @@ -875,15 +904,15 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { return false; }(); if (canDelete) { - addAction(tr::lng_mediaview_delete(tr::now), [=] { onDelete(); }); + addAction(tr::lng_mediaview_delete(tr::now), [=] { deleteMedia(); }); } - addAction(tr::lng_mediaview_save_as(tr::now), [=] { onSaveAs(); }); + addAction(tr::lng_mediaview_save_as(tr::now), [=] { saveAs(); }); if (const auto overviewType = computeOverviewType()) { const auto text = _document ? tr::lng_mediaview_files_all(tr::now) : tr::lng_mediaview_photos_all(tr::now); - addAction(text, [=] { onOverview(); }); + addAction(text, [=] { showMediaOverview(); }); } } @@ -969,19 +998,64 @@ void OverlayWidget::updateCursor() { : (_over == OverNone ? style::cur_default : style::cur_pointer)); } -int OverlayWidget::contentRotation() const { - if (!_streamed) { - return _rotation; - } - return (_rotation + (_streamed - ? _streamed->instance.info().video.rotation - : 0)) % 360; +int OverlayWidget::finalContentRotation() const { + return _streamed + ? ((_rotation + (_streamed + ? _streamed->instance.info().video.rotation + : 0)) % 360) + : _rotation; } -QRect OverlayWidget::contentRect() const { +QRect OverlayWidget::finalContentRect() const { return { _x, _y, _w, _h }; } +OverlayWidget::ContentGeometry OverlayWidget::contentGeometry() const { + const auto toRotation = qreal(finalContentRotation()); + const auto toRectRotated = QRectF(finalContentRect()); + const auto toRectCenter = toRectRotated.center(); + const auto toRect = ((int(toRotation) % 180) == 90) + ? QRectF( + toRectCenter.x() - toRectRotated.height() / 2., + toRectCenter.y() - toRectRotated.width() / 2., + toRectRotated.height(), + toRectRotated.width()) + : toRectRotated; + if (!_geometryAnimation.animating()) { + return { toRect, toRotation }; + } + const auto fromRect = _oldGeometry.rect; + const auto fromRotation = _oldGeometry.rotation; + const auto progress = _geometryAnimation.value(1.); + const auto rotationDelta = (toRotation - fromRotation); + const auto useRotationDelta = (rotationDelta > 180.) + ? (rotationDelta - 360.) + : (rotationDelta <= -180.) + ? (rotationDelta + 360.) + : rotationDelta; + const auto rotation = fromRotation + useRotationDelta * progress; + const auto useRotation = (rotation > 360.) + ? (rotation - 360.) + : (rotation < 0.) + ? (rotation + 360.) + : rotation; + const auto useRect = QRectF( + fromRect.x() + (toRect.x() - fromRect.x()) * progress, + fromRect.y() + (toRect.y() - fromRect.y()) * progress, + fromRect.width() + (toRect.width() - fromRect.width()) * progress, + fromRect.height() + (toRect.height() - fromRect.height()) * progress + ); + return { useRect, useRotation }; +} + +void OverlayWidget::updateContentRect() { + if (_opengl) { + update(); + } else { + update(finalContentRect()); + } +} + void OverlayWidget::contentSizeChanged() { _width = _w; _height = _h; @@ -1035,6 +1109,7 @@ void OverlayWidget::resizeContentByScreenSize() { } _x = (width() - _w) / 2; _y = (height() - _h) / 2; + _geometryAnimation.stop(); } float64 OverlayWidget::radialProgress() const { @@ -1256,14 +1331,12 @@ void OverlayWidget::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pr update(QRegion(_saveMsg) + _captionRect); } -void OverlayWidget::showSaveMsgFile() { - File::ShowInFolder(_saveMsgFilename); +rpl::lifetime &OverlayWidget::lifetime() { + return _surface->lifetime(); } -void OverlayWidget::updateMixerVideoVolume() const { - if (_streamed) { - Player::mixer()->setVideoVolume(Core::App().settings().videoVolume()); - } +void OverlayWidget::showSaveMsgFile() { + File::ShowInFolder(_saveMsgFilename); } void OverlayWidget::close() { @@ -1289,7 +1362,7 @@ void OverlayWidget::activateControls() { } } -void OverlayWidget::onHideControls(bool force) { +void OverlayWidget::hideControls(bool force) { if (!force) { if (!_dropdown->isHidden() || (_streamed && _streamed->controls.hasMenu()) @@ -1306,7 +1379,7 @@ void OverlayWidget::onHideControls(bool force) { } if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return; - _lastMouseMovePos = mapFromGlobal(QCursor::pos()); + _lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos()); _controlsState = ControlsHiding; _controlsAnimStarted = crl::now(); _controlsOpacity.start(0); @@ -1318,43 +1391,17 @@ void OverlayWidget::onHideControls(bool force) { void OverlayWidget::dropdownHidden() { setFocus(); _ignoringDropdown = true; - _lastMouseMovePos = mapFromGlobal(QCursor::pos()); + _lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos()); updateOver(_lastMouseMovePos); _ignoringDropdown = false; if (!_controlsHideTimer.isActive()) { - onHideControls(true); - } -} - -void OverlayWidget::onScreenResized(int screen) { - if (isHidden()) { - return; - } - - const auto screens = QApplication::screens(); - const auto changed = (screen >= 0 && screen < screens.size()) - ? screens[screen] - : nullptr; - if (windowHandle() - && windowHandle()->screen() - && changed - && windowHandle()->screen() == changed) { - updateGeometry(); - } -} - -void OverlayWidget::handleVisibleChanged(bool visible) { - if (visible) { - const auto screenList = QGuiApplication::screens(); - DEBUG_LOG(("Viewer Pos: Shown, screen number: %1") - .arg(screenList.indexOf(windowHandle()->screen()))); - - moveToScreen(); + hideControls(true); } } void OverlayWidget::handleScreenChanged(QScreen *screen) { - if (!isVisible()) { + subscribeToScreenGeometry(); + if (isHidden()) { return; } @@ -1365,13 +1412,30 @@ void OverlayWidget::handleScreenChanged(QScreen *screen) { moveToScreen(); } -void OverlayWidget::onToMessage() { +void OverlayWidget::subscribeToScreenGeometry() { + _screenGeometryLifetime.destroy(); + const auto screen = window()->screen(); + if (!screen) { + return; + } + base::qt_signal_producer( + screen, + &QScreen::geometryChanged + ) | rpl::start_with_next([=] { + updateGeometry(); + }, _screenGeometryLifetime); +} + +void OverlayWidget::toMessage() { if (!_session) { return; } + if (const auto item = _session->data().message(_msgid)) { close(); - Ui::showPeerHistoryAtItem(item); + if (const auto window = findWindow()) { + window->showPeerHistoryAtItem(item); + } } } @@ -1380,13 +1444,13 @@ void OverlayWidget::notifyFileDialogShown(bool shown) { return; } if (shown) { - Ui::Platform::BringToBack(this); + Ui::Platform::BringToBack(_widget); } else { - Ui::Platform::ShowOverAll(this); + Ui::Platform::ShowOverAll(_widget); } } -void OverlayWidget::onSaveAs() { +void OverlayWidget::saveAs() { QString file; if (_document) { const auto &location = _document->location(true); @@ -1440,9 +1504,10 @@ void OverlayWidget::onSaveAs() { } } else if (_photo && _photo->hasVideo()) { if (const auto bytes = _photoMedia->videoContent(); !bytes.isEmpty()) { + const auto photo = _photo; auto filter = qsl("Video Files (*.mp4);;") + FileDialog::AllFilesFilter(); FileDialog::GetWritePath( - this, + _widget.get(), tr::lng_save_video(tr::now), filter, filedialogDefaultName( @@ -1451,7 +1516,7 @@ void OverlayWidget::onSaveAs() { QString(), false, _photo->date), - crl::guard(this, [=, photo = _photo](const QString &result) { + crl::guard(_widget, [=](const QString &result) { QFile f(result); if (!result.isEmpty() && _photo == photo @@ -1469,9 +1534,10 @@ void OverlayWidget::onSaveAs() { } const auto image = _photoMedia->image(Data::PhotoSize::Large)->original(); + const auto photo = _photo; auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter(); FileDialog::GetWritePath( - this, + _widget.get(), tr::lng_save_photo(tr::now), filter, filedialogDefaultName( @@ -1480,23 +1546,21 @@ void OverlayWidget::onSaveAs() { QString(), false, _photo->date), - crl::guard(this, [=, photo = _photo](const QString &result) { + crl::guard(_widget, [=](const QString &result) { if (!result.isEmpty() && _photo == photo) { image.save(result, "JPG"); } })); } - activateWindow(); - QApplication::setActiveWindow(this); - setFocus(); + activate(); } -void OverlayWidget::onDocClick() { +void OverlayWidget::handleDocumentClick() { if (_document->loading()) { - onSaveCancel(); + saveCancel(); } else { - DocumentOpenClickHandler::Open( - fileOrigin(), + Data::ResolveDocument( + findWindow(), _document, _document->owner().message(_msgid)); if (_document->loading() && !_radial.animating()) { @@ -1509,12 +1573,12 @@ PeerData *OverlayWidget::ui_getPeerForMouseAction() { return _history ? _history->peer.get() : nullptr; } -void OverlayWidget::onDownload() { +void OverlayWidget::downloadMedia() { if (!_photo && !_document) { return; } if (Core::App().settings().askDownloadPath()) { - return onSaveAs(); + return saveAs(); } QString path; @@ -1596,7 +1660,7 @@ void OverlayWidget::onDownload() { } } -void OverlayWidget::onSaveCancel() { +void OverlayWidget::saveCancel() { if (_document && _document->loading()) { _document->cancel(); if (_documentMedia->canBePlayed()) { @@ -1605,7 +1669,7 @@ void OverlayWidget::onSaveCancel() { } } -void OverlayWidget::onShowInFolder() { +void OverlayWidget::showInFolder() { if (!_document) return; auto filepath = _document->filepath(true); @@ -1615,7 +1679,7 @@ void OverlayWidget::onShowInFolder() { } } -void OverlayWidget::onForward() { +void OverlayWidget::forwardMedia() { if (!_session) { return; } @@ -1634,7 +1698,7 @@ void OverlayWidget::onForward() { { 1, item->fullId() }); } -void OverlayWidget::onDelete() { +void OverlayWidget::deleteMedia() { if (!_session) { return; } @@ -1667,7 +1731,7 @@ void OverlayWidget::onDelete() { } } -void OverlayWidget::onOverview() { +void OverlayWidget::showMediaOverview() { if (_menu) { _menu->hideMenu(true); } @@ -1678,12 +1742,10 @@ void OverlayWidget::onOverview() { } } -void OverlayWidget::onCopy() { +void OverlayWidget::copyMedia() { _dropdown->hideAnimated(Ui::DropdownMenu::HideOption::IgnoreShow); if (_document) { - QGuiApplication::clipboard()->setImage(videoShown() - ? transformVideoFrame(videoFrame()) - : transformStaticContent(_staticContent)); + QGuiApplication::clipboard()->setImage(transformedShownContent()); } else if (_photo && _photoMedia->loaded()) { const auto image = _photoMedia->image( Data::PhotoSize::Large)->original(); @@ -1691,31 +1753,23 @@ void OverlayWidget::onCopy() { } } -void OverlayWidget::onPhotoAttachedStickers() { - if (!_session || !_photo) { +void OverlayWidget::showAttachedStickers() { + if (!_session) { return; } const auto &active = _session->windows(); if (active.empty()) { return; } - _session->api().attachedStickers().requestAttachedStickerSets( - active.front(), - _photo); - close(); -} - -void OverlayWidget::onDocumentAttachedStickers() { - if (!_session || !_document) { + const auto window = active.front(); + auto &attachedStickers = _session->api().attachedStickers(); + if (_photo) { + attachedStickers.requestAttachedStickerSets(window, _photo); + } else if (_document) { + attachedStickers.requestAttachedStickerSets(window, _document); + } else { return; } - const auto &active = _session->windows(); - if (active.empty()) { - return; - } - _session->api().attachedStickers().requestAttachedStickerSets( - active.front(), - _document); close(); } @@ -2147,73 +2201,107 @@ void OverlayWidget::clearControlsState() { } } -void OverlayWidget::showPhoto( - not_null photo, - HistoryItem *context) { - setSession(&photo->session()); - - if (context) { - setContext(context); - } else { - setContext(v::null); - } - - clearControlsState(); - _firstOpenedPeerPhoto = false; - assignMediaPointer(photo); - - displayPhoto(photo, context); - preloadData(0); - activateControls(); +not_null OverlayWidget::window() const { + return _widget->windowHandle(); } -void OverlayWidget::showPhoto( - not_null photo, - not_null context) { - setSession(&photo->session()); - setContext(context); - - clearControlsState(); - _firstOpenedPeerPhoto = true; - assignMediaPointer(photo); - - displayPhoto(photo, nullptr); - preloadData(0); - activateControls(); +int OverlayWidget::width() const { + return _widget->width(); } -void OverlayWidget::showDocument( - not_null document, - HistoryItem *context) { - showDocument(document, context, Data::CloudTheme(), false); +int OverlayWidget::height() const { + return _widget->height(); } -void OverlayWidget::showTheme( - not_null document, - const Data::CloudTheme &cloud) { - showDocument(document, nullptr, cloud, false); +void OverlayWidget::update() { + _widget->update(); } -void OverlayWidget::showDocument( - not_null document, - HistoryItem *context, - const Data::CloudTheme &cloud, - bool continueStreaming) { - setSession(&document->session()); +void OverlayWidget::update(const QRegion ®ion) { + _widget->update(region); +} - if (context) { - setContext(context); - } else { - setContext(v::null); - } +bool OverlayWidget::isHidden() const { + return _widget->isHidden(); +} - clearControlsState(); +not_null OverlayWidget::widget() const { + return _widget; +} - _streamingStartPaused = false; - displayDocument(document, context, cloud, continueStreaming); - if (!isHidden()) { +void OverlayWidget::hide() { + clearBeforeHide(); + applyHideWindowWorkaround(); + _widget->hide(); +} + +void OverlayWidget::setCursor(style::cursor cursor) { + _widget->setCursor(cursor); +} + +void OverlayWidget::setFocus() { + _widget->setFocus(); +} + +void OverlayWidget::activate() { + _widget->raise(); + _widget->activateWindow(); + QApplication::setActiveWindow(_widget); + setFocus(); +} + +void OverlayWidget::show(OpenRequest request) { + const auto document = request.document(); + const auto photo = request.photo(); + const auto contextItem = request.item(); + const auto contextPeer = request.peer(); + if (photo) { + if (contextItem && contextPeer) { + return; + } + setSession(&photo->session()); + + if (contextPeer) { + setContext(contextPeer); + } else if (contextItem) { + setContext(contextItem); + } else { + setContext(v::null); + } + + clearControlsState(); + _firstOpenedPeerPhoto = (contextPeer != nullptr); + assignMediaPointer(photo); + + displayPhoto(photo, contextPeer ? nullptr : contextItem); preloadData(0); activateControls(); + } else if (document) { + setSession(&document->session()); + + if (contextItem) { + setContext(contextItem); + } else { + setContext(v::null); + } + + clearControlsState(); + + _streamingStartPaused = false; + displayDocument( + document, + contextItem, + request.cloudTheme() + ? *request.cloudTheme() + : Data::CloudTheme(), + request.continueStreaming()); + if (!isHidden()) { + preloadData(0); + activateControls(); + } + } + if (const auto controller = request.controller()) { + _window = base::make_weak(&controller->window()); } } @@ -2234,7 +2322,7 @@ void OverlayWidget::displayPhoto(not_null photo, HistoryItem *item) refreshMediaViewer(); - _staticContent = QPixmap(); + _staticContent = QImage(); if (_photo->videoCanBePlayed()) { initStreaming(); } @@ -2289,7 +2377,7 @@ void OverlayWidget::displayDocument( const Data::CloudTheme &cloud, bool continueStreaming) { _fullScreenVideo = false; - _staticContent = QPixmap(); + _staticContent = QImage(); clearStreaming(_document != doc); destroyThemePreview(); assignMediaPointer(doc); @@ -2304,11 +2392,12 @@ void OverlayWidget::displayDocument( if (_document) { if (_document->sticker()) { if (const auto image = _documentMedia->getStickerLarge()) { - _staticContent = image->pix(); + setStaticContent(image->original()); } else if (const auto thumbnail = _documentMedia->thumbnail()) { - _staticContent = thumbnail->pixBlurred( + setStaticContent(thumbnail->pixBlurred( _document->dimensions.width(), - _document->dimensions.height()); + _document->dimensions.height() + ).toImage()); } } else { if (_documentMedia->canBePlayed() @@ -2326,11 +2415,12 @@ void OverlayWidget::displayDocument( if (location.accessEnable()) { const auto &path = location.name(); if (QImageReader(path).canRead()) { - _staticContent = PrepareStaticImage(path); + setStaticContent(PrepareStaticImage(path)); _touchbarDisplay.fire(TouchBarItemType::Photo); } } else if (!_documentMedia->bytes().isEmpty()) { - _staticContent = PrepareStaticImage(_documentMedia->bytes()); + setStaticContent( + PrepareStaticImage(_documentMedia->bytes())); if (!_staticContent.isNull()) { _touchbarDisplay.fire(TouchBarItemType::Photo); } @@ -2342,20 +2432,19 @@ void OverlayWidget::displayDocument( refreshCaption(item); _docIconRect = QRect((width() - st::mediaviewFileIconSize) / 2, (height() - st::mediaviewFileIconSize) / 2, st::mediaviewFileIconSize, st::mediaviewFileIconSize); - if (documentBubbleShown()) { - if (!_document || !_document->hasThumbnail()) { - int32 colorIndex = documentColorIndex(_document, _docExt); - _docIconColor = documentColor(colorIndex); - const style::icon *(thumbs[]) = { &st::mediaviewFileBlue, &st::mediaviewFileGreen, &st::mediaviewFileRed, &st::mediaviewFileYellow }; - _docIcon = thumbs[colorIndex]; + int32 colorIndex = documentColorIndex(_document, _docExt); + _docIconColor = documentColor(colorIndex); + const style::icon *(thumbs[]) = { &st::mediaviewFileBlue, &st::mediaviewFileGreen, &st::mediaviewFileRed, &st::mediaviewFileYellow }; + _docIcon = thumbs[colorIndex]; - int32 extmaxw = (st::mediaviewFileIconSize - st::mediaviewFileExtPadding * 2); - _docExtWidth = st::mediaviewFileExtFont->width(_docExt); - if (_docExtWidth > extmaxw) { - _docExt = st::mediaviewFileExtFont->elided(_docExt, extmaxw, Qt::ElideMiddle); - _docExtWidth = st::mediaviewFileExtFont->width(_docExt); - } - } else { + int32 extmaxw = (st::mediaviewFileIconSize - st::mediaviewFileExtPadding * 2); + _docExtWidth = st::mediaviewFileExtFont->width(_docExt); + if (_docExtWidth > extmaxw) { + _docExt = st::mediaviewFileExtFont->elided(_docExt, extmaxw, Qt::ElideMiddle); + _docExtWidth = st::mediaviewFileExtFont->width(_docExt); + } + if (documentBubbleShown()) { + if (_document && _document->hasThumbnail()) { _document->loadThumbnail(fileOrigin()); const auto tw = _documentMedia->thumbnailSize().width(); const auto th = _documentMedia->thumbnailSize().height(); @@ -2394,11 +2483,10 @@ void OverlayWidget::displayDocument( // _docSize is updated in updateControls() _docRect = QRect((width() - st::mediaviewFileSize.width()) / 2, (height() - st::mediaviewFileSize.height()) / 2, st::mediaviewFileSize.width(), st::mediaviewFileSize.height()); - _docIconRect = myrtlrect(_docRect.x() + st::mediaviewFilePadding, _docRect.y() + st::mediaviewFilePadding, st::mediaviewFileIconSize, st::mediaviewFileIconSize); + _docIconRect = QRect(_docRect.x() + st::mediaviewFilePadding, _docRect.y() + st::mediaviewFilePadding, st::mediaviewFileIconSize, st::mediaviewFileIconSize); } else if (_themePreviewShown) { updateThemePreviewGeometry(); } else if (!_staticContent.isNull()) { - _staticContent.setDevicePixelRatio(cRetinaFactor()); const auto size = style::ConvertScale( flipSizeByRotation(_staticContent.size())); _w = size.width(); @@ -2409,6 +2497,9 @@ void OverlayWidget::displayDocument( _h = contentSize.height(); } contentSizeChanged(); + if (videoShown()) { + applyVideoSize(); + } refreshFromLabel(item); _blurred = false; if (_showAsPip && _streamed && !videoIsGifOrUserpic()) { @@ -2444,17 +2535,19 @@ void OverlayWidget::updateThemePreviewGeometry() { void OverlayWidget::displayFinished() { updateControls(); if (isHidden()) { - Ui::Platform::UpdateOverlayed(this); moveToScreen(); + //setAttribute(Qt::WA_DontShowOnScreen); + //OverlayParent::setVisibleHook(true); + //OverlayParent::setVisibleHook(false); + //setAttribute(Qt::WA_DontShowOnScreen, false); + Ui::Platform::UpdateOverlayed(_widget); if (Platform::IsLinux()) { - showFullScreen(); + _widget->showFullScreen(); } else { - show(); + _widget->show(); } - Ui::Platform::ShowOverAll(this); - activateWindow(); - QApplication::setActiveWindow(this); - setFocus(); + Ui::Platform::ShowOverAll(_widget); + activate(); } } @@ -2568,7 +2661,7 @@ void OverlayWidget::initStreamingThumbnail() { const auto h = size.height(); const auto options = VideoThumbOptions(_document); const auto goodOptions = (options & ~Images::Option::Blurred); - _staticContent = (good + setStaticContent((good ? good : thumbnail ? thumbnail @@ -2579,25 +2672,27 @@ void OverlayWidget::initStreamingThumbnail() { h, good ? goodOptions : options, w / cIntRetinaFactor(), - h / cIntRetinaFactor()); - _staticContent.setDevicePixelRatio(cRetinaFactor()); + h / cIntRetinaFactor() + ).toImage()); } void OverlayWidget::streamingReady(Streaming::Information &&info) { if (videoShown()) { applyVideoSize(); + } else { + updateContentRect(); } - update(contentRect()); } void OverlayWidget::applyVideoSize() { const auto contentSize = style::ConvertScale(videoSize()); if (contentSize != QSize(_width, _height)) { - update(contentRect()); + updateContentRect(); _w = contentSize.width(); _h = contentSize.height(); contentSizeChanged(); } + updateContentRect(); } bool OverlayWidget::createStreamingObjects() { @@ -2607,14 +2702,14 @@ bool OverlayWidget::createStreamingObjects() { _streamed = std::make_unique( _document, fileOrigin(), - this, + _widget, static_cast(this), [=] { waitingAnimationCallback(); }); } else { _streamed = std::make_unique( _photo, fileOrigin(), - this, + _widget, static_cast(this), [=] { waitingAnimationCallback(); }); } @@ -2622,6 +2717,7 @@ bool OverlayWidget::createStreamingObjects() { _streamed = nullptr; return false; } + ++_streamedCreated; _streamed->instance.setPriority(kOverlayLoaderPriority); _streamed->instance.lockPlayer(); _streamed->withSound = _document @@ -2639,27 +2735,28 @@ bool OverlayWidget::createStreamingObjects() { return true; } -QImage OverlayWidget::transformVideoFrame(QImage frame) const { - Expects(videoShown()); - - const auto rotation = contentRotation(); - if (rotation != 0) { - frame = RotateFrameImage(std::move(frame), rotation); - } - const auto requiredSize = videoSize(); - if (frame.size() != requiredSize) { - frame = frame.scaled( - requiredSize, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - } - return frame; +QImage OverlayWidget::transformedShownContent() const { + return transformShownContent( + videoShown() ? currentVideoFrameImage() : _staticContent, + finalContentRotation()); } -QImage OverlayWidget::transformStaticContent(QPixmap content) const { - return _rotation - ? RotateFrameImage(content.toImage(), _rotation) - : content.toImage(); +QImage OverlayWidget::transformShownContent( + QImage content, + int rotation) const { + if (rotation) { + content = RotateFrameImage(std::move(content), rotation); + } + if (videoShown()) { + const auto requiredSize = videoSize(); + if (content.size() != requiredSize) { + content = content.scaled( + requiredSize, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + } + return content; } void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { @@ -2670,7 +2767,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) { }, [&](const PreloadedVideo &update) { updatePlaybackState(); }, [&](const UpdateVideo &update) { - this->update(contentRect()); + updateContentRect(); Core::App().updateNonIdle(); updatePlaybackState(); }, [&](const PreloadedAudio &update) { @@ -2743,7 +2840,7 @@ void OverlayWidget::initThemePreview() { const auto weakSession = base::make_weak(&_document->session()); const auto path = _document->location().name(); const auto id = _themePreviewId = openssl::RandomValue(); - const auto weak = Ui::MakeWeak(this); + const auto weak = Ui::MakeWeak(_widget); auto langStrings = CollectStrings(); crl::async([=, data = std::move(current)]() mutable { auto preview = GeneratePreview( @@ -2762,7 +2859,7 @@ void OverlayWidget::initThemePreview() { _themePreview = std::move(result); if (_themePreview) { _themeApply.create( - this, + _widget, tr::lng_theme_preview_apply(), st::themePreviewApplyButton); _themeApply->show(); @@ -2778,14 +2875,14 @@ void OverlayWidget::initThemePreview() { } }); _themeCancel.create( - this, + _widget, tr::lng_cancel(), st::themePreviewCancelButton); _themeCancel->show(); _themeCancel->setClickedCallback([this] { close(); }); if (const auto slug = _themeCloudData.slug; !slug.isEmpty()) { _themeShare.create( - this, + _widget, tr::lng_theme_share(), st::themePreviewCancelButton); _themeShare->show(); @@ -2793,7 +2890,7 @@ void OverlayWidget::initThemePreview() { QGuiApplication::clipboard()->setText( session->createInternalLinkFull("addtheme/" + slug)); Ui::Toast::Show( - this, + _widget, tr::lng_background_link_copied(tr::now)); }); } else { @@ -2849,6 +2946,8 @@ void OverlayWidget::playbackControlsToPictureInPicture() { } void OverlayWidget::playbackControlsRotate() { + _oldGeometry = contentGeometry(); + _geometryAnimation.stop(); if (_photo) { auto &storage = _photo->owner().mediaRotation(); storage.set(_photo, storage.get(_photo) - 90); @@ -2860,11 +2959,18 @@ void OverlayWidget::playbackControlsRotate() { _rotation = storage.get(_document); if (videoShown()) { applyVideoSize(); - update(contentRect()); } else { redisplayContent(); } } + if (_opengl) { + _geometryAnimation.start( + [=] { update(); }, + 0., + 1., + st::widgetFadeDuration/*, + st::easeOutCirc*/); + } } void OverlayWidget::playbackPauseResume() { @@ -2896,9 +3002,9 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) { if (videoShown()) { _streamed->instance.saveFrameToCover(); const auto saved = base::take(_rotation); - _staticContent = Images::PixmapFast(transformVideoFrame(videoFrame())); + setStaticContent(transformedShownContent()); _rotation = saved; - update(contentRect()); + updateContentRect(); } auto options = Streaming::PlaybackOptions(); options.position = position; @@ -2943,9 +3049,11 @@ void OverlayWidget::playbackControlsSeekFinished(crl::time position) { } void OverlayWidget::playbackControlsVolumeChanged(float64 volume) { + if (_streamed) { + Player::mixer()->setVideoVolume(volume); + } Core::App().settings().setVideoVolume(volume); Core::App().saveSettingsDelayed(); - updateMixerVideoVolume(); } float64 OverlayWidget::playbackControlsCurrentVolume() { @@ -2991,11 +3099,15 @@ void OverlayWidget::switchToPip() { const auto msgId = _msgid; const auto closeAndContinue = [=] { _showAsPip = false; - showDocument(document, document->owner().message(msgId), {}, true); + show(OpenRequest( + findWindow(), + document, + document->owner().message(msgId), + true)); }; _showAsPip = true; _pip = std::make_unique( - this, + _widget, document, msgId, _streamed->instance.shared(), @@ -3089,16 +3201,19 @@ void OverlayWidget::validatePhotoImage(Image *image, bool blurred) { } const auto use = flipSizeByRotation({ _width, _height }) * cIntRetinaFactor(); - _staticContent = image->pixNoCache( + setStaticContent(image->pixNoCache( use.width(), use.height(), Images::Option::Smooth - | (blurred ? Images::Option::Blurred : Images::Option(0))); - _staticContent.setDevicePixelRatio(cRetinaFactor()); + | (blurred ? Images::Option::Blurred : Images::Option(0)) + ).toImage()); _blurred = blurred; } void OverlayWidget::validatePhotoCurrentImage() { + if (!_photo) { + return; + } validatePhotoImage(_photoMedia->image(Data::PhotoSize::Large), false); validatePhotoImage(_photoMedia->image(Data::PhotoSize::Thumbnail), true); validatePhotoImage(_photoMedia->image(Data::PhotoSize::Small), true); @@ -3116,258 +3231,72 @@ void OverlayWidget::validatePhotoCurrentImage() { } } -void OverlayWidget::paintEvent(QPaintEvent *e) { - const auto r = e->rect(); - const auto region = e->region(); - const auto contentShown = _photo || documentContentShown(); - const auto opaqueContentShown = contentShown - && (!_document - || (!_document->isVideoMessage() && !_document->sticker())); - const auto bgRegion = opaqueContentShown - ? (region - contentRect()) - : region; - - auto ms = crl::now(); - - Painter p(this); - - bool name = false; - - p.setClipRegion(region); - - // main bg - const auto m = p.compositionMode(); - p.setCompositionMode(QPainter::CompositionMode_Source); - const auto bgColor = _fullScreenVideo ? st::mediaviewVideoBg : st::mediaviewBg; - for (const auto rect : bgRegion) { - p.fillRect(rect, bgColor); +Ui::GL::ChosenRenderer OverlayWidget::chooseRenderer( + Ui::GL::Capabilities capabilities) { + const auto use = Platform::IsMac() + ? true + : capabilities.transparency; + LOG(("OpenGL: %1 (OverlayWidget)").arg(Logs::b(use))); + if (use) { + _opengl = true; + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::OpenGL, + }; } - p.setCompositionMode(m); + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::Raster, + }; +} - // photo - if (_photo) { - validatePhotoCurrentImage(); - } - p.setOpacity(1); - if (contentShown) { - const auto rect = contentRect(); - if (rect.intersects(r)) { - if (videoShown()) { - paintTransformedVideoFrame(p); - } else { - paintTransformedStaticContent(p); +void OverlayWidget::paint(not_null renderer) { + renderer->paintBackground(); + if (contentShown()) { + if (videoShown()) { + renderer->paintTransformedVideoFrame(contentGeometry()); + if (_streamed->instance.player().ready()) { + _streamed->instance.markFrameShown(); } - - const auto radial = _radial.animating(); - const auto radialOpacity = radial ? _radial.opacity() : 0.; - paintRadialLoading(p, radial, radialOpacity); + } else { + validatePhotoCurrentImage(); + const auto fillTransparentBackground = (!_document + || (!_document->sticker() && !_document->isVideoMessage())) + && _staticContentTransparent; + renderer->paintTransformedStaticContent( + _staticContent, + contentGeometry(), + _staticContentTransparent, + fillTransparentBackground); } - if (_saveMsgStarted && _saveMsg.intersects(r)) { - float64 dt = float64(ms) - _saveMsgStarted, hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown; - if (dt < st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + st::mediaviewSaveMsgHiding) { - if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) { - _saveMsgOpacity.start(0); - } - float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing); - _saveMsgOpacity.update(qMin(progress, 1.), anim::linear); - if (_saveMsgOpacity.current() > 0) { - p.setOpacity(_saveMsgOpacity.current()); - Ui::FillRoundRect(p, _saveMsg, st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners); - st::mediaviewSaveMsgCheck.paint(p, _saveMsg.topLeft() + st::mediaviewSaveMsgCheckPos, width()); - - p.setPen(st::mediaviewSaveMsgFg); - p.setTextPalette(st::mediaviewTextPalette); - _saveMsgText.draw(p, _saveMsg.x() + st::mediaviewSaveMsgPadding.left(), _saveMsg.y() + st::mediaviewSaveMsgPadding.top(), _saveMsg.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right()); - p.restoreTextPalette(); - p.setOpacity(1); - } - if (!_blurred) { - auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt); - _saveMsgUpdater.callOnce(nextFrame); - } - } else { - _saveMsgStarted = 0; - } - } - } else if (_themePreviewShown) { - paintThemePreview(p, r); - } else if (documentBubbleShown()) { - if (_docRect.intersects(r)) { - p.fillRect(_docRect, st::mediaviewFileBg); - if (_docIconRect.intersects(r)) { - const auto radial = _radial.animating(); - const auto radialOpacity = radial ? _radial.opacity() : 0.; - if (!_document || !_document->hasThumbnail()) { - p.fillRect(_docIconRect, _docIconColor); - if ((!_document || _documentMedia->loaded()) && (!radial || radialOpacity < 1) && _docIcon) { - _docIcon->paint(p, _docIconRect.x() + (_docIconRect.width() - _docIcon->width()), _docIconRect.y(), width()); - p.setPen(st::mediaviewFileExtFg); - p.setFont(st::mediaviewFileExtFont); - if (!_docExt.isEmpty()) { - p.drawText(_docIconRect.x() + (_docIconRect.width() - _docExtWidth) / 2, _docIconRect.y() + st::mediaviewFileExtTop + st::mediaviewFileExtFont->ascent, _docExt); - } - } - } else if (const auto thumbnail = _documentMedia->thumbnail()) { - int32 rf(cIntRetinaFactor()); - p.drawPixmap(_docIconRect.topLeft(), thumbnail->pix(_docThumbw), QRect(_docThumbx * rf, _docThumby * rf, st::mediaviewFileIconSize * rf, st::mediaviewFileIconSize * rf)); - } - - paintRadialLoading(p, radial, radialOpacity); - } - - if (!_docIconRect.contains(r)) { - name = true; - p.setPen(st::mediaviewFileNameFg); - p.setFont(st::mediaviewFileNameFont); - p.drawTextLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileNameTop, width(), _docName, _docNameWidth); - - p.setPen(st::mediaviewFileSizeFg); - p.setFont(st::mediaviewFont); - p.drawTextLeft(_docRect.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, _docRect.y() + st::mediaviewFilePadding + st::mediaviewFileSizeTop, width(), _docSize, _docSizeWidth); - } + paintRadialLoading(renderer); + } else { + if (_themePreviewShown) { + renderer->paintThemePreview(_themePreviewRect); + } else if (documentBubbleShown() && !_docRect.isEmpty()) { + renderer->paintDocumentBubble(_docRect, _docIconRect); } } + updateSaveMsgState(); + if (_saveMsgStarted && _saveMsgOpacity.current() > 0.) { + renderer->paintSaveMsg(_saveMsg); + } - float64 co = _fullScreenVideo ? 0. : _controlsOpacity.current(); - if (co > 0) { - // left nav bar - if (_leftNav.intersects(r) && _leftNavVisible) { - auto o = overLevel(OverLeftNav); - if (o > 0) { - p.setOpacity(o * co); - for (const auto &rect : region) { - const auto fill = _leftNav.intersected(rect); - if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); - } - } - if (_leftNavIcon.intersects(r)) { - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewLeft.paintInCenter(p, _leftNavIcon); - } - } - - // right nav bar - if (_rightNav.intersects(r) && _rightNavVisible) { - auto o = overLevel(OverRightNav); - if (o > 0) { - p.setOpacity(o * co); - for (const auto &rect : region) { - const auto fill = _rightNav.intersected(rect); - if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); - } - } - if (_rightNavIcon.intersects(r)) { - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewRight.paintInCenter(p, _rightNavIcon); - } - } - - // close button - if (_closeNav.intersects(r)) { - auto o = overLevel(OverClose); - if (o > 0) { - p.setOpacity(o * co); - for (const auto &rect : region) { - const auto fill = _closeNav.intersected(rect); - if (!fill.isEmpty()) p.fillRect(fill, st::mediaviewControlBg); - } - } - if (_closeNavIcon.intersects(r)) { - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewClose.paintInCenter(p, _closeNavIcon); - } - } - - // save button - if (_saveVisible && _saveNavIcon.intersects(r)) { - auto o = overLevel(OverSave); - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewSave.paintInCenter(p, _saveNavIcon); - } - - // rotate button - if (_rotateVisible && _rotateNavIcon.intersects(r)) { - auto o = overLevel(OverRotate); - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewRotate.paintInCenter(p, _rotateNavIcon); - } - - // more area - if (_moreNavIcon.intersects(r)) { - auto o = overLevel(OverMore); - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - st::mediaviewMore.paintInCenter(p, _moreNavIcon); - } - - p.setPen(st::mediaviewControlFg); - p.setFont(st::mediaviewThickFont); - - // header - if (_headerNav.intersects(r)) { - auto o = _headerHasLink ? overLevel(OverHeader) : 0; - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - p.drawText(_headerNav.left(), _headerNav.top() + st::mediaviewThickFont->ascent, _headerText); - - if (o > 0) { - p.setOpacity(o * co); - p.drawLine(_headerNav.left(), _headerNav.top() + st::mediaviewThickFont->ascent + 1, _headerNav.right(), _headerNav.top() + st::mediaviewThickFont->ascent + 1); - } - } - - p.setFont(st::mediaviewFont); - - // name - if (_nameNav.isValid() && _nameNav.intersects(r)) { - float64 o = _from ? overLevel(OverName) : 0.; - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - _fromNameLabel.drawElided(p, _nameNav.left(), _nameNav.top(), _nameNav.width()); - - if (o > 0) { - p.setOpacity(o * co); - p.drawLine(_nameNav.left(), _nameNav.top() + st::mediaviewFont->ascent + 1, _nameNav.right(), _nameNav.top() + st::mediaviewFont->ascent + 1); - } - } - - // date - if (_dateNav.intersects(r)) { - float64 o = overLevel(OverDate); - p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * co); - p.drawText(_dateNav.left(), _dateNav.top() + st::mediaviewFont->ascent, _dateText); - - if (o > 0) { - p.setOpacity(o * co); - p.drawLine(_dateNav.left(), _dateNav.top() + st::mediaviewFont->ascent + 1, _dateNav.right(), _dateNav.top() + st::mediaviewFont->ascent + 1); - } - } - - // caption + const auto opacity = _fullScreenVideo ? 0. : _controlsOpacity.current(); + if (opacity > 0) { + paintControls(renderer, opacity); + renderer->paintFooter(footerGeometry(), opacity); if (!_caption.isEmpty()) { - QRect outer(_captionRect.marginsAdded(st::mediaviewCaptionPadding)); - if (outer.intersects(r)) { - p.setOpacity(co); - p.setBrush(st::mediaviewCaptionBg); - p.setPen(Qt::NoPen); - p.drawRoundedRect(outer, st::mediaviewCaptionRadius, st::mediaviewCaptionRadius); - if (_captionRect.intersects(r)) { - p.setTextPalette(st::mediaviewTextPalette); - p.setPen(st::mediaviewCaptionFg); - _caption.drawElided(p, _captionRect.x(), _captionRect.y(), _captionRect.width(), _captionRect.height() / st::mediaviewCaptionStyle.font->height); - p.restoreTextPalette(); - } - } + renderer->paintCaption(captionGeometry(), opacity); } - - if (_groupThumbs && _groupThumbsRect.intersects(r)) { - p.setOpacity(co); - _groupThumbs->paint( - p, - _groupThumbsLeft, - _groupThumbsTop, - width()); - if (_groupThumbs->hidden()) { - _groupThumbs = nullptr; - _groupThumbsRect = QRect(); - } + if (_groupThumbs) { + renderer->paintGroupThumbs( + QRect( + _groupThumbsLeft, + _groupThumbsTop, + width() - 2 * _groupThumbsLeft, + _groupThumbs->height()), + opacity); } } checkGroupThumbsAnimation(); @@ -3380,63 +3309,8 @@ void OverlayWidget::checkGroupThumbsAnimation() { } } -void OverlayWidget::paintTransformedVideoFrame(Painter &p) { - Expects(_streamed != nullptr); - - const auto rect = contentRect(); - const auto image = videoFrameForDirectPaint(); - - PainterHighQualityEnabler hq(p); - - const auto rotation = contentRotation(); - if (UsePainterRotation(rotation)) { - if (rotation) { - p.save(); - p.rotate(rotation); - } - p.drawImage(RotatedRect(rect, rotation), image); - if (rotation) { - p.restore(); - } - } else { - p.drawImage(rect, transformVideoFrame(image)); - } - if (_streamed->instance.player().ready()) { - _streamed->instance.markFrameShown(); - } -} - -void OverlayWidget::paintTransformedStaticContent(Painter &p) { - const auto rect = contentRect(); - - PainterHighQualityEnabler hq(p); - if ((!_document - || (!_document->sticker() && !_document->isVideoMessage())) - && (_staticContent.isNull() || _staticContent.hasAlpha())) { - p.fillRect(rect, _transparentBrush); - } - if (_staticContent.isNull()) { - return; - } - const auto rotation = contentRotation(); - if (UsePainterRotation(rotation)) { - if (rotation) { - p.save(); - p.rotate(rotation); - } - p.drawPixmap(RotatedRect(rect, rotation), _staticContent); - if (rotation) { - p.restore(); - } - } else { - p.drawImage(rect, transformStaticContent(_staticContent)); - } -} - -void OverlayWidget::paintRadialLoading( - Painter &p, - bool radial, - float64 radialOpacity) { +void OverlayWidget::paintRadialLoading(not_null renderer) { + const auto radial = _radial.animating(); if (_streamed) { if (!_streamed->instance.waitingShown()) { return; @@ -3445,27 +3319,11 @@ void OverlayWidget::paintRadialLoading( return; } + const auto radialOpacity = radial ? _radial.opacity() : 0.; const auto inner = radialRect(); Assert(!inner.isEmpty()); -#ifdef USE_OPENGL_OVERLAY_WIDGET - { - if (_radialCache.size() != inner.size() * cIntRetinaFactor()) { - _radialCache = QImage( - inner.size() * cIntRetinaFactor(), - QImage::Format_ARGB32_Premultiplied); - _radialCache.setDevicePixelRatio(cRetinaFactor()); - } - _radialCache.fill(Qt::transparent); - - Painter q(&_radialCache); - const auto moved = inner.translated(-inner.topLeft()); - paintRadialLoadingContent(q, moved, radial, radialOpacity); - } - p.drawImage(inner.topLeft(), _radialCache); -#else // USE_OPENGL_OVERLAY_WIDGET - paintRadialLoadingContent(p, inner, radial, radialOpacity); -#endif // USE_OPENGL_OVERLAY_WIDGET + renderer->paintRadialLoading(inner, radial, radialOpacity); } void OverlayWidget::paintRadialLoadingContent( @@ -3527,19 +3385,22 @@ void OverlayWidget::paintRadialLoadingContent( } } -void OverlayWidget::paintThemePreview(Painter &p, QRect clip) { - auto fill = _themePreviewRect.intersected(clip); +void OverlayWidget::paintThemePreviewContent( + Painter &p, + QRect outer, + QRect clip) { + const auto fill = outer.intersected(clip); if (!fill.isEmpty()) { if (_themePreview) { p.drawImage( - myrtlrect(_themePreviewRect).topLeft(), + outer.topLeft(), _themePreview->preview); } else { p.fillRect(fill, st::themePreviewBg); p.setFont(st::themePreviewLoadingFont); p.setPen(st::themePreviewLoadingFg); p.drawText( - _themePreviewRect, + outer, (_themePreviewId ? tr::lng_theme_preview_generating(tr::now) : tr::lng_theme_preview_invalid(tr::now)), @@ -3547,111 +3408,384 @@ void OverlayWidget::paintThemePreview(Painter &p, QRect clip) { } } - auto fillOverlay = [&](QRect fill) { - auto clipped = fill.intersected(clip); + const auto fillOverlay = [&](QRect fill) { + const auto clipped = fill.intersected(clip); if (!clipped.isEmpty()) { p.setOpacity(st::themePreviewOverlayOpacity); p.fillRect(clipped, st::themePreviewBg); p.setOpacity(1.); } }; - auto titleRect = QRect(_themePreviewRect.x(), _themePreviewRect.y(), _themePreviewRect.width(), st::themePreviewMargin.top()); + auto titleRect = QRect( + outer.x(), + outer.y(), + outer.width(), + st::themePreviewMargin.top()); if (titleRect.x() < 0) { - titleRect = QRect(0, _themePreviewRect.y(), width(), st::themePreviewMargin.top()); + titleRect = QRect( + 0, + outer.y(), + width(), + st::themePreviewMargin.top()); } - if (auto fillTitleRect = (titleRect.y() < 0)) { + if (const auto fillTitleRect = (titleRect.y() < 0)) { titleRect.moveTop(0); fillOverlay(titleRect); } - titleRect = titleRect.marginsRemoved(QMargins(st::themePreviewMargin.left(), st::themePreviewTitleTop, st::themePreviewMargin.right(), titleRect.height() - st::themePreviewTitleTop - st::themePreviewTitleFont->height)); + titleRect = titleRect.marginsRemoved(QMargins( + st::themePreviewMargin.left(), + st::themePreviewTitleTop, + st::themePreviewMargin.right(), + (titleRect.height() + - st::themePreviewTitleTop + - st::themePreviewTitleFont->height))); if (titleRect.intersects(clip)) { p.setFont(st::themePreviewTitleFont); p.setPen(st::themePreviewTitleFg); const auto title = _themeCloudData.title.isEmpty() ? tr::lng_theme_preview_title(tr::now) : _themeCloudData.title; - const auto elided = st::themePreviewTitleFont->elided(title, titleRect.width()); + const auto elided = st::themePreviewTitleFont->elided( + title, + titleRect.width()); p.drawTextLeft(titleRect.x(), titleRect.y(), width(), elided); } - auto buttonsRect = QRect(_themePreviewRect.x(), _themePreviewRect.y() + _themePreviewRect.height() - st::themePreviewMargin.bottom(), _themePreviewRect.width(), st::themePreviewMargin.bottom()); - if (auto fillButtonsRect = (buttonsRect.y() + buttonsRect.height() > height())) { + auto buttonsRect = QRect( + outer.x(), + outer.y() + outer.height() - st::themePreviewMargin.bottom(), + outer.width(), + st::themePreviewMargin.bottom()); + if (const auto fillButtonsRect + = (buttonsRect.y() + buttonsRect.height() > height())) { buttonsRect.moveTop(height() - buttonsRect.height()); fillOverlay(buttonsRect); } if (_themeShare && _themeCloudData.usersCount > 0) { p.setFont(st::boxTextFont); p.setPen(st::windowSubTextFg); - const auto left = _themeShare->x() + _themeShare->width() - (st::themePreviewCancelButton.width / 2); - const auto baseline = _themeShare->y() + st::themePreviewCancelButton.padding.top() + +st::themePreviewCancelButton.textTop + st::themePreviewCancelButton.font->ascent; - p.drawText(left, baseline, tr::lng_theme_preview_users(tr::now, lt_count, _themeCloudData.usersCount)); + const auto left = outer.x() + + (_themeShare->x() - _themePreviewRect.x()) + + _themeShare->width() + - (st::themePreviewCancelButton.width / 2); + const auto baseline = outer.y() + + (_themeShare->y() - _themePreviewRect.y()) + + st::themePreviewCancelButton.padding.top() + + st::themePreviewCancelButton.textTop + + st::themePreviewCancelButton.font->ascent; + p.drawText( + left, + baseline, + tr::lng_theme_preview_users( + tr::now, + lt_count, + _themeCloudData.usersCount)); } } -void OverlayWidget::keyPressEvent(QKeyEvent *e) { - const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier); +void OverlayWidget::paintDocumentBubbleContent( + Painter &p, + QRect outer, + QRect icon, + QRect clip) const { + p.fillRect(outer, st::mediaviewFileBg); + if (icon.intersects(clip)) { + if (!_document || !_document->hasThumbnail()) { + p.fillRect(icon, _docIconColor); + const auto radial = _radial.animating(); + const auto radialOpacity = radial ? _radial.opacity() : 0.; + if ((!_document || _documentMedia->loaded()) && (!radial || radialOpacity < 1) && _docIcon) { + _docIcon->paint(p, icon.x() + (icon.width() - _docIcon->width()), icon.y(), width()); + p.setPen(st::mediaviewFileExtFg); + p.setFont(st::mediaviewFileExtFont); + if (!_docExt.isEmpty()) { + p.drawText(icon.x() + (icon.width() - _docExtWidth) / 2, icon.y() + st::mediaviewFileExtTop + st::mediaviewFileExtFont->ascent, _docExt); + } + } + } else if (const auto thumbnail = _documentMedia->thumbnail()) { + int32 rf(cIntRetinaFactor()); + p.drawPixmap(icon.topLeft(), thumbnail->pix(_docThumbw), QRect(_docThumbx * rf, _docThumby * rf, st::mediaviewFileIconSize * rf, st::mediaviewFileIconSize * rf)); + } + } + if (!icon.contains(clip)) { + p.setPen(st::mediaviewFileNameFg); + p.setFont(st::mediaviewFileNameFont); + p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileNameTop, width(), _docName, _docNameWidth); + + p.setPen(st::mediaviewFileSizeFg); + p.setFont(st::mediaviewFont); + p.drawTextLeft(outer.x() + 2 * st::mediaviewFilePadding + st::mediaviewFileIconSize, outer.y() + st::mediaviewFilePadding + st::mediaviewFileSizeTop, width(), _docSize, _docSizeWidth); + } +} + +void OverlayWidget::paintSaveMsgContent( + Painter &p, + QRect outer, + QRect clip) { + p.setOpacity(_saveMsgOpacity.current()); + Ui::FillRoundRect(p, outer, st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners); + st::mediaviewSaveMsgCheck.paint(p, outer.topLeft() + st::mediaviewSaveMsgCheckPos, width()); + + p.setPen(st::mediaviewSaveMsgFg); + p.setTextPalette(st::mediaviewTextPalette); + _saveMsgText.draw(p, outer.x() + st::mediaviewSaveMsgPadding.left(), outer.y() + st::mediaviewSaveMsgPadding.top(), outer.width() - st::mediaviewSaveMsgPadding.left() - st::mediaviewSaveMsgPadding.right()); + p.restoreTextPalette(); + p.setOpacity(1); +} + +void OverlayWidget::paintControls( + not_null renderer, + float64 opacity) { + struct Control { + OverState state = OverNone; + bool visible = false; + const QRect &outer; + const QRect &inner; + const style::icon &icon; + }; + const QRect kEmpty; + // When adding / removing controls please update RendererGL. + const Control controls[] = { + { + OverLeftNav, + _leftNavVisible, + _leftNav, + _leftNavIcon, + st::mediaviewLeft }, + { + OverRightNav, + _rightNavVisible, + _rightNav, + _rightNavIcon, + st::mediaviewRight }, + { + OverClose, + true, + _closeNav, + _closeNavIcon, + st::mediaviewClose }, + { + OverSave, + _saveVisible, + kEmpty, + _saveNavIcon, + st::mediaviewSave }, + { + OverRotate, + _rotateVisible, + kEmpty, + _rotateNavIcon, + st::mediaviewRotate }, + { + OverMore, + true, + kEmpty, + _moreNavIcon, + st::mediaviewMore }, + }; + + renderer->paintControlsStart(); + for (const auto &control : controls) { + if (!control.visible) { + continue; + } + const auto bg = overLevel(control.state); + const auto icon = bg * st::mediaviewIconOverOpacity + + (1 - bg) * st::mediaviewIconOpacity; + renderer->paintControl( + control.state, + control.outer, + bg * opacity, + control.inner, + icon * opacity, + control.icon); + } +} + +void OverlayWidget::paintFooterContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity) { + p.setPen(st::mediaviewControlFg); + p.setFont(st::mediaviewThickFont); + + // header + const auto shift = outer.topLeft() - _headerNav.topLeft(); + const auto header = _headerNav.translated(shift); + const auto name = _nameNav.translated(shift); + const auto date = _dateNav.translated(shift); + if (header.intersects(clip)) { + auto o = _headerHasLink ? overLevel(OverHeader) : 0; + p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity); + p.drawText(header.left(), header.top() + st::mediaviewThickFont->ascent, _headerText); + + if (o > 0) { + p.setOpacity(o * opacity); + p.drawLine(header.left(), header.top() + st::mediaviewThickFont->ascent + 1, header.right(), header.top() + st::mediaviewThickFont->ascent + 1); + } + } + + p.setFont(st::mediaviewFont); + + // name + if (_nameNav.isValid() && name.intersects(clip)) { + float64 o = _from ? overLevel(OverName) : 0.; + p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity); + _fromNameLabel.drawElided(p, name.left(), name.top(), name.width()); + + if (o > 0) { + p.setOpacity(o * opacity); + p.drawLine(name.left(), name.top() + st::mediaviewFont->ascent + 1, name.right(), name.top() + st::mediaviewFont->ascent + 1); + } + } + + // date + if (date.intersects(clip)) { + float64 o = overLevel(OverDate); + p.setOpacity((o * st::mediaviewIconOverOpacity + (1 - o) * st::mediaviewIconOpacity) * opacity); + p.drawText(date.left(), date.top() + st::mediaviewFont->ascent, _dateText); + + if (o > 0) { + p.setOpacity(o * opacity); + p.drawLine(date.left(), date.top() + st::mediaviewFont->ascent + 1, date.right(), date.top() + st::mediaviewFont->ascent + 1); + } + } +} + +QRect OverlayWidget::footerGeometry() const { + return _headerNav.united(_nameNav).united(_dateNav); +} + +void OverlayWidget::paintCaptionContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity) { + const auto inner = outer.marginsRemoved(st::mediaviewCaptionPadding); + p.setOpacity(opacity); + p.setBrush(st::mediaviewCaptionBg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(outer, st::mediaviewCaptionRadius, st::mediaviewCaptionRadius); + if (inner.intersects(clip)) { + p.setTextPalette(st::mediaviewTextPalette); + p.setPen(st::mediaviewCaptionFg); + _caption.drawElided(p, inner.x(), inner.y(), inner.width(), inner.height() / st::mediaviewCaptionStyle.font->height); + p.restoreTextPalette(); + } +} + +QRect OverlayWidget::captionGeometry() const { + return _captionRect.marginsAdded(st::mediaviewCaptionPadding); +} + +void OverlayWidget::paintGroupThumbsContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity) { + p.setOpacity(opacity); + _groupThumbs->paint(p, outer.x(), outer.y(), width()); + if (_groupThumbs->hidden()) { + _groupThumbs = nullptr; + _groupThumbsRect = QRect(); + } +} + +void OverlayWidget::updateSaveMsgState() { + if (!_saveMsgStarted) { + return; + } + float64 dt = float64(crl::now()) - _saveMsgStarted; + float64 hidingDt = dt - st::mediaviewSaveMsgShowing - st::mediaviewSaveMsgShown; + if (dt >= st::mediaviewSaveMsgShowing + + st::mediaviewSaveMsgShown + + st::mediaviewSaveMsgHiding) { + _saveMsgStarted = 0; + return; + } + if (hidingDt >= 0 && _saveMsgOpacity.to() > 0.5) { + _saveMsgOpacity.start(0); + } + float64 progress = (hidingDt >= 0) ? (hidingDt / st::mediaviewSaveMsgHiding) : (dt / st::mediaviewSaveMsgShowing); + _saveMsgOpacity.update(qMin(progress, 1.), anim::linear); + if (!_blurred) { + const auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) + ? int(AnimationTimerDelta) + : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt); + _saveMsgUpdater.callOnce(nextFrame); + } +} + +void OverlayWidget::handleKeyPress(not_null e) { + const auto key = e->key(); + const auto modifiers = e->modifiers(); + const auto ctrl = modifiers.testFlag(Qt::ControlModifier); if (_streamed) { // Ctrl + F for full screen toggle is in eventFilter(). - const auto toggleFull = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) - && (e->modifiers().testFlag(Qt::AltModifier) || ctrl); + const auto toggleFull = (modifiers.testFlag(Qt::AltModifier) || ctrl) + && (key == Qt::Key_Enter || key == Qt::Key_Return); if (toggleFull) { playbackToggleFullScreen(); return; - } else if (e->key() == Qt::Key_Space) { + } else if (key == Qt::Key_Space) { playbackPauseResume(); return; } else if (_fullScreenVideo) { - if (e->key() == Qt::Key_Escape) { + if (key == Qt::Key_Escape) { playbackToggleFullScreen(); } return; } } - if (!_menu && e->key() == Qt::Key_Escape) { + if (!_menu && key == Qt::Key_Escape) { if (_document && _document->loading() && !_streamed) { - onDocClick(); + handleDocumentClick(); } else { close(); } } else if (e == QKeySequence::Save || e == QKeySequence::SaveAs) { - onSaveAs(); - } else if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && ctrl)) { - onCopy(); - } else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Space) { + saveAs(); + } else if (key == Qt::Key_Copy || (key == Qt::Key_C && ctrl)) { + copyMedia(); + } else if (key == Qt::Key_Enter + || key == Qt::Key_Return + || key == Qt::Key_Space) { if (_streamed) { playbackPauseResume(); - } else if (_document && !_document->loading() && (documentBubbleShown() || !_documentMedia->loaded())) { - onDocClick(); + } else if (_document + && !_document->loading() + && (documentBubbleShown() || !_documentMedia->loaded())) { + handleDocumentClick(); } - } else if (e->key() == Qt::Key_Left) { + } else if (key == Qt::Key_Left) { if (_controlsHideTimer.isActive()) { activateControls(); } moveToNext(-1); - } else if (e->key() == Qt::Key_Right) { + } else if (key == Qt::Key_Right) { if (_controlsHideTimer.isActive()) { activateControls(); } moveToNext(1); } else if (ctrl) { - if (e->key() == Qt::Key_Plus || e->key() == Qt::Key_Equal || e->key() == Qt::Key_Asterisk || e->key() == ']') { + if (key == Qt::Key_Plus + || key == Qt::Key_Equal + || key == Qt::Key_Asterisk + || key == ']') { zoomIn(); - } else if (e->key() == Qt::Key_Minus || e->key() == Qt::Key_Underscore) { + } else if (key == Qt::Key_Minus || key == Qt::Key_Underscore) { zoomOut(); - } else if (e->key() == Qt::Key_0) { + } else if (key == Qt::Key_0) { zoomReset(); - } else if (e->key() == Qt::Key_I) { + } else if (key == Qt::Key_I) { update(); } } } -void OverlayWidget::wheelEvent(QWheelEvent *e) { -#ifdef OS_MAC_OLD - constexpr auto step = 120; -#else // OS_MAC_OLD - constexpr auto step = static_cast(QWheelEvent::DefaultDeltasPerStep); -#endif // OS_MAC_OLD +void OverlayWidget::handleWheelEvent(not_null e) { + constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep); _verticalWheelDelta += e->angleDelta().y(); while (qAbs(_verticalWheelDelta) >= step) { @@ -3659,23 +3793,15 @@ void OverlayWidget::wheelEvent(QWheelEvent *e) { _verticalWheelDelta += step; if (e->modifiers().testFlag(Qt::ControlModifier)) { zoomOut(); - } else { -#ifndef OS_MAC_OLD - if (e->source() == Qt::MouseEventNotSynthesized) { - moveToNext(1); - } -#endif // OS_MAC_OLD + } else if (e->source() == Qt::MouseEventNotSynthesized) { + moveToNext(1); } } else { _verticalWheelDelta -= step; if (e->modifiers().testFlag(Qt::ControlModifier)) { zoomIn(); - } else { -#ifndef OS_MAC_OLD - if (e->source() == Qt::MouseEventNotSynthesized) { - moveToNext(-1); - } -#endif // OS_MAC_OLD + } else if (e->source() == Qt::MouseEventNotSynthesized) { + moveToNext(-1); } } } @@ -3691,6 +3817,9 @@ void OverlayWidget::setZoomLevel(int newZoom, bool force) { const auto contentSize = videoShown() ? style::ConvertScale(videoSize()) : QSize(_width, _height); + _oldGeometry = contentGeometry(); + _geometryAnimation.stop(); + _w = contentSize.width(); _h = contentSize.height(); if (z >= 0) { @@ -3714,6 +3843,14 @@ void OverlayWidget::setZoomLevel(int newZoom, bool force) { _y = qRound(ny / (-z + 1) + height() / 2.); } snapXY(); + if (_opengl) { + _geometryAnimation.start( + [=] { update(); }, + 0., + 1., + st::widgetFadeDuration/*, + anim::easeOutCirc*/); + } update(); } @@ -3832,7 +3969,7 @@ void OverlayWidget::setSession(not_null session) { clearSession(); _session = session; - setWindowIcon(Window::CreateIcon(session)); + _widget->setWindowIcon(Window::CreateIcon(session)); session->downloaderTaskFinished( ) | rpl::start_with_next([=] { @@ -3905,7 +4042,7 @@ void OverlayWidget::preloadData(int delta) { if (!_index) { return; } - auto from = *_index + (delta ? delta : -1); + auto from = *_index + (delta ? -delta : -1); auto till = *_index + (delta ? delta * kPreloadCount : 1); if (from > till) std::swap(from, till); @@ -3931,21 +4068,23 @@ void OverlayWidget::preloadData(int delta) { _preloadDocuments = std::move(documents); } -void OverlayWidget::mousePressEvent(QMouseEvent *e) { - updateOver(e->pos()); +void OverlayWidget::handleMousePress( + QPoint position, + Qt::MouseButton button) { + updateOver(position); if (_menu || !_receiveMouse) { return; } ClickHandler::pressed(); - if (e->button() == Qt::LeftButton) { + if (button == Qt::LeftButton) { _down = OverNone; if (!ClickHandler::getPressed()) { if (_over == OverLeftNav && moveToNext(-1)) { - _lastAction = e->pos(); + _lastAction = position; } else if (_over == OverRightNav && moveToNext(1)) { - _lastAction = e->pos(); + _lastAction = position; } else if (_over == OverName || _over == OverDate || _over == OverHeader @@ -3956,31 +4095,32 @@ void OverlayWidget::mousePressEvent(QMouseEvent *e) { || _over == OverClose || _over == OverVideo) { _down = _over; - } else if (!_saveMsg.contains(e->pos()) || !_saveMsgStarted) { + } else if (!_saveMsg.contains(position) || !_saveMsgStarted) { _pressed = true; _dragging = 0; updateCursor(); - _mStart = e->pos(); + _mStart = position; _xStart = _x; _yStart = _y; } } - } else if (e->button() == Qt::MiddleButton) { + } else if (button == Qt::MiddleButton) { zoomReset(); } activateControls(); } -void OverlayWidget::mouseDoubleClickEvent(QMouseEvent *e) { - updateOver(e->pos()); +bool OverlayWidget::handleDoubleClick( + QPoint position, + Qt::MouseButton button) { + updateOver(position); - if (_over == OverVideo && _streamed) { - playbackToggleFullScreen(); - playbackPauseResume(); - } else { - e->ignore(); - return OverlayParent::mouseDoubleClickEvent(e); + if (_over != OverVideo || !_streamed || button != Qt::LeftButton) { + return false; } + playbackToggleFullScreen(); + playbackPauseResume(); + return true; } void OverlayWidget::snapXY() { @@ -3996,13 +4136,17 @@ void OverlayWidget::snapXY() { if (_y > ymax) _y = ymax; } -void OverlayWidget::mouseMoveEvent(QMouseEvent *e) { - updateOver(e->pos()); - if (_lastAction.x() >= 0 && (e->pos() - _lastAction).manhattanLength() >= st::mediaviewDeltaFromLastAction) { +void OverlayWidget::handleMouseMove(QPoint position) { + updateOver(position); + if (_lastAction.x() >= 0 + && ((position - _lastAction).manhattanLength() + >= st::mediaviewDeltaFromLastAction)) { _lastAction = QPoint(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction); } if (_pressed) { - if (!_dragging && (e->pos() - _mStart).manhattanLength() >= QApplication::startDragDistance()) { + if (!_dragging + && ((position - _mStart).manhattanLength() + >= QApplication::startDragDistance())) { _dragging = QRect(_x, _y, _w, _h).contains(_mStart) ? 1 : -1; if (_dragging > 0) { if (_w > width() || _h > height()) { @@ -4013,8 +4157,8 @@ void OverlayWidget::mouseMoveEvent(QMouseEvent *e) { } } if (_dragging > 0) { - _x = _xStart + (e->pos() - _mStart).x(); - _y = _yStart + (e->pos() - _mStart).y(); + _x = _xStart + (position - _mStart).x(); + _y = _yStart + (position - _mStart).y(); snapXY(); update(); } @@ -4130,7 +4274,7 @@ void OverlayWidget::updateOver(QPoint pos) { updateOverState(OverMore); } else if (_closeNav.contains(pos)) { updateOverState(OverClose); - } else if (documentContentShown() && contentRect().contains(pos)) { + } else if (documentContentShown() && finalContentRect().contains(pos)) { if ((_document->isVideoFile() || _document->isVideoMessage()) && _streamed) { updateOverState(OverVideo); } else if (!_streamed && !_documentMedia->loaded()) { @@ -4143,8 +4287,10 @@ void OverlayWidget::updateOver(QPoint pos) { } } -void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { - updateOver(e->pos()); +void OverlayWidget::handleMouseRelease( + QPoint position, + Qt::MouseButton button) { + updateOver(position); if (const auto activated = ClickHandler::unpressed()) { if (activated->dragText() == qstr("internal:show_saved_message")) { @@ -4156,7 +4302,7 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { if (_session) { Core::App().domain().activate(&_session->account()); } - ActivateClickHandler(this, activated, e->button()); + ActivateClickHandler(_widget, activated, button); return; } @@ -4166,17 +4312,17 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { Ui::showPeerProfile(_from); } } else if (_over == OverDate && _down == OverDate) { - onToMessage(); + toMessage(); } else if (_over == OverHeader && _down == OverHeader) { - onOverview(); + showMediaOverview(); } else if (_over == OverSave && _down == OverSave) { - onDownload(); + downloadMedia(); } else if (_over == OverRotate && _down == OverRotate) { playbackControlsRotate(); } else if (_over == OverIcon && _down == OverIcon) { - onDocClick(); + handleDocumentClick(); } else if (_over == OverMore && _down == OverMore) { - InvokeQueued(this, [=] { onDropdown(); }); + InvokeQueued(_widget, [=] { showDropdown(); }); } else if (_over == OverClose && _down == OverClose) { close(); } else if (_over == OverVideo && _down == OverVideo) { @@ -4186,22 +4332,23 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { } else if (_pressed) { if (_dragging) { if (_dragging > 0) { - _x = _xStart + (e->pos() - _mStart).x(); - _y = _yStart + (e->pos() - _mStart).y(); + _x = _xStart + (position - _mStart).x(); + _y = _yStart + (position - _mStart).y(); snapXY(); update(); } _dragging = 0; setCursor(style::cur_default); - } else if ((e->pos() - _lastAction).manhattanLength() >= st::mediaviewDeltaFromLastAction) { + } else if ((position - _lastAction).manhattanLength() + >= st::mediaviewDeltaFromLastAction) { if (_themePreviewShown) { - if (!_themePreviewRect.contains(e->pos())) { + if (!_themePreviewRect.contains(position)) { close(); } } else if (!_document || documentContentShown() || !documentBubbleShown() - || !_docRect.contains(e->pos())) { + || !_docRect.contains(position)) { close(); } } @@ -4213,29 +4360,41 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) { } } -void OverlayWidget::contextMenuEvent(QContextMenuEvent *e) { - if (e->reason() != QContextMenuEvent::Mouse || QRect(_x, _y, _w, _h).contains(e->pos())) { - _menu = base::make_unique_q( - this, - st::mediaviewPopupMenu); - fillContextMenuActions([&] (const QString &text, Fn handler) { - _menu->addAction(text, std::move(handler)); - }); - _menu->setDestroyedCallback(crl::guard(this, [=] { - activateControls(); - _receiveMouse = false; - InvokeQueued(this, [=] { receiveMouse(); }); - })); - _menu->popup(e->globalPos()); - e->accept(); - activateControls(); +bool OverlayWidget::handleContextMenu(std::optional position) { + if (position && !QRect(_x, _y, _w, _h).contains(*position)) { + return false; } + _menu = base::make_unique_q( + _widget, + st::mediaviewPopupMenu); + fillContextMenuActions([&] (const QString &text, Fn handler) { + _menu->addAction(text, std::move(handler)); + }); + _menu->setDestroyedCallback(crl::guard(_widget, [=] { + activateControls(); + _receiveMouse = false; + InvokeQueued(_widget, [=] { receiveMouse(); }); + })); + _menu->popup(QCursor::pos()); + activateControls(); + return true; } -void OverlayWidget::touchEvent(QTouchEvent *e) { +bool OverlayWidget::handleTouchEvent(not_null e) { + if (e->device()->type() != QTouchDevice::TouchScreen) { + return false; + } else if (e->type() == QEvent::TouchBegin + && !e->touchPoints().isEmpty() + && _widget->childAt( + _widget->mapFromGlobal( + e->touchPoints().cbegin()->screenPos().toPoint()))) { + return false; + } switch (e->type()) { case QEvent::TouchBegin: { - if (_touchPress || e->touchPoints().isEmpty()) return; + if (_touchPress || e->touchPoints().isEmpty()) { + break; + } _touchTimer.callOnce(QApplication::startDragTime()); _touchPress = true; _touchMove = _touchRightButton = false; @@ -4243,32 +4402,32 @@ void OverlayWidget::touchEvent(QTouchEvent *e) { } break; case QEvent::TouchUpdate: { - if (!_touchPress || e->touchPoints().isEmpty()) return; + if (!_touchPress || e->touchPoints().isEmpty()) { + break; + } if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { _touchMove = true; } } break; case QEvent::TouchEnd: { - if (!_touchPress) return; - auto weak = Ui::MakeWeak(this); + if (!_touchPress) { + break; + } + auto weak = Ui::MakeWeak(_widget); if (!_touchMove) { - Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton); - auto mapped = mapFromGlobal(_touchStart); - - QMouseEvent pressEvent(QEvent::MouseButtonPress, mapped, mapped, _touchStart, btn, Qt::MouseButtons(btn), Qt::KeyboardModifiers()); - pressEvent.accept(); - if (weak) mousePressEvent(&pressEvent); - - QMouseEvent releaseEvent(QEvent::MouseButtonRelease, mapped, mapped, _touchStart, btn, Qt::MouseButtons(btn), Qt::KeyboardModifiers()); - if (weak) mouseReleaseEvent(&releaseEvent); + const auto button = _touchRightButton + ? Qt::RightButton + : Qt::LeftButton; + const auto position = _widget->mapFromGlobal(_touchStart); + if (weak) handleMousePress(position, button); + if (weak) handleMouseRelease(position, button); if (weak && _touchRightButton) { - QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart); - contextMenuEvent(&contextEvent); + handleContextMenu(position); } } else if (_touchMove) { - if ((!_leftNavVisible || !_leftNav.contains(mapFromGlobal(_touchStart))) && (!_rightNavVisible || !_rightNav.contains(mapFromGlobal(_touchStart)))) { + if ((!_leftNavVisible || !_leftNav.contains(_widget->mapFromGlobal(_touchStart))) && (!_rightNavVisible || !_rightNav.contains(_widget->mapFromGlobal(_touchStart)))) { QPoint d = (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart); if (d.x() * d.x() > d.y() * d.y() && (d.x() > st::mediaviewSwipeDistance || d.x() < -st::mediaviewSwipeDistance)) { moveToNext(d.x() > 0 ? -1 : 1); @@ -4286,54 +4445,56 @@ void OverlayWidget::touchEvent(QTouchEvent *e) { _touchTimer.cancel(); } break; } + return true; } -bool OverlayWidget::eventHook(QEvent *e) { - if (e->type() == QEvent::UpdateRequest) { - _wasRepainted = true; - } else if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { - QTouchEvent *ev = static_cast(e); - if (ev->device()->type() == QTouchDevice::TouchScreen) { - if (ev->type() != QEvent::TouchBegin || ev->touchPoints().isEmpty() || !childAt(mapFromGlobal(ev->touchPoints().cbegin()->screenPos().toPoint()))) { - touchEvent(ev); - return true; - } - } - } else if (e->type() == QEvent::Wheel) { - QWheelEvent *ev = static_cast(e); - if (ev->phase() == Qt::ScrollBegin) { - _accumScroll = ev->angleDelta(); - } else { - _accumScroll += ev->angleDelta(); - if (ev->phase() == Qt::ScrollEnd) { - if (ev->angleDelta().x() != 0) { - if (_accumScroll.x() * _accumScroll.x() > _accumScroll.y() * _accumScroll.y() && _accumScroll.x() != 0) { - moveToNext(_accumScroll.x() > 0 ? -1 : 1); - } - _accumScroll = QPoint(); - } - } - } +void OverlayWidget::toggleApplicationEventFilter(bool install) { + if (!install) { + _applicationEventFilter = nullptr; + return; + } else if (_applicationEventFilter) { + return; } - return OverlayParent::eventHook(e); + class Filter final : public QObject { + public: + explicit Filter(not_null owner) : _owner(owner) { + } + + private: + bool eventFilter(QObject *obj, QEvent *e) override { + return obj && e && _owner->filterApplicationEvent(obj, e); + } + + const not_null _owner; + + }; + + _applicationEventFilter = std::make_unique(this); + qApp->installEventFilter(_applicationEventFilter.get()); } -bool OverlayWidget::eventFilter(QObject *obj, QEvent *e) { - auto type = e->type(); +bool OverlayWidget::filterApplicationEvent( + not_null object, + not_null e) { + const auto type = e->type(); if (type == QEvent::ShortcutOverride) { - const auto keyEvent = static_cast(e); + const auto keyEvent = static_cast(e.get()); const auto ctrl = keyEvent->modifiers().testFlag(Qt::ControlModifier); if (keyEvent->key() == Qt::Key_F && ctrl && _streamed) { playbackToggleFullScreen(); } return true; - } - if ((type == QEvent::MouseMove || type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease) && obj->isWidgetType()) { - if (isAncestorOf(static_cast(obj))) { - const auto mouseEvent = static_cast(e); - const auto mousePosition = mapFromGlobal(mouseEvent->globalPos()); + } else if (type == QEvent::MouseMove + || type == QEvent::MouseButtonPress + || type == QEvent::MouseButtonRelease) { + if (object->isWidgetType() + && _widget->isAncestorOf(static_cast(object.get()))) { + const auto mouseEvent = static_cast(e.get()); + const auto mousePosition = _widget->mapFromGlobal( + mouseEvent->globalPos()); const auto delta = (mousePosition - _lastMouseMovePos); - auto activate = delta.manhattanLength() >= st::mediaviewDeltaFromLastAction; + auto activate = delta.manhattanLength() + >= st::mediaviewDeltaFromLastAction; if (activate) { _lastMouseMovePos = mousePosition; } @@ -4349,29 +4510,60 @@ bool OverlayWidget::eventFilter(QObject *obj, QEvent *e) { } } } - return OverlayParent::eventFilter(obj, e); + return false; } void OverlayWidget::applyHideWindowWorkaround() { -#ifdef USE_OPENGL_OVERLAY_WIDGET - // QOpenGLWidget can't properly destroy a child widget if - // it is hidden exactly after that, so it must be repainted - // before it is hidden without the child widget. - if (!isHidden()) { - _dropdown->hideFast(); - hideChildren(); - _wasRepainted = false; - repaint(); - if (!_wasRepainted) { - // Qt has some optimization to prevent too frequent repaints. - // If the previous repaint was less than 1/60 second it silently - // converts repaint() call to an update() call. But we have to - // repaint right now, before hide(), with _streamingControls destroyed. - auto event = QEvent(QEvent::UpdateRequest); - QApplication::sendEvent(this, &event); + // QOpenGLWidget can't properly destroy a child widget if it is hidden + // exactly after that, the child is cached in the backing store. + // So on next paint we force full backing store repaint. + if (_opengl && !isHidden() && !_hideWorkaround) { + _hideWorkaround = std::make_unique(_widget); + _hideWorkaround->setGeometry(_widget->rect()); + _hideWorkaround->show(); + _hideWorkaround->paintRequest( + ) | rpl::start_with_next([=] { + QPainter(_hideWorkaround.get()).fillRect(_hideWorkaround->rect(), QColor(0, 1, 0, 1)); + crl::on_main(_hideWorkaround.get(), [=] { + _hideWorkaround.reset(); + }); + }, _hideWorkaround->lifetime()); + _hideWorkaround->update(); + + if (Platform::IsWindows()) { + Ui::Platform::UpdateOverlayed(_widget); } } -#endif // USE_OPENGL_OVERLAY_WIDGET +} + +Window::SessionController *OverlayWidget::findWindow() const { + if (!_session) { + return nullptr; + } + + const auto window = _window.get(); + if (window) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == _session) { + return controller; + } + } + } + + const auto &active = _session->windows(); + if (!active.empty()) { + return active.front(); + } else if (window) { + Window::SessionController *controllerPtr = nullptr; + window->invokeForSessionController( + &_session->account(), + [&](not_null newController) { + controllerPtr = newController; + }); + return controllerPtr; + } + + return nullptr; } // #TODO unite and check @@ -4394,38 +4586,29 @@ void OverlayWidget::clearBeforeHide() { _controlsOpacity = anim::value(1, 1); _groupThumbs = nullptr; _groupThumbsRect = QRect(); + for (const auto child : _widget->children()) { + if (child->isWidgetType()) { + static_cast(child)->hide(); + } + } } void OverlayWidget::clearAfterHide() { clearStreaming(); destroyThemePreview(); _radial.stop(); - _staticContent = QPixmap(); + _staticContent = QImage(); _themePreview = nullptr; _themeApply.destroyDelayed(); _themeCancel.destroyDelayed(); _themeShare.destroyDelayed(); } -void OverlayWidget::setVisibleHook(bool visible) { - if (!visible) { - applyHideWindowWorkaround(); - clearBeforeHide(); - } - OverlayParent::setVisibleHook(visible); - if (visible) { - QCoreApplication::instance()->installEventFilter(this); - } else { - QCoreApplication::instance()->removeEventFilter(this); - clearAfterHide(); - } -} - void OverlayWidget::receiveMouse() { _receiveMouse = true; } -void OverlayWidget::onDropdown() { +void OverlayWidget::showDropdown() { _dropdown->clearActions(); fillContextMenuActions([&] (const QString &text, Fn handler) { _dropdown->addAction(text, std::move(handler)); @@ -4435,7 +4618,7 @@ void OverlayWidget::onDropdown() { _dropdown->setFocus(); } -void OverlayWidget::onTouchTimer() { +void OverlayWidget::handleTouchTimer() { _touchRightButton = true; } @@ -4518,7 +4701,7 @@ void OverlayWidget::updateHeader() { hwidth = width() / 3; _headerText = st::mediaviewThickFont->elided(_headerText, hwidth, Qt::ElideMiddle); } - _headerNav = myrtlrect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height); + _headerNav = QRect(st::mediaviewTextLeft, height() - st::mediaviewHeaderTop, hwidth, st::mediaviewThickFont->height); } float64 OverlayWidget::overLevel(OverState control) const { diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index dc9234e0a..3d567edf5 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "ui/rp_widget.h" +#include "ui/gl/gl_surface.h" #include "ui/widgets/dropdown_menu.h" #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_cloud_themes.h" // Data::CloudTheme. #include "media/view/media_view_playback_controls.h" +#include "media/view/media_view_open_common.h" namespace Data { class PhotoMedia; @@ -27,6 +29,10 @@ namespace Ui { class PopupMenu; class LinkButton; class RoundButton; +namespace GL { +struct ChosenRenderer; +struct Capabilities; +} // namespace GL } // namespace Ui namespace Window { @@ -42,38 +48,22 @@ struct TrackState; namespace Streaming { struct Information; struct Update; +struct FrameWithInfo; enum class Error; } // namespace Streaming } // namespace Media -namespace Media { -namespace View { +namespace Media::View { class GroupThumbs; class Pip; -#if defined Q_OS_MAC && !defined OS_MAC_OLD -#define USE_OPENGL_OVERLAY_WIDGET -#endif // Q_OS_MAC && !OS_MAC_OLD - -struct OverlayParentTraits : Ui::RpWidgetDefaultTraits { - static constexpr bool kSetZeroGeometry = false; -}; - -#ifdef USE_OPENGL_OVERLAY_WIDGET -using OverlayParent = Ui::RpWidgetWrap; -#else // USE_OPENGL_OVERLAY_WIDGET -using OverlayParent = Ui::RpWidgetWrap; -#endif // USE_OPENGL_OVERLAY_WIDGET - class OverlayWidget final - : public OverlayParent - , public ClickHandlerHost + : public ClickHandlerHost , private PlaybackControls::Delegate { - Q_OBJECT - public: OverlayWidget(); + ~OverlayWidget(); enum class TouchBarItemType { Photo, @@ -81,26 +71,26 @@ public: None, }; - void showPhoto(not_null photo, HistoryItem *context); - void showPhoto(not_null photo, not_null context); - void showDocument( - not_null document, - HistoryItem *context); - void showTheme( - not_null document, - const Data::CloudTheme &cloud); + [[nodiscard]] bool isHidden() const; + [[nodiscard]] not_null widget() const; + void hide(); + void setCursor(style::cursor cursor); + void setFocus(); + void activate(); - void leaveToChildEvent(QEvent *e, QWidget *child) override { // e -- from enterEvent() of child TWidget - updateOverState(OverNone); - } - void enterFromChildEvent(QEvent *e, QWidget *child) override { // e -- from leaveEvent() of child TWidget - updateOver(mapFromGlobal(QCursor::pos())); - } + void show(OpenRequest request); - void close(); + //void leaveToChildEvent(QEvent *e, QWidget *child) override { + // // e -- from enterEvent() of child TWidget + // updateOverState(OverNone); + //} + //void enterFromChildEvent(QEvent *e, QWidget *child) override { + // // e -- from leaveEvent() of child TWidget + // updateOver(mapFromGlobal(QCursor::pos())); + //} void activateControls(); - void onDocClick(); + void close(); PeerData *ui_getPeerForMouseAction(); @@ -108,40 +98,20 @@ public: void clearSession(); - ~OverlayWidget(); - // ClickHandlerHost interface void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override; void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; -private Q_SLOTS: - void onHideControls(bool force = false); - - void onScreenResized(int screen); - - void onToMessage(); - void onSaveAs(); - void onDownload(); - void onSaveCancel(); - void onShowInFolder(); - void onForward(); - void onDelete(); - void onOverview(); - void onCopy(); - void receiveMouse(); - void onPhotoAttachedStickers(); - void onDocumentAttachedStickers(); - - void onDropdown(); - - void onTouchTimer(); - - void updateImage(); + rpl::lifetime &lifetime(); private: struct Streamed; struct PipWrap; + class Renderer; + class RendererSW; + class RendererGL; + // If changing, see paintControls()! enum OverState { OverNone, OverLeftNav, @@ -168,25 +138,34 @@ private: QuickSave, SaveAs, }; + struct ContentGeometry { + QRectF rect; + qreal rotation = 0.; + }; - void paintEvent(QPaintEvent *e) override; - void moveEvent(QMoveEvent *e) override; - void resizeEvent(QResizeEvent *e) override; + [[nodiscard]] not_null window() const; + [[nodiscard]] int width() const; + [[nodiscard]] int height() const; + void update(); + void update(const QRegion ®ion); - void keyPressEvent(QKeyEvent *e) override; - void wheelEvent(QWheelEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseDoubleClickEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - void touchEvent(QTouchEvent *e); + [[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer( + Ui::GL::Capabilities capabilities); + void paint(not_null renderer); - bool eventHook(QEvent *e) override; - bool eventFilter(QObject *obj, QEvent *e) override; - - void setVisibleHook(bool visible) override; + void handleMousePress(QPoint position, Qt::MouseButton button); + void handleMouseRelease(QPoint position, Qt::MouseButton button); + void handleMouseMove(QPoint position); + bool handleContextMenu(std::optional position); + bool handleDoubleClick(QPoint position, Qt::MouseButton button); + bool handleTouchEvent(not_null e); + void handleWheelEvent(not_null e); + void handleKeyPress(not_null e); + void toggleApplicationEventFilter(bool install); + bool filterApplicationEvent( + not_null object, + not_null e); void setSession(not_null session); void playbackControlsPlay() override; @@ -210,6 +189,25 @@ private: void playbackPauseMusic(); void switchToPip(); + void hideControls(bool force = false); + void subscribeToScreenGeometry(); + + void toMessage(); + void saveAs(); + void downloadMedia(); + void saveCancel(); + void showInFolder(); + void forwardMedia(); + void deleteMedia(); + void showMediaOverview(); + void copyMedia(); + void receiveMouse(); + void showAttachedStickers(); + void showDropdown(); + void handleTouchTimer(); + void handleDocumentClick(); + void updateImage(); + void clearBeforeHide(); void clearAfterHide(); @@ -222,7 +220,6 @@ private: bool moveToNext(int delta); void preloadData(int delta); - void handleVisibleChanged(bool visible); void handleScreenChanged(QScreen *screen); bool contentCanBeSaved() const; @@ -241,7 +238,6 @@ private: void refreshLang(); void showSaveMsgFile(); - void updateMixerVideoVolume() const; struct SharedMedia; using SharedMediaType = SharedMediaWithLastSlice::Type; @@ -286,11 +282,6 @@ private: void resizeCenteredControls(); void resizeContentByScreenSize(); - void showDocument( - not_null document, - HistoryItem *context, - const Data::CloudTheme &cloud, - bool continueStreaming); void displayPhoto(not_null photo, HistoryItem *item); void displayDocument( DocumentData *document, @@ -325,8 +316,10 @@ private: void documentUpdated(DocumentData *doc); void changingMsgId(not_null row, MsgId oldId); - [[nodiscard]] int contentRotation() const; - [[nodiscard]] QRect contentRect() const; + [[nodiscard]] int finalContentRotation() const; + [[nodiscard]] QRect finalContentRect() const; + [[nodiscard]] ContentGeometry contentGeometry() const; + void updateContentRect(); void contentSizeChanged(); // Radial animation interface. @@ -350,13 +343,39 @@ private: void zoomReset(); void zoomUpdate(int32 &newZoom); - void paintRadialLoading(Painter &p, bool radial, float64 radialOpacity); + void paintRadialLoading(not_null renderer); void paintRadialLoadingContent( Painter &p, QRect inner, bool radial, float64 radialOpacity) const; - void paintThemePreview(Painter &p, QRect clip); + void paintThemePreviewContent(Painter &p, QRect outer, QRect clip); + void paintDocumentBubbleContent( + Painter &p, + QRect outer, + QRect icon, + QRect clip) const; + void paintSaveMsgContent(Painter &p, QRect outer, QRect clip); + void paintControls(not_null renderer, float64 opacity); + void paintFooterContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity); + [[nodiscard]] QRect footerGeometry() const; + void paintCaptionContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity); + [[nodiscard]] QRect captionGeometry() const; + void paintGroupThumbsContent( + Painter &p, + QRect outer, + QRect clip, + float64 opacity); + + void updateSaveMsgState(); void updateOverRect(OverState state); bool updateOverState(OverState newState); @@ -374,21 +393,31 @@ private: [[nodiscard]] bool videoShown() const; [[nodiscard]] QSize videoSize() const; [[nodiscard]] bool videoIsGifOrUserpic() const; - [[nodiscard]] QImage videoFrame() const; - [[nodiscard]] QImage videoFrameForDirectPaint() const; - [[nodiscard]] QImage transformVideoFrame(QImage frame) const; - [[nodiscard]] QImage transformStaticContent(QPixmap content) const; + [[nodiscard]] QImage videoFrame() const; // ARGB (changes prepare format) + [[nodiscard]] QImage currentVideoFrameImage() const; // RGB (may convert) + [[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV + [[nodiscard]] int streamedIndex() const; + [[nodiscard]] QImage transformedShownContent() const; + [[nodiscard]] QImage transformShownContent( + QImage content, + int rotation) const; [[nodiscard]] bool documentContentShown() const; [[nodiscard]] bool documentBubbleShown() const; - void paintTransformedVideoFrame(Painter &p); - void paintTransformedStaticContent(Painter &p); + void setStaticContent(QImage image); + [[nodiscard]] bool contentShown() const; + [[nodiscard]] bool opaqueContentShown() const; void clearStreaming(bool savePosition = true); bool canInitStreaming() const; void applyHideWindowWorkaround(); - QBrush _transparentBrush; + Window::SessionController *findWindow() const; + bool _opengl = false; + const std::unique_ptr _surface; + const not_null _widget; + + base::weak_ptr _window; Main::Session *_session = nullptr; rpl::lifetime _sessionLifetime; PhotoData *_photo = nullptr; @@ -441,11 +470,18 @@ private: QPoint _mStart; bool _pressed = false; int32 _dragging = 0; - QPixmap _staticContent; + QImage _staticContent; + bool _staticContentTransparent = false; bool _blurred = true; + ContentGeometry _oldGeometry; + Ui::Animations::Simple _geometryAnimation; + rpl::lifetime _screenGeometryLifetime; + std::unique_ptr _applicationEventFilter; + std::unique_ptr _streamed; std::unique_ptr _pip; + int _streamedCreated = 0; bool _showAsPip = false; const style::icon *_docIcon = nullptr; @@ -453,6 +489,7 @@ private: QString _docName, _docSize, _docExt; int _docNameWidth = 0, _docSizeWidth = 0, _docExtWidth = 0; QRect _docRect, _docIconRect; + QImage _docRectImage; int _docThumbx = 0, _docThumby = 0, _docThumbw = 0; object_ptr _docDownload; object_ptr _docSaveAs; @@ -460,7 +497,6 @@ private: QRect _photoRadialRect; Ui::RadialAnimation _radial; - QImage _radialCache; History *_migrated = nullptr; History *_history = nullptr; // if conversation photos or files overview @@ -516,12 +552,12 @@ private: bool _touchRightButton = false; base::Timer _touchTimer; QPoint _touchStart; - QPoint _accumScroll; QString _saveMsgFilename; crl::time _saveMsgStarted = 0; anim::value _saveMsgOpacity; QRect _saveMsg; + QImage _saveMsgImage; base::Timer _saveMsgUpdater; Ui::Text::String _saveMsgText; SavePhotoVideo _savePhotoVideoWhenLoaded = SavePhotoVideo::None; @@ -544,9 +580,8 @@ private: object_ptr _themeShare = { nullptr }; Data::CloudTheme _themeCloudData; - bool _wasRepainted = false; + std::unique_ptr _hideWorkaround; }; -} // namespace View -} // namespace Media +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_pip.cpp b/Telegram/SourceFiles/media/view/media_view_pip.cpp index f224f8045..efaf89b73 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.cpp +++ b/Telegram/SourceFiles/media/view/media_view_pip.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_utility.h" #include "media/view/media_view_playback_progress.h" +#include "media/view/media_view_pip_opengl.h" +#include "media/view/media_view_pip_raster.h" #include "media/audio/media_audio.h" #include "data/data_document.h" #include "data/data_document_media.h" @@ -26,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/fade_wrap.h" #include "ui/widgets/shadow.h" #include "ui/text/format_values.h" +#include "ui/gl/gl_surface.h" #include "window/window_controller.h" #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -259,54 +262,6 @@ constexpr auto kMsInSecond = 1000; return result; } -Streaming::FrameRequest UnrotateRequest( - const Streaming::FrameRequest &request, - int rotation) { - if (!rotation) { - return request; - } - const auto unrotatedCorner = [&](RectPart corner) { - if (!(request.corners & corner)) { - return RectPart(0); - } - switch (corner) { - case RectPart::TopLeft: - return (rotation == 90) - ? RectPart::BottomLeft - : (rotation == 180) - ? RectPart::BottomRight - : RectPart::TopRight; - case RectPart::TopRight: - return (rotation == 90) - ? RectPart::TopLeft - : (rotation == 180) - ? RectPart::BottomLeft - : RectPart::BottomRight; - case RectPart::BottomRight: - return (rotation == 90) - ? RectPart::TopRight - : (rotation == 180) - ? RectPart::TopLeft - : RectPart::BottomLeft; - case RectPart::BottomLeft: - return (rotation == 90) - ? RectPart::BottomRight - : (rotation == 180) - ? RectPart::TopRight - : RectPart::TopLeft; - } - Unexpected("Corner in rotateCorner."); - }; - auto result = request; - result.outer = FlipSizeByRotation(request.outer, rotation); - result.resize = FlipSizeByRotation(request.resize, rotation); - result.corners = unrotatedCorner(RectPart::TopLeft) - | unrotatedCorner(RectPart::TopRight) - | unrotatedCorner(RectPart::BottomRight) - | unrotatedCorner(RectPart::BottomLeft); - return result; -} - Qt::Edges RectPartToQtEdges(RectPart rectPart) { switch (rectPart) { case RectPart::TopLeft: @@ -355,7 +310,7 @@ QRect RotatedRect(QRect rect, int rotation) { } bool UsePainterRotation(int rotation) { - return Platform::IsMac() || !(rotation % 180); + return !(rotation % 180); } QSize FlipSizeByRotation(QSize size, int rotation) { @@ -372,32 +327,42 @@ QImage RotateFrameImage(QImage image, int rotation) { PipPanel::PipPanel( QWidget *parent, - Fn paint) -: _parent(parent) -, _paint(std::move(paint)) { - setWindowFlags(Qt::Tool + Fn renderer) +: _content(Ui::GL::CreateSurface(std::move(renderer))) +, _parent(parent) { +} + +void PipPanel::init() { + widget()->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus); - setAttribute(Qt::WA_ShowWithoutActivating); - setAttribute(Qt::WA_MacAlwaysShowToolWindow); - setAttribute(Qt::WA_NoSystemBackground); - setAttribute(Qt::WA_TranslucentBackground); - Ui::Platform::IgnoreAllActivation(this); - Ui::Platform::InitOnTopPanel(this); - setMouseTracking(true); - resize(0, 0); - hide(); - createWinId(); + widget()->setAttribute(Qt::WA_ShowWithoutActivating); + widget()->setAttribute(Qt::WA_MacAlwaysShowToolWindow); + widget()->setAttribute(Qt::WA_NoSystemBackground); + widget()->setAttribute(Qt::WA_TranslucentBackground); + Ui::Platform::IgnoreAllActivation(widget()); + Ui::Platform::InitOnTopPanel(widget()); + widget()->setMouseTracking(true); + widget()->resize(0, 0); + widget()->hide(); + widget()->createWinId(); + + rp()->shownValue( + ) | rpl::filter([=](bool shown) { + return shown; + }) | rpl::start_with_next([=] { + // Workaround Qt's forced transient parent. + Ui::Platform::ClearTransientParent(widget()); + }, rp()->lifetime()); } -void PipPanel::setVisibleHook(bool visible) { - PipParent::setVisibleHook(visible); +not_null PipPanel::widget() const { + return _content->rpWidget(); +} - // workaround Qt's forced transient parent - if (visible) { - Ui::Platform::ClearTransientParent(this); - } +not_null PipPanel::rp() const { + return _content.get(); } void PipPanel::setAspectRatio(QSize ratio) { @@ -408,7 +373,7 @@ void PipPanel::setAspectRatio(QSize ratio) { if (_ratio.isEmpty()) { _ratio = QSize(1, 1); } - if (!size().isEmpty()) { + if (!widget()->size().isEmpty()) { setPosition(countPosition()); } } @@ -426,13 +391,17 @@ void PipPanel::setPosition(Position position) { } QRect PipPanel::inner() const { - return rect().marginsRemoved(_padding); + return widget()->rect().marginsRemoved(_padding); } RectParts PipPanel::attached() const { return _attached; } +bool PipPanel::useTransparency() const { + return _useTransparency; +} + void PipPanel::setDragDisabled(bool disabled) { _dragDisabled = disabled; if (_dragState) { @@ -449,7 +418,10 @@ rpl::producer<> PipPanel::saveGeometryRequests() const { } QScreen *PipPanel::myScreen() const { - return windowHandle() ? windowHandle()->screen() : nullptr; + if (const auto window = widget()->windowHandle()) { + return window->screen(); + } + return nullptr; } PipPanel::Position PipPanel::countPosition() const { @@ -459,7 +431,7 @@ PipPanel::Position PipPanel::countPosition() const { } auto result = Position(); result.screen = screen->geometry(); - result.geometry = geometry().marginsRemoved(_padding); + result.geometry = widget()->geometry().marginsRemoved(_padding); const auto available = screen->availableGeometry(); const auto skip = st::pipBorderSkip; const auto left = result.geometry.x(); @@ -498,9 +470,9 @@ void PipPanel::setPositionDefault() { return nullptr; }; const auto parentScreen = widgetScreen(_parent); - const auto myScreen = widgetScreen(this); + const auto myScreen = widgetScreen(widget()); if (parentScreen && myScreen && myScreen != parentScreen) { - windowHandle()->setScreen(parentScreen); + widget()->windowHandle()->setScreen(parentScreen); } const auto screen = parentScreen ? parentScreen @@ -581,61 +553,36 @@ void PipPanel::setPositionOnScreen(Position position, QRect available) { geometry += _padding; setGeometry(geometry); - setMinimumSize(minimalSize); - setMaximumSize( + widget()->setMinimumSize(minimalSize); + widget()->setMaximumSize( std::max(minimalSize.width(), maximalSize.width()), std::max(minimalSize.height(), maximalSize.height())); updateDecorations(); - update(); } -void PipPanel::paintEvent(QPaintEvent *e) { - QPainter p(this); - - if (_useTransparency) { - Ui::Platform::StartTranslucentPaint(p, e->region()); - } - - auto request = FrameRequest(); - const auto inner = this->inner(); - request.resize = request.outer = inner.size() * style::DevicePixelRatio(); - request.corners = RectPart(0) - | ((_attached & (RectPart::Left | RectPart::Top)) - ? RectPart(0) - : RectPart::TopLeft) - | ((_attached & (RectPart::Top | RectPart::Right)) - ? RectPart(0) - : RectPart::TopRight) - | ((_attached & (RectPart::Right | RectPart::Bottom)) - ? RectPart(0) - : RectPart::BottomRight) - | ((_attached & (RectPart::Bottom | RectPart::Left)) - ? RectPart(0) - : RectPart::BottomLeft); - request.radius = ImageRoundRadius::Large; - if (_useTransparency) { - const auto sides = RectPart::AllSides & ~_attached; - Ui::Shadow::paint(p, inner, width(), st::callShadow); - } - _paint(p, request); +void PipPanel::update() { + widget()->update(); } -void PipPanel::mousePressEvent(QMouseEvent *e) { - if (e->button() != Qt::LeftButton) { +void PipPanel::setGeometry(QRect geometry) { + widget()->setGeometry(geometry); +} + +void PipPanel::handleMousePress(QPoint position, Qt::MouseButton button) { + if (button != Qt::LeftButton) { return; } - updateOverState(e->pos()); + updateOverState(position); _pressState = _overState; - _pressPoint = e->globalPos(); + _pressPoint = QCursor::pos(); } -void PipPanel::mouseReleaseEvent(QMouseEvent *e) { - if (e->button() != Qt::LeftButton || !base::take(_pressState)) { +void PipPanel::handleMouseRelease(QPoint position, Qt::MouseButton button) { + if (button != Qt::LeftButton || !base::take(_pressState)) { return; - } else if (!base::take(_dragState)) { - //playbackPauseResume(); - } else { - finishDrag(e->globalPos()); + } else if (base::take(_dragState)) { + finishDrag(QCursor::pos()); + updateOverState(position); } } @@ -649,26 +596,28 @@ void PipPanel::updateOverState(QPoint point) { const auto top = count(RectPart::Top, _padding.top()); const auto right = count(RectPart::Right, _padding.right()); const auto bottom = count(RectPart::Bottom, _padding.bottom()); + const auto width = widget()->width(); + const auto height = widget()->height(); const auto overState = [&] { if (point.x() < left) { if (point.y() < top) { return RectPart::TopLeft; - } else if (point.y() >= height() - bottom) { + } else if (point.y() >= height - bottom) { return RectPart::BottomLeft; } else { return RectPart::Left; } - } else if (point.x() >= width() - right) { + } else if (point.x() >= width - right) { if (point.y() < top) { return RectPart::TopRight; - } else if (point.y() >= height() - bottom) { + } else if (point.y() >= height - bottom) { return RectPart::BottomRight; } else { return RectPart::Right; } } else if (point.y() < top) { return RectPart::Top; - } else if (point.y() >= height() - bottom) { + } else if (point.y() >= height - bottom) { return RectPart::Bottom; } else { return RectPart::Center; @@ -676,7 +625,7 @@ void PipPanel::updateOverState(QPoint point) { }(); if (_overState != overState) { _overState = overState; - setCursor([&] { + widget()->setCursor([&] { switch (_overState) { case RectPart::Center: return style::cur_pointer; @@ -698,19 +647,19 @@ void PipPanel::updateOverState(QPoint point) { } } -void PipPanel::mouseMoveEvent(QMouseEvent *e) { +void PipPanel::handleMouseMove(QPoint position) { if (!_pressState) { - updateOverState(e->pos()); + updateOverState(position); return; } - const auto point = e->globalPos(); + const auto point = QCursor::pos(); const auto distance = QApplication::startDragDistance(); if (!_dragState && (point - _pressPoint).manhattanLength() > distance && !_dragDisabled) { _dragState = _pressState; updateDecorations(); - _dragStartGeometry = geometry().marginsRemoved(_padding); + _dragStartGeometry = widget()->geometry().marginsRemoved(_padding); } if (_dragState) { if (Platform::IsWayland()) { @@ -726,9 +675,9 @@ void PipPanel::startSystemDrag() { const auto stateEdges = RectPartToQtEdges(*_dragState); if (stateEdges) { - windowHandle()->startSystemResize(stateEdges); + widget()->windowHandle()->startSystemResize(stateEdges); } else { - windowHandle()->startSystemMove(); + widget()->windowHandle()->startSystemMove(); } } @@ -779,8 +728,8 @@ void PipPanel::processDrag(QPoint point) { void PipPanel::finishDrag(QPoint point) { const auto screen = ScreenFromPosition(point); - const auto inner = geometry().marginsRemoved(_padding); - const auto position = pos(); + const auto inner = widget()->geometry().marginsRemoved(_padding); + const auto position = widget()->pos(); const auto clamped = [&] { auto result = position; if (Platform::IsWayland()) { @@ -811,7 +760,8 @@ void PipPanel::finishDrag(QPoint point) { void PipPanel::updatePositionAnimated() { const auto progress = _positionAnimation.value(1.); if (!_positionAnimation.animating()) { - move(_positionAnimationTo - QPoint(_padding.left(), _padding.top())); + widget()->move(_positionAnimationTo + - QPoint(_padding.left(), _padding.top())); if (!_dragState) { updateDecorations(); } @@ -819,7 +769,7 @@ void PipPanel::updatePositionAnimated() { } const auto from = QPointF(_positionAnimationFrom); const auto to = QPointF(_positionAnimationTo); - move((from + (to - from) * progress).toPoint() + widget()->move((from + (to - from) * progress).toPoint() - QPoint(_padding.left(), _padding.top())); } @@ -828,7 +778,8 @@ void PipPanel::moveAnimated(QPoint to) { return; } _positionAnimationTo = to; - _positionAnimationFrom = pos() + QPoint(_padding.left(), _padding.top()); + _positionAnimationFrom = widget()->pos() + + QPoint(_padding.left(), _padding.top()); _positionAnimation.stop(); _positionAnimation.start( [=] { updatePositionAnimated(); }, @@ -861,7 +812,7 @@ void PipPanel::updateDecorations() { _attached = position.attached; _padding = padding; _useTransparency = use; - setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency); + widget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency); setGeometry(newGeometry); update(); } @@ -879,10 +830,14 @@ Pip::Pip( , _instance(std::move(shared), [=] { waitingAnimationCallback(); }) , _panel( _delegate->pipParentWidget(), - [=](QPainter &p, const FrameRequest &request) { paint(p, request); }) + [=](Ui::GL::Capabilities capabilities) { + return chooseRenderer(capabilities); + }) , _playbackProgress(std::make_unique()) , _rotation(data->owner().mediaRotation().get(data)) -, _roundRect(ImageRoundRadius::Large, st::radialBg) +, _lastPositiveVolume((Core::App().settings().videoVolume() > 0.) + ? Core::App().settings().videoVolume() + : Core::Settings::kDefaultVolume) , _closeAndContinue(std::move(closeAndContinue)) , _destroy(std::move(destroy)) { setupPanel(); @@ -892,12 +847,13 @@ Pip::Pip( _data->session().account().sessionChanges( ) | rpl::start_with_next([=] { _destroy(); - }, _panel.lifetime()); + }, _panel.rp()->lifetime()); } Pip::~Pip() = default; void Pip::setupPanel() { + _panel.init(); const auto size = [&] { if (!_instance.info().video.size.isEmpty()) { return _instance.info().video.size; @@ -912,14 +868,14 @@ void Pip::setupPanel() { }(); _panel.setAspectRatio(FlipSizeByRotation(size, _rotation)); _panel.setPosition(Deserialize(_delegate->pipLoadGeometry())); - _panel.show(); + _panel.widget()->show(); _panel.saveGeometryRequests( ) | rpl::start_with_next([=] { saveGeometry(); - }, _panel.lifetime()); + }, _panel.rp()->lifetime()); - _panel.events( + _panel.rp()->events( ) | rpl::start_with_next([=](not_null e) { const auto mousePosition = [&] { return static_cast(e.get())->pos(); @@ -943,11 +899,11 @@ void Pip::setupPanel() { handleDoubleClick(mouseButton()); break; } - }, _panel.lifetime()); + }, _panel.rp()->lifetime()); } void Pip::handleClose() { - crl::on_main(&_panel, [=] { + crl::on_main(_panel.widget(), [=] { _destroy(); }); } @@ -957,27 +913,34 @@ void Pip::handleLeave() { } void Pip::handleMouseMove(QPoint position) { + const auto weak = Ui::MakeWeak(_panel.widget()); + const auto guard = gsl::finally([&] { + if (weak) { + _panel.handleMouseMove(position); + } + }); setOverState(computeState(position)); seekUpdate(position); + volumeControllerUpdate(position); } void Pip::setOverState(OverState state) { if (_over == state) { return; } - const auto was = _over; + const auto wasShown = ResolveShownOver(_over); _over = state; - const auto nowShown = (_over != OverState::None); - if ((was != OverState::None) != nowShown) { + const auto nowAreShown = (ResolveShownOver(_over) != OverState::None); + if ((wasShown != OverState::None) != nowAreShown) { _controlsShown.start( [=] { _panel.update(); }, - nowShown ? 0. : 1., - nowShown ? 1. : 0., + nowAreShown ? 0. : 1., + nowAreShown ? 1. : 0., st::fadeWrapDuration, anim::linear); } if (!_pressed) { - updateActiveState(was); + updateActiveState(wasShown); } _panel.update(); } @@ -986,27 +949,29 @@ void Pip::setPressedState(std::optional state) { if (_pressed == state) { return; } - const auto was = activeState(); + const auto wasShown = shownActiveState(); _pressed = state; - updateActiveState(was); + updateActiveState(wasShown); } -Pip::OverState Pip::activeState() const { - return _pressed.value_or(_over); +Pip::OverState Pip::shownActiveState() const { + return ResolveShownOver(_pressed.value_or(_over)); } float64 Pip::activeValue(const Button &button) const { - return button.active.value((activeState() == button.state) ? 1. : 0.); + const auto shownState = ResolveShownOver(button.state); + return button.active.value((shownActiveState() == shownState) ? 1. : 0.); } -void Pip::updateActiveState(OverState was) { +void Pip::updateActiveState(OverState wasShown) { const auto check = [&](Button &button) { - const auto now = (activeState() == button.state); - if ((was == button.state) != now) { + const auto shownState = ResolveShownOver(button.state); + const auto nowIsShown = (shownActiveState() == shownState); + if ((wasShown == shownState) != nowIsShown) { button.active.start( - [=, &button] { _panel.update(button.icon); }, - now ? 0. : 1., - now ? 1. : 0., + [=, &button] { _panel.widget()->update(button.icon); }, + nowIsShown ? 0. : 1., + nowIsShown ? 1. : 0., st::fadeWrapDuration, anim::linear); } @@ -1015,39 +980,78 @@ void Pip::updateActiveState(OverState was) { check(_enlarge); check(_play); check(_playback); + check(_volumeToggle); + check(_volumeController); +} + +Pip::OverState Pip::ResolveShownOver(OverState state) { + return (state == OverState::VolumeController) + ? OverState::VolumeToggle + : state; } void Pip::handleMousePress(QPoint position, Qt::MouseButton button) { + const auto weak = Ui::MakeWeak(_panel.widget()); + const auto guard = gsl::finally([&] { + if (weak) { + _panel.handleMousePress(position, button); + } + }); if (button != Qt::LeftButton) { return; } _pressed = _over; - if (_over == OverState::Playback) { + if (_over == OverState::Playback || _over == OverState::VolumeController) { _panel.setDragDisabled(true); } seekUpdate(position); + volumeControllerUpdate(position); } void Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) { + Expects(1 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + + const auto weak = Ui::MakeWeak(_panel.widget()); + const auto guard = gsl::finally([&] { + if (weak) { + _panel.handleMouseRelease(position, button); + } + }); if (button != Qt::LeftButton) { return; } seekUpdate(position); + + Assert(2 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + + volumeControllerUpdate(position); + + Assert(3 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + const auto pressed = base::take(_pressed); if (pressed && *pressed == OverState::Playback) { _panel.setDragDisabled(false); + + Assert(4 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + seekFinish(_playbackProgress->value()); - return; + } else if (pressed && *pressed == OverState::VolumeController) { + _panel.setDragDisabled(false); + _panel.update(); } else if (_panel.dragging() || !pressed || *pressed != _over) { _lastHandledPress = std::nullopt; - return; - } - - _lastHandledPress = _over; - switch (_over) { - case OverState::Close: _panel.close(); break; - case OverState::Enlarge: _closeAndContinue(); break; - case OverState::Other: playbackPauseResume(); break; + } else { + _lastHandledPress = _over; + switch (_over) { + case OverState::Close: _panel.widget()->close(); break; + case OverState::Enlarge: _closeAndContinue(); break; + case OverState::VolumeToggle: volumeToggled(); break; + case OverState::Other: playbackPauseResume(); break; + } } } @@ -1094,6 +1098,9 @@ void Pip::seekProgress(float64 value) { } void Pip::seekFinish(float64 value) { + Expects(5 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + if (!_lastDurationMs) { return; } @@ -1107,12 +1114,40 @@ void Pip::seekFinish(float64 value) { restartAtSeekPosition(positionMs); } +void Pip::volumeChanged(float64 volume) { + if (volume > 0.) { + _lastPositiveVolume = volume; + } + Player::mixer()->setVideoVolume(volume); + Core::App().settings().setVideoVolume(volume); + Core::App().saveSettingsDelayed(); +} + +void Pip::volumeToggled() { + const auto volume = Core::App().settings().videoVolume(); + volumeChanged(volume ? 0. : _lastPositiveVolume); + _panel.update(); +} + +void Pip::volumeControllerUpdate(QPoint position) { + if (!_pressed || *_pressed != OverState::VolumeController) { + return; + } + const auto unbound = (position.x() - _volumeController.icon.x()) + / float64(_volumeController.icon.width()); + const auto value = std::clamp(unbound, 0., 1.); + volumeChanged(value); + _panel.update(); +} + void Pip::setupButtons() { _close.state = OverState::Close; _enlarge.state = OverState::Enlarge; _playback.state = OverState::Playback; + _volumeToggle.state = OverState::VolumeToggle; + _volumeController.state = OverState::VolumeController; _play.state = OverState::Other; - _panel.sizeValue( + _panel.rp()->sizeValue( ) | rpl::map([=] { return _panel.inner(); }) | rpl::start_with_next([=](QRect rect) { @@ -1127,6 +1162,31 @@ void Pip::setupButtons() { rect.y(), st::pipEnlargeIcon.width() + 2 * skip, st::pipEnlargeIcon.height() + 2 * skip); + + const auto volumeSkip = st::pipPlaybackSkip; + const auto volumeHeight = 2 * volumeSkip + st::pipPlaybackWide; + const auto volumeToggleWidth = st::pipVolumeIcon0.width() + + 2 * skip; + const auto volumeToggleHeight = st::pipVolumeIcon0.height() + + 2 * skip; + const auto volumeWidth = (((st::mediaviewVolumeWidth + 2 * skip) + + _close.area.width() + + _enlarge.area.width() + + volumeToggleWidth) < rect.width()) + ? st::mediaviewVolumeWidth + : 0; + _volumeController.area = QRect( + rect.x() + rect.width() - volumeWidth - 2 * volumeSkip, + rect.y() + (volumeToggleHeight - volumeHeight) / 2, + volumeWidth, + volumeHeight); + _volumeToggle.area = QRect( + _volumeController.area.x() + - st::pipVolumeIcon0.width() + - skip, + rect.y(), + volumeToggleWidth, + volumeToggleHeight); if (!IsWindowControlsOnLeft()) { _close.area.moveLeft(rect.x() + rect.width() @@ -1136,15 +1196,26 @@ void Pip::setupButtons() { + rect.width() - (_enlarge.area.x() - rect.x()) - _enlarge.area.width()); + _volumeToggle.area.moveLeft(rect.x()); + _volumeController.area.moveLeft(_volumeToggle.area.x() + + _volumeToggle.area.width()); } _close.icon = _close.area.marginsRemoved({ skip, skip, skip, skip }); _enlarge.icon = _enlarge.area.marginsRemoved( { skip, skip, skip, skip }); + _volumeToggle.icon = _volumeToggle.area.marginsRemoved( + { skip, skip, skip, skip }); _play.icon = QRect( rect.x() + (rect.width() - st::pipPlayIcon.width()) / 2, rect.y() + (rect.height() - st::pipPlayIcon.height()) / 2, st::pipPlayIcon.width(), st::pipPlayIcon.height()); + const auto volumeArea = _volumeController.area; + _volumeController.icon = (volumeArea.width() > 2 * volumeSkip + && volumeArea.height() > 2 * volumeSkip) + ? volumeArea.marginsRemoved( + { volumeSkip, volumeSkip, volumeSkip, volumeSkip }) + : QRect(); const auto playbackSkip = st::pipPlaybackSkip; const auto playbackHeight = 2 * playbackSkip + st::pipPlaybackWide; _playback.area = QRect( @@ -1154,12 +1225,12 @@ void Pip::setupButtons() { playbackHeight); _playback.icon = _playback.area.marginsRemoved( { playbackSkip, playbackSkip, playbackSkip, playbackSkip }); - }, _panel.lifetime()); + }, _panel.rp()->lifetime()); _playbackProgress->setValueChangedCallback([=]( float64 value, float64 receivedTill) { - _panel.update(_playback.area); + _panel.widget()->update(_playback.area); }); } @@ -1188,137 +1259,191 @@ void Pip::setupStreaming() { updatePlaybackState(); } -void Pip::paint(QPainter &p, FrameRequest request) { - const auto image = videoFrameForDirectPaint( - UnrotateRequest(request, _rotation)); - const auto inner = _panel.inner(); - const auto rect = QRect{ - inner.topLeft(), - request.outer / style::DevicePixelRatio() +Ui::GL::ChosenRenderer Pip::chooseRenderer( + Ui::GL::Capabilities capabilities) { + const auto use = Platform::IsMac() + ? true + : capabilities.transparency; + LOG(("OpenGL: %1 (PipPanel)").arg(Logs::b(use))); + if (use) { + _opengl = true; + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::OpenGL, + }; + } + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::Raster, }; - if (UsePainterRotation(_rotation)) { - if (_rotation) { - p.save(); - p.rotate(_rotation); - } - p.drawImage(RotatedRect(rect, _rotation), image); - if (_rotation) { - p.restore(); - } - } else { - p.drawImage(rect, RotateFrameImage(image, _rotation)); - } - if (canUseVideoFrame()) { - _instance.markFrameShown(); - } - paintRadialLoading(p); - paintControls(p); } -void Pip::paintControls(QPainter &p) const { - const auto shown = _controlsShown.value( +void Pip::paint(not_null renderer) const { + const auto controlsShown = _controlsShown.value( (_over != OverState::None) ? 1. : 0.); - if (!shown) { - return; + auto geometry = ContentGeometry{ + .inner = _panel.inner(), + .attached = (_panel.useTransparency() + ? _panel.attached() + : RectPart::AllSides), + .fade = controlsShown, + .outer = _panel.widget()->size(), + .rotation = _rotation, + .videoRotation = _instance.info().video.rotation, + .useTransparency = _panel.useTransparency(), + }; + if (canUseVideoFrame()) { + renderer->paintTransformedVideoFrame(geometry); + _instance.markFrameShown(); + } else { + const auto content = staticContent(); + if (_preparedCoverState == ThumbState::Cover) { + geometry.rotation += base::take(geometry.videoRotation); + } + renderer->paintTransformedStaticContent(staticContent(), geometry); + } + if (_instance.waitingShown()) { + renderer->paintRadialLoading(countRadialRect(), controlsShown); + } + if (controlsShown > 0) { + paintButtons(renderer, controlsShown); + paintPlayback(renderer, controlsShown); + paintVolumeController(renderer, controlsShown); } - p.setOpacity(shown); - paintFade(p); - paintButtons(p); - paintPlayback(p); - paintPlaybackTexts(p); } -void Pip::paintFade(QPainter &p) const { - using Part = RectPart; - const auto sides = _panel.attached(); - const auto rounded = RectPart(0) - | ((sides & (Part::Top | Part::Left)) ? Part(0) : Part::TopLeft) - | ((sides & (Part::Top | Part::Right)) ? Part(0) : Part::TopRight) - | ((sides & (Part::Bottom | Part::Right)) - ? Part(0) - : Part::BottomRight) - | ((sides & (Part::Bottom | Part::Left)) - ? Part(0) - : Part::BottomLeft); - _roundRect.paintSomeRounded( - p, - _panel.inner(), - rounded | Part::NoTopBottom | Part::Top | Part::Bottom); -} - -void Pip::paintButtons(QPainter &p) const { - const auto opacity = p.opacity(); - const auto outer = _panel.width(); +void Pip::paintButtons(not_null renderer, float64 shown) const { + const auto outer = _panel.widget()->width(); const auto drawOne = [&]( const Button &button, const style::icon &icon, const style::icon &iconOver) { - const auto over = activeValue(button); - if (over < 1.) { - icon.paint(p, button.icon.x(), button.icon.y(), outer); - } - if (over > 0.) { - p.setOpacity(over * opacity); - iconOver.paint(p, button.icon.x(), button.icon.y(), outer); - p.setOpacity(opacity); - } + renderer->paintButton( + button, + outer, + shown, + activeValue(button), + icon, + iconOver); }; + + renderer->paintButtonsStart(); drawOne( _play, _showPause ? st::pipPauseIcon : st::pipPlayIcon, _showPause ? st::pipPauseIconOver : st::pipPlayIconOver); drawOne(_close, st::pipCloseIcon, st::pipCloseIconOver); drawOne(_enlarge, st::pipEnlargeIcon, st::pipEnlargeIconOver); + const auto volume = Core::App().settings().videoVolume(); + if (volume <= 0.) { + drawOne( + _volumeToggle, + st::pipVolumeIcon0, + st::pipVolumeIcon0Over); + } else if (volume < 1 / 2.) { + drawOne( + _volumeToggle, + st::pipVolumeIcon1, + st::pipVolumeIcon1Over); + } else { + drawOne( + _volumeToggle, + st::pipVolumeIcon2, + st::pipVolumeIcon2Over); + } } -void Pip::paintPlayback(QPainter &p) const { +void Pip::paintPlayback(not_null renderer, float64 shown) const { + const auto outer = QRect( + _playback.icon.x(), + _playback.icon.y() - st::pipPlaybackFont->height, + _playback.icon.width(), + st::pipPlaybackFont->height + _playback.icon.height()); + renderer->paintPlayback(outer, shown); +} + +void Pip::paintPlaybackContent( + QPainter &p, + QRect outer, + float64 shown) const { + p.setOpacity(shown); + paintPlaybackProgress(p, outer); + paintPlaybackTexts(p, outer); +} + +void Pip::paintPlaybackProgress(QPainter &p, QRect outer) const { const auto radius = _playback.icon.height() / 2; - const auto shown = activeValue(_playback); const auto progress = _playbackProgress->value(); - const auto width = _playback.icon.width(); + const auto active = activeValue(_playback); const auto height = anim::interpolate( st::pipPlaybackWidth, _playback.icon.height(), - activeValue(_playback)); - const auto left = _playback.icon.x(); - const auto top = _playback.icon.y() + _playback.icon.height() - height; - const auto done = int(std::round(width * progress)); + active); + const auto rect = QRect( + outer.x(), + (outer.y() + + st::pipPlaybackFont->height + + _playback.icon.height() + - height), + outer.width(), + height); + + paintProgressBar(p, rect, progress, radius, active); +} + +void Pip::paintProgressBar( + QPainter &p, + const QRect &rect, + float64 progress, + int radius, + float64 active) const { + const auto done = int(std::round(rect.width() * progress)); PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); if (done > 0) { - p.setBrush(st::mediaviewPipPlaybackActive); - p.setClipRect(left, top, done, height); + p.setBrush(anim::brush( + st::mediaviewPipControlsFg, + st::mediaviewPipPlaybackActive, + active)); + p.setClipRect(rect.x(), rect.y(), done, rect.height()); p.drawRoundedRect( - left, - top, - std::min(done + radius, width), - height, + rect.x(), + rect.y(), + std::min(done + radius, rect.width()), + rect.height(), radius, radius); } - if (done < width) { - const auto from = std::max(left + done - radius, left); + if (done < rect.width()) { + const auto from = std::max(rect.x() + done - radius, rect.x()); p.setBrush(st::mediaviewPipPlaybackInactive); - p.setClipRect(left + done, top, width - done, height); + p.setClipRect( + rect.x() + done, + rect.y(), + rect.width() - done, + rect.height()); p.drawRoundedRect( from, - top, - left + width - from, - height, + rect.y(), + rect.x() + rect.width() - from, + rect.height(), radius, radius); } p.setClipping(false); } -void Pip::paintPlaybackTexts(QPainter &p) const { - const auto left = _playback.area.x() + st::pipPlaybackTextSkip; - const auto right = _playback.area.x() +void Pip::paintPlaybackTexts(QPainter &p, QRect outer) const { + const auto left = outer.x() + - _playback.icon.x() + + _playback.area.x() + + st::pipPlaybackTextSkip; + const auto right = outer.x() + - _playback.icon.x() + + _playback.area.x() + _playback.area.width() - st::pipPlaybackTextSkip; - const auto top = _playback.icon.y() - - st::pipPlaybackFont->height - + st::pipPlaybackFont->ascent; + const auto top = outer.y() + st::pipPlaybackFont->ascent; p.setFont(st::pipPlaybackFont); p.setPen(st::mediaviewPipControlsFgOver); @@ -1326,6 +1451,37 @@ void Pip::paintPlaybackTexts(QPainter &p) const { p.drawText(right - _timeLeftWidth, top, _timeLeft); } +void Pip::paintVolumeController( + not_null renderer, + float64 shown) const { + if (_volumeController.icon.isEmpty()) { + return; + } + renderer->paintVolumeController(_volumeController.icon, shown); +} + +void Pip::paintVolumeControllerContent( + QPainter &p, + QRect outer, + float64 shown) const { + p.setOpacity(shown); + + const auto radius = _volumeController.icon.height() / 2; + const auto volume = Core::App().settings().videoVolume(); + const auto active = activeValue(_volumeController); + const auto height = anim::interpolate( + st::pipPlaybackWidth, + _volumeController.icon.height(), + active); + const auto rect = QRect( + outer.x(), + outer.y() + radius - height / 2, + outer.width(), + height); + + paintProgressBar(p, rect, volume, radius, active); +} + void Pip::handleStreamingUpdate(Streaming::Update &&update) { using namespace Streaming; @@ -1390,7 +1546,7 @@ void Pip::updatePlaybackTexts( _timeAlready = already; _timeLeft = left; _timeLeftWidth = st::pipPlaybackFont->width(_timeLeft); - _panel.update(QRect( + _panel.widget()->update(QRect( _playback.area.x(), _playback.icon.y() - st::pipPlaybackFont->height, _playback.area.width(), @@ -1398,12 +1554,12 @@ void Pip::updatePlaybackTexts( } void Pip::handleStreamingError(Streaming::Error &&error) { - _panel.close(); + _panel.widget()->close(); } void Pip::playbackPauseResume() { if (_instance.player().failed()) { - _panel.close(); + _panel.widget()->close(); } else if (_instance.player().finished() || !_instance.player().active()) { _startPaused = false; @@ -1418,13 +1574,30 @@ void Pip::playbackPauseResume() { } void Pip::restartAtSeekPosition(crl::time position) { + Expects(6 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + if (!_instance.info().video.cover.isNull()) { + _preparedCoverStorage = QImage(); + _preparedCoverState = ThumbState::Empty; _instance.saveFrameToCover(); } + + Assert(7 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + auto options = Streaming::PlaybackOptions(); options.position = position; options.audioId = _instance.player().prepareLegacyState().id; + + Assert(8 && _delegate->pipPlaybackSpeed() >= 0.5 + && _delegate->pipPlaybackSpeed() <= 2.); // Debugging strange crash. + options.speed = _delegate->pipPlaybackSpeed(); + + Assert(9 && options.speed >= 0.5 + && options.speed <= 2.); // Debugging strange crash. + _instance.play(options); if (_startPaused) { _instance.pause(); @@ -1439,12 +1612,19 @@ bool Pip::canUseVideoFrame() const { } QImage Pip::videoFrame(const FrameRequest &request) const { - if (canUseVideoFrame()) { - _preparedCoverStorage = QImage(); - return _instance.frame(request); - } - const auto &cover = _instance.info().video.cover; + Expects(canUseVideoFrame()); + return _instance.frame(request); +} + +Streaming::FrameWithInfo Pip::videoFrameWithInfo() const { + Expects(canUseVideoFrame()); + + return _instance.frameWithInfo(); +} + +QImage Pip::staticContent() const { + const auto &cover = _instance.info().video.cover; const auto media = _data->activeMediaView(); const auto use = media ? media @@ -1467,114 +1647,32 @@ QImage Pip::videoFrame(const FrameRequest &request) const { : blurred ? ThumbState::Inline : ThumbState::Empty; - if (_preparedCoverStorage.isNull() - || _preparedCoverRequest != request - || _preparedCoverState < state) { - _preparedCoverRequest = request; - _preparedCoverState = state; - if (state == ThumbState::Cover) { - _preparedCoverStorage = Streaming::PrepareByRequest( - _instance.info().video.cover, - false, - _instance.info().video.rotation, - request, + if (!_preparedCoverStorage.isNull() && _preparedCoverState >= state) { + return _preparedCoverStorage; + } + _preparedCoverState = state; + if (state == ThumbState::Cover) { + _preparedCoverStorage = _instance.info().video.cover; + } else { + _preparedCoverStorage = (good + ? good + : thumb + ? thumb + : blurred + ? blurred + : Image::BlankMedia().get())->original(); + if (!good) { + _preparedCoverStorage = Images::prepareBlur( std::move(_preparedCoverStorage)); - } else if (!request.resize.isEmpty()) { - using Option = Images::Option; - const auto options = Option::Smooth - | (good ? Option(0) : Option::Blurred) - | Option::RoundedLarge - | ((request.corners & RectPart::TopLeft) - ? Option::RoundedTopLeft - : Option(0)) - | ((request.corners & RectPart::TopRight) - ? Option::RoundedTopRight - : Option(0)) - | ((request.corners & RectPart::BottomRight) - ? Option::RoundedBottomRight - : Option(0)) - | ((request.corners & RectPart::BottomLeft) - ? Option::RoundedBottomLeft - : Option(0)); - _preparedCoverStorage = (good - ? good - : thumb - ? thumb - : blurred - ? blurred - : Image::BlankMedia().get())->pixNoCache( - request.resize.width(), - request.resize.height(), - options, - request.outer.width(), - request.outer.height()).toImage(); } } return _preparedCoverStorage; } -QImage Pip::videoFrameForDirectPaint(const FrameRequest &request) const { - const auto result = videoFrame(request); - -#ifdef USE_OPENGL_OVERLAY_WIDGET - const auto bytesPerLine = result.bytesPerLine(); - if (bytesPerLine == result.width() * 4) { - return result; - } - - // On macOS 10.8+ we use QOpenGLWidget as OverlayWidget base class. - // The OpenGL painter can't paint textures where byte data is with strides. - // So in that case we prepare a compact copy of the frame to render. - // - // See Qt commit ed557c037847e343caa010562952b398f806adcd - // - auto &cache = _frameForDirectPaint; - if (cache.size() != result.size()) { - cache = QImage(result.size(), result.format()); - } - const auto height = result.height(); - const auto line = cache.bytesPerLine(); - Assert(line == result.width() * 4); - Assert(line < bytesPerLine); - - auto from = result.bits(); - auto to = cache.bits(); - for (auto y = 0; y != height; ++y) { - memcpy(to, from, line); - to += line; - from += bytesPerLine; - } - return cache; -#endif // USE_OPENGL_OVERLAY_WIDGET - - return result; -} - -void Pip::paintRadialLoading(QPainter &p) const { - const auto inner = countRadialRect(); -#ifdef USE_OPENGL_OVERLAY_WIDGET - { - if (_radialCache.size() != inner.size() * cIntRetinaFactor()) { - _radialCache = QImage( - inner.size() * cIntRetinaFactor(), - QImage::Format_ARGB32_Premultiplied); - _radialCache.setDevicePixelRatio(cRetinaFactor()); - } - _radialCache.fill(Qt::transparent); - - Painter q(&_radialCache); - paintRadialLoadingContent(q, inner.translated(-inner.topLeft())); - } - p.drawImage(inner.topLeft(), _radialCache); -#else // USE_OPENGL_OVERLAY_WIDGET - paintRadialLoadingContent(p, inner); -#endif // USE_OPENGL_OVERLAY_WIDGET -} - -void Pip::paintRadialLoadingContent(QPainter &p, const QRect &inner) const { - if (!_instance.waitingShown()) { - return; - } +void Pip::paintRadialLoadingContent( + QPainter &p, + const QRect &inner, + QColor fg) const { const auto arc = inner.marginsRemoved(QMargins( st::radialLine, st::radialLine, @@ -1593,8 +1691,8 @@ void Pip::paintRadialLoadingContent(QPainter &p, const QRect &inner) const { _instance.waitingState(), arc.topLeft(), arc.size(), - _panel.width(), - st::radialFg, + _panel.widget()->width(), + fg, st::radialLine); } @@ -1617,13 +1715,17 @@ Pip::OverState Pip::computeState(QPoint position) const { return OverState::Enlarge; } else if (_playback.area.contains(position)) { return OverState::Playback; + } else if (_volumeToggle.area.contains(position)) { + return OverState::VolumeToggle; + } else if (_volumeController.area.contains(position)) { + return OverState::VolumeController; } else { return OverState::Other; } } void Pip::waitingAnimationCallback() { - _panel.update(countRadialRect()); + _panel.widget()->update(countRadialRect()); } } // namespace View diff --git a/Telegram/SourceFiles/media/view/media_view_pip.h b/Telegram/SourceFiles/media/view/media_view_pip.h index a4a5f80e5..155481549 100644 --- a/Telegram/SourceFiles/media/view/media_view_pip.h +++ b/Telegram/SourceFiles/media/view/media_view_pip.h @@ -22,6 +22,10 @@ namespace Ui { class IconButton; template class FadeWrap; +namespace GL { +struct ChosenRenderer; +struct Capabilities; +} // namespace GL } // namespace Ui namespace Media { @@ -38,17 +42,7 @@ class PlaybackProgress; [[nodiscard]] QSize FlipSizeByRotation(QSize size, int rotation); [[nodiscard]] QImage RotateFrameImage(QImage image, int rotation); -#if defined Q_OS_MAC && !defined OS_MAC_OLD -#define USE_OPENGL_OVERLAY_WIDGET -#endif // Q_OS_MAC && !OS_MAC_OLD - -#ifdef USE_OPENGL_OVERLAY_WIDGET -using PipParent = Ui::RpWidgetWrap; -#else // USE_OPENGL_OVERLAY_WIDGET -using PipParent = Ui::RpWidget; -#endif // USE_OPENGL_OVERLAY_WIDGET - -class PipPanel final : public PipParent { +class PipPanel final { public: struct Position { RectParts attached = RectPart(0); @@ -56,30 +50,34 @@ public: QRect geometry; QRect screen; }; - using FrameRequest = Streaming::FrameRequest; PipPanel( QWidget *parent, - Fn paint); + Fn renderer); + void init(); + + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null rp() const; + + void update(); + void setGeometry(QRect geometry); void setAspectRatio(QSize ratio); [[nodiscard]] Position countPosition() const; void setPosition(Position position); [[nodiscard]] QRect inner() const; [[nodiscard]] RectParts attached() const; + [[nodiscard]] bool useTransparency() const; + void setDragDisabled(bool disabled); [[nodiscard]] bool dragging() const; + void handleMousePress(QPoint position, Qt::MouseButton button); + void handleMouseRelease(QPoint position, Qt::MouseButton button); + void handleMouseMove(QPoint position); + [[nodiscard]] rpl::producer<> saveGeometryRequests() const; -protected: - void paintEvent(QPaintEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - - void setVisibleHook(bool visible) override; - private: void setPositionDefault(); void setPositionOnScreen(Position position, QRect available); @@ -93,8 +91,8 @@ private: void moveAnimated(QPoint to); void updateDecorations(); - QPointer _parent; - Fn _paint; + const std::unique_ptr _content; + const QPointer _parent; RectParts _attached = RectParts(); RectParts _snapped = RectParts(); QSize _ratio; @@ -141,6 +139,8 @@ private: Close, Enlarge, Playback, + VolumeToggle, + VolumeController, Other, }; enum class ThumbState { @@ -156,13 +156,31 @@ private: OverState state = OverState::None; Ui::Animations::Simple active; }; + struct ContentGeometry { + QRect inner; + RectParts attached = RectParts(); + float64 fade = 0.; + QSize outer; + int rotation = 0; + int videoRotation = 0; + bool useTransparency = false; + }; + struct StaticContent { + QImage image; + bool blurred = false; + }; using FrameRequest = Streaming::FrameRequest; + class Renderer; + class RendererGL; + class RendererSW; void setupPanel(); void setupButtons(); void setupStreaming(); - void paint(QPainter &p, FrameRequest request); void playbackPauseResume(); + void volumeChanged(float64 volume); + void volumeToggled(); + void volumeControllerUpdate(QPoint position); void waitingAnimationCallback(); void handleStreamingUpdate(Streaming::Update &&update); void handleStreamingError(Streaming::Error &&error); @@ -174,16 +192,22 @@ private: [[nodiscard]] bool canUseVideoFrame() const; [[nodiscard]] QImage videoFrame(const FrameRequest &request) const; - [[nodiscard]] QImage videoFrameForDirectPaint( - const FrameRequest &request) const; + [[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV + [[nodiscard]] QImage staticContent() const; [[nodiscard]] OverState computeState(QPoint position) const; void setOverState(OverState state); void setPressedState(std::optional state); - [[nodiscard]] OverState activeState() const; + [[nodiscard]] OverState shownActiveState() const; [[nodiscard]] float64 activeValue(const Button &button) const; void updateActiveState(OverState was); void updatePlaybackTexts(int64 position, int64 length, int64 frequency); + [[nodiscard]] static OverState ResolveShownOver(OverState state); + + [[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer( + Ui::GL::Capabilities capabilities); + void paint(not_null renderer) const; + void handleMouseMove(QPoint position); void handleMousePress(QPoint position, Qt::MouseButton button); void handleMouseRelease(QPoint position, Qt::MouseButton button); @@ -191,13 +215,28 @@ private: void handleLeave(); void handleClose(); - void paintControls(QPainter &p) const; - void paintFade(QPainter &p) const; - void paintButtons(QPainter &p) const; - void paintPlayback(QPainter &p) const; - void paintPlaybackTexts(QPainter &p) const; - void paintRadialLoading(QPainter &p) const; - void paintRadialLoadingContent(QPainter &p, const QRect &inner) const; + void paintRadialLoadingContent( + QPainter &p, + const QRect &inner, + QColor fg) const; + void paintButtons(not_null renderer, float64 shown) const; + void paintPlayback(not_null renderer, float64 shown) const; + void paintPlaybackContent(QPainter &p, QRect outer, float64 shown) const; + void paintPlaybackProgress(QPainter &p, QRect outer) const; + void paintProgressBar( + QPainter &p, + const QRect &rect, + float64 progress, + int radius, + float64 active) const; + void paintPlaybackTexts(QPainter &p, QRect outer) const; + void paintVolumeController( + not_null renderer, + float64 shown) const; + void paintVolumeControllerContent( + QPainter &p, + QRect outer, + float64 shown) const; [[nodiscard]] QRect countRadialRect() const; void seekUpdate(QPoint position); @@ -208,6 +247,7 @@ private: not_null _data; FullMsgId _contextId; Streaming::Instance _instance; + bool _opengl = false; PipPanel _panel; QSize _size; std::unique_ptr _playbackProgress; @@ -219,6 +259,7 @@ private: QString _timeAlready, _timeLeft; int _timeLeftWidth = 0; int _rotation = 0; + float64 _lastPositiveVolume = 1.; crl::time _seekPositionMs = -1; crl::time _lastDurationMs = 0; OverState _over = OverState::None; @@ -228,20 +269,15 @@ private: Button _enlarge; Button _playback; Button _play; + Button _volumeToggle; + Button _volumeController; Ui::Animations::Simple _controlsShown; - Ui::RoundRect _roundRect; FnMut _closeAndContinue; FnMut _destroy; -#ifdef USE_OPENGL_OVERLAY_WIDGET - mutable QImage _frameForDirectPaint; - mutable QImage _radialCache; -#endif // USE_OPENGL_OVERLAY_WIDGET - mutable QImage _preparedCoverStorage; - mutable FrameRequest _preparedCoverRequest; - mutable ThumbState _preparedCoverState; + mutable ThumbState _preparedCoverState = ThumbState::Empty; }; diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp new file mode 100644 index 000000000..5925c435c --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp @@ -0,0 +1,781 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/view/media_view_pip_opengl.h" + +#include "ui/gl/gl_shader.h" +#include "ui/gl/gl_primitives.h" +#include "ui/widgets/shadow.h" +#include "media/streaming/media_streaming_common.h" +#include "base/platform/base_platform_info.h" +#include "styles/style_media_view.h" +#include "styles/style_calls.h" // st::callShadow. + +namespace Media::View { +namespace { + +using namespace Ui::GL; + +constexpr auto kRadialLoadingOffset = 4; +constexpr auto kPlaybackOffset = kRadialLoadingOffset + 4; +constexpr auto kVolumeControllerOffset = kPlaybackOffset + 4; +constexpr auto kControlsOffset = kVolumeControllerOffset + 4; +constexpr auto kControlValues = 4 * 4 + 2 * 4; + +[[nodiscard]] ShaderPart FragmentAddControlOver() { + return { + .header = R"( +varying vec2 o_texcoord; +uniform float o_opacity; +)", + .body = R"( + vec4 over = texture2D(s_texture, o_texcoord); + result = result * (1. - o_opacity) + + vec4(over.b, over.g, over.r, over.a) * o_opacity; +)", + }; +} + +[[nodiscard]] ShaderPart FragmentApplyFade() { + return { + .header = R"( +uniform vec4 fadeColor; // Premultiplied. +)", + .body = R"( + result = result * (1. - fadeColor.a) + fadeColor; +)", + }; +} + +ShaderPart FragmentSampleShadow() { + return { + .header = R"( +uniform sampler2D h_texture; +uniform vec2 h_size; +uniform vec4 h_extend; +uniform vec4 h_components; +)", + .body = R"( + vec4 extended = vec4( // Left-Bottom-Width-Height rectangle. + roundRect.xy - h_extend.xw, + roundRect.zw + h_extend.xw + h_extend.zy); + vec2 inside = (gl_FragCoord.xy - extended.xy); + vec2 insideOtherCorner = (inside + h_size - extended.zw); + vec4 outsideCorners = step( + vec4(h_components.xy, inside), + vec4(inside, extended.zw - h_components.xy)); + vec4 insideCorners = vec4(1.) - outsideCorners; + vec2 linear = outsideCorners.xy * outsideCorners.zw; + vec2 h_size_half = 0.5 * h_size; + + vec2 bottomleft = inside * insideCorners.x * insideCorners.y; + vec2 bottomright = vec2(insideOtherCorner.x, inside.y) + * insideCorners.z + * insideCorners.y; + vec2 topright = insideOtherCorner * insideCorners.z * insideCorners.w; + vec2 topleft = vec2(inside.x, insideOtherCorner.y) + * insideCorners.x + * insideCorners.w; + + vec2 left = vec2(inside.x, h_size_half.y) + * step(inside.x, h_components.z) + * linear.y; + vec2 bottom = vec2(h_size_half.x, inside.y) + * step(inside.y, h_components.w) + * linear.x; + vec2 right = vec2(insideOtherCorner.x, h_size_half.y) + * step(h_size.x - h_components.z, insideOtherCorner.x) + * linear.y; + vec2 top = vec2(h_size_half.x, insideOtherCorner.y) + * step(h_size.y - h_components.w, insideOtherCorner.y) + * linear.x; + + vec2 uv = bottomleft + + bottomright + + topleft + + topright + + left + + bottom + + right + + top; + result = texture2D(h_texture, uv / h_size); +)", + }; +} + +ShaderPart FragmentRoundToShadow() { + const auto shadow = FragmentSampleShadow(); + return { + .header = R"( +uniform vec4 roundRect; +uniform float roundRadius; +)" + shadow.header + R"( + +float roundedCorner() { + vec2 rectHalf = roundRect.zw / 2.; + vec2 rectCenter = roundRect.xy + rectHalf; + vec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter); + vec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5); + vec2 fromCenterWithRadius = fromRectCenter + vectorRadius; + vec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf) + - rectHalf; + float rounded = length(fromRoundingCenter) - roundRadius; + + return 1. - smoothstep(0., 1., rounded); +} + +vec4 shadow() { + vec4 result; + +)" + shadow.body + R"( + + return result; +} +)", + .body = R"( + float round = roundedCorner(); + result = result * round + shadow() * (1. - round); +)", + }; +} + +} // namespace + +Pip::RendererGL::RendererGL(not_null owner) +: _owner(owner) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _radialImage.invalidate(); + _playbackImage.invalidate(); + _volumeControllerImage.invalidate(); + invalidateControls(); + }, _lifetime); +} + +void Pip::RendererGL::init( + not_null widget, + QOpenGLFunctions &f) { + constexpr auto kQuads = 8; + constexpr auto kQuadVertices = kQuads * 4; + constexpr auto kQuadValues = kQuadVertices * 4; + constexpr auto kControlsValues = kControlsCount * kControlValues; + constexpr auto kValues = kQuadValues + kControlsValues; + + _contentBuffer.emplace(); + _contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw); + _contentBuffer->create(); + _contentBuffer->bind(); + _contentBuffer->allocate(kValues * sizeof(GLfloat)); + + _textures.ensureCreated(f); + + _argb32Program.emplace(); + _texturedVertexShader = LinkProgram( + &*_argb32Program, + VertexShader({ + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentApplyFade(), + FragmentRoundToShadow(), + })).vertex; + + _yuv420Program.emplace(); + LinkProgram( + &*_yuv420Program, + _texturedVertexShader, + FragmentShader({ + FragmentSampleYUV420Texture(), + FragmentApplyFade(), + FragmentRoundToShadow(), + })); + + _imageProgram.emplace(); + LinkProgram( + &*_imageProgram, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + })); + + _controlsProgram.emplace(); + LinkProgram( + &*_controlsProgram, + VertexShader({ + VertexViewportTransform(), + VertexPassTextureCoord(), + VertexPassTextureCoord('o'), + }), + FragmentShader({ + FragmentSampleARGB32Texture(), + FragmentAddControlOver(), + FragmentGlobalOpacity(), + })); + + createShadowTexture(); +} + +void Pip::RendererGL::deinit( + not_null widget, + QOpenGLFunctions &f) { + _textures.destroy(f); + _imageProgram = std::nullopt; + _texturedVertexShader = nullptr; + _argb32Program = std::nullopt; + _yuv420Program = std::nullopt; + _controlsProgram = std::nullopt; + _contentBuffer = std::nullopt; +} + +void Pip::RendererGL::createShadowTexture() { + const auto &shadow = st::callShadow; + const auto size = 2 * st::callShadow.topLeft.size() + + QSize(st::roundRadiusLarge, st::roundRadiusLarge); + auto image = QImage( + size * cIntRetinaFactor(), + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(cRetinaFactor()); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + Ui::Shadow::paint( + p, + QRect(QPoint(), size).marginsRemoved(shadow.extend), + size.width(), + shadow); + } + _shadowImage.setImage(std::move(image)); +} + +void Pip::RendererGL::paint( + not_null widget, + QOpenGLFunctions &f) { + const auto factor = widget->devicePixelRatio(); + if (_factor != factor) { + _factor = factor; + _controlsImage.invalidate(); + } + _blendingEnabled = false; + _viewport = widget->size(); + _uniformViewport = QVector2D( + _viewport.width() * _factor, + _viewport.height() * _factor); + _f = &f; + _owner->paint(this); + _f = nullptr; +} + +std::optional Pip::RendererGL::clearColor() { + return QColor(0, 0, 0, 0); +} + +void Pip::RendererGL::paintTransformedVideoFrame( + ContentGeometry geometry) { + const auto data = _owner->videoFrameWithInfo(); + if (data.format == Streaming::FrameFormat::None) { + return; + } + geometry.rotation = (geometry.rotation + geometry.videoRotation) % 360; + if (data.format == Streaming::FrameFormat::ARGB32) { + Assert(!data.original.isNull()); + paintTransformedStaticContent(data.original, geometry); + return; + } + Assert(data.format == Streaming::FrameFormat::YUV420); + Assert(!data.yuv420->size.isEmpty()); + const auto yuv = data.yuv420; + _yuv420Program->bind(); + + const auto upload = (_trackFrameIndex != data.index); + _trackFrameIndex = data.index; + + _f->glActiveTexture(GL_TEXTURE0); + _textures.bind(*_f, 1); + if (upload) { + _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + uploadTexture( + GL_RED, + GL_RED, + yuv->size, + _lumaSize, + yuv->y.stride, + yuv->y.data); + _lumaSize = yuv->size; + } + _f->glActiveTexture(GL_TEXTURE1); + _textures.bind(*_f, 2); + if (upload) { + uploadTexture( + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->u.stride, + yuv->u.data); + } + _f->glActiveTexture(GL_TEXTURE2); + _textures.bind(*_f, 3); + if (upload) { + uploadTexture( + GL_RED, + GL_RED, + yuv->chromaSize, + _chromaSize, + yuv->v.stride, + yuv->v.data); + _chromaSize = yuv->chromaSize; + _f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + _yuv420Program->setUniformValue("y_texture", GLint(0)); + _yuv420Program->setUniformValue("u_texture", GLint(1)); + _yuv420Program->setUniformValue("v_texture", GLint(2)); + + paintTransformedContent(&*_yuv420Program, geometry); +} + +void Pip::RendererGL::paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry) { + _argb32Program->bind(); + + _f->glActiveTexture(GL_TEXTURE0); + _textures.bind(*_f, 0); + const auto cacheKey = image.cacheKey(); + const auto upload = (_cacheKey != cacheKey); + if (upload) { + _cacheKey = cacheKey; + const auto stride = image.bytesPerLine() / 4; + const auto data = image.constBits(); + uploadTexture( + GL_RGBA, + GL_RGBA, + image.size(), + _rgbaSize, + stride, + data); + _rgbaSize = image.size(); + } + _argb32Program->setUniformValue("s_texture", GLint(0)); + + paintTransformedContent(&*_argb32Program, geometry); +} + +void Pip::RendererGL::paintTransformedContent( + not_null program, + ContentGeometry geometry) { + std::array, 4> rect = { { + { { -1.f, 1.f } }, + { { 1.f, 1.f } }, + { { 1.f, -1.f } }, + { { -1.f, -1.f } }, + } }; + if (const auto shift = (geometry.rotation / 90); shift != 0) { + std::rotate(begin(rect), begin(rect) + shift, end(rect)); + } + const auto xscale = 1.f / geometry.inner.width(); + const auto yscale = 1.f / geometry.inner.height(); + const GLfloat coords[] = { + rect[0][0], rect[0][1], + -geometry.inner.x() * xscale, + -geometry.inner.y() * yscale, + + rect[1][0], rect[1][1], + (geometry.outer.width() - geometry.inner.x()) * xscale, + -geometry.inner.y() * yscale, + + rect[2][0], rect[2][1], + (geometry.outer.width() - geometry.inner.x()) * xscale, + (geometry.outer.height() - geometry.inner.y()) * yscale, + + rect[3][0], rect[3][1], + -geometry.inner.x() * xscale, + (geometry.outer.height() - geometry.inner.y()) * yscale, + }; + + _contentBuffer->write(0, coords, sizeof(coords)); + + const auto rgbaFrame = _chromaSize.isEmpty(); + _f->glActiveTexture(rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3); + _shadowImage.bind(*_f); + + const auto globalFactor = cIntRetinaFactor(); + const auto fadeAlpha = st::radialBg->c.alphaF() * geometry.fade; + const auto roundRect = transformRect(RoundingRect(geometry)); + program->setUniformValue("roundRect", Uniform(roundRect)); + program->setUniformValue("h_texture", GLint(rgbaFrame ? 1 : 3)); + program->setUniformValue("h_size", QSizeF(_shadowImage.image().size())); + program->setUniformValue("h_extend", QVector4D( + st::callShadow.extend.left() * globalFactor, + st::callShadow.extend.top() * globalFactor, + st::callShadow.extend.right() * globalFactor, + st::callShadow.extend.bottom() * globalFactor)); + program->setUniformValue("h_components", QVector4D( + float(st::callShadow.topLeft.width() * globalFactor), + float(st::callShadow.topLeft.height() * globalFactor), + float(st::callShadow.left.width() * globalFactor), + float(st::callShadow.top.height() * globalFactor))); + program->setUniformValue( + "roundRadius", + GLfloat(st::roundRadiusLarge * _factor)); + program->setUniformValue("fadeColor", QVector4D( + float(st::radialBg->c.redF() * fadeAlpha), + float(st::radialBg->c.greenF() * fadeAlpha), + float(st::radialBg->c.blueF() * fadeAlpha), + float(fadeAlpha))); + + FillTexturedRectangle(*_f, &*program); +} + +void Pip::RendererGL::uploadTexture( + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const { + _f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride); + if (hasSize != size) { + _f->glTexImage2D( + GL_TEXTURE_2D, + 0, + internalformat, + size.width(), + size.height(), + 0, + format, + GL_UNSIGNED_BYTE, + data); + } else { + _f->glTexSubImage2D( + GL_TEXTURE_2D, + 0, + 0, + 0, + size.width(), + size.height(), + format, + GL_UNSIGNED_BYTE, + data); + } + _f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + +void Pip::RendererGL::paintRadialLoading( + QRect inner, + float64 controlsShown) { + paintUsingRaster(_radialImage, inner, [&](Painter &&p) { + // Raster renderer paints content, then radial loading, then fade. + // Here we paint fade together with the content, so we should emulate + // radial loading being under the fade. + // + // The loading background is the same color as the fade (radialBg), + // so nothing should be done with it. But the fade should be added + // to the radial loading line color (radialFg). + const auto newInner = QRect(QPoint(), inner.size()); + const auto fg = st::radialFg->c; + const auto fade = st::radialBg->c; + const auto fadeAlpha = controlsShown * fade.alphaF(); + const auto fgAlpha = 1. - fadeAlpha; + const auto color = (fadeAlpha == 0.) ? fg : QColor( + int(std::round(fg.red() * fgAlpha + fade.red() * fadeAlpha)), + int(std::round(fg.green() * fgAlpha + fade.green() * fadeAlpha)), + int(std::round(fg.blue() * fgAlpha + fade.blue() * fadeAlpha)), + fg.alpha()); + + _owner->paintRadialLoadingContent(p, newInner, color); + }, kRadialLoadingOffset, true); +} + +void Pip::RendererGL::paintPlayback(QRect outer, float64 shown) { + paintUsingRaster(_playbackImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintPlaybackContent(p, newOuter, shown); + }, kPlaybackOffset, true); +} + +void Pip::RendererGL::paintVolumeController(QRect outer, float64 shown) { + paintUsingRaster(_volumeControllerImage, outer, [&](Painter &&p) { + const auto newOuter = QRect(QPoint(), outer.size()); + _owner->paintVolumeControllerContent(p, newOuter, shown); + }, kVolumeControllerOffset, true); +} + +void Pip::RendererGL::paintButtonsStart() { + validateControls(); + _f->glActiveTexture(GL_TEXTURE0); + _controlsImage.bind(*_f); + toggleBlending(true); +} + +void Pip::RendererGL::paintButton( + const Button &button, + int outerWidth, + float64 shown, + float64 over, + const style::icon &icon, + const style::icon &iconOver) { + const auto tryIndex = [&](int stateIndex) -> std::optional { + const auto result = ControlMeta(button.state, stateIndex); + return (result.icon == &icon && result.iconOver == &iconOver) + ? std::make_optional(result) + : std::nullopt; + }; + const auto meta = tryIndex(0) + ? *tryIndex(0) + : tryIndex(1) + ? *tryIndex(1) + : *tryIndex(2); + Assert(meta.icon == &icon && meta.iconOver == &iconOver); + + const auto offset = kControlsOffset + (meta.index * kControlValues) / 4; + const auto iconRect = _controlsImage.texturedRect( + button.icon, + _controlsTextures[meta.index * 2 + 0]); + const auto iconOverRect = _controlsImage.texturedRect( + button.icon, + _controlsTextures[meta.index * 2 + 1]); + const auto iconGeometry = transformRect(iconRect.geometry); + const GLfloat coords[] = { + iconGeometry.left(), iconGeometry.top(), + iconRect.texture.left(), iconRect.texture.bottom(), + + iconGeometry.right(), iconGeometry.top(), + iconRect.texture.right(), iconRect.texture.bottom(), + + iconGeometry.right(), iconGeometry.bottom(), + iconRect.texture.right(), iconRect.texture.top(), + + iconGeometry.left(), iconGeometry.bottom(), + iconRect.texture.left(), iconRect.texture.top(), + + iconOverRect.texture.left(), iconOverRect.texture.bottom(), + iconOverRect.texture.right(), iconOverRect.texture.bottom(), + iconOverRect.texture.right(), iconOverRect.texture.top(), + iconOverRect.texture.left(), iconOverRect.texture.top(), + }; + _contentBuffer->write( + offset * 4 * sizeof(GLfloat), + coords, + sizeof(coords)); + _controlsProgram->bind(); + _controlsProgram->setUniformValue("o_opacity", GLfloat(over)); + _controlsProgram->setUniformValue("g_opacity", GLfloat(shown)); + _controlsProgram->setUniformValue("viewport", _uniformViewport); + + GLint overTexcoord = _controlsProgram->attributeLocation("o_texcoordIn"); + _f->glVertexAttribPointer( + overTexcoord, + 2, + GL_FLOAT, + GL_FALSE, + 2 * sizeof(GLfloat), + reinterpret_cast((offset + 4) * 4 * sizeof(GLfloat))); + _f->glEnableVertexAttribArray(overTexcoord); + FillTexturedRectangle(*_f, &*_controlsProgram, offset); + _f->glDisableVertexAttribArray(overTexcoord); +} + +auto Pip::RendererGL::ControlMeta(OverState control, int index) +-> Control { + Expects(index >= 0); + + switch (control) { + case OverState::Close: Assert(index < 1); return { + 0, + &st::pipCloseIcon, + &st::pipCloseIconOver, + }; + case OverState::Enlarge: Assert(index < 1); return { + 1, + &st::pipEnlargeIcon, + &st::pipEnlargeIconOver, + }; + case OverState::VolumeToggle: Assert(index < 3); return { + (2 + index), + (index == 0 + ? &st::pipVolumeIcon0 + : (index == 1) + ? &st::pipVolumeIcon1 + : &st::pipVolumeIcon2), + (index == 0 + ? &st::pipVolumeIcon0Over + : (index == 1) + ? &st::pipVolumeIcon1Over + : &st::pipVolumeIcon2Over), + }; + case OverState::Other: Assert(index < 2); return { + (5 + index), + (index ? &st::pipPauseIcon : &st::pipPlayIcon), + (index ? &st::pipPauseIconOver : &st::pipPlayIconOver), + }; + } + Unexpected("Control value in Pip::RendererGL::ControlIndex."); +} + +void Pip::RendererGL::validateControls() { + if (!_controlsImage.image().isNull()) { + return; + } + const auto metas = { + ControlMeta(OverState::Close), + ControlMeta(OverState::Enlarge), + ControlMeta(OverState::VolumeToggle), + ControlMeta(OverState::VolumeToggle, 1), + ControlMeta(OverState::VolumeToggle, 2), + ControlMeta(OverState::Other), + ControlMeta(OverState::Other, 1), + }; + auto maxWidth = 0; + auto fullHeight = 0; + for (const auto meta : metas) { + Assert(meta.icon->size() == meta.iconOver->size()); + maxWidth = std::max(meta.icon->width(), maxWidth); + fullHeight += 2 * meta.icon->height(); + } + auto image = QImage( + QSize(maxWidth, fullHeight) * _factor, + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + image.setDevicePixelRatio(_factor); + { + auto p = QPainter(&image); + auto index = 0; + auto height = 0; + const auto paint = [&](not_null icon) { + icon->paint(p, 0, height, maxWidth); + _controlsTextures[index++] = QRect( + QPoint(0, height) * _factor, + icon->size() * _factor); + height += icon->height(); + }; + for (const auto meta : metas) { + paint(meta.icon); + paint(meta.iconOver); + } + } + _controlsImage.setImage(std::move(image)); +} + +void Pip::RendererGL::invalidateControls() { + _controlsImage.invalidate(); + ranges::fill(_controlsTextures, QRect()); +} + +void Pip::RendererGL::paintUsingRaster( + Ui::GL::Image &image, + QRect rect, + Fn method, + int bufferOffset, + bool transparent) { + auto raster = image.takeImage(); + const auto size = rect.size() * _factor; + if (raster.width() < size.width() || raster.height() < size.height()) { + raster = QImage(size, QImage::Format_ARGB32_Premultiplied); + raster.setDevicePixelRatio(_factor); + if (!transparent + && (raster.width() > size.width() + || raster.height() > size.height())) { + raster.fill(Qt::transparent); + } + } else if (raster.devicePixelRatio() != _factor) { + raster.setDevicePixelRatio(_factor); + } + + if (transparent) { + raster.fill(Qt::transparent); + } + method(Painter(&raster)); + + _f->glActiveTexture(GL_TEXTURE0); + + image.setImage(std::move(raster), size); + image.bind(*_f); + + const auto textured = image.texturedRect(rect, QRect(QPoint(), size)); + const auto geometry = transformRect(textured.geometry); + const GLfloat coords[] = { + geometry.left(), geometry.top(), + textured.texture.left(), textured.texture.bottom(), + + geometry.right(), geometry.top(), + textured.texture.right(), textured.texture.bottom(), + + geometry.right(), geometry.bottom(), + textured.texture.right(), textured.texture.top(), + + geometry.left(), geometry.bottom(), + textured.texture.left(), textured.texture.top(), + }; + _contentBuffer->write( + bufferOffset * 4 * sizeof(GLfloat), + coords, + sizeof(coords)); + + _imageProgram->bind(); + _imageProgram->setUniformValue("viewport", _uniformViewport); + _imageProgram->setUniformValue("s_texture", GLint(0)); + _imageProgram->setUniformValue("g_opacity", GLfloat(1)); + + toggleBlending(transparent); + FillTexturedRectangle(*_f, &*_imageProgram, bufferOffset); +} + +void Pip::RendererGL::toggleBlending(bool enabled) { + if (_blendingEnabled == enabled) { + return; + } else if (enabled) { + _f->glEnable(GL_BLEND); + _f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } else { + _f->glDisable(GL_BLEND); + } + _blendingEnabled = enabled; +} + +QRect Pip::RendererGL::RoundingRect(ContentGeometry geometry) { + const auto inner = geometry.inner; + const auto attached = geometry.attached; + const auto added = std::max({ + st::roundRadiusLarge, + inner.x(), + inner.y(), + geometry.outer.width() - inner.x() - inner.width(), + geometry.outer.height() - inner.y() - inner.height(), + st::callShadow.topLeft.width(), + st::callShadow.topLeft.height(), + st::callShadow.topRight.width(), + st::callShadow.topRight.height(), + st::callShadow.bottomRight.width(), + st::callShadow.bottomRight.height(), + st::callShadow.bottomLeft.width(), + st::callShadow.bottomLeft.height(), + }); + return geometry.inner.marginsAdded({ + (attached & RectPart::Left) ? added : 0, + (attached & RectPart::Top) ? added : 0, + (attached & RectPart::Right) ? added : 0, + (attached & RectPart::Bottom) ? added : 0, + }); +} + +Rect Pip::RendererGL::transformRect(const Rect &raster) const { + return TransformRect(raster, _viewport, _factor); +} + +Rect Pip::RendererGL::transformRect(const QRectF &raster) const { + return TransformRect(raster, _viewport, _factor); +} + +Rect Pip::RendererGL::transformRect(const QRect &raster) const { + return TransformRect(Rect(raster), _viewport, _factor); +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_pip_opengl.h b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h new file mode 100644 index 000000000..d9d06154a --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_pip_opengl.h @@ -0,0 +1,129 @@ +/* +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 "media/view/media_view_pip_renderer.h" +#include "ui/gl/gl_image.h" +#include "ui/gl/gl_primitives.h" + +#include + +namespace Media::View { + +class Pip::RendererGL final : public Pip::Renderer { +public: + explicit RendererGL(not_null owner); + + void init( + not_null widget, + QOpenGLFunctions &f) override; + + void deinit( + not_null widget, + QOpenGLFunctions &f) override; + + void paint( + not_null widget, + QOpenGLFunctions &f) override; + + std::optional clearColor() override; + +private: + struct Control { + int index = -1; + not_null icon; + not_null iconOver; + }; + void createShadowTexture(); + + void paintTransformedVideoFrame(ContentGeometry geometry) override; + void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry) override; + void paintTransformedContent( + not_null program, + ContentGeometry geometry); + void paintRadialLoading( + QRect inner, + float64 controlsShown) override; + void paintButtonsStart() override; + void paintButton( + const Button &button, + int outerWidth, + float64 shown, + float64 over, + const style::icon &icon, + const style::icon &iconOver) override; + void paintPlayback(QRect outer, float64 shown) override; + void paintVolumeController(QRect outer, float64 shown) override; + + void paintUsingRaster( + Ui::GL::Image &image, + QRect rect, + Fn method, + int bufferOffset, + bool transparent = false); + + void validateControls(); + void invalidateControls(); + void toggleBlending(bool enabled); + + [[nodiscard]] QRect RoundingRect(ContentGeometry geometry); + + [[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const; + [[nodiscard]] Ui::GL::Rect transformRect( + const Ui::GL::Rect &raster) const; + + void uploadTexture( + GLint internalformat, + GLint format, + QSize size, + QSize hasSize, + int stride, + const void *data) const; + + const not_null _owner; + + QOpenGLFunctions *_f = nullptr; + QSize _viewport; + float _factor = 1.; + QVector2D _uniformViewport; + + std::optional _contentBuffer; + std::optional _imageProgram; + std::optional _controlsProgram; + QOpenGLShader *_texturedVertexShader = nullptr; + std::optional _argb32Program; + std::optional _yuv420Program; + Ui::GL::Textures<4> _textures; + QSize _rgbaSize; + QSize _lumaSize; + QSize _chromaSize; + quint64 _cacheKey = 0; + int _trackFrameIndex = 0; + + Ui::GL::Image _radialImage; + Ui::GL::Image _controlsImage; + Ui::GL::Image _playbackImage; + Ui::GL::Image _volumeControllerImage; + Ui::GL::Image _shadowImage; + + static constexpr auto kControlsCount = 7; + [[nodiscard]] static Control ControlMeta( + OverState control, + int index = 0); + std::array _controlsTextures; + + bool _blendingEnabled = false; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_pip_raster.cpp b/Telegram/SourceFiles/media/view/media_view_pip_raster.cpp new file mode 100644 index 000000000..685850180 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_pip_raster.cpp @@ -0,0 +1,246 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/view/media_view_pip_raster.h" + +#include "ui/image/image_prepare.h" +#include "ui/widgets/shadow.h" +#include "styles/style_calls.h" // st::callShadow. + +namespace Media::View { +namespace { + +[[nodiscard]] Streaming::FrameRequest UnrotateRequest( + const Streaming::FrameRequest &request, + int rotation) { + if (!rotation) { + return request; + } + const auto unrotatedCorner = [&](RectPart corner) { + if (!(request.corners & corner)) { + return RectPart(0); + } + switch (corner) { + case RectPart::TopLeft: + return (rotation == 90) + ? RectPart::BottomLeft + : (rotation == 180) + ? RectPart::BottomRight + : RectPart::TopRight; + case RectPart::TopRight: + return (rotation == 90) + ? RectPart::TopLeft + : (rotation == 180) + ? RectPart::BottomLeft + : RectPart::BottomRight; + case RectPart::BottomRight: + return (rotation == 90) + ? RectPart::TopRight + : (rotation == 180) + ? RectPart::TopLeft + : RectPart::BottomLeft; + case RectPart::BottomLeft: + return (rotation == 90) + ? RectPart::BottomRight + : (rotation == 180) + ? RectPart::TopRight + : RectPart::TopLeft; + } + Unexpected("Corner in rotateCorner."); + }; + auto result = request; + result.outer = FlipSizeByRotation(request.outer, rotation); + result.resize = FlipSizeByRotation(request.resize, rotation); + result.corners = unrotatedCorner(RectPart::TopLeft) + | unrotatedCorner(RectPart::TopRight) + | unrotatedCorner(RectPart::BottomRight) + | unrotatedCorner(RectPart::BottomLeft); + return result; +} + +} // namespace + +Pip::RendererSW::RendererSW(not_null owner) +: _owner(owner) +, _roundRect(ImageRoundRadius::Large, st::radialBg) { +} + +void Pip::RendererSW::paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { + _p = &p; + _clip = &clip; + _clipOuter = clip.boundingRect(); + _owner->paint(this); + _p = nullptr; + _clip = nullptr; +} + +void Pip::RendererSW::paintTransformedVideoFrame( + ContentGeometry geometry) { + paintTransformedImage( + _owner->videoFrame(frameRequest(geometry)), + geometry); +} + +void Pip::RendererSW::paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry) { + paintTransformedImage( + staticContentByRequest(image, frameRequest(geometry)), + geometry); +} + +void Pip::RendererSW::paintFade(ContentGeometry geometry) const { + using Part = RectPart; + const auto sides = geometry.attached; + const auto rounded = RectPart(0) + | ((sides & (Part::Top | Part::Left)) ? Part(0) : Part::TopLeft) + | ((sides & (Part::Top | Part::Right)) ? Part(0) : Part::TopRight) + | ((sides & (Part::Bottom | Part::Right)) + ? Part(0) + : Part::BottomRight) + | ((sides & (Part::Bottom | Part::Left)) + ? Part(0) + : Part::BottomLeft); + _roundRect.paintSomeRounded( + *_p, + geometry.inner, + rounded | Part::NoTopBottom | Part::Top | Part::Bottom); +} + +void Pip::RendererSW::paintButtonsStart() { +} + +void Pip::RendererSW::paintButton( + const Button &button, + int outerWidth, + float64 shown, + float64 over, + const style::icon &icon, + const style::icon &iconOver) { + if (over < 1.) { + _p->setOpacity(shown); + icon.paint(*_p, button.icon.x(), button.icon.y(), outerWidth); + } + if (over > 0.) { + _p->setOpacity(over * shown); + iconOver.paint(*_p, button.icon.x(), button.icon.y(), outerWidth); + } +} + +Pip::FrameRequest Pip::RendererSW::frameRequest( + ContentGeometry geometry) const { + auto result = FrameRequest(); + result.outer = geometry.inner.size() * style::DevicePixelRatio(); + result.resize = result.outer; + result.corners = RectPart(0) + | ((geometry.attached & (RectPart::Left | RectPart::Top)) + ? RectPart(0) + : RectPart::TopLeft) + | ((geometry.attached & (RectPart::Top | RectPart::Right)) + ? RectPart(0) + : RectPart::TopRight) + | ((geometry.attached & (RectPart::Right | RectPart::Bottom)) + ? RectPart(0) + : RectPart::BottomRight) + | ((geometry.attached & (RectPart::Bottom | RectPart::Left)) + ? RectPart(0) + : RectPart::BottomLeft); + result.radius = ImageRoundRadius::Large; + return UnrotateRequest(result, geometry.rotation); +} + +QImage Pip::RendererSW::staticContentByRequest( + const QImage &image, + const FrameRequest &request) { + if (request.resize.isEmpty()) { + return QImage(); + } else if (!_preparedStaticContent.isNull() + && _preparedStaticRequest == request + && image.cacheKey() == _preparedStaticKey) { + return _preparedStaticContent; + } + _preparedStaticKey = image.cacheKey(); + _preparedStaticRequest = request; + //_preparedCoverStorage = Streaming::PrepareByRequest( + // _instance.info().video.cover, + // false, + // _instance.info().video.rotation, + // request, + // std::move(_preparedCoverStorage)); + using Option = Images::Option; + const auto options = Option::Smooth + | Option::RoundedLarge + | ((request.corners & RectPart::TopLeft) + ? Option::RoundedTopLeft + : Option(0)) + | ((request.corners & RectPart::TopRight) + ? Option::RoundedTopRight + : Option(0)) + | ((request.corners & RectPart::BottomRight) + ? Option::RoundedBottomRight + : Option(0)) + | ((request.corners & RectPart::BottomLeft) + ? Option::RoundedBottomLeft + : Option(0)); + _preparedStaticContent = Images::prepare( + image, + request.resize.width(), + request.resize.height(), + options, + request.outer.width(), + request.outer.height()); + return _preparedStaticContent; +} + +void Pip::RendererSW::paintTransformedImage( + const QImage &image, + ContentGeometry geometry) { + const auto rect = geometry.inner; + const auto rotation = geometry.rotation; + if (geometry.useTransparency) { + const auto sides = RectPart::AllSides & ~geometry.attached; + Ui::Shadow::paint(*_p, rect, geometry.outer.width(), st::callShadow); + } + + if (UsePainterRotation(rotation)) { + if (rotation) { + _p->save(); + _p->rotate(rotation); + } + PainterHighQualityEnabler hq(*_p); + _p->drawImage(RotatedRect(rect, rotation), image); + if (rotation) { + _p->restore(); + } + } else { + _p->drawImage(rect, RotateFrameImage(image, rotation)); + } + + if (geometry.fade > 0) { + _p->setOpacity(geometry.fade); + paintFade(geometry); + } +} + +void Pip::RendererSW::paintRadialLoading( + QRect inner, + float64 controlsShown) { + _owner->paintRadialLoadingContent(*_p, inner, st::radialFg->c); +} + +void Pip::RendererSW::paintPlayback(QRect outer, float64 shown) { + _owner->paintPlaybackContent(*_p, outer, shown); +} + +void Pip::RendererSW::paintVolumeController(QRect outer, float64 shown) { + _owner->paintVolumeControllerContent(*_p, outer, shown); +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_pip_raster.h b/Telegram/SourceFiles/media/view/media_view_pip_raster.h new file mode 100644 index 000000000..d24513c43 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_pip_raster.h @@ -0,0 +1,66 @@ +/* +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 "media/view/media_view_pip_renderer.h" + +namespace Media::View { + +class Pip::RendererSW final : public Pip::Renderer { +public: + explicit RendererSW(not_null owner); + + void paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) override; + +private: + void paintTransformedVideoFrame(ContentGeometry geometry) override; + void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry) override; + void paintTransformedImage( + const QImage &image, + ContentGeometry geometry); + void paintRadialLoading( + QRect inner, + float64 controlsShown) override; + void paintButtonsStart() override; + void paintButton( + const Button &button, + int outerWidth, + float64 shown, + float64 over, + const style::icon &icon, + const style::icon &iconOver) override; + void paintPlayback(QRect outer, float64 shown) override; + void paintVolumeController(QRect outer, float64 shown) override; + + void paintFade(ContentGeometry geometry) const; + + [[nodiscard]] FrameRequest frameRequest(ContentGeometry geometry) const; + [[nodiscard]] QImage staticContentByRequest( + const QImage &image, + const FrameRequest &request); + + const not_null _owner; + + Painter *_p = nullptr; + const QRegion *_clip = nullptr; + QRect _clipOuter; + + Ui::RoundRect _roundRect; + + QImage _preparedStaticContent; + FrameRequest _preparedStaticRequest; + qint64 _preparedStaticKey = 0; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_pip_renderer.h b/Telegram/SourceFiles/media/view/media_view_pip_renderer.h new file mode 100644 index 000000000..d73ccd1a2 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_pip_renderer.h @@ -0,0 +1,37 @@ +/* +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 "media/view/media_view_pip.h" +#include "ui/gl/gl_surface.h" + +namespace Media::View { + +class Pip::Renderer : public Ui::GL::Renderer { +public: + virtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0; + virtual void paintTransformedStaticContent( + const QImage &image, + ContentGeometry geometry) = 0; + virtual void paintRadialLoading( + QRect inner, + float64 controlsShown) = 0; + virtual void paintButtonsStart() = 0; + virtual void paintButton( + const Button &button, + int outerWidth, + float64 shown, + float64 over, + const style::icon &icon, + const style::icon &iconOver) = 0; + virtual void paintPlayback(QRect outer, float64 shown) = 0; + virtual void paintVolumeController(QRect outer, float64 shown) = 0; + +}; + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp index 09e292878..b9416bc94 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.cpp @@ -172,7 +172,8 @@ MenuSpeedItem::MenuSpeedItem( } float64 MenuSpeedItem::computeSpeed(float64 value) const { - return anim::interpolate(kMinSpeed, kMaxSpeed, value) / 100.; + return anim::interpolate(kMinSpeed, kMaxSpeed, std::clamp(value, 0., 1.)) + / 100.; } QString MenuSpeedItem::speedString(float64 value) const { diff --git a/Telegram/SourceFiles/mtproto/config_loader.cpp b/Telegram/SourceFiles/mtproto/config_loader.cpp index 0177a4019..d9f55fac8 100644 --- a/Telegram/SourceFiles/mtproto/config_loader.cpp +++ b/Telegram/SourceFiles/mtproto/config_loader.cpp @@ -13,7 +13,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_dc_options.h" #include "mtproto/mtproto_config.h" #include "mtproto/mtp_instance.h" -#include "facades.h" namespace MTP { namespace details { @@ -28,9 +27,11 @@ ConfigLoader::ConfigLoader( not_null instance, const QString &phone, Fn onDone, - FailHandler onFail) + FailHandler onFail, + bool proxyEnabled) : _instance(instance) , _phone(phone) +, _proxyEnabled(proxyEnabled) , _doneHandler(onDone) , _failHandler(onFail) { _enumDCTimer.setCallback([this] { enumerate(); }); @@ -115,7 +116,7 @@ void ConfigLoader::enumerate() { } void ConfigLoader::refreshSpecialLoader() { - if (Global::ProxySettings() == ProxyData::Settings::Enabled) { + if (_proxyEnabled) { _specialLoader.reset(); return; } @@ -174,7 +175,7 @@ void ConfigLoader::addSpecialEndpoint( void ConfigLoader::sendSpecialRequest() { terminateSpecialRequest(); - if (Global::ProxySettings() == ProxyData::Settings::Enabled) { + if (_proxyEnabled) { _specialLoader.reset(); return; } @@ -233,5 +234,9 @@ void ConfigLoader::specialConfigLoaded(const MTPConfig &result) { _instance->dcOptions().setFromList(data.vdc_options()); } +void ConfigLoader::setProxyEnabled(bool value) { + _proxyEnabled = value; +} + } // namespace details } // namespace MTP diff --git a/Telegram/SourceFiles/mtproto/config_loader.h b/Telegram/SourceFiles/mtproto/config_loader.h index 9a0feb915..13a80b1b0 100644 --- a/Telegram/SourceFiles/mtproto/config_loader.h +++ b/Telegram/SourceFiles/mtproto/config_loader.h @@ -26,11 +26,13 @@ public: not_null instance, const QString &phone, Fn onDone, - FailHandler onFail); + FailHandler onFail, + bool proxyEnabled); ~ConfigLoader(); void load(); void setPhone(const QString &phone); + void setProxyEnabled(bool value); private: mtpRequestId sendRequest(ShiftedDcId shiftedDcId); @@ -67,6 +69,7 @@ private: DcId _specialEnumCurrent = 0; mtpRequestId _specialEnumRequest = 0; QString _phone; + bool _proxyEnabled = false; Fn _doneHandler; FailHandler _failHandler; diff --git a/Telegram/SourceFiles/mtproto/connection_abstract.cpp b/Telegram/SourceFiles/mtproto/connection_abstract.cpp index cf2f3bdb0..72f0e063a 100644 --- a/Telegram/SourceFiles/mtproto/connection_abstract.cpp +++ b/Telegram/SourceFiles/mtproto/connection_abstract.cpp @@ -189,9 +189,7 @@ ConnectionPointer AbstractConnection::Create( } uint32 AbstractConnection::extendedNotSecurePadding() const { - return requiresExtendedPadding() - ? uint32(openssl::RandomValue() & 0x3F) - : 0; + return uint32(openssl::RandomValue() & 0x3F); } } // namespace details diff --git a/Telegram/SourceFiles/mtproto/connection_abstract.h b/Telegram/SourceFiles/mtproto/connection_abstract.h index 926c818a7..a262206bc 100644 --- a/Telegram/SourceFiles/mtproto/connection_abstract.h +++ b/Telegram/SourceFiles/mtproto/connection_abstract.h @@ -81,7 +81,8 @@ public: const QString &ip, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) = 0; + int16 protocolDcId, + bool protocolForFiles) = 0; virtual void timedOut() { } [[nodiscard]] virtual bool isConnected() const = 0; @@ -91,9 +92,6 @@ public: [[nodiscard]] virtual bool needHttpWait() { return false; } - [[nodiscard]] virtual bool requiresExtendedPadding() const { - return false; - } [[nodiscard]] virtual int32 debugState() const = 0; diff --git a/Telegram/SourceFiles/mtproto/connection_http.cpp b/Telegram/SourceFiles/mtproto/connection_http.cpp index ee0d4756f..bebdb4e33 100644 --- a/Telegram/SourceFiles/mtproto/connection_http.cpp +++ b/Telegram/SourceFiles/mtproto/connection_http.cpp @@ -67,7 +67,8 @@ void HttpConnection::connectToServer( const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) { + int16 protocolDcId, + bool protocolForFiles) { _address = address; connect( &_manager, @@ -122,7 +123,13 @@ qint32 HttpConnection::handleError(QNetworkReply *reply) { // returnes "maybe ba case QNetworkReply::TemporaryNetworkFailureError: case QNetworkReply::NetworkSessionFailedError: case QNetworkReply::BackgroundRequestNotAllowedError: - case QNetworkReply::UnknownNetworkError: LOG(("HTTP Error: network error %1 - %2").arg(reply->error()).arg(reply->errorString())); break; + case QNetworkReply::UnknownNetworkError: + if (reply->error() == QNetworkReply::UnknownNetworkError) { + DEBUG_LOG(("HTTP Error: network error %1 - %2").arg(reply->error()).arg(reply->errorString())); + } else { + LOG(("HTTP Error: network error %1 - %2").arg(reply->error()).arg(reply->errorString())); + } + break; // proxy errors (101-199): case QNetworkReply::ProxyConnectionRefusedError: diff --git a/Telegram/SourceFiles/mtproto/connection_http.h b/Telegram/SourceFiles/mtproto/connection_http.h index 8fb867aa5..bbc7a44a7 100644 --- a/Telegram/SourceFiles/mtproto/connection_http.h +++ b/Telegram/SourceFiles/mtproto/connection_http.h @@ -29,7 +29,8 @@ public: const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) override; + int16 protocolDcId, + bool protocolForFiles) override; bool isConnected() const override; bool usingHttpWait() override; bool needHttpWait() override; @@ -40,7 +41,7 @@ public: QString tag() const override; static mtpBuffer handleResponse(QNetworkReply *reply); - static qint32 handleError(QNetworkReply *reply); // returnes error code + static qint32 handleError(QNetworkReply *reply); // Returns error code. private: QUrl url() const; diff --git a/Telegram/SourceFiles/mtproto/connection_resolving.cpp b/Telegram/SourceFiles/mtproto/connection_resolving.cpp index 392a311a3..ae4b6f9e5 100644 --- a/Telegram/SourceFiles/mtproto/connection_resolving.cpp +++ b/Telegram/SourceFiles/mtproto/connection_resolving.cpp @@ -83,7 +83,8 @@ void ResolvingConnection::setChild(ConnectionPointer &&child) { _address, _port, _protocolSecret, - _protocolDcId); + _protocolDcId, + _protocolForFiles); } } @@ -197,12 +198,6 @@ void ResolvingConnection::sendData(mtpBuffer &&buffer) { _child->sendData(std::move(buffer)); } -bool ResolvingConnection::requiresExtendedPadding() const { - Expects(_child != nullptr); - - return _child->requiresExtendedPadding(); -} - void ResolvingConnection::disconnectFromServer() { _address = QString(); _port = 0; @@ -218,7 +213,8 @@ void ResolvingConnection::connectToServer( const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) { + int16 protocolDcId, + bool protocolForFiles) { if (!_child) { InvokeQueued(this, [=] { emitError(kErrorCodeOther); }); return; @@ -227,6 +223,7 @@ void ResolvingConnection::connectToServer( _port = port; _protocolSecret = protocolSecret; _protocolDcId = protocolDcId; + _protocolForFiles = protocolForFiles; DEBUG_LOG(("Resolving Info: dc:%1 proxy '%2' connects a child '%3'").arg( QString::number(_protocolDcId), _proxy.host +':' + QString::number(_proxy.port), @@ -237,7 +234,8 @@ void ResolvingConnection::connectToServer( address, port, protocolSecret, - protocolDcId); + protocolDcId, + protocolForFiles); } bool ResolvingConnection::isConnected() const { diff --git a/Telegram/SourceFiles/mtproto/connection_resolving.h b/Telegram/SourceFiles/mtproto/connection_resolving.h index eeb137113..82d58bed4 100644 --- a/Telegram/SourceFiles/mtproto/connection_resolving.h +++ b/Telegram/SourceFiles/mtproto/connection_resolving.h @@ -32,9 +32,9 @@ public: const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) override; + int16 protocolDcId, + bool protocolForFiles) override; bool isConnected() const override; - bool requiresExtendedPadding() const override; int32 debugState() const override; @@ -63,6 +63,7 @@ private: int _port = 0; bytes::vector _protocolSecret; int16 _protocolDcId = 0; + bool _protocolForFiles = false; base::Timer _timeoutTimer; }; diff --git a/Telegram/SourceFiles/mtproto/connection_tcp.cpp b/Telegram/SourceFiles/mtproto/connection_tcp.cpp index 06838b3a8..4e37af10a 100644 --- a/Telegram/SourceFiles/mtproto/connection_tcp.cpp +++ b/Telegram/SourceFiles/mtproto/connection_tcp.cpp @@ -35,7 +35,6 @@ public: virtual uint32 id() const = 0; virtual bool supportsArbitraryLength() const = 0; - virtual bool requiresExtendedPadding() const = 0; virtual void prepareKey(bytes::span key, bytes::const_span source) = 0; virtual bytes::span finalizePacket(mtpBuffer &buffer) = 0; @@ -58,7 +57,6 @@ public: uint32 id() const override; bool supportsArbitraryLength() const override; - bool requiresExtendedPadding() const override; void prepareKey(bytes::span key, bytes::const_span source) override; bytes::span finalizePacket(mtpBuffer &buffer) override; @@ -75,10 +73,6 @@ bool TcpConnection::Protocol::Version0::supportsArbitraryLength() const { return false; } -bool TcpConnection::Protocol::Version0::requiresExtendedPadding() const { - return false; -} - void TcpConnection::Protocol::Version0::prepareKey( bytes::span key, bytes::const_span source) { @@ -142,7 +136,6 @@ class TcpConnection::Protocol::Version1 : public Version0 { public: explicit Version1(bytes::vector &&secret); - bool requiresExtendedPadding() const override; void prepareKey(bytes::span key, bytes::const_span source) override; private: @@ -154,10 +147,6 @@ TcpConnection::Protocol::Version1::Version1(bytes::vector &&secret) : _secret(std::move(secret)) { } -bool TcpConnection::Protocol::Version1::requiresExtendedPadding() const { - return true; -} - void TcpConnection::Protocol::Version1::prepareKey( bytes::span key, bytes::const_span source) { @@ -425,12 +414,6 @@ void TcpConnection::socketDisconnected() { } } -bool TcpConnection::requiresExtendedPadding() const { - Expects(_protocol != nullptr); - - return _protocol->requiresExtendedPadding(); -} - void TcpConnection::sendData(mtpBuffer &&buffer) { Expects(buffer.size() > 2); @@ -512,7 +495,8 @@ void TcpConnection::connectToServer( const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) { + int16 protocolDcId, + bool protocolForFiles) { Expects(_address.isEmpty()); Expects(_port == 0); Expects(_protocol == nullptr); @@ -543,7 +527,8 @@ void TcpConnection::connectToServer( _socket = AbstractSocket::Create( thread(), secret, - ToNetworkProxy(_proxy)); + ToNetworkProxy(_proxy), + protocolForFiles); _protocolDcId = protocolDcId; _socket->connected( diff --git a/Telegram/SourceFiles/mtproto/connection_tcp.h b/Telegram/SourceFiles/mtproto/connection_tcp.h index 1fc5319a1..a13ff0159 100644 --- a/Telegram/SourceFiles/mtproto/connection_tcp.h +++ b/Telegram/SourceFiles/mtproto/connection_tcp.h @@ -32,10 +32,10 @@ public: const QString &address, int port, const bytes::vector &protocolSecret, - int16 protocolDcId) override; + int16 protocolDcId, + bool protocolForFiles) override; void timedOut() override; bool isConnected() const override; - bool requiresExtendedPadding() const override; int32 debugState() const override; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.cpp index 2ad0a44a6..055b5c256 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.cpp @@ -15,11 +15,16 @@ namespace MTP::details { std::unique_ptr AbstractSocket::Create( not_null thread, const bytes::vector &secret, - const QNetworkProxy &proxy) { + const QNetworkProxy &proxy, + bool protocolForFiles) { if (secret.size() >= 21 && secret[0] == bytes::type(0xEE)) { - return std::make_unique(thread, secret, proxy); + return std::make_unique( + thread, + secret, + proxy, + protocolForFiles); } else { - return std::make_unique(thread, proxy); + return std::make_unique(thread, proxy, protocolForFiles); } } diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.h b/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.h index 61da8b5e3..ce1431fff 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.h +++ b/Telegram/SourceFiles/mtproto/details/mtproto_abstract_socket.h @@ -17,7 +17,8 @@ public: static std::unique_ptr Create( not_null thread, const bytes::vector &secret, - const QNetworkProxy &proxy); + const QNetworkProxy &proxy, + bool protocolForFiles); explicit AbstractSocket(not_null thread) { moveToThread(thread); @@ -53,6 +54,9 @@ public: virtual int32 debugState() = 0; protected: + static const int kFilesSendBufferSize = 2 * 1024 * 1024; + static const int kFilesReceiveBufferSize = 2 * 1024 * 1024; + rpl::event_stream<> _connected; rpl::event_stream<> _disconnected; rpl::event_stream<> _readyRead; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.cpp index 0f6b0b56c..db7a06ce8 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_dc_key_binder.cpp @@ -25,7 +25,7 @@ namespace { auto serialized = SerializedRequest::Serialize(data); serialized.setMsgId(realMsgId); serialized.setSeqNo(0); - serialized.addPadding(false, true); + serialized.addPadding(true); constexpr auto kMsgIdPosition = SerializedRequest::kMessageIdPosition; constexpr auto kMinMessageSize = 5; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 8d6eb07ac..c28e1335d 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,13 +65,17 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/89.0.4389.90 Safari/537.36"); + "Chrome/90.0.4430.85 Safari/537.36"); return kResult; } std::vector ParseDnsResponse( const QByteArray &bytes, std::optional typeRestriction) { + if (bytes.isEmpty()) { + return {}; + } + // Read and store to "result" all the data bytes from the response: // { .., // "Answer": [ @@ -339,7 +343,7 @@ QByteArray DomainResolver::finalizeRequest( const AttemptKey &key, not_null reply) { if (reply->error() != QNetworkReply::NoError) { - LOG(("Resolve Error: Failed to get response, error: %2 (%3)" + DEBUG_LOG(("Resolve Error: Failed to get response, error: %2 (%3)" ).arg(reply->errorString() ).arg(reply->error())); } diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.cpp index 8d47ed822..a053a18ac 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_rsa_public_key.cpp @@ -54,6 +54,12 @@ enum class Format { Unknown, }; +struct BIODeleter { + void operator()(BIO *value) { + BIO_free(value); + } +}; + Format GuessFormat(bytes::const_span key) { const auto array = QByteArray::fromRawData( reinterpret_cast(key.data()), @@ -68,14 +74,16 @@ Format GuessFormat(bytes::const_span key) { RSA *CreateRaw(bytes::const_span key) { const auto format = GuessFormat(key); - const auto bio = BIO_new_mem_buf( - const_cast(key.data()), - key.size()); + const auto bio = std::unique_ptr{ + BIO_new_mem_buf( + const_cast(key.data()), + key.size()), + }; switch (format) { case Format::RSAPublicKey: - return PEM_read_bio_RSAPublicKey(bio, nullptr, nullptr, nullptr); + return PEM_read_bio_RSAPublicKey(bio.get(), nullptr, nullptr, nullptr); case Format::RSA_PUBKEY: - return PEM_read_bio_RSA_PUBKEY(bio, nullptr, nullptr, nullptr); + return PEM_read_bio_RSA_PUBKEY(bio.get(), nullptr, nullptr, nullptr); } Unexpected("format in RSAPublicKey::Private::Create."); } diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.cpp index 5b04acafb..3b3032281 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.cpp @@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace MTP::details { namespace { -uint32 CountPaddingPrimesCount(uint32 requestSize, bool extended, bool old) { - if (old) { +uint32 CountPaddingPrimesCount( + uint32 requestSize, + bool forAuthKeyInner) { + if (forAuthKeyInner) { return ((8 + requestSize) & 0x03) ? (4 - ((8 + requestSize) & 0x03)) : 0; @@ -27,12 +29,8 @@ uint32 CountPaddingPrimesCount(uint32 requestSize, bool extended, bool old) { result += 4; } - if (extended) { - // Some more random padding. - result += ((openssl::RandomValue() & 0x0F) << 2); - } - - return result; + // Some more random padding. + return result + ((openssl::RandomValue() & 0x0F) << 2); } } // namespace @@ -100,12 +98,14 @@ uint32 SerializedRequest::getSeqNo() const { return uint32((*_data)[kSeqNoPosition]); } -void SerializedRequest::addPadding(bool extended, bool old) { +void SerializedRequest::addPadding(bool forAuthKeyInner) { Expects(_data != nullptr); Expects(_data->size() > kMessageBodyPosition); const auto requestSize = (tl::count_length(*this) >> 2); - const auto padding = CountPaddingPrimesCount(requestSize, extended, old); + const auto padding = CountPaddingPrimesCount( + requestSize, + forAuthKeyInner); const auto fullSize = kMessageBodyPosition + requestSize + padding; if (uint32(_data->size()) != fullSize) { _data->resize(fullSize); diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.h b/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.h index ad9901b2b..7d8d42605 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.h +++ b/Telegram/SourceFiles/mtproto/details/mtproto_serialized_request.h @@ -65,7 +65,7 @@ public: void setSeqNo(uint32 seqNo); [[nodiscard]] uint32 getSeqNo() const; - void addPadding(bool extended, bool old); + void addPadding(bool forAuthKeyInner); [[nodiscard]] uint32 messageSize() const; [[nodiscard]] bool needAck() const; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.cpp index aec382f3d..ef9f2f327 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.cpp @@ -12,10 +12,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace MTP::details { -TcpSocket::TcpSocket(not_null thread, const QNetworkProxy &proxy) +TcpSocket::TcpSocket( + not_null thread, + const QNetworkProxy &proxy, + bool protocolForFiles) : AbstractSocket(thread) { _socket.moveToThread(thread); _socket.setProxy(proxy); + if (protocolForFiles) { + _socket.setSocketOption( + QAbstractSocket::SendBufferSizeSocketOption, + kFilesSendBufferSize); + _socket.setSocketOption( + QAbstractSocket::ReceiveBufferSizeSocketOption, + kFilesReceiveBufferSize); + } const auto wrap = [&](auto handler) { return [=](auto &&...args) { InvokeQueued(this, [=] { handler(args...); }); @@ -122,9 +133,9 @@ void TcpSocket::LogError(int errorCode, const QString &errorText) { LOG(("TCP Error: socket timeout - %1").arg(errorText)); break; - case QAbstractSocket::NetworkError: - LOG(("TCP Error: network - %1").arg(errorText)); - break; + case QAbstractSocket::NetworkError: { + DEBUG_LOG(("TCP Error: network - %1").arg(errorText)); + } break; case QAbstractSocket::ProxyAuthenticationRequiredError: case QAbstractSocket::ProxyConnectionRefusedError: diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.h b/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.h index 6c8bac882..28e3c4665 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.h +++ b/Telegram/SourceFiles/mtproto/details/mtproto_tcp_socket.h @@ -13,7 +13,10 @@ namespace MTP::details { class TcpSocket final : public AbstractSocket { public: - TcpSocket(not_null thread, const QNetworkProxy &proxy); + TcpSocket( + not_null thread, + const QNetworkProxy &proxy, + bool protocolForFiles); void connectToHost(const QString &address, int port) override; bool isGoodStartNonce(bytes::const_span nonce) override; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp index 34030993a..962e0268b 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.cpp @@ -446,13 +446,22 @@ void ClientHelloGenerator::writeTimestamp() { TlsSocket::TlsSocket( not_null thread, const bytes::vector &secret, - const QNetworkProxy &proxy) + const QNetworkProxy &proxy, + bool protocolForFiles) : AbstractSocket(thread) , _secret(secret) { Expects(_secret.size() >= 21 && _secret[0] == bytes::type(0xEE)); _socket.moveToThread(thread); _socket.setProxy(proxy); + if (protocolForFiles) { + _socket.setSocketOption( + QAbstractSocket::SendBufferSizeSocketOption, + kFilesSendBufferSize); + _socket.setSocketOption( + QAbstractSocket::ReceiveBufferSizeSocketOption, + kFilesReceiveBufferSize); + } const auto wrap = [&](auto handler) { return [=](auto &&...args) { InvokeQueued(this, [=] { handler(args...); }); diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.h b/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.h index 01848230a..4bced8d75 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.h +++ b/Telegram/SourceFiles/mtproto/details/mtproto_tls_socket.h @@ -16,7 +16,8 @@ public: TlsSocket( not_null thread, const bytes::vector &secret, - const QNetworkProxy &proxy); + const QNetworkProxy &proxy, + bool protocolForFiles); void connectToHost(const QString &address, int port) override; bool isGoodStartNonce(bytes::const_span nonce) override; diff --git a/Telegram/SourceFiles/mtproto/mtp_instance.cpp b/Telegram/SourceFiles/mtproto/mtp_instance.cpp index 344e64ce7..795e1c729 100644 --- a/Telegram/SourceFiles/mtproto/mtp_instance.cpp +++ b/Telegram/SourceFiles/mtproto/mtp_instance.cpp @@ -20,13 +20,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_account.h" // Account::configUpdated. #include "apiwrap.h" #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_instance.h" #include "lang/lang_cloud_manager.h" #include "base/unixtime.h" #include "base/call_delayed.h" #include "base/timer.h" #include "base/network_reachability.h" -#include "facades.h" // Proxies list. namespace MTP { namespace { @@ -279,6 +279,8 @@ private: base::Timer _checkDelayedTimer; + Core::SettingsProxy &_proxySettings; + rpl::lifetime _lifetime; }; @@ -299,7 +301,8 @@ Instance::Private::Private( , _instance(instance) , _mode(mode) , _config(std::move(fields.config)) -, _networkReachability(base::NetworkReachability::Instance()) { +, _networkReachability(base::NetworkReachability::Instance()) +, _proxySettings(Core::App().settings().proxy()) { Expects(_config != nullptr); const auto idealThreadPoolSize = QThread::idealThreadCount(); @@ -338,6 +341,13 @@ Instance::Private::Private( _mainDcId = fields.mainDcId; _mainDcIdForced = true; } + + _proxySettings.connectionTypeChanges( + ) | rpl::start_with_next([=] { + if (_configLoader) { + _configLoader->setProxyEnabled(_proxySettings.isEnabled()); + } + }, _lifetime); } void Instance::Private::start() { @@ -397,11 +407,12 @@ void Instance::Private::applyDomainIps( } return true; }; - for (auto &proxy : Global::RefProxiesList()) { + for (auto &proxy : _proxySettings.list()) { applyToProxy(proxy); } - if (applyToProxy(Global::RefSelectedProxy()) - && (Global::ProxySettings() == ProxyData::Settings::Enabled)) { + auto selected = _proxySettings.selected(); + if (applyToProxy(selected) && _proxySettings.isEnabled()) { + _proxySettings.setSelected(selected); for (const auto &[shiftedDcId, session] : _sessions) { session->refreshOptions(); } @@ -427,11 +438,13 @@ void Instance::Private::setGoodProxyDomain( } return true; }; - for (auto &proxy : Global::RefProxiesList()) { + for (auto &proxy : _proxySettings.list()) { applyToProxy(proxy); } - if (applyToProxy(Global::RefSelectedProxy()) - && (Global::ProxySettings() == ProxyData::Settings::Enabled)) { + + auto selected = _proxySettings.selected(); + if (applyToProxy(selected) && _proxySettings.isEnabled()) { + _proxySettings.setSelected(selected); Core::App().refreshGlobalProxy(); } } @@ -473,7 +486,8 @@ void Instance::Private::requestConfig() { [=](const MTPConfig &result) { configLoadDone(result); }, [=](const Error &error, const Response &) { return configLoadFail(error); - }); + }, + _proxySettings.isEnabled()); _configLoader->load(); } @@ -1266,12 +1280,12 @@ bool Instance::Private::onErrorDefault( int breakpoint = 0; } auto badGuestDc = (code == 400) && (type == qsl("FILE_ID_INVALID")); - QRegularExpressionMatch m; - if ((m = QRegularExpression("^(FILE|PHONE|NETWORK|USER)_MIGRATE_(\\d+)$").match(type)).hasMatch()) { + QRegularExpressionMatch m1, m2; + if ((m1 = QRegularExpression("^(FILE|PHONE|NETWORK|USER)_MIGRATE_(\\d+)$").match(type)).hasMatch()) { if (!requestId) return false; auto dcWithShift = ShiftedDcId(0); - auto newdcWithShift = ShiftedDcId(m.captured(2).toInt()); + auto newdcWithShift = ShiftedDcId(m1.captured(2).toInt()); if (const auto shiftedDcId = queryRequestByDc(requestId)) { dcWithShift = *shiftedDcId; } else { @@ -1329,7 +1343,11 @@ bool Instance::Private::onErrorDefault( (dcWithShift < 0) ? -newdcWithShift : newdcWithShift); session->sendPrepared(request); return true; - } else if (code < 0 || code >= 500 || (m = QRegularExpression("^FLOOD_WAIT_(\\d+)$").match(type)).hasMatch()) { + } else if (code < 0 + || code >= 500 + || (m1 = QRegularExpression("^FLOOD_WAIT_(\\d+)$").match(type)).hasMatch() + || ((m2 = QRegularExpression("^SLOWMODE_WAIT_(\\d+)$").match(type)).hasMatch() + && m2.captured(1).toInt() < 3)) { if (!requestId) return false; int32 secs = 1; @@ -1340,9 +1358,11 @@ bool Instance::Private::onErrorDefault( } else { _requestsDelays.emplace(requestId, secs); } - } else { - secs = m.captured(1).toInt(); + } else if (m1.hasMatch()) { + secs = m1.captured(1).toInt(); // if (secs >= 60) return false; + } else if (m2.hasMatch()) { + secs = m2.captured(1).toInt(); } auto sendAt = crl::now() + secs * 1000 + 10; auto it = _delayedRequests.begin(), e = _delayedRequests.end(); diff --git a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp index cb401a589..1a031b561 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp @@ -30,9 +30,11 @@ namespace { [[nodiscard]] ProxyData::Status HexMtprotoPasswordStatus( const QString &password) { const auto size = password.size() / 2; + const auto type1 = password[0].toLower(); + const auto type2 = password[1].toLower(); const auto valid = (size == 16) - || (size == 17 && (password[0] == 'd') && (password[1] == 'd')) - || (size >= 21 && (password[0] == 'e') && (password[1] == 'e')); + || (size == 17 && (type1 == 'd') && (type2 == 'd')) + || (size >= 21 && (type1 == 'e') && (type2 == 'e')); if (valid) { return ProxyData::Status::Valid; } else if (size < 16) { diff --git a/Telegram/SourceFiles/mtproto/session.cpp b/Telegram/SourceFiles/mtproto/session.cpp index a52efb0ae..0148ff112 100644 --- a/Telegram/SourceFiles/mtproto/session.cpp +++ b/Telegram/SourceFiles/mtproto/session.cpp @@ -10,9 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/details/mtproto_dcenter.h" #include "mtproto/session_private.h" #include "mtproto/mtproto_auth_key.h" +#include "core/application.h" +#include "core/core_settings.h" #include "base/unixtime.h" #include "base/openssl_help.h" -#include "facades.h" namespace MTP { namespace details { @@ -238,22 +239,19 @@ void Session::restart() { } void Session::refreshOptions() { - const auto &proxy = Global::SelectedProxy(); - const auto proxyType = - (Global::ProxySettings() == ProxyData::Settings::Enabled - ? proxy.type - : ProxyData::Type::None); + auto &settings = Core::App().settings().proxy(); + const auto &proxy = settings.selected(); + const auto isEnabled = settings.isEnabled(); + const auto proxyType = (isEnabled ? proxy.type : ProxyData::Type::None); const auto useTcp = (proxyType != ProxyData::Type::Http); const auto useHttp = (proxyType != ProxyData::Type::Mtproto); const auto useIPv4 = true; - const auto useIPv6 = Global::TryIPv6(); + const auto useIPv6 = settings.tryIPv6(); _data->setOptions(SessionOptions( _instance->systemLangCode(), _instance->cloudLangCode(), _instance->langPackName(), - (Global::ProxySettings() == ProxyData::Settings::Enabled - ? proxy - : ProxyData()), + (isEnabled ? proxy : ProxyData()), useIPv4, useIPv6, useHttp, diff --git a/Telegram/SourceFiles/mtproto/session_private.cpp b/Telegram/SourceFiles/mtproto/session_private.cpp index 98ecddc89..d929ba00f 100644 --- a/Telegram/SourceFiles/mtproto/session_private.cpp +++ b/Telegram/SourceFiles/mtproto/session_private.cpp @@ -226,9 +226,17 @@ void SessionPrivate::appendTestConnection( }); }); + const auto protocolForFiles = isDownloadDcId(_shiftedDcId) + //|| isUploadDcId(_shiftedDcId) + || (_realDcType == DcType::Cdn); const auto protocolDcId = getProtocolDcId(); InvokeQueued(_testConnections.back().data, [=] { - weak->connectToServer(ip, port, protocolSecret, protocolDcId); + weak->connectToServer( + ip, + port, + protocolSecret, + protocolDcId, + protocolForFiles); }); } @@ -1255,17 +1263,16 @@ void SessionPrivate::handleReceived() { return restart(); } + constexpr auto kMinPaddingSize = 12U; + constexpr auto kMaxPaddingSize = 1024U; + auto encryptedInts = ints + kExternalHeaderIntsCount; auto encryptedIntsCount = (intsCount - kExternalHeaderIntsCount) & ~0x03U; auto encryptedBytesCount = encryptedIntsCount * kIntSize; auto decryptedBuffer = QByteArray(encryptedBytesCount, Qt::Uninitialized); auto msgKey = *(MTPint128*)(ints + 2); -#ifdef TDESKTOP_MTPROTO_OLD - aesIgeDecrypt_oldmtp(encryptedInts, decryptedBuffer.data(), encryptedBytesCount, _encryptionKey, msgKey); -#else // TDESKTOP_MTPROTO_OLD aesIgeDecrypt(encryptedInts, decryptedBuffer.data(), encryptedBytesCount, _encryptionKey, msgKey); -#endif // TDESKTOP_MTPROTO_OLD auto decryptedInts = reinterpret_cast(decryptedBuffer.constData()); auto serverSalt = *(uint64*)&decryptedInts[0]; @@ -1275,31 +1282,10 @@ void SessionPrivate::handleReceived() { auto needAck = ((seqNo & 0x01) != 0); auto messageLength = *(uint32*)&decryptedInts[7]; auto fullDataLength = kEncryptedHeaderIntsCount * kIntSize + messageLength; // Without padding. - auto badMessageLength = (messageLength > kMaxMessageLength); // Can underflow, but it is an unsigned type, so we just check the range later. auto paddingSize = static_cast(encryptedBytesCount) - static_cast(fullDataLength); -#ifdef TDESKTOP_MTPROTO_OLD - constexpr auto kMinPaddingSize_oldmtp = 0U; - constexpr auto kMaxPaddingSize_oldmtp = 15U; - badMessageLength |= (/*paddingSize < kMinPaddingSize_oldmtp || */paddingSize > kMaxPaddingSize_oldmtp); - - auto hashedDataLength = badMessageLength ? encryptedBytesCount : fullDataLength; - auto sha1ForMsgKeyCheck = hashSha1(decryptedInts, hashedDataLength); - - constexpr auto kMsgKeyShift_oldmtp = 4U; - if (ConstTimeIsDifferent(&msgKey, sha1ForMsgKeyCheck.data() + kMsgKeyShift_oldmtp, sizeof(msgKey))) { - LOG(("TCP Error: bad SHA1 hash after aesDecrypt in message.")); - TCP_LOG(("TCP Error: bad message %1").arg(Logs::mb(encryptedInts, encryptedBytesCount).str())); - - return restart(); - } -#else // TDESKTOP_MTPROTO_OLD - constexpr auto kMinPaddingSize = 12U; - constexpr auto kMaxPaddingSize = 1024U; - badMessageLength |= (paddingSize < kMinPaddingSize || paddingSize > kMaxPaddingSize); - std::array sha256Buffer = { { 0 } }; SHA256_CTX msgKeyLargeContext; @@ -1315,9 +1301,11 @@ void SessionPrivate::handleReceived() { return restart(); } -#endif // TDESKTOP_MTPROTO_OLD - if (badMessageLength || (messageLength & 0x03)) { + if ((messageLength > kMaxMessageLength) + || (messageLength & 0x03) + || (paddingSize < kMinPaddingSize) + || (paddingSize > kMaxPaddingSize)) { LOG(("TCP Error: bad msg_len received %1, data size: %2").arg(messageLength).arg(encryptedBytesCount)); TCP_LOG(("TCP Error: bad message %1").arg(Logs::mb(encryptedInts, encryptedBytesCount).str())); @@ -2608,12 +2596,7 @@ void SessionPrivate::destroyTemporaryKey() { bool SessionPrivate::sendSecureRequest( SerializedRequest &&request, bool needAnyResponse) { -#ifdef TDESKTOP_MTPROTO_OLD - const auto oldPadding = true; -#else // TDESKTOP_MTPROTO_OLD - const auto oldPadding = false; -#endif // TDESKTOP_MTPROTO_OLD - request.addPadding(_connection->requiresExtendedPadding(), oldPadding); + request.addPadding(false); uint32 fullSize = request->size(); if (fullSize < 9) { @@ -2635,27 +2618,6 @@ bool SessionPrivate::sendSecureRequest( ).arg(getProtocolDcId() ).arg(_encryptionKey->keyId())); -#ifdef TDESKTOP_MTPROTO_OLD - uint32 padding = fullSize - 4 - messageSize; - - uchar encryptedSHA[20]; - MTPint128 &msgKey(*(MTPint128*)(encryptedSHA + 4)); - hashSha1( - request->constData(), - (fullSize - padding) * sizeof(mtpPrime), - encryptedSHA); - - auto packet = _connection->prepareSecurePacket(_keyId, msgKey, fullSize); - const auto prefix = packet.size(); - packet.resize(prefix + fullSize); - - aesIgeEncrypt_oldmtp( - request->constData(), - &packet[prefix], - fullSize * sizeof(mtpPrime), - _encryptionKey, - msgKey); -#else // TDESKTOP_MTPROTO_OLD uchar encryptedSHA256[32]; MTPint128 &msgKey(*(MTPint128*)(encryptedSHA256 + 8)); @@ -2675,7 +2637,6 @@ bool SessionPrivate::sendSecureRequest( fullSize * sizeof(mtpPrime), _encryptionKey, msgKey); -#endif // TDESKTOP_MTPROTO_OLD DEBUG_LOG(("MTP Info: sending request, size: %1, num: %2, time: %3").arg(fullSize + 6).arg((*request)[4]).arg((*request)[5])); diff --git a/Telegram/SourceFiles/mtproto/special_config_request.cpp b/Telegram/SourceFiles/mtproto/special_config_request.cpp index f1239bba9..12f292a7a 100644 --- a/Telegram/SourceFiles/mtproto/special_config_request.cpp +++ b/Telegram/SourceFiles/mtproto/special_config_request.cpp @@ -380,7 +380,7 @@ void SpecialConfigRequest::requestFinished( not_null reply) { handleHeaderUnixtime(reply); const auto result = finalizeRequest(reply); - if (!_callback) { + if (!_callback || result.isEmpty()) { return; } @@ -407,7 +407,7 @@ void SpecialConfigRequest::requestFinished( QByteArray SpecialConfigRequest::finalizeRequest( not_null reply) { if (reply->error() != QNetworkReply::NoError) { - LOG(("Config Error: Failed to get response, error: %2 (%3)" + DEBUG_LOG(("Config Error: Failed to get response, error: %2 (%3)" ).arg(reply->errorString() ).arg(reply->error())); } diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 402bdcf7a..dd7b7fcb2 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "overview/overview_layout_delegate.h" #include "data/data_document.h" +#include "data/data_document_resolver.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "data/data_media_types.h" @@ -16,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_photo_media.h" #include "data/data_document_media.h" +#include "data/data_file_click_handler.h" #include "styles/style_overview.h" #include "styles/style_chat.h" #include "core/file_utilities.h" @@ -38,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt_adapters.h" #include "ui/effects/round_checkbox.h" #include "ui/image/image.h" +#include "ui/text/format_song_document_name.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/cached_round_corners.h" @@ -56,38 +59,6 @@ TextParseOptions _documentNameOptions = { Qt::LayoutDirectionAuto, // dir }; -TextWithEntities ComposeNameWithEntities(DocumentData *document) { - TextWithEntities result; - const auto song = document->song(); - if (!song || (song->title.isEmpty() && song->performer.isEmpty())) { - result.text = document->filename().isEmpty() - ? qsl("Unknown File") - : document->filename(); - result.entities.push_back({ - EntityType::Semibold, - 0, - result.text.size() - }); - } else if (song->performer.isEmpty()) { - result.text = song->title; - result.entities.push_back({ - EntityType::Semibold, - 0, - result.text.size() - }); - } else { - result.text = song->performer - + QString::fromUtf8(" \xe2\x80\x93 ") - + (song->title.isEmpty() ? qsl("Unknown Track") : song->title); - result.entities.push_back({ - EntityType::Semibold, - 0, - song->performer.size() - }); - } - return result; -} - } // namespace class Checkbox { @@ -215,9 +186,17 @@ void RadialProgressItem::setDocumentLinks( not_null document) { const auto context = parent()->fullId(); setLinks( - std::make_shared(document, context), + std::make_shared( + document, + crl::guard(this, [=](FullMsgId id) { + delegate()->openDocument(document, id); + }), + context), std::make_shared(document, context), - std::make_shared(document, context)); + std::make_shared( + document, + nullptr, + context)); } void RadialProgressItem::clickHandlerActiveChanged( @@ -302,7 +281,10 @@ Photo::Photo( not_null photo) : ItemBase(delegate, parent) , _data(photo) -, _link(std::make_shared(photo, parent->fullId())) { +, _link(std::make_shared( + photo, + crl::guard(this, [=](FullMsgId id) { delegate->openPhoto(photo, id); }), + parent->fullId())) { if (_data->inlineThumbnailBytes().isEmpty() && (_data->hasExact(Data::PhotoSize::Small) || _data->hasExact(Data::PhotoSize::Thumbnail))) { @@ -623,7 +605,12 @@ Voice::Voice( const style::OverviewFileLayout &st) : RadialProgressItem(delegate, parent) , _data(voice) -, _namel(std::make_shared(_data, parent->fullId())) +, _namel(std::make_shared( + _data, + crl::guard(this, [=](FullMsgId id) { + delegate->openDocument(_data, id); + }), + parent->fullId())) , _st(st) { AddComponents(Info::Bit()); @@ -930,12 +917,20 @@ Document::Document( : RadialProgressItem(delegate, parent) , _data(document) , _msgl(goToMessageClickHandler(parent)) -, _namel(std::make_shared(_data, parent->fullId())) +, _namel(std::make_shared( + _data, + crl::guard(this, [=](FullMsgId id) { + delegate->openDocument(_data, id); + }), + parent->fullId())) , _st(st) , _date(langDateTime(base::unixtime::parse(_data->date))) , _datew(st::normalFont->width(_date)) , _colorIndex(documentColorIndex(_data, _ext)) { - _name.setMarkedText(st::defaultTextStyle, ComposeNameWithEntities(_data), _documentNameOptions); + _name.setMarkedText( + st::defaultTextStyle, + Ui::Text::FormatSongNameFor(_data).textWithEntities(), + _documentNameOptions); AddComponents(Info::Bit()); @@ -1481,6 +1476,9 @@ Link::Link( if (_page->document) { _photol = std::make_shared( _page->document, + crl::guard(this, [=](FullMsgId id) { + delegate->openDocument(_page->document, id); + }), parent->fullId()); } else if (_page->photo) { if (_page->type == WebPageType::Profile || _page->type == WebPageType::Video) { @@ -1490,6 +1488,9 @@ Link::Link( || _page->siteName == qstr("Facebook")) { _photol = std::make_shared( _page->photo, + crl::guard(this, [=](FullMsgId id) { + delegate->openPhoto(_page->photo, id); + }), parent->fullId()); } else { _photol = createHandler(_page->url); diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index 40bc22903..1d5480f63 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -40,7 +40,7 @@ public: }; -class ItemBase : public LayoutItemBase { +class ItemBase : public LayoutItemBase, public base::has_weak_ptr { public: ItemBase(not_null delegate, not_null parent); ~ItemBase(); diff --git a/Telegram/SourceFiles/overview/overview_layout_delegate.h b/Telegram/SourceFiles/overview/overview_layout_delegate.h index f57d9d418..e35608821 100644 --- a/Telegram/SourceFiles/overview/overview_layout_delegate.h +++ b/Telegram/SourceFiles/overview/overview_layout_delegate.h @@ -17,6 +17,11 @@ public: virtual void registerHeavyItem(not_null item) = 0; virtual void unregisterHeavyItem(not_null item) = 0; + virtual void openPhoto(not_null photo, FullMsgId id) = 0; + virtual void openDocument( + not_null document, + FullMsgId id) = 0; + }; } // namespace Layout diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index 81b7b1cf6..a0d422282 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -61,9 +61,6 @@ passportPasswordForgotBottom: 36px; passportPanelScroll: ScrollArea(defaultScrollArea) { deltat: 6px; deltab: 6px; - - topsh: 0px; - bottomsh: 0px; } passportPanelAuthorize: RoundButton(passportPasswordSubmit) { diff --git a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp index 4db42b392..55839b257 100644 --- a/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp @@ -198,8 +198,8 @@ bool Get( parent = parent->window(); } #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION - if (XDP::Use(type)) { - return XDP::Get( + { + const auto result = XDP::Get( parent, files, remoteContent, @@ -207,18 +207,24 @@ bool Get( filter, type, startFile); + + if (result.has_value()) { + return *result; + } } #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION if (const auto integration = GtkIntegration::Instance()) { - if (integration->useFileDialog(type)) { - return integration->getFileDialog( - parent, - files, - remoteContent, - caption, - filter, - type, - startFile); + const auto result = integration->getFileDialog( + parent, + files, + remoteContent, + caption, + filter, + type, + startFile); + + if (result.has_value()) { + return *result; } } // avoid situation when portals don't work diff --git a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp index 032b91bab..21d62c25d 100644 --- a/Telegram/SourceFiles/platform/linux/launcher_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/launcher_linux.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include #include @@ -75,12 +76,17 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { return false; } - const auto binaryName = (action == UpdaterLaunch::JustRelaunch) - ? cExeName() - : QStringLiteral("Updater"); + const auto binaryPath = (action == UpdaterLaunch::JustRelaunch) + ? (cExeDir() + cExeName()) + : (cWriteProtected() + ? (cWorkingDir() + qsl("tupdates/temp/Updater")) + : (cExeDir() + qsl("Updater"))); auto argumentsList = Arguments(); - argumentsList.push(QFile::encodeName(cExeDir() + binaryName)); + if (action == UpdaterLaunch::PerformUpdate && cWriteProtected()) { + argumentsList.push("pkexec"); + } + argumentsList.push(QFile::encodeName(binaryPath)); if (cLaunchMode() == LaunchModeAutoStart) { argumentsList.push("-autostart"); @@ -118,6 +124,9 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { if (customWorkingDir()) { argumentsList.push("-workdir_custom"); } + if (cWriteProtected()) { + argumentsList.push("-writeprotected"); + } } if (!cUseEnvApi()) { @@ -139,8 +148,16 @@ bool Launcher::launchUpdater(UpdaterLaunch action) { pid_t pid = fork(); switch (pid) { case -1: return false; - case 0: execv(args[0], args); return false; + case 0: execvp(args[0], args); return false; } + + // pkexec needs an alive parent + if (action == UpdaterLaunch::PerformUpdate && cWriteProtected()) { + waitpid(pid, nullptr, 0); + // launch new version in the same environment + return launchUpdater(UpdaterLaunch::JustRelaunch); + } + return true; } diff --git a/Telegram/SourceFiles/platform/linux/linux_gdk_helper.cpp b/Telegram/SourceFiles/platform/linux/linux_gdk_helper.cpp index ab65c36cc..c19149fc2 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gdk_helper.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gdk_helper.cpp @@ -7,8 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/linux/linux_gdk_helper.h" +#include "base/platform/linux/base_linux_gtk_integration.h" #include "base/platform/linux/base_linux_gtk_integration_p.h" #include "platform/linux/linux_gtk_integration_p.h" +#include "platform/linux/linux_wayland_integration.h" + +#include #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION extern "C" { @@ -16,21 +20,21 @@ extern "C" { } // extern "C" #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION +#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION +extern "C" { +#include +} // extern "C" +#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + namespace Platform { namespace internal { +namespace { +using base::Platform::GtkIntegration; using namespace Platform::Gtk; -enum class GtkLoaded { - GtkNone, - Gtk2, - Gtk3, -}; - -GtkLoaded gdk_helper_loaded = GtkLoaded::GtkNone; - #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION -// To be able to compile with gtk-3.0 headers as well +// To be able to compile with gtk-3.0 headers #define GdkDrawable GdkWindow // Gtk 2 @@ -44,12 +48,6 @@ f_gdk_x11_drawable_get_xid gdk_x11_drawable_get_xid = nullptr; using f_gdk_x11_window_get_type = GType (*)(void); f_gdk_x11_window_get_type gdk_x11_window_get_type = nullptr; -// To be able to compile with gtk-2.0 headers as well -template -inline bool gdk_is_x11_window_check(Object *obj) { - return g_type_cit_helper(obj, gdk_x11_window_get_type()); -} - using f_gdk_window_get_display = GdkDisplay*(*)(GdkWindow *window); f_gdk_window_get_display gdk_window_get_display = nullptr; @@ -60,62 +58,87 @@ using f_gdk_x11_window_get_xid = Window(*)(GdkWindow *window); f_gdk_x11_window_get_xid gdk_x11_window_get_xid = nullptr; #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION -bool GdkHelperLoadGtk2(QLibrary &lib) { -#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION -#ifdef LINK_TO_GTK - return false; -#else // LINK_TO_GTK - if (!LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xdisplay)) return false; - if (!LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xid)) return false; - return true; -#endif // !LINK_TO_GTK -#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION - return false; -#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION +#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION +using f_gdk_wayland_window_get_type = GType (*)(void); +f_gdk_wayland_window_get_type gdk_wayland_window_get_type = nullptr; + +using f_gdk_wayland_window_set_transient_for_exported = gboolean(*)(GdkWindow *window, char *parent_handle_str); +f_gdk_wayland_window_set_transient_for_exported gdk_wayland_window_set_transient_for_exported = nullptr; +#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + +void GdkHelperLoadGtk2(QLibrary &lib) { +#if !defined DESKTOP_APP_DISABLE_X11_INTEGRATION && !defined LINK_TO_GTK + LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xdisplay); + LOAD_GTK_SYMBOL(lib, gdk_x11_drawable_get_xid); +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION && !LINK_TO_GTK } -bool GdkHelperLoadGtk3(QLibrary &lib) { +void GdkHelperLoadGtk3(QLibrary &lib) { #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION - if (!LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_type)) return false; - if (!LOAD_GTK_SYMBOL(lib, gdk_window_get_display)) return false; - if (!LOAD_GTK_SYMBOL(lib, gdk_x11_display_get_xdisplay)) return false; - if (!LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_xid)) return false; - return true; -#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION - return false; -#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION + LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_type); + LOAD_GTK_SYMBOL(lib, gdk_window_get_display); + LOAD_GTK_SYMBOL(lib, gdk_x11_display_get_xdisplay); + LOAD_GTK_SYMBOL(lib, gdk_x11_window_get_xid); +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION +#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + LOAD_GTK_SYMBOL(lib, gdk_wayland_window_get_type); + LOAD_GTK_SYMBOL(lib, gdk_wayland_window_set_transient_for_exported); +#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION } +} // namespace + void GdkHelperLoad(QLibrary &lib) { - gdk_helper_loaded = GtkLoaded::GtkNone; - if (GdkHelperLoadGtk3(lib)) { - gdk_helper_loaded = GtkLoaded::Gtk3; - } else if (GdkHelperLoadGtk2(lib)) { - gdk_helper_loaded = GtkLoaded::Gtk2; + if (const auto integration = GtkIntegration::Instance()) { + if (integration->checkVersion(3, 0, 0)) { + GdkHelperLoadGtk3(lib); + } else { + GdkHelperLoadGtk2(lib); + } } } -bool GdkHelperLoaded() { -#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION - return gdk_helper_loaded != GtkLoaded::GtkNone; -#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION - return true; -#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION -} - -void XSetTransientForHint(GdkWindow *window, quintptr winId) { -#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION - if (gdk_helper_loaded == GtkLoaded::Gtk2) { - ::XSetTransientForHint(gdk_x11_drawable_get_xdisplay(window), - gdk_x11_drawable_get_xid(window), - winId); - } else if (gdk_helper_loaded == GtkLoaded::Gtk3) { - if (gdk_is_x11_window_check(window)) { - ::XSetTransientForHint(gdk_x11_display_get_xdisplay(gdk_window_get_display(window)), - gdk_x11_window_get_xid(window), - winId); +void GdkSetTransientFor(GdkWindow *window, QWindow *parent) { +#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + if (gdk_wayland_window_get_type != nullptr + && gdk_wayland_window_set_transient_for_exported != nullptr + && GDK_IS_WAYLAND_WINDOW(window)) { + if (const auto integration = WaylandIntegration::Instance()) { + if (const auto handle = integration->nativeHandle(parent) + ; !handle.isEmpty()) { + auto handleUtf8 = handle.toUtf8(); + gdk_wayland_window_set_transient_for_exported( + window, + handleUtf8.data()); + return; + } } } +#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + if (gdk_x11_window_get_type != nullptr + && gdk_x11_display_get_xdisplay != nullptr + && gdk_x11_window_get_xid != nullptr + && gdk_window_get_display != nullptr + && GDK_IS_X11_WINDOW(window)) { + XSetTransientForHint( + gdk_x11_display_get_xdisplay(gdk_window_get_display(window)), + gdk_x11_window_get_xid(window), + parent->winId()); + return; + } +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION + +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + if (gdk_x11_drawable_get_xdisplay != nullptr + && gdk_x11_drawable_get_xid != nullptr) { + XSetTransientForHint( + gdk_x11_drawable_get_xdisplay(window), + gdk_x11_drawable_get_xid(window), + parent->winId()); + return; + } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION } diff --git a/Telegram/SourceFiles/platform/linux/linux_gdk_helper.h b/Telegram/SourceFiles/platform/linux/linux_gdk_helper.h index 6ca953689..df06803c9 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gdk_helper.h +++ b/Telegram/SourceFiles/platform/linux/linux_gdk_helper.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once class QLibrary; +class QWindow; extern "C" { #include @@ -18,8 +19,7 @@ namespace Platform { namespace internal { void GdkHelperLoad(QLibrary &lib); -bool GdkHelperLoaded(); -void XSetTransientForHint(GdkWindow *window, quintptr winId); +void GdkSetTransientFor(GdkWindow *window, QWindow *parent); } // namespace internal } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/linux_gsd_media_keys.cpp b/Telegram/SourceFiles/platform/linux/linux_gsd_media_keys.cpp index 4151736de..c5f19e03f 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gsd_media_keys.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gsd_media_keys.cpp @@ -27,28 +27,7 @@ constexpr auto kMATEObjectPath = "/org/mate/SettingsDaemon/MediaKeys"_cs; constexpr auto kInterface = kService; constexpr auto kMATEInterface = "org.mate.SettingsDaemon.MediaKeys"_cs; -} // namespace - -class GSDMediaKeys::Private : public sigc::trackable { -public: - Glib::RefPtr dbusConnection; - - Glib::ustring service; - Glib::ustring objectPath; - Glib::ustring interface; - uint signalId = 0; - bool grabbed = false; - - void keyPressed( - const Glib::RefPtr &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters); -}; - -void GSDMediaKeys::Private::keyPressed( +void KeyPressed( const Glib::RefPtr &connection, const Glib::ustring &sender_name, const Glib::ustring &object_path, @@ -83,6 +62,19 @@ void GSDMediaKeys::Private::keyPressed( } } +} // namespace + +class GSDMediaKeys::Private { +public: + Glib::RefPtr dbusConnection; + + Glib::ustring service; + Glib::ustring objectPath; + Glib::ustring interface; + uint signalId = 0; + bool grabbed = false; +}; + GSDMediaKeys::GSDMediaKeys() : _private(std::make_unique()) { try { @@ -126,7 +118,7 @@ GSDMediaKeys::GSDMediaKeys() _private->grabbed = true; _private->signalId = _private->dbusConnection->signal_subscribe( - sigc::mem_fun(_private.get(), &Private::keyPressed), + sigc::ptr_fun(KeyPressed), _private->service, _private->interface, "MediaPlayerKeyPressed", diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.cpp index 8606af301..9bc60b9cf 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.cpp @@ -22,10 +22,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Platform { namespace FileDialog { namespace Gtk { +namespace { using namespace Platform::Gtk; - -namespace { +using Type = ::FileDialog::internal::Type; // GTK file chooser image preview: thanks to Chromium @@ -70,8 +70,7 @@ QStringList cleanFilterList(const QString &filter) { } bool Supported() { - return internal::GdkHelperLoaded() - && (gtk_widget_hide_on_delete != nullptr) + return (gtk_widget_hide_on_delete != nullptr) && (gtk_clipboard_store != nullptr) && (gtk_clipboard_get != nullptr) && (gtk_widget_destroy != nullptr) @@ -136,13 +135,11 @@ public: protected: static void onResponse(QGtkDialog *dialog, int response); - static void onUpdatePreview(QGtkDialog *dialog); private: void onParentWindowDestroyed(); GtkWidget *gtkWidget = nullptr; - GtkWidget *_preview = nullptr; rpl::event_stream<> _accept; rpl::event_stream<> _reject; @@ -193,6 +190,7 @@ public: private: static void onSelectionChanged(GtkDialog *dialog, GtkFileDialog *helper); static void onCurrentFolderChanged(GtkFileDialog *helper); + static void onUpdatePreview(GtkDialog *gtkDialog, GtkFileDialog *helper); void applyOptions(); void setNameFilters(const QStringList &filters); @@ -216,6 +214,7 @@ private: QHash _filters; QHash _filterNames; QScopedPointer d; + GtkWidget *_preview = nullptr; rpl::lifetime _lifetime; }; @@ -223,11 +222,6 @@ private: QGtkDialog::QGtkDialog(GtkWidget *gtkWidget) : gtkWidget(gtkWidget) { g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), this); g_signal_connect(G_OBJECT(gtkWidget), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), nullptr); - if (PreviewSupported()) { - _preview = gtk_image_new(); - g_signal_connect_swapped(G_OBJECT(gtkWidget), "update-preview", G_CALLBACK(onUpdatePreview), this); - gtk_file_chooser_set_preview_widget(gtk_file_chooser_cast(gtkWidget), _preview); - } } QGtkDialog::~QGtkDialog() { @@ -236,7 +230,7 @@ QGtkDialog::~QGtkDialog() { } GtkDialog *QGtkDialog::gtkDialog() const { - return gtk_dialog_cast(gtkWidget); + return GTK_DIALOG(gtkWidget); } void QGtkDialog::exec() { @@ -273,7 +267,7 @@ void QGtkDialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindo gtk_widget_realize(gtkWidget); // creates X window if (parent) { - internal::XSetTransientForHint(gtk_widget_get_window(gtkWidget), parent->winId()); + internal::GdkSetTransientFor(gtk_widget_get_window(gtkWidget), parent); } if (modality != Qt::NonModal) { @@ -305,32 +299,6 @@ void QGtkDialog::onResponse(QGtkDialog *dialog, int response) { dialog->_reject.fire({}); } -void QGtkDialog::onUpdatePreview(QGtkDialog* dialog) { - auto filename = gtk_file_chooser_get_preview_filename(gtk_file_chooser_cast(dialog->gtkWidget)); - if (!filename) { - gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), false); - return; - } - - // Don't attempt to open anything which isn't a regular file. If a named pipe, - // this may hang. See https://crbug.com/534754. - struct stat stat_buf; - if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) { - g_free(filename); - gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), false); - return; - } - - // This will preserve the image's aspect ratio. - auto pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth, kPreviewHeight, nullptr); - g_free(filename); - if (pixbuf) { - gtk_image_set_from_pixbuf(gtk_image_cast(dialog->_preview), pixbuf); - g_object_unref(pixbuf); - } - gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), pixbuf ? true : false); -} - void QGtkDialog::onParentWindowDestroyed() { // The Gtk*DialogHelper classes own this object. Make sure the parent doesn't delete it. setParent(nullptr); @@ -361,8 +329,14 @@ GtkFileDialog::GtkFileDialog(QWidget *parent, const QString &caption, const QStr onRejected(); }, _lifetime); - g_signal_connect(gtk_file_chooser_cast(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this); - g_signal_connect_swapped(gtk_file_chooser_cast(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this); + g_signal_connect(GTK_FILE_CHOOSER(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this); + g_signal_connect_swapped(GTK_FILE_CHOOSER(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this); + + if (PreviewSupported()) { + _preview = gtk_image_new(); + g_signal_connect(G_OBJECT(d->gtkDialog()), "update-preview", G_CALLBACK(onUpdatePreview), this); + gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(d->gtkDialog()), _preview); + } } GtkFileDialog::~GtkFileDialog() { @@ -436,7 +410,7 @@ bool GtkFileDialog::defaultNameFilterDisables() const { void GtkFileDialog::setDirectory(const QString &directory) { GtkDialog *gtkDialog = d->gtkDialog(); - gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), directory.toUtf8().constData()); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(gtkDialog), directory.toUtf8().constData()); } QDir GtkFileDialog::directory() const { @@ -447,7 +421,7 @@ QDir GtkFileDialog::directory() const { QString ret; GtkDialog *gtkDialog = d->gtkDialog(); - gchar *folder = gtk_file_chooser_get_current_folder(gtk_file_chooser_cast(gtkDialog)); + gchar *folder = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(gtkDialog)); if (folder) { ret = QString::fromUtf8(folder); g_free(folder); @@ -468,7 +442,7 @@ QStringList GtkFileDialog::selectedFiles() const { QStringList selection; GtkDialog *gtkDialog = d->gtkDialog(); - GSList *filenames = gtk_file_chooser_get_filenames(gtk_file_chooser_cast(gtkDialog)); + GSList *filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(gtkDialog)); for (GSList *it = filenames; it; it = it->next) selection += QString::fromUtf8((const char*)it->data); g_slist_free(filenames); @@ -483,13 +457,13 @@ void GtkFileDialog::selectNameFilter(const QString &filter) { GtkFileFilter *gtkFilter = _filters.value(filter); if (gtkFilter) { GtkDialog *gtkDialog = d->gtkDialog(); - gtk_file_chooser_set_filter(gtk_file_chooser_cast(gtkDialog), gtkFilter); + gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(gtkDialog), gtkFilter); } } QString GtkFileDialog::selectedNameFilter() const { GtkDialog *gtkDialog = d->gtkDialog(); - GtkFileFilter *gtkFilter = gtk_file_chooser_get_filter(gtk_file_chooser_cast(gtkDialog)); + GtkFileFilter *gtkFilter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(gtkDialog)); return _filterNames.value(gtkFilter); } @@ -526,6 +500,32 @@ void GtkFileDialog::onCurrentFolderChanged(GtkFileDialog *dialog) { // emit dialog->directoryEntered(dialog->directory()); } +void GtkFileDialog::onUpdatePreview(GtkDialog *gtkDialog, GtkFileDialog *helper) { + auto filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(gtkDialog)); + if (!filename) { + gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(gtkDialog), false); + return; + } + + // Don't attempt to open anything which isn't a regular file. If a named pipe, + // this may hang. See https://crbug.com/534754. + struct stat stat_buf; + if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) { + g_free(filename); + gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(gtkDialog), false); + return; + } + + // This will preserve the image's aspect ratio. + auto pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth, kPreviewHeight, nullptr); + g_free(filename); + if (pixbuf) { + gtk_image_set_from_pixbuf(GTK_IMAGE(helper->_preview), pixbuf); + g_object_unref(pixbuf); + } + gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(gtkDialog), pixbuf ? true : false); +} + GtkFileChooserAction gtkFileChooserAction(QFileDialog::FileMode fileMode, QFileDialog::AcceptMode acceptMode) { switch (fileMode) { case QFileDialog::AnyFile: @@ -547,17 +547,17 @@ GtkFileChooserAction gtkFileChooserAction(QFileDialog::FileMode fileMode, QFileD void GtkFileDialog::applyOptions() { GtkDialog *gtkDialog = d->gtkDialog(); - gtk_window_set_title(gtk_window_cast(gtkDialog), _windowTitle.toUtf8().constData()); - gtk_file_chooser_set_local_only(gtk_file_chooser_cast(gtkDialog), true); + gtk_window_set_title(GTK_WINDOW(gtkDialog), _windowTitle.toUtf8().constData()); + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(gtkDialog), true); const GtkFileChooserAction action = gtkFileChooserAction(_fileMode, _acceptMode); - gtk_file_chooser_set_action(gtk_file_chooser_cast(gtkDialog), action); + gtk_file_chooser_set_action(GTK_FILE_CHOOSER(gtkDialog), action); const bool selectMultiple = (_fileMode == QFileDialog::ExistingFiles); - gtk_file_chooser_set_select_multiple(gtk_file_chooser_cast(gtkDialog), selectMultiple); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(gtkDialog), selectMultiple); const bool confirmOverwrite = !_options.testFlag(QFileDialog::DontConfirmOverwrite); - gtk_file_chooser_set_do_overwrite_confirmation(gtk_file_chooser_cast(gtkDialog), confirmOverwrite); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(gtkDialog), confirmOverwrite); if (!_nameFilters.isEmpty()) setNameFilters(_nameFilters); @@ -568,12 +568,12 @@ void GtkFileDialog::applyOptions() { for_const (const auto &filename, _initialFiles) { if (_acceptMode == QFileDialog::AcceptSave) { QFileInfo fi(filename); - gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), fi.path().toUtf8().constData()); - gtk_file_chooser_set_current_name(gtk_file_chooser_cast(gtkDialog), fi.fileName().toUtf8().constData()); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(gtkDialog), fi.path().toUtf8().constData()); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(gtkDialog), fi.fileName().toUtf8().constData()); } else if (filename.endsWith('/')) { - gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), filename.toUtf8().constData()); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(gtkDialog), filename.toUtf8().constData()); } else { - gtk_file_chooser_select_filename(gtk_file_chooser_cast(gtkDialog), filename.toUtf8().constData()); + gtk_file_chooser_select_filename(GTK_FILE_CHOOSER(gtkDialog), filename.toUtf8().constData()); } } @@ -585,19 +585,19 @@ void GtkFileDialog::applyOptions() { GtkWidget *acceptButton = gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_OK); if (acceptButton) { /*if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept)) - gtk_button_set_label(gtk_button_cast(acceptButton), opts->labelText(QFileDialogOptions::Accept).toUtf8().constData()); + gtk_button_set_label(GTK_BUTTON(acceptButton), opts->labelText(QFileDialogOptions::Accept).toUtf8().constData()); else*/ if (_acceptMode == QFileDialog::AcceptOpen) - gtk_button_set_label(gtk_button_cast(acceptButton), tr::lng_open_link(tr::now).toUtf8().constData()); + gtk_button_set_label(GTK_BUTTON(acceptButton), tr::lng_open_link(tr::now).toUtf8().constData()); else - gtk_button_set_label(gtk_button_cast(acceptButton), tr::lng_settings_save(tr::now).toUtf8().constData()); + gtk_button_set_label(GTK_BUTTON(acceptButton), tr::lng_settings_save(tr::now).toUtf8().constData()); } GtkWidget *rejectButton = gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_CANCEL); if (rejectButton) { /*if (opts->isLabelExplicitlySet(QFileDialogOptions::Reject)) - gtk_button_set_label(gtk_button_cast(rejectButton), opts->labelText(QFileDialogOptions::Reject).toUtf8().constData()); + gtk_button_set_label(GTK_BUTTON(rejectButton), opts->labelText(QFileDialogOptions::Reject).toUtf8().constData()); else*/ - gtk_button_set_label(gtk_button_cast(rejectButton), tr::lng_cancel(tr::now).toUtf8().constData()); + gtk_button_set_label(GTK_BUTTON(rejectButton), tr::lng_cancel(tr::now).toUtf8().constData()); } } } @@ -605,7 +605,7 @@ void GtkFileDialog::applyOptions() { void GtkFileDialog::setNameFilters(const QStringList &filters) { GtkDialog *gtkDialog = d->gtkDialog(); Q_FOREACH (GtkFileFilter *filter, _filters) - gtk_file_chooser_remove_filter(gtk_file_chooser_cast(gtkDialog), filter); + gtk_file_chooser_remove_filter(GTK_FILE_CHOOSER(gtkDialog), filter); _filters.clear(); _filterNames.clear(); @@ -632,7 +632,7 @@ void GtkFileDialog::setNameFilters(const QStringList &filters) { gtk_file_filter_add_pattern(gtkFilter, caseInsensitiveExt.toUtf8().constData()); } - gtk_file_chooser_add_filter(gtk_file_chooser_cast(gtkDialog), gtkFilter); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(gtkDialog), gtkFilter); _filters.insert(filter, gtkFilter); _filterNames.insert(gtkFilter, filter); @@ -641,17 +641,7 @@ void GtkFileDialog::setNameFilters(const QStringList &filters) { } // namespace -bool Use(Type type) { - if (!Supported() - || (FileDialogType() > ImplementationType::GTK)) { - return false; - } - - return (FileDialogType() == ImplementationType::GTK) - || DesktopEnvironment::IsGtkBased(); -} - -bool Get( +std::optional Get( QPointer parent, QStringList &files, QByteArray &remoteContent, @@ -659,6 +649,12 @@ bool Get( const QString &filter, Type type, QString startFile) { + if (!Supported() + || (FileDialogType() <= ImplementationType::GTK + && !DesktopEnvironment::IsGtkBased())) { + return std::nullopt; + } + if (cDialogLastPath().isEmpty()) { InitLastPath(); } diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.h b/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.h index 070bfd74a..b12c7f415 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.h +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_file_dialog.h @@ -13,16 +13,13 @@ namespace Platform { namespace FileDialog { namespace Gtk { -using Type = ::FileDialog::internal::Type; - -bool Use(Type type = Type::ReadFile); -bool Get( +std::optional Get( QPointer parent, QStringList &files, QByteArray &remoteContent, const QString &caption, const QString &filter, - Type type, + ::FileDialog::internal::Type type, QString startFile); } // namespace Gtk diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_integration.cpp b/Telegram/SourceFiles/platform/linux/linux_gtk_integration.cpp index 00b67e152..8fb76c3ac 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_integration.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_integration.cpp @@ -158,17 +158,13 @@ std::optional GtkIntegration::scaleFactor() const { return gdk_monitor_get_scale_factor(monitor); } -bool GtkIntegration::useFileDialog(FileDialogType type) const { - return FileDialog::Gtk::Use(type); -} - -bool GtkIntegration::getFileDialog( +std::optional GtkIntegration::getFileDialog( QPointer parent, QStringList &files, QByteArray &remoteContent, const QString &caption, const QString &filter, - FileDialogType type, + ::FileDialog::internal::Type type, QString startFile) const { return FileDialog::Gtk::Get( parent, diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_integration.h b/Telegram/SourceFiles/platform/linux/linux_gtk_integration.h index 623db1498..e1ff44e63 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_integration.h +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_integration.h @@ -20,16 +20,13 @@ public: [[nodiscard]] std::optional scaleFactor() const; - using FileDialogType = ::FileDialog::internal::Type; - [[nodiscard]] bool useFileDialog( - FileDialogType type = FileDialogType::ReadFile) const; - [[nodiscard]] bool getFileDialog( + [[nodiscard]] std::optional getFileDialog( QPointer parent, QStringList &files, QByteArray &remoteContent, const QString &caption, const QString &filter, - FileDialogType type, + ::FileDialog::internal::Type type, QString startFile) const; [[nodiscard]] bool showOpenWithDialog(const QString &filepath) const; diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_integration_dummy.cpp b/Telegram/SourceFiles/platform/linux/linux_gtk_integration_dummy.cpp index f5b5695e6..a0dfb2ba0 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_integration_dummy.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_integration_dummy.cpp @@ -24,19 +24,15 @@ std::optional GtkIntegration::scaleFactor() const { return std::nullopt; } -bool GtkIntegration::useFileDialog(FileDialogType type) const { - return false; -} - -bool GtkIntegration::getFileDialog( +std::optional GtkIntegration::getFileDialog( QPointer parent, QStringList &files, QByteArray &remoteContent, const QString &caption, const QString &filter, - FileDialogType type, + ::FileDialog::internal::Type type, QString startFile) const { - return false; + return std::nullopt; } bool GtkIntegration::showOpenWithDialog(const QString &filepath) const { diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_integration_p.h b/Telegram/SourceFiles/platform/linux/linux_gtk_integration_p.h index 66ace0eb3..d518e816b 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_integration_p.h +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_integration_p.h @@ -12,10 +12,6 @@ extern "C" { #include } // extern "C" -// To be able to compile with gtk-2.0 headers as well -typedef struct _GdkMonitor GdkMonitor; -typedef struct _GtkAppChooser GtkAppChooser; - namespace Platform { namespace Gtk { @@ -31,6 +27,7 @@ inline GtkSelectionData* (*gtk_clipboard_wait_for_contents)(GtkClipboard *clipbo inline GdkPixbuf* (*gtk_clipboard_wait_for_image)(GtkClipboard *clipboard) = nullptr; inline gboolean (*gtk_selection_data_targets_include_image)(const GtkSelectionData *selection_data, gboolean writable) = nullptr; inline void (*gtk_selection_data_free)(GtkSelectionData *data) = nullptr; +inline GType (*gtk_file_chooser_get_type)(void) G_GNUC_CONST = nullptr; inline GtkWidget* (*gtk_file_chooser_dialog_new)(const gchar *title, GtkWindow *parent, GtkFileChooserAction action, const gchar *first_button_text, ...) G_GNUC_NULL_TERMINATED = nullptr; inline gboolean (*gtk_file_chooser_set_current_folder)(GtkFileChooser *chooser, const gchar *filename) = nullptr; inline gchar* (*gtk_file_chooser_get_current_folder)(GtkFileChooser *chooser) = nullptr; @@ -39,12 +36,15 @@ inline gboolean (*gtk_file_chooser_select_filename)(GtkFileChooser *chooser, con inline GSList* (*gtk_file_chooser_get_filenames)(GtkFileChooser *chooser) = nullptr; inline void (*gtk_file_chooser_set_filter)(GtkFileChooser *chooser, GtkFileFilter *filter) = nullptr; inline GtkFileFilter* (*gtk_file_chooser_get_filter)(GtkFileChooser *chooser) = nullptr; +inline GType (*gtk_window_get_type)(void) G_GNUC_CONST = nullptr; inline void (*gtk_window_set_title)(GtkWindow *window, const gchar *title) = nullptr; inline void (*gtk_file_chooser_set_local_only)(GtkFileChooser *chooser, gboolean local_only) = nullptr; inline void (*gtk_file_chooser_set_action)(GtkFileChooser *chooser, GtkFileChooserAction action) = nullptr; inline void (*gtk_file_chooser_set_select_multiple)(GtkFileChooser *chooser, gboolean select_multiple) = nullptr; inline void (*gtk_file_chooser_set_do_overwrite_confirmation)(GtkFileChooser *chooser, gboolean do_overwrite_confirmation) = nullptr; +inline GType (*gtk_dialog_get_type)(void) G_GNUC_CONST = nullptr; inline GtkWidget* (*gtk_dialog_get_widget_for_response)(GtkDialog *dialog, gint response_id) = nullptr; +inline GType (*gtk_button_get_type)(void) G_GNUC_CONST = nullptr; inline void (*gtk_button_set_label)(GtkButton *button, const gchar *label) = nullptr; inline void (*gtk_file_chooser_remove_filter)(GtkFileChooser *chooser, GtkFileFilter *filter) = nullptr; inline void (*gtk_file_filter_set_name)(GtkFileFilter *filter, const gchar *name) = nullptr; @@ -54,65 +54,14 @@ inline void (*gtk_file_chooser_set_preview_widget)(GtkFileChooser *chooser, GtkW inline gchar* (*gtk_file_chooser_get_preview_filename)(GtkFileChooser *chooser) = nullptr; inline void (*gtk_file_chooser_set_preview_widget_active)(GtkFileChooser *chooser, gboolean active) = nullptr; inline GtkFileFilter* (*gtk_file_filter_new)(void) = nullptr; +inline GType (*gtk_image_get_type)(void) G_GNUC_CONST = nullptr; inline GtkWidget* (*gtk_image_new)(void) = nullptr; inline void (*gtk_image_set_from_pixbuf)(GtkImage *image, GdkPixbuf *pixbuf) = nullptr; +inline GType (*gtk_app_chooser_get_type)(void) G_GNUC_CONST = nullptr; inline GtkWidget* (*gtk_app_chooser_dialog_new)(GtkWindow *parent, GtkDialogFlags flags, GFile *file) = nullptr; inline GAppInfo* (*gtk_app_chooser_get_app_info)(GtkAppChooser *self) = nullptr; inline void (*gdk_window_set_modal_hint)(GdkWindow *window, gboolean modal) = nullptr; inline void (*gdk_window_focus)(GdkWindow *window, guint32 timestamp) = nullptr; - -template -inline Result *g_type_cic_helper(Object *instance, GType iface_type) { - return reinterpret_cast(g_type_check_instance_cast(reinterpret_cast(instance), iface_type)); -} - -inline GType (*gtk_dialog_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkDialog *gtk_dialog_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_dialog_get_type()); -} - -inline GType (*gtk_file_chooser_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkFileChooser *gtk_file_chooser_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_file_chooser_get_type()); -} - -inline GType (*gtk_image_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkImage *gtk_image_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_image_get_type()); -} - -inline GType (*gtk_button_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkButton *gtk_button_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_button_get_type()); -} - -inline GType (*gtk_window_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkWindow *gtk_window_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_window_get_type()); -} - -inline GType (*gtk_app_chooser_get_type)(void) G_GNUC_CONST = nullptr; -template -inline GtkAppChooser *gtk_app_chooser_cast(Object *obj) { - return g_type_cic_helper(obj, gtk_app_chooser_get_type()); -} - -template -inline bool g_type_cit_helper(Object *instance, GType iface_type) { - if (!instance) return false; - - auto ginstance = reinterpret_cast(instance); - if (ginstance->g_class && ginstance->g_class->g_type == iface_type) { - return true; - } - return g_type_check_instance_is_a(ginstance, iface_type); -} - inline gint (*gtk_dialog_run)(GtkDialog *dialog) = nullptr; inline GdkAtom (*gdk_atom_intern)(const gchar *atom_name, gboolean only_if_exists) = nullptr; inline GdkDisplay* (*gdk_display_get_default)(void) = nullptr; diff --git a/Telegram/SourceFiles/platform/linux/linux_gtk_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_gtk_open_with_dialog.cpp index 1c8710bae..3c8aac4b4 100644 --- a/Telegram/SourceFiles/platform/linux/linux_gtk_open_with_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_gtk_open_with_dialog.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include +#include namespace Platform { namespace File { @@ -21,9 +22,14 @@ namespace { using namespace Platform::Gtk; +struct GtkWidgetDeleter { + void operator()(GtkWidget *widget) { + gtk_widget_destroy(widget); + } +}; + bool Supported() { - return Platform::internal::GdkHelperLoaded() - && (gtk_app_chooser_dialog_new != nullptr) + return (gtk_app_chooser_dialog_new != nullptr) && (gtk_app_chooser_get_app_info != nullptr) && (gtk_app_chooser_get_type != nullptr) && (gtk_widget_get_window != nullptr) @@ -35,48 +41,42 @@ bool Supported() { class GtkOpenWithDialog : public QWindow { public: GtkOpenWithDialog(const QString &filepath); - ~GtkOpenWithDialog(); bool exec(); private: static void handleResponse(GtkOpenWithDialog *dialog, int responseId); - GFile *_gfileInstance = nullptr; - GtkWidget *_gtkWidget = nullptr; + const Glib::RefPtr _file; + const std::unique_ptr _gtkWidget; QEventLoop _loop; std::optional _result; }; GtkOpenWithDialog::GtkOpenWithDialog(const QString &filepath) -: _gfileInstance(g_file_new_for_path(filepath.toUtf8().constData())) +: _file(Gio::File::create_for_path(filepath.toStdString())) , _gtkWidget(gtk_app_chooser_dialog_new( nullptr, GTK_DIALOG_MODAL, - _gfileInstance)) { + _file->gobj())) { g_signal_connect_swapped( - _gtkWidget, + _gtkWidget.get(), "response", G_CALLBACK(handleResponse), this); } -GtkOpenWithDialog::~GtkOpenWithDialog() { - gtk_widget_destroy(_gtkWidget); - g_object_unref(_gfileInstance); -} - bool GtkOpenWithDialog::exec() { - gtk_widget_realize(_gtkWidget); + gtk_widget_realize(_gtkWidget.get()); if (const auto activeWindow = Core::App().activeWindow()) { - Platform::internal::XSetTransientForHint( - gtk_widget_get_window(_gtkWidget), - activeWindow->widget().get()->windowHandle()->winId()); + Platform::internal::GdkSetTransientFor( + gtk_widget_get_window(_gtkWidget.get()), + activeWindow->widget()->windowHandle()); } QGuiApplicationPrivate::showModalWindow(this); - gtk_widget_show(_gtkWidget); + gtk_widget_show(_gtkWidget.get()); if (!_result.has_value()) { _loop.exec(); @@ -87,20 +87,20 @@ bool GtkOpenWithDialog::exec() { } void GtkOpenWithDialog::handleResponse(GtkOpenWithDialog *dialog, int responseId) { - GAppInfo *chosenAppInfo = nullptr; + Glib::RefPtr chosenAppInfo; dialog->_result = true; switch (responseId) { case GTK_RESPONSE_OK: - chosenAppInfo = gtk_app_chooser_get_app_info( - gtk_app_chooser_cast(dialog->_gtkWidget)); + chosenAppInfo = Glib::wrap(gtk_app_chooser_get_app_info( + GTK_APP_CHOOSER(dialog->_gtkWidget.get()))); if (chosenAppInfo) { - GList *uris = nullptr; - uris = g_list_prepend(uris, g_file_get_uri(dialog->_gfileInstance)); - g_app_info_launch_uris(chosenAppInfo, uris, nullptr, nullptr); - g_list_free(uris); - g_object_unref(chosenAppInfo); + try { + chosenAppInfo->launch_uri(dialog->_file->get_uri()); + } catch (...) { + } + chosenAppInfo = {}; } break; diff --git a/Telegram/SourceFiles/platform/linux/linux_wayland_integration.cpp b/Telegram/SourceFiles/platform/linux/linux_wayland_integration.cpp index a3ff42761..16db763d8 100644 --- a/Telegram/SourceFiles/platform/linux/linux_wayland_integration.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_wayland_integration.cpp @@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include +#include +#include +#include using namespace KWayland::Client; @@ -25,6 +29,18 @@ public: return _registry; } + [[nodiscard]] XdgExporter *xdgExporter() { + return _xdgExporter.get(); + } + + [[nodiscard]] PlasmaShell *plasmaShell() { + return _plasmaShell.get(); + } + + [[nodiscard]] AppMenuManager *appMenuManager() { + return _appMenuManager.get(); + } + [[nodiscard]] QEventLoop &interfacesLoop() { return _interfacesLoop; } @@ -35,12 +51,27 @@ public: private: ConnectionThread _connection; + ConnectionThread *_applicationConnection = nullptr; Registry _registry; + Registry _applicationRegistry; + std::unique_ptr _xdgExporter; + std::unique_ptr _plasmaShell; + std::unique_ptr _appMenuManager; QEventLoop _interfacesLoop; bool _interfacesAnnounced = false; }; -WaylandIntegration::Private::Private() { +WaylandIntegration::Private::Private() +: _applicationConnection(ConnectionThread::fromApplication(this)) { + _applicationRegistry.create(_applicationConnection); + _applicationRegistry.setup(); + + connect( + _applicationConnection, + &ConnectionThread::connectionDied, + &_applicationRegistry, + &Registry::destroy); + connect(&_connection, &ConnectionThread::connected, [=] { LOG(("Successfully connected to Wayland server at socket: %1") .arg(_connection.socketName())); @@ -62,6 +93,51 @@ WaylandIntegration::Private::Private() { } }); + connect( + &_applicationRegistry, + &Registry::exporterUnstableV2Announced, + [=](uint name, uint version) { + _xdgExporter = std::unique_ptr{ + _applicationRegistry.createXdgExporter(name, version), + }; + + connect( + _applicationConnection, + &ConnectionThread::connectionDied, + _xdgExporter.get(), + &XdgExporter::destroy); + }); + + connect( + &_applicationRegistry, + &Registry::plasmaShellAnnounced, + [=](uint name, uint version) { + _plasmaShell = std::unique_ptr{ + _applicationRegistry.createPlasmaShell(name, version), + }; + + connect( + _applicationConnection, + &ConnectionThread::connectionDied, + _plasmaShell.get(), + &PlasmaShell::destroy); + }); + + connect( + &_applicationRegistry, + &Registry::appMenuAnnounced, + [=](uint name, uint version) { + _appMenuManager = std::unique_ptr{ + _applicationRegistry.createAppMenuManager(name, version), + }; + + connect( + _applicationConnection, + &ConnectionThread::connectionDied, + _appMenuManager.get(), + &AppMenuManager::destroy); + }); + _connection.initConnection(); } @@ -89,5 +165,70 @@ bool WaylandIntegration::supportsXdgDecoration() { Registry::Interface::XdgDecorationUnstableV1); } +QString WaylandIntegration::nativeHandle(QWindow *window) { + if (const auto exporter = _private->xdgExporter()) { + if (const auto surface = Surface::fromWindow(window)) { + if (const auto exported = exporter->exportTopLevel( + surface, + surface)) { + QEventLoop loop; + QObject::connect( + exported, + &XdgExported::done, + &loop, + &QEventLoop::quit); + loop.exec(); + return exported->handle(); + } + } + } + return {}; +} + +bool WaylandIntegration::skipTaskbarSupported() { + return _private->plasmaShell(); +} + +void WaylandIntegration::skipTaskbar(QWindow *window, bool skip) { + const auto shell = _private->plasmaShell(); + if (!shell) { + return; + } + + const auto surface = Surface::fromWindow(window); + if (!surface) { + return; + } + + const auto plasmaSurface = shell->createSurface(surface, surface); + if (!plasmaSurface) { + return; + } + + plasmaSurface->setSkipTaskbar(skip); +} + +void WaylandIntegration::registerAppMenu( + QWindow *window, + const QString &serviceName, + const QString &objectPath) { + const auto manager = _private->appMenuManager(); + if (!manager) { + return; + } + + const auto surface = Surface::fromWindow(window); + if (!surface) { + return; + } + + const auto appMenu = manager->create(surface, surface); + if (!appMenu) { + return; + } + + appMenu->setAddress(serviceName, objectPath); +} + } // namespace internal } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/linux_wayland_integration.h b/Telegram/SourceFiles/platform/linux/linux_wayland_integration.h index b2f716184..a37f10e34 100644 --- a/Telegram/SourceFiles/platform/linux/linux_wayland_integration.h +++ b/Telegram/SourceFiles/platform/linux/linux_wayland_integration.h @@ -12,9 +12,16 @@ namespace internal { class WaylandIntegration { public: - static WaylandIntegration *Instance(); + [[nodiscard]] static WaylandIntegration *Instance(); void waitForInterfaceAnnounce(); - bool supportsXdgDecoration(); + [[nodiscard]] bool supportsXdgDecoration(); + [[nodiscard]] QString nativeHandle(QWindow *window); + [[nodiscard]] bool skipTaskbarSupported(); + void skipTaskbar(QWindow *window, bool skip); + void registerAppMenu( + QWindow *window, + const QString &serviceName, + const QString &objectPath); private: WaylandIntegration(); diff --git a/Telegram/SourceFiles/platform/linux/linux_wayland_integration_dummy.cpp b/Telegram/SourceFiles/platform/linux/linux_wayland_integration_dummy.cpp index 166e8b575..7ab26b1e2 100644 --- a/Telegram/SourceFiles/platform/linux/linux_wayland_integration_dummy.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_wayland_integration_dummy.cpp @@ -33,5 +33,22 @@ bool WaylandIntegration::supportsXdgDecoration() { return false; } +QString WaylandIntegration::nativeHandle(QWindow *window) { + return {}; +} + +bool WaylandIntegration::skipTaskbarSupported() { + return false; +} + +void WaylandIntegration::skipTaskbar(QWindow *window, bool skip) { +} + +void WaylandIntegration::registerAppMenu( + QWindow *window, + const QString &serviceName, + const QString &objectPath) { +} + } // namespace internal } // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.cpp index b6b4ee2a8..b5425a0a6 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_file_utilities.h" #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_glibmm_helper.h" +#include "platform/linux/linux_wayland_integration.h" #include "storage/localstorage.h" #include "base/openssl_help.h" #include "base/qt_adapters.h" @@ -20,11 +21,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +using Platform::internal::WaylandIntegration; + namespace Platform { namespace FileDialog { namespace XDP { namespace { +using Type = ::FileDialog::internal::Type; + constexpr auto kXDGDesktopPortalService = "org.freedesktop.portal.Desktop"_cs; constexpr auto kXDGDesktopPortalObjectPath = "/org/freedesktop/portal/desktop"_cs; constexpr auto kXDGDesktopPortalFileChooserInterface = "org.freedesktop.portal.FileChooser"_cs; @@ -33,6 +38,8 @@ constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; const char *filterRegExp = "^(.*)\\(([a-zA-Z0-9_.,*? +;#\\-\\[\\]@\\{\\}/!<>\\$%&=^~:\\|]*)\\)$"; +std::optional FileChooserPortalVersion; + auto QStringListToStd(const QStringList &list) { std::vector result; ranges::transform( @@ -69,44 +76,55 @@ auto MakeFilterList(const QString &filter) { return result; } -std::optional FileChooserPortalVersion() { - try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::BUS_TYPE_SESSION); - - auto reply = connection->call_sync( - std::string(kXDGDesktopPortalObjectPath), - std::string(kPropertiesInterface), - "Get", - base::Platform::MakeGlibVariant(std::tuple{ - Glib::ustring( - std::string(kXDGDesktopPortalFileChooserInterface)), - Glib::ustring("version"), - }), - std::string(kXDGDesktopPortalService)); - - return base::Platform::GlibVariantCast( - base::Platform::GlibVariantCast( - reply.get_child(0))); - } catch (const Glib::Error &e) { - static const auto NotSupportedErrors = { - "org.freedesktop.DBus.Error.Disconnected", - "org.freedesktop.DBus.Error.ServiceUnknown", - }; - - const auto errorName = Gio::DBus::ErrorUtils::get_remote_error(e); - if (ranges::contains(NotSupportedErrors, errorName)) { - return std::nullopt; +void ComputeFileChooserPortalVersion() { + const auto connection = [] { + try { + return Gio::DBus::Connection::get_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION); + } catch (...) { + return Glib::RefPtr(); } + }(); - LOG(("XDP File Dialog Error: %1").arg( - QString::fromStdString(e.what()))); - } catch (const std::exception &e) { - LOG(("XDP File Dialog Error: %1").arg( - QString::fromStdString(e.what()))); + if (!connection) { + return; } - return std::nullopt; + connection->call( + std::string(kXDGDesktopPortalObjectPath), + std::string(kPropertiesInterface), + "Get", + base::Platform::MakeGlibVariant(std::tuple{ + Glib::ustring( + std::string(kXDGDesktopPortalFileChooserInterface)), + Glib::ustring("version"), + }), + [=](const Glib::RefPtr &result) { + try { + auto reply = connection->call_finish(result); + + FileChooserPortalVersion = + base::Platform::GlibVariantCast( + base::Platform::GlibVariantCast( + reply.get_child(0))); + } catch (const Glib::Error &e) { + static const auto NotSupportedErrors = { + "org.freedesktop.DBus.Error.ServiceUnknown", + }; + + const auto errorName = + Gio::DBus::ErrorUtils::get_remote_error(e); + + if (!ranges::contains(NotSupportedErrors, errorName)) { + LOG(("XDP File Dialog Error: %1").arg( + QString::fromStdString(e.what()))); + } + } catch (const std::exception &e) { + LOG(("XDP File Dialog Error: %1").arg( + QString::fromStdString(e.what()))); + } + }, + std::string(kXDGDesktopPortalService)); } // This is a patched copy of file dialog from qxdgdesktopportal theme plugin. @@ -115,7 +133,7 @@ std::optional FileChooserPortalVersion() { // // XDP file dialog is a dialog obtained via a DBus service // provided by the current desktop environment. -class XDPFileDialog : public QDialog, public sigc::trackable { +class XDPFileDialog : public QDialog { public: enum ConditionType : uint { GlobalPattern = 0, @@ -171,15 +189,15 @@ public: int exec() override; + bool failedToOpen() { + return _failedToOpen; + } + private: void openPortal(); void gotResponse( - const Glib::RefPtr &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters); + uint response, + const std::map &results); void showHelper( Qt::WindowFlags windowFlags, @@ -191,11 +209,10 @@ private: rpl::producer<> rejected(); Glib::RefPtr _dbusConnection; - Glib::RefPtr _cancellable; uint _requestSignalId = 0; // Options - WId _winId = 0; + QWindow *_parent = nullptr; QFileDialog::Options _options; QFileDialog::AcceptMode _acceptMode = QFileDialog::AcceptOpen; QFileDialog::FileMode _fileMode = QFileDialog::ExistingFile; @@ -210,6 +227,7 @@ private: Glib::ustring _selectedMimeTypeFilter; Glib::ustring _selectedNameFilter; std::vector _selectedFiles; + bool _failedToOpen = false; rpl::event_stream<> _accept; rpl::event_stream<> _reject; @@ -245,10 +263,6 @@ XDPFileDialog::XDPFileDialog( } XDPFileDialog::~XDPFileDialog() { - if (_cancellable) { - _cancellable->cancel(); - } - if (_dbusConnection && _requestSignalId != 0) { _dbusConnection->signal_unsubscribe(_requestSignalId); } @@ -257,8 +271,13 @@ XDPFileDialog::~XDPFileDialog() { void XDPFileDialog::openPortal() { std::stringstream parentWindowId; - if (IsX11()) { - parentWindowId << "x11:" << std::hex << _winId; + if (const auto integration = WaylandIntegration::Instance()) { + if (const auto handle = integration->nativeHandle(_parent) + ; !handle.isEmpty()) { + parentWindowId << "wayland:" << handle.toStdString(); + } + } else if (IsX11() && _parent) { + parentWindowId << "x11:" << std::hex << _parent->winId(); } std::map options; @@ -400,16 +419,41 @@ void XDPFileDialog::openPortal() { + '/' + handleToken; + const auto responseCallback = crl::guard(this, [=]( + const Glib::RefPtr &connection, + const Glib::ustring &sender_name, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters) { + try { + auto parametersCopy = parameters; + + const auto response = base::Platform::GlibVariantCast( + parametersCopy.get_child(0)); + + const auto results = base::Platform::GlibVariantCast< + std::map< + Glib::ustring, + Glib::VariantBase + >>(parametersCopy.get_child(1)); + + gotResponse(response, results); + } catch (const std::exception &e) { + LOG(("XDP File Dialog Error: %1").arg( + QString::fromStdString(e.what()))); + + _reject.fire({}); + } + }); + _requestSignalId = _dbusConnection->signal_subscribe( - sigc::mem_fun(this, &XDPFileDialog::gotResponse), + responseCallback, {}, "org.freedesktop.portal.Request", "Response", requestPath); - // synchronize functor deletion by this cancellable - _cancellable = Gio::Cancellable::create(); - _dbusConnection->call( std::string(kXDGDesktopPortalObjectPath), std::string(kXDGDesktopPortalFileChooserInterface), @@ -421,24 +465,54 @@ void XDPFileDialog::openPortal() { _windowTitle, options, }), - [=](const Glib::RefPtr &result) { + crl::guard(this, [=](const Glib::RefPtr &result) { try { - _dbusConnection->call_finish(result); + auto reply = _dbusConnection->call_finish(result); + + const auto handle = base::Platform::GlibVariantCast< + Glib::ustring>(reply.get_child(0)); + + if (handle != requestPath) { + _dbusConnection->signal_unsubscribe( + _requestSignalId); + + _requestSignalId = _dbusConnection->signal_subscribe( + responseCallback, + {}, + "org.freedesktop.portal.Request", + "Response", + handle); + } } catch (const Glib::Error &e) { + static const auto NotSupportedErrors = { + "org.freedesktop.DBus.Error.ServiceUnknown", + }; + + const auto errorName = + Gio::DBus::ErrorUtils::get_remote_error(e); + + if (!ranges::contains(NotSupportedErrors, errorName)) { + LOG(("XDP File Dialog Error: %1").arg( + QString::fromStdString(e.what()))); + } + + crl::on_main([=] { + _failedToOpen = true; + _reject.fire({}); + }); + } catch (const std::exception &e) { LOG(("XDP File Dialog Error: %1").arg( QString::fromStdString(e.what()))); crl::on_main([=] { + _failedToOpen = true; _reject.fire({}); }); } - }, - _cancellable, + }), std::string(kXDGDesktopPortalService)); - } catch (const Glib::Error &e) { - LOG(("XDP File Dialog Error: %1").arg( - QString::fromStdString(e.what()))); - + } catch (...) { + _failedToOpen = true; _reject.fire({}); } } @@ -496,6 +570,9 @@ int XDPFileDialog::exec() { setResult(0); show(); + if (failedToOpen()) { + return result(); + } QPointer guard = this; @@ -562,30 +639,15 @@ void XDPFileDialog::showHelper( Qt::WindowModality windowModality, QWindow *parent) { _modal = windowModality != Qt::NonModal; - _winId = parent ? parent->winId() : 0; + _parent = parent; openPortal(); } void XDPFileDialog::gotResponse( - const Glib::RefPtr &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { + uint response, + const std::map &results) { try { - auto parametersCopy = parameters; - - const auto response = base::Platform::GlibVariantCast( - parametersCopy.get_child(0)); - - const auto results = base::Platform::GlibVariantCast< - std::map< - Glib::ustring, - Glib::VariantBase - >>(parametersCopy.get_child(1)); - if (!response) { if (const auto i = results.find("uris"); i != end(results)) { _selectedFiles = base::Platform::GlibVariantCast< @@ -639,14 +701,11 @@ rpl::producer<> XDPFileDialog::rejected() { } // namespace -bool Use(Type type) { - static const auto Version = FileChooserPortalVersion(); - return (FileDialogType() <= ImplementationType::XDP) - && Version.has_value() - && (type != Type::ReadFolder || *Version >= 3); +void Start() { + ComputeFileChooserPortalVersion(); } -bool Get( +std::optional Get( QPointer parent, QStringList &files, QByteArray &remoteContent, @@ -654,6 +713,12 @@ bool Get( const QString &filter, Type type, QString startFile) { + if (FileDialogType() > ImplementationType::XDP + || !FileChooserPortalVersion.has_value() + || (type == Type::ReadFolder && *FileChooserPortalVersion < 3)) { + return std::nullopt; + } + static const auto docRegExp = QRegularExpression("^/run/user/\\d+/doc"); if (cDialogLastPath().isEmpty() || cDialogLastPath().contains(docRegExp)) { @@ -683,6 +748,9 @@ bool Get( dialog.selectFile(startFile); const auto res = dialog.exec(); + if (dialog.failedToOpen()) { + return std::nullopt; + } if (type != Type::ReadFolder) { // Save last used directory for all queries except directory choosing. diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.h b/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.h index 1ded00e06..369fc8fea 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.h +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.h @@ -13,16 +13,14 @@ namespace Platform { namespace FileDialog { namespace XDP { -using Type = ::FileDialog::internal::Type; - -bool Use(Type type = Type::ReadFile); -bool Get( +void Start(); +std::optional Get( QPointer parent, QStringList &files, QByteArray &remoteContent, const QString &caption, const QString &filter, - Type type, + ::FileDialog::internal::Type type, QString startFile); } // namespace XDP diff --git a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp index 64d3db95d..6b7260a6f 100644 --- a/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_xdp_open_with_dialog.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/base_platform_info.h" #include "base/platform/linux/base_linux_glibmm_helper.h" +#include "platform/linux/linux_wayland_integration.h" #include "core/application.h" #include "window/window_controller.h" #include "base/openssl_help.h" @@ -16,11 +17,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#include #include #include #include +using Platform::internal::WaylandIntegration; + namespace Platform { namespace File { namespace internal { @@ -76,22 +78,25 @@ bool XDPOpenWithDialog::exec() { } const auto fdGuard = gsl::finally([&] { ::close(fd); }); - auto outFdList = Glib::RefPtr(); const auto parentWindowId = [&]() -> Glib::ustring { std::stringstream result; - if (const auto activeWindow = Core::App().activeWindow()) { - if (IsX11()) { - result - << "x11:" - << std::hex - << activeWindow - ->widget() - .get() - ->windowHandle() - ->winId(); - } + + const auto activeWindow = Core::App().activeWindow(); + if (!activeWindow) { + return result.str(); } + + const auto window = activeWindow->widget()->windowHandle(); + if (const auto integration = WaylandIntegration::Instance()) { + if (const auto handle = integration->nativeHandle(window) + ; !handle.isEmpty()) { + result << "wayland:" << handle.toStdString(); + } + } else if (IsX11()) { + result << "x11:" << std::hex << window->winId(); + } + return result.str(); }(); @@ -131,6 +136,10 @@ bool XDPOpenWithDialog::exec() { } }); + const auto fdList = Gio::UnixFDList::create(); + fdList->append(fd); + auto outFdList = Glib::RefPtr(); + connection->call_sync( std::string(kXDGDesktopPortalObjectPath), std::string(kXDGDesktopPortalOpenURIInterface), @@ -152,7 +161,7 @@ bool XDPOpenWithDialog::exec() { }, }), }), - Glib::wrap(g_unix_fd_list_new_from_array(&fd, 1)), + fdList, outFdList, std::string(kXDGDesktopPortalService)); diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 0d58ac4fc..9217659a0 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" #include "platform/linux/specific_linux.h" +#include "platform/linux/linux_wayland_integration.h" #include "history/history.h" #include "history/history_widget.h" #include "history/history_inner_widget.h" @@ -16,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "mainwindow.h" #include "core/application.h" +#include "core/core_settings.h" #include "core/sandbox.h" #include "boxes/peer_list_controllers.h" #include "boxes/about_box.h" @@ -24,10 +26,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "base/platform/base_platform_info.h" -#include "base/call_delayed.h" +#include "base/event_filter.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/input_fields.h" -#include "facades.h" #include "app.h" #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION @@ -59,6 +60,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Platform { namespace { +using internal::WaylandIntegration; +using WorkMode = Core::Settings::WorkMode; + constexpr auto kPanelTrayIconName = "kotatogram-panel"_cs; constexpr auto kMutePanelTrayIconName = "kotatogram-mute-panel"_cs; constexpr auto kAttentionPanelTrayIconName = "kotatogram-attention-panel"_cs; @@ -88,15 +92,15 @@ int TrayIconCustomId = 0; bool TrayIconCounterDisabled = false; #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION -bool XCBSkipTaskbar(QWindow *window, bool set) { +void XCBSkipTaskbar(QWindow *window, bool skip) { const auto connection = base::Platform::XCB::GetConnectionFromQt(); if (!connection) { - return false; + return; } const auto root = base::Platform::XCB::GetRootWindowFromQt(); if (!root.has_value()) { - return false; + return; } const auto stateAtom = base::Platform::XCB::GetAtom( @@ -104,7 +108,7 @@ bool XCBSkipTaskbar(QWindow *window, bool set) { "_NET_WM_STATE"); if (!stateAtom.has_value()) { - return false; + return; } const auto skipTaskbarAtom = base::Platform::XCB::GetAtom( @@ -112,7 +116,7 @@ bool XCBSkipTaskbar(QWindow *window, bool set) { "_NET_WM_STATE_SKIP_TASKBAR"); if (!skipTaskbarAtom.has_value()) { - return false; + return; } xcb_client_message_event_t xev; @@ -121,7 +125,7 @@ bool XCBSkipTaskbar(QWindow *window, bool set) { xev.sequence = 0; xev.window = window->winId(); xev.format = 32; - xev.data.data32[0] = set ? 1 : 0; + xev.data.data32[0] = skip ? 1 : 0; xev.data.data32[1] = *skipTaskbarAtom; xev.data.data32[2] = 0; xev.data.data32[3] = 0; @@ -134,19 +138,52 @@ bool XCBSkipTaskbar(QWindow *window, bool set) { XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, reinterpret_cast(&xev)); +} - return true; +void XCBSetDesktopFileName(QWindow *window) { + const auto connection = base::Platform::XCB::GetConnectionFromQt(); + if (!connection) { + return; + } + + const auto desktopFileAtom = base::Platform::XCB::GetAtom( + connection, + "_KDE_NET_WM_DESKTOP_FILE"); + + const auto utf8Atom = base::Platform::XCB::GetAtom( + connection, + "UTF8_STRING"); + + if (!desktopFileAtom.has_value() || !utf8Atom.has_value()) { + return; + } + + const auto filename = QGuiApplication::desktopFileName() + .chopped(8) + .toUtf8(); + + xcb_change_property( + connection, + XCB_PROP_MODE_REPLACE, + window->winId(), + *desktopFileAtom, + *utf8Atom, + 8, + filename.size(), + filename.data()); } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION -bool SkipTaskbar(QWindow *window, bool set) { +void SkipTaskbar(QWindow *window, bool skip) { + if (const auto integration = WaylandIntegration::Instance()) { + integration->skipTaskbar(window, skip); + } + #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION if (IsX11()) { - return XCBSkipTaskbar(window, set); + XCBSkipTaskbar(window, skip); } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION - - return false; } QString GetPanelIconName(int counter, bool muted) { @@ -464,8 +501,18 @@ bool UseUnityCounter() { bool IsSNIAvailable() { try { - const auto connection = Gio::DBus::Connection::get_sync( - Gio::DBus::BusType::BUS_TYPE_SESSION); + const auto connection = [] { + try { + return Gio::DBus::Connection::get_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION); + } catch (...) { + return Glib::RefPtr(); + } + }(); + + if (!connection) { + return false; + } auto reply = connection->call_sync( std::string(kSNIWatcherObjectPath), @@ -482,7 +529,6 @@ bool IsSNIAvailable() { reply.get_child(0))); } catch (const Glib::Error &e) { static const auto NotSupportedErrors = { - "org.freedesktop.DBus.Error.Disconnected", "org.freedesktop.DBus.Error.ServiceUnknown", }; @@ -525,7 +571,15 @@ bool IsAppMenuSupported() { // This call must be made from the same bus connection as DBusMenuExporter // So it must use QDBusConnection -void RegisterAppMenu(uint winId, const QString &menuPath) { +void RegisterAppMenu(QWindow *window, const QString &menuPath) { + if (const auto integration = WaylandIntegration::Instance()) { + integration->registerAppMenu( + window, + QDBusConnection::sessionBus().baseService(), + menuPath); + return; + } + auto message = QDBusMessage::createMethodCall( kAppMenuService.utf16(), kAppMenuObjectPath.utf16(), @@ -533,7 +587,7 @@ void RegisterAppMenu(uint winId, const QString &menuPath) { qsl("RegisterWindow")); message.setArguments({ - winId, + window->winId(), QVariant::fromValue(QDBusObjectPath(menuPath)) }); @@ -542,7 +596,11 @@ void RegisterAppMenu(uint winId, const QString &menuPath) { // This call must be made from the same bus connection as DBusMenuExporter // So it must use QDBusConnection -void UnregisterAppMenu(uint winId) { +void UnregisterAppMenu(QWindow *window) { + if (const auto integration = WaylandIntegration::Instance()) { + return; + } + auto message = QDBusMessage::createMethodCall( kAppMenuService.utf16(), kAppMenuObjectPath.utf16(), @@ -550,7 +608,7 @@ void UnregisterAppMenu(uint winId) { qsl("UnregisterWindow")); message.setArguments({ - winId + window->winId() }); QDBusConnection::sessionBus().send(message); @@ -659,6 +717,27 @@ void MainWindow::initHook() { } catch (...) { } + base::install_event_filter(windowHandle(), [=](not_null e) { + if (e->type() == QEvent::Expose) { + auto ee = static_cast(e.get()); + if (ee->region().isNull()) { + return base::EventFilterResult::Continue; + } + if (!windowHandle() + || windowHandle()->parent() + || !windowHandle()->isVisible()) { + return base::EventFilterResult::Continue; + } + handleNativeSurfaceChanged(true); + } else if (e->type() == QEvent::Hide) { + if (!windowHandle() || windowHandle()->parent()) { + return base::EventFilterResult::Continue; + } + handleNativeSurfaceChanged(false); + } + return base::EventFilterResult::Continue; + }); + if (_appMenuSupported) { LOG(("Using D-Bus global menu.")); } else { @@ -672,6 +751,10 @@ void MainWindow::initHook() { } #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION +#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION + XCBSetDesktopFileName(windowHandle()); +#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION + LOG(("System tray available: %1").arg(Logs::b(trayAvailable()))); } @@ -760,7 +843,7 @@ void MainWindow::handleSNIHostRegistered() { _sniAvailable = true; - if (Global::WorkMode().value() == dbiwmWindowOnly) { + if (Core::App().settings().workMode() == WorkMode::WindowOnly) { return; } @@ -776,7 +859,7 @@ void MainWindow::handleSNIHostRegistered() { SkipTaskbar( windowHandle(), - Global::WorkMode().value() == dbiwmTrayOnly); + Core::App().settings().workMode() == WorkMode::TrayOnly); } void MainWindow::handleSNIOwnerChanged( @@ -785,7 +868,7 @@ void MainWindow::handleSNIOwnerChanged( const QString &newOwner) { _sniAvailable = IsSNIAvailable(); - if (Global::WorkMode().value() == dbiwmWindowOnly) { + if (Core::App().settings().workMode() == WorkMode::WindowOnly) { return; } @@ -811,7 +894,8 @@ void MainWindow::handleSNIOwnerChanged( SkipTaskbar( windowHandle(), - (Global::WorkMode().value() == dbiwmTrayOnly) && trayAvailable()); + (Core::App().settings().workMode() == WorkMode::TrayOnly) + && trayAvailable()); } void MainWindow::handleAppMenuOwnerChanged( @@ -827,9 +911,9 @@ void MainWindow::handleAppMenuOwnerChanged( } if (_appMenuSupported && _mainMenuExporter) { - RegisterAppMenu(winId(), kMainMenuObjectPath.utf16()); + RegisterAppMenu(windowHandle(), kMainMenuObjectPath.utf16()); } else { - UnregisterAppMenu(winId()); + UnregisterAppMenu(windowHandle()); } } #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION @@ -868,10 +952,10 @@ void MainWindow::psSetupTrayIcon() { } } -void MainWindow::workmodeUpdated(DBIWorkMode mode) { +void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) { if (!trayAvailable()) { return; - } else if (mode == dbiwmWindowOnly) { + } else if (mode == WorkMode::WindowOnly) { #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION if (_sniTrayIcon) { _sniTrayIcon->setContextMenu(0); @@ -889,7 +973,7 @@ void MainWindow::workmodeUpdated(DBIWorkMode mode) { psSetupTrayIcon(); } - SkipTaskbar(windowHandle(), mode == dbiwmTrayOnly); + SkipTaskbar(windowHandle(), mode == WorkMode::TrayOnly); } void MainWindow::unreadCounterChangedHook() { @@ -1103,7 +1187,8 @@ void MainWindow::createGlobalMenu() { return; } - Ui::show(PrepareContactsBox(sessionController())); + sessionController()->show( + PrepareContactsBox(sessionController())); })); psAddContact = tools->addAction( @@ -1154,7 +1239,7 @@ void MainWindow::createGlobalMenu() { psMainMenu); if (_appMenuSupported) { - RegisterAppMenu(winId(), kMainMenuObjectPath.utf16()); + RegisterAppMenu(windowHandle(), kMainMenuObjectPath.utf16()); } updateGlobalMenu(); @@ -1277,22 +1362,20 @@ void MainWindow::updateGlobalMenuHook() { #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION -void MainWindow::handleVisibleChangedHook(bool visible) { - if (visible) { - base::call_delayed(1, this, [=] { - SkipTaskbar( - windowHandle(), - (Global::WorkMode().value() == dbiwmTrayOnly) - && trayAvailable()); - }); +void MainWindow::handleNativeSurfaceChanged(bool exist) { + if (exist) { + SkipTaskbar( + windowHandle(), + (Core::App().settings().workMode() == WorkMode::TrayOnly) + && trayAvailable()); } #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION if (_appMenuSupported && _mainMenuExporter) { - if (visible) { - RegisterAppMenu(winId(), kMainMenuObjectPath.utf16()); + if (exist) { + RegisterAppMenu(windowHandle(), kMainMenuObjectPath.utf16()); } else { - UnregisterAppMenu(winId()); + UnregisterAppMenu(windowHandle()); } } #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION @@ -1320,7 +1403,7 @@ MainWindow::~MainWindow() { delete _sniTrayIcon; if (_appMenuSupported) { - UnregisterAppMenu(winId()); + UnregisterAppMenu(windowHandle()); } delete _mainMenuExporter; diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.h b/Telegram/SourceFiles/platform/linux/main_window_linux.h index 04a7b2007..cc796bac2 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.h +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.h @@ -47,12 +47,11 @@ protected: void initHook() override; void unreadCounterChangedHook() override; void updateGlobalMenuHook() override; - void handleVisibleChangedHook(bool visible) override; void initTrayMenuHook() override; bool hasTrayIcon() const override; - void workmodeUpdated(DBIWorkMode mode) override; + void workmodeUpdated(Core::Settings::WorkMode mode) override; void createGlobalMenu() override; QSystemTrayIcon *trayIcon = nullptr; @@ -76,6 +75,7 @@ private: base::unique_qptr _trayIconMenuXEmbed; void updateIconCounters(); + void handleNativeSurfaceChanged(bool exist); #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION StatusNotifierItem *_sniTrayIcon = nullptr; diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 87b1a7059..82955b01c 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "main/main_session.h" #include "lang/lang_keys.h" +#include "base/weak_ptr.h" #include #include @@ -34,6 +35,8 @@ constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs; constexpr auto kInterface = kService; constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; +using namespace base::Platform; + struct ServerInformation { QString name; QString vendor; @@ -46,17 +49,15 @@ bool InhibitionSupported = false; std::optional CurrentServerInformation; QStringList CurrentCapabilities; -void StartServiceAsync( - Fn callback, - const Glib::RefPtr &cancellable = Glib::RefPtr()) { +void StartServiceAsync(Fn callback) { try { const auto connection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::BUS_TYPE_SESSION); - base::Platform::DBus::StartServiceByNameAsync( + DBus::StartServiceByNameAsync( connection, std::string(kService), - [=](Fn result) { + [=](Fn result) { try { result(); // get the error if any } catch (const Glib::Error &e) { @@ -76,15 +77,14 @@ void StartServiceAsync( QString::fromStdString(e.what()))); } - crl::on_main([=] { callback(); }); - }, - cancellable); + crl::on_main(callback); + }); return; } catch (...) { } - crl::on_main([=] { callback(); }); + crl::on_main(callback); } bool GetServiceRegistered() { @@ -94,7 +94,7 @@ bool GetServiceRegistered() { const auto hasOwner = [&] { try { - return base::Platform::DBus::NameHasOwner( + return DBus::NameHasOwner( connection, std::string(kService)); } catch (...) { @@ -105,7 +105,7 @@ bool GetServiceRegistered() { static const auto activatable = [&] { try { return ranges::contains( - base::Platform::DBus::ListActivatableNames(connection), + DBus::ListActivatableNames(connection), Glib::ustring(std::string(kService))); } catch (...) { return false; @@ -134,17 +134,17 @@ void GetServerInformation( try { auto reply = connection->call_finish(result); - const auto name = base::Platform::GlibVariantCast< - Glib::ustring>(reply.get_child(0)); + const auto name = GlibVariantCast( + reply.get_child(0)); - const auto vendor = base::Platform::GlibVariantCast< - Glib::ustring>(reply.get_child(1)); + const auto vendor = GlibVariantCast( + reply.get_child(1)); - const auto version = base::Platform::GlibVariantCast< - Glib::ustring>(reply.get_child(2)); + const auto version = GlibVariantCast( + reply.get_child(2)); - const auto specVersion = base::Platform::GlibVariantCast< - Glib::ustring>(reply.get_child(3)); + const auto specVersion = GlibVariantCast( + reply.get_child(3)); crl::on_main([=] { callback(ServerInformation{ @@ -195,8 +195,8 @@ void GetCapabilities(Fn callback) { QStringList value; ranges::transform( - base::Platform::GlibVariantCast< - std::vector>(reply.get_child(0)), + GlibVariantCast>( + reply.get_child(0)), ranges::back_inserter(value), QString::fromStdString); @@ -235,7 +235,7 @@ void GetInhibitionSupported(Fn callback) { std::string(kObjectPath), std::string(kPropertiesInterface), "Get", - base::Platform::MakeGlibVariant(std::tuple{ + MakeGlibVariant(std::tuple{ Glib::ustring(std::string(kInterface)), Glib::ustring("Inhibited"), }), @@ -286,7 +286,7 @@ bool Inhibited() { Gio::DBus::BusType::BUS_TYPE_SESSION); // a hack for snap's activation restriction - base::Platform::DBus::StartServiceByName( + DBus::StartServiceByName( connection, std::string(kService)); @@ -294,15 +294,14 @@ bool Inhibited() { std::string(kObjectPath), std::string(kPropertiesInterface), "Get", - base::Platform::MakeGlibVariant(std::tuple{ + MakeGlibVariant(std::tuple{ Glib::ustring(std::string(kInterface)), Glib::ustring("Inhibited"), }), std::string(kService)); - return base::Platform::GlibVariantCast( - base::Platform::GlibVariantCast( - reply.get_child(0))); + return GlibVariantCast( + GlibVariantCast(reply.get_child(0))); } catch (const Glib::Error &e) { LOG(("Native Notification Error: %1").arg( QString::fromStdString(e.what()))); @@ -357,16 +356,18 @@ Glib::ustring GetImageKey(const QVersionNumber &specificationVersion) { return "icon_data"; } -class NotificationData : public sigc::trackable { +class NotificationData final : public base::has_weak_ptr { public: using NotificationId = Window::Notifications::Manager::NotificationId; NotificationData( - const base::weak_ptr &manager, + not_null manager, + NotificationId id); + + [[nodiscard]] bool init( const QString &title, const QString &subtitle, const QString &msg, - NotificationId id, bool hideReplyButton); NotificationData(const NotificationData &other) = delete; @@ -381,10 +382,10 @@ public: void setImage(const QString &imagePath); private: - Glib::RefPtr _dbusConnection; - Glib::RefPtr _cancellable; - base::weak_ptr _manager; + const not_null _manager; + NotificationId _id; + Glib::RefPtr _dbusConnection; Glib::ustring _title; Glib::ustring _body; std::vector _actions; @@ -395,51 +396,81 @@ private: uint _actionInvokedSignalId = 0; uint _notificationRepliedSignalId = 0; uint _notificationClosedSignalId = 0; - NotificationId _id; - - void notificationShown( - const Glib::RefPtr &result); void notificationClosed(uint id, uint reason); void actionInvoked(uint id, const Glib::ustring &actionName); void notificationReplied(uint id, const Glib::ustring &text); - void signalEmitted( - const Glib::RefPtr &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters); - }; -using Notification = std::shared_ptr; +using Notification = std::unique_ptr; NotificationData::NotificationData( - const base::weak_ptr &manager, - const QString &title, - const QString &subtitle, - const QString &msg, - NotificationId id, - bool hideReplyButton) -: _cancellable(Gio::Cancellable::create()) -, _manager(manager) -, _title(title.toStdString()) -, _imageKey(GetImageKey(CurrentServerInformationValue().specVersion)) + not_null manager, + NotificationId id) +: _manager(manager) , _id(id) { +} + +bool NotificationData::init( + const QString &title, + const QString &subtitle, + const QString &msg, + bool hideReplyButton) { try { _dbusConnection = Gio::DBus::Connection::get_sync( Gio::DBus::BusType::BUS_TYPE_SESSION); } catch (const Glib::Error &e) { LOG(("Native Notification Error: %1").arg( QString::fromStdString(e.what()))); - - return; + return false; } + const auto weak = base::make_weak(this); const auto capabilities = CurrentCapabilities; + const auto signalEmitted = [=]( + const Glib::RefPtr &connection, + const Glib::ustring &sender_name, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &signal_name, + Glib::VariantContainerBase parameters) { + try { + if (signal_name == "ActionInvoked") { + const auto id = GlibVariantCast( + parameters.get_child(0)); + + const auto actionName = GlibVariantCast( + parameters.get_child(1)); + + crl::on_main(weak, [=] { actionInvoked(id, actionName); }); + } else if (signal_name == "NotificationReplied") { + const auto id = GlibVariantCast( + parameters.get_child(0)); + + const auto text = GlibVariantCast( + parameters.get_child(1)); + + crl::on_main(weak, [=] { notificationReplied(id, text); }); + } else if (signal_name == "NotificationClosed") { + const auto id = GlibVariantCast( + parameters.get_child(0)); + + const auto reason = GlibVariantCast( + parameters.get_child(1)); + + crl::on_main(weak, [=] { notificationClosed(id, reason); }); + } + } catch (const std::exception &e) { + LOG(("Native Notification Error: %1").arg( + QString::fromStdString(e.what()))); + } + }; + + _title = title.toStdString(); + _imageKey = GetImageKey(CurrentServerInformationValue().specVersion); + if (capabilities.contains(qsl("body-markup"))) { _body = subtitle.isEmpty() ? msg.toHtmlEscaped().toStdString() @@ -468,7 +499,7 @@ NotificationData::NotificationData( tr::lng_notification_reply(tr::now).toStdString()); _notificationRepliedSignalId = _dbusConnection->signal_subscribe( - sigc::mem_fun(this, &NotificationData::signalEmitted), + signalEmitted, std::string(kService), std::string(kInterface), "NotificationReplied", @@ -481,7 +512,7 @@ NotificationData::NotificationData( } _actionInvokedSignalId = _dbusConnection->signal_subscribe( - sigc::mem_fun(this, &NotificationData::signalEmitted), + signalEmitted, std::string(kService), std::string(kInterface), "ActionInvoked", @@ -515,18 +546,15 @@ NotificationData::NotificationData( QGuiApplication::desktopFileName().chopped(8).toStdString()); _notificationClosedSignalId = _dbusConnection->signal_subscribe( - sigc::mem_fun(this, &NotificationData::signalEmitted), + signalEmitted, std::string(kService), std::string(kInterface), "NotificationClosed", std::string(kObjectPath)); + return true; } NotificationData::~NotificationData() { - if (_cancellable) { - _cancellable->cancel(); - } - if (_dbusConnection) { if (_actionInvokedSignalId != 0) { _dbusConnection->signal_unsubscribe(_actionInvokedSignalId); @@ -544,17 +572,19 @@ NotificationData::~NotificationData() { void NotificationData::show() { // a hack for snap's activation restriction - StartServiceAsync([=] { + const auto weak = base::make_weak(this); + StartServiceAsync(crl::guard(weak, [=] { const auto iconName = _imageKey.empty() || _hints.find(_imageKey) == end(_hints) ? Glib::ustring(GetIconName().toStdString()) : Glib::ustring(); + const auto connection = _dbusConnection; - _dbusConnection->call( + connection->call( std::string(kObjectPath), std::string(kInterface), "Notify", - base::Platform::MakeGlibVariant(std::tuple{ + MakeGlibVariant(std::tuple{ Glib::ustring(std::string(AppName)), uint(0), iconName, @@ -564,32 +594,28 @@ void NotificationData::show() { _hints, -1, }), - sigc::mem_fun(this, &NotificationData::notificationShown), + [=](const Glib::RefPtr &result) { + try { + auto reply = connection->call_finish(result); + const auto notificationId = GlibVariantCast( + reply.get_child(0)); + crl::on_main(weak, [=] { + _notificationId = notificationId; + }); + return; + } catch (const Glib::Error &e) { + LOG(("Native Notification Error: %1").arg( + QString::fromStdString(e.what()))); + } catch (const std::exception &e) { + LOG(("Native Notification Error: %1").arg( + QString::fromStdString(e.what()))); + } + crl::on_main(weak, [=] { + _manager->clearNotification(_id); + }); + }, std::string(kService)); - }, _cancellable); -} - -void NotificationData::notificationShown( - const Glib::RefPtr &result) { - try { - auto reply = _dbusConnection->call_finish(result); - _notificationId = base::Platform::GlibVariantCast( - reply.get_child(0)); - - return; - } catch (const Glib::Error &e) { - LOG(("Native Notification Error: %1").arg( - QString::fromStdString(e.what()))); - } catch (const std::exception &e) { - LOG(("Native Notification Error: %1").arg( - QString::fromStdString(e.what()))); - } - - const auto manager = _manager; - const auto my = _id; - crl::on_main(manager, [=] { - manager->clearNotification(my); - }); + })); } void NotificationData::close() { @@ -597,11 +623,12 @@ void NotificationData::close() { std::string(kObjectPath), std::string(kInterface), "CloseNotification", - base::Platform::MakeGlibVariant(std::tuple{ + MakeGlibVariant(std::tuple{ _notificationId, }), {}, std::string(kService)); + _manager->clearNotification(_id); } void NotificationData::setImage(const QString &imagePath) { @@ -612,7 +639,7 @@ void NotificationData::setImage(const QString &imagePath) { const auto image = QImage(imagePath) .convertToFormat(QImage::Format_RGBA8888); - _hints[_imageKey] = base::Platform::MakeGlibVariant(std::tuple{ + _hints[_imageKey] = MakeGlibVariant(std::tuple{ image.width(), image.height(), image.bytesPerLine(), @@ -625,54 +652,9 @@ void NotificationData::setImage(const QString &imagePath) { }); } -void NotificationData::signalEmitted( - const Glib::RefPtr &connection, - const Glib::ustring &sender_name, - const Glib::ustring &object_path, - const Glib::ustring &interface_name, - const Glib::ustring &signal_name, - const Glib::VariantContainerBase ¶meters) { - try { - auto parametersCopy = parameters; - - if (signal_name == "ActionInvoked") { - const auto id = base::Platform::GlibVariantCast( - parametersCopy.get_child(0)); - - const auto actionName = base::Platform::GlibVariantCast< - Glib::ustring>(parametersCopy.get_child(1)); - - actionInvoked(id, actionName); - } else if (signal_name == "NotificationReplied") { - const auto id = base::Platform::GlibVariantCast( - parametersCopy.get_child(0)); - - const auto text = base::Platform::GlibVariantCast( - parametersCopy.get_child(1)); - - notificationReplied(id, text); - } else if (signal_name == "NotificationClosed") { - const auto id = base::Platform::GlibVariantCast( - parametersCopy.get_child(0)); - - const auto reason = base::Platform::GlibVariantCast( - parametersCopy.get_child(1)); - - notificationClosed(id, reason); - } - } catch (const std::exception &e) { - LOG(("Native Notification Error: %1").arg( - QString::fromStdString(e.what()))); - } -} - void NotificationData::notificationClosed(uint id, uint reason) { if (id == _notificationId) { - const auto manager = _manager; - const auto my = _id; - crl::on_main(manager, [=] { - manager->clearNotification(my); - }); + _manager->clearNotification(_id); } } @@ -685,17 +667,9 @@ void NotificationData::actionInvoked( if (actionName == "default" || actionName == "mail-reply-sender") { - const auto manager = _manager; - const auto my = _id; - crl::on_main(manager, [=] { - manager->notificationActivated(my); - }); + _manager->notificationActivated(_id); } else if (actionName == "mail-mark-read") { - const auto manager = _manager; - const auto my = _id; - crl::on_main(manager, [=] { - manager->notificationReplied(my, {}); - }); + _manager->notificationReplied(_id, {}); } } @@ -703,13 +677,9 @@ void NotificationData::notificationReplied( uint id, const Glib::ustring &text) { if (id == _notificationId) { - const auto manager = _manager; - const auto my = _id; - crl::on_main(manager, [=] { - manager->notificationReplied( - my, - { QString::fromStdString(text), {} }); - }); + _manager->notificationReplied( + _id, + { QString::fromStdString(text), {} }); } } @@ -823,17 +793,19 @@ public: ~Private(); private: + const not_null _manager; + base::flat_map< FullPeer, base::flat_map> _notifications; Window::Notifications::CachedUserpics _cachedUserpics; - base::weak_ptr _manager; + }; Manager::Private::Private(not_null manager, Type type) -: _cachedUserpics(type) -, _manager(manager) { +: _manager(manager) +, _cachedUserpics(type) { if (!Supported()) { return; } @@ -878,13 +850,18 @@ void Manager::Private::showNotification( .sessionId = peer->session().uniqueId(), .peerId = peer->id }; - auto notification = std::make_shared( + const auto notificationId = NotificationId{ .full = key, .msgId = msgId }; + auto notification = std::make_unique( _manager, + notificationId); + const auto inited = notification->init( title, subtitle, msg, - NotificationId{ .full = key, .msgId = msgId }, hideReplyButton); + if (!inited) { + return; + } if (!hideNameAndPhoto) { const auto userpicKey = peer->userpicUniqueKey(userpicView); @@ -893,22 +870,24 @@ void Manager::Private::showNotification( } auto i = _notifications.find(key); - if (i != _notifications.cend()) { + if (i != end(_notifications)) { auto j = i->second.find(msgId); - if (j != i->second.end()) { - auto oldNotification = j->second; + if (j != end(i->second)) { + auto oldNotification = std::move(j->second); i->second.erase(j); oldNotification->close(); i = _notifications.find(key); } } - if (i == _notifications.cend()) { + if (i == end(_notifications)) { i = _notifications.emplace( key, base::flat_map()).first; } - i->second.emplace(msgId, notification); - notification->show(); + const auto j = i->second.emplace( + msgId, + std::move(notification)).first; + j->second->show(); } void Manager::Private::clearAll() { diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index d4becaf7e..c3ff1c0a8 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -8,14 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "platform/platform_notifications_manager.h" -#include "base/weak_ptr.h" namespace Platform { namespace Notifications { -class Manager - : public Window::Notifications::NativeManager - , public base::has_weak_ptr { +class Manager : public Window::Notifications::NativeManager { public: Manager(not_null system); void clearNotification(NotificationId id); diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 26a6b2ca3..002f0150f 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -28,7 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/linux/base_linux_dbus_utilities.h" #include "base/platform/linux/base_linux_xdp_utilities.h" #include "platform/linux/linux_notification_service_watcher.h" -#include "platform/linux/linux_mpris_support.h" +#include "platform/linux/linux_xdp_file_dialog.h" #include "platform/linux/linux_gsd_media_keys.h" #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION @@ -51,6 +51,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#ifdef Q_OS_LINUX +#include +#endif // Q_OS_LINUX #include #include #include @@ -98,18 +101,22 @@ PortalAutostart::PortalAutostart(bool start, bool silent) { const auto parentWindowId = [&]() -> Glib::ustring { std::stringstream result; - if (const auto activeWindow = Core::App().activeWindow()) { - if (IsX11()) { - result - << "x11:" - << std::hex - << activeWindow - ->widget() - .get() - ->windowHandle() - ->winId(); - } + + const auto activeWindow = Core::App().activeWindow(); + if (!activeWindow) { + return result.str(); } + + const auto window = activeWindow->widget()->windowHandle(); + if (const auto integration = WaylandIntegration::Instance()) { + if (const auto handle = integration->nativeHandle(window) + ; !handle.isEmpty()) { + result << "wayland:" << handle.toStdString(); + } + } else if (IsX11()) { + result << "x11:" << std::hex << window->winId(); + } + return result.str(); }(); @@ -413,26 +420,136 @@ void SetGtkScaleFactor() { cSetScreenScale(style::CheckScale(scaleFactor * 100)); } +void SetDarkMode() { + static const auto Inited = [] { + QObject::connect( + qGuiApp, + &QGuiApplication::paletteChanged, + SetDarkMode); + +#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION + using XDPSettingWatcher = base::Platform::XDP::SettingWatcher; + static const XDPSettingWatcher KdeColorSchemeWatcher( + [=]( + const Glib::ustring &group, + const Glib::ustring &key, + const Glib::VariantBase &value) { + if (group == "org.kde.kdeglobals.General" + && key == "ColorScheme") { + SetDarkMode(); + } + }); +#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION + + const auto integration = BaseGtkIntegration::Instance(); + if (integration) { + integration->connectToSetting( + "gtk-theme-name", + SetDarkMode); + + if (integration->checkVersion(3, 0, 0)) { + integration->connectToSetting( + "gtk-application-prefer-dark-theme", + SetDarkMode); + } + } + + return true; + }(); + + std::optional result; + const auto setter = gsl::finally([&] { + crl::on_main([=] { + Core::App().settings().setSystemDarkMode(result); + }); + }); + + const auto styleName = QApplication::style()->metaObject()->className(); + if (styleName != qstr("QFusionStyle") + && styleName != qstr("QWindowsStyle")) { + result = false; + + const auto paletteBackgroundGray = qGray( + QPalette().color(QPalette::Window).rgb()); + + if (paletteBackgroundGray < kDarkColorLimit) { + result = true; + return; + } + } + +#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION + try { + using namespace base::Platform::XDP; + + const auto kdeBackgroundColorOptional = ReadSetting( + "org.kde.kdeglobals.Colors:Window", + "BackgroundNormal"); + + if (kdeBackgroundColorOptional.has_value()) { + const auto kdeBackgroundColorList = QString::fromStdString( + base::Platform::GlibVariantCast( + *kdeBackgroundColorOptional)).split(','); + + if (kdeBackgroundColorList.size() >= 3) { + result = false; + + const auto kdeBackgroundGray = qGray( + kdeBackgroundColorList[0].toInt(), + kdeBackgroundColorList[1].toInt(), + kdeBackgroundColorList[2].toInt()); + + if (kdeBackgroundGray < kDarkColorLimit) { + result = true; + return; + } + } + } + } catch (...) { + } +#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION + + const auto integration = BaseGtkIntegration::Instance(); + if (integration) { + if (integration->checkVersion(3, 0, 0)) { + const auto preferDarkTheme = integration->getBoolSetting( + qsl("gtk-application-prefer-dark-theme")); + + if (preferDarkTheme.has_value()) { + result = false; + + if (*preferDarkTheme) { + result = true; + return; + } + } + } + + const auto themeName = integration->getStringSetting( + qsl("gtk-theme-name")); + + if (themeName.has_value()) { + result = false; + + if (themeName->contains(qsl("-dark"), Qt::CaseInsensitive)) { + result = true; + return; + } + } + } +} + } // namespace void SetWatchingMediaKeys(bool watching) { #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION - static std::unique_ptr MPRISInstance; static std::unique_ptr GSDInstance; if (watching) { - if (!MPRISInstance) { - MPRISInstance = std::make_unique(); - } - if (!GSDInstance) { GSDInstance = std::make_unique(); } } else { - if (MPRISInstance) { - MPRISInstance = nullptr; - } - if (GSDInstance) { GSDInstance = nullptr; } @@ -506,119 +623,7 @@ QImage GetImageFromClipboard() { } std::optional IsDarkMode() { - std::optional failResult; - - if (static auto Once = false; !std::exchange(Once, true)) { - const auto onChanged = [] { - Core::Sandbox::Instance().customEnterFromEventLoop([] { - Core::App().settings().setSystemDarkMode(IsDarkMode()); - }); - }; - - QObject::connect( - qGuiApp, - &QGuiApplication::paletteChanged, - onChanged); - -#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION - using XDPSettingWatcher = base::Platform::XDP::SettingWatcher; - static const XDPSettingWatcher KdeColorSchemeWatcher( - [=]( - const Glib::ustring &group, - const Glib::ustring &key, - const Glib::VariantBase &value) { - if (group == "org.kde.kdeglobals.General" - && key == "ColorScheme") { - onChanged(); - } - }); -#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION - - const auto integration = BaseGtkIntegration::Instance(); - if (integration) { - integration->connectToSetting( - "gtk-theme-name", - onChanged); - - if (integration->checkVersion(3, 0, 0)) { - integration->connectToSetting( - "gtk-application-prefer-dark-theme", - onChanged); - } - } - } - - const auto styleName = QApplication::style()->metaObject()->className(); - if (styleName != qstr("QFusionStyle") - && styleName != qstr("QWindowsStyle")) { - failResult = false; - - const auto paletteBackgroundGray = qGray( - QPalette().color(QPalette::Window).rgb()); - - if (paletteBackgroundGray < kDarkColorLimit) { - return true; - } - } - -#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION - try { - using namespace base::Platform::XDP; - - const auto kdeBackgroundColorOptional = ReadSetting( - "org.kde.kdeglobals.Colors:Window", - "BackgroundNormal"); - - if (kdeBackgroundColorOptional.has_value()) { - const auto kdeBackgroundColorList = QString::fromStdString( - base::Platform::GlibVariantCast( - *kdeBackgroundColorOptional)).split(','); - - if (kdeBackgroundColorList.size() >= 3) { - failResult = false; - - const auto kdeBackgroundGray = qGray( - kdeBackgroundColorList[0].toInt(), - kdeBackgroundColorList[1].toInt(), - kdeBackgroundColorList[2].toInt()); - - if (kdeBackgroundGray < kDarkColorLimit) { - return true; - } - } - } - } catch (...) { - } -#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION - - const auto integration = BaseGtkIntegration::Instance(); - if (integration) { - if (integration->checkVersion(3, 0, 0)) { - const auto preferDarkTheme = integration->getBoolSetting( - qsl("gtk-application-prefer-dark-theme")); - - if (preferDarkTheme.has_value()) { - failResult = false; - - if (*preferDarkTheme) { - return true; - } - } - } - - const auto themeName = integration->getStringSetting( - qsl("gtk-theme-name")); - - if (themeName.has_value()) { - failResult = false; - - if (themeName->contains(qsl("-dark"), Qt::CaseInsensitive)) { - return true; - } - } - } - - return failResult; + return Core::App().settings().systemDarkMode(); } bool AutostartSupported() { @@ -636,9 +641,15 @@ bool TrayIconSupported() { } bool SkipTaskbarSupported() { + if (const auto integration = WaylandIntegration::Instance()) { + return integration->skipTaskbarSupported(); + } + #ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION - return IsX11() - && base::Platform::XCB::IsSupportedByWM("_NET_WM_STATE_SKIP_TASKBAR"); + if (IsX11()) { + return base::Platform::XCB::IsSupportedByWM( + "_NET_WM_STATE_SKIP_TASKBAR"); + } #endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION return false; @@ -646,9 +657,6 @@ bool SkipTaskbarSupported() { } // namespace Platform -void psWriteDump() { -} - void psActivateProcess(uint64 pid) { // objc_activateProgram(); } @@ -737,6 +745,10 @@ namespace Platform { void start() { LOG(("Launcher filename: %1").arg(QGuiApplication::desktopFileName())); +#ifndef DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + qputenv("QT_WAYLAND_SHELL_INTEGRATION", "desktop-app-xdg-shell;xdg-shell;wl-shell"); +#endif // !DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + qputenv("PULSE_PROP_application.name", AppName.utf8()); qputenv("PULSE_PROP_application.icon_name", GetIconName().toLatin1()); @@ -748,15 +760,10 @@ void start() { if (const auto integration = BaseGtkIntegration::Instance()) { integration->prepareEnvironment(); - integration->load(); } else { g_warning("GTK integration is disabled, some features unavailable."); } - if (const auto integration = GtkIntegration::Instance()) { - integration->load(); - } - #ifdef DESKTOP_APP_USE_PACKAGED_RLOTTIE g_warning( "Application has been built with foreign rlottie, " @@ -953,11 +960,12 @@ namespace ThirdParty { void start() { if (const auto integration = BaseGtkIntegration::Instance()) { + integration->load(); integration->initializeSettings(); } - if (!cQtScale()) { - SetGtkScaleFactor(); + if (const auto integration = GtkIntegration::Instance()) { + integration->load(); } // wait for interface announce to know if native window frame is supported @@ -965,8 +973,12 @@ void start() { integration->waitForInterfaceAnnounce(); } + SetGtkScaleFactor(); + crl::async(SetDarkMode); + #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION NSWInstance = std::make_unique(); + FileDialog::XDP::Start(); #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION } @@ -1013,6 +1025,14 @@ void psAutoStart(bool start, bool silent) { void psSendToMenu(bool send, bool silent) { } +void sendfileFallback(FILE *out, FILE *in) { + static const int BufSize = 65536; + char buf[BufSize]; + while (size_t size = fread(buf, 1, BufSize, in)) { + fwrite(buf, 1, size, out); + } +} + bool linuxMoveFile(const char *from, const char *to) { FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb"); if (!ffrom) { @@ -1023,11 +1043,6 @@ bool linuxMoveFile(const char *from, const char *to) { fclose(ffrom); return false; } - static const int BufSize = 65536; - char buf[BufSize]; - while (size_t size = fread(buf, 1, BufSize, ffrom)) { - fwrite(buf, 1, size, fto); - } struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c //let's say this wont fail since you already worked OK on that fp @@ -1036,6 +1051,32 @@ bool linuxMoveFile(const char *from, const char *to) { fclose(fto); return false; } + +#ifdef Q_OS_LINUX + ssize_t copied = sendfile( + fileno(fto), + fileno(ffrom), + nullptr, + fst.st_size); + if (copied == -1) { + DEBUG_LOG(("Update Error: " + "Copy by sendfile '%1' to '%2' failed, error: %3, fallback now." + ).arg(from + ).arg(to + ).arg(errno)); + sendfileFallback(fto, ffrom); + } else { + DEBUG_LOG(("Update Info: " + "Copy by sendfile '%1' to '%2' done, size: %3, result: %4." + ).arg(from + ).arg(to + ).arg(fst.st_size + ).arg(copied)); + } +#else // Q_OS_LINUX + sendfileFallback(fto, ffrom); +#endif // Q_OS_LINUX + //update to the same uid/gid if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) { fclose(ffrom); diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.h b/Telegram/SourceFiles/platform/linux/specific_linux.h index c1ce882ba..7edf24c5c 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.h +++ b/Telegram/SourceFiles/platform/linux/specific_linux.h @@ -26,6 +26,9 @@ void InstallLauncher(bool force = false); inline void IgnoreApplicationActivationRightNow() { } +inline void WriteCrashDumpDetails() { +} + } // namespace Platform inline void psCheckLocalSocket(const QString &serverName) { @@ -35,8 +38,6 @@ inline void psCheckLocalSocket(const QString &serverName) { } } -void psWriteDump(); - void psActivateProcess(uint64 pid = 0); QString psAppDataPath(); void psAutoStart(bool start, bool silent = false); diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.h b/Telegram/SourceFiles/platform/mac/main_window_mac.h index bdee966d3..f81c1832b 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.h +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.h @@ -71,7 +71,7 @@ protected: void updateGlobalMenuHook() override; - void workmodeUpdated(DBIWorkMode mode) override; + void workmodeUpdated(Core::Settings::WorkMode mode) override; QSystemTrayIcon *trayIcon = nullptr; QMenu *trayIconMenu = nullptr; diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.mm b/Telegram/SourceFiles/platform/mac/main_window_mac.mm index 31d4e3393..8fe36686c 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.mm +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.mm @@ -45,7 +45,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include -#include @interface MainWindowObserver : NSObject { } @@ -106,25 +105,6 @@ private: #endif // OS_MAC_OLD -class EventFilter : public QAbstractNativeEventFilter { -public: - EventFilter(not_null window) : _window(window) { - } - - bool nativeEventFilter( - const QByteArray &eventType, - void *message, - long *result) { - return Core::Sandbox::Instance().customEnterFromEventLoop([&] { - return _window->psFilterNativeEvent(message); - }); - } - -private: - not_null _window; - -}; - [[nodiscard]] QImage TrayIconBack(bool darkMode, bool selected = false) { static const auto WithColor = [](QColor color) { return st::macTrayIcon.instance(color, 100); @@ -175,8 +155,6 @@ public: void enableShadow(WId winId); - bool filterNativeEvent(void *event); - void willEnterFullScreen(); void willExitFullScreen(); void didExitFullScreen(); @@ -210,8 +188,6 @@ private: int _generalPasteboardChangeCount = -1; bool _generalPasteboardHasText = false; - EventFilter _nativeEventFilter; - }; } // namespace Platform @@ -239,11 +215,11 @@ private: } - (void) screenIsLocked:(NSNotification *)aNotification { - Global::SetScreenIsLocked(true); + Core::App().setScreenIsLocked(true); } - (void) screenIsUnlocked:(NSNotification *)aNotification { - Global::SetScreenIsLocked(false); + Core::App().setScreenIsLocked(false); } - (void) windowWillEnterFullScreen:(NSNotification *)aNotification { @@ -283,8 +259,7 @@ void ForceDisabled(QAction *action, bool disabled) { MainWindow::Private::Private(not_null window) : _public(window) -, _observer([[MainWindowObserver alloc] init:this]) -, _nativeEventFilter(window) { +, _observer([[MainWindowObserver alloc] init:this]) { _generalPasteboard = [NSPasteboard generalPasteboard]; @autoreleasepool { @@ -294,11 +269,6 @@ MainWindow::Private::Private(not_null window) [[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsLocked:) name:Q2NSString(strNotificationAboutScreenLocked()) object:nil]; [[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsUnlocked:) name:Q2NSString(strNotificationAboutScreenUnlocked()) object:nil]; -#ifndef OS_MAC_STORE - // Register defaults for the whitelist of apps that want to use media keys - [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers], kMediaKeyUsingBundleIdentifiersDefaultsKey, nil]]; -#endif // !OS_MAC_STORE - } } @@ -493,21 +463,6 @@ void MainWindow::Private::enableShadow(WId winId) { // [[(NSView*)winId window] setHasShadow:YES]; } -bool MainWindow::Private::filterNativeEvent(void *event) { - NSEvent *e = static_cast(event); - if (e && [e type] == NSSystemDefined && [e subtype] == SPSystemDefinedEventMediaKeys) { -#ifndef OS_MAC_STORE - // If event tap is not installed, handle events that reach the app instead - if (![SPMediaKeyTap usesGlobalMediaKeyTap]) { - return objc_handleMediaKeyEvent(e); - } -#else // !OS_MAC_STORE - return objc_handleMediaKeyEvent(e); -#endif // else for !OS_MAC_STORE - } - return false; -} - MainWindow::Private::~Private() { [_observer release]; } @@ -515,8 +470,6 @@ MainWindow::Private::~Private() { MainWindow::MainWindow(not_null controller) : Window::MainWindow(controller) , _private(std::make_unique(this)) { - QCoreApplication::instance()->installNativeEventFilter( - &_private->_nativeEventFilter); #ifndef OS_MAC_OLD auto forceOpenGL = std::make_unique(this); @@ -614,9 +567,9 @@ void MainWindow::psSetupTrayIcon() { trayIcon->show(); } -void MainWindow::workmodeUpdated(DBIWorkMode mode) { +void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) { psSetupTrayIcon(); - if (mode == dbiwmWindowOnly) { + if (mode == Core::Settings::WorkMode::WindowOnly) { if (trayIcon) { trayIcon->setContextMenu(0); delete trayIcon; @@ -793,7 +746,7 @@ void MainWindow::createGlobalMenu() { if (!sessionController()) { return; } - Ui::show(PrepareContactsBox(sessionController())); + sessionController()->show(PrepareContactsBox(sessionController())); })); { auto callback = [=] { @@ -954,10 +907,6 @@ void MainWindow::updateGlobalMenuHook() { ForceDisabled(psClearFormat, !canApplyMarkdown); } -bool MainWindow::psFilterNativeEvent(void *event) { - return _private->filterNativeEvent(event); -} - bool MainWindow::eventFilter(QObject *obj, QEvent *evt) { QEvent::Type t = evt->type(); if (t == QEvent::FocusIn || t == QEvent::FocusOut) { diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index 594d7228f..499cb8fed 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/mac/notifications_manager_mac.h" +#include "core/application.h" +#include "core/core_settings.h" #include "base/platform/base_platform_info.h" #include "platform/platform_specific.h" #include "base/platform/mac/base_utilities_mac.h" @@ -15,7 +17,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/empty_userpic.h" #include "main/main_session.h" #include "mainwindow.h" -#include "facades.h" #include "styles/style_window.h" #include @@ -172,7 +173,7 @@ void Create(Window::Notifications::System *system) { } } -class Manager::Private : public QObject, private base::Subscriber { +class Manager::Private : public QObject { public: Private(Manager *manager); @@ -224,20 +225,22 @@ private: ClearFinish>; std::vector _clearingTasks; + rpl::lifetime _lifetime; + }; Manager::Private::Private(Manager *manager) : _managerId(openssl::RandomValue()) , _managerIdString(QString::number(_managerId)) , _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId]) { - updateDelegate(); - subscribe(Global::RefWorkMode(), [this](DBIWorkMode mode) { + Core::App().settings().workModeValue( + ) | rpl::start_with_next([=](Core::Settings::WorkMode mode) { // We need to update the delegate _after_ the tray icon change was done in Qt. // Because Qt resets the delegate. crl::on_main(this, [=] { updateDelegate(); }); - }); + }, _lifetime); } void Manager::Private::showNotification( diff --git a/Telegram/SourceFiles/platform/mac/specific_mac.h b/Telegram/SourceFiles/platform/mac/specific_mac.h index 91b97a584..adc5d3cdd 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac.h +++ b/Telegram/SourceFiles/platform/mac/specific_mac.h @@ -52,8 +52,6 @@ inline void psCheckLocalSocket(const QString &serverName) { } } -void psWriteDump(); - void psActivateProcess(uint64 pid = 0); QString psAppDataPath(); void psAutoStart(bool start, bool silent = false); diff --git a/Telegram/SourceFiles/platform/mac/specific_mac.mm b/Telegram/SourceFiles/platform/mac/specific_mac.mm index d6726a80a..6b283d461 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac.mm +++ b/Telegram/SourceFiles/platform/mac/specific_mac.mm @@ -33,17 +33,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include -#include #include #include -void psWriteDump() { -#ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS - double v = objc_appkitVersion(); - CrashReports::dump() << "OS-Version: " << v; -#endif // DESKTOP_APP_DISABLE_CRASH_REPORTS -} - void psActivateProcess(uint64 pid) { if (!pid) { const auto window = Core::App().activeWindow(); @@ -113,6 +105,13 @@ std::optional IsDarkMode() { : std::nullopt; } +void WriteCrashDumpDetails() { +#ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS + double v = objc_appkitVersion(); + CrashReports::dump() << "OS-Version: " << v; +#endif // DESKTOP_APP_DISABLE_CRASH_REPORTS +} + void RegisterCustomScheme(bool force) { OSStatus result = LSSetDefaultHandlerForURLScheme(CFSTR("tg"), (CFStringRef)[[NSBundle mainBundle] bundleIdentifier]); DEBUG_LOG(("App Info: set default handler for 'tg' scheme result: %1").arg(result)); diff --git a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm index b0714c5e7..edd85a666 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac_p.mm +++ b/Telegram/SourceFiles/platform/mac/specific_mac_p.mm @@ -33,16 +33,54 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include -#include + +using Platform::Q2NSString; +using Platform::NS2QString; namespace { constexpr auto kIgnoreActivationTimeoutMs = 500; +NSMenuItem *CreateMenuItem( + QString title, + rpl::lifetime &lifetime, + Fn callback, + bool enabled = true) { + id block = [^{ + Core::Sandbox::Instance().customEnterFromEventLoop(callback); + } copy]; + + NSMenuItem *item = [[NSMenuItem alloc] + initWithTitle:Q2NSString(title) + action:@selector(invoke) + keyEquivalent:@""]; + [item setTarget:block]; + [item setEnabled:enabled]; + + lifetime.add([=] { + [block release]; + }); + return [item autorelease]; +} + } // namespace -using Platform::Q2NSString; -using Platform::NS2QString; +@interface RpMenu : NSMenu { +} + +- (rpl::lifetime &) lifetime; + +@end // @interface Menu + +@implementation RpMenu { + rpl::lifetime _lifetime; +} + +- (rpl::lifetime &) lifetime { + return _lifetime; +} + +@end // @implementation Menu @interface qVisualize : NSObject { } @@ -92,24 +130,19 @@ using Platform::NS2QString; } - (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag; -- (void) applicationDidFinishLaunching:(NSNotification *)aNotification; - (void) applicationDidBecomeActive:(NSNotification *)aNotification; - (void) applicationDidResignActive:(NSNotification *)aNotification; - (void) receiveWakeNote:(NSNotification*)note; -- (void) setWatchingMediaKeys:(bool)watching; -- (bool) isWatchingMediaKeys; -- (void) mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event; - - (void) ignoreApplicationActivationRightNow; +- (NSMenu *) applicationDockMenu:(NSApplication *)sender; + @end // @interface ApplicationDelegate ApplicationDelegate *_sharedDelegate = nil; @implementation ApplicationDelegate { - SPMediaKeyTap *_keyTap; - bool _watchingMediaKeys; bool _ignoreActivation; base::Timer _ignoreActivationStop; } @@ -124,24 +157,10 @@ ApplicationDelegate *_sharedDelegate = nil; } - (void) applicationDidFinishLaunching:(NSNotification *)aNotification { - _keyTap = nullptr; - _watchingMediaKeys = false; _ignoreActivation = false; _ignoreActivationStop.setCallback([self] { _ignoreActivation = false; }); -#ifndef OS_MAC_STORE - if ([SPMediaKeyTap usesGlobalMediaKeyTap]) { - if (!Platform::IsMac10_14OrGreater()) { - _keyTap = [[SPMediaKeyTap alloc] initWithDelegate:self]; - } else { - // In macOS Mojave it requires accessibility features. - LOG(("Media key monitoring disabled starting with Mojave.")); - } - } else { - LOG(("Media key monitoring disabled")); - } -#endif // else for !OS_MAC_STORE } - (void) applicationDidBecomeActive:(NSNotification *)aNotification { @@ -175,46 +194,56 @@ ApplicationDelegate *_sharedDelegate = nil; }); } -- (void) setWatchingMediaKeys:(bool)watching { - if (_watchingMediaKeys != watching) { - _watchingMediaKeys = watching; - if (_keyTap) { -#ifndef OS_MAC_STORE - if (_watchingMediaKeys) { - [_keyTap startWatchingMediaKeys]; - } else { - [_keyTap stopWatchingMediaKeys]; - } -#endif // else for !OS_MAC_STORE - } - } -} - -- (bool) isWatchingMediaKeys { - return _watchingMediaKeys; -} - -- (void) mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)e { - Core::Sandbox::Instance().customEnterFromEventLoop([&] { - if (e && [e type] == NSSystemDefined && [e subtype] == SPSystemDefinedEventMediaKeys) { - objc_handleMediaKeyEvent(e); - } - }); -} - - (void) ignoreApplicationActivationRightNow { _ignoreActivation = true; _ignoreActivationStop.callOnce(kIgnoreActivationTimeoutMs); } +- (NSMenu *) applicationDockMenu:(NSApplication *)sender { + RpMenu* dockMenu = [[[RpMenu alloc] initWithTitle: @""] autorelease]; + [dockMenu setAutoenablesItems:false]; + + auto notifyCallback = [] { + auto &settings = Core::App().settings(); + settings.setDesktopNotify(!settings.desktopNotify()); + }; + [dockMenu addItem:CreateMenuItem( + Core::App().settings().desktopNotify() + ? tr::lng_disable_notifications_from_tray(tr::now) + : tr::lng_enable_notifications_from_tray(tr::now), + [dockMenu lifetime], + std::move(notifyCallback))]; + + using namespace Media::Player; + const auto state = instance()->getState(instance()->getActiveType()); + if (!IsStoppedOrStopping(state.state)) { + [dockMenu addItem:[NSMenuItem separatorItem]]; + [dockMenu addItem:CreateMenuItem( + tr::lng_mac_menu_player_previous(tr::now), + [dockMenu lifetime], + [] { instance()->previous(); }, + instance()->previousAvailable(instance()->getActiveType()))]; + [dockMenu addItem:CreateMenuItem( + IsPausedOrPausing(state.state) + ? tr::lng_mac_menu_player_resume(tr::now) + : tr::lng_mac_menu_player_pause(tr::now), + [dockMenu lifetime], + [] { instance()->playPause(); })]; + [dockMenu addItem:CreateMenuItem( + tr::lng_mac_menu_player_next(tr::now), + [dockMenu lifetime], + [] { instance()->next(); }, + instance()->nextAvailable(instance()->getActiveType()))]; + } + + return dockMenu; +} + @end // @implementation ApplicationDelegate namespace Platform { void SetWatchingMediaKeys(bool watching) { - if (_sharedDelegate) { - [_sharedDelegate setWatchingMediaKeys:watching]; - } } void SetApplicationIcon(const QIcon &icon) { @@ -229,43 +258,6 @@ void SetApplicationIcon(const QIcon &icon) { } // namespace Platform -bool objc_handleMediaKeyEvent(void *ev) { - auto e = reinterpret_cast(ev); - - int keyCode = (([e data1] & 0xFFFF0000) >> 16); - int keyFlags = ([e data1] & 0x0000FFFF); - int keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA; - int keyRepeat = (keyFlags & 0x1); - - if (!_sharedDelegate || ![_sharedDelegate isWatchingMediaKeys]) { - return false; - } - - switch (keyCode) { - case NX_KEYTYPE_PLAY: - if (keyState == 0) { // Play pressed and released - Media::Player::instance()->playPause(); - return true; - } - break; - - case NX_KEYTYPE_FAST: - if (keyState == 0) { // Next pressed and released - Media::Player::instance()->next(); - return true; - } - break; - - case NX_KEYTYPE_REWIND: - if (keyState == 0) { // Previous pressed and released - Media::Player::instance()->previous(); - return true; - } - break; - } - return false; -} - void objc_debugShowAlert(const QString &str) { @autoreleasepool { diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm index 85e48229c..e2ab74355 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_pinned_chats_item.mm @@ -441,7 +441,8 @@ TimeId CalculateOnlineTill(not_null peer) { _gestures.events( ) | rpl::filter([=] { return !(*waitForFinish); - }) | rpl::start_with_next([=](not_null gesture) { + }) | rpl::start_with_next([=]( + not_null gesture) { const auto currentPosition = [gesture locationInView:self].x; switch ([gesture state]) { @@ -530,7 +531,8 @@ TimeId CalculateOnlineTill(not_null peer) { pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width()); userpic.setDevicePixelRatio(cRetinaFactor()); pin->userpic = std::move(userpic); - [self setNeedsDisplayInRect:PeerRectByIndex(pin->index)]; + const auto userpicIndex = pin->index + [self shift]; + [self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)]; }; const auto updateUserpics = [=] { ranges::for_each(_pins, singleUserpic); @@ -653,12 +655,15 @@ TimeId CalculateOnlineTill(not_null peer) { peerChangedLifetime->destroy(); for (const auto &pin : _pins) { const auto peer = pin->peer; + const auto index = pin->index; + _session->changes().peerUpdates( peer, UpdateFlag::Photo - ) | rpl::start_with_next( - listenToDownloaderFinished, - *peerChangedLifetime); + ) | rpl::start_with_next([=](const Data::PeerUpdate &update) { + _pins[index]->userpicView = update.peer->createUserpicView(); + listenToDownloaderFinished(); + }, *peerChangedLifetime); if (const auto user = peer->asUser()) { if (!user->isServiceUser() @@ -668,7 +673,6 @@ TimeId CalculateOnlineTill(not_null peer) { } } - const auto index = pin->index; rpl::merge( _session->changes().historyUpdates( _session->data().history(peer), diff --git a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm index aa4dcd2f9..483b513e2 100644 --- a/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm +++ b/Telegram/SourceFiles/platform/mac/touchbar/items/mac_scrubber_item.mm @@ -464,7 +464,7 @@ void AppendEmojiPacks( auto callback = [=] { if (document) { if (const auto error = RestrictionToSendStickers(_controller)) { - Ui::show(Box(*error)); + _controller->show(Box(*error)); return true; } Api::SendExistingDocument( diff --git a/Telegram/SourceFiles/platform/platform_specific.h b/Telegram/SourceFiles/platform/platform_specific.h index d0bfb84dc..f7112b214 100644 --- a/Telegram/SourceFiles/platform/platform_specific.h +++ b/Telegram/SourceFiles/platform/platform_specific.h @@ -39,7 +39,8 @@ void IgnoreApplicationActivationRightNow(); bool AutostartSupported(); bool TrayIconSupported(); bool SkipTaskbarSupported(); -QImage GetImageFromClipboard(); +[[nodiscard]] QImage GetImageFromClipboard(); +void WriteCrashDumpDetails(); [[nodiscard]] std::optional IsDarkMode(); [[nodiscard]] inline bool IsDarkModeSupported() { diff --git a/Telegram/SourceFiles/platform/win/launcher_win.cpp b/Telegram/SourceFiles/platform/win/launcher_win.cpp index 59bdba893..3575c92a8 100644 --- a/Telegram/SourceFiles/platform/win/launcher_win.cpp +++ b/Telegram/SourceFiles/platform/win/launcher_win.cpp @@ -149,4 +149,4 @@ bool Launcher::launch( return true; } -} // namespace +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/main_window_win.cpp b/Telegram/SourceFiles/platform/win/main_window_win.cpp index 94f683f75..0bd606446 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.cpp +++ b/Telegram/SourceFiles/platform/win/main_window_win.cpp @@ -228,6 +228,7 @@ void MainWindow::psRefreshTaskbarIcon() { palette.setColor(QPalette::Window, (isActiveWindow() ? st::titleBgActive : st::titleBg)->c); refresher->setPalette(palette); refresher->show(); + refresher->raise(); refresher->activateWindow(); updateIconCounters(); @@ -269,9 +270,11 @@ void MainWindow::showTrayTooltip() { } } -void MainWindow::workmodeUpdated(DBIWorkMode mode) { +void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) { + using WorkMode = Core::Settings::WorkMode; + switch (mode) { - case dbiwmWindowAndTray: { + case WorkMode::WindowAndTray: { psSetupTrayIcon(); HWND psOwner = (HWND)GetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT); if (psOwner) { @@ -280,7 +283,7 @@ void MainWindow::workmodeUpdated(DBIWorkMode mode) { } } break; - case dbiwmTrayOnly: { + case WorkMode::TrayOnly: { psSetupTrayIcon(); HWND psOwner = (HWND)GetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT); if (!psOwner) { @@ -288,7 +291,7 @@ void MainWindow::workmodeUpdated(DBIWorkMode mode) { } } break; - case dbiwmWindowOnly: { + case WorkMode::WindowOnly: { if (trayIcon) { trayIcon->setContextMenu(0); trayIcon->deleteLater(); diff --git a/Telegram/SourceFiles/platform/win/main_window_win.h b/Telegram/SourceFiles/platform/win/main_window_win.h index 28bd7aa40..de8218075 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.h +++ b/Telegram/SourceFiles/platform/win/main_window_win.h @@ -85,7 +85,7 @@ protected: void showTrayTooltip() override; - void workmodeUpdated(DBIWorkMode mode) override; + void workmodeUpdated(Core::Settings::WorkMode mode) override; bool initSizeFromSystem() override; diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index f9b3fd167..b8d8f8ae0 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -19,7 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "main/main_session.h" #include "mainwindow.h" -#include "facades.h" // Global::ScreenIsLocked. #include "windows_quiethours_h.h" #include @@ -425,7 +424,7 @@ bool SkipAudioForCustom() { return (UserNotificationState == QUNS_NOT_PRESENT) || (UserNotificationState == QUNS_PRESENTATION_MODE) - || Global::ScreenIsLocked(); + || Core::App().screenIsLocked(); } bool SkipToastForCustom() { diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index 5d85c014d..7426e66f6 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -293,6 +293,31 @@ bool AutostartSupported() { return !IsWindowsStoreBuild(); } +void WriteCrashDumpDetails() { +#ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS + PROCESS_MEMORY_COUNTERS data = { 0 }; + if (Dlls::GetProcessMemoryInfo + && Dlls::GetProcessMemoryInfo( + GetCurrentProcess(), + &data, + sizeof(data))) { + const auto mb = 1024 * 1024; + CrashReports::dump() + << "Memory-usage: " + << (data.PeakWorkingSetSize / mb) + << " MB (peak), " + << (data.WorkingSetSize / mb) + << " MB (current)\n"; + CrashReports::dump() + << "Pagefile-usage: " + << (data.PeakPagefileUsage / mb) + << " MB (peak), " + << (data.PagefileUsage / mb) + << " MB (current)\n"; + } +#endif // DESKTOP_APP_DISABLE_CRASH_REPORTS +} + } // namespace Platform namespace { @@ -531,31 +556,6 @@ void psSendToMenu(bool send, bool silent) { _manageAppLnk(send, silent, CSIDL_SENDTO, L"-sendpath", L"Kotatogram send to link.\nYou can disable send to menu item in Kotatogram settings."); } -void psWriteDump() { -#ifndef DESKTOP_APP_DISABLE_CRASH_REPORTS - PROCESS_MEMORY_COUNTERS data = { 0 }; - if (Dlls::GetProcessMemoryInfo - && Dlls::GetProcessMemoryInfo( - GetCurrentProcess(), - &data, - sizeof(data))) { - const auto mb = 1024 * 1024; - CrashReports::dump() - << "Memory-usage: " - << (data.PeakWorkingSetSize / mb) - << " MB (peak), " - << (data.WorkingSetSize / mb) - << " MB (current)\n"; - CrashReports::dump() - << "Pagefile-usage: " - << (data.PeakPagefileUsage / mb) - << " MB (peak), " - << (data.PagefileUsage / mb) - << " MB (current)\n"; - } -#endif // DESKTOP_APP_DISABLE_CRASH_REPORTS -} - bool psLaunchMaps(const Data::LocationPoint &point) { return QDesktopServices::openUrl(qsl("bingmaps:?lvl=16&collection=point.%1_%2_Point").arg(point.latAsString()).arg(point.lonAsString())); } diff --git a/Telegram/SourceFiles/platform/win/specific_win.h b/Telegram/SourceFiles/platform/win/specific_win.h index bd3d234e6..d35c3fcf3 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.h +++ b/Telegram/SourceFiles/platform/win/specific_win.h @@ -47,8 +47,6 @@ inline void finish() { inline void psCheckLocalSocket(const QString &) { } -void psWriteDump(); - void psActivateProcess(uint64 pid = 0); QString psAppDataPath(); QString psAppDataPathOld(); diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.cpp b/Telegram/SourceFiles/platform/win/windows_dlls.cpp index e88fad409..ce84175f5 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.cpp +++ b/Telegram/SourceFiles/platform/win/windows_dlls.cpp @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include + +#define LOAD_SYMBOL(lib, name) ::base::Platform::LoadMethod(lib, #name, name) + namespace Platform { namespace Dlls { @@ -42,84 +46,124 @@ void init() { u"rstrtmgr.dll"_q, u"psapi.dll"_q, u"user32.dll"_q, + u"d3d11.dll"_q, + u"dxgi.dll"_q, }; for (const auto &lib : list) { SafeLoadLibrary(lib); } } -f_SetWindowTheme SetWindowTheme; -//f_RefreshImmersiveColorPolicyState RefreshImmersiveColorPolicyState; -//f_AllowDarkModeForApp AllowDarkModeForApp; -//f_SetPreferredAppMode SetPreferredAppMode; -//f_AllowDarkModeForWindow AllowDarkModeForWindow; -//f_FlushMenuThemes FlushMenuThemes; -f_OpenAs_RunDLL OpenAs_RunDLL; -f_SHOpenWithDialog SHOpenWithDialog; -f_SHAssocEnumHandlers SHAssocEnumHandlers; -f_SHCreateItemFromParsingName SHCreateItemFromParsingName; -f_WTSRegisterSessionNotification WTSRegisterSessionNotification; -f_WTSUnRegisterSessionNotification WTSUnRegisterSessionNotification; -f_SHQueryUserNotificationState SHQueryUserNotificationState; -f_SHChangeNotify SHChangeNotify; -f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; -f_PropVariantToString PropVariantToString; -f_PSStringFromPropertyKey PSStringFromPropertyKey; -f_DwmIsCompositionEnabled DwmIsCompositionEnabled; -f_DwmSetWindowAttribute DwmSetWindowAttribute; -f_GetProcessMemoryInfo GetProcessMemoryInfo; -f_SetWindowCompositionAttribute SetWindowCompositionAttribute; +// D3D11.DLL + +HRESULT (__stdcall *D3D11CreateDevice)( + _In_opt_ IDXGIAdapter* pAdapter, + D3D_DRIVER_TYPE DriverType, + HMODULE Software, + UINT Flags, + _In_reads_opt_(FeatureLevels) CONST D3D_FEATURE_LEVEL* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + _COM_Outptr_opt_ ID3D11Device** ppDevice, + _Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel, + _COM_Outptr_opt_ ID3D11DeviceContext** ppImmediateContext); + +// DXGI.DLL + +HRESULT (__stdcall *CreateDXGIFactory1)( + REFIID riid, + _COM_Outptr_ void **ppFactory); void start() { init(); const auto LibShell32 = SafeLoadLibrary(u"shell32.dll"_q); - LoadMethod(LibShell32, "SHAssocEnumHandlers", SHAssocEnumHandlers); - LoadMethod(LibShell32, "SHCreateItemFromParsingName", SHCreateItemFromParsingName); - LoadMethod(LibShell32, "SHOpenWithDialog", SHOpenWithDialog); - LoadMethod(LibShell32, "OpenAs_RunDLLW", OpenAs_RunDLL); - LoadMethod(LibShell32, "SHQueryUserNotificationState", SHQueryUserNotificationState); - LoadMethod(LibShell32, "SHChangeNotify", SHChangeNotify); - LoadMethod(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + LOAD_SYMBOL(LibShell32, SHAssocEnumHandlers); + LOAD_SYMBOL(LibShell32, SHCreateItemFromParsingName); + LOAD_SYMBOL(LibShell32, SHOpenWithDialog); + LOAD_SYMBOL(LibShell32, OpenAs_RunDLL); + LOAD_SYMBOL(LibShell32, SHQueryUserNotificationState); + LOAD_SYMBOL(LibShell32, SHChangeNotify); + LOAD_SYMBOL(LibShell32, SetCurrentProcessExplicitAppUserModelID); const auto LibUxTheme = SafeLoadLibrary(u"uxtheme.dll"_q); - LoadMethod(LibUxTheme, "SetWindowTheme", SetWindowTheme); + LOAD_SYMBOL(LibUxTheme, SetWindowTheme); //if (IsWindows10OrGreater()) { // static const auto kSystemVersion = QOperatingSystemVersion::current(); // static const auto kMinor = kSystemVersion.minorVersion(); // static const auto kBuild = kSystemVersion.microVersion(); // if (kMinor > 0 || (kMinor == 0 && kBuild >= 17763)) { // if (kBuild < 18362) { - // LoadMethod(LibUxTheme, "AllowDarkModeForApp", AllowDarkModeForApp, 135); + // LOAD_SYMBOL(LibUxTheme, AllowDarkModeForApp, 135); // } else { - // LoadMethod(LibUxTheme, "SetPreferredAppMode", SetPreferredAppMode, 135); + // LOAD_SYMBOL(LibUxTheme, SetPreferredAppMode, 135); // } - // LoadMethod(LibUxTheme, "AllowDarkModeForWindow", AllowDarkModeForWindow, 133); - // LoadMethod(LibUxTheme, "RefreshImmersiveColorPolicyState", RefreshImmersiveColorPolicyState, 104); - // LoadMethod(LibUxTheme, "FlushMenuThemes", FlushMenuThemes, 136); + // LOAD_SYMBOL(LibUxTheme, AllowDarkModeForWindow, 133); + // LOAD_SYMBOL(LibUxTheme, RefreshImmersiveColorPolicyState, 104); + // LOAD_SYMBOL(LibUxTheme, FlushMenuThemes, 136); // } //} if (IsWindowsVistaOrGreater()) { const auto LibWtsApi32 = SafeLoadLibrary(u"wtsapi32.dll"_q); - LoadMethod(LibWtsApi32, "WTSRegisterSessionNotification", WTSRegisterSessionNotification); - LoadMethod(LibWtsApi32, "WTSUnRegisterSessionNotification", WTSUnRegisterSessionNotification); + LOAD_SYMBOL(LibWtsApi32, WTSRegisterSessionNotification); + LOAD_SYMBOL(LibWtsApi32, WTSUnRegisterSessionNotification); const auto LibPropSys = SafeLoadLibrary(u"propsys.dll"_q); - LoadMethod(LibPropSys, "PropVariantToString", PropVariantToString); - LoadMethod(LibPropSys, "PSStringFromPropertyKey", PSStringFromPropertyKey); + LOAD_SYMBOL(LibPropSys, PropVariantToString); + LOAD_SYMBOL(LibPropSys, PSStringFromPropertyKey); const auto LibDwmApi = SafeLoadLibrary(u"dwmapi.dll"_q); - LoadMethod(LibDwmApi, "DwmIsCompositionEnabled", DwmIsCompositionEnabled); - LoadMethod(LibDwmApi, "DwmSetWindowAttribute", DwmSetWindowAttribute); + LOAD_SYMBOL(LibDwmApi, DwmIsCompositionEnabled); + LOAD_SYMBOL(LibDwmApi, DwmSetWindowAttribute); } const auto LibPsApi = SafeLoadLibrary(u"psapi.dll"_q); - LoadMethod(LibPsApi, "GetProcessMemoryInfo", GetProcessMemoryInfo); + LOAD_SYMBOL(LibPsApi, GetProcessMemoryInfo); const auto LibUser32 = SafeLoadLibrary(u"user32.dll"_q); - LoadMethod(LibUser32, "SetWindowCompositionAttribute", SetWindowCompositionAttribute); + LOAD_SYMBOL(LibUser32, SetWindowCompositionAttribute); + + const auto LibD3D11 = SafeLoadLibrary(u"d3d11.dll"_q); + LOAD_SYMBOL(LibD3D11, D3D11CreateDevice); + + const auto LibDXGI = SafeLoadLibrary(u"dxgi.dll"_q); + LOAD_SYMBOL(LibDXGI, CreateDXGIFactory1); } } // namespace Dlls } // namespace Platform + +HRESULT WINAPI D3D11CreateDevice( + _In_opt_ IDXGIAdapter* pAdapter, + D3D_DRIVER_TYPE DriverType, + HMODULE Software, + UINT Flags, + _In_reads_opt_(FeatureLevels) CONST D3D_FEATURE_LEVEL* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + _COM_Outptr_opt_ ID3D11Device** ppDevice, + _Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel, + _COM_Outptr_opt_ ID3D11DeviceContext** ppImmediateContext) { + return Platform::Dlls::D3D11CreateDevice + ? Platform::Dlls::D3D11CreateDevice( + pAdapter, + DriverType, + Software, + Flags, + pFeatureLevels, + FeatureLevels, + SDKVersion, + ppDevice, + pFeatureLevel, + ppImmediateContext) + : S_FALSE; +} + +HRESULT WINAPI CreateDXGIFactory1( + REFIID riid, + _COM_Outptr_ void **ppFactory) { + return Platform::Dlls::CreateDXGIFactory1 + ? Platform::Dlls::CreateDXGIFactory1(riid, ppFactory) + : S_FALSE; +} diff --git a/Telegram/SourceFiles/platform/win/windows_dlls.h b/Telegram/SourceFiles/platform/win/windows_dlls.h index 7f34e59e4..0a31aaba2 100644 --- a/Telegram/SourceFiles/platform/win/windows_dlls.h +++ b/Telegram/SourceFiles/platform/win/windows_dlls.h @@ -23,25 +23,20 @@ namespace Platform { namespace Dlls { void init(); - -// KERNEL32.DLL -using f_SetDllDirectory = BOOL(FAR STDAPICALLTYPE*)(LPCWSTR lpPathName); -extern f_SetDllDirectory SetDllDirectory; - void start(); +// KERNEL32.DLL +inline BOOL(__stdcall *SetDllDirectory)(LPCWSTR lpPathName); + // UXTHEME.DLL -using f_SetWindowTheme = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *SetWindowTheme)( HWND hWnd, LPCWSTR pszSubAppName, LPCWSTR pszSubIdList); -extern f_SetWindowTheme SetWindowTheme; -//using f_RefreshImmersiveColorPolicyState = void(FAR STDAPICALLTYPE*)(); -//extern f_RefreshImmersiveColorPolicyState RefreshImmersiveColorPolicyState; +//inline void(__stdcall *RefreshImmersiveColorPolicyState)(); // -//using f_AllowDarkModeForApp = BOOL(FAR STDAPICALLTYPE*)(BOOL allow); -//extern f_AllowDarkModeForApp AllowDarkModeForApp; +//inline BOOL(__stdcall *AllowDarkModeForApp)(BOOL allow); // //enum class PreferredAppMode { // Default, @@ -51,101 +46,74 @@ extern f_SetWindowTheme SetWindowTheme; // Max //}; // -//using f_SetPreferredAppMode = PreferredAppMode(FAR STDAPICALLTYPE*)(PreferredAppMode appMode); -//extern f_SetPreferredAppMode SetPreferredAppMode; -// -//using f_AllowDarkModeForWindow = BOOL(FAR STDAPICALLTYPE*)(HWND hwnd, BOOL allow); -//extern f_AllowDarkModeForWindow AllowDarkModeForWindow; -// -//using f_FlushMenuThemes = void(FAR STDAPICALLTYPE*)(); -//extern f_FlushMenuThemes FlushMenuThemes; +//inline PreferredAppMode(__stdcall *SetPreferredAppMode)( +// PreferredAppMode appMode); +//inline BOOL(__stdcall *AllowDarkModeForWindow)(HWND hwnd, BOOL allow); +//inline void(__stdcall *FlushMenuThemes)(); // SHELL32.DLL -using f_SHAssocEnumHandlers = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *SHAssocEnumHandlers)( PCWSTR pszExtra, ASSOC_FILTER afFilter, IEnumAssocHandlers **ppEnumHandler); -extern f_SHAssocEnumHandlers SHAssocEnumHandlers; - -using f_SHCreateItemFromParsingName = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *SHCreateItemFromParsingName)( PCWSTR pszPath, IBindCtx *pbc, REFIID riid, void **ppv); -extern f_SHCreateItemFromParsingName SHCreateItemFromParsingName; - -using f_SHOpenWithDialog = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *SHOpenWithDialog)( HWND hwndParent, const OPENASINFO *poainfo); -extern f_SHOpenWithDialog SHOpenWithDialog; - -using f_OpenAs_RunDLL = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *OpenAs_RunDLL)( HWND hWnd, HINSTANCE hInstance, LPCWSTR lpszCmdLine, int nCmdShow); -extern f_OpenAs_RunDLL OpenAs_RunDLL; - -using f_SHQueryUserNotificationState = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *SHQueryUserNotificationState)( QUERY_USER_NOTIFICATION_STATE *pquns); -extern f_SHQueryUserNotificationState SHQueryUserNotificationState; - -using f_SHChangeNotify = void(FAR STDAPICALLTYPE*)( +inline void(__stdcall *SHChangeNotify)( LONG wEventId, UINT uFlags, __in_opt LPCVOID dwItem1, __in_opt LPCVOID dwItem2); -extern f_SHChangeNotify SHChangeNotify; - -using f_SetCurrentProcessExplicitAppUserModelID - = HRESULT(FAR STDAPICALLTYPE*)(__in PCWSTR AppID); -extern f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; +inline HRESULT(__stdcall *SetCurrentProcessExplicitAppUserModelID)( + __in PCWSTR AppID); // WTSAPI32.DLL -using f_WTSRegisterSessionNotification = BOOL(FAR STDAPICALLTYPE*)( +inline BOOL(__stdcall *WTSRegisterSessionNotification)( HWND hWnd, DWORD dwFlags); -extern f_WTSRegisterSessionNotification WTSRegisterSessionNotification; - -using f_WTSUnRegisterSessionNotification = BOOL(FAR STDAPICALLTYPE*)( +inline BOOL(__stdcall *WTSUnRegisterSessionNotification)( HWND hWnd); -extern f_WTSUnRegisterSessionNotification WTSUnRegisterSessionNotification; // PROPSYS.DLL -using f_PropVariantToString = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *PropVariantToString)( _In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); -extern f_PropVariantToString PropVariantToString; - -using f_PSStringFromPropertyKey = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *PSStringFromPropertyKey)( _In_ REFPROPERTYKEY pkey, _Out_writes_(cch) LPWSTR psz, _In_ UINT cch); -extern f_PSStringFromPropertyKey PSStringFromPropertyKey; // DWMAPI.DLL -using f_DwmIsCompositionEnabled = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *DwmIsCompositionEnabled)( _Out_ BOOL* pfEnabled); -extern f_DwmIsCompositionEnabled DwmIsCompositionEnabled; - -using f_DwmSetWindowAttribute = HRESULT(FAR STDAPICALLTYPE*)( +inline HRESULT(__stdcall *DwmSetWindowAttribute)( HWND hwnd, DWORD dwAttribute, _In_reads_bytes_(cbAttribute) LPCVOID pvAttribute, DWORD cbAttribute); -extern f_DwmSetWindowAttribute DwmSetWindowAttribute; // PSAPI.DLL -using f_GetProcessMemoryInfo = BOOL(FAR STDAPICALLTYPE*)( +inline BOOL(__stdcall *GetProcessMemoryInfo)( HANDLE Process, PPROCESS_MEMORY_COUNTERS ppsmemCounters, DWORD cb); -extern f_GetProcessMemoryInfo GetProcessMemoryInfo; // USER32.DLL @@ -186,8 +154,9 @@ struct WINDOWCOMPOSITIONATTRIBDATA { SIZE_T cbData; }; -using f_SetWindowCompositionAttribute = BOOL(WINAPI *)(HWND hWnd, WINDOWCOMPOSITIONATTRIBDATA*); -extern f_SetWindowCompositionAttribute SetWindowCompositionAttribute; +inline BOOL(__stdcall *SetWindowCompositionAttribute)( + HWND hWnd, + WINDOWCOMPOSITIONATTRIBDATA*); } // namespace Dlls } // namespace Platform diff --git a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp index 29affd3ff..eab91c48d 100644 --- a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp +++ b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp @@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "ui/inactive_press.h" #include "mainwindow.h" -#include "facades.h" #include "app.h" #include @@ -236,9 +235,9 @@ bool EventFilter::mainWindowEvent( case WM_WTSSESSION_CHANGE: { if (wParam == WTS_SESSION_LOGOFF || wParam == WTS_SESSION_LOCK) { - Global::SetScreenIsLocked(true); + Core::App().setScreenIsLocked(true); } else if (wParam == WTS_SESSION_LOGON || wParam == WTS_SESSION_UNLOCK) { - Global::SetScreenIsLocked(false); + Core::App().setScreenIsLocked(false); } } return false; @@ -255,9 +254,7 @@ bool EventFilter::mainWindowEvent( } else { _window->shadowsDeactivate(); } - if (Global::started()) { - _window->update(); - } + _window->update(); } return false; case WM_WINDOWPOSCHANGING: diff --git a/Telegram/SourceFiles/profile/profile_back_button.cpp b/Telegram/SourceFiles/profile/profile_back_button.cpp index 2e6158653..50c70d559 100644 --- a/Telegram/SourceFiles/profile/profile_back_button.cpp +++ b/Telegram/SourceFiles/profile/profile_back_button.cpp @@ -14,21 +14,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_window.h" #include "styles/style_profile.h" #include "styles/style_info.h" -#include "facades.h" namespace Profile { BackButton::BackButton( QWidget *parent, not_null session, - const QString &text) + const QString &text, + rpl::producer oneColumnValue) : Ui::AbstractButton(parent) , _session(session) , _text(text.toUpper()) { setCursor(style::cur_pointer); - subscribe(Adaptive::Changed(), [=] { updateAdaptiveLayout(); }); - updateAdaptiveLayout(); + std::move( + oneColumnValue + ) | rpl::start_with_next([=](bool oneColumn) { + if (!oneColumn) { + _unreadBadgeLifetime.destroy(); + } else if (!_unreadBadgeLifetime) { + _session->data().unreadBadgeChanges( + ) | rpl::start_with_next([=] { + rtlupdate( + 0, + 0, + st::titleUnreadCounterRight, + st::titleUnreadCounterTop); + }, _unreadBadgeLifetime); + } + }, lifetime()); } void BackButton::setText(const QString &text) { @@ -57,15 +71,4 @@ void BackButton::onStateChanged(State was, StateChangeSource source) { } } -void BackButton::updateAdaptiveLayout() { - if (!Adaptive::OneColumn()) { - _unreadBadgeLifetime.destroy(); - } else if (!_unreadBadgeLifetime) { - _session->data().unreadBadgeChanges( - ) | rpl::start_with_next([=] { - rtlupdate(0, 0, st::titleUnreadCounterRight, st::titleUnreadCounterTop); - }, _unreadBadgeLifetime); - } -} - } // namespace Profile diff --git a/Telegram/SourceFiles/profile/profile_back_button.h b/Telegram/SourceFiles/profile/profile_back_button.h index a3037fce2..98056ce9e 100644 --- a/Telegram/SourceFiles/profile/profile_back_button.h +++ b/Telegram/SourceFiles/profile/profile_back_button.h @@ -20,7 +20,8 @@ public: BackButton( QWidget *parent, not_null session, - const QString &text); + const QString &text, + rpl::producer oneColumnValue); void setText(const QString &text); @@ -31,8 +32,6 @@ protected: void onStateChanged(State was, StateChangeSource source) override; private: - void updateAdaptiveLayout(); - const not_null _session; rpl::lifetime _unreadBadgeLifetime; diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.cpp b/Telegram/SourceFiles/profile/profile_block_group_members.cpp index e7c55def7..50bd842ee 100644 --- a/Telegram/SourceFiles/profile/profile_block_group_members.cpp +++ b/Telegram/SourceFiles/profile/profile_block_group_members.cpp @@ -44,10 +44,8 @@ GroupMembersWidget::GroupMembersWidget( QWidget *parent, not_null peer, const style::PeerListItem &st) -: PeerListWidget(parent, peer, QString(), st, tr::lng_profile_kick(tr::now)) { - _updateOnlineTimer.setSingleShot(true); - connect(&_updateOnlineTimer, SIGNAL(timeout()), this, SLOT(onUpdateOnlineDisplay())); - +: PeerListWidget(parent, peer, QString(), st, tr::lng_profile_kick(tr::now)) +, _updateOnlineTimer([=] { updateOnlineDisplay(); }) { peer->session().changes().peerUpdates( UpdateFlag::Admins | UpdateFlag::Members @@ -188,7 +186,7 @@ void GroupMembersWidget::updateItemStatusText(Item *item) { } if (_updateOnlineAt <= _now || _updateOnlineAt > member->onlineTextTill) { _updateOnlineAt = member->onlineTextTill; - _updateOnlineTimer.start((_updateOnlineAt - _now + 1) * 1000); + _updateOnlineTimer.callOnce((_updateOnlineAt - _now + 1) * 1000); } } @@ -258,7 +256,6 @@ void GroupMembersWidget::updateOnlineCount() { } if (_onlineCount != newOnlineCount) { _onlineCount = newOnlineCount; - onlineCountUpdated(_onlineCount); } } @@ -440,7 +437,7 @@ auto GroupMembersWidget::computeMember(not_null user) return it->second; } -void GroupMembersWidget::onUpdateOnlineDisplay() { +void GroupMembersWidget::updateOnlineDisplay() { if (_sortByOnline) { _now = base::unixtime::now(); diff --git a/Telegram/SourceFiles/profile/profile_block_group_members.h b/Telegram/SourceFiles/profile/profile_block_group_members.h index e7a794cef..5420df7ff 100644 --- a/Telegram/SourceFiles/profile/profile_block_group_members.h +++ b/Telegram/SourceFiles/profile/profile_block_group_members.h @@ -7,10 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" #include "profile/profile_block_peer_list.h" -#include - namespace Ui { class FlatLabel; } // namespace Ui @@ -22,7 +21,6 @@ struct PeerUpdate; namespace Profile { class GroupMembersWidget : public PeerListWidget { - Q_OBJECT public: GroupMembersWidget( @@ -36,13 +34,9 @@ public: ~GroupMembersWidget(); -Q_SIGNALS: - void onlineCountUpdated(int onlineCount); - -private Q_SLOTS: - void onUpdateOnlineDisplay(); - private: + void updateOnlineDisplay(); + // Observed notifications. void notifyPeerUpdated(const Data::PeerUpdate &update); @@ -85,7 +79,7 @@ private: int _onlineCount = 0; TimeId _updateOnlineAt = 0; - QTimer _updateOnlineTimer; + base::Timer _updateOnlineTimer; }; diff --git a/Telegram/SourceFiles/settings/settings_advanced.cpp b/Telegram/SourceFiles/settings/settings_advanced.cpp index 8a5c57dbc..df5c395b8 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.cpp +++ b/Telegram/SourceFiles/settings/settings_advanced.cpp @@ -22,16 +22,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_specific.h" #include "platform/platform_window_title.h" #include "base/platform/base_platform_info.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "lang/lang_keys.h" #include "core/update_checker.h" #include "core/application.h" #include "storage/localstorage.h" +#include "storage/storage_domain.h" #include "data/data_session.h" #include "main/main_account.h" +#include "main/main_domain.h" #include "main/main_session.h" #include "mtproto/facade.h" -#include "facades.h" #include "app.h" #include "styles/style_settings.h" @@ -44,11 +46,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { void SetupConnectionType( + not_null controller, not_null account, not_null container) { const auto connectionType = [=] { const auto transport = account->mtp().dctransport(); - if (Global::ProxySettings() != MTP::ProxyData::Settings::Enabled) { + if (!Core::App().settings().proxy().isEnabled()) { return transport.isEmpty() ? tr::lng_connection_auto_connecting(tr::now) : tr::lng_connection_auto(tr::now, lt_transport, transport); @@ -62,13 +65,13 @@ void SetupConnectionType( container, tr::lng_settings_connection_type(), rpl::merge( - base::ObservableViewer(Global::RefConnectionTypeChanged()), + Core::App().settings().proxy().connectionTypeChanges(), // Handle language switch. tr::lng_connection_auto_connecting() | rpl::to_empty ) | rpl::map(connectionType), st::settingsButton); button->addClickHandler([=] { - Ui::show(ProxiesBoxController::CreateOwningBox(account)); + controller->show(ProxiesBoxController::CreateOwningBox(account)); }); } @@ -303,7 +306,7 @@ void SetupSpellchecker( Spellchecker::ButtonManageDictsState(session), st::settingsButton )->addClickHandler([=] { - Ui::show(Box(controller)); + controller->show(Box(controller)); }); button->toggledValue( @@ -313,7 +316,11 @@ void SetupSpellchecker( #endif // !TDESKTOP_DISABLE_SPELLCHECK } -void SetupSystemIntegrationContent(not_null container) { +void SetupSystemIntegrationContent( + Window::SessionController *controller, + not_null container) { + using WorkMode = Core::Settings::WorkMode; + const auto checkbox = [&](rpl::producer &&label, bool checked) { return object_ptr( container, @@ -339,18 +346,18 @@ void SetupSystemIntegrationContent(not_null container) { }; if (Platform::TrayIconSupported()) { const auto trayEnabled = [] { - const auto workMode = Global::WorkMode().value(); - return (workMode == dbiwmTrayOnly) - || (workMode == dbiwmWindowAndTray); + const auto workMode = Core::App().settings().workMode(); + return (workMode == WorkMode::TrayOnly) + || (workMode == WorkMode::WindowAndTray); }; const auto tray = addCheckbox( tr::lng_settings_workmode_tray(), trayEnabled()); const auto taskbarEnabled = [] { - const auto workMode = Global::WorkMode().value(); - return (workMode == dbiwmWindowOnly) - || (workMode == dbiwmWindowAndTray); + const auto workMode = Core::App().settings().workMode(); + return (workMode == WorkMode::WindowOnly) + || (workMode == WorkMode::WindowAndTray); }; const auto taskbar = Platform::SkipTaskbarSupported() ? addCheckbox( @@ -361,14 +368,15 @@ void SetupSystemIntegrationContent(not_null container) { const auto updateWorkmode = [=] { const auto newMode = tray->checked() ? ((!taskbar || taskbar->checked()) - ? dbiwmWindowAndTray - : dbiwmTrayOnly) - : dbiwmWindowOnly; - if ((newMode == dbiwmWindowAndTray || newMode == dbiwmTrayOnly) - && Global::WorkMode().value() != newMode) { + ? WorkMode::WindowAndTray + : WorkMode::TrayOnly) + : WorkMode::WindowOnly; + if ((newMode == WorkMode::WindowAndTray + || newMode == WorkMode::TrayOnly) + && Core::App().settings().workMode() != newMode) { cSetSeenTrayTooltip(false); } - Global::RefWorkMode().set(newMode); + Core::App().settings().setWorkMode(newMode); Local::writeSettings(); }; @@ -409,9 +417,10 @@ void SetupSystemIntegrationContent(not_null container) { Core::App().saveSettingsDelayed(); }, nativeFrame->lifetime()); } - if (Platform::AutostartSupported()) { - const auto minimizedToggled = [] { - return cStartMinimized() && !Global::LocalPasscode(); + if (Platform::AutostartSupported() && controller) { + const auto minimizedToggled = [=] { + return cStartMinimized() + && !controller->session().domain().local().hasLocalPasscode(); }; const auto autostart = addCheckbox( @@ -441,9 +450,9 @@ void SetupSystemIntegrationContent(not_null container) { ) | rpl::filter([=](bool checked) { return (checked != minimizedToggled()); }) | rpl::start_with_next([=](bool checked) { - if (Global::LocalPasscode()) { + if (controller->session().domain().local().hasLocalPasscode()) { minimized->entity()->setChecked(false); - Ui::show(Box( + controller->show(Box( tr::ktg_error_start_minimized_passcoded(tr::now))); } else { cSetStartMinimized(checked); @@ -451,8 +460,7 @@ void SetupSystemIntegrationContent(not_null container) { } }, minimized->lifetime()); - base::ObservableViewer( - Global::RefLocalPasscodeChanged() + controller->session().domain().local().localPasscodeChanged( ) | rpl::start_with_next([=] { minimized->entity()->setChecked(minimizedToggled()); }, minimized->lifetime()); @@ -474,9 +482,11 @@ void SetupSystemIntegrationContent(not_null container) { } } -void SetupSystemIntegrationOptions(not_null container) { +void SetupSystemIntegrationOptions( + not_null controller, + not_null container) { auto wrap = object_ptr(container); - SetupSystemIntegrationContent(wrap.data()); + SetupSystemIntegrationContent(controller, wrap.data()); if (wrap->count() > 0) { container->add(object_ptr( container, @@ -502,13 +512,51 @@ void SetupAnimations(not_null container) { }, container->lifetime()); } +void SetupOpenGL( + not_null controller, + not_null container) { + const auto toggles = container->lifetime().make_state< + rpl::event_stream + >(); + const auto button = AddButton( + container, + tr::lng_settings_enable_opengl(), + st::settingsButton + )->toggleOn( + toggles->events_starting_with_copy( + !Core::App().settings().disableOpenGL()) + ); + button->toggledValue( + ) | rpl::filter([](bool enabled) { + return (enabled == Core::App().settings().disableOpenGL()); + }) | rpl::start_with_next([=](bool enabled) { + const auto confirmed = crl::guard(button, [=] { + Core::App().settings().setDisableOpenGL(!enabled); + Local::writeSettings(); + App::restart(); + }); + const auto cancelled = crl::guard(button, [=] { + toggles->fire(!enabled); + }); + controller->show(Box( + tr::lng_settings_need_restart(tr::now), + tr::lng_settings_restart_now(tr::now), + confirmed, + cancelled)); + }, container->lifetime()); +} + void SetupPerformance( not_null controller, not_null container) { SetupAnimations(container); + if (!Platform::IsMac()) { + SetupOpenGL(controller, container); + } } void SetupSystemIntegration( + not_null controller, not_null container, Fn showOther) { AddDivider(container); @@ -521,7 +569,7 @@ void SetupSystemIntegration( )->addClickHandler([=] { showOther(Type::Calls); }); - SetupSystemIntegrationOptions(container); + SetupSystemIntegrationOptions(controller, container); AddSkip(container); } @@ -562,11 +610,14 @@ void Advanced::setupContent(not_null controller) { addDivider(); AddSkip(content); AddSubsectionTitle(content, tr::lng_settings_network_proxy()); - SetupConnectionType(&controller->session().account(), content); + SetupConnectionType( + &controller->window(), + &controller->session().account(), + content); AddSkip(content); SetupDataStorage(controller, content); SetupAutoDownload(controller, content); - SetupSystemIntegration(content, [=](Type type) { + SetupSystemIntegration(controller, content, [=](Type type) { _showOther.fire_copy(type); }); diff --git a/Telegram/SourceFiles/settings/settings_advanced.h b/Telegram/SourceFiles/settings/settings_advanced.h index 220b8be1a..60924f9d3 100644 --- a/Telegram/SourceFiles/settings/settings_advanced.h +++ b/Telegram/SourceFiles/settings/settings_advanced.h @@ -13,14 +13,21 @@ namespace Main { class Account; } // namespace Main +namespace Window { +class Controller; +} // namespace Window + namespace Settings { void SetupConnectionType( + not_null controller, not_null account, not_null container); bool HasUpdate(); void SetupUpdate(not_null container); -void SetupSystemIntegrationContent(not_null container); +void SetupSystemIntegrationContent( + Window::SessionController *controller, + not_null container); void SetupAnimations(not_null container); class Advanced : public Section { diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index a46108e07..40b76c788 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "core/application.h" #include "core/core_settings.h" +#include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_video_bubble.h" #include "webrtc/webrtc_media_devices.h" @@ -124,7 +125,7 @@ void Calls::setupContent() { call->setCurrentVideoDevice(deviceId); } }); - Ui::show(Box([=](not_null box) { + _controller->show(Box([=](not_null box) { SingleChoiceBox(box, { .title = tr::lng_settings_call_camera(), .options = options, @@ -154,7 +155,9 @@ void Calls::setupContent() { track->renderNextFrame( ) | rpl::start_with_next([=] { const auto size = track->frameSize(); - if (size.isEmpty() || Core::App().calls().currentCall()) { + if (size.isEmpty() + || Core::App().calls().currentCall() + || Core::App().calls().currentGroupCall()) { return; } const auto width = bubbleWrap->width(); @@ -166,9 +169,13 @@ void Calls::setupContent() { bubbleWrap->update(); }, bubbleWrap->lifetime()); - Core::App().calls().currentCallValue( - ) | rpl::start_with_next([=](::Calls::Call *value) { - if (value) { + using namespace rpl::mappers; + rpl::combine( + Core::App().calls().currentCallValue(), + Core::App().calls().currentGroupCallValue(), + _1 || _2 + ) | rpl::start_with_next([=](bool has) { + if (has) { track->setState(VideoState::Inactive); bubbleWrap->resize(bubbleWrap->width(), 0); } else { @@ -193,7 +200,7 @@ void Calls::setupContent() { ), st::settingsButton )->addClickHandler([=] { - Ui::show(ChooseAudioOutputBox(crl::guard(this, [=]( + _controller->show(ChooseAudioOutputBox(crl::guard(this, [=]( const QString &id, const QString &name) { _outputNameStream.fire_copy(name); @@ -214,7 +221,7 @@ void Calls::setupContent() { ), st::settingsButton )->addClickHandler([=] { - Ui::show(ChooseAudioInputBox(crl::guard(this, [=]( + _controller->show(ChooseAudioInputBox(crl::guard(this, [=]( const QString &id, const QString &name) { _inputNameStream.fire_copy(name); @@ -244,40 +251,6 @@ void Calls::setupContent() { AddSkip(content); AddSubsectionTitle(content, tr::lng_settings_call_section_other()); -//#if defined Q_OS_MAC && !defined OS_MAC_STORE -// AddButton( -// content, -// tr::lng_settings_call_audio_ducking(), -// st::settingsButton -// )->toggleOn( -// rpl::single(settings.callAudioDuckingEnabled()) -// )->toggledValue() | rpl::filter([](bool enabled) { -// return (enabled != Core::App().settings().callAudioDuckingEnabled()); -// }) | rpl::start_with_next([=](bool enabled) { -// Core::App().settings().setCallAudioDuckingEnabled(enabled); -// Core::App().saveSettingsDelayed(); -// if (const auto call = Core::App().calls().currentCall()) { -// call->setAudioDuckingEnabled(enabled); -// } -// }, content->lifetime()); -//#endif // Q_OS_MAC && !OS_MAC_STORE - - //const auto backend = [&]() -> QString { - // using namespace Webrtc; - // switch (settings.callAudioBackend()) { - // case Backend::OpenAL: return "OpenAL"; - // case Backend::ADM: return "WebRTC ADM"; - // case Backend::ADM2: return "WebRTC ADM2"; - // } - // Unexpected("Value in backend."); - //}(); - //AddButton( - // content, - // rpl::single("Call audio backend: " + backend), - // st::settingsButton - //)->addClickHandler([] { - // Ui::show(ChooseAudioBackendBox()); - //}); AddButton( content, tr::lng_settings_call_accept_calls(), @@ -295,11 +268,12 @@ void Calls::setupContent() { content, tr::lng_settings_call_open_system_prefs(), st::settingsButton - )->addClickHandler([] { + )->addClickHandler([=] { const auto opened = Platform::OpenSystemSettings( Platform::SystemSettingsType::Audio); if (!opened) { - Ui::show(Box(tr::lng_linux_no_audio_prefs(tr::now))); + _controller->show( + Box(tr::lng_linux_no_audio_prefs(tr::now))); } }); @@ -331,7 +305,7 @@ void Calls::requestPermissionAndStartTestingMicrophone() { Platform::PermissionType::Microphone); Ui::hideLayer(); }; - Ui::show(Box( + _controller->show(Box( tr::ktg_no_mic_permission(tr::now), tr::lng_menu_settings(tr::now), showSystemSettings)); diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index c257a1ba8..8550d3240 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/themes/window_themes_embedded.h" #include "window/themes/window_theme_editor_box.h" #include "window/themes/window_themes_cloud_list.h" +#include "window/window_adaptive.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "storage/localstorage.h" @@ -415,7 +416,7 @@ BackgroundRow::BackgroundRow( updateImage(); _chooseFromGallery->addClickHandler([=] { - Ui::show(Box(controller)); + controller->show(Box(controller)); }); _chooseFromFile->addClickHandler([=] { ChooseFromFile(controller, this); @@ -634,7 +635,7 @@ void ChooseFromFile( auto local = Data::CustomWallPaper(); local.setLocalImageAsThumbnail(std::make_shared( std::move(image))); - Ui::show(Box(controller, local)); + controller->show(Box(controller, local)); }); FileDialog::GetOpenPath( parent.get(), @@ -724,7 +725,7 @@ void SetupStickersEmoji( &st::settingsIconStickers, st::settingsChatIconLeft )->addClickHandler([=] { - Ui::show( + controller->show( Box(controller, StickersBox::Section::Installed)); }); @@ -735,7 +736,7 @@ void SetupStickersEmoji( &st::settingsIconEmoji, st::settingsChatIconLeft )->addClickHandler([=] { - Ui::show(Box(session)); + controller->show(Box(session)); }); AddSkip(container, st::settingsCheckboxesSkip); @@ -861,7 +862,7 @@ void SetupDataStorage( st::settingsButton, tr::lng_download_path()); path->entity()->addClickHandler([=] { - Ui::show(Box(controller)); + controller->show(Box(controller)); }); path->toggleOn(ask->toggledValue() | rpl::map(!_1)); #endif // OS_WIN_STORE @@ -900,7 +901,8 @@ void SetupAutoDownload( std::move(label), st::settingsButton )->addClickHandler([=] { - Ui::show(Box(&controller->session(), source)); + controller->show( + Box(&controller->session(), source)); }); }; add(tr::lng_media_auto_in_private(), Source::User); @@ -967,19 +969,15 @@ void SetupChatBackground( tile->setChecked(tiled); }, tile->lifetime()); - adaptive->toggleOn(rpl::single( - rpl::empty_value() - ) | rpl::then(base::ObservableViewer( - Adaptive::Changed() - )) | rpl::map([] { - return (Global::AdaptiveChatLayout() == Adaptive::ChatLayout::Wide); + adaptive->toggleOn(controller->adaptive().chatLayoutValue( + ) | rpl::map([](Window::Adaptive::ChatLayout layout) { + return (layout == Window::Adaptive::ChatLayout::Wide); })); adaptive->entity()->checkedChanges( ) | rpl::start_with_next([=](bool checked) { Core::App().settings().setAdaptiveForWide(checked); Core::App().saveSettingsDelayed(); - Adaptive::Changed().notify(); }, adaptive->lifetime()); } @@ -1145,7 +1143,7 @@ void SetupDefaultThemes( // in Window::Theme::Revert which is called by Editor. // // So we check here, before we change the saved accent color. - Ui::show(Box( + window->show(Box( tr::lng_theme_editor_cant_change_theme(tr::now))); return; } @@ -1272,7 +1270,9 @@ void SetupCloudThemes( wrap->setDuration(0)->toggleOn(list->empty() | rpl::map(!_1)); } -void SetupAutoNightMode(not_null container) { +void SetupAutoNightMode( + not_null controller, + not_null container) { if (!Platform::IsDarkModeSupported()) { return; } @@ -1297,7 +1297,7 @@ void SetupAutoNightMode(not_null container) { }) | rpl::start_with_next([=](bool checked) { if (checked && Window::Theme::Background()->editingTheme()) { autoNight->setChecked(false); - Ui::show(Box( + controller->show(Box( tr::lng_theme_editor_cant_change_theme(tr::now))); } else { Core::App().settings().setSystemDarkModeEnabled(checked); @@ -1454,7 +1454,7 @@ void Chat::setupContent(not_null controller) { const auto content = Ui::CreateChild(this); SetupThemeOptions(controller, content); - SetupAutoNightMode(content); + SetupAutoNightMode(controller, content); SetupCloudThemes(controller, content); SetupChatBackground(controller, content); SetupStickersEmoji(controller, content); diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 7449dc1ee..60f194532 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -39,9 +39,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { object_ptr
CreateSection( - Type type, - not_null parent, - not_null controller) { + Type type, + not_null parent, + not_null controller) { switch (type) { case Type::Main: return object_ptr
(parent, controller); @@ -80,8 +80,8 @@ void AddDivider(not_null container) { } void AddDividerText( - not_null container, - rpl::producer text) { + not_null container, + rpl::producer text) { container->add(object_ptr( container, object_ptr( @@ -91,37 +91,49 @@ void AddDividerText( st::settingsDividerLabelPadding)); } +not_null AddButtonIcon( + not_null button, + const style::icon *leftIcon, + int iconLeft, + const style::color *leftIconOver) { + const auto icon = Ui::CreateChild(button.get()); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->resize(leftIcon->size()); + button->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + icon->moveToLeft( + iconLeft ? iconLeft : st::settingsSectionIconLeft, + (size.height() - icon->height()) / 2, + size.width()); + }, icon->lifetime()); + icon->paintRequest( + ) | rpl::start_with_next([=] { + Painter p(icon); + const auto width = icon->width(); + const auto paintOver = (button->isOver() || button->isDown()) + && !button->isDisabled(); + if (!paintOver) { + leftIcon->paint(p, QPoint(), width); + } else if (leftIconOver) { + leftIcon->paint(p, QPoint(), width, (*leftIconOver)->c); + } else { + leftIcon->paint(p, QPoint(), width, st::menuIconFgOver->c); + } + }, icon->lifetime()); + return icon; +} + object_ptr