Compare commits

..

115 Commits

Author SHA1 Message Date
John Preston
486424af4f Beta version 2.5.2: Update cmake_helpers. 2020-12-25 18:57:21 +04:00
John Preston
f3614d6402 Beta version 2.5.2: Add in-app changelog. 2020-12-25 18:30:41 +04:00
John Preston
71151f6bf6 Beta version 2.5.2.
- Fix possible crash in video calls.
- Fix possible crash in connecting to voice chats.
- Use different audio module code on Windows in calls.
2020-12-25 16:45:18 +04:00
John Preston
2c0ef9c4e9 Improve connecting animation in voice chats. 2020-12-25 16:37:46 +04:00
John Preston
930e971881 Use more modern audio backend in calls on Windows. 2020-12-25 16:11:51 +04:00
John Preston
a576025d4f Always show invited at the end of voice chat. 2020-12-25 15:44:17 +04:00
John Preston
bcd2560e8f Reuse the code for userpics in Calls::TopBar. 2020-12-25 14:10:08 +04:00
John Preston
aede42b0b6 Update submodules. 2020-12-25 14:10:08 +04:00
Ilya Fedin
56728a066e Fix blurry tray icon with svg themes
QIcon::actualSize doesn't work as expected with svg themes, get actual pixmap and check its size instead.
2020-12-24 22:46:09 +03:00
John Preston
1951b7a8a1 Fix possible infinite recursion in video calls. 2020-12-24 14:38:46 +04:00
John Preston
0dc0f588c4 Don't offer sending .pdf-s as photos. 2020-12-24 13:52:38 +04:00
John Preston
7d22c631ca Fix voice chat members context menu. 2020-12-24 13:30:05 +04:00
John Preston
cf5cc3646a Fix multi-pin bar render after theme switch. 2020-12-24 07:59:34 +04:00
Ilya Fedin
375820d5cf glib was missed in stage-packages in snap 2020-12-24 07:48:00 +04:00
Ilya Fedin
3955543699 Remove QtSvg from snap in telegram part
Since it was needed only for lxqt-qtplugin
2020-12-24 07:48:00 +04:00
Ilya Fedin
c03da00e37 Fix getting version tag in snap action 2020-12-24 07:48:00 +04:00
Ilya Fedin
edcd462fb9 Use ibus portal in snap
It's supported since snapd 2.46 that was released in August
2020-12-24 07:48:00 +04:00
Ilya Fedin
e99558abeb Remove linux LastUserInputTime dependency since it's only in lib_base 2020-12-24 07:47:13 +04:00
Ilya Fedin
feff514a07 Update openal in snap to 1.21.0
And remove unneeded dependencies
2020-12-23 20:31:01 +04:00
John Preston
b1b25b0df9 Version 2.5.1.
- Fix crash in voice calls.
2020-12-23 15:01:31 +04:00
John Preston
dfee8238c6 Fix crash in legacy groups speaking typings handling. 2020-12-23 14:45:37 +04:00
John Preston
b7216c40fc Version 2.5.
- Turn any of your group chats into a hop-on, hop-off conference call.
- Get up to several thousand participants in each voice chat.
- Control the number of speakers with flexible admin tools.
2020-12-23 11:59:50 +04:00
John Preston
670d618439 Workaround tg_owt/openal conflict in a patched OpenAL on macOS. 2020-12-22 23:38:38 +04:00
John Preston
d16bc36bae Update API scheme. 2020-12-22 22:39:30 +04:00
John Preston
31417fd005 Fix highlighting of self row in voice chat. 2020-12-22 20:28:13 +04:00
John Preston
ae6decf70b Support ctrl+m/ctrl+w in voice chat panel. 2020-12-22 20:28:13 +04:00
John Preston
c80da25450 Show better tooltip for force muted in voice chat. 2020-12-22 19:38:02 +04:00
John Preston
7fd09084fd Use separate keys for message links and links. 2020-12-22 19:16:54 +04:00
John Preston
d7496f9824 Fix possible crash in app shutdown. 2020-12-22 19:16:54 +04:00
John Preston
f94280be7f Use langpack strings in a better way. 2020-12-22 19:16:54 +04:00
Ilya Fedin
0ff6c555b1 Use Platform::IsWayland in linux_gdk_helper 2020-12-22 18:37:52 +04:00
Ilya Fedin
596c7892c7 Use desktop-app::external_qt_static_plugins 2020-12-22 17:36:25 +04:00
John Preston
902e0fc8fb Remove dll loading in harfbuzz on Windows. 2020-12-22 14:29:51 +04:00
23rd
91e97b3d65 Fixed paint of group with wide thumbs in media viewer.
Fixed #8392.
2020-12-22 12:57:22 +03:00
John Preston
92bc278052 Allow inviting contacts to voice chats. 2020-12-22 12:33:06 +04:00
23rd
16c7ec5b05 Fixed stack overflow crash in applying draft at end of voice recording.
Regression was introduced in 50ed60f443.
2020-12-22 11:24:56 +03:00
23rd
348712059b Moved date and progress text formatting to tg_ui:ui/text/format_values. 2020-12-22 09:11:04 +03:00
23rd
b3f6fe1c10 Removed Enter key from box for clearing history.
Fixed #9781.
2020-12-22 08:37:36 +03:00
23rd
055ce1ee24 Fixed editing of last message on Up arrow key when message is forwarded.
Fixed #9782.
2020-12-22 07:56:51 +03:00
23rd
c14313d64a Replaced bezier circles in record button with blobs. 2020-12-21 14:03:04 +03:00
23rd
25665167fa Updated Qt version in README.md. 2020-12-19 18:59:50 +03:00
John Preston
af6c7c7d09 Beta version 2.4.15: Update Dockerfile. 2020-12-19 19:35:45 +04:00
John Preston
9c20cf3543 Beta version 2.4.15: Fix build on macOS. 2020-12-19 18:39:29 +04:00
John Preston
4ef2918bcc Beta version 2.4.15: Update version. 2020-12-19 14:27:09 +04:00
John Preston
eaf9b58337 Beta version 2.4.15.
- Improve design of voice chats.
- Fix sending of voice messages as replies.
- Fix 'Open With' menu position in macOS.
- Fix freeze on secondary screen disconnect.
2020-12-19 14:22:31 +04:00
23rd
b85bbadd74 Added Connecting state to Calls::TopBar. 2020-12-19 13:15:14 +03:00
John Preston
7a07acb124 Fix group call bar in legacy groups. 2020-12-18 19:21:57 +04:00
23rd
2fb220985a Updated doc for macOS. 2020-12-18 17:45:33 +03:00
23rd
4b41962ff6 Added hiding of Blob animations in GroupCallBar. 2020-12-18 17:45:33 +03:00
John Preston
e73b522411 Start using media device manager from lib_webrtc. 2020-12-18 18:43:49 +04:00
John Preston
c5ad7c7c89 Add start/end/reconnecting sounds to voice chats. 2020-12-18 12:44:19 +04:00
John Preston
d301601360 Fix mouse/keyboard input on Windows. 2020-12-18 12:43:51 +04:00
23rd
f8039f9b99 Added cancel button to VoiceRecordBar. 2020-12-18 06:00:07 +03:00
23rd
50ed60f443 Fixed ability to reply with voice message. 2020-12-18 03:40:43 +03:00
Ilya Fedin
a0b0799399 Fully disable session manager interaction
Since session manager functionality is not used anyway
2020-12-17 19:13:44 +04:00
John Preston
f0b8d4e62b Add sub-label-s to CallMuteButton. 2020-12-17 18:02:56 +04:00
23rd
d799dfe564 Updated colors for force muted state in Calls::TopBar. 2020-12-17 15:58:24 +03:00
John Preston
cae7e9c502 Fix Open With menu position in macOS. 2020-12-17 16:06:36 +04:00
John Preston
7aa12b6e07 Improve macOS voice chat window title. 2020-12-17 15:59:41 +04:00
John Preston
bb8647dd4c Show participants count in the subtitle. 2020-12-17 14:19:33 +04:00
John Preston
9dc6f117a7 Redesign invite members to voice chat button. 2020-12-16 19:31:16 +04:00
John Preston
cabd7a276b Add 'invited' members to voice chats. 2020-12-16 15:58:58 +04:00
John Preston
a85be4ccd0 Show information about ForceMuted state. 2020-12-16 15:29:15 +04:00
John Preston
29e7ea9b36 Use 'Space' in voice chat as push-to-talk key. 2020-12-16 13:14:52 +04:00
John Preston
d732a35904 Use 'participants' for voice chats. 2020-12-16 12:20:46 +04:00
John Preston
b68d5f854d Fix infinite 'Loading...' when no members in the group. 2020-12-16 12:20:46 +04:00
John Preston
146a9c2794 Don't edit old 'voice chat started' message. 2020-12-16 12:20:46 +04:00
John Preston
00fac70140 Fix resetting all sessions. 2020-12-16 12:20:46 +04:00
John Preston
6b5c422e95 Fix mute another voice chat admin. 2020-12-15 20:02:28 +04:00
Ilya Fedin
945fa2dd4b Build without libICE and libSM on Linux
There's no any session management anyway
2020-12-15 18:59:38 +04:00
Ilya Fedin
9493a3e8d2 Fix freeze on screen disconnect 2020-12-15 18:54:16 +04:00
tux93
823409175e Fix changelog.txt path in generate_appdata_changelog 2020-12-15 18:04:54 +04:00
John Preston
703b21b4e5 Leave voice chat without confirmation. 2020-12-15 14:49:31 +04:00
John Preston
b550eb5ab2 Send speaking typings each 3 seconds. 2020-12-15 14:49:08 +04:00
John Preston
aa5e8422bf Fix input device edit in Calls Settings.
Fixes #9930.
2020-12-15 14:23:40 +04:00
John Preston
15620b5c2d Divide speaking status and background noise. 2020-12-15 14:16:44 +04:00
John Preston
7f7e7b94d6 Fix Escape in comments section. 2020-12-15 12:42:10 +04:00
Ilya Fedin
df9d19b16a Clean up static plugins list
- Qt example's Wayland integrations
- Legacy xdg-shell implementations
- QSvgIcon that was never needed, but added by an accident with lxqt-qtplugin
2020-12-15 12:35:37 +04:00
Ilya Fedin
af17046a76 Enable xlib and libdrm in ffmpeg build in order to have va-x11 and va-drm enabled 2020-12-15 12:05:55 +04:00
John Preston
cb6db82809 Beta version 2.4.14.
- Create voice chats in legacy groups.
- Fix sticker pack opening.
- Fix group status display.
- Fix group members display.
2020-12-14 21:12:06 +04:00
John Preston
ad2d2c203f Respect 'just_joined' participant flag. 2020-12-14 20:08:47 +04:00
John Preston
7ab919e249 Allow skipping more bad packets after a good one. 2020-12-14 19:56:24 +04:00
John Preston
d69caacded Improve top bar icons layout. 2020-12-14 19:56:01 +04:00
John Preston
b6483cb65c Use shared_ptr<Memento> for sections. 2020-12-14 18:48:10 +04:00
23rd
baba7e272d Updated phrase and colors for force muted state in group calls. 2020-12-14 17:58:45 +04:00
23rd
77775b5f7c Added ability to use application while recording voice message. 2020-12-14 17:09:24 +04:00
23rd
dc7a754418 Added ability to prevent application lock and account switch. 2020-12-14 17:09:24 +04:00
23rd
24b8377a2a Added initial implementation of display sections prevent. 2020-12-14 17:09:24 +04:00
23rd
c8643aa1ee Wrapped raw SectionMemento with unique_ptr. 2020-12-14 17:09:24 +04:00
23rd
de6b460754 Fixed visual glitch in Replies section with tall input field. 2020-12-14 17:09:23 +04:00
23rd
bb4be4f3dd Replaced float interpolation with implementation from lib_ui. 2020-12-14 17:09:23 +04:00
23rd
62fe44bde8 Slightly refactored LinearBlob animation in Calls::TopBar. 2020-12-14 17:09:23 +04:00
23rd
de20ff05eb Fixed draft applying while recording voice message in HistoryWidget.
Relevant commit: d15a5c9bdc.
2020-12-14 17:09:23 +04:00
John Preston
1b624d67b8 Support voice chats in legacy groups, with migration. 2020-12-14 16:52:18 +04:00
Ilya Fedin
cd3b989e70 Use xcb to set transient parent for gtk file dialog 2020-12-14 16:50:59 +04:00
Himself65
e9e4c7a8cc Update VS2019 build instructions
Clone submodules in https://github.com/desktop-app/tg_owt
2020-12-14 14:04:09 +04:00
Andrew Lane
144e2e217f Update Linux build instructions
Co-authored-by: Ilya Fedin <fedin-ilja2010@ya.ru>
2020-12-14 14:03:11 +04:00
Ilya Fedin
dcd1e62c36 Move media viewer back to the right screen if it was moved by window manager 2020-12-14 14:02:15 +04:00
Ilya Fedin
c9d2ef278f Call moveToScreen from handleVisibleChanged in OverlayWidget 2020-12-14 14:02:15 +04:00
Ilya Fedin
78a666a19d Make updateGeometry don't need the argument 2020-12-14 14:02:15 +04:00
John Preston
2b91eedcd4 Update API scheme. 2020-12-13 23:27:51 +04:00
John Preston
7217d14f09 Show speaking animations in voice chat bar. 2020-12-11 18:53:02 +04:00
John Preston
fe23ba086a Animate userpics in join voice chat bar. 2020-12-11 17:16:37 +04:00
John Preston
424ba1dbea Track speaking state in non-joined voice chats. 2020-12-11 15:04:34 +04:00
John Preston
49b8340695 Fix top bar controls in chat view. 2020-12-11 14:20:17 +04:00
John Preston
4b31b4792a Restore call window from minimized on bar click. 2020-12-11 12:01:55 +04:00
John Preston
df420e4ccf Revert "Use gtk not only to get image from clipboard, but also to set"
This reverts commit f88c132c96.

Fixes #9885.
2020-12-11 12:00:52 +04:00
Ilya Fedin
cc35653c2c Add changelog2appdata into cmake build 2020-12-10 13:15:26 +04:00
Filippe LeMarchand
64c791a9ce Add changelog2appdata.py from flatpak 2020-12-10 13:15:26 +04:00
Ilya Fedin
7809cb8d30 Add execute bit to Telegram/build/docker/centos_env/build.sh 2020-12-10 09:56:40 +04:00
Ilya Fedin
379a3d74e2 Fix build without gtk 2020-12-10 09:43:47 +04:00
Ilya Fedin
c4f985ca38 Fix OpenAL build in Windows action 2020-12-10 09:43:30 +04:00
Ilya Fedin
6af180d6b5 Remove unneeded anymore Yum install step from Linux action 2020-12-10 07:26:46 +03:00
Ilya Fedin
05147016b0 Simplify XErrorHandlerRestorer API 2020-12-10 07:15:39 +03:00
Ilya Fedin
3297bdadb5 Add new dependencies to snap build 2020-12-09 22:36:06 +03:00
202 changed files with 4650 additions and 3076 deletions

View File

@@ -72,12 +72,6 @@ jobs:
- name: Get repository name.
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
- name: Yum install.
run: |
yum -y autoremove git
yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.7-1.x86_64.rpm
yum -y install git
- name: Clone.
uses: actions/checkout@v2
with:

View File

@@ -51,6 +51,7 @@ jobs:
- name: Clone.
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: recursive
- name: First set up.

View File

@@ -218,7 +218,7 @@ jobs:
-A Win32 ^
-D LIBTYPE:STRING=STATIC ^
-D FORCE_STATIC_VCRT=ON ^
-D ALSOFT_BACKEND_WASAPI=OFF
-D ALSOFT_BACKEND_DSOUND=OFF
msbuild -m OpenAL.vcxproj /property:Configuration=Debug

View File

@@ -32,7 +32,7 @@ Version **1.8.15** was the last that supports older systems
## Third-party
* Qt 5.12.8, 5.6.2 and 5.3.2 slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
* Qt 5.15.2, 5.6.2 and 5.3.2 slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
* OpenSSL 1.1.1 and 1.0.1 ([OpenSSL License](https://www.openssl.org/source/license.html))
* WebRTC ([New BSD License](https://github.com/desktop-app/tg_owt/blob/master/LICENSE))
* zlib 1.2.11 ([zlib License](http://www.zlib.net/zlib_license.html))

View File

@@ -33,6 +33,7 @@ include(cmake/td_mtproto.cmake)
include(cmake/td_lang.cmake)
include(cmake/td_scheme.cmake)
include(cmake/td_ui.cmake)
include(cmake/generate_appdata_changelog.cmake)
set_target_properties(Telegram PROPERTIES AUTOMOC ON AUTORCC ON)
@@ -59,6 +60,7 @@ PRIVATE
desktop-app::external_rlottie
desktop-app::external_zlib
desktop-app::external_minizip
desktop-app::external_qt_static_plugins
desktop-app::external_qt
desktop-app::external_qr_code_generator
desktop-app::external_crash_reports
@@ -70,9 +72,6 @@ PRIVATE
if (LINUX)
target_link_libraries(Telegram
PRIVATE
desktop-app::external_nimf_qt5
desktop-app::external_qt5ct_support
desktop-app::external_xcb_screensaver
desktop-app::external_xcb
desktop-app::external_glib
)
@@ -82,28 +81,19 @@ if (LINUX)
PRIVATE
desktop-app::external_statusnotifieritem
desktop-app::external_dbusmenu_qt
desktop-app::external_fcitx_qt5
desktop-app::external_fcitx5_qt5
desktop-app::external_hime_qt
)
endif()
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
target_link_libraries(Telegram
if (DESKTOP_APP_USE_PACKAGED
AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
target_include_directories(Telegram
PRIVATE
desktop-app::external_materialdecoration
${WAYLAND_CLIENT_INCLUDE_DIRS}
)
if (DESKTOP_APP_USE_PACKAGED
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
target_include_directories(Telegram
PRIVATE
${WAYLAND_CLIENT_INCLUDE_DIRS}
)
endif()
endif()
if (NOT TDESKTOP_DISABLE_GTK_INTEGRATION)
@@ -234,6 +224,8 @@ PRIVATE
boxes/peer_list_box.h
boxes/peer_list_controllers.cpp
boxes/peer_list_controllers.h
boxes/peer_lists_box.cpp
boxes/peer_lists_box.h
boxes/passcode_box.cpp
boxes/passcode_box.h
boxes/photo_crop_box.cpp
@@ -1097,7 +1089,6 @@ PRIVATE
mainwidget.h
mainwindow.cpp
mainwindow.h
qt_static_plugins.cpp
settings.cpp
settings.h
stdafx.h
@@ -1319,6 +1310,7 @@ endif()
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
include(GNUInstallDirs)
configure_file("../lib/xdg/telegramdesktop.appdata.xml.in" "${CMAKE_CURRENT_BINARY_DIR}/telegramdesktop.appdata.xml" @ONLY)
generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/telegramdesktop.appdata.xml")
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "telegram.png")
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "telegram.png")

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1343,8 +1343,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_broadcast_silent_ph" = "Silent broadcast...";
"lng_send_anonymous_ph" = "Send anonymously...";
"lng_record_cancel" = "Release outside this field to cancel";
"lng_record_lock_cancel" = "Click outside of the circle to cancel";
"lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?";
"lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded voice message?";
"lng_record_lock_discard" = "Discard";
"lng_will_be_notified" = "Members will be notified when you post";
"lng_wont_be_notified" = "Members will not be notified when you post";
@@ -1498,6 +1498,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_add_to_group" = "Add to group";
"lng_context_copy_link" = "Copy Link";
"lng_context_copy_message_link" = "Copy Message Link";
"lng_context_copy_post_link" = "Copy Post Link";
"lng_context_copy_email" = "Copy Email Address";
"lng_context_copy_hashtag" = "Copy Hashtag";
@@ -1819,8 +1820,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_inactive" = "listening";
"lng_group_call_settings" = "Settings";
"lng_group_call_unmute" = "Unmute";
"lng_group_call_unmute_sub" = "or hold spacebar to talk";
"lng_group_call_you_are_live" = "You are Live";
"lng_group_call_force_muted" = "You are muted";
"lng_group_call_force_muted" = "Muted by admin";
"lng_group_call_force_muted_sub" = "You are in Listen Only mode";
"lng_group_call_connecting" = "Connecting...";
"lng_group_call_leave" = "Leave";
"lng_group_call_leave_title" = "Leave voice chat";
@@ -1829,8 +1832,15 @@ 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_also_end" = "End voice chat";
"lng_group_call_settings_title" = "Settings";
"lng_group_call_invite" = "Invite Member";
"lng_group_call_invited_status" = "invited";
"lng_group_call_invite_title" = "Invite members";
"lng_group_call_invite_button" = "Invite";
"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}» yet. Add them to the group?";
"lng_group_call_add_to_group_some" = "Some of those users aren't members of «{group}» yet. Add them to the group?";
"lng_group_call_add_to_group_all" = "Those users aren't members of «{group}» yet. Add them to the group?";
"lng_group_call_invite_members" = "Group members";
"lng_group_call_invite_search_results" = "Search results";
"lng_group_call_new_muted" = "Mute new members";
"lng_group_call_speakers" = "Speakers";
"lng_group_call_microphone" = "Microphone";
@@ -1847,8 +1857,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_call_invite_done_many#one" = "You invited **{count} member** to the voice chat.";
"lng_group_call_invite_done_many#other" = "You invited **{count} members** to the voice chat.";
"lng_group_call_no_members" = "click to join";
"lng_group_call_members#one" = "{count} member";
"lng_group_call_members#other" = "{count} members";
"lng_group_call_members#one" = "{count} participant";
"lng_group_call_members#other" = "{count} participants";
"lng_group_call_no_anonymous" = "Sorry, anonymous group admins can't join voice chats.";
"lng_group_call_too_many" = "Sorry, there are too many members in this voice chat. Please try again later.";
"lng_group_call_context_mute" = "Mute";

View File

@@ -6,5 +6,8 @@
<file alias="call_end.mp3">../../sounds/call_end.mp3</file>
<file alias="call_incoming.mp3">../../sounds/call_incoming.mp3</file>
<file alias="call_outgoing.mp3">../../sounds/call_outgoing.mp3</file>
<file alias="group_call_start.mp3">../../sounds/group_call_start.mp3</file>
<file alias="group_call_connect.mp3">../../sounds/group_call_connect.mp3</file>
<file alias="group_call_end.mp3">../../sounds/group_call_end.mp3</file>
</qresource>
</RCC>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -63,7 +63,7 @@ inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMe
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
inputMediaDocument#33473058 flags:# id:InputDocument ttl_seconds:flags.0?int query:flags.1?string = InputMedia;
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
@@ -122,12 +122,12 @@ userStatusLastWeek#7bf09fc = UserStatus;
userStatusLastMonth#77ebc742 = UserStatus;
chatEmpty#9ba2d800 id:int = Chat;
chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat;
chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat;
chatForbidden#7328bdb id:int title:string = Chat;
channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat;
channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
chatFull#1b7c9db3 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull;
chatFull#dc8c181 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall = ChatFull;
channelFull#ef3a6acd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall = ChatFull;
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
@@ -365,8 +365,9 @@ updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update;
updateChannelUserTyping#ff2abe9f flags:# channel_id:int top_msg_id:flags.0?int user_id:int action:SendMessageAction = Update;
updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector<int> pts:int pts_count:int = Update;
updatePinnedChannelMessages#8588878b flags:# pinned:flags.0?true channel_id:int messages:Vector<int> pts:int pts_count:int = Update;
updateChat#1330a196 chat_id:int = Update;
updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector<GroupCallParticipant> version:int = Update;
updateGroupCall#5724806e channel_id:int call:GroupCall = Update;
updateGroupCall#a45eb99b chat_id:int call:GroupCall = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@@ -547,7 +548,7 @@ inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;
inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet;
inputStickerSetDice#e67f520e emoticon:string = InputStickerSet;
stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet;
stickerSet#40e237a8 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int count:int hash:int = StickerSet;
messages.stickerSet#b60a24a6 set:StickerSet packs:Vector<StickerPack> documents:Vector<Document> = messages.StickerSet;
@@ -1194,7 +1195,7 @@ groupCall#55903081 flags:# join_muted:flags.1?true can_change_join_muted:flags.2
inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;
groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant;
groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant;
phone.groupCall#66ab0bfc call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string users:Vector<User> = phone.GroupCall;
@@ -1560,7 +1561,7 @@ phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall durati
phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates;
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;
phone.createGroupCall#e428fa02 channel:InputChannel random_id:int = Updates;
phone.createGroupCall#bd3dabe0 peer:InputPeer random_id:int = Updates;
phone.joinGroupCall#5f9c8e62 flags:# muted:flags.0?true call:InputGroupCall params:DataJSON = Updates;
phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;
phone.editGroupCallMember#63146ae4 flags:# muted:flags.0?true call:InputGroupCall user_id:InputUser = Updates;

View File

@@ -9,7 +9,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="2.4.13.0" />
Version="2.5.2.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,4,13,0
PRODUCTVERSION 2,4,13,0
FILEVERSION 2,5,2,0
PRODUCTVERSION 2,5,2,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.4.13.0"
VALUE "FileVersion", "2.5.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.4.13.0"
VALUE "ProductVersion", "2.5.2.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,4,13,0
PRODUCTVERSION 2,4,13,0
FILEVERSION 2,5,2,0
PRODUCTVERSION 2,5,2,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.4.13.0"
VALUE "FileVersion", "2.5.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.4.13.0"
VALUE "ProductVersion", "2.5.2.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -125,13 +125,20 @@ void Authorizations::requestTerminate(
Fn<void(const MTPBool &result)> &&done,
Fn<void(const RPCError &error)> &&fail,
std::optional<uint64> hash) {
auto request = hash
? MTPaccount_ResetAuthorization(MTP_long(*hash))
: MTPaccount_ResetAuthorization();
_api.request(std::move(request))
.done(std::move(done))
.fail(std::move(fail))
.send();
const auto send = [&](auto request) {
_api.request(
std::move(request)
).done(
std::move(done)
).fail(
std::move(fail)
).send();
};
if (hash) {
send(MTPaccount_ResetAuthorization(MTP_long(*hash)));
} else {
send(MTPauth_ResetAuthorizations());
}
}
Authorizations::List Authorizations::list() const {

View File

@@ -19,7 +19,8 @@ namespace Api {
namespace {
constexpr auto kCancelTypingActionTimeout = crl::time(5000);
constexpr auto kSetMyActionForMs = 10 * crl::time(1000);
constexpr auto kSendMySpeakingInterval = 3 * crl::time(1000);
constexpr auto kSendMyTypingInterval = 5 * crl::time(1000);
constexpr auto kSendTypingsToOfflineFor = TimeId(30);
} // namespace
@@ -82,12 +83,15 @@ bool SendProgressManager::updated(const Key &key, bool doing) {
const auto now = crl::now();
const auto i = _updated.find(key);
if (doing) {
const auto sendEach = (key.type == SendProgressType::Speaking)
? kSendMySpeakingInterval
: kSendMyTypingInterval;
if (i == end(_updated)) {
_updated.emplace(key, now + kSetMyActionForMs);
} else if (i->second > now + (kSetMyActionForMs / 2)) {
_updated.emplace(key, now + 2 * sendEach);
} else if (i->second > now + sendEach) {
return false;
} else {
i->second = now + kSetMyActionForMs;
i->second = now + 2 * sendEach;
}
} else {
if (i == end(_updated)) {

View File

@@ -182,7 +182,8 @@ void SendExistingDocument(
return MTP_inputMediaDocument(
MTP_flags(0),
document->mtpInput(),
MTPint());
MTPint(), // ttl_seconds
MTPstring()); // query
};
SendExistingMedia(
std::move(message),

View File

@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "data/data_chat_filters.h"
#include "data/data_cloud_themes.h"
#include "data/data_group_call.h"
#include "data/data_drafts.h"
#include "data/data_histories.h"
#include "data/data_folder.h"
@@ -232,6 +233,27 @@ Updates::Updates(not_null<Main::Session*> session)
)).done([=](const MTPupdates_State &result) {
stateDone(result);
}).send();
using namespace rpl::mappers;
base::ObservableViewer(
api().fullPeerUpdated()
) | rpl::filter([](not_null<PeerData*> peer) {
return peer->isChat() || peer->isMegagroup();
}) | rpl::start_with_next([=](not_null<PeerData*> peer) {
if (const auto users = _pendingSpeakingCallMembers.take(peer)) {
if (const auto call = peer->groupCall()) {
for (const auto [userId, when] : *users) {
call->applyActiveUpdate(
userId,
Data::LastSpokeTimes{
.anything = when,
.voice = when
},
peer->owner().userLoaded(userId));
}
}
}
}, _lifetime);
}
Main::Session &Updates::session() const {
@@ -890,6 +912,58 @@ bool Updates::isQuitPrevent() {
updateOnline();
return true;
}
void Updates::handleSendActionUpdate(
PeerId peerId,
MsgId rootId,
UserId userId,
const MTPSendMessageAction &action) {
const auto history = session().data().historyLoaded(peerId);
if (!history) {
return;
}
const auto peer = history->peer;
const auto user = (userId == session().userId())
? session().user().get()
: session().data().userLoaded(userId);
const auto isSpeakingInCall = (action.type()
== mtpc_speakingInGroupCallAction);
if (isSpeakingInCall) {
if (!peer->isChat() && !peer->isChannel()) {
return;
}
const auto call = peer->groupCall();
const auto now = crl::now();
if (call) {
call->applyActiveUpdate(
userId,
Data::LastSpokeTimes{ .anything = now, .voice = now },
user);
} else {
const auto chat = peer->asChat();
const auto channel = peer->asChannel();
const auto active = chat
? (chat->flags() & MTPDchat::Flag::f_call_active)
: (channel->flags() & MTPDchannel::Flag::f_call_active);
if (active) {
_pendingSpeakingCallMembers.emplace(
peer).first->second[userId] = now;
session().api().requestFullPeer(peer);
}
}
}
if (!user || user->isSelf()) {
return;
}
const auto when = requestingDifference()
? 0
: base::unixtime::now();
session().data().registerSendAction(
history,
rootId,
user,
action,
when);
}
void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
switch (updates.type()) {
@@ -1580,57 +1654,29 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updateUserTyping: {
auto &d = update.c_updateUserTyping();
const auto userId = peerFromUser(d.vuser_id());
const auto history = session().data().historyLoaded(userId);
const auto user = session().data().userLoaded(d.vuser_id().v);
if (history && user) {
const auto when = requestingDifference() ? 0 : base::unixtime::now();
session().data().registerSendAction(
history,
MsgId(),
user,
d.vaction(),
when);
}
handleSendActionUpdate(
peerFromUser(d.vuser_id()),
0,
d.vuser_id().v,
d.vaction());
} break;
case mtpc_updateChatUserTyping: {
auto &d = update.c_updateChatUserTyping();
const auto history = session().data().historyLoaded(
peerFromChat(d.vchat_id()));
const auto user = (d.vuser_id().v == session().userId())
? nullptr
: session().data().userLoaded(d.vuser_id().v);
if (history && user) {
const auto when = requestingDifference() ? 0 : base::unixtime::now();
session().data().registerSendAction(
history,
MsgId(),
user,
d.vaction(),
when);
}
handleSendActionUpdate(
peerFromChat(d.vchat_id()),
0,
d.vuser_id().v,
d.vaction());
} break;
case mtpc_updateChannelUserTyping: {
const auto &d = update.c_updateChannelUserTyping();
const auto history = session().data().historyLoaded(
peerFromChannel(d.vchannel_id()));
const auto user = (d.vuser_id().v == session().userId())
? nullptr
: session().data().userLoaded(d.vuser_id().v);
if (history && user) {
const auto when = requestingDifference()
? 0
: base::unixtime::now();
const auto rootId = d.vtop_msg_id().value_or_empty();
session().data().registerSendAction(
history,
rootId,
user,
d.vaction(),
when);
}
handleSendActionUpdate(
peerFromChannel(d.vchannel_id()),
d.vtop_msg_id().value_or_empty(),
d.vuser_id().v,
d.vaction());
} break;
case mtpc_updateChatParticipants: {
@@ -1799,15 +1845,6 @@ void Updates::feedUpdate(const MTPUpdate &update) {
case mtpc_updatePhoneCallSignalingData:
case mtpc_updateGroupCallParticipants:
case mtpc_updateGroupCall: {
if (update.type() == mtpc_updateGroupCall) {
const auto &data = update.c_updateGroupCall();
if (data.vcall().type() == mtpc_groupCallDiscarded) {
const auto &call = data.vcall().c_groupCallDiscarded();
session().data().groupCallDiscarded(
call.vid().v,
call.vduration().v);
}
}
Core::App().calls().handleUpdate(&session(), update);
} break;

View File

@@ -122,6 +122,12 @@ private:
base::flat_map<not_null<ChannelData*>, crl::time> &whenMap,
crl::time &curTime);
void handleSendActionUpdate(
PeerId peerId,
MsgId rootId,
UserId userId,
const MTPSendMessageAction &action);
const not_null<Main::Session*> _session;
int32 _updatesDate = 0;
@@ -160,6 +166,9 @@ private:
bool _handlingChannelDifference = false;
base::flat_map<int, ActiveChatTracker> _activeChats;
base::flat_map<
not_null<PeerData*>,
base::flat_map<UserId, crl::time>> _pendingSpeakingCallMembers;
mtpRequestId _onlineRequest = 0;
base::Timer _idleFinishTimer;

View File

@@ -3412,7 +3412,7 @@ void ApiWrap::jumpToHistoryDate(not_null<PeerData*> peer, const QDate &date) {
// requestMessageAfterDate(feed, date, [=](Data::MessagePosition result) {
// Ui::hideLayer();
// App::wnd()->sessionController()->showSection(
// HistoryFeed::Memento(feed, result));
// std::make_shared<HistoryFeed::Memento>(feed, result));
// });
//}
@@ -3458,7 +3458,8 @@ void ApiWrap::checkForUnreadMentions(
void ApiWrap::addChatParticipants(
not_null<PeerData*> peer,
const std::vector<not_null<UserData*>> &users) {
const std::vector<not_null<UserData*>> &users,
Fn<void(bool)> done) {
if (const auto chat = peer->asChat()) {
for (const auto user : users) {
request(MTPmessages_AddChatUser(
@@ -3467,8 +3468,10 @@ void ApiWrap::addChatParticipants(
MTP_int(kForwardMessagesOnAdd)
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
if (done) done(true);
}).fail([=](const RPCError &error) {
ShowAddParticipantsError(error.type(), peer, { 1, user });
if (done) done(false);
}).afterDelay(crl::time(5)).send();
}
} else if (const auto channel = peer->asChannel()) {
@@ -3480,14 +3483,17 @@ void ApiWrap::addChatParticipants(
auto list = QVector<MTPInputUser>();
list.reserve(qMin(int(users.size()), int(kMaxUsersPerInvite)));
const auto send = [&] {
const auto callback = base::take(done);
request(MTPchannels_InviteToChannel(
channel->inputChannel,
MTP_vector<MTPInputUser>(list)
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
requestParticipantsCountDelayed(channel);
if (callback) callback(true);
}).fail([=](const RPCError &error) {
ShowAddParticipantsError(error.type(), peer, users);
if (callback) callback(false);
}).afterDelay(crl::time(5)).send();
};
for (const auto user : users) {
@@ -4639,7 +4645,8 @@ void ApiWrap::uploadAlbumMedia(
fields.vid(),
fields.vaccess_hash(),
fields.vfile_reference()),
MTP_int(data.vttl_seconds().value_or_empty()));
MTP_int(data.vttl_seconds().value_or_empty()),
MTPstring()); // query
sendAlbumWithUploaded(item, groupId, media);
} break;
}

View File

@@ -366,7 +366,8 @@ public:
Fn<void()> callbackNotModified = nullptr);
void addChatParticipants(
not_null<PeerData*> peer,
const std::vector<not_null<UserData*>> &users);
const std::vector<not_null<UserData*>> &users,
Fn<void(bool)> done = nullptr);
rpl::producer<SendAction> sendActions() const {
return _sendActions.events();

View File

@@ -632,7 +632,7 @@ void GroupInfoBox::submit() {
not_null<PeerListBox*> box) {
auto create = [box, title, weak] {
if (weak) {
auto rows = box->peerListCollectSelectedRows();
auto rows = box->collectSelectedRows();
if (!rows.empty()) {
weak->createGroup(box, title, rows);
}
@@ -643,7 +643,8 @@ void GroupInfoBox::submit() {
};
Ui::show(
Box<PeerListBox>(
std::make_unique<AddParticipantsBoxController>(_navigation),
std::make_unique<AddParticipantsBoxController>(
&_navigation->session()),
std::move(initBox)),
Ui::LayerOption::KeepOther);
}

View File

@@ -801,7 +801,10 @@ void DeleteMessagesBox::resizeEvent(QResizeEvent *e) {
void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
deleteAndClear();
// Don't make the clearing history so easy.
if (!_wipeHistoryPeer) {
deleteAndClear();
}
} else {
BoxContent::keyPressEvent(e);
}

View File

@@ -80,7 +80,7 @@ auto ListFromMimeData(not_null<const QMimeData*> data) {
if (result.error == Error::None) {
return result;
} else if (data->hasImage()) {
auto image = Platform::GetClipboardImage();
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}

View File

@@ -33,38 +33,36 @@ namespace {
class PrivacyExceptionsBoxController : public ChatsListBoxController {
public:
PrivacyExceptionsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
rpl::producer<QString> title,
const std::vector<not_null<PeerData*>> &selected);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
std::vector<not_null<PeerData*>> getResult() const;
protected:
void prepareViewHook() override;
std::unique_ptr<Row> createRow(not_null<History*> history) override;
private:
not_null<Window::SessionNavigation*> _navigation;
const not_null<Main::Session*> _session;
rpl::producer<QString> _title;
std::vector<not_null<PeerData*>> _selected;
};
PrivacyExceptionsBoxController::PrivacyExceptionsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
rpl::producer<QString> title,
const std::vector<not_null<PeerData*>> &selected)
: ChatsListBoxController(navigation)
, _navigation(navigation)
: ChatsListBoxController(session)
, _session(session)
, _title(std::move(title))
, _selected(selected) {
}
Main::Session &PrivacyExceptionsBoxController::session() const {
return _navigation->session();
return *_session;
}
void PrivacyExceptionsBoxController::prepareViewHook() {
@@ -72,10 +70,6 @@ void PrivacyExceptionsBoxController::prepareViewHook() {
delegate()->peerListAddSelectedPeers(_selected);
}
std::vector<not_null<PeerData*>> PrivacyExceptionsBoxController::getResult() const {
return delegate()->peerListCollectSelectedRows();
}
void PrivacyExceptionsBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
@@ -146,13 +140,13 @@ void EditPrivacyBox::editExceptions(
Exception exception,
Fn<void()> done) {
auto controller = std::make_unique<PrivacyExceptionsBoxController>(
_window,
&_window->session(),
_controller->exceptionBoxTitle(exception),
exceptions(exception));
auto initBox = [=, controller = controller.get()](
not_null<PeerListBox*> box) {
box->addButton(tr::lng_settings_save(), crl::guard(this, [=] {
exceptions(exception) = controller->getResult();
exceptions(exception) = box->collectSelectedRows();
const auto type = [&] {
switch (exception) {
case Exception::Always: return Exception::Never;

View File

@@ -320,7 +320,7 @@ void EditExceptions(
const auto include = (options & Flag::Contacts) != Flags(0);
const auto rules = data->current();
auto controller = std::make_unique<EditFilterChatsListController>(
window,
&window->session(),
(include
? tr::lng_filters_include_title()
: tr::lng_filters_exclude_title()),
@@ -331,7 +331,7 @@ void EditExceptions(
auto initBox = [=](not_null<PeerListBox*> box) {
box->setCloseByOutsideClick(false);
box->addButton(tr::lng_settings_save(), crl::guard(context, [=] {
const auto peers = box->peerListCollectSelectedRows();
const auto peers = box->collectSelectedRows();
const auto rules = data->current();
auto &&histories = ranges::view::all(
peers

View File

@@ -68,7 +68,6 @@ public:
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
@@ -209,11 +208,6 @@ int TypeDelegate::peerListSelectedRowsCount() {
return 0;
}
auto TypeDelegate::peerListCollectSelectedRows()
-> std::vector<not_null<PeerData*>> {
return {};
}
void TypeDelegate::peerListScrollToTop() {
}
@@ -347,13 +341,13 @@ void PaintFilterChatsTypeIcon(
}
EditFilterChatsListController::EditFilterChatsListController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
rpl::producer<QString> title,
Flags options,
Flags selected,
const base::flat_set<not_null<History*>> &peers)
: ChatsListBoxController(navigation)
, _navigation(navigation)
: ChatsListBoxController(session)
, _session(session)
, _title(std::move(title))
, _peers(peers)
, _options(options)
@@ -361,7 +355,7 @@ EditFilterChatsListController::EditFilterChatsListController(
}
Main::Session &EditFilterChatsListController::session() const {
return _navigation->session();
return *_session;
}
void EditFilterChatsListController::rowClicked(not_null<PeerListRow*> row) {

View File

@@ -41,7 +41,7 @@ public:
using Flags = Data::ChatFilter::Flags;
EditFilterChatsListController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
rpl::producer<QString> title,
Flags options,
Flags selected,
@@ -64,7 +64,7 @@ private:
void updateTitle();
const not_null<Window::SessionNavigation*> _navigation;
const not_null<Main::Session*> _session;
rpl::producer<QString> _title;
base::flat_set<not_null<History*>> _peers;
Flags _options;

View File

@@ -399,7 +399,7 @@ int PeerListBox::peerListSelectedRowsCount() {
return _select ? _select->entity()->getItemsCount() : 0;
}
auto PeerListBox::peerListCollectSelectedRows()
auto PeerListBox::collectSelectedRows()
-> std::vector<not_null<PeerData*>> {
auto result = std::vector<not_null<PeerData*>>();
auto items = _select
@@ -982,6 +982,18 @@ void PeerListContent::setAboveWidget(object_ptr<TWidget> widget) {
}
}
void PeerListContent::setAboveSearchWidget(object_ptr<TWidget> widget) {
_aboveSearchWidget = std::move(widget);
if (_aboveSearchWidget) {
_aboveSearchWidget->setParent(this);
}
}
void PeerListContent::setHideEmpty(bool hide) {
_hideEmpty = hide;
resizeToWidth(width());
}
void PeerListContent::setBelowWidget(object_ptr<TWidget> widget) {
_belowWidget = std::move(widget);
if (_belowWidget) {
@@ -990,6 +1002,9 @@ void PeerListContent::setBelowWidget(object_ptr<TWidget> widget) {
}
int PeerListContent::labelHeight() const {
if (_hideEmpty && !shownRowsCount()) {
return 0;
}
auto computeLabelHeight = [](auto &label) {
if (!label) {
return 0;
@@ -1082,34 +1097,45 @@ void PeerListContent::paintEvent(QPaintEvent *e) {
}
int PeerListContent::resizeGetHeight(int newWidth) {
const auto rowsCount = shownRowsCount();
const auto hideAll = !rowsCount && _hideEmpty;
_aboveHeight = 0;
if (_aboveWidget) {
_aboveWidget->resizeToWidth(newWidth);
_aboveWidget->moveToLeft(0, 0, newWidth);
if (showingSearch()) {
if (hideAll || showingSearch()) {
_aboveWidget->hide();
} else {
_aboveWidget->show();
_aboveHeight = _aboveWidget->height();
}
}
const auto rowsCount = shownRowsCount();
if (_aboveSearchWidget) {
_aboveSearchWidget->resizeToWidth(newWidth);
_aboveSearchWidget->moveToLeft(0, 0, newWidth);
if (hideAll || !showingSearch()) {
_aboveSearchWidget->hide();
} else {
_aboveSearchWidget->show();
_aboveHeight = _aboveSearchWidget->height();
}
}
const auto labelTop = rowsTop() + qMax(1, shownRowsCount()) * _rowHeight;
const auto labelWidth = newWidth - 2 * st::contactsPadding.left();
if (_description) {
_description->resizeToWidth(labelWidth);
_description->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
_description->setVisible(!showingSearch());
_description->setVisible(!hideAll && !showingSearch());
}
if (_searchNoResults) {
_searchNoResults->resizeToWidth(labelWidth);
_searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
_searchNoResults->setVisible(showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());
_searchNoResults->setVisible(!hideAll && showingSearch() && _filterResults.empty() && !_controller->isSearchLoading());
}
if (_searchLoading) {
_searchLoading->resizeToWidth(labelWidth);
_searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth);
_searchLoading->setVisible(showingSearch() && _filterResults.empty() && _controller->isSearchLoading());
_searchLoading->setVisible(!hideAll && showingSearch() && _filterResults.empty() && _controller->isSearchLoading());
}
const auto label = labelHeight();
const auto belowTop = (label > 0 || rowsCount > 0)
@@ -1119,7 +1145,7 @@ int PeerListContent::resizeGetHeight(int newWidth) {
if (_belowWidget) {
_belowWidget->resizeToWidth(newWidth);
_belowWidget->moveToLeft(0, belowTop, newWidth);
if (showingSearch()) {
if (hideAll || showingSearch()) {
_belowWidget->hide();
} else {
_belowWidget->show();
@@ -1203,33 +1229,56 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) {
}
}
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
void PeerListContent::showRowMenu(
not_null<PeerListRow*> row,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
showRowMenu(findRowIndex(row), QCursor::pos(), std::move(destroyed));
}
bool PeerListContent::showRowMenu(
RowIndex index,
QPoint globalPos,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
if (_contextMenu) {
_contextMenu->deleteLater();
_contextMenu->setDestroyedCallback(nullptr);
_contextMenu = nullptr;
}
setContexted(Selected());
if (e->reason() == QContextMenuEvent::Mouse) {
handleMouseMove(e->globalPos());
}
setContexted(_selected);
if (_pressButton != Qt::LeftButton) {
mousePressReleased(_pressButton);
}
if (const auto row = getRow(_contexted.index)) {
_contextMenu = _controller->rowContextMenu(this, row);
if (_contextMenu) {
_contextMenu->setDestroyedCallback(crl::guard(
this,
[this] {
setContexted(Selected());
handleMouseMove(QCursor::pos());
}));
_contextMenu->popup(e->globalPos());
e->accept();
}
const auto row = getRow(index);
if (!row) {
return false;
}
_contextMenu = _controller->rowContextMenu(this, row);
const auto raw = _contextMenu.get();
if (!raw) {
return false;
}
setContexted({ index, false });
raw->setDestroyedCallback(crl::guard(
this,
[=] {
setContexted(Selected());
handleMouseMove(QCursor::pos());
if (destroyed) {
destroyed(raw);
}
}));
raw->popup(globalPos);
return true;
}
void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
if (e->reason() == QContextMenuEvent::Mouse) {
handleMouseMove(e->globalPos());
}
if (showRowMenu(_selected.index, e->globalPos())) {
e->accept();
}
}
@@ -1351,15 +1400,18 @@ crl::time PeerListContent::paintRow(
return (refreshStatusAt - ms);
}
void PeerListContent::selectSkip(int direction) {
if (_pressed.index.value >= 0) {
return;
PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
if (hasPressed()) {
return { _selected.index.value, _selected.index.value };
}
_mouseSelection = false;
_lastMousePosition = std::nullopt;
auto newSelectedIndex = _selected.index.value + direction;
auto result = SkipResult();
result.shouldMoveTo = newSelectedIndex;
auto rowsCount = shownRowsCount();
auto index = 0;
auto firstEnabled = -1, lastEnabled = -1;
@@ -1415,14 +1467,36 @@ void PeerListContent::selectSkip(int direction) {
}
update();
_selectedIndex = _selected.index.value;
result.reallyMovedTo = _selected.index.value;
return result;
}
void PeerListContent::selectSkipPage(int height, int direction) {
auto rowsToSkip = height / _rowHeight;
if (!rowsToSkip) return;
if (!rowsToSkip) {
return;
}
selectSkip(rowsToSkip * direction);
}
rpl::producer<int> PeerListContent::selectedIndexValue() const {
return _selectedIndex.value();
}
bool PeerListContent::hasSelection() const {
return _selected.index.value >= 0;
}
bool PeerListContent::hasPressed() const {
return _pressed.index.value >= 0;
}
void PeerListContent::clearSelection() {
setSelected(Selected());
}
void PeerListContent::loadProfilePhotos() {
if (_visibleTop >= _visibleBottom) return;
@@ -1569,14 +1643,17 @@ void PeerListContent::setSearchQuery(
clearSearchRows();
}
void PeerListContent::submitted() {
bool PeerListContent::submitted() {
if (const auto row = getRow(_selected.index)) {
_controller->rowClicked(row);
return true;
} else if (showingSearch()) {
if (const auto row = getRow(RowIndex(0))) {
_controller->rowClicked(row);
return true;
}
}
return false;
}
void PeerListContent::visibleTopBottomUpdated(
@@ -1590,11 +1667,14 @@ void PeerListContent::visibleTopBottomUpdated(
void PeerListContent::setSelected(Selected selected) {
updateRow(_selected.index);
if (_selected != selected) {
_selected = selected;
updateRow(_selected.index);
setCursor(_selected.action ? style::cur_pointer : style::cur_default);
if (_selected == selected) {
return;
}
_selected = selected;
updateRow(_selected.index);
setCursor(_selected.action ? style::cur_pointer : style::cur_default);
_selectedIndex = _selected.index.value;
}
void PeerListContent::setContexted(Selected contexted) {

View File

@@ -254,10 +254,12 @@ class PeerListDelegate {
public:
virtual void peerListSetTitle(rpl::producer<QString> title) = 0;
virtual void peerListSetAdditionalTitle(rpl::producer<QString> title) = 0;
virtual void peerListSetHideEmpty(bool hide) = 0;
virtual void peerListSetDescription(object_ptr<Ui::FlatLabel> description) = 0;
virtual void peerListSetSearchLoading(object_ptr<Ui::FlatLabel> loading) = 0;
virtual void peerListSetSearchNoResults(object_ptr<Ui::FlatLabel> noResults) = 0;
virtual void peerListSetAboveWidget(object_ptr<TWidget> aboveWidget) = 0;
virtual void peerListSetAboveSearchWidget(object_ptr<TWidget> aboveWidget) = 0;
virtual void peerListSetBelowWidget(object_ptr<TWidget> belowWidget) = 0;
virtual void peerListSetSearchMode(PeerListSearchMode mode) = 0;
virtual void peerListAppendRow(std::unique_ptr<PeerListRow> row) = 0;
@@ -298,8 +300,10 @@ public:
peerListFinishSelectedRowsBunch();
}
virtual void peerListShowRowMenu(
not_null<PeerListRow*> row,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) = 0;
virtual int peerListSelectedRowsCount() = 0;
virtual std::vector<not_null<PeerData*>> peerListCollectSelectedRows() = 0;
virtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;
virtual void peerListRestoreState(
std::unique_ptr<PeerListState> state) = 0;
@@ -499,13 +503,20 @@ public:
QWidget *parent,
not_null<PeerListController*> controller);
void selectSkip(int direction);
struct SkipResult {
int shouldMoveTo = 0;
int reallyMovedTo = 0;
};
SkipResult selectSkip(int direction);
void selectSkipPage(int height, int direction);
[[nodiscard]] rpl::producer<int> selectedIndexValue() const;
[[nodiscard]] bool hasSelection() const;
[[nodiscard]] bool hasPressed() const;
void clearSelection();
void searchQueryChanged(QString query);
void submitted();
bool submitted();
// Interface for the controller.
void appendRow(std::unique_ptr<PeerListRow> row);
@@ -525,7 +536,9 @@ public:
void setSearchLoading(object_ptr<Ui::FlatLabel> loading);
void setSearchNoResults(object_ptr<Ui::FlatLabel> noResults);
void setAboveWidget(object_ptr<TWidget> widget);
void setAboveSearchWidget(object_ptr<TWidget> widget);
void setBelowWidget(object_ptr<TWidget> width);
void setHideEmpty(bool hide);
void refreshRows();
void setSearchMode(PeerListSearchMode mode);
@@ -547,6 +560,10 @@ public:
std::unique_ptr<PeerListState> saveState() const;
void restoreState(std::unique_ptr<PeerListState> state);
void showRowMenu(
not_null<PeerListRow*> row,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed);
auto scrollToRequests() const {
return _scrollToRequests.events();
}
@@ -630,6 +647,11 @@ private:
RowIndex findRowIndex(not_null<PeerListRow*> row, RowIndex hint = RowIndex());
QRect getActiveActionRect(not_null<PeerListRow*> row, RowIndex index) const;
bool showRowMenu(
RowIndex index,
QPoint globalPos,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr);
crl::time paintRow(Painter &p, crl::time ms, RowIndex index);
void addRowEntry(not_null<PeerListRow*> row);
@@ -667,6 +689,7 @@ private:
Selected _selected;
Selected _pressed;
Selected _contexted;
rpl::variable<int> _selectedIndex = -1;
bool _mouseSelection = false;
std::optional<QPoint> _lastMousePosition;
Qt::MouseButton _pressButton = Qt::LeftButton;
@@ -685,7 +708,9 @@ private:
int _aboveHeight = 0;
int _belowHeight = 0;
bool _hideEmpty = false;
object_ptr<TWidget> _aboveWidget = { nullptr };
object_ptr<TWidget> _aboveSearchWidget = { nullptr };
object_ptr<TWidget> _belowWidget = { nullptr };
object_ptr<Ui::FlatLabel> _description = { nullptr };
object_ptr<Ui::FlatLabel> _searchNoResults = { nullptr };
@@ -703,6 +728,9 @@ public:
_content = content;
}
void peerListSetHideEmpty(bool hide) override {
_content->setHideEmpty(hide);
}
void peerListAppendRow(
std::unique_ptr<PeerListRow> row) override {
_content->appendRow(std::move(row));
@@ -767,6 +795,9 @@ public:
void peerListSetAboveWidget(object_ptr<TWidget> aboveWidget) override {
_content->setAboveWidget(std::move(aboveWidget));
}
void peerListSetAboveSearchWidget(object_ptr<TWidget> aboveWidget) override {
_content->setAboveSearchWidget(std::move(aboveWidget));
}
void peerListSetBelowWidget(object_ptr<TWidget> belowWidget) override {
_content->setBelowWidget(std::move(belowWidget));
}
@@ -804,6 +835,11 @@ public:
std::unique_ptr<PeerListState> state) override {
_content->restoreState(std::move(state));
}
void peerListShowRowMenu(
not_null<PeerListRow*> row,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) override {
_content->showRowMenu(row, std::move(destroyed));
}
protected:
not_null<PeerListContent*> content() const {
@@ -824,6 +860,8 @@ public:
std::unique_ptr<PeerListController> controller,
Fn<void(not_null<PeerListBox*>)> init);
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
void peerListSetTitle(rpl::producer<QString> title) override {
setTitle(std::move(title));
}
@@ -840,7 +878,6 @@ public:
anim::type animated) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
void peerListScrollToTop() override;
protected:

View File

@@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "history/history.h"
#include "dialogs/dialogs_main_list.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller.h" // onShowAddContact()
#include "facades.h"
#include "styles/style_boxes.h"
#include "styles/style_profile.h"
@@ -115,7 +115,8 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
[=] { controller->widget()->onShowAddContact(); });
};
return Box<PeerListBox>(
std::make_unique<ContactsBoxController>(controller),
std::make_unique<ContactsBoxController>(
&sessionController->session()),
std::move(delegate));
}
@@ -159,9 +160,9 @@ void PeerListRowWithLink::paintAction(
}
PeerListGlobalSearchController::PeerListGlobalSearchController(
not_null<Window::SessionNavigation*> navigation)
: _navigation(navigation)
, _api(&_navigation->session().mtp()) {
not_null<Main::Session*> session)
: _session(session)
, _api(&session->mtp()) {
_timer.setCallback([this] { searchOnServer(); });
}
@@ -210,8 +211,8 @@ void PeerListGlobalSearchController::searchDone(
auto &contacts = result.c_contacts_found();
auto query = _query;
if (requestId) {
_navigation->session().data().processUsers(contacts.vusers());
_navigation->session().data().processChats(contacts.vchats());
_session->data().processUsers(contacts.vusers());
_session->data().processChats(contacts.vchats());
auto it = _queries.find(requestId);
if (it != _queries.cend()) {
query = it->second;
@@ -221,7 +222,7 @@ void PeerListGlobalSearchController::searchDone(
}
const auto feedList = [&](const MTPVector<MTPPeer> &list) {
for (const auto &mtpPeer : list.v) {
const auto peer = _navigation->session().data().peerLoaded(
const auto peer = _session->data().peerLoaded(
peerFromMTP(mtpPeer));
if (peer) {
delegate()->peerListSearchAddRow(peer);
@@ -246,9 +247,9 @@ ChatsListBoxController::Row::Row(not_null<History*> history)
}
ChatsListBoxController::ChatsListBoxController(
not_null<Window::SessionNavigation*> navigation)
not_null<Main::Session*> session)
: ChatsListBoxController(
std::make_unique<PeerListGlobalSearchController>(navigation)) {
std::make_unique<PeerListGlobalSearchController>(session)) {
}
ChatsListBoxController::ChatsListBoxController(
@@ -354,21 +355,21 @@ bool ChatsListBoxController::appendRow(not_null<History*> history) {
}
ContactsBoxController::ContactsBoxController(
not_null<Window::SessionNavigation*> navigation)
: PeerListController(
std::make_unique<PeerListGlobalSearchController>(navigation))
, _navigation(navigation) {
not_null<Main::Session*> session)
: ContactsBoxController(
session,
std::make_unique<PeerListGlobalSearchController>(session)) {
}
ContactsBoxController::ContactsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
std::unique_ptr<PeerListSearchController> searchController)
: PeerListController(std::move(searchController))
, _navigation(navigation) {
, _session(session) {
}
Main::Session &ContactsBoxController::session() const {
return _navigation->session();
return *_session;
}
void ContactsBoxController::prepare() {
@@ -435,26 +436,24 @@ bool ContactsBoxController::appendRow(not_null<UserData*> user) {
return false;
}
std::unique_ptr<PeerListRow> ContactsBoxController::createRow(not_null<UserData*> user) {
std::unique_ptr<PeerListRow> ContactsBoxController::createRow(
not_null<UserData*> user) {
return std::make_unique<PeerListRow>(user);
}
void AddBotToGroupBoxController::Start(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot) {
void AddBotToGroupBoxController::Start(not_null<UserData*> bot) {
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [box] { box->closeBox(); });
};
Ui::show(Box<PeerListBox>(
std::make_unique<AddBotToGroupBoxController>(navigation, bot),
std::make_unique<AddBotToGroupBoxController>(bot),
std::move(initBox)));
}
AddBotToGroupBoxController::AddBotToGroupBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot)
: ChatsListBoxController(SharingBotGame(bot)
? std::make_unique<PeerListGlobalSearchController>(navigation)
? std::make_unique<PeerListGlobalSearchController>(&bot->session())
: nullptr)
, _bot(bot) {
}
@@ -572,15 +571,15 @@ void AddBotToGroupBoxController::prepareViewHook() {
}
ChooseRecipientBoxController::ChooseRecipientBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
FnMut<void(not_null<PeerData*>)> callback)
: ChatsListBoxController(navigation)
, _navigation(navigation)
: ChatsListBoxController(session)
, _session(session)
, _callback(std::move(callback)) {
}
Main::Session &ChooseRecipientBoxController::session() const {
return _navigation->session();
return *_session;
}
void ChooseRecipientBoxController::prepareViewHook() {

View File

@@ -32,7 +32,6 @@ class History;
namespace Window {
class SessionController;
class SessionNavigation;
} // namespace Window
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(
@@ -65,8 +64,7 @@ private:
class PeerListGlobalSearchController : public PeerListSearchController {
public:
PeerListGlobalSearchController(
not_null<Window::SessionNavigation*> navigation);
explicit PeerListGlobalSearchController(not_null<Main::Session*> session);
void searchQuery(const QString &query) override;
bool isLoading() override;
@@ -79,7 +77,7 @@ private:
void searchOnServer();
void searchDone(const MTPcontacts_Found &result, mtpRequestId requestId);
const not_null<Window::SessionNavigation*> _navigation;
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::Timer _timer;
QString _query;
@@ -104,7 +102,7 @@ public:
};
ChatsListBoxController(not_null<Window::SessionNavigation*> navigation);
ChatsListBoxController(not_null<Main::Session*> session);
ChatsListBoxController(
std::unique_ptr<PeerListSearchController> searchController);
@@ -127,15 +125,15 @@ private:
class ContactsBoxController : public PeerListController {
public:
explicit ContactsBoxController(not_null<Main::Session*> session);
ContactsBoxController(
not_null<Window::SessionNavigation*> navigation);
ContactsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
std::unique_ptr<PeerListSearchController> searchController);
Main::Session &session() const override;
[[nodiscard]] Main::Session &session() const override;
void prepare() override final;
std::unique_ptr<PeerListRow> createSearchRow(not_null<PeerData*> peer) override final;
[[nodiscard]] std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override final;
void rowClicked(not_null<PeerListRow*> row) override;
protected:
@@ -150,7 +148,7 @@ private:
void checkForEmptyRows();
bool appendRow(not_null<UserData*> user);
const not_null<Window::SessionNavigation*> _navigation;
const not_null<Main::Session*> _session;
};
@@ -158,13 +156,9 @@ class AddBotToGroupBoxController
: public ChatsListBoxController
, public base::has_weak_ptr {
public:
static void Start(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot);
static void Start(not_null<UserData*> bot);
AddBotToGroupBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> bot);
explicit AddBotToGroupBoxController(not_null<UserData*> bot);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
@@ -186,7 +180,7 @@ private:
void shareBotGame(not_null<PeerData*> chat);
void addBotToGroup(not_null<PeerData*> chat);
not_null<UserData*> _bot;
const not_null<UserData*> _bot;
};
@@ -195,7 +189,7 @@ class ChooseRecipientBoxController
, public base::has_weak_ptr {
public:
ChooseRecipientBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<Main::Session*> session,
FnMut<void(not_null<PeerData*>)> callback);
Main::Session &session() const override;
@@ -210,7 +204,7 @@ protected:
std::unique_ptr<Row> createRow(not_null<History*> history) override;
private:
const not_null<Window::SessionNavigation*> _navigation;
const not_null<Main::Session*> _session;
FnMut<void(not_null<PeerData*>)> _callback;
};

View File

@@ -0,0 +1,429 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peer_lists_box.h"
#include "lang/lang_keys.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
PeerListsBox::PeerListsBox(
QWidget*,
std::vector<std::unique_ptr<PeerListController>> controllers,
Fn<void(not_null<PeerListsBox*>)> init)
: _lists(makeLists(std::move(controllers)))
, _init(std::move(init)) {
Expects(!_lists.empty());
}
auto PeerListsBox::collectSelectedRows()
-> std::vector<not_null<PeerData*>> {
auto result = std::vector<not_null<PeerData*>>();
auto items = _select
? _select->entity()->getItems()
: QVector<uint64>();
if (!items.empty()) {
result.reserve(items.size());
const auto session = &firstController()->session();
for (const auto itemId : items) {
const auto foreign = [&] {
for (const auto &list : _lists) {
if (list.controller->isForeignRow(itemId)) {
return true;
}
}
return false;
}();
if (!foreign) {
result.push_back(session->data().peer(itemId));
}
}
}
return result;
}
PeerListsBox::List PeerListsBox::makeList(
std::unique_ptr<PeerListController> controller) {
auto delegate = std::make_unique<Delegate>(this, controller.get());
return {
std::move(controller),
std::move(delegate),
};
}
std::vector<PeerListsBox::List> PeerListsBox::makeLists(
std::vector<std::unique_ptr<PeerListController>> controllers) {
auto result = std::vector<List>();
result.reserve(controllers.size());
for (auto &controller : controllers) {
result.push_back(makeList(std::move(controller)));
}
return result;
}
not_null<PeerListController*> PeerListsBox::firstController() const {
return _lists.front().controller.get();
}
void PeerListsBox::createMultiSelect() {
Expects(_select == nullptr);
auto entity = object_ptr<Ui::MultiSelect>(
this,
(firstController()->selectSt()
? *firstController()->selectSt()
: st::defaultMultiSelect),
tr::lng_participant_filter());
_select.create(this, std::move(entity));
_select->heightValue(
) | rpl::start_with_next(
[this] { updateScrollSkips(); },
lifetime());
_select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) {
for (const auto &list : _lists) {
if (list.content->submitted()) {
break;
}
}
});
_select->entity()->setQueryChangedCallback([=](const QString &query) {
searchQueryChanged(query);
});
_select->entity()->setItemRemovedCallback([=](uint64 itemId) {
for (const auto &list : _lists) {
if (list.controller->handleDeselectForeignRow(itemId)) {
return;
}
}
const auto session = &firstController()->session();
if (const auto peer = session->data().peerLoaded(itemId)) {
const auto id = peer->id;
for (const auto &list : _lists) {
if (const auto row = list.delegate->peerListFindRow(id)) {
list.content->changeCheckState(
row,
false,
anim::type::normal);
update();
}
list.controller->itemDeselectedHook(peer);
}
}
});
_select->resizeToWidth(firstController()->contentWidth());
_select->moveToLeft(0, 0);
}
int PeerListsBox::getTopScrollSkip() const {
auto result = 0;
if (_select && !_select->isHidden()) {
result += _select->height();
}
return result;
}
void PeerListsBox::updateScrollSkips() {
// If we show / hide the search field scroll top is fixed.
// If we resize search field by bubbles scroll bottom is fixed.
setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed);
if (!_select->animating()) {
_scrollBottomFixed = true;
}
}
void PeerListsBox::prepare() {
auto rows = setInnerWidget(
object_ptr<Ui::VerticalLayout>(this),
st::boxScroll);
for (auto &list : _lists) {
const auto content = rows->add(object_ptr<PeerListContent>(
rows,
list.controller.get()));
list.content = content;
list.delegate->setContent(content);
list.controller->setDelegate(list.delegate.get());
content->scrollToRequests(
) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
const auto skip = content->y();
onScrollToY(
skip + request.ymin,
(request.ymax >= 0) ? (skip + request.ymax) : request.ymax);
}, lifetime());
content->selectedIndexValue(
) | rpl::filter([=](int index) {
return (index >= 0);
}) | rpl::start_with_next([=] {
for (const auto &list : _lists) {
if (list.content && list.content != content) {
list.content->clearSelection();
}
}
}, lifetime());
}
rows->resizeToWidth(firstController()->contentWidth());
setDimensions(firstController()->contentWidth(), st::boxMaxListHeight);
if (_select) {
_select->finishAnimating();
Ui::SendPendingMoveResizeEvents(_select);
_scrollBottomFixed = true;
onScrollToY(0);
}
if (_init) {
_init(this);
}
}
void PeerListsBox::keyPressEvent(QKeyEvent *e) {
const auto skipRows = [&](int rows) {
if (rows == 0) {
return;
}
for (const auto &list : _lists) {
if (list.content->hasPressed()) {
return;
}
}
const auto from = begin(_lists), till = end(_lists);
auto i = from;
for (; i != till; ++i) {
if (i->content->hasSelection()) {
break;
}
}
if (i == till && rows < 0) {
return;
}
if (rows > 0) {
if (i == till) {
i = from;
}
for (; i != till; ++i) {
const auto result = i->content->selectSkip(rows);
if (result.shouldMoveTo - result.reallyMovedTo >= rows) {
continue;
} else if (result.reallyMovedTo >= result.shouldMoveTo) {
return;
} else {
rows = result.shouldMoveTo - result.reallyMovedTo;
}
}
} else {
for (++i; i != from;) {
const auto result = (--i)->content->selectSkip(rows);
if (result.shouldMoveTo - result.reallyMovedTo <= rows) {
continue;
} else if (result.reallyMovedTo <= result.shouldMoveTo) {
return;
} else {
rows = result.shouldMoveTo - result.reallyMovedTo;
}
}
}
};
const auto rowsInPage = [&] {
const auto rowHeight = firstController()->computeListSt().item.height;
return height() / rowHeight;
};
if (e->key() == Qt::Key_Down) {
skipRows(1);
} else if (e->key() == Qt::Key_Up) {
skipRows(-1);
} else if (e->key() == Qt::Key_PageDown) {
skipRows(rowsInPage());
} else if (e->key() == Qt::Key_PageUp) {
skipRows(-rowsInPage());
} else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) {
_select->entity()->clearQuery();
} else {
BoxContent::keyPressEvent(e);
}
}
void PeerListsBox::searchQueryChanged(const QString &query) {
onScrollToY(0);
for (const auto &list : _lists) {
list.content->searchQueryChanged(query);
}
}
void PeerListsBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
if (_select) {
_select->resizeToWidth(width());
_select->moveToLeft(0, 0);
updateScrollSkips();
}
for (const auto &list : _lists) {
list.content->resizeToWidth(width());
}
}
void PeerListsBox::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto &bg = (firstController()->listSt()
? *firstController()->listSt()
: st::peerListBox).bg;
for (const auto rect : e->region()) {
p.fillRect(rect, bg);
}
}
void PeerListsBox::setInnerFocus() {
if (!_select || !_select->toggled()) {
_lists.front().content->setFocus();
} else {
_select->entity()->setInnerFocus();
}
}
PeerListsBox::Delegate::Delegate(
not_null<PeerListsBox*> box,
not_null<PeerListController*> controller)
: _box(box)
, _controller(controller) {
}
void PeerListsBox::Delegate::peerListSetTitle(rpl::producer<QString> title) {
}
void PeerListsBox::Delegate::peerListSetAdditionalTitle(
rpl::producer<QString> title) {
}
void PeerListsBox::Delegate::peerListSetRowChecked(
not_null<PeerListRow*> row,
bool checked) {
if (checked) {
_box->addSelectItem(row, anim::type::normal);
PeerListContentDelegate::peerListSetRowChecked(row, checked);
peerListUpdateRow(row);
// This call deletes row from _searchRows.
_box->_select->entity()->clearQuery();
} else {
// The itemRemovedCallback will call changeCheckState() here.
_box->_select->entity()->removeItem(row->id());
peerListUpdateRow(row);
}
}
void PeerListsBox::Delegate::peerListSetForeignRowChecked(
not_null<PeerListRow*> row,
bool checked,
anim::type animated) {
if (checked) {
_box->addSelectItem(row, animated);
// This call deletes row from _searchRows.
_box->_select->entity()->clearQuery();
} else {
// The itemRemovedCallback will call changeCheckState() here.
_box->_select->entity()->removeItem(row->id());
}
}
void PeerListsBox::Delegate::peerListScrollToTop() {
_box->onScrollToY(0);
}
void PeerListsBox::Delegate::peerListSetSearchMode(PeerListSearchMode mode) {
PeerListContentDelegate::peerListSetSearchMode(mode);
_box->setSearchMode(mode);
}
void PeerListsBox::setSearchMode(PeerListSearchMode mode) {
auto selectVisible = (mode != PeerListSearchMode::Disabled);
if (selectVisible && !_select) {
createMultiSelect();
_select->toggle(!selectVisible, anim::type::instant);
}
if (_select) {
_select->toggle(selectVisible, anim::type::normal);
_scrollBottomFixed = false;
setInnerFocus();
}
}
void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() {
Expects(_box->_select != nullptr);
_box->_select->entity()->finishItemsBunch();
}
bool PeerListsBox::Delegate::peerListIsRowChecked(
not_null<PeerListRow*> row) {
return _box->_select
? _box->_select->entity()->hasItem(row->id())
: false;
}
int PeerListsBox::Delegate::peerListSelectedRowsCount() {
return _box->_select ? _box->_select->entity()->getItemsCount() : 0;
}
void PeerListsBox::addSelectItem(
not_null<PeerData*> peer,
anim::type animated) {
addSelectItem(
peer->id,
peer->shortName(),
PaintUserpicCallback(peer, false),
animated);
}
void PeerListsBox::addSelectItem(
not_null<PeerListRow*> row,
anim::type animated) {
addSelectItem(
row->id(),
row->generateShortName(),
row->generatePaintUserpicCallback(),
animated);
}
void PeerListsBox::addSelectItem(
uint64 itemId,
const QString &text,
Ui::MultiSelect::PaintRoundImage paintUserpic,
anim::type animated) {
if (!_select) {
createMultiSelect();
_select->hide(anim::type::instant);
}
const auto &activeBg = (firstController()->selectSt()
? *firstController()->selectSt()
: st::defaultMultiSelect).item.textActiveBg;
if (animated == anim::type::instant) {
_select->entity()->addItemInBunch(
itemId,
text,
activeBg,
std::move(paintUserpic));
} else {
_select->entity()->addItem(
itemId,
text,
activeBg,
std::move(paintUserpic));
}
}

View File

@@ -0,0 +1,101 @@
/*
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"
class PeerListsBox : public Ui::BoxContent {
public:
PeerListsBox(
QWidget*,
std::vector<std::unique_ptr<PeerListController>> controllers,
Fn<void(not_null<PeerListsBox*>)> init);
[[nodiscard]] std::vector<not_null<PeerData*>> collectSelectedRows();
protected:
void prepare() override;
void setInnerFocus() override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
class Delegate final : public PeerListContentDelegate {
public:
Delegate(
not_null<PeerListsBox*> box,
not_null<PeerListController*> controller);
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
void peerListSetSearchMode(PeerListSearchMode mode) override;
void peerListSetRowChecked(
not_null<PeerListRow*> row,
bool checked) override;
void peerListSetForeignRowChecked(
not_null<PeerListRow*> row,
bool checked,
anim::type animated) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) override {
_box->addSelectItem(peer, anim::type::instant);
}
void peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) override {
_box->addSelectItem(row, anim::type::instant);
}
void peerListFinishSelectedRowsBunch() override;
private:
const not_null<PeerListsBox*> _box;
const not_null<PeerListController*> _controller;
};
struct List {
std::unique_ptr<PeerListController> controller;
std::unique_ptr<Delegate> delegate;
PeerListContent *content = nullptr;
};
friend class Delegate;
[[nodiscard]] List makeList(
std::unique_ptr<PeerListController> controller);
[[nodiscard]] std::vector<List> makeLists(
std::vector<std::unique_ptr<PeerListController>> controllers);
[[nodiscard]] not_null<PeerListController*> firstController() const;
void addSelectItem(
not_null<PeerData*> peer,
anim::type animated);
void addSelectItem(
not_null<PeerListRow*> row,
anim::type animated);
void addSelectItem(
uint64 itemId,
const QString &text,
PaintRoundImageCallback paintUserpic,
anim::type animated);
void setSearchMode(PeerListSearchMode mode);
void createMultiSelect();
int getTopScrollSkip() const;
void updateScrollSkips();
void searchQueryChanged(const QString &query);
object_ptr<Ui::SlideWrap<Ui::MultiSelect>> _select = { nullptr };
std::vector<List> _lists;
Fn<void(PeerListsBox*)> _init;
bool _scrollBottomFixed = false;
};

View File

@@ -51,28 +51,21 @@ base::flat_set<not_null<UserData*>> GetAlreadyInFromPeer(PeerData *peer) {
} // namespace
AddParticipantsBoxController::AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation)
: ContactsBoxController(
navigation,
std::make_unique<PeerListGlobalSearchController>(navigation)) {
not_null<Main::Session*> session)
: ContactsBoxController(session) {
}
AddParticipantsBoxController::AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer)
: AddParticipantsBoxController(
navigation,
peer,
GetAlreadyInFromPeer(peer)) {
}
AddParticipantsBoxController::AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> &&alreadyIn)
: ContactsBoxController(
navigation,
std::make_unique<PeerListGlobalSearchController>(navigation))
: ContactsBoxController(&peer->session())
, _peer(peer)
, _alreadyIn(std::move(alreadyIn)) {
subscribeToMigration();
@@ -179,7 +172,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers(
not_null<PeerListBox*> box) const {
Expects(_peer != nullptr);
const auto rows = box->peerListCollectSelectedRows();
const auto rows = box->collectSelectedRows();
const auto users = ranges::view::all(
rows
) | ranges::view::transform([](not_null<PeerData*> peer) {
@@ -198,9 +191,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers(
void AddParticipantsBoxController::Start(
not_null<Window::SessionNavigation*> navigation,
not_null<ChatData*> chat) {
auto controller = std::make_unique<AddParticipantsBoxController>(
navigation,
chat);
auto controller = std::make_unique<AddParticipantsBoxController>(chat);
const auto weak = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
box->addButton(tr::lng_participant_invite(), [=] {
@@ -223,7 +214,6 @@ void AddParticipantsBoxController::Start(
base::flat_set<not_null<UserData*>> &&alreadyIn,
bool justCreated) {
auto controller = std::make_unique<AddParticipantsBoxController>(
navigation,
channel,
std::move(alreadyIn));
const auto weak = controller.get();

View File

@@ -27,16 +27,16 @@ public:
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> &&alreadyIn);
explicit AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation);
explicit AddParticipantsBoxController(not_null<Main::Session*> session);
explicit AddParticipantsBoxController(not_null<PeerData*> peer);
AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
AddParticipantsBoxController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> &&alreadyIn);
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
void rowClicked(not_null<PeerListRow*> row) override;
void itemDeselectedHook(not_null<PeerData*> peer) override;

View File

@@ -1369,9 +1369,12 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
return false;
}
auto added = false;
_additional.fillFromPeer();
for (const auto user : info->lastParticipants) {
appendRow(user);
if (appendRow(user)) {
added = true;
}
//
// Don't count lastParticipants in _offset, because we don't know
@@ -1383,7 +1386,7 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
if (_onlineSorter) {
_onlineSorter->sort();
}
return true;
return added;
}
void ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {

View File

@@ -1000,13 +1000,15 @@ void Controller::fillManageSection() {
st::infoIconBlacklist);
}
if (hasRecentActions) {
auto callback = [=] {
_navigation->showSection(
std::make_shared<AdminLog::SectionMemento>(channel));
};
AddButtonWithCount(
_controls.buttonsLayout,
tr::lng_manage_peer_recent_actions(),
rpl::single(QString()), //Empty count.
[=] {
_navigation->showSection(AdminLog::SectionMemento(channel));
},
std::move(callback),
st::infoIconRecentActions);
}

View File

@@ -767,7 +767,7 @@ bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
if (result.error == Ui::PreparedList::Error::None) {
return result;
} else if (data->hasImage()) {
auto image = Platform::GetClipboardImage();
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}

View File

@@ -292,7 +292,7 @@ void SessionsContent::terminateAll() {
auto callback = [=] {
const auto reset = crl::guard(weak, [=] {
_authorizations->cancelCurrentRequest();
shortPollSessions();
_authorizations->reload();
});
_authorizations->requestTerminate(
[=](const MTPBool &result) { reset(); },

View File

@@ -334,14 +334,20 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
_setHash = set.vhash().v;
_setFlags = set.vflags().v;
_setInstallDate = set.vinstalled_date().value_or(0);
if (const auto thumb = set.vthumb()) {
_setThumbnail = Images::FromPhotoSize(
&_controller->session(),
set,
*thumb);
} else {
_setThumbnail = ImageWithLocation();
}
_setThumbnail = [&] {
if (const auto thumbs = set.vthumbs()) {
for (const auto &thumb : thumbs->v) {
const auto result = Images::FromPhotoSize(
&_controller->session(),
set,
thumb);
if (result.location.valid()) {
return result;
}
}
}
return ImageWithLocation();
}();
const auto &sets = _controller->session().data().stickers().sets();
const auto it = sets.find(_setId);
if (it != sets.cend()) {

View File

@@ -9,6 +9,7 @@ using "ui/basic.style";
using "ui/widgets/widgets.style";
using "ui/layers/layers.style";
using "ui/chat/chat.style"; // GroupCallUserpics
using "window/window.style";
CallSignalBars {
@@ -465,8 +466,8 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
}
height: 52px;
photoPosition: point(12px, 6px);
namePosition: point(68px, 7px);
statusPosition: point(68px, 26px);
namePosition: point(63px, 7px);
statusPosition: point(63px, 26px);
photoSize: 40px;
nameFg: groupCallMembersFg;
nameFgChecked: groupCallMembersFg;
@@ -477,11 +478,17 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
groupCallMembersList: PeerList(defaultPeerList) {
bg: groupCallMembersBg;
about: FlatLabel(defaultPeerListAbout) {
textFg: groupCallMemberInactiveStatus;
textFg: groupCallMemberNotJoinedStatus;
}
item: groupCallMembersListItem;
}
groupCallInviteDividerLabel: FlatLabel(defaultFlatLabel) {
textFg: groupCallMemberNotJoinedStatus;
}
groupCallInviteDividerPadding: margins(17px, 7px, 17px, 7px);
groupCallInviteMembersList: PeerList(groupCallMembersList) {
padding: margins(0px, 10px, 0px, 10px);
item: PeerListItem(groupCallMembersListItem) {
statusFg: groupCallMemberNotJoinedStatus;
statusFgOver: groupCallMemberNotJoinedStatus;
@@ -512,26 +519,36 @@ groupCallMultiSelect: MultiSelect(defaultMultiSelect) {
}
}
groupCallMembersHeader: 47px;
groupCallMembersTop: 62px;
groupCallTitleTop: 14px;
groupCallSubtitleTop: 33px;
groupCallMembersMargin: margins(16px, 16px, 16px, 28px);
groupCallAddMember: IconButton(defaultIconButton) {
width: 36px;
height: 36px;
iconPosition: point(3px, 5px);
icon: icon {{ "info_add_member", groupCallMemberInactiveIcon }};
iconOver: icon {{ "info_add_member", groupCallMemberInactiveIcon }};
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 36px;
groupCallAddMember: SettingsButton(defaultSettingsButton) {
textFg: groupCallMemberNotJoinedStatus;
textFgOver: groupCallMemberNotJoinedStatus;
textBg: groupCallMembersBg;
textBgOver: groupCallMembersBgOver;
font: semiboldFont;
height: 22px;
padding: margins(63px, 17px, 22px, 11px);
ripple: groupCallRipple;
}
groupCallHeaderPosition: point(16px, 16px);
groupCallHeaderLabel: FlatLabel(defaultFlatLabel) {
groupCallAddMemberIcon: icon {{ "info_add_member", groupCallMemberInactiveIcon, point(0px, 3px) }};
groupCallAddMemberIconLeft: 16px;
groupCallSubtitleLabel: FlatLabel(defaultFlatLabel) {
maxHeight: 18px;
textFg: groupCallMemberNotJoinedStatus;
}
groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) {
textFg: groupCallMembersFg;
style: TextStyle(defaultTextStyle) {
font: semiboldFont;
linkFont: semiboldFont;
linkFontOver: semiboldFont;
font: font(semibold 14px);
linkFont: font(semibold 14px);
linkFontOver: font(semibold 14px);
}
}
groupCallAddButtonPosition: point(10px, 7px);
@@ -562,6 +579,8 @@ groupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCross
fg: groupCallMemberMutedIcon;
icon: icon {{ "calls/group_calls_unmuted", groupCallMemberActiveIcon }};
}
groupCallMemberInvited: icon {{ "calls/group_calls_invited", groupCallMemberInactiveIcon }};
groupCallMemberInvitedPosition: point(2px, 12px);
groupCallSettings: CallButton(callMicrophoneMute) {
button: IconButton(callButton) {
@@ -584,12 +603,15 @@ groupCallHangup: CallButton(callHangup) {
label: callButtonLabel;
}
groupCallButtonSkip: 43px;
groupCallButtonBottomSkip: 134px;
groupCallMuteBottomSkip: 149px;
groupCallButtonBottomSkip: 145px;
groupCallMuteBottomSkip: 160px;
groupCallTopBarUserpicSize: 28px;
groupCallTopBarUserpicShift: 8px;
groupCallTopBarUserpicStroke: 2px;
groupCallTopBarUserpics: GroupCallUserpics {
size: 28px;
shift: 8px;
stroke: 2px;
align: align(left);
}
groupCallTopBarJoin: RoundButton(defaultActiveButton) {
width: -26px;
height: 26px;
@@ -722,8 +744,8 @@ groupCallTitleCloseIconOver: icon {
};
groupCallTitle: WindowTitle(defaultWindowTitle) {
height: 0px;
bg: groupCallBg;
bgActive: groupCallBg;
bg: transparent;
bgActive: transparent;
fg: transparent;
fgActive: transparent;
minimize: IconButton(groupCallTitleButton) {
@@ -750,16 +772,11 @@ groupCallTitle: WindowTitle(defaultWindowTitle) {
closeIconActiveOver: groupCallTitleCloseIconOver;
}
groupCallMajorBlobMinRadius: 2px;
groupCallMajorBlobMaxRadius: 2px;
groupCallMajorBlobIdleRadius: 2px;
groupCallMajorBlobMaxRadius: 4px;
groupCallMinorBlobMinRadius: 3px;
groupCallMinorBlobMaxRadius: 9px;
groupCallMajorBlobTopOffset: 0px;
groupCallMinorBlobTopOffset: 6px;
groupCallBlobWidthAdditional: 40px;
groupCallMinorBlobIdleRadius: 3px;
groupCallMinorBlobMaxRadius: 12px;
callTopBarMuteCrossLine: CrossLineAnimation {
fg: callBarFg;

View File

@@ -368,10 +368,10 @@ void Call::setupOutgoingVideo() {
_errors.fire({ ErrorType::NoCamera });
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
} else if (_state.current() != State::Established
&& state != started
&& !_videoCapture) {
&& (state != Webrtc::VideoState::Inactive)
&& (started == Webrtc::VideoState::Inactive)) {
_errors.fire({ ErrorType::NotStartedCall });
_videoOutgoing->setState(started);
_videoOutgoing->setState(Webrtc::VideoState::Inactive);
} else if (state != Webrtc::VideoState::Inactive
&& _instance
&& !_instance->supportsVideo()) {
@@ -951,20 +951,20 @@ void Call::setState(State state) {
_startTime = crl::now();
break;
case State::ExchangingKeys:
_delegate->playSound(Delegate::Sound::Connecting);
_delegate->callPlaySound(Delegate::CallSound::Connecting);
break;
case State::Ended:
_delegate->playSound(Delegate::Sound::Ended);
_delegate->callPlaySound(Delegate::CallSound::Ended);
[[fallthrough]];
case State::EndedByOtherDevice:
_delegate->callFinished(this);
break;
case State::Failed:
_delegate->playSound(Delegate::Sound::Ended);
_delegate->callPlaySound(Delegate::CallSound::Ended);
_delegate->callFailed(this);
break;
case State::Busy:
_delegate->playSound(Delegate::Sound::Busy);
_delegate->callPlaySound(Delegate::CallSound::Busy);
break;
}
}

View File

@@ -62,15 +62,16 @@ public:
virtual void callFailed(not_null<Call*> call) = 0;
virtual void callRedial(not_null<Call*> call) = 0;
enum class Sound {
enum class CallSound {
Connecting,
Busy,
Ended,
};
virtual void playSound(Sound sound) = 0;
virtual void callPlaySound(CallSound sound) = 0;
virtual void callRequestPermissionsOrFail(
Fn<void()> onSuccess,
bool video) = 0;
virtual auto getVideoCapture()
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;

View File

@@ -12,17 +12,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "lang/lang_hardcoded.h"
#include "boxes/confirm_box.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 "webrtc/webrtc_media_devices.h"
#include <tgcalls/group/GroupInstanceImpl.h>
@@ -41,20 +43,31 @@ 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<Webrtc::MediaDevices> CreateMediaDevices() {
const auto &settings = Core::App().settings();
return Webrtc::CreateMediaDevices(
settings.callInputDeviceId(),
settings.callOutputDeviceId(),
settings.callVideoInputDeviceId());
}
} // namespace
GroupCall::GroupCall(
not_null<Delegate*> delegate,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
const MTPInputGroupCall &inputCall)
: _delegate(delegate)
, _channel(channel)
, _history(channel->owner().history(channel))
, _api(&_channel->session().mtp())
, _peer(peer)
, _history(peer->owner().history(peer))
, _api(&peer->session().mtp())
, _lastSpokeCheckTimer([=] { checkLastSpoke(); })
, _checkJoinedTimer([=] { checkJoined(); })
, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) {
, _pushToTalkCancelTimer([=] { pushToTalkCancel(); })
, _connectingSoundTimer([=] { playConnectingSoundOnce(); })
, _mediaDevices(CreateMediaDevices()) {
_muted.value(
) | rpl::combine_previous(
) | rpl::start_with_next([=](MuteState previous, MuteState state) {
@@ -70,8 +83,8 @@ GroupCall::GroupCall(
const auto id = inputCall.c_inputGroupCall().vid().v;
if (id) {
if (const auto call = _channel->call(); call && call->id() == id) {
if (!_channel->canManageCall() && call->joinMuted()) {
if (const auto call = _peer->groupCall(); call && call->id() == id) {
if (!_peer->canManageGroupCall() && call->joinMuted()) {
_muted = MuteState::ForceMuted;
}
}
@@ -80,6 +93,22 @@ GroupCall::GroupCall(
} else {
start();
}
_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() {
@@ -108,9 +137,22 @@ void GroupCall::setState(State state) {
}
_state = state;
if (_state.current() == State::Joined && !_pushToTalkStarted) {
_pushToTalkStarted = true;
applyGlobalShortcutChanges();
if (state == State::Joined) {
stopConnectingSound();
if (!_hadJoinedState) {
_hadJoinedState = true;
applyGlobalShortcutChanges();
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Started);
}
if (const auto call = _peer->groupCall(); call && call->id() == _id) {
call->setInCall();
}
} else if (state == State::Connecting || state == State::Joining) {
if (_hadJoinedState) {
playConnectingSound();
}
} else {
stopConnectingSound();
}
if (false
@@ -121,6 +163,10 @@ void GroupCall::setState(State state) {
destroyController();
}
switch (state) {
case State::HangingUp:
case State::FailedHangingUp:
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended);
break;
case State::Ended:
_delegate->groupCallFinished(this);
break;
@@ -135,13 +181,29 @@ void GroupCall::setState(State state) {
}
}
void GroupCall::playConnectingSound() {
if (_connectingSoundTimer.isActive()) {
return;
}
playConnectingSoundOnce();
_connectingSoundTimer.callEach(kPlayConnectingEach);
}
void GroupCall::stopConnectingSound() {
_connectingSoundTimer.cancel();
}
void GroupCall::playConnectingSoundOnce() {
_delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting);
}
void GroupCall::start() {
_createRequestId = _api.request(MTPphone_CreateGroupCall(
_channel->inputChannel,
_peer->input,
MTP_int(rand_value<int32>())
)).done([=](const MTPUpdates &result) {
_acceptFields = true;
_channel->session().api().applyUpdates(result);
_peer->session().api().applyUpdates(result);
_acceptFields = false;
}).fail([=](const RPCError &error) {
LOG(("Call Error: Could not create, error: %1"
@@ -157,7 +219,13 @@ void GroupCall::start() {
void GroupCall::join(const MTPInputGroupCall &inputCall) {
setState(State::Joining);
_channel->setCall(inputCall);
if (const auto chat = _peer->asChat()) {
chat->setGroupCall(inputCall);
} else if (const auto group = _peer->asMegagroup()) {
group->setGroupCall(inputCall);
} else {
Unexpected("Peer type in GroupCall::join.");
}
inputCall.match([&](const MTPDinputGroupCall &data) {
_id = data.vid().v;
@@ -166,7 +234,7 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) {
});
using Update = Data::GroupCall::ParticipantUpdate;
_channel->call()->participantUpdated(
_peer->groupCall()->participantUpdated(
) | rpl::filter([=](const Update &update) {
return (_instance != nullptr) && !update.now;
}) | rpl::start_with_next([=](const Update &update) {
@@ -174,6 +242,10 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) {
_instance->removeSsrcs({ update.was->ssrc });
}, _lifetime);
SubscribeToMigration(_peer, _lifetime, [=](not_null<ChannelData*> group) {
_peer = group;
});
}
void GroupCall::rejoin() {
@@ -229,7 +301,7 @@ void GroupCall::rejoin() {
: State::Connecting);
applySelfInCallLocally();
maybeSendMutedUpdate(wasMuteState);
_channel->session().api().applyUpdates(updates);
_peer->session().api().applyUpdates(updates);
}).fail([=](const RPCError &error) {
const auto type = error.type();
LOG(("Call Error: Could not join, error: %1").arg(type));
@@ -255,13 +327,13 @@ void GroupCall::rejoin() {
}
void GroupCall::applySelfInCallLocally() {
const auto call = _channel->call();
const auto call = _peer->groupCall();
if (!call || call->id() != _id) {
return;
}
using Flag = MTPDgroupCallParticipant::Flag;
const auto &participants = call->participants();
const auto self = _channel->session().user();
const auto self = _peer->session().user();
const auto i = ranges::find(
participants,
self,
@@ -307,7 +379,7 @@ void GroupCall::discard() {
// 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(); });
_channel->session().api().applyUpdates(result);
_peer->session().api().applyUpdates(result);
}).fail([=](const RPCError &error) {
hangup();
}).send();
@@ -338,7 +410,7 @@ void GroupCall::finish(FinishType type) {
// We want to leave request still being sent and processed even if
// the call is already destroyed.
const auto session = &_channel->session();
const auto session = &_peer->session();
const auto weak = base::make_weak(this);
session->api().request(MTPphone_LeaveGroupCall(
inputCall(),
@@ -453,7 +525,7 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
return;
}
const auto self = _channel->session().userId();
const auto self = _peer->session().userId();
for (const auto &participant : data.vparticipants().v) {
participant.match([&](const MTPDgroupCallParticipant &data) {
if (data.vuser_id().v != self) {
@@ -482,31 +554,33 @@ void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) {
}
void GroupCall::createAndStartController() {
using AudioLevels = std::vector<std::pair<uint32_t, float>>;
const auto &settings = Core::App().settings();
const auto weak = base::make_weak(this);
const auto myLevel = std::make_shared<float>();
const auto myLevel = std::make_shared<tgcalls::GroupLevelValue>();
tgcalls::GroupInstanceDescriptor descriptor = {
.config = tgcalls::GroupConfig{
},
.networkStateUpdated = [=](bool connected) {
crl::on_main(weak, [=] { setInstanceConnected(connected); });
},
.audioLevelsUpdated = [=](const AudioLevels &data) {
if (!data.empty()) {
crl::on_main(weak, [=] { audioLevelsUpdated(data); });
.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); });
},
.myAudioLevelUpdated = [=](float level) {
if (*myLevel != level) { // Don't send many 0 while we're muted.
*myLevel = level;
crl::on_main(weak, [=] { myLevelUpdated(level); });
}
},
.initialInputDeviceId = settings.callInputDeviceId().toStdString(),
.initialOutputDeviceId = settings.callOutputDeviceId().toStdString(),
.initialInputDeviceId = _audioInputId.toStdString(),
.initialOutputDeviceId = _audioOutputId.toStdString(),
};
if (Logs::DebugEnabled()) {
auto callLogFolder = cWorkingDir() + qsl("DebugLogs");
@@ -526,6 +600,7 @@ void GroupCall::createAndStartController() {
LOG(("Call Info: Creating group instance"));
_instance = std::make_unique<tgcalls::GroupInstanceImpl>(
std::move(descriptor));
updateInstanceMuteState();
//raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
@@ -539,28 +614,32 @@ void GroupCall::updateInstanceMuteState() {
&& state != MuteState::PushToTalk);
}
void GroupCall::handleLevelsUpdated(
gsl::span<const std::pair<std::uint32_t, float>> data) {
Expects(!data.empty());
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 &[ssrc, level] : data) {
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 self = (ssrc == _mySsrc);
_levelUpdates.fire(LevelUpdate{
.ssrc = ssrc,
.value = level,
.voice = voice,
.self = self
});
if (level <= kSpeakLevelThreshold) {
continue;
}
if (self
&& voice
&& (!_lastSendProgressUpdate
|| _lastSendProgressUpdate + kUpdateSendActionEach < now)) {
_lastSendProgressUpdate = now;
_channel->session().sendProgressManager().update(
_peer->session().sendProgressManager().update(
_history,
Api::SendProgressType::Speaking);
}
@@ -568,13 +647,21 @@ void GroupCall::handleLevelsUpdated(
check = true;
const auto i = _lastSpoke.find(ssrc);
if (i == _lastSpoke.end()) {
_lastSpoke.emplace(ssrc, now);
_lastSpoke.emplace(ssrc, Data::LastSpokeTimes{
.anything = now,
.voice = voice ? now : 0,
});
checkNow = true;
} else {
if (i->second + kCheckLastSpokeInterval / 3 <= now) {
if ((i->second.anything + kCheckLastSpokeInterval / 3 <= now)
|| (voice
&& i->second.voice + kCheckLastSpokeInterval / 3 <= now)) {
checkNow = true;
}
i->second = now;
i->second.anything = now;
if (voice) {
i->second.voice = now;
}
}
}
if (checkNow) {
@@ -584,18 +671,8 @@ void GroupCall::handleLevelsUpdated(
}
}
void GroupCall::myLevelUpdated(float level) {
const auto pair = std::pair<std::uint32_t, float>{ _mySsrc, level };
handleLevelsUpdated({ &pair, &pair + 1 });
}
void GroupCall::audioLevelsUpdated(
const std::vector<std::pair<std::uint32_t, float>> &data) {
handleLevelsUpdated(gsl::make_span(data));
}
void GroupCall::checkLastSpoke() {
const auto real = _channel->call();
const auto real = _peer->groupCall();
if (!real || real->id() != _id) {
return;
}
@@ -605,7 +682,7 @@ void GroupCall::checkLastSpoke() {
auto list = base::take(_lastSpoke);
for (auto i = list.begin(); i != list.end();) {
const auto [ssrc, when] = *i;
if (when + kCheckLastSpokeInterval >= now) {
if (when.anything + kCheckLastSpokeInterval >= now) {
hasRecent = true;
++i;
} else {
@@ -678,7 +755,7 @@ void GroupCall::sendMutedUpdate() {
MTP_inputUserSelf()
)).done([=](const MTPUpdates &result) {
_updateMuteRequestId = 0;
_channel->session().api().applyUpdates(result);
_peer->session().api().applyUpdates(result);
}).fail([=](const RPCError &error) {
_updateMuteRequestId = 0;
if (error.type() == u"GROUPCALL_FORBIDDEN"_q) {
@@ -689,14 +766,20 @@ void GroupCall::sendMutedUpdate() {
}).send();
}
rpl::producer<bool> GroupCall::connectingValue() const {
using namespace rpl::mappers;
return _state.value() | rpl::map(
_1 == State::Creating
|| _1 == State::Joining
|| _1 == State::Connecting
) | rpl::distinct_until_changed();
}
void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) {
if (_instance) {
const auto id = deviceId.toStdString();
if (input) {
_instance->setAudioInputDevice(id);
} else {
_instance->setAudioOutputDevice(id);
}
if (input) {
_mediaDevices->switchToAudioInput(deviceId);
} else {
_mediaDevices->switchToAudioOutput(deviceId);
}
}
@@ -711,7 +794,7 @@ void GroupCall::toggleMute(not_null<UserData*> user, bool mute) {
inputCall(),
user->inputUser
)).done([=](const MTPUpdates &result) {
_channel->session().api().applyUpdates(result);
_peer->session().api().applyUpdates(result);
}).fail([=](const RPCError &error) {
if (error.type() == u"GROUPCALL_FORBIDDEN"_q) {
LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember."
@@ -723,11 +806,11 @@ void GroupCall::toggleMute(not_null<UserData*> user, bool mute) {
std::variant<int, not_null<UserData*>> GroupCall::inviteUsers(
const std::vector<not_null<UserData*>> &users) {
const auto real = _channel->call();
const auto real = _peer->groupCall();
if (!real || real->id() != _id) {
return 0;
}
const auto owner = &_channel->owner();
const auto owner = &_peer->owner();
const auto &invited = owner->invitedToCallUsers(_id);
const auto &participants = real->participants();
auto &&toInvite = users | ranges::view::filter([&](
@@ -748,7 +831,7 @@ std::variant<int, not_null<UserData*>> GroupCall::inviteUsers(
inputCall(),
MTP_vector<MTPInputUser>(slice)
)).done([=](const MTPUpdates &result) {
_channel->session().api().applyUpdates(result);
_peer->session().api().applyUpdates(result);
}).send();
slice.clear();
};
@@ -756,7 +839,7 @@ std::variant<int, not_null<UserData*>> GroupCall::inviteUsers(
if (!count && slice.empty()) {
result = user;
}
owner->registerInvitedToCallUser(_id, _channel, user);
owner->registerInvitedToCallUser(_id, _peer, user);
slice.push_back(user->inputUser);
if (slice.size() == kMaxInvitePerSlice) {
sendSlice();
@@ -808,21 +891,26 @@ void GroupCall::applyGlobalShortcutChanges() {
}
_pushToTalk = shortcut;
_shortcutManager->startWatching(_pushToTalk, [=](bool pressed) {
const auto delay = Core::App().settings().groupCallPushToTalkDelay();
if (muted() == MuteState::ForceMuted
|| muted() == MuteState::Active) {
return;
} else if (pressed) {
_pushToTalkCancelTimer.cancel();
setMuted(MuteState::PushToTalk);
} else if (delay) {
_pushToTalkCancelTimer.callOnce(delay);
} else {
pushToTalkCancel();
}
pushToTalk(
pressed,
Core::App().settings().groupCallPushToTalkDelay());
});
}
void GroupCall::pushToTalk(bool pressed, crl::time delay) {
if (muted() == MuteState::ForceMuted
|| 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) {
@@ -864,7 +952,7 @@ void GroupCall::handleControllerError(const QString &error) {
// "{user}",
// _user->name)));
} else if (error == u"ERROR_AUDIO_IO"_q) {
Ui::show(Box<InformBox>(tr::lng_call_error_audio_io(tr::now)));
//Ui::show(Box<InformBox>(tr::lng_call_error_audio_io(tr::now)));
}
//finish(FinishType::Failed);
}

View File

@@ -17,6 +17,7 @@ class History;
namespace tgcalls {
class GroupInstanceImpl;
struct GroupLevelsUpdate;
} // namespace tgcalls
namespace base {
@@ -24,6 +25,14 @@ class GlobalShortcutManager;
class GlobalShortcutValue;
} // namespace base
namespace Webrtc {
class MediaDevices;
} // namespace Webrtc
namespace Data {
struct LastSpokeTimes;
} // namespace Data
namespace Calls {
enum class MuteState {
@@ -42,6 +51,7 @@ enum class MuteState {
struct LevelUpdate {
uint32 ssrc = 0;
float value = 0.;
bool voice = false;
bool self = false;
};
@@ -55,21 +65,28 @@ public:
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
virtual void groupCallRequestPermissionsOrFail(
Fn<void()> onSuccess) = 0;
enum class GroupCallSound {
Started,
Connecting,
Ended,
};
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
};
using GlobalShortcutManager = base::GlobalShortcutManager;
GroupCall(
not_null<Delegate*> delegate,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
const MTPInputGroupCall &inputCall);
~GroupCall();
[[nodiscard]] uint64 id() const {
return _id;
}
[[nodiscard]] not_null<ChannelData*> channel() const {
return _channel;
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
void start();
@@ -103,6 +120,7 @@ public:
[[nodiscard]] rpl::producer<State> stateValue() const {
return _state.value();
}
[[nodiscard]] rpl::producer<bool> connectingValue() const;
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
return _levelUpdates.events();
@@ -120,6 +138,8 @@ public:
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
void applyGlobalShortcutChanges();
void pushToTalk(bool pressed, crl::time delay);
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
@@ -146,11 +166,7 @@ private:
void applySelfInCallLocally();
void rejoin();
void myLevelUpdated(float level);
void audioLevelsUpdated(
const std::vector<std::pair<std::uint32_t, float>> &data);
void handleLevelsUpdated(
gsl::span<const std::pair<std::uint32_t, float>> data);
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
void setInstanceConnected(bool connected);
void checkLastSpoke();
void pushToTalkCancel();
@@ -158,11 +174,15 @@ private:
void checkGlobalShortcutAvailability();
void checkJoined();
void playConnectingSound();
void stopConnectingSound();
void playConnectingSoundOnce();
[[nodiscard]] MTPInputGroupCall inputCall() const;
const not_null<Delegate*> _delegate;
const not_null<ChannelData*> _channel;
const not_null<History*> _history;
not_null<PeerData*> _peer; // Can change in legacy group migration.
not_null<History*> _history; // Can change in legacy group migration.
MTP::Sender _api;
rpl::variable<State> _state = State::Creating;
bool _instanceConnected = false;
@@ -178,7 +198,7 @@ private:
std::unique_ptr<tgcalls::GroupInstanceImpl> _instance;
rpl::event_stream<LevelUpdate> _levelUpdates;
base::flat_map<uint32, crl::time> _lastSpoke;
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
base::Timer _lastSpokeCheckTimer;
base::Timer _checkJoinedTimer;
@@ -187,7 +207,12 @@ private:
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
base::Timer _pushToTalkCancelTimer;
bool _pushToTalkStarted = false;
base::Timer _connectingSoundTimer;
bool _hadJoinedState = false;
std::unique_ptr<Webrtc::MediaDevices> _mediaDevices;
QString _audioInputId;
QString _audioOutputId;
rpl::lifetime _lifetime;

View File

@@ -9,10 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/calls_group_call.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.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 "ui/paint/blobs.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/scroll_area.h"
@@ -24,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h" // Core::App().domain().activate.
#include "main/main_session.h"
#include "base/timer.h"
#include "boxes/peers/edit_participants_box.h"
#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"
@@ -84,6 +87,7 @@ public:
Active,
Inactive,
Muted,
Invited,
};
void setSkipLevelUpdate(bool value);
@@ -96,6 +100,9 @@ public:
[[nodiscard]] uint32 ssrc() const {
return _ssrc;
}
[[nodiscard]] bool sounding() const {
return _sounding;
}
[[nodiscard]] bool speaking() const {
return _speaking;
}
@@ -112,7 +119,9 @@ public:
st::groupCallActiveButton.height);
}
bool actionDisabled() const override {
return peer()->isSelf() || !_delegate->rowCanMuteMembers();
return peer()->isSelf()
|| (_state == State::Invited)
|| !_delegate->rowCanMuteMembers();
}
QMargins actionMargins() const override {
return QMargins(
@@ -131,6 +140,15 @@ public:
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(
@@ -146,7 +164,7 @@ private:
Ui::Paint::Blobs blobs;
crl::time lastTime = 0;
crl::time lastSpeakingUpdateTime = 0;
crl::time lastSoundingUpdateTime = 0;
float64 enter = 0.;
QImage userpicCache;
@@ -155,6 +173,7 @@ private:
rpl::lifetime lifetime;
};
void refreshStatus() override;
void setSounding(bool sounding);
void setSpeaking(bool speaking);
void setState(State state);
void setSsrc(uint32 ssrc);
@@ -171,6 +190,7 @@ private:
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
uint32 _ssrc = 0;
bool _sounding = false;
bool _speaking = false;
bool _skipLevelUpdate = false;
@@ -217,10 +237,15 @@ private:
[[nodiscard]] std::unique_ptr<Row> createSelfRow();
[[nodiscard]] std::unique_ptr<Row> createRow(
const Data::GroupCall::Participant &participant);
[[nodiscard]] std::unique_ptr<Row> createInvitedRow(
not_null<UserData*> user);
void prepareRows(not_null<Data::GroupCall*> real);
//void repaintByTimer();
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row);
void setupListChangeViewers(not_null<GroupCall*> call);
void subscribeToChanges(not_null<Data::GroupCall*> real);
void updateRow(
@@ -235,13 +260,15 @@ private:
Row *findRow(not_null<UserData*> user) const;
[[nodiscard]] Data::GroupCall *resolvedRealCall() const;
void appendInvitedUsers();
const base::weak_ptr<GroupCall> _call;
const not_null<ChannelData*> _channel;
not_null<PeerData*> _peer;
// Use only resolvedRealCall() method, not this value directly.
Data::GroupCall *_realCallRawValue = nullptr;
uint64 _realId = 0;
bool _prepared = false;
rpl::event_stream<MuteRequest> _toggleMuteRequests;
rpl::event_stream<not_null<UserData*>> _kickMemberRequests;
@@ -251,10 +278,10 @@ private:
base::unique_qptr<Ui::PopupMenu> _menu;
base::flat_set<not_null<PeerData*>> _menuCheckRowsAfterHidden;
base::flat_map<uint32, not_null<Row*>> _speakingRowBySsrc;
Ui::Animations::Basic _speakingAnimation;
base::flat_map<uint32, not_null<Row*>> _soundingRowBySsrc;
Ui::Animations::Basic _soundingAnimation;
crl::time _speakingAnimationHideLastTime = 0;
crl::time _soundingAnimationHideLastTime = 0;
bool _skipRowLevelUpdate = false;
Ui::CrossLineAnimation _inactiveCrossLine;
@@ -277,22 +304,21 @@ void Row::setSkipLevelUpdate(bool value) {
void Row::updateState(const Data::GroupCall::Participant *participant) {
setSsrc(participant ? participant->ssrc : 0);
if (!participant) {
if (peer()->isSelf()) {
setCustomStatus(tr::lng_group_call_connecting(tr::now));
} else {
setCustomStatus(QString());
}
setState(State::Inactive);
setState(State::Invited);
setSounding(false);
setSpeaking(false);
} else if (!participant->muted
|| (participant->speaking && participant->ssrc != 0)) {
|| (participant->sounding && participant->ssrc != 0)) {
setState(State::Active);
setSounding(participant->sounding && participant->ssrc != 0);
setSpeaking(participant->speaking && participant->ssrc != 0);
} else if (participant->canSelfUnmute) {
setState(State::Inactive);
setSounding(false);
setSpeaking(false);
} else {
setState(State::Muted);
setSounding(false);
setSpeaking(false);
}
}
@@ -307,7 +333,14 @@ void Row::setSpeaking(bool speaking) {
_speaking ? 0. : 1.,
_speaking ? 1. : 0.,
st::widgetFadeDuration);
if (!_speaking) {
}
void Row::setSounding(bool sounding) {
if (_sounding == sounding) {
return;
}
_sounding = sounding;
if (!_sounding) {
_blobsAnimation = nullptr;
} else if (!_blobsAnimation) {
_blobsAnimation = std::make_unique<BlobsAnimation>(
@@ -357,7 +390,7 @@ void Row::updateLevel(float level) {
}
if (level >= GroupCall::kSpeakLevelThreshold) {
_blobsAnimation->lastSpeakingUpdateTime = crl::now();
_blobsAnimation->lastSoundingUpdateTime = crl::now();
}
_blobsAnimation->blobs.setLevel(level);
}
@@ -365,14 +398,14 @@ void Row::updateLevel(float level) {
void Row::updateBlobAnimation(crl::time now) {
Expects(_blobsAnimation != nullptr);
const auto speakingFinishesAt = _blobsAnimation->lastSpeakingUpdateTime
+ Data::GroupCall::kSpeakStatusKeptFor;
const auto speakingStartsFinishing = speakingFinishesAt
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
+ Data::GroupCall::kSoundStatusKeptFor;
const auto soundingStartsFinishing = soundingFinishesAt
- kBlobsEnterDuration;
const auto speakingFinishes = (speakingStartsFinishing < now);
if (speakingFinishes) {
const auto soundingFinishes = (soundingStartsFinishing < now);
if (soundingFinishes) {
_blobsAnimation->enter = std::clamp(
(speakingFinishesAt - now) / float64(kBlobsEnterDuration),
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
0.,
1.);
} else if (_blobsAnimation->enter < 1.) {
@@ -417,8 +450,13 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
if (_blobsAnimation) {
const auto shift = QPointF(x + size / 2., y + size / 2.);
auto hq = PainterHighQualityEnabler(p);
p.translate(shift);
_blobsAnimation->blobs.paint(p, st::groupCallMemberActiveStatus);
const auto brush = anim::brush(
st::groupCallMemberInactiveStatus,
st::groupCallMemberActiveStatus,
_speakingAnimation.value(_speaking ? 1. : 0.));
_blobsAnimation->blobs.paint(p, brush);
p.translate(-shift);
p.setOpacity(1.);
@@ -454,6 +492,36 @@ auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback {
};
}
void Row::paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) {
if (_state != State::Invited) {
PeerListRow::paintStatusText(
p,
st,
x,
y,
availableWidth,
outerWidth,
selected);
return;
}
p.setFont(st::normalFont);
p.setPen(st::groupCallMemberNotJoinedStatus);
p.drawTextLeft(
x,
y,
outerWidth,
(peer()->isSelf()
? tr::lng_status_connecting(tr::now)
: tr::lng_group_call_invited_status(tr::now)));
}
void Row::paintAction(
Painter &p,
int x,
@@ -462,6 +530,20 @@ void Row::paintAction(
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,
@@ -472,12 +554,6 @@ void Row::paintAction(
_actionRipple.reset();
}
}
const auto iconRect = style::rtlrect(
x,
y,
size.width(),
size.height(),
outerWidth);
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
const auto active = _activeAnimation.value(
(_state == State::Active) ? 1. : 0.);
@@ -517,7 +593,7 @@ MembersController::MembersController(
not_null<GroupCall*> call,
not_null<QWidget*> menuParent)
: _call(call)
, _channel(call->channel())
, _peer(call->peer())
, _menuParent(menuParent)
, _inactiveCrossLine(st::groupCallMemberInactiveCrossLine)
, _coloredCrossLine(st::groupCallMemberColoredCrossLine) {
@@ -535,28 +611,28 @@ MembersController::MembersController(
) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
const auto hide = !(!animDisabled && !deactivated);
if (!(hide && _speakingAnimationHideLastTime)) {
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
if (!(hide && _soundingAnimationHideLastTime)) {
_soundingAnimationHideLastTime = hide ? crl::now() : 0;
}
for (const auto [_, row] : _speakingRowBySsrc) {
for (const auto [_, row] : _soundingRowBySsrc) {
if (hide) {
updateRowLevel(row, 0.);
}
row->setSkipLevelUpdate(hide);
}
if (!hide && !_speakingAnimation.animating()) {
_speakingAnimation.start();
if (!hide && !_soundingAnimation.animating()) {
_soundingAnimation.start();
}
_skipRowLevelUpdate = hide;
}, _lifetime);
_speakingAnimation.init([=](crl::time now) {
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
_soundingAnimation.init([=](crl::time now) {
if (const auto &last = _soundingAnimationHideLastTime; (last > 0)
&& (now - last >= kBlobsEnterDuration)) {
_speakingAnimation.stop();
_soundingAnimation.stop();
return false;
}
for (const auto [ssrc, row] : _speakingRowBySsrc) {
for (const auto [ssrc, row] : _soundingRowBySsrc) {
row->updateBlobAnimation(now);
delegate()->peerListUpdateRow(row);
}
@@ -565,19 +641,16 @@ MembersController::MembersController(
}
MembersController::~MembersController() {
if (_menu) {
_menu->setDestroyedCallback(nullptr);
_menu = nullptr;
}
base::take(_menu);
}
void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
const auto channel = call->channel();
channel->session().changes().peerFlagsValue(
channel,
const auto peer = call->peer();
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::GroupCall
) | rpl::map([=] {
return channel->call();
return peer->groupCall();
}) | rpl::filter([=](Data::GroupCall *real) {
const auto call = _call.get();
return call && real && (real->id() == call->id());
@@ -590,7 +663,7 @@ void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
call->stateValue(
) | rpl::start_with_next([=] {
const auto call = _call.get();
const auto real = channel->call();
const auto real = peer->groupCall();
if (call && real && (real->id() == call->id())) {
//updateRow(channel->session().user());
}
@@ -598,8 +671,8 @@ void MembersController::setupListChangeViewers(not_null<GroupCall*> call) {
call->levelUpdates(
) | rpl::start_with_next([=](const LevelUpdate &update) {
const auto i = _speakingRowBySsrc.find(update.ssrc);
if (i != end(_speakingRowBySsrc)) {
const auto i = _soundingRowBySsrc.find(update.ssrc);
if (i != end(_soundingRowBySsrc)) {
updateRowLevel(i->second, update.value);
}
}, _lifetime);
@@ -627,6 +700,7 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
const auto user = update.was ? update.was->user : update.now->user;
if (!update.now) {
if (const auto row = findRow(user)) {
const auto owner = &user->owner();
if (user->isSelf()) {
updateRow(row, nullptr);
} else {
@@ -638,6 +712,30 @@ void MembersController::subscribeToChanges(not_null<Data::GroupCall*> real) {
updateRow(update.was, *update.now);
}
}, _lifetime);
if (_prepared) {
appendInvitedUsers();
}
}
void MembersController::appendInvitedUsers() {
for (const auto user : _peer->owner().invitedToCallUsers(_realId)) {
if (auto row = createInvitedRow(user)) {
delegate()->peerListAppendRow(std::move(row));
}
}
delegate()->peerListRefreshRows();
using Invite = Data::Session::InviteToCall;
_peer->owner().invitesToCalls(
) | rpl::filter([=](const Invite &invite) {
return (invite.id == _realId);
}) | rpl::start_with_next([=](const Invite &invite) {
if (auto row = createInvitedRow(invite.user)) {
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
}
}, _lifetime);
}
void MembersController::updateRow(
@@ -652,7 +750,21 @@ void MembersController::updateRow(
if (row->speaking()) {
delegate()->peerListPrependRow(std::move(row));
} else {
static constexpr auto kInvited = Row::State::Invited;
const auto reorder = [&] {
const auto count = delegate()->peerListFullRowsCount();
if (!count) {
return false;
}
const auto row = delegate()->peerListRowAt(count - 1).get();
return (static_cast<Row*>(row)->state() == kInvited);
}();
delegate()->peerListAppendRow(std::move(row));
if (reorder) {
delegate()->peerListPartitionRows([](const PeerListRow &row) {
return static_cast<const Row&>(row).state() != kInvited;
});
}
}
delegate()->peerListRefreshRows();
}
@@ -697,41 +809,41 @@ void MembersController::checkSpeakingRowPosition(not_null<Row*> row) {
void MembersController::updateRow(
not_null<Row*> row,
const Data::GroupCall::Participant *participant) {
const auto wasSpeaking = row->speaking();
const auto wasSounding = row->sounding();
const auto wasSsrc = row->ssrc();
row->setSkipLevelUpdate(_skipRowLevelUpdate);
row->updateState(participant);
const auto nowSpeaking = row->speaking();
const auto nowSounding = row->sounding();
const auto nowSsrc = row->ssrc();
const auto wasNoSpeaking = _speakingRowBySsrc.empty();
const auto wasNoSounding = _soundingRowBySsrc.empty();
if (wasSsrc == nowSsrc) {
if (nowSpeaking != wasSpeaking) {
if (nowSpeaking) {
_speakingRowBySsrc.emplace(nowSsrc, row);
if (nowSounding != wasSounding) {
if (nowSounding) {
_soundingRowBySsrc.emplace(nowSsrc, row);
} else {
_speakingRowBySsrc.remove(nowSsrc);
_soundingRowBySsrc.remove(nowSsrc);
}
}
} else {
_speakingRowBySsrc.remove(wasSsrc);
if (nowSpeaking) {
_soundingRowBySsrc.remove(wasSsrc);
if (nowSounding) {
Assert(nowSsrc != 0);
_speakingRowBySsrc.emplace(nowSsrc, row);
_soundingRowBySsrc.emplace(nowSsrc, row);
}
}
const auto nowNoSpeaking = _speakingRowBySsrc.empty();
if (wasNoSpeaking && !nowNoSpeaking) {
_speakingAnimation.start();
} else if (nowNoSpeaking && !wasNoSpeaking) {
_speakingAnimation.stop();
const auto nowNoSounding = _soundingRowBySsrc.empty();
if (wasNoSounding && !nowNoSounding) {
_soundingAnimation.start();
} else if (nowNoSounding && !wasNoSounding) {
_soundingAnimation.stop();
}
delegate()->peerListUpdateRow(row);
}
void MembersController::removeRow(not_null<Row*> row) {
_speakingRowBySsrc.remove(row->ssrc());
_soundingRowBySsrc.remove(row->ssrc());
delegate()->peerListRemoveRow(row);
}
@@ -750,14 +862,14 @@ Row *MembersController::findRow(not_null<UserData*> user) const {
Data::GroupCall *MembersController::resolvedRealCall() const {
return (_realCallRawValue
&& (_channel->call() == _realCallRawValue)
&& (_peer->groupCall() == _realCallRawValue)
&& (_realCallRawValue->id() == _realId))
? _realCallRawValue
: nullptr;
}
Main::Session &MembersController::session() const {
return _call->channel()->session();
return _call->peer()->session();
}
void MembersController::prepare() {
@@ -767,14 +879,19 @@ void MembersController::prepare() {
setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
const auto call = _call.get();
if (const auto real = _channel->call();
if (const auto real = _peer->groupCall();
real && call && real->id() == call->id()) {
prepareRows(real);
} else if (auto row = createSelfRow()) {
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
}
loadMoreRows();
if (_realId) {
appendInvitedUsers();
}
_prepared = true;
}
void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
@@ -803,10 +920,10 @@ void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
}
}
if (!foundSelf) {
const auto self = _channel->session().user();
const auto self = _peer->session().user();
const auto i = ranges::find(
participants,
_channel->session().user(),
_peer->session().user(),
&Data::GroupCall::Participant::user);
auto row = (i != end(participants)) ? createRow(*i) : createSelfRow();
if (row) {
@@ -826,7 +943,7 @@ void MembersController::prepareRows(not_null<Data::GroupCall*> real) {
}
void MembersController::loadMoreRows() {
if (const auto real = _channel->call()) {
if (const auto real = _peer->groupCall()) {
real->requestParticipants();
}
}
@@ -837,7 +954,7 @@ auto MembersController::toggleMuteRequests() const
}
bool MembersController::rowCanMuteMembers() {
return _channel->canManageCall();
return _peer->canManageGroupCall();
}
void MembersController::rowUpdateRow(not_null<Row*> row) {
@@ -899,29 +1016,20 @@ auto MembersController::kickMemberRequests() const
}
void MembersController::rowClicked(not_null<PeerListRow*> row) {
if (_menu) {
_menu->setDestroyedCallback(nullptr);
_menu->deleteLater();
_menu = nullptr;
}
_menu = rowContextMenu(_menuParent, row);
if (const auto raw = _menu.get()) {
raw->setDestroyedCallback([=] {
if (_menu && _menu.get() != raw) {
return;
}
auto saved = base::take(_menu);
for (const auto peer : base::take(_menuCheckRowsAfterHidden)) {
if (const auto row = findRow(peer->asUser())) {
if (row->speaking()) {
checkSpeakingRowPosition(row);
}
delegate()->peerListShowRowMenu(row, [=](not_null<Ui::PopupMenu*> menu) {
if (!_menu || _menu.get() != menu) {
return;
}
auto saved = base::take(_menu);
for (const auto peer : base::take(_menuCheckRowsAfterHidden)) {
if (const auto row = findRow(peer->asUser())) {
if (row->speaking()) {
checkSpeakingRowPosition(row);
}
}
_menu = std::move(saved);
});
raw->popup(QCursor::pos());
}
}
_menu = std::move(saved);
});
}
void MembersController::rowActionClicked(
@@ -932,6 +1040,23 @@ void MembersController::rowActionClicked(
base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = createRowContextMenu(parent, row);
if (result) {
// First clear _menu value, so that we don't check row positions yet.
base::take(_menu);
// Here unique_qptr is used like a shared pointer, where
// not the last destroyed pointer destroys the object, but the first.
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
}
return result;
}
base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
Expects(row->peer()->isUser());
if (row->peer()->isSelf()) {
@@ -943,7 +1068,29 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
parent,
st::groupCallPopupMenu);
const auto mute = (real->state() != Row::State::Muted);
const auto muteState = real->state();
const auto admin = [&] {
if (const auto chat = _peer->asChat()) {
return chat->admins.contains(user)
|| (chat->creator == user->bareId());
} else if (const auto group = _peer->asMegagroup()) {
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;
}();
const auto mute = admin
? (muteState == Row::State::Active)
: (muteState != Row::State::Muted);
const auto toggleMute = crl::guard(this, [=] {
_toggleMuteRequests.fire(MuteRequest{
.user = user,
@@ -991,14 +1138,18 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
};
const auto showHistory = [=] {
performOnMainWindow([=](not_null<Window::SessionController*> window) {
window->showPeerHistory(user);
window->showPeerHistory(
user,
Window::SectionShow::Way::Forward);
});
};
const auto removeFromGroup = crl::guard(this, [=] {
_kickMemberRequests.fire_copy(user);
});
if (_channel->canManageCall()) {
if ((muteState != Row::State::Invited)
&& _peer->canManageGroupCall()
&& (!admin || mute)) {
result->addAction(
(mute
? tr::lng_group_call_context_mute(tr::now)
@@ -1011,7 +1162,18 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
result->addAction(
tr::lng_context_send_message(tr::now),
showHistory);
if (_channel->canRestrictUser(user)) {
const auto canKick = [&] {
if (static_cast<Row*>(row.get())->state() == Row::State::Invited) {
return false;
} else if (const auto chat = _peer->asChat()) {
return chat->amCreator()
|| (chat->canBanMembers() && !chat->admins.contains(user));
} else if (const auto group = _peer->asMegagroup()) {
return group->canRestrictUser(user);
}
return false;
}();
if (canKick) {
result->addAction(
tr::lng_context_remove_from_group(tr::now),
removeFromGroup);
@@ -1020,7 +1182,7 @@ base::unique_qptr<Ui::PopupMenu> MembersController::rowContextMenu(
}
std::unique_ptr<Row> MembersController::createSelfRow() {
const auto self = _channel->session().user();
const auto self = _peer->session().user();
auto result = std::make_unique<Row>(this, self);
updateRow(result.get(), nullptr);
return result;
@@ -1033,6 +1195,16 @@ std::unique_ptr<Row> MembersController::createRow(
return result;
}
std::unique_ptr<Row> MembersController::createInvitedRow(
not_null<UserData*> user) {
if (findRow(user)) {
return nullptr;
}
auto result = std::make_unique<Row>(this, user);
updateRow(result.get(), nullptr);
return result;
}
} // namespace
GroupMembers::GroupMembers(
@@ -1042,20 +1214,11 @@ GroupMembers::GroupMembers(
, _call(call)
, _scroll(this, st::defaultSolidScroll)
, _listController(std::make_unique<MembersController>(call, parent)) {
setupHeader(call);
setupAddMember(call);
setupList();
setContent(_list);
setupFakeRoundCorners();
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
paintRequest(
) | rpl::start_with_next([=](QRect clip) {
const auto headerPart = clip.intersected(
QRect(0, 0, width(), _header->height()));
if (!headerPart.isEmpty()) {
QPainter(this).fillRect(headerPart, st::groupCallMembersBg);
}
}, lifetime());
}
auto GroupMembers::toggleMuteRequests() const
@@ -1071,10 +1234,10 @@ auto GroupMembers::kickMemberRequests() const
}
int GroupMembers::desiredHeight() const {
auto desired = _header ? _header->height() : 0;
const auto top = _addMember ? _addMember->height() : 0;
auto count = [&] {
if (const auto call = _call.get()) {
if (const auto real = call->channel()->call()) {
if (const auto real = call->peer()->groupCall()) {
if (call->id() == real->id()) {
return real->fullCount();
}
@@ -1083,7 +1246,7 @@ int GroupMembers::desiredHeight() const {
return 0;
}();
const auto use = std::max(count, _list->fullRowsCount());
return (_header ? _header->height() : 0)
return top
+ (use * st::groupCallMembersList.item.height)
+ (use ? st::lineWidth : 0);
}
@@ -1093,111 +1256,107 @@ rpl::producer<int> GroupMembers::desiredHeightValue() const {
_listController.get());
return rpl::combine(
heightValue(),
_addMemberButton.value(),
controller->fullCountValue()
) | rpl::map([=] {
return desiredHeight();
});
}
void GroupMembers::setupHeader(not_null<GroupCall*> call) {
_header = object_ptr<Ui::FixedHeightWidget>(
this,
st::groupCallMembersHeader);
auto parent = _header.data();
_titleWrap = Ui::CreateChild<Ui::RpWidget>(parent);
_title = setupTitle(call);
_addMember = Ui::CreateChild<Ui::IconButton>(
parent,
st::groupCallAddMember);
setupButtons(call);
widthValue(
) | rpl::start_with_next([this](int width) {
_header->resizeToWidth(width);
}, _header->lifetime());
}
object_ptr<Ui::FlatLabel> GroupMembers::setupTitle(
not_null<GroupCall*> call) {
const auto controller = static_cast<MembersController*>(
_listController.get());
auto result = object_ptr<Ui::FlatLabel>(
_titleWrap,
tr::lng_chat_status_members(
lt_count_decimal,
controller->fullCountValue() | tr::to_count(),
Ui::Text::Upper
),
st::groupCallHeaderLabel);
result->setAttribute(Qt::WA_TransparentForMouseEvents);
return result;
}
void GroupMembers::setupButtons(not_null<GroupCall*> call) {
void GroupMembers::setupAddMember(not_null<GroupCall*> call) {
using namespace rpl::mappers;
_addMember->showOn(Data::CanWriteValue(
call->channel().get()
));
_addMember->addClickHandler([=] { // TODO throttle(ripple duration)
_addMemberRequests.fire({});
});
_canAddMembers = Data::CanWriteValue(call->peer().get());
SubscribeToMigration(
call->peer(),
lifetime(),
[=](not_null<ChannelData*> channel) {
_canAddMembers = Data::CanWriteValue(channel.get());
});
_canAddMembers.value(
) | rpl::start_with_next([=](bool can) {
if (!can) {
_addMemberButton = nullptr;
_addMember.destroy();
updateControlsGeometry();
return;
}
_addMember = Settings::CreateButton(
this,
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();
}, lifetime());
}
void GroupMembers::setupList() {
auto topSkip = _header ? _header->height() : 0;
rpl::producer<int> GroupMembers::fullCountValue() const {
return static_cast<MembersController*>(
_listController.get())->fullCountValue();
}
//tr::lng_chat_status_members(
// lt_count_decimal,
// controller->fullCountValue() | tr::to_count(),
// Ui::Text::Upper
//),
void GroupMembers::setupList() {
_listController->setStyleOverrides(&st::groupCallMembersList);
_list = _scroll->setOwnedWidget(object_ptr<ListWidget>(
this,
_listController.get()));
sizeValue(
) | rpl::start_with_next([=](QSize size) {
_scroll->setGeometry(0, topSkip, size.width(), size.height() - topSkip);
_list->resizeToWidth(size.width());
_list->heightValue(
) | rpl::start_with_next([=] {
resizeToList();
}, _list->lifetime());
_list->heightValue(
) | rpl::start_with_next([=](int listHeight) {
auto newHeight = (listHeight > 0)
? (topSkip + listHeight + st::lineWidth)
: 0;
resize(width(), newHeight);
}, _list->lifetime());
_list->moveToLeft(0, topSkip);
_list->show();
updateControlsGeometry();
}
void GroupMembers::resizeEvent(QResizeEvent *e) {
if (_header) {
updateHeaderControlsGeometry(width());
updateControlsGeometry();
}
void GroupMembers::resizeToList() {
if (!_list) {
return;
}
const auto listHeight = _list->height();
const auto newHeight = (listHeight > 0)
? ((_addMember ? _addMember->height() : 0)
+ listHeight
+ st::lineWidth)
: 0;
if (height() == newHeight) {
updateControlsGeometry();
} else {
resize(width(), newHeight);
}
}
void GroupMembers::updateHeaderControlsGeometry(int newWidth) {
auto availableWidth = newWidth
- st::groupCallAddButtonPosition.x();
_addMember->moveToLeft(
availableWidth - _addMember->width(),
st::groupCallAddButtonPosition.y(),
newWidth);
if (!_addMember->isHidden()) {
availableWidth -= _addMember->width();
void GroupMembers::updateControlsGeometry() {
if (!_list) {
return;
}
_titleWrap->resize(
availableWidth - _addMember->width() - st::groupCallHeaderPosition.x(),
_title->height());
_titleWrap->moveToLeft(
st::groupCallHeaderPosition.x(),
st::groupCallHeaderPosition.y(),
newWidth);
_titleWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
_title->resizeToWidth(_titleWrap->width());
_title->moveToLeft(0, 0);
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());
}
void GroupMembers::setupFakeRoundCorners() {
@@ -1275,6 +1434,9 @@ void GroupMembers::peerListSetTitle(rpl::producer<QString> title) {
void GroupMembers::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
void GroupMembers::peerListSetHideEmpty(bool hide) {
}
bool GroupMembers::peerListIsRowChecked(not_null<PeerListRow*> row) {
return false;
}
@@ -1286,10 +1448,6 @@ int GroupMembers::peerListSelectedRowsCount() {
return 0;
}
std::vector<not_null<PeerData*>> GroupMembers::peerListCollectSelectedRows() {
return {};
}
void GroupMembers::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Calls::GroupMembers.");
}

View File

@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class ScrollArea;
class SettingsButton;
} // namespace Ui
namespace Data {
@@ -36,6 +37,7 @@ public:
[[nodiscard]] int desiredHeight() const;
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
[[nodiscard]] rpl::producer<MuteRequest> toggleMuteRequests() const;
[[nodiscard]] auto kickMemberRequests() const
-> rpl::producer<not_null<UserData*>>;
@@ -55,10 +57,10 @@ private:
// PeerListContentDelegate interface.
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
void peerListSetHideEmpty(bool hide) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
std::vector<not_null<PeerData*>> peerListCollectSelectedRows() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
@@ -67,25 +69,22 @@ private:
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
void setupHeader(not_null<GroupCall*> call);
object_ptr<Ui::FlatLabel> setupTitle(not_null<GroupCall*> call);
void setupAddMember(not_null<GroupCall*> call);
void resizeToList();
void setupList();
void setupFakeRoundCorners();
void setupButtons(not_null<GroupCall*> call);
void updateHeaderControlsGeometry(int newWidth);
void updateControlsGeometry();
const base::weak_ptr<GroupCall> _call;
object_ptr<Ui::ScrollArea> _scroll;
std::unique_ptr<PeerListController> _listController;
object_ptr<Ui::RpWidget> _header = { nullptr };
object_ptr<Ui::SettingsButton> _addMember = { nullptr };
rpl::variable<Ui::SettingsButton*> _addMemberButton = nullptr;
ListWidget *_list = { nullptr };
rpl::event_stream<> _addMemberRequests;
Ui::RpWidget *_titleWrap = nullptr;
Ui::FlatLabel *_title = nullptr;
Ui::IconButton *_addMember = nullptr;
rpl::variable<bool> _canAddMembers;
};

View File

@@ -22,12 +22,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#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 "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 "app.h"
#include "apiwrap.h" // api().kickParticipant.
#include "styles/style_calls.h"
@@ -44,12 +48,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Calls {
namespace {
constexpr auto kSpacePushToTalkDelay = crl::time(250);
class InviteController final : public ParticipantsBoxController {
public:
InviteController(
not_null<ChannelData*> channel,
base::flat_set<not_null<UserData*>> alreadyIn,
int fullInCount);
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn);
void prepare() override;
@@ -60,45 +65,100 @@ public:
void itemDeselectedHook(not_null<PeerData*> peer) override;
std::variant<int, not_null<UserData*>> inviteSelectedUsers(
not_null<PeerListBox*> box,
not_null<GroupCall*> call) const;
[[nodiscard]] auto peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*>;
[[nodiscard]] rpl::producer<not_null<UserData*>> rowAdded() const;
[[nodiscard]] bool hasRowFor(not_null<PeerData*> peer) const;
private:
void updateTitle() const;
[[nodiscard]] int alreadyInCount() const;
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
[[nodiscard]] int fullCount() const;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) const override;
const not_null<ChannelData*> _channel;
not_null<PeerData*> _peer;
const base::flat_set<not_null<UserData*>> _alreadyIn;
const int _fullInCount = 0;
mutable base::flat_set<not_null<UserData*>> _skippedUsers;
mutable base::flat_set<not_null<UserData*>> _inGroup;
rpl::event_stream<not_null<UserData*>> _rowAdded;
};
class InviteContactsController final : public AddParticipantsBoxController {
public:
InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> discoveredInGroup);
private:
void prepareViewHook() override;
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
rpl::producer<not_null<UserData*>> _discoveredInGroup;
rpl::lifetime _lifetime;
};
[[nodiscard]] object_ptr<Ui::RpWidget> CreateSectionSubtitle(
QWidget *parent,
rpl::producer<QString> text) {
auto result = object_ptr<Ui::FixedHeightWidget>(
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<Ui::FlatLabel>(
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<ChannelData*> channel,
base::flat_set<not_null<UserData*>> alreadyIn,
int fullInCount)
: ParticipantsBoxController(CreateTag{}, nullptr, channel, Role::Members)
, _channel(channel)
, _alreadyIn(std::move(alreadyIn))
, _fullInCount(std::max(fullInCount, int(_alreadyIn.size()))) {
_skippedUsers.emplace(channel->session().user());
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn)
: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members)
, _peer(peer)
, _alreadyIn(std::move(alreadyIn)) {
SubscribeToMigration(
_peer,
lifetime(),
[=](not_null<ChannelData*> channel) { _peer = channel; });
}
void InviteController::prepare() {
delegate()->peerListSetHideEmpty(true);
ParticipantsBoxController::prepare();
updateTitle();
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<PeerListRow*> row) {
delegate()->peerListSetRowChecked(row, !row->checked());
updateTitle();
}
base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
@@ -108,63 +168,73 @@ base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
}
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
updateTitle();
}
int InviteController::alreadyInCount() const {
return std::max(_fullInCount, int(_alreadyIn.size()));
bool InviteController::hasRowFor(not_null<PeerData*> peer) const {
return (delegate()->peerListFindRow(peer->id) != nullptr);
}
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
return _alreadyIn.contains(user);
}
int InviteController::fullCount() const {
return alreadyInCount() + delegate()->peerListSelectedRowsCount();
}
std::unique_ptr<PeerListRow> InviteController::createRow(
not_null<UserData*> user) const {
if (user->isSelf() || user->isBot()) {
if (_skippedUsers.emplace(user).second) {
updateTitle();
}
return nullptr;
}
auto result = std::make_unique<PeerListRow>(user);
_rowAdded.fire_copy(user);
_inGroup.emplace(user);
if (isAlreadyIn(user)) {
result->setDisabledState(PeerListRow::State::DisabledChecked);
}
return result;
}
void InviteController::updateTitle() const {
const auto inOrInvited = fullCount() - 1; // minus self
const auto canBeInvited = std::max({
delegate()->peerListFullRowsCount(), // minus self and bots
_channel->membersCount() - int(_skippedUsers.size()), // self + bots
inOrInvited
});
const auto additional = canBeInvited
? qsl("%1 / %2").arg(inOrInvited).arg(canBeInvited)
: QString();
delegate()->peerListSetTitle(tr::lng_group_call_invite_title());
delegate()->peerListSetAdditionalTitle(rpl::single(additional));
auto InviteController::peersWithRows() const
-> not_null<const base::flat_set<not_null<UserData*>>*> {
return &_inGroup;
}
std::variant<int, not_null<UserData*>> InviteController::inviteSelectedUsers(
not_null<PeerListBox*> box,
not_null<GroupCall*> call) const {
const auto rows = box->peerListCollectSelectedRows();
const auto users = ranges::view::all(
rows
) | ranges::view::transform([](not_null<PeerData*> peer) {
Expects(peer->isUser());
Expects(!peer->isSelf());
rpl::producer<not_null<UserData*>> InviteController::rowAdded() const {
return _rowAdded.events();
}
return not_null<UserData*>(peer->asUser());
}) | ranges::to_vector;
return call->inviteUsers(users);
InviteContactsController::InviteContactsController(
not_null<PeerData*> peer,
base::flat_set<not_null<UserData*>> alreadyIn,
not_null<const base::flat_set<not_null<UserData*>>*> inGroup,
rpl::producer<not_null<UserData*>> 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<UserData*> user) {
if (auto row = delegate()->peerListFindRow(user->id)) {
delegate()->peerListRemoveRow(row);
}
}, _lifetime);
}
std::unique_ptr<PeerListRow> InviteContactsController::createRow(
not_null<UserData*> user) {
return _inGroup->contains(user)
? nullptr
: AddParticipantsBoxController::createRow(user);
}
} // namespace
@@ -180,7 +250,7 @@ void LeaveGroupCallBox(
box.get(),
tr::lng_group_call_leave_sure(),
(inCall ? st::groupCallBoxLabel : st::boxLabel)));
const auto discard = call->channel()->canManageCall()
const auto discard = call->peer()->canManageGroupCall()
? box->addRow(object_ptr<Ui::Checkbox>(
box.get(),
tr::lng_group_call_end(),
@@ -209,9 +279,24 @@ void LeaveGroupCallBox(
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void GroupCallConfirmBox(
not_null<Ui::GenericBox*> box,
const QString &text,
rpl::producer<QString> button,
Fn<void()> callback) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
text,
st::groupCallBoxLabel),
st::boxPadding);
box->addButton(std::move(button), callback);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
GroupPanel::GroupPanel(not_null<GroupCall*> call)
: _call(call)
, _channel(call->channel())
, _peer(call->peer())
, _window(std::make_unique<Ui::Window>(Core::App().getModalParent()))
, _layerBg(std::make_unique<Ui::LayerManager>(_window->body()))
#ifdef Q_OS_WIN
@@ -231,6 +316,12 @@ GroupPanel::GroupPanel(not_null<GroupCall*> call)
, _hangup(widget(), st::groupCallHangup) {
_layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
_settings->setColorOverrides(_mute->colorOverrides());
_layerBg->setHideByBackgroundClick(true);
SubscribeToMigration(
_peer,
_window->lifetime(),
[=](not_null<ChannelData*> channel) { migrate(channel); });
initWindow();
initWidget();
@@ -247,33 +338,65 @@ bool GroupPanel::isActive() const {
&& !(_window->windowState() & Qt::WindowMinimized);
}
void GroupPanel::minimize() {
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
}
void GroupPanel::close() {
_window->close();
}
void GroupPanel::showAndActivate() {
if (_window->isHidden()) {
_window->show();
}
const auto state = _window->windowState();
if (state & Qt::WindowMinimized) {
_window->setWindowState(state & ~Qt::WindowMinimized);
}
_window->raise();
_window->setWindowState(_window->windowState() | Qt::WindowActive);
_window->activateWindow();
_window->setFocus();
}
void GroupPanel::migrate(not_null<ChannelData*> channel) {
_peer = channel;
_peerLifetime.destroy();
subscribeToPeerChanges();
_title.destroy();
refreshTitle();
}
void GroupPanel::subscribeToPeerChanges() {
Info::Profile::NameValue(
_peer
) | rpl::start_with_next([=](const TextWithEntities &name) {
_window->setTitle(name.text);
}, _peerLifetime);
}
void GroupPanel::initWindow() {
_window->setAttribute(Qt::WA_OpaquePaintEvent);
_window->setAttribute(Qt::WA_NoSystemBackground);
_window->setWindowIcon(
QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly)));
_window->setTitleStyle(st::callTitle);
_window->setTitleStyle(st::groupCallTitle);
Info::Profile::NameValue(
_channel
) | rpl::start_with_next([=](const TextWithEntities &name) {
_window->setTitle(name.text);
}, _window->lifetime());
subscribeToPeerChanges();
base::install_event_filter(_window.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Close && handleClose()) {
e->ignore();
return base::EventFilterResult::Cancel;
} else if (e->type() == QEvent::KeyPress
|| e->type() == QEvent::KeyRelease) {
if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Space) {
if (_call) {
_call->pushToTalk(
e->type() == QEvent::KeyPress,
kSpacePushToTalkDelay);
}
}
}
return base::EventFilterResult::Continue;
});
@@ -309,30 +432,35 @@ void GroupPanel::initWidget() {
}, widget()->lifetime());
}
void GroupPanel::hangup(bool discardCallChecked) {
void GroupPanel::endCall() {
if (!_call) {
return;
} else if (!_call->peer()->canManageGroupCall()) {
_call->hangup();
return;
}
_layerBg->showBox(Box(
LeaveGroupCallBox,
_call,
discardCallChecked,
false,
BoxContext::GroupCallPanel));
}
void GroupPanel::initControls() {
_mute->clicks(
) | rpl::filter([=](Qt::MouseButton button) {
return (button == Qt::LeftButton)
&& _call
&& (_call->muted() != MuteState::ForceMuted);
return (button == Qt::LeftButton) && (_call != nullptr);
}) | rpl::start_with_next([=] {
_call->setMuted((_call->muted() == MuteState::Muted)
? MuteState::Active
: MuteState::Muted);
if (_call->muted() == MuteState::ForceMuted) {
_mute->shake();
} else {
_call->setMuted((_call->muted() == MuteState::Muted)
? MuteState::Active
: MuteState::Muted);
}
}, _mute->lifetime());
_hangup->setClickedCallback([=] { hangup(false); });
_hangup->setClickedCallback([=] { endCall(); });
_settings->setClickedCallback([=] {
if (_call) {
_layerBg->showBox(Box(GroupCallSettingsBox, _call));
@@ -340,7 +468,7 @@ void GroupPanel::initControls() {
});
_settings->setText(tr::lng_menu_settings());
_hangup->setText(tr::lng_box_leave());
_hangup->setText(tr::lng_group_call_leave());
_members->desiredHeightValue(
) | rpl::start_with_next([=] {
@@ -357,7 +485,7 @@ void GroupPanel::initWithCall(GroupCall *call) {
return;
}
_channel = _call->channel();
_peer = _call->peer();
call->stateValue(
) | rpl::filter([](State state) {
@@ -395,14 +523,9 @@ void GroupPanel::initWithCall(GroupCall *call) {
}
}, _callLifetime);
using namespace rpl::mappers;
rpl::combine(
_call->mutedValue() | MapPushToTalkToActive(),
_call->stateValue() | rpl::map(
_1 == State::Creating
|| _1 == State::Joining
|| _1 == State::Connecting
)
_call->connectingValue()
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](MuteState mute, bool connecting) {
_mute->setState(Ui::CallMuteButtonState{
@@ -413,6 +536,13 @@ void GroupPanel::initWithCall(GroupCall *call) {
: mute == MuteState::Muted
? tr::lng_group_call_unmute(tr::now)
: tr::lng_group_call_you_are_live(tr::now)),
.subtext = (connecting
? QString()
: mute == MuteState::ForceMuted
? tr::lng_group_call_force_muted_sub(tr::now)
: mute == MuteState::Muted
? tr::lng_group_call_unmute_sub(tr::now)
: QString()),
.type = (connecting
? Ui::CallMuteButtonType::Connecting
: mute == MuteState::ForceMuted
@@ -425,63 +555,142 @@ void GroupPanel::initWithCall(GroupCall *call) {
}
void GroupPanel::addMembers() {
const auto real = _channel->call();
const auto real = _peer->groupCall();
if (!_call || !real || real->id() != _call->id()) {
return;
}
auto alreadyIn = _channel->owner().invitedToCallUsers(real->id());
auto alreadyIn = _peer->owner().invitedToCallUsers(real->id());
for (const auto &participant : real->participants()) {
alreadyIn.emplace(participant.user);
}
alreadyIn.emplace(_channel->session().user());
alreadyIn.emplace(_peer->session().user());
auto controller = std::make_unique<InviteController>(
_channel,
std::move(alreadyIn),
real->fullCount());
_peer,
alreadyIn);
controller->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
const auto weak = base::make_weak(_call);
auto initBox = [=, controller = controller.get()](
not_null<PeerListBox*> box) {
box->addButton(tr::lng_group_call_invite_button(), [=] {
if (const auto call = weak.get()) {
const auto result = controller->inviteSelectedUsers(box, call);
auto contactsController = std::make_unique<InviteContactsController>(
_peer,
std::move(alreadyIn),
controller->peersWithRows(),
controller->rowAdded());
contactsController->setStyleOverrides(
&st::groupCallInviteMembersList,
&st::groupCallMultiSelect);
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::WithEntities),
.st = &st::defaultToast,
});
} else if (const auto count = std::get_if<int>(&result)) {
if (*count > 0) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
Ui::Text::RichLangValue),
.st = &st::defaultToast,
});
}
} else {
Unexpected("Result in GroupCall::inviteUsers.");
}
const auto weak = base::make_weak(_call);
const auto invite = [=](const std::vector<not_null<UserData*>> &users) {
const auto call = weak.get();
if (!call) {
return;
}
const auto result = call->inviteUsers(users);
if (const auto user = std::get_if<not_null<UserData*>>(&result)) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_user(
tr::now,
lt_user,
Ui::Text::Bold((*user)->firstName),
Ui::Text::WithEntities),
.st = &st::defaultToast,
});
} else if (const auto count = std::get_if<int>(&result)) {
if (*count > 0) {
Ui::Toast::Show(
widget(),
Ui::Toast::Config{
.text = tr::lng_group_call_invite_done_many(
tr::now,
lt_count,
*count,
Ui::Text::RichLangValue),
.st = &st::defaultToast,
});
}
box->closeBox();
} else {
Unexpected("Result in GroupCall::inviteUsers.");
}
};
const auto inviteWithAdd = [=](
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> finish) {
_peer->session().api().addChatParticipants(
_peer,
nonMembers,
[=](bool) { invite(users); finish(); });
};
const auto inviteWithConfirmation = [=](
const std::vector<not_null<UserData*>> &users,
const std::vector<not_null<UserData*>> &nonMembers,
Fn<void()> 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<QPointer<Ui::GenericBox>>();
const auto finishWithConfirm = [=] {
if (*shared) {
(*shared)->closeBox();
}
finish();
};
auto box = Box(
GroupCallConfirmBox,
text,
tr::lng_participant_invite(),
[=] { inviteWithAdd(users, nonMembers, finishWithConfirm); });
*shared = box.data();
_layerBg->showBox(std::move(box));
};
auto initBox = [=, controller = controller.get()](
not_null<PeerListsBox*> 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::view::all(
rows
) | ranges::view::transform([](not_null<PeerData*> peer) {
return not_null<UserData*>(peer->asUser());
}) | ranges::to_vector;
const auto nonMembers = ranges::view::all(
users
) | ranges::view::filter([&](not_null<UserData*> 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(); });
};
_layerBg->showBox(Box<PeerListBox>(std::move(controller), initBox));
auto controllers = std::vector<std::unique_ptr<PeerListController>>();
controllers.push_back(std::move(controller));
controllers.push_back(std::move(contactsController));
_layerBg->showBox(Box<PeerListsBox>(std::move(controllers), initBox));
}
void GroupPanel::kickMember(not_null<UserData*> user) {
@@ -508,17 +717,21 @@ void GroupPanel::kickMember(not_null<UserData*> user) {
}
void GroupPanel::kickMemberSure(not_null<UserData*> user) {
const auto currentRestrictedRights = [&]() -> MTPChatBannedRights {
const auto it = _channel->mgInfo->lastRestricted.find(user);
return (it != _channel->mgInfo->lastRestricted.cend())
? it->second.rights
: MTP_chatBannedRights(MTP_flags(0), MTP_int(0));
}();
if (const auto chat = _peer->asChat()) {
chat->session().api().kickParticipant(chat, user);
} else if (const auto channel = _peer->asChannel()) {
const auto currentRestrictedRights = [&]() -> MTPChatBannedRights {
const auto it = channel->mgInfo->lastRestricted.find(user);
return (it != channel->mgInfo->lastRestricted.cend())
? it->second.rights
: MTP_chatBannedRights(MTP_flags(0), MTP_int(0));
}();
_channel->session().api().kickParticipant(
_channel,
user,
currentRestrictedRights);
channel->session().api().kickParticipant(
channel,
user,
currentRestrictedRights);
}
}
void GroupPanel::initLayout() {
@@ -550,22 +763,22 @@ void GroupPanel::initGeometry() {
}
int GroupPanel::computeMembersListTop() const {
#ifdef Q_OS_WIN
return st::callTitleButton.height + st::groupCallMembersMargin.top() / 2;
#elif defined Q_OS_MAC // Q_OS_WIN
return st::groupCallMembersMargin.top() * 2;
#else // Q_OS_WIN || Q_OS_MAC
return st::groupCallMembersMargin.top();
#endif // Q_OS_WIN || Q_OS_MAC
if (computeTitleRect().has_value()) {
return st::groupCallMembersTop;
}
return st::groupCallMembersTop
- (st::groupCallSubtitleTop - st::groupCallTitleTop);
}
std::optional<QRect> GroupPanel::computeTitleRect() const {
#ifdef Q_OS_WIN
const auto controls = _controls->geometry();
return QRect(0, 0, controls.x(), controls.height());
#else // Q_OS_WIN
#elif defined Q_OS_MAC // Q_OS_WIN
return QRect(70, 0, widget()->width() - 70, 28);
#else // Q_OS_WIN || Q_OS_MAC
return std::nullopt;
#endif // Q_OS_WIN
#endif // Q_OS_WIN || Q_OS_MAC
}
void GroupPanel::updateControlsGeometry() {
@@ -609,13 +822,14 @@ void GroupPanel::refreshTitle() {
if (!_title) {
_title.create(
widget(),
Info::Profile::NameValue(_channel),
st::groupCallHeaderLabel);
Info::Profile::NameValue(_peer),
st::groupCallTitleLabel);
_title->show();
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
const auto best = _title->naturalWidth();
const auto from = (widget()->width() - best) / 2;
const auto top = (computeMembersListTop() - _title->height()) / 2;
const auto top = st::groupCallTitleTop;
const auto left = titleRect->x();
if (from >= left && from + best <= left + titleRect->width()) {
_title->resizeToWidth(best);
@@ -633,6 +847,25 @@ void GroupPanel::refreshTitle() {
} else if (_title) {
_title.destroy();
}
if (!_subtitle) {
_subtitle.create(
widget(),
tr::lng_group_call_members(
lt_count_decimal,
_members->fullCountValue() | tr::to_count()),
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 GroupPanel::paint(QRect clip) {

View File

@@ -68,6 +68,8 @@ public:
~GroupPanel();
[[nodiscard]] bool isActive() const;
void minimize();
void close();
void showAndActivate();
void closeBeforeDestroy();
@@ -90,7 +92,7 @@ private:
void updateControlsGeometry();
void showControls();
void hangup(bool discardCallChecked);
void endCall();
void addMembers();
void kickMember(not_null<UserData*> user);
@@ -99,8 +101,11 @@ private:
[[nodiscard]] std::optional<QRect> computeTitleRect() const;
void refreshTitle();
void migrate(not_null<ChannelData*> channel);
void subscribeToPeerChanges();
GroupCall *_call = nullptr;
not_null<ChannelData*> _channel;
not_null<PeerData*> _peer;
const std::unique_ptr<Ui::Window> _window;
const std::unique_ptr<Ui::LayerManager> _layerBg;
@@ -112,12 +117,15 @@ private:
rpl::lifetime _callLifetime;
object_ptr<Ui::FlatLabel> _title = { nullptr };
object_ptr<Ui::FlatLabel> _subtitle = { nullptr };
object_ptr<GroupMembers> _members;
object_ptr<Ui::CallButton> _settings;
std::unique_ptr<Ui::CallMuteButton> _mute;
object_ptr<Ui::CallButton> _hangup;
rpl::lifetime _peerLifetime;
};
} // namespace Calls

View File

@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/global_shortcuts.h"
#include "base/platform/base_platform_info.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_group_call.h"
#include "core/application.h"
#include "boxes/single_choice_box.h"
@@ -44,19 +45,19 @@ constexpr auto kDelaysCount = 201;
constexpr auto kCheckAccessibilityInterval = crl::time(500);
void SaveCallJoinMuted(
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
uint64 callId,
bool joinMuted) {
const auto call = channel->call();
const auto call = peer->groupCall();
if (!call
|| call->id() != callId
|| !channel->canManageCall()
|| !peer->canManageGroupCall()
|| !call->canChangeJoinMuted()
|| call->joinMuted() == joinMuted) {
return;
}
call->setJoinMutedLocally(joinMuted);
channel->session().api().request(MTPphone_ToggleGroupCallSettings(
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
MTP_flags(MTPphone_ToggleGroupCallSettings::Flag::f_join_muted),
call->input(),
MTP_bool(joinMuted)
@@ -101,8 +102,8 @@ void GroupCallSettingsBox(
};
const auto state = box->lifetime().make_state<State>();
const auto channel = call->channel();
const auto real = channel->call();
const auto peer = call->peer();
const auto real = peer->groupCall();
const auto id = call->id();
const auto goodReal = (real && real->id() == id);
@@ -111,7 +112,7 @@ void GroupCallSettingsBox(
const auto joinMuted = goodReal ? real->joinMuted() : false;
const auto canChangeJoinMuted = (goodReal && real->canChangeJoinMuted());
const auto addCheck = (channel->canManageCall() && canChangeJoinMuted);
const auto addCheck = (peer->canManageGroupCall() && canChangeJoinMuted);
if (addCheck) {
AddSkip(layout);
}
@@ -402,11 +403,24 @@ void GroupCallSettingsBox(
//AddSkip(layout);
const auto lookupLink = [=] {
return channel->hasUsername()
? channel->session().createInternalLinkFull(channel->username)
: channel->inviteLink();
if (const auto group = peer->asMegagroup()) {
return group->hasUsername()
? group->session().createInternalLinkFull(group->username)
: group->inviteLink();
} else if (const auto chat = peer->asChat()) {
return chat->inviteLink();
}
return QString();
};
if (!lookupLink().isEmpty() || channel->canHaveInviteLink()) {
const auto canCreateLink = [&] {
if (const auto chat = peer->asChat()) {
return chat->canHaveInviteLink();
} else if (const auto group = peer->asMegagroup()) {
return group->canHaveInviteLink();
}
return false;
};
if (!lookupLink().isEmpty() || canCreateLink()) {
const auto copyLink = [=] {
const auto link = lookupLink();
if (link.isEmpty()) {
@@ -427,12 +441,17 @@ void GroupCallSettingsBox(
)->addClickHandler([=] {
if (!copyLink() && !state->generatingLink) {
state->generatingLink = true;
channel->session().api().request(MTPmessages_ExportChatInvite(
channel->input
peer->session().api().request(MTPmessages_ExportChatInvite(
peer->input
)).done([=](const MTPExportedChatInvite &result) {
if (result.type() == mtpc_chatInviteExported) {
channel->setInviteLink(
qs(result.c_chatInviteExported().vlink()));
const auto link = qs(
result.c_chatInviteExported().vlink());
if (const auto chat = peer->asChat()) {
chat->setInviteLink(link);
} else if (const auto channel = peer->asChannel()) {
channel->setInviteLink(link);
}
copyLink();
}
}).send();
@@ -440,7 +459,7 @@ void GroupCallSettingsBox(
});
}
if (channel->canManageCall()) {
if (peer->canManageGroupCall()) {
AddButton(
layout,
tr::lng_group_call_end(),
@@ -472,7 +491,7 @@ void GroupCallSettingsBox(
if (canChangeJoinMuted
&& muteJoined
&& muteJoined->toggled() != joinMuted) {
SaveCallJoinMuted(channel, id, muteJoined->toggled());
SaveCallJoinMuted(peer, id, muteJoined->toggled());
}
}, box->lifetime());
box->addButton(tr::lng_box_done(), [=] {

View File

@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "media/audio/media_audio_track.h"
#include "platform/platform_specific.h"
@@ -58,12 +59,12 @@ void Instance::startOutgoingCall(not_null<UserData*> user, bool video) {
}), video);
}
void Instance::startOrJoinGroupCall(not_null<ChannelData*> channel) {
void Instance::startOrJoinGroupCall(not_null<PeerData*> peer) {
destroyCurrentCall();
const auto call = channel->call();
const auto call = peer->groupCall();
createGroupCall(
channel,
peer,
call ? call->input() : MTP_inputGroupCall(MTPlong(), MTPlong()));
}
@@ -97,35 +98,45 @@ void Instance::groupCallFailed(not_null<GroupCall*> call) {
});
}
void Instance::playSound(Sound sound) {
switch (sound) {
case Sound::Busy: {
if (!_callBusyTrack) {
_callBusyTrack = Media::Audio::Current().createTrack();
_callBusyTrack->fillFromFile(
Core::App().settings().getSoundPath(qsl("call_busy")));
}
_callBusyTrack->playOnce();
} break;
case Sound::Ended: {
if (!_callEndedTrack) {
_callEndedTrack = Media::Audio::Current().createTrack();
_callEndedTrack->fillFromFile(
Core::App().settings().getSoundPath(qsl("call_end")));
}
_callEndedTrack->playOnce();
} break;
case Sound::Connecting: {
if (!_callConnectingTrack) {
_callConnectingTrack = Media::Audio::Current().createTrack();
_callConnectingTrack->fillFromFile(
Core::App().settings().getSoundPath(qsl("call_connect")));
}
_callConnectingTrack->playOnce();
} break;
not_null<Media::Audio::Track*> Instance::ensureSoundLoaded(
const QString &key) {
const auto i = _tracks.find(key);
if (i != end(_tracks)) {
return i->second.get();
}
const auto result = _tracks.emplace(
key,
Media::Audio::Current().createTrack()).first->second.get();
result->fillFromFile(Core::App().settings().getSoundPath(key));
return result;
}
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::Connecting: return "group_call_connect";
}
Unexpected("GroupCallSound in Instance::groupCallPlaySound.");
return "";
}());
}
void Instance::destroyCall(not_null<Call*> call) {
@@ -183,17 +194,17 @@ void Instance::destroyGroupCall(not_null<GroupCall*> call) {
}
void Instance::createGroupCall(
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
const MTPInputGroupCall &inputCall) {
destroyCurrentCall();
auto call = std::make_unique<GroupCall>(
getGroupCallDelegate(),
channel,
peer,
inputCall);
const auto raw = call.get();
channel->session().account().sessionChanges(
peer->session().account().sessionChanges(
) | rpl::start_with_next([=] {
destroyGroupCall(raw);
}, raw->lifetime());
@@ -387,7 +398,7 @@ void Instance::handleGroupCallUpdate(
existing->applyUpdate(call);
}
if (_currentGroupCall
&& (&_currentGroupCall->channel()->session() == session)) {
&& (&_currentGroupCall->peer()->session() == session)) {
_currentGroupCall->handleUpdate(call);
}
}
@@ -402,7 +413,7 @@ void Instance::handleGroupCallUpdate(
existing->applyUpdate(update);
}
if (_currentGroupCall
&& (&_currentGroupCall->channel()->session() == session)
&& (&_currentGroupCall->peer()->session() == session)
&& (_currentGroupCall->id() == callId)) {
_currentGroupCall->handleUpdate(update);
}
@@ -458,7 +469,7 @@ bool Instance::hasActivePanel(not_null<Main::Session*> session) const {
return (&_currentCall->user()->session() == session)
&& _currentCallPanel->isActive();
} else if (inGroupCall()) {
return (&_currentGroupCall->channel()->session() == session)
return (&_currentGroupCall->peer()->session() == session)
&& _currentGroupCallPanel->isActive();
}
return false;
@@ -475,6 +486,25 @@ bool Instance::activateCurrentCall() {
return false;
}
bool Instance::minimizeCurrentActiveCall() {
if (inCall() && _currentCallPanel->isActive()) {
_currentCallPanel->minimize();
return true;
} else if (inGroupCall() && _currentGroupCallPanel->isActive()) {
_currentGroupCallPanel->minimize();
return true;
}
return false;
}
bool Instance::closeCurrentActiveCall() {
if (inGroupCall() && _currentGroupCallPanel->isActive()) {
_currentGroupCallPanel->close();
return true;
}
return false;
}
Call *Instance::currentCall() const {
return _currentCall.get();
}

View File

@@ -40,7 +40,7 @@ public:
~Instance();
void startOutgoingCall(not_null<UserData*> user, bool video);
void startOrJoinGroupCall(not_null<ChannelData*> channel);
void startOrJoinGroupCall(not_null<PeerData*> peer);
void handleUpdate(
not_null<Main::Session*> session,
const MTPUpdate &update);
@@ -55,6 +55,8 @@ public:
[[nodiscard]] bool hasActivePanel(
not_null<Main::Session*> session) const;
bool activateCurrentCall();
bool minimizeCurrentActiveCall();
bool closeCurrentActiveCall();
auto getVideoCapture()
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
@@ -64,6 +66,9 @@ public:
[[nodiscard]] bool isQuitPrevent();
private:
using CallSound = Call::Delegate::CallSound;
using GroupCallSound = GroupCall::Delegate::GroupCallSound;
[[nodiscard]] not_null<Call::Delegate*> getCallDelegate() {
return static_cast<Call::Delegate*>(this);
}
@@ -73,6 +78,10 @@ private:
[[nodiscard]] DhConfig getDhConfig() const override {
return _dhConfig;
}
not_null<Media::Audio::Track*> ensureSoundLoaded(const QString &key);
void playSoundOnce(const QString &key);
void callFinished(not_null<Call*> call) override;
void callFailed(not_null<Call*> call) override;
void callRedial(not_null<Call*> call) override;
@@ -81,24 +90,26 @@ private:
bool video) override {
requestPermissionsOrFail(std::move(onSuccess), video);
}
void callPlaySound(CallSound sound) override;
void groupCallFinished(not_null<GroupCall*> call) override;
void groupCallFailed(not_null<GroupCall*> call) override;
void groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override {
requestPermissionsOrFail(std::move(onSuccess), false);
}
void groupCallPlaySound(GroupCallSound sound) override;
using Sound = Call::Delegate::Sound;
void playSound(Sound sound) override;
void createCall(not_null<UserData*> user, Call::Type type, bool video);
void destroyCall(not_null<Call*> call);
void createGroupCall(
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
const MTPInputGroupCall &inputCall);
void destroyGroupCall(not_null<GroupCall*> call);
void requestPermissionOrFail(Platform::PermissionType type, Fn<void()> onSuccess);
void requestPermissionOrFail(
Platform::PermissionType type,
Fn<void()> onSuccess);
void refreshDhConfig();
void refreshServerConfig(not_null<Main::Session*> session);
@@ -132,9 +143,7 @@ private:
rpl::event_stream<GroupCall*> _currentGroupCallChanges;
std::unique_ptr<GroupPanel> _currentGroupCallPanel;
std::unique_ptr<Media::Audio::Track> _callConnectingTrack;
std::unique_ptr<Media::Audio::Track> _callEndedTrack;
std::unique_ptr<Media::Audio::Track> _callBusyTrack;
base::flat_map<QString, std::unique_ptr<Media::Audio::Track>> _tracks;
};

View File

@@ -209,12 +209,22 @@ bool Panel::isActive() const {
}
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->setWindowState(_window->windowState() | Qt::WindowActive);
_window->activateWindow();
_window->setFocus();
}
void Panel::minimize() {
_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
}
void Panel::replaceCall(not_null<Call*> call) {
reinitWithCall(call);
updateControlsGeometry();

View File

@@ -53,6 +53,7 @@ public:
[[nodiscard]] bool isActive() const;
void showAndActivate();
void minimize();
void replaceCall(not_null<Call*> call);
void closeBeforeDestroy();

View File

@@ -11,10 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/paint/blobs_linear.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/chat/group_call_userpics.h" // Ui::GroupCallUser.
#include "ui/chat/group_call_bar.h" // Ui::GroupCallBarContent.
#include "ui/layers/generic_box.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/text/format_values.h"
#include "ui/toast/toast.h"
#include "lang/lang_keys.h"
#include "core/application.h"
#include "calls/calls_call.h"
@@ -24,73 +26,98 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_group_call_tracker.h" // ContentByCall.
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_changes.h"
#include "main/main_session.h"
#include "boxes/abstract_box.h"
#include "base/timer.h"
#include "app.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h" // style::GroupCallUserpics
#include "styles/style_layers.h"
namespace Calls {
enum class BarState {
Connecting,
Active,
Muted,
ForceMuted,
};
namespace {
constexpr auto kMaxUsersInBar = 3;
constexpr auto kUpdateDebugTimeoutMs = crl::time(500);
constexpr auto kSwitchStateDuration = 120;
constexpr auto kMinorBlobAlpha = 76. / 255.;
constexpr auto kBlobLevelDuration1 = 250;
constexpr auto kBlobLevelDuration2 = 120;
constexpr auto kHideBlobsDuration = crl::time(500);
constexpr auto kBlobLevelDuration = crl::time(250);
constexpr auto kBlobUpdateInterval = crl::time(100);
auto LinearBlobs() -> std::array<Ui::Paint::LinearBlobs::BlobData, 3> {
return { {
auto BarStateFromMuteState(MuteState state, bool connecting) {
return (connecting
? BarState::Connecting
: state == MuteState::ForceMuted
? BarState::ForceMuted
: state == MuteState::Muted
? BarState::Muted
: BarState::Active);
};
auto LinearBlobs() {
return std::vector<Ui::Paint::LinearBlobs::BlobData>{
{
.segmentsCount = 5,
.minScale = 1.,
.minRadius = (float)st::groupCallMajorBlobMinRadius,
.minRadius = 0.,
.maxRadius = (float)st::groupCallMajorBlobMaxRadius,
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
.speedScale = .3,
.alpha = 1.,
.topOffset = st::groupCallMajorBlobTopOffset,
},
{
.segmentsCount = 7,
.minScale = 1.,
.minRadius = (float)st::groupCallMinorBlobMinRadius,
.minRadius = 0.,
.maxRadius = (float)st::groupCallMinorBlobMaxRadius,
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
.speedScale = .7,
.alpha = kMinorBlobAlpha,
.topOffset = st::groupCallMinorBlobTopOffset,
},
{
.segmentsCount = 8,
.minScale = 1.,
.minRadius = (float)st::groupCallMinorBlobMinRadius,
.minRadius = 0.,
.maxRadius = (float)st::groupCallMinorBlobMaxRadius,
.idleRadius = (float)st::groupCallMinorBlobIdleRadius,
.speedScale = .7,
.alpha = kMinorBlobAlpha,
.topOffset = st::groupCallMinorBlobTopOffset,
},
} };
};
}
auto Colors() {
using Vector = std::vector<QColor>;
return base::flat_map<MuteState, Vector>{
using Colors = anim::gradient_colors;
return base::flat_map<BarState, Colors>{
{
MuteState::ForceMuted,
Vector{ st::groupCallMembersBg->c, st::groupCallMembersBg->c }
BarState::ForceMuted,
Colors(QGradientStops{
{ 0.0, st::groupCallForceMutedBar1->c },
{ .35, st::groupCallForceMutedBar2->c },
{ 1.0, st::groupCallForceMutedBar3->c } })
},
{
MuteState::Active,
Vector{ st::groupCallLive1->c, st::groupCallLive2->c }
BarState::Active,
Colors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })
},
{
MuteState::Muted,
Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c }
BarState::Muted,
Colors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })
},
{
BarState::Connecting,
Colors(st::callBarBgMuted->c)
},
};
}
@@ -139,6 +166,10 @@ void DebugInfoBox::updateText() {
} // namespace
struct TopBar::User {
Ui::GroupCallUser data;
};
class Mute final : public Ui::IconButton {
public:
Mute(QWidget *parent, const style::IconButton &st)
@@ -209,6 +240,12 @@ TopBar::TopBar(
: RpWidget(parent)
, _call(call)
, _groupCall(groupCall)
, _userpics(call
? nullptr
: std::make_unique<Ui::GroupCallUserpics>(
st::groupCallTopBarUserpics,
rpl::single(true),
[=] { updateUserpics(); }))
, _durationLabel(_call
? object_ptr<Ui::LabelSimple>(this, st::callBarLabel)
: object_ptr<Ui::LabelSimple>(nullptr))
@@ -237,7 +274,9 @@ 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) {
if (group->muted() == MuteState::ForceMuted) {
Ui::Toast::Show(tr::lng_group_call_force_muted_sub(tr::now));
} else {
group->setMuted((group->muted() == MuteState::Muted)
? MuteState::Active
: MuteState::Muted);
@@ -248,27 +287,36 @@ void TopBar::initControls() {
const auto mapToState = [](bool muted) {
return muted ? MuteState::Muted : MuteState::Active;
};
const auto fromState = _mute->lifetime().make_state<MuteState>(
_call ? mapToState(_call->muted()) : _groupCall->muted());
const auto fromState = _mute->lifetime().make_state<BarState>(
BarStateFromMuteState(
_call
? mapToState(_call->muted())
: _groupCall->muted(),
false));
auto muted = _call
? _call->mutedValue() | rpl::map(mapToState)
: (_groupCall->mutedValue()
| MapPushToTalkToActive()
| rpl::distinct_until_changed()
| rpl::type_erased());
? rpl::combine(
_call->mutedValue() | rpl::map(mapToState),
rpl::single(false)) | rpl::type_erased()
: rpl::combine(
(_groupCall->mutedValue()
| MapPushToTalkToActive()
| rpl::distinct_until_changed()
| rpl::type_erased()),
_groupCall->connectingValue());
std::move(
muted
) | rpl::start_with_next([=](MuteState state) {
setMuted(state != MuteState::Active);
) | rpl::map(
BarStateFromMuteState
) | rpl::start_with_next([=](BarState state) {
_isGroupConnecting = (state == BarState::Connecting);
setMuted(state != BarState::Active);
update();
const auto isForceMuted = (state == MuteState::ForceMuted);
const auto isForceMuted = (state == BarState::ForceMuted);
if (isForceMuted) {
_mute->clearState();
}
_mute->setAttribute(
Qt::WA_TransparentForMouseEvents,
isForceMuted);
_mute->setPointerCursor(!isForceMuted);
const auto to = 1.;
const auto from = _switchStateAnimation.animating()
@@ -278,8 +326,8 @@ void TopBar::initControls() {
const auto toMuted = state;
*fromState = state;
const auto crossFrom = (fromMuted != MuteState::Active) ? 1. : 0.;
const auto crossTo = (toMuted != MuteState::Active) ? 1. : 0.;
const auto crossFrom = (fromMuted != BarState::Active) ? 1. : 0.;
const auto crossTo = (toMuted != BarState::Active) ? 1. : 0.;
auto animationCallback = [=](float64 value) {
if (_groupCall) {
@@ -290,7 +338,7 @@ void TopBar::initControls() {
const auto crossProgress = (crossFrom == crossTo)
? crossTo
: (crossFrom + float64(crossTo - crossFrom) * value);
: anim::interpolateF(crossFrom, crossTo, value);
_mute->setProgress(crossProgress);
};
@@ -305,6 +353,14 @@ void TopBar::initControls() {
if (const auto group = _groupCall.get()) {
subscribeToMembersChanges(group);
_isGroupConnecting.value(
) | rpl::start_with_next([=](bool isConnecting) {
_mute->setAttribute(
Qt::WA_TransparentForMouseEvents,
isConnecting);
updateInfoLabels();
}, lifetime());
}
if (const auto call = _call.get()) {
@@ -335,11 +391,15 @@ void TopBar::initControls() {
if (const auto call = _call.get()) {
call->hangup();
} else if (const auto group = _groupCall.get()) {
Ui::show(Box(
LeaveGroupCallBox,
group,
false,
BoxContext::MainWindow));
if (!group->peer()->canManageGroupCall()) {
group->hangup();
} else {
Ui::show(Box(
LeaveGroupCallBox,
group,
false,
BoxContext::MainWindow));
}
}
});
updateDurationText();
@@ -353,14 +413,12 @@ void TopBar::initBlobsUnder(
return;
}
static constexpr auto kHideDuration = kBlobLevelDuration1 * 2;
struct State {
Ui::Paint::LinearBlobs paint = {
LinearBlobs() | ranges::to_vector,
kBlobLevelDuration1,
kBlobLevelDuration2,
1.
LinearBlobs(),
kBlobLevelDuration,
1.,
Ui::Paint::LinearBlob::Direction::TopDown
};
Ui::Animations::Simple hideAnimation;
Ui::Animations::Basic animation;
@@ -369,8 +427,6 @@ void TopBar::initBlobsUnder(
crl::time lastTime = 0;
float lastLevel = 0.;
float levelBeforeLast = 0.;
int maxHeight = st::groupCallMinorBlobMinRadius
+ st::groupCallMinorBlobMaxRadius;
};
_blobs = base::make_unique_q<Ui::RpWidget>(blobsParent);
@@ -387,7 +443,7 @@ void TopBar::initBlobsUnder(
state->animation.init([=](crl::time now) {
if (const auto last = state->hideLastTime; (last > 0)
&& (now - last >= kHideDuration)) {
&& (now - last >= kHideBlobsDuration)) {
state->animation.stop();
return false;
}
@@ -408,23 +464,14 @@ void TopBar::initBlobsUnder(
auto hideBlobs = rpl::combine(
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
Core::App().appDeactivatedValue(),
group->stateValue(
) | rpl::map([](Calls::GroupCall::State state) {
using State = Calls::GroupCall::State;
if (state != State::Creating
&& state != State::Joining
&& state != State::Joined
&& state != State::Connecting) {
return true;
}
return false;
}) | rpl::distinct_until_changed()
) | rpl::map([](bool animDisabled, bool hide, bool isBadState) {
return isBadState || animDisabled || hide;
group->connectingValue()
) | rpl::map([](bool animDisabled, bool hide, bool connecting) {
return connecting || animDisabled || hide;
});
std::move(
hideBlobs
) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool hide) {
if (hide) {
state->paint.setLevel(0.);
@@ -443,7 +490,7 @@ void TopBar::initBlobsUnder(
const auto to = hide ? 1. : 0.;
state->hideAnimation.start([=](float64) {
_blobs->update();
}, from, to, kHideDuration);
}, from, to, kHideBlobsDuration);
}, lifetime());
std::move(
@@ -451,7 +498,7 @@ void TopBar::initBlobsUnder(
) | rpl::start_with_next([=](QRect rect) {
_blobs->resize(
rect.width(),
std::min(state->maxHeight, rect.height()));
(int)state->paint.maxRadius());
_blobs->moveToLeft(rect.x(), rect.y() + rect.height());
}, lifetime());
@@ -473,12 +520,9 @@ void TopBar::initBlobsUnder(
p.setOpacity(1. - hidden);
}
const auto top = -_blobs->height() * hidden;
const auto drawUnder = QRect(
0,
top,
_blobs->width() + st::groupCallBlobWidthAdditional,
0);
state->paint.paint(p, _groupBrush, drawUnder, 0, 0);
const auto width = _blobs->width();
p.translate(0, top);
state->paint.paint(p, _groupBrush, width);
}, _blobs->lifetime());
group->levelUpdates(
@@ -501,12 +545,12 @@ void TopBar::initBlobsUnder(
}
void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
const auto channel = call->channel();
channel->session().changes().peerFlagsValue(
channel,
const auto peer = call->peer();
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::GroupCall
) | rpl::map([=] {
return channel->call();
return peer->groupCall();
}) | rpl::filter([=](Data::GroupCall *real) {
const auto call = _groupCall.get();
return call && real && (real->id() == call->id());
@@ -515,20 +559,46 @@ void TopBar::subscribeToMembersChanges(not_null<GroupCall*> call) {
) | rpl::map([=](not_null<Data::GroupCall*> real) {
return HistoryView::GroupCallTracker::ContentByCall(
real,
HistoryView::UserpicsInRowStyle{
.size = st::groupCallTopBarUserpicSize,
.shift = st::groupCallTopBarUserpicShift,
.stroke = st::groupCallTopBarUserpicStroke,
});
st::groupCallTopBarUserpics.size);
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const Ui::GroupCallBarContent &content) {
const auto changed = (_userpics.size() != content.userpics.size());
_userpics = content.userpics;
if (changed) {
updateControlsGeometry();
) | rpl::filter([=](const Ui::GroupCallBarContent &content) {
if (_users.size() != content.users.size()) {
return true;
}
update();
for (auto i = 0, count = int(_users.size()); i != count; ++i) {
if (_users[i].userpicKey != content.users[i].userpicKey
|| _users[i].id != content.users[i].id) {
return true;
}
}
return false;
}) | rpl::start_with_next([=](const Ui::GroupCallBarContent &content) {
_users = content.users;
for (auto &user : _users) {
user.speaking = false;
}
_userpics->update(_users, !isHidden());
}, lifetime());
_userpics->widthValue(
) | rpl::start_with_next([=](int width) {
_userpicsWidth = width;
updateControlsGeometry();
}, lifetime());
call->peer()->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Name
) | rpl::filter([=](const Data::PeerUpdate &update) {
// _peer may change for the same Panel.
const auto call = _groupCall.get();
return (call != nullptr) && (update.peer == call->peer());
}) | rpl::start_with_next([=] {
updateInfoLabels();
}, lifetime());
}
void TopBar::updateUserpics() {
update(_mute->width(), 0, _userpics->maxWidth(), height());
}
void TopBar::updateInfoLabels() {
@@ -544,10 +614,13 @@ void TopBar::setInfoLabels() {
_fullInfoLabel->setText(fullName.toUpper());
_shortInfoLabel->setText(shortName.toUpper());
} else if (const auto group = _groupCall.get()) {
const auto channel = group->channel();
const auto name = channel->name;
_fullInfoLabel->setText(name.toUpper());
_shortInfoLabel->setText(name.toUpper());
const auto peer = group->peer();
const auto name = peer->name;
const auto text = _isGroupConnecting.current()
? tr::lng_group_call_connecting(tr::now)
: name.toUpper();
_fullInfoLabel->setText(text);
_shortInfoLabel->setText(text);
}
}
@@ -588,8 +661,13 @@ void TopBar::updateControlsGeometry() {
_durationLabel->moveToLeft(left, st::callBarLabelTop);
left += _durationLabel->width() + st::callBarSkip;
}
if (!_userpics.isNull()) {
left += _userpics.width() / _userpics.devicePixelRatio();
if (_userpicsWidth) {
const auto single = st::groupCallTopBarUserpics.size;
const auto skip = anim::interpolate(
0,
st::callBarSkip,
std::min(_userpicsWidth, single) / float64(single));
left += _userpicsWidth + skip;
}
if (_signalBars) {
_signalBars->moveToLeft(left, (height() - _signalBars->height()) / 2);
@@ -641,11 +719,10 @@ void TopBar::paintEvent(QPaintEvent *e) {
: (_muted ? st::callBarBgMuted : st::callBarBg);
p.fillRect(e->rect(), std::move(brush));
if (!_userpics.isNull()) {
const auto imageSize = _userpics.size()
/ _userpics.devicePixelRatio();
const auto top = (height() - imageSize.height()) / 2;
p.drawImage(_mute->width(), top, _userpics);
if (_userpicsWidth) {
const auto size = st::groupCallTopBarUserpics.size;
const auto top = (height() - size) / 2;
_userpics->paint(p, _mute->width(), top, size);
}
}

View File

@@ -20,6 +20,8 @@ class IconButton;
class AbstractButton;
class LabelSimple;
class FlatLabel;
struct GroupCallUser;
class GroupCallUserpics;
} // namespace Ui
namespace Main {
@@ -33,6 +35,7 @@ class GroupCall;
class SignalBars;
class Mute;
enum class MuteState;
enum class BarState;
class TopBar : public Ui::RpWidget {
public:
@@ -49,6 +52,8 @@ protected:
void paintEvent(QPaintEvent *e) override;
private:
struct User;
TopBar(
QWidget *parent,
const base::weak_ptr<Call> &call,
@@ -63,12 +68,15 @@ private:
void setMuted(bool mute);
void subscribeToMembersChanges(not_null<GroupCall*> call);
void updateUserpics();
const base::weak_ptr<Call> _call;
const base::weak_ptr<GroupCall> _groupCall;
bool _muted = false;
QImage _userpics;
std::vector<Ui::GroupCallUser> _users;
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
int _userpicsWidth = 0;
object_ptr<Ui::LabelSimple> _durationLabel;
object_ptr<SignalBars> _signalBars;
object_ptr<Ui::FlatLabel> _fullInfoLabel;
@@ -79,8 +87,10 @@ private:
object_ptr<Ui::IconButton> _hangup;
base::unique_qptr<Ui::RpWidget> _blobs;
rpl::variable<bool> _isGroupConnecting = false;
QBrush _groupBrush;
anim::linear_gradients<MuteState> _gradients;
anim::linear_gradients<BarState> _gradients;
Ui::Animations::Simple _switchStateAnimation;
base::Timer _updateDurationTimer;

View File

@@ -789,9 +789,17 @@ bool Application::openCustomUrl(
}
void Application::preventOrInvoke(Fn<void()> &&callback) {
_window->preventOrInvoke(std::move(callback));
}
void Application::lockByPasscode() {
_passcodeLock = true;
_window->setupPasscodeLock();
preventOrInvoke([=] {
if (_window) {
_passcodeLock = true;
_window->setupPasscodeLock();
}
});
}
void Application::unlockPasscode() {
@@ -907,18 +915,25 @@ bool Application::closeActiveWindow() {
if (hideMediaView()) {
return true;
}
if (const auto window = activeWindow()) {
window->close();
return true;
if (!calls().closeCurrentActiveCall()) {
if (const auto window = activeWindow()) {
if (window->widget()->isVisible()
&& window->widget()->isActive()) {
window->close();
return true;
}
}
}
return false;
}
bool Application::minimizeActiveWindow() {
hideMediaView();
if (const auto window = activeWindow()) {
window->minimize();
return true;
if (!calls().minimizeCurrentActiveCall()) {
if (const auto window = activeWindow()) {
window->minimize();
return true;
}
}
return false;
}

View File

@@ -277,6 +277,8 @@ public:
void switchFreeType();
void writeInstallBetaVersionsSetting();
void preventOrInvoke(Fn<void()> &&callback);
void call_handleObservables();
protected:

View File

@@ -21,60 +21,6 @@ namespace {
std::map<int, const char*> BetaLogs() {
return {
{
2001008,
"- Add support for full group message history export.\n"
"- Allow export of a single chat message history in JSON format."
},
{
2001014,
"- Support for multiple accounts."
},
{
2001017,
"- Fix messages editing in a non-active account.\n"
"- Fix large animated emoji messages editing.\n"
"- Fix high definition GIF animations opening in media viewer.\n"
"- Multiple crash fixes."
},
{
2001018,
"- Fix a possible crash in Picture-in-Picture video player.\n"
"- Fix copying links from message texts.\n"
"- Raise file size limit to 2000 MB.\n"
"- Allow using system window frame in Windows and Linux."
},
{
2001019,
"- File uploading in an inactive account correctly finishes.\n"
"- Stickers panel works correctly after switching between accounts.\n"
"- Large .webp files are not shown as stickers.\n"
"- MacBook TouchBar support was fully rewritten with fixes for multiple accounts.\n"
"- Custom window title bar works in all Linux versions.\n"
"- Passcode doesn't auto-lock while you're active in other apps on Linux X11."
},
{
2001021,
"- Edit your scheduled messages.\n"
"- See the unread messages indicator for your additional accounts on the main menu button.\n"
"- Use Auto-Night Mode to make Telegram night mode match the system Dark Mode settings.\n"
"- Enjoy dark native window frame for Telegram night mode on Windows.\n"
},
{
2004006,
"- Fix image compression option when sending files with drag-n-drop.\n"
@@ -109,6 +55,34 @@ std::map<int, const char*> BetaLogs() {
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"
"- Fix possible crash in connecting to voice chats.\n"
"- Use different audio module code on Windows in calls.\n"
}
};
};

View File

@@ -1591,7 +1591,7 @@ void UpdateApplication() {
if (const auto window = App::wnd()) {
if (const auto controller = window->sessionController()) {
controller->showSection(
Info::Memento(
std::make_shared<Info::Memento>(
Info::Settings::Tag{ controller->session().user() },
Info::Section::SettingsType::Advanced),
Window::SectionShow());

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 2004013;
constexpr auto AppVersionStr = "2.4.13";
constexpr auto AppVersion = 2005002;
constexpr auto AppVersionStr = "2.5.2";
constexpr auto AppBetaVersion = true;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -104,13 +104,9 @@ void ChannelData::setInviteLink(const QString &newInviteLink) {
}
}
QString ChannelData::inviteLink() const {
return _inviteLink;
}
bool ChannelData::canHaveInviteLink() const {
return (adminRights() & AdminRight::f_invite_users)
|| amCreator();
return amCreator()
|| (adminRights() & AdminRight::f_invite_users);
}
void ChannelData::setLocation(const MTPChannelLocation &data) {
@@ -524,10 +520,6 @@ bool ChannelData::canRestrictUser(not_null<UserData*> user) const {
return adminRights() & AdminRight::f_ban_users;
}
bool ChannelData::canManageCall() const {
return amCreator() || (adminRights() & AdminRight::f_manage_call);
}
void ChannelData::setAdminRights(const MTPChatAdminRights &rights) {
if (rights.c_chatAdminRights().vflags().v == adminRights()) {
return;
@@ -677,14 +669,24 @@ void ChannelData::privateErrorReceived() {
}
}
void ChannelData::setCall(const MTPInputGroupCall &call) {
void ChannelData::migrateCall(std::unique_ptr<Data::GroupCall> call) {
Expects(_call == nullptr);
Expects(call != nullptr);
_call = std::move(call);
_call->setPeer(this);
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
addFlags(MTPDchannel::Flag::f_call_active);
}
void ChannelData::setGroupCall(const MTPInputGroupCall &call) {
call.match([&](const MTPDinputGroupCall &data) {
if (_call && _call->id() == data.vid().v) {
return;
} else if (!_call && !data.vid().v) {
return;
} else if (!data.vid().v) {
clearCall();
clearGroupCall();
return;
}
const auto hasCall = (_call != nullptr);
@@ -701,7 +703,7 @@ void ChannelData::setCall(const MTPInputGroupCall &call) {
});
}
void ChannelData::clearCall() {
void ChannelData::clearGroupCall() {
if (!_call) {
return;
}
@@ -744,9 +746,9 @@ void ApplyChannelUpdate(
auto canEditStickers = channel->canEditStickers();
if (const auto call = update.vcall()) {
channel->setCall(*call);
channel->setGroupCall(*call);
} else {
channel->clearCall();
channel->clearGroupCall();
}
channel->setFullFlags(update.vflags().v);

View File

@@ -11,10 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_pts_waiter.h"
#include "data/data_location.h"
namespace Data {
class GroupCall;
} // namespace Data
struct ChannelLocation {
QString address;
Data::LocationPoint point;
@@ -307,10 +303,11 @@ public:
[[nodiscard]] bool canDelete() const;
[[nodiscard]] bool canEditAdmin(not_null<UserData*> user) const;
[[nodiscard]] bool canRestrictUser(not_null<UserData*> user) const;
[[nodiscard]] bool canManageCall() const;
void setInviteLink(const QString &newInviteLink);
[[nodiscard]] QString inviteLink() const;
[[nodiscard]] QString inviteLink() const {
return _inviteLink;
}
[[nodiscard]] bool canHaveInviteLink() const;
void setLocation(const MTPChannelLocation &data);
@@ -399,11 +396,12 @@ public:
[[nodiscard]] QString invitePeekHash() const;
void privateErrorReceived();
[[nodiscard]] Data::GroupCall *call() const {
[[nodiscard]] Data::GroupCall *groupCall() const {
return _call.get();
}
void setCall(const MTPInputGroupCall &call);
void clearCall();
void migrateCall(std::unique_ptr<Data::GroupCall> call);
void setGroupCall(const MTPInputGroupCall &call);
void clearGroupCall();
// Still public data members.
uint64 access = 0;

View File

@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_group_call.h"
#include "history/history.h"
#include "main/main_session.h"
#include "apiwrap.h"
@@ -24,6 +25,14 @@ using UpdateFlag = Data::PeerUpdate::Flag;
ChatData::ChatData(not_null<Data::Session*> owner, PeerId id)
: PeerData(owner, id)
, inputChat(MTP_int(bareId())) {
_flags.changes(
) | rpl::start_with_next([=](const Flags::Change &change) {
if (change.diff & MTPDchat::Flag::f_call_not_empty) {
if (const auto history = this->owner().historyLoaded(this)) {
history->updateChatListEntry();
}
}
}, _lifetime);
}
void ChatData::setPhoto(const MTPChatPhoto &photo) {
@@ -124,6 +133,11 @@ void ChatData::setInviteLink(const QString &newInviteLink) {
}
}
bool ChatData::canHaveInviteLink() const {
return amCreator()
|| (adminRights() & AdminRight::f_invite_users);
}
void ChatData::setAdminRights(const MTPChatAdminRights &rights) {
if (rights.c_chatAdminRights().vflags().v == adminRights()) {
return;
@@ -176,6 +190,47 @@ void ChatData::setMigrateToChannel(ChannelData *channel) {
}
}
void ChatData::setGroupCall(const MTPInputGroupCall &call) {
if (migrateTo()) {
return;
}
call.match([&](const MTPDinputGroupCall &data) {
if (_call && _call->id() == data.vid().v) {
return;
} else if (!_call && !data.vid().v) {
return;
} else if (!data.vid().v) {
clearGroupCall();
return;
}
const auto hasCall = (_call != nullptr);
if (hasCall) {
owner().unregisterGroupCall(_call.get());
}
_call = std::make_unique<Data::GroupCall>(
this,
data.vid().v,
data.vaccess_hash().v);
owner().registerGroupCall(_call.get());
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
addFlags(MTPDchat::Flag::f_call_active);
});
}
void ChatData::clearGroupCall() {
if (!_call) {
return;
} else if (const auto group = migrateTo(); group && !group->groupCall()) {
group->migrateCall(base::take(_call));
} else {
owner().unregisterGroupCall(_call.get());
_call = nullptr;
}
session().changes().peerUpdated(this, UpdateFlag::GroupCall);
removeFlags(MTPDchat::Flag::f_call_active
| MTPDchat::Flag::f_call_not_empty);
}
namespace Data {
void ApplyChatUpdate(
@@ -310,6 +365,12 @@ void ApplyChatUpdate(
void ApplyChatUpdate(not_null<ChatData*> chat, const MTPDchatFull &update) {
ApplyChatUpdate(chat, update.vparticipants());
if (const auto call = update.vcall()) {
chat->setGroupCall(*call);
} else {
chat->clearGroupCall();
}
if (const auto info = update.vbot_info()) {
for (const auto &item : info->v) {
item.match([&](const MTPDbotInfo &data) {

View File

@@ -18,6 +18,7 @@ public:
| MTPDchat::Flag::f_deactivated
| MTPDchat::Flag::f_migrated_to
| MTPDchat::Flag::f_admin_rights
| MTPDchat::Flag::f_call_not_empty
| MTPDchat::Flag::f_default_banned_rights;
using Flags = Data::Flags<
MTPDchat::Flags,
@@ -141,9 +142,10 @@ public:
void applyEditAdmin(not_null<UserData*> user, bool isAdmin);
void setInviteLink(const QString &newInviteLink);
QString inviteLink() const {
[[nodiscard]] QString inviteLink() const {
return _inviteLink;
}
[[nodiscard]] bool canHaveInviteLink() const;
void refreshBotStatus();
enum class UpdateStatus {
@@ -162,6 +164,12 @@ public:
ChannelData *getMigrateToChannel() const;
void setMigrateToChannel(ChannelData *channel);
[[nodiscard]] Data::GroupCall *groupCall() const {
return _call.get();
}
void setGroupCall(const MTPInputGroupCall &call);
void clearGroupCall();
// Still public data members.
const MTPint inputChat;
@@ -185,7 +193,10 @@ private:
AdminRightFlags _adminRights;
int _version = 0;
std::unique_ptr<Data::GroupCall> _call;
ChannelData *_migratedTo = nullptr;
rpl::lifetime _lifetime;
};

View File

@@ -7,30 +7,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_group_call.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "calls/calls_instance.h"
#include "calls/calls_group_call.h"
#include "core/application.h"
#include "apiwrap.h"
namespace Data {
namespace {
constexpr auto kRequestPerPage = 30;
constexpr auto kSpeakingAfterActive = crl::time(6000);
constexpr auto kActiveAfterJoined = crl::time(1000);
} // namespace
GroupCall::GroupCall(
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
uint64 id,
uint64 accessHash)
: _channel(channel)
, _id(id)
, _accessHash(accessHash) {
: _id(id)
, _accessHash(accessHash)
, _peer(peer)
, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); }) {
}
GroupCall::~GroupCall() {
api().request(_unknownSsrcsRequestId).cancel();
api().request(_unknownUsersRequestId).cancel();
api().request(_participantsRequestId).cancel();
api().request(_reloadRequestId).cancel();
}
@@ -39,14 +47,21 @@ uint64 GroupCall::id() const {
return _id;
}
not_null<ChannelData*> GroupCall::channel() const {
return _channel;
not_null<PeerData*> GroupCall::peer() const {
return _peer;
}
MTPInputGroupCall GroupCall::input() const {
return MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash));
}
void GroupCall::setPeer(not_null<PeerData*> peer) {
Expects(peer->migrateFrom() == _peer);
Expects(_peer->migrateTo() == peer);
_peer = peer;
}
auto GroupCall::participants() const
-> const std::vector<Participant> & {
return _participants;
@@ -70,7 +85,7 @@ void GroupCall::requestParticipants() {
)).done([=](const MTPphone_GroupParticipants &result) {
result.match([&](const MTPDphone_groupParticipants &data) {
_nextOffset = qs(data.vnext_offset());
_channel->owner().processUsers(data.vusers());
_peer->owner().processUsers(data.vusers());
applyParticipantsSlice(
data.vparticipants().v,
ApplySliceSource::SliceLoaded);
@@ -85,30 +100,43 @@ void GroupCall::requestParticipants() {
});
_participantsSliceAdded.fire({});
_participantsRequestId = 0;
changeChannelEmptyCallFlag();
changePeerEmptyCallFlag();
}).fail([=](const RPCError &error) {
_fullCount = _participants.size();
_allReceived = true;
_participantsRequestId = 0;
changeChannelEmptyCallFlag();
changePeerEmptyCallFlag();
}).send();
}
void GroupCall::changeChannelEmptyCallFlag() {
constexpr auto flag = MTPDchannel::Flag::f_call_not_empty;
if (_channel->call() != this) {
void GroupCall::changePeerEmptyCallFlag() {
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
constexpr auto chatFlag = MTPDchat::Flag::f_call_not_empty;
constexpr auto channelFlag = MTPDchannel::Flag::f_call_not_empty;
if (_peer->groupCall() != this) {
return;
} else if (fullCount() > 0) {
if (!(_channel->flags() & flag)) {
_channel->addFlags(flag);
_channel->session().changes().peerUpdated(
_channel,
if (chat && !(chat->flags() & chatFlag)) {
chat->addFlags(chatFlag);
chat->session().changes().peerUpdated(
chat,
Data::PeerUpdate::Flag::GroupCall);
} else if (channel && !(channel->flags() & channelFlag)) {
channel->addFlags(channelFlag);
channel->session().changes().peerUpdated(
channel,
Data::PeerUpdate::Flag::GroupCall);
}
} else if (_channel->flags() & flag) {
_channel->removeFlags(flag);
_channel->session().changes().peerUpdated(
_channel,
} else if (chat && (chat->flags() & chatFlag)) {
chat->removeFlags(chatFlag);
chat->session().changes().peerUpdated(
chat,
Data::PeerUpdate::Flag::GroupCall);
} else if (channel && (channel->flags() & channelFlag)) {
channel->removeFlags(channelFlag);
channel->session().changes().peerUpdated(
channel,
Data::PeerUpdate::Flag::GroupCall);
}
}
@@ -159,13 +187,17 @@ void GroupCall::applyCall(const MTPGroupCall &call, bool force) {
_canChangeJoinMuted = data.is_can_change_join_muted();
_version = data.vversion().v;
_fullCount = data.vparticipants_count().v;
changeChannelEmptyCallFlag();
changePeerEmptyCallFlag();
}, [&](const MTPDgroupCallDiscarded &data) {
const auto id = _id;
const auto channel = _channel;
crl::on_main(&channel->session(), [=] {
if (channel->call() && channel->call()->id() == id) {
channel->clearCall();
const auto peer = _peer;
crl::on_main(&peer->session(), [=] {
if (peer->groupCall() && peer->groupCall()->id() == id) {
if (const auto chat = peer->asChat()) {
chat->clearGroupCall();
} else if (const auto channel = peer->asChannel()) {
channel->clearGroupCall();
}
}
});
});
@@ -182,8 +214,9 @@ void GroupCall::reload() {
MTPphone_GetGroupCall(input())
).done([=](const MTPphone_GroupCall &result) {
result.match([&](const MTPDphone_groupCall &data) {
_channel->owner().processUsers(data.vusers());
_peer->owner().processUsers(data.vusers());
_participants.clear();
_speakingByActiveFinishes.clear();
_userBySsrc.clear();
applyParticipantsSlice(
data.vparticipants().v,
@@ -201,11 +234,15 @@ void GroupCall::reload() {
void GroupCall::applyParticipantsSlice(
const QVector<MTPGroupCallParticipant> &list,
ApplySliceSource sliceSource) {
const auto amInCall = inCall();
const auto now = base::unixtime::now();
const auto speakingAfterActive = TimeId(kSpeakingAfterActive / 1000);
auto changedCount = _fullCount.current();
for (const auto &participant : list) {
participant.match([&](const MTPDgroupCallParticipant &data) {
const auto userId = data.vuser_id().v;
const auto user = _channel->owner().user(userId);
const auto user = _peer->owner().user(userId);
const auto i = ranges::find(
_participants,
user,
@@ -216,6 +253,7 @@ void GroupCall::applyParticipantsSlice(
.was = *i,
};
_userBySsrc.erase(i->ssrc);
_speakingByActiveFinishes.remove(user);
_participants.erase(i);
if (sliceSource != ApplySliceSource::SliceLoaded) {
_participantUpdates.fire(std::move(update));
@@ -231,10 +269,16 @@ void GroupCall::applyParticipantsSlice(
: std::nullopt;
const auto canSelfUnmute = !data.is_muted()
|| data.is_can_self_unmute();
const auto lastActive = data.vactive_date().value_or(
was ? was->lastActive : 0);
const auto speaking = canSelfUnmute
&& ((was ? was->speaking : false)
|| (!amInCall
&& (lastActive + speakingAfterActive > now)));
const auto value = Participant{
.user = user,
.date = data.vdate().v,
.lastActive = was ? was->lastActive : 0,
.lastActive = lastActive,
.ssrc = uint32(data.vsource().v),
.speaking = canSelfUnmute && (was ? was->speaking : false),
.muted = data.is_muted(),
@@ -243,8 +287,7 @@ void GroupCall::applyParticipantsSlice(
if (i == end(_participants)) {
_userBySsrc.emplace(value.ssrc, user);
_participants.push_back(value);
_channel->owner().unregisterInvitedToCallUser(_id, user);
++changedCount;
_peer->owner().unregisterInvitedToCallUser(_id, user);
} else {
if (i->ssrc != value.ssrc) {
_userBySsrc.erase(i->ssrc);
@@ -252,6 +295,9 @@ void GroupCall::applyParticipantsSlice(
}
*i = value;
}
if (data.is_just_joined()) {
++changedCount;
}
if (sliceSource != ApplySliceSource::SliceLoaded) {
_participantUpdates.fire({
.was = was,
@@ -262,53 +308,31 @@ void GroupCall::applyParticipantsSlice(
}
if (sliceSource == ApplySliceSource::UpdateReceived) {
_fullCount = changedCount;
changeChannelEmptyCallFlag();
changePeerEmptyCallFlag();
}
}
void GroupCall::applyParticipantsMutes(
const MTPDupdateGroupCallParticipants &update) {
for (const auto &participant : update.vparticipants().v) {
participant.match([&](const MTPDgroupCallParticipant &data) {
if (data.is_left()) {
return;
}
const auto userId = data.vuser_id().v;
const auto user = _channel->owner().user(userId);
const auto i = ranges::find(
_participants,
user,
&Participant::user);
if (i != end(_participants)) {
const auto was = *i;
i->muted = data.is_muted();
i->canSelfUnmute = !i->muted || data.is_can_self_unmute();
if (!i->canSelfUnmute) {
i->speaking = false;
}
_participantUpdates.fire({
.was = was,
.now = *i,
});
}
});
}
}
void GroupCall::applyLastSpoke(uint32 ssrc, crl::time when, crl::time now) {
void GroupCall::applyLastSpoke(
uint32 ssrc,
LastSpokeTimes when,
crl::time now) {
const auto i = _userBySsrc.find(ssrc);
if (i == end(_userBySsrc)) {
_unknownSpokenSsrcs.emplace(ssrc, when);
requestUnknownSsrcs();
_unknownSpokenSsrcs[ssrc] = when;
requestUnknownParticipants();
return;
}
const auto j = ranges::find(_participants, i->second, &Participant::user);
Assert(j != end(_participants));
const auto speaking = (when + kSpeakStatusKeptFor >= now)
_speakingByActiveFinishes.remove(j->user);
const auto sounding = (when.anything + kSoundStatusKeptFor >= now)
&& j->canSelfUnmute;
if (j->speaking != speaking) {
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;
_participantUpdates.fire({
.was = was,
@@ -317,15 +341,92 @@ void GroupCall::applyLastSpoke(uint32 ssrc, crl::time when, crl::time now) {
}
}
void GroupCall::requestUnknownSsrcs() {
if (_unknownSsrcsRequestId || _unknownSpokenSsrcs.empty()) {
void GroupCall::applyActiveUpdate(
UserId userId,
LastSpokeTimes when,
UserData *userLoaded) {
if (inCall()) {
return;
}
const auto i = userLoaded
? ranges::find(
_participants,
not_null{ userLoaded },
&Participant::user)
: _participants.end();
if (i == end(_participants)) {
_unknownSpokenUids[userId] = when;
requestUnknownParticipants();
return;
} else if (!i->canSelfUnmute) {
return;
}
const auto was = std::make_optional(*i);
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) {
return;
}
_speakingByActiveFinishes[i->user] = finishes;
if (!_speakingByActiveFinishTimer.isActive()) {
_speakingByActiveFinishTimer.callOnce(finishes - now);
}
i->lastActive = lastActive;
i->speaking = true;
i->canSelfUnmute = true;
if (!was->speaking || !was->canSelfUnmute) {
_participantUpdates.fire({
.was = was,
.now = *i,
});
}
}
void GroupCall::checkFinishSpeakingByActive() {
const auto now = crl::now();
auto nearest = 0;
auto stop = std::vector<not_null<UserData*>>();
for (auto i = begin(_speakingByActiveFinishes); i != end(_speakingByActiveFinishes);) {
const auto when = i->second;
if (now >= when) {
stop.push_back(i->first);
i = _speakingByActiveFinishes.erase(i);
} else {
if (!nearest || nearest > when) {
nearest = when;
}
++i;
}
}
for (const auto user : stop) {
const auto i = ranges::find(_participants, user, &Participant::user);
if (i->speaking) {
const auto was = *i;
i->speaking = false;
_participantUpdates.fire({
.was = was,
.now = *i,
});
}
}
if (nearest) {
_speakingByActiveFinishTimer.callOnce(nearest - now);
}
}
void GroupCall::requestUnknownParticipants() {
if (_unknownUsersRequestId
|| (_unknownSpokenSsrcs.empty() && _unknownSpokenUids.empty())) {
return;
}
const auto ssrcs = [&] {
if (_unknownSpokenSsrcs.size() < kRequestPerPage) {
return base::take(_unknownSpokenSsrcs);
}
auto result = base::flat_map<uint32, crl::time>();
auto result = base::flat_map<uint32, LastSpokeTimes>();
result.reserve(kRequestPerPage);
while (result.size() < kRequestPerPage) {
const auto [ssrc, when] = _unknownSpokenSsrcs.back();
@@ -334,53 +435,127 @@ void GroupCall::requestUnknownSsrcs() {
}
return result;
}();
auto inputs = QVector<MTPint>();
inputs.reserve(ssrcs.size());
const auto uids = [&] {
if (_unknownSpokenUids.size() + ssrcs.size() < kRequestPerPage) {
return base::take(_unknownSpokenUids);
}
auto result = base::flat_map<UserId, LastSpokeTimes>();
const auto available = (kRequestPerPage - int(ssrcs.size()));
if (available > 0) {
result.reserve(available);
while (result.size() < available) {
const auto [userId, when] = _unknownSpokenUids.back();
result.emplace(userId, when);
_unknownSpokenUids.erase(_unknownSpokenUids.end() - 1);
}
}
return result;
}();
auto ssrcInputs = QVector<MTPint>();
ssrcInputs.reserve(ssrcs.size());
for (const auto [ssrc, when] : ssrcs) {
inputs.push_back(MTP_int(ssrc));
ssrcInputs.push_back(MTP_int(ssrc));
}
_unknownSsrcsRequestId = api().request(MTPphone_GetGroupParticipants(
auto uidInputs = QVector<MTPint>();
uidInputs.reserve(uids.size());
for (const auto [userId, when] : uids) {
uidInputs.push_back(MTP_int(userId));
}
_unknownUsersRequestId = api().request(MTPphone_GetGroupParticipants(
input(),
MTP_vector<MTPint>(), // ids
MTP_vector<MTPint>(inputs),
MTP_vector<MTPint>(uidInputs),
MTP_vector<MTPint>(ssrcInputs),
MTP_string(QString()),
MTP_int(kRequestPerPage)
)).done([=](const MTPphone_GroupParticipants &result) {
result.match([&](const MTPDphone_groupParticipants &data) {
_channel->owner().processUsers(data.vusers());
_peer->owner().processUsers(data.vusers());
applyParticipantsSlice(
data.vparticipants().v,
ApplySliceSource::UnknownLoaded);
});
_unknownSsrcsRequestId = 0;
_unknownUsersRequestId = 0;
const auto now = crl::now();
for (const auto [ssrc, when] : ssrcs) {
applyLastSpoke(ssrc, when, now);
_unknownSpokenSsrcs.remove(ssrc);
}
requestUnknownSsrcs();
for (const auto [userId, when] : uids) {
if (const auto user = _peer->owner().userLoaded(userId)) {
const auto isParticipant = ranges::contains(
_participants,
not_null{ user },
&Participant::user);
if (isParticipant) {
applyActiveUpdate(userId, when, user);
}
}
_unknownSpokenUids.remove(userId);
}
requestUnknownParticipants();
}).fail([=](const RPCError &error) {
_unknownSsrcsRequestId = 0;
_unknownUsersRequestId = 0;
for (const auto [ssrc, when] : ssrcs) {
_unknownSpokenSsrcs.remove(ssrc);
}
requestUnknownSsrcs();
for (const auto [userId, when] : uids) {
_unknownSpokenUids.remove(userId);
}
requestUnknownParticipants();
}).send();
}
void GroupCall::setInCall() {
_unknownSpokenUids.clear();
if (_speakingByActiveFinishes.empty()) {
return;
}
auto restartTimer = true;
const auto latest = crl::now() + kActiveAfterJoined;
for (auto &[user, when] : _speakingByActiveFinishes) {
if (when > latest) {
when = latest;
} else {
restartTimer = false;
}
}
if (restartTimer) {
_speakingByActiveFinishTimer.callOnce(kActiveAfterJoined);
}
}
bool GroupCall::inCall() const {
const auto current = Core::App().calls().currentGroupCall();
return (current != nullptr)
&& (current->id() == _id)
&& (current->state() == Calls::GroupCall::State::Joined);
}
void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) {
const auto version = update.vversion().v;
if (version < _version) {
return;
} else if (version == _version) {
applyParticipantsMutes(update);
return;
} else if (version != _version + 1) {
applyParticipantsMutes(update);
reload();
const auto applyUpdate = [&] {
if (version < _version) {
return false;
}
auto versionShouldIncrement = false;
for (const auto &participant : update.vparticipants().v) {
const auto versioned = participant.match([&](
const MTPDgroupCallParticipant &data) {
return data.is_versioned();
});
if (versioned) {
versionShouldIncrement = true;
break;
}
}
return versionShouldIncrement
? (version == _version + 1)
: (version == _version);
}();
if (!applyUpdate) {
return;
}
_version = update.vversion().v;
_version = version;
applyUpdateChecked(update);
}
@@ -404,7 +579,7 @@ bool GroupCall::canChangeJoinMuted() const {
}
ApiWrap &GroupCall::api() const {
return _channel->session().api();
return _peer->session().api();
}
} // namespace Data

View File

@@ -7,27 +7,37 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
class UserData;
class ChannelData;
class PeerData;
class ApiWrap;
namespace Data {
struct LastSpokeTimes {
crl::time anything = 0;
crl::time voice = 0;
};
class GroupCall final {
public:
GroupCall(not_null<ChannelData*> channel, uint64 id, uint64 accessHash);
GroupCall(not_null<PeerData*> peer, uint64 id, uint64 accessHash);
~GroupCall();
[[nodiscard]] uint64 id() const;
[[nodiscard]] not_null<ChannelData*> channel() const;
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] MTPInputGroupCall input() const;
void setPeer(not_null<PeerData*> peer);
struct Participant {
not_null<UserData*> user;
TimeId date = 0;
TimeId lastActive = 0;
uint32 ssrc = 0;
bool sounding = false;
bool speaking = false;
bool muted = false;
bool canSelfUnmute = false;
@@ -37,7 +47,7 @@ public:
std::optional<Participant> now;
};
static constexpr auto kSpeakStatusKeptFor = crl::time(350);
static constexpr auto kSoundStatusKeptFor = crl::time(350);
[[nodiscard]] auto participants() const
-> const std::vector<Participant> &;
@@ -52,11 +62,16 @@ public:
void applyUpdate(const MTPDupdateGroupCallParticipants &update);
void applyUpdateChecked(
const MTPDupdateGroupCallParticipants &update);
void applyLastSpoke(uint32 ssrc, crl::time when, crl::time now);
void applyLastSpoke(uint32 ssrc, LastSpokeTimes when, crl::time now);
void applyActiveUpdate(
UserId userId,
LastSpokeTimes when,
UserData *userLoaded);
[[nodiscard]] int fullCount() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
void setInCall();
void reload();
void setJoinMutedLocally(bool muted);
@@ -71,30 +86,33 @@ private:
};
[[nodiscard]] ApiWrap &api() const;
[[nodiscard]] bool inCall() const;
void applyCall(const MTPGroupCall &call, bool force);
void applyParticipantsSlice(
const QVector<MTPGroupCallParticipant> &list,
ApplySliceSource sliceSource);
void applyParticipantsMutes(
const MTPDupdateGroupCallParticipants &update);
void requestUnknownSsrcs();
void changeChannelEmptyCallFlag();
void requestUnknownParticipants();
void changePeerEmptyCallFlag();
void checkFinishSpeakingByActive();
const not_null<ChannelData*> _channel;
const uint64 _id = 0;
const uint64 _accessHash = 0;
not_null<PeerData*> _peer;
int _version = 0;
mtpRequestId _participantsRequestId = 0;
mtpRequestId _reloadRequestId = 0;
std::vector<Participant> _participants;
base::flat_map<uint32, not_null<UserData*>> _userBySsrc;
base::flat_map<not_null<UserData*>, crl::time> _speakingByActiveFinishes;
base::Timer _speakingByActiveFinishTimer;
QString _nextOffset;
rpl::variable<int> _fullCount = 0;
base::flat_map<uint32, crl::time> _unknownSpokenSsrcs;
mtpRequestId _unknownSsrcsRequestId = 0;
base::flat_map<uint32, LastSpokeTimes> _unknownSpokenSsrcs;
base::flat_map<UserId, LastSpokeTimes> _unknownSpokenUids;
mtpRequestId _unknownUsersRequestId = 0;
rpl::event_stream<ParticipantUpdate> _participantUpdates;
rpl::event_stream<> _participantsSliceAdded;

View File

@@ -888,6 +888,26 @@ bool PeerData::canSendPolls() const {
return false;
}
bool PeerData::canManageGroupCall() const {
if (const auto chat = asChat()) {
return chat->amCreator()
|| (chat->adminRights() & ChatAdminRight::f_manage_call);
} else if (const auto group = asMegagroup()) {
return group->amCreator()
|| (group->adminRights() & ChatAdminRight::f_manage_call);
}
return false;
}
Data::GroupCall *PeerData::groupCall() const {
if (const auto chat = asChat()) {
return chat->groupCall();
} else if (const auto group = asMegagroup()) {
return group->groupCall();
}
return nullptr;
}
void PeerData::setIsBlocked(bool is) {
const auto status = is
? BlockStatus::Blocked

View File

@@ -29,6 +29,7 @@ class Session;
namespace Data {
class Session;
class GroupCall;
int PeerColorIndex(PeerId peerId);
int PeerColorIndex(int32 bareId);
@@ -202,6 +203,7 @@ public:
[[nodiscard]] rpl::producer<bool> slowmodeAppliedValue() const;
[[nodiscard]] int slowmodeSecondsLeft() const;
[[nodiscard]] bool canSendPolls() const;
[[nodiscard]] bool canManageGroupCall() const;
[[nodiscard]] UserData *asUser();
[[nodiscard]] const UserData *asUser() const;
@@ -383,6 +385,8 @@ public:
}
void setLoadedStatus(LoadedStatus status);
[[nodiscard]] Data::GroupCall *groupCall() const;
const PeerId id;
QString name;
MTPinputPeer input = MTP_inputPeerEmpty();

View File

@@ -604,7 +604,12 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
});
}
chat->setFlags(data.vflags().v);
const auto callFlag = MTPDchat::Flag::f_call_not_empty;
const auto callNotEmpty = (data.vflags().v & callFlag)
|| (chat->groupCall()
&& chat->groupCall()->fullCount() > 0);
chat->setFlags(data.vflags().v
| (callNotEmpty ? callFlag : MTPDchat::Flag(0)));
chat->count = data.vparticipants_count().v;
if (canAddMembers != chat->canAddMembers()) {
@@ -651,13 +656,21 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
channel->setDefaultRestrictions(
MTP_chatBannedRights(MTP_flags(0), MTP_int(0)));
}
const auto callFlag = MTPDchannel::Flag::f_call_not_empty;
const auto callNotEmpty = (data.vflags().v & callFlag)
|| (channel->groupCall()
&& channel->groupCall()->fullCount() > 0);
if (minimal) {
auto mask = 0
| MTPDchannel::Flag::f_broadcast
| MTPDchannel::Flag::f_verified
| MTPDchannel::Flag::f_megagroup
| MTPDchannel::Flag::f_call_active
| MTPDchannel::Flag::f_call_not_empty
| MTPDchannel_ClientFlag::f_forbidden;
channel->setFlags((channel->flags() & ~mask) | (data.vflags().v & mask));
channel->setFlags((channel->flags() & ~mask)
| (data.vflags().v & mask)
| (callNotEmpty ? callFlag : MTPDchannel::Flag(0)));
if (channel->input.type() == mtpc_inputPeerEmpty
|| channel->inputChannel.type() == mtpc_inputChannelEmpty) {
channel->setAccessHash(data.vaccess_hash().value_or_empty());
@@ -686,9 +699,6 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
} else {
channel->setUnavailableReasons({});
}
const auto callFlag = MTPDchannel::Flag::f_call_not_empty;
const auto callNotEmpty = (data.vflags().v & callFlag)
|| (channel->call() && channel->call()->fullCount() > 0);
channel->setFlags(data.vflags().v
| (callNotEmpty ? callFlag : MTPDchannel::Flag(0)));
//if (const auto feedId = data.vfeed_id()) { // #feed
@@ -804,14 +814,6 @@ void Session::unregisterGroupCall(not_null<GroupCall*> call) {
_groupCalls.remove(call->id());
}
rpl::producer<Session::GroupCallDiscard> Session::groupCallDiscards() const {
return _groupCallDiscarded.events();
}
void Session::groupCallDiscarded(uint64 id, int duration) {
_groupCallDiscarded.fire({ id, duration });
}
GroupCall *Session::groupCall(uint64 callId) const {
const auto i = _groupCalls.find(callId);
return (i != end(_groupCalls)) ? i->second.get() : nullptr;
@@ -826,9 +828,9 @@ auto Session::invitedToCallUsers(uint64 callId) const
void Session::registerInvitedToCallUser(
uint64 callId,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
not_null<UserData*> user) {
const auto call = channel->call();
const auto call = peer->groupCall();
if (call && call->id() == callId) {
const auto inCall = ranges::contains(
call->participants(),
@@ -839,6 +841,7 @@ void Session::registerInvitedToCallUser(
}
}
_invitedToCallUsers[callId].emplace(user);
_invitesToCalls.fire({ callId, user });
}
void Session::unregisterInvitedToCallUser(
@@ -1164,6 +1167,7 @@ void Session::setupMigrationViewer() {
return;
}
chat->clearGroupCall();
if (const auto from = historyLoaded(chat)) {
if (const auto to = historyLoaded(channel)) {
if (to->inChatList() && from->inChatList()) {

View File

@@ -158,21 +158,22 @@ public:
void unregisterGroupCall(not_null<GroupCall*> call);
GroupCall *groupCall(uint64 callId) const;
struct GroupCallDiscard {
uint64 id = 0;
int duration = 0;
};
rpl::producer<GroupCallDiscard> groupCallDiscards() const;
void groupCallDiscarded(uint64 id, int duration);
[[nodiscard]] auto invitedToCallUsers(uint64 callId) const
-> const base::flat_set<not_null<UserData*>> &;
void registerInvitedToCallUser(
uint64 callId,
not_null<ChannelData*> channel,
not_null<PeerData*> peer,
not_null<UserData*> user);
void unregisterInvitedToCallUser(uint64 callId, not_null<UserData*> user);
struct InviteToCall {
uint64 id = 0;
not_null<UserData*> user;
};
[[nodiscard]] rpl::producer<InviteToCall> invitesToCalls() const {
return _invitesToCalls.events();
}
void enumerateUsers(Fn<void(not_null<UserData*>)> action) const;
void enumerateGroups(Fn<void(not_null<PeerData*>)> action) const;
void enumerateChannels(Fn<void(not_null<ChannelData*>)> action) const;
@@ -930,8 +931,8 @@ private:
base::flat_set<not_null<ViewElement*>> _heavyViewParts;
base::flat_map<uint64, not_null<GroupCall*>> _groupCalls;
rpl::event_stream<InviteToCall> _invitesToCalls;
base::flat_map<uint64, base::flat_set<not_null<UserData*>>> _invitedToCallUsers;
rpl::event_stream<GroupCallDiscard> _groupCallDiscarded;
History *_topPromoted = nullptr;

View File

@@ -56,7 +56,7 @@ void SharedMediaShowOverview(
return;
}
}
windows.front()->showSection(Info::Memento(
windows.front()->showSection(std::make_shared<Info::Memento>(
history->peer,
Info::Section(type)));
}

View File

@@ -321,7 +321,7 @@ bool Stickers::applyArchivedResultFake() {
MTP_long(raw->access),
MTP_string(raw->title),
MTP_string(raw->shortName),
MTP_photoSizeEmpty(MTP_string()),
MTP_vector<MTPPhotoSize>(),
MTP_int(0),
MTP_int(raw->count),
MTP_int(raw->hash));
@@ -814,10 +814,20 @@ void Stickers::featuredSetsReceived(
auto it = sets.find(data->vid().v);
const auto title = getSetTitle(*data);
const auto installDate = data->vinstalled_date().value_or_empty();
const auto thumb = data->vthumb();
const auto thumbnail = thumb
? Images::FromPhotoSize(&session(), *data, *thumb)
: ImageWithLocation();
const auto thumbnail = [&] {
if (const auto thumbs = data->vthumbs()) {
for (const auto &thumb : thumbs->v) {
const auto result = Images::FromPhotoSize(
&session(),
*data,
thumb);
if (result.location.valid()) {
return result;
}
}
}
return ImageWithLocation();
}();
if (it == sets.cend()) {
auto setClientFlags = MTPDstickerSet_ClientFlag::f_featured
| MTPDstickerSet_ClientFlag::f_not_loaded;
@@ -1126,10 +1136,20 @@ StickersSet *Stickers::feedSet(const MTPDstickerSet &data) {
auto it = sets.find(data.vid().v);
auto title = getSetTitle(data);
auto flags = MTPDstickerSet::Flags(0);
const auto thumb = data.vthumb();
const auto thumbnail = thumb
? Images::FromPhotoSize(&session(), data, *thumb)
: ImageWithLocation();
const auto thumbnail = [&] {
if (const auto thumbs = data.vthumbs()) {
for (const auto &thumb : thumbs->v) {
const auto result = Images::FromPhotoSize(
&session(),
data,
thumb);
if (result.location.valid()) {
return result;
}
}
}
return ImageWithLocation();
}();
if (it == sets.cend()) {
it = sets.emplace(data.vid().v, std::make_unique<StickersSet>(
&owner(),

View File

@@ -1776,7 +1776,7 @@ bool Widget::onCancelSearch() {
if (const auto peer = _searchInChat.peer()) {
Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
//} else if (const auto feed = _searchInChat.feed()) { // #feed
// controller()->showSection(HistoryFeed::Memento(feed));
// controller()->showSection(std::make_shared<HistoryFeed::Memento>(feed));
} else {
Unexpected("Empty key in onCancelSearch().");
}
@@ -1800,7 +1800,7 @@ void Widget::onCancelSearchInChat() {
if (const auto peer = _searchInChat.peer()) {
Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
//} else if (const auto feed = _searchInChat.feed()) { // #feed
// controller()->showSection(HistoryFeed::Memento(feed));
// controller()->showSection(std::make_shared<HistoryFeed::Memento>(feed));
} else {
Unexpected("Empty key in onCancelSearchInPeer().");
}

View File

@@ -21,7 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/message_field.h"
#include "boxes/sticker_set_box.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "core/application.h"
@@ -1220,9 +1219,7 @@ void InnerWidget::copyContextImage(not_null<PhotoData*> photo) {
}
const auto image = media->image(Data::PhotoSize::Large)->original();
if (!Platform::SetClipboardImage(image)) {
QGuiApplication::clipboard()->setImage(image);
}
QGuiApplication::clipboard()->setImage(image);
}
void InnerWidget::copySelectedText() {

View File

@@ -371,8 +371,8 @@ void Widget::setupShortcuts() {
}, lifetime());
}
std::unique_ptr<Window::SectionMemento> Widget::createMemento() {
auto result = std::make_unique<SectionMemento>(channel());
std::shared_ptr<Window::SectionMemento> Widget::createMemento() {
auto result = std::make_shared<SectionMemento>(channel());
saveState(result.get());
return result;
}

View File

@@ -59,7 +59,7 @@ public:
bool showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
std::unique_ptr<Window::SectionMemento> createMemento() override;
std::shared_ptr<Window::SectionMemento> createMemento() override;
void setInternalState(const QRect &geometry, not_null<SectionMemento*> memento);

View File

@@ -1053,9 +1053,11 @@ void History::applyServiceChanges(
} break;
case mtpc_messageActionGroupCall: {
const auto &d = action.c_messageActionGroupCall();
if (const auto channel = peer->asChannel()) {
const auto &d = action.c_messageActionGroupCall();
channel->setCall(d.vcall());
channel->setGroupCall(d.vcall());
} else if (const auto chat = peer->asChat()) {
chat->setGroupCall(d.vcall());
}
} break;
}

View File

@@ -38,7 +38,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/sticker_set_box.h"
#include "chat_helpers/message_field.h"
#include "history/history_widget.h"
#include "platform/platform_specific.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "mainwindow.h"
@@ -1648,7 +1647,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
addDocumentActions(lnkDocument->document());
}
if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
HistoryView::CopyPostLink(session, itemId, HistoryView::Context::History);
});
}
@@ -1789,7 +1788,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
QGuiApplication::clipboard()->setText(text);
});
} else if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
HistoryView::CopyPostLink(session, itemId, HistoryView::Context::History);
});
}
@@ -1900,9 +1899,7 @@ void HistoryInner::copyContextImage(not_null<PhotoData*> photo) {
}
const auto image = media->image(Data::PhotoSize::Large)->original();
if (!Platform::SetClipboardImage(image)) {
QGuiApplication::clipboard()->setImage(image);
}
QGuiApplication::clipboard()->setImage(image);
}
void HistoryInner::showStickerPackInfo(not_null<DocumentData*> document) {

View File

@@ -776,7 +776,8 @@ bool HistoryItem::canBeEditedFromHistory() const {
}
if ((IsServerMsgId(id) || isScheduled())
&& !serviceMsg()
&& (out() || history()->peer->isSelf())) {
&& (out() || history()->peer->isSelf())
&& !Has<HistoryMessageForwarded>()) {
return true;
}
return false;

View File

@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_game.h"
#include "data/data_channel.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_changes.h"
#include "data/data_group_call.h" // Data::GroupCall::id().
#include "core/application.h"
@@ -38,18 +39,27 @@ namespace {
constexpr auto kPinnedMessageTextLimit = 16;
[[nodiscard]] rpl::producer<bool> ChannelHasThisCallValue(
not_null<ChannelData*> channel,
[[nodiscard]] bool PeerCallKnown(not_null<PeerData*> peer) {
if (peer->groupCall() != nullptr) {
return true;
} else if (const auto chat = peer->asChat()) {
return !(chat->flags() & MTPDchat::Flag::f_call_active);
} else if (const auto channel = peer->asChannel()) {
return !(channel->flags() & MTPDchannel::Flag::f_call_active);
}
return true;
}
[[nodiscard]] rpl::producer<bool> PeerHasThisCallValue(
not_null<PeerData*> peer,
uint64 id) {
return channel->session().changes().peerFlagsValue(
channel,
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::GroupCall
) | rpl::filter([=] {
return (channel->call() != nullptr)
|| !(channel->flags()
& MTPDchannel::Flag::f_call_active);
return PeerCallKnown(peer);
}) | rpl::map([=] {
const auto call = channel->call();
const auto call = peer->groupCall();
return (call && call->id() == id);
}) | rpl::distinct_until_changed(
) | rpl::take_while([=](bool hasThisCall) {
@@ -59,15 +69,15 @@ constexpr auto kPinnedMessageTextLimit = 16;
);
}
[[nodiscard]] std::optional<bool> ChannelHasThisCall(
not_null<ChannelData*> channel,
[[nodiscard]] std::optional<bool> PeerHasThisCall(
not_null<PeerData*> peer,
uint64 id) {
const auto call = channel->call();
const auto call = peer->groupCall();
return call
? std::make_optional(call->id() == id)
: (channel->flags() & MTPDchannel::Flag::f_call_active)
? std::nullopt
: std::make_optional(false);
: PeerCallKnown(peer)
? std::make_optional(false)
: std::nullopt;
}
[[nodiscard]] uint64 CallIdFromInput(const MTPInputGroupCall &data) {
@@ -76,20 +86,20 @@ constexpr auto kPinnedMessageTextLimit = 16;
});
}
[[nodiscard]] ClickHandlerPtr ChannelCallClickHandler(
not_null<ChannelData*> megagroup,
[[nodiscard]] ClickHandlerPtr GroupCallClickHandler(
not_null<PeerData*> peer,
uint64 callId) {
return std::make_shared<LambdaClickHandler>([=] {
const auto call = megagroup->call();
const auto call = peer->groupCall();
if (call && call->id() == callId) {
const auto &windows = megagroup->session().windows();
const auto &windows = peer->session().windows();
if (windows.empty()) {
Core::App().domain().activate(&megagroup->session().account());
Core::App().domain().activate(&peer->session().account());
if (windows.empty()) {
return;
}
}
windows.front()->startOrJoinGroupCall(megagroup);
windows.front()->startOrJoinGroupCall(peer);
}
});
}
@@ -338,10 +348,8 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
return prepareDiscardedCallText(duration->v);
}
const auto callId = CallIdFromInput(action.vcall());
const auto channel = history()->peer->asChannel();
const auto linkCallId = !channel
? 0
: ChannelHasThisCall(channel, callId).value_or(false)
const auto peer = history()->peer;
const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false)
? callId
: 0;
return prepareStartedCallText(linkCallId);
@@ -350,16 +358,14 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
auto prepareInviteToGroupCall = [this](const MTPDmessageActionInviteToGroupCall &action) {
const auto callId = CallIdFromInput(action.vcall());
const auto owner = &history()->owner();
const auto channel = history()->peer->asChannel();
const auto peer = history()->peer;
for (const auto id : action.vusers().v) {
const auto user = owner->user(id.v);
if (channel && callId) {
owner->registerInvitedToCallUser(callId, channel, user);
if (callId) {
owner->registerInvitedToCallUser(callId, peer, user);
}
};
const auto linkCallId = !channel
? 0
: ChannelHasThisCall(channel, callId).value_or(false)
const auto linkCallId = PeerHasThisCall(peer, callId).value_or(false)
? callId
: 0;
return prepareInvitedToCallText(action.vusers().v, linkCallId);
@@ -534,10 +540,10 @@ HistoryService::PreparedText HistoryService::prepareStartedCallText(
uint64 linkCallId) {
auto result = PreparedText{};
result.links.push_back(fromLink());
const auto channel = history()->peer->asChannel();
auto chatText = tr::lng_action_group_call_started_chat(tr::now);
if (channel && linkCallId) {
result.links.push_back(ChannelCallClickHandler(channel, linkCallId));
if (linkCallId) {
const auto peer = history()->peer;
result.links.push_back(GroupCallClickHandler(peer, linkCallId));
chatText = textcmdLink(2, chatText);
}
result.text = tr::lng_action_group_call_started(
@@ -552,14 +558,14 @@ HistoryService::PreparedText HistoryService::prepareStartedCallText(
HistoryService::PreparedText HistoryService::prepareInvitedToCallText(
const QVector<MTPint> &users,
uint64 linkCallId) {
const auto channel = history()->peer->asChannel();
const auto owner = &channel->owner();
const auto owner = &history()->owner();
auto chatText = tr::lng_action_invite_user_chat(tr::now);
auto result = PreparedText{};
result.links.push_back(fromLink());
auto linkIndex = 1;
if (channel && linkCallId) {
result.links.push_back(ChannelCallClickHandler(channel, linkCallId));
if (linkCallId) {
const auto peer = history()->peer;
result.links.push_back(GroupCallClickHandler(peer, linkCallId));
chatText = textcmdLink(++linkIndex, chatText);
}
if (users.size() == 1) {
@@ -929,47 +935,35 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) {
const auto id = CallIdFromInput(data.vcall());
call->lifetime.destroy();
history()->owner().groupCallDiscards(
) | rpl::filter([=](Data::Session::GroupCallDiscard discard) {
return (discard.id == id);
}) | rpl::start_with_next([=](
Data::Session::GroupCallDiscard discard) {
RemoveComponents(HistoryServiceOngoingCall::Bit());
updateText(prepareDiscardedCallText(discard.duration));
}, call->lifetime);
if (const auto channel = history()->peer->asChannel()) {
const auto has = ChannelHasThisCall(channel, id);
if (!has.has_value()) {
ChannelHasThisCallValue(
channel,
id
) | rpl::start_with_next([=](bool has) {
updateText(prepareStartedCallText(has ? id : 0));
}, call->lifetime);
} else if (*has) {
ChannelHasThisCallValue(
channel,
id
) | rpl::skip(1) | rpl::start_with_next([=](bool has) {
Assert(!has);
updateText(prepareStartedCallText(0));
}, call->lifetime);
}
const auto peer = history()->peer;
const auto has = PeerHasThisCall(peer, id);
if (!has.has_value()) {
PeerHasThisCallValue(
peer,
id
) | rpl::start_with_next([=](bool has) {
updateText(prepareStartedCallText(has ? id : 0));
}, call->lifetime);
} else if (*has) {
PeerHasThisCallValue(
peer,
id
) | rpl::skip(1) | rpl::start_with_next([=](bool has) {
Assert(!has);
updateText(prepareStartedCallText(0));
}, call->lifetime);
}
}
} else if (message.vaction().type() == mtpc_messageActionInviteToGroupCall) {
const auto &data = message.vaction().c_messageActionInviteToGroupCall();
const auto id = CallIdFromInput(data.vcall());
const auto channel = history()->peer->asChannel();
const auto has = channel
? ChannelHasThisCall(channel, id)
: std::make_optional(false);
const auto peer = history()->peer;
const auto has = PeerHasThisCall(peer, id);
auto hasLink = !has.has_value()
? ChannelHasThisCallValue(channel, id)
? PeerHasThisCallValue(peer, id)
: (*has)
? ChannelHasThisCallValue(
channel,
? PeerHasThisCallValue(
peer,
id) | rpl::skip(1) | rpl::type_erased()
: rpl::producer<bool>();
if (!hasLink) {

View File

@@ -687,7 +687,7 @@ HistoryWidget::HistoryWidget(
cancelReply(lastKeyboardUsed);
crl::on_main(this, [=, history = action.history]{
controller->showSection(
HistoryView::ScheduledMemento(history));
std::make_shared<HistoryView::ScheduledMemento>(history));
});
} else {
fastShowAtEnd(action.history);
@@ -757,9 +757,6 @@ void HistoryWidget::initVoiceRecordBar() {
});
_voiceRecordBar->setLockBottom(std::move(scrollHeight));
}
_voiceRecordBar->setEscFilter([=]() -> bool {
return _replyToId || (_nonEmptySelection && _list);
});
_voiceRecordBar->setSendButtonGeometryValue(_send->geometryValue());
@@ -776,6 +773,12 @@ void HistoryWidget::initVoiceRecordBar() {
return false;
});
const auto applyLocalDraft = [=] {
if (_history && _history->localDraft()) {
applyDraft();
}
};
_voiceRecordBar->sendActionUpdates(
) | rpl::start_with_next([=](const auto &data) {
if (!_history) {
@@ -802,8 +805,12 @@ void HistoryWidget::initVoiceRecordBar() {
data.duration,
action);
_voiceRecordBar->clearListenState();
applyLocalDraft();
}, lifetime());
_voiceRecordBar->cancelRequests(
) | rpl::start_with_next(applyLocalDraft, lifetime());
_voiceRecordBar->lockShowStarts(
) | rpl::start_with_next([=] {
updateHistoryDownVisibility();
@@ -1442,7 +1449,7 @@ void HistoryWidget::activate() {
updateHistoryGeometry();
}
}
if (App::wnd()) App::wnd()->setInnerFocus();
controller()->widget()->setInnerFocus();
}
void HistoryWidget::setInnerFocus() {
@@ -1498,7 +1505,7 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query, U
} else if (to.section == Section::Scheduled) {
history->setDraft(Data::DraftKey::Scheduled(), std::move(draft));
controller()->showSection(
HistoryView::ScheduledMemento(history));
std::make_shared<HistoryView::ScheduledMemento>(history));
} else {
history->setLocalDraft(std::move(draft));
if (history == _history) {
@@ -1616,6 +1623,10 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) {
void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
InvokeQueued(this, [=] { updateStickersByEmoji(); });
if (_voiceRecordBar->isActive()) {
return;
}
auto draft = !_history
? nullptr
: _history->localEditDraft()
@@ -2065,7 +2076,7 @@ void HistoryWidget::refreshScheduledToggle() {
_scheduled->show();
_scheduled->addClickHandler([=] {
controller()->showSection(
HistoryView::ScheduledMemento(_history));
std::make_shared<HistoryView::ScheduledMemento>(_history));
});
orderWidgets(); // Raise drag areas to the top.
} else if (_scheduled && !has) {
@@ -3091,7 +3102,7 @@ void HistoryWidget::send(Api::SendOptions options) {
return;
}
if (_voiceRecordBar && _voiceRecordBar->isListenState()) {
if (_voiceRecordBar->isListenState()) {
_voiceRecordBar->requestToSendWithOptions(options);
return;
}
@@ -3336,7 +3347,7 @@ void HistoryWidget::doneShow() {
_groupCallBar->finishAnimating();
}
checkHistoryActivation();
App::wnd()->setInnerFocus();
controller()->widget()->setInnerFocus();
_preserveScrollTop = false;
}
@@ -3875,7 +3886,7 @@ bool HistoryWidget::pushTabbedSelectorToThirdSection(
Core::App().settings().setTabbedReplacedWithInfo(false);
controller()->resizeForThirdSection();
controller()->showSection(
ChatHelpers::TabbedMemento(),
std::make_shared<ChatHelpers::TabbedMemento>(),
params.withThirdColumn());
return true;
}
@@ -3906,6 +3917,14 @@ void HistoryWidget::setTabbedPanel(std::unique_ptr<TabbedPanel> panel) {
}
}
bool HistoryWidget::preventsClose(Fn<void()> &&continueCallback) const {
if (_voiceRecordBar->isActive()) {
_voiceRecordBar->showDiscardBox(std::move(continueCallback));
return true;
}
return false;
}
void HistoryWidget::toggleTabbedSelectorMode() {
if (!_peer) {
return;
@@ -4321,7 +4340,7 @@ bool HistoryWidget::confirmSendingFiles(
}
if (hasImage) {
auto image = Platform::GetClipboardImage();
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}
@@ -5374,7 +5393,7 @@ void HistoryWidget::refreshPinnedBarButton(bool many) {
const auto id = _pinnedTracker->currentMessageId();
if (id.message) {
controller()->showSection(
HistoryView::PinnedMemento(
std::make_shared<HistoryView::PinnedMemento>(
_history,
((!_migrated || id.message.channel)
? id.message.msg
@@ -5388,17 +5407,18 @@ void HistoryWidget::refreshPinnedBarButton(bool many) {
void HistoryWidget::setupGroupCallTracker() {
Expects(_history != nullptr);
const auto channel = _history->peer->asChannel();
if (!channel) {
const auto peer = _history->peer;
if (!peer->asMegagroup() && !peer->asChat()) {
_groupCallTracker = nullptr;
_groupCallBar = nullptr;
return;
}
_groupCallTracker = std::make_unique<HistoryView::GroupCallTracker>(
channel);
peer);
_groupCallBar = std::make_unique<Ui::GroupCallBar>(
this,
_groupCallTracker->content());
_groupCallTracker->content(),
Core::App().appDeactivatedValue());
rpl::single(
rpl::empty_value()
@@ -5419,16 +5439,15 @@ void HistoryWidget::setupGroupCallTracker() {
_groupCallBar->barClicks(),
_groupCallBar->joinClicks()
) | rpl::start_with_next([=] {
const auto channel = _history->peer->asChannel();
if (!channel) {
return;
} else if (channel->amAnonymous()) {
const auto peer = _history->peer;
const auto channel = peer->asChannel();
if (channel && channel->amAnonymous()) {
Ui::ShowMultilineToast({
.text = tr::lng_group_call_no_anonymous(tr::now),
});
return;
} else if (channel->call()) {
controller()->startOrJoinGroupCall(channel);
} else if (peer->groupCall()) {
controller()->startOrJoinGroupCall(peer);
}
}, _groupCallBar->lifetime());
@@ -5633,7 +5652,7 @@ void HistoryWidget::editMessage(FullMsgId itemId) {
}
void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
if (_voiceRecordBar && _voiceRecordBar->isListenState()) {
if (_voiceRecordBar->isListenState()) {
Ui::show(Box<InformBox>(tr::lng_edit_caption_voice(tr::now)));
return;
}
@@ -6107,6 +6126,8 @@ void HistoryWidget::escape() {
_fieldAutocomplete->hideAnimated();
} else if (_replyToId && _field->getTextWithTags().text.isEmpty()) {
cancelReply();
} else if (auto &voice = _voiceRecordBar; voice->isActive()) {
voice->showDiscardBox(nullptr, anim::type::normal);
} else {
_cancelRequests.fire({});
}

View File

@@ -119,6 +119,8 @@ public:
void historyLoaded();
[[nodiscard]] bool preventsClose(Fn<void()> &&continueCallback) const;
// When resizing the widget with top edge moved up or down and we
// want to add this top movement to the scroll position, so inner
// content will not move.

View File

@@ -796,6 +796,9 @@ void ComposeControls::showStarted() {
if (_voiceRecordBar) {
_voiceRecordBar->hideFast();
}
if (_autocomplete) {
_autocomplete->hideFast();
}
_wrap->hide();
_writeRestricted->hide();
}
@@ -810,6 +813,9 @@ void ComposeControls::showFinished() {
if (_voiceRecordBar) {
_voiceRecordBar->hideFast();
}
if (_autocomplete) {
_autocomplete->hideFast();
}
updateWrappingVisibility();
_voiceRecordBar->orderControls();
}
@@ -1154,6 +1160,8 @@ void ComposeControls::initAutocomplete() {
_autocomplete.get(),
[=] { checkAutocomplete(); },
Qt::QueuedConnection);
_autocomplete->hideFast();
}
void ComposeControls::updateStickersByEmoji() {
@@ -1567,10 +1575,6 @@ void ComposeControls::initVoiceRecordBar() {
_voiceRecordBar->setLockBottom(std::move(bottom));
}
_voiceRecordBar->setEscFilter([=] {
return (isEditingMessage() || replyingToMessage());
});
_voiceRecordBar->updateSendButtonTypeRequests(
) | rpl::start_with_next([=] {
updateSendButtonType();
@@ -1639,7 +1643,15 @@ void ComposeControls::updateControlsGeometry(QSize size) {
- _send->width()
- _tabbedSelectorToggle->width()
- (_botCommandShown ? _botCommandStart->width() : 0);
_field->resizeToWidth(fieldWidth);
{
const auto oldFieldHeight = _field->height();
_field->resizeToWidth(fieldWidth);
// If a height of the field is changed
// then this method will be called with the updated size.
if (oldFieldHeight != _field->height()) {
return;
}
}
const auto buttonsTop = size.height() - _attachToggle->height();
@@ -1708,7 +1720,11 @@ void ComposeControls::paintBackground(QRect clip) {
}
void ComposeControls::escape() {
_cancelRequests.fire({});
if (const auto voice = _voiceRecordBar.get(); voice->isActive()) {
voice->showDiscardBox(nullptr, anim::type::normal);
} else {
_cancelRequests.fire({});
}
}
bool ComposeControls::pushTabbedSelectorToThirdSection(
@@ -1728,7 +1744,7 @@ bool ComposeControls::pushTabbedSelectorToThirdSection(
&st::historyRecordVoiceRippleBgActive);
_window->resizeForThirdSection();
_window->showSection(
ChatHelpers::TabbedMemento(),
std::make_shared<ChatHelpers::TabbedMemento>(),
params.withThirdColumn());
return true;
}
@@ -2130,6 +2146,14 @@ bool ComposeControls::isRecording() const {
return _voiceRecordBar->isRecording();
}
bool ComposeControls::preventsClose(Fn<void()> &&continueCallback) const {
if (_voiceRecordBar->isActive()) {
_voiceRecordBar->showDiscardBox(std::move(continueCallback));
return true;
}
return false;
}
void ComposeControls::updateInlineBotQuery() {
if (!_history) {
return;

View File

@@ -135,6 +135,8 @@ public:
[[nodiscard]] bool isEditingMessage() const;
[[nodiscard]] FullMsgId replyingToMessage() const;
[[nodiscard]] bool preventsClose(Fn<void()> &&continueCallback) const;
void showForGrab();
void showStarted();
void showFinished();

View File

@@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h"
#include "styles/style_media_player.h"
#include "ui/controls/send_button.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "window/window_session_controller.h"
@@ -61,10 +62,6 @@ enum class FilterType {
Cancel,
};
inline float64 InterpolateF(int a, int b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
}
[[nodiscard]] auto InactiveColor(const QColor &c) {
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
}
@@ -614,11 +611,14 @@ public:
RecordLock(not_null<Ui::RpWidget*> parent);
void requestPaintProgress(float64 progress);
void requestPaintLockToStopProgress(float64 progress);
[[nodiscard]] rpl::producer<> locks() const;
[[nodiscard]] bool isLocked() const;
[[nodiscard]] bool isStopState() const;
[[nodiscard]] float64 lockToStopProgress() const;
protected:
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
@@ -633,9 +633,9 @@ private:
const QRect _rippleRect;
const QPen _arcPen;
Ui::Animations::Simple _lockAnimation;
Ui::Animations::Simple _lockEnderAnimation;
float64 _lockToStopProgress = 0.;
rpl::variable<float64> _progress = 0.;
};
@@ -665,8 +665,8 @@ void RecordLock::init() {
if (!shown) {
setCursor(style::cur_default);
setAttribute(Qt::WA_TransparentForMouseEvents, true);
_lockAnimation.stop();
_lockEnderAnimation.stop();
_lockToStopProgress = 0.;
_progress = 0.;
}
}, lifetime());
@@ -678,32 +678,13 @@ void RecordLock::init() {
const auto top = anim::interpolate(
0,
height() - st::historyRecordLockTopShadow.height() * 2,
_lockAnimation.value(1.));
_lockToStopProgress);
p.translate(0, top);
drawProgress(p);
return;
}
drawProgress(p);
}, lifetime());
locks(
) | rpl::start_with_next([=] {
const auto &duration = st::historyRecordVoiceShowDuration;
const auto from = 0.;
const auto to = 1.;
auto callback = [=](float64 value) {
update();
if (value == to) {
setCursor(style::cur_pointer);
setAttribute(Qt::WA_TransparentForMouseEvents, false);
resize(
st::historyRecordLockTopShadow.width(),
st::historyRecordLockTopShadow.width());
}
};
_lockAnimation.start(std::move(callback), from, to, duration);
}, lifetime());
}
void RecordLock::drawProgress(Painter &p) {
@@ -776,8 +757,6 @@ void RecordLock::drawProgress(Painter &p) {
}
{
PainterHighQualityEnabler hq(p);
const auto lockToStopProgress =
_lockAnimation.value(isLocked() ? 1. : 0);
const auto &arcOffset = st::historyRecordLockIconLineSkip;
const auto &size = st::historyRecordLockIconSize;
@@ -786,18 +765,18 @@ void RecordLock::drawProgress(Painter &p) {
const auto &blockHeight = st::historyRecordLockIconBottomHeight;
const auto blockRectWidth = InterpolateF(
const auto blockRectWidth = anim::interpolateF(
size.width(),
st::historyRecordStopIconWidth,
lockToStopProgress);
const auto blockRectHeight = InterpolateF(
_lockToStopProgress);
const auto blockRectHeight = anim::interpolateF(
blockHeight,
st::historyRecordStopIconWidth,
lockToStopProgress);
const auto blockRectTop = InterpolateF(
_lockToStopProgress);
const auto blockRectTop = anim::interpolateF(
size.height() - blockHeight,
std::round((size.height() - blockRectHeight) / 2.),
lockToStopProgress);
_lockToStopProgress);
const auto blockRect = QRectF(
(size.width() - blockRectWidth) / 2,
@@ -812,11 +791,11 @@ void RecordLock::drawProgress(Painter &p) {
inner.x() + (inner.width() - size.width()) / 2,
inner.y() + (originTop.height() * 2 - size.height()) / 2);
{
const auto xRadius = anim::interpolate(2, 3, lockToStopProgress);
const auto xRadius = anim::interpolate(2, 3, _lockToStopProgress);
p.drawRoundedRect(blockRect, xRadius, 3);
}
const auto offsetTranslate = lockToStopProgress *
const auto offsetTranslate = _lockToStopProgress *
(lineHeight + arcHeight + _arcPen.width() * 2);
p.translate(
size.width() - arcOffset,
@@ -838,7 +817,7 @@ void RecordLock::drawProgress(Painter &p) {
0,
180 * 16);
const auto lockProgress = 1. - _lockAnimation.value(1.);
const auto lockProgress = 1. - _lockToStopProgress;
if (progress == 1. && lockProgress < 1.) {
p.drawLine(
-arcWidth,
@@ -869,6 +848,23 @@ void RecordLock::requestPaintProgress(float64 progress) {
setProgress(progress);
}
void RecordLock::requestPaintLockToStopProgress(float64 progress) {
_lockToStopProgress = progress;
if (isStopState()) {
setCursor(style::cur_pointer);
setAttribute(Qt::WA_TransparentForMouseEvents, false);
resize(
st::historyRecordLockTopShadow.width(),
st::historyRecordLockTopShadow.width());
}
update();
}
float64 RecordLock::lockToStopProgress() const {
return _lockToStopProgress;
}
void RecordLock::setProgress(float64 progress) {
_progress = progress;
update();
@@ -879,7 +875,7 @@ bool RecordLock::isLocked() const {
}
bool RecordLock::isStopState() const {
return isLocked() && (_lockAnimation.value(1.) == 1.);
return isLocked() && (_lockToStopProgress == 1.);
}
rpl::producer<> RecordLock::locks() const {
@@ -895,6 +891,77 @@ QPoint RecordLock::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
}
class CancelButton final : public Ui::RippleButton {
public:
CancelButton(not_null<Ui::RpWidget*> parent, int height);
void requestPaintProgress(float64 progress);
protected:
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
private:
void init();
const int _width;
const QRect _rippleRect;
rpl::variable<float64> _showProgress = 0.;
Ui::Text::String _text;
};
CancelButton::CancelButton(not_null<Ui::RpWidget*> parent, int height)
: Ui::RippleButton(parent, st::defaultLightButton.ripple)
, _width(st::historyRecordCancelButtonWidth)
, _rippleRect(QRect(0, (height - _width) / 2, _width, _width))
, _text(st::semiboldTextStyle, tr::lng_selected_clear(tr::now).toUpper()) {
resize(_width, height);
init();
}
void CancelButton::init() {
_showProgress.value(
) | rpl::start_with_next([=](float64 progress) {
const auto hasProgress = (progress > 0.);
if (isHidden() == !hasProgress) {
setVisible(hasProgress);
}
update();
}, lifetime());
paintRequest(
) | rpl::start_with_next([=] {
Painter p(this);
p.setOpacity(_showProgress.current());
paintRipple(p, _rippleRect.x(), _rippleRect.y());
p.setPen(st::historyRecordCancelButtonFg);
_text.draw(
p,
0,
(height() - _text.minHeight()) / 2,
width(),
style::al_center);
}, lifetime());
}
QImage CancelButton::prepareRippleMask() const {
return Ui::RippleAnimation::ellipseMask(_rippleRect.size());
}
QPoint CancelButton::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
}
void CancelButton::requestPaintProgress(float64 progress) {
_showProgress = progress;
}
VoiceRecordBar::VoiceRecordBar(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> sectionWidget,
@@ -909,7 +976,12 @@ VoiceRecordBar::VoiceRecordBar(
, _level(std::make_unique<VoiceRecordButton>(
sectionWidget,
_controller->widget()->leaveEvents()))
, _cancel(std::make_unique<CancelButton>(this, recorderHeight))
, _startTimer([=] { startRecording(); })
, _message(
st::historyRecordTextStyle,
tr::lng_record_cancel(tr::now),
TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto })
, _cancelFont(st::historyRecordFont) {
resize(QSize(parent->width(), recorderHeight));
init();
@@ -996,6 +1068,7 @@ void VoiceRecordBar::init() {
_cancelFont->width(FormatVoiceDuration(kMaxSamples)),
ascent);
}
_cancel->moveToLeft((size.width() - _cancel->width()) / 2, 0);
updateMessageGeometry();
updateLockGeometry();
}, lifetime());
@@ -1009,6 +1082,14 @@ void VoiceRecordBar::init() {
p.fillRect(clip, st::historyComposeAreaBg);
p.setOpacity(std::min(p.opacity(), 1. - showListenAnimationRatio()));
const auto opacity = p.opacity();
_cancel->requestPaintProgress(_lock->isStopState()
? (opacity * _lock->lockToStopProgress())
: 0.);
if (!opacity) {
return;
}
if (clip.intersects(_messageRect)) {
// The message should be painted first to avoid flickering.
drawMessage(p, activeAnimationRatio());
@@ -1059,7 +1140,8 @@ void VoiceRecordBar::init() {
const auto &duration = st::historyRecordVoiceShowDuration;
auto callback = [=](float64 value) {
_listen->requestPaintProgress(value);
_level->requestPaintProgress(to - value);
const auto reverseValue = to - value;
_level->requestPaintProgress(reverseValue);
update();
if (to == value) {
_recordingLifetime.destroy();
@@ -1073,7 +1155,6 @@ void VoiceRecordBar::init() {
_lock->locks(
) | rpl::start_with_next([=] {
installClickOutsideFilter();
_level->setType(VoiceRecordButton::Type::Send);
_level->clicks(
@@ -1088,23 +1169,15 @@ void VoiceRecordBar::init() {
) | rpl::start_with_next([=](bool enter) {
_inField = enter;
}, _recordingLifetime);
}, lifetime());
rpl::merge(
_lock->locks(),
shownValue() | rpl::to_empty
) | rpl::start_with_next([=] {
const auto direction = Qt::LayoutDirectionAuto;
_message.setText(
st::historyRecordTextStyle,
_lock->isLocked()
? tr::lng_record_lock_cancel(tr::now)
: tr::lng_record_cancel(tr::now),
TextParseOptions{ TextParseMultiline, 0, 0, direction });
updateMessageGeometry();
// Update a whole widget to clear a previous text.
update();
const auto &duration = st::historyRecordVoiceShowDuration;
const auto from = 0.;
const auto to = 1.;
auto callback = [=](float64 value) {
_lock->requestPaintLockToStopProgress(value);
update();
};
_lockToStopAnimation.start(std::move(callback), from, to, duration);
}, lifetime());
_send->events(
@@ -1139,6 +1212,10 @@ void VoiceRecordBar::init() {
installListenStateFilter();
}, lifetime());
_cancel->setClickedCallback([=] {
hideAnimated();
});
}
void VoiceRecordBar::activeAnimate(bool active) {
@@ -1176,10 +1253,6 @@ void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
_showAnimation.start(std::move(animationCallback), from, to, duration);
}
void VoiceRecordBar::setEscFilter(Fn<bool()> &&callback) {
_escFilter = std::move(callback);
}
void VoiceRecordBar::setStartRecordingFilter(Fn<bool()> &&callback) {
_startRecordingFilter = std::move(callback);
}
@@ -1230,6 +1303,9 @@ void VoiceRecordBar::startRecording() {
}, [=] {
stop(false);
}, _recordingLifetime);
_recordingLifetime.add([=] {
_recording = false;
});
};
visibilityAnimate(true, std::move(appearanceCallback));
show();
@@ -1290,12 +1366,12 @@ void VoiceRecordBar::stop(bool send) {
void VoiceRecordBar::finish() {
_recordingLifetime.destroy();
_lockShowing = false;
_recording = false;
_inField = false;
_redCircleProgress = 0.;
_recordingSamples = 0;
_showAnimation.stop();
_lockToStopAnimation.stop();
_listen = nullptr;
@@ -1313,7 +1389,9 @@ void VoiceRecordBar::hideFast() {
void VoiceRecordBar::stopRecording(StopType type) {
using namespace ::Media::Capture;
if (type == StopType::Cancel) {
instance()->stop();
instance()->stop(crl::guard(this, [=](Result &&data) {
_cancelRequests.fire({});
}));
return;
}
instance()->stop(crl::guard(this, [=](Result &&data) {
@@ -1383,12 +1461,17 @@ void VoiceRecordBar::drawMessage(Painter &p, float64 recordActive) {
st::historyRecordCancelActive,
1. - recordActive));
const auto opacity = p.opacity();
p.setOpacity(opacity * (1. - _lock->lockToStopProgress()));
_message.draw(
p,
_messageRect.x(),
_messageRect.y(),
_messageRect.width(),
style::al_center);
p.setOpacity(opacity);
}
void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
@@ -1410,15 +1493,24 @@ rpl::producer<VoiceToSend> VoiceRecordBar::sendVoiceRequests() const {
return _sendVoiceRequests.events();
}
rpl::producer<> VoiceRecordBar::cancelRequests() const {
return _cancelRequests.events();
}
bool VoiceRecordBar::isRecording() const {
return _recording.current();
}
bool VoiceRecordBar::isActive() const {
return isRecording() || isListenState();
}
void VoiceRecordBar::hideAnimated() {
if (isHidden()) {
return;
}
visibilityAnimate(false, [=] { hide(); });
_lockShowing = false;
visibilityAnimate(false, [=] { hideFast(); });
}
void VoiceRecordBar::finishAnimating() {
@@ -1493,76 +1585,6 @@ void VoiceRecordBar::orderControls() {
_lock->raise();
}
void VoiceRecordBar::installClickOutsideFilter() {
const auto box = _recordingLifetime.make_state<QPointer<ConfirmBox>>();
const auto showBox = [=] {
if (*box) {
return;
}
auto sure = [=](Fn<void()> &&close) {
stop(false);
close();
};
*box = Ui::show(Box<ConfirmBox>(
tr::lng_record_lock_cancel_sure(tr::now),
tr::lng_record_lock_discard(tr::now),
st::attentionBoxButton,
std::move(sure)));
};
const auto computeResult = [=](not_null<QEvent*> e) {
using Type = FilterType;
if (!_lock->isLocked()) {
return Type::Continue;
}
const auto type = e->type();
const auto noBox = !(*box);
if (type == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(e.get())->key();
const auto isEsc = (key == Qt::Key_Escape);
const auto isEnter = (key == Qt::Key_Enter
|| key == Qt::Key_Return);
if (noBox) {
if (isEnter) {
stop(true);
return Type::Cancel;
} else if (isEsc && (_escFilter && _escFilter())) {
return Type::Continue;
}
return Type::ShowBox;
}
return (isEsc || isEnter) ? Type::Continue : Type::ShowBox;
} else if (type == QEvent::ContextMenu || type == QEvent::Shortcut) {
return Type::ShowBox;
} else if (type == QEvent::MouseButtonPress) {
return (noBox && !_inField.current() && !_lock->underMouse())
? Type::ShowBox
: Type::Continue;
}
return Type::Continue;
};
auto filterCallback = [=](not_null<QEvent*> e) {
using Result = base::EventFilterResult;
switch(computeResult(e)) {
case FilterType::ShowBox: {
showBox();
return Result::Cancel;
}
case FilterType::Continue: return Result::Continue;
case FilterType::Cancel: return Result::Cancel;
default: return Result::Continue;
}
};
auto filter = base::install_event_filter(
QCoreApplication::instance(),
std::move(filterCallback));
_recordingLifetime.make_state<base::unique_qptr<QObject>>(
std::move(filter));
}
void VoiceRecordBar::installListenStateFilter() {
auto keyFilterCallback = [=](not_null<QEvent*> e) {
using Result = base::EventFilterResult;
@@ -1575,7 +1597,6 @@ void VoiceRecordBar::installListenStateFilter() {
const auto keyEvent = static_cast<QKeyEvent*>(e.get());
const auto key = keyEvent->key();
const auto isSpace = (key == Qt::Key_Space);
const auto isEsc = (key == Qt::Key_Escape);
const auto isEnter = (key == Qt::Key_Enter
|| key == Qt::Key_Return);
if (isSpace && !keyEvent->isAutoRepeat() && _listen) {
@@ -1586,14 +1607,6 @@ void VoiceRecordBar::installListenStateFilter() {
requestToSendWithOptions({});
return Result::Cancel;
}
if (isEsc) {
if (_escFilter && _escFilter()) {
return Result::Continue;
} else {
hideAnimated();
return Result::Cancel;
}
}
return Result::Continue;
}
default: return Result::Continue;
@@ -1608,4 +1621,30 @@ void VoiceRecordBar::installListenStateFilter() {
std::move(keyFilter));
}
void VoiceRecordBar::showDiscardBox(
Fn<void()> &&callback,
anim::type animated) {
if (!isActive()) {
return;
}
auto sure = [=, callback = std::move(callback)](Fn<void()> &&close) {
if (animated == anim::type::instant) {
hideFast();
} else {
hideAnimated();
}
close();
if (callback) {
callback();
}
};
Ui::show(Box<ConfirmBox>(
(isListenState()
? tr::lng_record_listen_cancel_sure
: tr::lng_record_lock_cancel_sure)(tr::now),
tr::lng_record_lock_discard(tr::now),
st::attentionBoxButton,
std::move(sure)));
}
} // namespace HistoryView::Controls

View File

@@ -28,6 +28,7 @@ namespace HistoryView::Controls {
class VoiceRecordButton;
class ListenWrap;
class RecordLock;
class CancelButton;
class VoiceRecordBar final : public Ui::RpWidget {
public:
@@ -47,6 +48,10 @@ public:
int recorderHeight);
~VoiceRecordBar();
void showDiscardBox(
Fn<void()> &&callback,
anim::type animated = anim::type::instant);
void startRecording();
void finishAnimating();
void hideAnimated();
@@ -57,6 +62,7 @@ public:
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
@@ -66,12 +72,12 @@ public:
void setLockBottom(rpl::producer<int> &&bottom);
void setSendButtonGeometryValue(rpl::producer<QRect> &&geometry);
void setEscFilter(Fn<bool()> &&callback);
void setStartRecordingFilter(Fn<bool()> &&callback);
[[nodiscard]] bool isRecording() const;
[[nodiscard]] bool isLockPresent() const;
[[nodiscard]] bool isListenState() const;
[[nodiscard]] bool isActive() const;
private:
enum class StopType {
@@ -99,7 +105,6 @@ private:
void drawMessage(Painter &p, float64 recordActive);
void startRedCircleAnimation();
void installClickOutsideFilter();
void installListenStateFilter();
bool isTypeRecord() const;
@@ -119,12 +124,14 @@ private:
const std::shared_ptr<Ui::SendButton> _send;
const std::unique_ptr<RecordLock> _lock;
const std::unique_ptr<VoiceRecordButton> _level;
const std::unique_ptr<CancelButton> _cancel;
std::unique_ptr<ListenWrap> _listen;
base::Timer _startTimer;
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
rpl::event_stream<> _cancelRequests;
rpl::event_stream<> _listenChanges;
int _centerY = 0;
@@ -134,7 +141,6 @@ private:
Ui::Text::String _message;
Fn<bool()> _escFilter;
Fn<bool()> _startRecordingFilter;
rpl::variable<bool> _recording = false;
@@ -148,6 +154,7 @@ private:
rpl::lifetime _recordingLifetime;
Ui::Animations::Simple _showLockAnimation;
Ui::Animations::Simple _lockToStopAnimation;
Ui::Animations::Simple _showListenAnimation;
Ui::Animations::Simple _activeAnimation;
Ui::Animations::Simple _showAnimation;

View File

@@ -7,631 +7,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_voice_record_button.h"
#include "ui/paint/blobs.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include <QMatrix>
namespace HistoryView::Controls {
namespace {
constexpr auto kSegmentsCount = 12;
constexpr auto kMajorDegreeOffset = 360 / kSegmentsCount;
constexpr auto kSixtyDegrees = 60;
constexpr auto kMaxLevel = 1800.;
constexpr auto kBlobAlpha = 76. / 255.;
constexpr auto kBlobMaxSpeed = 5.0;
constexpr auto kLevelDuration = 100. + 500. * 0.33;
constexpr auto kBlobsScaleEnterDuration = crl::time(250);
constexpr auto kEnterIdleAnimationDuration = crl::time(1200);
constexpr auto kRotationSpeed = 0.36 * 0.1;
constexpr auto kRandomAdditionFactor = 0.15;
constexpr auto kIdleRadiusGlobalFactor = 0.56;
constexpr auto kIdleRadiusFactor = 0.15 * 0.5;
constexpr auto kOpacityMajor = 0.30;
constexpr auto kOpacityMinor = 0.15;
constexpr auto kIdleRotationSpeed = 0.2;
constexpr auto kIdleRotateDiff = 0.1 * kIdleRotationSpeed;
constexpr auto kWaveAngle = 0.03;
constexpr auto kAnimationSpeedMajor = 1.5 - 0.65;
constexpr auto kAnimationSpeedMinor = 1.5 - 0.45;
constexpr auto kAnimationSpeedCircle = 1.5 - 0.25;
constexpr auto kAmplitudeDiffFactorMax = 500. - 100.;
constexpr auto kAmplitudeDiffFactorMajor = 300. - 100.;
constexpr auto kAmplitudeDiffFactorMinor = 400. - 100.;
constexpr auto kFlingDistanceFactorMajor = 8 * 16;
constexpr auto kFlingDistanceFactorMinor = 20 * 16;
constexpr auto kFlingInAnimationDurationMajor = 200;
constexpr auto kFlingInAnimationDurationMinor = 350;
constexpr auto kFlingOutAnimationDurationMajor = 220;
constexpr auto kFlingOutAnimationDurationMinor = 380;
constexpr auto kSineWaveSpeedMajor = 0.02 * 0.2;
constexpr auto kSineWaveSpeedMinor = 0.026 * 0.2;
constexpr auto kSmallWaveRadius = 0.55;
constexpr auto kFlingDistance = 0.50;
constexpr auto kMinDivider = 100.;
constexpr auto kMaxAmplitude = 1800.;
constexpr auto kZeroPoint = QPointF(0, 0);
template <typename Number>
void Normalize(Number &value, Number right) {
if (value >= right) {
value -= right;
}
}
float64 RandomAdditional() {
return (rand_value<int>() % 100 / 100.);
}
void PerformAnimation(
rpl::producer<crl::time> &&animationTicked,
Fn<void(float64)> &&applyValue,
Fn<void()> &&finishCallback,
float64 duration,
float64 from,
float64 to,
rpl::lifetime &lifetime) {
lifetime.destroy();
const auto animValue =
lifetime.make_state<anim::value>(from, to);
const auto animStarted = crl::now();
std::move(
animationTicked
) | rpl::start_with_next([=,
applyValue = std::move(applyValue),
finishCallback = std::move(finishCallback),
&lifetime](crl::time now) mutable {
const auto dt = anim::Disabled()
? 1.
: ((now - animStarted) / duration);
if (dt >= 1.) {
animValue->finish();
applyValue(animValue->current());
lifetime.destroy();
if (finishCallback) {
finishCallback();
}
} else {
animValue->update(dt, anim::linear);
applyValue(animValue->current());
}
}, lifetime);
auto Blobs() {
return std::vector<Ui::Paint::Blobs::BlobData>{
{
.segmentsCount = 9,
.minScale = 0.605229,
.minRadius = (float)st::historyRecordMinorBlobMinRadius,
.maxRadius = (float)st::historyRecordMinorBlobMaxRadius,
.speedScale = 1.,
.alpha = kBlobAlpha,
.maxSpeed = kBlobMaxSpeed,
},
{
.segmentsCount = 12,
.minScale = 0.553943,
.minRadius = (float)st::historyRecordMajorBlobMinRadius,
.maxRadius = (float)st::historyRecordMajorBlobMaxRadius,
.speedScale = 1.,
.alpha = kBlobAlpha,
.maxSpeed = kBlobMaxSpeed,
},
};
}
} // namespace
class ContinuousValue {
public:
ContinuousValue() = default;
ContinuousValue(float64 duration) : _duration(duration) {
}
void start(float64 to, float64 duration) {
_to = to;
_delta = (_to - _cur) / duration;
}
void start(float64 to) {
start(to, _duration);
}
void reset() {
_to = _cur = _delta = 0.;
}
float64 current() const {
return _cur;
}
float64 to() const {
return _to;
}
float64 delta() const {
return _delta;
}
void update(crl::time dt, Fn<void(float64 &)> &&callback = nullptr) {
if (_to != _cur) {
_cur += _delta * dt;
if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) {
_cur = _to;
}
if (callback) {
callback(_cur);
}
}
}
private:
float64 _duration = 0.;
float64 _to = 0.;
float64 _cur = 0.;
float64 _delta = 0.;
};
class CircleBezier final {
public:
CircleBezier(int n);
void computeRandomAdditionals();
void paintCircle(
Painter &p,
const QColor &c,
float64 radius,
float64 cubicBezierFactor,
float64 idleStateDiff,
float64 radiusDiff,
float64 randomFactor);
private:
struct Points {
QPointF point;
QPointF control;
};
const int _segmentsCount;
const float64 _segmentLength;
std::vector<float64> _randomAdditionals;
};
class Wave final {
public:
Wave(
rpl::producer<crl::time> animationTicked,
int n,
float64 rotationOffset,
float64 amplitudeRadius,
float64 amplitudeWaveDiff,
float64 fling,
int flingDistanceFactor,
int flingInAnimationDuration,
int flingOutAnimationDuration,
float64 amplitudeDiffSpeed,
float64 amplitudeDiffFactor,
bool isDirectionClockwise);
void setValue(float64 to);
void tick(float64 circleRadius, crl::time dt);
void reset();
void paint(Painter &p, QColor c);
private:
void initEnterIdleAnimation(rpl::producer<crl::time> animationTicked);
void initFlingAnimation(rpl::producer<crl::time> animationTicked);
const std::unique_ptr<CircleBezier> _circleBezier;
const float _rotationOffset;
const float64 _idleGlobalRadius;
const float64 _amplitudeRadius;
const float64 _amplitudeWaveDiff;
const float64 _randomAdditions;
const float64 _fling;
const int _flingDistanceFactor;
const int _flingInAnimationDuration;
const int _flingOutAnimationDuration;
const float64 _amplitudeInAnimationDuration;
const float64 _amplitudeOutAnimationDuration;
const int _directionClockwise;
bool _incRandomAdditionals = false;
bool _isIdle = true;
bool _wasFling = false;
float64 _flingRadius = 0.;
float64 _idleRadius = 0.;
float64 _idleRotation = 0.;
float64 _lastRadius = 0.;
float64 _rotation = 0.;
float64 _sineAngleMax = 0.;
float64 _waveAngle = 0.;
float64 _waveDiff = 0.;
ContinuousValue _levelValue;
rpl::event_stream<float64> _flingAnimationRequests;
rpl::event_stream<> _enterIdleAnimationRequests;
rpl::lifetime _animationEnterIdleLifetime;
rpl::lifetime _animationFlingLifetime;
rpl::lifetime _lifetime;
};
class RecordCircle final {
public:
RecordCircle(rpl::producer<crl::time> animationTicked);
void reset();
void setAmplitude(float64 value);
void paint(Painter &p, QColor c);
private:
const std::unique_ptr<Wave> _majorWave;
const std::unique_ptr<Wave> _minorWave;
crl::time _lastUpdateTime = 0;
ContinuousValue _levelValue;
};
CircleBezier::CircleBezier(int n)
: _segmentsCount(n)
, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n)))
, _randomAdditionals(n) {
}
void CircleBezier::computeRandomAdditionals() {
ranges::generate(_randomAdditionals, RandomAdditional);
}
void CircleBezier::paintCircle(
Painter &p,
const QColor &c,
float64 radius,
float64 cubicBezierFactor,
float64 idleStateDiff,
float64 radiusDiff,
float64 randomFactor) {
PainterHighQualityEnabler hq(p);
const auto r1 = radius - idleStateDiff / 2. - radiusDiff / 2.;
const auto r2 = radius + radiusDiff / 2. + idleStateDiff / 2.;
const auto l = _segmentLength * std::max(r1, r2) * cubicBezierFactor;
auto m = QMatrix();
const auto preparePoints = [&](int i, bool isStart) -> Points {
Normalize(i, _segmentsCount);
const auto randomAddition = randomFactor * _randomAdditionals[i];
const auto r = ((i % 2 == 0) ? r1 : r2) + randomAddition;
m.reset();
m.rotate(360. / _segmentsCount * i);
const auto sign = isStart ? 1 : -1;
return {
(isStart && i) ? QPointF() : m.map(QPointF(0, -r)),
m.map(QPointF(sign * (l + randomAddition * _segmentLength), -r)),
};
};
const auto &[startPoint, _] = preparePoints(0, true);
auto path = QPainterPath();
path.moveTo(startPoint);
for (auto i = 0; i < _segmentsCount; i++) {
const auto &[_, startControl] = preparePoints(i, true);
const auto &[end, endControl] = preparePoints(i + 1, false);
path.cubicTo(startControl, endControl, end);
}
p.setBrush(Qt::NoBrush);
auto pen = QPen(Qt::NoPen);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
p.setPen(pen);
p.fillPath(path, c);
p.drawPath(path);
}
Wave::Wave(
rpl::producer<crl::time> animationTicked,
int n,
float64 rotationOffset,
float64 amplitudeRadius,
float64 amplitudeWaveDiff,
float64 fling,
int flingDistanceFactor,
int flingInAnimationDuration,
int flingOutAnimationDuration,
float64 amplitudeDiffSpeed,
float64 amplitudeDiffFactor,
bool isDirectionClockwise)
: _circleBezier(std::make_unique<CircleBezier>(n))
, _rotationOffset(rotationOffset)
, _idleGlobalRadius(st::historyRecordRadiusDiffMin * kIdleRadiusGlobalFactor)
, _amplitudeRadius(amplitudeRadius)
, _amplitudeWaveDiff(amplitudeWaveDiff)
, _randomAdditions(st::historyRecordRandomAddition * kRandomAdditionFactor)
, _fling(fling)
, _flingDistanceFactor(flingDistanceFactor)
, _flingInAnimationDuration(flingInAnimationDuration)
, _flingOutAnimationDuration(flingOutAnimationDuration)
, _amplitudeInAnimationDuration(kMinDivider
+ amplitudeDiffFactor * amplitudeDiffSpeed)
, _amplitudeOutAnimationDuration(kMinDivider
+ kAmplitudeDiffFactorMax * amplitudeDiffSpeed)
, _directionClockwise(isDirectionClockwise ? 1 : -1)
, _rotation(rotationOffset) {
initEnterIdleAnimation(rpl::duplicate(animationTicked));
initFlingAnimation(std::move(animationTicked));
}
void Wave::reset() {
_incRandomAdditionals = false;
_isIdle = true;
_wasFling = false;
_flingRadius = 0.;
_idleRadius = 0.;
_idleRotation = 0.;
_lastRadius = 0.;
_rotation = 0.;
_sineAngleMax = 0.;
_waveAngle = 0.;
_waveDiff = 0.;
_levelValue.reset();
}
void Wave::setValue(float64 to) {
const auto duration = (to <= _levelValue.current())
? _amplitudeOutAnimationDuration
: _amplitudeInAnimationDuration;
_levelValue.start(to, duration);
const auto idle = to < 0.1;
if (_isIdle != idle && idle) {
_enterIdleAnimationRequests.fire({});
}
_isIdle = idle;
if (!_isIdle) {
_animationEnterIdleLifetime.destroy();
}
}
void Wave::initEnterIdleAnimation(rpl::producer<crl::time> animationTicked) {
_enterIdleAnimationRequests.events(
) | rpl::start_with_next([=] {
const auto &k = kSixtyDegrees;
const auto rotation = _rotation;
const auto rotationTo = std::round(rotation / k) * k
+ _rotationOffset;
const auto waveDiff = _waveDiff;
auto applyValue = [=](float64 v) {
_rotation = rotationTo + (rotation - rotationTo) * v;
_waveDiff = 1. + (waveDiff - 1.) * v;
_waveAngle = std::acos(_waveDiff * _directionClockwise);
};
PerformAnimation(
rpl::duplicate(animationTicked),
std::move(applyValue),
nullptr,
kEnterIdleAnimationDuration,
1,
0,
_animationEnterIdleLifetime);
}, _lifetime);
}
void Wave::initFlingAnimation(rpl::producer<crl::time> animationTicked) {
_flingAnimationRequests.events(
) | rpl::start_with_next([=](float64 delta) {
const auto fling = _fling * 2;
const auto flingDistance = delta
* _amplitudeRadius
* _flingDistanceFactor
* fling;
const auto applyValue = [=](float64 v) {
_flingRadius = v;
};
auto finishCallback = [=] {
PerformAnimation(
rpl::duplicate(animationTicked),
applyValue,
nullptr,
_flingOutAnimationDuration * fling,
flingDistance,
0,
_animationFlingLifetime);
};
PerformAnimation(
rpl::duplicate(animationTicked),
applyValue,
std::move(finishCallback),
_flingInAnimationDuration * fling,
_flingRadius,
flingDistance,
_animationFlingLifetime);
}, _lifetime);
}
void Wave::tick(float64 circleRadius, crl::time dt) {
auto amplitudeCallback = [&](float64 &value) {
if (std::abs(value - _levelValue.to()) * _amplitudeRadius
< (st::historyRecordRandomAddition / 2)) {
if (!_wasFling) {
_flingAnimationRequests.fire_copy(_levelValue.delta());
_wasFling = true;
}
} else {
_wasFling = false;
}
};
_levelValue.update(dt, std::move(amplitudeCallback));
_idleRadius = circleRadius * kIdleRadiusFactor;
{
const auto to = _levelValue.to();
const auto delta = (_sineAngleMax - to);
if (std::abs(delta) - 0.25 < 0) {
_sineAngleMax = to;
} else {
_sineAngleMax -= 0.25 * ((delta < 0) ? -1 : 1);
}
}
if (!_isIdle) {
_rotation += dt
* (kRotationSpeed * 4. * std::min(_levelValue.current() / .5, 1.)
+ kRotationSpeed * 0.5);
Normalize(_rotation, 360.);
} else {
_idleRotation += kIdleRotateDiff * dt;
Normalize(_idleRotation, 360.);
}
_lastRadius = circleRadius;
if (!_isIdle) {
_waveAngle += (_amplitudeWaveDiff * _sineAngleMax) * dt;
_waveDiff = std::cos(_waveAngle) * _directionClockwise;
if ((_waveDiff != 0) && ((_waveDiff > 0) == _incRandomAdditionals)) {
_circleBezier->computeRandomAdditionals();
_incRandomAdditionals = !_incRandomAdditionals;
}
}
}
void Wave::paint(Painter &p, QColor c) {
const auto amplitude = _levelValue.current();
const auto waveAmplitude = std::min(amplitude / .3, 1.);
const auto radiusDiff = st::historyRecordRadiusDiffMin
+ st::historyRecordRadiusDiff * kWaveAngle * _levelValue.to();
const auto diffFactor = 0.35 * waveAmplitude * _waveDiff;
const auto radius = (_lastRadius + _amplitudeRadius * amplitude)
+ _idleGlobalRadius
+ (_flingRadius * waveAmplitude);
const auto cubicBezierFactor = 1.
+ std::abs(diffFactor) * waveAmplitude
+ (1. - waveAmplitude) * kIdleRadiusFactor;
const auto circleRadiusDiff = std::max(
radiusDiff * diffFactor,
st::historyRecordLevelMainRadius - radius);
p.rotate((_rotation + _idleRotation) * _directionClockwise);
_circleBezier->paintCircle(
p,
c,
radius,
cubicBezierFactor,
_idleRadius * (1. - waveAmplitude),
circleRadiusDiff,
waveAmplitude * _waveDiff * _randomAdditions);
p.rotate(0);
}
RecordCircle::RecordCircle(rpl::producer<crl::time> animationTicked)
: _majorWave(std::make_unique<Wave>(
rpl::duplicate(animationTicked),
kSegmentsCount,
kMajorDegreeOffset,
st::historyRecordMajorAmplitudeRadius,
kSineWaveSpeedMajor,
0.,
kFlingDistanceFactorMajor,
kFlingInAnimationDurationMajor,
kFlingOutAnimationDurationMajor,
kAnimationSpeedMajor,
kAmplitudeDiffFactorMajor,
true))
, _minorWave(std::make_unique<Wave>(
std::move(animationTicked),
kSegmentsCount,
0,
st::historyRecordMinorAmplitudeRadius
+ st::historyRecordMinorAmplitudeRadius * kSmallWaveRadius,
kSineWaveSpeedMinor,
kFlingDistance,
kFlingDistanceFactorMinor,
kFlingInAnimationDurationMinor,
kFlingOutAnimationDurationMinor,
kAnimationSpeedMinor,
kAmplitudeDiffFactorMinor,
false))
, _levelValue(kMinDivider
+ kAmplitudeDiffFactorMax * kAnimationSpeedCircle) {
}
void RecordCircle::reset() {
_majorWave->reset();
_minorWave->reset();
_levelValue.reset();
}
void RecordCircle::setAmplitude(float64 value) {
const auto to = std::min(kMaxAmplitude, value) / kMaxAmplitude;
_levelValue.start(to);
_majorWave->setValue(to);
_minorWave->setValue(to);
}
void RecordCircle::paint(Painter &p, QColor c) {
const auto dt = crl::now() - _lastUpdateTime;
_levelValue.update(dt);
const auto &mainRadius = st::historyRecordLevelMainRadiusAmplitude;
const auto radius = (st::historyRecordLevelMainRadius
+ (anim::Disabled() ? 0 : mainRadius * _levelValue.current()));
if (!anim::Disabled()) {
_majorWave->tick(radius, dt);
_minorWave->tick(radius, dt);
_lastUpdateTime = crl::now();
const auto opacity = p.opacity();
p.setOpacity(kOpacityMajor);
_majorWave->paint(p, c);
p.setOpacity(kOpacityMinor);
_minorWave->paint(p, c);
p.setOpacity(opacity);
}
p.setPen(Qt::NoPen);
p.setBrush(c);
p.drawEllipse(kZeroPoint, radius, radius);
}
VoiceRecordButton::VoiceRecordButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<> leaveWindowEventProducer)
: AbstractButton(parent)
, _recordCircle(std::make_unique<RecordCircle>(
_recordAnimationTicked.events()))
, _center(st::historyRecordLevelMaxRadius)
, _recordingAnimation([=](crl::time now) {
if (!anim::Disabled()) {
update();
}
_recordAnimationTicked.fire_copy(now);
return true;
}) {
const auto h = st::historyRecordLevelMaxRadius * 2;
resize(h, h);
, _blobs(std::make_unique<Ui::Paint::Blobs>(
Blobs(),
kLevelDuration,
kMaxLevel))
, _center(_blobs->maxRadius()) {
resize(_center * 2, _center * 2);
std::move(
leaveWindowEventProducer
) | rpl::start_with_next([=] {
@@ -643,45 +68,71 @@ VoiceRecordButton::VoiceRecordButton(
VoiceRecordButton::~VoiceRecordButton() = default;
void VoiceRecordButton::requestPaintLevel(quint16 level) {
_recordCircle->setAmplitude(level);
if (_blobsHideLastTime) {
return;
}
_blobs->setLevel(level);
update();
}
void VoiceRecordButton::init() {
const auto hasProgress = [](auto value) { return value != 0.; };
const auto stateChangedAnimation =
lifetime().make_state<Ui::Animations::Simple>();
const auto currentState = lifetime().make_state<Type>(_state.current());
rpl::single(
anim::Disabled()
) | rpl::then(
anim::Disables()
) | rpl::start_with_next([=](bool hide) {
if (hide) {
_blobs->setLevel(0.);
}
_blobsHideLastTime = hide ? crl::now() : 0;
if (!hide && !_animation.animating()) {
_animation.start();
}
}, lifetime());
const auto &mainRadiusMin = st::historyRecordMainBlobMinRadius;
const auto mainRadiusDiff = st::historyRecordMainBlobMaxRadius
- mainRadiusMin;
paintRequest(
) | rpl::start_with_next([=](const QRect &clip) {
Painter p(this);
const auto progress = _showProgress.current();
const auto complete = (progress == 1.);
const auto hideProgress = _blobsHideLastTime
? 1. - std::clamp(
((crl::now() - _blobsHideLastTime)
/ (float64)kBlobsScaleEnterDuration),
0.,
1.)
: 1.;
const auto showProgress = _showProgress.current();
const auto complete = (showProgress == 1.);
p.translate(_center, _center);
if (!complete) {
p.scale(progress, progress);
}
PainterHighQualityEnabler hq(p);
const auto color = anim::color(
const auto brush = QBrush(anim::color(
st::historyRecordVoiceFgInactive,
st::historyRecordVoiceFgActive,
_colorProgress.current());
_recordCircle->paint(p, color);
p.resetTransform();
_colorProgress));
_blobs->paint(p, brush, showProgress * hideProgress);
const auto radius = (mainRadiusMin
+ (mainRadiusDiff * _blobs->currentLevel())) * showProgress;
p.setPen(Qt::NoPen);
p.setBrush(brush);
p.drawEllipse(QPointF(), radius, radius);
if (!complete) {
p.setOpacity(progress);
p.setOpacity(showProgress);
}
// Paint icon.
{
const auto stateProgress = stateChangedAnimation->value(0.);
const auto stateProgress = _stateChangedAnimation.value(0.);
const auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;
p.translate(_center, _center);
if (scale < 1.) {
p.scale(scale, scale);
}
@@ -701,21 +152,33 @@ void VoiceRecordButton::init() {
}
}, lifetime());
_animation.init([=](crl::time now) {
if (const auto &last = _blobsHideLastTime; (last > 0)
&& (now - last >= kBlobsScaleEnterDuration)) {
_animation.stop();
return false;
}
_blobs->updateLevel(now - _lastUpdateTime);
_lastUpdateTime = now;
update();
return true;
});
rpl::merge(
shownValue(),
_showProgress.value(
) | rpl::map(hasProgress) | rpl::distinct_until_changed()
) | rpl::map(rpl::mappers::_1 != 0.) | rpl::distinct_until_changed()
) | rpl::start_with_next([=](bool show) {
setVisible(show);
setMouseTracking(show);
if (!show) {
_recordingAnimation.stop();
_animation.stop();
_showProgress = 0.;
_recordCircle->reset();
_blobs->resetLevel();
_state = Type::Record;
} else {
if (!_recordingAnimation.animating()) {
_recordingAnimation.start();
if (!_animation.animating()) {
_animation.start();
}
}
}, lifetime());
@@ -736,7 +199,7 @@ void VoiceRecordButton::init() {
update();
};
const auto duration = st::historyRecordVoiceDuration * 2;
stateChangedAnimation->start(std::move(callback), 0., to, duration);
_stateChangedAnimation.start(std::move(callback), 0., to, duration);
}, lifetime());
}
@@ -757,8 +220,15 @@ rpl::producer<bool> VoiceRecordButton::actives() const {
});
}
rpl::producer<> VoiceRecordButton::clicks() const {
return Ui::AbstractButton::clicks(
) | rpl::to_empty | rpl::filter([=] {
return inCircle(mapFromGlobal(QCursor::pos()));
});
}
bool VoiceRecordButton::inCircle(const QPoint &localPos) const {
const auto &radii = st::historyRecordLevelMaxRadius;
const auto &radii = st::historyRecordMainBlobMaxRadius;
const auto dx = std::abs(localPos.x() - _center);
if (dx > radii) {
return false;
@@ -778,6 +248,9 @@ void VoiceRecordButton::requestPaintProgress(float64 progress) {
}
void VoiceRecordButton::requestPaintColor(float64 progress) {
if (_colorProgress == progress) {
return;
}
_colorProgress = progress;
update();
}

View File

@@ -11,9 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace HistoryView::Controls {
namespace Ui {
namespace Paint {
class Blobs;
} // namespace Paint
} // namespace Ui
class RecordCircle;
namespace HistoryView::Controls {
class VoiceRecordButton final : public Ui::AbstractButton {
public:
@@ -34,25 +38,28 @@ public:
void requestPaintLevel(quint16 level);
[[nodiscard]] rpl::producer<bool> actives() const;
[[nodiscard]] rpl::producer<> clicks() const;
[[nodiscard]] bool inCircle(const QPoint &localPos) const;
private:
void init();
rpl::event_stream<crl::time> _recordAnimationTicked;
std::unique_ptr<RecordCircle> _recordCircle;
std::unique_ptr<Ui::Paint::Blobs> _blobs;
crl::time _lastUpdateTime = 0;
crl::time _blobsHideLastTime = 0;
const int _center;
rpl::variable<float64> _showProgress = 0.;
rpl::variable<float64> _colorProgress = 0.;
float64 _colorProgress = 0.;
rpl::variable<bool> _inCircle = false;
rpl::variable<Type> _state = Type::Record;
// This can animate for a very long time (like in music playing),
// so it should be a Basic, not a Simple animation.
Ui::Animations::Basic _recordingAnimation;
Ui::Animations::Basic _animation;
Ui::Animations::Simple _stateChangedAnimation;
};
} // namespace HistoryView::Controls

View File

@@ -39,7 +39,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_scheduled_messages.h"
#include "core/file_utilities.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "lang/lang_keys.h"
@@ -126,9 +125,7 @@ void CopyImage(not_null<PhotoData*> photo) {
}
const auto image = media->image(Data::PhotoSize::Large)->original();
if (!Platform::SetClipboardImage(image)) {
QGuiApplication::clipboard()->setImage(image);
}
QGuiApplication::clipboard()->setImage(image);
}
void ShowStickerPackInfo(
@@ -290,7 +287,7 @@ void AddPostLinkAction(
: Context::History;
menu->addAction(
(item->history()->peer->isMegagroup()
? tr::lng_context_copy_link
? tr::lng_context_copy_message_link
: tr::lng_context_copy_post_link)(tr::now),
[=] { CopyPostLink(session, itemId, context); });
}

View File

@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_group_call.h"
#include "main/main_session.h"
#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/calls_instance.h"
@@ -24,7 +25,7 @@ namespace HistoryView {
void GenerateUserpicsInRow(
QImage &result,
const std::vector<UserpicInRow> &list,
const UserpicsInRowStyle &st,
const style::GroupCallUserpics &st,
int maxElements) {
const auto count = int(list.size());
if (!count) {
@@ -61,13 +62,13 @@ void GenerateUserpicsInRow(
}
}
GroupCallTracker::GroupCallTracker(not_null<ChannelData*> channel)
: _channel(channel) {
GroupCallTracker::GroupCallTracker(not_null<PeerData*> peer)
: _peer(peer) {
}
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
not_null<Data::GroupCall*> call,
const UserpicsInRowStyle &st) {
int userpicSize) {
struct State {
std::vector<UserpicInRow> userpics;
Ui::GroupCallBarContent current;
@@ -118,7 +119,10 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
}
for (auto i = 0; i != kLimit - already; ++i) {
if (adding[i]) {
state->userpics.push_back(UserpicInRow{ adding[i]->user });
state->userpics.push_back(UserpicInRow{
.peer = adding[i]->user,
.speaking = adding[i]->speaking,
});
}
}
return true;
@@ -127,18 +131,27 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
static const auto RegenerateUserpics = [](
not_null<State*> state,
not_null<Data::GroupCall*> call,
const UserpicsInRowStyle &st,
int userpicSize,
bool force = false) {
const auto result = FillMissingUserpics(state, call) || force;
if (!result) {
return false;
}
GenerateUserpicsInRow(
state->current.userpics,
state->userpics,
st);
state->current.users.reserve(state->userpics.size());
state->current.users.clear();
state->someUserpicsNotLoaded = false;
for (const auto &userpic : state->userpics) {
for (auto &userpic : state->userpics) {
userpic.peer->loadUserpic();
const auto pic = userpic.peer->genUserpic(
userpic.view,
userpicSize);
userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view);
state->current.users.push_back({
.userpic = pic.toImage(),
.userpicKey = userpic.uniqueKey,
.id = userpic.peer->bareId(),
.speaking = userpic.speaking,
});
if (userpic.peer->hasUserpic()
&& userpic.peer->useEmptyUserpic(userpic.view)) {
state->someUserpicsNotLoaded = true;
@@ -151,7 +164,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
not_null<State*> state,
not_null<Data::GroupCall*> call,
not_null<UserData*> user,
const UserpicsInRowStyle &st) {
int userpicSize) {
const auto i = ranges::find(
state->userpics,
user,
@@ -160,7 +173,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
return false;
}
state->userpics.erase(i);
RegenerateUserpics(state, call, st, true);
RegenerateUserpics(state, call, userpicSize, true);
return true;
};
@@ -168,16 +181,21 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
not_null<State*> state,
not_null<Data::GroupCall*> call,
not_null<UserData*> user,
const UserpicsInRowStyle &st) {
int userpicSize) {
Expects(state->userpics.size() <= kLimit);
const auto &participants = call->participants();
auto i = state->userpics.begin();
auto i = begin(state->userpics);
// Find where to put a new speaking userpic.
for (; i != state->userpics.end(); ++i) {
for (; i != end(state->userpics); ++i) {
if (i->peer == user) {
return false;
if (i->speaking) {
return false;
}
const auto index = i - begin(state->userpics);
state->current.users[index].speaking = i->speaking = true;
return true;
}
const auto j = ranges::find(
participants,
@@ -194,7 +212,10 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
}
// Add the new speaking to the place we found.
const auto added = state->userpics.insert(i, UserpicInRow{ user });
const auto added = state->userpics.insert(i, UserpicInRow{
.peer = user,
.speaking = true,
});
// Remove him from the tail, if he was there.
for (auto i = added + 1; i != state->userpics.end(); ++i) {
@@ -219,7 +240,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
}
Assert(state->userpics.size() <= kLimit);
}
RegenerateUserpics(state, call, st, true);
RegenerateUserpics(state, call, userpicSize, true);
return true;
};
@@ -244,39 +265,58 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
) | rpl::start_with_next([=](const ParticipantUpdate &update) {
const auto user = update.now ? update.now->user : update.was->user;
if (!update.now) {
if (RemoveUserpic(state, call, user, st)) {
if (RemoveUserpic(state, call, user, userpicSize)) {
pushNext();
}
} else if (update.now->speaking
&& (!update.was || !update.was->speaking)) {
if (CheckPushToFront(state, call, user, st)) {
if (CheckPushToFront(state, call, user, userpicSize)) {
pushNext();
}
} else {
auto updateSpeakingState = update.was.has_value()
&& (update.now->speaking != update.was->speaking);
if (updateSpeakingState) {
const auto i = ranges::find(
state->userpics,
user,
&UserpicInRow::peer);
if (i != end(state->userpics)) {
const auto index = i - begin(state->userpics);
state->current.users[index].speaking
= i->speaking
= update.now->speaking;
} else {
updateSpeakingState = false;
}
}
if (RegenerateUserpics(state, call, userpicSize)
|| updateSpeakingState) {
pushNext();
}
} else if (RegenerateUserpics(state, call, st)) {
pushNext();
}
}, lifetime);
call->participantsSliceAdded(
) | rpl::filter([=] {
return RegenerateUserpics(state, call, st);
return RegenerateUserpics(state, call, userpicSize);
}) | rpl::start_with_next(pushNext, lifetime);
call->channel()->session().downloaderTaskFinished(
call->peer()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return state->someUserpicsNotLoaded;
}) | rpl::start_with_next([=] {
for (const auto &userpic : state->userpics) {
if (userpic.peer->userpicUniqueKey(userpic.view)
!= userpic.uniqueKey) {
RegenerateUserpics(state, call, st, true);
RegenerateUserpics(state, call, userpicSize, true);
pushNext();
return;
}
}
}, lifetime);
RegenerateUserpics(state, call, st);
RegenerateUserpics(state, call, userpicSize);
call->fullCountValue(
) | rpl::start_with_next([=](int count) {
@@ -289,15 +329,15 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::ContentByCall(
}
rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
const auto channel = _channel;
const auto peer = _peer;
return rpl::combine(
channel->session().changes().peerFlagsValue(
channel,
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::GroupCall),
Core::App().calls().currentGroupCallValue()
) | rpl::map([=](const auto&, Calls::GroupCall *current) {
const auto call = channel->call();
return (call && (!current || current->channel() != channel))
const auto call = peer->groupCall();
return (call && (!current || current->peer() != peer))
? call
: nullptr;
}) | rpl::distinct_until_changed(
@@ -308,12 +348,7 @@ rpl::producer<Ui::GroupCallBarContent> GroupCallTracker::content() const {
} else if (!call->fullCount() && !call->participantsLoaded()) {
call->reload();
}
const auto st = UserpicsInRowStyle{
.size = st::historyGroupCallUserpicSize,
.shift = st::historyGroupCallUserpicShift,
.stroke = st::historyGroupCallUserpicStroke,
};
return ContentByCall(call, st);
return ContentByCall(call, st::historyGroupCallUserpics.size);
}) | rpl::flatten_latest();
}

View File

@@ -18,39 +18,38 @@ class GroupCall;
class CloudImageView;
} // namespace Data
namespace style {
struct GroupCallUserpics;
} // namespace style
namespace HistoryView {
struct UserpicInRow {
not_null<PeerData*> peer;
bool speaking = false;
mutable std::shared_ptr<Data::CloudImageView> view;
mutable InMemoryKey uniqueKey;
};
struct UserpicsInRowStyle {
int size = 0;
int shift = 0;
int stroke = 0;
};
void GenerateUserpicsInRow(
QImage &result,
const std::vector<UserpicInRow> &list,
const UserpicsInRowStyle &st,
const style::GroupCallUserpics &st,
int maxElements = 0);
class GroupCallTracker final {
public:
GroupCallTracker(not_null<ChannelData*> channel);
explicit GroupCallTracker(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<Ui::GroupCallBarContent> content() const;
[[nodiscard]] rpl::producer<> joinClicks() const;
[[nodiscard]] static rpl::producer<Ui::GroupCallBarContent> ContentByCall(
not_null<Data::GroupCall*> call,
const UserpicsInRowStyle &st);
int userpicSize);
private:
not_null<ChannelData*> _channel;
const not_null<PeerData*> _peer;
rpl::event_stream<> _joinClicks;

View File

@@ -795,8 +795,8 @@ void Message::paintCommentsButton(
auto &list = _comments->userpics;
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
const auto count = std::min(int(views->recentRepliers.size()), limit);
const auto single = st::historyCommentsUserpicSize;
const auto shift = st::historyCommentsUserpicOverlap;
const auto single = st::historyCommentsUserpics.size;
const auto shift = st::historyCommentsUserpics.shift;
const auto regenerate = [&] {
if (list.size() != count) {
return true;
@@ -828,12 +828,11 @@ void Message::paintCommentsButton(
while (list.size() > count) {
list.pop_back();
}
const auto st = UserpicsInRowStyle{
.size = single,
.shift = shift,
.stroke = st::historyCommentsUserpicStroke,
};
GenerateUserpicsInRow(_comments->cachedUserpics, list, st, limit);
GenerateUserpicsInRow(
_comments->cachedUserpics,
list,
st::historyCommentsUserpics,
limit);
}
p.drawImage(
left,
@@ -2135,8 +2134,8 @@ int Message::minWidthForMedia() const {
const auto views = data()->Get<HistoryMessageViews>();
if (data()->repliesAreComments() && !views->replies.text.isEmpty()) {
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
const auto single = st::historyCommentsUserpicSize;
const auto shift = st::historyCommentsUserpicOverlap;
const auto single = st::historyCommentsUserpics.size;
const auto shift = st::historyCommentsUserpics.shift;
const auto added = single
+ (limit - 1) * (single - shift)
+ st::historyCommentsSkipLeft

View File

@@ -342,8 +342,8 @@ void PinnedWidget::setInternalState(
restoreState(memento);
}
std::unique_ptr<Window::SectionMemento> PinnedWidget::createMemento() {
auto result = std::make_unique<PinnedMemento>(history());
std::shared_ptr<Window::SectionMemento> PinnedWidget::createMemento() {
auto result = std::make_shared<PinnedMemento>(history());
saveState(result.get());
return result;
}

View File

@@ -56,7 +56,7 @@ public:
bool showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
std::unique_ptr<Window::SectionMemento> createMemento() override;
std::shared_ptr<Window::SectionMemento> createMemento() override;
bool showMessage(
PeerId peerId,
const Window::SectionShow &params,

View File

@@ -603,7 +603,7 @@ bool RepliesWidget::confirmSendingFiles(
}
if (hasImage) {
auto image = Platform::GetClipboardImage();
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}
@@ -1315,6 +1315,10 @@ Dialogs::RowDescriptor RepliesWidget::activeChat() const {
};
}
bool RepliesWidget::preventsClose(Fn<void()> &&continueCallback) const {
return _composeControls->preventsClose(std::move(continueCallback));
}
QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &params) {
_topBar->updateControlsVisibility();
if (params.withTopBarShadow) _topBarShadow->hide();
@@ -1363,8 +1367,8 @@ bool RepliesWidget::returnTabbedSelector() {
return _composeControls->returnTabbedSelector();
}
std::unique_ptr<Window::SectionMemento> RepliesWidget::createMemento() {
auto result = std::make_unique<RepliesMemento>(history(), _rootId);
std::shared_ptr<Window::SectionMemento> RepliesWidget::createMemento() {
auto result = std::make_shared<RepliesMemento>(history(), _rootId);
saveState(result.get());
return result;
}

Some files were not shown because too many files have changed in this diff Show More