Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e117d08b2c | ||
|
|
58783170cd | ||
|
|
9ba2426c02 | ||
|
|
2cbaa7b03d | ||
|
|
836a0f3a73 | ||
|
|
e9977f551f | ||
|
|
78abe362cd | ||
|
|
47c2922d55 | ||
|
|
690f3fecbb | ||
|
|
d9242db7b3 | ||
|
|
a9b5e22b37 | ||
|
|
7b8f1704dc | ||
|
|
de88ddf42b | ||
|
|
b1e707c346 | ||
|
|
36b7b3a7cd | ||
|
|
d44349d612 | ||
|
|
53919be913 | ||
|
|
8dd7d04fff | ||
|
|
384f6d9a31 | ||
|
|
d51195a537 | ||
|
|
10bedad9bf | ||
|
|
4027d3c07f | ||
|
|
ec78d3b83b | ||
|
|
acf61c24bd | ||
|
|
338183e647 | ||
|
|
59e0de85e3 | ||
|
|
2e6941c23f | ||
|
|
e1c33aa87b | ||
|
|
70d8cede9b | ||
|
|
7587bbd4bc | ||
|
|
668f0498ed | ||
|
|
32ff331744 | ||
|
|
bc90463893 | ||
|
|
7fb486d8d2 | ||
|
|
538df0e67c | ||
|
|
60bb93170e | ||
|
|
766370221b | ||
|
|
8007e6d354 | ||
|
|
443053b927 | ||
|
|
b6000bc1a8 | ||
|
|
8bdf8d42d9 | ||
|
|
c084396f8f | ||
|
|
b29877554c | ||
|
|
a2d17f6f3f | ||
|
|
54018aec90 | ||
|
|
2699f9bcf6 | ||
|
|
094890622a | ||
|
|
bee4ec5ddf | ||
|
|
29f533b170 | ||
|
|
296c2a7b5b | ||
|
|
c4decce7f0 | ||
|
|
a396f507f8 | ||
|
|
eb96fd1b97 | ||
|
|
8cc7d80121 | ||
|
|
d59139202d | ||
|
|
a3cd4a55de | ||
|
|
0cb7388ee0 | ||
|
|
2700e9b326 | ||
|
|
d36d5b2980 | ||
|
|
028437549d | ||
|
|
1f17551bf4 | ||
|
|
cd301c9df2 | ||
|
|
ce43aab4cd | ||
|
|
86d1ab00e0 | ||
|
|
93b2a52f34 | ||
|
|
d3da830ef8 | ||
|
|
ec4deb87c6 | ||
|
|
d832a0a186 | ||
|
|
65800e17a5 | ||
|
|
9c5dd452dd | ||
|
|
78940413a0 | ||
|
|
cc2f459f26 | ||
|
|
d1c98eb22a | ||
|
|
1fe9a0ee5a | ||
|
|
80a87f963a | ||
|
|
2c0051b083 | ||
|
|
4a938170e1 | ||
|
|
f3a27c4fcb | ||
|
|
f43b11fd6a | ||
|
|
93d382c416 | ||
|
|
48e4060d62 | ||
|
|
bbc7ef100c | ||
|
|
c22468c943 | ||
|
|
7d538ea080 | ||
|
|
aad1ff95be | ||
|
|
72f0fb6892 | ||
|
|
a0baad4942 | ||
|
|
5e40d60e34 | ||
|
|
314f787042 | ||
|
|
7f340da0ec | ||
|
|
acafa2bcad | ||
|
|
404d8da1a3 | ||
|
|
26be7840b4 | ||
|
|
520b84f967 | ||
|
|
e7f6a8a476 | ||
|
|
0f3ef4b35c | ||
|
|
60d88693ec | ||
|
|
ff6e2b6d31 | ||
|
|
bc52ad7bf2 | ||
|
|
daa24de171 | ||
|
|
68f0e25227 | ||
|
|
320f2bea7b | ||
|
|
666692e341 | ||
|
|
304cc33b76 | ||
|
|
e0f4aca336 | ||
|
|
ccdff5baef | ||
|
|
16fe056c99 | ||
|
|
fb5c155f7e | ||
|
|
eccbb0df7a | ||
|
|
0bcfcb4ea4 | ||
|
|
b6b4d5c387 | ||
|
|
bb7bfe95e5 | ||
|
|
ac1e5a08d8 | ||
|
|
4a05f0fd05 | ||
|
|
f1dff4d0b7 | ||
|
|
96343603cd | ||
|
|
9dccab915d | ||
|
|
0decf928f0 | ||
|
|
44faef3027 | ||
|
|
52a590334c | ||
|
|
8ea9709305 | ||
|
|
751c7e05f9 | ||
|
|
ebe0aa27e4 | ||
|
|
4ef7f0dd24 | ||
|
|
4779c036f5 | ||
|
|
1480a4e270 | ||
|
|
6a167a4b0f | ||
|
|
9b43f57dfa | ||
|
|
fc74840b55 | ||
|
|
f408b1bb11 | ||
|
|
7a3e16b92f | ||
|
|
939045d606 | ||
|
|
b3ee80e495 | ||
|
|
1c17a76b80 | ||
|
|
08a55721bf | ||
|
|
42991bfa2c | ||
|
|
46897689d3 | ||
|
|
9b3acf7162 | ||
|
|
4436e3ba69 | ||
|
|
78b3f21e7a | ||
|
|
e2ae0a7a4f | ||
|
|
5ed8f0baa5 | ||
|
|
f1d4e48c59 | ||
|
|
48ac431134 | ||
|
|
d6a27c04b9 | ||
|
|
388e9cec09 | ||
|
|
b01f70176b | ||
|
|
e7b4a5db61 | ||
|
|
d5a1d28779 | ||
|
|
e0da4159ae | ||
|
|
0eef766ea1 | ||
|
|
aebd53363c | ||
|
|
f18bb1c780 | ||
|
|
eecb7c0622 | ||
|
|
84c0b46266 | ||
|
|
fb030b3520 | ||
|
|
42aac81fd8 | ||
|
|
09175d2b6b | ||
|
|
46f64f75c7 | ||
|
|
9b5425de14 | ||
|
|
7b1135ecd4 | ||
|
|
2ca63ae324 | ||
|
|
5b02323487 | ||
|
|
cf99eef9c9 | ||
|
|
c54e05d220 | ||
|
|
a651853534 | ||
|
|
a1a81b812a | ||
|
|
e74d0ef4e2 | ||
|
|
ca4caec819 | ||
|
|
900c12d1d0 | ||
|
|
4d61701790 | ||
|
|
4fd0e734ea | ||
|
|
b4d1ba07a6 | ||
|
|
6974c511ea | ||
|
|
d30dd7450f | ||
|
|
f877e653e9 | ||
|
|
699ffc8b50 | ||
|
|
135b81bbb4 | ||
|
|
d9f5c2d16e | ||
|
|
2e0f04ce93 | ||
|
|
8ebb084f0f | ||
|
|
f2bc07458d | ||
|
|
b4daca860f | ||
|
|
ac6919680a | ||
|
|
c6749f2d5c | ||
|
|
47fd936306 | ||
|
|
147439ad34 | ||
|
|
1a3eb0f209 | ||
|
|
acd2c514ce | ||
|
|
fccff0c03a | ||
|
|
7cf8483f9a | ||
|
|
63bad22ac4 | ||
|
|
b009f06375 | ||
|
|
8aa5ebf621 | ||
|
|
53ac770772 | ||
|
|
fbfe47528b | ||
|
|
71443f7def | ||
|
|
a1b1165039 | ||
|
|
c060243201 | ||
|
|
93f84e2b65 | ||
|
|
9cd971a53f | ||
|
|
b22b819683 | ||
|
|
8779ac3ec3 | ||
|
|
16ec935feb | ||
|
|
e59d229097 | ||
|
|
9028d9a339 | ||
|
|
6999a1b5e4 | ||
|
|
7ef6d33ea9 | ||
|
|
3824f19ce5 | ||
|
|
930268ed0d | ||
|
|
cb0a3fdb6e | ||
|
|
4c0a2b1d3d | ||
|
|
6fd34dee5a | ||
|
|
1c56c4dfe5 | ||
|
|
53321160c4 | ||
|
|
5b29d3bb72 | ||
|
|
4796a3116a | ||
|
|
6183569dc2 | ||
|
|
3296a2addb | ||
|
|
235cb0c574 | ||
|
|
570fbd9a00 | ||
|
|
e205b521fd | ||
|
|
ce15754575 | ||
|
|
18085bae70 | ||
|
|
61536b42c9 | ||
|
|
532f166b22 | ||
|
|
c1d3a78316 | ||
|
|
e524d9694d | ||
|
|
1efa889913 | ||
|
|
c73788da24 | ||
|
|
5ec277b2ed | ||
|
|
e327e1bc9c | ||
|
|
37c5c1dc98 | ||
|
|
156e1a00d3 | ||
|
|
3aa9a2f2c0 | ||
|
|
e1f5b57fd3 | ||
|
|
cf757b6c7b | ||
|
|
bd2da8d632 | ||
|
|
01b7e387eb | ||
|
|
60bf6b1f70 | ||
|
|
7839c6294c | ||
|
|
20cadfad29 | ||
|
|
2b93362a57 | ||
|
|
5476f297e1 | ||
|
|
b79fbe2123 | ||
|
|
d9a76fdd9a | ||
|
|
721a69f2b1 | ||
|
|
a49b3e67d0 | ||
|
|
297600ca6d | ||
|
|
15019051c0 | ||
|
|
2b7e8bf449 | ||
|
|
928798dc16 | ||
|
|
1a01ec17d6 | ||
|
|
69da4d749b | ||
|
|
b1fc95b1d9 | ||
|
|
0b7f104c95 | ||
|
|
912974cb80 | ||
|
|
9bbb8ef31c | ||
|
|
e01f53a58c | ||
|
|
82c69a03ef | ||
|
|
f8aa79aaf5 | ||
|
|
700ae79b93 | ||
|
|
8dd8ab6366 | ||
|
|
28d19a99e1 | ||
|
|
6627de6460 | ||
|
|
795d452493 | ||
|
|
2d7f52972d | ||
|
|
6229c0020c | ||
|
|
8db1bf9052 | ||
|
|
2740c5db23 | ||
|
|
512e6de39b | ||
|
|
e3c328c7e3 | ||
|
|
9eb5ea599e | ||
|
|
929032d598 | ||
|
|
09fb42291d | ||
|
|
e6665a8305 | ||
|
|
a785d83791 | ||
|
|
08a64f0dc0 | ||
|
|
0babad837d |
2
.github/workflows/mac.yml
vendored
2
.github/workflows/mac.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
macos:
|
||||
name: MacOS
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
3
.github/workflows/mac_packaged.yml
vendored
3
.github/workflows/mac_packaged.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
run: |
|
||||
brew update
|
||||
brew upgrade || true
|
||||
brew install ada-url autoconf automake boost cmake ffmpeg jpeg-xl libavif libheif libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz
|
||||
brew install ada-url autoconf automake boost cmake ffmpeg@6 jpeg-xl libavif libheif libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz
|
||||
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
|
||||
|
||||
xcodebuild -version > CACHE_KEY.txt
|
||||
@@ -82,6 +82,7 @@ jobs:
|
||||
fi
|
||||
echo "CACHE_KEY=`md5 -q CACHE_KEY.txt`" >> $GITHUB_ENV
|
||||
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=$(grep 'set(QT_SUPPORTED_MIN_MACOS_VERSION' /opt/homebrew/Cellar/qt/6.9.2/lib/cmake/Qt6/Qt6ConfigExtras.cmake | sed -E 's/^.*"(.*)"\)$/\1/')" >> $GITHUB_ENV
|
||||
echo "LibrariesPath=`pwd`" >> $GITHUB_ENV
|
||||
|
||||
curl -o tg_owt-version.json https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
stale-issue-message: |
|
||||
Hey there!
|
||||
|
||||
@@ -45,7 +45,7 @@ Version **1.8.15** was the last that supports older systems
|
||||
* Qt 6 ([LGPL](http://doc.qt.io/qt-6/lgpl.html)) and Qt 5.15 ([LGPL](http://doc.qt.io/qt-5/lgpl.html)) slightly patched
|
||||
* OpenSSL 3.2.1 ([Apache License 2.0](https://www.openssl.org/source/apache-license-2.0.txt))
|
||||
* WebRTC ([New BSD License](https://github.com/desktop-app/tg_owt/blob/master/LICENSE))
|
||||
* zlib 1.2.11 ([zlib License](http://www.zlib.net/zlib_license.html))
|
||||
* zlib ([zlib License](http://www.zlib.net/zlib_license.html))
|
||||
* LZMA SDK 9.20 ([public domain](http://www.7-zip.org/sdk.html))
|
||||
* liblzma ([public domain](http://tukaani.org/xz/))
|
||||
* Google Breakpad ([License](https://chromium.googlesource.com/breakpad/breakpad/+/master/LICENSE))
|
||||
|
||||
@@ -35,7 +35,7 @@ include(cmake/td_mtproto.cmake)
|
||||
include(cmake/td_scheme.cmake)
|
||||
include(cmake/td_tde2e.cmake)
|
||||
include(cmake/td_ui.cmake)
|
||||
include(cmake/generate_appdata_changelog.cmake)
|
||||
include(cmake/generate_appstream_changelog.cmake)
|
||||
|
||||
if (DESKTOP_APP_TEST_APPS)
|
||||
include(cmake/tests.cmake)
|
||||
@@ -363,6 +363,14 @@ PRIVATE
|
||||
calls/group/calls_group_members_row.h
|
||||
calls/group/calls_group_menu.cpp
|
||||
calls/group/calls_group_menu.h
|
||||
calls/group/calls_group_message_encryption.cpp
|
||||
calls/group/calls_group_message_encryption.h
|
||||
calls/group/calls_group_message_field.cpp
|
||||
calls/group/calls_group_message_field.h
|
||||
calls/group/calls_group_messages.cpp
|
||||
calls/group/calls_group_messages.h
|
||||
calls/group/calls_group_messages_ui.cpp
|
||||
calls/group/calls_group_messages_ui.h
|
||||
calls/group/calls_group_panel.cpp
|
||||
calls/group/calls_group_panel.h
|
||||
calls/group/calls_group_rtmp.cpp
|
||||
@@ -775,6 +783,8 @@ PRIVATE
|
||||
history/view/controls/history_view_voice_record_bar.h
|
||||
history/view/controls/history_view_webpage_processor.cpp
|
||||
history/view/controls/history_view_webpage_processor.h
|
||||
history/view/media/history_view_birthday_suggestion.cpp
|
||||
history/view/media/history_view_birthday_suggestion.h
|
||||
history/view/media/history_view_call.cpp
|
||||
history/view/media/history_view_call.h
|
||||
history/view/media/history_view_contact.cpp
|
||||
@@ -908,6 +918,8 @@ PRIVATE
|
||||
history/view/history_view_schedule_box.h
|
||||
history/view/history_view_scheduled_section.cpp
|
||||
history/view/history_view_scheduled_section.h
|
||||
history/view/history_view_self_forwards_tagger.cpp
|
||||
history/view/history_view_self_forwards_tagger.h
|
||||
history/view/history_view_send_action.cpp
|
||||
history/view/history_view_send_action.h
|
||||
history/view/history_view_service_message.cpp
|
||||
@@ -952,6 +964,8 @@ PRIVATE
|
||||
history/history_inner_widget.h
|
||||
history/history_location_manager.cpp
|
||||
history/history_location_manager.h
|
||||
history/history_streamed_drafts.cpp
|
||||
history/history_streamed_drafts.h
|
||||
history/history_translation.cpp
|
||||
history/history_translation.h
|
||||
history/history_unread_things.cpp
|
||||
@@ -1044,8 +1058,6 @@ PRIVATE
|
||||
info/profile/info_profile_members_controllers.h
|
||||
info/profile/info_profile_phone_menu.cpp
|
||||
info/profile/info_profile_phone_menu.h
|
||||
info/profile/info_profile_text.cpp
|
||||
info/profile/info_profile_text.h
|
||||
info/profile/info_profile_values.cpp
|
||||
info/profile/info_profile_values.h
|
||||
info/profile/info_profile_widget.cpp
|
||||
@@ -1795,6 +1807,8 @@ if (WIN32)
|
||||
if (QT_VERSION LESS 6)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::win_directx_helper)
|
||||
endif()
|
||||
|
||||
target_link_options(Telegram PRIVATE /PDBPAGESIZE:8192)
|
||||
elseif (APPLE)
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
target_link_libraries(Telegram PRIVATE desktop-app::external_iconv)
|
||||
@@ -2130,7 +2144,7 @@ if (LINUX AND DESKTOP_APP_USE_PACKAGED)
|
||||
include(GNUInstallDirs)
|
||||
configure_file("../lib/xdg/org.telegram.desktop.service" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.service" @ONLY)
|
||||
configure_file("../lib/xdg/org.telegram.desktop.metainfo.xml" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml" @ONLY)
|
||||
generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml")
|
||||
generate_appstream_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/org.telegram.desktop.metainfo.xml")
|
||||
install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "org.telegram.desktop.png")
|
||||
install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "org.telegram.desktop.png")
|
||||
|
||||
BIN
Telegram/Resources/animations/cake.tgs
Normal file
BIN
Telegram/Resources/animations/cake.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/animations/camera_outline.tgs
Normal file
BIN
Telegram/Resources/animations/camera_outline.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/animations/photo_suggest_icon.tgs
Normal file
BIN
Telegram/Resources/animations/photo_suggest_icon.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/animations/toast/saved_messages.tgs
Normal file
BIN
Telegram/Resources/animations/toast/saved_messages.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/animations/toast/tagged.tgs
Normal file
BIN
Telegram/Resources/animations/toast/tagged.tgs
Normal file
Binary file not shown.
BIN
Telegram/Resources/icons/calls/call_message.png
Normal file
BIN
Telegram/Resources/icons/calls/call_message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 692 B |
BIN
Telegram/Resources/icons/calls/call_message@2x.png
Normal file
BIN
Telegram/Resources/icons/calls/call_message@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Telegram/Resources/icons/calls/call_message@3x.png
Normal file
BIN
Telegram/Resources/icons/calls/call_message@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
9
Telegram/Resources/icons/chat/new_topic.svg
Normal file
9
Telegram/Resources/icons/chat/new_topic.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Menu / new_topic</title>
|
||||
<g id="Icon-/-Menu-/-new_topic" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M12,3.15416667 C12.3865993,3.15416667 12.7,3.46756734 12.7,3.85416667 C12.7,4.24076599 12.3865993,4.55416667 12,4.55416667 L7.24826389,4.55416667 C5.76035508,4.55416667 4.55416667,5.76035508 4.55416667,7.24826389 L4.55416667,16.7517361 C4.55416667,18.2396449 5.76035508,19.4458333 7.24826389,19.4458333 L16.7517361,19.4458333 C18.2396449,19.4458333 19.4458333,18.2396449 19.4458333,16.7517361 L19.4458333,12 C19.4458333,11.6134007 19.759234,11.3 20.1458333,11.3 C20.5324327,11.3 20.8458333,11.6134007 20.8458333,12 L20.8458333,16.7517361 C20.8458333,19.0128436 19.0128436,20.8458333 16.7517361,20.8458333 L7.24826389,20.8458333 C4.98715643,20.8458333 3.15416667,19.0128436 3.15416667,16.7517361 L3.15416667,7.24826389 C3.15416667,4.98715643 4.98715643,3.15416667 7.24826389,3.15416667 L12,3.15416667 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
<path d="M19.3511597,5.34872828 C20.2469248,6.24449337 20.2469248,7.69681554 19.3511597,8.59258064 L12.7489306,15.1948097 C12.4577752,15.4859651 12.0995331,15.7011118 11.7057162,15.8213249 L10.029497,16.3329929 C9.10119412,16.6163585 8.11894274,16.0935336 7.83557714,15.1652307 C7.71876473,14.7825544 7.73602986,14.3714738 7.88451862,13.99994 L8.70227653,11.9538276 C8.8286621,11.6375983 9.01800726,11.3503585 9.2588125,11.1095532 L15.5634724,4.80489335 C16.4592375,3.90912826 17.9115597,3.90912826 18.8073247,4.80489335 L19.3511597,5.34872828 Z M18.3612102,6.33867777 L17.8173753,5.79484285 C17.4683442,5.44581176 16.902453,5.44581176 16.5534219,5.79484285 L10.248762,12.0995027 C10.1421189,12.2061458 10.0582655,12.3333529 10.0022944,12.4733983 L9.18453646,14.5195106 C9.15433809,14.59507 9.15082685,14.678672 9.17458316,14.7564974 C9.23221162,14.9452877 9.43197346,15.0516153 9.62076372,14.9939869 L11.296983,14.4823189 C11.4713888,14.4290813 11.63004,14.3338014 11.7589811,14.2048602 L18.3612102,7.60263114 C18.7102413,7.25360006 18.7102413,6.68770886 18.3612102,6.33867777 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
<polygon id="Path" fill="#FFFFFF" fill-rule="nonzero" points="15.5907211 6.54669192 17.5755336 8.53150439 16.5855841 9.52145388 14.6007716 7.53664141"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
7
Telegram/Resources/icons/menu/reorder.svg
Normal file
7
Telegram/Resources/icons/menu/reorder.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Menu / reorder</title>
|
||||
<g id="Icon-/-Menu-/-reorder" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M20.4060728,15.0078431 C21.2543281,15.0078431 21.9419748,15.6954899 21.9419748,16.5437451 L21.9419748,19.064098 C21.9419748,19.9123532 21.2543281,20.6 20.4060728,20.6 L17.9103341,20.6 C17.0620789,20.6 16.3744321,19.9123532 16.3744321,19.064098 L16.3744321,16.5437451 C16.3744321,15.6954899 17.0620789,15.0078431 17.9103341,15.0078431 L20.4060728,15.0078431 Z M16.6624648,3.4 C18.1998993,3.4 19.4462362,4.64633689 19.4462362,6.18377135 L19.446,10.561 L21.5745455,8.42203928 C21.782239,8.21317523 22.105482,8.18912467 22.339778,8.35046222 L22.4230702,8.41965499 C22.6580423,8.65331023 22.6591098,9.03320772 22.4254545,9.26817977 L19.3380547,12.3729793 C19.0636746,12.6458372 18.6245193,12.6446032 18.3544177,12.3729793 L15.2670178,9.26817977 C15.0333625,9.03320772 15.03443,8.65331023 15.2694021,8.41965499 C15.5043741,8.18599974 15.8842716,8.18706723 16.1179268,8.42203928 L18.246,10.563 L18.2462362,6.18377135 C18.2462362,5.30907859 17.5371576,4.6 16.6624648,4.6 L6.36754271,4.6 C5.49284994,4.6 4.78377135,5.30907859 4.78377135,6.18377135 L4.783,15.007 L5.4316407,15.0078431 C6.27989595,15.0078431 6.96754271,15.6954899 6.96754271,16.5437451 L6.96754271,19.064098 C6.96754271,19.9123532 6.27989595,20.6 5.4316407,20.6 L2.93590201,20.6 C2.08764675,20.6 1.4,19.9123532 1.4,19.064098 L1.4,16.5437451 C1.4,15.6954899 2.08764675,15.0078431 2.93590201,15.0078431 L3.583,15.007 L3.58377135,6.18377135 C3.58377135,4.64633689 4.83010824,3.4 6.36754271,3.4 L16.6624648,3.4 Z M12.9188568,15.0078431 C13.767112,15.0078431 14.4547588,15.6954899 14.4547588,16.5437451 L14.4547588,19.064098 C14.4547588,19.9123532 13.767112,20.6 12.9188568,20.6 L10.4231181,20.6 C9.57486282,20.6 8.88721607,19.9123532 8.88721607,19.064098 L8.88721607,16.5437451 C8.88721607,15.6954899 9.57486282,15.0078431 10.4231181,15.0078431 L12.9188568,15.0078431 Z M20.4060728,16.2078431 L17.9103341,16.2078431 C17.7248206,16.2078431 17.5744321,16.3582316 17.5744321,16.5437451 L17.5744321,19.064098 C17.5744321,19.2496115 17.7248206,19.4 17.9103341,19.4 L20.4060728,19.4 C20.5915864,19.4 20.7419748,19.2496115 20.7419748,19.064098 L20.7419748,16.5437451 C20.7419748,16.3582316 20.5915864,16.2078431 20.4060728,16.2078431 Z M5.4316407,16.2078431 L2.93590201,16.2078431 C2.75038845,16.2078431 2.6,16.3582316 2.6,16.5437451 L2.6,19.064098 C2.6,19.2496115 2.75038845,19.4 2.93590201,19.4 L5.4316407,19.4 C5.61715425,19.4 5.76754271,19.2496115 5.76754271,19.064098 L5.76754271,16.5437451 C5.76754271,16.3582316 5.61715425,16.2078431 5.4316407,16.2078431 Z M12.9188568,16.2078431 L10.4231181,16.2078431 C10.2376045,16.2078431 10.0872161,16.3582316 10.0872161,16.5437451 L10.0872161,19.064098 C10.0872161,19.2496115 10.2376045,19.4 10.4231181,19.4 L12.9188568,19.4 C13.1043703,19.4 13.2547588,19.2496115 13.2547588,19.064098 L13.2547588,16.5437451 C13.2547588,16.3582316 13.1043703,16.2078431 12.9188568,16.2078431 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
7
Telegram/Resources/icons/settings/birthday_add.svg
Normal file
7
Telegram/Resources/icons/settings/birthday_add.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Menu / birthday_add</title>
|
||||
<g id="Icon-/-Menu-/-birthday_add" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M19.4857266,16.2888889 C19.8170975,16.2888889 20.0857266,16.557518 20.0857266,16.8888889 L20.0856667,18.7328889 L21.9305556,18.7333333 C22.2287893,18.7333333 22.4762023,18.9509229 22.5227026,19.2360102 L22.5305556,19.3333333 C22.5305556,19.6647042 22.2619264,19.9333333 21.9305556,19.9333333 L20.0856667,19.9328889 L20.0857266,21.7777778 C20.0857266,22.0760115 19.868137,22.3234245 19.5830498,22.3699248 L19.4857266,22.3777778 C19.1543558,22.3777778 18.8857266,22.1091486 18.8857266,21.7777778 L18.8856667,19.9328889 L17.0416667,19.9333333 C16.7434329,19.9333333 16.4960199,19.7157437 16.4495197,19.4306564 L16.4416667,19.3333333 C16.4416667,19.0019625 16.7102958,18.7333333 17.0416667,18.7333333 L18.8856667,18.7328889 L18.8857266,16.8888889 C18.8857266,16.5906551 19.1033163,16.3432421 19.3884035,16.2967419 L19.4857266,16.2888889 Z M8.41204757,10.3213892 C8.4968615,10.6417222 8.30593555,10.9701587 7.98560254,11.0549726 C6.00141779,11.5803212 4.96111111,12.4866846 4.96111111,13.4472478 C4.96111111,13.5837542 4.98907974,13.7199344 5.04305686,13.8546242 C5.05815315,13.8780132 5.06968916,13.9034625 5.07949993,13.9299664 C5.67337842,15.1862671 8.51737301,16.291625 12,16.291625 C15.9250424,16.291625 19.0388889,14.8875907 19.0388889,13.4472478 C19.0388889,12.5127848 18.0774561,11.638341 16.2036879,11.1068602 C15.8848931,11.0164364 15.6997618,10.684699 15.7901857,10.3659042 C15.8806095,10.0471093 16.2123468,9.86197804 16.5311417,9.95240189 C18.8627692,10.6137512 20.2388889,11.8653615 20.2388889,13.4472478 C20.2388889,13.979393 20.0501074,14.4768826 19.7060685,14.9283577 C19.6140075,14.9115452 19.5205276,14.9027778 19.425,14.9027778 C18.6281077,14.9027778 17.9737164,15.5128973 17.9034658,16.2914966 L17.9034247,16.3114033 C16.4008483,17.0540936 14.3028164,17.491625 12,17.491625 C9.47842329,17.491625 7.20238688,16.967017 5.68481102,16.0915954 L6.42111111,18.7214444 L6.44,18.7222222 L6.44511111,18.8074444 L6.45530551,18.8430624 L6.4709096,18.9292533 C6.76328017,19.9158038 9.09544378,20.8722222 12,20.8722222 C13.262879,20.8722222 14.4175503,20.6914166 15.3434009,20.4059417 C15.6196573,20.6869274 16.004765,20.8611111 16.4305556,20.8611111 L17.2836209,20.8606349 C16.0386964,21.6207641 14.1238391,22.0722222 12,22.0722222 C8.52835644,22.0722222 5.61510021,20.8659524 5.27347003,19.0770457 L3.95558458,14.3602924 C3.82830877,14.0698882 3.76111111,13.7647359 3.76111111,13.4472478 C3.76111111,11.8222874 5.22885268,10.5435228 7.67846416,9.89494413 C7.99879717,9.81013021 8.32723365,10.0010562 8.41204757,10.3213892 Z M12.4583333,7.42777778 C13.380342,7.42777778 14.1277778,8.17521351 14.1277778,9.09722222 L14.1277778,13.0694444 C14.1277778,13.9914532 13.380342,14.7388889 12.4583333,14.7388889 L11.8472222,14.7388889 C10.9252135,14.7388889 10.1777778,13.9914532 10.1777778,13.0694444 L10.1777778,9.09722222 C10.1777778,8.17521351 10.9252135,7.42777778 11.8472222,7.42777778 L12.4583333,7.42777778 Z M12.4583333,8.62777778 L11.8472222,8.62777778 C11.5879552,8.62777778 11.3777778,8.83795521 11.3777778,9.09722222 L11.3777778,13.0694444 C11.3777778,13.3287115 11.5879552,13.5388889 11.8472222,13.5388889 L12.4583333,13.5388889 C12.7176003,13.5388889 12.9277778,13.3287115 12.9277778,13.0694444 L12.9277778,9.09722222 C12.9277778,8.83795521 12.7176003,8.62777778 12.4583333,8.62777778 Z M12.0207549,1.46944444 C12.8988738,1.46944444 14.1277778,3.40600437 14.1277778,4.64408449 C14.1277778,6.06372135 13.4385567,6.82401696 12,6.82401696 C10.3912998,6.82401696 9.40287158,5.20228362 10.2271046,3.79129771 L10.3125678,3.6554701 C10.6068441,3.21972221 11.1748869,2.01278865 11.1599792,2.03824823 C11.3659094,1.68655903 11.6259528,1.46944444 12.0207549,1.46944444 Z M12.1011111,2.85144444 L12.0713457,2.91751396 L11.8811707,3.30172279 L11.6899023,3.66987931 C11.557639,3.91763569 11.4205876,4.1589191 11.3070312,4.32706715 C10.8867233,4.94943561 11.2769618,5.62401696 12,5.62401696 C12.7481114,5.62401696 12.9277778,5.42582286 12.9277778,4.64408449 C12.9277778,4.29594473 12.7310746,3.77237759 12.4219529,3.28525029 C12.3528553,3.1763634 12.281237,3.07447531 12.2108288,2.98385177 L12.1011111,2.85144444 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -522,6 +522,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_use_native_notifications" = "Use native notifications";
|
||||
"lng_settings_notifications_position" = "Location on the screen";
|
||||
"lng_settings_notifications_count" = "Notifications count";
|
||||
"lng_settings_notifications_display" = "Display for notifications";
|
||||
"lng_settings_notifications_display_default" = "Default";
|
||||
"lng_settings_sound_allowed" = "Allow sound";
|
||||
"lng_settings_alert_windows" = "Flash the taskbar icon";
|
||||
"lng_settings_alert_mac" = "Bounce the Dock icon";
|
||||
@@ -777,6 +779,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_birthday_contacts" = "Only your contacts can see your birthday. {link}";
|
||||
"lng_settings_birthday_contacts_link" = "Change >";
|
||||
"lng_settings_birthday_saved" = "Your date of birth was updated.";
|
||||
"lng_settings_birthday_suggested" = "Date of birth was suggested to {user}";
|
||||
"lng_settings_birthday_reset" = "Remove";
|
||||
"lng_settings_channel_label" = "Personal channel";
|
||||
"lng_settings_channel_add" = "Add";
|
||||
@@ -984,6 +987,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_color_subscribe" = "Subscribe to {link} to choose a custom color for your name.";
|
||||
"lng_settings_color_changed" = "Your name color has been updated!";
|
||||
"lng_settings_color_changed_channel" = "Your channel color has been updated!";
|
||||
"lng_settings_color_apply" = "Apply Style";
|
||||
|
||||
"lng_suggest_hide_new_title" = "Hide new chats?";
|
||||
"lng_suggest_hide_new_about" = "You are receiving lots of new chats from users who are not in your Contact List.\n\nDo you want to have such chats **automatically muted** and **archived**?";
|
||||
@@ -1602,6 +1606,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_info_birthday_today_years#other" = "{date}\n({count} years old)";
|
||||
"lng_info_birthday_today_label" = "Birthday today";
|
||||
"lng_info_birthday_today" = "{emoji} {date}";
|
||||
"lng_info_notes_label" = "Notes";
|
||||
"lng_info_notes_private" = "only visible to you";
|
||||
"lng_edit_note" = "Edit Note";
|
||||
"lng_delete_note" = "Delete Note";
|
||||
"lng_info_bio_label" = "Bio";
|
||||
"lng_info_link_label" = "Link";
|
||||
"lng_info_location_label" = "Location";
|
||||
@@ -1625,6 +1633,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_info_group_title" = "Group Info";
|
||||
"lng_info_channel_title" = "Channel Info";
|
||||
"lng_info_topic_title" = "Topic Info";
|
||||
"lng_info_thread_title" = "Thread Info";
|
||||
"lng_profile_enable_notifications" = "Notifications";
|
||||
"lng_profile_send_message" = "Send Message";
|
||||
"lng_profile_open_app" = "Open App";
|
||||
@@ -1639,7 +1648,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_profile_suggest_photo_from_clipboard" = "Suggest From Clipboard";
|
||||
"lng_profile_set_photo_for" = "Set Profile Photo";
|
||||
"lng_profile_set_photo_for_from_clipboard" = "Set From Clipboard";
|
||||
"lng_profile_set_photo_for_about" = "You can replace {user}'s photo with another photo that only you will see.";
|
||||
"lng_profile_photo_reset" = "Reset to Original";
|
||||
"lng_profile_photo_reset_sure" = "Are you sure you want to reset {user}'s photo to the original?";
|
||||
"lng_profile_photo_from_clipboard" = "From clipboard";
|
||||
"lng_profile_suggest_sure" = "You can suggest {user} to set this photo for their Telegram profile.";
|
||||
"lng_profile_suggest_button" = "Suggest";
|
||||
@@ -1950,6 +1961,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_manage_monoforum_price" = "Price for each message";
|
||||
"lng_manage_monoforum_about" = "Allow users to send messages to your channel, with the option to charge a fee for each message.";
|
||||
"lng_manage_monoforum_price_about" = "Your channel will receive {percent} of the selected fee ({amount}) for each incoming message.";
|
||||
"lng_manage_monoforum_link_subtitle" = "Link to direct messages";
|
||||
|
||||
"lng_manage_history_visibility_title" = "Chat history for new members";
|
||||
"lng_manage_history_visibility_shown" = "Visible";
|
||||
@@ -2221,6 +2233,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_gift_transferred_unknown" = "Someone transferred you a gift";
|
||||
"lng_action_gift_transferred_unknown_channel" = "Someone transferred a gift to {channel}";
|
||||
"lng_action_gift_transferred_self" = "You transferred a unique collectible";
|
||||
"lng_action_gift_displayed_self" = "You've started displaying {name} on your Telegram profile page.";
|
||||
"lng_action_gift_transferred_self_channel" = "You transferred a gift to {channel}";
|
||||
"lng_action_gift_transferred_mine" = "You transferred a gift to {user}";
|
||||
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
|
||||
@@ -2261,6 +2274,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_suggested_video_me" = "You suggested this photo for {user}'s Telegram profile.";
|
||||
"lng_action_suggested_video" = "{user} suggests this photo for your Telegram profile.";
|
||||
"lng_action_suggested_video_button" = "View Photo";
|
||||
"lng_action_suggested_birthday_me" = "You suggest {user} add a date of birth:";
|
||||
"lng_action_suggested_birthday" = "{user} suggests you add your date of birth:";
|
||||
"lng_action_suggested_birtday_button" = "View";
|
||||
"lng_action_attach_menu_bot_allowed" = "You allowed this bot to message you when you added it to your attachment menu.";
|
||||
"lng_action_webapp_bot_allowed" = "You allowed this bot to message you in its web-app.";
|
||||
"lng_action_set_wallpaper_me" = "You set a new wallpaper for this chat";
|
||||
@@ -2281,6 +2297,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_topic_hidden" = "\"{topic}\" was hidden";
|
||||
"lng_action_topic_unhidden" = "\"{topic}\" was unhidden";
|
||||
"lng_action_topic_placeholder" = "topic";
|
||||
"lng_action_topic_bot_thread" = "thread";
|
||||
"lng_action_topic_renamed" = "{from} renamed the {link} to \"{title}\"";
|
||||
"lng_action_topic_icon_changed" = "{from} changed the {link} icon to {emoji}";
|
||||
"lng_action_topic_icon_removed" = "{from} removed the {link} icon";
|
||||
@@ -2578,6 +2595,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_bot_allow_write_title" = "Allow messaging";
|
||||
"lng_bot_allow_write" = "Do you want to allow this bot to send you messages?";
|
||||
"lng_bot_allow_write_confirm" = "Allow";
|
||||
"lng_bot_new_chat" = "New Chat";
|
||||
"lng_bot_new_thread_title" = "New Thread";
|
||||
"lng_bot_new_thread_about" = "Type any message to create a new thread.";
|
||||
"lng_bot_show_threads_list" = "Show Threads List";
|
||||
|
||||
"lng_attach_failed" = "Failed";
|
||||
"lng_attach_file" = "File";
|
||||
@@ -3582,17 +3603,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_premium_by_stars" = "or {amount}";
|
||||
"lng_gift_stars_subtitle" = "Gift Stars";
|
||||
"lng_gift_stars_about" = "Give {name} gifts that can be kept on your profile or converted to Stars. {link}";
|
||||
"lng_gift_stars_about_collectibles" = "Collectible gifts are unique digital items you can exchange or sell. {link}";
|
||||
"lng_gift_stars_link" = "What are Stars >";
|
||||
"lng_gift_stars_limited" = "limited";
|
||||
"lng_gift_stars_sold_out" = "sold out";
|
||||
"lng_gift_stars_resale" = "resale";
|
||||
"lng_gift_stars_on_sale" = "on sale";
|
||||
"lng_gift_stars_premium" = "premium";
|
||||
"lng_gift_stars_your_left#one" = "{count} left";
|
||||
"lng_gift_stars_your_left#other" = "{count} left";
|
||||
"lng_gift_stars_your_finished" = "none left";
|
||||
"lng_gift_stars_tabs_all" = "All Gifts";
|
||||
"lng_gift_stars_tabs_my" = "My Gifts";
|
||||
"lng_gift_stars_tabs_limited" = "Limited";
|
||||
"lng_gift_stars_tabs_in_stock" = "In Stock";
|
||||
"lng_gift_stars_tabs_resale" = "Resale";
|
||||
"lng_gift_stars_tabs_collectibles" = "Collectibles";
|
||||
"lng_gift_send_title" = "Send a Gift";
|
||||
"lng_gift_send_message" = "Enter Message";
|
||||
"lng_gift_send_anonymous" = "Hide My Name";
|
||||
@@ -3661,9 +3684,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_self_about" = "Buy yourself a gift to display on your page or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others later.";
|
||||
"lng_gift_channel_title" = "Send a Gift";
|
||||
"lng_gift_channel_about" = "Select a gift to show appreciation for {name}.";
|
||||
"lng_gift_released_by" = "Released by {name}";
|
||||
"lng_gift_released_by" = "released by {name}";
|
||||
"lng_gift_unique_owner" = "Owner";
|
||||
"lng_gift_unique_address_copied" = "Address copied to clipboard.";
|
||||
"lng_gift_unique_telegram" = "Telegram";
|
||||
"lng_gift_unique_status" = "Status";
|
||||
"lng_gift_unique_status_non" = "Non-Unique";
|
||||
"lng_gift_unique_upgrade" = "Upgrade";
|
||||
@@ -3685,6 +3709,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_unique_info_sender_comment" = "Gifted by {from} to {recipient} on {date} with the comment \"{text}\".";
|
||||
"lng_gift_unique_info_reciever" = "Gifted to {recipient} on {date}.";
|
||||
"lng_gift_unique_info_reciever_comment" = "Gifted to {recipient} on {date} with the comment \"{text}\".";
|
||||
"lng_gift_unique_info_remove_title" = "Remove Description";
|
||||
"lng_gift_unique_info_remove_text" = "Do you want to permanently remove this description from your gift?";
|
||||
"lng_gift_unique_info_remove_confirm" = "Remove for {cost}";
|
||||
"lng_gift_unique_info_removed" = "Removed {name}'s Description!";
|
||||
"lng_gift_availability_left#one" = "{count} of {amount} left";
|
||||
"lng_gift_availability_left#other" = "{count} of {amount} left";
|
||||
"lng_gift_availability_none" = "None of {amount} left";
|
||||
@@ -3749,6 +3777,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_upgrade_tradable_about_user" = "{name} will be able to sell the gift on Telegram and NFT marketplaces.";
|
||||
"lng_gift_upgrade_tradable_about_channel" = "Admins of {name} will be able to sell the gift on Telegram and NFT marketplaces.";
|
||||
"lng_gift_upgrade_button" = "Upgrade for {price}";
|
||||
"lng_gift_upgrade_decreases" = "Price decreases in {time}";
|
||||
"lng_gift_upgrade_see_table" = "See how this price will decrease {arrow}";
|
||||
"lng_gift_upgrade_prices_about" = "Upgrade cost drops every minute.";
|
||||
"lng_gift_upgrade_prices_title" = "Upgrade Cost";
|
||||
"lng_gift_upgrade_prices_subtitle" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
||||
"lng_gift_upgrade_free" = "Upgrade for Free";
|
||||
"lng_gift_upgrade_confirm" = "Confirm";
|
||||
"lng_gift_upgrade_add_my" = "Add my name to the gift";
|
||||
@@ -3793,6 +3826,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_transfer_sell" = "Sell";
|
||||
"lng_gift_transfer_update" = "Change Price";
|
||||
"lng_gift_transfer_unlist" = "Unlist";
|
||||
"lng_gift_transfer_locked_title" = "Action Locked";
|
||||
"lng_gift_transfer_locked_text" = "Transfer this gift to your Telegram account on Fragment to unlock this action.";
|
||||
"lng_gift_sell_unlist_title" = "Unlist {name}";
|
||||
"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?";
|
||||
"lng_gift_sell_title" = "Price in Stars";
|
||||
@@ -3871,6 +3906,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_collection_delete_sure" = "Are you sure you want to delete this collection?";
|
||||
"lng_gift_collection_delete_button" = "Delete";
|
||||
"lng_gift_collection_add_to" = "Add to Collection";
|
||||
"lng_gift_collection_reorder" = "Reorder";
|
||||
"lng_gift_collection_reorder_exit" = "Apply Reorder";
|
||||
"lng_gift_collection_remove_from" = "Remove from Collection";
|
||||
"lng_gift_locked_title" = "Gift Locked";
|
||||
|
||||
"lng_accounts_limit_title" = "Limit Reached";
|
||||
@@ -4356,6 +4394,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_view_group" = "View group info";
|
||||
"lng_context_view_channel" = "View channel info";
|
||||
"lng_context_view_topic" = "View topic info";
|
||||
"lng_context_view_thread" = "View thread info";
|
||||
"lng_context_hide_psa" = "Hide this announcement";
|
||||
"lng_context_pin_to_top" = "Pin";
|
||||
"lng_context_unpin_from_top" = "Unpin";
|
||||
@@ -4492,6 +4531,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_add_tag_phrase" = "to messages {arrow}";
|
||||
"lng_add_tag_phrase_long" = "to your Saved Messages {arrow}";
|
||||
"lng_unlock_tags" = "Unlock";
|
||||
"lng_add_tag_selector#one" = "You can add a tag to the message";
|
||||
"lng_add_tag_selector#other" = "You can add a tag to the messages";
|
||||
"lng_message_tagged_with" = "Message tagged with {emoji}";
|
||||
"lng_tagged_view_saved" = "View";
|
||||
|
||||
"lng_context_animated_emoji" = "This message contains emoji from **{name} pack**.";
|
||||
"lng_context_animated_emoji_many#one" = "This message contains emoji from **{count} pack**.";
|
||||
@@ -4773,6 +4816,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_contact_phone_after" = "Phone number will be visible once {user} adds you as a contact.";
|
||||
"lng_contact_share_phone" = "Share my phone number";
|
||||
"lng_contact_phone_will_be_shared" = "You can make your phone visible to {user}.";
|
||||
"lng_contact_add_notes" = "Note";
|
||||
"lng_contact_add_notes_about" = "Notes are only visible to you.";
|
||||
"lng_contact_notes_limit_reached#one" = "You've reached the contact note limit. Please make the note shorter by {count} character.";
|
||||
"lng_contact_notes_limit_reached#other" = "You've reached the contact note limit. Please make the note shorter by {count} characters.";
|
||||
"lng_suggest_photo_for" = "Suggest Photo for {user}";
|
||||
"lng_suggest_birthday" = "Suggest Date of Birth";
|
||||
"lng_suggest_birthday_box_title" = "{user}'s Date of Birth";
|
||||
"lng_suggest_birthday_box_confirm" = "Suggest";
|
||||
"lng_set_photo_for_user" = "Set Photo for {user}";
|
||||
"lng_contact_photo_replace_info" = "You can replace {user}'s photo with another photo that only you will see.";
|
||||
"lng_edit_contact_title" = "Edit contact";
|
||||
"lng_edit_channel_title" = "Edit channel";
|
||||
"lng_edit_bot_title" = "Edit bot";
|
||||
@@ -5008,8 +5061,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_payments_webview_no_use" = "Unfortunately, you can't use payments with current system configuration.";
|
||||
"lng_payments_webview_install_edge" = "Please install {link}.";
|
||||
"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.";
|
||||
"lng_payments_webview_enable_opengl" = "Please enable OpenGL in application settings.";
|
||||
"lng_payments_webview_switch_x11" = "Unsupported display server. Please switch to X11.";
|
||||
"lng_payments_webview_update_windows" = "Please update your system to Windows 8.1 or later.";
|
||||
"lng_payments_sure_close" = "Are you sure you want to close this payment form? The changes you made will be lost.";
|
||||
"lng_payments_receipt_label" = "Receipt";
|
||||
@@ -5160,6 +5211,7 @@ 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_video" = "Video";
|
||||
"lng_group_call_message" = "Message";
|
||||
"lng_group_call_screen_share_start" = "Share Screen";
|
||||
"lng_group_call_screen_share_stop" = "Stop Sharing";
|
||||
"lng_group_call_screen_title" = "Screen {index}";
|
||||
@@ -5221,6 +5273,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_group_call_invite_search_results" = "Search results";
|
||||
"lng_group_call_invite_limit" = "This is currently the maximum allowed number of participants.";
|
||||
"lng_group_call_new_muted" = "Mute new participants";
|
||||
"lng_group_call_enable_messages" = "Enable messages";
|
||||
"lng_group_call_speakers" = "Speakers";
|
||||
"lng_group_call_microphone" = "Microphone";
|
||||
"lng_group_call_push_to_talk" = "Push-to-Talk Shortcut";
|
||||
@@ -6174,6 +6227,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
"lng_todo_title" = "Checklist";
|
||||
"lng_todo_title_group" = "Group Checklist";
|
||||
"lng_todo_title_user" = "Checklist";
|
||||
"lng_todo_completed#one" = "{count} of {total} completed";
|
||||
"lng_todo_completed#other" = "{count} of {total} completed";
|
||||
"lng_todo_completed_none" = "None of {total} completed";
|
||||
@@ -6441,6 +6495,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_ringtones_error_max_size" = "Sorry, but your file is too big. The maximum size for ringtones is {size}.";
|
||||
"lng_ringtones_error_max_duration" = "Sorry, but your file is too long. The maximum duration for ringtones is {duration}.";
|
||||
|
||||
"lng_bot_thread_edit" = "Edit Thread";
|
||||
"lng_bot_thread_title" = "Thread Name";
|
||||
"lng_bot_thread_choose_title_and_icon" = "Choose a thread name and icon";
|
||||
|
||||
"lng_forum_topic_new" = "New Topic";
|
||||
"lng_forum_topic_edit" = "Edit Topic";
|
||||
"lng_forum_topic_title" = "Topic Name";
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
<file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file>
|
||||
<file alias="no_chats.tgs">../../animations/no_chats.tgs</file>
|
||||
<file alias="transcribe_loading.tgs">../../animations/transcribe_loading.tgs</file>
|
||||
<file alias="cake.tgs">../../animations/cake.tgs</file>
|
||||
<file alias="camera_outline.tgs">../../animations/camera_outline.tgs</file>
|
||||
<file alias="photo_suggest_icon.tgs">../../animations/photo_suggest_icon.tgs</file>
|
||||
<file alias="toast/saved_messages.tgs">../../animations/toast/saved_messages.tgs</file>
|
||||
<file alias="toast/tagged.tgs">../../animations/toast/tagged.tgs</file>
|
||||
|
||||
<file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file>
|
||||
<file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="6.1.2.0" />
|
||||
Version="6.2.3.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 6,1,2,0
|
||||
PRODUCTVERSION 6,1,2,0
|
||||
FILEVERSION 6,2,3,0
|
||||
PRODUCTVERSION 6,2,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "6.1.2.0"
|
||||
VALUE "FileVersion", "6.2.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.1.2.0"
|
||||
VALUE "ProductVersion", "6.2.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 6,1,2,0
|
||||
PRODUCTVERSION 6,1,2,0
|
||||
FILEVERSION 6,2,3,0
|
||||
PRODUCTVERSION 6,2,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "6.1.2.0"
|
||||
VALUE "FileVersion", "6.2.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "6.1.2.0"
|
||||
VALUE "ProductVersion", "6.2.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -98,7 +98,7 @@ Authorizations::Authorizations(not_null<ApiWrap*> api)
|
||||
api->session().saveSettingsDelayed();
|
||||
}) {
|
||||
_unreviewed = api->session().settings().unreviewed();
|
||||
removeExpiredUnreviewed();
|
||||
crl::on_main(&api->session(), [=] { removeExpiredUnreviewed(); });
|
||||
Core::App().settings().deviceModelChanges(
|
||||
) | rpl::start_with_next([=](const QString &model) {
|
||||
auto changed = false;
|
||||
|
||||
@@ -159,6 +159,7 @@ void MessagesSearch::searchReceived(
|
||||
// Don't apply cached data!
|
||||
owner.processUsers(data.vusers());
|
||||
owner.processChats(data.vchats());
|
||||
_history->peer->processTopics(data.vtopics());
|
||||
}
|
||||
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
|
||||
const auto total = int(data.vmessages().v.size());
|
||||
@@ -168,6 +169,7 @@ void MessagesSearch::searchReceived(
|
||||
// Don't apply cached data!
|
||||
owner.processUsers(data.vusers());
|
||||
owner.processChats(data.vchats());
|
||||
_history->peer->processTopics(data.vtopics());
|
||||
}
|
||||
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
|
||||
// data.vnext_rate() is used only in global search.
|
||||
@@ -178,17 +180,14 @@ void MessagesSearch::searchReceived(
|
||||
// Don't apply cached data!
|
||||
owner.processUsers(data.vusers());
|
||||
owner.processChats(data.vchats());
|
||||
}
|
||||
if (const auto channel = _history->peer->asChannel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
if (_requestId != 0) {
|
||||
// Don't apply cached data!
|
||||
channel->processTopics(data.vtopics());
|
||||
if (const auto channel = _history->peer->asChannel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
} else {
|
||||
LOG(("API Error: "
|
||||
"received messages.channelMessages when no channel "
|
||||
"was passed!"));
|
||||
}
|
||||
} else {
|
||||
LOG(("API Error: "
|
||||
"received messages.channelMessages when no channel "
|
||||
"was passed!"));
|
||||
_history->peer->processTopics(data.vtopics());
|
||||
}
|
||||
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
|
||||
const auto total = int(data.vcount().v);
|
||||
|
||||
@@ -22,13 +22,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace Api {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
|
||||
return TimeId(msgId >> 32);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Polls::Polls(not_null<ApiWrap*> api)
|
||||
: _session(&api->session())
|
||||
|
||||
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "payments/payments_form.h"
|
||||
#include "ui/chat/chat_style.h" // ColorCollectible
|
||||
#include "ui/text/format_values.h"
|
||||
|
||||
namespace Api {
|
||||
@@ -865,6 +866,7 @@ std::optional<Data::StarGift> FromTL(
|
||||
.lastSaleDate = data.vlast_sale_date().value_or_empty(),
|
||||
.lockedUntilDate = data.vlocked_until_date().value_or_empty(),
|
||||
.requirePremium = data.is_require_premium(),
|
||||
.peerColorAvailable = data.is_peer_color_available(),
|
||||
.upgradable = data.vupgrade_stars().has_value(),
|
||||
.birthday = data.is_birthday(),
|
||||
.soldOut = data.is_sold_out(),
|
||||
@@ -900,6 +902,12 @@ std::optional<Data::StarGift> FromTL(
|
||||
const auto themeUser = themeUserId
|
||||
? session->data().peer(themeUserId).get()
|
||||
: nullptr;
|
||||
const auto colorCollectible = (data.vpeer_color()
|
||||
&& data.vpeer_color()->type() == mtpc_peerColorCollectible)
|
||||
? std::make_shared<Ui::ColorCollectible>(
|
||||
Data::ParseColorCollectible(
|
||||
data.vpeer_color()->c_peerColorCollectible()))
|
||||
: nullptr;
|
||||
auto result = Data::StarGift{
|
||||
.id = data.vid().v,
|
||||
.unique = std::make_shared<Data::UniqueGift>(Data::UniqueGift{
|
||||
@@ -907,11 +915,15 @@ std::optional<Data::StarGift> FromTL(
|
||||
.initialGiftId = data.vgift_id().v,
|
||||
.slug = qs(data.vslug()),
|
||||
.title = qs(data.vtitle()),
|
||||
.giftAddress = qs(data.vgift_address().value_or_empty()),
|
||||
.ownerAddress = qs(data.vowner_address().value_or_empty()),
|
||||
.ownerName = qs(data.vowner_name().value_or_empty()),
|
||||
.ownerId = (data.vowner_id()
|
||||
? peerFromMTP(*data.vowner_id())
|
||||
: PeerId()),
|
||||
.hostId = (data.vhost_id()
|
||||
? peerFromMTP(*data.vhost_id())
|
||||
: PeerId()),
|
||||
.releasedBy = releasedBy,
|
||||
.themeUser = themeUser,
|
||||
.nanoTonForResale = FindTonForResale(data.vresell_amount()),
|
||||
@@ -930,6 +942,7 @@ std::optional<Data::StarGift> FromTL(
|
||||
data.vvalue_amount().value_or_empty()),
|
||||
})
|
||||
: nullptr),
|
||||
.peerColor = colorCollectible,
|
||||
}),
|
||||
.document = model->document,
|
||||
.releasedBy = releasedBy,
|
||||
@@ -986,6 +999,8 @@ std::optional<Data::SavedStarGift> FromTL(
|
||||
.starsConverted = int64(data.vconvert_stars().value_or_empty()),
|
||||
.starsUpgradedBySender = int64(
|
||||
data.vupgrade_stars().value_or_empty()),
|
||||
.starsForDetailsRemove = int64(
|
||||
data.vdrop_original_details_stars().value_or_empty()),
|
||||
.giftPrepayUpgradeHash = qs(
|
||||
data.vprepaid_upgrade_hash().value_or_empty()),
|
||||
.fromId = (data.vfrom_id()
|
||||
|
||||
@@ -25,10 +25,6 @@ namespace {
|
||||
|
||||
constexpr auto kSendTogglesDelay = 3 * crl::time(1000);
|
||||
|
||||
[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
|
||||
return TimeId(msgId >> 32);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TodoLists::TodoLists(not_null<ApiWrap*> api)
|
||||
|
||||
@@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "history/history_streamed_drafts.h"
|
||||
#include "history/history_unread_things.h"
|
||||
#include "core/application.h"
|
||||
#include "storage/storage_account.h"
|
||||
@@ -1096,6 +1097,9 @@ void Updates::handleSendActionUpdate(
|
||||
const auto from = (fromId == session().userPeerId())
|
||||
? session().user().get()
|
||||
: session().data().peerLoaded(fromId);
|
||||
const auto when = requestingDifference()
|
||||
? 0
|
||||
: base::unixtime::now();
|
||||
if (action.type() == mtpc_speakingInGroupCallAction) {
|
||||
handleSpeakingInCall(peer, fromId, from);
|
||||
}
|
||||
@@ -1108,10 +1112,11 @@ void Updates::handleSendActionUpdate(
|
||||
const auto &data = action.c_sendMessageEmojiInteractionSeen();
|
||||
handleEmojiInteraction(peer, qs(data.vemoticon()));
|
||||
return;
|
||||
} else if (action.type() == mtpc_sendMessageTextDraftAction) {
|
||||
const auto &data = action.c_sendMessageTextDraftAction();
|
||||
history->streamedDrafts().apply(rootId, fromId, when, data);
|
||||
return;
|
||||
}
|
||||
const auto when = requestingDifference()
|
||||
? 0
|
||||
: base::unixtime::now();
|
||||
session().data().sendActionManager().registerFor(
|
||||
history,
|
||||
rootId,
|
||||
@@ -1622,6 +1627,17 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
auto &d = update.c_updateNewChannelMessage();
|
||||
auto channel = session().data().channelLoaded(peerToChannel(PeerFromMessage(d.vmessage())));
|
||||
const auto isDataLoaded = AllDataLoadedForMessage(&session(), d.vmessage());
|
||||
{
|
||||
// Todo delete.
|
||||
const auto messageId = IdFromMessage(d.vmessage());
|
||||
if (const auto history = channel ? session().data().historyLoaded(channel) : nullptr) {
|
||||
if (history->isUnknownMessageDeleted(messageId)) {
|
||||
LOG(("Unknown message deleted detected for channel %1, message %2")
|
||||
.arg(channel->id.value & PeerId::kChatTypeMask)
|
||||
.arg(messageId.bare));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!requestingDifference() && (!channel || isDataLoaded != DataIsLoadedResult::Ok)) {
|
||||
MTP_LOG(0, ("getDifference "
|
||||
"{ good - after not all data loaded in updateNewChannelMessage }%1"
|
||||
@@ -1951,7 +1967,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
auto &d = update.c_updateUserTyping();
|
||||
handleSendActionUpdate(
|
||||
peerFromUser(d.vuser_id()),
|
||||
0,
|
||||
d.vtop_msg_id().value_or_empty(),
|
||||
peerFromUser(d.vuser_id()),
|
||||
d.vaction());
|
||||
} break;
|
||||
@@ -2123,7 +2139,9 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
case mtpc_updateGroupCallParticipants:
|
||||
case mtpc_updateGroupCallChainBlocks:
|
||||
case mtpc_updateGroupCallConnection:
|
||||
case mtpc_updateGroupCall: {
|
||||
case mtpc_updateGroupCall:
|
||||
case mtpc_updateGroupCallMessage:
|
||||
case mtpc_updateGroupCallEncryptedMessage: {
|
||||
Core::App().calls().handleUpdate(&session(), update);
|
||||
} break;
|
||||
|
||||
@@ -2484,9 +2502,9 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
}
|
||||
} break;
|
||||
|
||||
case mtpc_updateChannelPinnedTopic: {
|
||||
const auto &d = update.c_updateChannelPinnedTopic();
|
||||
const auto peerId = peerFromChannel(d.vchannel_id());
|
||||
case mtpc_updatePinnedForumTopic: {
|
||||
const auto &d = update.c_updatePinnedForumTopic();
|
||||
const auto peerId = peerFromMTP(d.vpeer());
|
||||
if (const auto peer = session().data().peerLoaded(peerId)) {
|
||||
const auto rootId = d.vtopic_id().v;
|
||||
if (const auto topic = peer->forumTopicFor(rootId)) {
|
||||
@@ -2497,9 +2515,9 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
||||
}
|
||||
} break;
|
||||
|
||||
case mtpc_updateChannelPinnedTopics: {
|
||||
const auto &d = update.c_updateChannelPinnedTopics();
|
||||
const auto peerId = peerFromChannel(d.vchannel_id());
|
||||
case mtpc_updatePinnedForumTopics: {
|
||||
const auto &d = update.c_updatePinnedForumTopics();
|
||||
const auto peerId = peerFromMTP(d.vpeer());
|
||||
if (const auto peer = session().data().peerLoaded(peerId)) {
|
||||
if (const auto forum = peer->forum()) {
|
||||
const auto done = [&] {
|
||||
|
||||
@@ -134,7 +134,7 @@ rpl::producer<rpl::no_value, Usernames::Error> Usernames::toggle(
|
||||
if (list.empty()) {
|
||||
if (error == Error::Unknown) {
|
||||
it->second.done.fire_done();
|
||||
} else if (error == Error::TooMuch) {
|
||||
} else {
|
||||
it->second.done.fire_error_copy(error);
|
||||
}
|
||||
_toggleRequests.remove(peerId);
|
||||
@@ -149,6 +149,8 @@ rpl::producer<rpl::no_value, Usernames::Error> Usernames::toggle(
|
||||
const auto type = error.type();
|
||||
if (type == u"USERNAMES_ACTIVE_TOO_MUCH"_q) {
|
||||
pop(Error::TooMuch);
|
||||
} else if (type.startsWith(u"FLOOD_WAIT_"_q)) {
|
||||
pop(Error::Flood);
|
||||
} else {
|
||||
pop(Error::Unknown);
|
||||
}
|
||||
@@ -158,19 +160,19 @@ rpl::producer<rpl::no_value, Usernames::Error> Usernames::toggle(
|
||||
_api.request(MTPaccount_ToggleUsername(
|
||||
MTP_string(username),
|
||||
MTP_bool(active)
|
||||
)).done(done).fail(fail).send();
|
||||
)).done(done).fail(fail).handleFloodErrors().send();
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
_api.request(MTPchannels_ToggleUsername(
|
||||
channel->inputChannel,
|
||||
MTP_string(username),
|
||||
MTP_bool(active)
|
||||
)).done(done).fail(fail).send();
|
||||
)).done(done).fail(fail).handleFloodErrors().send();
|
||||
} else if (const auto botUserInput = BotUserInput(peer)) {
|
||||
_api.request(MTPbots_ToggleUsername(
|
||||
*botUserInput,
|
||||
MTP_string(username),
|
||||
MTP_bool(active)
|
||||
)).done(done).fail(fail).send();
|
||||
)).done(done).fail(fail).handleFloodErrors().send();
|
||||
} else {
|
||||
return rpl::never<rpl::no_value, Error>();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class Usernames final {
|
||||
public:
|
||||
enum class Error {
|
||||
TooMuch,
|
||||
Flood,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
|
||||
@@ -107,10 +107,6 @@ using PhotoFileLocationId = Data::PhotoFileLocationId;
|
||||
using DocumentFileLocationId = Data::DocumentFileLocationId;
|
||||
using UpdatedFileReferences = Data::UpdatedFileReferences;
|
||||
|
||||
[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
|
||||
return TimeId(msgId >> 32);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> ShowForPeer(
|
||||
not_null<PeerData*> peer) {
|
||||
if (const auto window = Core::App().windowFor(peer)) {
|
||||
@@ -155,6 +151,14 @@ void ShowChannelsLimitBox(not_null<PeerData*> peer) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Api {
|
||||
|
||||
TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
|
||||
return TimeId(msgId >> 32);
|
||||
}
|
||||
|
||||
} // namespace Api
|
||||
|
||||
ApiWrap::ApiWrap(not_null<Main::Session*> session)
|
||||
: MTP::Sender(&session->account().mtp())
|
||||
, _session(session)
|
||||
@@ -205,6 +209,27 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
|
||||
|
||||
ApiWrap::~ApiWrap() = default;
|
||||
|
||||
void ApiWrap::ProcessRecentSelfForwards(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdates &updates,
|
||||
PeerId targetPeerId,
|
||||
PeerId fromPeerId) {
|
||||
auto newIds = MessageIdsList();
|
||||
updates.match([&](const MTPDupdates &data) {
|
||||
for (const auto &update : data.vupdates().v) {
|
||||
update.match([&](const MTPDupdateMessageID &d) {
|
||||
newIds.push_back(FullMsgId(targetPeerId, d.vid().v));
|
||||
}, [](const auto &) {});
|
||||
}
|
||||
}, [](const auto &) {});
|
||||
if (!newIds.empty()) {
|
||||
session->data().addRecentSelfForwards({
|
||||
.fromPeerId = fromPeerId,
|
||||
.ids = newIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Main::Session &ApiWrap::session() const {
|
||||
return *_session;
|
||||
}
|
||||
@@ -375,9 +400,9 @@ void ApiWrap::savePinnedOrder(not_null<Data::Forum*> forum) {
|
||||
order,
|
||||
ranges::back_inserter(topics),
|
||||
input);
|
||||
request(MTPchannels_ReorderPinnedForumTopics(
|
||||
MTP_flags(MTPchannels_ReorderPinnedForumTopics::Flag::f_force),
|
||||
forum->channel()->inputChannel,
|
||||
request(MTPmessages_ReorderPinnedForumTopics(
|
||||
MTP_flags(MTPmessages_ReorderPinnedForumTopics::Flag::f_force),
|
||||
forum->peer()->input,
|
||||
MTP_vector(topics)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
applyUpdates(result);
|
||||
@@ -1089,6 +1114,8 @@ void ApiWrap::requestWallPaper(
|
||||
void ApiWrap::requestFullPeer(not_null<PeerData*> peer) {
|
||||
if (_fullPeerRequests.contains(peer)) {
|
||||
return;
|
||||
} else if (!peer->isUser() && !peer->barSettings().has_value()) {
|
||||
requestPeerSettings(peer);
|
||||
}
|
||||
|
||||
const auto requestId = [&] {
|
||||
@@ -1887,7 +1914,7 @@ void ApiWrap::sendNotifySettingsUpdates() {
|
||||
for (const auto topic : base::take(_updateNotifyTopics)) {
|
||||
request(MTPaccount_UpdateNotifySettings(
|
||||
MTP_inputNotifyForumTopic(
|
||||
topic->channel()->input,
|
||||
topic->peer()->input,
|
||||
MTP_int(topic->rootId())),
|
||||
topic->notify().serialize()
|
||||
)).afterDelay(kSmallDelayMs).send();
|
||||
@@ -2180,7 +2207,7 @@ void ApiWrap::saveDraftsToCloud() {
|
||||
history->finishSavingCloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
const auto cloudDraft = history->cloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId);
|
||||
@@ -2201,7 +2228,7 @@ void ApiWrap::saveDraftsToCloud() {
|
||||
history->finishSavingCloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
const auto cloudDraft = history->cloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId);
|
||||
@@ -3052,18 +3079,20 @@ void ApiWrap::requestMessageAfterDate(
|
||||
return &messages.vmessages().v;
|
||||
};
|
||||
const auto list = result.match([&](
|
||||
const MTPDmessages_messages &data) {
|
||||
const MTPDmessages_messages &data) {
|
||||
peer->processTopics(data.vtopics());
|
||||
return handleMessages(data);
|
||||
}, [&](const MTPDmessages_messagesSlice &data) {
|
||||
peer->processTopics(data.vtopics());
|
||||
return handleMessages(data);
|
||||
}, [&](const MTPDmessages_channelMessages &data) {
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
channel->processTopics(data.vtopics());
|
||||
} else {
|
||||
LOG(("API Error: received messages.channelMessages when "
|
||||
"no channel was passed! (ApiWrap::jumpToDate)"));
|
||||
}
|
||||
peer->processTopics(data.vtopics());
|
||||
return handleMessages(data);
|
||||
}, [&](const MTPDmessages_messagesNotModified &) {
|
||||
LOG(("API Error: received messages.messagesNotModified! "
|
||||
@@ -3515,6 +3544,13 @@ void ApiWrap::forwardMessages(
|
||||
shared->callback();
|
||||
}
|
||||
finish();
|
||||
if (peer->isSelf() && session().premium()) {
|
||||
ProcessRecentSelfForwards(
|
||||
_session,
|
||||
result,
|
||||
peer->id,
|
||||
forwardFrom->id);
|
||||
}
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
if (idsCopy) {
|
||||
for (const auto &[randomId, itemId] : *idsCopy) {
|
||||
@@ -3879,7 +3915,9 @@ void ApiWrap::sendShortcutMessages(
|
||||
}).send();
|
||||
}
|
||||
|
||||
void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
void ApiWrap::sendMessage(
|
||||
MessageToSend &&message,
|
||||
std::optional<MsgId> localMessageId) {
|
||||
const auto history = message.action.history;
|
||||
const auto peer = history->peer;
|
||||
auto &textWithTags = message.textWithTags;
|
||||
@@ -3929,7 +3967,9 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
|
||||
auto newId = FullMsgId(
|
||||
peer->id,
|
||||
_session->data().nextLocalMessageId());
|
||||
localMessageId
|
||||
? std::exchange(localMessageId, std::nullopt).value()
|
||||
: _session->data().nextLocalMessageId());
|
||||
auto randomId = base::RandomValue<uint64>();
|
||||
|
||||
TextUtilities::Trim(sending);
|
||||
@@ -4055,7 +4095,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
history->finishSavingCloudDraft(
|
||||
draftTopicRootId,
|
||||
draftMonoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
}
|
||||
};
|
||||
const auto fail = [=](
|
||||
@@ -4070,7 +4110,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
history->finishSavingCloudDraft(
|
||||
draftTopicRootId,
|
||||
draftMonoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
}
|
||||
};
|
||||
const auto mtpShortcut = Data::ShortcutIdToMTP(
|
||||
@@ -4266,7 +4306,7 @@ void ApiWrap::sendInlineResult(
|
||||
history->finishSavingCloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
if (done) {
|
||||
done(true);
|
||||
}
|
||||
@@ -4275,7 +4315,7 @@ void ApiWrap::sendInlineResult(
|
||||
history->finishSavingCloudDraft(
|
||||
topicRootId,
|
||||
monoforumPeerId,
|
||||
UnixtimeFromMsgId(response.outerMsgId));
|
||||
Api::UnixtimeFromMsgId(response.outerMsgId));
|
||||
if (done) {
|
||||
done(false);
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ QString RequestKey(Types &&...values) {
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId);
|
||||
|
||||
} // namespace Api
|
||||
|
||||
class ApiWrap final : public MTP::Sender {
|
||||
@@ -363,7 +365,9 @@ public:
|
||||
void sendShortcutMessages(
|
||||
not_null<PeerData*> peer,
|
||||
BusinessShortcutId id);
|
||||
void sendMessage(MessageToSend &&message);
|
||||
void sendMessage(
|
||||
MessageToSend &&message,
|
||||
std::optional<MsgId> localMessageId = std::nullopt);
|
||||
void sendBotStart(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
not_null<UserData*> bot,
|
||||
@@ -428,6 +432,12 @@ public:
|
||||
|
||||
static constexpr auto kJoinErrorDuration = 5 * crl::time(1000);
|
||||
|
||||
static void ProcessRecentSelfForwards(
|
||||
not_null<Main::Session*> session,
|
||||
const MTPUpdates &updates,
|
||||
PeerId targetPeerId,
|
||||
PeerId fromPeerId);
|
||||
|
||||
private:
|
||||
struct MessageDataRequest {
|
||||
using Callbacks = std::vector<Fn<void()>>;
|
||||
|
||||
@@ -754,6 +754,12 @@ createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px);
|
||||
|
||||
sendGifWithCaptionEmojiPosition: point(-30px, 23px);
|
||||
|
||||
notesFieldWithEmoji: InputField(defaultInputField) {
|
||||
textMargins: margins(0px, 28px, 30px, 4px);
|
||||
// border: 0px;
|
||||
// borderActive: 0px;
|
||||
}
|
||||
|
||||
backgroundCheckbox: Checkbox(defaultCheckbox) {
|
||||
textFg: msgServiceFg;
|
||||
textFgActive: msgServiceFg;
|
||||
@@ -807,6 +813,7 @@ urlAuthCheckbox: Checkbox(defaultBoxCheckbox) {
|
||||
|
||||
addContactFieldMargin: margins(19px, 0px, 19px, 10px);
|
||||
addContactWarningMargin: margins(19px, 10px, 19px, 5px);
|
||||
editContactSuggestBirthday: icon{{ "settings/birthday_add-22x22", lightButtonFg }};
|
||||
blockUserConfirmation: FlatLabel(boxLabel) {
|
||||
minWidth: 240px;
|
||||
}
|
||||
|
||||
@@ -112,8 +112,9 @@ void DeleteMessagesBox::prepare() {
|
||||
Ui::Text::RichLangValue);
|
||||
deleteStyle = &st::attentionBoxButton;
|
||||
} else if (_wipeHistoryJustClear) {
|
||||
_revokeJustClearForChannel = true;
|
||||
details.text = (peer->isChannel() && !peer->isMegagroup())
|
||||
const auto isChannel = peer->isChannel() && !peer->isMegagroup();
|
||||
_revokeJustClearForChannel = isChannel;
|
||||
details.text = isChannel
|
||||
? tr::lng_sure_delete_channel_history(
|
||||
tr::now,
|
||||
lt_channel,
|
||||
|
||||
@@ -8,37 +8,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/edit_privacy_box.h"
|
||||
|
||||
#include "api/api_global_privacy.h"
|
||||
#include "apiwrap.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "boxes/peers/edit_peer_invite_link.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "settings/settings_privacy_controllers.h"
|
||||
#include "settings/settings_privacy_security.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "apiwrap.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "ui/boxes/peer_qr_box.h"
|
||||
#include "ui/controls/invite_link_buttons.h"
|
||||
#include "ui/controls/invite_link_label.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
namespace {
|
||||
@@ -473,15 +478,15 @@ auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
|
||||
const auto labels = raw->add(object_ptr<Ui::RpWidget>(raw));
|
||||
const auto min = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
QString::number(minValue),
|
||||
Lang::FormatCountDecimal(minValue),
|
||||
*labelStyle);
|
||||
const auto max = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
QString::number(maxValue),
|
||||
Lang::FormatCountDecimal(maxValue),
|
||||
*labelStyle);
|
||||
const auto current = Ui::CreateChild<Ui::FlatLabel>(
|
||||
raw,
|
||||
QString::number(value),
|
||||
Lang::FormatCountDecimal(value),
|
||||
*labelStyle);
|
||||
min->setTextColorOverride(st::windowSubTextFg->c);
|
||||
max->setTextColorOverride(st::windowSubTextFg->c);
|
||||
@@ -511,7 +516,7 @@ auto PrivacyExceptionsBoxController::createRow(not_null<History*> history)
|
||||
};
|
||||
const auto updateByValue = [=](int value) {
|
||||
current->setText(value > 0
|
||||
? tr::lng_action_gift_for_stars(tr::now, lt_count, value)
|
||||
? tr::lng_action_gift_for_stars(tr::now, lt_count_decimal, value)
|
||||
: tr::lng_manage_monoforum_free(tr::now));
|
||||
|
||||
state->index = 0;
|
||||
@@ -1312,6 +1317,58 @@ void EditDirectMessagesPriceBox(
|
||||
*result = stars;
|
||||
}, box->lifetime());
|
||||
|
||||
if (const auto username = channel->username(); !username.isEmpty()) {
|
||||
Ui::AddSkip(inner);
|
||||
Ui::AddSubsectionTitle(
|
||||
inner,
|
||||
tr::lng_manage_monoforum_link_subtitle());
|
||||
|
||||
constexpr auto kDirectParam = "?direct"_cs;
|
||||
const auto link = channel->session().createInternalLinkFull(username)
|
||||
+ kDirectParam.utf8();
|
||||
const auto copyLink = [=] {
|
||||
TextUtilities::SetClipboardText(TextForMimeData::Simple(link));
|
||||
box->uiShow()->showToast(tr::lng_group_invite_copied(tr::now));
|
||||
};
|
||||
const auto shareLink = [=] {
|
||||
box->uiShow()->showBox(ShareInviteLinkBox(channel, link));
|
||||
};
|
||||
const auto createMenu = [=] {
|
||||
auto result = base::make_unique_q<Ui::PopupMenu>(
|
||||
inner,
|
||||
st::popupMenuWithIcons);
|
||||
result->addAction(
|
||||
tr::lng_group_invite_context_qr(tr::now),
|
||||
[=] {
|
||||
box->uiShow()->showBox(Box([=](
|
||||
not_null<Ui::GenericBox*> qrBox) {
|
||||
Ui::FillPeerQrBox(qrBox, channel, link, nullptr);
|
||||
}));
|
||||
},
|
||||
&st::menuIconQrCode);
|
||||
return result;
|
||||
};
|
||||
|
||||
auto linkText = Ui::Text::StripUrlProtocol(link);
|
||||
const auto label = inner->lifetime().make_state<Ui::InviteLinkLabel>(
|
||||
inner,
|
||||
rpl::single(std::move(linkText)),
|
||||
createMenu);
|
||||
inner->add(
|
||||
label->take(),
|
||||
st::inviteLinkFieldPadding);
|
||||
|
||||
label->clicks() | rpl::start_with_next(copyLink, label->lifetime());
|
||||
|
||||
Ui::AddSkip(inner);
|
||||
|
||||
AddCopyShareLinkButtons(inner, copyLink, shareLink);
|
||||
Ui::AddSkip(inner);
|
||||
Ui::AddSkip(inner);
|
||||
|
||||
Ui::AddDivider(inner);
|
||||
}
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto weak = base::make_weak(box);
|
||||
callback(toggle->toggled() ? *result : std::optional<int>());
|
||||
|
||||
@@ -296,7 +296,10 @@ Tasks::Task::Task(
|
||||
tr::lng_todo_create_list_add()))
|
||||
, _limit(session->appConfig().todoListItemTextLimit()) {
|
||||
InitField(outer, _field, session);
|
||||
_field->setMaxLength(_limit + kErrorLimit);
|
||||
|
||||
// Don't limit max length, because user can paste long list of items.
|
||||
//_field->setMaxLength(_limit + kErrorLimit);
|
||||
|
||||
_field->show();
|
||||
if (locked) {
|
||||
_field->setDisabled(true);
|
||||
|
||||
@@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/filter_icons.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@@ -593,7 +594,7 @@ void EditFilterBox(
|
||||
) | rpl::start_with_next([=](const QRect &r) {
|
||||
const auto h = st::normalFont->height;
|
||||
preview->setGeometry(
|
||||
colors->x(),
|
||||
rect::right(colors) - st::settingsFilterTagPreviewSkip,
|
||||
r.y() + (r.height() - h) / 2 + st::lineWidth,
|
||||
colors->width(),
|
||||
h);
|
||||
|
||||
@@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "settings/settings_premium.h"
|
||||
#include "ui/basic_click_handlers.h" // UrlClickHandler::Open.
|
||||
#include "ui/boxes/boost_box.h" // StartFireworks.
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
@@ -587,6 +588,41 @@ void AddTableRow(
|
||||
return MakeValueWithSmallButton(table, label, std::move(text), handler);
|
||||
}
|
||||
|
||||
void AddUniqueGiftPropertyRows(
|
||||
not_null<Ui::RpWidget*> container,
|
||||
not_null<Ui::TableLayout*> table,
|
||||
not_null<Data::UniqueGift*> unique) {
|
||||
const auto tooltip = std::make_shared<InfoTooltipData>(InfoTooltipData{
|
||||
.parent = container,
|
||||
});
|
||||
const auto showTooltip = [=](
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
rpl::producer<TextWithEntities> text) {
|
||||
ShowInfoTooltip(tooltip, widget, std::move(text), kTooltipDuration);
|
||||
};
|
||||
const auto showRarity = [=](
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
int rarity) {
|
||||
const auto percent = QString::number(rarity / 10.) + '%';
|
||||
showTooltip(widget, tr::lng_gift_unique_rarity(
|
||||
lt_percent,
|
||||
rpl::single(TextWithEntities{ percent }),
|
||||
Ui::Text::WithEntities));
|
||||
};
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_model(),
|
||||
MakeAttributeValue(table, unique->model, showRarity));
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_backdrop(),
|
||||
MakeAttributeValue(table, unique->backdrop, showRarity));
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_symbol(),
|
||||
MakeAttributeValue(table, unique->pattern, showRarity));
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(
|
||||
not_null<Ui::TableLayout*> table,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
@@ -1398,14 +1434,109 @@ QString TonAddressUrl(
|
||||
return prefix + address;
|
||||
}
|
||||
|
||||
struct AddedUniqueDetails {
|
||||
object_ptr<Ui::RpWidget> widget;
|
||||
not_null<Ui::FlatLabel*> label;
|
||||
};
|
||||
[[nodiscard]] AddedUniqueDetails MakeUniqueDetails(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Settings::CreditsEntryBoxStyleOverrides st,
|
||||
Ui::Text::MarkedContext context,
|
||||
int removeCost,
|
||||
Fn<void(Fn<void()> removed)> remove) {
|
||||
auto owned = object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
rpl::duplicate(text),
|
||||
(st.tableValueMessage
|
||||
? *st.tableValueMessage
|
||||
: st::giveawayGiftMessage),
|
||||
st::defaultPopupMenu,
|
||||
context);
|
||||
const auto label = owned.data();
|
||||
if (!remove) {
|
||||
return {
|
||||
.widget = std::move(owned),
|
||||
.label = label,
|
||||
};
|
||||
}
|
||||
owned.release();
|
||||
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = result.data();
|
||||
|
||||
label->setParent(raw);
|
||||
label->show();
|
||||
const auto icon = Ui::CreateChild<Ui::IconButton>(
|
||||
raw,
|
||||
st::giveawayGiftMessageRemove);
|
||||
raw->widthValue() | rpl::start_with_next([=](int outer) {
|
||||
label->resizeToWidth(outer - icon->width());
|
||||
const auto height = std::max(label->height(), icon->height());
|
||||
|
||||
icon->moveToRight(0, (height - icon->height()) / 2);
|
||||
label->move(0, (height - label->height()) / 2);
|
||||
|
||||
raw->resize(outer, height);
|
||||
}, raw->lifetime());
|
||||
icon->setClickedCallback([=] {
|
||||
const auto weak = base::make_weak(raw);
|
||||
show->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
const auto confirming = base::make_weak(box);
|
||||
const auto confirmed = [=] {
|
||||
remove([=] {
|
||||
delete weak.get();
|
||||
if (const auto strong = confirming.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
});
|
||||
};
|
||||
Ui::ConfirmBox(box, {
|
||||
.text = tr::lng_gift_unique_info_remove_text(),
|
||||
.confirmed = confirmed,
|
||||
.confirmText = tr::lng_gift_unique_info_remove_confirm(
|
||||
lt_cost,
|
||||
rpl::single(
|
||||
Ui::Text::IconEmoji(&st::starIconEmoji).append(
|
||||
Lang::FormatCountDecimal(removeCost))),
|
||||
Ui::Text::RichLangValue),
|
||||
.title = tr::lng_gift_unique_info_remove_title(),
|
||||
});
|
||||
box->addRow(
|
||||
object_ptr<Ui::TableLayout>(
|
||||
box,
|
||||
st.table ? *st.table : st::giveawayGiftCodeTable)
|
||||
)->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
rpl::duplicate(text),
|
||||
(st.tableValueMessage
|
||||
? *st.tableValueMessage
|
||||
: st::giveawayGiftMessage),
|
||||
st::defaultPopupMenu,
|
||||
context),
|
||||
nullptr,
|
||||
st::giveawayGiftCodeLabelMargin,
|
||||
st::giveawayGiftCodeValueMargin);
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
.widget = std::move(result),
|
||||
.label = label,
|
||||
};
|
||||
}
|
||||
|
||||
void AddStarGiftTable(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
Settings::CreditsEntryBoxStyleOverrides st,
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
Fn<void()> convertToStars,
|
||||
Fn<void()> startUpgrade) {
|
||||
auto table = container->add(
|
||||
Fn<void()> startUpgrade,
|
||||
Fn<void(Fn<void()> removed)> removeDetails) {
|
||||
const auto table = container->add(
|
||||
object_ptr<Ui::TableLayout>(
|
||||
container,
|
||||
st.table ? *st.table : st::giveawayGiftCodeTable),
|
||||
@@ -1432,7 +1563,10 @@ void AddStarGiftTable(
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_credits_box_history_entry_peer(),
|
||||
MakePeerTableValue(table, show, PeerId(entry.bareGiftResaleRecipientId)),
|
||||
MakePeerTableValue(
|
||||
table,
|
||||
show,
|
||||
PeerId(entry.bareGiftResaleRecipientId)),
|
||||
st::giveawayGiftCodePeerMargin);
|
||||
} else if (unique && entry.bareGiftOwnerId) {
|
||||
const auto ownerId = PeerId(entry.bareGiftOwnerId);
|
||||
@@ -1484,6 +1618,38 @@ void AddStarGiftTable(
|
||||
tr::lng_gift_unique_owner(),
|
||||
std::move(label));
|
||||
}
|
||||
|
||||
if (const auto hostId = PeerId(entry.bareGiftHostId)) {
|
||||
const auto was = std::make_shared<std::optional<CollectibleId>>();
|
||||
const auto handleChange = [=](
|
||||
not_null<Ui::RpWidget*> badge,
|
||||
EmojiStatusId emojiStatusId) {
|
||||
const auto id = emojiStatusId.collectible
|
||||
? emojiStatusId.collectible->id
|
||||
: 0;
|
||||
const auto show = [&](const auto &phrase) {
|
||||
showTooltip(badge, phrase(
|
||||
lt_name,
|
||||
rpl::single(Ui::Text::Bold(UniqueGiftName(*unique))),
|
||||
Ui::Text::WithEntities));
|
||||
};
|
||||
if (!*was || *was == id) {
|
||||
*was = id;
|
||||
return;
|
||||
} else if (*was == unique->id) {
|
||||
show(tr::lng_gift_wear_end_toast);
|
||||
} else if (id == unique->id) {
|
||||
show(tr::lng_gift_wear_start_toast);
|
||||
}
|
||||
*was = id;
|
||||
};
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_telegram(),
|
||||
MakePeerWithStatusValue(table, show, hostId, handleChange),
|
||||
st::giveawayGiftCodePeerMargin);
|
||||
}
|
||||
|
||||
} else if (giftToChannel) {
|
||||
AddTableRow(
|
||||
table,
|
||||
@@ -1544,27 +1710,7 @@ void AddStarGiftTable(
|
||||
rpl::single(Ui::Text::WithEntities(langDateTime(entry.date))));
|
||||
}
|
||||
if (unique) {
|
||||
const auto showRarity = [=](
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
int rarity) {
|
||||
const auto percent = QString::number(rarity / 10.) + '%';
|
||||
showTooltip(widget, tr::lng_gift_unique_rarity(
|
||||
lt_percent,
|
||||
rpl::single(TextWithEntities{ percent }),
|
||||
Ui::Text::WithEntities));
|
||||
};
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_model(),
|
||||
MakeAttributeValue(table, unique->model, showRarity));
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_backdrop(),
|
||||
MakeAttributeValue(table, unique->backdrop, showRarity));
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_symbol(),
|
||||
MakeAttributeValue(table, unique->pattern, showRarity));
|
||||
AddUniqueGiftPropertyRows(container, table, unique);
|
||||
} else {
|
||||
AddTableRow(
|
||||
table,
|
||||
@@ -1623,60 +1769,63 @@ void AddStarGiftTable(
|
||||
: nullptr;
|
||||
const auto date = base::unixtime::parse(original.date).date();
|
||||
const auto dateText = TextWithEntities{ langDayOfMonth(date) };
|
||||
auto label = object_ptr<Ui::FlatLabel>(
|
||||
auto details = from
|
||||
? (original.message.empty()
|
||||
? tr::lng_gift_unique_info_sender(
|
||||
lt_from,
|
||||
rpl::single(Ui::Text::Link(from->name(), 2)),
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_gift_unique_info_sender_comment(
|
||||
lt_from,
|
||||
rpl::single(Ui::Text::Link(from->name(), 2)),
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
lt_text,
|
||||
rpl::single(original.message),
|
||||
Ui::Text::WithEntities))
|
||||
: (original.message.empty()
|
||||
? tr::lng_gift_unique_info_reciever(
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_gift_unique_info_reciever_comment(
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
lt_text,
|
||||
rpl::single(original.message),
|
||||
Ui::Text::WithEntities));
|
||||
const auto tmp = std::make_shared<Ui::RpWidget*>(nullptr);
|
||||
auto made = MakeUniqueDetails(
|
||||
show,
|
||||
table,
|
||||
(from
|
||||
? (original.message.empty()
|
||||
? tr::lng_gift_unique_info_sender(
|
||||
lt_from,
|
||||
rpl::single(Ui::Text::Link(from->name(), 2)),
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_gift_unique_info_sender_comment(
|
||||
lt_from,
|
||||
rpl::single(Ui::Text::Link(from->name(), 2)),
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
lt_text,
|
||||
rpl::single(original.message),
|
||||
Ui::Text::WithEntities))
|
||||
: (original.message.empty()
|
||||
? tr::lng_gift_unique_info_reciever(
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_gift_unique_info_reciever_comment(
|
||||
lt_recipient,
|
||||
rpl::single(Ui::Text::Link(to->name(), 1)),
|
||||
lt_date,
|
||||
rpl::single(dateText),
|
||||
lt_text,
|
||||
rpl::single(original.message),
|
||||
Ui::Text::WithEntities))),
|
||||
(st.tableValueMessage
|
||||
? *st.tableValueMessage
|
||||
: st::giveawayGiftMessage),
|
||||
st::defaultPopupMenu,
|
||||
Core::TextContext({ .session = session }));
|
||||
std::move(details),
|
||||
st,
|
||||
Core::TextContext({ .session = session }),
|
||||
entry.starsForDetailsRemove,
|
||||
std::move(removeDetails));
|
||||
const auto showBoxLink = [=](not_null<PeerData*> peer) {
|
||||
return std::make_shared<LambdaClickHandler>([=] {
|
||||
show->showBox(PrepareShortInfoBox(peer, show));
|
||||
});
|
||||
};
|
||||
label->setLink(1, showBoxLink(to));
|
||||
made.label->setLink(1, showBoxLink(to));
|
||||
if (from) {
|
||||
label->setLink(2, showBoxLink(from));
|
||||
made.label->setLink(2, showBoxLink(from));
|
||||
}
|
||||
label->setSelectable(true);
|
||||
made.label->setSelectable(true);
|
||||
*tmp = made.widget.data();
|
||||
table->addRow(
|
||||
std::move(label),
|
||||
std::move(made.widget),
|
||||
nullptr,
|
||||
st::giveawayGiftCodeLabelMargin,
|
||||
st::giveawayGiftCodeValueMargin);
|
||||
@@ -1699,6 +1848,25 @@ void AddStarGiftTable(
|
||||
}
|
||||
}
|
||||
|
||||
void AddTransferGiftTable(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
std::shared_ptr<Data::UniqueGift> unique) {
|
||||
const auto table = container->add(
|
||||
object_ptr<Ui::TableLayout>(
|
||||
container,
|
||||
st::giveawayGiftCodeTable),
|
||||
st::giveawayGiftCodeTableMargin);
|
||||
AddUniqueGiftPropertyRows(container, table, unique.get());
|
||||
if (const auto value = unique->value.get()) {
|
||||
AddTableRow(
|
||||
table,
|
||||
tr::lng_gift_unique_value(),
|
||||
rpl::single(
|
||||
FormatValuePrice(value->valuePrice, value->currency, true)));
|
||||
}
|
||||
}
|
||||
|
||||
void AddCreditsHistoryEntryTable(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
|
||||
@@ -23,6 +23,7 @@ struct CreditsHistoryEntry;
|
||||
struct GiveawayStart;
|
||||
struct GiveawayResults;
|
||||
struct SubscriptionEntry;
|
||||
struct UniqueGift;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
@@ -77,7 +78,12 @@ void AddStarGiftTable(
|
||||
Settings::CreditsEntryBoxStyleOverrides st,
|
||||
const Data::CreditsHistoryEntry &entry,
|
||||
Fn<void()> convertToStars,
|
||||
Fn<void()> startUpgrade);
|
||||
Fn<void()> startUpgrade,
|
||||
Fn<void(Fn<void()> removed)> removeDetails);
|
||||
void AddTransferGiftTable(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
std::shared_ptr<Data::UniqueGift> unique);
|
||||
void AddCreditsHistoryEntryTable(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
|
||||
@@ -138,6 +138,8 @@ void CreateModerateMessagesBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const HistoryItemsList &items,
|
||||
Fn<void()> confirmed) {
|
||||
Expects(!items.empty());
|
||||
|
||||
using Controller = Ui::ExpandablePeerListController;
|
||||
|
||||
const auto [allCanBan, allCanDelete, participants]
|
||||
@@ -158,8 +160,12 @@ void CreateModerateMessagesBox(
|
||||
participants.size()).width(),
|
||||
0);
|
||||
|
||||
const auto session = &items.front()->history()->session();
|
||||
const auto historyPeerId = items.front()->history()->peer->id;
|
||||
const auto itemsCount = int(items.size());
|
||||
const auto firstItem = items.front();
|
||||
const auto history = firstItem->history();
|
||||
const auto session = &history->session();
|
||||
const auto historyPeerId = history->peer->id;
|
||||
const auto ids = session->data().itemsToIds(items);
|
||||
|
||||
using Request = Fn<void(not_null<PeerData*>, not_null<ChannelData*>)>;
|
||||
const auto sequentiallyRequest = [=](
|
||||
@@ -242,11 +248,11 @@ void CreateModerateMessagesBox(
|
||||
const auto title = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
(items.size() == 1)
|
||||
(itemsCount == 1)
|
||||
? tr::lng_selected_delete_sure_this()
|
||||
: tr::lng_selected_delete_sure(
|
||||
lt_count,
|
||||
rpl::single(items.size()) | tr::to_count()),
|
||||
rpl::single(itemsCount) | tr::to_count()),
|
||||
st::boxLabel));
|
||||
Ui::AddSkip(inner);
|
||||
Ui::AddSkip(inner);
|
||||
@@ -264,7 +270,6 @@ void CreateModerateMessagesBox(
|
||||
Ui::AddExpandablePeerList(report, controller, inner);
|
||||
handleSubmition(report);
|
||||
|
||||
const auto ids = items.front()->from()->owner().itemsToIds(items);
|
||||
handleConfirmation(report, controller, [=](
|
||||
not_null<PeerData*> p,
|
||||
not_null<ChannelData*> c) {
|
||||
@@ -296,12 +301,11 @@ void CreateModerateMessagesBox(
|
||||
: tr::lng_delete_all_from_user(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(items.front()->from()->name()),
|
||||
Ui::Text::Bold(firstItem->from()->name()),
|
||||
Ui::Text::WithEntities),
|
||||
false,
|
||||
st::defaultBoxCheckbox),
|
||||
st::boxRowPadding + buttonPadding);
|
||||
const auto history = items.front()->history();
|
||||
auto messagesCounts = MessagesCountValue(history, participants);
|
||||
|
||||
const auto controller = box->lifetime().make_state<Controller>(
|
||||
@@ -311,6 +315,10 @@ void CreateModerateMessagesBox(
|
||||
});
|
||||
Ui::AddExpandablePeerList(deleteAll, controller, inner);
|
||||
{
|
||||
auto itemFromIds = items | ranges::views::transform([](
|
||||
const auto &item) {
|
||||
return item->from()->id;
|
||||
}) | ranges::to_vector;
|
||||
tr::lng_selected_delete_sure(
|
||||
lt_count,
|
||||
rpl::combine(
|
||||
@@ -320,7 +328,7 @@ void CreateModerateMessagesBox(
|
||||
: rpl::merge(
|
||||
controller->toggleRequestsFromInner.events(),
|
||||
controller->checkAllRequests.events())
|
||||
) | rpl::map([=, s = items.size()](const auto &map, bool c) {
|
||||
) | rpl::map([=](const auto &map, bool c) {
|
||||
const auto checked = (isSingle && !c)
|
||||
? Participants()
|
||||
: controller->collectRequests
|
||||
@@ -335,9 +343,9 @@ void CreateModerateMessagesBox(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &item : items) {
|
||||
for (const auto &fromId : itemFromIds) {
|
||||
for (const auto &peer : checked) {
|
||||
if (peer->id == item->from()->id) {
|
||||
if (peer->id == fromId) {
|
||||
result--;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -728,8 +728,8 @@ QString PeerListRow::generateShortName() {
|
||||
}
|
||||
|
||||
Ui::PeerUserpicView &PeerListRow::ensureUserpicView() {
|
||||
if (!_userpic.cloud && peer()->userpicPaintingPeer()->hasUserpic()) {
|
||||
_userpic = peer()->userpicPaintingPeer()->createUserpicView();
|
||||
if (!_userpic.cloud && peer()->hasUserpic()) {
|
||||
_userpic = peer()->createUserpicView();
|
||||
}
|
||||
return _userpic;
|
||||
}
|
||||
@@ -738,9 +738,9 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback(
|
||||
bool forceRound) {
|
||||
const auto saved = !_savedMessagesStatus.isEmpty();
|
||||
const auto replies = _isRepliesMessagesChat;
|
||||
const auto peer = this->peer()->userpicPaintingPeer();
|
||||
const auto peer = this->peer();
|
||||
auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView();
|
||||
if (forceRound && peer->isForum()) {
|
||||
if (forceRound && (peer->isForum() || peer->isMonoforum())) {
|
||||
return ForceRoundUserpicCallback(peer);
|
||||
}
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable {
|
||||
|
||||
@@ -981,9 +981,9 @@ void ChooseTopicSearchController::searchQuery(const QString &query) {
|
||||
}
|
||||
|
||||
void ChooseTopicSearchController::searchOnServer() {
|
||||
_requestId = _api.request(MTPchannels_GetForumTopics(
|
||||
MTP_flags(MTPchannels_GetForumTopics::Flag::f_q),
|
||||
_forum->channel()->inputChannel,
|
||||
_requestId = _api.request(MTPmessages_GetForumTopics(
|
||||
MTP_flags(MTPmessages_GetForumTopics::Flag::f_q),
|
||||
_forum->peer()->input,
|
||||
MTP_string(_query),
|
||||
MTP_int(_offsetDate),
|
||||
MTP_int(_offsetId),
|
||||
|
||||
@@ -7,28 +7,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "boxes/peers/edit_contact_box.h"
|
||||
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "api/api_peer_photo.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "boxes/peers/edit_peer_common.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_premium_limits.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "editor/photo_editor_layer_widget.h"
|
||||
#include "history/view/controls/history_view_characters_limit.h"
|
||||
#include "info/profile/info_profile_cover.h"
|
||||
#include "info/userpic/info_userpic_emoji_builder_common.h"
|
||||
#include "info/userpic/info_userpic_emoji_builder_menu_item.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_common.h"
|
||||
#include "lottie/lottie_frame_generator.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/animated_icon.h"
|
||||
#include "ui/controls/emoji_button_factory.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "info/profile/info_profile_cover.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_peer_photo.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kAnimationStartFrame = 0;
|
||||
constexpr auto kAnimationEndFrame = 21;
|
||||
|
||||
QString UserPhone(not_null<UserData*> user) {
|
||||
const auto phone = user->phone();
|
||||
return phone.isEmpty()
|
||||
@@ -43,17 +85,22 @@ void SendRequest(
|
||||
const QString &first,
|
||||
const QString &last,
|
||||
const QString &phone,
|
||||
const TextWithEntities ¬e,
|
||||
Fn<void()> done) {
|
||||
const auto wasContact = user->isContact();
|
||||
using Flag = MTPcontacts_AddContact::Flag;
|
||||
user->session().api().request(MTPcontacts_AddContact(
|
||||
MTP_flags(sharePhone
|
||||
? Flag::f_add_phone_privacy_exception
|
||||
: Flag(0)),
|
||||
MTP_flags(Flag::f_note
|
||||
| (sharePhone ? Flag::f_add_phone_privacy_exception : Flag(0))),
|
||||
user->inputUser,
|
||||
MTP_string(first),
|
||||
MTP_string(last),
|
||||
MTP_string(phone)
|
||||
MTP_string(phone),
|
||||
note.text.isEmpty()
|
||||
? MTPTextWithEntities()
|
||||
: MTP_textWithEntities(
|
||||
MTP_string(note.text),
|
||||
Api::EntitiesToMTP(&user->session(), note.entities))
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
user->setName(
|
||||
first,
|
||||
@@ -83,7 +130,8 @@ public:
|
||||
Controller(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user);
|
||||
not_null<UserData*> user,
|
||||
bool focusOnNotes = false);
|
||||
|
||||
void prepare();
|
||||
|
||||
@@ -91,17 +139,40 @@ private:
|
||||
void setupContent();
|
||||
void setupCover();
|
||||
void setupNameFields();
|
||||
void setupNotesField();
|
||||
void setupPhotoButtons();
|
||||
void setupDeleteContactButton();
|
||||
void setupWarning();
|
||||
void setupSharePhoneNumber();
|
||||
void initNameFields(
|
||||
not_null<Ui::InputField*> first,
|
||||
not_null<Ui::InputField*> last,
|
||||
bool inverted);
|
||||
void showPhotoMenu(bool suggest);
|
||||
void choosePhotoFile(bool suggest);
|
||||
void processChosenPhoto(QImage &&image, bool suggest);
|
||||
void processChosenPhotoWithMarkup(
|
||||
UserpicBuilder::Result &&data,
|
||||
bool suggest);
|
||||
void executeWithDelay(
|
||||
Fn<void()> callback,
|
||||
bool suggest,
|
||||
bool startAnimation = true);
|
||||
void finishIconAnimation(bool suggest);
|
||||
|
||||
not_null<Ui::GenericBox*> _box;
|
||||
not_null<Window::SessionController*> _window;
|
||||
not_null<UserData*> _user;
|
||||
bool _focusOnNotes = false;
|
||||
Ui::Checkbox *_sharePhone = nullptr;
|
||||
Ui::InputField *_notesField = nullptr;
|
||||
Ui::InputField *_firstNameField = nullptr;
|
||||
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
base::unique_qptr<Ui::PopupMenu> _photoMenu;
|
||||
std::unique_ptr<Ui::AnimatedIcon> _suggestIcon;
|
||||
std::unique_ptr<Ui::AnimatedIcon> _cameraIcon;
|
||||
Ui::RpWidget *_suggestIconWidget = nullptr;
|
||||
Ui::RpWidget *_cameraIconWidget = nullptr;
|
||||
QString _phone;
|
||||
Fn<void()> _focus;
|
||||
Fn<void()> _save;
|
||||
@@ -112,10 +183,12 @@ private:
|
||||
Controller::Controller(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user)
|
||||
not_null<UserData*> user,
|
||||
bool focusOnNotes)
|
||||
: _box(box)
|
||||
, _window(window)
|
||||
, _user(user)
|
||||
, _focusOnNotes(focusOnNotes)
|
||||
, _phone(UserPhone(user)) {
|
||||
}
|
||||
|
||||
@@ -134,6 +207,9 @@ void Controller::prepare() {
|
||||
void Controller::setupContent() {
|
||||
setupCover();
|
||||
setupNameFields();
|
||||
setupNotesField();
|
||||
setupPhotoButtons();
|
||||
setupDeleteContactButton();
|
||||
setupWarning();
|
||||
setupSharePhoneNumber();
|
||||
}
|
||||
@@ -154,13 +230,14 @@ void Controller::setupCover() {
|
||||
|
||||
void Controller::setupNameFields() {
|
||||
const auto inverted = langFirstNameGoesSecond();
|
||||
const auto first = _box->addRow(
|
||||
_firstNameField = _box->addRow(
|
||||
object_ptr<Ui::InputField>(
|
||||
_box,
|
||||
st::defaultInputField,
|
||||
tr::lng_signup_firstname(),
|
||||
_user->firstName),
|
||||
st::addContactFieldMargin);
|
||||
const auto first = _firstNameField;
|
||||
auto preparedLast = object_ptr<Ui::InputField>(
|
||||
_box,
|
||||
st::defaultInputField,
|
||||
@@ -188,6 +265,11 @@ void Controller::initNameFields(
|
||||
_box->setTabOrder(last, first);
|
||||
}
|
||||
_focus = [=] {
|
||||
if (_focusOnNotes && _notesField) {
|
||||
_notesField->setFocusFast();
|
||||
_notesField->setCursorPosition(_notesField->getLastText().size());
|
||||
return;
|
||||
}
|
||||
const auto firstValue = getValue(first);
|
||||
const auto lastValue = getValue(last);
|
||||
const auto empty = firstValue.isEmpty() && lastValue.isEmpty();
|
||||
@@ -203,6 +285,22 @@ void Controller::initNameFields(
|
||||
(inverted ? last : first)->showError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_notesField) {
|
||||
const auto limit = Data::PremiumLimits(
|
||||
&_user->session()).contactNoteLengthCurrent();
|
||||
const auto remove = Ui::ComputeFieldCharacterCount(_notesField)
|
||||
- limit;
|
||||
if (remove > 0) {
|
||||
_box->showToast(tr::lng_contact_notes_limit_reached(
|
||||
tr::now,
|
||||
lt_count,
|
||||
remove));
|
||||
_notesField->setFocus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto user = _user;
|
||||
const auto personal = _updatedPersonalPhoto
|
||||
? _updatedPersonalPhoto()
|
||||
@@ -218,6 +316,16 @@ void Controller::initNameFields(
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto noteValue = _notesField
|
||||
? [&] {
|
||||
auto textWithTags = _notesField->getTextWithAppliedMarkdown();
|
||||
return TextWithEntities{
|
||||
base::take(textWithTags.text),
|
||||
TextUtilities::ConvertTextTagsToEntities(
|
||||
base::take(textWithTags.tags)),
|
||||
};
|
||||
}()
|
||||
: TextWithEntities();
|
||||
SendRequest(
|
||||
base::make_weak(_box),
|
||||
user,
|
||||
@@ -225,6 +333,7 @@ void Controller::initNameFields(
|
||||
firstValue,
|
||||
lastValue,
|
||||
_phone,
|
||||
noteValue,
|
||||
done);
|
||||
};
|
||||
const auto submit = [=] {
|
||||
@@ -257,6 +366,344 @@ void Controller::setupWarning() {
|
||||
st::addContactWarningMargin);
|
||||
}
|
||||
|
||||
void Controller::setupNotesField() {
|
||||
Ui::AddSkip(_box->verticalLayout());
|
||||
Ui::AddDivider(_box->verticalLayout());
|
||||
Ui::AddSkip(_box->verticalLayout());
|
||||
_notesField = _box->addRow(
|
||||
object_ptr<Ui::InputField>(
|
||||
_box,
|
||||
st::notesFieldWithEmoji,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_contact_add_notes(),
|
||||
QString()),
|
||||
st::addContactFieldMargin);
|
||||
_notesField->setMarkdownSet(Ui::MarkdownSet::Notes);
|
||||
_notesField->setCustomTextContext(Core::TextContext({
|
||||
.session = &_user->session()
|
||||
}));
|
||||
_notesField->setTextWithTags({
|
||||
_user->note().text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(_user->note().entities)
|
||||
});
|
||||
|
||||
_notesField->setMarkdownReplacesEnabled(rpl::single(
|
||||
Ui::MarkdownEnabledState{
|
||||
Ui::MarkdownEnabled{
|
||||
{
|
||||
Ui::InputField::kTagBold,
|
||||
Ui::InputField::kTagItalic,
|
||||
Ui::InputField::kTagUnderline,
|
||||
Ui::InputField::kTagStrikeOut,
|
||||
Ui::InputField::kTagSpoiler
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
const auto container = _box->getDelegate()->outerContainer();
|
||||
using Selector = ChatHelpers::TabbedSelector;
|
||||
_emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
|
||||
container,
|
||||
_window,
|
||||
object_ptr<Selector>(
|
||||
nullptr,
|
||||
_window->uiShow(),
|
||||
Window::GifPauseReason::Layer,
|
||||
Selector::Mode::EmojiOnly));
|
||||
_emojiPanel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
_emojiPanel->hide();
|
||||
_emojiPanel->selector()->setCurrentPeer(_window->session().user());
|
||||
_emojiPanel->selector()->emojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
|
||||
Ui::InsertEmojiAtCursor(_notesField->textCursor(), data.emoji);
|
||||
}, _notesField->lifetime());
|
||||
_emojiPanel->selector()->customEmojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
||||
const auto info = data.document->sticker();
|
||||
if (info
|
||||
&& info->setType == Data::StickersType::Emoji
|
||||
&& !_window->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_window,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
Data::InsertCustomEmoji(_notesField, data.document);
|
||||
}
|
||||
}, _notesField->lifetime());
|
||||
|
||||
const auto emojiButton = Ui::AddEmojiToggleToField(
|
||||
_notesField,
|
||||
_box,
|
||||
_window,
|
||||
_emojiPanel.get(),
|
||||
st::sendGifWithCaptionEmojiPosition);
|
||||
emojiButton->show();
|
||||
|
||||
using Limit = HistoryView::Controls::CharactersLimitLabel;
|
||||
struct LimitState {
|
||||
base::unique_qptr<Limit> charsLimitation;
|
||||
};
|
||||
const auto limitState = _notesField->lifetime().make_state<LimitState>();
|
||||
|
||||
const auto checkCharsLimitation = [=, w = _notesField->window()] {
|
||||
const auto limit = Data::PremiumLimits(
|
||||
&_user->session()).contactNoteLengthCurrent();
|
||||
const auto remove = Ui::ComputeFieldCharacterCount(_notesField)
|
||||
- limit;
|
||||
if (!limitState->charsLimitation) {
|
||||
const auto border = _notesField->st().borderActive;
|
||||
limitState->charsLimitation = base::make_unique_q<Limit>(
|
||||
_box->verticalLayout(),
|
||||
emojiButton,
|
||||
style::al_top,
|
||||
QMargins{ 0, -border - _notesField->st().border, 0, 0 });
|
||||
rpl::combine(
|
||||
limitState->charsLimitation->geometryValue(),
|
||||
_notesField->geometryValue()
|
||||
) | rpl::start_with_next([=](QRect limit, QRect field) {
|
||||
limitState->charsLimitation->setVisible(
|
||||
(w->mapToGlobal(limit.bottomLeft()).y() - border)
|
||||
< w->mapToGlobal(field.bottomLeft()).y());
|
||||
limitState->charsLimitation->raise();
|
||||
}, limitState->charsLimitation->lifetime());
|
||||
}
|
||||
limitState->charsLimitation->setLeft(remove);
|
||||
};
|
||||
|
||||
_notesField->changes() | rpl::start_with_next([=] {
|
||||
checkCharsLimitation();
|
||||
}, _notesField->lifetime());
|
||||
|
||||
Ui::AddDividerText(
|
||||
_box->verticalLayout(),
|
||||
tr::lng_contact_add_notes_about());
|
||||
}
|
||||
|
||||
void Controller::setupPhotoButtons() {
|
||||
if (!_user->isContact()) {
|
||||
return;
|
||||
}
|
||||
const auto iconPlaceholder = st::restoreUserpicIcon.size * 2;
|
||||
auto nameValue = _firstNameField
|
||||
? rpl::merge(
|
||||
rpl::single(_firstNameField->getLastText().trimmed()),
|
||||
_firstNameField->changes() | rpl::map([=] {
|
||||
return _firstNameField->getLastText().trimmed();
|
||||
})) | rpl::map([=](const QString &text) {
|
||||
return text.isEmpty() ? Ui::kQEllipsis : text;
|
||||
})
|
||||
: rpl::single(_user->shortName()) | rpl::type_erased();
|
||||
const auto inner = _box->verticalLayout();
|
||||
Ui::AddSkip(inner);
|
||||
|
||||
const auto suggestBirthdayWrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner)));
|
||||
|
||||
const auto suggestBirthdayButton = Settings::AddButtonWithIcon(
|
||||
suggestBirthdayWrap->entity(),
|
||||
tr::lng_suggest_birthday(),
|
||||
st::settingsButtonLight,
|
||||
{ &st::editContactSuggestBirthday });
|
||||
suggestBirthdayButton->setClickedCallback([=] {
|
||||
Core::App().openInternalUrl(
|
||||
u"internal:edit_birthday:suggest:%1"_q.arg(
|
||||
peerToUser(_user->id).bare),
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.sessionWindow = base::make_weak(_window),
|
||||
}));
|
||||
});
|
||||
suggestBirthdayWrap->toggleOn(rpl::single(!_user->birthday().valid()
|
||||
&& !_user->starsPerMessageChecked()));
|
||||
|
||||
_suggestIcon = Ui::MakeAnimatedIcon({
|
||||
.generator = [] {
|
||||
return std::make_unique<Lottie::FrameGenerator>(
|
||||
Lottie::ReadContent(
|
||||
QByteArray(),
|
||||
u":/animations/photo_suggest_icon.tgs"_q));
|
||||
},
|
||||
.sizeOverride = iconPlaceholder,
|
||||
.colorized = true,
|
||||
});
|
||||
|
||||
_cameraIcon = Ui::MakeAnimatedIcon({
|
||||
.generator = [] {
|
||||
return std::make_unique<Lottie::FrameGenerator>(
|
||||
Lottie::ReadContent(
|
||||
QByteArray(),
|
||||
u":/animations/camera_outline.tgs"_q));
|
||||
},
|
||||
.sizeOverride = iconPlaceholder,
|
||||
.colorized = true,
|
||||
});
|
||||
|
||||
const auto suggestButtonWrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner)));
|
||||
suggestButtonWrap->toggleOn(
|
||||
rpl::single(!_user->starsPerMessageChecked()));
|
||||
|
||||
const auto suggestButton = Settings::AddButtonWithIcon(
|
||||
suggestButtonWrap->entity(),
|
||||
tr::lng_suggest_photo_for(lt_user, rpl::duplicate(nameValue)),
|
||||
st::settingsButtonLight,
|
||||
{ nullptr });
|
||||
|
||||
_suggestIconWidget = Ui::CreateChild<Ui::RpWidget>(suggestButton);
|
||||
_suggestIconWidget->resize(iconPlaceholder);
|
||||
_suggestIconWidget->paintRequest() | rpl::start_with_next([=] {
|
||||
if (_suggestIcon && _suggestIcon->valid()) {
|
||||
auto p = QPainter(_suggestIconWidget);
|
||||
const auto frame = _suggestIcon->frame(st::lightButtonFg->c);
|
||||
p.drawImage(_suggestIconWidget->rect(), frame);
|
||||
}
|
||||
}, _suggestIconWidget->lifetime());
|
||||
|
||||
suggestButton->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||
_suggestIconWidget->move(
|
||||
st::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,
|
||||
(size.height() - _suggestIconWidget->height()) / 2);
|
||||
}, _suggestIconWidget->lifetime());
|
||||
|
||||
suggestButton->setClickedCallback([=] {
|
||||
if (_suggestIcon && _suggestIcon->valid()) {
|
||||
_suggestIcon->setCustomStartFrame(kAnimationStartFrame);
|
||||
_suggestIcon->setCustomEndFrame(kAnimationEndFrame);
|
||||
_suggestIcon->jumpToStart([=] { _suggestIconWidget->update(); });
|
||||
_suggestIcon->animate([=] { _suggestIconWidget->update(); });
|
||||
}
|
||||
showPhotoMenu(true);
|
||||
});
|
||||
|
||||
const auto setButton = Settings::AddButtonWithIcon(
|
||||
inner,
|
||||
tr::lng_set_photo_for_user(lt_user, rpl::duplicate(nameValue)),
|
||||
st::settingsButtonLight,
|
||||
{ nullptr });
|
||||
|
||||
_cameraIconWidget = Ui::CreateChild<Ui::RpWidget>(setButton);
|
||||
_cameraIconWidget->resize(iconPlaceholder);
|
||||
_cameraIconWidget->paintRequest() | rpl::start_with_next([=] {
|
||||
if (_cameraIcon && _cameraIcon->valid()) {
|
||||
auto p = QPainter(_cameraIconWidget);
|
||||
const auto frame = _cameraIcon->frame(st::lightButtonFg->c);
|
||||
p.drawImage(_cameraIconWidget->rect(), frame);
|
||||
}
|
||||
}, _cameraIconWidget->lifetime());
|
||||
|
||||
setButton->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||
_cameraIconWidget->move(
|
||||
st::settingsButtonLight.iconLeft - iconPlaceholder.width() / 4,
|
||||
(size.height() - _cameraIconWidget->height()) / 2);
|
||||
}, _cameraIconWidget->lifetime());
|
||||
|
||||
setButton->setClickedCallback([=] {
|
||||
if (_cameraIcon && _cameraIcon->valid()) {
|
||||
_cameraIcon->setCustomStartFrame(kAnimationStartFrame);
|
||||
_cameraIcon->setCustomEndFrame(kAnimationEndFrame);
|
||||
_cameraIcon->jumpToStart([=] { _cameraIconWidget->update(); });
|
||||
_cameraIcon->animate([=] { _cameraIconWidget->update(); });
|
||||
}
|
||||
showPhotoMenu(false);
|
||||
});
|
||||
|
||||
const auto resetButtonWrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner)));
|
||||
|
||||
const auto resetButton = Settings::AddButtonWithIcon(
|
||||
resetButtonWrap->entity(),
|
||||
tr::lng_profile_photo_reset(),
|
||||
st::settingsButtonLight,
|
||||
{ nullptr });
|
||||
|
||||
const auto userpicButton = Ui::CreateChild<Ui::UserpicButton>(
|
||||
resetButton,
|
||||
_window,
|
||||
_user,
|
||||
Ui::UserpicButton::Role::Custom,
|
||||
Ui::UserpicButton::Source::NonPersonalIfHasPersonal,
|
||||
st::restoreUserpicIcon);
|
||||
userpicButton->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
resetButton->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
userpicButton->move(
|
||||
st::settingsButtonLight.iconLeft,
|
||||
(size.height() - userpicButton->height()) / 2);
|
||||
}, userpicButton->lifetime());
|
||||
resetButtonWrap->toggleOn(
|
||||
_user->session().changes().peerFlagsValue(
|
||||
_user,
|
||||
Data::PeerUpdate::Flag::FullInfo | Data::PeerUpdate::Flag::Photo
|
||||
) | rpl::map([=] {
|
||||
return _user->hasPersonalPhoto();
|
||||
}) | rpl::distinct_until_changed());
|
||||
|
||||
resetButton->setClickedCallback([=] {
|
||||
_window->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_profile_photo_reset_sure(
|
||||
tr::now,
|
||||
lt_user,
|
||||
_user->shortName()),
|
||||
.confirmed = [=](Fn<void()> close) {
|
||||
_window->session().api().peerPhoto().clearPersonal(_user);
|
||||
close();
|
||||
},
|
||||
.confirmText = tr::lng_profile_photo_reset(tr::now),
|
||||
}));
|
||||
});
|
||||
|
||||
Ui::AddSkip(inner);
|
||||
|
||||
Ui::AddDividerText(
|
||||
inner,
|
||||
tr::lng_contact_photo_replace_info(lt_user, std::move(nameValue)));
|
||||
Ui::AddSkip(inner);
|
||||
}
|
||||
|
||||
void Controller::setupDeleteContactButton() {
|
||||
if (!_user->isContact()) {
|
||||
return;
|
||||
}
|
||||
const auto inner = _box->verticalLayout();
|
||||
const auto deleteButton = Settings::AddButtonWithIcon(
|
||||
inner,
|
||||
tr::lng_info_delete_contact(),
|
||||
st::settingsAttentionButton,
|
||||
{ nullptr });
|
||||
deleteButton->setClickedCallback([=] {
|
||||
const auto text = tr::lng_sure_delete_contact(
|
||||
tr::now,
|
||||
lt_contact,
|
||||
_user->name());
|
||||
const auto deleteSure = [=](Fn<void()> &&close) {
|
||||
close();
|
||||
_user->session().api().request(MTPcontacts_DeleteContacts(
|
||||
MTP_vector<MTPInputUser>(1, _user->inputUser)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
_user->session().api().applyUpdates(result);
|
||||
_box->closeBox();
|
||||
}).send();
|
||||
};
|
||||
_window->show(Ui::MakeConfirmBox({
|
||||
.text = text,
|
||||
.confirmed = deleteSure,
|
||||
.confirmText = tr::lng_box_delete(),
|
||||
.confirmStyle = &st::attentionBoxButton,
|
||||
}));
|
||||
});
|
||||
Ui::AddSkip(inner);
|
||||
}
|
||||
|
||||
void Controller::setupSharePhoneNumber() {
|
||||
const auto settings = _user->barSettings();
|
||||
if (!settings
|
||||
@@ -279,11 +726,174 @@ void Controller::setupSharePhoneNumber() {
|
||||
|
||||
}
|
||||
|
||||
void Controller::showPhotoMenu(bool suggest) {
|
||||
_photoMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||
_box,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
QObject::connect(_photoMenu.get(), &QObject::destroyed, [=] {
|
||||
finishIconAnimation(suggest);
|
||||
});
|
||||
|
||||
_photoMenu->addAction(
|
||||
tr::lng_attach_photo(tr::now),
|
||||
[=] { executeWithDelay([=] { choosePhotoFile(suggest); }, suggest); },
|
||||
&st::menuIconPhoto);
|
||||
|
||||
if (const auto data = QGuiApplication::clipboard()->mimeData()) {
|
||||
if (data->hasImage()) {
|
||||
auto callback = [=] {
|
||||
Editor::PrepareProfilePhoto(
|
||||
_box,
|
||||
&_window->window(),
|
||||
Editor::EditorData{
|
||||
.about = (suggest
|
||||
? tr::lng_profile_suggest_sure(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_user->shortName()),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_profile_set_personal_sure(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_user->shortName()),
|
||||
Ui::Text::WithEntities)),
|
||||
.confirm = (suggest
|
||||
? tr::lng_profile_suggest_button(tr::now)
|
||||
: tr::lng_profile_set_photo_button(tr::now)),
|
||||
.cropType = Editor::EditorData::CropType::Ellipse,
|
||||
.keepAspectRatio = true,
|
||||
},
|
||||
[=](QImage &&editedImage) {
|
||||
processChosenPhoto(std::move(editedImage), suggest);
|
||||
},
|
||||
qvariant_cast<QImage>(data->imageData()));
|
||||
};
|
||||
_photoMenu->addAction(
|
||||
tr::lng_profile_photo_from_clipboard(tr::now),
|
||||
[=] { executeWithDelay(callback, suggest); },
|
||||
&st::menuIconPhoto);
|
||||
}
|
||||
}
|
||||
|
||||
UserpicBuilder::AddEmojiBuilderAction(
|
||||
_window,
|
||||
_photoMenu.get(),
|
||||
_window->session().api().peerPhoto().emojiListValue(
|
||||
Api::PeerPhoto::EmojiListType::Profile),
|
||||
[=](UserpicBuilder::Result data) {
|
||||
processChosenPhotoWithMarkup(std::move(data), suggest);
|
||||
},
|
||||
false);
|
||||
|
||||
_photoMenu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void Controller::choosePhotoFile(bool suggest) {
|
||||
Editor::PrepareProfilePhotoFromFile(
|
||||
_box,
|
||||
&_window->window(),
|
||||
Editor::EditorData{
|
||||
.about = (suggest
|
||||
? tr::lng_profile_suggest_sure(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_user->shortName()),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_profile_set_personal_sure(
|
||||
tr::now,
|
||||
lt_user,
|
||||
Ui::Text::Bold(_user->shortName()),
|
||||
Ui::Text::WithEntities)),
|
||||
.confirm = (suggest
|
||||
? tr::lng_profile_suggest_button(tr::now)
|
||||
: tr::lng_profile_set_photo_button(tr::now)),
|
||||
.cropType = Editor::EditorData::CropType::Ellipse,
|
||||
.keepAspectRatio = true,
|
||||
},
|
||||
[=](QImage &&image) {
|
||||
processChosenPhoto(std::move(image), suggest);
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::processChosenPhoto(QImage &&image, bool suggest) {
|
||||
Api::PeerPhoto::UserPhoto photo{
|
||||
.image = base::duplicate(image),
|
||||
};
|
||||
if (suggest) {
|
||||
_window->session().api().peerPhoto().suggest(_user, std::move(photo));
|
||||
_window->showPeerHistory(_user->id);
|
||||
} else {
|
||||
_window->session().api().peerPhoto().upload(_user, std::move(photo));
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::processChosenPhotoWithMarkup(
|
||||
UserpicBuilder::Result &&data,
|
||||
bool suggest) {
|
||||
Api::PeerPhoto::UserPhoto photo{
|
||||
.image = std::move(data.image),
|
||||
.markupDocumentId = data.id,
|
||||
.markupColors = std::move(data.colors),
|
||||
};
|
||||
if (suggest) {
|
||||
_window->session().api().peerPhoto().suggest(_user, std::move(photo));
|
||||
_window->showPeerHistory(_user->id);
|
||||
} else {
|
||||
_window->session().api().peerPhoto().upload(_user, std::move(photo));
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::finishIconAnimation(bool suggest) {
|
||||
const auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();
|
||||
const auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;
|
||||
if (icon && icon->valid()) {
|
||||
icon->setCustomStartFrame(icon->frameIndex());
|
||||
icon->setCustomEndFrame(-1);
|
||||
icon->animate([=] { widget->update(); });
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::executeWithDelay(
|
||||
Fn<void()> callback,
|
||||
bool suggest,
|
||||
bool startAnimation) {
|
||||
const auto icon = suggest ? _suggestIcon.get() : _cameraIcon.get();
|
||||
const auto widget = suggest ? _suggestIconWidget : _cameraIconWidget;
|
||||
|
||||
if (startAnimation && icon && icon->valid()) {
|
||||
icon->setCustomStartFrame(icon->frameIndex());
|
||||
icon->setCustomEndFrame(-1);
|
||||
icon->animate([=] { widget->update(); });
|
||||
}
|
||||
|
||||
if (icon && icon->valid() && icon->animating()) {
|
||||
base::call_delayed(50, [=] {
|
||||
executeWithDelay(callback, suggest, false);
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditContactBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->lifetime().make_state<Controller>(box, window, user)->prepare();
|
||||
}
|
||||
|
||||
void EditContactNoteBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->lifetime().make_state<Controller>(
|
||||
box,
|
||||
window,
|
||||
user,
|
||||
true)->prepare();
|
||||
}
|
||||
|
||||
@@ -19,3 +19,8 @@ void EditContactBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user);
|
||||
|
||||
void EditContactNoteBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<UserData*> user);
|
||||
|
||||
@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/emoji_fly_animation.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_icons.h"
|
||||
@@ -411,9 +410,12 @@ void EditForumTopicBox(
|
||||
const auto topic = (!creating && forum->peer->forum())
|
||||
? forum->peer->forum()->topicFor(rootId)
|
||||
: nullptr;
|
||||
const auto bot = forum->peer->isBot();
|
||||
const auto created = topic && !topic->creating();
|
||||
box->setTitle(creating
|
||||
? tr::lng_forum_topic_new()
|
||||
: bot
|
||||
? tr::lng_bot_thread_edit()
|
||||
: tr::lng_forum_topic_edit());
|
||||
|
||||
box->setMaxHeight(st::editTopicMaxHeight);
|
||||
@@ -441,7 +443,9 @@ void EditForumTopicBox(
|
||||
object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::defaultInputField,
|
||||
tr::lng_forum_topic_title(),
|
||||
(bot
|
||||
? tr::lng_bot_thread_title()
|
||||
: tr::lng_forum_topic_title()),
|
||||
topic ? topic->title() : QString()),
|
||||
st::editTopicTitleMargin);
|
||||
box->setFocusCallback([=] {
|
||||
@@ -493,7 +497,9 @@ void EditForumTopicBox(
|
||||
}, title->lifetime());
|
||||
|
||||
if (!topic || !topic->isGeneral()) {
|
||||
Ui::AddDividerText(top, tr::lng_forum_choose_title_and_icon());
|
||||
Ui::AddDividerText(top, bot
|
||||
? tr::lng_bot_thread_choose_title_and_icon()
|
||||
: tr::lng_forum_choose_title_and_icon());
|
||||
|
||||
box->setScrollStyle(st::reactPanelScroll);
|
||||
|
||||
@@ -515,8 +521,7 @@ void EditForumTopicBox(
|
||||
}
|
||||
|
||||
const auto create = [=] {
|
||||
const auto channel = forum->peer->asChannel();
|
||||
if (!channel || !channel->isForum()) {
|
||||
if (!forum->peer->isForum()) {
|
||||
box->closeBox();
|
||||
return;
|
||||
} else if (title->getLastText().trimmed().isEmpty()) {
|
||||
@@ -527,7 +532,7 @@ void EditForumTopicBox(
|
||||
controller->showSection(
|
||||
std::make_shared<ChatMemento>(ChatViewId{
|
||||
.history = forum,
|
||||
.repliesRootId = channel->forum()->reserveCreatingId(
|
||||
.repliesRootId = forum->peer->forum()->reserveCreatingId(
|
||||
title->getLastText().trimmed(),
|
||||
state->defaultIcon.current().colorId,
|
||||
state->iconId.current()),
|
||||
@@ -554,13 +559,13 @@ void EditForumTopicBox(
|
||||
topic->applyIconId(state->iconId.current());
|
||||
box->closeBox();
|
||||
} else {
|
||||
using Flag = MTPchannels_EditForumTopic::Flag;
|
||||
using Flag = MTPmessages_EditForumTopic::Flag;
|
||||
const auto api = &forum->session().api();
|
||||
const auto weak = base::make_weak(box);
|
||||
state->requestId = api->request(MTPchannels_EditForumTopic(
|
||||
state->requestId = api->request(MTPmessages_EditForumTopic(
|
||||
MTP_flags(Flag::f_title
|
||||
| (topic->isGeneral() ? Flag() : Flag::f_icon_emoji_id)),
|
||||
topic->channel()->inputChannel,
|
||||
topic->peer()->input,
|
||||
MTP_int(rootId),
|
||||
MTP_string(title->getLastText().trimmed()),
|
||||
MTP_long(state->iconId.current()),
|
||||
|
||||
@@ -13,8 +13,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/peers/replace_boost_box.h"
|
||||
#include "boxes/background_box.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "boxes/stickers_box.h"
|
||||
#include "boxes/transfer_gift_box.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_changes.h"
|
||||
@@ -23,13 +26,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_emoji_statuses.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_premium_limits.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "info/channel_statistics/boosts/info_boosts_widget.h"
|
||||
#include "info/peer_gifts/info_peer_gifts_common.h"
|
||||
#include "info/profile/info_profile_emoji_status_panel.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "iv/iv_data.h"
|
||||
@@ -42,11 +48,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/controls/button_labels.h"
|
||||
#include "ui/controls/sub_tabs.h"
|
||||
#include "ui/effects/path_shift_gradient.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/color_contrast.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/vertical_list.h"
|
||||
@@ -54,6 +64,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_info.h" // defaultSubTabs.
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
@@ -66,13 +78,16 @@ using namespace Settings;
|
||||
constexpr auto kFakeChannelId = ChannelId(0xFFFFFFF000ULL);
|
||||
constexpr auto kFakeWebPageId = WebPageId(0xFFFFFFFF00000000ULL);
|
||||
constexpr auto kSelectAnimationDuration = crl::time(150);
|
||||
constexpr auto kUnsetColorIndex = uint8(0xFF);
|
||||
|
||||
class ColorSample final : public Ui::AbstractButton {
|
||||
public:
|
||||
ColorSample(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<Ui::ChatStyle> style,
|
||||
rpl::producer<uint8> colorIndex,
|
||||
rpl::producer<std::shared_ptr<Ui::ColorCollectible>> collectible,
|
||||
const QString &name);
|
||||
ColorSample(
|
||||
not_null<QWidget*> parent,
|
||||
@@ -90,6 +105,7 @@ private:
|
||||
std::shared_ptr<Ui::ChatStyle> _style;
|
||||
Ui::Text::String _name;
|
||||
uint8 _index = 0;
|
||||
std::shared_ptr<Ui::ColorCollectible> _collectible;
|
||||
Ui::Animations::Simple _selectAnimation;
|
||||
bool _selected = false;
|
||||
bool _simple = false;
|
||||
@@ -121,7 +137,8 @@ public:
|
||||
std::shared_ptr<Ui::ChatTheme> theme,
|
||||
not_null<PeerData*> peer,
|
||||
rpl::producer<uint8> colorIndexValue,
|
||||
rpl::producer<DocumentId> backgroundEmojiId);
|
||||
rpl::producer<DocumentId> backgroundEmojiId,
|
||||
rpl::producer<std::optional<Ui::ColorCollectible>> colorCollectible);
|
||||
~PreviewWrap();
|
||||
|
||||
private:
|
||||
@@ -170,16 +187,33 @@ private:
|
||||
|
||||
ColorSample::ColorSample(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<Ui::ChatStyle> style,
|
||||
rpl::producer<uint8> colorIndex,
|
||||
rpl::producer<std::shared_ptr<Ui::ColorCollectible>> collectible,
|
||||
const QString &name)
|
||||
: AbstractButton(parent)
|
||||
, _style(style)
|
||||
, _name(st::semiboldTextStyle, name) {
|
||||
std::move(
|
||||
colorIndex
|
||||
) | rpl::start_with_next([=](uint8 index) {
|
||||
, _style(style) {
|
||||
rpl::combine(
|
||||
std::move(colorIndex),
|
||||
std::move(collectible)
|
||||
) | rpl::start_with_next([=](
|
||||
uint8 index,
|
||||
std::shared_ptr<Ui::ColorCollectible> collectible) {
|
||||
_index = index;
|
||||
_collectible = std::move(collectible);
|
||||
if (const auto raw = _collectible.get()) {
|
||||
_name.setMarkedText(
|
||||
st::semiboldTextStyle,
|
||||
Data::SingleCustomEmoji(raw->giftEmojiId),
|
||||
kMarkupTextOptions,
|
||||
Core::TextContext({
|
||||
.session = session,
|
||||
.repaint = [=] { update(); },
|
||||
}));
|
||||
} else {
|
||||
_name.setText(st::semiboldTextStyle, name);
|
||||
}
|
||||
setNaturalWidth([&] {
|
||||
if (_name.isEmpty() || _style->colorPatternIndex(_index)) {
|
||||
return st::settingsColorSampleSize;
|
||||
@@ -225,7 +259,21 @@ void ColorSample::paintEvent(QPaintEvent *e) {
|
||||
if (!_simple && !colors.outlines[1].alpha()) {
|
||||
const auto radius = height() / 2;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(colors.bg);
|
||||
if (const auto raw = _collectible.get()) {
|
||||
const auto withBg = [&](const QColor &color) {
|
||||
return Ui::CountContrast(st::windowBg->c, color);
|
||||
};
|
||||
const auto dark = (withBg({ 0, 0, 0 })
|
||||
< withBg({ 255, 255, 255 }));
|
||||
const auto name = (dark && raw->darkAccentColor.alpha() > 0)
|
||||
? raw->darkAccentColor
|
||||
: raw->accentColor;
|
||||
auto bg = name;
|
||||
bg.setAlpha(0.12 * 255);
|
||||
p.setBrush(bg);
|
||||
} else {
|
||||
p.setBrush(colors.bg);
|
||||
}
|
||||
p.drawRoundedRect(rect(), radius, radius);
|
||||
|
||||
const auto padding = st::settingsColorSamplePadding;
|
||||
@@ -291,7 +339,8 @@ PreviewWrap::PreviewWrap(
|
||||
std::shared_ptr<Ui::ChatTheme> theme,
|
||||
not_null<PeerData*> peer,
|
||||
rpl::producer<uint8> colorIndexValue,
|
||||
rpl::producer<DocumentId> backgroundEmojiId)
|
||||
rpl::producer<DocumentId> backgroundEmojiId,
|
||||
rpl::producer<std::optional<Ui::ColorCollectible>> colorCollectible)
|
||||
: RpWidget(box)
|
||||
, _box(box)
|
||||
, _peer(peer)
|
||||
@@ -357,13 +406,24 @@ PreviewWrap::PreviewWrap(
|
||||
|
||||
_fake->setName(peer->name(), QString());
|
||||
std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) {
|
||||
_fake->changeColorIndex(index);
|
||||
update();
|
||||
if (index != kUnsetColorIndex) {
|
||||
_fake->changeColorIndex(index);
|
||||
update();
|
||||
}
|
||||
}, lifetime());
|
||||
std::move(backgroundEmojiId) | rpl::start_with_next([=](DocumentId id) {
|
||||
_fake->changeBackgroundEmojiId(id);
|
||||
update();
|
||||
}, lifetime());
|
||||
std::move(colorCollectible) | rpl::start_with_next([=](
|
||||
std::optional<Ui::ColorCollectible> &&collectible) {
|
||||
if (collectible) {
|
||||
_fake->changeColorCollectible(std::move(*collectible));
|
||||
} else {
|
||||
_fake->clearColorCollectible();
|
||||
}
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
const auto session = &_history->session();
|
||||
session->data().viewRepaintRequest(
|
||||
@@ -517,6 +577,7 @@ void LevelBadge::paintEvent(QPaintEvent *e) {
|
||||
struct SetValues {
|
||||
uint8 colorIndex = 0;
|
||||
DocumentId backgroundEmojiId = 0;
|
||||
std::optional<Ui::ColorCollectible> colorCollectible;
|
||||
EmojiStatusId statusId;
|
||||
TimeId statusUntil = 0;
|
||||
bool statusChanged = false;
|
||||
@@ -527,16 +588,32 @@ void Set(
|
||||
SetValues values) {
|
||||
const auto wasIndex = peer->colorIndex();
|
||||
const auto wasEmojiId = peer->backgroundEmojiId();
|
||||
const auto &wasColorCollectible = peer->colorCollectible();
|
||||
|
||||
const auto setLocal = [=](uint8 index, DocumentId emojiId) {
|
||||
const auto setLocal = [=](
|
||||
uint8 index,
|
||||
DocumentId emojiId,
|
||||
std::optional<Ui::ColorCollectible> colorCollectible) {
|
||||
using UpdateFlag = Data::PeerUpdate::Flag;
|
||||
peer->changeColorIndex(index);
|
||||
if (index == kUnsetColorIndex) {
|
||||
peer->clearColorIndex();
|
||||
} else {
|
||||
peer->changeColorIndex(index);
|
||||
}
|
||||
if (colorCollectible) {
|
||||
peer->changeColorCollectible(*colorCollectible);
|
||||
} else {
|
||||
peer->clearColorCollectible();
|
||||
}
|
||||
peer->changeBackgroundEmojiId(emojiId);
|
||||
peer->session().changes().peerUpdated(
|
||||
peer,
|
||||
UpdateFlag::Color | UpdateFlag::BackgroundEmoji);
|
||||
};
|
||||
setLocal(values.colorIndex, values.backgroundEmojiId);
|
||||
setLocal(
|
||||
values.colorIndex,
|
||||
values.backgroundEmojiId,
|
||||
values.colorCollectible);
|
||||
|
||||
const auto done = [=] {
|
||||
show->showToast(peer->isSelf()
|
||||
@@ -546,7 +623,9 @@ void Set(
|
||||
const auto fail = [=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
if (type != u"CHAT_NOT_MODIFIED"_q) {
|
||||
setLocal(wasIndex, wasEmojiId);
|
||||
setLocal(wasIndex, wasEmojiId, wasColorCollectible
|
||||
? *wasColorCollectible
|
||||
: std::optional<Ui::ColorCollectible>());
|
||||
show->showToast(type);
|
||||
}
|
||||
};
|
||||
@@ -557,10 +636,18 @@ void Set(
|
||||
};
|
||||
if (peer->isSelf()) {
|
||||
using Flag = MTPaccount_UpdateColor::Flag;
|
||||
using ColorFlag = MTPDpeerColor::Flag;
|
||||
send(MTPaccount_UpdateColor(
|
||||
MTP_flags(Flag::f_color | Flag::f_background_emoji_id),
|
||||
MTP_int(values.colorIndex),
|
||||
MTP_long(values.backgroundEmojiId)));
|
||||
MTP_flags(Flag::f_color),
|
||||
(values.colorCollectible
|
||||
? MTP_inputPeerColorCollectible(
|
||||
MTP_long(values.colorCollectible->collectibleId))
|
||||
: MTP_peerColor(
|
||||
MTP_flags(ColorFlag()
|
||||
| ColorFlag::f_color
|
||||
| ColorFlag::f_background_emoji_id),
|
||||
MTP_int(values.colorIndex),
|
||||
MTP_long(values.backgroundEmojiId)))));
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
if (peer->isBroadcast()) {
|
||||
using Flag = MTPchannels_UpdateColor::Flag;
|
||||
@@ -582,29 +669,38 @@ void Set(
|
||||
}
|
||||
}
|
||||
|
||||
bool ShowPremiumToast(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<PeerData*> peer) {
|
||||
if (!peer->isSelf() || show->session().premium()) {
|
||||
return false;
|
||||
}
|
||||
Settings::ShowPremiumPromoToast(
|
||||
show,
|
||||
tr::lng_settings_color_subscribe(
|
||||
tr::now,
|
||||
lt_link,
|
||||
Ui::Text::Link(
|
||||
Ui::Text::Bold(
|
||||
tr::lng_send_as_premium_required_link(tr::now))),
|
||||
Ui::Text::WithEntities),
|
||||
u"name_color"_q);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Apply(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<PeerData*> peer,
|
||||
SetValues values,
|
||||
Fn<void()> close,
|
||||
Fn<void()> cancel) {
|
||||
const auto session = &peer->session();
|
||||
if (peer->colorIndex() == values.colorIndex
|
||||
&& peer->backgroundEmojiId() == values.backgroundEmojiId
|
||||
&& (!peer->colorCollectible() == !values.colorCollectible)
|
||||
&& (!peer->colorCollectible()
|
||||
|| (*peer->colorCollectible() == *values.colorCollectible))
|
||||
&& !values.statusChanged) {
|
||||
close();
|
||||
} else if (peer->isSelf() && !session->premium()) {
|
||||
Settings::ShowPremiumPromoToast(
|
||||
show,
|
||||
tr::lng_settings_color_subscribe(
|
||||
tr::now,
|
||||
lt_link,
|
||||
Ui::Text::Link(
|
||||
Ui::Text::Bold(
|
||||
tr::lng_send_as_premium_required_link(tr::now))),
|
||||
Ui::Text::WithEntities),
|
||||
u"name_color"_q);
|
||||
cancel();
|
||||
} else if (peer->isSelf()) {
|
||||
Set(show, peer, values);
|
||||
close();
|
||||
@@ -658,7 +754,7 @@ public:
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Ui::ChatStyle> style,
|
||||
rpl::producer<std::vector<uint8>> indices,
|
||||
uint8 index,
|
||||
rpl::producer<uint8> index,
|
||||
Fn<void(uint8)> callback);
|
||||
|
||||
private:
|
||||
@@ -669,7 +765,7 @@ private:
|
||||
const std::shared_ptr<Ui::ChatStyle> _style;
|
||||
std::vector<std::unique_ptr<ColorSample>> _samples;
|
||||
const Fn<void(uint8)> _callback;
|
||||
uint8 _index = 0;
|
||||
rpl::variable<uint8> _index;
|
||||
|
||||
};
|
||||
|
||||
@@ -677,12 +773,12 @@ ColorSelector::ColorSelector(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
std::shared_ptr<Ui::ChatStyle> style,
|
||||
rpl::producer<std::vector<uint8>> indices,
|
||||
uint8 index,
|
||||
rpl::producer<uint8> index,
|
||||
Fn<void(uint8)> callback)
|
||||
: RpWidget(box)
|
||||
, _style(style)
|
||||
, _callback(std::move(callback))
|
||||
, _index(index) {
|
||||
, _index(std::move(index)) {
|
||||
std::move(
|
||||
indices
|
||||
) | rpl::start_with_next([=](std::vector<uint8> indices) {
|
||||
@@ -692,6 +788,7 @@ ColorSelector::ColorSelector(
|
||||
|
||||
void ColorSelector::fillFrom(std::vector<uint8> indices) {
|
||||
auto samples = std::vector<std::unique_ptr<ColorSample>>();
|
||||
const auto initial = _index.current();
|
||||
const auto add = [&](uint8 index) {
|
||||
auto i = ranges::find(_samples, index, &ColorSample::index);
|
||||
if (i != end(_samples)) {
|
||||
@@ -702,37 +799,36 @@ void ColorSelector::fillFrom(std::vector<uint8> indices) {
|
||||
this,
|
||||
_style,
|
||||
index,
|
||||
index == _index));
|
||||
index == initial));
|
||||
samples.back()->show();
|
||||
samples.back()->setClickedCallback([=] {
|
||||
if (_index != index) {
|
||||
_callback(index);
|
||||
|
||||
ranges::find(
|
||||
_samples,
|
||||
_index,
|
||||
&ColorSample::index
|
||||
)->get()->setSelected(false);
|
||||
_index = index;
|
||||
ranges::find(
|
||||
_samples,
|
||||
_index,
|
||||
&ColorSample::index
|
||||
)->get()->setSelected(true);
|
||||
}
|
||||
_callback(index);
|
||||
});
|
||||
}
|
||||
};
|
||||
for (const auto index : indices) {
|
||||
add(index);
|
||||
}
|
||||
if (!ranges::contains(indices, _index)) {
|
||||
add(_index);
|
||||
if (initial != kUnsetColorIndex && !ranges::contains(indices, initial)) {
|
||||
add(initial);
|
||||
}
|
||||
_samples = std::move(samples);
|
||||
if (width() > 0) {
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
_index.value(
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::start_with_next([=](uint8 was, uint8 now) {
|
||||
const auto i = ranges::find(_samples, was, &ColorSample::index);
|
||||
if (i != end(_samples)) {
|
||||
i->get()->setSelected(false);
|
||||
}
|
||||
const auto j = ranges::find(_samples, now, &ColorSample::index);
|
||||
if (j != end(_samples)) {
|
||||
j->get()->setSelected(true);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
int ColorSelector::resizeGetHeight(int newWidth) {
|
||||
@@ -840,7 +936,7 @@ int ColorSelector::resizeGetHeight(int newWidth) {
|
||||
}
|
||||
auto p = QPainter(right);
|
||||
const auto height = right->height();
|
||||
if (state->emoji) {
|
||||
if (state->emoji && state->index != kUnsetColorIndex) {
|
||||
const auto colors = style->coloredValues(false, state->index);
|
||||
state->emoji->paint(p, {
|
||||
.textColor = colors.name,
|
||||
@@ -1121,6 +1217,336 @@ int ColorSelector::resizeGetHeight(int newWidth) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void AddColorGiftTabs(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void(uint64 giftId)> chosen) {
|
||||
using namespace Info::PeerGifts;
|
||||
|
||||
struct State {
|
||||
rpl::variable<std::vector<Data::StarGift>> list;
|
||||
Ui::SubTabs *tabs = nullptr;
|
||||
};
|
||||
const auto state = container->lifetime().make_state<State>();
|
||||
|
||||
GiftsStars(
|
||||
session,
|
||||
session->user()
|
||||
) | rpl::start_with_next([=](const std::vector<GiftTypeStars> &list) {
|
||||
auto filtered = std::vector<Data::StarGift>();
|
||||
for (const auto &gift : list) {
|
||||
if (gift.info.peerColorAvailable && gift.resale) {
|
||||
filtered.push_back(gift.info);
|
||||
}
|
||||
}
|
||||
state->list = std::move(filtered);
|
||||
}, container->lifetime());
|
||||
|
||||
state->list.value(
|
||||
) | rpl::start_with_next([=](const std::vector<Data::StarGift> &list) {
|
||||
auto tabs = std::vector<Ui::SubTabs::Tab>();
|
||||
tabs.push_back({
|
||||
.id = u"my"_q,
|
||||
.text = tr::lng_gift_stars_tabs_my(tr::now, Ui::Text::WithEntities),
|
||||
});
|
||||
for (const auto &gift : list) {
|
||||
auto text = TextWithEntities();
|
||||
tabs.push_back({
|
||||
.id = QString::number(gift.id),
|
||||
.text = Data::SingleCustomEmoji(
|
||||
gift.document).append(' ').append(gift.resellTitle),
|
||||
});
|
||||
}
|
||||
const auto context = Core::TextContext({
|
||||
.session = session,
|
||||
});
|
||||
if (!state->tabs) {
|
||||
state->tabs = container->add(
|
||||
object_ptr<Ui::SubTabs>(
|
||||
container,
|
||||
st::defaultSubTabs,
|
||||
Ui::SubTabs::Options{
|
||||
.selected = u"my"_q,
|
||||
.centered = true,
|
||||
},
|
||||
std::move(tabs),
|
||||
context));
|
||||
|
||||
state->tabs->activated(
|
||||
) | rpl::start_with_next([=](const QString &id) {
|
||||
state->tabs->setActiveTab(id);
|
||||
chosen(id.toULongLong());
|
||||
}, state->tabs->lifetime());
|
||||
} else {
|
||||
state->tabs->setTabs(std::move(tabs), context);
|
||||
}
|
||||
container->resizeToWidth(container->width());
|
||||
}, container->lifetime());
|
||||
}
|
||||
|
||||
void AddGiftSelector(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Main::Session*> session,
|
||||
rpl::producer<uint64> showingGiftIdValue,
|
||||
Fn<void(std::shared_ptr<Data::UniqueGift> selected)> chosen,
|
||||
rpl::producer<std::optional<Ui::ColorCollectible>> selected) {
|
||||
using namespace Info::PeerGifts;
|
||||
|
||||
const auto raw = container->add(
|
||||
object_ptr<Ui::VisibleRangeWidget>(container));
|
||||
|
||||
struct List {
|
||||
std::vector<GiftTypeStars> list;
|
||||
rpl::lifetime loading;
|
||||
QString offset;
|
||||
bool loaded = false;
|
||||
};
|
||||
struct State {
|
||||
std::optional<Delegate> delegate;
|
||||
rpl::variable<uint64> showingGiftId;
|
||||
base::flat_map<uint64, List> lists;
|
||||
List *current = nullptr;
|
||||
std::vector<bool> validated;
|
||||
std::vector<std::unique_ptr<GiftButton>> buttons;
|
||||
rpl::variable<Ui::VisibleRange> visibleRange;
|
||||
rpl::variable<std::optional<Ui::ColorCollectible>> selected;
|
||||
int perRow = 1;
|
||||
|
||||
Fn<void()> loadMore;
|
||||
Fn<void()> resize;
|
||||
Fn<void()> rebuild;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
state->delegate.emplace(session, GiftButtonMode::Full);
|
||||
state->showingGiftId = std::move(showingGiftIdValue);
|
||||
state->selected = std::move(selected);
|
||||
const auto shadow = st::defaultDropdownMenu.wrap.shadow;
|
||||
const auto extend = shadow.extend;
|
||||
state->loadMore = [=] {
|
||||
const auto selfId = session->userPeerId();
|
||||
const auto shownGiftId = state->showingGiftId.current();
|
||||
if (state->current->loaded || state->current->loading) {
|
||||
return;
|
||||
} else if (shownGiftId) {
|
||||
state->current->loading = Data::ResaleGiftsSlice(
|
||||
session,
|
||||
shownGiftId,
|
||||
{},
|
||||
state->current->offset
|
||||
) | rpl::start_with_next([=](Data::ResaleGiftsDescriptor slice) {
|
||||
auto &entry = state->lists[shownGiftId];
|
||||
entry.loading.destroy();
|
||||
entry.offset = slice.offset;
|
||||
entry.loaded = entry.offset.isEmpty();
|
||||
if (state->showingGiftId.current() != shownGiftId) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &list = state->current->list;
|
||||
for (const auto &gift : slice.list) {
|
||||
if (gift.unique && gift.unique->peerColor) {
|
||||
list.push_back({
|
||||
.info = gift,
|
||||
.resale = true,
|
||||
.mine = (gift.unique->ownerId == selfId),
|
||||
});
|
||||
}
|
||||
}
|
||||
state->resize();
|
||||
});
|
||||
} else {
|
||||
state->current->loading = Data::MyUniqueGiftsSlice(
|
||||
session,
|
||||
Data::MyUniqueType::OwnedAndHosted,
|
||||
state->current->offset
|
||||
) | rpl::start_with_next([=](Data::MyGiftsDescriptor slice) {
|
||||
auto &entry = state->lists[shownGiftId];
|
||||
entry.loading.destroy();
|
||||
entry.offset = slice.offset;
|
||||
entry.loaded = entry.offset.isEmpty();
|
||||
if (state->showingGiftId.current() != shownGiftId) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &list = state->current->list;
|
||||
for (const auto &gift : slice.list) {
|
||||
if (gift.info.unique && gift.info.unique->peerColor) {
|
||||
list.push_back({ .info = gift.info });
|
||||
}
|
||||
}
|
||||
state->resize();
|
||||
});
|
||||
}
|
||||
};
|
||||
state->rebuild = [=] {
|
||||
const auto shownGiftId = state->showingGiftId.current();
|
||||
const auto width = st::boxWideWidth;
|
||||
const auto padding = st::giftBoxPadding;
|
||||
const auto available = width - padding.left() - padding.right();
|
||||
const auto range = state->visibleRange.current();
|
||||
const auto count = int(state->current->list.size());
|
||||
|
||||
auto &buttons = state->buttons;
|
||||
if (buttons.size() < count) {
|
||||
buttons.resize(count);
|
||||
}
|
||||
auto &validated = state->validated;
|
||||
validated.resize(count);
|
||||
|
||||
auto x = padding.left();
|
||||
auto y = padding.top();
|
||||
const auto single = state->delegate->buttonSize();
|
||||
const auto perRow = state->perRow;
|
||||
const auto singlew = single.width() + st::giftBoxGiftSkip.x();
|
||||
const auto singleh = single.height() + st::giftBoxGiftSkip.y();
|
||||
const auto rowFrom = std::max(range.top - y, 0) / singleh;
|
||||
const auto rowTill = (std::max(range.bottom - y + st::giftBoxGiftSkip.y(), 0) + singleh - 1)
|
||||
/ singleh;
|
||||
Assert(rowTill >= rowFrom);
|
||||
const auto first = rowFrom * perRow;
|
||||
const auto last = std::min(rowTill * perRow, count);
|
||||
const auto current = state->selected.current();
|
||||
const auto selectedId = current ? current->collectibleId : 0;
|
||||
auto checkedFrom = 0;
|
||||
auto checkedTill = int(buttons.size());
|
||||
const auto ensureButton = [&](int index) {
|
||||
auto &button = buttons[index];
|
||||
if (!button) {
|
||||
validated[index] = false;
|
||||
for (; checkedFrom != first; ++checkedFrom) {
|
||||
if (buttons[checkedFrom]) {
|
||||
button = std::move(buttons[checkedFrom]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!button) {
|
||||
for (; checkedTill != last; ) {
|
||||
--checkedTill;
|
||||
if (buttons[checkedTill]) {
|
||||
button = std::move(buttons[checkedTill]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!button) {
|
||||
const auto delegate = &*state->delegate;
|
||||
button = std::make_unique<GiftButton>(raw, delegate);
|
||||
}
|
||||
const auto raw = button.get();
|
||||
if (validated[index]) {
|
||||
return;
|
||||
}
|
||||
raw->show();
|
||||
validated[index] = true;
|
||||
const auto &gift = state->current->list[index];
|
||||
raw->setDescriptor({ gift }, shownGiftId
|
||||
? GiftButtonMode::Full
|
||||
: GiftButtonMode::Minimal);
|
||||
raw->setClickedCallback([=, unique = gift.info.unique] {
|
||||
chosen(unique);
|
||||
});
|
||||
raw->setGeometry(QRect(QPoint(x, y), single), extend);
|
||||
raw->toggleSelected(
|
||||
gift.info.unique->id == selectedId,
|
||||
GiftSelectionMode::Inset,
|
||||
anim::type::instant);
|
||||
};
|
||||
y += rowFrom * singleh;
|
||||
for (auto row = rowFrom; row != rowTill; ++row) {
|
||||
for (auto col = 0; col != perRow; ++col) {
|
||||
const auto index = row * perRow + col;
|
||||
if (index >= count) {
|
||||
break;
|
||||
}
|
||||
const auto last = !((col + 1) % perRow);
|
||||
if (last) {
|
||||
x = padding.left() + available - single.width();
|
||||
}
|
||||
ensureButton(index);
|
||||
if (last) {
|
||||
x = padding.left();
|
||||
y += singleh;
|
||||
} else {
|
||||
x += singlew;
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto till = std::min(int(buttons.size()), rowTill * perRow);
|
||||
for (auto i = count; i < till; ++i) {
|
||||
if (const auto button = buttons[i].get()) {
|
||||
button->hide();
|
||||
}
|
||||
}
|
||||
|
||||
state->selected.value(
|
||||
) | rpl::combine_previous() | rpl::start_with_next([=](
|
||||
const std::optional<Ui::ColorCollectible> &was,
|
||||
const std::optional<Ui::ColorCollectible> &now) {
|
||||
const auto wasId = was ? was->collectibleId : 0;
|
||||
const auto nowId = now ? now->collectibleId : 0;
|
||||
const auto find = [&](uint64 id) -> GiftButton* {
|
||||
if (!id) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto i = 0, count = int(state->current->list.size())
|
||||
; i != count
|
||||
; ++i) {
|
||||
const auto &gift = state->current->list[i];
|
||||
if (gift.info.unique->id == id) {
|
||||
return state->buttons[i].get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
if (const auto button = find(wasId)) {
|
||||
button->toggleSelected(false, GiftSelectionMode::Inset);
|
||||
}
|
||||
if (const auto button = find(nowId)) {
|
||||
button->toggleSelected(true, GiftSelectionMode::Inset);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
const auto page = range.bottom - range.top;
|
||||
if (page > 0 && range.bottom + page > raw->height()) {
|
||||
state->loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
const auto width = st::boxWideWidth;
|
||||
const auto padding = st::giftBoxPadding;
|
||||
const auto available = width - padding.left() - padding.right();
|
||||
state->perRow = available / state->delegate->buttonSize().width();
|
||||
|
||||
state->resize = [=] {
|
||||
const auto count = int(state->current->list.size());
|
||||
state->validated.clear();
|
||||
|
||||
const auto rows = (count + state->perRow - 1) / state->perRow;
|
||||
const auto height = padding.top()
|
||||
+ (rows * state->delegate->buttonSize().height())
|
||||
+ ((rows - 1) * st::giftBoxGiftSkip.y())
|
||||
+ padding.bottom();
|
||||
raw->resize(raw->width(), height);
|
||||
|
||||
state->rebuild();
|
||||
};
|
||||
|
||||
state->showingGiftId.value(
|
||||
) | rpl::start_with_next([=](uint64 showingId) {
|
||||
state->current = &state->lists[showingId];
|
||||
state->buttons.clear();
|
||||
state->delegate.emplace(session, showingId
|
||||
? GiftButtonMode::Full
|
||||
: GiftButtonMode::Minimal);
|
||||
state->resize();
|
||||
}, raw->lifetime());
|
||||
|
||||
state->visibleRange = raw->visibleRange();
|
||||
state->visibleRange.value(
|
||||
) | rpl::start_with_next(state->rebuild, raw->lifetime());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddLevelBadge(
|
||||
@@ -1169,20 +1595,32 @@ void EditPeerColorBox(
|
||||
? tr::lng_settings_color_title()
|
||||
: tr::lng_edit_channel_color());
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::giftBox);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
struct State {
|
||||
rpl::variable<uint8> index;
|
||||
rpl::variable<DocumentId> emojiId;
|
||||
rpl::variable<EmojiStatusId> statusId;
|
||||
rpl::variable<std::optional<Ui::ColorCollectible>> collectible;
|
||||
rpl::variable<uint64> showingGiftId;
|
||||
std::shared_ptr<Data::UniqueGift> buyCollectible;
|
||||
TimeId statusUntil = 0;
|
||||
bool statusChanged = false;
|
||||
bool changing = false;
|
||||
bool applying = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->index = peer->colorIndex();
|
||||
state->index = peer->colorCollectible()
|
||||
? kUnsetColorIndex
|
||||
: peer->colorIndex();
|
||||
state->emojiId = peer->backgroundEmojiId();
|
||||
state->statusId = peer->emojiStatusId();
|
||||
state->collectible = peer->colorCollectible()
|
||||
? *peer->colorCollectible()
|
||||
: std::optional<Ui::ColorCollectible>();
|
||||
if (group) {
|
||||
Settings::AddDividerTextWithLottie(box->verticalLayout(), {
|
||||
.lottie = u"palette"_q,
|
||||
@@ -1199,7 +1637,8 @@ void EditPeerColorBox(
|
||||
theme,
|
||||
peer,
|
||||
state->index.value(),
|
||||
state->emojiId.value()
|
||||
state->emojiId.value(),
|
||||
state->collectible.value()
|
||||
), style::margins());
|
||||
|
||||
auto indices = peer->session().api().peerColors().suggestedValue();
|
||||
@@ -1210,8 +1649,15 @@ void EditPeerColorBox(
|
||||
box,
|
||||
style,
|
||||
std::move(indices),
|
||||
state->index.current(),
|
||||
[=](uint8 index) { state->index = index; }),
|
||||
state->index.value(),
|
||||
[=](uint8 index) {
|
||||
if (state->collectible.current()) {
|
||||
state->buyCollectible = nullptr;
|
||||
state->collectible = std::nullopt;
|
||||
state->emojiId = 0;
|
||||
}
|
||||
state->index = index;
|
||||
}),
|
||||
{ margin, skip, margin, skip });
|
||||
|
||||
Ui::AddDividerText(
|
||||
@@ -1221,10 +1667,15 @@ void EditPeerColorBox(
|
||||
: tr::lng_settings_color_about_channel()),
|
||||
st::peerAppearanceDividerTextMargin);
|
||||
|
||||
Ui::AddSkip(container, st::settingsColorSampleSkip);
|
||||
const auto iconWrap = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container)));
|
||||
const auto iconInner = iconWrap->entity();
|
||||
|
||||
container->add(CreateEmojiIconButton(
|
||||
container,
|
||||
Ui::AddSkip(iconInner, st::settingsColorSampleSkip);
|
||||
iconInner->add(CreateEmojiIconButton(
|
||||
iconInner,
|
||||
show,
|
||||
style,
|
||||
peer,
|
||||
@@ -1232,13 +1683,19 @@ void EditPeerColorBox(
|
||||
state->emojiId.value(),
|
||||
[=](DocumentId id) { state->emojiId = id; }));
|
||||
|
||||
Ui::AddSkip(container, st::settingsColorSampleSkip);
|
||||
Ui::AddSkip(iconInner, st::settingsColorSampleSkip);
|
||||
Ui::AddDividerText(
|
||||
container,
|
||||
iconInner,
|
||||
(peer->isSelf()
|
||||
? tr::lng_settings_color_emoji_about()
|
||||
: tr::lng_settings_color_emoji_about_channel()),
|
||||
st::peerAppearanceDividerTextMargin);
|
||||
|
||||
iconWrap->toggleOn(state->collectible.value(
|
||||
) | rpl::map([](const std::optional<Ui::ColorCollectible> &value) {
|
||||
return !value.has_value();
|
||||
}));
|
||||
iconWrap->finishAnimating();
|
||||
}
|
||||
|
||||
if (const auto channel = peer->asChannel()) {
|
||||
@@ -1324,28 +1781,108 @@ void EditPeerColorBox(
|
||||
? tr::lng_edit_channel_status_about_group()
|
||||
: tr::lng_edit_channel_status_about()),
|
||||
st::peerAppearanceDividerTextMargin);
|
||||
} else if (peer->isSelf()) {
|
||||
Ui::AddSkip(container, st::settingsColorSampleSkip);
|
||||
|
||||
const auto session = &peer->session();
|
||||
AddColorGiftTabs(container, session, [=](uint64 giftId) {
|
||||
state->showingGiftId = giftId;
|
||||
});
|
||||
|
||||
auto showingGiftId = state->showingGiftId.value();
|
||||
AddGiftSelector(container, session, std::move(showingGiftId), [=](
|
||||
std::shared_ptr<Data::UniqueGift> selected) {
|
||||
state->index = selected->peerColor ? kUnsetColorIndex : 0;
|
||||
state->emojiId = selected->peerColor
|
||||
? selected->peerColor->backgroundEmojiId
|
||||
: 0;
|
||||
state->buyCollectible = (selected->peerColor
|
||||
&& (selected->ownerId != session->userPeerId())
|
||||
&& selected->starsForResale > 0)
|
||||
? selected
|
||||
: nullptr;
|
||||
state->collectible = selected->peerColor
|
||||
? *selected->peerColor
|
||||
: std::optional<Ui::ColorCollectible>();
|
||||
}, state->collectible.value());
|
||||
}
|
||||
|
||||
box->addButton(tr::lng_settings_apply(), [=] {
|
||||
const auto button = box->addButton(tr::lng_settings_color_apply(), [=] {
|
||||
if (state->applying) {
|
||||
return;
|
||||
} else if (ShowPremiumToast(show, peer)) {
|
||||
return;
|
||||
}
|
||||
state->applying = true;
|
||||
Apply(show, peer, {
|
||||
const auto values = SetValues{
|
||||
state->index.current(),
|
||||
state->emojiId.current(),
|
||||
state->collectible.current(),
|
||||
state->statusId.current(),
|
||||
state->statusUntil,
|
||||
state->statusChanged,
|
||||
}, crl::guard(box, [=] {
|
||||
};
|
||||
if (const auto buy = state->buyCollectible) {
|
||||
const auto done = [=, weak = base::make_weak(box)](bool ok) {
|
||||
if (ok) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->closeBox();
|
||||
}
|
||||
Apply(show, peer, values, [] {}, [] {});
|
||||
}
|
||||
};
|
||||
const auto to = peer->session().user();
|
||||
ShowBuyResaleGiftBox(show, buy, false, to, done);
|
||||
return;
|
||||
}
|
||||
state->applying = true;
|
||||
Apply(show, peer, values, crl::guard(box, [=] {
|
||||
box->closeBox();
|
||||
}), crl::guard(box, [=] {
|
||||
state->applying = false;
|
||||
}));
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
state->collectible.value(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto buy = state->buyCollectible.get();
|
||||
while (!button->children().isEmpty()) {
|
||||
delete button->children().first();
|
||||
}
|
||||
if (!buy) {
|
||||
button->setText(rpl::combine(
|
||||
tr::lng_settings_color_apply(),
|
||||
Data::AmPremiumValue(&peer->session())
|
||||
) | rpl::map([=](const QString &text, bool premium) {
|
||||
auto result = TextWithEntities();
|
||||
if (!premium && peer->isSelf()) {
|
||||
result.append(Ui::Text::IconEmoji(&st::giftBoxLock));
|
||||
}
|
||||
result.append(text);
|
||||
return result;
|
||||
}));
|
||||
} else if (buy->onlyAcceptTon) {
|
||||
button->setText(rpl::single(QString()));
|
||||
Ui::SetButtonTwoLabels(
|
||||
button,
|
||||
tr::lng_gift_buy_resale_button(
|
||||
lt_cost,
|
||||
rpl::single(Data::FormatGiftResaleTon(*buy)),
|
||||
Ui::Text::WithEntities),
|
||||
tr::lng_gift_buy_resale_equals(
|
||||
lt_cost,
|
||||
rpl::single(Ui::Text::IconEmoji(
|
||||
&st::starIconEmojiSmall
|
||||
).append(Lang::FormatCountDecimal(buy->starsForResale))),
|
||||
Ui::Text::WithEntities),
|
||||
st::resaleButtonTitle,
|
||||
st::resaleButtonSubtitle);
|
||||
} else {
|
||||
button->setText(tr::lng_gift_buy_resale_button(
|
||||
lt_cost,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::starIconEmoji).append(
|
||||
Lang::FormatCountDecimal(buy->starsForResale))),
|
||||
Ui::Text::WithEntities));
|
||||
}
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
void SetupPeerColorSample(
|
||||
@@ -1359,12 +1896,20 @@ void SetupPeerColorSample(
|
||||
) | rpl::map([=] {
|
||||
return peer->colorIndex();
|
||||
});
|
||||
auto colorCollectibleValue = peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Color
|
||||
) | rpl::map([=] {
|
||||
return peer->colorCollectible();
|
||||
});
|
||||
const auto name = peer->shortName();
|
||||
|
||||
const auto sample = Ui::CreateChild<ColorSample>(
|
||||
button.get(),
|
||||
&peer->session(),
|
||||
style,
|
||||
rpl::duplicate(colorIndexValue),
|
||||
rpl::duplicate(colorCollectibleValue),
|
||||
name);
|
||||
sample->show();
|
||||
|
||||
|
||||
@@ -1135,7 +1135,7 @@ void Controller::fillForumButton() {
|
||||
_forumSavedValue = _peer->isForum();
|
||||
_forumTabsSavedValue = !_peer->isChannel()
|
||||
|| !_peer->isForum()
|
||||
|| _peer->asChannel()->useSubsectionTabs();
|
||||
|| _peer->useSubsectionTabs();
|
||||
|
||||
const auto changes = std::make_shared<rpl::event_stream<>>();
|
||||
const auto label = [=] {
|
||||
|
||||
@@ -50,7 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSlowmodeValues = 7;
|
||||
constexpr auto kSlowmodeValues = 8;
|
||||
constexpr auto kBoostsUnrestrictValues = 5;
|
||||
constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000);
|
||||
constexpr auto kDefaultChargeStars = 10;
|
||||
@@ -188,12 +188,13 @@ int SlowmodeDelayByIndex(int index) {
|
||||
|
||||
switch (index) {
|
||||
case 0: return 0;
|
||||
case 1: return 10;
|
||||
case 2: return 30;
|
||||
case 3: return 60;
|
||||
case 4: return 5 * 60;
|
||||
case 5: return 15 * 60;
|
||||
case 6: return 60 * 60;
|
||||
case 1: return 5;
|
||||
case 2: return 10;
|
||||
case 3: return 30;
|
||||
case 4: return 60;
|
||||
case 5: return 5 * 60;
|
||||
case 6: return 15 * 60;
|
||||
case 7: return 60 * 60;
|
||||
}
|
||||
Unexpected("Index in SlowmodeDelayByIndex.");
|
||||
}
|
||||
|
||||
@@ -7,23 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "boxes/peers/edit_peer_usernames_list.h"
|
||||
|
||||
#include "api/api_filter_updates.h"
|
||||
#include "api/api_user_names.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/layers/show.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/text/text_utilities.h" // Ui::Text::RichLangValue.
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/vertical_layout_reorder.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_boxes.h" // contactsStatusFont.
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
@@ -208,6 +211,17 @@ UsernamesList::UsernamesList(
|
||||
}
|
||||
}
|
||||
load();
|
||||
|
||||
rpl::merge(
|
||||
peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Username),
|
||||
peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Usernames)
|
||||
) | rpl::start_with_next([=] {
|
||||
load();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void UsernamesList::load() {
|
||||
@@ -250,6 +264,8 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) {
|
||||
username.username);
|
||||
const auto status = (username.editable && _focusCallback)
|
||||
? tr::lng_usernames_edit(tr::now)
|
||||
: (username.editable && !username.active)
|
||||
? tr::lng_usernames_non_active(tr::now)
|
||||
: username.active
|
||||
? tr::lng_usernames_active(tr::now)
|
||||
: tr::lng_usernames_non_active(tr::now);
|
||||
@@ -265,8 +281,20 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) {
|
||||
if (username.editable) {
|
||||
if (_focusCallback) {
|
||||
_focusCallback();
|
||||
return;
|
||||
}
|
||||
if (_isBot) {
|
||||
const auto hasActiveAuction = ranges::any_of(
|
||||
usernames,
|
||||
[](const Data::Username &u) {
|
||||
return !u.editable && u.active;
|
||||
});
|
||||
if (!hasActiveAuction && username.active) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = _peer->isSelf()
|
||||
@@ -309,10 +337,13 @@ void UsernamesList::rebuild(const Data::Usernames &usernames) {
|
||||
rpl::single(kMaxUsernames),
|
||||
Ui::Text::RichLangValue)));
|
||||
}
|
||||
if (error == Api::Usernames::Error::Flood) {
|
||||
_show->showToast(
|
||||
tr::lng_flood_error(tr::now));
|
||||
}
|
||||
load();
|
||||
_toggleLifetime.destroy();
|
||||
}, [=] {
|
||||
load();
|
||||
_toggleLifetime.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,9 +230,13 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {
|
||||
if (fill > 0) {
|
||||
const auto t = roundedHeight + _scrollTop;
|
||||
p.drawImage(
|
||||
QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor),
|
||||
QRect(0, t, roundedWidth, roundedWidth - t),
|
||||
image,
|
||||
QRect(0, t, roundedWidth * factor, (roundedWidth - t) * factor));
|
||||
QRect(
|
||||
0,
|
||||
t * factor,
|
||||
roundedWidth * factor,
|
||||
(roundedWidth - t) * factor));
|
||||
}
|
||||
if (covered <= 0) {
|
||||
return;
|
||||
@@ -241,9 +245,9 @@ void PeerShortInfoCover::paintCoverImage(QPainter &p, const QImage &image) {
|
||||
const auto from = top - rounded;
|
||||
auto q = QPainter(&_roundedTopImage);
|
||||
q.drawImage(
|
||||
QRect(0, 0, roundedWidth * factor, rounded * factor),
|
||||
QRect(0, 0, roundedWidth, rounded),
|
||||
image,
|
||||
QRect(0, _scrollTop, roundedWidth * factor, rounded * factor));
|
||||
QRect(0, _scrollTop * factor, roundedWidth * factor, rounded * factor));
|
||||
q.end();
|
||||
_roundedTopImage = Images::Round(
|
||||
std::move(_roundedTopImage),
|
||||
@@ -811,6 +815,10 @@ void PeerShortInfoBox::prepareRows() {
|
||||
birthdayLabel(),
|
||||
birthdayValue() | Ui::Text::ToWithEntities(),
|
||||
tr::lng_mediaview_copy(tr::now));
|
||||
addInfoLine(
|
||||
tr::lng_info_notes_label(),
|
||||
noteValue(),
|
||||
_st.labeled);
|
||||
}
|
||||
|
||||
void PeerShortInfoBox::resizeEvent(QResizeEvent *e) {
|
||||
@@ -917,3 +925,9 @@ rpl::producer<TextWithEntities> PeerShortInfoBox::aboutValue() const {
|
||||
return fields.about;
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
rpl::producer<TextWithEntities> PeerShortInfoBox::noteValue() const {
|
||||
return _fields.value() | rpl::map([](const PeerShortInfoFields &fields) {
|
||||
return fields.note;
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ struct PeerShortInfoFields {
|
||||
TextWithEntities about;
|
||||
QString username;
|
||||
Data::Birthday birthday;
|
||||
TextWithEntities note;
|
||||
bool isBio = false;
|
||||
};
|
||||
|
||||
@@ -188,6 +189,7 @@ private:
|
||||
[[nodiscard]] rpl::producer<QString> birthdayLabel() const;
|
||||
[[nodiscard]] rpl::producer<QString> birthdayValue() const;
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> aboutValue() const;
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> noteValue() const;
|
||||
|
||||
const style::ShortInfoBox &_st;
|
||||
const PeerShortInfoType _type = PeerShortInfoType::User;
|
||||
|
||||
@@ -210,7 +210,8 @@ void ProcessFullPhoto(
|
||||
| UpdateFlag::PhoneNumber
|
||||
| UpdateFlag::Username
|
||||
| UpdateFlag::About
|
||||
| UpdateFlag::Birthday)
|
||||
| UpdateFlag::Birthday
|
||||
| UpdateFlag::ContactNote)
|
||||
) | rpl::map([=] {
|
||||
const auto user = peer->asUser();
|
||||
const auto username = peer->username();
|
||||
@@ -237,6 +238,7 @@ void ProcessFullPhoto(
|
||||
? ('@' + username)
|
||||
: QString()),
|
||||
.birthday = user ? user->birthday() : Data::Birthday(),
|
||||
.note = user ? user->note() : TextWithEntities(),
|
||||
.isBio = (user && !user->isBot()),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,8 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "apiwrap.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_premium_limits.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_premium_limits.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_cloud_themes.h"
|
||||
#include "data/data_session.h"
|
||||
@@ -32,8 +34,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
||||
namespace {
|
||||
@@ -833,3 +837,174 @@ object_ptr<Ui::RpWidget> CreateUserpicsWithMoreBadge(
|
||||
}, overlay->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
class UniqueGiftBackground final : public Ui::DynamicImage {
|
||||
public:
|
||||
UniqueGiftBackground(
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<Data::UniqueGift> unique)
|
||||
: _session(session)
|
||||
, _unique(std::move(unique)) {
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> clone() override {
|
||||
return std::make_shared<UniqueGiftBackground>(_session, _unique);
|
||||
}
|
||||
|
||||
void subscribeToUpdates(Fn<void()> callback) override {
|
||||
_repaint = std::move(callback);
|
||||
if (!_repaint) {
|
||||
_patternEmoji = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QImage image(int size) override {
|
||||
if (!_patternEmoji) {
|
||||
_patternEmoji = _session->data().customEmojiManager().create(
|
||||
_unique->pattern.document,
|
||||
[=] { ready(); },
|
||||
Data::CustomEmojiSizeTag::Large);
|
||||
[[maybe_unused]] const auto preload = _patternEmoji->ready();
|
||||
}
|
||||
const auto inner = QRect(0, 0, size, size);
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
if (_backgroundCache.size() != inner.size() * ratio) {
|
||||
_backgroundCache = QImage(
|
||||
inner.size() * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_backgroundCache.fill(Qt::transparent);
|
||||
_backgroundCache.setDevicePixelRatio(ratio);
|
||||
|
||||
const auto radius = st::giftBoxGiftRadius;
|
||||
auto p = QPainter(&_backgroundCache);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto gradient = QRadialGradient(
|
||||
inner.center(),
|
||||
inner.width() / 2);
|
||||
gradient.setStops({
|
||||
{ 0., _unique->backdrop.centerColor },
|
||||
{ 1., _unique->backdrop.edgeColor },
|
||||
});
|
||||
p.setBrush(gradient);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(inner, radius, radius);
|
||||
_backroundPatterned = false;
|
||||
}
|
||||
if (!_backroundPatterned && _patternEmoji->ready()) {
|
||||
_backroundPatterned = true;
|
||||
auto p = QPainter(&_backgroundCache);
|
||||
p.setClipRect(inner);
|
||||
const auto skip = inner.width() / 3;
|
||||
Ui::PaintPoints(
|
||||
p,
|
||||
Ui::PatternPointsSmall(),
|
||||
_patternCache,
|
||||
_patternEmoji.get(),
|
||||
*_unique,
|
||||
QRect(-skip, 0, inner.width() + 2 * skip, inner.height()));
|
||||
}
|
||||
return _backgroundCache;
|
||||
}
|
||||
|
||||
private:
|
||||
void ready() {
|
||||
if (!_backroundPatterned && _repaint) {
|
||||
_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const std::shared_ptr<Data::UniqueGift> _unique;
|
||||
Fn<void()> _repaint;
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;
|
||||
QImage _backgroundCache;
|
||||
base::flat_map<float64, QImage> _patternCache;
|
||||
bool _backroundPatterned = false;
|
||||
|
||||
};
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateGiftTransfer(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Data::UniqueGift> unique,
|
||||
not_null<PeerData*> to) {
|
||||
struct State {
|
||||
QImage layer;
|
||||
QPoint giftPosition;
|
||||
std::shared_ptr<UniqueGiftBackground> bg;
|
||||
std::shared_ptr<Ui::Text::CustomEmoji> sticker;
|
||||
};
|
||||
const auto st = &st::boostReplaceUserpicsRow;
|
||||
const auto full = st->button.size.height()
|
||||
+ st::boostReplaceIconAdd.y()
|
||||
+ st::lineWidth;
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent, full);
|
||||
const auto raw = result.data();
|
||||
const auto right = CreateChild<Ui::UserpicButton>(raw, to, st->button);
|
||||
const auto overlay = CreateChild<Ui::RpWidget>(raw);
|
||||
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
state->bg = std::make_shared<UniqueGiftBackground>(
|
||||
&to->session(),
|
||||
unique);
|
||||
state->bg->subscribeToUpdates([=] {
|
||||
overlay->update();
|
||||
});
|
||||
const auto tag = Data::CustomEmojiSizeTag::Isolated;
|
||||
state->sticker = to->owner().customEmojiManager().create(
|
||||
unique->model.document,
|
||||
[=] { overlay->update(); },
|
||||
tag);
|
||||
overlay->update();
|
||||
|
||||
raw->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
const auto skip = st::boostReplaceUserpicsSkip;
|
||||
const auto total = right->width() + skip + right->width();
|
||||
auto x = (width - total) / 2;
|
||||
state->giftPosition = QPoint(x, 0);
|
||||
x += right->width() + skip;
|
||||
right->moveToLeft(x, 0);
|
||||
overlay->setGeometry(QRect(0, 0, width, raw->height()));
|
||||
}, raw->lifetime());
|
||||
|
||||
overlay->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto outerw = overlay->width();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
if (state->layer.size() != QSize(outerw, full) * ratio) {
|
||||
state->layer = QImage(
|
||||
QSize(outerw, full) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
state->layer.setDevicePixelRatio(ratio);
|
||||
}
|
||||
state->layer.fill(Qt::transparent);
|
||||
|
||||
auto q = QPainter(&state->layer);
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
const auto from = QRect(state->giftPosition, right->size());
|
||||
const auto esize = Data::FrameSizeFromTag(tag) / ratio;
|
||||
q.drawImage(from, state->bg->image(from.width()));
|
||||
state->sticker->paint(q, {
|
||||
.textColor = st::windowFg->c,
|
||||
.now = crl::now(),
|
||||
.position = from.topLeft() + QPoint(
|
||||
(from.width() - esize) / 2,
|
||||
(from.height() - esize) / 2),
|
||||
});
|
||||
|
||||
const auto size = st::boostReplaceArrow.size();
|
||||
st::boostReplaceArrow.paint(
|
||||
q,
|
||||
(state->giftPosition.x()
|
||||
+ right->width()
|
||||
+ (st::boostReplaceUserpicsSkip - size.width()) / 2),
|
||||
(right->height() - size.height()) / 2,
|
||||
outerw);
|
||||
|
||||
q.end();
|
||||
|
||||
auto p = QPainter(overlay);
|
||||
p.drawImage(0, 0, state->layer);
|
||||
}, overlay->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ struct UserpicsRow;
|
||||
|
||||
class ChannelData;
|
||||
|
||||
namespace Data {
|
||||
struct UniqueGift;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
@@ -72,3 +76,8 @@ enum class UserpicsTransferType {
|
||||
rpl::producer<std::vector<not_null<PeerData*>>> peers,
|
||||
const style::UserpicsRow &st,
|
||||
int limit);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateGiftTransfer(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Data::UniqueGift> unique,
|
||||
not_null<PeerData*> to);
|
||||
|
||||
@@ -188,7 +188,7 @@ void Controller::confirmAdd(not_null<PeerData*> peer) {
|
||||
}, field->lifetime());
|
||||
|
||||
field->setMaxLength(limit * 2);
|
||||
Ui::AddLengthLimitLabel(field, limit, std::nullopt);
|
||||
Ui::AddLengthLimitLabel(field, limit);
|
||||
|
||||
Ui::AddDividerText(box->verticalLayout(), phrases.about());
|
||||
}));
|
||||
|
||||
@@ -311,8 +311,17 @@ void CaptionBox(
|
||||
}
|
||||
}
|
||||
|
||||
const auto send = [=](Api::SendOptions options) {
|
||||
done(std::move(options), input->getTextWithTags());
|
||||
const auto send = [=, show = controller->uiShow()](
|
||||
Api::SendOptions options) {
|
||||
const auto textWithTags = input->getTextWithTags();
|
||||
const auto remove = Ui::ComputeFieldCharacterCount(input)
|
||||
- Data::PremiumLimits(&show->session()).captionLengthCurrent();
|
||||
if (remove > 0) {
|
||||
show->showToast(
|
||||
tr::lng_edit_limit_reached(tr::now, lt_count, remove));
|
||||
return;
|
||||
}
|
||||
done(std::move(options), textWithTags);
|
||||
};
|
||||
const auto confirm = box->addButton(
|
||||
std::move(confirmText),
|
||||
@@ -387,6 +396,16 @@ void EditCaptionBox(
|
||||
show->showToast(tr::lng_edit_error(tr::now));
|
||||
return;
|
||||
}
|
||||
const auto textLength = textWithTags.text.size();
|
||||
const auto limit = Data::PremiumLimits(
|
||||
&item->history()->session()).captionLengthCurrent();
|
||||
if (textLength > limit) {
|
||||
show->showToast(tr::lng_edit_limit_reached(
|
||||
tr::now,
|
||||
lt_count,
|
||||
textLength - limit));
|
||||
return;
|
||||
}
|
||||
auto text = TextWithEntities{
|
||||
std::move(textWithTags.text),
|
||||
ConvertTextTagsToEntities(std::move(textWithTags.tags)),
|
||||
|
||||
@@ -1733,6 +1733,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
|
||||
const auto donePhraseArgs = CreateForwardedMessagePhraseArgs(
|
||||
result,
|
||||
msgIds);
|
||||
const auto showRecentForwardsToSelf = result.size() == 1
|
||||
&& result.front()->peer()->isSelf();
|
||||
const auto requestType = Data::Histories::RequestType::Send;
|
||||
for (const auto thread : result) {
|
||||
if (!comment.text.isEmpty()) {
|
||||
@@ -1790,13 +1792,22 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
|
||||
Api::SuggestToMTP(options.suggest)
|
||||
)).done([=](const MTPUpdates &updates, mtpRequestId reqId) {
|
||||
threadHistory->session().api().applyUpdates(updates);
|
||||
if (showRecentForwardsToSelf) {
|
||||
ApiWrap::ProcessRecentSelfForwards(
|
||||
&threadHistory->session(),
|
||||
updates,
|
||||
peer->id,
|
||||
history->peer->id);
|
||||
}
|
||||
state->requests.remove(reqId);
|
||||
if (state->requests.empty()) {
|
||||
if (show->valid()) {
|
||||
auto phrase = rpl::variable<TextWithEntities>(
|
||||
ChatHelpers::ForwardedMessagePhrase(
|
||||
donePhraseArgs)).current();
|
||||
show->showToast(std::move(phrase));
|
||||
if (!phrase.empty()) {
|
||||
show->showToast(std::move(phrase));
|
||||
}
|
||||
show->hideLayer();
|
||||
}
|
||||
}
|
||||
@@ -1954,7 +1965,9 @@ void FastShareMessageToSelf(
|
||||
auto phrase = rpl::variable<TextWithEntities>(
|
||||
ChatHelpers::ForwardedMessagePhrase(
|
||||
donePhraseArgs)).current();
|
||||
show->showToast(std::move(phrase));
|
||||
if (!phrase.empty()) {
|
||||
show->showToast(std::move(phrase));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -649,7 +649,9 @@ void ChangeSetNameBox(
|
||||
field->selectAll();
|
||||
constexpr auto kMaxSetNameLength = 50;
|
||||
field->setMaxLength(kMaxSetNameLength);
|
||||
Ui::AddLengthLimitLabel(field, kMaxSetNameLength, kMaxSetNameLength + 1);
|
||||
Ui::AddLengthLimitLabel(field, kMaxSetNameLength, {
|
||||
.customThreshold = kMaxSetNameLength + 1,
|
||||
});
|
||||
box->setFocusCallback([=] { field->setFocusFast(); });
|
||||
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
||||
const auto save = [=, show = box->uiShow()] {
|
||||
@@ -748,7 +750,7 @@ void StickerSetBox::updateButtons() {
|
||||
_inner->setReorderState(true);
|
||||
updateButtons();
|
||||
},
|
||||
&st::menuIconManage);
|
||||
&st::menuIconReorder);
|
||||
});
|
||||
}();
|
||||
if (_inner->notInstalled()) {
|
||||
|
||||
@@ -11,16 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "api/api_credits.h"
|
||||
#include "api/api_cloud_password.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h" // CreatePe...tionSubtitle.
|
||||
#include "boxes/peers/replace_boost_box.h"
|
||||
#include "boxes/gift_premium_box.h"
|
||||
#include "boxes/passcode_box.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "data/data_cloud_themes.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_star_gift.h"
|
||||
#include "data/data_thread.h"
|
||||
#include "data/data_user.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h" // CreatePe...tionSubtitle.
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/star_gift_box.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "payments/payments_checkout_process.h"
|
||||
@@ -38,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_boxes.h" // peerListSingleRow.
|
||||
#include "styles/style_credits.h" // starIconEmoji.
|
||||
#include "styles/style_dialogs.h" // recentPeersSpecialName.
|
||||
#include "styles/style_info.h" // defaultSubTabs.
|
||||
#include "styles/style_layers.h" // boxLabel.
|
||||
|
||||
namespace {
|
||||
@@ -140,6 +143,8 @@ void ExportOnBlockchain(
|
||||
) | rpl::take(
|
||||
1
|
||||
) | rpl::start_with_next([=](const Core::CloudPasswordState &pass) {
|
||||
state->lifetime.destroy();
|
||||
|
||||
auto fields = PasscodeBox::CloudFields::From(pass);
|
||||
fields.customTitle = tr::lng_gift_transfer_password_title();
|
||||
fields.customDescription
|
||||
@@ -578,17 +583,16 @@ void ShowTransferToBox(
|
||||
Fn<void()> closeParentBox) {
|
||||
const auto stars = gift->starsForTransfer;
|
||||
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->setTitle(tr::lng_gift_transfer_title(
|
||||
lt_name,
|
||||
rpl::single(UniqueGiftName(*gift))));
|
||||
|
||||
auto transfer = (stars > 0)
|
||||
? tr::lng_gift_transfer_button_for(
|
||||
lt_price,
|
||||
tr::lng_action_gift_for_stars(
|
||||
lt_count,
|
||||
rpl::single(stars * 1.)))
|
||||
: tr::lng_gift_transfer_button();
|
||||
rpl::single(Ui::Text::IconEmoji(
|
||||
&st::starIconEmoji
|
||||
).append(Lang::FormatCreditsAmountDecimal(
|
||||
CreditsAmount(stars)
|
||||
))),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_gift_transfer_button(Ui::Text::WithEntities);
|
||||
|
||||
struct State {
|
||||
bool sent = false;
|
||||
@@ -621,6 +625,10 @@ void ShowTransferToBox(
|
||||
TransferGift(controller, peer, gift, savedId, done);
|
||||
};
|
||||
|
||||
box->addRow(
|
||||
CreateGiftTransfer(box->verticalLayout(), gift, peer),
|
||||
QMargins(0, st::boxPadding.top(), 0, 0));
|
||||
|
||||
Ui::ConfirmBox(box, {
|
||||
.text = (stars > 0)
|
||||
? tr::lng_gift_transfer_sure_for(
|
||||
@@ -643,6 +651,9 @@ void ShowTransferToBox(
|
||||
.confirmed = std::move(callback),
|
||||
.confirmText = std::move(transfer),
|
||||
});
|
||||
|
||||
const auto show = controller->uiShow();
|
||||
AddTransferGiftTable(show, box->verticalLayout(), gift);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -781,7 +792,7 @@ void ShowBuyResaleGiftBox(
|
||||
std::shared_ptr<Data::UniqueGift> gift,
|
||||
bool forceTon,
|
||||
not_null<PeerData*> to,
|
||||
Fn<void()> closeParentBox) {
|
||||
Fn<void(bool ok)> closeParentBox) {
|
||||
show->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
struct State {
|
||||
rpl::variable<bool> ton;
|
||||
@@ -802,6 +813,7 @@ void ShowBuyResaleGiftBox(
|
||||
const auto tabs = box->addRow(
|
||||
object_ptr<Ui::SubTabs>(
|
||||
box,
|
||||
st::defaultSubTabs,
|
||||
Ui::SubTabsOptions{
|
||||
.selected = (state->ton.current()
|
||||
? u"ton"_q
|
||||
@@ -846,12 +858,12 @@ void ShowBuyResaleGiftBox(
|
||||
const auto weak = base::make_weak(box);
|
||||
const auto done = [=](Payments::CheckoutResult result) {
|
||||
if (result == Payments::CheckoutResult::Cancelled) {
|
||||
closeParentBox();
|
||||
closeParentBox(false);
|
||||
close();
|
||||
} else if (result != Payments::CheckoutResult::Paid) {
|
||||
state->sent = false;
|
||||
} else {
|
||||
closeParentBox();
|
||||
closeParentBox(true);
|
||||
close();
|
||||
}
|
||||
};
|
||||
@@ -937,3 +949,19 @@ bool ShowTransferGiftLater(
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShowActionLocked(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
const QString &slug) {
|
||||
const auto open = [=] {
|
||||
UrlClickHandler::Open(u"https://fragment.com/gift/"_q
|
||||
+ slug
|
||||
+ u"?collection=my"_q);
|
||||
};
|
||||
show->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_gift_transfer_locked_text(),
|
||||
.confirmed = [=](Fn<void()> close) { open(); close(); },
|
||||
.confirmText = tr::lng_gift_transfer_confirm_button(),
|
||||
.title = tr::lng_gift_transfer_locked_title(),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ void ShowBuyResaleGiftBox(
|
||||
std::shared_ptr<Data::UniqueGift> gift,
|
||||
bool forceTon,
|
||||
not_null<PeerData*> to,
|
||||
Fn<void()> closeParentBox);
|
||||
Fn<void(bool ok)> closeParentBox);
|
||||
|
||||
bool ShowResaleGiftLater(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
@@ -64,3 +64,7 @@ void SetPeerTheme(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &token,
|
||||
const std::shared_ptr<Ui::ChatTheme> &theme);
|
||||
|
||||
void ShowActionLocked(
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
const QString &slug);
|
||||
|
||||
@@ -260,8 +260,10 @@ callMuteButtonLabel: FlatLabel(defaultFlatLabel) {
|
||||
}
|
||||
}
|
||||
callMuteButtonActiveInner: IconButton {
|
||||
width: 112px;
|
||||
height: 138px;
|
||||
// width: 112px;
|
||||
// height: 138px;
|
||||
width: 68px;
|
||||
height: 79px;
|
||||
}
|
||||
callMuteButtonSmallActiveInner: IconButton {
|
||||
width: 68px;
|
||||
@@ -270,11 +272,15 @@ callMuteButtonSmallActiveInner: IconButton {
|
||||
callMuteButtonActive: CallButton {
|
||||
button: callMuteButtonActiveInner;
|
||||
bg: groupCallLive1;
|
||||
bgSize: 77px;
|
||||
bgPosition: point(18px, 18px);
|
||||
outerRadius: 18px;
|
||||
// bgSize: 77px;
|
||||
// bgPosition: point(18px, 18px);
|
||||
// outerRadius: 18px;
|
||||
bgSize: 42px;
|
||||
bgPosition: point(13px, 13px);
|
||||
outerRadius: 13px;
|
||||
outerBg: callAnswerBgOuter;
|
||||
label: callMuteButtonLabel;
|
||||
//label: callMuteButtonLabel;
|
||||
label: callButtonLabel;
|
||||
}
|
||||
callMuteButton: CallMuteButton {
|
||||
active: callMuteButtonActive;
|
||||
@@ -286,10 +292,14 @@ callMuteButton: CallMuteButton {
|
||||
sublabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: groupCallMemberNotJoinedStatus;
|
||||
}
|
||||
labelsSkip: 8px;
|
||||
sublabelSkip: 14px;
|
||||
lottieSize: size(54px, 54px);
|
||||
lottieTop: 31px;
|
||||
labelsSkip: 0px;
|
||||
sublabelSkip: 0px;
|
||||
// labelsSkip: 8px;
|
||||
// sublabelSkip: 14px;
|
||||
lottieSize: size(36px, 36px);
|
||||
lottieTop: 17px;
|
||||
// lottieSize: size(54px, 54px);
|
||||
// lottieTop: 31px;
|
||||
}
|
||||
callMuteButtonSmallActive: CallButton(callMuteButtonActive) {
|
||||
button: callMuteButtonSmallActiveInner;
|
||||
@@ -928,6 +938,10 @@ groupCallHangupInner: IconButton(callButton) {
|
||||
color: groupCallLeaveBgRipple;
|
||||
}
|
||||
}
|
||||
groupCallMessageInner: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/call_message", groupCallIconFg }};
|
||||
iconPosition: point(-1px, 16px);
|
||||
}
|
||||
groupCallSettings: CallButton(callMicrophoneMute) {
|
||||
button: groupCallSettingsInner;
|
||||
}
|
||||
@@ -943,6 +957,15 @@ groupCallVideoInnerActive: IconButton(groupCallVideoInner) {
|
||||
groupCallVideoActive: CallButton(groupCallVideo) {
|
||||
button: groupCallVideoInnerActive;
|
||||
}
|
||||
groupCallMessage: CallButton(groupCallSettings) {
|
||||
button: groupCallMessageInner;
|
||||
}
|
||||
groupCallMessageInnerActive: IconButton(groupCallMessageInner) {
|
||||
icon: icon {{ "calls/call_message", groupCallIconFg }};
|
||||
}
|
||||
groupCallMessageActive: CallButton(groupCallMessage) {
|
||||
button: groupCallMessageInnerActive;
|
||||
}
|
||||
groupCallHangup: CallButton(callHangup) {
|
||||
button: groupCallHangupInner;
|
||||
bg: groupCallLeaveBg;
|
||||
@@ -978,6 +1001,20 @@ groupCallVideoActiveSmall: CallButton(groupCallVideoSmall) {
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallMessageSmall: CallButton(groupCallSettingsSmall) {
|
||||
button: IconButton(groupCallMessageInner) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallMessageActiveSmall: CallButton(groupCallMessageSmall) {
|
||||
button: IconButton(groupCallMessageInnerActive) {
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
rippleAreaPosition: point(8px, 12px);
|
||||
}
|
||||
}
|
||||
groupCallScreenShareSmall: CallButton(groupCallSettingsSmall) {
|
||||
button: IconButton(groupCallSettingsInner) {
|
||||
icon: icon {{ "calls/calls_present", groupCallIconFg }};
|
||||
@@ -1658,6 +1695,19 @@ groupCallInviteLink: SettingsButton(createCallInviteLink) {
|
||||
}
|
||||
groupCallInviteLinkIcon: icon {{ "info/edit/group_manage_links", mediaviewTextLinkFg }};
|
||||
|
||||
groupCallMessagesScroll: ScrollArea(defaultScrollArea) {
|
||||
barHidden: true;
|
||||
}
|
||||
groupCallMessagePadding: margins(8px, 2px, 8px, 2px);
|
||||
groupCallMessageSkip: 8px;
|
||||
groupCallMessagePalette: TextPalette(defaultTextPalette) {
|
||||
linkFg: radialFg;
|
||||
monoFg: radialFg;
|
||||
spoilerFg: radialFg;
|
||||
}
|
||||
groupCallUserpic: 20px;
|
||||
groupCallUserpicPadding: margins(2px, 2px, 4px, 2px);
|
||||
|
||||
confcallLinkMenu: IconButton(boxTitleClose) {
|
||||
icon: icon {{ "title_menu_dots", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "title_menu_dots", boxTitleCloseFgOver }};
|
||||
|
||||
@@ -596,6 +596,10 @@ void Instance::handleUpdate(
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallChainBlocks &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallMessage &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
handleGroupCallUpdate(session, update);
|
||||
}, [](const auto &) {
|
||||
Unexpected("Update type in Calls::Instance::handleUpdate.");
|
||||
});
|
||||
@@ -711,11 +715,17 @@ void Instance::handleGroupCallUpdate(
|
||||
groupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [&](const MTPDupdateGroupCallConnection &data) {
|
||||
groupCall->handlePossibleCreateOrJoinResponse(data);
|
||||
}, [&](const MTPDupdateGroupCallMessage &data) {
|
||||
groupCall->handleIncomingMessage(data);
|
||||
}, [&](const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
groupCall->handleIncomingMessage(data);
|
||||
}, [](const auto &) {
|
||||
});
|
||||
}
|
||||
|
||||
if (update.type() == mtpc_updateGroupCallConnection) {
|
||||
if (update.type() == mtpc_updateGroupCallConnection
|
||||
|| update.type() == mtpc_updateGroupCallMessage
|
||||
|| update.type() == mtpc_updateGroupCallEncryptedMessage) {
|
||||
return;
|
||||
}
|
||||
const auto callId = update.match([](const MTPDupdateGroupCall &data) {
|
||||
|
||||
@@ -1144,6 +1144,9 @@ void Panel::updateControlsGeometry() {
|
||||
_bodySt->muteSize,
|
||||
_bodySt->muteStroke);
|
||||
|
||||
if (_name->naturalWidth() > innerWidth) {
|
||||
_name->resizeToWidth(innerWidth);
|
||||
}
|
||||
_name->moveToLeft(
|
||||
(widget()->width() - _name->width()) / 2,
|
||||
_bodyTop + _bodySt->nameTop);
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/group/calls_group_call.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_app_config.h"
|
||||
@@ -597,6 +598,7 @@ GroupCall::GroupCall(
|
||||
, _peer(join.peer)
|
||||
, _history(_peer->owner().history(_peer))
|
||||
, _api(&_peer->session().mtp())
|
||||
, _messages(std::make_unique<Group::Messages>(this, &_api))
|
||||
, _joinAs(join.joinAs)
|
||||
, _possibleJoinAs(std::move(join.possibleJoinAs))
|
||||
, _joinHash(join.joinHash)
|
||||
@@ -956,6 +958,10 @@ void GroupCall::setScheduledDate(TimeId date) {
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::setMessagesEnabled(bool enabled) {
|
||||
_messagesEnabled = enabled && !_rtmp;
|
||||
}
|
||||
|
||||
void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
||||
_listenersHidden = real->listenersHidden();
|
||||
|
||||
@@ -964,6 +970,11 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
||||
setScheduledDate(date);
|
||||
}, _lifetime);
|
||||
|
||||
real->messagesEnabledValue(
|
||||
) | rpl::start_with_next([=](bool enabled) {
|
||||
setMessagesEnabled(enabled);
|
||||
}, _lifetime);
|
||||
|
||||
// Postpone creating video tracks, so that we know if Panel
|
||||
// supports OpenGL and we don't need ARGB32 frames at all.
|
||||
Ui::PostponeCall(this, [=] {
|
||||
@@ -1710,6 +1721,8 @@ void GroupCall::startConference() {
|
||||
return;
|
||||
}
|
||||
applyInputCall(_conferenceCall->input());
|
||||
_realChanges.fire_copy(_conferenceCall.get());
|
||||
|
||||
initialJoinRequested();
|
||||
joinDone(
|
||||
TimestampInMsFromMsgId(response.outerMsgId),
|
||||
@@ -2301,6 +2314,9 @@ void GroupCall::handlePossibleCreateOrJoinResponse(
|
||||
const auto rtmp = data.is_rtmp_stream();
|
||||
_rtmp = rtmp;
|
||||
setScheduledDate(scheduleDate);
|
||||
if (!conference()) {
|
||||
setMessagesEnabled(data.is_messages_enabled());
|
||||
}
|
||||
if (const auto chat = _peer->asChat()) {
|
||||
chat->setGroupCall(input, scheduleDate, rtmp);
|
||||
} else if (const auto group = _peer->asChannel()) {
|
||||
@@ -2316,6 +2332,9 @@ void GroupCall::handlePossibleCreateOrJoinResponse(
|
||||
return;
|
||||
}
|
||||
setScheduledDate(data.vschedule_date().value_or_empty());
|
||||
if (!conference()) {
|
||||
setMessagesEnabled(data.is_messages_enabled());
|
||||
}
|
||||
if (const auto streamDcId = data.vstream_dc_id()) {
|
||||
_broadcastDcId = MTP::BareDcId(streamDcId->v);
|
||||
}
|
||||
@@ -2370,6 +2389,32 @@ void GroupCall::handlePossibleCreateOrJoinResponse(
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCall::handleIncomingMessage(
|
||||
const MTPDupdateGroupCallMessage &data) {
|
||||
const auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {
|
||||
return data.vid().v;
|
||||
}, [](const auto &) -> CallId {
|
||||
Unexpected("slug/msg in GroupCall::handleIncomingMessage");
|
||||
});
|
||||
if (id != _id || conference()) {
|
||||
return;
|
||||
}
|
||||
_messages->received(data);
|
||||
}
|
||||
|
||||
void GroupCall::handleIncomingMessage(
|
||||
const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
const auto id = data.vcall().match([&](const MTPDinputGroupCall &data) {
|
||||
return data.vid().v;
|
||||
}, [](const auto &) -> CallId {
|
||||
Unexpected("slug/msg in GroupCall::handleIncomingMessage");
|
||||
});
|
||||
if (id != _id || !conference()) {
|
||||
return;
|
||||
}
|
||||
_messages->received(data);
|
||||
}
|
||||
|
||||
void GroupCall::handlePossibleDiscarded(const MTPDgroupCallDiscarded &data) {
|
||||
if (data.vid().v == _id) {
|
||||
LOG(("Call Info: Hangup after groupCallDiscarded."));
|
||||
@@ -2995,9 +3040,7 @@ bool GroupCall::tryCreateController() {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
.e2eEncryptDecrypt = (_e2eEncryptDecrypt
|
||||
? _e2eEncryptDecrypt->callback()
|
||||
: nullptr),
|
||||
.e2eEncryptDecrypt = e2eEncryptDecrypt(),
|
||||
};
|
||||
if (Logs::DebugEnabled()) {
|
||||
auto callLogFolder = cWorkingDir() + u"DebugLogs"_q;
|
||||
@@ -3050,9 +3093,7 @@ bool GroupCall::tryCreateScreencast() {
|
||||
.videoCapture = _screenCapture,
|
||||
.videoContentType = tgcalls::VideoContentType::Screencast,
|
||||
.videoCodecPreferences = lookupVideoCodecPreferences(),
|
||||
.e2eEncryptDecrypt = (_e2eEncryptDecrypt
|
||||
? _e2eEncryptDecrypt->callback()
|
||||
: nullptr),
|
||||
.e2eEncryptDecrypt = e2eEncryptDecrypt(),
|
||||
};
|
||||
|
||||
LOG(("Call Info: Creating group screen instance"));
|
||||
@@ -4095,6 +4136,17 @@ void GroupCall::setNotRequireARGB32() {
|
||||
_requireARGB32 = false;
|
||||
}
|
||||
|
||||
std::function<std::vector<uint8_t>(
|
||||
std::vector<uint8_t> const &,
|
||||
int64_t, bool,
|
||||
int32_t)> GroupCall::e2eEncryptDecrypt() const {
|
||||
return _e2eEncryptDecrypt ? _e2eEncryptDecrypt->callback() : nullptr;
|
||||
}
|
||||
|
||||
void GroupCall::sendMessage(TextWithTags message) {
|
||||
_messages->send(std::move(message));
|
||||
}
|
||||
|
||||
auto GroupCall::otherParticipantStateValue() const
|
||||
-> rpl::producer<Group::ParticipantState> {
|
||||
return _otherParticipantStateValue.events();
|
||||
|
||||
@@ -60,6 +60,7 @@ struct RejoinEvent;
|
||||
struct RtmpInfo;
|
||||
enum class VideoQuality;
|
||||
enum class Error;
|
||||
class Messages;
|
||||
} // namespace Group
|
||||
|
||||
struct InviteRequest;
|
||||
@@ -244,6 +245,9 @@ public:
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> joinAsValue() const {
|
||||
return _joinAs.value();
|
||||
}
|
||||
[[nodiscard]] not_null<Group::Messages*> messages() const {
|
||||
return _messages.get();
|
||||
}
|
||||
[[nodiscard]] bool showChooseJoinAs() const;
|
||||
[[nodiscard]] TimeId scheduleDate() const {
|
||||
return _scheduleDate;
|
||||
@@ -278,6 +282,9 @@ public:
|
||||
void handlePossibleCreateOrJoinResponse(const MTPDupdateGroupCall &data);
|
||||
void handlePossibleCreateOrJoinResponse(
|
||||
const MTPDupdateGroupCallConnection &data);
|
||||
void handleIncomingMessage(const MTPDupdateGroupCallMessage &data);
|
||||
void handleIncomingMessage(
|
||||
const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
void changeTitle(const QString &title);
|
||||
void toggleRecording(
|
||||
bool enabled,
|
||||
@@ -415,6 +422,12 @@ public:
|
||||
[[nodiscard]] rpl::producer<bool> videoIsWorkingValue() const {
|
||||
return _videoIsWorking.value();
|
||||
}
|
||||
[[nodiscard]] bool messagesEnabled() const {
|
||||
return _messagesEnabled.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> messagesEnabledValue() const {
|
||||
return _messagesEnabled.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isSharingScreen() const;
|
||||
[[nodiscard]] rpl::producer<bool> isSharingScreenValue() const;
|
||||
@@ -446,6 +459,13 @@ public:
|
||||
void pushToTalk(bool pressed, crl::time delay);
|
||||
void setNotRequireARGB32();
|
||||
|
||||
[[nodiscard]] std::function<std::vector<uint8_t>(
|
||||
std::vector<uint8_t> const &,
|
||||
int64_t, bool,
|
||||
int32_t)> e2eEncryptDecrypt() const;
|
||||
void sendMessage(TextWithTags message);
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -577,6 +597,7 @@ private:
|
||||
void saveDefaultJoinAs(not_null<PeerData*> as);
|
||||
void subscribeToReal(not_null<Data::GroupCall*> real);
|
||||
void setScheduledDate(TimeId date);
|
||||
void setMessagesEnabled(bool enabled);
|
||||
void rejoinPresentation();
|
||||
void leavePresentation();
|
||||
void checkNextJoinAction();
|
||||
@@ -647,7 +668,6 @@ private:
|
||||
|
||||
[[nodiscard]] int activeVideoSendersCount() const;
|
||||
|
||||
[[nodiscard]] MTPInputGroupCall inputCall() const;
|
||||
[[nodiscard]] MTPInputGroupCall inputCallSafe() const;
|
||||
|
||||
const not_null<Delegate*> _delegate;
|
||||
@@ -667,6 +687,7 @@ private:
|
||||
base::flat_set<uint32> _unresolvedSsrcs;
|
||||
rpl::event_stream<Error> _errors;
|
||||
std::vector<Fn<void()>> _rejoinedCallbacks;
|
||||
std::unique_ptr<Group::Messages> _messages;
|
||||
bool _recordingStoppedByMe = false;
|
||||
bool _requestedVideoChannelsUpdateScheduled = false;
|
||||
|
||||
@@ -697,6 +718,7 @@ private:
|
||||
rpl::variable<bool> _canManage = false;
|
||||
rpl::variable<bool> _videoIsWorking = false;
|
||||
rpl::variable<bool> _emptyRtmp = false;
|
||||
rpl::variable<bool> _messagesEnabled = false;
|
||||
bool _initialMuteStateSent = false;
|
||||
bool _acceptFields = false;
|
||||
|
||||
|
||||
@@ -281,7 +281,8 @@ void ShowConferenceCallLinkBox(
|
||||
MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_reset_invite_hash),
|
||||
call->input(),
|
||||
MTPbool()) // join_muted
|
||||
MTPBool(), // join_muted
|
||||
MTPBool()) // messages_enabled
|
||||
).done([=](const MTPUpdates &result) {
|
||||
call->session().api().applyUpdates(result);
|
||||
ShowConferenceCallLinkBox(show, call, args);
|
||||
|
||||
@@ -513,8 +513,7 @@ void FillMenu(
|
||||
const auto addEditRecording = !conference
|
||||
&& call->canManage()
|
||||
&& !real->scheduleDate();
|
||||
const auto addScreenCast = !wide
|
||||
&& call->videoIsWorking()
|
||||
const auto addScreenCast = call->videoIsWorking()
|
||||
&& !real->scheduleDate();
|
||||
if (addEditJoinAs) {
|
||||
menu->addAction(MakeJoinAsAction(
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
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_message_encryption.h"
|
||||
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
//[[nodiscard]] MTPJSONValue String(const QByteArray &value) {
|
||||
// return MTP_jsonString(MTP_bytes(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Int(int value) {
|
||||
// return MTP_jsonNumber(MTP_double(value));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONObjectValue Value(
|
||||
// const QByteArray &name,
|
||||
// const MTPJSONValue &value) {
|
||||
// return MTP_jsonObjectValue(MTP_bytes(name), value);
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Object(
|
||||
// const QByteArray &cons,
|
||||
// QVector<MTPJSONObjectValue> &&values) {
|
||||
// values.insert(values.begin(), Value("_", String(cons)));
|
||||
// return MTP_jsonObject(MTP_vector<MTPJSONObjectValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Array(QVector<MTPJSONValue> &&values) {
|
||||
// return MTP_jsonArray(MTP_vector<MTPJSONValue>(std::move(values)));
|
||||
//}
|
||||
//
|
||||
//template <typename MTPD>
|
||||
//[[nodiscard]] MTPJSONValue SimpleEntity(
|
||||
// const QByteArray &name,
|
||||
// const MTPD &data) {
|
||||
// return Object(name, {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] MTPJSONValue Entity(const MTPMessageEntity &entity) {
|
||||
// return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
// return SimpleEntity("messageEntityBold", data);
|
||||
// }, [](const MTPDmessageEntityItalic &data) {
|
||||
// return SimpleEntity("messageEntityItalic", data);
|
||||
// }, [](const MTPDmessageEntityUnderline &data) {
|
||||
// return SimpleEntity("messageEntityUnderline", data);
|
||||
// }, [](const MTPDmessageEntityStrike &data) {
|
||||
// return SimpleEntity("messageEntityStrike", data);
|
||||
// }, [](const MTPDmessageEntitySpoiler &data) {
|
||||
// return SimpleEntity("messageEntitySpoiler", data);
|
||||
// }, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
// return Object("messageEntityCustomEmoji", {
|
||||
// Value("offset", Int(data.voffset().v)),
|
||||
// Value("length", Int(data.vlength().v)),
|
||||
// Value(
|
||||
// "document_id",
|
||||
// String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
// });
|
||||
// }, [](const auto &data) {
|
||||
// return MTP_jsonNull();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QVector<MTPJSONValue> Entities(
|
||||
// const QVector<MTPMessageEntity> &list) {
|
||||
// auto result = QVector<MTPJSONValue>();
|
||||
// result.reserve(list.size());
|
||||
// for (const auto &entity : list) {
|
||||
// if (const auto e = Entity(entity); e.type() != mtpc_jsonNull) {
|
||||
// result.push_back(e);
|
||||
// }
|
||||
// }
|
||||
// return result;
|
||||
//}
|
||||
//
|
||||
//[[nodiscard]] QByteArray Serialize(const MTPJSONValue &value) {
|
||||
// auto counter = ::tl::details::LengthCounter();
|
||||
// value.write(counter);
|
||||
// auto buffer = mtpBuffer();
|
||||
// buffer.reserve(counter.length);
|
||||
// value.write(buffer);
|
||||
// return QByteArray(
|
||||
// reinterpret_cast<const char*>(buffer.constData()),
|
||||
// buffer.size() * sizeof(buffer.front()));
|
||||
//}
|
||||
|
||||
[[nodiscard]] QJsonValue String(const QByteArray &value) {
|
||||
return QJsonValue(QString::fromUtf8(value));
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Int(int value) {
|
||||
return QJsonValue(double(value));
|
||||
}
|
||||
|
||||
struct JsonObjectValue {
|
||||
const char *name = nullptr;
|
||||
QJsonValue value;
|
||||
};
|
||||
|
||||
[[nodiscard]] JsonObjectValue Value(
|
||||
const char *name,
|
||||
const QJsonValue &value) {
|
||||
return JsonObjectValue{ name, value };
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Object(
|
||||
const char *cons,
|
||||
QVector<JsonObjectValue> &&values) {
|
||||
auto result = QJsonObject();
|
||||
result.insert("_", cons);
|
||||
for (const auto &value : values) {
|
||||
result.insert(value.name, value.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Array(QVector<QJsonValue> &&values) {
|
||||
auto result = QJsonArray();
|
||||
for (const auto &value : values) {
|
||||
result.push_back(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename MTPD>
|
||||
[[nodiscard]] QJsonValue SimpleEntity(
|
||||
const char *name,
|
||||
const MTPD &data) {
|
||||
return Object(name, {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonValue Entity(const MTPMessageEntity &entity) {
|
||||
return entity.match([](const MTPDmessageEntityBold &data) {
|
||||
return SimpleEntity("messageEntityBold", data);
|
||||
}, [](const MTPDmessageEntityItalic &data) {
|
||||
return SimpleEntity("messageEntityItalic", data);
|
||||
}, [](const MTPDmessageEntityUnderline &data) {
|
||||
return SimpleEntity("messageEntityUnderline", data);
|
||||
}, [](const MTPDmessageEntityStrike &data) {
|
||||
return SimpleEntity("messageEntityStrike", data);
|
||||
}, [](const MTPDmessageEntitySpoiler &data) {
|
||||
return SimpleEntity("messageEntitySpoiler", data);
|
||||
}, [](const MTPDmessageEntityCustomEmoji &data) {
|
||||
return Object("messageEntityCustomEmoji", {
|
||||
Value("offset", Int(data.voffset().v)),
|
||||
Value("length", Int(data.vlength().v)),
|
||||
Value(
|
||||
"document_id",
|
||||
String(QByteArray::number(int64(data.vdocument_id().v)))),
|
||||
});
|
||||
}, [](const auto &data) {
|
||||
return QJsonValue(QJsonValue::Null);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<QJsonValue> Entities(
|
||||
const QVector<MTPMessageEntity> &list) {
|
||||
auto result = QVector<QJsonValue>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entity : list) {
|
||||
if (const auto e = Entity(entity); !e.isNull()) {
|
||||
result.push_back(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray Serialize(const QJsonValue &value) {
|
||||
return QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<QJsonValue> GetValue(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto i = object.find(name);
|
||||
return (i != object.end()) ? *i : std::optional<QJsonValue>();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<int> GetInt(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
return int(base::SafeRound(maybeValue->toDouble()));
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toInt(&ok);
|
||||
return ok ? result : std::optional<int>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<uint64> GetLong(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
if (const auto maybeValue = GetValue(object, name)) {
|
||||
if (maybeValue->isDouble()) {
|
||||
const auto value = maybeValue->toDouble();
|
||||
return (value >= 0.)
|
||||
? uint64(base::SafeRound(value))
|
||||
: std::optional<uint64>();
|
||||
} else if (maybeValue->isString()) {
|
||||
auto ok = false;
|
||||
const auto result = maybeValue->toString().toLongLong(&ok);
|
||||
return ok ? uint64(result) : std::optional<uint64>();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetString(
|
||||
const QJsonObject &object,
|
||||
const char *name) {
|
||||
const auto maybeValue = GetValue(object, name);
|
||||
return (maybeValue && maybeValue->isString())
|
||||
? maybeValue->toString()
|
||||
: std::optional<QString>();
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::optional<QString> GetCons(const QJsonObject &object) {
|
||||
return GetString(object, "_");
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Unsupported(
|
||||
const QJsonObject &object,
|
||||
const QString &cons = QString()) {
|
||||
const auto maybeMinLayer = GetInt(object, "_min_layer");
|
||||
const auto layer = int(MTP::details::kCurrentLayer);
|
||||
if (maybeMinLayer.value_or(layer) > layer) {
|
||||
LOG(("E2E Error: _min_layer too large: %1 > %2").arg(*maybeMinLayer).arg(layer));
|
||||
return true;
|
||||
} else if (!cons.isEmpty() && GetCons(object) != cons) {
|
||||
LOG(("E2E Error: Expected %1 here.").arg(cons));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<MTPMessageEntity> GetEntity(
|
||||
const QString &text,
|
||||
const QJsonObject &object) {
|
||||
const auto cons = GetCons(object).value_or(QString());
|
||||
const auto offset = GetInt(object, "offset").value_or(-1);
|
||||
const auto length = GetInt(object, "length").value_or(0);
|
||||
if (Unsupported(object)
|
||||
|| (offset < 0)
|
||||
|| (length <= 0)
|
||||
|| (offset >= text.size())
|
||||
|| (length > text.size())
|
||||
|| (offset + length > text.size())) {
|
||||
return {};
|
||||
}
|
||||
const auto simple = [&](const auto &make) {
|
||||
return make(MTP_int(offset), MTP_int(length));
|
||||
};
|
||||
if (cons == "messageEntityBold") {
|
||||
return simple(MTP_messageEntityBold);
|
||||
} else if (cons == "messageEntityItalic") {
|
||||
return simple(MTP_messageEntityItalic);
|
||||
} else if (cons == "messageEntityUnderline") {
|
||||
return simple(MTP_messageEntityUnderline);
|
||||
} else if (cons == "messageEntityStrike") {
|
||||
return simple(MTP_messageEntityStrike);
|
||||
} else if (cons == "messageEntitySpoiler") {
|
||||
return simple(MTP_messageEntitySpoiler);
|
||||
} else if (cons == "messageEntityCustomEmoji") {
|
||||
const auto maybeDocumentId = GetLong(object, "document_id");
|
||||
if (const auto documentId = maybeDocumentId.value_or(0)) {
|
||||
return MTP_messageEntityCustomEmoji(
|
||||
MTP_int(offset),
|
||||
MTP_int(length),
|
||||
MTP_long(documentId));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
[[nodiscard]] QVector<MTPMessageEntity> GetEntities(
|
||||
const QString &text,
|
||||
const QJsonArray &list) {
|
||||
auto result = QVector<MTPMessageEntity>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &entry : list) {
|
||||
if (const auto entity = GetEntity(text, entry.toObject())) {
|
||||
result.push_back(*entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QByteArray SerializeMessage(const PreparedMessage &data) {
|
||||
return Serialize(Object("groupCallMessage", {
|
||||
Value(
|
||||
"random_id",
|
||||
String(QByteArray::number(int64(data.randomId)))),
|
||||
Value(
|
||||
"message",
|
||||
Object("textWithEntities", {
|
||||
Value("text", String(data.message.data().vtext().v)),
|
||||
Value(
|
||||
"entities",
|
||||
Array(Entities(data.message.data().ventities().v))),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data) {
|
||||
auto error = QJsonParseError();
|
||||
auto document = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError
|
||||
|| !document.isObject()) {
|
||||
LOG(("E2E Error: Bad json in Calls::Group::DeserializeMessage."));
|
||||
return {};
|
||||
}
|
||||
const auto groupCallMessage = document.object();
|
||||
if (Unsupported(groupCallMessage, "groupCallMessage")) {
|
||||
return {};
|
||||
}
|
||||
const auto randomId = GetLong(groupCallMessage, "random_id").value_or(0);
|
||||
if (!randomId) {
|
||||
return {};
|
||||
}
|
||||
const auto message = groupCallMessage["message"].toObject();
|
||||
if (Unsupported(message, "textWithEntities")) {
|
||||
return {};
|
||||
}
|
||||
const auto maybeText = GetString(message, "text");
|
||||
if (!maybeText) {
|
||||
return {};
|
||||
}
|
||||
const auto &text = *maybeText;
|
||||
const auto maybeEntities = GetValue(message, "entities");
|
||||
if (!maybeEntities || !maybeEntities->isArray()) {
|
||||
return {};
|
||||
}
|
||||
const auto entities = GetEntities(text, maybeEntities->toArray());
|
||||
return PreparedMessage{
|
||||
.randomId = randomId,
|
||||
.message = MTP_textWithEntities(
|
||||
MTP_string(text),
|
||||
MTP_vector<MTPMessageEntity>(entities)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct PreparedMessage {
|
||||
uint64 randomId = 0;
|
||||
MTPTextWithEntities message;
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray SerializeMessage(const PreparedMessage &data);
|
||||
[[nodiscard]] std::optional<PreparedMessage> DeserializeMessage(
|
||||
const QByteArray &data);
|
||||
|
||||
} // namespace Calls::Group
|
||||
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
662
Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
Normal file
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
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_message_field.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "history/view/reactions/history_view_reactions_strip.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kErrorLimit = 99;
|
||||
|
||||
using Chosen = HistoryView::Reactions::ChosenReaction;
|
||||
|
||||
} // namespace
|
||||
|
||||
class ReactionPanel final {
|
||||
public:
|
||||
ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry);
|
||||
~ReactionPanel();
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> chosen() const;
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void raise();
|
||||
void hideIfCollapsed();
|
||||
void collapse();
|
||||
|
||||
private:
|
||||
struct Hiding;
|
||||
|
||||
void create();
|
||||
void updateShowState();
|
||||
void fadeOutSelector();
|
||||
void startAnimation();
|
||||
|
||||
const not_null<QWidget*> _outer;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
std::unique_ptr<Ui::RpWidget> _parent;
|
||||
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
|
||||
std::vector<std::unique_ptr<Hiding>> _hiding;
|
||||
rpl::event_stream<Chosen> _chosen;
|
||||
Ui::Animations::Simple _showing;
|
||||
rpl::variable<float64> _shownValue;
|
||||
rpl::variable<QRect> _fieldGeometry;
|
||||
rpl::variable<bool> _expanded;
|
||||
rpl::variable<bool> _shown = false;
|
||||
|
||||
};
|
||||
|
||||
struct ReactionPanel::Hiding {
|
||||
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
|
||||
}
|
||||
|
||||
Ui::RpWidget widget;
|
||||
Ui::Animations::Simple animation;
|
||||
QImage frame;
|
||||
};
|
||||
|
||||
ReactionPanel::ReactionPanel(
|
||||
not_null<QWidget*> outer,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<QRect> fieldGeometry)
|
||||
: _outer(outer)
|
||||
, _show(std::move(show))
|
||||
, _fieldGeometry(std::move(fieldGeometry)) {
|
||||
}
|
||||
|
||||
ReactionPanel::~ReactionPanel() = default;
|
||||
|
||||
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
|
||||
return _chosen.events();
|
||||
}
|
||||
|
||||
void ReactionPanel::show() {
|
||||
if (_shown.current()) {
|
||||
return;
|
||||
}
|
||||
create();
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
const auto duration = st::defaultPanelAnimation.heightDuration
|
||||
* st::defaultPopupMenu.showDuration;
|
||||
_shown = true;
|
||||
_showing.start([=] { updateShowState(); }, 0., 1., duration);
|
||||
updateShowState();
|
||||
_parent->show();
|
||||
}
|
||||
|
||||
void ReactionPanel::hide() {
|
||||
if (!_selector) {
|
||||
return;
|
||||
}
|
||||
_selector->beforeDestroy();
|
||||
if (!anim::Disabled()) {
|
||||
fadeOutSelector();
|
||||
}
|
||||
_shown = false;
|
||||
_expanded = false;
|
||||
_showing.stop();
|
||||
_selector = nullptr;
|
||||
_parent = nullptr;
|
||||
}
|
||||
|
||||
void ReactionPanel::raise() {
|
||||
if (_parent) {
|
||||
_parent->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::hideIfCollapsed() {
|
||||
if (!_expanded.current()) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::collapse() {
|
||||
if (_expanded.current()) {
|
||||
hide();
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void ReactionPanel::create() {
|
||||
auto reactions = Data::LookupPossibleReactions(&_show->session());
|
||||
if (reactions.recent.empty()) {
|
||||
return;
|
||||
}
|
||||
_parent = std::make_unique<Ui::RpWidget>(_outer);
|
||||
_parent->show();
|
||||
|
||||
_parent->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(event->pos())) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _parent->lifetime());
|
||||
|
||||
_selector = std::make_unique<HistoryView::Reactions::Selector>(
|
||||
_parent.get(),
|
||||
st::storiesReactionsPan,
|
||||
_show,
|
||||
std::move(reactions),
|
||||
TextWithEntities(),
|
||||
[=](bool fast) { hide(); },
|
||||
nullptr, // iconFactory
|
||||
nullptr, // paused
|
||||
true);
|
||||
|
||||
_selector->chosen(
|
||||
) | rpl::start_with_next([=](Chosen reaction) {
|
||||
if (reaction.id.custom() && !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
_chosen.fire(std::move(reaction));
|
||||
hide();
|
||||
}
|
||||
}, _selector->lifetime());
|
||||
|
||||
const auto desiredWidth = st::storiesReactionsWidth;
|
||||
const auto maxWidth = desiredWidth * 2;
|
||||
const auto width = _selector->countWidth(desiredWidth, maxWidth);
|
||||
const auto margins = _selector->marginsForShadow();
|
||||
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
|
||||
width);
|
||||
const auto full = margins.left() + width + margins.right();
|
||||
|
||||
_shownValue = 0.;
|
||||
rpl::combine(
|
||||
_fieldGeometry.value(),
|
||||
_shownValue.value(),
|
||||
_expanded.value()
|
||||
) | rpl::start_with_next([=](QRect field, float64 shown, bool expanded) {
|
||||
const auto width = margins.left()
|
||||
+ _selector->countAppearedWidth(shown)
|
||||
+ margins.right();
|
||||
const auto available = field.y();
|
||||
const auto min = st::storiesReactionsBottomSkip
|
||||
+ st::reactStripHeight;
|
||||
const auto max = min
|
||||
+ margins.top()
|
||||
+ categoriesTop
|
||||
+ st::storiesReactionsAddedTop;
|
||||
const auto height = expanded ? std::min(available, max) : min;
|
||||
const auto top = field.y() - height;
|
||||
const auto shift = (width / 2);
|
||||
const auto right = (field.x() + field.width() / 2 + shift);
|
||||
_parent->setGeometry(QRect((right - width), top, full, height));
|
||||
const auto innerTop = height
|
||||
- st::storiesReactionsBottomSkip
|
||||
- st::reactStripHeight;
|
||||
const auto maxAdded = innerTop - margins.top() - categoriesTop;
|
||||
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
|
||||
_selector->setSpecialExpandTopSkip(added);
|
||||
_selector->initGeometry(innerTop);
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->willExpand(
|
||||
) | rpl::start_with_next([=] {
|
||||
_expanded = true;
|
||||
|
||||
const auto raw = _parent.get();
|
||||
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
const auto event = static_cast<QMouseEvent*>(e.get());
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
if (!_selector
|
||||
|| !_selector->geometry().contains(
|
||||
_parent->mapFromGlobal(event->globalPos()))) {
|
||||
collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
}, _selector->lifetime());
|
||||
|
||||
_selector->escapes() | rpl::start_with_next([=] {
|
||||
collapse();
|
||||
}, _selector->lifetime());
|
||||
}
|
||||
|
||||
void ReactionPanel::fadeOutSelector() {
|
||||
const auto geometry = Ui::MapFrom(
|
||||
_outer,
|
||||
_parent.get(),
|
||||
_selector->geometry());
|
||||
_hiding.push_back(std::make_unique<Hiding>(_outer));
|
||||
const auto raw = _hiding.back().get();
|
||||
raw->frame = Ui::GrabWidgetToImage(_selector.get());
|
||||
raw->widget.setGeometry(geometry);
|
||||
raw->widget.show();
|
||||
raw->widget.paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (const auto opacity = raw->animation.value(0.)) {
|
||||
auto p = QPainter(&raw->widget);
|
||||
p.setOpacity(opacity);
|
||||
p.drawImage(0, 0, raw->frame);
|
||||
}
|
||||
}, raw->widget.lifetime());
|
||||
Ui::PostponeCall(&raw->widget, [=] {
|
||||
raw->animation.start([=] {
|
||||
if (raw->animation.animating()) {
|
||||
raw->widget.update();
|
||||
} else {
|
||||
const auto i = ranges::find(
|
||||
_hiding,
|
||||
raw,
|
||||
&std::unique_ptr<Hiding>::get);
|
||||
if (i != end(_hiding)) {
|
||||
_hiding.erase(i);
|
||||
}
|
||||
}
|
||||
}, 1., 0., st::slideWrapDuration);
|
||||
});
|
||||
}
|
||||
|
||||
void ReactionPanel::updateShowState() {
|
||||
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
|
||||
const auto opacity = 1.;
|
||||
const auto appearing = _showing.animating();
|
||||
const auto toggling = false;
|
||||
_shownValue = progress;
|
||||
_selector->updateShowState(progress, opacity, appearing, toggling);
|
||||
}
|
||||
|
||||
MessageField::MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer)
|
||||
: _parent(parent)
|
||||
, _show(std::move(show))
|
||||
, _wrap(std::make_unique<Ui::RpWidget>(_parent))
|
||||
, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {
|
||||
createControls(peer);
|
||||
}
|
||||
|
||||
MessageField::~MessageField() = default;
|
||||
|
||||
void MessageField::createControls(PeerData *peer) {
|
||||
setupBackground();
|
||||
|
||||
const auto &st = st::storiesComposeControls;
|
||||
_field = Ui::CreateChild<Ui::InputField>(
|
||||
_wrap.get(),
|
||||
st.field,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_message_ph());
|
||||
_field->setMaxLength(_limit + kErrorLimit);
|
||||
_field->setMinHeight(
|
||||
st::historySendSize.height() - 2 * st::historySendPadding);
|
||||
_field->setMaxHeight(st::historyComposeFieldMaxHeight);
|
||||
_field->setDocumentMargin(4.);
|
||||
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
|
||||
|
||||
_reactionPanel = std::make_unique<ReactionPanel>(
|
||||
_parent,
|
||||
_show,
|
||||
_wrap->geometryValue());
|
||||
_fieldFocused = _field->focusedChanges();
|
||||
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
|
||||
return field->getLastText().trimmed().isEmpty();
|
||||
});
|
||||
rpl::combine(
|
||||
_fieldFocused.value(),
|
||||
_fieldEmpty.value()
|
||||
) | rpl::start_with_next([=](bool focused, bool empty) {
|
||||
if (!focused) {
|
||||
_reactionPanel->hideIfCollapsed();
|
||||
} else if (empty) {
|
||||
_reactionPanel->show();
|
||||
} else {
|
||||
_reactionPanel->hide();
|
||||
}
|
||||
}, _field->lifetime());
|
||||
|
||||
_reactionPanel->chosen(
|
||||
) | rpl::start_with_next([=](Chosen reaction) {
|
||||
if (const auto customId = reaction.id.custom()) {
|
||||
const auto document = _show->session().data().document(customId);
|
||||
if (const auto sticker = document->sticker()) {
|
||||
if (const auto alt = sticker->alt; !alt.isEmpty()) {
|
||||
const auto length = int(alt.size());
|
||||
const auto data = Data::SerializeCustomEmojiId(customId);
|
||||
const auto tag = Ui::InputField::CustomEmojiLink(data);
|
||||
_submitted.fire({ alt, { { 0, length, tag } } });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_submitted.fire({ reaction.id.emoji() });
|
||||
}
|
||||
_reactionPanel->hide();
|
||||
}, _field->lifetime());
|
||||
|
||||
const auto show = _show;
|
||||
const auto allow = [=](not_null<DocumentData*> emoji) {
|
||||
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
InitMessageFieldHandlers({
|
||||
.session = &show->session(),
|
||||
.show = show,
|
||||
.field = _field,
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(ChatHelpers::PauseReason::Layer);
|
||||
},
|
||||
.allowPremiumEmoji = allow,
|
||||
.fieldStyle = &st.files.caption,
|
||||
.allowMarkdownTags = {
|
||||
Ui::InputField::kTagBold,
|
||||
Ui::InputField::kTagItalic,
|
||||
Ui::InputField::kTagUnderline,
|
||||
Ui::InputField::kTagStrikeOut,
|
||||
Ui::InputField::kTagSpoiler,
|
||||
},
|
||||
});
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
_parent,
|
||||
_field,
|
||||
&_show->session(),
|
||||
{
|
||||
.suggestCustomEmoji = true,
|
||||
.allowCustomWithoutPremium = allow,
|
||||
.st = &st.suggestions,
|
||||
});
|
||||
|
||||
_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);
|
||||
_send->show();
|
||||
|
||||
using Selector = ChatHelpers::TabbedSelector;
|
||||
_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(
|
||||
_parent,
|
||||
ChatHelpers::TabbedPanelDescriptor{
|
||||
.ownedSelector = object_ptr<Selector>(
|
||||
nullptr,
|
||||
ChatHelpers::TabbedSelectorDescriptor{
|
||||
.show = _show,
|
||||
.st = st.tabbed,
|
||||
.level = ChatHelpers::PauseReason::Layer,
|
||||
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
|
||||
.features = {
|
||||
.stickersSettings = false,
|
||||
.openStickerSets = false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const auto panel = _emojiPanel.get();
|
||||
panel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
panel->hide();
|
||||
panel->selector()->setCurrentPeer(peer);
|
||||
panel->selector()->emojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
|
||||
Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
|
||||
}, lifetime());
|
||||
panel->selector()->customEmojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
||||
const auto info = data.document->sticker();
|
||||
if (info
|
||||
&& info->setType == Data::StickersType::Emoji
|
||||
&& !_show->session().premium()) {
|
||||
ShowPremiumPreviewBox(
|
||||
_show,
|
||||
PremiumFeature::AnimatedEmoji);
|
||||
} else {
|
||||
Data::InsertCustomEmoji(_field, data.document);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);
|
||||
_emojiToggle->show();
|
||||
|
||||
_emojiToggle->installEventFilter(panel);
|
||||
_emojiToggle->addClickHandler([=] {
|
||||
panel->toggleAnimated();
|
||||
});
|
||||
|
||||
_width.value(
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_1 > 0
|
||||
) | rpl::start_with_next([=](int newWidth) {
|
||||
const auto fieldWidth = newWidth
|
||||
- st::historySendPadding
|
||||
- _emojiToggle->width()
|
||||
- _send->width();
|
||||
_field->resizeToWidth(fieldWidth);
|
||||
_field->moveToLeft(
|
||||
st::historySendPadding,
|
||||
st::historySendPadding,
|
||||
newWidth);
|
||||
updateWrapSize(newWidth);
|
||||
}, _lifetime);
|
||||
|
||||
rpl::combine(
|
||||
_width.value(),
|
||||
_field->heightValue()
|
||||
) | rpl::start_with_next([=](int width, int height) {
|
||||
if (width <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto minHeight = st::historySendSize.height()
|
||||
- 2 * st::historySendPadding;
|
||||
_send->moveToRight(0, height - minHeight, width);
|
||||
_emojiToggle->moveToRight(_send->width(), height - minHeight, width);
|
||||
updateWrapSize();
|
||||
}, _lifetime);
|
||||
|
||||
_field->cancelled() | rpl::start_with_next([=] {
|
||||
_closeRequests.fire({});
|
||||
}, _lifetime);
|
||||
|
||||
const auto updateLimitPosition = [=](QSize parent, QSize label) {
|
||||
const auto skip = st::historySendPadding;
|
||||
return QPoint(parent.width() - label.width() - skip, skip);
|
||||
};
|
||||
Ui::AddLengthLimitLabel(_field, _limit, {
|
||||
.customParent = _wrap.get(),
|
||||
.customUpdatePosition = updateLimitPosition,
|
||||
});
|
||||
|
||||
rpl::merge(
|
||||
_field->submits() | rpl::to_empty,
|
||||
_send->clicks() | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
auto text = _field->getTextWithTags();
|
||||
if (text.text.size() <= _limit) {
|
||||
_submitted.fire(std::move(text));
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::updateEmojiPanelGeometry() {
|
||||
const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
|
||||
const auto local = _parent->mapFromGlobal(global);
|
||||
_emojiPanel->moveBottomRight(
|
||||
local.y(),
|
||||
local.x() + _emojiToggle->width() * 3);
|
||||
}
|
||||
|
||||
void MessageField::setupBackground() {
|
||||
_wrap->paintRequest() | rpl::start_with_next([=] {
|
||||
const auto radius = st::historySendSize.height() / 2.;
|
||||
auto p = QPainter(_wrap.get());
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::storiesComposeBg);
|
||||
p.drawRoundedRect(_wrap->rect(), radius, radius);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessageField::resizeToWidth(int newWidth) {
|
||||
_width = newWidth;
|
||||
if (_wrap->isHidden()) {
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
}
|
||||
updateEmojiPanelGeometry();
|
||||
}
|
||||
|
||||
void MessageField::move(int x, int y) {
|
||||
_wrap->move(x, y);
|
||||
if (_cache) {
|
||||
_cache->move(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::toggle(bool shown) {
|
||||
if (_shown == shown) {
|
||||
return;
|
||||
} else if (shown) {
|
||||
Assert(_width.current() > 0);
|
||||
Ui::SendPendingMoveResizeEvents(_wrap.get());
|
||||
} else if (Ui::InFocusChain(_field)) {
|
||||
_parent->setFocus();
|
||||
}
|
||||
_shown = shown;
|
||||
if (!anim::Disabled()) {
|
||||
if (!_cache) {
|
||||
auto image = Ui::GrabWidgetToImage(_wrap.get());
|
||||
_cache = std::make_unique<Ui::RpWidget>(_parent);
|
||||
const auto raw = _cache.get();
|
||||
raw->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto scale = raw->height() / float64(_wrap->height());
|
||||
const auto target = _wrap->rect();
|
||||
const auto center = target.center();
|
||||
p.translate(center);
|
||||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
p.drawImage(target, image);
|
||||
}, raw->lifetime());
|
||||
raw->show();
|
||||
raw->move(_wrap->pos());
|
||||
raw->resize(_wrap->width(), 0);
|
||||
|
||||
_wrap->hide();
|
||||
}
|
||||
_shownAnimation.start(
|
||||
[=] { shownAnimationCallback(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration,
|
||||
anim::easeOutCirc);
|
||||
}
|
||||
shownAnimationCallback();
|
||||
}
|
||||
|
||||
void MessageField::raise() {
|
||||
_wrap->raise();
|
||||
if (_cache) {
|
||||
_cache->raise();
|
||||
}
|
||||
if (_reactionPanel) {
|
||||
_reactionPanel->raise();
|
||||
}
|
||||
if (_emojiPanel) {
|
||||
_emojiPanel->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageField::updateWrapSize(int widthOverride) {
|
||||
const auto width = widthOverride ? widthOverride : _wrap->width();
|
||||
const auto height = _field->height() + 2 * st::historySendPadding;
|
||||
_wrap->resize(width, height);
|
||||
updateHeight();
|
||||
}
|
||||
|
||||
void MessageField::updateHeight() {
|
||||
_height = int(base::SafeRound(
|
||||
_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));
|
||||
}
|
||||
|
||||
void MessageField::shownAnimationCallback() {
|
||||
updateHeight();
|
||||
if (_shownAnimation.animating()) {
|
||||
Assert(_cache != nullptr);
|
||||
_cache->resize(_cache->width(), _height.current());
|
||||
_cache->update();
|
||||
} else if (_shown) {
|
||||
_cache = nullptr;
|
||||
_wrap->show();
|
||||
_field->setFocusFast();
|
||||
} else {
|
||||
_closed.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
int MessageField::height() const {
|
||||
return _height.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> MessageField::heightValue() const {
|
||||
return _height.value();
|
||||
}
|
||||
|
||||
rpl::producer<TextWithTags> MessageField::submitted() const {
|
||||
return _submitted.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closeRequests() const {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MessageField::closed() const {
|
||||
return _closed.events();
|
||||
}
|
||||
|
||||
rpl::lifetime &MessageField::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
88
Telegram/SourceFiles/calls/group/calls_group_message_field.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Ui {
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
class ReactionPanel;
|
||||
|
||||
class MessageField final {
|
||||
public:
|
||||
MessageField(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
PeerData *peer);
|
||||
~MessageField();
|
||||
|
||||
void resizeToWidth(int newWidth);
|
||||
void move(int x, int y);
|
||||
void toggle(bool shown);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<TextWithTags> submitted() const;
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> closed() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void createControls(PeerData *peer);
|
||||
void setupBackground();
|
||||
void shownAnimationCallback();
|
||||
void updateEmojiPanelGeometry();
|
||||
void updateWrapSize(int widthOverride = 0);
|
||||
void updateHeight();
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
const std::unique_ptr<Ui::RpWidget> _wrap;
|
||||
|
||||
int _limit = 0;
|
||||
Ui::InputField *_field = nullptr;
|
||||
Ui::SendButton *_send = nullptr;
|
||||
Ui::EmojiButton *_emojiToggle = nullptr;
|
||||
std::unique_ptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||
std::unique_ptr<ReactionPanel> _reactionPanel;
|
||||
rpl::variable<bool> _fieldFocused;
|
||||
rpl::variable<bool> _fieldEmpty = true;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
rpl::variable<int> _height;
|
||||
|
||||
bool _shown = false;
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
std::unique_ptr<Ui::RpWidget> _cache;
|
||||
|
||||
rpl::event_stream<TextWithTags> _submitted;
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::event_stream<> _closed;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
241
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
241
Telegram/SourceFiles/calls/group/calls_group_messages.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
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_messages.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_message_encryption.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
|
||||
: _call(call)
|
||||
, _api(api)
|
||||
, _destroyTimer([=] { checkDestroying(); })
|
||||
, _ttl(_call->peer()->session().appConfig().groupCallMessageTTL()) {
|
||||
Ui::PostponeCall(_call, [=] {
|
||||
_call->real(
|
||||
) | rpl::start_with_next([=](not_null<Data::GroupCall*> call) {
|
||||
_real = call;
|
||||
if (ready()) {
|
||||
sendPending();
|
||||
} else {
|
||||
Unexpected("Not ready call.");
|
||||
}
|
||||
}, _lifetime);
|
||||
});
|
||||
}
|
||||
|
||||
bool Messages::ready() const {
|
||||
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
|
||||
}
|
||||
|
||||
void Messages::send(TextWithTags text) {
|
||||
if (!ready()) {
|
||||
_pending.push_back(std::move(text));
|
||||
return;
|
||||
}
|
||||
|
||||
auto prepared = TextWithEntities{
|
||||
text.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(text.tags)
|
||||
};
|
||||
auto serialized = MTPTextWithEntities(MTP_textWithEntities(
|
||||
MTP_string(prepared.text),
|
||||
Api::EntitiesToMTP(
|
||||
&_real->session(),
|
||||
prepared.entities,
|
||||
Api::ConvertOption::SkipLocal)));
|
||||
|
||||
const auto randomId = base::RandomValue<uint64>();
|
||||
const auto from = _call->joinAs();
|
||||
_messages.push_back({
|
||||
.randomId = randomId,
|
||||
.peer = from,
|
||||
.text = std::move(prepared),
|
||||
});
|
||||
|
||||
if (!_call->conference()) {
|
||||
_api->request(MTPphone_SendGroupCallMessage(
|
||||
_call->inputCall(),
|
||||
MTP_long(randomId),
|
||||
serialized
|
||||
)).done([=](const MTPBool &, const MTP::Response &response) {
|
||||
sent(randomId, response);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
} else {
|
||||
const auto bytes = SerializeMessage({ randomId, serialized });
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(from->id).bare;
|
||||
const auto encrypt = _call->e2eEncryptDecrypt();
|
||||
const auto encrypted = encrypt(v, int64_t(userId), true, 0);
|
||||
|
||||
_api->request(MTPphone_SendGroupCallEncryptedMessage(
|
||||
_call->inputCall(),
|
||||
MTP_bytes(bytes::make_span(encrypted))
|
||||
)).done([=](const MTPBool &, const MTP::Response &response) {
|
||||
sent(randomId, response);
|
||||
}).fail([=](const MTP::Error &, const MTP::Response &response) {
|
||||
failed(randomId, response);
|
||||
}).send();
|
||||
}
|
||||
checkDestroying(true);
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
received(data.vrandom_id().v, data.vfrom_id(), data.vmessage());
|
||||
pushChanges();
|
||||
}
|
||||
|
||||
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
|
||||
if (!ready()) {
|
||||
return;
|
||||
}
|
||||
const auto fromId = data.vfrom_id();
|
||||
const auto &bytes = data.vencrypted_message().v;
|
||||
auto v = std::vector<std::uint8_t>(bytes.size());
|
||||
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
|
||||
|
||||
const auto userId = peerToUser(peerFromMTP(fromId)).bare;
|
||||
const auto decrypt = _call->e2eEncryptDecrypt();
|
||||
const auto decrypted = decrypt(v, int64_t(userId), false, 0);
|
||||
|
||||
const auto deserialized = DeserializeMessage(QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(decrypted.data()),
|
||||
decrypted.size()));
|
||||
if (!deserialized) {
|
||||
LOG(("API Error: Can't parse decrypted message"));
|
||||
return;
|
||||
}
|
||||
received(deserialized->randomId, fromId, deserialized->message, true);
|
||||
pushChanges();
|
||||
}
|
||||
|
||||
void Messages::received(
|
||||
uint64 randomId,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
bool checkCustomEmoji) {
|
||||
const auto peer = _call->peer();
|
||||
const auto i = ranges::find(_messages, randomId, &Message::randomId);
|
||||
if (i != end(_messages)) {
|
||||
if (peerFromMTP(from) == peer->session().userPeerId() && !i->date) {
|
||||
i->date = base::unixtime::now();
|
||||
checkDestroying(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto allowedEntityTypes = std::vector<EntityType>{
|
||||
EntityType::Code,
|
||||
EntityType::Bold,
|
||||
EntityType::Semibold,
|
||||
EntityType::Spoiler,
|
||||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::CustomEmoji,
|
||||
};
|
||||
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
|
||||
allowedEntityTypes.pop_back();
|
||||
}
|
||||
_messages.push_back({
|
||||
.randomId = randomId,
|
||||
.date = base::unixtime::now(),
|
||||
.peer = peer->owner().peer(peerFromMTP(from)),
|
||||
.text = Ui::Text::Filtered(
|
||||
Api::ParseTextWithEntities(&peer->session(), message),
|
||||
allowedEntityTypes),
|
||||
});
|
||||
checkDestroying(true);
|
||||
}
|
||||
|
||||
void Messages::checkDestroying(bool afterChanges) {
|
||||
auto next = TimeId();
|
||||
const auto now = base::unixtime::now();
|
||||
const auto destroyTime = now - _ttl;
|
||||
const auto initial = _messages.size();
|
||||
for (auto i = begin(_messages); i != end(_messages);) {
|
||||
const auto date = i->date;
|
||||
if (!date) {
|
||||
++i;
|
||||
} else if (date <= destroyTime) {
|
||||
i = _messages.erase(i);
|
||||
} else if (!next) {
|
||||
next = date + _ttl - now;
|
||||
++i;
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
_destroyTimer.cancel();
|
||||
} else {
|
||||
const auto delay = next * crl::time(1000);
|
||||
if (!_destroyTimer.isActive()
|
||||
|| (_destroyTimer.remainingTime() > delay)) {
|
||||
_destroyTimer.callOnce(delay);
|
||||
}
|
||||
}
|
||||
if (afterChanges || (_messages.size() < initial)) {
|
||||
pushChanges();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<Message>> Messages::listValue() const {
|
||||
return _changes.events_starting_with_copy(_messages);
|
||||
}
|
||||
|
||||
void Messages::sendPending() {
|
||||
Expects(_real != nullptr);
|
||||
|
||||
for (auto &pending : base::take(_pending)) {
|
||||
send(std::move(pending));
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::pushChanges() {
|
||||
_changes.fire_copy(_messages);
|
||||
}
|
||||
|
||||
void Messages::sent(uint64 randomId, const MTP::Response &response) {
|
||||
const auto i = ranges::find(_messages, randomId, &Message::randomId);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Messages::failed(uint64 randomId, const MTP::Response &response) {
|
||||
const auto i = ranges::find(_messages, randomId, &Message::randomId);
|
||||
if (i != end(_messages) && !i->date) {
|
||||
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
|
||||
i->failed = true;
|
||||
checkDestroying(true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
77
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
77
Telegram/SourceFiles/calls/group/calls_group_messages.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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/timer.h"
|
||||
|
||||
namespace Calls {
|
||||
class GroupCall;
|
||||
} // namespace Calls
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace MTP {
|
||||
class Sender;
|
||||
struct Response;
|
||||
} // namespace MTP
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message {
|
||||
uint64 randomId = 0;
|
||||
TimeId date = 0;
|
||||
not_null<PeerData*> peer;
|
||||
TextWithEntities text;
|
||||
bool failed = false;
|
||||
};
|
||||
|
||||
class Messages final {
|
||||
public:
|
||||
Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api);
|
||||
|
||||
void send(TextWithTags text);
|
||||
|
||||
void received(const MTPDupdateGroupCallMessage &data);
|
||||
void received(const MTPDupdateGroupCallEncryptedMessage &data);
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<Message>> listValue() const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool ready() const;
|
||||
void sendPending();
|
||||
void pushChanges();
|
||||
void checkDestroying(bool afterChanges = false);
|
||||
|
||||
void received(
|
||||
uint64 randomId,
|
||||
const MTPPeer &from,
|
||||
const MTPTextWithEntities &message,
|
||||
bool checkCustomEmoji = false);
|
||||
void sent(uint64 randomId, const MTP::Response &response);
|
||||
void failed(uint64 randomId, const MTP::Response &response);
|
||||
|
||||
const not_null<GroupCall*> _call;
|
||||
const not_null<MTP::Sender*> _api;
|
||||
|
||||
Data::GroupCall *_real = nullptr;
|
||||
|
||||
std::vector<TextWithTags> _pending;
|
||||
|
||||
base::Timer _destroyTimer;
|
||||
std::vector<Message> _messages;
|
||||
rpl::event_stream<std::vector<Message>> _changes;
|
||||
|
||||
TimeId _ttl = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
842
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
842
Telegram/SourceFiles/calls/group/calls_group_messages_ui.cpp
Normal file
@@ -0,0 +1,842 @@
|
||||
/*
|
||||
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_messages_ui.h"
|
||||
|
||||
#include "boxes/peers/prepare_short_info_box.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_message_reaction_id.h"
|
||||
#include "data/data_session.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/reaction_fly_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/elastic_scroll.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
namespace Calls::Group {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMessageBgOpacity = 0.8;
|
||||
|
||||
[[nodiscard]] int CountMessageRadius() {
|
||||
const auto minHeight = st::groupCallMessagePadding.top()
|
||||
+ st::messageTextStyle.font->height
|
||||
+ st::groupCallMessagePadding.bottom();
|
||||
return minHeight / 2;
|
||||
}
|
||||
|
||||
void ReceiveSomeMouseEvents(
|
||||
not_null<Ui::ElasticScroll*> scroll,
|
||||
Fn<bool(QPoint)> handleClick) {
|
||||
class EventFilter final : public QObject {
|
||||
public:
|
||||
explicit EventFilter(
|
||||
not_null<Ui::ElasticScroll*> scroll,
|
||||
Fn<bool(QPoint)> handleClick)
|
||||
: QObject(scroll)
|
||||
, _handleClick(std::move(handleClick)) {
|
||||
}
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) {
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
return mousePressFilter(
|
||||
watched,
|
||||
static_cast<QMouseEvent*>(event));
|
||||
} else if (event->type() == QEvent::Wheel) {
|
||||
return wheelFilter(
|
||||
watched,
|
||||
static_cast<QWheelEvent*>(event));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mousePressFilter(
|
||||
QObject *watched,
|
||||
not_null<QMouseEvent*> event) {
|
||||
Expects(parent()->isWidgetType());
|
||||
|
||||
const auto scroll = static_cast<Ui::ElasticScroll*>(parent());
|
||||
if (watched != scroll->window()->windowHandle()) {
|
||||
return false;
|
||||
}
|
||||
const auto global = event->globalPos();
|
||||
const auto local = scroll->mapFromGlobal(global);
|
||||
if (!scroll->rect().contains(local)) {
|
||||
return false;
|
||||
}
|
||||
return _handleClick(local + QPoint(0, scroll->scrollTop()));
|
||||
}
|
||||
|
||||
bool wheelFilter(QObject *watched, not_null<QWheelEvent*> event) {
|
||||
Expects(parent()->isWidgetType());
|
||||
|
||||
const auto scroll = static_cast<Ui::ElasticScroll*>(parent());
|
||||
if (watched != scroll->window()->windowHandle()
|
||||
|| !scroll->scrollTopMax()) {
|
||||
return false;
|
||||
}
|
||||
const auto global = event->globalPosition().toPoint();
|
||||
const auto local = scroll->mapFromGlobal(global);
|
||||
if (!scroll->rect().contains(local)) {
|
||||
return false;
|
||||
}
|
||||
auto e = QWheelEvent(
|
||||
event->position(),
|
||||
event->globalPosition(),
|
||||
event->pixelDelta(),
|
||||
event->angleDelta(),
|
||||
event->buttons(),
|
||||
event->modifiers(),
|
||||
event->phase(),
|
||||
event->inverted(),
|
||||
event->source());
|
||||
e.setTimestamp(crl::now());
|
||||
QGuiApplication::sendEvent(scroll, &e);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
Fn<bool(QPoint)> _handleClick;
|
||||
|
||||
};
|
||||
|
||||
scroll->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
qApp->installEventFilter(
|
||||
new EventFilter(scroll, std::move(handleClick)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct MessagesUi::MessageView {
|
||||
uint64 id = 0;
|
||||
PeerData *from = nullptr;
|
||||
ClickHandlerPtr fromLink;
|
||||
Ui::Animations::Simple toggleAnimation;
|
||||
Ui::Animations::Simple sentAnimation;
|
||||
Data::ReactionId reactionId;
|
||||
std::unique_ptr<Ui::InfiniteRadialAnimation> sendingAnimation;
|
||||
std::unique_ptr<Ui::ReactionFlyAnimation> reactionAnimation;
|
||||
std::unique_ptr<Ui::RpWidget> reactionWidget;
|
||||
QPoint reactionShift;
|
||||
Ui::PeerUserpicView view;
|
||||
Ui::Text::String text;
|
||||
int top = 0;
|
||||
int width = 0;
|
||||
int left = 0;
|
||||
int height = 0;
|
||||
int realHeight = 0;
|
||||
bool removed = false;
|
||||
bool sending = false;
|
||||
bool failed = false;
|
||||
};
|
||||
|
||||
MessagesUi::MessagesUi(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown)
|
||||
: _parent(parent)
|
||||
, _show(std::move(show))
|
||||
, _messageBg([] {
|
||||
auto result = st::groupCallBg->c;
|
||||
result.setAlphaF(kMessageBgOpacity);
|
||||
return result;
|
||||
})
|
||||
, _messageBgRect(CountMessageRadius(), _messageBg.color())
|
||||
, _fadeHeight(st::normalFont->height) {
|
||||
setupList(std::move(messages), std::move(shown));
|
||||
}
|
||||
|
||||
MessagesUi::~MessagesUi() = default;
|
||||
|
||||
void MessagesUi::setupList(
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown) {
|
||||
rpl::combine(
|
||||
std::move(messages),
|
||||
std::move(shown)
|
||||
) | rpl::start_with_next([=](std::vector<Message> &&list, bool shown) {
|
||||
if (!shown) {
|
||||
list.clear();
|
||||
}
|
||||
auto from = begin(list);
|
||||
auto till = end(list);
|
||||
for (auto &entry : _views) {
|
||||
if (!entry.removed) {
|
||||
const auto id = entry.id;
|
||||
const auto i = ranges::find(
|
||||
from,
|
||||
till,
|
||||
id,
|
||||
&Message::randomId);
|
||||
if (i == till) {
|
||||
toggleMessage(entry, false);
|
||||
continue;
|
||||
} else if (entry.failed != i->failed) {
|
||||
setContentFailed(entry);
|
||||
updateMessageSize(entry);
|
||||
repaintMessage(entry.id);
|
||||
} else if (entry.sending != (i->date == 0)) {
|
||||
animateMessageSent(entry);
|
||||
}
|
||||
if (i == from) {
|
||||
++from;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto addedSendingToBottom = false;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
if (!ranges::contains(_views, i->randomId, &MessageView::id)) {
|
||||
if (i + 1 == till && !i->date) {
|
||||
addedSendingToBottom = true;
|
||||
}
|
||||
appendMessage(*i);
|
||||
}
|
||||
}
|
||||
if (addedSendingToBottom) {
|
||||
const auto from = _scroll->scrollTop();
|
||||
const auto till = _scroll->scrollTopMax();
|
||||
_scrollToBottomAnimation.start([=] {
|
||||
_scroll->scrollToY(_scrollToBottomAnimation.value(till));
|
||||
}, from, till, st::slideDuration, anim::easeOutCirc);
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void MessagesUi::animateMessageSent(MessageView &entry) {
|
||||
const auto id = entry.id;
|
||||
entry.sending = false;
|
||||
entry.sentAnimation.start([=] {
|
||||
repaintMessage(id);
|
||||
}, 0., 1, st::slideDuration, anim::easeOutCirc);
|
||||
repaintMessage(id);
|
||||
}
|
||||
|
||||
void MessagesUi::updateMessageSize(MessageView &entry) {
|
||||
const auto &padding = st::groupCallMessagePadding;
|
||||
|
||||
const auto hasUserpic = !entry.failed;
|
||||
const auto userpicPadding = st::groupCallUserpicPadding;
|
||||
const auto userpicSize = st::groupCallUserpic;
|
||||
const auto leftSkip = hasUserpic
|
||||
? (userpicPadding.left() + userpicSize + userpicPadding.right())
|
||||
: padding.left();
|
||||
const auto widthSkip = leftSkip + padding.right();
|
||||
const auto inner = _width - widthSkip;
|
||||
|
||||
const auto size = Ui::Text::CountOptimalTextSize(
|
||||
entry.text,
|
||||
std::min(st::groupCallWidth / 2, inner),
|
||||
inner);
|
||||
|
||||
const auto textHeight = size.height();
|
||||
entry.width = size.width() + widthSkip;
|
||||
entry.left = (_width - entry.width) / 2;
|
||||
updateReactionPosition(entry);
|
||||
|
||||
const auto contentHeight = padding.top() + textHeight + padding.bottom();
|
||||
const auto userpicHeight = hasUserpic
|
||||
? (userpicPadding.top() + userpicSize + userpicPadding.bottom())
|
||||
: 0;
|
||||
|
||||
const auto skip = st::groupCallMessageSkip;
|
||||
entry.realHeight = skip + std::max(contentHeight, userpicHeight);
|
||||
}
|
||||
|
||||
bool MessagesUi::updateMessageHeight(MessageView &entry) {
|
||||
const auto height = entry.toggleAnimation.animating()
|
||||
? anim::interpolate(
|
||||
0,
|
||||
entry.realHeight,
|
||||
entry.toggleAnimation.value(entry.removed ? 0. : 1.))
|
||||
: entry.realHeight;
|
||||
if (entry.height == height) {
|
||||
return false;
|
||||
}
|
||||
entry.height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MessagesUi::setContentFailed(MessageView &entry) {
|
||||
entry.failed = true;
|
||||
entry.text = Ui::Text::String(
|
||||
st::messageTextStyle,
|
||||
TextWithEntities().append(
|
||||
QString::fromUtf8("\xe2\x9d\x97\xef\xb8\x8f")
|
||||
).append(' ').append(
|
||||
Ui::Text::Italic(u"Failed to send the message."_q)),
|
||||
kMarkupTextOptions,
|
||||
st::groupCallWidth / 4);
|
||||
}
|
||||
|
||||
void MessagesUi::setContent(
|
||||
MessageView &entry,
|
||||
const TextWithEntities &text) {
|
||||
entry.text = Ui::Text::String(
|
||||
st::messageTextStyle,
|
||||
text,
|
||||
kMarkupTextOptions,
|
||||
st::groupCallWidth / 4,
|
||||
Core::TextContext({
|
||||
.session = &_show->session(),
|
||||
.repaint = [this, id = entry.id] { repaintMessage(id); },
|
||||
}));
|
||||
entry.text.setLink(1, entry.fromLink);
|
||||
if (entry.text.hasSpoilers()) {
|
||||
const auto id = entry.id;
|
||||
const auto guard = base::make_weak(_messages);
|
||||
entry.text.setSpoilerLinkFilter([=](const ClickContext &context) {
|
||||
if (context.button != Qt::LeftButton || !guard) {
|
||||
return false;
|
||||
}
|
||||
const auto i = ranges::find(
|
||||
_views,
|
||||
_revealedSpoilerId,
|
||||
&MessageView::id);
|
||||
if (i != end(_views) && _revealedSpoilerId != id) {
|
||||
i->text.setSpoilerRevealed(false, anim::type::normal);
|
||||
}
|
||||
_revealedSpoilerId = id;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::toggleMessage(MessageView &entry, bool shown) {
|
||||
const auto id = entry.id;
|
||||
entry.removed = !shown;
|
||||
entry.toggleAnimation.start(
|
||||
[=] { repaintMessage(id); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::slideWrapDuration,
|
||||
shown ? anim::easeOutCirc : anim::easeInCirc);
|
||||
repaintMessage(id);
|
||||
}
|
||||
|
||||
void MessagesUi::repaintMessage(uint64 id) {
|
||||
auto i = ranges::find(_views, id, &MessageView::id);
|
||||
if (i == end(_views)) {
|
||||
return;
|
||||
} else if (i->removed && !i->toggleAnimation.animating()) {
|
||||
const auto top = i->top;
|
||||
i = _views.erase(i);
|
||||
recountHeights(i, top);
|
||||
return;
|
||||
}
|
||||
if (!i->sending && !i->sentAnimation.animating()) {
|
||||
i->sendingAnimation = nullptr;
|
||||
}
|
||||
if (i->toggleAnimation.animating() || i->height != i->realHeight) {
|
||||
if (updateMessageHeight(*i)) {
|
||||
recountHeights(i, i->top);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_messages->update(0, i->top, _messages->width(), i->height);
|
||||
}
|
||||
|
||||
void MessagesUi::recountHeights(
|
||||
std::vector<MessageView>::iterator i,
|
||||
int top) {
|
||||
auto from = top;
|
||||
for (auto e = end(_views); i != e; ++i) {
|
||||
i->top = top;
|
||||
top += i->height;
|
||||
updateReactionPosition(*i);
|
||||
}
|
||||
if (_views.empty()) {
|
||||
delete base::take(_messages);
|
||||
_scroll = nullptr;
|
||||
} else {
|
||||
updateGeometries();
|
||||
_messages->update(0, from, _messages->width(), top - from);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::appendMessage(const Message &data) {
|
||||
const auto top = _views.empty()
|
||||
? 0
|
||||
: (_views.back().top + _views.back().height);
|
||||
|
||||
if (!_scroll) {
|
||||
setupMessagesWidget();
|
||||
}
|
||||
|
||||
auto &entry = _views.emplace_back();
|
||||
const auto id = entry.id = data.randomId;
|
||||
const auto repaint = [=] {
|
||||
repaintMessage(id);
|
||||
};
|
||||
const auto peer = entry.from = data.peer;
|
||||
entry.fromLink = std::make_shared<LambdaClickHandler>([=] {
|
||||
_show->show(
|
||||
PrepareShortInfoBox(peer, _show, &st::storiesShortInfoBox));
|
||||
});
|
||||
entry.sending = !data.date;
|
||||
if (data.failed) {
|
||||
setContentFailed(entry);
|
||||
} else {
|
||||
setContent(
|
||||
entry,
|
||||
Ui::Text::Link(Ui::Text::Bold(data.peer->shortName()), 1).append(
|
||||
' ').append(data.text));
|
||||
}
|
||||
entry.top = top;
|
||||
updateMessageSize(entry);
|
||||
if (entry.sending) {
|
||||
using namespace Ui;
|
||||
const auto &st = st::defaultInfiniteRadialAnimation;
|
||||
entry.sendingAnimation = std::make_unique<InfiniteRadialAnimation>(
|
||||
repaint,
|
||||
st);
|
||||
entry.sendingAnimation->start(0);
|
||||
}
|
||||
entry.height = 0;
|
||||
toggleMessage(entry, true);
|
||||
checkReactionContent(entry, data.text);
|
||||
}
|
||||
|
||||
void MessagesUi::checkReactionContent(
|
||||
MessageView &entry,
|
||||
const TextWithEntities &text) {
|
||||
auto outLength = 0;
|
||||
using Type = Data::Reactions::Type;
|
||||
const auto reactions = &_show->session().data().reactions();
|
||||
const auto set = [&](Data::ReactionId id) {
|
||||
reactions->preloadAnimationsFor(id);
|
||||
entry.reactionId = std::move(id);
|
||||
};
|
||||
if (text.entities.size() == 1
|
||||
&& text.entities.front().type() == EntityType::CustomEmoji
|
||||
&& text.entities.front().offset() == 0
|
||||
&& text.entities.front().length() == text.text.size()) {
|
||||
set({ text.entities.front().data().toULongLong() });
|
||||
} else if (const auto emoji = Ui::Emoji::Find(text.text, &outLength)) {
|
||||
if (outLength < text.text.size()) {
|
||||
return;
|
||||
}
|
||||
const auto &all = reactions->list(Type::All);
|
||||
for (const auto &reaction : all) {
|
||||
if (reaction.id.custom()) {
|
||||
continue;
|
||||
}
|
||||
const auto &text = reaction.id.emoji();
|
||||
if (Ui::Emoji::Find(text) != emoji) {
|
||||
continue;
|
||||
}
|
||||
set(reaction.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::startReactionAnimation(MessageView &entry) {
|
||||
entry.reactionWidget = std::make_unique<Ui::RpWidget>(_parent);
|
||||
const auto raw = entry.reactionWidget.get();
|
||||
raw->show();
|
||||
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
if (!_effectsLifetime) {
|
||||
rpl::combine(
|
||||
_scroll->scrollTopValue(),
|
||||
_scroll->RpWidget::positionValue()
|
||||
) | rpl::start_with_next([=](int yshift, QPoint point) {
|
||||
_reactionBasePosition = point - QPoint(0, yshift);
|
||||
for (auto &view : _views) {
|
||||
updateReactionPosition(view);
|
||||
}
|
||||
}, _effectsLifetime);
|
||||
}
|
||||
|
||||
entry.reactionAnimation = std::make_unique<Ui::ReactionFlyAnimation>(
|
||||
&_show->session().data().reactions(),
|
||||
Ui::ReactionFlyAnimationArgs{
|
||||
.id = entry.reactionId,
|
||||
.effectOnly = true,
|
||||
},
|
||||
[=] { raw->update(); },
|
||||
st::reactionInlineImage);
|
||||
updateReactionPosition(entry);
|
||||
|
||||
const auto effectSize = st::reactionInlineImage * 2;
|
||||
const auto animation = entry.reactionAnimation.get();
|
||||
raw->resize(effectSize, effectSize);
|
||||
raw->paintRequest() | rpl::start_with_next([=] {
|
||||
if (animation->finished()) {
|
||||
crl::on_main(raw, [=] {
|
||||
removeReaction(raw);
|
||||
});
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(raw);
|
||||
const auto size = raw->width();
|
||||
const auto skip = (size - st::reactionInlineImage) / 2;
|
||||
const auto target = QRect(
|
||||
QPoint(skip, skip),
|
||||
QSize(st::reactionInlineImage, st::reactionInlineImage));
|
||||
animation->paintGetArea(
|
||||
p,
|
||||
QPoint(),
|
||||
target,
|
||||
st::radialFg->c,
|
||||
QRect(),
|
||||
crl::now());
|
||||
}, raw->lifetime());
|
||||
}
|
||||
|
||||
void MessagesUi::removeReaction(not_null<Ui::RpWidget*> widget) {
|
||||
const auto i = ranges::find_if(_views, [&](const MessageView &entry) {
|
||||
return entry.reactionWidget.get() == widget;
|
||||
});
|
||||
if (i != end(_views)) {
|
||||
i->reactionId = {};
|
||||
i->reactionWidget = nullptr;
|
||||
i->reactionAnimation = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
void MessagesUi::updateReactionPosition(MessageView &entry) {
|
||||
if (const auto widget = entry.reactionWidget.get()) {
|
||||
if (entry.failed) {
|
||||
widget->resize(0, 0);
|
||||
return;
|
||||
}
|
||||
const auto padding = st::groupCallMessagePadding;
|
||||
const auto userpicSize = st::groupCallUserpic;
|
||||
const auto userpicPadding = st::groupCallUserpicPadding;
|
||||
const auto esize = st::emojiSize;
|
||||
const auto eleft = entry.text.maxWidth() - st::emojiPadding - esize;
|
||||
const auto etop = (st::normalFont->height - esize) / 2;
|
||||
const auto effectSize = st::reactionInlineImage * 2;
|
||||
entry.reactionShift = QPoint(entry.left, entry.top)
|
||||
+ QPoint(
|
||||
userpicPadding.left() + userpicSize + userpicPadding.right(),
|
||||
padding.top())
|
||||
+ QPoint(eleft + (esize / 2), etop + (esize / 2))
|
||||
- QPoint(effectSize / 2, effectSize / 2);
|
||||
widget->move(_reactionBasePosition + entry.reactionShift);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::updateTopFade() {
|
||||
const auto topFadeShown = (_scroll->scrollTop() > 0);
|
||||
if (_topFadeShown != topFadeShown) {
|
||||
_topFadeShown = topFadeShown;
|
||||
//const auto from = topFadeShown ? 0. : 1.;
|
||||
//const auto till = topFadeShown ? 1. : 0.;
|
||||
//_topFadeAnimation.start([=] {
|
||||
_messages->update(
|
||||
0,
|
||||
_scroll->scrollTop(),
|
||||
_messages->width(),
|
||||
_fadeHeight);
|
||||
//}, from, till, st::slideWrapDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::updateBottomFade() {
|
||||
const auto max = _scroll->scrollTopMax();
|
||||
const auto bottomFadeShown = (_scroll->scrollTop() < max);
|
||||
if (_bottomFadeShown != bottomFadeShown) {
|
||||
_bottomFadeShown = bottomFadeShown;
|
||||
//const auto from = bottomFadeShown ? 0. : 1.;
|
||||
//const auto till = bottomFadeShown ? 1. : 0.;
|
||||
//_bottomFadeAnimation.start([=] {
|
||||
_messages->update(
|
||||
0,
|
||||
_scroll->scrollTop() + _scroll->height() - _fadeHeight,
|
||||
_messages->width(),
|
||||
_fadeHeight);
|
||||
//}, from, till, st::slideWrapDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::setupMessagesWidget() {
|
||||
_scroll = std::make_unique<Ui::ElasticScroll>(
|
||||
_parent,
|
||||
st::groupCallMessagesScroll);
|
||||
const auto scroll = _scroll.get();
|
||||
|
||||
_messages = scroll->setOwnedWidget(object_ptr<Ui::RpWidget>(scroll));
|
||||
rpl::combine(
|
||||
scroll->scrollTopValue(),
|
||||
scroll->heightValue(),
|
||||
_messages->heightValue()
|
||||
) | rpl::start_with_next([=] {
|
||||
updateTopFade();
|
||||
updateBottomFade();
|
||||
}, scroll->lifetime());
|
||||
|
||||
ReceiveSomeMouseEvents(scroll, [=](QPoint point) {
|
||||
for (const auto &entry : _views) {
|
||||
if (entry.failed || entry.top + entry.height <= point.y()) {
|
||||
continue;
|
||||
} else if (entry.top >= point.y()
|
||||
|| entry.left >= point.x()
|
||||
|| entry.left + entry.width <= point.x()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto padding = st::groupCallMessagePadding;
|
||||
const auto userpicSize = st::groupCallUserpic;
|
||||
const auto userpicPadding = st::groupCallUserpicPadding;
|
||||
const auto leftSkip = userpicPadding.left()
|
||||
+ userpicSize
|
||||
+ userpicPadding.right();
|
||||
const auto userpic = QRect(
|
||||
entry.left + userpicPadding.left(),
|
||||
entry.top + userpicPadding.top(),
|
||||
userpicSize,
|
||||
userpicSize);
|
||||
const auto link = userpic.contains(point)
|
||||
? entry.fromLink
|
||||
: entry.text.getState(point - QPoint(
|
||||
entry.left + leftSkip,
|
||||
entry.top + padding.top()
|
||||
), entry.width - leftSkip - padding.right()).link;
|
||||
if (link) {
|
||||
ActivateClickHandler(_messages, link, Qt::LeftButton);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
_messages->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||
const auto start = scroll->scrollTop();
|
||||
const auto end = start + scroll->height();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
|
||||
if ((_canvas.width() < scroll->width() * ratio)
|
||||
|| (_canvas.height() < scroll->height() * ratio)) {
|
||||
_canvas = QImage(
|
||||
scroll->size() * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_canvas.setDevicePixelRatio(ratio);
|
||||
}
|
||||
auto p = Painter(&_canvas);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
p.fillRect(QRect(QPoint(), scroll->size()), QColor(0, 0, 0, 0));
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
const auto now = crl::now();
|
||||
const auto skip = st::groupCallMessageSkip;
|
||||
const auto padding = st::groupCallMessagePadding;
|
||||
p.translate(0, -start);
|
||||
for (auto &entry : _views) {
|
||||
if (entry.height <= skip || entry.top + entry.height <= start) {
|
||||
continue;
|
||||
} else if (entry.top >= end) {
|
||||
break;
|
||||
}
|
||||
const auto use = entry.realHeight - skip;
|
||||
const auto width = entry.width;
|
||||
p.setBrush(st::radialBg);
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto scaled = (entry.height < entry.realHeight);
|
||||
if (scaled) {
|
||||
const auto used = entry.height - skip;
|
||||
const auto mx = scaled ? (entry.left + (width / 2)) : 0;
|
||||
const auto my = scaled ? (entry.top + (used / 2)) : 0;
|
||||
const auto scale = used / float64(use);
|
||||
p.save();
|
||||
p.translate(mx, my);
|
||||
p.scale(scale, scale);
|
||||
p.setOpacity(scale);
|
||||
p.translate(-mx, -my);
|
||||
}
|
||||
_messageBgRect.paint(p, { entry.left, entry.top, width, use });
|
||||
|
||||
auto leftSkip = padding.left();
|
||||
const auto hasUserpic = !entry.failed;
|
||||
if (hasUserpic) {
|
||||
const auto userpicSize = st::groupCallUserpic;
|
||||
const auto userpicPadding = st::groupCallUserpicPadding;
|
||||
const auto position = QPoint(
|
||||
entry.left + userpicPadding.left(),
|
||||
entry.top + userpicPadding.top());
|
||||
const auto rect = QRect(
|
||||
position,
|
||||
QSize(userpicSize, userpicSize));
|
||||
entry.from->paintUserpic(p, entry.view, {
|
||||
.position = position,
|
||||
.size = userpicSize,
|
||||
.shape = Ui::PeerUserpicShape::Circle,
|
||||
});
|
||||
if (const auto animation = entry.sendingAnimation.get()) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::groupCallBg->p;
|
||||
const auto shift = userpicPadding.left();
|
||||
pen.setWidthF(shift);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto state = animation->computeState();
|
||||
const auto sent = entry.sending
|
||||
? 0.
|
||||
: entry.sentAnimation.value(1.);
|
||||
p.setOpacity(state.shown * (1. - sent));
|
||||
p.drawArc(
|
||||
rect.marginsRemoved({ shift, shift, shift, shift }),
|
||||
state.arcFrom,
|
||||
state.arcLength);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
leftSkip = userpicPadding.left()
|
||||
+ userpicSize
|
||||
+ userpicPadding.right();
|
||||
}
|
||||
|
||||
p.setPen(st::radialFg);
|
||||
entry.text.draw(p, {
|
||||
.position = {
|
||||
entry.left + leftSkip,
|
||||
entry.top + padding.top()
|
||||
},
|
||||
.availableWidth = entry.width - leftSkip - padding.right(),
|
||||
.palette = &st::groupCallMessagePalette,
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.now = now,
|
||||
.paused = !_messages->window()->isActiveWindow(),
|
||||
});
|
||||
if (!scaled && entry.reactionId && !entry.reactionAnimation) {
|
||||
startReactionAnimation(entry);
|
||||
}
|
||||
|
||||
p.restore();
|
||||
}
|
||||
p.translate(0, start);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
const auto topFade = _topFadeAnimation.value(
|
||||
_topFadeShown ? 1. : 0.);
|
||||
if (topFade) {
|
||||
auto gradientTop = QLinearGradient(0, 0, 0, _fadeHeight);
|
||||
gradientTop.setStops({
|
||||
{ 0., QColor(255, 255, 255, 0) },
|
||||
{ 1., QColor(255, 255, 255, 255) },
|
||||
});
|
||||
p.setOpacity(topFade);
|
||||
p.setBrush(gradientTop);
|
||||
p.drawRect(0, 0, scroll->width(), _fadeHeight);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
const auto bottomFade = _bottomFadeAnimation.value(
|
||||
_bottomFadeShown ? 1. : 0.);
|
||||
if (bottomFade) {
|
||||
const auto till = scroll->height();
|
||||
const auto from = till - _fadeHeight;
|
||||
auto gradientBottom = QLinearGradient(0, from, 0, till);
|
||||
gradientBottom.setStops({
|
||||
{ 0., QColor(255, 255, 255, 255) },
|
||||
{ 1., QColor(255, 255, 255, 0) },
|
||||
});
|
||||
p.setBrush(gradientBottom);
|
||||
p.drawRect(0, from, scroll->width(), _fadeHeight);
|
||||
}
|
||||
QPainter(_messages).drawImage(
|
||||
QRect(QPoint(0, start), scroll->size()),
|
||||
_canvas,
|
||||
QRect(QPoint(), scroll->size() * ratio));
|
||||
}, _lifetime);
|
||||
|
||||
scroll->show();
|
||||
applyWidth();
|
||||
}
|
||||
|
||||
void MessagesUi::applyWidth() {
|
||||
if (!_scroll || _width < st::groupCallWidth * 2 / 3) {
|
||||
return;
|
||||
}
|
||||
auto top = 0;
|
||||
for (auto &entry : _views) {
|
||||
entry.top = top;
|
||||
|
||||
updateMessageSize(entry);
|
||||
updateMessageHeight(entry);
|
||||
|
||||
top += entry.height;
|
||||
}
|
||||
updateGeometries();
|
||||
}
|
||||
|
||||
void MessagesUi::updateGeometries() {
|
||||
const auto scrollBottom = (_scroll->scrollTop() + _scroll->height());
|
||||
const auto atBottom = (scrollBottom >= _messages->height());
|
||||
|
||||
const auto height = _views.empty()
|
||||
? 0
|
||||
: (_views.back().top + _views.back().height);
|
||||
_messages->setGeometry(0, 0, _width, height);
|
||||
|
||||
const auto min = std::min(height, _availableHeight);
|
||||
_scroll->setGeometry(_left, _bottom - min, _width, min);
|
||||
|
||||
if (atBottom) {
|
||||
_scroll->scrollToY(std::max(height - _scroll->height(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::move(int left, int bottom, int width, int availableHeight) {
|
||||
if (_left != left
|
||||
|| _bottom != bottom
|
||||
|| _width != width
|
||||
|| _availableHeight != availableHeight) {
|
||||
_left = left;
|
||||
_bottom = bottom;
|
||||
_width = width;
|
||||
_availableHeight = availableHeight;
|
||||
applyWidth();
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesUi::raise() {
|
||||
if (_scroll) {
|
||||
_scroll->raise();
|
||||
}
|
||||
for (const auto &view : _views) {
|
||||
if (const auto widget = view.reactionWidget.get()) {
|
||||
widget->raise();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rpl::lifetime &MessagesUi::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Calls::Group
|
||||
108
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
108
Telegram/SourceFiles/calls/group/calls_group_messages_ui.h
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/round_rect.h"
|
||||
|
||||
struct TextWithTags;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
class TabbedPanel;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class ElasticScroll;
|
||||
class EmojiButton;
|
||||
class InputField;
|
||||
class SendButton;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Calls::Group {
|
||||
|
||||
struct Message;
|
||||
|
||||
class MessagesUi final {
|
||||
public:
|
||||
MessagesUi(
|
||||
not_null<QWidget*> parent,
|
||||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown);
|
||||
~MessagesUi();
|
||||
|
||||
void move(int left, int bottom, int width, int availableHeight);
|
||||
void raise();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct MessageView;
|
||||
|
||||
void setupList(
|
||||
rpl::producer<std::vector<Message>> messages,
|
||||
rpl::producer<bool> shown);
|
||||
void toggleMessage(MessageView &entry, bool shown);
|
||||
void setContentFailed(MessageView &entry);
|
||||
void setContent(MessageView &entry, const TextWithEntities &text);
|
||||
void updateMessageSize(MessageView &entry);
|
||||
bool updateMessageHeight(MessageView &entry);
|
||||
void animateMessageSent(MessageView &entry);
|
||||
void repaintMessage(uint64 id);
|
||||
void recountHeights(std::vector<MessageView>::iterator i, int top);
|
||||
void appendMessage(const Message &data);
|
||||
void checkReactionContent(
|
||||
MessageView &entry,
|
||||
const TextWithEntities &text);
|
||||
void startReactionAnimation(MessageView &entry);
|
||||
void updateReactionPosition(MessageView &entry);
|
||||
void removeReaction(not_null<Ui::RpWidget*> widget);
|
||||
void setupMessagesWidget();
|
||||
void applyWidth();
|
||||
void updateGeometries();
|
||||
void updateTopFade();
|
||||
void updateBottomFade();
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
std::unique_ptr<Ui::ElasticScroll> _scroll;
|
||||
Ui::Animations::Simple _scrollToBottomAnimation;
|
||||
Ui::RpWidget *_messages = nullptr;
|
||||
QImage _canvas;
|
||||
|
||||
std::vector<MessageView> _views;
|
||||
style::complex_color _messageBg;
|
||||
Ui::RoundRect _messageBgRect;
|
||||
|
||||
QPoint _reactionBasePosition;
|
||||
rpl::lifetime _effectsLifetime;
|
||||
|
||||
Ui::Animations::Simple _topFadeAnimation;
|
||||
Ui::Animations::Simple _bottomFadeAnimation;
|
||||
int _fadeHeight = 0;
|
||||
bool _topFadeShown = false;
|
||||
bool _bottomFadeShown = false;
|
||||
|
||||
int _left = 0;
|
||||
int _bottom = 0;
|
||||
int _width = 0;
|
||||
int _availableHeight = 0;
|
||||
|
||||
uint64 _revealedSpoilerId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Calls::Group
|
||||
@@ -8,16 +8,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_members.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/group/calls_group_toasts.h"
|
||||
#include "calls/group/calls_group_invite_controller.h"
|
||||
#include "calls/group/calls_group_members.h"
|
||||
#include "calls/group/calls_group_menu.h"
|
||||
#include "calls/group/calls_group_message_field.h"
|
||||
#include "calls/group/calls_group_messages.h"
|
||||
#include "calls/group/calls_group_messages_ui.h"
|
||||
#include "calls/group/calls_group_settings.h"
|
||||
#include "calls/group/calls_group_toasts.h"
|
||||
#include "calls/group/calls_group_viewport.h"
|
||||
#include "calls/group/ui/calls_group_scheduled_labels.h"
|
||||
#include "calls/group/ui/desktop_capture_choose_source.h"
|
||||
#include "calls/calls_emoji_fingerprint.h"
|
||||
#include "calls/calls_window.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "ui/platform/ui_platform_window_title.h" // TitleLayout
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "ui/controls/call_mute_button.h"
|
||||
@@ -50,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/session/session_show.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "menu/menu_send.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
@@ -94,6 +100,96 @@ void UnpinMaximized(not_null<QWidget*> widget) {
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
class Show final : public ChatHelpers::Show {
|
||||
public:
|
||||
Show(not_null<Panel*> panel, std::shared_ptr<Ui::Show> base)
|
||||
: _panel(panel)
|
||||
, _base(std::move(base)) {
|
||||
}
|
||||
|
||||
void activate() override {
|
||||
if (const auto panel = _panel.get()) {
|
||||
if (!panel->window()->isHidden()) {
|
||||
panel->window()->activateWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showOrHideBoxOrLayer(
|
||||
std::variant<
|
||||
v::null_t,
|
||||
object_ptr<Ui::BoxContent>,
|
||||
std::unique_ptr<Ui::LayerWidget>> &&layer,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) const override {
|
||||
_base->showOrHideBoxOrLayer(
|
||||
std::move(layer),
|
||||
options,
|
||||
anim::type::normal);
|
||||
}
|
||||
not_null<QWidget*> toastParent() const override {
|
||||
return _base->toastParent();
|
||||
}
|
||||
bool valid() const override {
|
||||
return _panel.get() != nullptr;
|
||||
}
|
||||
operator bool() const override {
|
||||
return valid();
|
||||
}
|
||||
|
||||
Main::Session &session() const override {
|
||||
const auto panel = _panel.get();
|
||||
Assert(panel != nullptr);
|
||||
|
||||
return panel->call()->peer()->session();
|
||||
}
|
||||
bool paused(ChatHelpers::PauseReason reason) const override {
|
||||
const auto panel = _panel.get();
|
||||
if (!panel) {
|
||||
return false;
|
||||
} else if (panel->window()->isHidden()
|
||||
|| (!panel->window()->isFullScreen()
|
||||
&& !panel->window()->isActiveWindow())) {
|
||||
return true;
|
||||
} else if (reason < ChatHelpers::PauseReason::Layer
|
||||
&& panel->callWindow()->topShownLayer() != nullptr) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
rpl::producer<> pauseChanged() const override {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
rpl::producer<bool> adjustShadowLeft() const override {
|
||||
return rpl::single(false);
|
||||
}
|
||||
SendMenu::Details sendMenuDetails() const override {
|
||||
return { SendMenu::Type::Disabled };
|
||||
}
|
||||
|
||||
bool showMediaPreview(
|
||||
Data::FileOrigin origin,
|
||||
not_null<DocumentData*> document) const override {
|
||||
return false; // #TODO stories
|
||||
}
|
||||
bool showMediaPreview(
|
||||
Data::FileOrigin origin,
|
||||
not_null<PhotoData*> photo) const override {
|
||||
return false; // #TODO stories
|
||||
}
|
||||
|
||||
void processChosenSticker(
|
||||
ChatHelpers::FileChosen &&chosen) const override {
|
||||
//_panel->emojiChosen(std::move(chosen));
|
||||
}
|
||||
|
||||
private:
|
||||
const base::weak_ptr<Panel> _panel;
|
||||
const std::shared_ptr<Ui::Show> _base;
|
||||
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Panel::ControlsBackgroundNarrow {
|
||||
@@ -104,6 +200,7 @@ struct Panel::ControlsBackgroundNarrow {
|
||||
|
||||
Ui::RpWidget shadow;
|
||||
Ui::RpWidget blocker;
|
||||
int shadowHeight = 0;
|
||||
};
|
||||
|
||||
Panel::Panel(not_null<GroupCall*> call)
|
||||
@@ -141,6 +238,11 @@ Panel::Panel(not_null<GroupCall*> call, ConferencePanelMigration info)
|
||||
, _hangup(widget(), st::groupCallHangup)
|
||||
, _stickedTooltipsShown(Core::App().settings().hiddenGroupCallTooltips()
|
||||
& ~StickedTooltip::Microphone) // Always show tooltip about mic.
|
||||
, _messages(std::make_unique<MessagesUi>(
|
||||
widget(),
|
||||
uiShow(),
|
||||
_call->messages()->listValue(),
|
||||
_call->messagesEnabledValue()))
|
||||
, _toasts(std::make_unique<Toasts>(this))
|
||||
, _controlsBackgroundColor([] {
|
||||
auto result = st::groupCallBg->c;
|
||||
@@ -191,12 +293,11 @@ bool Panel::isActive() const {
|
||||
return window()->isActiveWindow() && isVisible();
|
||||
}
|
||||
|
||||
std::shared_ptr<Main::SessionShow> Panel::sessionShow() {
|
||||
return Main::MakeSessionShow(uiShow(), &_peer->session());
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::Show> Panel::uiShow() {
|
||||
return _window->uiShow();
|
||||
std::shared_ptr<ChatHelpers::Show> Panel::uiShow() {
|
||||
if (!_cachedShow) {
|
||||
_cachedShow = std::make_shared<Show>(this, _window->uiShow());
|
||||
}
|
||||
return _cachedShow;
|
||||
}
|
||||
|
||||
void Panel::minimize() {
|
||||
@@ -387,6 +488,47 @@ void Panel::initWidget() {
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Panel::toggleMessageTyping() {
|
||||
const auto typing = !_messageTyping.current();
|
||||
if (_messageField) {
|
||||
_messageField->toggle(typing);
|
||||
} else if (typing) {
|
||||
_messageField = std::make_unique<MessageField>(
|
||||
widget(),
|
||||
uiShow(),
|
||||
_call->conference() ? nullptr : _call->peer().get());
|
||||
|
||||
updateButtonsGeometry();
|
||||
_messageField->toggle(true);
|
||||
|
||||
_messageField->submitted(
|
||||
) | rpl::start_with_next([=](TextWithTags text) {
|
||||
_call->sendMessage(std::move(text));
|
||||
|
||||
_messageField->toggle(false);
|
||||
_messageTyping = false;
|
||||
updateWideControlsVisibility();
|
||||
}, _messageField->lifetime());
|
||||
|
||||
_messageField->heightValue() | rpl::start_with_next([=] {
|
||||
updateButtonsGeometry();
|
||||
}, _messageField->lifetime());
|
||||
|
||||
_messageField->closeRequests() | rpl::start_with_next([=] {
|
||||
if (_messageTyping.current()) {
|
||||
toggleMessageTyping();
|
||||
}
|
||||
}, _messageField->lifetime());
|
||||
|
||||
_messageField->closed() | rpl::start_with_next([=] {
|
||||
_messageField = nullptr;
|
||||
updateButtonsGeometry();
|
||||
}, _messageField->lifetime());
|
||||
}
|
||||
_messageTyping = typing;
|
||||
updateWideControlsVisibility();
|
||||
}
|
||||
|
||||
void Panel::endCall() {
|
||||
if (!_call->canManage()) {
|
||||
_call->hangup();
|
||||
@@ -458,6 +600,7 @@ void Panel::initControls() {
|
||||
}, _mute->lifetime());
|
||||
|
||||
initShareAction();
|
||||
createMessageButton();
|
||||
refreshLeftButton();
|
||||
refreshVideoButtons();
|
||||
|
||||
@@ -568,6 +711,19 @@ void Panel::refreshLeftButton() {
|
||||
updateButtonsStyles();
|
||||
}
|
||||
|
||||
rpl::producer<Ui::CallButtonColors> Panel::toggleableOverrides(
|
||||
rpl::producer<bool> active) {
|
||||
return rpl::combine(
|
||||
std::move(active),
|
||||
_mute->colorOverrides()
|
||||
) | rpl::map([](bool active, Ui::CallButtonColors colors) {
|
||||
if (active && colors.bg) {
|
||||
colors.bg->setAlpha(kOverrideActiveColorBgAlpha);
|
||||
}
|
||||
return colors;
|
||||
});
|
||||
}
|
||||
|
||||
void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
|
||||
const auto create = overrideWideMode.value_or(mode() == PanelMode::Wide)
|
||||
|| (!_call->scheduleDate() && _call->videoIsWorking());
|
||||
@@ -582,17 +738,6 @@ void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto toggleableOverrides = [&](rpl::producer<bool> active) {
|
||||
return rpl::combine(
|
||||
std::move(active),
|
||||
_mute->colorOverrides()
|
||||
) | rpl::map([](bool active, Ui::CallButtonColors colors) {
|
||||
if (active && colors.bg) {
|
||||
colors.bg->setAlpha(kOverrideActiveColorBgAlpha);
|
||||
}
|
||||
return colors;
|
||||
});
|
||||
};
|
||||
if (!_video) {
|
||||
_video.create(
|
||||
widget(),
|
||||
@@ -642,6 +787,19 @@ void Panel::refreshVideoButtons(std::optional<bool> overrideWideMode) {
|
||||
raiseControls();
|
||||
}
|
||||
|
||||
void Panel::createMessageButton() {
|
||||
if (!_message) {
|
||||
_message.create(
|
||||
widget(),
|
||||
st::groupCallMessageSmall,
|
||||
&st::groupCallMessageActiveSmall);
|
||||
_message->show();
|
||||
_message->setClickedCallback([=] { toggleMessageTyping(); });
|
||||
_message->setColorOverrides(
|
||||
toggleableOverrides(_messageTyping.value()));
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::hideStickedTooltip(StickedTooltipHide hide) {
|
||||
if (!_stickedTooltipClose || !_niceTooltipControl) {
|
||||
return;
|
||||
@@ -913,7 +1071,7 @@ Fn<void()> Panel::shareConferenceLinkCallback() {
|
||||
return [=] {
|
||||
Expects(_call->conference());
|
||||
|
||||
ShowConferenceCallLinkBox(sessionShow(), _call->conferenceCall(), {
|
||||
ShowConferenceCallLinkBox(uiShow(), _call->conferenceCall(), {
|
||||
.st = DarkConferenceCallLinkStyle(),
|
||||
});
|
||||
};
|
||||
@@ -921,7 +1079,7 @@ Fn<void()> Panel::shareConferenceLinkCallback() {
|
||||
|
||||
void Panel::migrationShowShareLink() {
|
||||
ShowConferenceCallLinkBox(
|
||||
sessionShow(),
|
||||
uiShow(),
|
||||
_call->conferenceCall(),
|
||||
{ .st = DarkConferenceCallLinkStyle() });
|
||||
}
|
||||
@@ -988,6 +1146,7 @@ void Panel::raiseControls() {
|
||||
&_screenShare,
|
||||
&_wideMenu,
|
||||
&_video,
|
||||
&_message,
|
||||
&_hangup
|
||||
};
|
||||
for (const auto button : buttons) {
|
||||
@@ -1015,6 +1174,10 @@ void Panel::raiseControls() {
|
||||
if (_pinOnTop) {
|
||||
_pinOnTop->raise();
|
||||
}
|
||||
_messages->raise();
|
||||
if (_messageField) {
|
||||
_messageField->raise();
|
||||
}
|
||||
_window->raiseLayers();
|
||||
if (_niceTooltip) {
|
||||
_niceTooltip->raise();
|
||||
@@ -1100,7 +1263,8 @@ void Panel::toggleWideControls(bool shown) {
|
||||
|
||||
void Panel::updateWideControlsVisibility() {
|
||||
const auto shown = _showWideControls
|
||||
|| (_stickedTooltipClose != nullptr);
|
||||
|| (_stickedTooltipClose != nullptr)
|
||||
|| _messageTyping.current();
|
||||
if (_wideControlsShown == shown) {
|
||||
return;
|
||||
}
|
||||
@@ -1204,6 +1368,11 @@ void Panel::subscribeToChanges(not_null<Data::GroupCall*> real) {
|
||||
showStickedTooltip();
|
||||
}, lifetime());
|
||||
|
||||
_call->messagesEnabledValue() | rpl::start_with_next([=] {
|
||||
updateButtonsGeometry();
|
||||
raiseControls();
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_call->videoIsWorkingValue(),
|
||||
_call->isSharingScreenValue()
|
||||
@@ -1690,6 +1859,16 @@ void Panel::updateButtonsStyles() {
|
||||
? rpl::single(QString())
|
||||
: tr::lng_group_call_video());
|
||||
}
|
||||
if (_message) {
|
||||
_message->setStyle(
|
||||
wide ? st::groupCallMessageSmall : st::groupCallMessage,
|
||||
(wide
|
||||
? &st::groupCallMessageActiveSmall
|
||||
: &st::groupCallMessageActive));
|
||||
_message->setText(wide
|
||||
? rpl::single(QString())
|
||||
: tr::lng_group_call_message());
|
||||
}
|
||||
if (_settings) {
|
||||
_settings->setText(wide
|
||||
? rpl::single(QString())
|
||||
@@ -1825,6 +2004,7 @@ void Panel::setupControlsBackgroundNarrow() {
|
||||
const auto height = std::max(
|
||||
st::groupCallMembersShadowHeight,
|
||||
st::groupCallMembersFadeSkip + st::groupCallMembersFadeHeight);
|
||||
_controlsBackgroundNarrow->shadowHeight = height;
|
||||
const auto full = lifetime.make_state<QImage>(
|
||||
QSize(1, height * factor),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
@@ -1877,7 +2057,9 @@ void Panel::setupControlsBackgroundNarrow() {
|
||||
const auto inner = _members->getInnerGeometry().translated(
|
||||
_members->x() - _controlsBackgroundNarrow->shadow.x(),
|
||||
_members->y() - _controlsBackgroundNarrow->shadow.y());
|
||||
const auto faded = clip.intersected(inner);
|
||||
const auto bottom = _controlsBackgroundNarrow->shadowHeight;
|
||||
const auto faded = clip.intersected(inner).intersected(
|
||||
QRect(clip.x(), 0, clip.width(), bottom));
|
||||
if (!faded.isEmpty()) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
@@ -1889,9 +2071,8 @@ void Panel::setupControlsBackgroundNarrow() {
|
||||
full->width(),
|
||||
faded.height() * factor));
|
||||
}
|
||||
const auto bottom = inner.y() + inner.height();
|
||||
const auto after = clip.intersected(QRect(
|
||||
0,
|
||||
inner.x(),
|
||||
bottom,
|
||||
inner.width(),
|
||||
_controlsBackgroundNarrow->shadow.height() - bottom));
|
||||
@@ -2023,6 +2204,8 @@ void Panel::showNiceTooltip(
|
||||
return MuteButtonTooltip(_call);
|
||||
} else if (control == _hangup.data()) {
|
||||
return tr::lng_group_call_leave();
|
||||
} else if (control == _message.data()) {
|
||||
return tr::lng_group_call_message();
|
||||
}
|
||||
return rpl::producer<QString>();
|
||||
}();
|
||||
@@ -2159,6 +2342,7 @@ void Panel::trackControls(bool track, bool force) {
|
||||
};
|
||||
trackOne(_mute->outer());
|
||||
trackOne(_video);
|
||||
trackOne(_message);
|
||||
trackOne(_screenShare);
|
||||
trackOne(_wideMenu);
|
||||
trackOne(_settings);
|
||||
@@ -2216,8 +2400,11 @@ void Panel::updateButtonsGeometry() {
|
||||
widget->setVisible(shown);
|
||||
}
|
||||
};
|
||||
const auto messagesEnabled = _call->messagesEnabled();
|
||||
auto messagesBottomSkip = 0;
|
||||
if (mode() == PanelMode::Wide) {
|
||||
Assert(_video != nullptr);
|
||||
Assert(_message != nullptr);
|
||||
Assert(_screenShare != nullptr);
|
||||
Assert(_wideMenu != nullptr);
|
||||
Assert(_settings != nullptr);
|
||||
@@ -2232,31 +2419,74 @@ void Panel::updateButtonsGeometry() {
|
||||
_viewport->setControlsShown(rtmp ? 0. : shown);
|
||||
}
|
||||
|
||||
const auto buttonsTop = widget()->height() - anim::interpolate(
|
||||
0,
|
||||
st::groupCallButtonBottomSkipWide,
|
||||
shown);
|
||||
const auto addSkip = st::callMuteButtonSmall.active.outerRadius;
|
||||
const auto muteSize = _mute->innerSize().width() + 2 * addSkip;
|
||||
const auto skip = st::groupCallButtonSkipSmall;
|
||||
const auto fullWidth = (rtmp ? 0 : (_video->width() + skip))
|
||||
+ (rtmp ? 0 : (_screenShare->width() + skip))
|
||||
+ (rtmp ? 0 : (_message->width() + skip))
|
||||
+ (muteSize + skip)
|
||||
+ (_settings->width() + skip)
|
||||
+ _hangup->width();
|
||||
const auto membersSkip = st::groupCallNarrowSkip;
|
||||
const auto membersWidth = _call->rtmp()
|
||||
const auto membersWidth = rtmp
|
||||
? membersSkip
|
||||
: (st::groupCallNarrowMembersWidth + 2 * membersSkip);
|
||||
auto left = membersSkip + (widget()->width()
|
||||
- membersWidth
|
||||
- membersSkip
|
||||
- fullWidth) / 2;
|
||||
toggle(_screenShare, !hidden && !rtmp);
|
||||
if (!rtmp) {
|
||||
|
||||
const auto forMessagesLeft = left
|
||||
- st::groupCallControlsBackMargin.left();
|
||||
const auto forMessagesWidth = fullWidth
|
||||
+ st::groupCallControlsBackMargin.left()
|
||||
+ st::groupCallControlsBackMargin.right();
|
||||
const auto existingBottomSkip = st::groupCallButtonBottomSkipWide
|
||||
- _hangup->height()
|
||||
- st::groupCallControlsBackMargin.bottom();
|
||||
if (_messageField) {
|
||||
_messageField->resizeToWidth(forMessagesWidth);
|
||||
messagesBottomSkip += _messageField->height();
|
||||
|
||||
const auto y = widget()->height()
|
||||
- messagesBottomSkip
|
||||
- (existingBottomSkip / 2);
|
||||
_messageField->move(forMessagesLeft, y);
|
||||
}
|
||||
|
||||
const auto buttonsTop = widget()->height()
|
||||
- messagesBottomSkip
|
||||
- anim::interpolate(0, st::groupCallButtonBottomSkipWide, shown);
|
||||
const auto muteTop = buttonsTop + addSkip;
|
||||
|
||||
const auto forMessagesBottom = buttonsTop
|
||||
- st::groupCallControlsBackMargin.top()
|
||||
- (existingBottomSkip / 6);
|
||||
const auto forMessagesHeight = forMessagesBottom
|
||||
- (st::groupCallWideVideoTop * 1.5);
|
||||
|
||||
_messages->move(
|
||||
forMessagesLeft,
|
||||
forMessagesBottom,
|
||||
forMessagesWidth,
|
||||
forMessagesHeight);
|
||||
|
||||
toggle(_screenShare, !hidden && !rtmp && !messagesEnabled);
|
||||
toggle(_message, !hidden && !rtmp && messagesEnabled);
|
||||
if (!rtmp && !messagesEnabled) {
|
||||
_screenShare->moveToLeft(left, buttonsTop);
|
||||
left += _screenShare->width() + skip;
|
||||
} else if (!rtmp) {
|
||||
_wideMenu->moveToLeft(left, buttonsTop);
|
||||
_settings->moveToLeft(left, buttonsTop);
|
||||
left += _settings->width() + skip;
|
||||
}
|
||||
|
||||
const auto wideMenuShown = _call->canManage()
|
||||
|| _call->showChooseJoinAs();
|
||||
toggle(_settings, !hidden && !wideMenuShown);
|
||||
toggle(_wideMenu, !hidden && wideMenuShown);
|
||||
|
||||
toggle(_video, !hidden && !rtmp);
|
||||
if (!rtmp) {
|
||||
_video->moveToLeft(left, buttonsTop);
|
||||
@@ -2267,13 +2497,12 @@ void Panel::updateButtonsGeometry() {
|
||||
left += _settings->width() + skip;
|
||||
}
|
||||
toggle(_mute, !hidden);
|
||||
_mute->moveInner({ left + addSkip, buttonsTop + addSkip });
|
||||
_mute->moveInner({ left + addSkip, muteTop });
|
||||
left += muteSize + skip;
|
||||
const auto wideMenuShown = _call->canManage()
|
||||
|| _call->showChooseJoinAs();
|
||||
toggle(_settings, !hidden && !wideMenuShown);
|
||||
toggle(_wideMenu, !hidden && wideMenuShown);
|
||||
if (!rtmp) {
|
||||
if (!rtmp && messagesEnabled) {
|
||||
_message->moveToLeft(left, buttonsTop);
|
||||
left += _message->width() + skip;
|
||||
} else if (!rtmp) {
|
||||
_wideMenu->moveToLeft(left, buttonsTop);
|
||||
_settings->moveToLeft(left, buttonsTop);
|
||||
left += _settings->width() + skip;
|
||||
@@ -2294,33 +2523,91 @@ void Panel::updateButtonsGeometry() {
|
||||
refreshTitleGeometry();
|
||||
}
|
||||
} else {
|
||||
const auto muteTop = widget()->height()
|
||||
- st::groupCallMuteBottomSkip;
|
||||
const auto buttonsTop = widget()->height()
|
||||
- st::groupCallButtonBottomSkip;
|
||||
const auto addSkip = st::callMuteButton.active.outerRadius;
|
||||
const auto muteSize = _mute->innerSize().width();
|
||||
const auto fullWidth = muteSize
|
||||
+ 2 * (_settings ? _settings : _callShare)->width()
|
||||
+ 2 * st::groupCallButtonSkip;
|
||||
const auto single = (_settings ? _settings : _callShare)->width();
|
||||
const auto showVideoButton = videoButtonInNarrowMode();
|
||||
const auto four = !_callShare && !showVideoButton && messagesEnabled;
|
||||
const auto five = !four && !_callShare && messagesEnabled;
|
||||
const auto buttonSkip = four
|
||||
? (st::groupCallButtonSkip / 2)
|
||||
: five
|
||||
? ((st::groupCallWidth - 5 * single) / 6)
|
||||
: st::groupCallButtonSkip;
|
||||
const auto fullWidth = five
|
||||
? st::groupCallWidth
|
||||
: four
|
||||
? (4 * single + 3 * buttonSkip)
|
||||
: (muteSize + 2 * (single + st::groupCallButtonSkip));
|
||||
const auto forMessagesWidth = st::groupCallWidth
|
||||
- st::groupCallMembersMargin.left()
|
||||
- st::groupCallMembersMargin.right();
|
||||
const auto forMessagesLeft = (widget()->width() - forMessagesWidth)
|
||||
/ 2;
|
||||
const auto existingBottomSkip = st::groupCallButtonBottomSkip
|
||||
- _mute->innerSize().height();
|
||||
if (_messageField) {
|
||||
_messageField->resizeToWidth(forMessagesWidth);
|
||||
|
||||
const auto height = _messageField->height();
|
||||
messagesBottomSkip += height;
|
||||
|
||||
const auto y = widget()->height()
|
||||
- messagesBottomSkip
|
||||
- (existingBottomSkip / 3);
|
||||
_messageField->move(forMessagesLeft, y);
|
||||
}
|
||||
|
||||
const auto buttonsTop = widget()->height()
|
||||
- messagesBottomSkip
|
||||
- st::groupCallButtonBottomSkip;
|
||||
const auto muteTop = buttonsTop + addSkip;
|
||||
//const auto muteTop = widget()->height()
|
||||
// - messagesBottomSkip
|
||||
// - st::groupCallMuteBottomSkip;
|
||||
const auto forMessagesBottom = muteTop
|
||||
- (existingBottomSkip / 3);
|
||||
const auto forMessagesHeight = forMessagesBottom
|
||||
- st::groupCallMembersTop
|
||||
- (st::normalFont->height / 2);
|
||||
|
||||
_messages->move(
|
||||
forMessagesLeft,
|
||||
forMessagesBottom,
|
||||
forMessagesWidth,
|
||||
forMessagesHeight);
|
||||
|
||||
toggle(_mute, true);
|
||||
_mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop });
|
||||
const auto leftButtonLeft = (widget()->width() - fullWidth) / 2;
|
||||
const auto leftButtonLeft = (widget()->width() - fullWidth) / 2
|
||||
+ (five ? buttonSkip : 0);
|
||||
const auto nextButtonLeft = leftButtonLeft
|
||||
+ ((five || four) ? (single + buttonSkip) : 0);
|
||||
const auto muteButtonLeft = four
|
||||
? (nextButtonLeft + addSkip)
|
||||
: ((widget()->width() - muteSize) / 2);
|
||||
_mute->moveInner({ muteButtonLeft, muteTop });
|
||||
toggle(_screenShare, false);
|
||||
toggle(_wideMenu, false);
|
||||
toggle(_callShare, true);
|
||||
if (_callShare) {
|
||||
_callShare->moveToLeft(leftButtonLeft, buttonsTop);
|
||||
}
|
||||
const auto showVideoButton = videoButtonInNarrowMode();
|
||||
toggle(_video, !_callShare && showVideoButton);
|
||||
if (_video) {
|
||||
_video->setStyle(st::groupCallVideo, &st::groupCallVideoActive);
|
||||
_video->moveToLeft(leftButtonLeft, buttonsTop);
|
||||
}
|
||||
toggle(_settings, !_callShare && !showVideoButton);
|
||||
toggle(_settings, !_callShare && (five || !showVideoButton));
|
||||
if (_settings) {
|
||||
_settings->moveToLeft(leftButtonLeft, buttonsTop);
|
||||
_settings->moveToLeft(
|
||||
four ? leftButtonLeft : nextButtonLeft,
|
||||
buttonsTop);
|
||||
}
|
||||
toggle(_message, !_callShare && messagesEnabled);
|
||||
if (_message) {
|
||||
_message->moveToRight(nextButtonLeft, buttonsTop);
|
||||
}
|
||||
|
||||
toggle(_hangup, true);
|
||||
_hangup->moveToRight(leftButtonLeft, buttonsTop);
|
||||
}
|
||||
@@ -2332,17 +2619,19 @@ void Panel::updateButtonsGeometry() {
|
||||
_controlsBackgroundNarrow->shadow.setGeometry(
|
||||
left,
|
||||
(widget()->height()
|
||||
- messagesBottomSkip
|
||||
- st::groupCallMembersMargin.bottom()
|
||||
- _controlsBackgroundNarrow->shadow.height()),
|
||||
- _controlsBackgroundNarrow->shadowHeight),
|
||||
width,
|
||||
_controlsBackgroundNarrow->shadow.height());
|
||||
messagesBottomSkip + _controlsBackgroundNarrow->shadowHeight);
|
||||
_controlsBackgroundNarrow->blocker.setGeometry(
|
||||
left,
|
||||
(widget()->height()
|
||||
- messagesBottomSkip
|
||||
- st::groupCallMembersMargin.bottom()
|
||||
- st::groupCallMembersBottomSkip),
|
||||
width,
|
||||
st::groupCallMembersBottomSkip);
|
||||
messagesBottomSkip + st::groupCallMembersBottomSkip);
|
||||
}
|
||||
updateTooltipGeometry();
|
||||
}
|
||||
@@ -2619,6 +2908,10 @@ bool Panel::handleClose() {
|
||||
return false;
|
||||
}
|
||||
|
||||
not_null<Window*> Panel::callWindow() const {
|
||||
return _window.get();
|
||||
}
|
||||
|
||||
not_null<Ui::RpWindow*> Panel::window() const {
|
||||
return _window->window();
|
||||
}
|
||||
|
||||
@@ -16,21 +16,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
class Image;
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class SessionShow;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class LayerWidget;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
class AbstractButton;
|
||||
class ImportantTooltip;
|
||||
class DropdownMenu;
|
||||
@@ -47,6 +44,7 @@ class PaddingWrap;
|
||||
class ScrollArea;
|
||||
class GenericBox;
|
||||
class GroupCallScheduledLeft;
|
||||
struct CallButtonColors;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Toast {
|
||||
@@ -73,6 +71,8 @@ class Viewport;
|
||||
enum class PanelMode;
|
||||
enum class StickedTooltip;
|
||||
class MicLevelTester;
|
||||
class MessageField;
|
||||
class MessagesUi;
|
||||
|
||||
class Panel final
|
||||
: public base::has_weak_ptr
|
||||
@@ -97,9 +97,8 @@ public:
|
||||
void showAndActivate();
|
||||
void closeBeforeDestroy();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Main::SessionShow> sessionShow();
|
||||
[[nodiscard]] std::shared_ptr<Ui::Show> uiShow();
|
||||
|
||||
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
|
||||
[[nodiscard]] not_null<Window*> callWindow() const;
|
||||
[[nodiscard]] not_null<Ui::RpWindow*> window() const;
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
@@ -161,6 +160,7 @@ private:
|
||||
void setupControlsBackgroundWide();
|
||||
void setupControlsBackgroundNarrow();
|
||||
void showControls();
|
||||
void createMessageButton();
|
||||
void refreshLeftButton();
|
||||
void refreshVideoButtons(
|
||||
std::optional<bool> overrideWideMode = std::nullopt);
|
||||
@@ -171,6 +171,9 @@ private:
|
||||
void updateWideControlsVisibility();
|
||||
[[nodiscard]] bool videoButtonInNarrowMode() const;
|
||||
[[nodiscard]] Fn<void()> shareConferenceLinkCallback();
|
||||
void toggleMessageTyping();
|
||||
[[nodiscard]] rpl::producer<Ui::CallButtonColors> toggleableOverrides(
|
||||
rpl::producer<bool> active);
|
||||
|
||||
void endCall();
|
||||
|
||||
@@ -222,6 +225,7 @@ private:
|
||||
object_ptr<Ui::IconButton> _pinOnTop = { nullptr };
|
||||
object_ptr<Ui::DropdownMenu> _menu = { nullptr };
|
||||
rpl::variable<bool> _wideMenuShown = false;
|
||||
rpl::variable<bool> _messageTyping = false;
|
||||
object_ptr<Ui::AbstractButton> _joinAsToggle = { nullptr };
|
||||
object_ptr<Members> _members = { nullptr };
|
||||
std::unique_ptr<Viewport> _viewport;
|
||||
@@ -248,6 +252,7 @@ private:
|
||||
object_ptr<Ui::CallButton> _callShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _video = { nullptr };
|
||||
object_ptr<Ui::CallButton> _screenShare = { nullptr };
|
||||
object_ptr<Ui::CallButton> _message = { nullptr };
|
||||
std::unique_ptr<Ui::CallMuteButton> _mute;
|
||||
object_ptr<Ui::CallButton> _hangup;
|
||||
object_ptr<Ui::ImportantTooltip> _niceTooltip = { nullptr };
|
||||
@@ -256,6 +261,10 @@ private:
|
||||
StickedTooltips _stickedTooltipsShown;
|
||||
Fn<void()> _callShareLinkCallback;
|
||||
|
||||
std::shared_ptr<ChatHelpers::Show> _cachedShow;
|
||||
std::unique_ptr<MessageField> _messageField;
|
||||
std::unique_ptr<MessagesUi> _messages;
|
||||
|
||||
const std::unique_ptr<Toasts> _toasts;
|
||||
|
||||
std::unique_ptr<MicLevelTester> _micLevelTester;
|
||||
|
||||
@@ -80,11 +80,35 @@ void SaveCallJoinMuted(
|
||||
|| call->joinMuted() == joinMuted) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setJoinMutedLocally(joinMuted);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(MTPphone_ToggleGroupCallSettings::Flag::f_join_muted),
|
||||
MTP_flags(Flag::f_join_muted),
|
||||
call->input(),
|
||||
MTP_bool(joinMuted)
|
||||
MTP_bool(joinMuted),
|
||||
MTPBool() // messages_enabled
|
||||
)).send();
|
||||
}
|
||||
|
||||
void SaveCallMessagesEnabled(
|
||||
not_null<PeerData*> peer,
|
||||
CallId callId,
|
||||
bool messagesEnabled) {
|
||||
const auto call = peer->groupCall();
|
||||
if (!call
|
||||
|| call->id() != callId
|
||||
|| !peer->canManageGroupCall()
|
||||
|| !call->canChangeJoinMuted()
|
||||
|| call->messagesEnabled() == messagesEnabled) {
|
||||
return;
|
||||
}
|
||||
using Flag = MTPphone_ToggleGroupCallSettings::Flag;
|
||||
call->setMessagesEnabledLocally(messagesEnabled);
|
||||
peer->session().api().request(MTPphone_ToggleGroupCallSettings(
|
||||
MTP_flags(Flag::f_messages_enabled),
|
||||
call->input(),
|
||||
MTPBool(), // join_muted
|
||||
MTP_bool(messagesEnabled)
|
||||
)).send();
|
||||
}
|
||||
|
||||
@@ -233,7 +257,9 @@ void SettingsBox(
|
||||
};
|
||||
const auto peer = call->peer();
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
const auto real = peer->groupCall();
|
||||
const auto real = call->conference()
|
||||
? call->lookupReal()
|
||||
: peer->groupCall();
|
||||
const auto rtmp = call->rtmp();
|
||||
const auto id = call->id();
|
||||
const auto goodReal = (real && real->id() == id);
|
||||
@@ -241,11 +267,19 @@ void SettingsBox(
|
||||
const auto layout = box->verticalLayout();
|
||||
const auto &settings = Core::App().settings();
|
||||
|
||||
const auto joinMuted = goodReal ? real->joinMuted() : false;
|
||||
const auto joinMuted = !call->conference()
|
||||
&& goodReal
|
||||
&& real->joinMuted();
|
||||
const auto messagesEnabled = goodReal && real->messagesEnabled();
|
||||
const auto canChangeJoinMuted = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeJoinMuted();
|
||||
const auto addCheck = (peer->canManageGroupCall() && canChangeJoinMuted);
|
||||
const auto canChangeMessagesEnabled = !rtmp
|
||||
&& goodReal
|
||||
&& real->canChangeMessagesEnabled();
|
||||
const auto addCheck = canChangeJoinMuted && peer->canManageGroupCall();
|
||||
const auto addMessages = canChangeMessagesEnabled
|
||||
&& (call->conference() || peer->canManageGroupCall());
|
||||
|
||||
const auto addDivider = [&] {
|
||||
layout->add(object_ptr<Ui::BoxContentDivider>(
|
||||
@@ -254,7 +288,7 @@ void SettingsBox(
|
||||
st::groupCallDividerBg));
|
||||
};
|
||||
|
||||
if (addCheck) {
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
const auto muteJoined = addCheck
|
||||
@@ -263,7 +297,14 @@ void SettingsBox(
|
||||
tr::lng_group_call_new_muted(),
|
||||
st::groupCallSettingsButton))->toggleOn(rpl::single(joinMuted))
|
||||
: nullptr;
|
||||
if (addCheck) {
|
||||
const auto enableMessages = addMessages
|
||||
? layout->add(object_ptr<Ui::SettingsButton>(
|
||||
layout,
|
||||
tr::lng_group_call_enable_messages(),
|
||||
st::groupCallSettingsButton))->toggleOn(
|
||||
rpl::single(messagesEnabled))
|
||||
: nullptr;
|
||||
if (addCheck || addMessages) {
|
||||
Ui::AddSkip(layout);
|
||||
}
|
||||
|
||||
@@ -810,6 +851,16 @@ void SettingsBox(
|
||||
&& muteJoined->toggled() != joinMuted) {
|
||||
SaveCallJoinMuted(peer, id, muteJoined->toggled());
|
||||
}
|
||||
if (canChangeMessagesEnabled
|
||||
&& enableMessages
|
||||
&& enableMessages->toggled() != messagesEnabled) {
|
||||
const auto value = enableMessages->toggled();
|
||||
if (!call->conference()) {
|
||||
SaveCallMessagesEnabled(peer, id, value);
|
||||
} else if (const auto real = call->lookupReal()) {
|
||||
real->setMessagesEnabledLocally(value);
|
||||
}
|
||||
}
|
||||
}, box->lifetime());
|
||||
box->addButton(tr::lng_box_done(), [=] {
|
||||
box->closeBox();
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "calls/group/calls_group_call.h"
|
||||
#include "calls/group/calls_group_common.h"
|
||||
#include "calls/group/calls_group_panel.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "ui/layers/show.h"
|
||||
|
||||
@@ -420,6 +420,7 @@ emojiPanHover: windowBgOver;
|
||||
emojiPanSlideDuration: 200;
|
||||
emojiPanArea: size(34px, 32px);
|
||||
emojiPanRadius: 8px;
|
||||
emojiPanReactionsPreviewPadding: margins(10px, 20px, 10px, 20px);
|
||||
|
||||
defaultTabbedSearchCancel: CrossButton {
|
||||
width: 33px;
|
||||
@@ -814,6 +815,15 @@ reactPanelScrollRounded: ScrollArea(emojiScroll) {
|
||||
deltat: 14px;
|
||||
deltab: 14px;
|
||||
}
|
||||
selfForwardsTaggerStripSkip: 26px;
|
||||
selfForwardsTaggerIcon: size(32px, 32px);
|
||||
selfForwardsTaggerToast: Toast(defaultToast) {
|
||||
minWidth: 160px;
|
||||
maxWidth: 380px;
|
||||
radius: 9px;
|
||||
padding: margins(54px, 12px, 19px, 12px);
|
||||
iconPosition: point(15px, 6px);
|
||||
}
|
||||
|
||||
choosePeerGroupIcon: icon {{ "info/edit/create_group", lightButtonFg }};
|
||||
choosePeerChannelIcon: icon {{ "info/edit/create_channel", lightButtonFg }};
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "chat_helpers/emoji_list_widget.h"
|
||||
|
||||
#include "window/window_media_preview.h"
|
||||
#include "api/api_peer_photo.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
@@ -50,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "core/application.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
@@ -480,6 +482,8 @@ EmojiListWidget::EmojiListWidget(
|
||||
, _features(descriptor.features)
|
||||
, _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle)
|
||||
, _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode)
|
||||
, _mediaPreviewParent(descriptor.mediaPreviewParent)
|
||||
, _mediaPreviewMargins(descriptor.mediaPreviewMargins)
|
||||
, _api(&session().mtp())
|
||||
, _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1)
|
||||
, _premiumIcon(_mode == Mode::EmojiStatus
|
||||
@@ -677,13 +681,45 @@ void EmojiListWidget::applyNextSearchQuery() {
|
||||
void EmojiListWidget::showPreview() {
|
||||
if (const auto over = std::get_if<OverEmoji>(&_pressed)) {
|
||||
if (const auto custom = lookupCustomEmoji(over)) {
|
||||
const auto document = custom.document;
|
||||
_show->showMediaPreview(document->stickerSetOrigin(), document);
|
||||
showPreviewFor(custom.document);
|
||||
_previewShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmojiListWidget::showPreviewFor(not_null<DocumentData*> document) {
|
||||
if ((_mode == Mode::FullReactions || _mode == Mode::RecentReactions)
|
||||
&& _mediaPreviewParent) {
|
||||
ensureMediaPreview();
|
||||
_mediaPreview->showPreview(document->stickerSetOrigin(), document);
|
||||
} else {
|
||||
_show->showMediaPreview(document->stickerSetOrigin(), document);
|
||||
}
|
||||
}
|
||||
|
||||
void EmojiListWidget::ensureMediaPreview() {
|
||||
if (!_mediaPreviewParent) {
|
||||
return;
|
||||
}
|
||||
if (_mediaPreview) {
|
||||
_mediaPreview->raise();
|
||||
return;
|
||||
}
|
||||
const auto controller = Core::App().findWindow(_show->toastParent());
|
||||
const auto sessionController = controller
|
||||
? controller->sessionController()
|
||||
: nullptr;
|
||||
if (sessionController) {
|
||||
_mediaPreview.create(_mediaPreviewParent, sessionController);
|
||||
_mediaPreview->setCustomPadding(st::emojiPanReactionsPreviewPadding);
|
||||
_mediaPreview->setBackgroundMargins(_mediaPreviewMargins);
|
||||
_mediaPreview->setCornersSkip(st::emojiPanRadius - st::lineWidth);
|
||||
_mediaPreview->show();
|
||||
_mediaPreview->setGeometry(_mediaPreviewParent->geometry());
|
||||
_mediaPreview->raise();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EmojiPtr> EmojiListWidget::collectPlainSearchResults() {
|
||||
return SearchEmoji(_searchQuery, _searchEmoji);
|
||||
}
|
||||
@@ -1763,6 +1799,9 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
if (_previewShown) {
|
||||
if (_mediaPreview) {
|
||||
_mediaPreview->hidePreview();
|
||||
}
|
||||
_previewShown = false;
|
||||
return;
|
||||
} else if (v::is_null(_selected) || _selected != pressed) {
|
||||
@@ -2739,11 +2778,8 @@ void EmojiListWidget::setSelected(OverState newSelected) {
|
||||
} else if (_previewShown && _pressed != _selected) {
|
||||
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
|
||||
if (const auto custom = lookupCustomEmoji(over)) {
|
||||
const auto document = custom.document;
|
||||
_pressed = _selected;
|
||||
_show->showMediaPreview(
|
||||
document->stickerSetOrigin(),
|
||||
document);
|
||||
showPreviewFor(custom.document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ struct RepaintRequest;
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
class MediaPreviewWidget;
|
||||
} // namespace Window
|
||||
|
||||
namespace ChatHelpers {
|
||||
@@ -98,6 +99,8 @@ struct EmojiListDescriptor {
|
||||
base::flat_set<DocumentId> freeEffects;
|
||||
const style::EmojiPan *st = nullptr;
|
||||
ComposeFeatures features;
|
||||
QWidget *mediaPreviewParent = nullptr;
|
||||
QMargins mediaPreviewMargins;
|
||||
};
|
||||
|
||||
class EmojiListWidget final
|
||||
@@ -399,6 +402,8 @@ private:
|
||||
uint64 setId);
|
||||
|
||||
void showPreview();
|
||||
void showPreviewFor(not_null<DocumentData*> document);
|
||||
void ensureMediaPreview();
|
||||
|
||||
void applyNextSearchQuery();
|
||||
|
||||
@@ -406,6 +411,8 @@ private:
|
||||
const ComposeFeatures _features;
|
||||
const bool _onlyUnicodeEmoji;
|
||||
Mode _mode = Mode::Full;
|
||||
QWidget *_mediaPreviewParent = nullptr;
|
||||
QMargins _mediaPreviewMargins;
|
||||
std::unique_ptr<Ui::TabbedSearch> _search;
|
||||
MTP::Sender _api;
|
||||
const int _staticCount = 0;
|
||||
@@ -479,6 +486,9 @@ private:
|
||||
base::Timer _previewTimer;
|
||||
bool _previewShown = false;
|
||||
|
||||
|
||||
object_ptr<Window::MediaPreviewWidget> _mediaPreview = { nullptr };
|
||||
|
||||
rpl::event_stream<EmojiChosen> _chosen;
|
||||
rpl::event_stream<FileChosen> _customChosen;
|
||||
rpl::event_stream<> _jumpedToPremium;
|
||||
|
||||
@@ -576,6 +576,7 @@ void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
|
||||
st::historySendSize.height() - 2 * st::historySendPadding);
|
||||
field->setMaxHeight(st::historyComposeFieldMaxHeight);
|
||||
|
||||
// st::messageSendingAnimationTextFromOffset.
|
||||
field->setDocumentMargin(4.);
|
||||
field->setAdditionalMargin(style::ConvertScale(4) - 4);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ rpl::producer<TextWithEntities> ForwardedMessagePhrase(
|
||||
Assert(args.to1);
|
||||
|
||||
if (args.to1->isSelf()) {
|
||||
if (args.toSelfWithPremiumIsEmpty && args.to1->isPremium()) {
|
||||
return {};
|
||||
}
|
||||
return (args.singleMessage
|
||||
? tr::lng_share_message_to_saved_messages
|
||||
: tr::lng_share_messages_to_saved_messages)(
|
||||
|
||||
@@ -16,6 +16,7 @@ struct ForwardedMessagePhraseArgs final {
|
||||
bool singleMessage = false;
|
||||
PeerData *to1 = nullptr;
|
||||
PeerData *to2 = nullptr;
|
||||
bool toSelfWithPremiumIsEmpty = true;
|
||||
};
|
||||
|
||||
rpl::producer<TextWithEntities> ForwardedMessagePhrase(
|
||||
|
||||
@@ -1392,7 +1392,7 @@ Window::Controller *Application::windowForShowingHistory(
|
||||
|
||||
Window::Controller *Application::windowForShowingForum(
|
||||
not_null<Data::Forum*> forum) const {
|
||||
const auto tabs = forum->channel()->useSubsectionTabs();
|
||||
const auto tabs = forum->bot() || forum->peer()->useSubsectionTabs();
|
||||
const auto id = Window::SeparateId(
|
||||
tabs ? Window::SeparateType::Chat : Window::SeparateType::Forum,
|
||||
forum->history());
|
||||
|
||||
@@ -241,7 +241,8 @@ QByteArray Settings::serialize() const {
|
||||
+ sizeof(qint32) * 3
|
||||
+ Serialize::bytearraySize(_tonsiteStorageToken)
|
||||
+ sizeof(qint32) * 8
|
||||
+ sizeof(ushort);
|
||||
+ sizeof(ushort)
|
||||
+ sizeof(qint32); // _notificationsDisplayChecksum
|
||||
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
@@ -404,7 +405,8 @@ QByteArray Settings::serialize() const {
|
||||
<< qint32(_ivZoom.current())
|
||||
<< qint32(_systemDarkModeEnabled.current() ? 1 : 0)
|
||||
<< qint32(_quickDialogAction)
|
||||
<< _notificationsVolume;
|
||||
<< _notificationsVolume
|
||||
<< _notificationsDisplayChecksum;
|
||||
}
|
||||
|
||||
Ensures(result.size() == size);
|
||||
@@ -435,6 +437,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
qint32 nativeNotifications = _nativeNotifications ? (*_nativeNotifications ? 1 : 2) : 0;
|
||||
qint32 notificationsCount = _notificationsCount;
|
||||
qint32 notificationsCorner = static_cast<qint32>(_notificationsCorner);
|
||||
qint32 notificationsDisplayChecksum = _notificationsDisplayChecksum;
|
||||
qint32 autoLock = _autoLock;
|
||||
QString playbackDeviceId = _playbackDeviceId.current();
|
||||
QString captureDeviceId = _captureDeviceId.current();
|
||||
@@ -869,6 +872,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
if (!stream.atEnd()) {
|
||||
stream >> notificationsVolume;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> notificationsDisplayChecksum;
|
||||
}
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
LOG(("App Error: "
|
||||
"Bad data for Core::Settings::constructFromSerialized()"));
|
||||
@@ -908,6 +914,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
case ScreenCorner::BottomRight:
|
||||
case ScreenCorner::BottomLeft: _notificationsCorner = uncheckedNotificationsCorner; break;
|
||||
}
|
||||
_notificationsDisplayChecksum = notificationsDisplayChecksum;
|
||||
_includeMutedCounter = (includeMutedCounter == 1);
|
||||
_includeMutedCounterFolders = (includeMutedCounterFolders == 1);
|
||||
_countUnreadMessages = (countUnreadMessages == 1);
|
||||
|
||||
@@ -241,6 +241,12 @@ public:
|
||||
void setNotificationsCorner(ScreenCorner corner) {
|
||||
_notificationsCorner = corner;
|
||||
}
|
||||
[[nodiscard]] int32 notificationsDisplayChecksum() const {
|
||||
return _notificationsDisplayChecksum;
|
||||
}
|
||||
void setNotificationsDisplayChecksum(int32 checksum) {
|
||||
_notificationsDisplayChecksum = checksum;
|
||||
}
|
||||
[[nodiscard]] bool includeMutedCounter() const {
|
||||
return _includeMutedCounter;
|
||||
}
|
||||
@@ -988,6 +994,7 @@ private:
|
||||
bool _skipToastsInFocus = false;
|
||||
int _notificationsCount = 3;
|
||||
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
|
||||
int32 _notificationsDisplayChecksum = 0;
|
||||
bool _includeMutedCounter = true;
|
||||
bool _includeMutedCounterFolders = true;
|
||||
bool _countUnreadMessages = true;
|
||||
|
||||
@@ -72,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session_settings.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "inline_bots/bot_attach_web_view.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
@@ -928,6 +929,58 @@ bool ShowEditBirthday(
|
||||
} else if (controller->showFrozenError()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto captured = match->captured(1);
|
||||
if (captured.startsWith(u":suggest:"_q)) {
|
||||
const auto userIdStr = captured.mid(9); // Skip ":suggest:"
|
||||
const auto userId = UserId(userIdStr.toULongLong());
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto targetUser
|
||||
= controller->session().data().userLoaded(userId);
|
||||
if (!targetUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto save = [=](Data::Birthday result) {
|
||||
using BFlag = MTPDbirthday::Flag;
|
||||
controller->session().api().request(MTPusers_SuggestBirthday(
|
||||
targetUser->inputUser,
|
||||
MTP_birthday(
|
||||
MTP_flags(result.year() ? BFlag::f_year : BFlag()),
|
||||
MTP_int(result.day()),
|
||||
MTP_int(result.month()),
|
||||
MTP_int(result.year()))
|
||||
)).done(crl::guard(controller, [=] {
|
||||
controller->showPeerHistory(targetUser);
|
||||
controller->showToast(
|
||||
tr::lng_settings_birthday_suggested(
|
||||
tr::now,
|
||||
lt_user,
|
||||
targetUser->name()));
|
||||
})).fail(crl::guard(controller, [=](const MTP::Error &error) {
|
||||
const auto type = error.type();
|
||||
controller->showToast(type.startsWith(u"FLOOD_WAIT_"_q)
|
||||
? tr::lng_flood_error(tr::now)
|
||||
: (u"Error: "_q + error.type()));
|
||||
})).handleFloodErrors().send();
|
||||
};
|
||||
|
||||
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
box->setTitle(tr::lng_suggest_birthday_box_title(
|
||||
lt_user,
|
||||
Info::Profile::NameValue(targetUser)));
|
||||
Ui::EditBirthdayBox(
|
||||
box,
|
||||
Data::Birthday(),
|
||||
save,
|
||||
Ui::EditBirthdayType::Suggest);
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto user = controller->session().user();
|
||||
const auto save = [=](Data::Birthday result) {
|
||||
user->setBirthday(result);
|
||||
@@ -950,8 +1003,20 @@ bool ShowEditBirthday(
|
||||
: (u"Error: "_q + error.type()));
|
||||
})).handleFloodErrors().send();
|
||||
};
|
||||
if (match->captured(1).isEmpty()) {
|
||||
controller->show(Box(Ui::EditBirthdayBox, user->birthday(), save));
|
||||
if (captured.startsWith(u":suggestion_"_q)) {
|
||||
const auto suggested = Data::Birthday::FromSerialized(
|
||||
captured.mid(u":suggestion_"_q.size()).toInt());
|
||||
controller->show(Box(
|
||||
Ui::EditBirthdayBox,
|
||||
suggested,
|
||||
save,
|
||||
Ui::EditBirthdayType::ConfirmSuggestion));
|
||||
} else if (captured.isEmpty()) {
|
||||
controller->show(Box(
|
||||
Ui::EditBirthdayBox,
|
||||
user->birthday(),
|
||||
save,
|
||||
Ui::EditBirthdayType::Edit));
|
||||
} else {
|
||||
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
Ui::EditBirthdayBox(box, user->birthday(), save);
|
||||
|
||||
@@ -810,7 +810,9 @@ const QStringList &Errors() {
|
||||
}
|
||||
|
||||
bool MarkChatSwitchKeyPressHandled(Qt::Key key, bool handled) {
|
||||
if (handled) {
|
||||
if (!key) {
|
||||
return false;
|
||||
} else if (handled) {
|
||||
if (ranges::contains(ChatSwitchKeyPressHandled, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 6001002;
|
||||
constexpr auto AppVersionStr = "6.1.2";
|
||||
constexpr auto AppVersion = 6002003;
|
||||
constexpr auto AppVersionStr = "6.2.3";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -65,9 +65,9 @@ void RecentSharedMediaGifts::request(
|
||||
auto conter = 0;
|
||||
for (const auto &gift : data.vgifts().v) {
|
||||
if (auto parsed = Api::FromTL(peer, gift)) {
|
||||
entry.ids.push_front(parsed->info.document->id);
|
||||
entry.ids.push_back(parsed->info.document->id);
|
||||
if (entry.ids.size() > kMaxGifts) {
|
||||
entry.ids.pop_back();
|
||||
entry.ids.pop_front();
|
||||
}
|
||||
if (++conter >= kMaxGifts) {
|
||||
break;
|
||||
|
||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_media_preload.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
@@ -55,6 +56,13 @@ template <typename Fields>
|
||||
SponsoredMessages::SponsoredMessages(not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _clearTimer([=] { clearOldRequests(); }) {
|
||||
Data::AmPremiumValue(
|
||||
_session
|
||||
) | rpl::start_with_next([=](bool premium) {
|
||||
if (premium) {
|
||||
clear();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
SponsoredMessages::~SponsoredMessages() {
|
||||
@@ -535,12 +543,8 @@ void SponsoredMessages::append(
|
||||
: PhotoId(0),
|
||||
.mediaPhotoId = (mediaPhoto ? mediaPhoto->id : 0),
|
||||
.mediaDocumentId = (mediaDocument ? mediaDocument->id : 0),
|
||||
.backgroundEmojiId = data.vcolor().has_value()
|
||||
? data.vcolor()->data().vbackground_emoji_id().value_or_empty()
|
||||
: uint64(0),
|
||||
.colorIndex = uint8(data.vcolor().has_value()
|
||||
? data.vcolor()->data().vcolor().value_or_empty()
|
||||
: 0),
|
||||
.backgroundEmojiId = BackgroundEmojiIdFromColor(data.vcolor()),
|
||||
.colorIndex = ColorIndexFromColor(data.vcolor()),
|
||||
.isLinkInternal = !UrlRequiresConfirmation(qs(data.vurl())),
|
||||
.isRecommended = data.is_recommended(),
|
||||
.canReport = data.is_can_report(),
|
||||
|
||||
@@ -100,28 +100,29 @@ struct PeerUpdate {
|
||||
PaysPerMessage = (1ULL << 36),
|
||||
GiftSettings = (1ULL << 37),
|
||||
StarsRating = (1ULL << 38),
|
||||
ContactNote = (1ULL << 39),
|
||||
|
||||
// For chats and channels
|
||||
InviteLinks = (1ULL << 39),
|
||||
Members = (1ULL << 40),
|
||||
Admins = (1ULL << 41),
|
||||
BannedUsers = (1ULL << 42),
|
||||
Rights = (1ULL << 43),
|
||||
PendingRequests = (1ULL << 44),
|
||||
Reactions = (1ULL << 45),
|
||||
InviteLinks = (1ULL << 40),
|
||||
Members = (1ULL << 41),
|
||||
Admins = (1ULL << 42),
|
||||
BannedUsers = (1ULL << 43),
|
||||
Rights = (1ULL << 44),
|
||||
PendingRequests = (1ULL << 45),
|
||||
Reactions = (1ULL << 46),
|
||||
|
||||
// For channels
|
||||
ChannelAmIn = (1ULL << 46),
|
||||
StickersSet = (1ULL << 47),
|
||||
EmojiSet = (1ULL << 48),
|
||||
DiscussionLink = (1ULL << 49),
|
||||
MonoforumLink = (1ULL << 50),
|
||||
ChannelLocation = (1ULL << 51),
|
||||
Slowmode = (1ULL << 52),
|
||||
GroupCall = (1ULL << 53),
|
||||
ChannelAmIn = (1ULL << 47),
|
||||
StickersSet = (1ULL << 48),
|
||||
EmojiSet = (1ULL << 49),
|
||||
DiscussionLink = (1ULL << 50),
|
||||
MonoforumLink = (1ULL << 51),
|
||||
ChannelLocation = (1ULL << 52),
|
||||
Slowmode = (1ULL << 53),
|
||||
GroupCall = (1ULL << 54),
|
||||
|
||||
// For iteration
|
||||
LastUsedBit = (1ULL << 53),
|
||||
LastUsedBit = (1ULL << 54),
|
||||
};
|
||||
using Flags = base::flags<Flag>;
|
||||
friend inline constexpr auto is_flag_type(Flag) { return true; }
|
||||
|
||||
@@ -1182,12 +1182,6 @@ void ChannelData::setStoriesState(StoriesState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelData::processTopics(const MTPVector<MTPForumTopic> &topics) {
|
||||
if (const auto forum = this->forum()) {
|
||||
forum->applyReceivedTopics(topics);
|
||||
}
|
||||
}
|
||||
|
||||
int ChannelData::levelHint() const {
|
||||
return _levelHint;
|
||||
}
|
||||
|
||||
@@ -545,8 +545,6 @@ public:
|
||||
return mgInfo ? mgInfo->monoforum() : nullptr;
|
||||
}
|
||||
|
||||
void processTopics(const MTPVector<MTPForumTopic> &topics);
|
||||
|
||||
[[nodiscard]] int levelHint() const;
|
||||
void updateLevelHint(int levelHint);
|
||||
|
||||
|
||||
@@ -513,7 +513,7 @@ void CloudThemes::myGiftThemesLoadMore(bool reload) {
|
||||
}
|
||||
_myGiftThemesRequestId = _session->api().request(
|
||||
MTPaccount_GetUniqueGiftChatThemes(
|
||||
MTP_int(reload ? 0 : _myGiftThemesTokens.size()),
|
||||
MTP_string(reload ? QString() : _myGiftThemesNextOffset),
|
||||
MTP_int(kGiftThemesLimit),
|
||||
MTP_long(_myGiftThemesHash))
|
||||
).done([=](const MTPaccount_ChatThemes &result) {
|
||||
@@ -536,7 +536,11 @@ void CloudThemes::myGiftThemesLoadMore(bool reload) {
|
||||
processGiftThemeGetToken(data));
|
||||
});
|
||||
}
|
||||
_myGiftThemesLoaded = (got < kGiftThemesLimit);
|
||||
if (const auto next = data.vnext_offset()) {
|
||||
_myGiftThemesNextOffset = qs(*next);
|
||||
} else {
|
||||
_myGiftThemesLoaded = true;
|
||||
}
|
||||
_myGiftThemesUpdates.fire({});
|
||||
}, [&](const MTPDaccount_chatThemesNotModified &) {
|
||||
if (!reload) {
|
||||
|
||||
@@ -160,6 +160,7 @@ private:
|
||||
std::vector<QString> _myGiftThemesTokens;
|
||||
rpl::event_stream<> _myGiftThemesUpdates;
|
||||
uint64 _myGiftThemesHash = 0;
|
||||
QString _myGiftThemesNextOffset;
|
||||
bool _myGiftThemesLoaded = false;
|
||||
|
||||
base::Timer _reloadCurrentTimer;
|
||||
|
||||
@@ -66,6 +66,7 @@ struct CreditsHistoryEntry final {
|
||||
uint64 bareGiveawayMsgId = 0;
|
||||
uint64 bareGiftStickerId = 0;
|
||||
uint64 bareGiftOwnerId = 0;
|
||||
uint64 bareGiftHostId = 0;
|
||||
uint64 bareGiftReleasedById = 0;
|
||||
uint64 bareGiftResaleRecipientId = 0;
|
||||
uint64 bareActorId = 0;
|
||||
@@ -97,6 +98,7 @@ struct CreditsHistoryEntry final {
|
||||
int starsConverted = 0;
|
||||
int starsToUpgrade = 0;
|
||||
int starsUpgradedBySender = 0;
|
||||
int starsForDetailsRemove = 0;
|
||||
int premiumMonthsForStars = 0;
|
||||
int floodSkip = 0;
|
||||
bool converted : 1 = false;
|
||||
|
||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_forum_icons.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_replies_list.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/notify/data_notify_settings.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
@@ -48,12 +49,13 @@ constexpr auto kShowTopicNamesCount = 8;
|
||||
Forum::Forum(not_null<History*> history)
|
||||
: _history(history)
|
||||
, _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) {
|
||||
Expects(_history->peer->isChannel());
|
||||
Expects(_history->peer->isChannel()
|
||||
|| _history->peer->isBot());
|
||||
|
||||
if (_history->inChatList()) {
|
||||
preloadTopics();
|
||||
}
|
||||
if (channel()->canCreateTopics()) {
|
||||
if (peer()->canCreateTopics()) {
|
||||
owner().forumIcons().requestDefaultIfUnknown();
|
||||
}
|
||||
}
|
||||
@@ -98,7 +100,15 @@ not_null<History*> Forum::history() const {
|
||||
return _history;
|
||||
}
|
||||
|
||||
not_null<ChannelData*> Forum::channel() const {
|
||||
not_null<PeerData*> Forum::peer() const {
|
||||
return _history->peer;
|
||||
}
|
||||
|
||||
UserData *Forum::bot() const {
|
||||
return _history->peer->asBot();
|
||||
}
|
||||
|
||||
ChannelData *Forum::channel() const {
|
||||
return _history->peer->asChannel();
|
||||
}
|
||||
|
||||
@@ -107,6 +117,14 @@ not_null<Dialogs::MainList*> Forum::topicsList() {
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::destroyed() const {
|
||||
if (const auto bot = this->bot()) {
|
||||
return bot->flagsValue(
|
||||
) | rpl::filter([=](const UserData::Flags::Change &update) {
|
||||
using Flag = UserData::Flag;
|
||||
return (update.diff & Flag::Forum)
|
||||
&& !(update.value & Flag::Forum);
|
||||
}) | rpl::take(1) | rpl::to_empty;
|
||||
}
|
||||
return channel()->flagsValue(
|
||||
) | rpl::filter([=](const ChannelData::Flags::Change &update) {
|
||||
using Flag = ChannelData::Flag;
|
||||
@@ -142,9 +160,9 @@ void Forum::requestTopics() {
|
||||
}
|
||||
const auto firstLoad = !_offset.date;
|
||||
const auto loadCount = firstLoad ? kTopicsFirstLoad : kTopicsPerPage;
|
||||
_requestId = session().api().request(MTPchannels_GetForumTopics(
|
||||
_requestId = session().api().request(MTPmessages_GetForumTopics(
|
||||
MTP_flags(0),
|
||||
channel()->inputChannel,
|
||||
peer()->input,
|
||||
MTPstring(), // q
|
||||
MTP_int(_offset.date),
|
||||
MTP_int(_offset.id),
|
||||
@@ -169,7 +187,7 @@ void Forum::requestTopics() {
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
_requestId = 0;
|
||||
_topicsList.setLoaded();
|
||||
if (error.type() == u"CHANNEL_FORUM_MISSING"_q) {
|
||||
if (error.type() == u"CHANNEL_FORUM_MISSING"_q && channel()) {
|
||||
const auto flags = channel()->flags() & ~ChannelDataFlag::Forum;
|
||||
channel()->setFlags(flags);
|
||||
}
|
||||
@@ -280,7 +298,7 @@ Thread *Forum::activeSubsectionThread() const {
|
||||
}
|
||||
|
||||
void Forum::markUnreadCountsUnknown(MsgId readTillId) {
|
||||
if (!channel()->useSubsectionTabs()) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
@@ -294,7 +312,7 @@ void Forum::markUnreadCountsUnknown(MsgId readTillId) {
|
||||
void Forum::updateUnreadCounts(
|
||||
MsgId readTillId,
|
||||
const base::flat_map<not_null<ForumTopic*>, int> &counts) {
|
||||
if (!channel()->useSubsectionTabs()) {
|
||||
if (!peer()->useSubsectionTabs()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[rootId, topic] : _topics) {
|
||||
@@ -331,7 +349,9 @@ void Forum::applyReceivedTopics(
|
||||
owner().processUsers(data.vusers());
|
||||
owner().processChats(data.vchats());
|
||||
owner().processMessages(data.vmessages(), NewMessageType::Existing);
|
||||
channel()->ptsReceived(data.vpts().v);
|
||||
if (const auto channel = this->channel()) {
|
||||
channel->ptsReceived(data.vpts().v);
|
||||
}
|
||||
applyReceivedTopics(data.vtopics(), std::move(callback));
|
||||
if (!_staleRootIds.empty()) {
|
||||
requestSomeStale();
|
||||
@@ -404,8 +424,8 @@ void Forum::requestSomeStale() {
|
||||
_staleRequestId = histories.sendRequest(_history, type, [=](
|
||||
Fn<void()> finish) {
|
||||
return session().api().request(
|
||||
MTPchannels_GetForumTopicsByID(
|
||||
channel()->inputChannel,
|
||||
MTPmessages_GetForumTopicsByID(
|
||||
peer()->input,
|
||||
MTP_vector<MTPint>(rootIds))
|
||||
).done([=](const MTPmessages_ForumTopics &result) {
|
||||
_staleRequestId = 0;
|
||||
@@ -439,7 +459,7 @@ void Forum::requestTopic(MsgId rootId, Fn<void()> done) {
|
||||
if (!request.id
|
||||
&& _staleRootIds.emplace(rootId).second
|
||||
&& (_staleRootIds.size() == 1)) {
|
||||
crl::on_main(&session(), [peer = channel()] {
|
||||
crl::on_main(&session(), [peer = peer()] {
|
||||
if (const auto forum = peer->forum()) {
|
||||
forum->requestSomeStale();
|
||||
}
|
||||
@@ -569,7 +589,8 @@ ForumTopic *Forum::enforceTopicFor(MsgId rootId) {
|
||||
}
|
||||
|
||||
bool Forum::topicDeleted(MsgId rootId) const {
|
||||
return _topicsDeleted.contains(rootId);
|
||||
return _topicsDeleted.contains(rootId)
|
||||
|| (rootId == ForumTopic::kGeneralId && peer()->isBot());
|
||||
}
|
||||
|
||||
rpl::producer<> Forum::chatsListChanges() const {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user