Compare commits
382 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e494f55a | ||
|
|
0b4605a656 | ||
|
|
18a86e500b | ||
|
|
e19af1257c | ||
|
|
e0159e15b2 | ||
|
|
b6e77537e2 | ||
|
|
beaa4190eb | ||
|
|
7924979dfb | ||
|
|
570ed5691d | ||
|
|
c25779b844 | ||
|
|
7304f2b695 | ||
|
|
b4bff939b1 | ||
|
|
1f816c249b | ||
|
|
8591d58798 | ||
|
|
9290cd3a16 | ||
|
|
dc0aaec4a4 | ||
|
|
eefa7263b5 | ||
|
|
7885be4a94 | ||
|
|
583c3d3429 | ||
|
|
6d0d399250 | ||
|
|
0e89c93993 | ||
|
|
b422ec025e | ||
|
|
c8535acad8 | ||
|
|
e2a97e2ae9 | ||
|
|
77a019325d | ||
|
|
115dc460ac | ||
|
|
3df1a73cf5 | ||
|
|
5cf69366d1 | ||
|
|
8c2b1168af | ||
|
|
a425024f21 | ||
|
|
e85026ec46 | ||
|
|
1c6e2eae04 | ||
|
|
4db5624beb | ||
|
|
6d08542afa | ||
|
|
707b36dc12 | ||
|
|
da3e140069 | ||
|
|
5334372671 | ||
|
|
16db8468fa | ||
|
|
2ed3543b53 | ||
|
|
5b4d442799 | ||
|
|
9669a8a44a | ||
|
|
80fe2f57e9 | ||
|
|
824fbc21e8 | ||
|
|
bf7f117323 | ||
|
|
9b488f03a1 | ||
|
|
295a863d69 | ||
|
|
0d814066d6 | ||
|
|
1af8e89eb9 | ||
|
|
7cf79e1f8a | ||
|
|
019fd83c8a | ||
|
|
65779ec37e | ||
|
|
2d90a06078 | ||
|
|
d2c8780c0f | ||
|
|
54dd63d61a | ||
|
|
7852c82eab | ||
|
|
77c8bf8176 | ||
|
|
daa14466e5 | ||
|
|
aad38c2809 | ||
|
|
2c50d3d87b | ||
|
|
0fe7c07007 | ||
|
|
c22d200c17 | ||
|
|
9e6afa0d4e | ||
|
|
3340b2dc03 | ||
|
|
386fae952b | ||
|
|
a164cb9480 | ||
|
|
bc9b288617 | ||
|
|
5c7229f875 | ||
|
|
658d5a1322 | ||
|
|
52e841ec29 | ||
|
|
df28da4d97 | ||
|
|
8dac6896d6 | ||
|
|
f18e157e46 | ||
|
|
b2bf8244dd | ||
|
|
4e0355d09f | ||
|
|
7059336ff0 | ||
|
|
94f10ce72e | ||
|
|
68be54288c | ||
|
|
10636d931f | ||
|
|
3b1aa55d21 | ||
|
|
37f59095f4 | ||
|
|
f85d1b8a29 | ||
|
|
ece491eee7 | ||
|
|
750c13e5fe | ||
|
|
0fde35f59e | ||
|
|
5ab8a7d9c5 | ||
|
|
b1fad4f7e0 | ||
|
|
4f0f815201 | ||
|
|
f2286fdffd | ||
|
|
46dfde881a | ||
|
|
95f7704d14 | ||
|
|
64a6838764 | ||
|
|
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 |
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 \
|
||||
|
||||
21
.github/workflows/win.yml
vendored
@@ -284,9 +284,16 @@ jobs:
|
||||
msbuild -m opus.sln /property:Configuration=Debug /property:Platform="Win32"
|
||||
msbuild -m opus.sln /property:Configuration=Release /property:Platform="Win32"
|
||||
|
||||
echo "Workaround for FFmpeg."
|
||||
copy Win32\Release\m.lib Win32\Release\ssp.lib
|
||||
copy Win32\Release\m.lib Win32\Debug\ssp.lib
|
||||
- name: Rnnoise.
|
||||
shell: cmd
|
||||
run: |
|
||||
%VC%
|
||||
|
||||
git clone %GIT%/desktop-app/rnnoise.git
|
||||
mkdir rnnoise\out
|
||||
cd rnnoise\out
|
||||
cmake -A Win32 ..
|
||||
cmake --build . --config Debug
|
||||
|
||||
- name: FFmpeg cache.
|
||||
id: cache-ffmpeg
|
||||
@@ -302,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
|
||||
@@ -341,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%" ^
|
||||
@@ -381,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 ^
|
||||
@@ -409,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
|
||||
|
||||
@@ -53,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
|
||||
@@ -352,6 +373,8 @@ PRIVATE
|
||||
core/core_cloud_password.h
|
||||
core/core_settings.cpp
|
||||
core/core_settings.h
|
||||
core/core_settings_proxy.cpp
|
||||
core/core_settings_proxy.h
|
||||
core/crash_report_window.cpp
|
||||
core/crash_report_window.h
|
||||
core/crash_reports.cpp
|
||||
@@ -399,6 +422,8 @@ PRIVATE
|
||||
data/data_document.h
|
||||
data/data_document_media.cpp
|
||||
data/data_document_media.h
|
||||
data/data_document_resolver.cpp
|
||||
data/data_document_resolver.h
|
||||
data/data_drafts.cpp
|
||||
data/data_drafts.h
|
||||
data/data_folder.cpp
|
||||
@@ -708,6 +733,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,14 +789,25 @@ PRIVATE
|
||||
media/streaming/media_streaming_video_track.h
|
||||
media/view/media_view_group_thumbs.cpp
|
||||
media/view/media_view_group_thumbs.h
|
||||
media/view/media_view_overlay_opengl.cpp
|
||||
media/view/media_view_overlay_opengl.h
|
||||
media/view/media_view_overlay_raster.cpp
|
||||
media/view/media_view_overlay_raster.h
|
||||
media/view/media_view_overlay_renderer.h
|
||||
media/view/media_view_overlay_widget.cpp
|
||||
media/view/media_view_overlay_widget.h
|
||||
media/view/media_view_pip.cpp
|
||||
media/view/media_view_pip.h
|
||||
media/view/media_view_pip_opengl.cpp
|
||||
media/view/media_view_pip_opengl.h
|
||||
media/view/media_view_pip_raster.cpp
|
||||
media/view/media_view_pip_raster.h
|
||||
media/view/media_view_pip_renderer.h
|
||||
media/view/media_view_playback_controls.cpp
|
||||
media/view/media_view_playback_controls.h
|
||||
media/view/media_view_playback_progress.cpp
|
||||
media/view/media_view_playback_progress.h
|
||||
media/view/media_view_open_common.h
|
||||
mtproto/config_loader.cpp
|
||||
mtproto/config_loader.h
|
||||
mtproto/connection_abstract.cpp
|
||||
@@ -836,8 +874,6 @@ PRIVATE
|
||||
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
|
||||
@@ -1040,6 +1076,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
|
||||
@@ -1053,6 +1091,8 @@ PRIVATE
|
||||
window/section_memento.h
|
||||
window/section_widget.cpp
|
||||
window/section_widget.h
|
||||
window/window_adaptive.cpp
|
||||
window/window_adaptive.h
|
||||
window/window_connecting_widget.cpp
|
||||
window/window_connecting_widget.h
|
||||
window/window_controller.cpp
|
||||
@@ -1211,8 +1251,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.";
|
||||
@@ -496,6 +497,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_background_text1" = "Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff!";
|
||||
"lng_background_text2" = "I can't even take you seriously right now.";
|
||||
"lng_background_bad_link" = "This background link appears to be invalid.";
|
||||
"lng_background_gradient_unsupported" = "Telegram Desktop doesn't support gradient backgrounds yet.";
|
||||
"lng_background_apply" = "Apply";
|
||||
"lng_background_share" = "Share";
|
||||
"lng_background_link_copied" = "Link copied to clipboard";
|
||||
@@ -1401,6 +1403,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";
|
||||
@@ -1996,8 +2000,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";
|
||||
@@ -2015,6 +2025,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";
|
||||
@@ -2037,6 +2058,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_ptt_delay_s" = "{amount}s";
|
||||
"lng_group_call_ptt_delay" = "Push to Talk release delay: {delay}";
|
||||
"lng_group_call_share" = "Share Invite Link";
|
||||
"lng_group_call_noise_suppression" = "Enable Noise Suppression";
|
||||
"lng_group_call_limit#one" = "Video is only available\nfor the first {count} member";
|
||||
"lng_group_call_limit#other" = "Video is only available\nfor the first {count} members";
|
||||
"lng_group_call_video_paused" = "Video is paused";
|
||||
"lng_group_call_share_speaker" = "Users with this link can speak";
|
||||
"lng_group_call_copy_speaker_link" = "Copy Speaker Link";
|
||||
"lng_group_call_copy_listener_link" = "Copy Listener Link";
|
||||
@@ -2057,6 +2082,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";
|
||||
@@ -2070,6 +2099,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...";
|
||||
@@ -2106,6 +2137,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";
|
||||
@@ -2772,6 +2812,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.5.0" />
|
||||
Version="2.7.9.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,5,0
|
||||
PRODUCTVERSION 2,7,5,0
|
||||
FILEVERSION 2,7,9,0
|
||||
PRODUCTVERSION 2,7,9,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.5.0"
|
||||
VALUE "FileVersion", "2.7.9.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.7.5.0"
|
||||
VALUE "ProductVersion", "2.7.9.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,7,5,0
|
||||
PRODUCTVERSION 2,7,5,0
|
||||
FILEVERSION 2,7,9,0
|
||||
PRODUCTVERSION 2,7,9,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.5.0"
|
||||
VALUE "FileVersion", "2.7.9.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.7.5.0"
|
||||
VALUE "ProductVersion", "2.7.9.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();
|
||||
|
||||
@@ -36,10 +36,12 @@ void AttachedStickers::request(
|
||||
return;
|
||||
}
|
||||
if (result.v.isEmpty()) {
|
||||
Ui::show(Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
strongController->show(
|
||||
Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
return;
|
||||
} else if (result.v.size() > 1) {
|
||||
Ui::show(Box<StickersBox>(strongController, result));
|
||||
strongController->show(
|
||||
Box<StickersBox>(strongController, result));
|
||||
return;
|
||||
}
|
||||
// Single attached sticker pack.
|
||||
@@ -52,12 +54,15 @@ void AttachedStickers::request(
|
||||
const auto setId = (setData->vid().v && setData->vaccess_hash().v)
|
||||
? MTP_inputStickerSetID(setData->vid(), setData->vaccess_hash())
|
||||
: MTP_inputStickerSetShortName(setData->vshort_name());
|
||||
Ui::show(
|
||||
strongController->show(
|
||||
Box<StickerSetBox>(strongController, setId),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
Ui::show(Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
if (const auto strongController = weak.get()) {
|
||||
strongController->show(
|
||||
Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ void CheckChatInvite(
|
||||
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
|
||||
Core::App().hideMediaView();
|
||||
result.match([=](const MTPDchatInvite &data) {
|
||||
const auto box = Ui::show(Box<ConfirmInviteBox>(
|
||||
const auto strongController = weak.get();
|
||||
if (!strongController) {
|
||||
return;
|
||||
}
|
||||
const auto box = strongController->show(Box<ConfirmInviteBox>(
|
||||
session,
|
||||
data,
|
||||
invitePeekChannel,
|
||||
@@ -80,7 +84,10 @@ void CheckChatInvite(
|
||||
return;
|
||||
}
|
||||
Core::App().hideMediaView();
|
||||
Ui::show(Box<InformBox>(tr::lng_group_invite_bad_link(tr::now)));
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->show(
|
||||
Box<InformBox>(tr::lng_group_invite_bad_link(tr::now)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -212,6 +212,11 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
|
||||
}, _session->lifetime());
|
||||
|
||||
setupSupportMode();
|
||||
|
||||
Core::App().settings().proxy().connectionTypeValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
refreshTopPromotion();
|
||||
}, _session->lifetime());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -261,10 +266,10 @@ void ApiWrap::refreshTopPromotion() {
|
||||
return;
|
||||
}
|
||||
const auto key = [&]() -> std::pair<QString, uint32> {
|
||||
if (Global::ProxySettings() != MTP::ProxyData::Settings::Enabled) {
|
||||
if (!Core::App().settings().proxy().isEnabled()) {
|
||||
return {};
|
||||
}
|
||||
const auto &proxy = Global::SelectedProxy();
|
||||
const auto &proxy = Core::App().settings().proxy().selected();
|
||||
if (proxy.type != MTP::ProxyData::Type::Mtproto) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "core/application.h"
|
||||
#include "mainwindow.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
@@ -45,7 +44,6 @@ void AutoLockBox::prepare() {
|
||||
void AutoLockBox::durationChanged(int seconds) {
|
||||
Core::App().settings().setAutoLock(seconds);
|
||||
Core::App().saveSettingsDelayed();
|
||||
Global::RefLocalPasscodeChanged().notify();
|
||||
|
||||
Core::App().checkAutoLock();
|
||||
closeBox();
|
||||
|
||||
@@ -151,7 +151,7 @@ void BackgroundBox::prepare() {
|
||||
|
||||
_inner->chooseEvents(
|
||||
) | rpl::start_with_next([=](const Data::WallPaper &paper) {
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
Box<BackgroundPreviewBox>(_controller, paper),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}, _inner->lifetime());
|
||||
@@ -176,7 +176,7 @@ void BackgroundBox::removePaper(const Data::WallPaper &paper) {
|
||||
paper.mtpSettings()
|
||||
)).send();
|
||||
};
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
Box<ConfirmBox>(
|
||||
tr::lng_background_sure_delete(tr::now),
|
||||
tr::lng_selected_delete(tr::now),
|
||||
|
||||
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_document_resolver.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
@@ -769,23 +770,25 @@ bool BackgroundPreviewBox::Start(
|
||||
const QString &slug,
|
||||
const QMap<QString, QString> ¶ms) {
|
||||
if (const auto paper = Data::WallPaper::FromColorSlug(slug)) {
|
||||
Ui::show(Box<BackgroundPreviewBox>(
|
||||
controller->show(Box<BackgroundPreviewBox>(
|
||||
controller,
|
||||
paper->withUrlParams(params)));
|
||||
return true;
|
||||
}
|
||||
if (!IsValidWallPaperSlug(slug)) {
|
||||
Ui::show(Box<InformBox>(tr::lng_background_bad_link(tr::now)));
|
||||
controller->show(
|
||||
Box<InformBox>(tr::lng_background_bad_link(tr::now)));
|
||||
return false;
|
||||
}
|
||||
controller->session().api().requestWallPaper(slug, crl::guard(controller, [=](
|
||||
const Data::WallPaper &result) {
|
||||
Ui::show(Box<BackgroundPreviewBox>(
|
||||
controller->show(Box<BackgroundPreviewBox>(
|
||||
controller,
|
||||
result.withUrlParams(params)));
|
||||
}), [](const MTP::Error &error) {
|
||||
Ui::show(Box<InformBox>(tr::lng_background_bad_link(tr::now)));
|
||||
});
|
||||
}), crl::guard(controller, [=](const MTP::Error &error) {
|
||||
controller->show(
|
||||
Box<InformBox>(tr::lng_background_bad_link(tr::now)));
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/qthelp_url.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "main/main_account.h"
|
||||
#include "mtproto/facade.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
@@ -26,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "facades.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
@@ -185,7 +186,10 @@ class ProxiesBox : public Ui::BoxContent {
|
||||
public:
|
||||
using View = ProxiesBoxController::ItemView;
|
||||
|
||||
ProxiesBox(QWidget*, not_null<ProxiesBoxController*> controller);
|
||||
ProxiesBox(
|
||||
QWidget*,
|
||||
not_null<ProxiesBoxController*> controller,
|
||||
Core::SettingsProxy &settings);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
@@ -200,6 +204,7 @@ private:
|
||||
void refreshProxyForCalls();
|
||||
|
||||
not_null<ProxiesBoxController*> _controller;
|
||||
Core::SettingsProxy &_settings;
|
||||
QPointer<Ui::Checkbox> _tryIPv6;
|
||||
std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
|
||||
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
|
||||
@@ -565,8 +570,10 @@ void ProxyRow::showMenu() {
|
||||
|
||||
ProxiesBox::ProxiesBox(
|
||||
QWidget*,
|
||||
not_null<ProxiesBoxController*> controller)
|
||||
not_null<ProxiesBoxController*> controller,
|
||||
Core::SettingsProxy &settings)
|
||||
: _controller(controller)
|
||||
, _settings(settings)
|
||||
, _initialWrap(this) {
|
||||
_controller->views(
|
||||
) | rpl::start_with_next([=](View &&view) {
|
||||
@@ -590,11 +597,11 @@ void ProxiesBox::setupContent() {
|
||||
object_ptr<Ui::Checkbox>(
|
||||
inner,
|
||||
tr::lng_connection_try_ipv6(tr::now),
|
||||
Global::TryIPv6()),
|
||||
_settings.tryIPv6()),
|
||||
st::proxyTryIPv6Padding);
|
||||
_proxySettings
|
||||
= std::make_shared<Ui::RadioenumGroup<ProxyData::Settings>>(
|
||||
Global::ProxySettings());
|
||||
_settings.settings());
|
||||
inner->add(
|
||||
object_ptr<Ui::Radioenum<ProxyData::Settings>>(
|
||||
inner,
|
||||
@@ -622,7 +629,7 @@ void ProxiesBox::setupContent() {
|
||||
object_ptr<Ui::Checkbox>(
|
||||
inner,
|
||||
tr::lng_proxy_use_for_calls(tr::now),
|
||||
Global::UseProxyForCalls()),
|
||||
_settings.useProxyForCalls()),
|
||||
style::margins(
|
||||
0,
|
||||
st::proxyUsePadding.top(),
|
||||
@@ -651,7 +658,7 @@ void ProxiesBox::setupContent() {
|
||||
|
||||
_proxySettings->setChangedCallback([=](ProxyData::Settings value) {
|
||||
if (!_controller->setProxySettings(value)) {
|
||||
_proxySettings->setValue(Global::ProxySettings());
|
||||
_proxySettings->setValue(_settings.settings());
|
||||
addNewProxy();
|
||||
}
|
||||
refreshProxyForCalls();
|
||||
@@ -1050,20 +1057,22 @@ void ProxyBox::addLabel(
|
||||
|
||||
ProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)
|
||||
: _account(account)
|
||||
, _settings(Core::App().settings().proxy())
|
||||
, _saveTimer([] { Local::writeSettings(); }) {
|
||||
_list = ranges::views::all(
|
||||
Global::ProxiesList()
|
||||
_settings.list()
|
||||
) | ranges::views::transform([&](const ProxyData &proxy) {
|
||||
return Item{ ++_idCounter, proxy };
|
||||
}) | ranges::to_vector;
|
||||
|
||||
subscribe(Global::RefConnectionTypeChanged(), [=] {
|
||||
_proxySettingsChanges.fire_copy(Global::ProxySettings());
|
||||
const auto i = findByProxy(Global::SelectedProxy());
|
||||
_settings.connectionTypeChanges(
|
||||
) | rpl::start_with_next([=] {
|
||||
_proxySettingsChanges.fire_copy(_settings.settings());
|
||||
const auto i = findByProxy(_settings.selected());
|
||||
if (i != end(_list)) {
|
||||
updateView(*i);
|
||||
}
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
for (auto &item : _list) {
|
||||
refreshChecker(item);
|
||||
@@ -1086,17 +1095,32 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
||||
proxy.password = fields.value(qsl("secret"));
|
||||
}
|
||||
if (proxy) {
|
||||
const auto displayed = "https://" + server + "/";
|
||||
const auto parsed = QUrl::fromUserInput(displayed);
|
||||
const auto displayUrl = !UrlClickHandler::IsSuspicious(displayed)
|
||||
? displayed
|
||||
: parsed.isValid()
|
||||
? QString::fromUtf8(parsed.toEncoded())
|
||||
: UrlClickHandler::ShowEncoded(displayed);
|
||||
const auto displayServer = QString(
|
||||
displayUrl
|
||||
).replace(
|
||||
QRegularExpression(
|
||||
"^https://",
|
||||
QRegularExpression::CaseInsensitiveOption),
|
||||
QString()
|
||||
).replace(QRegularExpression("/$"), QString());
|
||||
const auto text = tr::lng_sure_enable_socks(
|
||||
tr::now,
|
||||
lt_server,
|
||||
server,
|
||||
displayServer,
|
||||
lt_port,
|
||||
QString::number(port))
|
||||
+ (proxy.type == Type::Mtproto
|
||||
? "\n\n" + tr::lng_proxy_sponsor_warning(tr::now)
|
||||
: QString());
|
||||
auto callback = [=](Fn<void()> &&close) {
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = Core::App().settings().proxy().list();
|
||||
if (!ranges::contains(proxies, proxy)) {
|
||||
proxies.push_back(proxy);
|
||||
}
|
||||
@@ -1123,7 +1147,7 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
||||
auto ProxiesBoxController::proxySettingsValue() const
|
||||
-> rpl::producer<ProxyData::Settings> {
|
||||
return _proxySettingsChanges.events_starting_with_copy(
|
||||
Global::ProxySettings()
|
||||
_settings.settings()
|
||||
) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
@@ -1134,6 +1158,7 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
||||
: Variants::Tcp;
|
||||
const auto mtproto = &_account->mtp();
|
||||
const auto dcId = mtproto->mainDcId();
|
||||
const auto forFiles = false;
|
||||
|
||||
item.state = ItemState::Checking;
|
||||
const auto setup = [&](Checker &checker, const bytes::vector &secret) {
|
||||
@@ -1152,7 +1177,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
||||
item.data.host,
|
||||
item.data.port,
|
||||
secret,
|
||||
dcId);
|
||||
dcId,
|
||||
forFiles);
|
||||
item.checkerv6 = nullptr;
|
||||
} else {
|
||||
const auto options = mtproto->dcOptions().lookup(
|
||||
@@ -1164,7 +1190,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
||||
Variants::Address address) {
|
||||
const auto &list = options.data[address][type];
|
||||
if (list.empty()
|
||||
|| (address == Variants::IPv6 && !Global::TryIPv6())) {
|
||||
|| ((address == Variants::IPv6)
|
||||
&& !Core::App().settings().proxy().tryIPv6())) {
|
||||
checker = nullptr;
|
||||
return;
|
||||
}
|
||||
@@ -1174,7 +1201,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
||||
QString::fromStdString(endpoint.ip),
|
||||
endpoint.port,
|
||||
endpoint.secret,
|
||||
dcId);
|
||||
dcId,
|
||||
forFiles);
|
||||
};
|
||||
connect(item.checker, Variants::IPv4);
|
||||
connect(item.checkerv6, Variants::IPv6);
|
||||
@@ -1225,7 +1253,7 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::CreateOwningBox(
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> ProxiesBoxController::create() {
|
||||
auto result = Box<ProxiesBox>(this);
|
||||
auto result = Box<ProxiesBox>(this, _settings);
|
||||
for (const auto &item : _list) {
|
||||
updateView(item);
|
||||
}
|
||||
@@ -1263,14 +1291,13 @@ void ProxiesBoxController::shareItem(int id) {
|
||||
|
||||
void ProxiesBoxController::applyItem(int id) {
|
||||
auto item = findById(id);
|
||||
if ((Global::ProxySettings() == ProxyData::Settings::Enabled)
|
||||
&& Global::SelectedProxy() == item->data) {
|
||||
if (_settings.isEnabled() && (_settings.selected() == item->data)) {
|
||||
return;
|
||||
} else if (item->deleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto j = findByProxy(Global::SelectedProxy());
|
||||
auto j = findByProxy(_settings.selected());
|
||||
|
||||
Core::App().setCurrentProxy(
|
||||
item->data,
|
||||
@@ -1288,12 +1315,13 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
||||
item->deleted = deleted;
|
||||
|
||||
if (deleted) {
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = _settings.list();
|
||||
proxies.erase(ranges::remove(proxies, item->data), end(proxies));
|
||||
|
||||
if (item->data == Global::SelectedProxy()) {
|
||||
_lastSelectedProxy = base::take(Global::RefSelectedProxy());
|
||||
if (Global::ProxySettings() == ProxyData::Settings::Enabled) {
|
||||
if (item->data == _settings.selected()) {
|
||||
_lastSelectedProxy = _settings.selected();
|
||||
_settings.setSelected(MTP::ProxyData());
|
||||
if (_settings.isEnabled()) {
|
||||
_lastSelectedProxyUsed = true;
|
||||
Core::App().setCurrentProxy(
|
||||
ProxyData(),
|
||||
@@ -1304,7 +1332,7 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = _settings.list();
|
||||
if (ranges::find(proxies, item->data) == end(proxies)) {
|
||||
auto insertBefore = item + 1;
|
||||
while (insertBefore != end(_list) && insertBefore->deleted) {
|
||||
@@ -1316,15 +1344,15 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
||||
proxies.insert(insertBeforeIt, item->data);
|
||||
}
|
||||
|
||||
if (!Global::SelectedProxy() && _lastSelectedProxy == item->data) {
|
||||
Assert(Global::ProxySettings() != ProxyData::Settings::Enabled);
|
||||
if (!_settings.selected() && _lastSelectedProxy == item->data) {
|
||||
Assert(!_settings.isEnabled());
|
||||
|
||||
if (base::take(_lastSelectedProxyUsed)) {
|
||||
Core::App().setCurrentProxy(
|
||||
base::take(_lastSelectedProxy),
|
||||
ProxyData::Settings::Enabled);
|
||||
} else {
|
||||
Global::SetSelectedProxy(base::take(_lastSelectedProxy));
|
||||
_settings.setSelected(base::take(_lastSelectedProxy));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1352,7 +1380,7 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {
|
||||
void ProxiesBoxController::replaceItemWith(
|
||||
std::vector<Item>::iterator which,
|
||||
std::vector<Item>::iterator with) {
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = _settings.list();
|
||||
proxies.erase(ranges::remove(proxies, which->data), end(proxies));
|
||||
|
||||
_views.fire({ which->id });
|
||||
@@ -1372,7 +1400,7 @@ void ProxiesBoxController::replaceItemValue(
|
||||
restoreItem(which->id);
|
||||
}
|
||||
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = _settings.list();
|
||||
const auto i = ranges::find(proxies, which->data);
|
||||
Assert(i != end(proxies));
|
||||
*i = proxy;
|
||||
@@ -1403,7 +1431,7 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::addNewItemBox() {
|
||||
}
|
||||
|
||||
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
|
||||
auto &proxies = Global::RefProxiesList();
|
||||
auto &proxies = _settings.list();
|
||||
proxies.push_back(proxy);
|
||||
|
||||
_list.push_back({ ++_idCounter, proxy });
|
||||
@@ -1412,43 +1440,42 @@ void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
|
||||
}
|
||||
|
||||
bool ProxiesBoxController::setProxySettings(ProxyData::Settings value) {
|
||||
if (Global::ProxySettings() == value) {
|
||||
if (_settings.settings() == value) {
|
||||
return true;
|
||||
} else if (value == ProxyData::Settings::Enabled) {
|
||||
if (Global::ProxiesList().empty()) {
|
||||
if (_settings.list().empty()) {
|
||||
return false;
|
||||
} else if (!Global::SelectedProxy()) {
|
||||
Global::SetSelectedProxy(Global::ProxiesList().back());
|
||||
auto j = findByProxy(Global::SelectedProxy());
|
||||
} else if (!_settings.selected()) {
|
||||
_settings.setSelected(_settings.list().back());
|
||||
auto j = findByProxy(_settings.selected());
|
||||
if (j != end(_list)) {
|
||||
updateView(*j);
|
||||
}
|
||||
}
|
||||
}
|
||||
Core::App().setCurrentProxy(Global::SelectedProxy(), value);
|
||||
Core::App().setCurrentProxy(_settings.selected(), value);
|
||||
saveDelayed();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProxiesBoxController::setProxyForCalls(bool enabled) {
|
||||
if (Global::UseProxyForCalls() == enabled) {
|
||||
if (_settings.useProxyForCalls() == enabled) {
|
||||
return;
|
||||
}
|
||||
Global::SetUseProxyForCalls(enabled);
|
||||
if ((Global::ProxySettings() == ProxyData::Settings::Enabled)
|
||||
&& Global::SelectedProxy().supportsCalls()) {
|
||||
Global::RefConnectionTypeChanged().notify();
|
||||
_settings.setUseProxyForCalls(enabled);
|
||||
if (_settings.isEnabled() && _settings.selected().supportsCalls()) {
|
||||
_settings.connectionTypeChangesNotify();
|
||||
}
|
||||
saveDelayed();
|
||||
}
|
||||
|
||||
void ProxiesBoxController::setTryIPv6(bool enabled) {
|
||||
if (Global::TryIPv6() == enabled) {
|
||||
if (Core::App().settings().proxy().tryIPv6() == enabled) {
|
||||
return;
|
||||
}
|
||||
Global::SetTryIPv6(enabled);
|
||||
Core::App().settings().proxy().setTryIPv6(enabled);
|
||||
_account->mtp().restart();
|
||||
Global::RefConnectionTypeChanged().notify();
|
||||
_settings.connectionTypeChangesNotify();
|
||||
saveDelayed();
|
||||
}
|
||||
|
||||
@@ -1461,7 +1488,7 @@ auto ProxiesBoxController::views() const -> rpl::producer<ItemView> {
|
||||
}
|
||||
|
||||
void ProxiesBoxController::updateView(const Item &item) {
|
||||
const auto selected = (Global::SelectedProxy() == item.data);
|
||||
const auto selected = (_settings.selected() == item.data);
|
||||
const auto deleted = item.deleted;
|
||||
const auto type = [&] {
|
||||
switch (item.data.type) {
|
||||
@@ -1475,8 +1502,7 @@ void ProxiesBoxController::updateView(const Item &item) {
|
||||
Unexpected("Proxy type in ProxiesBoxController::updateView.");
|
||||
}();
|
||||
const auto state = [&] {
|
||||
if (!selected
|
||||
|| (Global::ProxySettings() != ProxyData::Settings::Enabled)) {
|
||||
if (!selected || !_settings.isEnabled()) {
|
||||
return item.state;
|
||||
} else if (_account->mtp().dcstate() == MTP::ConnectedState) {
|
||||
return ItemState::Online;
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "mtproto/connection_abstract.h"
|
||||
#include "mtproto/mtproto_proxy_data.h"
|
||||
|
||||
@@ -28,7 +29,7 @@ namespace Main {
|
||||
class Account;
|
||||
} // namespace Main
|
||||
|
||||
class ProxiesBoxController : public base::Subscriber {
|
||||
class ProxiesBoxController {
|
||||
public:
|
||||
using ProxyData = MTP::ProxyData;
|
||||
using Type = ProxyData::Type;
|
||||
@@ -110,6 +111,7 @@ private:
|
||||
void addNewItem(const ProxyData &proxy);
|
||||
|
||||
const not_null<Main::Account*> _account;
|
||||
Core::SettingsProxy &_settings;
|
||||
int _idCounter = 0;
|
||||
std::vector<Item> _list;
|
||||
rpl::event_stream<ItemView> _views;
|
||||
@@ -119,4 +121,6 @@ private:
|
||||
ProxyData _lastSelectedProxy;
|
||||
bool _lastSelectedProxyUsed = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
@@ -1085,7 +1085,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
send({ .silent = true });
|
||||
};
|
||||
const auto sendScheduled = [=] {
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
HistoryView::PrepareScheduleBox(
|
||||
this,
|
||||
SendMenu::Type::Scheduled,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,8 +27,8 @@ public:
|
||||
return _y;
|
||||
}
|
||||
|
||||
base::Observable<void> &changed() {
|
||||
return _changed;
|
||||
rpl::producer<> changed() const {
|
||||
return _changed.events();
|
||||
}
|
||||
void setHSB(HSB hsb);
|
||||
void setRGB(int red, int green, int blue);
|
||||
@@ -61,7 +61,7 @@ private:
|
||||
float64 _y = 0.;
|
||||
|
||||
bool _choosing = false;
|
||||
base::Observable<void> _changed;
|
||||
rpl::event_stream<> _changed;
|
||||
|
||||
};
|
||||
|
||||
@@ -234,7 +234,7 @@ void EditColorBox::Picker::updateCurrentPoint(QPoint localPosition) {
|
||||
_x = x;
|
||||
_y = y;
|
||||
update();
|
||||
_changed.notify();
|
||||
_changed.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,8 +284,8 @@ public:
|
||||
};
|
||||
Slider(QWidget *parent, Direction direction, Type type, QColor color);
|
||||
|
||||
base::Observable<void> &changed() {
|
||||
return _changed;
|
||||
rpl::producer<> changed() const {
|
||||
return _changed.events();
|
||||
}
|
||||
float64 value() const {
|
||||
return _value;
|
||||
@@ -335,7 +335,7 @@ private:
|
||||
QBrush _transparent;
|
||||
|
||||
bool _choosing = false;
|
||||
base::Observable<void> _changed;
|
||||
rpl::event_stream<> _changed;
|
||||
|
||||
};
|
||||
|
||||
@@ -349,7 +349,9 @@ EditColorBox::Slider::Slider(
|
||||
, _type(type)
|
||||
, _color(color.red(), color.green(), color.blue())
|
||||
, _value(valueFromColor(color))
|
||||
, _transparent((_type == Type::Opacity) ? style::transparentPlaceholderBrush() : QBrush()) {
|
||||
, _transparent((_type == Type::Opacity)
|
||||
? style::TransparentPlaceholder()
|
||||
: QBrush()) {
|
||||
prepareMinSize();
|
||||
}
|
||||
|
||||
@@ -538,7 +540,7 @@ void EditColorBox::Slider::updateCurrentPoint(QPoint localPosition) {
|
||||
if (_value != value) {
|
||||
_value = value;
|
||||
update();
|
||||
_changed.notify();
|
||||
_changed.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +760,7 @@ EditColorBox::EditColorBox(
|
||||
, _greenField(this, st::colorValueInput, "G", 255)
|
||||
, _blueField(this, st::colorValueInput, "B", 255)
|
||||
, _result(this, st::colorResultInput)
|
||||
, _transparent(style::transparentPlaceholderBrush())
|
||||
, _transparent(style::TransparentPlaceholder())
|
||||
, _current(current)
|
||||
, _new(current) {
|
||||
if (_mode == Mode::RGBA) {
|
||||
@@ -824,16 +826,14 @@ void EditColorBox::prepare() {
|
||||
auto height = st::colorEditSkip + st::colorPickerSize + st::colorEditSkip + st::colorSliderWidth + st::colorEditSkip;
|
||||
setDimensions(st::colorEditWidth, height);
|
||||
|
||||
subscribe(_picker->changed(), [=] { updateFromControls(); });
|
||||
if (_hueSlider) {
|
||||
subscribe(_hueSlider->changed(), [=] { updateFromControls(); });
|
||||
}
|
||||
if (_opacitySlider) {
|
||||
subscribe(_opacitySlider->changed(), [=] { updateFromControls(); });
|
||||
}
|
||||
if (_lightnessSlider) {
|
||||
subscribe(_lightnessSlider->changed(), [=] { updateFromControls(); });
|
||||
}
|
||||
rpl::merge(
|
||||
_picker->changed(),
|
||||
(_hueSlider ? _hueSlider->changed() : rpl::never<>()),
|
||||
(_opacitySlider ? _opacitySlider->changed() : rpl::never<>()),
|
||||
(_lightnessSlider ? _lightnessSlider->changed() : rpl::never<>())
|
||||
) | rpl::start_with_next([=] {
|
||||
updateFromControls();
|
||||
}, lifetime());
|
||||
|
||||
boxClosing() | rpl::start_with_next([=] {
|
||||
if (_cancelCallback) {
|
||||
|
||||
@@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "boxes/abstract_box.h"
|
||||
|
||||
class EditColorBox : public Ui::BoxContent, private base::Subscriber {
|
||||
class EditColorBox : public Ui::BoxContent {
|
||||
public:
|
||||
enum class Mode {
|
||||
RGBA,
|
||||
|
||||
@@ -165,7 +165,7 @@ void EditPrivacyBox::editExceptions(
|
||||
}));
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
};
|
||||
Ui::show(
|
||||
_window->show(
|
||||
Box<PeerListBox>(std::move(controller), std::move(initBox)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
@@ -1166,15 +1166,19 @@ base::binary_guard LanguageBox::Show() {
|
||||
if (manager.languageList().empty()) {
|
||||
auto guard = std::make_shared<base::binary_guard>(
|
||||
result.make_guard());
|
||||
auto alive = std::make_shared<std::unique_ptr<base::Subscription>>(
|
||||
std::make_unique<base::Subscription>());
|
||||
**alive = manager.languageListChanged().add_subscription([=] {
|
||||
auto lifetime = std::make_shared<rpl::lifetime>();
|
||||
manager.languageListChanged(
|
||||
) | rpl::take(
|
||||
1
|
||||
) | rpl::start_with_next([=]() mutable {
|
||||
const auto show = guard->alive();
|
||||
*alive = nullptr;
|
||||
if (lifetime) {
|
||||
base::take(lifetime)->destroy();
|
||||
}
|
||||
if (show) {
|
||||
Ui::show(Box<LanguageBox>());
|
||||
}
|
||||
});
|
||||
}, *lifetime);
|
||||
} else {
|
||||
Ui::show(Box<LanguageBox>());
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "passport/passport_encryption.h"
|
||||
#include "passport/passport_panel_edit_contact.h"
|
||||
#include "settings/settings_privacy_security.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_boxes.h"
|
||||
@@ -119,7 +118,12 @@ PasscodeBox::PasscodeBox(
|
||||
, _turningOff(turningOff)
|
||||
, _about(st::boxWidth - st::boxPadding.left() * 1.5)
|
||||
, _oldPasscode(this, st::defaultInputField, tr::lng_passcode_enter_old())
|
||||
, _newPasscode(this, st::defaultInputField, Global::LocalPasscode() ? tr::lng_passcode_enter_new() : tr::lng_passcode_enter_first())
|
||||
, _newPasscode(
|
||||
this,
|
||||
st::defaultInputField,
|
||||
session->domain().local().hasLocalPasscode()
|
||||
? tr::lng_passcode_enter_new()
|
||||
: tr::lng_passcode_enter_first())
|
||||
, _reenterPasscode(this, st::defaultInputField, tr::lng_passcode_confirm_new())
|
||||
, _passwordHint(this, st::defaultInputField, tr::lng_cloud_password_hint())
|
||||
, _recoverEmail(this, st::defaultInputField, tr::lng_cloud_password_email())
|
||||
@@ -164,7 +168,9 @@ rpl::producer<> PasscodeBox::clearUnconfirmedPassword() const {
|
||||
}
|
||||
|
||||
bool PasscodeBox::currentlyHave() const {
|
||||
return _cloudPwd ? (!!_cloudFields.curRequest) : Global::LocalPasscode();
|
||||
return _cloudPwd
|
||||
? (!!_cloudFields.curRequest)
|
||||
: _session->domain().local().hasLocalPasscode();
|
||||
}
|
||||
|
||||
bool PasscodeBox::onlyCheckCurrent() const {
|
||||
@@ -520,7 +526,7 @@ void PasscodeBox::save(bool force) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Core::App().domain().local().checkPasscode(old.toUtf8())) {
|
||||
if (_session->domain().local().checkPasscode(old.toUtf8())) {
|
||||
cSetPasscodeBadTries(0);
|
||||
if (_turningOff) pwd = conf = QString();
|
||||
} else {
|
||||
@@ -588,7 +594,7 @@ void PasscodeBox::save(bool force) {
|
||||
closeReplacedBy();
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
cSetPasscodeBadTries(0);
|
||||
Core::App().domain().local().setPasscode(pwd.toUtf8());
|
||||
_session->domain().local().setPasscode(pwd.toUtf8());
|
||||
Core::App().localPasscodeChanged();
|
||||
if (weak) {
|
||||
closeBox();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -201,8 +201,10 @@ void SaveSlowmodeSeconds(
|
||||
void ShowEditPermissions(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer) {
|
||||
const auto box = Ui::show(
|
||||
Box<EditPeerPermissionsBox>(navigation, peer),
|
||||
auto content = Box<EditPeerPermissionsBox>(navigation, peer);
|
||||
const auto box = QPointer<EditPeerPermissionsBox>(content.data());
|
||||
navigation->parentController()->show(
|
||||
std::move(content),
|
||||
Ui::LayerOption::KeepOther);
|
||||
const auto saving = box->lifetime().make_state<int>(0);
|
||||
const auto save = [=](
|
||||
@@ -244,8 +246,10 @@ void ShowEditPermissions(
|
||||
void ShowEditInviteLinks(
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
not_null<PeerData*> peer) {
|
||||
const auto box = Ui::show(
|
||||
Box<EditPeerPermissionsBox>(navigation, peer),
|
||||
auto content = Box<EditPeerPermissionsBox>(navigation, peer);
|
||||
const auto box = QPointer<EditPeerPermissionsBox>(content.data());
|
||||
navigation->parentController()->show(
|
||||
std::move(content),
|
||||
Ui::LayerOption::KeepOther);
|
||||
const auto saving = box->lifetime().make_state<int>(0);
|
||||
const auto save = [=](
|
||||
@@ -612,7 +616,7 @@ object_ptr<Ui::RpWidget> Controller::createStickersEdit() {
|
||||
tr::lng_group_stickers_add(tr::now),
|
||||
st::editPeerInviteLinkButton)
|
||||
)->addClickHandler([=] {
|
||||
Ui::show(
|
||||
_navigation->parentController()->show(
|
||||
Box<StickersBox>(_navigation->parentController(), channel),
|
||||
Ui::LayerOption::KeepOther);
|
||||
});
|
||||
@@ -649,7 +653,7 @@ void Controller::showEditPeerTypeBox(
|
||||
_usernameSavedValue = publicLink;
|
||||
refreshHistoryVisibility();
|
||||
});
|
||||
Ui::show(
|
||||
_navigation->parentController()->show(
|
||||
Box<EditPeerTypeBox>(
|
||||
_peer,
|
||||
_channelHasLocationOriginalValue,
|
||||
@@ -681,7 +685,7 @@ void Controller::showEditLinkedChatBox() {
|
||||
|| channel->canEditPreHistoryHidden()));
|
||||
|
||||
if (const auto chat = *_linkedChatSavedValue) {
|
||||
*box = Ui::show(
|
||||
*box = _navigation->parentController()->show(
|
||||
EditLinkedChatBox(
|
||||
_navigation,
|
||||
channel,
|
||||
@@ -709,7 +713,7 @@ void Controller::showEditLinkedChatBox() {
|
||||
for (const auto &item : list) {
|
||||
chats.emplace_back(_peer->owner().processChat(item));
|
||||
}
|
||||
*box = Ui::show(
|
||||
*box = _navigation->parentController()->show(
|
||||
EditLinkedChatBox(
|
||||
_navigation,
|
||||
channel,
|
||||
@@ -858,7 +862,7 @@ void Controller::fillHistoryVisibilityButton() {
|
||||
_historyVisibilitySavedValue = checked;
|
||||
});
|
||||
const auto buttonCallback = [=] {
|
||||
Ui::show(
|
||||
_navigation->parentController()->show(
|
||||
Box<EditPeerHistoryVisibilityBox>(
|
||||
_peer,
|
||||
boxCallback,
|
||||
@@ -1023,9 +1027,15 @@ void Controller::fillManageSection() {
|
||||
wrap->entity(),
|
||||
tr::lng_manage_peer_invite_links(),
|
||||
rpl::duplicate(count) | ToPositiveNumberString(),
|
||||
[=] { Ui::show(
|
||||
Box(ManageInviteLinksBox, _peer, _peer->session().user(), 0, 0),
|
||||
Ui::LayerOption::KeepOther);
|
||||
[=] {
|
||||
_navigation->parentController()->show(
|
||||
Box(
|
||||
ManageInviteLinksBox,
|
||||
_peer,
|
||||
_peer->session().user(),
|
||||
0,
|
||||
0),
|
||||
Ui::LayerOption::KeepOther);
|
||||
},
|
||||
st::infoIconInviteLinks);
|
||||
|
||||
@@ -1520,7 +1530,7 @@ void Controller::deleteWithConfirmation() {
|
||||
const auto deleteCallback = crl::guard(this, [=] {
|
||||
deleteChannel();
|
||||
});
|
||||
Ui::show(
|
||||
_navigation->parentController()->show(
|
||||
Box<ConfirmBox>(
|
||||
text,
|
||||
tr::lng_box_delete(tr::now),
|
||||
|
||||
@@ -133,8 +133,8 @@ QImage QrExact(const Qr::Data &data, int pixel, QColor color) {
|
||||
skip,
|
||||
skip,
|
||||
Intro::details::TelegramLogoImage().scaled(
|
||||
logoSize,
|
||||
logoSize,
|
||||
logoSize * cIntRetinaFactor(),
|
||||
logoSize * cIntRetinaFactor(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1002,7 +1002,7 @@ void SendFilesBox::sendScheduled() {
|
||||
? SendMenu::Type::ScheduledToUser
|
||||
: _sendMenuType;
|
||||
const auto callback = [=](Api::SendOptions options) { send(options); };
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
HistoryView::PrepareScheduleBox(this, type, callback),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
@@ -63,12 +63,14 @@ public:
|
||||
bool loaded() const;
|
||||
bool notInstalled() const;
|
||||
bool official() const;
|
||||
rpl::producer<TextWithEntities> title() const;
|
||||
QString shortName() const;
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> title() const;
|
||||
[[nodiscard]] QString shortName() const;
|
||||
|
||||
void install();
|
||||
rpl::producer<uint64> setInstalled() const;
|
||||
rpl::producer<> updateControls() const;
|
||||
[[nodiscard]] rpl::producer<uint64> setInstalled() const;
|
||||
[[nodiscard]] rpl::producer<> updateControls() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<Error> errors() const;
|
||||
|
||||
~Inner();
|
||||
|
||||
@@ -137,6 +139,7 @@ private:
|
||||
|
||||
rpl::event_stream<uint64> _setInstalled;
|
||||
rpl::event_stream<> _updateControls;
|
||||
rpl::event_stream<Error> _errors;
|
||||
|
||||
};
|
||||
|
||||
@@ -153,7 +156,7 @@ QPointer<Ui::BoxContent> StickerSetBox::Show(
|
||||
not_null<DocumentData*> document) {
|
||||
if (const auto sticker = document->sticker()) {
|
||||
if (sticker->set.type() != mtpc_inputStickerSetEmpty) {
|
||||
return Ui::show(
|
||||
return controller->show(
|
||||
Box<StickerSetBox>(controller, sticker->set),
|
||||
Ui::LayerOption::KeepOther).data();
|
||||
}
|
||||
@@ -186,6 +189,11 @@ void StickerSetBox::prepare() {
|
||||
_controller->session().api().stickerSetInstalled(setId);
|
||||
closeBox();
|
||||
}, lifetime());
|
||||
|
||||
_inner->errors(
|
||||
) | rpl::start_with_next([=](Error error) {
|
||||
handleError(error);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void StickerSetBox::addStickers() {
|
||||
@@ -198,6 +206,52 @@ void StickerSetBox::copyStickersLink() {
|
||||
QGuiApplication::clipboard()->setText(url);
|
||||
}
|
||||
|
||||
void StickerSetBox::handleError(Error error) {
|
||||
const auto guard = gsl::finally(crl::guard(this, [=] {
|
||||
closeBox();
|
||||
}));
|
||||
|
||||
switch (error) {
|
||||
case Error::NotFound:
|
||||
_controller->show(
|
||||
Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
break;
|
||||
default: Unexpected("Error in StickerSetBox::handleError.");
|
||||
}
|
||||
}
|
||||
|
||||
void StickerSetBox::archiveStickers() {
|
||||
const auto weak = base::make_weak(_controller.get());
|
||||
const auto setId = _set.c_inputStickerSetID().vid().v;
|
||||
_controller->session().api().request(MTPmessages_InstallStickerSet(
|
||||
_set,
|
||||
MTP_boolTrue()
|
||||
)).done([=](const MTPmessages_StickerSetInstallResult &result) {
|
||||
const auto controller = weak.get();
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
if (result.type() == mtpc_messages_stickerSetInstallResultSuccess) {
|
||||
Ui::Toast::Show(tr::lng_stickers_has_been_archived(tr::now));
|
||||
|
||||
const auto &session = controller->session();
|
||||
auto &order = session.data().stickers().setsOrderRef();
|
||||
const auto index = order.indexOf(setId);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
order.removeAt(index);
|
||||
|
||||
session.local().writeInstalledStickers();
|
||||
session.local().writeArchivedStickers();
|
||||
|
||||
session.data().stickers().notifyUpdated();
|
||||
}
|
||||
}).fail([](const MTP::Error &error) {
|
||||
Ui::Toast::Show(Lang::Hard::ServerError());
|
||||
}).send();
|
||||
}
|
||||
|
||||
void StickerSetBox::updateTitleAndButtons() {
|
||||
setTitle(_inner->title());
|
||||
updateButtons();
|
||||
@@ -237,6 +291,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(); });
|
||||
@@ -274,7 +346,7 @@ StickerSetBox::Inner::Inner(
|
||||
gotSet(result);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_loaded = true;
|
||||
Ui::show(Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
_errors.fire(Error::NotFound);
|
||||
}).send();
|
||||
|
||||
_controller->session().api().updateStickers();
|
||||
@@ -369,7 +441,7 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) {
|
||||
});
|
||||
|
||||
if (_pack.isEmpty()) {
|
||||
Ui::show(Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
_errors.fire(Error::NotFound);
|
||||
return;
|
||||
} else {
|
||||
int32 rows = _pack.size() / kStickersPanelPerRow + ((_pack.size() % kStickersPanelPerRow) ? 1 : 0);
|
||||
@@ -389,6 +461,10 @@ rpl::producer<> StickerSetBox::Inner::updateControls() const {
|
||||
return _updateControls.events();
|
||||
}
|
||||
|
||||
rpl::producer<StickerSetBox::Error> StickerSetBox::Inner::errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
void StickerSetBox::Inner::installDone(
|
||||
const MTPmessages_StickerSetInstallResult &result) {
|
||||
auto &sets = _controller->session().data().stickers().setsRef();
|
||||
@@ -745,7 +821,7 @@ QString StickerSetBox::Inner::shortName() const {
|
||||
|
||||
void StickerSetBox::Inner::install() {
|
||||
if (isMasksSet()) {
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
Box<InformBox>(tr::lng_stickers_masks_pack(tr::now)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
return;
|
||||
@@ -758,7 +834,7 @@ void StickerSetBox::Inner::install() {
|
||||
)).done([=](const MTPmessages_StickerSetInstallResult &result) {
|
||||
installDone(result);
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
Ui::show(Box<InformBox>(tr::lng_stickers_not_found(tr::now)));
|
||||
_errors.fire(Error::NotFound);
|
||||
}).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,16 @@ protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
enum class Error {
|
||||
NotFound,
|
||||
};
|
||||
|
||||
void updateTitleAndButtons();
|
||||
void updateButtons();
|
||||
void addStickers();
|
||||
void copyStickersLink();
|
||||
void archiveStickers();
|
||||
void handleError(Error error);
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
MTPInputStickerSet _set;
|
||||
|
||||
@@ -68,9 +68,7 @@ private:
|
||||
};
|
||||
|
||||
// This class is hold in header because it requires Qt preprocessing.
|
||||
class StickersBox::Inner
|
||||
: public Ui::RpWidget
|
||||
, private base::Subscriber {
|
||||
class StickersBox::Inner : public Ui::RpWidget {
|
||||
public:
|
||||
using Section = StickersBox::Section;
|
||||
|
||||
@@ -85,7 +83,9 @@ public:
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
base::Observable<int> scrollToY;
|
||||
rpl::producer<int> scrollsToY() const {
|
||||
return _scrollsToY.events();
|
||||
}
|
||||
void setInnerFocus();
|
||||
|
||||
void saveGroupSet();
|
||||
@@ -276,6 +276,8 @@ private:
|
||||
int _above = -1;
|
||||
rpl::event_stream<int> _draggingScrollDelta;
|
||||
|
||||
rpl::event_stream<int> _scrollsToY;
|
||||
|
||||
int _minHeight = 0;
|
||||
|
||||
int _scrollbar = 0;
|
||||
@@ -387,9 +389,10 @@ StickersBox::StickersBox(
|
||||
, _section(Section::Installed)
|
||||
, _installed(0, this, controller, megagroup)
|
||||
, _megagroupSet(megagroup) {
|
||||
subscribe(_installed.widget()->scrollToY, [=](int y) {
|
||||
_installed.widget()->scrollsToY(
|
||||
) | rpl::start_with_next([=](int y) {
|
||||
onScrollToY(y);
|
||||
});
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
StickersBox::StickersBox(
|
||||
@@ -1589,7 +1592,7 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
}();
|
||||
const auto showSetByRow = [&](const Row &row) {
|
||||
setSelected(SelectedRow());
|
||||
Ui::show(
|
||||
_controller->show(
|
||||
Box<StickerSetBox>(_controller, row.set->mtpInput()),
|
||||
Ui::LayerOption::KeepOther);
|
||||
};
|
||||
@@ -1898,7 +1901,7 @@ void StickersBox::Inner::rebuild() {
|
||||
void StickersBox::Inner::setMegagroupSelectedSet(const MTPInputStickerSet &set) {
|
||||
_megagroupSetInput = set;
|
||||
rebuild();
|
||||
scrollToY.notify(0, true);
|
||||
_scrollsToY.fire(0);
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
|
||||
@@ -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: 108px;
|
||||
groupCallControlsBackMargin: margins(10px, 0px, 10px, 0px);
|
||||
groupCallControlsBackRadius: 12px;
|
||||
groupCallMuteBottomSkip: 116px;
|
||||
|
||||
groupCallMembersMargin: margins(16px, 16px, 16px, 60px);
|
||||
groupCallMembersTopSkip: 6px;
|
||||
groupCallMembersBottomSkip: 80px;
|
||||
groupCallMembersShadowHeight: 160px;
|
||||
groupCallMembersFadeSkip: 10px;
|
||||
groupCallMembersFadeHeight: 100px;
|
||||
|
||||
groupCallTopBarUserpics: GroupCallUserpics {
|
||||
size: 28px;
|
||||
@@ -780,17 +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,130 @@ groupCallStartsInTop: 10px;
|
||||
groupCallStartsWhenTop: 160px;
|
||||
groupCallCountdownFont: font(64px semibold);
|
||||
groupCallCountdownTop: 52px;
|
||||
|
||||
desktopCaptureMargins: margins(12px, 8px, 12px, 6px);
|
||||
desktopCaptureSourceSize: size(235px, 165px);
|
||||
desktopCaptureSourceSkips: size(2px, 10px);
|
||||
desktopCaptureSourceTitle: WindowTitle(groupCallTitle) {
|
||||
bg: groupCallMembersBgOver;
|
||||
bgActive: groupCallMembersBgOver;
|
||||
height: 21px;
|
||||
}
|
||||
desktopCapturePadding: margins(7px, 7px, 7px, 33px);
|
||||
desktopCaptureLabelBottom: 7px;
|
||||
desktopCaptureLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 200px;
|
||||
maxHeight: 20px;
|
||||
textFg: groupCallMembersFg;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
desktopCaptureCancel: RoundButton(defaultBoxButton) {
|
||||
textFg: groupCallActiveFg;
|
||||
textFgOver: groupCallActiveFg;
|
||||
numbersTextFg: groupCallActiveFg;
|
||||
numbersTextFgOver: groupCallActiveFg;
|
||||
textBg: groupCallMembersBg;
|
||||
textBgOver: groupCallMembersBgOver;
|
||||
|
||||
ripple: groupCallRipple;
|
||||
}
|
||||
desktopCaptureFinish: RoundButton(desktopCaptureCancel) {
|
||||
textFg: groupCallMemberMutedIcon;
|
||||
textFgOver: groupCallMemberMutedIcon;
|
||||
}
|
||||
desktopCaptureSubmit: RoundButton(desktopCaptureCancel) {
|
||||
textFg: groupCallIconFg;
|
||||
textFgOver: groupCallIconFg;
|
||||
numbersTextFg: groupCallIconFg;
|
||||
numbersTextFgOver: groupCallIconFg;
|
||||
textBg: groupCallMuted1;
|
||||
textBgOver: groupCallMuted1;
|
||||
|
||||
ripple: RippleAnimation(groupCallRipple) {
|
||||
color: shadowFg;
|
||||
}
|
||||
}
|
||||
|
||||
groupCallNarrowSkip: 9px;
|
||||
groupCallNarrowMembersWidth: 204px;
|
||||
groupCallNarrowVideoHeight: 120px;
|
||||
groupCallWideModeWidthMin: 600px;
|
||||
groupCallWideModeSize: size(960px, 580px);
|
||||
groupCallNarrowInactiveCrossLine: CrossLineAnimation {
|
||||
fg: groupCallMemberNotJoinedStatus;
|
||||
icon: icon {{ "calls/video_mini_mute", groupCallMemberNotJoinedStatus }};
|
||||
startPosition: point(3px, 0px);
|
||||
endPosition: point(13px, 12px);
|
||||
stroke: 3px;
|
||||
strokeDenominator: 2;
|
||||
}
|
||||
groupCallNarrowColoredCrossLine: CrossLineAnimation(groupCallNarrowInactiveCrossLine) {
|
||||
fg: groupCallMemberNotJoinedStatus;
|
||||
icon: icon {{ "calls/video_mini_mute", groupCallMemberActiveStatus }};
|
||||
}
|
||||
groupCallNarrowRaisedHand: icon {{ "calls/video_mini_speak", groupCallMemberInactiveStatus }};
|
||||
groupCallNarrowCameraIcon: icon {{ "calls/video_mini_video", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowScreenIcon: icon {{ "calls/video_mini_screencast", groupCallMemberNotJoinedStatus }};
|
||||
groupCallNarrowIconPosition: point(-4px, 2px);
|
||||
groupCallNarrowIconSkip: 15px;
|
||||
groupCallOutline: 2px;
|
||||
groupCallVideoCrossLine: CrossLineAnimation(groupCallMemberColoredCrossLine) {
|
||||
fg: groupCallVideoTextFg;
|
||||
icon: icon {{ "calls/video_over_mute", groupCallVideoTextFg }};
|
||||
}
|
||||
|
||||
GroupCallVideoTile {
|
||||
shadowHeight: pixels;
|
||||
namePosition: point;
|
||||
pin: CrossLineAnimation;
|
||||
pinPosition: point;
|
||||
pinPadding: margins;
|
||||
pinTextPosition: point;
|
||||
back: icon;
|
||||
iconPosition: point;
|
||||
}
|
||||
|
||||
groupCallVideoTile: GroupCallVideoTile {
|
||||
shadowHeight: 40px;
|
||||
namePosition: point(15px, 8px);
|
||||
pin: CrossLineAnimation {
|
||||
fg: groupCallVideoTextFg;
|
||||
icon: icon {{ "calls/video_over_pin", groupCallVideoTextFg }};
|
||||
startPosition: point(7px, 4px);
|
||||
endPosition: point(17px, 14px);
|
||||
stroke: 3px;
|
||||
strokeDenominator: 2;
|
||||
}
|
||||
pinPosition: point(18px, 18px);
|
||||
pinPadding: margins(6px, 2px, 12px, 1px);
|
||||
pinTextPosition: point(1px, 3px);
|
||||
back: icon {{ "calls/video_back", groupCallVideoTextFg }};
|
||||
iconPosition: point(10px, 5px);
|
||||
}
|
||||
|
||||
groupCallVideoSmallSkip: 4px;
|
||||
groupCallVideoLargeSkip: 6px;
|
||||
groupCallVideoPlaceholderHeight: 212px;
|
||||
groupCallVideoPlaceholderIconTop: 50px;
|
||||
groupCallVideoPlaceholderTextTop: 120px;
|
||||
|
||||
groupCallTooltip: Tooltip(defaultTooltip) {
|
||||
textBg: groupCallMembersBg;
|
||||
textFg: groupCallMembersFg;
|
||||
textBorder: groupCallMembersBgOver;
|
||||
}
|
||||
groupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) {
|
||||
bg: importantTooltipBg;
|
||||
padding: margins(10px, 3px, 10px, 5px);
|
||||
radius: 4px;
|
||||
arrow: 4px;
|
||||
}
|
||||
groupCallNiceTooltipLabel: FlatLabel(defaultImportantTooltipLabel) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(11px);
|
||||
linkFont: font(11px);
|
||||
linkFontOver: font(11px underline);
|
||||
}
|
||||
}
|
||||
groupCallNiceTooltipTop: 4px;
|
||||
groupCallPaused: icon {{ "calls/video_large_paused", groupCallVideoTextFg }};
|
||||
|
||||
@@ -357,7 +357,7 @@ base::unique_qptr<Ui::PopupMenu> BoxController::rowContextMenu(
|
||||
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(parent);
|
||||
result->addAction(tr::lng_context_delete_selected(tr::now), [=] {
|
||||
Ui::show(
|
||||
_window->show(
|
||||
Box<DeleteMessagesBox>(session, base::duplicate(ids)),
|
||||
Ui::LayerOption::KeepOther);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "webrtc/webrtc_create_adm.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "facades.h"
|
||||
|
||||
#include <tgcalls/Instance.h>
|
||||
#include <tgcalls/VideoCaptureInterface.h>
|
||||
@@ -161,8 +160,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 +383,7 @@ void Call::setupOutgoingVideo() {
|
||||
// Paused not supported right now.
|
||||
Assert(state == Webrtc::VideoState::Active);
|
||||
if (!_videoCapture) {
|
||||
_videoCapture = _delegate->getVideoCapture();
|
||||
_videoCapture = _delegate->callGetVideoCapture();
|
||||
_videoCapture->setOutput(_videoOutgoing->sink());
|
||||
}
|
||||
if (_instance) {
|
||||
@@ -801,16 +804,19 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
|
||||
AppendServer(descriptor.rtcServers, connection);
|
||||
}
|
||||
|
||||
if (Global::UseProxyForCalls()
|
||||
&& (Global::ProxySettings() == MTP::ProxyData::Settings::Enabled)) {
|
||||
const auto &selected = Global::SelectedProxy();
|
||||
if (selected.supportsCalls() && !selected.host.isEmpty()) {
|
||||
Assert(selected.type == MTP::ProxyData::Type::Socks5);
|
||||
descriptor.proxy = std::make_unique<tgcalls::Proxy>();
|
||||
descriptor.proxy->host = selected.host.toStdString();
|
||||
descriptor.proxy->port = selected.port;
|
||||
descriptor.proxy->login = selected.user.toStdString();
|
||||
descriptor.proxy->password = selected.password.toStdString();
|
||||
{
|
||||
auto &settingsProxy = Core::App().settings().proxy();
|
||||
using ProxyData = MTP::ProxyData;
|
||||
if (settingsProxy.useProxyForCalls() && settingsProxy.isEnabled()) {
|
||||
const auto &selected = settingsProxy.selected();
|
||||
if (selected.supportsCalls() && !selected.host.isEmpty()) {
|
||||
Assert(selected.type == ProxyData::Type::Socks5);
|
||||
descriptor.proxy = std::make_unique<tgcalls::Proxy>();
|
||||
descriptor.proxy->host = selected.host.toStdString();
|
||||
descriptor.proxy->port = selected.port;
|
||||
descriptor.proxy->login = selected.user.toStdString();
|
||||
descriptor.proxy->password = selected.password.toStdString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
3140
Telegram/SourceFiles/calls/group/calls_group_call.cpp
Normal file
657
Telegram/SourceFiles/calls/group/calls_group_call.h
Normal file
@@ -0,0 +1,657 @@
|
||||
/*
|
||||
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);
|
||||
void setNoiseSuppression(bool enabled);
|
||||
|
||||
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;
|
||||
rpl::variable<QSize> trackSize;
|
||||
PeerData *peer = nullptr;
|
||||
rpl::lifetime lifetime;
|
||||
Group::VideoQuality quality = Group::VideoQuality();
|
||||
bool shown = false;
|
||||
|
||||
[[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();
|
||||
}
|
||||
[[nodiscard]] bool hasNotShownVideo() const {
|
||||
return _hasNotShownVideo.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> hasNotShownVideoValue() const {
|
||||
return _hasNotShownVideo.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 refreshHasNotShownVideo();
|
||||
|
||||
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;
|
||||
rpl::variable<bool> _hasNotShownVideo = 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
|
||||