Compare commits
339 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c2318919 | ||
|
|
8d8fffd306 | ||
|
|
ecb53e3e0b | ||
|
|
468d43c4c7 | ||
|
|
425a632965 | ||
|
|
bf581a1ba4 | ||
|
|
7a0ba58ffd | ||
|
|
4543656aa3 | ||
|
|
8d72026cbd | ||
|
|
32e47c24b4 | ||
|
|
69eaecc218 | ||
|
|
3ffbb94fdb | ||
|
|
4f8bab6a5f | ||
|
|
2f0fd398d5 | ||
|
|
c6fde48936 | ||
|
|
8356bac6d7 | ||
|
|
b553520a48 | ||
|
|
3c86da78af | ||
|
|
fdbf63229d | ||
|
|
d22601461a | ||
|
|
13c70a9ce9 | ||
|
|
1cfbf24635 | ||
|
|
f90f1c02c3 | ||
|
|
8d0f5bb828 | ||
|
|
2f986660ff | ||
|
|
f9f98975a1 | ||
|
|
e0e2b973f1 | ||
|
|
aeb994dd40 | ||
|
|
112c597556 | ||
|
|
fc94045f41 | ||
|
|
2a8055c513 | ||
|
|
fba116f0d5 | ||
|
|
b38f89d69e | ||
|
|
84f6a5f957 | ||
|
|
054223efe0 | ||
|
|
de3ea30d69 | ||
|
|
c5a46d9d1b | ||
|
|
e41fb0d8fd | ||
|
|
5970f3de9e | ||
|
|
7878552e7d | ||
|
|
b3648d0147 | ||
|
|
dd79b3c0d5 | ||
|
|
3d76e6de55 | ||
|
|
ef61443342 | ||
|
|
4f8989fad7 | ||
|
|
945411274f | ||
|
|
f1f7330bf6 | ||
|
|
a1957fe5c0 | ||
|
|
9a21d55de7 | ||
|
|
303ad02c61 | ||
|
|
1886a5c4ed | ||
|
|
71ddfacfaa | ||
|
|
23c2bce1bb | ||
|
|
5324a626be | ||
|
|
4c5421916a | ||
|
|
2801bd99b8 | ||
|
|
38a0eb3b52 | ||
|
|
607263b8be | ||
|
|
42b62e90ca | ||
|
|
f6f0b02333 | ||
|
|
1858e7e8ac | ||
|
|
482ad74c57 | ||
|
|
68ae40ee56 | ||
|
|
24f8a88625 | ||
|
|
fc78769e9c | ||
|
|
5c54d3690c | ||
|
|
2cd8b00610 | ||
|
|
7ee35bc80c | ||
|
|
cfbbce26c4 | ||
|
|
dfb26cabfc | ||
|
|
70f0cce340 | ||
|
|
3af0c37c6b | ||
|
|
b1906a778e | ||
|
|
d752aa3481 | ||
|
|
afc5191644 | ||
|
|
254b02ad6b | ||
|
|
885365a1c2 | ||
|
|
245be4cd63 | ||
|
|
ce413f2946 | ||
|
|
0d84ba406f | ||
|
|
9047b3c121 | ||
|
|
3ff9543106 | ||
|
|
46b4a5fc5a | ||
|
|
bd456568ed | ||
|
|
93fa0e1df5 | ||
|
|
fe4c5155eb | ||
|
|
24c435bb5f | ||
|
|
e6977b2c33 | ||
|
|
28f83f2af4 | ||
|
|
5a6e8a0a8c | ||
|
|
003da28699 | ||
|
|
e8dd969e78 | ||
|
|
acce2a217d | ||
|
|
b964c681f8 | ||
|
|
c6dcc57c5e | ||
|
|
cf8e1cfd0f | ||
|
|
78b40a1f66 | ||
|
|
2fe75f8296 | ||
|
|
b22363224f | ||
|
|
b3c92ed3f4 | ||
|
|
464b0a0f30 | ||
|
|
dfcc13c7e6 | ||
|
|
ba6cee6f81 | ||
|
|
bcdfd2150d | ||
|
|
d19d6bbcd9 | ||
|
|
4080fa9bdc | ||
|
|
ce091b0b63 | ||
|
|
136e930362 | ||
|
|
78dfe940ef | ||
|
|
690fbe83fd | ||
|
|
f98e8f3e04 | ||
|
|
63febef3ed | ||
|
|
2599ae45d6 | ||
|
|
bfb03621c2 | ||
|
|
be53bd5293 | ||
|
|
a429500b57 | ||
|
|
ec9fa00f46 | ||
|
|
6a001f2e6c | ||
|
|
8bde53cd0f | ||
|
|
090d7d7112 | ||
|
|
97c7c0742c | ||
|
|
90efbf1210 | ||
|
|
38506d27a1 | ||
|
|
8a693bc932 | ||
|
|
0e49bf5dee | ||
|
|
6a967948de | ||
|
|
0771fc14db | ||
|
|
e1614a280f | ||
|
|
ddf81c949b | ||
|
|
b906b2f625 | ||
|
|
3f2b473287 | ||
|
|
9a9430b5e1 | ||
|
|
d659200a42 | ||
|
|
cb630c69f0 | ||
|
|
9a812090a2 | ||
|
|
5b0278847d | ||
|
|
9d07bb2946 | ||
|
|
b27d314fa7 | ||
|
|
df666ff724 | ||
|
|
3709714339 | ||
|
|
deecf80f20 | ||
|
|
6ea66bc527 | ||
|
|
513c8d1a65 | ||
|
|
49f71f4e1e | ||
|
|
0c5258b43a | ||
|
|
a0506f009a | ||
|
|
9f93dae6f9 | ||
|
|
45cca35724 | ||
|
|
1c42513e44 | ||
|
|
f3e6f5e772 | ||
|
|
c6f44e7928 | ||
|
|
7b6b32db74 | ||
|
|
e39f9bef1f | ||
|
|
479b604c0e | ||
|
|
e7ef3c4b6d | ||
|
|
87cae1c3a7 | ||
|
|
562fc74481 | ||
|
|
51d8e9c43d | ||
|
|
e50a7a2e42 | ||
|
|
aaad250a77 | ||
|
|
699730b7f4 | ||
|
|
302cffba1c | ||
|
|
e299aa032d | ||
|
|
ca6f70746c | ||
|
|
2af1d95650 | ||
|
|
df6f5d83d6 | ||
|
|
42baa3e1bc | ||
|
|
5f393babd6 | ||
|
|
b864563f47 | ||
|
|
3edb2d08ba | ||
|
|
d44f923277 | ||
|
|
ec468431b4 | ||
|
|
4774f438a9 | ||
|
|
f40659a7b4 | ||
|
|
047989abcf | ||
|
|
3e79b67032 | ||
|
|
ca4b1e6ae0 | ||
|
|
b56749426b | ||
|
|
cbe6e1caad | ||
|
|
748eb9ff12 | ||
|
|
385b98ff3d | ||
|
|
c12a50544e | ||
|
|
c64e953174 | ||
|
|
ccc599c83e | ||
|
|
a45064257a | ||
|
|
9510ba07f7 | ||
|
|
8e3dc76dd7 | ||
|
|
451332b2e7 | ||
|
|
445c798bbc | ||
|
|
c48c4d4283 | ||
|
|
b421d0c5cc | ||
|
|
f7454a4284 | ||
|
|
9144f4ea7b | ||
|
|
aaea367fba | ||
|
|
e0e878cbb1 | ||
|
|
b905a18161 | ||
|
|
f4ae7ecbe7 | ||
|
|
9a8812d00b | ||
|
|
13b3de683a | ||
|
|
64243d1437 | ||
|
|
a730c88491 | ||
|
|
bd90cc4134 | ||
|
|
316f0537c4 | ||
|
|
7f739065e8 | ||
|
|
bd83ed8130 | ||
|
|
e39ffbc83c | ||
|
|
1471e9b8e2 | ||
|
|
4c23d51be5 | ||
|
|
412cfb24d2 | ||
|
|
2a5977e97f | ||
|
|
64c34b7029 | ||
|
|
0db0abe608 | ||
|
|
5f4903a279 | ||
|
|
20ff79abf4 | ||
|
|
3a321d64f6 | ||
|
|
7e8d1f7974 | ||
|
|
9f41461209 | ||
|
|
6b10045b7b | ||
|
|
9ca6d0d893 | ||
|
|
2830049a53 | ||
|
|
50558de591 | ||
|
|
80e3e8a01e | ||
|
|
d38780c94d | ||
|
|
801435e57c | ||
|
|
8001efe6ab | ||
|
|
909a3cef9b | ||
|
|
9ac510a1ad | ||
|
|
00ce302b38 | ||
|
|
0dcc7a05f7 | ||
|
|
54c2769d8a | ||
|
|
2e400d88d3 | ||
|
|
d9aa660253 | ||
|
|
ba1dade4b0 | ||
|
|
a48649987e | ||
|
|
022c0a1327 | ||
|
|
69ceed5bbc | ||
|
|
b3fcb4ef36 | ||
|
|
8342b2d275 | ||
|
|
36888f844f | ||
|
|
75f220c3d9 | ||
|
|
1a784fc678 | ||
|
|
dac9017df1 | ||
|
|
7b3b5a1463 | ||
|
|
b7fc3f67d7 | ||
|
|
e0bfaad3a2 | ||
|
|
24c77a8956 | ||
|
|
380a0d1f86 | ||
|
|
b7f6fc9a2d | ||
|
|
e12fe974b2 | ||
|
|
b15623d435 | ||
|
|
eb8f709943 | ||
|
|
c93ddf6aac | ||
|
|
6e34360f7e | ||
|
|
c9d07cd0f8 | ||
|
|
9ff6b57b94 | ||
|
|
fb49b0ca27 | ||
|
|
fef1f80570 | ||
|
|
38cb1b195d | ||
|
|
ebdbe4a8d6 | ||
|
|
ba02a5c46a | ||
|
|
a6f379a17a | ||
|
|
a41b7b62ac | ||
|
|
5010c9033b | ||
|
|
93e4161d5e | ||
|
|
e0d6faf45b | ||
|
|
fbe4e3f0ec | ||
|
|
8e02c50f7d | ||
|
|
837485974a | ||
|
|
3cf739eca9 | ||
|
|
cfee688feb | ||
|
|
30d8894c30 | ||
|
|
0b86feeeb5 | ||
|
|
434ef34378 | ||
|
|
166c28c215 | ||
|
|
17c514e851 | ||
|
|
f7489592d6 | ||
|
|
3cb9312805 | ||
|
|
3722486b19 | ||
|
|
57b3982346 | ||
|
|
a8807bc915 | ||
|
|
71deaa48af | ||
|
|
e7ca35a276 | ||
|
|
2d8f43bd8c | ||
|
|
383acf0ffc | ||
|
|
ee156fc6a8 | ||
|
|
680a9a7ca7 | ||
|
|
7de8d6f9ac | ||
|
|
d79fab8b3c | ||
|
|
0cb32181c5 | ||
|
|
dba3c39726 | ||
|
|
95b4435396 | ||
|
|
f1a9884011 | ||
|
|
691dcb8ae1 | ||
|
|
db6b571f60 | ||
|
|
5ce1b00291 | ||
|
|
8332ba8450 | ||
|
|
b1c4524612 | ||
|
|
9a857659ce | ||
|
|
68dc00be27 | ||
|
|
ee00f12131 | ||
|
|
7444f17c4e | ||
|
|
99e70f7783 | ||
|
|
578833446d | ||
|
|
4fae827f1e | ||
|
|
98180d3a9e | ||
|
|
434a4af9ef | ||
|
|
298215542e | ||
|
|
197b3c1cb5 | ||
|
|
3cad89f299 | ||
|
|
99b9a46428 | ||
|
|
56a5363eb9 | ||
|
|
b1c95d719a | ||
|
|
d87ea056c6 | ||
|
|
34534a9653 | ||
|
|
5d0222b1c1 | ||
|
|
b72260f420 | ||
|
|
896eee9841 | ||
|
|
0d96657c33 | ||
|
|
41078869a9 | ||
|
|
89b11ef084 | ||
|
|
26d3995424 | ||
|
|
b6fad35146 | ||
|
|
70bf328e7d | ||
|
|
404538c989 | ||
|
|
9c9fc9e881 | ||
|
|
1d089366ff | ||
|
|
fe40464e33 | ||
|
|
728b1efb9a | ||
|
|
175f3d7a38 | ||
|
|
ae1fb8841a | ||
|
|
16ba20f898 | ||
|
|
d8ffc114d3 | ||
|
|
23bd76a8dd | ||
|
|
d85981cca0 | ||
|
|
7b466e0643 | ||
|
|
d984c5924d | ||
|
|
cfa3352caf | ||
|
|
05d2fc819c | ||
|
|
cb930a89ce |
2
.github/stale.yml
vendored
@@ -3,7 +3,7 @@ daysUntilStale: 180
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels: []
|
||||
exemptLabels: [ "enhancement" ]
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
14
.github/workflows/mac.yml
vendored
@@ -205,6 +205,16 @@ jobs:
|
||||
cd $LibrariesPath
|
||||
sudo cp -R opus-cache/. /
|
||||
|
||||
- name: Rnnoise.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone $GIT/desktop-app/rnnoise.git
|
||||
mkdir -p rnnoise/out/Debug
|
||||
cd rnnoise/out/Debug
|
||||
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ../..
|
||||
ninja
|
||||
|
||||
- name: Libiconv cache.
|
||||
id: cache-libiconv
|
||||
uses: actions/cache@v2
|
||||
@@ -240,7 +250,7 @@ jobs:
|
||||
|
||||
git clone $GIT/FFmpeg/FFmpeg.git ffmpeg
|
||||
cd ffmpeg
|
||||
git checkout release/4.2
|
||||
git checkout release/4.4
|
||||
CFLAGS=`freetype-config --cflags`
|
||||
LDFLAGS=`freetype-config --libs`
|
||||
PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig:/usr/X11/lib/pkgconfig
|
||||
@@ -317,7 +327,6 @@ jobs:
|
||||
--enable-decoder=pcm_u32be \
|
||||
--enable-decoder=pcm_u32le \
|
||||
--enable-decoder=pcm_u8 \
|
||||
--enable-decoder=pcm_zork \
|
||||
--enable-decoder=vorbis \
|
||||
--enable-decoder=wavpack \
|
||||
--enable-decoder=wmalossless \
|
||||
@@ -490,6 +499,7 @@ jobs:
|
||||
cmake -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DTG_OWT_SPECIAL_TARGET=mac \
|
||||
-DTG_OWT_BUILD_AUDIO_BACKENDS=OFF \
|
||||
-DTG_OWT_LIBJPEG_INCLUDE_PATH=$PREFIX/include \
|
||||
-DTG_OWT_OPENSSL_INCLUDE_PATH=`pwd`/../../../openssl_$OPENSSL_VER/include \
|
||||
-DTG_OWT_OPUS_INCLUDE_PATH=$PREFIX/include/opus \
|
||||
|
||||
24
.github/workflows/win.yml
vendored
@@ -114,6 +114,11 @@ jobs:
|
||||
- name: Choco installs.
|
||||
run: choco install --no-progress -y nasm yasm jom ninja
|
||||
|
||||
- name: NuGet sources.
|
||||
run: |
|
||||
nuget sources Disable -Name "Microsoft Visual Studio Offline Packages"
|
||||
nuget sources Add -Source https://api.nuget.org/v3/index.json & exit 0
|
||||
|
||||
- name: Patches.
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -279,6 +284,17 @@ jobs:
|
||||
msbuild -m opus.sln /property:Configuration=Debug /property:Platform="Win32"
|
||||
msbuild -m opus.sln /property:Configuration=Release /property:Platform="Win32"
|
||||
|
||||
- name: Rnnoise.
|
||||
shell: cmd
|
||||
run: |
|
||||
%VC%
|
||||
|
||||
git clone %GIT%/desktop-app/rnnoise.git
|
||||
mkdir rnnoise\out
|
||||
cd rnnoise\out
|
||||
cmake -A Win32 ..
|
||||
cmake --build . --config Debug
|
||||
|
||||
- name: FFmpeg cache.
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v2
|
||||
@@ -293,7 +309,7 @@ jobs:
|
||||
|
||||
git clone %GIT%/FFmpeg/FFmpeg.git ffmpeg
|
||||
cd ffmpeg
|
||||
git checkout release/4.2
|
||||
git checkout release/4.4
|
||||
set CHERE_INVOKING=enabled_from_arguments
|
||||
set MSYS2_PATH_TYPE=inherit
|
||||
call c:\tools\msys64\usr\bin\bash --login ../patches/build_ffmpeg_win.sh
|
||||
@@ -332,7 +348,7 @@ jobs:
|
||||
-confirm-license ^
|
||||
-static ^
|
||||
-static-runtime -I "%SSL%\include" ^
|
||||
-no-opengl ^
|
||||
-opengl dynamic ^
|
||||
-openssl-linked ^
|
||||
OPENSSL_LIBS_DEBUG="%SSL%\out32.dbg\libssl.lib %SSL%\out32.dbg\%LIBS%" ^
|
||||
OPENSSL_LIBS_RELEASE="%SSL%\out32\libssl.lib %SSL%\out32\%LIBS%" ^
|
||||
@@ -372,6 +388,7 @@ jobs:
|
||||
cmake -G Ninja ^
|
||||
-DCMAKE_BUILD_TYPE=Debug ^
|
||||
-DTG_OWT_SPECIAL_TARGET=win ^
|
||||
-DTG_OWT_BUILD_AUDIO_BACKENDS=OFF ^
|
||||
-DTG_OWT_LIBJPEG_INCLUDE_PATH=%cd%/../../../mozjpeg ^
|
||||
-DTG_OWT_OPENSSL_INCLUDE_PATH=%cd%/../../../openssl_%OPENSSL_VER%/include ^
|
||||
-DTG_OWT_OPUS_INCLUDE_PATH=%cd%/../../../opus/include ^
|
||||
@@ -400,6 +417,9 @@ jobs:
|
||||
fi
|
||||
echo "TDESKTOP_BUILD_DEFINE=$DEFINE" >> $GITHUB_ENV
|
||||
|
||||
- name: Free up some disk space.
|
||||
run: del /S *.pdb
|
||||
|
||||
- name: Telegram Desktop build.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
run: |
|
||||
|
||||
6
.gitmodules
vendored
@@ -91,3 +91,9 @@
|
||||
[submodule "Telegram/lib_webview"]
|
||||
path = Telegram/lib_webview
|
||||
url = https://github.com/desktop-app/lib_webview.git
|
||||
[submodule "Telegram/ThirdParty/mallocng"]
|
||||
path = Telegram/ThirdParty/mallocng
|
||||
url = https://github.com/desktop-app/mallocng.git
|
||||
[submodule "Telegram/lib_waylandshells"]
|
||||
path = Telegram/lib_waylandshells
|
||||
url = https://github.com/desktop-app/lib_waylandshells.git
|
||||
|
||||
@@ -15,7 +15,8 @@ The source code is published under GPLv3 with OpenSSL exception, the license is
|
||||
|
||||
The latest version is available for
|
||||
|
||||
* [Windows 7 and above](https://telegram.org/dl/desktop/win) ([portable](https://telegram.org/dl/desktop/win_portable))
|
||||
* [Windows 7 and above (64 bit)](https://telegram.org/dl/desktop/win64) ([portable](https://telegram.org/dl/desktop/win64_portable))
|
||||
* [Windows 7 and above (32 bit)](https://telegram.org/dl/desktop/win) ([portable](https://telegram.org/dl/desktop/win_portable))
|
||||
* [macOS 10.12 and above](https://telegram.org/dl/desktop/mac)
|
||||
* [Linux static build for 64 bit](https://telegram.org/dl/desktop/linux)
|
||||
* [Snap](https://snapcraft.io/telegram-desktop)
|
||||
@@ -52,7 +53,7 @@ Version **1.8.15** was the last that supports older systems
|
||||
* Guideline Support Library ([MIT License](https://github.com/Microsoft/GSL/blob/master/LICENSE))
|
||||
* Range-v3 ([Boost License](https://github.com/ericniebler/range-v3/blob/master/LICENSE.txt))
|
||||
* Open Sans font ([Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html))
|
||||
* Vazir font ([License](https://github.com/rastikerdar/vazir-font/blob/master/LICENSE))
|
||||
* Vazir font ([SIL Open Font License 1.1](https://github.com/rastikerdar/vazir-font/blob/master/OFL.txt))
|
||||
* Emoji alpha codes ([MIT License](https://github.com/emojione/emojione/blob/master/extras/alpha-codes/LICENSE.md))
|
||||
* Catch test framework ([Boost License](https://github.com/philsquared/Catch/blob/master/LICENSE.txt))
|
||||
* xxHash ([BSD License](https://github.com/Cyan4973/xxHash/blob/dev/LICENSE))
|
||||
|
||||
@@ -18,6 +18,9 @@ endif()
|
||||
add_subdirectory(lib_storage)
|
||||
add_subdirectory(lib_lottie)
|
||||
add_subdirectory(lib_qr)
|
||||
if (LINUX AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
add_subdirectory(lib_waylandshells)
|
||||
endif()
|
||||
add_subdirectory(lib_webrtc)
|
||||
add_subdirectory(lib_webview)
|
||||
add_subdirectory(codegen)
|
||||
@@ -89,6 +92,7 @@ elseif (LINUX)
|
||||
PRIVATE
|
||||
desktop-app::external_glibmm
|
||||
desktop-app::external_glib
|
||||
desktop-app::external_mallocng
|
||||
)
|
||||
|
||||
if (NOT DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
|
||||
@@ -109,6 +113,7 @@ elseif (LINUX)
|
||||
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
desktop-app::lib_waylandshells
|
||||
desktop-app::external_kwayland
|
||||
)
|
||||
endif()
|
||||
@@ -271,23 +276,39 @@ PRIVATE
|
||||
boxes/url_auth_box.h
|
||||
boxes/username_box.cpp
|
||||
boxes/username_box.h
|
||||
calls/group/calls_choose_join_as.cpp
|
||||
calls/group/calls_choose_join_as.h
|
||||
calls/group/calls_group_call.cpp
|
||||
calls/group/calls_group_call.h
|
||||
calls/group/calls_group_common.h
|
||||
calls/group/calls_group_invite_controller.cpp
|
||||
calls/group/calls_group_invite_controller.h
|
||||
calls/group/calls_group_members.cpp
|
||||
calls/group/calls_group_members.h
|
||||
calls/group/calls_group_members_row.cpp
|
||||
calls/group/calls_group_members_row.h
|
||||
calls/group/calls_group_menu.cpp
|
||||
calls/group/calls_group_menu.h
|
||||
calls/group/calls_group_panel.cpp
|
||||
calls/group/calls_group_panel.h
|
||||
calls/group/calls_group_settings.cpp
|
||||
calls/group/calls_group_settings.h
|
||||
calls/group/calls_group_toasts.cpp
|
||||
calls/group/calls_group_toasts.h
|
||||
calls/group/calls_group_viewport.cpp
|
||||
calls/group/calls_group_viewport.h
|
||||
calls/group/calls_group_viewport_opengl.cpp
|
||||
calls/group/calls_group_viewport_opengl.h
|
||||
calls/group/calls_group_viewport_raster.cpp
|
||||
calls/group/calls_group_viewport_raster.h
|
||||
calls/group/calls_group_viewport_tile.cpp
|
||||
calls/group/calls_group_viewport_tile.h
|
||||
calls/group/calls_volume_item.cpp
|
||||
calls/group/calls_volume_item.h
|
||||
calls/calls_box_controller.cpp
|
||||
calls/calls_box_controller.h
|
||||
calls/calls_call.cpp
|
||||
calls/calls_call.h
|
||||
calls/calls_choose_join_as.cpp
|
||||
calls/calls_choose_join_as.h
|
||||
calls/calls_group_call.cpp
|
||||
calls/calls_group_call.h
|
||||
calls/calls_group_common.h
|
||||
calls/calls_group_members.cpp
|
||||
calls/calls_group_members.h
|
||||
calls/calls_group_menu.cpp
|
||||
calls/calls_group_menu.h
|
||||
calls/calls_group_panel.cpp
|
||||
calls/calls_group_panel.h
|
||||
calls/calls_group_settings.cpp
|
||||
calls/calls_group_settings.h
|
||||
calls/calls_emoji_fingerprint.cpp
|
||||
calls/calls_emoji_fingerprint.h
|
||||
calls/calls_instance.cpp
|
||||
@@ -302,8 +323,8 @@ PRIVATE
|
||||
calls/calls_userpic.h
|
||||
calls/calls_video_bubble.cpp
|
||||
calls/calls_video_bubble.h
|
||||
calls/calls_volume_item.cpp
|
||||
calls/calls_volume_item.h
|
||||
calls/calls_video_incoming.cpp
|
||||
calls/calls_video_incoming.h
|
||||
chat_helpers/bot_keyboard.cpp
|
||||
chat_helpers/bot_keyboard.h
|
||||
chat_helpers/emoji_keywords.cpp
|
||||
@@ -708,6 +729,8 @@ PRIVATE
|
||||
main/main_session.h
|
||||
main/main_session_settings.cpp
|
||||
main/main_session_settings.h
|
||||
media/system_media_controls_manager.h
|
||||
media/system_media_controls_manager.cpp
|
||||
media/audio/media_audio.cpp
|
||||
media/audio/media_audio.h
|
||||
media/audio/media_audio_capture.cpp
|
||||
@@ -762,10 +785,20 @@ PRIVATE
|
||||
media/streaming/media_streaming_video_track.h
|
||||
media/view/media_view_group_thumbs.cpp
|
||||
media/view/media_view_group_thumbs.h
|
||||
media/view/media_view_overlay_opengl.cpp
|
||||
media/view/media_view_overlay_opengl.h
|
||||
media/view/media_view_overlay_raster.cpp
|
||||
media/view/media_view_overlay_raster.h
|
||||
media/view/media_view_overlay_renderer.h
|
||||
media/view/media_view_overlay_widget.cpp
|
||||
media/view/media_view_overlay_widget.h
|
||||
media/view/media_view_pip.cpp
|
||||
media/view/media_view_pip.h
|
||||
media/view/media_view_pip_opengl.cpp
|
||||
media/view/media_view_pip_opengl.h
|
||||
media/view/media_view_pip_raster.cpp
|
||||
media/view/media_view_pip_raster.h
|
||||
media/view/media_view_pip_renderer.h
|
||||
media/view/media_view_playback_controls.cpp
|
||||
media/view/media_view_playback_controls.h
|
||||
media/view/media_view_playback_progress.cpp
|
||||
@@ -830,15 +863,15 @@ PRIVATE
|
||||
platform/linux/linux_gsd_media_keys.h
|
||||
platform/linux/linux_gtk_file_dialog.cpp
|
||||
platform/linux/linux_gtk_file_dialog.h
|
||||
platform/linux/linux_gtk_integration_dummy.cpp
|
||||
platform/linux/linux_gtk_integration_p.h
|
||||
platform/linux/linux_gtk_integration.cpp
|
||||
platform/linux/linux_gtk_integration.h
|
||||
platform/linux/linux_gtk_open_with_dialog.cpp
|
||||
platform/linux/linux_gtk_open_with_dialog.h
|
||||
platform/linux/linux_mpris_support.cpp
|
||||
platform/linux/linux_mpris_support.h
|
||||
platform/linux/linux_notification_service_watcher.cpp
|
||||
platform/linux/linux_notification_service_watcher.h
|
||||
platform/linux/linux_wayland_integration_dummy.cpp
|
||||
platform/linux/linux_wayland_integration.cpp
|
||||
platform/linux/linux_wayland_integration.h
|
||||
platform/linux/linux_xdp_file_dialog.cpp
|
||||
@@ -851,6 +884,7 @@ PRIVATE
|
||||
platform/linux/launcher_linux.h
|
||||
platform/linux/main_window_linux.cpp
|
||||
platform/linux/main_window_linux.h
|
||||
platform/linux/notifications_manager_linux_dummy.cpp
|
||||
platform/linux/notifications_manager_linux.cpp
|
||||
platform/linux/notifications_manager_linux.h
|
||||
platform/linux/specific_linux.cpp
|
||||
@@ -860,6 +894,7 @@ PRIVATE
|
||||
platform/mac/file_utilities_mac.h
|
||||
platform/mac/launcher_mac.mm
|
||||
platform/mac/launcher_mac.h
|
||||
platform/mac/mac_iconv_helper.c
|
||||
platform/mac/main_window_mac.mm
|
||||
platform/mac/main_window_mac.h
|
||||
platform/mac/notifications_manager_mac.mm
|
||||
@@ -1036,6 +1071,8 @@ PRIVATE
|
||||
ui/search_field_controller.h
|
||||
ui/special_buttons.cpp
|
||||
ui/special_buttons.h
|
||||
ui/text/format_song_document_name.cpp
|
||||
ui/text/format_song_document_name.h
|
||||
ui/unread_badge.cpp
|
||||
ui/unread_badge.h
|
||||
window/main_window.cpp
|
||||
@@ -1135,16 +1172,20 @@ if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
|
||||
platform/linux/linux_xdp_open_with_dialog.h
|
||||
platform/linux/notifications_manager_linux.cpp
|
||||
)
|
||||
|
||||
nice_target_sources(Telegram ${src_loc}
|
||||
PRIVATE
|
||||
else()
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/notifications_manager_linux_dummy.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
remove_target_sources(Telegram ${src_loc} platform/linux/linux_wayland_integration.cpp)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/linux/linux_wayland_integration_dummy.cpp)
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/linux_wayland_integration.cpp
|
||||
)
|
||||
else()
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/linux_wayland_integration_dummy.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (DESKTOP_APP_DISABLE_GTK_INTEGRATION)
|
||||
@@ -1158,15 +1199,16 @@ if (DESKTOP_APP_DISABLE_GTK_INTEGRATION)
|
||||
platform/linux/linux_gtk_open_with_dialog.cpp
|
||||
platform/linux/linux_gtk_open_with_dialog.h
|
||||
)
|
||||
|
||||
nice_target_sources(Telegram ${src_loc}
|
||||
PRIVATE
|
||||
else()
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/linux/linux_gtk_integration_dummy.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/mac/mac_iconv_helper.c)
|
||||
if (DESKTOP_APP_USE_PACKAGED)
|
||||
remove_target_sources(Telegram ${src_loc}
|
||||
platform/mac/mac_iconv_helper.c
|
||||
)
|
||||
endif()
|
||||
|
||||
nice_target_sources(Telegram ${res_loc}
|
||||
@@ -1202,8 +1244,6 @@ if (WIN32)
|
||||
# $<IF:${release},"Appending compatibility manifest.","Finalizing build.">
|
||||
# )
|
||||
elseif (APPLE)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::external_sp_media_key_tap)
|
||||
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)
|
||||
endif()
|
||||
|
||||
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 932 B |
BIN
Telegram/Resources/icons/calls/calls_more.png
Normal file
|
After Width: | Height: | Size: 277 B |
BIN
Telegram/Resources/icons/calls/calls_more@2x.png
Normal file
|
After Width: | Height: | Size: 444 B |
BIN
Telegram/Resources/icons/calls/calls_more@3x.png
Normal file
|
After Width: | Height: | Size: 653 B |
BIN
Telegram/Resources/icons/calls/calls_present.png
Normal file
|
After Width: | Height: | Size: 553 B |
BIN
Telegram/Resources/icons/calls/calls_present@2x.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
Telegram/Resources/icons/calls/calls_present@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/calls/calls_settings.png
Normal file
|
After Width: | Height: | Size: 756 B |
BIN
Telegram/Resources/icons/calls/calls_settings@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/calls/calls_settings@3x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Telegram/Resources/icons/calls/video_back.png
Normal file
|
After Width: | Height: | Size: 294 B |
BIN
Telegram/Resources/icons/calls/video_back@2x.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
Telegram/Resources/icons/calls/video_back@3x.png
Normal file
|
After Width: | Height: | Size: 790 B |
BIN
Telegram/Resources/icons/calls/video_large_paused.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/calls/video_large_paused@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Telegram/Resources/icons/calls/video_large_paused@3x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
Telegram/Resources/icons/calls/video_mini_mute.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
Telegram/Resources/icons/calls/video_mini_mute@2x.png
Normal file
|
After Width: | Height: | Size: 761 B |
BIN
Telegram/Resources/icons/calls/video_mini_mute@3x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/calls/video_mini_screencast.png
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
Telegram/Resources/icons/calls/video_mini_screencast@2x.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
Telegram/Resources/icons/calls/video_mini_screencast@3x.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
Telegram/Resources/icons/calls/video_mini_speak.png
Normal file
|
After Width: | Height: | Size: 560 B |
BIN
Telegram/Resources/icons/calls/video_mini_speak@2x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Telegram/Resources/icons/calls/video_mini_speak@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/calls/video_mini_video.png
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
Telegram/Resources/icons/calls/video_mini_video@2x.png
Normal file
|
After Width: | Height: | Size: 480 B |
BIN
Telegram/Resources/icons/calls/video_mini_video@3x.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
Telegram/Resources/icons/calls/video_over_mute.png
Normal file
|
After Width: | Height: | Size: 596 B |
BIN
Telegram/Resources/icons/calls/video_over_mute@2x.png
Normal file
|
After Width: | Height: | Size: 965 B |
BIN
Telegram/Resources/icons/calls/video_over_mute@3x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/calls/video_over_pin.png
Normal file
|
After Width: | Height: | Size: 413 B |
BIN
Telegram/Resources/icons/calls/video_over_pin@2x.png
Normal file
|
After Width: | Height: | Size: 672 B |
BIN
Telegram/Resources/icons/calls/video_over_pin@3x.png
Normal file
|
After Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 854 B After Width: | Height: | Size: 664 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -451,6 +451,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_system_integration" = "System integration";
|
||||
"lng_settings_performance" = "Performance";
|
||||
"lng_settings_enable_animations" = "Enable animations";
|
||||
"lng_settings_enable_opengl" = "Enable OpenGL rendering for media";
|
||||
"lng_settings_sensitive_title" = "Sensitive content";
|
||||
"lng_settings_sensitive_disable_filtering" = "Disable filtering";
|
||||
"lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices.";
|
||||
@@ -1112,6 +1113,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_group_call_scheduled_group" = "{from} scheduled a voice chat for {date}";
|
||||
"lng_action_group_call_scheduled_channel" = "Voice chat scheduled for {date}";
|
||||
"lng_action_group_call_finished" = "Voice chat finished ({duration})";
|
||||
"lng_action_group_call_finished_group" = "{from} ended the voice chat ({duration})";
|
||||
"lng_action_add_user" = "{from} added {user}";
|
||||
"lng_action_add_users_many" = "{from} added {users}";
|
||||
"lng_action_add_users_and_one" = "{accumulated}, {user}";
|
||||
@@ -1400,6 +1402,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_stickers_search_sets" = "Search sticker sets";
|
||||
"lng_stickers_nothing_found" = "No stickers found";
|
||||
"lng_stickers_remove_pack_confirm" = "Remove";
|
||||
"lng_stickers_archive_pack" = "Archive Stickers";
|
||||
"lng_stickers_has_been_archived" = "Sticker pack has been archived.";
|
||||
|
||||
"lng_in_dlg_photo" = "Photo";
|
||||
"lng_in_dlg_album" = "Album";
|
||||
@@ -1995,8 +1999,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_raised_hand_status" = "wants to speak";
|
||||
"lng_group_call_settings" = "Settings";
|
||||
"lng_group_call_share_button" = "Share";
|
||||
"lng_group_call_video" = "Video";
|
||||
"lng_group_call_screen_share_start" = "Share Screen";
|
||||
"lng_group_call_screen_share_stop" = "Stop Sharing";
|
||||
"lng_group_call_screen_title" = "Screen {index}";
|
||||
"lng_group_call_unmute_small" = "Unmute";
|
||||
"lng_group_call_more" = "More";
|
||||
"lng_group_call_unmute" = "Unmute";
|
||||
"lng_group_call_unmute_sub" = "or hold spacebar to talk";
|
||||
"lng_group_call_unmute_sub" = "Hold space bar to temporarily unmute.";
|
||||
"lng_group_call_you_are_live" = "You are Live";
|
||||
"lng_group_call_force_muted" = "Muted by admin";
|
||||
"lng_group_call_force_muted_sub" = "You are in Listen Only mode";
|
||||
@@ -2014,6 +2024,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?";
|
||||
"lng_group_call_create_sure_channel" = "Are you sure you want to start a voice chat in this channel as your personal account?";
|
||||
"lng_group_call_join_sure_personal" = "Are you sure you want to join this voice chat as your personal account?";
|
||||
"lng_group_call_muted_no_camera" = "You can't turn on video while you're muted by admin.";
|
||||
"lng_group_call_muted_no_screen" = "You can't share your screen while you're muted by admin.";
|
||||
"lng_group_call_chat_no_camera" = "You can't turn on video in this chat.";
|
||||
"lng_group_call_chat_no_screen" = "You can't share your screen in this chat.";
|
||||
"lng_group_call_failed_screen" = "An error occured. Screencast has stopped.";
|
||||
"lng_group_call_tooltip_screen" = "Share screen";
|
||||
"lng_group_call_tooltip_camera" = "Your camera is off. Click here to enable camera.";
|
||||
"lng_group_call_tooltip_microphone" = "You are on mute. Click here to speak.";
|
||||
"lng_group_call_tooltip_camera_off" = "Disable camera";
|
||||
"lng_group_call_tooltip_force_muted" = "Muted by admin. Click if you want to speak.";
|
||||
"lng_group_call_tooltip_raised_hand" = "You asked to speak. We let the speakers know.";
|
||||
"lng_group_call_also_end" = "End voice chat";
|
||||
"lng_group_call_settings_title" = "Settings";
|
||||
"lng_group_call_invite" = "Invite Member";
|
||||
@@ -2056,6 +2077,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_context_remove_hand" = "Cancel request to speak";
|
||||
"lng_group_call_context_mute_for_me" = "Mute for me";
|
||||
"lng_group_call_context_unmute_for_me" = "Unmute for me";
|
||||
"lng_group_call_context_pin_camera" = "Pin video";
|
||||
"lng_group_call_context_unpin_camera" = "Unpin video";
|
||||
"lng_group_call_context_pin_screen" = "Pin screencast";
|
||||
"lng_group_call_context_unpin_screen" = "Unpin screencast";
|
||||
"lng_group_call_context_remove" = "Remove";
|
||||
"lng_group_call_remove_channel" = "Remove {channel} from the voice chat?";
|
||||
"lng_group_call_duration_days#one" = "{count} day";
|
||||
@@ -2069,6 +2094,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_mac_access" = "Telegram Desktop does not have access to system wide keyboard input required for Push to Talk.";
|
||||
"lng_group_call_mac_input" = "Please allow **Input Monitoring** for Telegram in Privacy Settings.";
|
||||
"lng_group_call_mac_accessibility" = "Please allow **Accessibility** for Telegram in Privacy Settings.\n\nApp restart may be required.";
|
||||
"lng_group_call_mac_screencast_access" = "Telegram Desktop does not have access to screen recording required for Screen Sharing.";
|
||||
"lng_group_call_mac_recording" = "Please allow **Screen Recording** for Telegram in Privacy Settings.";
|
||||
"lng_group_call_mac_settings" = "Open Settings";
|
||||
|
||||
"lng_group_call_start_as_header" = "Start Voice Chat as...";
|
||||
@@ -2105,6 +2132,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_recording_started" = "Voice chat recording started.";
|
||||
"lng_group_call_recording_stopped" = "Voice chat recording stopped.";
|
||||
"lng_group_call_recording_saved" = "Audio saved to Saved Messages.";
|
||||
"lng_group_call_pinned_camera_me" = "Your video is pinned.";
|
||||
"lng_group_call_pinned_screen_me" = "Your screencast is pinned.";
|
||||
"lng_group_call_pinned_camera" = "{user}'s video is pinned.";
|
||||
"lng_group_call_pinned_screen" = "{user}'s screencast is pinned.";
|
||||
"lng_group_call_unpinned_camera_me" = "Your video is unpinned.";
|
||||
"lng_group_call_unpinned_screen_me" = "Your screencast is unpinned.";
|
||||
"lng_group_call_unpinned_camera" = "{user}'s video is unpinned.";
|
||||
"lng_group_call_unpinned_screen" = "{user}'s screencast is unpinned.";
|
||||
"lng_group_call_sure_screencast" = "{user} is screensharing. This action will make your screencast pinned for all participants.";
|
||||
"lng_group_call_recording_start_sure" = "Do you want to start recording this chat and save the result into an audio file?\n\nOther members will see the chat is being recorded.";
|
||||
"lng_group_call_recording_stop_sure" = "Do you want to stop recording this chat?";
|
||||
"lng_group_call_recording_start_field" = "Recording Title";
|
||||
@@ -2771,6 +2807,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_mac_menu_show" = "Show Telegram";
|
||||
"lng_mac_menu_emoji_and_symbols" = "Emoji & Symbols";
|
||||
|
||||
"lng_mac_menu_player_pause" = "Pause";
|
||||
"lng_mac_menu_player_resume" = "Resume";
|
||||
"lng_mac_menu_player_next" = "Next";
|
||||
"lng_mac_menu_player_previous" = "Previous";
|
||||
"lng_mac_menu_player_stop" = "Stop";
|
||||
|
||||
"lng_mac_touchbar_favorite_stickers" = "Favorite stickers";
|
||||
|
||||
// Keys finished
|
||||
|
||||
@@ -68,7 +68,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro
|
||||
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#f4e096c3 flags:# multiple_allowed:flags.1?true can_forward:flags.2?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
|
||||
inputMediaInvoice#d9799874 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string = InputMedia;
|
||||
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
|
||||
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
|
||||
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
|
||||
@@ -223,7 +223,7 @@ peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bo
|
||||
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true geo_distance:flags.6?int = PeerSettings;
|
||||
|
||||
wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;
|
||||
wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
|
||||
wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
|
||||
|
||||
inputReportReasonSpam#58dbcab8 = ReportReason;
|
||||
inputReportReasonViolence#1e22c78d = ReportReason;
|
||||
@@ -356,7 +356,7 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector<int> = Update;
|
||||
updateTheme#8216fba3 theme:Theme = Update;
|
||||
updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;
|
||||
updateLoginToken#564fe691 = Update;
|
||||
updateMessagePollVote#42f88f2c poll_id:long user_id:int options:Vector<bytes> = Update;
|
||||
updateMessagePollVote#37f69f0b poll_id:long user_id:int options:Vector<bytes> qts:int = Update;
|
||||
updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;
|
||||
updateDialogFilterOrder#a5d72105 order:Vector<int> = Update;
|
||||
updateDialogFilters#3504914f = Update;
|
||||
@@ -375,6 +375,7 @@ updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update;
|
||||
updateChatParticipant#f3b3781f flags:# chat_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update;
|
||||
updateChannelParticipant#7fecb1ec flags:# channel_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update;
|
||||
updateBotStopped#7f9488a user_id:int date:int stopped:Bool qts:int = Update;
|
||||
updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update;
|
||||
|
||||
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
|
||||
|
||||
@@ -405,7 +406,7 @@ config#330b4067 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:fla
|
||||
|
||||
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
|
||||
|
||||
help.appUpdate#1da7158f flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector<MessageEntity> document:flags.1?Document url:flags.2?string = help.AppUpdate;
|
||||
help.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector<MessageEntity> document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate;
|
||||
help.noAppUpdate#c45a6536 = help.AppUpdate;
|
||||
|
||||
help.inviteText#18cb9f78 message:string = help.InviteText;
|
||||
@@ -649,7 +650,7 @@ inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:f
|
||||
inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaInvoice#d5348d85 flags:# multiple_allowed:flags.1?true can_forward:flags.3?true title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
|
||||
|
||||
inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult;
|
||||
inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult;
|
||||
@@ -1069,14 +1070,14 @@ chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags
|
||||
|
||||
inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper;
|
||||
inputWallPaperSlug#72091c80 slug:string = InputWallPaper;
|
||||
inputWallPaperNoFile#8427bbac = InputWallPaper;
|
||||
inputWallPaperNoFile#967a462e id:long = InputWallPaper;
|
||||
|
||||
account.wallPapersNotModified#1c199183 = account.WallPapers;
|
||||
account.wallPapers#702b65a9 hash:int wallpapers:Vector<WallPaper> = account.WallPapers;
|
||||
|
||||
codeSettings#debebe83 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true = CodeSettings;
|
||||
|
||||
wallPaperSettings#5086cf8 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings;
|
||||
wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings;
|
||||
|
||||
autoDownloadSettings#e04232f3 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:int file_size_max:int video_upload_maxbitrate:int = AutoDownloadSettings;
|
||||
|
||||
@@ -1204,11 +1205,11 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked;
|
||||
stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats;
|
||||
|
||||
groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall;
|
||||
groupCall#c95c6654 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true id:long access_hash:long participants_count:int params:flags.0?DataJSON title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall;
|
||||
groupCall#653dbaad flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall;
|
||||
|
||||
inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;
|
||||
|
||||
groupCallParticipant#b96b25ee flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long params:flags.6?DataJSON = GroupCallParticipant;
|
||||
groupCallParticipant#eba636fe flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo = GroupCallParticipant;
|
||||
|
||||
phone.groupCall#9e727aad call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string chats:Vector<Chat> users:Vector<User> = phone.GroupCall;
|
||||
|
||||
@@ -1245,6 +1246,10 @@ phone.joinAsPeers#afe5623f peers:Vector<Peer> chats:Vector<Chat> users:Vector<Us
|
||||
|
||||
phone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvite;
|
||||
|
||||
groupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector<int> = GroupCallParticipantVideoSourceGroup;
|
||||
|
||||
groupCallParticipantVideo#78e41663 flags:# paused:flags.0?true endpoint:string source_groups:Vector<GroupCallParticipantVideoSourceGroup> = GroupCallParticipantVideo;
|
||||
|
||||
---functions---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
@@ -1617,22 +1622,24 @@ phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhon
|
||||
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
|
||||
phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;
|
||||
phone.createGroupCall#48cdc6d8 flags:# peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates;
|
||||
phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates;
|
||||
phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates;
|
||||
phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;
|
||||
phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector<InputUser> = Updates;
|
||||
phone.discardGroupCall#7a777135 call:InputGroupCall = Updates;
|
||||
phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates;
|
||||
phone.getGroupCall#c7cb017 call:InputGroupCall = phone.GroupCall;
|
||||
phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector<InputPeer> sources:Vector<int> offset:string limit:int = phone.GroupParticipants;
|
||||
phone.checkGroupCall#b74a7bea call:InputGroupCall source:int = Bool;
|
||||
phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector<int> = Vector<int>;
|
||||
phone.toggleGroupCallRecord#c02a66d7 flags:# start:flags.0?true call:InputGroupCall title:flags.1?string = Updates;
|
||||
phone.editGroupCallParticipant#d975eb80 flags:# muted:flags.0?true call:InputGroupCall participant:InputPeer volume:flags.1?int raise_hand:flags.2?Bool = Updates;
|
||||
phone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates;
|
||||
phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates;
|
||||
phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers;
|
||||
phone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite;
|
||||
phone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates;
|
||||
phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates;
|
||||
phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool;
|
||||
phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates;
|
||||
phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates;
|
||||
|
||||
langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;
|
||||
langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;
|
||||
@@ -1649,4 +1656,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
|
||||
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
||||
|
||||
// LAYER 128
|
||||
// LAYER 129
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.7.3.0" />
|
||||
Version="2.7.7.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,7,3,0
|
||||
PRODUCTVERSION 2,7,3,0
|
||||
FILEVERSION 2,7,7,0
|
||||
PRODUCTVERSION 2,7,7,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.7.3.0"
|
||||
VALUE "FileVersion", "2.7.7.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.7.3.0"
|
||||
VALUE "ProductVersion", "2.7.7.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,7,3,0
|
||||
PRODUCTVERSION 2,7,3,0
|
||||
FILEVERSION 2,7,7,0
|
||||
PRODUCTVERSION 2,7,7,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.7.3.0"
|
||||
VALUE "FileVersion", "2.7.7.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.7.3.0"
|
||||
VALUE "ProductVersion", "2.7.7.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -47,6 +47,18 @@ typedef signed int int32;
|
||||
|
||||
namespace{
|
||||
|
||||
struct BIODeleter {
|
||||
void operator()(BIO *value) {
|
||||
BIO_free(value);
|
||||
}
|
||||
};
|
||||
|
||||
inline auto makeBIO(const void *buf, int len) {
|
||||
return std::unique_ptr<BIO, BIODeleter>{
|
||||
BIO_new_mem_buf(buf, len),
|
||||
};
|
||||
}
|
||||
|
||||
inline uint32 sha1Shift(uint32 v, uint32 shift) {
|
||||
return ((v << shift) | (v >> (32 - shift)));
|
||||
}
|
||||
@@ -430,7 +442,15 @@ int main(int argc, char *argv[])
|
||||
uint32 siglen = 0;
|
||||
|
||||
cout << "Signing..\n";
|
||||
RSA *prKey = PEM_read_bio_RSAPrivateKey(BIO_new_mem_buf(const_cast<char*>((BetaChannel || AlphaVersion) ? PrivateBetaKey : PrivateKey), -1), 0, 0, 0);
|
||||
RSA *prKey = [] {
|
||||
const auto bio = makeBIO(
|
||||
const_cast<char*>(
|
||||
(BetaChannel || AlphaVersion)
|
||||
? PrivateBetaKey
|
||||
: PrivateKey),
|
||||
-1);
|
||||
return PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0);
|
||||
}();
|
||||
if (!prKey) {
|
||||
cout << "Could not read RSA private key!\n";
|
||||
return -1;
|
||||
@@ -453,7 +473,15 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
|
||||
cout << "Checking signature..\n";
|
||||
RSA *pbKey = PEM_read_bio_RSAPublicKey(BIO_new_mem_buf(const_cast<char*>((BetaChannel || AlphaVersion) ? PublicBetaKey : PublicKey), -1), 0, 0, 0);
|
||||
RSA *pbKey = [] {
|
||||
const auto bio = makeBIO(
|
||||
const_cast<char*>(
|
||||
(BetaChannel || AlphaVersion)
|
||||
? PublicBetaKey
|
||||
: PublicKey),
|
||||
-1);
|
||||
return PEM_read_bio_RSAPublicKey(bio.get(), 0, 0, 0);
|
||||
}();
|
||||
if (!pbKey) {
|
||||
cout << "Could not read RSA public key!\n";
|
||||
return -1;
|
||||
@@ -510,7 +538,12 @@ QString countAlphaVersionSignature(quint64 version) { // duplicated in autoupdat
|
||||
|
||||
uint32 siglen = 0;
|
||||
|
||||
RSA *prKey = PEM_read_bio_RSAPrivateKey(BIO_new_mem_buf(const_cast<char*>(cAlphaPrivateKey.constData()), -1), 0, 0, 0);
|
||||
RSA *prKey = [&] {
|
||||
const auto bio = makeBIO(
|
||||
const_cast<char*>(cAlphaPrivateKey.constData()),
|
||||
-1);
|
||||
return PEM_read_bio_RSAPrivateKey(bio.get(), 0, 0, 0);
|
||||
}();
|
||||
if (!prKey) {
|
||||
cout << "Error: Could not read alpha private key!\n";
|
||||
return QString();
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <cstdio>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
@@ -87,7 +88,7 @@ void writeLog(const char *format, ...) {
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
bool copyFile(const char *from, const char *to) {
|
||||
bool copyFile(const char *from, const char *to, bool writeprotected) {
|
||||
FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb");
|
||||
if (!ffrom) {
|
||||
if (fto) fclose(fto);
|
||||
@@ -97,11 +98,6 @@ bool copyFile(const char *from, const char *to) {
|
||||
fclose(ffrom);
|
||||
return false;
|
||||
}
|
||||
static const int BufSize = 65536;
|
||||
char buf[BufSize];
|
||||
while (size_t size = fread(buf, 1, BufSize, ffrom)) {
|
||||
fwrite(buf, 1, size, fto);
|
||||
}
|
||||
|
||||
struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c
|
||||
//let's say this wont fail since you already worked OK on that fp
|
||||
@@ -110,8 +106,35 @@ bool copyFile(const char *from, const char *to) {
|
||||
fclose(fto);
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t copied = sendfile(
|
||||
fileno(fto),
|
||||
fileno(ffrom),
|
||||
nullptr,
|
||||
fst.st_size);
|
||||
|
||||
if (copied == -1) {
|
||||
writeLog(
|
||||
"Copy by sendfile '%s' to '%s' failed, error: %d, fallback now.",
|
||||
from,
|
||||
to,
|
||||
int(errno));
|
||||
static const int BufSize = 65536;
|
||||
char buf[BufSize];
|
||||
while (size_t size = fread(buf, 1, BufSize, ffrom)) {
|
||||
fwrite(buf, 1, size, fto);
|
||||
}
|
||||
} else {
|
||||
writeLog(
|
||||
"Copy by sendfile '%s' to '%s' done, size: %d, result: %d.",
|
||||
from,
|
||||
to,
|
||||
int(fst.st_size),
|
||||
int(copied));
|
||||
}
|
||||
|
||||
//update to the same uid/gid
|
||||
if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {
|
||||
if (!writeprotected && fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {
|
||||
fclose(ffrom);
|
||||
fclose(fto);
|
||||
return false;
|
||||
@@ -210,7 +233,7 @@ void delFolder() {
|
||||
rmdir(delFolder.c_str());
|
||||
}
|
||||
|
||||
bool update() {
|
||||
bool update(bool writeprotected) {
|
||||
writeLog("Update started..");
|
||||
|
||||
string updDir = workDir + "tupdates/temp", readyFilePath = workDir + "tupdates/temp/ready", tdataDir = workDir + "tupdates/temp/tdata";
|
||||
@@ -323,7 +346,7 @@ bool update() {
|
||||
writeLog("Copying file '%s' to '%s'..", fname.c_str(), tofname.c_str());
|
||||
int copyTries = 0, triesLimit = 30;
|
||||
do {
|
||||
if (!copyFile(fname.c_str(), tofname.c_str())) {
|
||||
if (!copyFile(fname.c_str(), tofname.c_str(), writeprotected)) {
|
||||
++copyTries;
|
||||
usleep(100000);
|
||||
} else {
|
||||
@@ -358,6 +381,7 @@ int main(int argc, char *argv[]) {
|
||||
bool needupdate = true;
|
||||
bool autostart = false;
|
||||
bool debug = false;
|
||||
bool writeprotected = false;
|
||||
bool tosettings = false;
|
||||
bool startintray = false;
|
||||
bool testmode = false;
|
||||
@@ -383,6 +407,8 @@ int main(int argc, char *argv[]) {
|
||||
tosettings = true;
|
||||
} else if (equal(argv[i], "-workdir_custom")) {
|
||||
customWorkingDir = true;
|
||||
} else if (equal(argv[i], "-writeprotected")) {
|
||||
writeprotected = true;
|
||||
} else if (equal(argv[i], "-key") && ++i < argc) {
|
||||
key = argv[i];
|
||||
} else if (equal(argv[i], "-workpath") && ++i < argc) {
|
||||
@@ -404,6 +430,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
if (needupdate) writeLog("Need to update!");
|
||||
if (autostart) writeLog("From autostart!");
|
||||
if (writeprotected) writeLog("Write Protected folder!");
|
||||
|
||||
updaterName = CurrentExecutablePath(argc, argv);
|
||||
writeLog("Updater binary full path is: %s", updaterName.c_str());
|
||||
@@ -454,7 +481,7 @@ int main(int argc, char *argv[]) {
|
||||
} else {
|
||||
writeLog("Passed workpath is '%s'", workDir.c_str());
|
||||
}
|
||||
update();
|
||||
update(writeprotected);
|
||||
}
|
||||
} else {
|
||||
writeLog("Error: bad exe name!");
|
||||
@@ -494,14 +521,17 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
args.push_back(nullptr);
|
||||
|
||||
pid_t pid = fork();
|
||||
switch (pid) {
|
||||
case -1:
|
||||
writeLog("fork() failed!");
|
||||
return 1;
|
||||
case 0:
|
||||
execv(path, args.data());
|
||||
return 1;
|
||||
// let the parent launch instead
|
||||
if (!writeprotected) {
|
||||
pid_t pid = fork();
|
||||
switch (pid) {
|
||||
case -1:
|
||||
writeLog("fork() failed!");
|
||||
return 1;
|
||||
case 0:
|
||||
execv(args[0], args.data());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
writeLog("Executed Telegram, closing log and quitting..");
|
||||
|
||||
@@ -867,7 +867,11 @@ void Updates::updateOnline() {
|
||||
}
|
||||
|
||||
bool Updates::isIdle() const {
|
||||
return _isIdle;
|
||||
return _isIdle.current();
|
||||
}
|
||||
|
||||
rpl::producer<bool> Updates::isIdleValue() const {
|
||||
return _isIdle.value();
|
||||
}
|
||||
|
||||
void Updates::updateOnline(bool gotOtherOffline) {
|
||||
@@ -881,7 +885,7 @@ void Updates::updateOnline(bool gotOtherOffline) {
|
||||
const auto idle = crl::now() - Core::App().lastNonIdleTime();
|
||||
if (idle >= config.offlineIdleTimeout) {
|
||||
isOnline = false;
|
||||
if (!_isIdle) {
|
||||
if (!isIdle()) {
|
||||
_isIdle = true;
|
||||
_idleFinishTimer.callOnce(900);
|
||||
}
|
||||
@@ -932,10 +936,9 @@ void Updates::updateOnline(bool gotOtherOffline) {
|
||||
void Updates::checkIdleFinish() {
|
||||
if (crl::now() - Core::App().lastNonIdleTime()
|
||||
< _session->serverConfig().offlineIdleTimeout) {
|
||||
updateOnline();
|
||||
_idleFinishTimer.cancel();
|
||||
_isIdle = false;
|
||||
updateOnline();
|
||||
App::wnd()->checkHistoryActivation();
|
||||
} else {
|
||||
_idleFinishTimer.callOnce(900);
|
||||
}
|
||||
@@ -1878,6 +1881,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
case mtpc_updatePhoneCall:
|
||||
case mtpc_updatePhoneCallSignalingData:
|
||||
case mtpc_updateGroupCallParticipants:
|
||||
case mtpc_updateGroupCallConnection:
|
||||
case mtpc_updateGroupCall: {
|
||||
Core::App().calls().handleUpdate(&session(), update);
|
||||
} break;
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
|
||||
void updateOnline();
|
||||
[[nodiscard]] bool isIdle() const;
|
||||
[[nodiscard]] rpl::producer<bool> isIdleValue() const;
|
||||
void checkIdleFinish();
|
||||
bool lastWasOnline() const;
|
||||
crl::time lastSetOnline() const;
|
||||
@@ -185,7 +186,7 @@ private:
|
||||
base::Timer _idleFinishTimer;
|
||||
crl::time _lastSetOnline = 0;
|
||||
bool _lastWasOnline = false;
|
||||
bool _isIdle = false;
|
||||
rpl::variable<bool> _isIdle = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
|
||||
@@ -433,7 +433,7 @@ editMediaButtonFileSkipRight: 1px;
|
||||
editMediaButtonFileSkipTop: 7px;
|
||||
|
||||
editMediaButtonIconFile: icon {{ "settings_edit", menuIconFg }};
|
||||
editMediaButtonIconPhoto: icon {{ "settings_edit", msgServiceFg }};
|
||||
editMediaButtonIconPhoto: icon {{ "settings_edit", roundedFg }};
|
||||
editMediaButton: IconButton {
|
||||
width: editMediaButtonSize;
|
||||
height: editMediaButtonSize;
|
||||
@@ -464,8 +464,8 @@ sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
|
||||
sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile;
|
||||
sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "history_file_cancel", menuIconFg, point(6px, 6px) }};
|
||||
|
||||
sendBoxAlbumGroupButtonMediaEdit: icon {{ "settings_edit", msgServiceFg, point(3px, -2px) }};
|
||||
sendBoxAlbumGroupButtonMediaDelete: icon {{ "history_file_cancel", msgServiceFg, point(2px, 5px) }};
|
||||
sendBoxAlbumGroupButtonMediaEdit: icon {{ "settings_edit", roundedFg, point(3px, -2px) }};
|
||||
sendBoxAlbumGroupButtonMediaDelete: icon {{ "history_file_cancel", roundedFg, point(2px, 5px) }};
|
||||
sendBoxAlbumGroupButtonMedia: IconButton {
|
||||
width: sendBoxAlbumGroupHeight;
|
||||
height: sendBoxAlbumGroupHeight;
|
||||
@@ -729,10 +729,6 @@ termsAgePadding: margins(22px, 16px, 16px, 0px);
|
||||
|
||||
themesSmallSkip: 10px;
|
||||
themesBackgroundSize: 120px;
|
||||
themesScroll: ScrollArea(defaultScrollArea) {
|
||||
bottomsh: 0px;
|
||||
topsh: 0px;
|
||||
}
|
||||
themesMenuToggle: IconButton(defaultIconButton) {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
@@ -1086,10 +1087,25 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
||||
proxy.password = fields.value(qsl("secret"));
|
||||
}
|
||||
if (proxy) {
|
||||
const auto displayed = "https://" + server + "/";
|
||||
const auto parsed = QUrl::fromUserInput(displayed);
|
||||
const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)
|
||||
? displayed
|
||||
: parsed.isValid()
|
||||
? QString::fromUtf8(parsed.toEncoded())
|
||||
: UrlClickHandler::ShowEncoded(displayed);
|
||||
const auto displayServer = QString(
|
||||
displayUrl
|
||||
).replace(
|
||||
QRegularExpression(
|
||||
"^https://",
|
||||
QRegularExpression::CaseInsensitiveOption),
|
||||
QString()
|
||||
).replace(QRegularExpression("/$"), QString());
|
||||
const auto text = tr::lng_sure_enable_socks(
|
||||
tr::now,
|
||||
lt_server,
|
||||
server,
|
||||
displayServer,
|
||||
lt_port,
|
||||
QString::number(port))
|
||||
+ (proxy.type == Type::Mtproto
|
||||
|
||||
@@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/text/format_song_document_name.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
@@ -220,7 +221,7 @@ EditCaptionBox::EditCaptionBox(
|
||||
const auto document = _documentMedia->owner();
|
||||
const auto nameString = document->isVoiceMessage()
|
||||
? tr::lng_media_audio(tr::now)
|
||||
: document->composeNameString();
|
||||
: Ui::Text::FormatSongNameFor(document).string();
|
||||
setName(nameString, document->size);
|
||||
_isImage = document->isImage();
|
||||
_isAudio = document->isVoiceMessage()
|
||||
@@ -486,13 +487,13 @@ void EditCaptionBox::handleStreamingError(Error &&error) {
|
||||
}
|
||||
|
||||
void EditCaptionBox::streamingReady(Information &&info) {
|
||||
const auto calculateGifDimensions = [&]() {
|
||||
const auto calculateGifDimensions = [&] {
|
||||
const auto scaled = QSize(
|
||||
info.video.size.width(),
|
||||
info.video.size.height()
|
||||
).scaled(
|
||||
st::sendMediaPreviewSize * cIntRetinaFactor(),
|
||||
st::confirmMaxHeight * cIntRetinaFactor(),
|
||||
st::sendMediaPreviewSize,
|
||||
st::confirmMaxHeight,
|
||||
Qt::KeepAspectRatio);
|
||||
_thumbw = _gifw = scaled.width();
|
||||
_thumbh = _gifh = scaled.height();
|
||||
@@ -549,10 +550,10 @@ void EditCaptionBox::updateEditPreview() {
|
||||
if (shouldAsDoc) {
|
||||
auto nameString = filename;
|
||||
if (const auto song = std::get_if<Info::Song>(fileMedia)) {
|
||||
nameString = Ui::ComposeNameString(
|
||||
nameString = Ui::Text::FormatSongName(
|
||||
filename,
|
||||
song->title,
|
||||
song->performer);
|
||||
song->performer).string();
|
||||
_isAudio = true;
|
||||
|
||||
if (auto cover = song->cover; !cover.isNull()) {
|
||||
@@ -620,7 +621,7 @@ void EditCaptionBox::updateEditMediaButton() {
|
||||
const auto icon = _doc
|
||||
? &st::editMediaButtonIconFile
|
||||
: &st::editMediaButtonIconPhoto;
|
||||
const auto color = _doc ? &st::windowBgRipple : &st::callFingerprintBg;
|
||||
const auto color = _doc ? &st::windowBgRipple : &st::roundedBg;
|
||||
_editMedia->setIconOverride(icon);
|
||||
_editMedia->setRippleColorOverride(color);
|
||||
_editMedia->setForceRippled(!_doc, anim::type::instant);
|
||||
|
||||
@@ -349,7 +349,9 @@ EditColorBox::Slider::Slider(
|
||||
, _type(type)
|
||||
, _color(color.red(), color.green(), color.blue())
|
||||
, _value(valueFromColor(color))
|
||||
, _transparent((_type == Type::Opacity) ? style::transparentPlaceholderBrush() : QBrush()) {
|
||||
, _transparent((_type == Type::Opacity)
|
||||
? style::TransparentPlaceholder()
|
||||
: QBrush()) {
|
||||
prepareMinSize();
|
||||
}
|
||||
|
||||
@@ -758,7 +760,7 @@ EditColorBox::EditColorBox(
|
||||
, _greenField(this, st::colorValueInput, "G", 255)
|
||||
, _blueField(this, st::colorValueInput, "B", 255)
|
||||
, _result(this, st::colorResultInput)
|
||||
, _transparent(style::transparentPlaceholderBrush())
|
||||
, _transparent(style::TransparentPlaceholder())
|
||||
, _current(current)
|
||||
, _new(current) {
|
||||
if (_mode == Mode::RGBA) {
|
||||
|
||||
@@ -434,6 +434,7 @@ PeerListRow::PeerListRow(not_null<PeerData*> peer)
|
||||
PeerListRow::PeerListRow(not_null<PeerData*> peer, PeerListRowId id)
|
||||
: _id(id)
|
||||
, _peer(peer)
|
||||
, _hidden(false)
|
||||
, _initialized(false)
|
||||
, _isSearchResult(false)
|
||||
, _isSavedMessagesChat(false)
|
||||
@@ -442,6 +443,7 @@ PeerListRow::PeerListRow(not_null<PeerData*> peer, PeerListRowId id)
|
||||
|
||||
PeerListRow::PeerListRow(PeerListRowId id)
|
||||
: _id(id)
|
||||
, _hidden(false)
|
||||
, _initialized(false)
|
||||
, _isSearchResult(false)
|
||||
, _isSavedMessagesChat(false)
|
||||
@@ -529,7 +531,7 @@ QString PeerListRow::generateShortName() {
|
||||
: peer()->shortName();
|
||||
}
|
||||
|
||||
std::shared_ptr<Data::CloudImageView> PeerListRow::ensureUserpicView() {
|
||||
std::shared_ptr<Data::CloudImageView> &PeerListRow::ensureUserpicView() {
|
||||
if (!_userpic) {
|
||||
_userpic = peer()->createUserpicView();
|
||||
}
|
||||
@@ -588,11 +590,14 @@ void PeerListRow::paintStatusText(
|
||||
_status.drawLeftElided(p, x, y, availableWidth, outerWidth);
|
||||
}
|
||||
|
||||
template <typename UpdateCallback>
|
||||
void PeerListRow::addRipple(const style::PeerListItem &st, QSize size, QPoint point, UpdateCallback updateCallback) {
|
||||
template <typename MaskGenerator, typename UpdateCallback>
|
||||
void PeerListRow::addRipple(const style::PeerListItem &st, MaskGenerator &&maskGenerator, QPoint point, UpdateCallback &&updateCallback) {
|
||||
if (!_ripple) {
|
||||
auto mask = Ui::RippleAnimation::rectMask(size);
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(st.button.ripple, std::move(mask), std::move(updateCallback));
|
||||
auto mask = maskGenerator();
|
||||
if (mask.isNull()) {
|
||||
return;
|
||||
}
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(st.button.ripple, std::move(mask), std::forward<UpdateCallback>(updateCallback));
|
||||
}
|
||||
_ripple->add(point);
|
||||
}
|
||||
@@ -741,12 +746,42 @@ PeerListContent::PeerListContent(
|
||||
_repaintByStatus.setCallback([this] { update(); });
|
||||
}
|
||||
|
||||
void PeerListContent::setMode(Mode mode) {
|
||||
if (mode == Mode::Default && _mode == Mode::Default) {
|
||||
return;
|
||||
}
|
||||
_mode = mode;
|
||||
switch (_mode) {
|
||||
case Mode::Default:
|
||||
_rowHeight = _st.item.height;
|
||||
break;
|
||||
case Mode::Custom:
|
||||
_rowHeight = _controller->customRowHeight();
|
||||
break;
|
||||
}
|
||||
const auto wasMouseSelection = _mouseSelection;
|
||||
const auto wasLastMousePosition = _lastMousePosition;
|
||||
_contextMenu = nullptr;
|
||||
if (wasMouseSelection) {
|
||||
setSelected(Selected());
|
||||
}
|
||||
setPressed(Selected());
|
||||
refreshRows();
|
||||
if (wasMouseSelection && wasLastMousePosition) {
|
||||
selectByMouse(*wasLastMousePosition);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::appendRow(std::unique_ptr<PeerListRow> row) {
|
||||
Expects(row != nullptr);
|
||||
|
||||
if (_rowsById.find(row->id()) == _rowsById.cend()) {
|
||||
row->setAbsoluteIndex(_rows.size());
|
||||
addRowEntry(row.get());
|
||||
if (!_hiddenRows.empty()) {
|
||||
Assert(!row->hidden());
|
||||
_filterResults.push_back(row.get());
|
||||
}
|
||||
_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
@@ -784,6 +819,17 @@ void PeerListContent::changeCheckState(
|
||||
[=] { updateRow(row); });
|
||||
}
|
||||
|
||||
void PeerListContent::setRowHidden(not_null<PeerListRow*> row, bool hidden) {
|
||||
Expects(!row->isSearchResult());
|
||||
|
||||
row->setHidden(hidden);
|
||||
if (hidden) {
|
||||
_hiddenRows.emplace(row);
|
||||
} else {
|
||||
_hiddenRows.remove(row);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContent::addRowEntry(not_null<PeerListRow*> row) {
|
||||
if (_controller->respectSavedMessagesChat() && !row->special()) {
|
||||
if (row->peer()->isSelf()) {
|
||||
@@ -850,6 +896,10 @@ void PeerListContent::prependRow(std::unique_ptr<PeerListRow> row) {
|
||||
|
||||
if (_rowsById.find(row->id()) == _rowsById.cend()) {
|
||||
addRowEntry(row.get());
|
||||
if (!_hiddenRows.empty()) {
|
||||
Assert(!row->hidden());
|
||||
_filterResults.insert(_filterResults.begin(), row.get());
|
||||
}
|
||||
_rows.insert(_rows.begin(), std::move(row));
|
||||
refreshIndices();
|
||||
}
|
||||
@@ -865,6 +915,10 @@ void PeerListContent::prependRowFromSearchResult(not_null<PeerListRow*> row) {
|
||||
Assert(_searchRows[index].get() == row);
|
||||
|
||||
row->setIsSearchResult(false);
|
||||
if (!_hiddenRows.empty()) {
|
||||
Assert(!row->hidden());
|
||||
_filterResults.insert(_filterResults.begin(), row);
|
||||
}
|
||||
_rows.insert(_rows.begin(), std::move(_searchRows[index]));
|
||||
refreshIndices();
|
||||
removeRowAtIndex(_searchRows, index);
|
||||
@@ -918,6 +972,7 @@ void PeerListContent::removeRow(not_null<PeerListRow*> row) {
|
||||
_filterResults.erase(
|
||||
ranges::remove(_filterResults, row),
|
||||
end(_filterResults));
|
||||
_hiddenRows.remove(row);
|
||||
removeRowAtIndex(eraseFrom, index);
|
||||
|
||||
restoreSelection();
|
||||
@@ -955,7 +1010,9 @@ void PeerListContent::convertRowToSearchResult(not_null<PeerListRow*> row) {
|
||||
|
||||
removeFromSearchIndex(row);
|
||||
row->setIsSearchResult(true);
|
||||
row->setHidden(false);
|
||||
row->setAbsoluteIndex(_searchRows.size());
|
||||
_hiddenRows.remove(row);
|
||||
_searchRows.push_back(std::move(_rows[index]));
|
||||
removeRowAtIndex(_rows, index);
|
||||
}
|
||||
@@ -1040,6 +1097,14 @@ int PeerListContent::labelHeight() const {
|
||||
}
|
||||
|
||||
void PeerListContent::refreshRows() {
|
||||
if (!_hiddenRows.empty()) {
|
||||
_filterResults.clear();
|
||||
for (const auto &row : _rows) {
|
||||
if (!row->hidden()) {
|
||||
_filterResults.push_back(row.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
resizeToWidth(width());
|
||||
if (_visibleBottom > 0) {
|
||||
checkScrollForPreload();
|
||||
@@ -1053,7 +1118,7 @@ void PeerListContent::refreshRows() {
|
||||
void PeerListContent::setSearchMode(PeerListSearchMode mode) {
|
||||
if (_searchMode != mode) {
|
||||
if (!addingToSearchIndex()) {
|
||||
for_const (auto &row, _rows) {
|
||||
for (const auto &row : _rows) {
|
||||
addToSearchIndex(row.get());
|
||||
}
|
||||
}
|
||||
@@ -1080,25 +1145,27 @@ void PeerListContent::clearSearchRows() {
|
||||
void PeerListContent::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
auto clip = e->rect();
|
||||
p.fillRect(clip, _st.item.button.textBg);
|
||||
const auto clip = e->rect();
|
||||
if (_mode != Mode::Custom) {
|
||||
p.fillRect(clip, _st.item.button.textBg);
|
||||
}
|
||||
|
||||
auto repaintByStatusAfter = _repaintByStatus.remainingTime();
|
||||
const auto repaintByStatusAfter = _repaintByStatus.remainingTime();
|
||||
auto repaintAfterMin = repaintByStatusAfter;
|
||||
|
||||
auto rowsTopCached = rowsTop();
|
||||
auto ms = crl::now();
|
||||
auto yFrom = clip.y() - rowsTopCached;
|
||||
auto yTo = clip.y() + clip.height() - rowsTopCached;
|
||||
const auto rowsTopCached = rowsTop();
|
||||
const auto now = crl::now();
|
||||
const auto yFrom = clip.y() - rowsTopCached;
|
||||
const auto yTo = clip.y() + clip.height() - rowsTopCached;
|
||||
p.translate(0, rowsTopCached);
|
||||
auto count = shownRowsCount();
|
||||
const auto count = shownRowsCount();
|
||||
if (count > 0) {
|
||||
auto from = floorclamp(yFrom, _rowHeight, 0, count);
|
||||
auto to = ceilclamp(yTo, _rowHeight, 0, count);
|
||||
const auto from = floorclamp(yFrom, _rowHeight, 0, count);
|
||||
const auto to = ceilclamp(yTo, _rowHeight, 0, count);
|
||||
p.translate(0, from * _rowHeight);
|
||||
for (auto index = from; index != to; ++index) {
|
||||
auto repaintAfter = paintRow(p, ms, RowIndex(index));
|
||||
if (repaintAfter >= 0
|
||||
const auto repaintAfter = paintRow(p, now, RowIndex(index));
|
||||
if (repaintAfter > 0
|
||||
&& (repaintAfterMin < 0
|
||||
|| repaintAfterMin > repaintAfter)) {
|
||||
repaintAfterMin = repaintAfter;
|
||||
@@ -1213,9 +1280,16 @@ void PeerListContent::mousePressEvent(QMouseEvent *e) {
|
||||
row->addActionRipple(point, std::move(updateCallback));
|
||||
}
|
||||
} else {
|
||||
auto size = QSize(width(), _rowHeight);
|
||||
auto point = mapFromGlobal(QCursor::pos()) - QPoint(0, getRowTop(_selected.index));
|
||||
row->addRipple(_st.item, size, point, std::move(updateCallback));
|
||||
if (_mode == Mode::Custom) {
|
||||
row->addRipple(_st.item, _controller->customRowRippleMaskGenerator(), point, std::move(updateCallback));
|
||||
} else {
|
||||
const auto maskGenerator = [&] {
|
||||
return Ui::RippleAnimation::rectMask(
|
||||
QSize(width(), _rowHeight));
|
||||
};
|
||||
row->addRipple(_st.item, maskGenerator, point, std::move(updateCallback));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim::Disabled()) {
|
||||
@@ -1246,13 +1320,22 @@ void PeerListContent::mousePressReleased(Qt::MouseButton button) {
|
||||
|
||||
void PeerListContent::showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
showRowMenu(findRowIndex(row), QCursor::pos(), std::move(destroyed));
|
||||
const auto index = findRowIndex(row);
|
||||
showRowMenu(
|
||||
index,
|
||||
row,
|
||||
QCursor::pos(),
|
||||
highlightRow,
|
||||
std::move(destroyed));
|
||||
}
|
||||
|
||||
bool PeerListContent::showRowMenu(
|
||||
RowIndex index,
|
||||
PeerListRow *row,
|
||||
QPoint globalPos,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed) {
|
||||
if (_contextMenu) {
|
||||
_contextMenu->setDestroyedCallback(nullptr);
|
||||
@@ -1263,7 +1346,9 @@ bool PeerListContent::showRowMenu(
|
||||
mousePressReleased(_pressButton);
|
||||
}
|
||||
|
||||
const auto row = getRow(index);
|
||||
if (highlightRow) {
|
||||
row = getRow(index);
|
||||
}
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
@@ -1274,11 +1359,15 @@ bool PeerListContent::showRowMenu(
|
||||
return false;
|
||||
}
|
||||
|
||||
setContexted({ index, false });
|
||||
if (highlightRow) {
|
||||
setContexted({ index, false });
|
||||
}
|
||||
raw->setDestroyedCallback(crl::guard(
|
||||
this,
|
||||
[=] {
|
||||
setContexted(Selected());
|
||||
if (highlightRow) {
|
||||
setContexted(Selected());
|
||||
}
|
||||
handleMouseMove(QCursor::pos());
|
||||
if (destroyed) {
|
||||
destroyed(raw);
|
||||
@@ -1292,7 +1381,7 @@ void PeerListContent::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
if (showRowMenu(_selected.index, e->globalPos())) {
|
||||
if (showRowMenu(_selected.index, nullptr, e->globalPos(), true)) {
|
||||
e->accept();
|
||||
}
|
||||
}
|
||||
@@ -1307,7 +1396,7 @@ void PeerListContent::setPressed(Selected pressed) {
|
||||
|
||||
crl::time PeerListContent::paintRow(
|
||||
Painter &p,
|
||||
crl::time ms,
|
||||
crl::time now,
|
||||
RowIndex index) {
|
||||
const auto row = getRow(index);
|
||||
Assert(row != nullptr);
|
||||
@@ -1315,13 +1404,15 @@ crl::time PeerListContent::paintRow(
|
||||
row->lazyInitialize(_st.item);
|
||||
|
||||
auto refreshStatusAt = row->refreshStatusTime();
|
||||
if (refreshStatusAt >= 0 && ms >= refreshStatusAt) {
|
||||
if (refreshStatusAt > 0 && now >= refreshStatusAt) {
|
||||
row->refreshStatus();
|
||||
refreshStatusAt = row->refreshStatusTime();
|
||||
}
|
||||
const auto refreshStatusIn = (refreshStatusAt > 0)
|
||||
? std::max(refreshStatusAt - now, crl::time(1))
|
||||
: 0;
|
||||
|
||||
const auto peer = row->special() ? nullptr : row->peer().get();
|
||||
const auto user = peer ? peer->asUser() : nullptr;
|
||||
const auto active = (_contexted.index.value >= 0)
|
||||
? _contexted
|
||||
: (_pressed.index.value >= 0)
|
||||
@@ -1330,6 +1421,11 @@ crl::time PeerListContent::paintRow(
|
||||
const auto selected = (active.index == index);
|
||||
const auto actionSelected = (selected && active.action);
|
||||
|
||||
if (_mode == Mode::Custom) {
|
||||
_controller->customRowPaint(p, now, row, selected);
|
||||
return refreshStatusIn;
|
||||
}
|
||||
|
||||
const auto &bg = selected
|
||||
? _st.item.button.textBgOver
|
||||
: _st.item.button.textBg;
|
||||
@@ -1412,7 +1508,7 @@ crl::time PeerListContent::paintRow(
|
||||
} else {
|
||||
row->paintStatusText(p, _st.item, _st.item.statusPosition.x(), _st.item.statusPosition.y(), statusw, width(), selected);
|
||||
}
|
||||
return (refreshStatusAt - ms);
|
||||
return refreshStatusIn;
|
||||
}
|
||||
|
||||
PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
||||
@@ -1562,6 +1658,8 @@ void PeerListContent::searchQueryChanged(QString query) {
|
||||
if (_normalizedSearchQuery != normalizedQuery) {
|
||||
setSearchQuery(query, normalizedQuery);
|
||||
if (_controller->searchInLocal() && !searchWordsList.isEmpty()) {
|
||||
Assert(_hiddenRows.empty());
|
||||
|
||||
auto minimalList = (const std::vector<not_null<PeerListRow*>>*)nullptr;
|
||||
for (const auto &searchWord : searchWordsList) {
|
||||
auto searchWordStart = searchWord[0].toLower();
|
||||
@@ -1611,15 +1709,17 @@ void PeerListContent::searchQueryChanged(QString query) {
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListState> PeerListContent::saveState() const {
|
||||
Expects(_hiddenRows.empty());
|
||||
|
||||
auto result = std::make_unique<PeerListState>();
|
||||
result->controllerState
|
||||
= std::make_unique<PeerListController::SavedStateBase>();
|
||||
result->list.reserve(_rows.size());
|
||||
for (auto &row : _rows) {
|
||||
for (const auto &row : _rows) {
|
||||
result->list.push_back(row->peer());
|
||||
}
|
||||
result->filterResults.reserve(_filterResults.size());
|
||||
for (auto &row : _filterResults) {
|
||||
for (const auto &row : _filterResults) {
|
||||
result->filterResults.push_back(row->peer());
|
||||
}
|
||||
result->searchQuery = _searchQuery;
|
||||
@@ -1740,15 +1840,25 @@ void PeerListContent::selectByMouse(QPoint globalPosition) {
|
||||
_mouseSelection = true;
|
||||
_lastMousePosition = globalPosition;
|
||||
const auto point = mapFromGlobal(globalPosition);
|
||||
const auto customMode = (_mode == Mode::Custom);
|
||||
auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(globalPosition));
|
||||
auto selected = Selected();
|
||||
auto rowsPointY = point.y() - rowsTop();
|
||||
selected.index.value = (in && rowsPointY >= 0 && rowsPointY < shownRowsCount() * _rowHeight) ? (rowsPointY / _rowHeight) : -1;
|
||||
selected.index.value = (in
|
||||
&& rowsPointY >= 0
|
||||
&& rowsPointY < shownRowsCount() * _rowHeight)
|
||||
? (rowsPointY / _rowHeight)
|
||||
: -1;
|
||||
if (selected.index.value >= 0) {
|
||||
auto row = getRow(selected.index);
|
||||
if (row->disabled()) {
|
||||
const auto row = getRow(selected.index);
|
||||
if (row->disabled()
|
||||
|| (customMode
|
||||
&& !_controller->customRowSelectionPoint(
|
||||
row,
|
||||
point.x(),
|
||||
rowsPointY - (selected.index.value * _rowHeight)))) {
|
||||
selected = Selected();
|
||||
} else {
|
||||
} else if (!customMode) {
|
||||
if (getActiveActionRect(row, selected.index).contains(point)) {
|
||||
selected.action = true;
|
||||
}
|
||||
@@ -1789,8 +1899,7 @@ void PeerListContent::updateRow(RowIndex index) {
|
||||
if (index.value < 0) {
|
||||
return;
|
||||
}
|
||||
auto row = getRow(index);
|
||||
if (row->disabled()) {
|
||||
if (const auto row = getRow(index); row && row->disabled()) {
|
||||
if (index == _selected.index) {
|
||||
setSelected(Selected());
|
||||
}
|
||||
@@ -1885,3 +1994,10 @@ PeerListContent::~PeerListContent() {
|
||||
_contextMenu->setDestroyedCallback(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerListContentDelegate::peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu *>)> destroyed) {
|
||||
_content->showRowMenu(row, highlightRow, std::move(destroyed));
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
return _id;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Data::CloudImageView> ensureUserpicView();
|
||||
[[nodiscard]] std::shared_ptr<Data::CloudImageView> &ensureUserpicView();
|
||||
|
||||
[[nodiscard]] virtual QString generateName();
|
||||
[[nodiscard]] virtual QString generateShortName();
|
||||
@@ -122,7 +122,7 @@ public:
|
||||
bool actionSelected) {
|
||||
}
|
||||
|
||||
void refreshName(const style::PeerListItem &st);
|
||||
virtual void refreshName(const style::PeerListItem &st);
|
||||
const Ui::Text::String &name() const {
|
||||
return _name;
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public:
|
||||
template <typename UpdateCallback>
|
||||
void setChecked(
|
||||
bool checked,
|
||||
const style::RoundImageCheckbox &st,
|
||||
const style::RoundImageCheckbox &st,
|
||||
anim::type animated,
|
||||
UpdateCallback callback) {
|
||||
if (checked && !_checkbox) {
|
||||
@@ -169,15 +169,21 @@ public:
|
||||
}
|
||||
setCheckedInternal(checked, animated);
|
||||
}
|
||||
void setHidden(bool hidden) {
|
||||
_hidden = hidden;
|
||||
}
|
||||
[[nodiscard]] bool hidden() const {
|
||||
return _hidden;
|
||||
}
|
||||
void finishCheckedAnimation();
|
||||
void invalidatePixmapsCache();
|
||||
|
||||
template <typename UpdateCallback>
|
||||
template <typename MaskGenerator, typename UpdateCallback>
|
||||
void addRipple(
|
||||
const style::PeerListItem &st,
|
||||
QSize size,
|
||||
MaskGenerator &&maskGenerator,
|
||||
QPoint point,
|
||||
UpdateCallback updateCallback);
|
||||
UpdateCallback &&updateCallback);
|
||||
void stopLastRipple();
|
||||
void paintRipple(Painter &p, int x, int y, int outerWidth);
|
||||
void paintUserpic(
|
||||
@@ -237,6 +243,7 @@ private:
|
||||
base::flat_set<QChar> _nameFirstLetters;
|
||||
int _absoluteIndex = -1;
|
||||
State _disabledState = State::Active;
|
||||
bool _hidden : 1;
|
||||
bool _initialized : 1;
|
||||
bool _isSearchResult : 1;
|
||||
bool _isSavedMessagesChat : 1;
|
||||
@@ -274,6 +281,7 @@ public:
|
||||
virtual void peerListConvertRowToSearchResult(not_null<PeerListRow*> row) = 0;
|
||||
virtual bool peerListIsRowChecked(not_null<PeerListRow*> row) = 0;
|
||||
virtual void peerListSetRowChecked(not_null<PeerListRow*> row, bool checked) = 0;
|
||||
virtual void peerListSetRowHidden(not_null<PeerListRow*> row, bool hidden) = 0;
|
||||
virtual void peerListSetForeignRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked,
|
||||
@@ -304,6 +312,7 @@ public:
|
||||
|
||||
virtual void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) = 0;
|
||||
virtual int peerListSelectedRowsCount() = 0;
|
||||
virtual std::unique_ptr<PeerListState> peerListSaveState() const = 0;
|
||||
@@ -450,6 +459,25 @@ public:
|
||||
[[nodiscard]] virtual bool respectSavedMessagesChat() const {
|
||||
return false;
|
||||
}
|
||||
[[nodiscard]] virtual int customRowHeight() {
|
||||
Unexpected("PeerListController::customRowHeight.");
|
||||
}
|
||||
virtual void customRowPaint(
|
||||
Painter &p,
|
||||
crl::time now,
|
||||
not_null<PeerListRow*> row,
|
||||
bool selected) {
|
||||
Unexpected("PeerListController::customRowPaint.");
|
||||
}
|
||||
[[nodiscard]] virtual bool customRowSelectionPoint(
|
||||
not_null<PeerListRow*> row,
|
||||
int x,
|
||||
int y) {
|
||||
Unexpected("PeerListController::customRowSelectionPoint.");
|
||||
}
|
||||
[[nodiscard]] virtual Fn<QImage()> customRowRippleMaskGenerator() {
|
||||
Unexpected("PeerListController::customRowRippleMaskGenerator.");
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual rpl::producer<int> onlineCountValue() const;
|
||||
|
||||
@@ -514,6 +542,12 @@ public:
|
||||
SkipResult selectSkip(int direction);
|
||||
void selectSkipPage(int height, int direction);
|
||||
|
||||
enum class Mode {
|
||||
Default,
|
||||
Custom,
|
||||
};
|
||||
void setMode(Mode mode);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> selectedIndexValue() const;
|
||||
[[nodiscard]] bool hasSelection() const;
|
||||
[[nodiscard]] bool hasPressed() const;
|
||||
@@ -552,6 +586,9 @@ public:
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked,
|
||||
anim::type animated);
|
||||
void setRowHidden(
|
||||
not_null<PeerListRow*> row,
|
||||
bool hidden);
|
||||
|
||||
template <typename ReorderCallback>
|
||||
void reorderRows(ReorderCallback &&callback) {
|
||||
@@ -560,6 +597,9 @@ public:
|
||||
callback(searchEntity.second.begin(), searchEntity.second.end());
|
||||
}
|
||||
refreshIndices();
|
||||
if (!_hiddenRows.empty()) {
|
||||
callback(_filterResults.begin(), _filterResults.end());
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -568,6 +608,7 @@ public:
|
||||
|
||||
void showRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed);
|
||||
|
||||
auto scrollToRequests() const {
|
||||
@@ -655,10 +696,12 @@ private:
|
||||
|
||||
bool showRowMenu(
|
||||
RowIndex index,
|
||||
PeerListRow *row,
|
||||
QPoint globalPos,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr);
|
||||
|
||||
crl::time paintRow(Painter &p, crl::time ms, RowIndex index);
|
||||
crl::time paintRow(Painter &p, crl::time now, RowIndex index);
|
||||
|
||||
void addRowEntry(not_null<PeerListRow*> row);
|
||||
void addToSearchIndex(not_null<PeerListRow*> row);
|
||||
@@ -666,7 +709,7 @@ private:
|
||||
void removeFromSearchIndex(not_null<PeerListRow*> row);
|
||||
void setSearchQuery(const QString &query, const QString &normalizedQuery);
|
||||
bool showingSearch() const {
|
||||
return !_searchQuery.isEmpty();
|
||||
return !_hiddenRows.empty() || !_searchQuery.isEmpty();
|
||||
}
|
||||
int shownRowsCount() const {
|
||||
return showingSearch() ? _filterResults.size() : _rows.size();
|
||||
@@ -688,6 +731,7 @@ private:
|
||||
not_null<PeerListController*> _controller;
|
||||
PeerListSearchMode _searchMode = PeerListSearchMode::Disabled;
|
||||
|
||||
Mode _mode = Mode::Default;
|
||||
int _rowHeight = 0;
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
@@ -711,6 +755,7 @@ private:
|
||||
QString _normalizedSearchQuery;
|
||||
QString _mentionHighlight;
|
||||
std::vector<not_null<PeerListRow*>> _filterResults;
|
||||
base::flat_set<not_null<PeerListRow*>> _hiddenRows;
|
||||
|
||||
int _aboveHeight = 0;
|
||||
int _belowHeight = 0;
|
||||
@@ -775,6 +820,11 @@ public:
|
||||
bool checked) override {
|
||||
_content->changeCheckState(row, checked, anim::type::normal);
|
||||
}
|
||||
void peerListSetRowHidden(
|
||||
not_null<PeerListRow*> row,
|
||||
bool hidden) override {
|
||||
_content->setRowHidden(row, hidden);
|
||||
}
|
||||
void peerListSetForeignRowChecked(
|
||||
not_null<PeerListRow*> row,
|
||||
bool checked,
|
||||
@@ -845,10 +895,9 @@ public:
|
||||
_content->restoreState(std::move(state));
|
||||
}
|
||||
void peerListShowRowMenu(
|
||||
not_null<PeerListRow*> row,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override {
|
||||
_content->showRowMenu(row, std::move(destroyed));
|
||||
}
|
||||
not_null<PeerListRow*> row,
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
|
||||
|
||||
protected:
|
||||
not_null<PeerListContent*> content() const {
|
||||
|
||||
@@ -533,7 +533,7 @@ void LinksController::rowClicked(not_null<PeerListRow*> row) {
|
||||
}
|
||||
|
||||
void LinksController::rowActionClicked(not_null<PeerListRow*> row) {
|
||||
delegate()->peerListShowRowMenu(row, nullptr);
|
||||
delegate()->peerListShowRowMenu(row, true);
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
|
||||
|
||||
@@ -198,6 +198,38 @@ void StickerSetBox::copyStickersLink() {
|
||||
QGuiApplication::clipboard()->setText(url);
|
||||
}
|
||||
|
||||
void StickerSetBox::archiveStickers() {
|
||||
const auto weak = base::make_weak(_controller.get());
|
||||
const auto setId = _set.c_inputStickerSetID().vid().v;
|
||||
_controller->session().api().request(MTPmessages_InstallStickerSet(
|
||||
_set,
|
||||
MTP_boolTrue()
|
||||
)).done([=](const MTPmessages_StickerSetInstallResult &result) {
|
||||
const auto controller = weak.get();
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
if (result.type() == mtpc_messages_stickerSetInstallResultSuccess) {
|
||||
Ui::Toast::Show(tr::lng_stickers_has_been_archived(tr::now));
|
||||
|
||||
const auto &session = controller->session();
|
||||
auto &order = session.data().stickers().setsOrderRef();
|
||||
const auto index = order.indexOf(setId);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
order.removeAt(index);
|
||||
|
||||
session.local().writeInstalledStickers();
|
||||
session.local().writeArchivedStickers();
|
||||
|
||||
session.data().stickers().notifyUpdated();
|
||||
}
|
||||
}).fail([](const MTP::Error &error) {
|
||||
Ui::Toast::Show(Lang::Hard::ServerError());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void StickerSetBox::updateTitleAndButtons() {
|
||||
setTitle(_inner->title());
|
||||
updateButtons();
|
||||
@@ -237,6 +269,24 @@ void StickerSetBox::updateButtons() {
|
||||
};
|
||||
addButton(tr::lng_stickers_share_pack(), std::move(share));
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
if (!_inner->shortName().isEmpty()) {
|
||||
const auto top = addTopButton(st::infoTopBarMenu);
|
||||
const auto archive = [=] {
|
||||
archiveStickers();
|
||||
closeBox();
|
||||
};
|
||||
const auto menu =
|
||||
std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
|
||||
top->setClickedCallback([=] {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(top);
|
||||
(*menu)->addAction(
|
||||
tr::lng_stickers_archive_pack(tr::now),
|
||||
archive);
|
||||
(*menu)->popup(QCursor::pos());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
@@ -42,6 +42,7 @@ private:
|
||||
void updateButtons();
|
||||
void addStickers();
|
||||
void copyStickersLink();
|
||||
void archiveStickers();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
MTPInputStickerSet _set;
|
||||
|
||||
@@ -175,6 +175,87 @@ callCameraUnmute: CallButton(callMicrophoneUnmute) {
|
||||
}
|
||||
callBottomShadowSize: 124px;
|
||||
|
||||
CallMuteButton {
|
||||
active: CallButton;
|
||||
muted: CallButton;
|
||||
labelAdditional: pixels;
|
||||
sublabel: FlatLabel;
|
||||
labelsSkip: pixels;
|
||||
sublabelSkip: pixels;
|
||||
lottieSize: size;
|
||||
lottieTop: pixels;
|
||||
}
|
||||
|
||||
callMuteButtonLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMembersFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(14px);
|
||||
linkFont: font(14px);
|
||||
linkFontOver: font(14px underline);
|
||||
}
|
||||
}
|
||||
callMuteButtonActiveInner: IconButton {
|
||||
width: 112px;
|
||||
height: 138px;
|
||||
}
|
||||
callMuteButtonSmallActiveInner: IconButton {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
callMuteButtonActive: CallButton {
|
||||
button: callMuteButtonActiveInner;
|
||||
bg: groupCallLive1;
|
||||
bgSize: 77px;
|
||||
bgPosition: point(18px, 18px);
|
||||
outerRadius: 18px;
|
||||
outerBg: callAnswerBgOuter;
|
||||
label: callMuteButtonLabel;
|
||||
}
|
||||
callMuteButton: CallMuteButton {
|
||||
active: callMuteButtonActive;
|
||||
muted: CallButton(callMuteButtonActive) {
|
||||
bg: groupCallMuted1;
|
||||
label: callMuteButtonLabel;
|
||||
}
|
||||
labelAdditional: 5px;
|
||||
sublabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
labelsSkip: 8px;
|
||||
sublabelSkip: 14px;
|
||||
lottieSize: size(54px, 54px);
|
||||
lottieTop: 31px;
|
||||
}
|
||||
callMuteButtonSmallActive: CallButton(callMuteButtonActive) {
|
||||
button: callMuteButtonSmallActiveInner;
|
||||
bgSize: 42px;
|
||||
bgPosition: point(13px, 13px);
|
||||
outerRadius: 13px;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
callMuteButtonSmall: CallMuteButton(callMuteButton) {
|
||||
active: callMuteButtonSmallActive;
|
||||
muted: CallButton(callMuteButtonSmallActive) {
|
||||
bg: groupCallMuted1;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
labelsSkip: 0px;
|
||||
sublabelSkip: 0px;
|
||||
lottieSize: size(36px, 36px);
|
||||
lottieTop: 17px;
|
||||
}
|
||||
|
||||
callMuteMinorBlobMinRadius: 64px;
|
||||
callMuteMinorBlobMaxRadius: 74px;
|
||||
callMuteMajorBlobMinRadius: 67px;
|
||||
callMuteMajorBlobMaxRadius: 77px;
|
||||
callMuteBlobRadiusForDiameter: 100px;
|
||||
|
||||
callConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
|
||||
color: lightButtonFg;
|
||||
thickness: 4px;
|
||||
}
|
||||
|
||||
callName: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 260px;
|
||||
maxHeight: 30px;
|
||||
@@ -208,55 +289,6 @@ callRemoteAudioMute: FlatLabel(callStatus) {
|
||||
}
|
||||
callRemoteAudioMuteSkip: 12px;
|
||||
|
||||
callMuteMainBlobMinRadius: 57px;
|
||||
callMuteMainBlobMaxRadius: 63px;
|
||||
callMuteMinorBlobMinRadius: 64px;
|
||||
callMuteMinorBlobMaxRadius: 74px;
|
||||
callMuteMajorBlobMinRadius: 67px;
|
||||
callMuteMajorBlobMaxRadius: 77px;
|
||||
|
||||
callMuteButtonActiveInner: IconButton {
|
||||
width: 136px;
|
||||
height: 165px;
|
||||
}
|
||||
callMuteButtonLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMembersFg;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(14px);
|
||||
linkFont: font(14px);
|
||||
linkFontOver: font(14px underline);
|
||||
}
|
||||
}
|
||||
callMuteButtonSublabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
callMuteButtonLabelsSkip: 5px;
|
||||
callMuteButtonSublabelSkip: 19px;
|
||||
callMuteButtonActive: CallButton {
|
||||
button: callMuteButtonActiveInner;
|
||||
bg: groupCallLive1;
|
||||
bgSize: 100px;
|
||||
bgPosition: point(18px, 18px);
|
||||
outerRadius: 18px;
|
||||
outerBg: callAnswerBgOuter;
|
||||
label: callMuteButtonLabel;
|
||||
}
|
||||
callMuteButtonMuted: CallButton(callMuteButtonActive) {
|
||||
bg: groupCallMuted1;
|
||||
label: callMuteButtonLabel;
|
||||
}
|
||||
callMuteButtonConnecting: CallButton(callMuteButtonMuted) {
|
||||
bg: callIconBg;
|
||||
label: callMuteButtonLabel;
|
||||
}
|
||||
callMuteButtonLabelAdditional: 5px;
|
||||
|
||||
callConnectingRadial: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
|
||||
color: lightButtonFg;
|
||||
thickness: 4px;
|
||||
size: size(100px, 100px);
|
||||
}
|
||||
|
||||
callBarHeight: 38px;
|
||||
callBarMuteToggle: IconButton {
|
||||
width: 41px;
|
||||
@@ -433,7 +465,8 @@ callTitle: WindowTitle(defaultWindowTitle) {
|
||||
closeIconActive: callTitleCloseIcon;
|
||||
closeIconActiveOver: callTitleCloseIconOver;
|
||||
}
|
||||
callTitleShadow: icon {{ "calls/calls_shadow_controls", windowShadowFg }};
|
||||
callTitleShadowRight: icon {{ "calls/calls_shadow_controls", windowShadowFg }};
|
||||
callTitleShadowLeft: icon {{ "calls/calls_shadow_controls-flip_horizontal", windowShadowFg }};
|
||||
|
||||
callErrorToast: Toast(defaultToast) {
|
||||
minWidth: 240px;
|
||||
@@ -442,8 +475,6 @@ callErrorToast: Toast(defaultToast) {
|
||||
groupCallWidth: 380px;
|
||||
groupCallHeight: 580px;
|
||||
|
||||
groupCallMuteButtonIconSize: size(67px, 67px);
|
||||
groupCallMuteButtonIconTop: 35px;
|
||||
groupCallRipple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: groupCallMembersBgRipple;
|
||||
}
|
||||
@@ -532,6 +563,11 @@ groupCallMembersListItem: PeerListItem(defaultPeerListItem) {
|
||||
statusFgOver: groupCallMemberInactiveStatus;
|
||||
statusFgActive: groupCallMemberActiveStatus;
|
||||
}
|
||||
groupCallNarrowMembersListItem: PeerListItem(groupCallMembersListItem) {
|
||||
statusFg: groupCallMemberNotJoinedStatus;
|
||||
statusFgOver: groupCallMemberNotJoinedStatus;
|
||||
statusFgActive: groupCallMemberActiveStatus;
|
||||
}
|
||||
groupCallMembersList: PeerList(defaultPeerList) {
|
||||
bg: groupCallMembersBg;
|
||||
about: FlatLabel(defaultPeerListAbout) {
|
||||
@@ -648,8 +684,8 @@ groupCallShareBoxList: PeerList(groupCallMembersList) {
|
||||
groupCallMembersTop: 51px;
|
||||
groupCallTitleTop: 8px;
|
||||
groupCallSubtitleTop: 26px;
|
||||
groupCallWideVideoTop: 24px;
|
||||
|
||||
groupCallMembersMargin: margins(16px, 16px, 16px, 28px);
|
||||
groupCallAddMember: SettingsButton(defaultSettingsButton) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
textFgOver: groupCallMemberNotJoinedStatus;
|
||||
@@ -678,7 +714,7 @@ groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) {
|
||||
}
|
||||
}
|
||||
groupCallAddButtonPosition: point(10px, 7px);
|
||||
groupCallMembersWidthMax: 360px;
|
||||
groupCallMembersWidthMax: 480px;
|
||||
groupCallRecordingMark: 6px;
|
||||
groupCallRecordingMarkSkip: 4px;
|
||||
groupCallRecordingMarkTop: 8px;
|
||||
@@ -704,6 +740,7 @@ groupCallJoinAsToggle: UserpicButton(defaultUserpicButton) {
|
||||
photoPosition: point(3px, 3px);
|
||||
}
|
||||
groupCallMenuPosition: point(-1px, 29px);
|
||||
groupCallWideMenuPosition: point(-2px, 28px);
|
||||
|
||||
groupCallActiveButton: IconButton {
|
||||
width: 36px;
|
||||
@@ -736,33 +773,106 @@ groupCallMemberRaisedHand: icon {{ "calls/group_calls_raised_hand", groupCallMem
|
||||
|
||||
groupCallSettingsInner: IconButton(callButton) {
|
||||
iconPosition: point(-1px, 22px);
|
||||
icon: icon {{ "calls/call_settings", groupCallIconFg }};
|
||||
icon: icon {{ "calls/calls_settings", groupCallIconFg }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: callMuteRipple;
|
||||
}
|
||||
}
|
||||
groupCallShareInner: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/group_calls_share", groupCallIconFg }};
|
||||
}
|
||||
groupCallVideoInner: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/call_camera_muted", groupCallIconFg }};
|
||||
iconPosition: point(-1px, 16px);
|
||||
}
|
||||
groupCallHangupInner: IconButton(callButton) {
|
||||
icon: icon {{ "calls/call_discard", groupCallIconFg }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: groupCallLeaveBgRipple;
|
||||
}
|
||||
}
|
||||
groupCallSettings: CallButton(callMicrophoneMute) {
|
||||
button: groupCallSettingsInner;
|
||||
}
|
||||
groupCallShare: CallButton(groupCallSettings) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/group_calls_share", groupCallIconFg }};
|
||||
}
|
||||
button: groupCallShareInner;
|
||||
}
|
||||
groupCallVideo: CallButton(groupCallSettings) {
|
||||
button: groupCallVideoInner;
|
||||
}
|
||||
groupCallVideoInnerActive: IconButton(groupCallVideoInner) {
|
||||
icon: icon {{ "calls/call_camera_active", groupCallIconFg }};
|
||||
}
|
||||
groupCallVideoActive: CallButton(groupCallVideo) {
|
||||
button: groupCallVideoInnerActive;
|
||||
}
|
||||
groupCallHangup: CallButton(callHangup) {
|
||||
button: IconButton(callButton) {
|
||||
icon: icon {{ "calls/call_discard", groupCallIconFg }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: groupCallLeaveBgRipple;
|
||||
}
|
||||
}
|
||||
button: groupCallHangupInner;
|
||||
bg: groupCallLeaveBg;
|
||||
outerBg: groupCallLeaveBg;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
groupCallButtonSkip: 43px;
|
||||
groupCallButtonBottomSkip: 145px;
|
||||
groupCallMuteBottomSkip: 160px;
|
||||
groupCallSettingsSmall: CallButton(groupCallSettings) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
bgPosition: point(8px, 12px);
|
||||
}
|
||||
groupCallHangupSmall: CallButton(groupCallHangup) {
|
||||
button: IconButton(groupCallHangupInner) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
bgPosition: point(8px, 12px);
|
||||
}
|
||||
groupCallVideoSmall: CallButton(groupCallSettingsSmall) {
|
||||
button: IconButton(groupCallVideoInner) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallVideoActiveSmall: CallButton(groupCallVideoSmall) {
|
||||
button: IconButton(groupCallVideoInnerActive) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallScreenShareSmall: CallButton(groupCallSettingsSmall) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/calls_present", groupCallIconFg }};
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallMenuToggleSmall: CallButton(groupCallSettingsSmall) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/calls_more", groupCallIconFg }};
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallButtonSkip: 40px;
|
||||
groupCallButtonSkipSmall: 5px;
|
||||
groupCallButtonBottomSkip: 113px;
|
||||
groupCallButtonBottomSkipSmall: 95px;
|
||||
groupCallButtonBottomSkipWide: 122px;
|
||||
groupCallControlsBackMargin: margins(10px, 0px, 10px, 0px);
|
||||
groupCallControlsBackRadius: 12px;
|
||||
groupCallMuteBottomSkip: 116px;
|
||||
|
||||
groupCallMembersMargin: margins(16px, 16px, 16px, 60px);
|
||||
groupCallMembersTopSkip: 6px;
|
||||
groupCallMembersBottomSkip: 80px;
|
||||
groupCallMembersShadowHeight: 160px;
|
||||
groupCallMembersFadeSkip: 10px;
|
||||
groupCallMembersFadeHeight: 100px;
|
||||
|
||||
groupCallTopBarUserpics: GroupCallUserpics {
|
||||
size: 28px;
|
||||
@@ -780,17 +890,18 @@ groupCallTopBarOpen: RoundButton(groupCallTopBarJoin) {
|
||||
color: shadowFg;
|
||||
}
|
||||
}
|
||||
groupCallBox: Box(defaultBox) {
|
||||
button: RoundButton(defaultBoxButton) {
|
||||
textFg: groupCallActiveFg;
|
||||
textFgOver: groupCallActiveFg;
|
||||
numbersTextFg: groupCallActiveFg;
|
||||
numbersTextFgOver: groupCallActiveFg;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
groupCallBoxButton: RoundButton(defaultBoxButton) {
|
||||
textFg: groupCallActiveFg;
|
||||
textFgOver: groupCallActiveFg;
|
||||
numbersTextFg: groupCallActiveFg;
|
||||
numbersTextFgOver: groupCallActiveFg;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
groupCallBox: Box(defaultBox) {
|
||||
button: groupCallBoxButton;
|
||||
margin: margins(0px, 56px, 0px, 10px);
|
||||
bg: groupCallMembersBg;
|
||||
title: FlatLabel(boxTitle) {
|
||||
@@ -807,7 +918,7 @@ groupCallLevelMeter: LevelMeter(defaultLevelMeter) {
|
||||
lineSpacing: 5px;
|
||||
lineCount: 44;
|
||||
activeFg: groupCallActiveFg;
|
||||
inactiveFg: groupCallMemberNotJoinedStatus;
|
||||
inactiveFg: groupCallMembersBgRipple;
|
||||
}
|
||||
groupCallCheckboxIcon: icon {{ "default_checkbox_check", groupCallMembersFg, point(4px, 7px) }};
|
||||
groupCallCheck: Check(defaultCheck) {
|
||||
@@ -1015,3 +1126,127 @@ groupCallStartsInTop: 10px;
|
||||
groupCallStartsWhenTop: 160px;
|
||||
groupCallCountdownFont: font(64px semibold);
|
||||
groupCallCountdownTop: 52px;
|
||||
|
||||
desktopCaptureMargins: margins(12px, 8px, 12px, 6px);
|
||||
desktopCaptureSourceSize: size(235px, 165px);
|
||||
desktopCaptureSourceSkips: size(2px, 10px);
|
||||
desktopCaptureSourceTitle: WindowTitle(groupCallTitle) {
|
||||
bg: groupCallMembersBgOver;
|
||||
bgActive: groupCallMembersBgOver;
|
||||
height: 21px;
|
||||
}
|
||||
desktopCapturePadding: margins(7px, 7px, 7px, 33px);
|
||||
desktopCaptureLabelBottom: 7px;
|
||||
desktopCaptureLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 200px;
|
||||
maxHeight: 20px;
|
||||
textFg: groupCallMembersFg;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
desktopCaptureCancel: RoundButton(defaultBoxButton) {
|
||||
textFg: groupCallActiveFg;
|
||||
textFgOver: groupCallActiveFg;
|
||||
numbersTextFg: groupCallActiveFg;
|
||||
numbersTextFgOver: groupCallActiveFg;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
desktopCaptureFinish: RoundButton(desktopCaptureCancel) {
|
||||
textFg: groupCallMemberMutedIcon;
|
||||
textFgOver: groupCallMemberMutedIcon;
|
||||
}
|
||||
desktopCaptureSubmit: RoundButton(desktopCaptureCancel) {
|
||||
textFg: groupCallIconFg;
|
||||
textFgOver: groupCallIconFg;
|
||||
numbersTextFg: groupCallIconFg;
|
||||
numbersTextFgOver: groupCallIconFg;
|
||||
textBg: groupCallMuted1;
|
||||
textBgOver: groupCallMuted1;
|
||||
|
||||
ripple: RippleAnimation(groupCallRipple) {
|
||||
color: shadowFg;
|
||||
}
|
||||
}
|
||||
|
||||
groupCallNarrowSkip: 9px;
|
||||
groupCallNarrowMembersWidth: 204px;
|
||||
groupCallNarrowVideoHeight: 120px;
|
||||
groupCallWideModeWidthMin: 600px;
|
||||
groupCallWideModeSize: size(960px, 580px);
|
||||
groupCallNarrowInactiveCrossLine: CrossLineAnimation {
|
||||
fg: groupCallMemberNotJoinedStatus;
|
||||
icon: icon {{ "calls/video_mini_mute", groupCallMemberNotJoinedStatus }};
|
||||
startPosition: point(3px, 0px);
|
||||
endPosition: point(13px, 12px);
|
||||
stroke: 3px;
|
||||
strokeDenominator: 2;
|
||||
}
|
||||
groupCallNarrowColoredCrossLine: CrossLineAnimation(groupCallNarrowInactiveCrossLine) {
|
||||
fg: groupCallMemberNotJoinedStatus;
|
||||
icon: icon {{ "calls/video_mini_mute", groupCallMemberActiveStatus }};
|
||||
}
|
||||
groupCallNarrowRaisedHand: icon {{ "calls/video_mini_speak", groupCallMemberInactiveStatus }};
|
||||
groupCallNarrowCameraIcon: icon {{ "calls/video_mini_video", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowScreenIcon: icon {{ "calls/video_mini_screencast", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowIconPosition: point(-4px, 2px);
|
||||
groupCallNarrowIconSkip: 15px;
|
||||
groupCallOutline: 2px;
|
||||
groupCallVideoCrossLine: CrossLineAnimation(groupCallMemberColoredCrossLine) {
|
||||
fg: groupCallVideoTextFg;
|
||||
icon: icon {{ "calls/video_over_mute", groupCallVideoTextFg }};
|
||||
}
|
||||
|
||||
GroupCallVideoTile {
|
||||
shadowHeight: pixels;
|
||||
namePosition: point;
|
||||
pin: CrossLineAnimation;
|
||||
pinPosition: point;
|
||||
pinPadding: margins;
|
||||
pinTextPosition: point;
|
||||
back: icon;
|
||||
iconPosition: point;
|
||||
}
|
||||
|
||||
groupCallVideoTile: GroupCallVideoTile {
|
||||
shadowHeight: 40px;
|
||||
namePosition: point(15px, 8px);
|
||||
pin: CrossLineAnimation {
|
||||
fg: groupCallVideoTextFg;
|
||||
icon: icon {{ "calls/video_over_pin", groupCallVideoTextFg }};
|
||||
startPosition: point(7px, 4px);
|
||||
endPosition: point(17px, 14px);
|
||||
stroke: 3px;
|
||||
strokeDenominator: 2;
|
||||
}
|
||||
pinPosition: point(18px, 18px);
|
||||
pinPadding: margins(6px, 2px, 12px, 1px);
|
||||
pinTextPosition: point(1px, 3px);
|
||||
back: icon {{ "calls/video_back", groupCallVideoTextFg }};
|
||||
iconPosition: point(10px, 5px);
|
||||
}
|
||||
|
||||
groupCallVideoSmallSkip: 4px;
|
||||
groupCallVideoLargeSkip: 6px;
|
||||
|
||||
groupCallTooltip: Tooltip(defaultTooltip) {
|
||||
textBg: groupCallMembersBg;
|
||||
textFg: groupCallMembersFg;
|
||||
textBorder: groupCallMembersBgOver;
|
||||
}
|
||||
groupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) {
|
||||
bg: importantTooltipBg;
|
||||
padding: margins(10px, 3px, 10px, 5px);
|
||||
radius: 4px;
|
||||
arrow: 4px;
|
||||
}
|
||||
groupCallNiceTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(11px);
|
||||
linkFont: font(11px);
|
||||
linkFontOver: font(11px underline);
|
||||
}
|
||||
}
|
||||
groupCallNiceTooltipTop: 4px;
|
||||
groupCallPaused: icon {{ "calls/video_large_paused", groupCallVideoTextFg }};
|
||||
|
||||
@@ -161,8 +161,12 @@ Call::Call(
|
||||
, _user(user)
|
||||
, _api(&_user->session().mtp())
|
||||
, _type(type)
|
||||
, _videoIncoming(std::make_unique<Webrtc::VideoTrack>(StartVideoState(video)))
|
||||
, _videoOutgoing(std::make_unique<Webrtc::VideoTrack>(StartVideoState(video))) {
|
||||
, _videoIncoming(
|
||||
std::make_unique<Webrtc::VideoTrack>(
|
||||
StartVideoState(video)))
|
||||
, _videoOutgoing(
|
||||
std::make_unique<Webrtc::VideoTrack>(
|
||||
StartVideoState(video))) {
|
||||
_discardByTimeoutTimer.setCallback([=] { hangup(); });
|
||||
|
||||
if (_type == Type::Outgoing) {
|
||||
@@ -380,7 +384,7 @@ void Call::setupOutgoingVideo() {
|
||||
// Paused not supported right now.
|
||||
Assert(state == Webrtc::VideoState::Active);
|
||||
if (!_videoCapture) {
|
||||
_videoCapture = _delegate->getVideoCapture();
|
||||
_videoCapture = _delegate->callGetVideoCapture();
|
||||
_videoCapture->setOutput(_videoOutgoing->sink());
|
||||
}
|
||||
if (_instance) {
|
||||
|
||||
@@ -53,6 +53,11 @@ struct Error {
|
||||
QString details;
|
||||
};
|
||||
|
||||
enum class CallType {
|
||||
Incoming,
|
||||
Outgoing,
|
||||
};
|
||||
|
||||
class Call : public base::has_weak_ptr {
|
||||
public:
|
||||
class Delegate {
|
||||
@@ -72,7 +77,7 @@ public:
|
||||
Fn<void()> onSuccess,
|
||||
bool video) = 0;
|
||||
|
||||
virtual auto getVideoCapture()
|
||||
virtual auto callGetVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
virtual ~Delegate() = default;
|
||||
@@ -81,11 +86,12 @@ public:
|
||||
|
||||
static constexpr auto kSoundSampleMs = 100;
|
||||
|
||||
enum class Type {
|
||||
Incoming,
|
||||
Outgoing,
|
||||
};
|
||||
Call(not_null<Delegate*> delegate, not_null<UserData*> user, Type type, bool video);
|
||||
using Type = CallType;
|
||||
Call(
|
||||
not_null<Delegate*> delegate,
|
||||
not_null<UserData*> user,
|
||||
Type type,
|
||||
bool video);
|
||||
|
||||
[[nodiscard]] Type type() const {
|
||||
return _type;
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/bytes.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "mtproto/mtproto_auth_key.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace tgcalls {
|
||||
class GroupInstanceCustomImpl;
|
||||
struct GroupLevelsUpdate;
|
||||
struct GroupNetworkState;
|
||||
struct GroupParticipantDescription;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
class GlobalShortcutManager;
|
||||
class GlobalShortcutValue;
|
||||
} // namespace base
|
||||
|
||||
namespace Webrtc {
|
||||
class MediaDevices;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Data {
|
||||
struct LastSpokeTimes;
|
||||
struct GroupCallParticipant;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
struct JoinInfo;
|
||||
struct RejoinEvent;
|
||||
} // namespace Group
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
RaisedHand,
|
||||
};
|
||||
|
||||
[[nodiscard]] inline auto MapPushToTalkToActive() {
|
||||
return rpl::map([=](MuteState state) {
|
||||
return (state == MuteState::PushToTalk) ? MuteState::Active : state;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGroupCallAdmin(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PeerData*> participantPeer);
|
||||
|
||||
struct LevelUpdate {
|
||||
uint32 ssrc = 0;
|
||||
float value = 0.;
|
||||
bool voice = false;
|
||||
bool me = false;
|
||||
};
|
||||
|
||||
class GroupCall final : public base::has_weak_ptr {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
virtual void groupCallFinished(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) = 0;
|
||||
|
||||
enum class GroupCallSound {
|
||||
Started,
|
||||
Connecting,
|
||||
AllowedToSpeak,
|
||||
Ended,
|
||||
};
|
||||
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
|
||||
};
|
||||
|
||||
using GlobalShortcutManager = base::GlobalShortcutManager;
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] uint64 id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> joinAs() const {
|
||||
return _joinAs;
|
||||
}
|
||||
[[nodiscard]] bool showChooseJoinAs() const;
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate;
|
||||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const;
|
||||
|
||||
[[nodiscard]] Data::GroupCall *lookupReal() const;
|
||||
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
|
||||
|
||||
void start(TimeId scheduleDate);
|
||||
void hangup();
|
||||
void discard();
|
||||
void rejoinAs(Group::JoinInfo info);
|
||||
void rejoinWithHash(const QString &hash);
|
||||
void join(const MTPInputGroupCall &inputCall);
|
||||
void handleUpdate(const MTPUpdate &update);
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void changeTitle(const QString &title);
|
||||
void toggleRecording(bool enabled, const QString &title);
|
||||
[[nodiscard]] bool recordingStoppedByMe() const {
|
||||
return _recordingStoppedByMe;
|
||||
}
|
||||
void startScheduledNow();
|
||||
void toggleScheduleStartSubscribed(bool subscribed);
|
||||
|
||||
void setMuted(MuteState mute);
|
||||
void setMutedAndUpdate(MuteState mute);
|
||||
[[nodiscard]] MuteState muted() const {
|
||||
return _muted.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<MuteState> mutedValue() const {
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState>;
|
||||
|
||||
enum State {
|
||||
Creating,
|
||||
Waiting,
|
||||
Joining,
|
||||
Connecting,
|
||||
Joined,
|
||||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
Ended,
|
||||
};
|
||||
[[nodiscard]] State state() const {
|
||||
return _state.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
|
||||
enum class InstanceState {
|
||||
Disconnected,
|
||||
TransitionToRtc,
|
||||
Connected,
|
||||
};
|
||||
[[nodiscard]] InstanceState instanceState() const {
|
||||
return _instanceState.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {
|
||||
return _instanceState.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
|
||||
return _levelUpdates.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {
|
||||
return _rejoinEvents.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {
|
||||
return _allowedToSpeakNotifications.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> titleChanged() const {
|
||||
return _titleChanged.events();
|
||||
}
|
||||
static constexpr auto kSpeakLevelThreshold = 0.2;
|
||||
|
||||
void setCurrentAudioDevice(bool input, const QString &deviceId);
|
||||
//void setAudioVolume(bool input, float level);
|
||||
void setAudioDuckingEnabled(bool enabled);
|
||||
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
std::variant<int, not_null<UserData*>> inviteUsers(
|
||||
const std::vector<not_null<UserData*>> &users);
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
class LoadPartTask;
|
||||
|
||||
public:
|
||||
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
|
||||
void broadcastPartCancel(not_null<LoadPartTask*> task);
|
||||
|
||||
private:
|
||||
using GlobalShortcutValue = base::GlobalShortcutValue;
|
||||
|
||||
struct LoadingPart {
|
||||
std::shared_ptr<LoadPartTask> task;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
enum class FinishType {
|
||||
None,
|
||||
Ended,
|
||||
Failed,
|
||||
};
|
||||
enum class InstanceMode {
|
||||
None,
|
||||
Rtc,
|
||||
Stream,
|
||||
};
|
||||
enum class SendUpdateType {
|
||||
Mute,
|
||||
RaiseHand,
|
||||
};
|
||||
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);
|
||||
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
|
||||
void handleUpdate(const MTPDupdateGroupCall &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
|
||||
void handleRequestError(const MTP::Error &error);
|
||||
void handleControllerError(const QString &error);
|
||||
void ensureControllerCreated();
|
||||
void destroyController();
|
||||
|
||||
void setState(State state);
|
||||
void finish(FinishType type);
|
||||
void maybeSendMutedUpdate(MuteState previous);
|
||||
void sendSelfUpdate(SendUpdateType type);
|
||||
void updateInstanceMuteState();
|
||||
void updateInstanceVolumes();
|
||||
void applyMeInCallLocally();
|
||||
void rejoin();
|
||||
void rejoin(not_null<PeerData*> as);
|
||||
void setJoinAs(not_null<PeerData*> as);
|
||||
void saveDefaultJoinAs(not_null<PeerData*> as);
|
||||
void subscribeToReal(not_null<Data::GroupCall*> real);
|
||||
void setScheduledDate(TimeId date);
|
||||
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setInstanceMode(InstanceMode mode);
|
||||
void checkLastSpoke();
|
||||
void pushToTalkCancel();
|
||||
|
||||
void checkGlobalShortcutAvailability();
|
||||
void checkJoined();
|
||||
void checkFirstTimeJoined();
|
||||
void notifyAboutAllowedToSpeak();
|
||||
|
||||
void playConnectingSound();
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
void requestParticipantsInformation(const std::vector<uint32_t> &ssrcs);
|
||||
void addParticipantsToInstance();
|
||||
void prepareParticipantForAdding(
|
||||
const Data::GroupCallParticipant &participant);
|
||||
void addPreparedParticipants();
|
||||
void addPreparedParticipantsDelayed();
|
||||
|
||||
void editParticipant(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyParticipantLocally(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyQueuedSelfUpdates();
|
||||
void applySelfUpdate(const MTPDgroupCallParticipant &data);
|
||||
void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
not_null<PeerData*> _peer; // Can change in legacy group migration.
|
||||
rpl::event_stream<PeerData*> _peerStream;
|
||||
not_null<History*> _history; // Can change in legacy group migration.
|
||||
MTP::Sender _api;
|
||||
rpl::event_stream<not_null<Data::GroupCall*>> _realChanges;
|
||||
rpl::variable<State> _state = State::Creating;
|
||||
rpl::variable<InstanceState> _instanceState
|
||||
= InstanceState::Disconnected;
|
||||
bool _instanceTransitioning = false;
|
||||
InstanceMode _instanceMode = InstanceMode::None;
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
std::vector<tgcalls::GroupParticipantDescription> _preparedParticipants;
|
||||
bool _addPreparedParticipantsScheduled = false;
|
||||
bool _recordingStoppedByMe = false;
|
||||
|
||||
MTP::DcId _broadcastDcId = 0;
|
||||
base::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;
|
||||
|
||||
not_null<PeerData*> _joinAs;
|
||||
std::vector<not_null<PeerData*>> _possibleJoinAs;
|
||||
QString _joinHash;
|
||||
|
||||
rpl::variable<MuteState> _muted = MuteState::Muted;
|
||||
bool _initialMuteStateSent = false;
|
||||
bool _acceptFields = false;
|
||||
|
||||
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
|
||||
std::vector<MTPGroupCallParticipant> _queuedSelfUpdates;
|
||||
|
||||
uint64 _id = 0;
|
||||
uint64 _accessHash = 0;
|
||||
uint32 _mySsrc = 0;
|
||||
TimeId _scheduleDate = 0;
|
||||
base::flat_set<uint32> _mySsrcs;
|
||||
mtpRequestId _createRequestId = 0;
|
||||
mtpRequestId _updateMuteRequestId = 0;
|
||||
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;
|
||||
rpl::event_stream<LevelUpdate> _levelUpdates;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
rpl::event_stream<Group::RejoinEvent> _rejoinEvents;
|
||||
rpl::event_stream<> _allowedToSpeakNotifications;
|
||||
rpl::event_stream<> _titleChanged;
|
||||
base::Timer _lastSpokeCheckTimer;
|
||||
base::Timer _checkJoinedTimer;
|
||||
|
||||
crl::time _lastSendProgressUpdate = 0;
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
|
||||
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
|
||||
base::Timer _pushToTalkCancelTimer;
|
||||
base::Timer _connectingSoundTimer;
|
||||
bool _hadJoinedState = false;
|
||||
|
||||
std::unique_ptr<Webrtc::MediaDevices> _mediaDevices;
|
||||
QString _audioInputId;
|
||||
QString _audioOutputId;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_instance.h"
|
||||
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "mtproto/mtproto_dh_utils.h"
|
||||
#include "core/application.h"
|
||||
#include "main/main_session.h"
|
||||
@@ -15,10 +18,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_panel.h"
|
||||
#include "calls/calls_group_panel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_channel.h"
|
||||
@@ -41,11 +44,142 @@ namespace {
|
||||
|
||||
constexpr auto kServerConfigUpdateTimeoutMs = 24 * 3600 * crl::time(1000);
|
||||
|
||||
using CallSound = Call::Delegate::CallSound;
|
||||
using GroupCallSound = GroupCall::Delegate::GroupCallSound;
|
||||
|
||||
} // namespace
|
||||
|
||||
Instance::Instance() = default;
|
||||
class Instance::Delegate final
|
||||
: public Call::Delegate
|
||||
, public GroupCall::Delegate {
|
||||
public:
|
||||
explicit Delegate(not_null<Instance*> instance);
|
||||
|
||||
Instance::~Instance() = default;
|
||||
DhConfig getDhConfig() const override;
|
||||
|
||||
void callFinished(not_null<Call*> call) override;
|
||||
void callFailed(not_null<Call*> call) override;
|
||||
void callRedial(not_null<Call*> call) override;
|
||||
void callRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess,
|
||||
bool video) override;
|
||||
void callPlaySound(CallSound sound) override;
|
||||
auto callGetVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
|
||||
|
||||
void groupCallFinished(not_null<GroupCall*> call) override;
|
||||
void groupCallFailed(not_null<GroupCall*> call) override;
|
||||
void groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override;
|
||||
void groupCallPlaySound(GroupCallSound sound) override;
|
||||
auto groupCallGetVideoCapture(const QString &deviceId)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
|
||||
FnMut<void()> groupCallAddAsyncWaiter() override;
|
||||
|
||||
private:
|
||||
const not_null<Instance*> _instance;
|
||||
|
||||
};
|
||||
|
||||
Instance::Delegate::Delegate(not_null<Instance*> instance)
|
||||
: _instance(instance) {
|
||||
}
|
||||
|
||||
DhConfig Instance::Delegate::getDhConfig() const {
|
||||
return *_instance->_cachedDhConfig;
|
||||
}
|
||||
|
||||
void Instance::Delegate::callFinished(not_null<Call*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
_instance->destroyCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::Delegate::callFailed(not_null<Call*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
_instance->destroyCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::Delegate::callRedial(not_null<Call*> call) {
|
||||
if (_instance->_currentCall.get() == call) {
|
||||
_instance->refreshDhConfig();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Delegate::callRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess,
|
||||
bool video) {
|
||||
_instance->requestPermissionsOrFail(std::move(onSuccess), video);
|
||||
}
|
||||
|
||||
void Instance::Delegate::callPlaySound(CallSound sound) {
|
||||
_instance->playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case CallSound::Busy: return "call_busy";
|
||||
case CallSound::Ended: return "call_end";
|
||||
case CallSound::Connecting: return "call_connect";
|
||||
}
|
||||
Unexpected("CallSound in Instance::callPlaySound.");
|
||||
}());
|
||||
}
|
||||
|
||||
auto Instance::Delegate::callGetVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> {
|
||||
return _instance->getVideoCapture();
|
||||
}
|
||||
|
||||
void Instance::Delegate::groupCallFinished(not_null<GroupCall*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
_instance->destroyGroupCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::Delegate::groupCallFailed(not_null<GroupCall*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
_instance->destroyGroupCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::Delegate::groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) {
|
||||
_instance->requestPermissionsOrFail(std::move(onSuccess), false);
|
||||
}
|
||||
|
||||
void Instance::Delegate::groupCallPlaySound(GroupCallSound sound) {
|
||||
_instance->playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case GroupCallSound::Started: return "group_call_start";
|
||||
case GroupCallSound::Ended: return "group_call_end";
|
||||
case GroupCallSound::AllowedToSpeak: return "group_call_allowed";
|
||||
case GroupCallSound::Connecting: return "group_call_connect";
|
||||
}
|
||||
Unexpected("GroupCallSound in Instance::groupCallPlaySound.");
|
||||
}());
|
||||
}
|
||||
|
||||
auto Instance::Delegate::groupCallGetVideoCapture(const QString &deviceId)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> {
|
||||
return _instance->getVideoCapture(deviceId);
|
||||
}
|
||||
|
||||
FnMut<void()> Instance::Delegate::groupCallAddAsyncWaiter() {
|
||||
return _instance->addAsyncWaiter();
|
||||
}
|
||||
|
||||
Instance::Instance()
|
||||
: _delegate(std::make_unique<Delegate>(this))
|
||||
, _cachedDhConfig(std::make_unique<DhConfig>())
|
||||
, _chooseJoinAs(std::make_unique<Group::ChooseJoinAsProcess>()) {
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
destroyCurrentCall();
|
||||
|
||||
while (!_asyncWaiters.empty()) {
|
||||
_asyncWaiters.front()->acquire();
|
||||
_asyncWaiters.erase(_asyncWaiters.begin());
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::startOutgoingCall(not_null<UserData*> user, bool video) {
|
||||
if (activateCurrentCall()) {
|
||||
@@ -72,7 +206,7 @@ void Instance::startOrJoinGroupCall(
|
||||
: peer->groupCall()
|
||||
? Group::ChooseJoinAsProcess::Context::Join
|
||||
: Group::ChooseJoinAsProcess::Context::Create;
|
||||
_chooseJoinAs.start(peer, context, [=](object_ptr<Ui::BoxContent> box) {
|
||||
_chooseJoinAs->start(peer, context, [=](object_ptr<Ui::BoxContent> box) {
|
||||
Ui::show(std::move(box), Ui::LayerOption::KeepOther);
|
||||
}, [=](QString text) {
|
||||
Ui::Toast::Show(text);
|
||||
@@ -85,36 +219,6 @@ void Instance::startOrJoinGroupCall(
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::callFinished(not_null<Call*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
destroyCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::callFailed(not_null<Call*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
destroyCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::callRedial(not_null<Call*> call) {
|
||||
if (_currentCall.get() == call) {
|
||||
refreshDhConfig();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::groupCallFinished(not_null<GroupCall*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
destroyGroupCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::groupCallFailed(not_null<GroupCall*> call) {
|
||||
crl::on_main(call, [=] {
|
||||
destroyGroupCall(call);
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Media::Audio::Track*> Instance::ensureSoundLoaded(
|
||||
const QString &key) {
|
||||
const auto i = _tracks.find(key);
|
||||
@@ -132,31 +236,6 @@ void Instance::playSoundOnce(const QString &key) {
|
||||
ensureSoundLoaded(key)->playOnce();
|
||||
}
|
||||
|
||||
void Instance::callPlaySound(CallSound sound) {
|
||||
playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case CallSound::Busy: return "call_busy";
|
||||
case CallSound::Ended: return "call_end";
|
||||
case CallSound::Connecting: return "call_connect";
|
||||
}
|
||||
Unexpected("CallSound in Instance::callPlaySound.");
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
|
||||
void Instance::groupCallPlaySound(GroupCallSound sound) {
|
||||
playSoundOnce([&] {
|
||||
switch (sound) {
|
||||
case GroupCallSound::Started: return "group_call_start";
|
||||
case GroupCallSound::Ended: return "group_call_end";
|
||||
case GroupCallSound::AllowedToSpeak: return "group_call_allowed";
|
||||
case GroupCallSound::Connecting: return "group_call_connect";
|
||||
}
|
||||
Unexpected("GroupCallSound in Instance::groupCallPlaySound.");
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
|
||||
void Instance::destroyCall(not_null<Call*> call) {
|
||||
if (_currentCall.get() == call) {
|
||||
_currentCallPanel->closeBeforeDestroy();
|
||||
@@ -174,7 +253,7 @@ void Instance::destroyCall(not_null<Call*> call) {
|
||||
}
|
||||
|
||||
void Instance::createCall(not_null<UserData*> user, Call::Type type, bool video) {
|
||||
auto call = std::make_unique<Call>(getCallDelegate(), user, type, video);
|
||||
auto call = std::make_unique<Call>(_delegate.get(), user, type, video);
|
||||
const auto raw = call.get();
|
||||
|
||||
user->session().account().sessionChanges(
|
||||
@@ -217,7 +296,7 @@ void Instance::createGroupCall(
|
||||
destroyCurrentCall();
|
||||
|
||||
auto call = std::make_unique<GroupCall>(
|
||||
getGroupCallDelegate(),
|
||||
_delegate.get(),
|
||||
std::move(info),
|
||||
inputCall);
|
||||
const auto raw = call.get();
|
||||
@@ -237,7 +316,7 @@ void Instance::refreshDhConfig() {
|
||||
|
||||
const auto weak = base::make_weak(_currentCall);
|
||||
_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(
|
||||
MTP_int(_dhConfig.version),
|
||||
MTP_int(_cachedDhConfig->version),
|
||||
MTP_int(MTP::ModExpFirst::kRandomPowerSize)
|
||||
)).done([=](const MTPmessages_DhConfig &result) {
|
||||
const auto call = weak.get();
|
||||
@@ -249,14 +328,14 @@ void Instance::refreshDhConfig() {
|
||||
Assert(random.size() == MTP::ModExpFirst::kRandomPowerSize);
|
||||
call->start(random);
|
||||
} else {
|
||||
callFailed(call);
|
||||
_delegate->callFailed(call);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
const auto call = weak.get();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
callFailed(call);
|
||||
_delegate->callFailed(call);
|
||||
}).send();
|
||||
}
|
||||
|
||||
@@ -277,13 +356,13 @@ bytes::const_span Instance::updateDhConfig(
|
||||
} else if (!validRandom(data.vrandom().v)) {
|
||||
return {};
|
||||
}
|
||||
_dhConfig.g = data.vg().v;
|
||||
_dhConfig.p = std::move(primeBytes);
|
||||
_dhConfig.version = data.vversion().v;
|
||||
_cachedDhConfig->g = data.vg().v;
|
||||
_cachedDhConfig->p = std::move(primeBytes);
|
||||
_cachedDhConfig->version = data.vversion().v;
|
||||
return bytes::make_span(data.vrandom().v);
|
||||
}, [&](const MTPDmessages_dhConfigNotModified &data)
|
||||
-> bytes::const_span {
|
||||
if (!_dhConfig.g || _dhConfig.p.empty()) {
|
||||
if (!_cachedDhConfig->g || _cachedDhConfig->p.empty()) {
|
||||
LOG(("API Error: dhConfigNotModified on zero version."));
|
||||
return {};
|
||||
} else if (!validRandom(data.vrandom().v)) {
|
||||
@@ -324,6 +403,8 @@ void Instance::handleUpdate(
|
||||
handleSignalingData(session, data);
|
||||
}, [&](const MTPDupdateGroupCall &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallConnection &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallParticipants &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [](const auto &) {
|
||||
@@ -357,6 +438,26 @@ void Instance::setCurrentAudioDevice(bool input, const QString &deviceId) {
|
||||
}
|
||||
}
|
||||
|
||||
FnMut<void()> Instance::addAsyncWaiter() {
|
||||
auto semaphore = std::make_unique<crl::semaphore>();
|
||||
const auto raw = semaphore.get();
|
||||
const auto weak = base::make_weak(this);
|
||||
_asyncWaiters.emplace(std::move(semaphore));
|
||||
return [raw, weak] {
|
||||
raw->release();
|
||||
crl::on_main(weak, [raw, weak] {
|
||||
auto &waiters = weak->_asyncWaiters;
|
||||
auto wrapped = std::unique_ptr<crl::semaphore>(raw);
|
||||
const auto i = waiters.find(wrapped);
|
||||
wrapped.release();
|
||||
|
||||
if (i != end(waiters)) {
|
||||
waiters.erase(i);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
bool Instance::isQuitPrevent() {
|
||||
if (!_currentCall || _currentCall->isIncomingWaiting()) {
|
||||
return false;
|
||||
@@ -415,10 +516,15 @@ void Instance::handleGroupCallUpdate(
|
||||
&& (&_currentGroupCall->peer()->session() == session)) {
|
||||
update.match([&](const MTPDupdateGroupCall &data) {
|
||||
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [&](const MTPDupdateGroupCallConnection &data) {
|
||||
_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [](const auto &) {
|
||||
});
|
||||
}
|
||||
|
||||
if (update.type() == mtpc_updateGroupCallConnection) {
|
||||
return;
|
||||
}
|
||||
const auto callId = update.match([](const MTPDupdateGroupCall &data) {
|
||||
return data.vcall().match([](const auto &data) {
|
||||
return data.vid().v;
|
||||
@@ -592,14 +698,19 @@ void Instance::requestPermissionOrFail(Platform::PermissionType type, Fn<void()>
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture() {
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture(
|
||||
QString deviceId) {
|
||||
if (deviceId.isEmpty()) {
|
||||
deviceId = Core::App().settings().callVideoInputDeviceId();
|
||||
}
|
||||
if (auto result = _videoCapture.lock()) {
|
||||
result->switchToDevice(deviceId.toStdString());
|
||||
return result;
|
||||
}
|
||||
auto result = std::shared_ptr<tgcalls::VideoCaptureInterface>(
|
||||
tgcalls::VideoCaptureInterface::Create(
|
||||
tgcalls::StaticThreads::getThreads(),
|
||||
Core::App().settings().callVideoInputDeviceId().toStdString()));
|
||||
deviceId.toStdString()));
|
||||
_videoCapture = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "mtproto/sender.h"
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_choose_join_as.h"
|
||||
|
||||
namespace crl {
|
||||
class semaphore;
|
||||
} // namespace crl
|
||||
|
||||
namespace Platform {
|
||||
enum class PermissionType;
|
||||
@@ -27,17 +28,22 @@ class Session;
|
||||
namespace Calls::Group {
|
||||
struct JoinInfo;
|
||||
class Panel;
|
||||
class ChooseJoinAsProcess;
|
||||
} // namespace Calls::Group
|
||||
|
||||
namespace tgcalls {
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Call;
|
||||
enum class CallType;
|
||||
class GroupCall;
|
||||
class Panel;
|
||||
struct DhConfig;
|
||||
|
||||
class Instance
|
||||
: private Call::Delegate
|
||||
, private GroupCall::Delegate
|
||||
, private base::Subscriber
|
||||
, public base::has_weak_ptr {
|
||||
class Instance : private base::Subscriber, public base::has_weak_ptr {
|
||||
public:
|
||||
Instance();
|
||||
~Instance();
|
||||
@@ -69,49 +75,24 @@ public:
|
||||
bool activateCurrentCall(const QString &joinHash = QString());
|
||||
bool minimizeCurrentActiveCall();
|
||||
bool closeCurrentActiveCall();
|
||||
auto getVideoCapture()
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
|
||||
[[nodiscard]] auto getVideoCapture(QString deviceId = QString())
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface>;
|
||||
void requestPermissionsOrFail(Fn<void()> onSuccess, bool video = true);
|
||||
|
||||
void setCurrentAudioDevice(bool input, const QString &deviceId);
|
||||
|
||||
[[nodiscard]] FnMut<void()> addAsyncWaiter();
|
||||
|
||||
[[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);
|
||||
}
|
||||
[[nodiscard]] not_null<GroupCall::Delegate*> getGroupCallDelegate() {
|
||||
return static_cast<GroupCall::Delegate*>(this);
|
||||
}
|
||||
[[nodiscard]] DhConfig getDhConfig() const override {
|
||||
return _dhConfig;
|
||||
}
|
||||
class Delegate;
|
||||
friend class Delegate;
|
||||
|
||||
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;
|
||||
void callRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess,
|
||||
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;
|
||||
|
||||
void createCall(not_null<UserData*> user, Call::Type type, bool video);
|
||||
void createCall(not_null<UserData*> user, CallType type, bool video);
|
||||
void destroyCall(not_null<Call*> call);
|
||||
|
||||
void createGroupCall(
|
||||
@@ -138,7 +119,8 @@ private:
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdate &update);
|
||||
|
||||
DhConfig _dhConfig;
|
||||
const std::unique_ptr<Delegate> _delegate;
|
||||
const std::unique_ptr<DhConfig> _cachedDhConfig;
|
||||
|
||||
crl::time _lastServerConfigUpdateTime = 0;
|
||||
base::weak_ptr<Main::Session> _serverConfigRequestSession;
|
||||
@@ -154,7 +136,9 @@ private:
|
||||
|
||||
base::flat_map<QString, std::unique_ptr<Media::Audio::Track>> _tracks;
|
||||
|
||||
Group::ChooseJoinAsProcess _chooseJoinAs;
|
||||
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
|
||||
|
||||
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "calls/calls_userpic.h"
|
||||
#include "calls/calls_video_bubble.h"
|
||||
#include "calls/calls_video_incoming.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/widgets/call_button.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@@ -29,6 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_shader.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/emoji_config.h"
|
||||
@@ -50,141 +53,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
#if defined Q_OS_MAC && !defined OS_MAC_OLD
|
||||
#define USE_OPENGL_OVERLAY_WIDGET
|
||||
#endif // Q_OS_MAC && !OS_MAC_OLD
|
||||
|
||||
#ifdef USE_OPENGL_OVERLAY_WIDGET
|
||||
using IncomingParent = Ui::RpWidgetWrap<QOpenGLWidget>;
|
||||
#else // USE_OPENGL_OVERLAY_WIDGET
|
||||
using IncomingParent = Ui::RpWidget;
|
||||
#endif // USE_OPENGL_OVERLAY_WIDGET
|
||||
|
||||
} // namespace
|
||||
|
||||
class Panel::Incoming final : public IncomingParent {
|
||||
public:
|
||||
Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void initBottomShadow();
|
||||
void fillTopShadow(QPainter &p);
|
||||
void fillBottomShadow(QPainter &p);
|
||||
|
||||
const not_null<Webrtc::VideoTrack*> _track;
|
||||
QPixmap _bottomShadow;
|
||||
|
||||
};
|
||||
|
||||
Panel::Incoming::Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track)
|
||||
: IncomingParent(parent)
|
||||
, _track(track) {
|
||||
initBottomShadow();
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
void Panel::Incoming::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
const auto [image, rotation] = _track->frameOriginalWithRotation();
|
||||
if (image.isNull()) {
|
||||
p.fillRect(e->rect(), Qt::black);
|
||||
} else {
|
||||
using namespace Media::View;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
if (UsePainterRotation(rotation)) {
|
||||
if (rotation) {
|
||||
p.save();
|
||||
p.rotate(rotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(rect(), rotation), image);
|
||||
if (rotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (rotation) {
|
||||
p.drawImage(rect(), RotateFrameImage(image, rotation));
|
||||
} else {
|
||||
p.drawImage(rect(), image);
|
||||
}
|
||||
fillBottomShadow(p);
|
||||
fillTopShadow(p);
|
||||
}
|
||||
_track->markFrameShown();
|
||||
}
|
||||
|
||||
void Panel::Incoming::initBottomShadow() {
|
||||
auto image = QImage(
|
||||
QSize(1, st::callBottomShadowSize) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
const auto colorFrom = uint32(0);
|
||||
const auto colorTill = uint32(74);
|
||||
const auto rows = image.height();
|
||||
const auto step = (uint64(colorTill - colorFrom) << 32) / rows;
|
||||
auto accumulated = uint64();
|
||||
auto bytes = image.bits();
|
||||
for (auto y = 0; y != rows; ++y) {
|
||||
accumulated += step;
|
||||
const auto color = (colorFrom + uint32(accumulated >> 32)) << 24;
|
||||
for (auto x = 0; x != image.width(); ++x) {
|
||||
*(reinterpret_cast<uint32*>(bytes) + x) = color;
|
||||
}
|
||||
bytes += image.bytesPerLine();
|
||||
}
|
||||
_bottomShadow = Images::PixmapFast(std::move(image));
|
||||
}
|
||||
|
||||
void Panel::Incoming::fillTopShadow(QPainter &p) {
|
||||
#ifdef Q_OS_WIN
|
||||
const auto width = parentWidget()->width();
|
||||
const auto position = QPoint(width - st::callTitleShadow.width(), 0);
|
||||
const auto shadowArea = QRect(
|
||||
position,
|
||||
st::callTitleShadow.size());
|
||||
const auto fill = shadowArea.intersected(geometry()).translated(-pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.save();
|
||||
p.setClipRect(fill);
|
||||
st::callTitleShadow.paint(p, position - pos(), width);
|
||||
p.restore();
|
||||
#endif // Q_OS_WIN
|
||||
}
|
||||
|
||||
void Panel::Incoming::fillBottomShadow(QPainter &p) {
|
||||
const auto shadowArea = QRect(
|
||||
0,
|
||||
parentWidget()->height() - st::callBottomShadowSize,
|
||||
parentWidget()->width(),
|
||||
st::callBottomShadowSize);
|
||||
const auto fill = shadowArea.intersected(geometry()).translated(-pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = cIntRetinaFactor();
|
||||
p.drawPixmap(
|
||||
fill,
|
||||
_bottomShadow,
|
||||
QRect(
|
||||
0,
|
||||
factor * (fill.y() - shadowArea.translated(-pos()).y()),
|
||||
factor,
|
||||
factor * fill.height()));
|
||||
}
|
||||
|
||||
Panel::Panel(not_null<Call*> call)
|
||||
: _call(call)
|
||||
, _user(call->user())
|
||||
, _window(std::make_unique<Ui::Window>())
|
||||
, _window(createWindow())
|
||||
#ifndef Q_OS_MAC
|
||||
, _controls(std::make_unique<Ui::Platform::TitleControls>(
|
||||
_window->body(),
|
||||
@@ -213,6 +86,25 @@ Panel::Panel(not_null<Call*> call)
|
||||
|
||||
Panel::~Panel() = default;
|
||||
|
||||
std::unique_ptr<Ui::Window> Panel::createWindow() {
|
||||
auto result = std::make_unique<Ui::Window>();
|
||||
const auto capabilities = Ui::GL::CheckCapabilities(result.get());
|
||||
const auto use = Platform::IsMac()
|
||||
? true
|
||||
: Platform::IsWindows()
|
||||
? capabilities.supported
|
||||
: capabilities.transparency;
|
||||
LOG(("OpenGL: %1 (Incoming)").arg(Logs::b(use)));
|
||||
_backend = use ? Ui::GL::Backend::OpenGL : Ui::GL::Backend::Raster;
|
||||
|
||||
if (use) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// We have to create a new window, if OpenGL initialization failed.
|
||||
return std::make_unique<Ui::Window>();
|
||||
}
|
||||
|
||||
bool Panel::isActive() const {
|
||||
return _window->isActiveWindow()
|
||||
&& _window->isVisible()
|
||||
@@ -392,7 +284,7 @@ void Panel::refreshIncomingGeometry() {
|
||||
Expects(_incoming != nullptr);
|
||||
|
||||
if (_incomingFrameSize.isEmpty()) {
|
||||
_incoming->hide();
|
||||
_incoming->widget()->hide();
|
||||
return;
|
||||
}
|
||||
const auto to = widget()->size();
|
||||
@@ -401,7 +293,7 @@ void Panel::refreshIncomingGeometry() {
|
||||
to,
|
||||
Qt::KeepAspectRatioByExpanding);
|
||||
|
||||
// If we cut out no more than 0.33 of the original, let's use expanding.
|
||||
// If we cut out no more than 0.25 of the original, let's use expanding.
|
||||
const auto use = ((big.width() * 3 <= to.width() * 4)
|
||||
&& (big.height() * 3 <= to.height() * 4))
|
||||
? big
|
||||
@@ -409,8 +301,8 @@ void Panel::refreshIncomingGeometry() {
|
||||
const auto pos = QPoint(
|
||||
(to.width() - use.width()) / 2,
|
||||
(to.height() - use.height()) / 2);
|
||||
_incoming->setGeometry(QRect(pos, use));
|
||||
_incoming->show();
|
||||
_incoming->widget()->setGeometry(QRect(pos, use));
|
||||
_incoming->widget()->show();
|
||||
}
|
||||
|
||||
void Panel::reinitWithCall(Call *call) {
|
||||
@@ -446,8 +338,9 @@ void Panel::reinitWithCall(Call *call) {
|
||||
_call->videoOutgoing());
|
||||
_incoming = std::make_unique<Incoming>(
|
||||
widget(),
|
||||
_call->videoIncoming());
|
||||
_incoming->hide();
|
||||
_call->videoIncoming(),
|
||||
_backend);
|
||||
_incoming->widget()->hide();
|
||||
|
||||
_call->mutedValue(
|
||||
) | rpl::start_with_next([=](bool mute) {
|
||||
@@ -474,28 +367,34 @@ void Panel::reinitWithCall(Call *call) {
|
||||
_call->videoIncoming()->renderNextFrame(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto track = _call->videoIncoming();
|
||||
const auto [frame, rotation] = track->frameOriginalWithRotation();
|
||||
setIncomingSize((rotation == 90 || rotation == 270)
|
||||
? QSize(frame.height(), frame.width())
|
||||
: frame.size());
|
||||
if (_incoming->isHidden()) {
|
||||
setIncomingSize(track->state() == Webrtc::VideoState::Active
|
||||
? track->frameSize()
|
||||
: QSize());
|
||||
if (_incoming->widget()->isHidden()) {
|
||||
return;
|
||||
}
|
||||
const auto incoming = incomingFrameGeometry();
|
||||
const auto outgoing = outgoingFrameGeometry();
|
||||
_incoming->update();
|
||||
_incoming->widget()->update();
|
||||
if (incoming.intersects(outgoing)) {
|
||||
widget()->update(outgoing);
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
||||
_call->videoIncoming()->stateValue(
|
||||
) | rpl::start_with_next([=](Webrtc::VideoState state) {
|
||||
setIncomingSize((state == Webrtc::VideoState::Active)
|
||||
? _call->videoIncoming()->frameSize()
|
||||
: QSize());
|
||||
}, _callLifetime);
|
||||
|
||||
_call->videoOutgoing()->renderNextFrame(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto incoming = incomingFrameGeometry();
|
||||
const auto outgoing = outgoingFrameGeometry();
|
||||
widget()->update(outgoing);
|
||||
if (incoming.intersects(outgoing)) {
|
||||
_incoming->update();
|
||||
_incoming->widget()->update();
|
||||
}
|
||||
}, _callLifetime);
|
||||
|
||||
@@ -539,7 +438,13 @@ void Panel::reinitWithCall(Call *call) {
|
||||
_name->setText(_user->name);
|
||||
updateStatusText(_call->state());
|
||||
|
||||
_incoming->lower();
|
||||
_answerHangupRedial->raise();
|
||||
_decline->raise();
|
||||
_cancel->raise();
|
||||
_camera->raise();
|
||||
_mute->raise();
|
||||
|
||||
_incoming->widget()->lower();
|
||||
}
|
||||
|
||||
void Panel::createRemoteAudioMute() {
|
||||
@@ -604,7 +509,7 @@ void Panel::showControls() {
|
||||
_cancel->setVisible(_cancel->toggled());
|
||||
|
||||
const auto shown = !_incomingFrameSize.isEmpty();
|
||||
_incoming->setVisible(shown);
|
||||
_incoming->widget()->setVisible(shown);
|
||||
_name->setVisible(!shown);
|
||||
_status->setVisible(!shown);
|
||||
_userpic->setVisible(!shown);
|
||||
@@ -648,9 +553,9 @@ void Panel::toggleFullScreen(bool fullscreen) {
|
||||
}
|
||||
|
||||
QRect Panel::incomingFrameGeometry() const {
|
||||
return (!_incoming || _incoming->isHidden())
|
||||
return (!_incoming || _incoming->widget()->isHidden())
|
||||
? QRect()
|
||||
: _incoming->geometry();
|
||||
: _incoming->widget()->geometry();
|
||||
}
|
||||
|
||||
QRect Panel::outgoingFrameGeometry() const {
|
||||
@@ -666,15 +571,31 @@ void Panel::updateControlsGeometry() {
|
||||
}
|
||||
if (_fingerprint) {
|
||||
#ifndef Q_OS_MAC
|
||||
const auto minRight = _controls->geometry().width()
|
||||
+ st::callFingerprintTop;
|
||||
const auto controlsGeometry = _controls->geometry();
|
||||
const auto halfWidth = widget()->width() / 2;
|
||||
const auto minLeft = (controlsGeometry.center().x() < halfWidth)
|
||||
? (controlsGeometry.width() + st::callFingerprintTop)
|
||||
: 0;
|
||||
const auto minRight = (controlsGeometry.center().x() >= halfWidth)
|
||||
? (controlsGeometry.width() + st::callFingerprintTop)
|
||||
: 0;
|
||||
_incoming->setControlsAlignment(minLeft
|
||||
? style::al_left
|
||||
: style::al_right);
|
||||
#else // !Q_OS_MAC
|
||||
const auto minLeft = 0;
|
||||
const auto minRight = 0;
|
||||
#endif // _controls
|
||||
const auto desired = (widget()->width() - _fingerprint->width()) / 2;
|
||||
_fingerprint->moveToRight(
|
||||
std::max(desired, minRight),
|
||||
st::callFingerprintTop);
|
||||
if (minLeft) {
|
||||
_fingerprint->moveToLeft(
|
||||
std::max(desired, minLeft),
|
||||
st::callFingerprintTop);
|
||||
} else {
|
||||
_fingerprint->moveToRight(
|
||||
std::max(desired, minRight),
|
||||
st::callFingerprintTop);
|
||||
}
|
||||
}
|
||||
const auto innerHeight = std::max(widget()->height(), st::callHeightMin);
|
||||
const auto innerWidth = widget()->width() - 2 * st::callInnerPadding;
|
||||
@@ -786,13 +707,13 @@ void Panel::paint(QRect clip) {
|
||||
Painter p(widget());
|
||||
|
||||
auto region = QRegion(clip);
|
||||
if (!_incoming->isHidden()) {
|
||||
region = region.subtracted(QRegion(_incoming->geometry()));
|
||||
if (!_incoming->widget()->isHidden()) {
|
||||
region = region.subtracted(QRegion(_incoming->widget()->geometry()));
|
||||
}
|
||||
for (const auto rect : region) {
|
||||
p.fillRect(rect, st::callBgOpaque);
|
||||
}
|
||||
if (_incoming && _incoming->isHidden()) {
|
||||
if (_incoming && _incoming->widget()->isHidden()) {
|
||||
_call->videoIncoming()->markFrameShown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ class FadeWrap;
|
||||
template <typename Widget>
|
||||
class PaddingWrap;
|
||||
class Window;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
namespace Platform {
|
||||
class TitleControls;
|
||||
} // namespace Platform
|
||||
@@ -67,6 +70,7 @@ private:
|
||||
Redial,
|
||||
};
|
||||
|
||||
std::unique_ptr<Ui::Window> createWindow();
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
@@ -80,9 +84,6 @@ private:
|
||||
|
||||
void handleClose();
|
||||
|
||||
QRect signalBarsRect() const;
|
||||
void paintSignalBarsBg(Painter &p);
|
||||
|
||||
void updateControlsGeometry();
|
||||
void updateHangupGeometry();
|
||||
void updateStatusGeometry();
|
||||
@@ -105,6 +106,7 @@ private:
|
||||
Call *_call = nullptr;
|
||||
not_null<UserData*> _user;
|
||||
|
||||
Ui::GL::Backend _backend = Ui::GL::Backend();
|
||||
const std::unique_ptr<Ui::Window> _window;
|
||||
std::unique_ptr<Incoming> _incoming;
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/calls_call.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_signal_bars.h"
|
||||
#include "calls/calls_group_menu.h" // Group::LeaveBox.
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h" // Group::LeaveBox.
|
||||
#include "history/view/history_view_group_call_tracker.h" // ContentByCall.
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_group_call.h"
|
||||
@@ -279,8 +280,7 @@ void TopBar::initControls() {
|
||||
if (const auto call = _call.get()) {
|
||||
call->setMuted(!call->muted());
|
||||
} else if (const auto group = _groupCall.get()) {
|
||||
if (group->muted() == MuteState::ForceMuted
|
||||
|| group->muted() == MuteState::RaisedHand) {
|
||||
if (group->mutedByAdmin()) {
|
||||
Ui::Toast::Show(tr::lng_group_call_force_muted_sub(tr::now));
|
||||
} else {
|
||||
group->setMuted((group->muted() == MuteState::Muted)
|
||||
|
||||
576
Telegram/SourceFiles/calls/calls_video_incoming.cpp
Normal file
@@ -0,0 +1,576 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_video_incoming.h"
|
||||
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_shader.h"
|
||||
#include "ui/gl/gl_image.h"
|
||||
#include "ui/gl/gl_primitives.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtGui/QOpenGLShader>
|
||||
#include <QtGui/QOpenGLBuffer>
|
||||
|
||||
namespace Calls {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBottomShadowAlphaMax = 74;
|
||||
|
||||
using namespace Ui::GL;
|
||||
|
||||
[[nodiscard]] ShaderPart FragmentBottomShadow() {
|
||||
return {
|
||||
.header = R"(
|
||||
uniform vec3 shadow; // fullHeight, shadowTop, maxOpacity
|
||||
)",
|
||||
.body = R"(
|
||||
float shadowCoord = shadow.y - gl_FragCoord.y;
|
||||
float shadowValue = clamp(shadowCoord / shadow.x, 0., 1.);
|
||||
float shadowShown = shadowValue * shadow.z;
|
||||
result = vec4(min(result.rgb, vec3(1.)) * (1. - shadowShown), result.a);
|
||||
)",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Panel::Incoming::RendererGL final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererGL(not_null<Incoming*> owner);
|
||||
|
||||
void init(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
void deinit(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
void paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
private:
|
||||
void uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const;
|
||||
void validateShadowImage();
|
||||
|
||||
const not_null<Incoming*> _owner;
|
||||
|
||||
QSize _viewport;
|
||||
float _factor = 1.;
|
||||
QVector2D _uniformViewport;
|
||||
|
||||
std::optional<QOpenGLBuffer> _contentBuffer;
|
||||
std::optional<QOpenGLShaderProgram> _argb32Program;
|
||||
QOpenGLShader *_texturedVertexShader = nullptr;
|
||||
std::optional<QOpenGLShaderProgram> _yuv420Program;
|
||||
std::optional<QOpenGLShaderProgram> _imageProgram;
|
||||
Ui::GL::Textures<4> _textures;
|
||||
QSize _rgbaSize;
|
||||
QSize _lumaSize;
|
||||
QSize _chromaSize;
|
||||
int _trackFrameIndex = 0;
|
||||
|
||||
Ui::GL::Image _controlsShadowImage;
|
||||
QRect _controlsShadowLeft;
|
||||
QRect _controlsShadowRight;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
class Panel::Incoming::RendererSW final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererSW(not_null<Incoming*> owner);
|
||||
|
||||
void paintFallback(
|
||||
Painter &&p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) override;
|
||||
|
||||
private:
|
||||
void initBottomShadow();
|
||||
void fillTopShadow(QPainter &p);
|
||||
void fillBottomShadow(QPainter &p);
|
||||
|
||||
const not_null<Incoming*> _owner;
|
||||
|
||||
QImage _bottomShadow;
|
||||
|
||||
};
|
||||
|
||||
Panel::Incoming::RendererGL::RendererGL(not_null<Incoming*> owner)
|
||||
: _owner(owner) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_controlsShadowImage.invalidate();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::init(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) {
|
||||
constexpr auto kQuads = 2;
|
||||
constexpr auto kQuadVertices = kQuads * 4;
|
||||
constexpr auto kQuadValues = kQuadVertices * 4;
|
||||
|
||||
_contentBuffer.emplace();
|
||||
_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||||
_contentBuffer->create();
|
||||
_contentBuffer->bind();
|
||||
_contentBuffer->allocate(kQuadValues * sizeof(GLfloat));
|
||||
|
||||
_textures.ensureCreated(f);
|
||||
|
||||
_imageProgram.emplace();
|
||||
_texturedVertexShader = LinkProgram(
|
||||
&*_imageProgram,
|
||||
VertexShader({
|
||||
VertexViewportTransform(),
|
||||
VertexPassTextureCoord(),
|
||||
}),
|
||||
FragmentShader({
|
||||
FragmentSampleARGB32Texture(),
|
||||
})).vertex;
|
||||
|
||||
_argb32Program.emplace();
|
||||
LinkProgram(
|
||||
&*_argb32Program,
|
||||
_texturedVertexShader,
|
||||
FragmentShader({
|
||||
FragmentSampleARGB32Texture(),
|
||||
FragmentBottomShadow(),
|
||||
}));
|
||||
|
||||
_yuv420Program.emplace();
|
||||
LinkProgram(
|
||||
&*_yuv420Program,
|
||||
_texturedVertexShader,
|
||||
FragmentShader({
|
||||
FragmentSampleYUV420Texture(),
|
||||
FragmentBottomShadow(),
|
||||
}));
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::deinit(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) {
|
||||
_textures.destroy(f);
|
||||
_imageProgram = std::nullopt;
|
||||
_texturedVertexShader = nullptr;
|
||||
_argb32Program = std::nullopt;
|
||||
_yuv420Program = std::nullopt;
|
||||
_contentBuffer = std::nullopt;
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) {
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
_owner->_track->markFrameShown();
|
||||
});
|
||||
const auto data = _owner->_track->frameWithInfo(false);
|
||||
if (data.format == Webrtc::FrameFormat::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto factor = widget->devicePixelRatio();
|
||||
if (_factor != factor) {
|
||||
_factor = factor;
|
||||
_controlsShadowImage.invalidate();
|
||||
}
|
||||
_viewport = widget->size();
|
||||
_uniformViewport = QVector2D(
|
||||
_viewport.width() * _factor,
|
||||
_viewport.height() * _factor);
|
||||
|
||||
const auto rgbaFrame = (data.format == Webrtc::FrameFormat::ARGB32);
|
||||
const auto upload = (_trackFrameIndex != data.index);
|
||||
_trackFrameIndex = data.index;
|
||||
auto &program = rgbaFrame ? _argb32Program : _yuv420Program;
|
||||
program->bind();
|
||||
if (rgbaFrame) {
|
||||
Assert(!data.original.isNull());
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_textures.bind(f, 0);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_RGBA,
|
||||
GL_RGBA,
|
||||
data.original.size(),
|
||||
_rgbaSize,
|
||||
data.original.bytesPerLine() / 4,
|
||||
data.original.constBits());
|
||||
_rgbaSize = data.original.size();
|
||||
}
|
||||
program->setUniformValue("s_texture", GLint(0));
|
||||
} else {
|
||||
Assert(data.format == Webrtc::FrameFormat::YUV420);
|
||||
Assert(!data.yuv420->size.isEmpty());
|
||||
const auto yuv = data.yuv420;
|
||||
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_textures.bind(f, 1);
|
||||
if (upload) {
|
||||
f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_RED,
|
||||
GL_RED,
|
||||
yuv->size,
|
||||
_lumaSize,
|
||||
yuv->y.stride,
|
||||
yuv->y.data);
|
||||
_lumaSize = yuv->size;
|
||||
}
|
||||
f.glActiveTexture(GL_TEXTURE1);
|
||||
_textures.bind(f, 2);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_RED,
|
||||
GL_RED,
|
||||
yuv->chromaSize,
|
||||
_chromaSize,
|
||||
yuv->u.stride,
|
||||
yuv->u.data);
|
||||
}
|
||||
f.glActiveTexture(GL_TEXTURE2);
|
||||
_textures.bind(f, 3);
|
||||
if (upload) {
|
||||
uploadTexture(
|
||||
f,
|
||||
GL_RED,
|
||||
GL_RED,
|
||||
yuv->chromaSize,
|
||||
_chromaSize,
|
||||
yuv->v.stride,
|
||||
yuv->v.data);
|
||||
_chromaSize = yuv->chromaSize;
|
||||
f.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
}
|
||||
program->setUniformValue("y_texture", GLint(0));
|
||||
program->setUniformValue("u_texture", GLint(1));
|
||||
program->setUniformValue("v_texture", GLint(2));
|
||||
}
|
||||
const auto rect = TransformRect(
|
||||
widget->rect(),
|
||||
_viewport,
|
||||
_factor);
|
||||
std::array<std::array<GLfloat, 2>, 4> texcoords = { {
|
||||
{ { 0.f, 1.f } },
|
||||
{ { 1.f, 1.f } },
|
||||
{ { 1.f, 0.f } },
|
||||
{ { 0.f, 0.f } },
|
||||
} };
|
||||
if (const auto shift = (data.rotation / 90); shift != 0) {
|
||||
std::rotate(
|
||||
begin(texcoords),
|
||||
begin(texcoords) + shift,
|
||||
end(texcoords));
|
||||
}
|
||||
|
||||
const auto width = widget->parentWidget()->width();
|
||||
const auto left = (_owner->_topControlsAlignment == style::al_left);
|
||||
validateShadowImage();
|
||||
const auto position = left
|
||||
? QPoint()
|
||||
: QPoint(width - st::callTitleShadowRight.width(), 0);
|
||||
const auto translated = position - widget->pos();
|
||||
const auto shadowArea = QRect(translated, st::callTitleShadowLeft.size());
|
||||
const auto shadow = _controlsShadowImage.texturedRect(
|
||||
shadowArea,
|
||||
(left ? _controlsShadowLeft : _controlsShadowRight),
|
||||
widget->rect());
|
||||
const auto shadowRect = TransformRect(
|
||||
shadow.geometry,
|
||||
_viewport,
|
||||
_factor);
|
||||
|
||||
const GLfloat coords[] = {
|
||||
rect.left(), rect.top(),
|
||||
texcoords[0][0], texcoords[0][1],
|
||||
|
||||
rect.right(), rect.top(),
|
||||
texcoords[1][0], texcoords[1][1],
|
||||
|
||||
rect.right(), rect.bottom(),
|
||||
texcoords[2][0], texcoords[2][1],
|
||||
|
||||
rect.left(), rect.bottom(),
|
||||
texcoords[3][0], texcoords[3][1],
|
||||
|
||||
shadowRect.left(), shadowRect.top(),
|
||||
shadow.texture.left(), shadow.texture.bottom(),
|
||||
|
||||
shadowRect.right(), shadowRect.top(),
|
||||
shadow.texture.right(), shadow.texture.bottom(),
|
||||
|
||||
shadowRect.right(), shadowRect.bottom(),
|
||||
shadow.texture.right(), shadow.texture.top(),
|
||||
|
||||
shadowRect.left(), shadowRect.bottom(),
|
||||
shadow.texture.left(), shadow.texture.top(),
|
||||
};
|
||||
|
||||
_contentBuffer->write(0, coords, sizeof(coords));
|
||||
|
||||
const auto bottomShadowArea = QRect(
|
||||
0,
|
||||
widget->parentWidget()->height() - st::callBottomShadowSize,
|
||||
widget->parentWidget()->width(),
|
||||
st::callBottomShadowSize);
|
||||
const auto bottomShadowFill = bottomShadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
const auto shadowHeight = bottomShadowFill.height();
|
||||
const auto shadowAlpha = (shadowHeight * kBottomShadowAlphaMax)
|
||||
/ (st::callBottomShadowSize * 255.);
|
||||
|
||||
program->setUniformValue("viewport", _uniformViewport);
|
||||
program->setUniformValue("shadow", QVector3D(
|
||||
shadowHeight * _factor,
|
||||
TransformRect(bottomShadowFill, _viewport, _factor).bottom(),
|
||||
shadowAlpha));
|
||||
|
||||
FillTexturedRectangle(f, &*program);
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
if (!shadowRect.empty()) {
|
||||
f.glEnable(GL_BLEND);
|
||||
f.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
const auto guard = gsl::finally([&] {
|
||||
f.glDisable(GL_BLEND);
|
||||
});
|
||||
|
||||
_imageProgram->bind();
|
||||
_imageProgram->setUniformValue("viewport", _uniformViewport);
|
||||
_imageProgram->setUniformValue("s_texture", GLint(0));
|
||||
|
||||
f.glActiveTexture(GL_TEXTURE0);
|
||||
_controlsShadowImage.bind(f);
|
||||
|
||||
FillTexturedRectangle(f, &*_imageProgram, 4);
|
||||
}
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::validateShadowImage() {
|
||||
if (_controlsShadowImage) {
|
||||
return;
|
||||
}
|
||||
const auto size = st::callTitleShadowLeft.size();
|
||||
const auto full = QSize(size.width(), 2 * size.height()) * int(_factor);
|
||||
auto image = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(_factor);
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
st::callTitleShadowLeft.paint(p, 0, 0, size.width());
|
||||
_controlsShadowLeft = QRect(0, 0, full.width(), full.height() / 2);
|
||||
st::callTitleShadowRight.paint(p, 0, size.height(), size.width());
|
||||
_controlsShadowRight = QRect(
|
||||
0,
|
||||
full.height() / 2,
|
||||
full.width(),
|
||||
full.height() / 2);
|
||||
}
|
||||
_controlsShadowImage.setImage(std::move(image));
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererGL::uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const {
|
||||
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);
|
||||
if (hasSize != size) {
|
||||
f.glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
internalformat,
|
||||
size.width(),
|
||||
size.height(),
|
||||
0,
|
||||
format,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
} else {
|
||||
f.glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
size.width(),
|
||||
size.height(),
|
||||
format,
|
||||
GL_UNSIGNED_BYTE,
|
||||
data);
|
||||
}
|
||||
f.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
|
||||
Panel::Incoming::RendererSW::RendererSW(not_null<Incoming*> owner)
|
||||
: _owner(owner) {
|
||||
initBottomShadow();
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::paintFallback(
|
||||
Painter &&p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) {
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
_owner->_track->markFrameShown();
|
||||
});
|
||||
const auto data = _owner->_track->frameWithInfo(true);
|
||||
const auto &image = data.original;
|
||||
const auto rotation = data.rotation;
|
||||
if (image.isNull()) {
|
||||
p.fillRect(clip.boundingRect(), Qt::black);
|
||||
} else {
|
||||
const auto rect = _owner->widget()->rect();
|
||||
using namespace Media::View;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
if (UsePainterRotation(rotation)) {
|
||||
if (rotation) {
|
||||
p.save();
|
||||
p.rotate(rotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(rect, rotation), image);
|
||||
if (rotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (rotation) {
|
||||
p.drawImage(rect, RotateFrameImage(image, rotation));
|
||||
} else {
|
||||
p.drawImage(rect, image);
|
||||
}
|
||||
fillBottomShadow(p);
|
||||
fillTopShadow(p);
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::initBottomShadow() {
|
||||
auto image = QImage(
|
||||
QSize(1, st::callBottomShadowSize) * cIntRetinaFactor(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
const auto colorFrom = uint32(0);
|
||||
const auto colorTill = uint32(kBottomShadowAlphaMax);
|
||||
const auto rows = image.height();
|
||||
const auto step = (uint64(colorTill - colorFrom) << 32) / rows;
|
||||
auto accumulated = uint64();
|
||||
auto bytes = image.bits();
|
||||
for (auto y = 0; y != rows; ++y) {
|
||||
accumulated += step;
|
||||
const auto color = (colorFrom + uint32(accumulated >> 32)) << 24;
|
||||
for (auto x = 0; x != image.width(); ++x) {
|
||||
*(reinterpret_cast<uint32*>(bytes) + x) = color;
|
||||
}
|
||||
bytes += image.bytesPerLine();
|
||||
}
|
||||
_bottomShadow = std::move(image);
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::fillTopShadow(QPainter &p) {
|
||||
#ifndef Q_OS_MAC
|
||||
const auto widget = _owner->widget();
|
||||
const auto width = widget->parentWidget()->width();
|
||||
const auto left = (_owner->_topControlsAlignment == style::al_left);
|
||||
const auto &icon = left
|
||||
? st::callTitleShadowLeft
|
||||
: st::callTitleShadowRight;
|
||||
const auto position = left
|
||||
? QPoint()
|
||||
: QPoint(width - icon.width(), 0);
|
||||
const auto shadowArea = QRect(position, icon.size());
|
||||
const auto fill = shadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.save();
|
||||
p.setClipRect(fill);
|
||||
icon.paint(p, position - widget->pos(), width);
|
||||
p.restore();
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void Panel::Incoming::RendererSW::fillBottomShadow(QPainter &p) {
|
||||
const auto widget = _owner->widget();
|
||||
const auto shadowArea = QRect(
|
||||
0,
|
||||
widget->parentWidget()->height() - st::callBottomShadowSize,
|
||||
widget->parentWidget()->width(),
|
||||
st::callBottomShadowSize);
|
||||
const auto fill = shadowArea.intersected(
|
||||
widget->geometry()).translated(-widget->pos());
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = cIntRetinaFactor();
|
||||
p.drawImage(
|
||||
fill,
|
||||
_bottomShadow,
|
||||
QRect(
|
||||
0,
|
||||
(factor
|
||||
* (fill.y() - shadowArea.translated(-widget->pos()).y())),
|
||||
factor,
|
||||
factor * fill.height()));
|
||||
}
|
||||
|
||||
Panel::Incoming::Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track,
|
||||
Ui::GL::Backend backend)
|
||||
: _surface(Ui::GL::CreateSurface(parent, chooseRenderer(backend)))
|
||||
, _track(track) {
|
||||
widget()->setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
widget()->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
not_null<QWidget*> Panel::Incoming::widget() const {
|
||||
return _surface->rpWidget();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidgetWrap*> Panel::Incoming::rp() const {
|
||||
return _surface.get();
|
||||
}
|
||||
|
||||
void Panel::Incoming::setControlsAlignment(style::align align) {
|
||||
if (_topControlsAlignment != align) {
|
||||
_topControlsAlignment = align;
|
||||
widget()->update();
|
||||
}
|
||||
}
|
||||
|
||||
Ui::GL::ChosenRenderer Panel::Incoming::chooseRenderer(
|
||||
Ui::GL::Backend backend) {
|
||||
_opengl = (backend == Ui::GL::Backend::OpenGL);
|
||||
return {
|
||||
.renderer = (_opengl
|
||||
? std::unique_ptr<Ui::GL::Renderer>(
|
||||
std::make_unique<RendererGL>(this))
|
||||
: std::make_unique<RendererSW>(this)),
|
||||
.backend = backend,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Calls
|
||||
45
Telegram/SourceFiles/calls/calls_video_incoming.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/calls_panel.h"
|
||||
|
||||
namespace Ui::GL {
|
||||
enum class Backend;
|
||||
struct ChosenRenderer;
|
||||
} // namespace Ui::GL
|
||||
|
||||
namespace Calls {
|
||||
|
||||
class Panel::Incoming final {
|
||||
public:
|
||||
Incoming(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Webrtc::VideoTrack*> track,
|
||||
Ui::GL::Backend backend);
|
||||
|
||||
[[nodiscard]] not_null<QWidget*> widget() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
|
||||
|
||||
void setControlsAlignment(style::align align);
|
||||
|
||||
private:
|
||||
class RendererGL;
|
||||
class RendererSW;
|
||||
|
||||
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
|
||||
Ui::GL::Backend backend);
|
||||
|
||||
const std::unique_ptr<Ui::RpWidgetWrap> _surface;
|
||||
const not_null<Webrtc::VideoTrack*> _track;
|
||||
style::align _topControlsAlignment = style::al_left;
|
||||
bool _opengl = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -5,10 +5,10 @@ the official desktop application for the Telegram messaging service.
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "calls/calls_group_menu.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
3087
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
646
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
@@ -0,0 +1,646 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/bytes.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "mtproto/mtproto_auth_key.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace tgcalls {
|
||||
class GroupInstanceCustomImpl;
|
||||
struct GroupLevelsUpdate;
|
||||
struct GroupNetworkState;
|
||||
struct GroupParticipantDescription;
|
||||
class VideoCaptureInterface;
|
||||
} // namespace tgcalls
|
||||
|
||||
namespace base {
|
||||
class GlobalShortcutManager;
|
||||
class GlobalShortcutValue;
|
||||
} // namespace base
|
||||
|
||||
namespace Webrtc {
|
||||
class MediaDevices;
|
||||
class VideoTrack;
|
||||
enum class VideoState;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Data {
|
||||
struct LastSpokeTimes;
|
||||
struct GroupCallParticipant;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Calls {
|
||||
|
||||
namespace Group {
|
||||
struct MuteRequest;
|
||||
struct VolumeRequest;
|
||||
struct ParticipantState;
|
||||
struct JoinInfo;
|
||||
struct RejoinEvent;
|
||||
enum class VideoQuality;
|
||||
enum class Error;
|
||||
} // namespace Group
|
||||
|
||||
enum class MuteState {
|
||||
Active,
|
||||
PushToTalk,
|
||||
Muted,
|
||||
ForceMuted,
|
||||
RaisedHand,
|
||||
};
|
||||
|
||||
[[nodiscard]] inline auto MapPushToTalkToActive() {
|
||||
return rpl::map([=](MuteState state) {
|
||||
return (state == MuteState::PushToTalk) ? MuteState::Active : state;
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGroupCallAdmin(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<PeerData*> participantPeer);
|
||||
|
||||
struct LevelUpdate {
|
||||
uint32 ssrc = 0;
|
||||
float value = 0.;
|
||||
bool voice = false;
|
||||
bool me = false;
|
||||
};
|
||||
|
||||
enum class VideoEndpointType {
|
||||
Camera,
|
||||
Screen,
|
||||
};
|
||||
|
||||
struct VideoEndpoint {
|
||||
VideoEndpoint() = default;
|
||||
VideoEndpoint(
|
||||
VideoEndpointType type,
|
||||
not_null<PeerData*> peer,
|
||||
std::string id)
|
||||
: type(type)
|
||||
, peer(peer)
|
||||
, id(std::move(id)) {
|
||||
}
|
||||
|
||||
VideoEndpointType type = VideoEndpointType::Camera;
|
||||
PeerData *peer = nullptr;
|
||||
std::string id;
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
return id.empty();
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const noexcept {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
inline bool operator==(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.id == b.id);
|
||||
}
|
||||
|
||||
inline bool operator!=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator<(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.peer < b.peer)
|
||||
|| (a.peer == b.peer && a.id < b.id);
|
||||
}
|
||||
|
||||
inline bool operator>(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (b < a);
|
||||
}
|
||||
|
||||
inline bool operator<=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
inline bool operator>=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct VideoStateToggle {
|
||||
VideoEndpoint endpoint;
|
||||
bool value = false;
|
||||
};
|
||||
|
||||
struct VideoQualityRequest {
|
||||
VideoEndpoint endpoint;
|
||||
Group::VideoQuality quality = Group::VideoQuality();
|
||||
};
|
||||
|
||||
struct ParticipantVideoParams;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ParticipantVideoParams> ParseVideoParams(
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &camera,
|
||||
const tl::conditional<MTPGroupCallParticipantVideo> &screen,
|
||||
const std::shared_ptr<ParticipantVideoParams> &existing);
|
||||
|
||||
[[nodiscard]] const std::string &GetCameraEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] const std::string &GetScreenEndpoint(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsCameraPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
[[nodiscard]] bool IsScreenPaused(
|
||||
const std::shared_ptr<ParticipantVideoParams> ¶ms);
|
||||
|
||||
class GroupCall final : public base::has_weak_ptr {
|
||||
public:
|
||||
class Delegate {
|
||||
public:
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
virtual void groupCallFinished(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallFailed(not_null<GroupCall*> call) = 0;
|
||||
virtual void groupCallRequestPermissionsOrFail(
|
||||
Fn<void()> onSuccess) = 0;
|
||||
|
||||
enum class GroupCallSound {
|
||||
Started,
|
||||
Connecting,
|
||||
AllowedToSpeak,
|
||||
Ended,
|
||||
};
|
||||
virtual void groupCallPlaySound(GroupCallSound sound) = 0;
|
||||
virtual auto groupCallGetVideoCapture(const QString &deviceId)
|
||||
-> std::shared_ptr<tgcalls::VideoCaptureInterface> = 0;
|
||||
|
||||
[[nodiscard]] virtual FnMut<void()> groupCallAddAsyncWaiter() = 0;
|
||||
};
|
||||
|
||||
using GlobalShortcutManager = base::GlobalShortcutManager;
|
||||
|
||||
GroupCall(
|
||||
not_null<Delegate*> delegate,
|
||||
Group::JoinInfo info,
|
||||
const MTPInputGroupCall &inputCall);
|
||||
~GroupCall();
|
||||
|
||||
[[nodiscard]] uint64 id() const {
|
||||
return _id;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> peer() const {
|
||||
return _peer;
|
||||
}
|
||||
[[nodiscard]] not_null<PeerData*> joinAs() const {
|
||||
return _joinAs;
|
||||
}
|
||||
[[nodiscard]] bool showChooseJoinAs() const;
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate;
|
||||
}
|
||||
[[nodiscard]] bool scheduleStartSubscribed() const;
|
||||
|
||||
[[nodiscard]] Data::GroupCall *lookupReal() const;
|
||||
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
|
||||
|
||||
void start(TimeId scheduleDate);
|
||||
void hangup();
|
||||
void discard();
|
||||
void rejoinAs(Group::JoinInfo info);
|
||||
void rejoinWithHash(const QString &hash);
|
||||
void join(const MTPInputGroupCall &inputCall);
|
||||
void handleUpdate(const MTPUpdate &update);
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void handlePossibleCreateOrJoinResponse(
|
||||
const MTPDupdateGroupCallConnection &data);
|
||||
void changeTitle(const QString &title);
|
||||
void toggleRecording(bool enabled, const QString &title);
|
||||
[[nodiscard]] bool recordingStoppedByMe() const {
|
||||
return _recordingStoppedByMe;
|
||||
}
|
||||
void startScheduledNow();
|
||||
void toggleScheduleStartSubscribed(bool subscribed);
|
||||
|
||||
bool emitShareScreenError();
|
||||
bool emitShareCameraError();
|
||||
|
||||
[[nodiscard]] rpl::producer<Group::Error> errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
void addVideoOutput(
|
||||
const std::string &endpoint,
|
||||
not_null<Webrtc::VideoTrack*> track);
|
||||
|
||||
void setMuted(MuteState mute);
|
||||
void setMutedAndUpdate(MuteState mute);
|
||||
[[nodiscard]] MuteState muted() const {
|
||||
return _muted.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<MuteState> mutedValue() const {
|
||||
return _muted.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState>;
|
||||
|
||||
enum State {
|
||||
Creating,
|
||||
Waiting,
|
||||
Joining,
|
||||
Connecting,
|
||||
Joined,
|
||||
FailedHangingUp,
|
||||
Failed,
|
||||
HangingUp,
|
||||
Ended,
|
||||
};
|
||||
[[nodiscard]] State state() const {
|
||||
return _state.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<State> stateValue() const {
|
||||
return _state.value();
|
||||
}
|
||||
|
||||
enum class InstanceState {
|
||||
Disconnected,
|
||||
TransitionToRtc,
|
||||
Connected,
|
||||
};
|
||||
[[nodiscard]] InstanceState instanceState() const {
|
||||
return _instanceState.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<InstanceState> instanceStateValue() const {
|
||||
return _instanceState.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<LevelUpdate> levelUpdates() const {
|
||||
return _levelUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamActiveUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamActiveUpdates.events();
|
||||
}
|
||||
[[nodiscard]] auto videoStreamShownUpdates() const
|
||||
-> rpl::producer<VideoStateToggle> {
|
||||
return _videoStreamShownUpdates.events();
|
||||
}
|
||||
void requestVideoQuality(
|
||||
const VideoEndpoint &endpoint,
|
||||
Group::VideoQuality quality);
|
||||
|
||||
[[nodiscard]] bool videoEndpointPinned() const {
|
||||
return _videoEndpointPinned.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {
|
||||
return _videoEndpointPinned.value();
|
||||
}
|
||||
void pinVideoEndpoint(VideoEndpoint endpoint);
|
||||
|
||||
void showVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {
|
||||
return _videoEndpointLarge.current();
|
||||
}
|
||||
[[nodiscard]] auto videoEndpointLargeValue() const
|
||||
-> rpl::producer<VideoEndpoint> {
|
||||
return _videoEndpointLarge.value();
|
||||
}
|
||||
|
||||
struct VideoTrack {
|
||||
std::unique_ptr<Webrtc::VideoTrack> track;
|
||||
PeerData *peer = nullptr;
|
||||
rpl::lifetime shownTrackingLifetime;
|
||||
Group::VideoQuality quality = Group::VideoQuality();
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return (track != nullptr);
|
||||
}
|
||||
[[nodiscard]] bool operator==(const VideoTrack &other) const {
|
||||
return (track == other.track) && (peer == other.peer);
|
||||
}
|
||||
[[nodiscard]] bool operator!=(const VideoTrack &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
[[nodiscard]] auto activeVideoTracks() const
|
||||
-> const base::flat_map<VideoEndpoint, VideoTrack> & {
|
||||
return _activeVideoTracks;
|
||||
}
|
||||
[[nodiscard]] auto shownVideoTracks() const
|
||||
-> const base::flat_set<VideoEndpoint> & {
|
||||
return _shownVideoTracks;
|
||||
}
|
||||
[[nodiscard]] rpl::producer<Group::RejoinEvent> rejoinEvents() const {
|
||||
return _rejoinEvents.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> allowedToSpeakNotifications() const {
|
||||
return _allowedToSpeakNotifications.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> titleChanged() const {
|
||||
return _titleChanged.events();
|
||||
}
|
||||
static constexpr auto kSpeakLevelThreshold = 0.2;
|
||||
|
||||
[[nodiscard]] bool mutedByAdmin() const;
|
||||
[[nodiscard]] bool canManage() const;
|
||||
[[nodiscard]] rpl::producer<bool> canManageValue() const;
|
||||
[[nodiscard]] bool videoIsWorking() const {
|
||||
return _videoIsWorking.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {
|
||||
return _videoIsWorking.value();
|
||||
}
|
||||
|
||||
void setCurrentAudioDevice(bool input, const QString &deviceId);
|
||||
void setCurrentVideoDevice(const QString &deviceId);
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;
|
||||
[[nodiscard]] bool isScreenPaused() const;
|
||||
[[nodiscard]] const std::string &screenSharingEndpoint() const;
|
||||
[[nodiscard]] bool isSharingCamera() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingCameraValue() const;
|
||||
[[nodiscard]] bool isCameraPaused() const;
|
||||
[[nodiscard]] const std::string &cameraSharingEndpoint() const;
|
||||
[[nodiscard]] QString screenSharingDeviceId() const;
|
||||
void toggleVideo(bool active);
|
||||
void toggleScreenSharing(std::optional<QString> uniqueId);
|
||||
[[nodiscard]] bool hasVideoWithFrames() const;
|
||||
[[nodiscard]] rpl::producer<bool> hasVideoWithFramesValue() const;
|
||||
|
||||
void toggleMute(const Group::MuteRequest &data);
|
||||
void changeVolume(const Group::VolumeRequest &data);
|
||||
std::variant<int, not_null<UserData*>> inviteUsers(
|
||||
const std::vector<not_null<UserData*>> &users);
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> ensureGlobalShortcutManager();
|
||||
void applyGlobalShortcutChanges();
|
||||
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
void setNotRequireARGB32();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
class LoadPartTask;
|
||||
class MediaChannelDescriptionsTask;
|
||||
|
||||
public:
|
||||
void broadcastPartStart(std::shared_ptr<LoadPartTask> task);
|
||||
void broadcastPartCancel(not_null<LoadPartTask*> task);
|
||||
void mediaChannelDescriptionsStart(
|
||||
std::shared_ptr<MediaChannelDescriptionsTask> task);
|
||||
void mediaChannelDescriptionsCancel(
|
||||
not_null<MediaChannelDescriptionsTask*> task);
|
||||
|
||||
private:
|
||||
using GlobalShortcutValue = base::GlobalShortcutValue;
|
||||
using Error = Group::Error;
|
||||
struct SinkPointer;
|
||||
|
||||
static constexpr uint32 kDisabledSsrc = uint32(-1);
|
||||
|
||||
struct LoadingPart {
|
||||
std::shared_ptr<LoadPartTask> task;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
enum class FinishType {
|
||||
None,
|
||||
Ended,
|
||||
Failed,
|
||||
};
|
||||
enum class InstanceMode {
|
||||
None,
|
||||
Rtc,
|
||||
Stream,
|
||||
};
|
||||
enum class SendUpdateType {
|
||||
Mute = 0x01,
|
||||
RaiseHand = 0x02,
|
||||
CameraStopped = 0x04,
|
||||
CameraPaused = 0x08,
|
||||
ScreenPaused = 0x10,
|
||||
};
|
||||
enum class JoinAction {
|
||||
None,
|
||||
Joining,
|
||||
Leaving,
|
||||
};
|
||||
struct JoinState {
|
||||
uint32 ssrc = 0;
|
||||
JoinAction action = JoinAction::None;
|
||||
bool nextActionPending = false;
|
||||
|
||||
void finish(uint32 updatedSsrc = 0) {
|
||||
action = JoinAction::None;
|
||||
ssrc = updatedSsrc;
|
||||
}
|
||||
};
|
||||
|
||||
friend inline constexpr bool is_flag_type(SendUpdateType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool mediaChannelDescriptionsFill(
|
||||
not_null<MediaChannelDescriptionsTask*> task,
|
||||
Fn<bool(uint32)> resolved = nullptr);
|
||||
void checkMediaChannelDescriptions(Fn<bool(uint32)> resolved = nullptr);
|
||||
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDgroupCall &data);
|
||||
void handlePossibleDiscarded(const MTPDgroupCallDiscarded &data);
|
||||
void handleUpdate(const MTPDupdateGroupCall &data);
|
||||
void handleUpdate(const MTPDupdateGroupCallParticipants &data);
|
||||
bool tryCreateController();
|
||||
void destroyController();
|
||||
bool tryCreateScreencast();
|
||||
void destroyScreencast();
|
||||
|
||||
void emitShareCameraError(Error error);
|
||||
void emitShareScreenError(Error error);
|
||||
|
||||
void setState(State state);
|
||||
void finish(FinishType type);
|
||||
void maybeSendMutedUpdate(MuteState previous);
|
||||
void sendSelfUpdate(SendUpdateType type);
|
||||
void updateInstanceMuteState();
|
||||
void updateInstanceVolumes();
|
||||
void applyMeInCallLocally();
|
||||
void rejoin();
|
||||
void leave();
|
||||
void rejoin(not_null<PeerData*> as);
|
||||
void setJoinAs(not_null<PeerData*> as);
|
||||
void saveDefaultJoinAs(not_null<PeerData*> as);
|
||||
void subscribeToReal(not_null<Data::GroupCall*> real);
|
||||
void setScheduledDate(TimeId date);
|
||||
void rejoinPresentation();
|
||||
void leavePresentation();
|
||||
void checkNextJoinAction();
|
||||
|
||||
void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data);
|
||||
void setInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setInstanceMode(InstanceMode mode);
|
||||
void setScreenInstanceConnected(tgcalls::GroupNetworkState networkState);
|
||||
void setScreenInstanceMode(InstanceMode mode);
|
||||
void checkLastSpoke();
|
||||
void pushToTalkCancel();
|
||||
|
||||
void checkGlobalShortcutAvailability();
|
||||
void checkJoined();
|
||||
void checkFirstTimeJoined();
|
||||
void notifyAboutAllowedToSpeak();
|
||||
|
||||
void playConnectingSound();
|
||||
void stopConnectingSound();
|
||||
void playConnectingSoundOnce();
|
||||
|
||||
void updateRequestedVideoChannels();
|
||||
void updateRequestedVideoChannelsDelayed();
|
||||
void fillActiveVideoEndpoints();
|
||||
|
||||
void editParticipant(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyParticipantLocally(
|
||||
not_null<PeerData*> participantPeer,
|
||||
bool mute,
|
||||
std::optional<int> volume);
|
||||
void applyQueuedSelfUpdates();
|
||||
void sendPendingSelfUpdates();
|
||||
void applySelfUpdate(const MTPDgroupCallParticipant &data);
|
||||
void applyOtherParticipantUpdate(const MTPDgroupCallParticipant &data);
|
||||
|
||||
void setupMediaDevices();
|
||||
void setupOutgoingVideo();
|
||||
void setScreenEndpoint(std::string endpoint);
|
||||
void setCameraEndpoint(std::string endpoint);
|
||||
void addVideoOutput(const std::string &endpoint, SinkPointer sink);
|
||||
void setVideoEndpointLarge(VideoEndpoint endpoint);
|
||||
|
||||
void markEndpointActive(
|
||||
VideoEndpoint endpoint,
|
||||
bool active,
|
||||
bool paused);
|
||||
void markTrackPaused(const VideoEndpoint &endpoint, bool paused);
|
||||
void markTrackShown(const VideoEndpoint &endpoint, bool shown);
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
not_null<PeerData*> _peer; // Can change in legacy group migration.
|
||||
rpl::event_stream<PeerData*> _peerStream;
|
||||
not_null<History*> _history; // Can change in legacy group migration.
|
||||
MTP::Sender _api;
|
||||
rpl::event_stream<not_null<Data::GroupCall*>> _realChanges;
|
||||
rpl::variable<State> _state = State::Creating;
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
rpl::event_stream<Error> _errors;
|
||||
bool _recordingStoppedByMe = false;
|
||||
bool _requestedVideoChannelsUpdateScheduled = false;
|
||||
|
||||
MTP::DcId _broadcastDcId = 0;
|
||||
base::flat_map<not_null<LoadPartTask*>, LoadingPart> _broadcastParts;
|
||||
base::flat_set<
|
||||
std::shared_ptr<
|
||||
MediaChannelDescriptionsTask>,
|
||||
base::pointer_comparator<MediaChannelDescriptionsTask>> _mediaChannelDescriptionses;
|
||||
|
||||
not_null<PeerData*> _joinAs;
|
||||
std::vector<not_null<PeerData*>> _possibleJoinAs;
|
||||
QString _joinHash;
|
||||
|
||||
rpl::variable<MuteState> _muted = MuteState::Muted;
|
||||
rpl::variable<bool> _canManage = false;
|
||||
rpl::variable<bool> _videoIsWorking = false;
|
||||
bool _initialMuteStateSent = false;
|
||||
bool _acceptFields = false;
|
||||
|
||||
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
|
||||
std::vector<MTPGroupCallParticipant> _queuedSelfUpdates;
|
||||
|
||||
uint64 _id = 0;
|
||||
uint64 _accessHash = 0;
|
||||
JoinState _joinState;
|
||||
JoinState _screenJoinState;
|
||||
std::string _cameraEndpoint;
|
||||
std::string _screenEndpoint;
|
||||
TimeId _scheduleDate = 0;
|
||||
base::flat_set<uint32> _mySsrcs;
|
||||
mtpRequestId _createRequestId = 0;
|
||||
mtpRequestId _selfUpdateRequestId = 0;
|
||||
|
||||
rpl::variable<InstanceState> _instanceState
|
||||
= InstanceState::Disconnected;
|
||||
bool _instanceTransitioning = false;
|
||||
InstanceMode _instanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _instance;
|
||||
base::has_weak_ptr _instanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _cameraCapture;
|
||||
rpl::variable<Webrtc::VideoState> _cameraState;
|
||||
rpl::variable<bool> _isSharingCamera = false;
|
||||
base::flat_map<std::string, SinkPointer> _pendingVideoOutputs;
|
||||
|
||||
rpl::variable<InstanceState> _screenInstanceState
|
||||
= InstanceState::Disconnected;
|
||||
InstanceMode _screenInstanceMode = InstanceMode::None;
|
||||
std::unique_ptr<tgcalls::GroupInstanceCustomImpl> _screenInstance;
|
||||
base::has_weak_ptr _screenInstanceGuard;
|
||||
std::shared_ptr<tgcalls::VideoCaptureInterface> _screenCapture;
|
||||
rpl::variable<Webrtc::VideoState> _screenState;
|
||||
rpl::variable<bool> _isSharingScreen = false;
|
||||
QString _screenDeviceId;
|
||||
|
||||
base::flags<SendUpdateType> _pendingSelfUpdates;
|
||||
bool _requireARGB32 = true;
|
||||
|
||||
rpl::event_stream<LevelUpdate> _levelUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamActiveUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamPausedUpdates;
|
||||
rpl::event_stream<VideoStateToggle> _videoStreamShownUpdates;
|
||||
base::flat_map<VideoEndpoint, VideoTrack> _activeVideoTracks;
|
||||
base::flat_set<VideoEndpoint> _shownVideoTracks;
|
||||
rpl::variable<VideoEndpoint> _videoEndpointLarge;
|
||||
rpl::variable<bool> _videoEndpointPinned = false;
|
||||
crl::time _videoLargeTillTime = 0;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
rpl::event_stream<Group::RejoinEvent> _rejoinEvents;
|
||||
rpl::event_stream<> _allowedToSpeakNotifications;
|
||||
rpl::event_stream<> _titleChanged;
|
||||
base::Timer _lastSpokeCheckTimer;
|
||||
base::Timer _checkJoinedTimer;
|
||||
|
||||
crl::time _lastSendProgressUpdate = 0;
|
||||
|
||||
std::shared_ptr<GlobalShortcutManager> _shortcutManager;
|
||||
std::shared_ptr<GlobalShortcutValue> _pushToTalk;
|
||||
base::Timer _pushToTalkCancelTimer;
|
||||
base::Timer _connectingSoundTimer;
|
||||
bool _hadJoinedState = false;
|
||||
|
||||
std::unique_ptr<Webrtc::MediaDevices> _mediaDevices;
|
||||
QString _audioInputId;
|
||||
QString _audioOutputId;
|
||||
QString _cameraInputId;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls
|
||||
@@ -13,6 +13,7 @@ namespace Calls::Group {
|
||||
|
||||
constexpr auto kDefaultVolume = 10000;
|
||||
constexpr auto kMaxVolume = 20000;
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
|
||||
struct MuteRequest {
|
||||
not_null<PeerData*> peer;
|
||||
@@ -47,4 +48,24 @@ struct JoinInfo {
|
||||
TimeId scheduleDate = 0;
|
||||
};
|
||||
|
||||
enum class PanelMode {
|
||||
Default,
|
||||
Wide,
|
||||
};
|
||||
|
||||
enum class VideoQuality {
|
||||
Thumbnail,
|
||||
Medium,
|
||||
Full,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
NoCamera,
|
||||
ScreenFailed,
|
||||
MutedNoCamera,
|
||||
MutedNoScreen,
|
||||
DisabledNoCamera,
|
||||
DisabledNoScreen,
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_invite_controller.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "boxes/peer_lists_box.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] object_ptr<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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InviteController::InviteController(
|
||||
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();
|
||||
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());
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> InviteController::rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void InviteController::itemDeselectedHook(not_null<PeerData*> peer) {
|
||||
}
|
||||
|
||||
bool InviteController::hasRowFor(not_null<PeerData*> peer) const {
|
||||
return (delegate()->peerListFindRow(peer->id.value) != nullptr);
|
||||
}
|
||||
|
||||
bool InviteController::isAlreadyIn(not_null<UserData*> user) const {
|
||||
return _alreadyIn.contains(user);
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> InviteController::createRow(
|
||||
not_null<PeerData*> participant) const {
|
||||
const auto user = participant->asUser();
|
||||
if (!user
|
||||
|| user->isSelf()
|
||||
|| user->isBot()
|
||||
|| (user->flags() & MTPDuser::Flag::f_deleted)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<PeerListRow>(user);
|
||||
_rowAdded.fire_copy(user);
|
||||
_inGroup.emplace(user);
|
||||
if (isAlreadyIn(user)) {
|
||||
result->setDisabledState(PeerListRow::State::DisabledChecked);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto InviteController::peersWithRows() const
|
||||
-> not_null<const base::flat_set<not_null<UserData*>>*> {
|
||||
return &_inGroup;
|
||||
}
|
||||
|
||||
rpl::producer<not_null<UserData*>> InviteController::rowAdded() const {
|
||||
return _rowAdded.events();
|
||||
}
|
||||
|
||||
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.value)) {
|
||||
delegate()->peerListRemoveRow(row);
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
std::unique_ptr<PeerListRow> InviteContactsController::createRow(
|
||||
not_null<UserData*> user) {
|
||||
return _inGroup->contains(user)
|
||||
? nullptr
|
||||
: AddParticipantsBoxController::createRow(user);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast) {
|
||||
const auto real = call->lookupReal();
|
||||
if (!real) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto peer = call->peer();
|
||||
auto alreadyIn = peer->owner().invitedToCallUsers(real->id());
|
||||
for (const auto &participant : real->participants()) {
|
||||
if (const auto user = participant.peer->asUser()) {
|
||||
alreadyIn.emplace(user);
|
||||
}
|
||||
}
|
||||
alreadyIn.emplace(peer->session().user());
|
||||
auto controller = std::make_unique<InviteController>(peer, alreadyIn);
|
||||
controller->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
|
||||
auto contactsController = std::make_unique<InviteContactsController>(
|
||||
peer,
|
||||
std::move(alreadyIn),
|
||||
controller->peersWithRows(),
|
||||
controller->rowAdded());
|
||||
contactsController->setStyleOverrides(
|
||||
&st::groupCallInviteMembersList,
|
||||
&st::groupCallMultiSelect);
|
||||
|
||||
const auto weak = base::make_weak(call.get());
|
||||
const auto invite = [=](const std::vector<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)) {
|
||||
showToast(tr::lng_group_call_invite_done_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold((*user)->firstName),
|
||||
Ui::Text::WithEntities));
|
||||
} else if (const auto count = std::get_if<int>(&result)) {
|
||||
if (*count > 0) {
|
||||
showToast(tr::lng_group_call_invite_done_many(
|
||||
tr::now,
|
||||
lt_count,
|
||||
*count,
|
||||
Ui::Text::RichLangValue));
|
||||
}
|
||||
} else {
|
||||
Unexpected("Result in GroupCall::inviteUsers.");
|
||||
}
|
||||
};
|
||||
const auto inviteWithAdd = [=](
|
||||
const std::vector<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 = [=](
|
||||
not_null<PeerListsBox*> parentBox,
|
||||
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();
|
||||
};
|
||||
const auto done = [=] {
|
||||
inviteWithAdd(users, nonMembers, finishWithConfirm);
|
||||
};
|
||||
auto box = ConfirmBox({
|
||||
.text = { text },
|
||||
.button = tr::lng_participant_invite(),
|
||||
.callback = done,
|
||||
});
|
||||
*shared = box.data();
|
||||
parentBox->getDelegate()->showBox(
|
||||
std::move(box),
|
||||
Ui::LayerOption::KeepOther,
|
||||
anim::type::normal);
|
||||
};
|
||||
auto initBox = [=, controller = controller.get()](
|
||||
not_null<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::views::all(
|
||||
rows
|
||||
) | ranges::views::transform([](not_null<PeerData*> peer) {
|
||||
return not_null<UserData*>(peer->asUser());
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto nonMembers = ranges::views::all(
|
||||
users
|
||||
) | ranges::views::filter([&](not_null<UserData*> user) {
|
||||
return !controller->hasRowFor(user);
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto finish = [box = Ui::MakeWeak(box)]() {
|
||||
if (box) {
|
||||
box->closeBox();
|
||||
}
|
||||
};
|
||||
inviteWithConfirmation(box, users, nonMembers, finish);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
};
|
||||
|
||||
auto controllers = std::vector<std::unique_ptr<PeerListController>>();
|
||||
controllers.push_back(std::move(controller));
|
||||
controllers.push_back(std::move(contactsController));
|
||||
return Box<PeerListsBox>(std::move(controllers), initBox);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "boxes/peers/add_participants_box.h"
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class InviteController final : public ParticipantsBoxController {
|
||||
public:
|
||||
InviteController(
|
||||
not_null<PeerData*> peer,
|
||||
base::flat_set<not_null<UserData*>> alreadyIn);
|
||||
|
||||
void prepare() override;
|
||||
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
|
||||
QWidget *parent,
|
||||
not_null<PeerListRow*> row) override;
|
||||
|
||||
void itemDeselectedHook(not_null<PeerData*> peer) override;
|
||||
|
||||
[[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:
|
||||
[[nodiscard]] bool isAlreadyIn(not_null<UserData*> user) const;
|
||||
|
||||
std::unique_ptr<PeerListRow> createRow(
|
||||
not_null<PeerData*> participant) const override;
|
||||
|
||||
not_null<PeerData*> _peer;
|
||||
const base::flat_set<not_null<UserData*>> _alreadyIn;
|
||||
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;
|
||||
|
||||
bool needsInviteLinkButton() override {
|
||||
return false;
|
||||
}
|
||||
|
||||
const not_null<const base::flat_set<not_null<UserData*>>*> _inGroup;
|
||||
rpl::producer<not_null<UserData*>> _discoveredInGroup;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareInviteBox(
|
||||
not_null<GroupCall*> call,
|
||||
Fn<void(TextWithEntities&&)> showToast);
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -10,8 +10,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/peer_list_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class ScrollArea;
|
||||
class VerticalLayout;
|
||||
class SettingsButton;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
@@ -24,8 +29,11 @@ class GroupCall;
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport;
|
||||
class MembersRow;
|
||||
struct VolumeRequest;
|
||||
struct MuteRequest;
|
||||
enum class PanelMode;
|
||||
|
||||
class Members final
|
||||
: public Ui::RpWidget
|
||||
@@ -33,8 +41,12 @@ class Members final
|
||||
public:
|
||||
Members(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<GroupCall*> call);
|
||||
not_null<GroupCall*> call,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend);
|
||||
~Members();
|
||||
|
||||
[[nodiscard]] not_null<Viewport*> viewport() const;
|
||||
[[nodiscard]] int desiredHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
[[nodiscard]] rpl::producer<int> fullCountValue() const;
|
||||
@@ -48,7 +60,14 @@ public:
|
||||
return _addMemberRequests.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] MembersRow *lookupRow(not_null<PeerData*> peer) const;
|
||||
|
||||
void setMode(PanelMode mode);
|
||||
[[nodiscard]] QRect getInnerGeometry() const;
|
||||
|
||||
private:
|
||||
class Controller;
|
||||
struct VideoTile;
|
||||
using ListWidget = PeerListContent;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
@@ -73,14 +92,20 @@ private:
|
||||
void setupList();
|
||||
void setupFakeRoundCorners();
|
||||
|
||||
void trackViewportGeometry();
|
||||
void updateControlsGeometry();
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
rpl::variable<PanelMode> _mode = PanelMode();
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
std::unique_ptr<PeerListController> _listController;
|
||||
object_ptr<Ui::SettingsButton> _addMember = { nullptr };
|
||||
rpl::variable<Ui::SettingsButton*> _addMemberButton = nullptr;
|
||||
ListWidget *_list = { nullptr };
|
||||
std::unique_ptr<Controller> _listController;
|
||||
not_null<Ui::VerticalLayout*> _layout;
|
||||
const not_null<Ui::RpWidget*> _videoWrap;
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::variable<Ui::RpWidget*> _addMemberButton = nullptr;
|
||||
RpWidget *_topSkip = nullptr;
|
||||
RpWidget *_bottomSkip = nullptr;
|
||||
ListWidget *_list = nullptr;
|
||||
rpl::event_stream<> _addMemberRequests;
|
||||
|
||||
rpl::variable<bool> _canAddMembers;
|
||||
773
Telegram/SourceFiles/calls/group/calls_group_members_row.cpp
Normal file
@@ -0,0 +1,773 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/paint/arcs.h"
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kArcsStrokeRatio = 0.8;
|
||||
|
||||
const auto kSpeakerThreshold = std::vector<float>{
|
||||
Group::kDefaultVolume * 0.1f / Group::kMaxVolume,
|
||||
Group::kDefaultVolume * 0.9f / Group::kMaxVolume };
|
||||
|
||||
auto RowBlobs() -> std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::groupCallRowBlobMinRadius,
|
||||
.maxRadius = (float)st::groupCallRowBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
[[nodiscard]] QString StatusPercentString(float volume) {
|
||||
return QString::number(int(std::round(volume * 200))) + '%';
|
||||
}
|
||||
|
||||
[[nodiscard]] int StatusPercentWidth(const QString &percent) {
|
||||
return st::normalFont->width(percent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MembersRow::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel);
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSoundingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
|
||||
QImage userpicCache;
|
||||
InMemoryKey userpicKey;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
struct MembersRow::StatusIcon {
|
||||
StatusIcon(bool shown, float volume);
|
||||
|
||||
const style::icon &speaker;
|
||||
Ui::Paint::ArcsAnimation arcs;
|
||||
Ui::Animations::Simple arcsAnimation;
|
||||
Ui::Animations::Simple shownAnimation;
|
||||
QString percent;
|
||||
int percentWidth = 0;
|
||||
int arcsWidth = 0;
|
||||
int wasArcsWidth = 0;
|
||||
bool shown = true;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
MembersRow::BlobsAnimation::BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
userpicCache = QImage();
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
MembersRow::StatusIcon::StatusIcon(bool shown, float volume)
|
||||
: speaker(st::groupCallStatusSpeakerIcon)
|
||||
, arcs(
|
||||
st::groupCallStatusSpeakerArcsAnimation,
|
||||
kSpeakerThreshold,
|
||||
volume,
|
||||
Ui::Paint::ArcsAnimation::Direction::Right)
|
||||
, percent(StatusPercentString(volume))
|
||||
, percentWidth(StatusPercentWidth(percent))
|
||||
, shown(shown) {
|
||||
}
|
||||
|
||||
MembersRow::MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer)
|
||||
: PeerListRow(participantPeer)
|
||||
, _delegate(delegate)
|
||||
, _sounding(false)
|
||||
, _speaking(false)
|
||||
, _raisedHandStatus(false)
|
||||
, _skipLevelUpdate(false)
|
||||
, _mutedByMe(false) {
|
||||
refreshStatus();
|
||||
_aboutText = participantPeer->about();
|
||||
}
|
||||
|
||||
MembersRow::~MembersRow() = default;
|
||||
|
||||
void MembersRow::setSkipLevelUpdate(bool value) {
|
||||
_skipLevelUpdate = value;
|
||||
}
|
||||
|
||||
void MembersRow::updateState(
|
||||
const Data::GroupCallParticipant *participant) {
|
||||
setSsrc(participant ? participant->ssrc : 0);
|
||||
setVolume(participant
|
||||
? participant->volume
|
||||
: Group::kDefaultVolume);
|
||||
if (!participant) {
|
||||
setState(State::Invited);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = false;
|
||||
_raisedHandRating = 0;
|
||||
} else if (!participant->muted
|
||||
|| (participant->sounding && participant->ssrc != 0)) {
|
||||
setState(State::Active);
|
||||
setSounding(participant->sounding && participant->ssrc != 0);
|
||||
setSpeaking(participant->speaking && participant->ssrc != 0);
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else if (participant->canSelfUnmute) {
|
||||
setState(State::Inactive);
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
_raisedHandRating = 0;
|
||||
} else {
|
||||
setSounding(false);
|
||||
setSpeaking(false);
|
||||
_mutedByMe = participant->mutedByMe;
|
||||
_raisedHandRating = participant->raisedHandRating;
|
||||
setState(_raisedHandRating ? State::RaisedHand : State::Muted);
|
||||
}
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void MembersRow::setSpeaking(bool speaking) {
|
||||
if (_speaking == speaking) {
|
||||
return;
|
||||
}
|
||||
_speaking = speaking;
|
||||
_speakingAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
_speaking ? 0. : 1.,
|
||||
_speaking ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
|
||||
if (!_speaking
|
||||
|| _mutedByMe
|
||||
|| (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand)) {
|
||||
if (_statusIcon) {
|
||||
_statusIcon = nullptr;
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
} else if (!_statusIcon) {
|
||||
_statusIcon = std::make_unique<StatusIcon>(
|
||||
(_volume != Group::kDefaultVolume),
|
||||
(float)_volume / Group::kMaxVolume);
|
||||
_statusIcon->arcs.setStrokeRatio(kArcsStrokeRatio);
|
||||
_statusIcon->arcsWidth = _statusIcon->arcs.finishedWidth();
|
||||
_statusIcon->arcs.startUpdateRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (!_statusIcon->arcsAnimation.animating()) {
|
||||
_statusIcon->wasArcsWidth = _statusIcon->arcsWidth;
|
||||
}
|
||||
auto callback = [=](float64 value) {
|
||||
_statusIcon->arcs.update(crl::now());
|
||||
_statusIcon->arcsWidth = anim::interpolate(
|
||||
_statusIcon->wasArcsWidth,
|
||||
_statusIcon->arcs.finishedWidth(),
|
||||
value);
|
||||
_delegate->rowUpdateRow(this);
|
||||
};
|
||||
_statusIcon->arcsAnimation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}, _statusIcon->lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setSounding(bool sounding) {
|
||||
if (_sounding == sounding) {
|
||||
return;
|
||||
}
|
||||
_sounding = sounding;
|
||||
if (!_sounding) {
|
||||
_blobsAnimation = nullptr;
|
||||
} else if (!_blobsAnimation) {
|
||||
_blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
RowBlobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
_blobsAnimation->lastTime = crl::now();
|
||||
updateLevel(GroupCall::kSpeakLevelThreshold);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::clearRaisedHandStatus() {
|
||||
if (!_raisedHandStatus) {
|
||||
return;
|
||||
}
|
||||
_raisedHandStatus = false;
|
||||
refreshStatus();
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::setState(State state) {
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
const auto wasActive = (_state == State::Active);
|
||||
const auto wasMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto wasRaisedHand = (_state == State::RaisedHand);
|
||||
_state = state;
|
||||
const auto nowActive = (_state == State::Active);
|
||||
const auto nowMuted = (_state == State::Muted)
|
||||
|| (_state == State::RaisedHand);
|
||||
const auto nowRaisedHand = (_state == State::RaisedHand);
|
||||
if (!wasRaisedHand && nowRaisedHand) {
|
||||
_raisedHandStatus = true;
|
||||
_delegate->rowScheduleRaisedHandStatusRemove(this);
|
||||
}
|
||||
if (nowActive != wasActive) {
|
||||
_activeAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowActive ? 0. : 1.,
|
||||
nowActive ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
if (nowMuted != wasMuted) {
|
||||
_mutedAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
nowMuted ? 0. : 1.,
|
||||
nowMuted ? 1. : 0.,
|
||||
st::widgetFadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::setSsrc(uint32 ssrc) {
|
||||
_ssrc = ssrc;
|
||||
}
|
||||
|
||||
void MembersRow::setVolume(int volume) {
|
||||
_volume = volume;
|
||||
if (_statusIcon) {
|
||||
const auto floatVolume = (float)volume / Group::kMaxVolume;
|
||||
_statusIcon->arcs.setValue(floatVolume);
|
||||
_statusIcon->percent = StatusPercentString(floatVolume);
|
||||
_statusIcon->percentWidth = StatusPercentWidth(_statusIcon->percent);
|
||||
|
||||
const auto shown = (volume != Group::kDefaultVolume);
|
||||
if (_statusIcon->shown != shown) {
|
||||
_statusIcon->shown = shown;
|
||||
_statusIcon->shownAnimation.start(
|
||||
[=] { _delegate->rowUpdateRow(this); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::groupCallSpeakerArcsAnimation.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::updateLevel(float level) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto spoke = (level >= GroupCall::kSpeakLevelThreshold)
|
||||
? crl::now()
|
||||
: crl::time();
|
||||
if (spoke && _speaking) {
|
||||
_speakingLastTime = spoke;
|
||||
}
|
||||
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spoke) {
|
||||
_blobsAnimation->lastSoundingUpdateTime = spoke;
|
||||
}
|
||||
_blobsAnimation->blobs.setLevel(level);
|
||||
}
|
||||
|
||||
void MembersRow::updateBlobAnimation(crl::time now) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime
|
||||
+ Data::GroupCall::kSoundStatusKeptFor;
|
||||
const auto soundingStartsFinishing = soundingFinishesAt
|
||||
- kBlobsEnterDuration;
|
||||
const auto soundingFinishes = (soundingStartsFinishing < now);
|
||||
if (soundingFinishes) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(soundingFinishesAt - now) / float64(kBlobsEnterDuration),
|
||||
0.,
|
||||
1.);
|
||||
} else if (_blobsAnimation->enter < 1.) {
|
||||
_blobsAnimation->enter = std::clamp(
|
||||
(_blobsAnimation->enter
|
||||
+ ((now - _blobsAnimation->lastTime)
|
||||
/ float64(kBlobsEnterDuration))),
|
||||
0.,
|
||||
1.);
|
||||
}
|
||||
_blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime);
|
||||
_blobsAnimation->lastTime = now;
|
||||
}
|
||||
|
||||
void MembersRow::ensureUserpicCache(
|
||||
std::shared_ptr<Data::CloudImageView> &view,
|
||||
int size) {
|
||||
Expects(_blobsAnimation != nullptr);
|
||||
|
||||
const auto user = peer();
|
||||
const auto key = user->userpicUniqueKey(view);
|
||||
const auto full = QSize(size, size) * kWideScale * cIntRetinaFactor();
|
||||
auto &cache = _blobsAnimation->userpicCache;
|
||||
if (cache.isNull()) {
|
||||
cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
cache.setDevicePixelRatio(cRetinaFactor());
|
||||
} else if (_blobsAnimation->userpicKey == key
|
||||
&& cache.size() == full) {
|
||||
return;
|
||||
}
|
||||
_blobsAnimation->userpicKey = key;
|
||||
cache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size);
|
||||
}
|
||||
}
|
||||
|
||||
void MembersRow::paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
if (!_blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
auto size = sizew;
|
||||
const auto shift = QPointF(x + size / 2., y + size / 2.);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.translate(shift);
|
||||
const auto brush = _mutedByMe
|
||||
? st::groupCallMemberMutedIcon->b
|
||||
: anim::brush(
|
||||
st::groupCallMemberInactiveStatus,
|
||||
st::groupCallMemberActiveStatus,
|
||||
_speakingAnimation.value(_speaking ? 1. : 0.));
|
||||
_blobsAnimation->blobs.paint(p, brush);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
void MembersRow::paintScaledUserpic(
|
||||
Painter &p,
|
||||
std::shared_ptr<Data::CloudImageView> &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode) {
|
||||
auto size = sizew;
|
||||
if (!_blobsAnimation) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
const auto enter = _blobsAnimation->enter;
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
const auto scaleUserpic = minScale
|
||||
+ (1. - minScale) * _blobsAnimation->blobs.currentLevel();
|
||||
const auto scale = scaleUserpic * enter + 1. * (1. - enter);
|
||||
if (scale == 1.) {
|
||||
peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size);
|
||||
return;
|
||||
}
|
||||
ensureUserpicCache(userpic, size);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
auto target = QRect(
|
||||
x + (1 - kWideScale) / 2 * size,
|
||||
y + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(
|
||||
target.marginsAdded(margins),
|
||||
_blobsAnimation->userpicCache);
|
||||
}
|
||||
|
||||
void MembersRow::paintMuteIcon(
|
||||
Painter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style) {
|
||||
_delegate->rowPaintIcon(p, iconRect, computeIconState(style));
|
||||
}
|
||||
|
||||
auto MembersRow::generatePaintUserpicCallback() -> PaintRoundImageCallback {
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) {
|
||||
const auto outer = outerWidth;
|
||||
paintComplexUserpic(p, x, y, outer, size, size, PanelMode::Default);
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected) {
|
||||
paintBlobs(p, x, y, sizew, sizeh, mode);
|
||||
paintScaledUserpic(
|
||||
p,
|
||||
ensureUserpicView(),
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
sizew,
|
||||
sizeh,
|
||||
mode);
|
||||
}
|
||||
|
||||
int MembersRow::statusIconWidth(bool skipIcon) const {
|
||||
if (!_statusIcon || !_speaking) {
|
||||
return 0;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (_statusIcon->speaker.width() + _statusIcon->arcsWidth);
|
||||
const auto full = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
return int(std::round(shown * full));
|
||||
}
|
||||
|
||||
int MembersRow::statusIconHeight() const {
|
||||
return (_statusIcon && _speaking) ? _statusIcon->speaker.height() : 0;
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon) {
|
||||
if (!_statusIcon) {
|
||||
return;
|
||||
}
|
||||
const auto shown = _statusIcon->shownAnimation.value(
|
||||
_statusIcon->shown ? 1. : 0.);
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.setFont(font);
|
||||
const auto color = (_speaking
|
||||
? st.statusFgActive
|
||||
: (selected ? st.statusFgOver : st.statusFg))->c;
|
||||
p.setPen(color);
|
||||
|
||||
const auto speakerRect = QRect(
|
||||
QPoint(x, y + (font->height - statusIconHeight()) / 2),
|
||||
_statusIcon->speaker.size());
|
||||
const auto arcPosition = speakerRect.topLeft()
|
||||
+ QPoint(
|
||||
speakerRect.width() - st::groupCallStatusSpeakerArcsSkip,
|
||||
speakerRect.height() / 2);
|
||||
const auto iconWidth = skipIcon
|
||||
? 0
|
||||
: (speakerRect.width() + _statusIcon->arcsWidth);
|
||||
const auto fullWidth = iconWidth
|
||||
+ _statusIcon->percentWidth
|
||||
+ st::normalFont->spacew;
|
||||
|
||||
p.save();
|
||||
if (shown < 1.) {
|
||||
const auto centerx = speakerRect.x() + fullWidth / 2;
|
||||
const auto centery = speakerRect.y() + speakerRect.height() / 2;
|
||||
p.translate(centerx, centery);
|
||||
p.scale(shown, shown);
|
||||
p.translate(-centerx, -centery);
|
||||
}
|
||||
if (!skipIcon) {
|
||||
_statusIcon->speaker.paint(
|
||||
p,
|
||||
speakerRect.topLeft(),
|
||||
speakerRect.width(),
|
||||
color);
|
||||
p.translate(arcPosition);
|
||||
_statusIcon->arcs.paint(p, color);
|
||||
p.translate(-arcPosition);
|
||||
}
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st.statusFgActive);
|
||||
p.drawTextLeft(
|
||||
x + iconWidth,
|
||||
y,
|
||||
fullWidth,
|
||||
_statusIcon->percent);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void MembersRow::setAbout(const QString &about) {
|
||||
if (_aboutText == about) {
|
||||
return;
|
||||
}
|
||||
_aboutText = about;
|
||||
_delegate->rowUpdateRow(this);
|
||||
}
|
||||
|
||||
void MembersRow::paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
paintComplexStatusText(
|
||||
p,
|
||||
st,
|
||||
x,
|
||||
y,
|
||||
availableWidth,
|
||||
outerWidth,
|
||||
selected,
|
||||
MembersRowStyle::Default);
|
||||
}
|
||||
|
||||
void MembersRow::paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style) {
|
||||
const auto skip = (style == MembersRowStyle::Default)
|
||||
? _delegate->rowPaintStatusIcon(
|
||||
p,
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
this,
|
||||
computeIconState(MembersRowStyle::Narrow))
|
||||
: 0;
|
||||
const auto narrowMode = (skip > 0);
|
||||
x += skip;
|
||||
availableWidth -= skip;
|
||||
const auto &font = st::normalFont;
|
||||
const auto about = (style == MembersRowStyle::Video)
|
||||
? QString()
|
||||
: ((_state == State::RaisedHand && !_raisedHandStatus)
|
||||
|| (_state != State::RaisedHand && !_speaking))
|
||||
? _aboutText
|
||||
: QString();
|
||||
if (about.isEmpty()
|
||||
&& _state != State::Invited
|
||||
&& !_mutedByMe) {
|
||||
paintStatusIcon(p, x, y, st, font, selected, narrowMode);
|
||||
|
||||
const auto translatedWidth = statusIconWidth(narrowMode);
|
||||
p.translate(translatedWidth, 0);
|
||||
const auto guard = gsl::finally([&] {
|
||||
p.translate(-translatedWidth, 0);
|
||||
});
|
||||
|
||||
const auto &style = (!narrowMode
|
||||
|| (_state == State::RaisedHand && _raisedHandStatus))
|
||||
? st
|
||||
: st::groupCallNarrowMembersListItem;
|
||||
PeerListRow::paintStatusText(
|
||||
p,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
availableWidth - translatedWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
return;
|
||||
}
|
||||
p.setFont(font);
|
||||
if (style == MembersRowStyle::Video) {
|
||||
p.setPen(st::groupCallVideoSubTextFg);
|
||||
} else if (_mutedByMe) {
|
||||
p.setPen(st::groupCallMemberMutedIcon);
|
||||
} else {
|
||||
p.setPen(st::groupCallMemberNotJoinedStatus);
|
||||
}
|
||||
p.drawTextLeft(
|
||||
x,
|
||||
y,
|
||||
outerWidth,
|
||||
(_mutedByMe
|
||||
? tr::lng_group_call_muted_by_me_status(tr::now)
|
||||
: !about.isEmpty()
|
||||
? font->m.elidedText(about, Qt::ElideRight, availableWidth)
|
||||
: _delegate->rowIsMe(peer())
|
||||
? tr::lng_status_connecting(tr::now)
|
||||
: tr::lng_group_call_invited_status(tr::now)));
|
||||
}
|
||||
|
||||
QSize MembersRow::actionSize() const {
|
||||
return _delegate->rowIsNarrow() ? QSize() : QSize(
|
||||
st::groupCallActiveButton.width,
|
||||
st::groupCallActiveButton.height);
|
||||
}
|
||||
|
||||
bool MembersRow::actionDisabled() const {
|
||||
return _delegate->rowIsMe(peer())
|
||||
|| (_state == State::Invited)
|
||||
|| !_delegate->rowCanMuteMembers();
|
||||
}
|
||||
|
||||
QMargins MembersRow::actionMargins() const {
|
||||
return QMargins(
|
||||
0,
|
||||
0,
|
||||
st::groupCallMemberButtonSkip,
|
||||
0);
|
||||
}
|
||||
|
||||
void MembersRow::paintAction(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) {
|
||||
auto size = actionSize();
|
||||
const auto iconRect = style::rtlrect(
|
||||
x,
|
||||
y,
|
||||
size.width(),
|
||||
size.height(),
|
||||
outerWidth);
|
||||
if (_state == State::Invited) {
|
||||
_actionRipple = nullptr;
|
||||
}
|
||||
if (_actionRipple) {
|
||||
_actionRipple->paint(
|
||||
p,
|
||||
x + st::groupCallActiveButton.rippleAreaPosition.x(),
|
||||
y + st::groupCallActiveButton.rippleAreaPosition.y(),
|
||||
outerWidth);
|
||||
if (_actionRipple->empty()) {
|
||||
_actionRipple.reset();
|
||||
}
|
||||
}
|
||||
paintMuteIcon(p, iconRect);
|
||||
}
|
||||
|
||||
MembersRowDelegate::IconState MembersRow::computeIconState(
|
||||
MembersRowStyle style) const {
|
||||
const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.);
|
||||
const auto active = _activeAnimation.value(
|
||||
(_state == State::Active) ? 1. : 0.);
|
||||
const auto muted = _mutedAnimation.value(
|
||||
(_state == State::Muted || _state == State::RaisedHand) ? 1. : 0.);
|
||||
return {
|
||||
.speaking = speaking,
|
||||
.active = active,
|
||||
.muted = muted,
|
||||
.mutedByMe = _mutedByMe,
|
||||
.raisedHand = (_state == State::RaisedHand),
|
||||
.invited = (_state == State::Invited),
|
||||
.style = style,
|
||||
};
|
||||
}
|
||||
|
||||
void MembersRow::showContextMenu() {
|
||||
return _delegate->rowShowContextMenu(this);
|
||||
}
|
||||
|
||||
void MembersRow::refreshStatus() {
|
||||
setCustomStatus(
|
||||
(_speaking
|
||||
? tr::lng_group_call_active(tr::now)
|
||||
: _raisedHandStatus
|
||||
? tr::lng_group_call_raised_hand_status(tr::now)
|
||||
: tr::lng_group_call_inactive(tr::now)),
|
||||
_speaking);
|
||||
}
|
||||
|
||||
void MembersRow::addActionRipple(QPoint point, Fn<void()> updateCallback) {
|
||||
if (!_actionRipple) {
|
||||
auto mask = Ui::RippleAnimation::ellipseMask(QSize(
|
||||
st::groupCallActiveButton.rippleAreaSize,
|
||||
st::groupCallActiveButton.rippleAreaSize));
|
||||
_actionRipple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::groupCallActiveButton.ripple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
}
|
||||
_actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition);
|
||||
}
|
||||
|
||||
void MembersRow::refreshName(const style::PeerListItem &st) {
|
||||
PeerListRow::refreshName(st);
|
||||
//_narrowName = Ui::Text::String();
|
||||
}
|
||||
|
||||
void MembersRow::stopLastActionRipple() {
|
||||
if (_actionRipple) {
|
||||
_actionRipple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
225
Telegram/SourceFiles/calls/group/calls_group_members_row.h
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
|
||||
class PeerData;
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
struct GroupCallParticipant;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
enum class MembersRowStyle {
|
||||
Default,
|
||||
Narrow,
|
||||
Video,
|
||||
};
|
||||
|
||||
class MembersRow;
|
||||
class MembersRowDelegate {
|
||||
public:
|
||||
struct IconState {
|
||||
float64 speaking = 0.;
|
||||
float64 active = 0.;
|
||||
float64 muted = 0.;
|
||||
bool mutedByMe = false;
|
||||
bool raisedHand = false;
|
||||
bool invited = false;
|
||||
MembersRowStyle style = MembersRowStyle::Default;
|
||||
};
|
||||
virtual bool rowIsMe(not_null<PeerData*> participantPeer) = 0;
|
||||
virtual bool rowCanMuteMembers() = 0;
|
||||
virtual void rowUpdateRow(not_null<MembersRow*> row) = 0;
|
||||
virtual void rowScheduleRaisedHandStatusRemove(
|
||||
not_null<MembersRow*> row) = 0;
|
||||
virtual void rowPaintIcon(
|
||||
Painter &p,
|
||||
QRect rect,
|
||||
const IconState &state) = 0;
|
||||
virtual int rowPaintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
not_null<MembersRow*> row,
|
||||
const IconState &state) = 0;
|
||||
virtual bool rowIsNarrow() = 0;
|
||||
virtual void rowShowContextMenu(not_null<PeerListRow*> row) = 0;
|
||||
};
|
||||
|
||||
class MembersRow final : public PeerListRow {
|
||||
public:
|
||||
MembersRow(
|
||||
not_null<MembersRowDelegate*> delegate,
|
||||
not_null<PeerData*> participantPeer);
|
||||
~MembersRow();
|
||||
|
||||
enum class State {
|
||||
Active,
|
||||
Inactive,
|
||||
Muted,
|
||||
RaisedHand,
|
||||
Invited,
|
||||
};
|
||||
|
||||
void setAbout(const QString &about);
|
||||
void setSkipLevelUpdate(bool value);
|
||||
void updateState(const Data::GroupCallParticipant *participant);
|
||||
void updateLevel(float level);
|
||||
void updateBlobAnimation(crl::time now);
|
||||
void clearRaisedHandStatus();
|
||||
[[nodiscard]] State state() const {
|
||||
return _state;
|
||||
}
|
||||
[[nodiscard]] uint32 ssrc() const {
|
||||
return _ssrc;
|
||||
}
|
||||
[[nodiscard]] bool sounding() const {
|
||||
return _sounding;
|
||||
}
|
||||
[[nodiscard]] bool speaking() const {
|
||||
return _speaking;
|
||||
}
|
||||
[[nodiscard]] bool mutedByMe() const {
|
||||
return _mutedByMe;
|
||||
}
|
||||
[[nodiscard]] crl::time speakingLastTime() const {
|
||||
return _speakingLastTime;
|
||||
}
|
||||
[[nodiscard]] int volume() const {
|
||||
return _volume;
|
||||
}
|
||||
[[nodiscard]] uint64 raisedHandRating() const {
|
||||
return _raisedHandRating;
|
||||
}
|
||||
|
||||
void addActionRipple(QPoint point, Fn<void()> updateCallback) override;
|
||||
void stopLastActionRipple() override;
|
||||
|
||||
void refreshName(const style::PeerListItem &st) override;
|
||||
|
||||
QSize actionSize() const override;
|
||||
bool actionDisabled() const override;
|
||||
QMargins actionMargins() const override;
|
||||
void paintAction(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
bool actionSelected) override;
|
||||
|
||||
PaintRoundImageCallback generatePaintUserpicCallback() override;
|
||||
void paintComplexUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode,
|
||||
bool selected = false);
|
||||
|
||||
void paintStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
void paintComplexStatusText(
|
||||
Painter &p,
|
||||
const style::PeerListItem &st,
|
||||
int x,
|
||||
int y,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected,
|
||||
MembersRowStyle style);
|
||||
void paintMuteIcon(
|
||||
Painter &p,
|
||||
QRect iconRect,
|
||||
MembersRowStyle style = MembersRowStyle::Default);
|
||||
[[nodiscard]] MembersRowDelegate::IconState computeIconState(
|
||||
MembersRowStyle style = MembersRowStyle::Default) const;
|
||||
|
||||
void showContextMenu();
|
||||
|
||||
private:
|
||||
struct BlobsAnimation;
|
||||
struct StatusIcon;
|
||||
|
||||
int statusIconWidth(bool skipIcon) const;
|
||||
int statusIconHeight() const;
|
||||
void paintStatusIcon(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
const style::PeerListItem &st,
|
||||
const style::font &font,
|
||||
bool selected,
|
||||
bool skipIcon);
|
||||
|
||||
void refreshStatus() override;
|
||||
void setSounding(bool sounding);
|
||||
void setSpeaking(bool speaking);
|
||||
void setState(State state);
|
||||
void setSsrc(uint32 ssrc);
|
||||
void setVolume(int volume);
|
||||
|
||||
void ensureUserpicCache(
|
||||
std::shared_ptr<Data::CloudImageView> &view,
|
||||
int size);
|
||||
void paintBlobs(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int sizew,
|
||||
int sizeh, PanelMode mode);
|
||||
void paintScaledUserpic(
|
||||
Painter &p,
|
||||
std::shared_ptr<Data::CloudImageView> &userpic,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int sizew,
|
||||
int sizeh,
|
||||
PanelMode mode);
|
||||
|
||||
const not_null<MembersRowDelegate*> _delegate;
|
||||
State _state = State::Inactive;
|
||||
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
|
||||
std::unique_ptr<BlobsAnimation> _blobsAnimation;
|
||||
std::unique_ptr<StatusIcon> _statusIcon;
|
||||
Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon.
|
||||
Ui::Animations::Simple _mutedAnimation; // For gray/red icon.
|
||||
Ui::Animations::Simple _activeAnimation; // For icon cross animation.
|
||||
QString _aboutText;
|
||||
crl::time _speakingLastTime = 0;
|
||||
uint64 _raisedHandRating = 0;
|
||||
uint32 _ssrc = 0;
|
||||
int _volume = Group::kDefaultVolume;
|
||||
bool _sounding : 1;
|
||||
bool _speaking : 1;
|
||||
bool _raisedHandStatus : 1;
|
||||
bool _skipLevelUpdate : 1;
|
||||
bool _mutedByMe : 1;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -5,11 +5,11 @@ the official desktop application for the Telegram messaging service.
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_group_menu.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_group_settings.h"
|
||||
#include "calls/calls_group_panel.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "info/profile/info_profile_values.h" // Info::Profile::NameValue.
|
||||
@@ -78,10 +78,6 @@ void StartGroupCallRecordingBox(
|
||||
});
|
||||
box->addButton(tr::lng_group_call_recording_start_button(), [=] {
|
||||
const auto result = input->getLastText().trimmed();
|
||||
if (result.isEmpty()) {
|
||||
input->showError();
|
||||
return;
|
||||
}
|
||||
box->closeBox();
|
||||
done(result);
|
||||
});
|
||||
@@ -590,7 +586,9 @@ void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox) {
|
||||
const auto weak = base::make_weak(call.get());
|
||||
const auto resolveReal = [=] {
|
||||
@@ -606,8 +604,10 @@ void FillMenu(
|
||||
}
|
||||
|
||||
const auto addEditJoinAs = call->showChooseJoinAs();
|
||||
const auto addEditTitle = peer->canManageGroupCall();
|
||||
const auto addEditRecording = peer->canManageGroupCall()
|
||||
const auto addEditTitle = call->canManage();
|
||||
const auto addEditRecording = call->canManage() && !real->scheduleDate();
|
||||
const auto addScreenCast = !wide
|
||||
&& call->videoIsWorking()
|
||||
&& !real->scheduleDate();
|
||||
if (addEditJoinAs) {
|
||||
menu->addAction(MakeJoinAsAction(
|
||||
@@ -660,6 +660,23 @@ void FillMenu(
|
||||
real->recordStartDateValue(),
|
||||
handler));
|
||||
}
|
||||
if (addScreenCast) {
|
||||
const auto sharing = call->isSharingScreen();
|
||||
const auto toggle = [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
if (sharing) {
|
||||
strong->toggleScreenSharing(std::nullopt);
|
||||
} else {
|
||||
chooseShareScreenSource();
|
||||
}
|
||||
}
|
||||
};
|
||||
menu->addAction(
|
||||
(call->isSharingScreen()
|
||||
? tr::lng_group_call_screen_share_stop(tr::now)
|
||||
: tr::lng_group_call_screen_share_start(tr::now)),
|
||||
toggle);
|
||||
}
|
||||
menu->addAction(tr::lng_group_call_settings(tr::now), [=] {
|
||||
if (const auto strong = weak.get()) {
|
||||
showBox(Box(SettingsBox, strong));
|
||||
@@ -677,8 +694,12 @@ void FillMenu(
|
||||
menu->addAction(MakeAttentionAction(
|
||||
menu->menu(),
|
||||
(real->scheduleDate()
|
||||
? tr::lng_group_call_cancel(tr::now)
|
||||
: tr::lng_group_call_end(tr::now)),
|
||||
? (call->canManage()
|
||||
? tr::lng_group_call_cancel(tr::now)
|
||||
: tr::lng_group_call_leave(tr::now))
|
||||
: (call->canManage()
|
||||
? tr::lng_group_call_end(tr::now)
|
||||
: tr::lng_group_call_leave(tr::now))),
|
||||
finish));
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ void FillMenu(
|
||||
not_null<Ui::DropdownMenu*> menu,
|
||||
not_null<PeerData*> peer,
|
||||
not_null<GroupCall*> call,
|
||||
bool wide,
|
||||
Fn<void()> chooseJoinAs,
|
||||
Fn<void()> chooseShareScreenSource,
|
||||
Fn<void(object_ptr<Ui::BoxContent>)> showBox);
|
||||
|
||||
[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeAttentionAction(
|
||||
2011
Telegram/SourceFiles/calls/group/calls_group_panel.cpp
Normal file
@@ -10,8 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_choose_join_as.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
@@ -25,11 +26,13 @@ class GroupCall;
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class ImportantTooltip;
|
||||
class DropdownMenu;
|
||||
class CallButton;
|
||||
class CallMuteButton;
|
||||
class IconButton;
|
||||
class FlatLabel;
|
||||
class RpWidget;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
template <typename Widget>
|
||||
@@ -39,6 +42,12 @@ class ScrollArea;
|
||||
class GenericBox;
|
||||
class LayerManager;
|
||||
class GroupCallScheduledLeft;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
} // namespace GL
|
||||
namespace Toast {
|
||||
class Instance;
|
||||
} // namespace Toast
|
||||
namespace Platform {
|
||||
class TitleControls;
|
||||
} // namespace Platform
|
||||
@@ -51,14 +60,21 @@ struct CallBodyLayout;
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Toasts;
|
||||
class Members;
|
||||
class Viewport;
|
||||
enum class PanelMode;
|
||||
|
||||
class Panel final {
|
||||
class Panel final : private Ui::DesktopCapture::ChooseSourceDelegate {
|
||||
public:
|
||||
Panel(not_null<GroupCall*> call);
|
||||
~Panel();
|
||||
|
||||
[[nodiscard]] not_null<GroupCall*> call() const;
|
||||
[[nodiscard]] bool isActive() const;
|
||||
|
||||
void showToast(TextWithEntities &&text, crl::time duration = 0);
|
||||
|
||||
void minimize();
|
||||
void close();
|
||||
void showAndActivate();
|
||||
@@ -66,9 +82,13 @@ public:
|
||||
|
||||
private:
|
||||
using State = GroupCall::State;
|
||||
struct ControlsBackgroundNarrow;
|
||||
|
||||
std::unique_ptr<Ui::Window> createWindow();
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> widget() const;
|
||||
|
||||
[[nodiscard]] PanelMode mode() const;
|
||||
|
||||
void paint(QRect clip);
|
||||
|
||||
void initWindow();
|
||||
@@ -79,23 +99,42 @@ private:
|
||||
void initGeometry();
|
||||
void setupScheduledLabels(rpl::producer<TimeId> date);
|
||||
void setupMembers();
|
||||
void setupJoinAsChangedToasts();
|
||||
void setupTitleChangedToasts();
|
||||
void setupAllowedToSpeakToasts();
|
||||
void setupVideo(not_null<Viewport*> viewport);
|
||||
void setupRealMuteButtonState(not_null<Data::GroupCall*> real);
|
||||
|
||||
bool handleClose();
|
||||
void startScheduledNow();
|
||||
void trackControls(bool track);
|
||||
void raiseControls();
|
||||
void enlargeVideo();
|
||||
void minimizeVideo();
|
||||
|
||||
void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime);
|
||||
void trackControlOver(not_null<Ui::RpWidget*> control, bool over);
|
||||
void showNiceTooltip(not_null<Ui::RpWidget*> control);
|
||||
|
||||
bool updateMode();
|
||||
void updateControlsGeometry();
|
||||
void updateButtonsGeometry();
|
||||
void updateButtonsStyles();
|
||||
void updateMembersGeometry();
|
||||
void refreshControlsBackground();
|
||||
void setupControlsBackgroundWide();
|
||||
void setupControlsBackgroundNarrow();
|
||||
void showControls();
|
||||
void refreshLeftButton();
|
||||
void refreshVideoButtons(
|
||||
std::optional<bool> overrideWideMode = std::nullopt);
|
||||
void refreshTopButton();
|
||||
void toggleWideControls(bool shown);
|
||||
[[nodiscard]] bool videoButtonInNarrowMode() const;
|
||||
|
||||
void endCall();
|
||||
|
||||
void showMainMenu();
|
||||
void chooseJoinAs();
|
||||
void chooseShareScreenSource();
|
||||
void screenSharingPrivacyRequest();
|
||||
void addMembers();
|
||||
void kickParticipant(not_null<PeerData*> participantPeer);
|
||||
void kickParticipantSure(not_null<PeerData*> participantPeer);
|
||||
@@ -108,11 +147,19 @@ private:
|
||||
void migrate(not_null<ChannelData*> channel);
|
||||
void subscribeToPeerChanges();
|
||||
|
||||
QWidget *chooseSourceParent() override;
|
||||
QString chooseSourceActiveDeviceId() override;
|
||||
rpl::lifetime &chooseSourceInstanceLifetime() override;
|
||||
void chooseSourceAccepted(const QString &deviceId) override;
|
||||
void chooseSourceStop() override;
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
not_null<PeerData*> _peer;
|
||||
|
||||
Ui::GL::Backend _backend = Ui::GL::Backend();
|
||||
const std::unique_ptr<Ui::Window> _window;
|
||||
const std::unique_ptr<Ui::LayerManager> _layerBg;
|
||||
rpl::variable<PanelMode> _mode;
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
std::unique_ptr<Ui::Platform::TitleControls> _controls;
|
||||
@@ -125,19 +172,40 @@ private:
|
||||
object_ptr<Ui::AbstractButton> _recordingMark = { nullptr };
|
||||
object_ptr<Ui::IconButton> _menuToggle = { nullptr };
|
||||
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
|
||||
rpl::variable<bool> _wideMenuShown = false;
|
||||
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
|
||||
object_ptr<Members> _members = { nullptr };
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
rpl::lifetime _trackControlsLifetime;
|
||||
rpl::lifetime _trackControlsOverStateLifetime;
|
||||
rpl::lifetime _trackControlsMenuLifetime;
|
||||
object_ptr<Ui::FlatLabel> _startsIn = { nullptr };
|
||||
object_ptr<Ui::RpWidget> _countdown = { nullptr };
|
||||
std::shared_ptr<Ui::GroupCallScheduledLeft> _countdownData;
|
||||
object_ptr<Ui::FlatLabel> _startsWhen = { nullptr };
|
||||
ChooseJoinAsProcess _joinAsProcess;
|
||||
std::optional<QRect> _lastSmallGeometry;
|
||||
std::optional<QRect> _lastLargeGeometry;
|
||||
bool _lastLargeMaximized = false;
|
||||
bool _showWideControls = false;
|
||||
bool _trackControls = false;
|
||||
bool _wideControlsShown = false;
|
||||
Ui::Animations::Simple _wideControlsAnimation;
|
||||
|
||||
object_ptr<Ui::RpWidget> _controlsBackgroundWide = { nullptr };
|
||||
std::unique_ptr<ControlsBackgroundNarrow> _controlsBackgroundNarrow;
|
||||
object_ptr<Ui::CallButton> _settings = { nullptr };
|
||||
object_ptr<Ui::CallButton> _share = { nullptr };
|
||||
object_ptr<Ui::CallButton> _wideMenu = { nullptr };
|
||||
object_ptr<Ui::CallButton> _callShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _video = { nullptr };
|
||||
object_ptr<Ui::CallButton> _screenShare = { nullptr };
|
||||
std::unique_ptr<Ui::CallMuteButton> _mute;
|
||||
object_ptr<Ui::CallButton> _hangup;
|
||||
Fn<void()> _shareLinkCallback;
|
||||
object_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };
|
||||
Fn<void()> _callShareLinkCallback;
|
||||
|
||||
const std::unique_ptr<Toasts> _toasts;
|
||||
base::weak_ptr<Ui::Toast::Instance> _lastToast;
|
||||
|
||||
rpl::lifetime _peerLifetime;
|
||||
|
||||
@@ -5,13 +5,13 @@ the official desktop application for the Telegram messaging service.
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
|
||||
#include "calls/calls_group_call.h"
|
||||
#include "calls/calls_group_menu.h" // LeaveBox.
|
||||
#include "calls/calls_group_common.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_menu.h" // LeaveBox.
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_choose_join_as.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_choose_join_as.h"
|
||||
#include "ui/widgets/level_meter.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
175
Telegram/SourceFiles/calls/group/calls_group_toasts.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_toasts.h"
|
||||
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toasts/common_toasts.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorDuration = 2 * crl::time(1000);
|
||||
|
||||
using State = GroupCall::State;
|
||||
|
||||
} // namespace
|
||||
|
||||
Toasts::Toasts(not_null<Panel*> panel)
|
||||
: _panel(panel)
|
||||
, _call(panel->call()) {
|
||||
setup();
|
||||
}
|
||||
|
||||
void Toasts::setup() {
|
||||
setupJoinAsChanged();
|
||||
setupTitleChanged();
|
||||
setupRequestedToSpeak();
|
||||
setupAllowedToSpeak();
|
||||
setupPinnedVideo();
|
||||
setupError();
|
||||
}
|
||||
|
||||
void Toasts::setupJoinAsChanged() {
|
||||
_call->rejoinEvents(
|
||||
) | rpl::filter([](RejoinEvent event) {
|
||||
return (event.wasJoinAs != event.nowJoinAs);
|
||||
}) | rpl::map([=] {
|
||||
return _call->stateValue() | rpl::filter([](State state) {
|
||||
return (state == State::Joined);
|
||||
}) | rpl::take(1);
|
||||
}) | rpl::flatten_latest() | rpl::start_with_next([=] {
|
||||
_panel->showToast(tr::lng_group_call_join_as_changed(
|
||||
tr::now,
|
||||
lt_name,
|
||||
Ui::Text::Bold(_call->joinAs()->name),
|
||||
Ui::Text::WithEntities));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupTitleChanged() {
|
||||
_call->titleChanged(
|
||||
) | rpl::filter([=] {
|
||||
return (_call->lookupReal() != nullptr);
|
||||
}) | rpl::map([=] {
|
||||
const auto peer = _call->peer();
|
||||
return peer->groupCall()->title().isEmpty()
|
||||
? peer->name
|
||||
: peer->groupCall()->title();
|
||||
}) | rpl::start_with_next([=](const QString &title) {
|
||||
_panel->showToast(tr::lng_group_call_title_changed(
|
||||
tr::now,
|
||||
lt_title,
|
||||
Ui::Text::Bold(title),
|
||||
Ui::Text::WithEntities));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupAllowedToSpeak() {
|
||||
_call->allowedToSpeakNotifications(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (_panel->isActive()) {
|
||||
_panel->showToast({
|
||||
tr::lng_group_call_can_speak_here(tr::now),
|
||||
});
|
||||
} else {
|
||||
const auto real = _call->lookupReal();
|
||||
const auto name = (real && !real->title().isEmpty())
|
||||
? real->title()
|
||||
: _call->peer()->name;
|
||||
Ui::ShowMultilineToast({
|
||||
.text = tr::lng_group_call_can_speak(
|
||||
tr::now,
|
||||
lt_chat,
|
||||
Ui::Text::Bold(name),
|
||||
Ui::Text::WithEntities),
|
||||
});
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupPinnedVideo() {
|
||||
_call->videoEndpointPinnedValue(
|
||||
) | rpl::map([=](bool pinned) {
|
||||
return pinned
|
||||
? _call->videoEndpointLargeValue()
|
||||
: rpl::single(_call->videoEndpointLarge());
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::start_with_next([=](const VideoEndpoint &endpoint) {
|
||||
const auto pinned = _call->videoEndpointPinned();
|
||||
const auto peer = endpoint.peer;
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
const auto text = [&] {
|
||||
const auto me = (peer == _call->joinAs());
|
||||
const auto camera = (endpoint.type == VideoEndpointType::Camera);
|
||||
if (me) {
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera_me
|
||||
: tr::lng_group_call_unpinned_camera_me)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen_me
|
||||
: tr::lng_group_call_unpinned_screen_me);
|
||||
return key(tr::now);
|
||||
}
|
||||
const auto key = camera
|
||||
? (pinned
|
||||
? tr::lng_group_call_pinned_camera
|
||||
: tr::lng_group_call_unpinned_camera)
|
||||
: (pinned
|
||||
? tr::lng_group_call_pinned_screen
|
||||
: tr::lng_group_call_unpinned_screen);
|
||||
return key(tr::now, lt_user, peer->shortName());
|
||||
}();
|
||||
_panel->showToast({ text });
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupRequestedToSpeak() {
|
||||
_call->mutedValue(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::start_with_next([=](MuteState was, MuteState now) {
|
||||
if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) {
|
||||
_panel->showToast({
|
||||
tr::lng_group_call_tooltip_raised_hand(tr::now),
|
||||
});
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Toasts::setupError() {
|
||||
_call->errors(
|
||||
) | rpl::start_with_next([=](Error error) {
|
||||
const auto key = [&] {
|
||||
switch (error) {
|
||||
case Error::NoCamera: return tr::lng_call_error_no_camera;
|
||||
case Error::ScreenFailed:
|
||||
return tr::lng_group_call_failed_screen;
|
||||
case Error::MutedNoCamera:
|
||||
return tr::lng_group_call_muted_no_camera;
|
||||
case Error::MutedNoScreen:
|
||||
return tr::lng_group_call_muted_no_screen;
|
||||
case Error::DisabledNoCamera:
|
||||
return tr::lng_group_call_chat_no_camera;
|
||||
case Error::DisabledNoScreen:
|
||||
return tr::lng_group_call_chat_no_screen;
|
||||
}
|
||||
Unexpected("Error in Calls::Group::Toasts::setupErrorToasts.");
|
||||
}();
|
||||
_panel->showToast({ key(tr::now) }, kErrorDuration);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
38
Telegram/SourceFiles/calls/group/calls_group_toasts.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Panel;
|
||||
|
||||
class Toasts final {
|
||||
public:
|
||||
explicit Toasts(not_null<Panel*> panel);
|
||||
|
||||
private:
|
||||
void setup();
|
||||
void setupJoinAsChanged();
|
||||
void setupTitleChanged();
|
||||
void setupRequestedToSpeak();
|
||||
void setupAllowedToSpeak();
|
||||
void setupPinnedVideo();
|
||||
void setupError();
|
||||
|
||||
const not_null<Panel*> _panel;
|
||||
const not_null<GroupCall*> _call;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
935
Telegram/SourceFiles/calls/group/calls_group_viewport.cpp
Normal file
@@ -0,0 +1,935 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
|
||||
#include "calls/group/calls_group_viewport_tile.h"
|
||||
#include "calls/group/calls_group_viewport_opengl.h"
|
||||
#include "calls/group/calls_group_viewport_raster.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "data/data_group_call.h" // MuteButtonTooltip.
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
#include <QtGui/QOpenGLShader>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QRect InterpolateRect(QRect a, QRect b, float64 ratio) {
|
||||
const auto left = anim::interpolate(a.x(), b.x(), ratio);
|
||||
const auto top = anim::interpolate(a.y(), b.y(), ratio);
|
||||
const auto right = anim::interpolate(
|
||||
a.x() + a.width(),
|
||||
b.x() + b.width(),
|
||||
ratio);
|
||||
const auto bottom = anim::interpolate(
|
||||
a.y() + a.height(),
|
||||
b.y() + b.height(),
|
||||
ratio);
|
||||
return { left, top, right - left, bottom - top };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::Viewport(
|
||||
not_null<QWidget*> parent,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend)
|
||||
: _mode(mode)
|
||||
, _content(Ui::GL::CreateSurface(parent, chooseRenderer(backend))) {
|
||||
setup();
|
||||
}
|
||||
|
||||
Viewport::~Viewport() = default;
|
||||
|
||||
not_null<QWidget*> Viewport::widget() const {
|
||||
return _content->rpWidget();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidgetWrap*> Viewport::rp() const {
|
||||
return _content.get();
|
||||
}
|
||||
|
||||
void Viewport::setup() {
|
||||
const auto raw = widget();
|
||||
|
||||
raw->resize(0, 0);
|
||||
raw->setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
raw->setMouseTracking(true);
|
||||
|
||||
_content->sizeValue(
|
||||
) | rpl::filter([=] {
|
||||
return wide();
|
||||
}) | rpl::start_with_next([=] {
|
||||
updateTilesGeometry();
|
||||
}, lifetime());
|
||||
|
||||
_content->events(
|
||||
) | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Enter) {
|
||||
Ui::Integration::Instance().registerLeaveSubscription(raw);
|
||||
_mouseInside = true;
|
||||
} else if (type == QEvent::Leave) {
|
||||
Ui::Integration::Instance().unregisterLeaveSubscription(raw);
|
||||
setSelected({});
|
||||
_mouseInside = false;
|
||||
} else if (type == QEvent::MouseButtonPress) {
|
||||
handleMousePress(
|
||||
static_cast<QMouseEvent*>(e.get())->pos(),
|
||||
static_cast<QMouseEvent*>(e.get())->button());
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
handleMouseRelease(
|
||||
static_cast<QMouseEvent*>(e.get())->pos(),
|
||||
static_cast<QMouseEvent*>(e.get())->button());
|
||||
} else if (type == QEvent::MouseMove) {
|
||||
handleMouseMove(static_cast<QMouseEvent*>(e.get())->pos());
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Viewport::setGeometry(QRect geometry) {
|
||||
Expects(wide());
|
||||
|
||||
if (widget()->geometry() != geometry) {
|
||||
_geometryStaleAfterModeChange = false;
|
||||
widget()->setGeometry(geometry);
|
||||
} else if (_geometryStaleAfterModeChange) {
|
||||
_geometryStaleAfterModeChange = false;
|
||||
updateTilesGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::resizeToWidth(int width) {
|
||||
Expects(!wide());
|
||||
|
||||
updateTilesGeometry(width);
|
||||
}
|
||||
|
||||
void Viewport::setScrollTop(int scrollTop) {
|
||||
if (_scrollTop == scrollTop) {
|
||||
return;
|
||||
}
|
||||
_scrollTop = scrollTop;
|
||||
updateTilesGeometry();
|
||||
}
|
||||
|
||||
bool Viewport::wide() const {
|
||||
return (_mode == PanelMode::Wide);
|
||||
}
|
||||
|
||||
void Viewport::setMode(PanelMode mode, not_null<QWidget*> parent) {
|
||||
if (_mode == mode && widget()->parent() == parent) {
|
||||
return;
|
||||
}
|
||||
_mode = mode;
|
||||
_scrollTop = 0;
|
||||
setControlsShown(1.);
|
||||
if (widget()->parent() != parent) {
|
||||
const auto hidden = widget()->isHidden();
|
||||
widget()->setParent(parent);
|
||||
if (!hidden) {
|
||||
widget()->show();
|
||||
}
|
||||
}
|
||||
if (!wide()) {
|
||||
for (const auto &tile : _tiles) {
|
||||
tile->toggleTopControlsShown(false);
|
||||
}
|
||||
} else if (_selected.tile) {
|
||||
_selected.tile->toggleTopControlsShown(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::handleMousePress(QPoint position, Qt::MouseButton button) {
|
||||
handleMouseMove(position);
|
||||
setPressed(_selected);
|
||||
}
|
||||
|
||||
void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) {
|
||||
handleMouseMove(position);
|
||||
const auto pressed = _pressed;
|
||||
setPressed({});
|
||||
if (const auto tile = pressed.tile) {
|
||||
if (pressed == _selected) {
|
||||
if (button == Qt::RightButton) {
|
||||
tile->row()->showContextMenu();
|
||||
} else if (!wide()
|
||||
|| (_hasTwoOrMore && !_large)
|
||||
|| pressed.element != Selection::Element::PinButton) {
|
||||
_clicks.fire_copy(tile->endpoint());
|
||||
} else if (pressed.element == Selection::Element::PinButton) {
|
||||
_pinToggles.fire(!tile->pinned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::handleMouseMove(QPoint position) {
|
||||
updateSelected(position);
|
||||
}
|
||||
|
||||
void Viewport::updateSelected(QPoint position) {
|
||||
if (!widget()->rect().contains(position)) {
|
||||
setSelected({});
|
||||
return;
|
||||
}
|
||||
for (const auto &tile : _tiles) {
|
||||
const auto geometry = tile->shown()
|
||||
? tile->geometry()
|
||||
: QRect();
|
||||
if (geometry.contains(position)) {
|
||||
const auto pin = wide()
|
||||
&& tile->pinOuter().contains(position - geometry.topLeft());
|
||||
const auto back = wide()
|
||||
&& tile->backOuter().contains(position - geometry.topLeft());
|
||||
setSelected({
|
||||
.tile = tile.get(),
|
||||
.element = (pin
|
||||
? Selection::Element::PinButton
|
||||
: back
|
||||
? Selection::Element::BackButton
|
||||
: Selection::Element::Tile),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSelected({});
|
||||
}
|
||||
|
||||
void Viewport::updateSelected() {
|
||||
updateSelected(widget()->mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
|
||||
void Viewport::setControlsShown(float64 shown) {
|
||||
_controlsShownRatio = shown;
|
||||
widget()->update();
|
||||
}
|
||||
|
||||
void Viewport::add(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<bool> pinned) {
|
||||
_tiles.push_back(std::make_unique<VideoTile>(
|
||||
endpoint,
|
||||
track,
|
||||
std::move(pinned),
|
||||
[=] { widget()->update(); }));
|
||||
|
||||
_tiles.back()->trackSizeValue(
|
||||
) | rpl::filter([](QSize size) {
|
||||
return !size.isEmpty();
|
||||
}) | rpl::start_with_next([=] {
|
||||
updateTilesGeometry();
|
||||
}, _tiles.back()->lifetime());
|
||||
|
||||
_tiles.back()->track()->stateValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateTilesGeometry();
|
||||
}, _tiles.back()->lifetime());
|
||||
}
|
||||
|
||||
void Viewport::remove(const VideoEndpoint &endpoint) {
|
||||
const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);
|
||||
if (i == end(_tiles)) {
|
||||
return;
|
||||
}
|
||||
const auto removing = i->get();
|
||||
const auto largeRemoved = (_large == removing);
|
||||
if (largeRemoved) {
|
||||
prepareLargeChangeAnimation();
|
||||
_large = nullptr;
|
||||
}
|
||||
if (_selected.tile == removing) {
|
||||
setSelected({});
|
||||
}
|
||||
if (_pressed.tile == removing) {
|
||||
setPressed({});
|
||||
}
|
||||
for (auto &geometry : _startTilesLayout.list) {
|
||||
if (geometry.tile == removing) {
|
||||
geometry.tile = nullptr;
|
||||
}
|
||||
}
|
||||
for (auto &geometry : _finishTilesLayout.list) {
|
||||
if (geometry.tile == removing) {
|
||||
geometry.tile = nullptr;
|
||||
}
|
||||
}
|
||||
_tiles.erase(i);
|
||||
if (largeRemoved) {
|
||||
startLargeChangeAnimation();
|
||||
} else {
|
||||
updateTilesGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::prepareLargeChangeAnimation() {
|
||||
if (!wide()) {
|
||||
return;
|
||||
} else if (_largeChangeAnimation.animating()) {
|
||||
updateTilesAnimated();
|
||||
const auto field = _finishTilesLayout.useColumns
|
||||
? &Geometry::columns
|
||||
: &Geometry::rows;
|
||||
for (auto &finish : _finishTilesLayout.list) {
|
||||
const auto tile = finish.tile;
|
||||
if (!tile) {
|
||||
continue;
|
||||
}
|
||||
finish.*field = tile->geometry();
|
||||
}
|
||||
_startTilesLayout = std::move(_finishTilesLayout);
|
||||
_largeChangeAnimation.stop();
|
||||
|
||||
_startTilesLayout.list.erase(
|
||||
ranges::remove(_startTilesLayout.list, nullptr, &Geometry::tile),
|
||||
end(_startTilesLayout.list));
|
||||
} else {
|
||||
_startTilesLayout = applyLarge(std::move(_startTilesLayout));
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::startLargeChangeAnimation() {
|
||||
Expects(!_largeChangeAnimation.animating());
|
||||
|
||||
if (!wide()
|
||||
|| anim::Disabled()
|
||||
|| (_startTilesLayout.list.size() < 2)
|
||||
|| !_opengl
|
||||
|| widget()->size().isEmpty()) {
|
||||
updateTilesGeometry();
|
||||
return;
|
||||
}
|
||||
_finishTilesLayout = applyLarge(
|
||||
countWide(widget()->width(), widget()->height()));
|
||||
if (_finishTilesLayout.list.empty()
|
||||
|| _finishTilesLayout.outer != _startTilesLayout.outer) {
|
||||
updateTilesGeometry();
|
||||
return;
|
||||
}
|
||||
_largeChangeAnimation.start(
|
||||
[=] { updateTilesAnimated(); },
|
||||
0.,
|
||||
1.,
|
||||
st::slideDuration);
|
||||
}
|
||||
|
||||
Viewport::Layout Viewport::applyLarge(Layout layout) const {
|
||||
auto &list = layout.list;
|
||||
if (!_large) {
|
||||
return layout;
|
||||
}
|
||||
const auto i = ranges::find(list, _large, &Geometry::tile);
|
||||
if (i == end(list)) {
|
||||
return layout;
|
||||
}
|
||||
const auto field = layout.useColumns
|
||||
? &Geometry::columns
|
||||
: &Geometry::rows;
|
||||
const auto fullWidth = layout.outer.width();
|
||||
const auto fullHeight = layout.outer.height();
|
||||
const auto largeRect = (*i).*field;
|
||||
const auto largeLeft = largeRect.x();
|
||||
const auto largeTop = largeRect.y();
|
||||
const auto largeRight = largeLeft + largeRect.width();
|
||||
const auto largeBottom = largeTop + largeRect.height();
|
||||
const auto largeCenter = largeRect.center();
|
||||
for (auto &geometry : list) {
|
||||
if (geometry.tile == _large) {
|
||||
geometry.*field = { QPoint(), layout.outer };
|
||||
} else if (layout.useColumns) {
|
||||
auto &rect = geometry.columns;
|
||||
const auto center = rect.center();
|
||||
if (center.x() < largeLeft) {
|
||||
rect = rect.translated(-largeLeft, 0);
|
||||
} else if (center.x() > largeRight) {
|
||||
rect = rect.translated(fullWidth - largeRight, 0);
|
||||
} else if (center.y() < largeTop) {
|
||||
rect = QRect(
|
||||
0,
|
||||
rect.y() - largeTop,
|
||||
fullWidth,
|
||||
rect.height());
|
||||
} else if (center.y() > largeBottom) {
|
||||
rect = QRect(
|
||||
0,
|
||||
rect.y() + (fullHeight - largeBottom),
|
||||
fullWidth,
|
||||
rect.height());
|
||||
}
|
||||
} else {
|
||||
auto &rect = geometry.rows;
|
||||
const auto center = rect.center();
|
||||
if (center.y() < largeTop) {
|
||||
rect = rect.translated(0, -largeTop);
|
||||
} else if (center.y() > largeBottom) {
|
||||
rect = rect.translated(0, fullHeight - largeBottom);
|
||||
} else if (center.x() < largeLeft) {
|
||||
rect = QRect(
|
||||
rect.x() - largeLeft,
|
||||
0,
|
||||
rect.width(),
|
||||
fullHeight);
|
||||
} else {
|
||||
rect = QRect(
|
||||
rect.x() + (fullWidth - largeRight),
|
||||
0,
|
||||
rect.width(),
|
||||
fullHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
void Viewport::updateTilesAnimated() {
|
||||
if (!_largeChangeAnimation.animating()) {
|
||||
updateTilesGeometry();
|
||||
return;
|
||||
}
|
||||
const auto ratio = _largeChangeAnimation.value(1.);
|
||||
const auto field = _finishTilesLayout.useColumns
|
||||
? &Geometry::columns
|
||||
: &Geometry::rows;
|
||||
for (const auto &finish : _finishTilesLayout.list) {
|
||||
const auto tile = finish.tile;
|
||||
if (!tile) {
|
||||
continue;
|
||||
}
|
||||
const auto i = ranges::find(
|
||||
_startTilesLayout.list,
|
||||
tile,
|
||||
&Geometry::tile);
|
||||
if (i == end(_startTilesLayout.list)) {
|
||||
LOG(("Tiles Animation Error 1!"));
|
||||
_largeChangeAnimation.stop();
|
||||
updateTilesGeometry();
|
||||
return;
|
||||
}
|
||||
const auto from = (*i).*field;
|
||||
const auto to = finish.*field;
|
||||
tile->setGeometry(
|
||||
InterpolateRect(from, to, ratio),
|
||||
TileAnimation{ from.size(), to.size(), ratio });
|
||||
}
|
||||
widget()->update();
|
||||
}
|
||||
|
||||
Viewport::Layout Viewport::countWide(int outerWidth, int outerHeight) const {
|
||||
auto result = Layout{ .outer = QSize(outerWidth, outerHeight) };
|
||||
auto &sizes = result.list;
|
||||
sizes.reserve(_tiles.size());
|
||||
for (const auto &tile : _tiles) {
|
||||
const auto video = tile.get();
|
||||
const auto size = video->trackOrUserpicSize();
|
||||
if (!size.isEmpty()) {
|
||||
sizes.push_back(Geometry{ video, size });
|
||||
}
|
||||
}
|
||||
if (sizes.empty()) {
|
||||
return result;
|
||||
} else if (sizes.size() == 1) {
|
||||
sizes.front().rows = { 0, 0, outerWidth, outerHeight };
|
||||
return result;
|
||||
}
|
||||
|
||||
auto columnsBlack = uint64();
|
||||
auto rowsBlack = uint64();
|
||||
const auto count = int(sizes.size());
|
||||
const auto skip = st::groupCallVideoLargeSkip;
|
||||
const auto slices = int(std::ceil(std::sqrt(float64(count))));
|
||||
{
|
||||
auto index = 0;
|
||||
const auto columns = slices;
|
||||
const auto sizew = (outerWidth + skip) / float64(columns);
|
||||
for (auto column = 0; column != columns; ++column) {
|
||||
const auto left = int(std::round(column * sizew));
|
||||
const auto width = int(std::round(column * sizew + sizew - skip))
|
||||
- left;
|
||||
const auto rows = int(std::round((count - index)
|
||||
/ float64(columns - column)));
|
||||
const auto sizeh = (outerHeight + skip) / float64(rows);
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
const auto top = int(std::round(row * sizeh));
|
||||
const auto height = int(std::round(
|
||||
row * sizeh + sizeh - skip)) - top;
|
||||
auto &geometry = sizes[index];
|
||||
geometry.columns = {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height };
|
||||
const auto scaled = geometry.size.scaled(
|
||||
width,
|
||||
height,
|
||||
Qt::KeepAspectRatio);
|
||||
columnsBlack += (scaled.width() < width)
|
||||
? (width - scaled.width()) * height
|
||||
: (height - scaled.height()) * width;
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
auto index = 0;
|
||||
const auto rows = slices;
|
||||
const auto sizeh = (outerHeight + skip) / float64(rows);
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
const auto top = int(std::round(row * sizeh));
|
||||
const auto height = int(std::round(row * sizeh + sizeh - skip))
|
||||
- top;
|
||||
const auto columns = int(std::round((count - index)
|
||||
/ float64(rows - row)));
|
||||
const auto sizew = (outerWidth + skip) / float64(columns);
|
||||
for (auto column = 0; column != columns; ++column) {
|
||||
const auto left = int(std::round(column * sizew));
|
||||
const auto width = int(std::round(
|
||||
column * sizew + sizew - skip)) - left;
|
||||
auto &geometry = sizes[index];
|
||||
geometry.rows = {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height };
|
||||
const auto scaled = geometry.size.scaled(
|
||||
width,
|
||||
height,
|
||||
Qt::KeepAspectRatio);
|
||||
rowsBlack += (scaled.width() < width)
|
||||
? (width - scaled.width()) * height
|
||||
: (height - scaled.height()) * width;
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
result.useColumns = (columnsBlack < rowsBlack);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Viewport::showLarge(const VideoEndpoint &endpoint) {
|
||||
// If a video get's switched off, GroupCall first unpins it,
|
||||
// then removes it from Large endpoint, then removes from active tracks.
|
||||
//
|
||||
// If we want to animate large video removal properly, we need to
|
||||
// delay this update and start animation directly from removing of the
|
||||
// track from the active list. Otherwise final state won't be correct.
|
||||
_updateLargeScheduled = [=] {
|
||||
const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint);
|
||||
const auto large = (i != end(_tiles)) ? i->get() : nullptr;
|
||||
if (_large != large) {
|
||||
prepareLargeChangeAnimation();
|
||||
_large = large;
|
||||
updateTopControlsVisibility();
|
||||
startLargeChangeAnimation();
|
||||
}
|
||||
|
||||
Ensures(!_large || !_large->trackOrUserpicSize().isEmpty());
|
||||
};
|
||||
crl::on_main(widget(), [=] {
|
||||
if (!_updateLargeScheduled) {
|
||||
return;
|
||||
}
|
||||
base::take(_updateLargeScheduled)();
|
||||
});
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometry() {
|
||||
updateTilesGeometry(widget()->width());
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometry(int outerWidth) {
|
||||
const auto mouseInside = _mouseInside.current();
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (mouseInside) {
|
||||
updateSelected();
|
||||
}
|
||||
widget()->update();
|
||||
});
|
||||
|
||||
const auto outerHeight = widget()->height();
|
||||
if (_tiles.empty() || !outerWidth) {
|
||||
_fullHeight = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wide()) {
|
||||
updateTilesGeometryWide(outerWidth, outerHeight);
|
||||
refreshHasTwoOrMore();
|
||||
_fullHeight = 0;
|
||||
} else {
|
||||
updateTilesGeometryNarrow(outerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::refreshHasTwoOrMore() {
|
||||
auto hasTwoOrMore = false;
|
||||
auto oneFound = false;
|
||||
for (const auto &tile : _tiles) {
|
||||
if (!tile->trackOrUserpicSize().isEmpty()) {
|
||||
if (oneFound) {
|
||||
hasTwoOrMore = true;
|
||||
break;
|
||||
}
|
||||
oneFound = true;
|
||||
}
|
||||
}
|
||||
if (_hasTwoOrMore == hasTwoOrMore) {
|
||||
return;
|
||||
}
|
||||
_hasTwoOrMore = hasTwoOrMore;
|
||||
updateCursor();
|
||||
updateTopControlsVisibility();
|
||||
}
|
||||
|
||||
void Viewport::updateTopControlsVisibility() {
|
||||
if (_selected.tile) {
|
||||
_selected.tile->toggleTopControlsShown(
|
||||
_hasTwoOrMore && wide() && _large && _large == _selected.tile);
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometryWide(int outerWidth, int outerHeight) {
|
||||
if (!outerHeight) {
|
||||
return;
|
||||
} else if (_largeChangeAnimation.animating()) {
|
||||
if (_startTilesLayout.outer == QSize(outerWidth, outerHeight)) {
|
||||
return;
|
||||
}
|
||||
_largeChangeAnimation.stop();
|
||||
}
|
||||
|
||||
_startTilesLayout = countWide(outerWidth, outerHeight);
|
||||
if (_large && !_large->trackOrUserpicSize().isEmpty()) {
|
||||
for (const auto &geometry : _startTilesLayout.list) {
|
||||
if (geometry.tile == _large) {
|
||||
setTileGeometry(_large, { 0, 0, outerWidth, outerHeight });
|
||||
} else {
|
||||
geometry.tile->hide();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto field = _startTilesLayout.useColumns
|
||||
? &Geometry::columns
|
||||
: &Geometry::rows;
|
||||
for (const auto &geometry : _startTilesLayout.list) {
|
||||
if (const auto video = geometry.tile) {
|
||||
setTileGeometry(video, geometry.*field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometryNarrow(int outerWidth) {
|
||||
if (outerWidth <= st::groupCallNarrowMembersWidth) {
|
||||
updateTilesGeometryColumn(outerWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto y = -_scrollTop;
|
||||
auto sizes = base::flat_map<not_null<VideoTile*>, QSize>();
|
||||
sizes.reserve(_tiles.size());
|
||||
for (const auto &tile : _tiles) {
|
||||
const auto video = tile.get();
|
||||
const auto size = video->trackOrUserpicSize();
|
||||
if (size.isEmpty()) {
|
||||
video->hide();
|
||||
} else {
|
||||
sizes.emplace(video, size);
|
||||
}
|
||||
}
|
||||
if (sizes.empty()) {
|
||||
_fullHeight = 0;
|
||||
return;
|
||||
} else if (sizes.size() == 1) {
|
||||
const auto size = sizes.front().second;
|
||||
const auto heightMin = (outerWidth * 9) / 16;
|
||||
const auto heightMax = (outerWidth * 3) / 4;
|
||||
const auto scaled = size.scaled(
|
||||
QSize(outerWidth, heightMax),
|
||||
Qt::KeepAspectRatio);
|
||||
const auto height = std::max(scaled.height(), heightMin);
|
||||
const auto skip = st::groupCallVideoSmallSkip;
|
||||
setTileGeometry(sizes.front().first, { 0, y, outerWidth, height });
|
||||
_fullHeight = height + skip;
|
||||
return;
|
||||
}
|
||||
const auto min = (st::groupCallWidth
|
||||
- st::groupCallMembersMargin.left()
|
||||
- st::groupCallMembersMargin.right()
|
||||
- st::groupCallVideoSmallSkip) / 2;
|
||||
const auto square = (outerWidth - st::groupCallVideoSmallSkip) / 2;
|
||||
const auto skip = (outerWidth - 2 * square);
|
||||
const auto put = [&](not_null<VideoTile*> tile, int column, int row) {
|
||||
setTileGeometry(tile, {
|
||||
(column == 2) ? 0 : column ? (outerWidth - square) : 0,
|
||||
y + row * (min + skip),
|
||||
(column == 2) ? outerWidth : square,
|
||||
min,
|
||||
});
|
||||
};
|
||||
const auto rows = (sizes.size() + 1) / 2;
|
||||
if (sizes.size() == 3) {
|
||||
put(sizes.front().first, 2, 0);
|
||||
put((sizes.begin() + 1)->first, 0, 1);
|
||||
put((sizes.begin() + 2)->first, 1, 1);
|
||||
} else {
|
||||
auto row = 0;
|
||||
auto column = 0;
|
||||
for (const auto &[video, endpoint] : sizes) {
|
||||
put(video, column, row);
|
||||
if (column) {
|
||||
++row;
|
||||
column = (row + 1 == rows && sizes.size() % 2) ? 2 : 0;
|
||||
} else {
|
||||
column = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
_fullHeight = rows * (min + skip);
|
||||
}
|
||||
|
||||
void Viewport::updateTilesGeometryColumn(int outerWidth) {
|
||||
const auto y = -_scrollTop;
|
||||
auto top = 0;
|
||||
const auto layoutNext = [&](not_null<VideoTile*> tile) {
|
||||
const auto size = tile->trackOrUserpicSize();
|
||||
const auto shown = !size.isEmpty() && _large && tile != _large;
|
||||
const auto height = shown
|
||||
? st::groupCallNarrowVideoHeight
|
||||
: 0;
|
||||
setTileGeometry(tile, { 0, y + top, outerWidth, height });
|
||||
top += height ? (height + st::groupCallVideoSmallSkip) : 0;
|
||||
};
|
||||
const auto topPeer = _large ? _large->row()->peer().get() : nullptr;
|
||||
const auto reorderNeeded = [&] {
|
||||
if (!_large) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &tile : _tiles) {
|
||||
if (tile.get() != _large && tile->row()->peer() == topPeer) {
|
||||
return (tile.get() != _tiles.front().get())
|
||||
&& !tile->trackOrUserpicSize().isEmpty();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (reorderNeeded) {
|
||||
_tilesForOrder.clear();
|
||||
_tilesForOrder.reserve(_tiles.size());
|
||||
for (const auto &tile : _tiles) {
|
||||
_tilesForOrder.push_back(tile.get());
|
||||
}
|
||||
ranges::stable_partition(
|
||||
_tilesForOrder,
|
||||
[&](not_null<VideoTile*> tile) {
|
||||
return (tile->row()->peer() == topPeer);
|
||||
});
|
||||
for (const auto &tile : _tilesForOrder) {
|
||||
layoutNext(tile);
|
||||
}
|
||||
} else {
|
||||
for (const auto &tile : _tiles) {
|
||||
layoutNext(tile.get());
|
||||
}
|
||||
}
|
||||
_fullHeight = top;
|
||||
}
|
||||
|
||||
void Viewport::setTileGeometry(not_null<VideoTile*> tile, QRect geometry) {
|
||||
tile->setGeometry(geometry);
|
||||
|
||||
const auto min = std::min(geometry.width(), geometry.height());
|
||||
const auto kMedium = style::ConvertScale(480);
|
||||
const auto kSmall = style::ConvertScale(240);
|
||||
const auto quality = (min >= kMedium)
|
||||
? VideoQuality::Full
|
||||
: (min >= kSmall)
|
||||
? VideoQuality::Medium
|
||||
: VideoQuality::Thumbnail;
|
||||
if (tile->updateRequestedQuality(quality)) {
|
||||
_qualityRequests.fire(VideoQualityRequest{
|
||||
.endpoint = tile->endpoint(),
|
||||
.quality = quality,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::setSelected(Selection value) {
|
||||
if (_selected == value) {
|
||||
return;
|
||||
}
|
||||
if (_selected.tile) {
|
||||
_selected.tile->toggleTopControlsShown(false);
|
||||
}
|
||||
_selected = value;
|
||||
updateTopControlsVisibility();
|
||||
updateCursor();
|
||||
}
|
||||
|
||||
void Viewport::updateCursor() {
|
||||
const auto pointer = _selected.tile && (!wide() || _hasTwoOrMore);
|
||||
widget()->setCursor(pointer ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
|
||||
void Viewport::setPressed(Selection value) {
|
||||
if (_pressed == value) {
|
||||
return;
|
||||
}
|
||||
_pressed = value;
|
||||
}
|
||||
|
||||
Ui::GL::ChosenRenderer Viewport::chooseRenderer(Ui::GL::Backend backend) {
|
||||
_opengl = (backend == Ui::GL::Backend::OpenGL);
|
||||
return {
|
||||
.renderer = (_opengl
|
||||
? std::unique_ptr<Ui::GL::Renderer>(
|
||||
std::make_unique<RendererGL>(this))
|
||||
: std::make_unique<RendererSW>(this)),
|
||||
.backend = backend,
|
||||
};
|
||||
}
|
||||
|
||||
bool Viewport::requireARGB32() const {
|
||||
return !_opengl;
|
||||
}
|
||||
|
||||
int Viewport::fullHeight() const {
|
||||
return _fullHeight.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> Viewport::fullHeightValue() const {
|
||||
return _fullHeight.value();
|
||||
}
|
||||
|
||||
rpl::producer<bool> Viewport::pinToggled() const {
|
||||
return _pinToggles.events();
|
||||
}
|
||||
|
||||
rpl::producer<VideoEndpoint> Viewport::clicks() const {
|
||||
return _clicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<VideoQualityRequest> Viewport::qualityRequests() const {
|
||||
return _qualityRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<bool> Viewport::mouseInsideValue() const {
|
||||
return _mouseInside.value();
|
||||
}
|
||||
|
||||
rpl::lifetime &Viewport::lifetime() {
|
||||
return _content->lifetime();
|
||||
}
|
||||
|
||||
QImage GenerateShadow(
|
||||
int height,
|
||||
int topAlpha,
|
||||
int bottomAlpha,
|
||||
QColor color) {
|
||||
Expects(topAlpha >= 0 && topAlpha < 256);
|
||||
Expects(bottomAlpha >= 0 && bottomAlpha < 256);
|
||||
Expects(height * style::DevicePixelRatio() < 65536);
|
||||
|
||||
const auto base = (uint32(color.red()) << 16)
|
||||
| (uint32(color.green()) << 8)
|
||||
| uint32(color.blue());
|
||||
const auto premultiplied = (topAlpha == bottomAlpha) || !base;
|
||||
auto result = QImage(
|
||||
QSize(1, height * style::DevicePixelRatio()),
|
||||
(premultiplied
|
||||
? QImage::Format_ARGB32_Premultiplied
|
||||
: QImage::Format_ARGB32));
|
||||
if (topAlpha == bottomAlpha) {
|
||||
color.setAlpha(topAlpha);
|
||||
result.fill(color);
|
||||
return result;
|
||||
}
|
||||
constexpr auto kShift = 16;
|
||||
constexpr auto kMultiply = (1U << kShift);
|
||||
const auto values = std::abs(topAlpha - bottomAlpha);
|
||||
const auto rows = uint32(result.height());
|
||||
const auto step = (values * kMultiply) / (rows - 1);
|
||||
const auto till = rows * uint32(step);
|
||||
Assert(result.bytesPerLine() == sizeof(uint32));
|
||||
auto ints = reinterpret_cast<uint32*>(result.bits());
|
||||
if (topAlpha < bottomAlpha) {
|
||||
for (auto i = uint32(0); i != till; i += step) {
|
||||
*ints++ = base | ((topAlpha + (i >> kShift)) << 24);
|
||||
}
|
||||
} else {
|
||||
for (auto i = uint32(0); i != till; i += step) {
|
||||
*ints++ = base | ((topAlpha - (i >> kShift)) << 24);
|
||||
}
|
||||
}
|
||||
if (!premultiplied) {
|
||||
result = std::move(result).convertToFormat(
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
rpl::producer<QString> MuteButtonTooltip(not_null<GroupCall*> call) {
|
||||
//return rpl::single(std::make_tuple(
|
||||
// (Data::GroupCall*)nullptr,
|
||||
// call->scheduleDate()
|
||||
//)) | rpl::then(call->real(
|
||||
//) | rpl::map([](not_null<Data::GroupCall*> real) {
|
||||
// using namespace rpl::mappers;
|
||||
// return real->scheduleDateValue(
|
||||
// ) | rpl::map([=](TimeId scheduleDate) {
|
||||
// return std::make_tuple(real.get(), scheduleDate);
|
||||
// });
|
||||
//}) | rpl::flatten_latest(
|
||||
//)) | rpl::map([=](
|
||||
// Data::GroupCall *real,
|
||||
// TimeId scheduleDate) -> rpl::producer<QString> {
|
||||
// if (scheduleDate) {
|
||||
// return rpl::combine(
|
||||
// call->canManageValue(),
|
||||
// (real
|
||||
// ? real->scheduleStartSubscribedValue()
|
||||
// : rpl::single(false))
|
||||
// ) | rpl::map([](bool canManage, bool subscribed) {
|
||||
// return canManage
|
||||
// ? tr::lng_group_call_start_now()
|
||||
// : subscribed
|
||||
// ? tr::lng_group_call_cancel_reminder()
|
||||
// : tr::lng_group_call_set_reminder();
|
||||
// }) | rpl::flatten_latest();
|
||||
// }
|
||||
return call->mutedValue(
|
||||
) | rpl::map([](MuteState muted) {
|
||||
switch (muted) {
|
||||
case MuteState::Active:
|
||||
case MuteState::PushToTalk:
|
||||
return tr::lng_group_call_you_are_live();
|
||||
case MuteState::ForceMuted:
|
||||
return tr::lng_group_call_tooltip_force_muted();
|
||||
case MuteState::RaisedHand:
|
||||
return tr::lng_group_call_tooltip_raised_hand();
|
||||
case MuteState::Muted:
|
||||
return tr::lng_group_call_tooltip_microphone();
|
||||
}
|
||||
Unexpected("Value in MuteState in showNiceTooltip.");
|
||||
}) | rpl::flatten_latest();
|
||||
//}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
202
Telegram/SourceFiles/calls/group/calls_group_viewport.h
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class RpWidgetWrap;
|
||||
namespace GL {
|
||||
enum class Backend;
|
||||
struct Capabilities;
|
||||
struct ChosenRenderer;
|
||||
} // namespace GL
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
struct VideoEndpoint;
|
||||
struct VideoQualityRequest;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Webrtc {
|
||||
class VideoTrack;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class MembersRow;
|
||||
enum class PanelMode;
|
||||
enum class VideoQuality;
|
||||
|
||||
struct VideoTileTrack {
|
||||
Webrtc::VideoTrack *track = nullptr;
|
||||
MembersRow *row = nullptr;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return track != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] inline bool operator==(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return (a.track == b.track) && (a.row == b.row);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool operator!=(
|
||||
VideoTileTrack a,
|
||||
VideoTileTrack b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
class Viewport final {
|
||||
public:
|
||||
Viewport(
|
||||
not_null<QWidget*> parent,
|
||||
PanelMode mode,
|
||||
Ui::GL::Backend backend);
|
||||
~Viewport();
|
||||
|
||||
[[nodiscard]] not_null<QWidget*> widget() const;
|
||||
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
|
||||
|
||||
void setMode(PanelMode mode, not_null<QWidget*> parent);
|
||||
void setControlsShown(float64 shown);
|
||||
void setGeometry(QRect geometry);
|
||||
void resizeToWidth(int newWidth);
|
||||
void setScrollTop(int scrollTop);
|
||||
|
||||
void add(
|
||||
const VideoEndpoint &endpoint,
|
||||
VideoTileTrack track,
|
||||
rpl::producer<bool> pinned);
|
||||
void remove(const VideoEndpoint &endpoint);
|
||||
void showLarge(const VideoEndpoint &endpoint);
|
||||
|
||||
[[nodiscard]] bool requireARGB32() const;
|
||||
[[nodiscard]] int fullHeight() const;
|
||||
[[nodiscard]] rpl::producer<int> fullHeightValue() const;
|
||||
[[nodiscard]] rpl::producer<bool> pinToggled() const;
|
||||
[[nodiscard]] rpl::producer<VideoEndpoint> clicks() const;
|
||||
[[nodiscard]] rpl::producer<VideoQualityRequest> qualityRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> mouseInsideValue() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Textures;
|
||||
class VideoTile;
|
||||
class RendererSW;
|
||||
class RendererGL;
|
||||
using TileId = quintptr;
|
||||
|
||||
struct Geometry {
|
||||
VideoTile *tile = nullptr;
|
||||
QSize size;
|
||||
QRect rows;
|
||||
QRect columns;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
std::vector<Geometry> list;
|
||||
QSize outer;
|
||||
bool useColumns = false;
|
||||
};
|
||||
|
||||
struct TileAnimation {
|
||||
QSize from;
|
||||
QSize to;
|
||||
float64 ratio = -1.;
|
||||
};
|
||||
|
||||
struct Selection {
|
||||
enum class Element {
|
||||
None,
|
||||
Tile,
|
||||
PinButton,
|
||||
BackButton,
|
||||
};
|
||||
VideoTile *tile = nullptr;
|
||||
Element element = Element::None;
|
||||
|
||||
inline bool operator==(Selection other) const {
|
||||
return (tile == other.tile) && (element == other.element);
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr auto kShadowMaxAlpha = 80;
|
||||
|
||||
void setup();
|
||||
[[nodiscard]] bool wide() const;
|
||||
|
||||
void updateCursor();
|
||||
void updateTilesGeometry();
|
||||
void updateTilesGeometry(int outerWidth);
|
||||
void updateTilesGeometryWide(int outerWidth, int outerHeight);
|
||||
void updateTilesGeometryNarrow(int outerWidth);
|
||||
void updateTilesGeometryColumn(int outerWidth);
|
||||
void setTileGeometry(not_null<VideoTile*> tile, QRect geometry);
|
||||
void refreshHasTwoOrMore();
|
||||
void updateTopControlsVisibility();
|
||||
|
||||
void prepareLargeChangeAnimation();
|
||||
void startLargeChangeAnimation();
|
||||
void updateTilesAnimated();
|
||||
[[nodiscard]] Layout countWide(int outerWidth, int outerHeight) const;
|
||||
[[nodiscard]] Layout applyLarge(Layout layout) const;
|
||||
|
||||
void setSelected(Selection value);
|
||||
void setPressed(Selection value);
|
||||
|
||||
void handleMousePress(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseRelease(QPoint position, Qt::MouseButton button);
|
||||
void handleMouseMove(QPoint position);
|
||||
void updateSelected(QPoint position);
|
||||
void updateSelected();
|
||||
|
||||
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
|
||||
Ui::GL::Backend backend);
|
||||
|
||||
PanelMode _mode = PanelMode();
|
||||
bool _opengl = false;
|
||||
bool _geometryStaleAfterModeChange = false;
|
||||
const std::unique_ptr<Ui::RpWidgetWrap> _content;
|
||||
std::vector<std::unique_ptr<VideoTile>> _tiles;
|
||||
std::vector<not_null<VideoTile*>> _tilesForOrder;
|
||||
rpl::variable<int> _fullHeight = 0;
|
||||
bool _hasTwoOrMore = false;
|
||||
int _scrollTop = 0;
|
||||
QImage _shadow;
|
||||
rpl::event_stream<VideoEndpoint> _clicks;
|
||||
rpl::event_stream<bool> _pinToggles;
|
||||
rpl::event_stream<VideoQualityRequest> _qualityRequests;
|
||||
float64 _controlsShownRatio = 1.;
|
||||
VideoTile *_large = nullptr;
|
||||
Fn<void()> _updateLargeScheduled;
|
||||
Ui::Animations::Simple _largeChangeAnimation;
|
||||
Layout _startTilesLayout;
|
||||
Layout _finishTilesLayout;
|
||||
Selection _selected;
|
||||
Selection _pressed;
|
||||
rpl::variable<bool> _mouseInside = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage GenerateShadow(
|
||||
int height,
|
||||
int topAlpha,
|
||||
int bottomAlpha,
|
||||
QColor color = QColor(0, 0, 0));
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> MuteButtonTooltip(
|
||||
not_null<GroupCall*> call);
|
||||
|
||||
} // namespace Calls::Group
|
||||
1393
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp
Normal file
167
Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/cross_line.h"
|
||||
#include "ui/gl/gl_primitives.h"
|
||||
#include "ui/gl/gl_surface.h"
|
||||
#include "ui/gl/gl_image.h"
|
||||
|
||||
#include <QtGui/QOpenGLBuffer>
|
||||
#include <QtGui/QOpenGLShaderProgram>
|
||||
|
||||
namespace Webrtc {
|
||||
struct FrameWithInfo;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class Viewport::RendererGL final : public Ui::GL::Renderer {
|
||||
public:
|
||||
explicit RendererGL(not_null<Viewport*> owner);
|
||||
|
||||
void init(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
void deinit(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
void paint(
|
||||
not_null<QOpenGLWidget*> widget,
|
||||
QOpenGLFunctions &f) override;
|
||||
|
||||
std::optional<QColor> clearColor() override;
|
||||
|
||||
private:
|
||||
struct TileData {
|
||||
quintptr id = 0;
|
||||
not_null<PeerData*> peer;
|
||||
Ui::GL::Textures<5> textures;
|
||||
Ui::GL::Framebuffers<2> framebuffers;
|
||||
Ui::Animations::Simple outlined;
|
||||
Ui::Animations::Simple paused;
|
||||
QImage userpicFrame;
|
||||
QRect nameRect;
|
||||
int nameVersion = 0;
|
||||
mutable int trackIndex = -1;
|
||||
mutable QSize rgbaSize;
|
||||
mutable QSize textureSize;
|
||||
mutable QSize textureChromaSize;
|
||||
mutable QSize textureBlurSize;
|
||||
bool stale = false;
|
||||
bool pause = false;
|
||||
bool outline = false;
|
||||
};
|
||||
struct Program {
|
||||
std::optional<QOpenGLShaderProgram> argb32;
|
||||
std::optional<QOpenGLShaderProgram> yuv420;
|
||||
};
|
||||
|
||||
void setDefaultViewport(QOpenGLFunctions &f);
|
||||
void paintTile(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject,
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &nameData);
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
|
||||
[[nodiscard]] Ui::GL::Rect transformRect(
|
||||
const Ui::GL::Rect &raster) const;
|
||||
|
||||
void ensureARGB32Program();
|
||||
void ensureButtonsImage();
|
||||
void prepareObjects(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void bindFrame(
|
||||
QOpenGLFunctions &f,
|
||||
const Webrtc::FrameWithInfo &data,
|
||||
TileData &tileData,
|
||||
Program &program);
|
||||
void drawDownscalePass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData);
|
||||
void drawFirstBlurPass(
|
||||
QOpenGLFunctions &f,
|
||||
TileData &tileData,
|
||||
QSize blurSize);
|
||||
void validateDatas();
|
||||
void validateNoiseTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLuint defaultFramebufferObject);
|
||||
void validateOutlineAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validatePausedAnimation(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data);
|
||||
void validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &tileData);
|
||||
|
||||
void uploadTexture(
|
||||
QOpenGLFunctions &f,
|
||||
GLint internalformat,
|
||||
GLint format,
|
||||
QSize size,
|
||||
QSize hasSize,
|
||||
int stride,
|
||||
const void *data) const;
|
||||
|
||||
[[nodiscard]] bool isExpanded(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
QSize tileSize) const;
|
||||
[[nodiscard]] float64 countExpandRatio(
|
||||
not_null<VideoTile*> tile,
|
||||
QSize unscaled,
|
||||
const TileAnimation &animation) const;
|
||||
|
||||
const not_null<Viewport*> _owner;
|
||||
|
||||
GLfloat _factor = 1.;
|
||||
QSize _viewport;
|
||||
bool _rgbaFrame = false;
|
||||
bool _userpicFrame;
|
||||
std::optional<QOpenGLBuffer> _frameBuffer;
|
||||
Program _downscaleProgram;
|
||||
std::optional<QOpenGLShaderProgram> _blurProgram;
|
||||
Program _frameProgram;
|
||||
std::optional<QOpenGLShaderProgram> _imageProgram;
|
||||
Ui::GL::Textures<1> _noiseTexture;
|
||||
Ui::GL::Framebuffers<1> _noiseFramebuffer;
|
||||
QOpenGLShader *_downscaleVertexShader = nullptr;
|
||||
QOpenGLShader *_frameVertexShader = nullptr;
|
||||
|
||||
Ui::GL::Image _buttons;
|
||||
QRect _pinOn;
|
||||
QRect _pinOff;
|
||||
QRect _back;
|
||||
QRect _muteOn;
|
||||
QRect _muteOff;
|
||||
QRect _paused;
|
||||
|
||||
Ui::GL::Image _names;
|
||||
std::vector<TileData> _tileData;
|
||||
std::vector<int> _tileDataIndices;
|
||||
|
||||
Ui::CrossLineAnimation _pinIcon;
|
||||
Ui::CrossLineAnimation _muteIcon;
|
||||
|
||||
Ui::RoundRect _pinBackground;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
301
Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "calls/group/calls_group_viewport_raster.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_viewport_tile.h"
|
||||
#include "calls/group/calls_group_members_row.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBlurRadius = 15;
|
||||
|
||||
} // namespace
|
||||
|
||||
Viewport::RendererSW::RendererSW(not_null<Viewport*> owner)
|
||||
: _owner(owner)
|
||||
, _pinIcon(st::groupCallVideoTile.pin)
|
||||
, _pinBackground(
|
||||
(st::groupCallVideoTile.pinPadding.top()
|
||||
+ st::groupCallVideoTile.pin.icon.height()
|
||||
+ st::groupCallVideoTile.pinPadding.bottom()) / 2,
|
||||
st::radialBg) {
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintFallback(
|
||||
Painter &&p,
|
||||
const QRegion &clip,
|
||||
Ui::GL::Backend backend) {
|
||||
auto bg = clip;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto bounding = clip.boundingRect();
|
||||
for (auto &[tile, tileData] : _tileData) {
|
||||
tileData.stale = true;
|
||||
}
|
||||
for (const auto &tile : _owner->_tiles) {
|
||||
if (!tile->shown()) {
|
||||
continue;
|
||||
}
|
||||
paintTile(p, tile.get(), bounding, bg);
|
||||
}
|
||||
for (const auto rect : bg) {
|
||||
p.fillRect(rect, st::groupCallBg);
|
||||
}
|
||||
for (auto i = _tileData.begin(); i != _tileData.end();) {
|
||||
if (i->second.stale) {
|
||||
i = _tileData.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::validateUserpicFrame(
|
||||
not_null<VideoTile*> tile,
|
||||
TileData &data) {
|
||||
if (!_userpicFrame) {
|
||||
data.userpicFrame = QImage();
|
||||
return;
|
||||
} else if (!data.userpicFrame.isNull()) {
|
||||
return;
|
||||
}
|
||||
auto userpic = QImage(
|
||||
tile->trackOrUserpicSize(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.fill(Qt::black);
|
||||
{
|
||||
auto p = Painter(&userpic);
|
||||
tile->row()->peer()->paintUserpicSquare(
|
||||
p,
|
||||
tile->row()->ensureUserpicView(),
|
||||
0,
|
||||
0,
|
||||
userpic.width());
|
||||
}
|
||||
data.userpicFrame = Images::BlurLargeImage(
|
||||
std::move(userpic),
|
||||
kBlurRadius);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTile(
|
||||
Painter &p,
|
||||
not_null<VideoTile*> tile,
|
||||
const QRect &clip,
|
||||
QRegion &bg) {
|
||||
const auto track = tile->track();
|
||||
const auto markGuard = gsl::finally([&] {
|
||||
tile->track()->markFrameShown();
|
||||
});
|
||||
const auto data = track->frameWithInfo(true);
|
||||
auto &tileData = _tileData[tile];
|
||||
tileData.stale = false;
|
||||
_userpicFrame = (data.format == Webrtc::FrameFormat::None);
|
||||
_pausedFrame = (track->state() == Webrtc::VideoState::Paused);
|
||||
validateUserpicFrame(tile, tileData);
|
||||
if (_userpicFrame || !_pausedFrame) {
|
||||
tileData.blurredFrame = QImage();
|
||||
} else if (tileData.blurredFrame.isNull()) {
|
||||
tileData.blurredFrame = Images::BlurLargeImage(
|
||||
data.original.scaled(
|
||||
VideoTile::PausedVideoSize(),
|
||||
Qt::KeepAspectRatio),
|
||||
kBlurRadius);
|
||||
}
|
||||
const auto &image = _userpicFrame
|
||||
? tileData.userpicFrame
|
||||
: _pausedFrame
|
||||
? tileData.blurredFrame
|
||||
: data.original;
|
||||
const auto frameRotation = _userpicFrame ? 0 : data.rotation;
|
||||
Assert(!image.isNull());
|
||||
|
||||
const auto fill = [&](QRect rect) {
|
||||
const auto intersected = rect.intersected(clip);
|
||||
if (!intersected.isEmpty()) {
|
||||
p.fillRect(intersected, st::groupCallMembersBg);
|
||||
bg -= intersected;
|
||||
}
|
||||
};
|
||||
|
||||
using namespace Media::View;
|
||||
const auto geometry = tile->geometry();
|
||||
const auto x = geometry.x();
|
||||
const auto y = geometry.y();
|
||||
const auto width = geometry.width();
|
||||
const auto height = geometry.height();
|
||||
const auto scaled = FlipSizeByRotation(
|
||||
image.size(),
|
||||
frameRotation
|
||||
).scaled(QSize(width, height), Qt::KeepAspectRatio);
|
||||
const auto left = (width - scaled.width()) / 2;
|
||||
const auto top = (height - scaled.height()) / 2;
|
||||
const auto target = QRect(QPoint(x + left, y + top), scaled);
|
||||
if (UsePainterRotation(frameRotation)) {
|
||||
if (frameRotation) {
|
||||
p.save();
|
||||
p.rotate(frameRotation);
|
||||
}
|
||||
p.drawImage(RotatedRect(target, frameRotation), image);
|
||||
if (frameRotation) {
|
||||
p.restore();
|
||||
}
|
||||
} else if (frameRotation) {
|
||||
p.drawImage(target, RotateFrameImage(image, frameRotation));
|
||||
} else {
|
||||
p.drawImage(target, image);
|
||||
}
|
||||
bg -= target;
|
||||
|
||||
if (left > 0) {
|
||||
fill({ x, y, left, height });
|
||||
}
|
||||
if (const auto right = left + scaled.width(); right < width) {
|
||||
fill({ x + right, y, width - right, height });
|
||||
}
|
||||
if (top > 0) {
|
||||
fill({ x, y, width, top });
|
||||
}
|
||||
if (const auto bottom = top + scaled.height(); bottom < height) {
|
||||
fill({ x, y + bottom, width, height - bottom });
|
||||
}
|
||||
|
||||
paintTileControls(p, x, y, width, height, tile);
|
||||
paintTileOutline(p, x, y, width, height, tile);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileOutline(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
if (!tile->row()->speaking()) {
|
||||
return;
|
||||
}
|
||||
const auto outline = st::groupCallOutline;
|
||||
const auto &color = st::groupCallMemberActiveIcon;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.fillRect(x, y, outline, height - outline, color);
|
||||
p.fillRect(x + outline, y, width - outline, outline, color);
|
||||
p.fillRect(
|
||||
x + width - outline,
|
||||
y + outline,
|
||||
outline,
|
||||
height - outline,
|
||||
color);
|
||||
p.fillRect(x, y + height - outline, width - outline, outline, color);
|
||||
}
|
||||
|
||||
void Viewport::RendererSW::paintTileControls(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
not_null<VideoTile*> tile) {
|
||||
p.setClipRect(x, y, width, height);
|
||||
const auto guard = gsl::finally([&] { p.setClipping(false); });
|
||||
|
||||
const auto wide = _owner->wide();
|
||||
if (wide) {
|
||||
// Pin.
|
||||
const auto pinInner = tile->pinInner();
|
||||
VideoTile::PaintPinButton(
|
||||
p,
|
||||
tile->pinned(),
|
||||
x + pinInner.x(),
|
||||
y + pinInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground,
|
||||
&_pinIcon);
|
||||
|
||||
// Back.
|
||||
const auto backInner = tile->backInner();
|
||||
VideoTile::PaintBackButton(
|
||||
p,
|
||||
x + backInner.x(),
|
||||
y + backInner.y(),
|
||||
_owner->widget()->width(),
|
||||
&_pinBackground);
|
||||
}
|
||||
|
||||
if (_pausedFrame) {
|
||||
p.fillRect(x, y, width, height, QColor(0, 0, 0, kShadowMaxAlpha));
|
||||
st::groupCallPaused.paintInCenter(p, { x, y, width, height });
|
||||
}
|
||||
|
||||
const auto shown = _owner->_controlsShownRatio;
|
||||
if (shown == 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &st = st::groupCallVideoTile;
|
||||
const auto fullShift = st.namePosition.y() + st::normalFont->height;
|
||||
const auto shift = anim::interpolate(fullShift, 0, shown);
|
||||
|
||||
// Shadow.
|
||||
if (_shadow.isNull()) {
|
||||
_shadow = GenerateShadow(st.shadowHeight, 0, kShadowMaxAlpha);
|
||||
}
|
||||
const auto shadowRect = QRect(
|
||||
x,
|
||||
y + (height - anim::interpolate(0, st.shadowHeight, shown)),
|
||||
width,
|
||||
st.shadowHeight);
|
||||
const auto shadowFill = shadowRect.intersected({ x, y, width, height });
|
||||
if (shadowFill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
if (!_pausedFrame) {
|
||||
p.drawImage(
|
||||
shadowFill,
|
||||
_shadow,
|
||||
QRect(
|
||||
0,
|
||||
(shadowFill.y() - shadowRect.y()) * factor,
|
||||
_shadow.width(),
|
||||
shadowFill.height() * factor));
|
||||
}
|
||||
const auto row = tile->row();
|
||||
row->lazyInitialize(st::groupCallMembersListItem);
|
||||
|
||||
// Mute.
|
||||
const auto &icon = st::groupCallVideoCrossLine.icon;
|
||||
const auto iconLeft = x + width - st.iconPosition.x() - icon.width();
|
||||
const auto iconTop = y + (height
|
||||
- st.iconPosition.y()
|
||||
- icon.height()
|
||||
+ shift);
|
||||
row->paintMuteIcon(
|
||||
p,
|
||||
{ iconLeft, iconTop, icon.width(), icon.height() },
|
||||
MembersRowStyle::Video);
|
||||
|
||||
// Name.
|
||||
p.setPen(st::groupCallVideoTextFg);
|
||||
const auto hasWidth = width
|
||||
- st.iconPosition.x() - icon.width()
|
||||
- st.namePosition.x();
|
||||
const auto nameLeft = x + st.namePosition.x();
|
||||
const auto nameTop = y + (height
|
||||
- st.namePosition.y()
|
||||
- st::semiboldFont->height
|
||||
+ shift);
|
||||
row->name().drawLeftElided(p, nameLeft, nameTop, hasWidth, width);
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||