Compare commits

...

150 Commits

Author SHA1 Message Date
John Preston
8c3c8f888d Version 5.7: Fix build with Xcode. 2024-10-31 21:35:51 +04:00
John Preston
a75d7f0381 Version 5.7.
- Sending video messages.
- New video quality selection in channels.
- Adding media to sent text messages via Edit.
- Ads in bots with revenue sharing with bot developers.
- Chat-specific hashtags.
2024-10-31 21:02:22 +04:00
John Preston
7684466acf Fix crash in quality auto-toggle. 2024-10-31 20:33:16 +04:00
John Preston
0067245739 Send more viewport_changed, just in case. 2024-10-31 20:33:15 +04:00
John Preston
4a5d8aa217 Fix build with MSVC. 2024-10-31 20:33:15 +04:00
John Preston
2d786aa02c Fix a typo in phrases. 2024-10-31 20:33:15 +04:00
Ilya Fedin
b0933b96ef Add error message for webview unsupported display server 2024-10-31 19:22:37 +04:00
Ilya Fedin
20fb73b626 Add error message for webview without OpenGL 2024-10-31 19:22:37 +04:00
Ilya Fedin
ae7bd7112b Stop recommending webkitgtk-6.0 2024-10-31 19:22:37 +04:00
Ilya Fedin
2365363dcc webkitgtk-6.0 -> webkit2gtk-4.1 in snap 2024-10-31 19:22:37 +04:00
Ilya Fedin
e0e4a7bec6 Always rely on __has_include(<winres.h>) 2024-10-31 19:22:37 +04:00
Ilya Fedin
ebd0c3696a Remove #ifndef __MINGW32__ from Windows notifications
It uses C++/WinRT nowadays just like all other code so there's no point in #ifndef'ing only it
2024-10-31 19:22:37 +04:00
Ilya Fedin
e0a0d9c039 Remove outdated ifdef
The file doesn't have __in anymore
2024-10-31 19:22:37 +04:00
Ilya Fedin
dae9f2ab2b Replace CMAKE_CXX_COMPILER_FRONTEND_VARIANT check with MSVC variable 2024-10-31 19:22:37 +04:00
Ilya Fedin
7168a00ee4 Update submodules 2024-10-31 19:22:37 +04:00
John Preston
1e2d0ced20 Update submodules. 2024-10-31 19:22:23 +04:00
John Preston
d6ac883efa Implement adaptive quality selection. 2024-10-31 18:04:17 +04:00
John Preston
a386d70ae4 Track qualities availability correctly. 2024-10-31 18:04:17 +04:00
John Preston
bc8bf672b4 Don't use shared video cover in viewer. 2024-10-31 18:04:17 +04:00
John Preston
e38998214f Disable reschedule/edit-media for processing. 2024-10-31 18:04:17 +04:00
John Preston
472a2fe802 Position ttl button correctly on record finish. 2024-10-31 18:04:17 +04:00
John Preston
2d6d89b1cf Fix recording single-listen tooltip text. 2024-10-31 18:04:17 +04:00
John Preston
66be2ac6ca Show toast/tooltip info on video processing. 2024-10-31 18:04:17 +04:00
John Preston
3137c9f3f7 Jump-to-scheduled on video processing. 2024-10-31 18:04:17 +04:00
John Preston
b9ebb02e72 Update API scheme on layer 192. 2024-10-31 18:04:17 +04:00
John Preston
1e02c475d6 Show speed / quality badges. 2024-10-31 18:04:16 +04:00
John Preston
4d2cda0692 Fix menu in case speed isn't controlled. 2024-10-31 18:04:16 +04:00
John Preston
cbd2b8f428 Query recording availability in the background.
Fixes #28576.
2024-10-31 18:04:16 +04:00
John Preston
93605db690 Use custom toast for video-not-available state. 2024-10-31 18:04:16 +04:00
John Preston
2567096de0 Send correct action on round recording. 2024-10-31 18:04:16 +04:00
John Preston
6ed25d012f Use new settings icon in the player. 2024-10-31 18:04:16 +04:00
John Preston
c2afef2bde Improve changing of video playback quality. 2024-10-31 18:04:16 +04:00
John Preston
0991e7d8a4 Mirror recorded round message. 2024-10-31 18:04:16 +04:00
John Preston
cf52f2a743 Improve multi-line menu item items. 2024-10-31 18:04:16 +04:00
John Preston
37dddda1a0 Save preferred video quality to settings. 2024-10-31 18:04:16 +04:00
John Preston
3f2f3ebd51 Allow switching video quality. 2024-10-31 18:04:16 +04:00
John Preston
7ba78540ac Keep alternative video qualities list. 2024-10-31 18:04:16 +04:00
23rd
19afb49fce Fixed display of empty context menu in section of history widget. 2024-10-31 16:25:41 +03:00
23rd
46ab553fa5 Rounded earn values from overview to two decimal digits. 2024-10-31 16:12:31 +03:00
23rd
68cc42047e Unified context menu creation for different types of sponsored messages. 2024-10-30 11:59:11 +03:00
23rd
e25cf27ba5 Replaced dropdown menu with popup in box for revenue sponsored messages. 2024-10-30 11:58:33 +03:00
23rd
3895e6d958 Fixed empty context menu from space of sponsored messages. 2024-10-30 11:58:33 +03:00
23rd
24cf3984c8 Implemented operator<< for FullMsgId to enable qDebug output. 2024-10-30 06:47:13 +03:00
23rd
6237675744 Moved out ui callbacks of bar from data class from sponsored messages. 2024-10-30 06:47:13 +03:00
23rd
30dae049ff Fixed non-animated display of top bar for sponsored messages. 2024-10-30 06:39:33 +03:00
23rd
1dc30caee9 Added hide button to bar for sponsored messages without photo. 2024-10-29 15:07:28 +03:00
23rd
b2d340cbfb Added top button to box about sponsored messages for bots. 2024-10-29 15:07:28 +03:00
23rd
7d52787e54 Added support of sponsored messages with revenue to bar. 2024-10-29 14:55:14 +03:00
23rd
ae3f16ccbd Changed behavior to show earlier button in dialog list to jump to top. 2024-10-28 19:34:27 +03:00
23rd
057222757b Added decimal separators to count of credits in balances of owned bot. 2024-10-28 17:47:17 +03:00
23rd
119f109904 Added api support of paid credits transactions from bots. 2024-10-28 17:47:17 +03:00
23rd
23a77b1ba4 Fixed width of menu item for IV zoom. 2024-10-28 15:55:28 +03:00
23rd
c076daa91f Added ability to implement platform-dependent zoom controller for IV. 2024-10-28 15:55:28 +03:00
23rd
b7ef5325ac Improved display of sponsored message bar for preloaded messages. 2024-10-28 13:26:10 +03:00
23rd
8b535c58fa Slightly improved sponsored message bar for instant display. 2024-10-28 13:25:27 +03:00
23rd
6bc8daaeda Added earn button to section of bot settings. 2024-10-27 14:17:51 +03:00
23rd
7a2562e5bb Added icons to earn buttons in profile sections of owned bots. 2024-10-27 13:42:17 +03:00
23rd
9e0c731b32 Moved out generation of credits menu icon from svg to td_ui. 2024-10-27 13:42:01 +03:00
23rd
2e0e4006a1 Moved out generation of currency icon from svg to td_ui. 2024-10-27 13:41:14 +03:00
23rd
187139473d Added subsection title to earn buttons in profile of owned bots. 2024-10-27 13:40:37 +03:00
23rd
dbb0a5ad28 Added ability to open currency earn section for owned bots. 2024-10-27 10:04:06 +03:00
23rd
24aaed44b9 Slightly simplified constructor of Info::BotEarn::InnerWidget. 2024-10-27 09:56:41 +03:00
23rd
32b8d83c04 Moved out Info::Statistics::Tag to separated file. 2024-10-27 09:56:41 +03:00
23rd
bf55c325ce Fixed hotkey to reset IV zoom. 2024-10-27 05:01:25 +03:00
23rd
3af288c74e Added currency balance to profile section of owned bots. 2024-10-26 19:20:56 +03:00
23rd
e306d9ba35 Added loading of balances on receiving full user for owned bots. 2024-10-26 19:20:50 +03:00
23rd
b23a877d7e Added ability to cache currency balance by peer. 2024-10-26 19:20:50 +03:00
23rd
08fda055fc Renamed Api::ChannelEarnStatistics with Api::EarnStatistics. 2024-10-26 19:20:50 +03:00
23rd
84055ed74e Added context menu to top bar of sponsored messages. 2024-10-26 17:55:07 +03:00
23rd
2db30690ce Added ability to open menu for sponsored messages without history item. 2024-10-26 17:55:07 +03:00
23rd
304bcfd343 Added initial button to top bar of sponsored messages to hide it. 2024-10-26 17:55:07 +03:00
23rd
8a1cf2bb3a Added initial implementation of top bar for sponsored messages. 2024-10-26 17:55:07 +03:00
23rd
c857c24a64 Added util functions to process photo and document as dynamic images. 2024-10-26 17:55:07 +03:00
23rd
bbdcb047d0 Added ability to request sponsored messages not only for channels. 2024-10-26 17:55:07 +03:00
John Preston
78f2e70956 Update API scheme to layer 192. 2024-10-26 16:36:17 +04:00
John Preston
75a75626ce Show info about popular apps section. 2024-10-26 16:34:44 +04:00
John Preston
cec9688d58 Implement public posts hashtag search preview. 2024-10-26 16:34:44 +04:00
John Preston
81492b7d3a Add "when edited" context menu information. 2024-10-26 16:34:43 +04:00
John Preston
9166acbbb9 Improve gift convert confirmation. 2024-10-26 16:34:43 +04:00
John Preston
36de2b6ca6 Improved received star gift box. 2024-10-26 16:34:43 +04:00
John Preston
21f909dd4b Improve star gift value table row. 2024-10-26 16:34:43 +04:00
John Preston
f2a92c9122 Update API scheme to layer 191. Sold out gifts. 2024-10-26 16:34:43 +04:00
John Preston
7ee2e3d8bc Support hashtags with mentions. 2024-10-25 18:14:44 +04:00
John Preston
f89aeb6ad4 Implement text -> media message editing. 2024-10-25 18:14:44 +04:00
John Preston
0397006894 Add more logging on failed top peers reading. 2024-10-25 18:14:44 +04:00
23rd
d6863074b2 Fixed error handler of wrong cloud password for withdrawal button. 2024-10-24 19:46:35 +03:00
23rd
9c185a30e0 Slightly improved position of button for QR of username. 2024-10-24 19:15:19 +03:00
23rd
a8f492a027 Improved colors of ministars in buttons from section of peer gifts. 2024-10-24 18:34:11 +03:00
23rd
0a92b1dc68 Slightly improved subtext in overview from statistics section. 2024-10-24 18:15:33 +03:00
23rd
e6d661f8ee Fixed statistics section of stories from megagroups. 2024-10-24 17:43:04 +03:00
23rd
f48dfb5d81 Added ability to add to contacts phone from text phone entity. 2024-10-24 17:04:22 +03:00
23rd
cd041e8366 Replaced empty settings button with simple ripple button. 2024-10-24 17:04:22 +03:00
23rd
6787ea883e Replaced empty icon buttons with simple circle ripple buttons. 2024-10-24 17:04:22 +03:00
John Preston
78937d716f Beta version 5.6.4.
- Add recording of video messages in case a camera is available.
- Add "Respect the Focus settings" for custom notifications on Windows.
2024-10-24 13:40:52 +04:00
John Preston
9713abc002 Fix build with GCC. 2024-10-24 13:36:22 +04:00
John Preston
b44b45cca0 Fix docker build with openh264 in ffmpeg. 2024-10-24 13:26:46 +04:00
John Preston
9e2cf0ed73 Nice faded show/hide of round recorder. 2024-10-24 13:26:46 +04:00
John Preston
b01d7ea5b9 Show recording init errors. 2024-10-24 13:26:46 +04:00
John Preston
ae89b65a98 Fix message selection checks position. 2024-10-24 13:26:46 +04:00
John Preston
9b9c3d788d Skip some first frames. 2024-10-24 13:26:46 +04:00
John Preston
ccc6c6daa5 Save last camera image as a placeholder. 2024-10-24 13:26:46 +04:00
John Preston
9ce6636c6a Create nice camera init effect. 2024-10-24 13:26:46 +04:00
John Preston
6287d306c2 Add better discard confirmations. 2024-10-24 13:26:46 +04:00
John Preston
6cfa053328 Add recorded round video preview. 2024-10-24 13:26:45 +04:00
John Preston
9514b6eecd Show mini-thumbnails when pausing recording. 2024-10-24 13:26:45 +04:00
John Preston
c8d4818d22 Blurred frame while paused. 2024-10-24 13:26:45 +04:00
John Preston
4142ada729 Concatenate two recordings. 2024-10-24 13:26:45 +04:00
John Preston
d7ffdbd78d Prepare for round record pause/resume. 2024-10-24 13:26:45 +04:00
John Preston
e8d87d37bb Use a shorter phrase for bot policy. 2024-10-24 13:26:45 +04:00
John Preston
343ffc23eb Add some dir="auto" to IV page.
I hope this fixes #28551.
2024-10-24 13:26:45 +04:00
John Preston
95e0086eed Remov unused variable. 2024-10-24 13:26:45 +04:00
John Preston
c010ecfe38 Allow sending one-time round videos. 2024-10-24 13:26:45 +04:00
John Preston
302e9371c8 Show record init errors. 2024-10-24 13:26:45 +04:00
John Preston
7060c0e6d7 Fix crash in context menu without an item.
Fixes #28552.
2024-10-24 13:26:42 +04:00
John Preston
20a4c7f9f4 Wait for both audio and video to start. 2024-10-24 13:24:44 +04:00
John Preston
e59e4afd3e Show recording progress. 2024-10-24 13:24:44 +04:00
John Preston
f74dd3ca1e Add author to the top of Reply in Another Chat. 2024-10-24 13:24:44 +04:00
John Preston
511cfc524f Adjust bitrates for video messages. 2024-10-24 13:24:44 +04:00
John Preston
4cf6173d25 Cut video messages round with white bg. 2024-10-24 13:24:44 +04:00
John Preston
17996757fd Show round video preview. 2024-10-24 13:24:44 +04:00
John Preston
6bc1049858 Use nice icon for video message recording. 2024-10-24 13:24:43 +04:00
John Preston
ff44f626ba Allow switching between voice/video. 2024-10-24 13:24:43 +04:00
John Preston
552343fa37 PoC video messages sending. 2024-10-24 13:24:43 +04:00
John Preston
4dc7fd8cd1 Add AAC/H264 encoders, MP4 muxer. 2024-10-24 13:24:43 +04:00
John Preston
285c96fd2e Add "Respect the Focus settings" on Windows. 2024-10-24 13:24:43 +04:00
John Preston
e6af33367e Accept bot username in chatbot setup. 2024-10-24 13:24:43 +04:00
23rd
7092fe2242 Added default ripple animation style with windowBgOver color. 2024-10-24 09:05:27 +04:00
23rd
a32ff46579 Added special hotkeys to change IV zoom more gradually. 2024-10-24 09:05:27 +04:00
23rd
7f20cf59d1 Added ministars to button in service messages for gifts. 2024-10-24 09:05:27 +04:00
23rd
a8a1b08127 Added ministars to buttons in section of peer gifts. 2024-10-24 09:05:27 +04:00
23rd
1a1e777b87 Fixed display of generic media with small width. 2024-10-24 09:05:27 +04:00
23rd
9e76e64064 Improved parts alignments of round video messages. 2024-10-24 09:05:27 +04:00
23rd
975ae17ef9 Moved selection marks to bottom of message bubbles. 2024-10-24 09:05:27 +04:00
23rd
ed9dcef66f Fixed display of selection marks for messages with huge code blocks. 2024-10-24 09:05:27 +04:00
23rd
b1e537e54e Added subscription icon to channel photo in ReceiptCreditsBox. 2024-10-24 09:05:27 +04:00
23rd
e5886862c3 Changed behavior to copy phone number from profile sections as trimmed. 2024-10-24 09:05:27 +04:00
Ilya Fedin
d85b668d4f Fix lambda execution for portal dark mode getter 2024-10-24 07:04:52 +02:00
Grigory
b363d8bfb5 fix(ui/webview_helpers): append 0 if color channel value is 1 char long 2024-10-19 18:43:11 +02:00
John Preston
754d467440 Version 5.6.3: Fix build with Xcode. 2024-10-15 22:13:06 +04:00
John Preston
598f08d6c7 Version 5.6.3.
- Add ability to change page scale in Instant View pages.
- Fix unnecessary timestamp jump to a new line.
- Fix secondary button positioning in miniapps.
- Fix a crash in QR code copy for a chat without a photo.
- Fix a crash in share options menu showing.
- Fix a crash on monitor disconnect.
2024-10-15 21:01:27 +04:00
23rd
224fdc1864 Returned back freedom to copy links even in non-forwardable histories.
This reverts commits b64c610abb
and 9cd194e60e.
2024-10-15 13:04:57 +03:00
23rd
e646b4dc9a Slightly improved position of selection marks for wide chats. 2024-10-15 12:59:15 +03:00
23rd
9077db2e97 Added verified mark of web view bots to box of confirmation to open. 2024-10-15 12:46:13 +03:00
23rd
7e14277ead Added ability to change zoom in IV. 2024-10-15 12:46:13 +03:00
John Preston
d351a7d697 Fix crash on monitor arrangement change. 2024-10-15 12:13:53 +04:00
John Preston
70ed43b811 Fix crash on send options menu show.
Fixes #28532.
2024-10-15 12:13:53 +04:00
John Preston
913083ebc6 Fix miniapp buttons positioning. 2024-10-15 12:13:52 +04:00
John Preston
588a95a7ae Fix time jumping to a new line. 2024-10-15 12:13:52 +04:00
23rd
c0a0ad4ec5 Added info to context menu for non-forwardable history. 2024-10-15 01:15:04 +03:00
23rd
5eafe96525 Fixed crash in share QR box on copy without profile photo. 2024-10-15 01:15:00 +03:00
259 changed files with 8854 additions and 2038 deletions

View File

@@ -995,6 +995,7 @@ PRIVATE
info/statistics/info_statistics_list_controllers.h
info/statistics/info_statistics_recent_message.cpp
info/statistics/info_statistics_recent_message.h
info/statistics/info_statistics_tag.h
info/statistics/info_statistics_widget.cpp
info/statistics/info_statistics_widget.h
info/stories/info_stories_inner_widget.cpp
@@ -1162,6 +1163,8 @@ PRIVATE
media/streaming/media_streaming_player.h
media/streaming/media_streaming_reader.cpp
media/streaming/media_streaming_reader.h
media/streaming/media_streaming_round_preview.cpp
media/streaming/media_streaming_round_preview.h
media/streaming/media_streaming_utility.cpp
media/streaming/media_streaming_utility.h
media/streaming/media_streaming_video_track.cpp
@@ -1501,6 +1504,8 @@ PRIVATE
ui/chat/choose_send_as.h
ui/chat/choose_theme_controller.cpp
ui/chat/choose_theme_controller.h
ui/chat/sponsored_message_bar.cpp
ui/chat/sponsored_message_bar.h
ui/controls/emoji_button_factory.cpp
ui/controls/emoji_button_factory.h
ui/controls/location_picker.cpp
@@ -1849,7 +1854,7 @@ endif()
set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
if (WIN32 AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
if (MSVC)
target_link_libraries(Telegram
PRIVATE
delayimp
@@ -1946,7 +1951,7 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
base/platform/win/base_windows_safe_library.h
)
target_include_directories(Updater PRIVATE ${lib_base_loc})
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
if (MSVC)
target_link_libraries(Updater
PRIVATE
delayimp

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -12,6 +12,7 @@ body {
margin: 0;
background-color: var(--td-window-bg);
color: var(--td-window-fg);
zoom: var(--td-zoom-percentage);
}
html.custom_scroll ::-webkit-scrollbar {

View File

@@ -72,6 +72,9 @@ var IV = {
}
},
frameKeyDown: function (e) {
const key0 = (e.key === '0')
|| (e.code === 'Key0')
|| (e.keyCode === 48);
const keyW = (e.key === 'w')
|| (e.code === 'KeyW')
|| (e.keyCode === 87);
@@ -81,12 +84,12 @@ var IV = {
const keyM = (e.key === 'm')
|| (e.code === 'KeyM')
|| (e.keyCode === 77);
if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM)) {
if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM || key0)) {
e.preventDefault();
IV.notify({
event: 'keydown',
modifier: e.ctrlKey ? 'ctrl' : 'cmd',
key: keyW ? 'w' : keyQ ? 'q' : 'm',
key: key0 ? '0' : keyW ? 'w' : keyQ ? 'q' : 'm',
});
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();

View File

@@ -499,8 +499,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_notify_global" = "Global settings";
"lng_settings_notify_title" = "Notifications for chats";
"lng_settings_desktop_notify" = "Desktop notifications";
"lng_settings_native_title" = "Native notifications";
"lng_settings_native_title" = "System integration";
"lng_settings_use_windows" = "Use Windows notifications";
"lng_settings_skip_in_focus" = "Respect system Focus mode";
"lng_settings_use_native_notifications" = "Use native notifications";
"lng_settings_notifications_position" = "Location on the screen";
"lng_settings_notifications_count" = "Notifications count";
@@ -1593,6 +1594,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_peer_bot_public_link" = "Public Link";
"lng_manage_peer_bot_public_links" = "Public Links";
"lng_manage_peer_bot_balance" = "Balance";
"lng_manage_peer_bot_balance_currency" = "Toncoin";
"lng_manage_peer_bot_balance_credits" = "Stars";
"lng_manage_peer_bot_edit_intro" = "Edit Intro";
"lng_manage_peer_bot_edit_commands" = "Edit Commands";
"lng_manage_peer_bot_edit_settings" = "Change Bot Settings";
@@ -1870,6 +1873,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_got_subtitle" = "Gift from {user}";
"lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
"lng_action_gift_got_gift_text" = "You can keep this gift on your page.";
"lng_action_gift_sent_subtitle" = "Gift for {user}";
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
@@ -2115,8 +2119,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_recommended_message_title" = "Recommended";
"lng_edited" = "edited";
"lng_commented" = "commented";
"lng_approximate" = "appx.";
"lng_edited_date" = "Edited: {date}";
"lng_sent_date" = "Sent: {date}";
"lng_approximate_about" = "Estimated date of video publishing.";
"lng_views_tooltip#one" = "Views: {count}";
"lng_views_tooltip#other" = "Views: {count}";
"lng_forwards_tooltip#one" = "Shares: {count}";
@@ -2151,6 +2157,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_media_cancel" = "Cancel";
"lng_media_video" = "Video";
"lng_media_audio" = "Voice message";
"lng_media_round" = "Video message";
"lng_media_auto_settings" = "Automatic media download";
"lng_media_auto_in_private" = "In private chats";
@@ -2442,12 +2449,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_history_entry_giveaway_name" = "Received Prize";
"lng_credits_box_history_entry_gift_sent" = "Sent Gift";
"lng_credits_box_history_entry_gift_converted" = "Converted Gift";
"lng_credits_box_history_entry_gift_unavailable" = "Unavailable";
"lng_credits_box_history_entry_gift_sold_out" = "This gift has sold out";
"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
"lng_credits_box_history_entry_gift_examples" = "Examples";
"lng_credits_box_history_entry_ads" = "Ads Platform";
"lng_credits_box_history_entry_premium_bot" = "Stars Top-Up";
"lng_credits_box_history_entry_api" = "Paid Broadcast";
"lng_credits_box_history_entry_floodskip_about#one" = "{count} Message";
"lng_credits_box_history_entry_floodskip_about#other" = "{count} Messages";
"lng_credits_box_history_entry_floodskip_row" = "Messages";
"lng_credits_box_history_entry_via_premium_bot" = "Premium Bot";
"lng_credits_box_history_entry_id" = "Transaction ID";
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
@@ -2988,6 +3001,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_link_reason_unclaimed" = "Incomplete Giveaway";
"lng_gift_link_reason_chosen" = "You were selected by the channel";
"lng_gift_link_label_date" = "Date";
"lng_gift_link_label_first_sale" = "First Sale";
"lng_gift_link_label_last_sale" = "Last Sale";
"lng_gift_link_label_value" = "Value";
"lng_gift_link_also_send" = "You can also {link} to a friend as a gift.";
"lng_gift_link_also_send_link" = "send this link";
"lng_gift_link_use" = "Use Link";
@@ -3044,8 +3060,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_convert_to_stars#one" = "Convert to {count} Star";
"lng_gift_convert_to_stars#other" = "Convert to {count} Stars";
"lng_gift_convert_sure_title" = "Convert Gift to Stars";
"lng_gift_convert_sure_text#one" = "Do you want to convert this gift from {user} to **{count} Star**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure_text#other" = "Do you want to convert this gift from {user} to **{count} Stars**?\n\nThis action cannot be undone.";
"lng_gift_convert_sure_confirm#one" = "Do you want to convert this gift from {user} to **{count} Star**?";
"lng_gift_convert_sure_confirm#other" = "Do you want to convert this gift from {user} to **{count} Stars**?";
"lng_gift_convert_sure_limit#one" = "Conversion is available for the next **{count} day**.";
"lng_gift_convert_sure_limit#other" = "Conversion is available for the next **{count} days**.";
"lng_gift_convert_sure_caution" = "This action cannot be undone. This will permanently destroy the gift.";
"lng_gift_convert_sure" = "Convert";
"lng_gift_display_done" = "The gift is now shown on your profile page.";
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
@@ -3054,6 +3073,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_sold_out_title" = "Sold Out!";
"lng_gift_sold_out_text#one" = "All {count} gift was already sold.";
"lng_gift_sold_out_text#other" = "All {count} gifts were already sold.";
"lng_gift_send_small" = "send a gift";
"lng_gift_sell_small#one" = "sell for {count} Star";
"lng_gift_sell_small#other" = "sell for {count} Stars";
"lng_accounts_limit_title" = "Limit Reached";
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
@@ -3243,11 +3265,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_record_cancel" = "Release outside this field to cancel";
"lng_record_cancel_stories" = "Release outside to cancel";
"lng_record_lock_cancel_sure" = "Do you want to stop recording and discard your voice message?";
"lng_record_lock_cancel_sure_round" = "Do you want to stop recording and discard your video message?";
"lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?";
"lng_record_listen_cancel_sure_round" = "Do you want to discard your recorded video message?";
"lng_record_lock_discard" = "Discard";
"lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message.";
"lng_record_voice_tip" = "Hold to record audio. Click to switch to video.";
"lng_record_video_tip" = "Hold to record video. Click to switch to audio.";
"lng_record_audio_problem" = "Could not start audio recording. Please check your microphone.";
"lng_record_video_problem" = "Could not start video recording. Please check your camera.";
"lng_record_once_first_tooltip" = "Click to set this message to **Play Once**.";
"lng_record_once_active_tooltip" = "The recipient will be able to listen only once.";
"lng_record_once_active_video" = "The recipient will be able to watch only once.";
"lng_will_be_notified" = "Subscribers will be notified when you post.";
"lng_wont_be_notified" = "Subscribers will receive a silent notification.";
"lng_willbe_history" = "Select a chat to start messaging";
@@ -3276,6 +3305,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_scheduled_send_now" = "Send message now?";
"lng_scheduled_send_now_many#one" = "Send {count} message now?";
"lng_scheduled_send_now_many#other" = "Send {count} messages now?";
"lng_scheduled_video_tip_title" = "Improving video...";
"lng_scheduled_video_tip_text" = "The video will be published after it's optimized for the best viewing experience.";
"lng_scheduled_video_tip" = "Processing video may take a few minutes.";
"lng_scheduled_video_published" = "Video Published.";
"lng_scheduled_video_view" = "View";
"lng_replies_view#one" = "View {count} Reply";
"lng_replies_view#other" = "View {count} Replies";
@@ -3359,6 +3393,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_settings" = "Settings";
"lng_bot_open" = "Open Bot";
"lng_bot_terms" = "Terms of Use";
"lng_bot_privacy" = "Privacy Policy";
"lng_bot_reload_page" = "Reload Page";
"lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachment menu so you can access it from any chat.";
"lng_bot_add_to_menu_done" = "Bot added to the menu.";
@@ -3576,6 +3611,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
"lng_context_spoiler_effect" = "Hide with Spoiler";
"lng_context_disable_spoiler" = "Remove Spoiler";
"lng_context_make_paid" = "Make This Content Paid";
@@ -3678,6 +3717,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_reply_in_another_title" = "Reply in...";
"lng_reply_in_another_chat" = "Reply in Another Chat";
"lng_reply_in_author" = "Message author";
"lng_reply_in_chats_list" = "Your chats";
"lng_reply_show_in_chat" = "Show in Chat";
"lng_reply_remove" = "Do Not Reply";
"lng_reply_about_quote" = "You can select a specific part to quote.";
@@ -3865,6 +3906,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_downloads" = "Downloads";
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
"lng_mediaview_rotate_video" = "Rotate video";
"lng_mediaview_quality_auto" = "Auto";
"lng_theme_preview_title" = "Theme Preview";
"lng_theme_preview_generating" = "Generating color theme preview...";
@@ -3946,7 +3988,9 @@ 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 (webkitgtk-6.0/webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.";
"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";
@@ -5168,13 +5212,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sponsored_revenued_subtitle" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:";
"lng_sponsored_revenued_info1_title" = "Respect Your Privacy";
"lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them.";
"lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them.";
"lng_sponsored_revenued_info2_title" = "Help the Channel Creator";
"lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed.";
"lng_sponsored_revenued_info2_bot_description" = "50% of the revenue from Telegram Ads goes to the developer of the mini app where they are displayed.";
"lng_sponsored_revenued_info3_title" = "Can Be Removed";
"lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
"lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
"lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}.";
"lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?";
"lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
"lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
"lng_sponsored_top_bar_hide" = "remove";
"lng_telegram_features_url" = "https://t.me/TelegramTips";
@@ -5478,6 +5527,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_title" = "Monetization";
"lng_channel_earn_about" = "Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}";
"lng_channel_earn_about_bot" = "Telegram shares 50% of the revenue from ads displayed in your bot. {link}";
"lng_channel_earn_about_link" = "Learn more {emoji}";
"lng_channel_earn_overview_title" = "Rewards overview";
"lng_channel_earn_available" = "Rewards available for collection";
@@ -5510,8 +5560,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channel_earn_cpm#one" = "{emoji} {count} CPM";
"lng_channel_earn_cpm#other" = "{emoji} {count} CPM";
"lng_channel_earn_learn_title" = "Earn From Your Channel";
"lng_channel_earn_bot_learn_title" = "Earn From Your Bot";
"lng_channel_earn_learn_in_subtitle" = "Telegram Ads";
"lng_channel_earn_learn_in_about" = "Telegram can display ads in your channel.";
"lng_channel_earn_learn_bot_in_about" = "Telegram can display ads in your bot.";
"lng_channel_earn_learn_split_subtitle" = "50:50 revenue split";
"lng_channel_earn_learn_split_about" = "You can receive 50% of the ad revenue as rewards in TON.";
"lng_channel_earn_learn_out_subtitle" = "Flexible withdrawals";
@@ -5558,6 +5610,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_iv_window_title" = "Instant View";
"lng_iv_wrong_layout" = "Wrong layout?";
"lng_iv_not_supported" = "This link appears to be invalid.";
"lng_iv_zoom_tooltip_ctrl" = "Hold Ctrl to zoom by 5%.\nHold Alt to zoom by 1%.";
"lng_iv_zoom_tooltip_cmd" = "Hold Cmd to zoom by 5%.\nHold Alt to zoom by 1%.";
"lng_limit_download_title" = "Download speed limited";
"lng_limit_download_subscribe" = "Subscribe to {link} to increase download speed {increase}.";
@@ -5595,6 +5649,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_channels_recommended" = "Similar channels";
"lng_bot_apps_your" = "Apps you use";
"lng_bot_apps_popular" = "Grossing apps";
"lng_bot_apps_which" = "Which apps are included here? {link}";
"lng_bot_apps_which_link" = "Learn >";
"lng_popular_apps_info_title" = "Top Mini Apps";
"lng_popular_apps_info_text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average.";
"lng_popular_apps_info_bot" = "@botfather";
"lng_popular_apps_info_here" = "here";
"lng_popular_apps_info_url" = "https://core.telegram.org/bots/webapps#launching-the-main-mini-app";
"lng_popular_apps_info_confirm" = "Understood";
"lng_font_box_title" = "Choose font family";
"lng_font_default" = "Default";

View File

@@ -7,6 +7,7 @@
<file alias="art/logo_256.png">../../art/logo_256.png</file>
<file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file>
<file alias="art/themeimage.jpg">../../art/themeimage.jpg</file>
<file alias="art/round_placeholder.jpg">../../art/round_placeholder.jpg</file>
<file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file>
<file alias="night.tdesktop-theme">../../night.tdesktop-theme</file>
<file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file>

View File

@@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="5.6.2.0" />
Version="5.7.0.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View File

@@ -6,17 +6,11 @@
//
// Generated from the TEXTINCLUDE 2 resource.
//
#if defined(__MINGW64__) || defined(__MINGW32__)
// MinGW-w64, MinGW
#if defined(__has_include) && __has_include(<winres.h>)
#include <winres.h>
#else
#include <afxres.h>
#include <winresrc.h>
#endif
#if defined(__has_include) && __has_include(<winres.h>)
#include <winres.h>
#else
// MSVC, Windows SDK
#include <winres.h>
#include <afxres.h>
#include <winresrc.h>
#endif
/////////////////////////////////////////////////////////////////////////////
@@ -44,8 +38,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,6,2,0
PRODUCTVERSION 5,6,2,0
FILEVERSION 5,7,0,0
PRODUCTVERSION 5,7,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +56,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "5.6.2.0"
VALUE "FileVersion", "5.7.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.6.2.0"
VALUE "ProductVersion", "5.7.0.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -6,17 +6,11 @@
//
// Generated from the TEXTINCLUDE 2 resource.
//
#if defined(__MINGW64__) || defined(__MINGW32__)
// MinGW-w64, MinGW
#if defined(__has_include) && __has_include(<winres.h>)
#include <winres.h>
#else
#include <afxres.h>
#include <winresrc.h>
#endif
#if defined(__has_include) && __has_include(<winres.h>)
#include <winres.h>
#else
// MSVC, Windows SDK
#include <winres.h>
#include <afxres.h>
#include <winresrc.h>
#endif
/////////////////////////////////////////////////////////////////////////////
@@ -35,8 +29,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 5,6,2,0
PRODUCTVERSION 5,6,2,0
FILEVERSION 5,7,0,0
PRODUCTVERSION 5,7,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +47,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "5.6.2.0"
VALUE "FileVersion", "5.7.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.6.2.0"
VALUE "ProductVersion", "5.7.0.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/empty_userpic.h"
@@ -129,6 +130,7 @@ void ConfirmSubscriptionBox(
struct State final {
std::shared_ptr<Data::PhotoMedia> photoMedia;
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
QImage frame;
std::optional<MTP::Sender> api;
Ui::RpWidget* saveButton = nullptr;
@@ -146,25 +148,45 @@ void ConfirmSubscriptionBox(
const auto userpic = userpicWrap->entity();
const auto photoSize = st::confirmInvitePhotoSize;
userpic->resize(Size(photoSize));
const auto creditsIconSize = photoSize / 3;
const auto creditsIconCallback =
Ui::PaintOutlinedColoredCreditsIconCallback(
creditsIconSize,
1.5);
state->frame = QImage(
Size(photoSize * style::DevicePixelRatio()),
QImage::Format_ARGB32_Premultiplied);
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
const auto options = Images::Option::RoundCircle;
userpic->paintRequest(
) | rpl::start_with_next([=, small = Data::PhotoSize::Small] {
auto p = QPainter(userpic);
if (state->photoMedia) {
if (const auto image = state->photoMedia->image(small)) {
p.drawPixmap(
state->frame.fill(Qt::transparent);
{
auto p = QPainter(&state->frame);
if (state->photoMedia) {
if (const auto image = state->photoMedia->image(small)) {
p.drawPixmap(
0,
0,
image->pix(Size(photoSize), { .options = options }));
}
} else if (state->photoEmpty) {
state->photoEmpty->paintCircle(
p,
0,
0,
image->pix(Size(photoSize), { .options = options }));
userpic->width(),
photoSize);
}
if (creditsIconCallback) {
p.translate(
photoSize - creditsIconSize,
photoSize - creditsIconSize);
creditsIconCallback(p);
}
} else if (state->photoEmpty) {
state->photoEmpty->paintCircle(
p,
0,
0,
userpic->width(),
photoSize);
}
auto p = QPainter(userpic);
p.drawImage(0, 0, state->frame);
}, userpicWrap->lifetime());
userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
if (photo) {

View File

@@ -39,8 +39,8 @@ constexpr auto kTransactionsLimit = 100;
if (const auto list = tl.data().vextended_media()) {
extended.reserve(list->v.size());
for (const auto &media : list->v) {
media.match([&](const MTPDmessageMediaPhoto &photo) {
if (const auto inner = photo.vphoto()) {
media.match([&](const MTPDmessageMediaPhoto &data) {
if (const auto inner = data.vphoto()) {
const auto photo = owner->processPhoto(*inner);
if (!photo->isNull()) {
extended.push_back(CreditsHistoryMedia{
@@ -49,9 +49,11 @@ constexpr auto kTransactionsLimit = 100;
});
}
}
}, [&](const MTPDmessageMediaDocument &document) {
if (const auto inner = document.vdocument()) {
const auto document = owner->processDocument(*inner);
}, [&](const MTPDmessageMediaDocument &data) {
if (const auto inner = data.vdocument()) {
const auto document = owner->processDocument(
*inner,
data.valt_documents());
if (document->isAnimation()
|| document->isVideoFile()
|| document->isGifv()) {
@@ -101,6 +103,8 @@ constexpr auto kTransactionsLimit = 100;
return Data::CreditsHistoryEntry::PeerType::PremiumBot;
}, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads;
}, [](const MTPDstarsTransactionPeerAPI &) {
return Data::CreditsHistoryEntry::PeerType::API;
}),
.subscriptionUntil = tl.data().vsubscription_period()
? base::unixtime::parse(base::unixtime::now()
@@ -113,6 +117,7 @@ constexpr auto kTransactionsLimit = 100;
.convertStars = int(stargift
? stargift->data().vconvert_stars().v
: 0),
.floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
.converted = stargift && incoming,
.reaction = tl.data().is_reaction(),
.refunded = tl.data().is_refund(),

View File

@@ -99,8 +99,8 @@ public:
[[nodiscard]] Data::CreditsEarnStatistics data() const;
private:
const bool _isUser = false;
Data::CreditsEarnStatistics _data;
bool _isUser = false;
mtpRequestId _requestId = 0;

View File

@@ -89,12 +89,15 @@ void HandleWithdrawalButton(
}
};
const auto fail = [=](const MTP::Error &error) {
show->showToast(error.type());
const auto message = error.type();
if (box && !box->handleCustomCheckError(message)) {
show->showToast(message);
}
};
if (channel) {
session->api().request(
MTPstats_GetBroadcastRevenueWithdrawalUrl(
channel->inputChannel,
channel->input,
result.result
)).done([=](const ChannelOutUrl &r) {
done(qs(r.data().vurl()));
@@ -134,7 +137,7 @@ void HandleWithdrawalButton(
if (channel) {
session->api().request(
MTPstats_GetBroadcastRevenueWithdrawalUrl(
channel->inputChannel,
channel->input,
MTP_inputCheckPasswordEmpty()
)).fail(fail).send();
} else if (peer) {

View File

@@ -776,6 +776,8 @@ std::optional<StarGift> FromTL(
.document = document,
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
.firstSaleDate = data.vfirst_sale_date().value_or_empty(),
.lastSaleDate = data.vlast_sale_date().value_or_empty(),
};
}
@@ -789,7 +791,7 @@ std::optional<UserStarGift> FromTL(
return {};
}
return UserStarGift{
.gift = std::move(*parsed),
.info = std::move(*parsed),
.message = (data.vmessage()
? TextWithEntities{
.text = qs(data.vmessage()->data().vtext()),

View File

@@ -80,10 +80,16 @@ struct StarGift {
not_null<DocumentData*> document;
int limitedLeft = 0;
int limitedCount = 0;
TimeId firstSaleDate = 0;
TimeId lastSaleDate = 0;
friend inline bool operator==(
const StarGift &,
const StarGift &) = default;
};
struct UserStarGift {
StarGift gift;
StarGift info;
TextWithEntities message;
int64 convertStars = 0;
PeerId fromId = 0;

View File

@@ -456,6 +456,7 @@ void SendConfirmedFile(
not_null<Main::Session*> session,
const std::shared_ptr<FilePrepareResult> &file) {
const auto isEditing = (file->type != SendMediaType::Audio)
&& (file->type != SendMediaType::Round)
&& (file->to.replaceMediaOf != 0);
const auto newId = FullMsgId(
file->to.peer,
@@ -525,7 +526,8 @@ void SendConfirmedFile(
// Shortcut messages have no 'edited' badge.
flags |= MessageFlag::HideEdited;
}
if (file->type == SendMediaType::Audio) {
if (file->type == SendMediaType::Audio
|| file->type == SendMediaType::Round) {
if (!peer->isChannel() || peer->isMegagroup()) {
flags |= MessageFlag::MediaIsUnread;
}
@@ -551,29 +553,25 @@ void SendConfirmedFile(
MTPint());
} else if (file->type == SendMediaType::Audio) {
const auto ttlSeconds = file->to.options.ttlSeconds;
const auto isVoice = [&] {
return file->document.match([](const MTPDdocumentEmpty &d) {
return false;
}, [](const MTPDdocument &d) {
return ranges::any_of(d.vattributes().v, [&](
const MTPDocumentAttribute &attribute) {
using Att = MTPDdocumentAttributeAudio;
return attribute.match([](const Att &data) -> bool {
return data.vflags().v & Att::Flag::f_voice;
}, [](const auto &) {
return false;
});
});
});
}();
using Flag = MTPDmessageMediaDocument::Flag;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| (isVoice ? Flag::f_voice : Flag())
| Flag::f_voice
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
file->document,
MTPVector<MTPDocument>(), // alt_documents
MTP_int(ttlSeconds));
} else if (file->type == SendMediaType::Round) {
using Flag = MTPDmessageMediaDocument::Flag;
const auto ttlSeconds = file->to.options.ttlSeconds;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| Flag::f_round
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->document,
MTPVector<MTPDocument>(), // alt_documents
MTP_int(ttlSeconds));
} else {
Unexpected("Type in sendFilesConfirmed.");
}

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_story.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
@@ -341,6 +342,10 @@ void PublicForwards::request(
.token = nextToken,
});
};
const auto processFail = [=] {
_requestId = 0;
done({});
};
constexpr auto kLimit = tl::make_int(100);
if (_fullId.messageId) {
@@ -349,14 +354,14 @@ void PublicForwards::request(
MTP_int(_fullId.messageId.msg),
MTP_string(token),
kLimit
)).done(processResult).fail([=] { _requestId = 0; }).send();
)).done(processResult).fail(processFail).send();
} else if (_fullId.storyId) {
_requestId = makeRequest(MTPstats_GetStoryPublicForwards(
channel->input,
MTP_int(_fullId.storyId.story),
MTP_string(token),
kLimit
)).done(processResult).fail([=] { _requestId = 0; }).send();
)).done(processResult).fail(processFail).send();
}
}
@@ -381,7 +386,7 @@ Data::PublicForwardsSlice MessageStatistics::firstSlice() const {
}
void MessageStatistics::request(Fn<void(Data::MessageStatistics)> done) {
if (channel()->isMegagroup()) {
if (channel()->isMegagroup() && !_storyId) {
return;
}
const auto requestFirstPublicForwards = [=](
@@ -681,17 +686,18 @@ Data::BoostStatus Boosts::boostStatus() const {
return _boostStatus;
}
ChannelEarnStatistics::ChannelEarnStatistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(channel) {
EarnStatistics::EarnStatistics(not_null<PeerData*> peer)
: StatisticsRequestSender(peer)
, _isUser(peer->isUser()) {
}
rpl::producer<rpl::no_value, QString> ChannelEarnStatistics::request() {
rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
makeRequest(MTPstats_GetBroadcastRevenueStats(
MTP_flags(0),
channel()->inputChannel
(_isUser ? user()->input : channel()->input)
)).done([=](const MTPstats_BroadcastRevenueStats &result) {
const auto &data = result.data();
const auto &balances = data.vbalances().data();
@@ -708,18 +714,22 @@ rpl::producer<rpl::no_value, QString> ChannelEarnStatistics::request() {
requestHistory({}, [=](Data::EarnHistorySlice &&slice) {
_data.firstHistorySlice = std::move(slice);
api().request(
MTPchannels_GetFullChannel(channel()->inputChannel)
).done([=](const MTPmessages_ChatFull &result) {
result.data().vfull_chat().match([&](
const MTPDchannelFull &d) {
_data.switchedOff = d.is_restricted_sponsored();
}, [](const auto &) {
});
if (!_isUser) {
api().request(
MTPchannels_GetFullChannel(channel()->inputChannel)
).done([=](const MTPmessages_ChatFull &result) {
result.data().vfull_chat().match([&](
const MTPDchannelFull &d) {
_data.switchedOff = d.is_restricted_sponsored();
}, [](const auto &) {
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
} else {
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}
});
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
@@ -729,7 +739,7 @@ rpl::producer<rpl::no_value, QString> ChannelEarnStatistics::request() {
};
}
void ChannelEarnStatistics::requestHistory(
void EarnStatistics::requestHistory(
const Data::EarnHistorySlice::OffsetToken &token,
Fn<void(Data::EarnHistorySlice)> done) {
if (_requestId) {
@@ -738,7 +748,7 @@ void ChannelEarnStatistics::requestHistory(
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
_requestId = api().request(MTPstats_GetBroadcastRevenueTransactions(
channel()->inputChannel,
(_isUser ? user()->input : channel()->input),
MTP_int(token),
(!token) ? kTlFirstSlice : kTlLimit
)).done([=](const MTPstats_BroadcastRevenueTransactions &result) {
@@ -799,7 +809,7 @@ void ChannelEarnStatistics::requestHistory(
}).send();
}
Data::EarnStatistics ChannelEarnStatistics::data() const {
Data::EarnStatistics EarnStatistics::data() const {
return _data;
}

View File

@@ -79,9 +79,9 @@ private:
};
class ChannelEarnStatistics final : public StatisticsRequestSender {
class EarnStatistics final : public StatisticsRequestSender {
public:
explicit ChannelEarnStatistics(not_null<ChannelData*> channel);
explicit EarnStatistics(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
void requestHistory(
@@ -94,6 +94,7 @@ public:
static constexpr auto kLimit = int(10);
private:
const bool _isUser = false;
Data::EarnStatistics _data;
mtpRequestId _requestId = 0;

View File

@@ -316,6 +316,9 @@ void Updates::feedUpdateVector(
} else if (policy == SkipUpdatePolicy::SkipExceptGroupCallParticipants) {
return;
}
if (policy == SkipUpdatePolicy::SkipNone) {
applyConvertToScheduledOnSend(updates);
}
for (const auto &entry : std::as_const(list)) {
const auto type = entry.type();
if ((policy == SkipUpdatePolicy::SkipMessageIds
@@ -329,6 +332,15 @@ void Updates::feedUpdateVector(
session().data().sendHistoryChangeNotifications();
}
void Updates::checkForSentToScheduled(const MTPUpdates &updates) {
updates.match([&](const MTPDupdates &data) {
applyConvertToScheduledOnSend(data.vupdates(), true);
}, [&](const MTPDupdatesCombined &data) {
applyConvertToScheduledOnSend(data.vupdates(), true);
}, [](const auto &) {
});
}
void Updates::feedMessageIds(const MTPVector<MTPUpdate> &updates) {
for (const auto &update : updates.v) {
if (update.type() == mtpc_updateMessageID) {
@@ -432,6 +444,7 @@ void Updates::feedChannelDifference(
session().data().processChats(data.vchats());
_handlingChannelDifference = true;
applyConvertToScheduledOnSend(data.vother_updates());
feedMessageIds(data.vother_updates());
session().data().processMessages(
data.vnew_messages(),
@@ -596,6 +609,7 @@ void Updates::feedDifference(
Core::App().checkAutoLock();
session().data().processUsers(users);
session().data().processChats(chats);
applyConvertToScheduledOnSend(other);
feedMessageIds(other);
session().data().processMessages(msgs, NewMessageType::Unread);
feedUpdateVector(other, SkipUpdatePolicy::SkipMessageIds);
@@ -881,6 +895,51 @@ void Updates::mtpUpdateReceived(const MTPUpdates &updates) {
}
}
void Updates::applyConvertToScheduledOnSend(
const MTPVector<MTPUpdate> &other,
bool skipScheduledCheck) {
for (const auto &update : other.v) {
update.match([&](const MTPDupdateNewScheduledMessage &data) {
const auto &message = data.vmessage();
const auto id = IdFromMessage(message);
const auto scheduledMessages = &_session->scheduledMessages();
const auto scheduledId = scheduledMessages->localMessageId(id);
for (const auto &updateId : other.v) {
updateId.match([&](const MTPDupdateMessageID &dataId) {
if (dataId.vid().v == id) {
auto &owner = session().data();
if (skipScheduledCheck) {
const auto peerId = PeerFromMessage(message);
const auto history = owner.historyLoaded(peerId);
if (history) {
_session->data().sentToScheduled({
.history = history,
.scheduledId = scheduledId,
});
}
return;
}
const auto rand = dataId.vrandom_id().v;
const auto localId = owner.messageIdByRandomId(rand);
if (const auto local = owner.message(localId)) {
if (!local->isScheduled()) {
_session->data().sentToScheduled({
.history = local->history(),
.scheduledId = scheduledId,
});
// We've sent a non-scheduled message,
// but it was converted to a scheduled.
local->destroy();
}
}
}
}, [](const auto &) {});
}
}, [](const auto &) {});
}
}
void Updates::applyGroupCallParticipantUpdates(const MTPUpdates &updates) {
updates.match([&](const MTPDupdates &data) {
session().data().processUsers(data.vusers());

View File

@@ -40,6 +40,8 @@ public:
void applyUpdatesNoPtsCheck(const MTPUpdates &updates);
void applyUpdateNoPtsCheck(const MTPUpdate &update);
void checkForSentToScheduled(const MTPUpdates &updates);
[[nodiscard]] int32 pts() const;
void updateOnline(crl::time lastNonIdleTime = 0);
@@ -131,6 +133,9 @@ private:
// Doesn't call sendHistoryChangeNotifications itself.
void feedUpdate(const MTPUpdate &update);
void applyConvertToScheduledOnSend(
const MTPVector<MTPUpdate> &other,
bool skipScheduledCheck = false);
void applyGroupCallParticipantUpdates(const MTPUpdates &updates);
bool whenGetDiffChanged(

View File

@@ -756,5 +756,19 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
const style::WhoRead &st) {
return WhoReacted(item, reaction, context, st, nullptr);
}
rpl::producer<Ui::WhoReadContent> WhenEdited(
not_null<PeerData*> author,
TimeId date) {
return rpl::single(Ui::WhoReadContent{
.participants = { Ui::WhoReadParticipant{
.name = author->name(),
.date = FormatReadDate(date, QDateTime::currentDateTime()),
.id = author->id.value,
} },
.type = Ui::WhoReadType::Edited,
.fullReadCount = 1,
});
}
} // namespace Api

View File

@@ -61,5 +61,8 @@ struct WhoReadList {
const Data::ReactionId &reaction,
not_null<QWidget*> context, // Cache results for this lifetime.
const style::WhoRead &st);
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenEdited(
not_null<PeerData*> author,
TimeId date);
} // namespace Api

View File

@@ -3329,6 +3329,7 @@ void ApiWrap::forwardMessages(
}
const auto requestType = Data::Histories::RequestType::Send;
const auto idsCopy = localIds;
const auto scheduled = action.options.scheduled;
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
history->sendRequestId = request(MTPmessages_ForwardMessages(
MTP_flags(sendFlags),
@@ -3341,6 +3342,9 @@ void ApiWrap::forwardMessages(
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId)
)).done([=](const MTPUpdates &result) {
if (!scheduled) {
this->updates().checkForSentToScheduled(result);
}
applyUpdates(result);
if (shared && !--shared->requestsLeft) {
shared->callback();
@@ -3502,6 +3506,7 @@ void ApiWrap::sendVoiceMessage(
QByteArray result,
VoiceWaveform waveform,
crl::time duration,
bool video,
const SendAction &action) {
const auto caption = TextWithTags();
const auto to = FileLoadTaskOptions(action);
@@ -3510,6 +3515,7 @@ void ApiWrap::sendVoiceMessage(
result,
duration,
waveform,
video,
to,
caption));
}

View File

@@ -317,6 +317,7 @@ public:
QByteArray result,
VoiceWaveform waveform,
crl::time duration,
bool video,
const SendAction &action);
void sendFiles(
Ui::PreparedList &&list,

View File

@@ -154,9 +154,7 @@ contactsSortButton: IconButton(defaultIconButton) {
iconPosition: point(10px, -1px);
rippleAreaPosition: point(1px, 6px);
rippleAreaSize: 42px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
contactsSortOnlineIcon: icon{{ "contacts_online", boxTitleCloseFg }};
contactsSortOnlineIconOver: icon{{ "contacts_online", boxTitleCloseFgOver }};
@@ -416,9 +414,7 @@ calendarPrevious: IconButton {
rippleAreaPosition: point(2px, 2px);
rippleAreaSize: 44px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
calendarPreviousDisabled: icon {{ "calendar_down-flip_vertical", menuIconFg }};
calendarNext: IconButton(calendarPrevious) {
@@ -616,9 +612,7 @@ proxyTryIPv6Padding: margins(22px, 8px, 22px, 5px);
proxyRowPadding: margins(22px, 8px, 8px, 8px);
proxyRowIconSkip: 32px;
proxyRowSkip: 2px;
proxyRowRipple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
proxyRowRipple: defaultRippleAnimationBgOver;
proxyRowTitleFg: windowFg;
proxyRowTitlePalette: TextPalette(defaultTextPalette) {
linkFg: windowSubTextFg;
@@ -683,9 +677,7 @@ themesMenuToggle: IconButton(defaultIconButton) {
rippleAreaPosition: point(4px, 4px);
rippleAreaSize: 36px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
themesMenuPosition: point(-2px, 25px);
@@ -738,9 +730,7 @@ createPollOptionRemove: CrossButton {
duration: 150;
loadingPeriod: 1000;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
createPollOptionRemovePosition: point(11px, 9px);
createPollOptionEmojiPositionSkip: 4px;
@@ -888,6 +878,13 @@ peerListWithInviteViaLink: PeerList(peerListBox) {
peerListSingleRow: PeerList(peerListBox) {
padding: margins(0px, 0px, 0px, 0px);
}
peerListSmallSkips: PeerList(peerListBox) {
padding: margins(
0px,
defaultVerticalListSkip,
0px,
defaultVerticalListSkip);
}
scheduleHeight: 95px;
scheduleDateTop: 38px;
@@ -951,9 +948,7 @@ sponsoredUrlButton: RoundButton(defaultActiveButton) {
textTop: 7px;
style: defaultTextStyle;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
requestPeerRestriction: FlatLabel(defaultFlatLabel) {

View File

@@ -238,7 +238,7 @@ EditCaptionBox::EditCaptionBox(
Fn<void()> saved)
: _controller(controller)
, _historyItem(item)
, _isAllowedEditMedia(item->media() && item->media()->allowsEditMedia())
, _isAllowedEditMedia(item->allowsEditMedia())
, _albumType(ComputeAlbumType(item))
, _controls(base::make_unique_q<Ui::VerticalLayout>(this))
, _scroll(base::make_unique_q<Ui::ScrollArea>(this, st::boxScroll))
@@ -253,8 +253,8 @@ EditCaptionBox::EditCaptionBox(
, _initialText(std::move(text))
, _initialList(std::move(list))
, _saved(std::move(saved)) {
Expects(item->media() != nullptr);
Expects(item->media()->allowsEditCaption());
Expects(!_initialList.files.empty());
Expects(!item->media() || item->media()->allowsEditCaption());
_mediaEditManager.start(item, spoilered, invertCaption);
@@ -422,7 +422,8 @@ void EditCaptionBox::prepare() {
setInitialText();
if (!setPreparedList(std::move(_initialList))) {
rebuildPreview();
crl::on_main(this, [=] { closeBox(); });
return;
}
setupEditEventHandler();
SetupShadowsToScrollContent(this, _scroll, _contentHeight.events());

View File

@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/prepare_short_info_box.h"
#include "boxes/peers/replace_boost_box.h" // BoostsForGift.
#include "boxes/premium_preview_box.h" // ShowPremiumPreviewBox.
#include "boxes/star_gift_box.h" // ShowStarGiftBox.
#include "data/data_boosts.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
@@ -123,7 +124,8 @@ namespace {
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> controller,
PeerId id) {
PeerId id,
bool withSendGiftButton = false) {
auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data();
@@ -134,15 +136,40 @@ namespace {
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(raw, peer, st);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
peer->name(),
withSendGiftButton ? peer->shortName() : peer->name(),
st::giveawayGiftCodeValue);
raw->widthValue(
) | rpl::start_with_next([=](int width) {
const auto send = withSendGiftButton
? Ui::CreateChild<Ui::RoundButton>(
raw,
tr::lng_gift_send_small(),
st::starGiftSmallButton)
: nullptr;
if (send) {
send->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
send->setClickedCallback([=] {
Ui::ShowStarGiftBox(controller->parentController(), peer);
});
}
rpl::combine(
raw->widthValue(),
send ? send->widthValue() : rpl::single(0)
) | rpl::start_with_next([=](int width, int sendWidth) {
const auto position = st::giveawayGiftCodeNamePosition;
label->resizeToNaturalWidth(width - position.x());
const auto sendSkip = sendWidth
? (st::normalFont->spacew + sendWidth)
: 0;
label->resizeToNaturalWidth(width - position.x() - sendSkip);
label->moveToLeft(position.x(), position.y(), width);
const auto top = (raw->height() - userpic->height()) / 2;
userpic->moveToLeft(0, top, width);
if (send) {
send->moveToLeft(
position.x() + label->width() + st::normalFont->spacew,
(position.y()
+ st::giveawayGiftCodeValue.style.font->ascent
- st::starGiftSmallButton.style.font->ascent),
width);
}
}, label->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
@@ -210,14 +237,82 @@ void AddTableRow(
valueMargins);
}
object_ptr<Ui::RpWidget> MakeStarGiftStarsValue(
not_null<QWidget*> parent,
not_null<Window::SessionNavigation*> controller,
const Data::CreditsHistoryEntry &entry,
Fn<void()> convertToStars) {
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
const auto session = &controller->session();
const auto makeContext = [session](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto star = session->data().customEmojiManager().creditsEmoji();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
raw,
rpl::single(
star.append(' ' + Lang::FormatCountDecimal(entry.credits))),
st::giveawayGiftCodeValue,
st::defaultPopupMenu,
std::move(makeContext));
const auto convert = convertToStars
? Ui::CreateChild<Ui::RoundButton>(
raw,
tr::lng_gift_sell_small(
lt_count_decimal,
rpl::single(entry.convertStars * 1.)),
st::starGiftSmallButton)
: nullptr;
if (convert) {
convert->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
convert->setClickedCallback(std::move(convertToStars));
}
rpl::combine(
raw->widthValue(),
convert ? convert->widthValue() : rpl::single(0)
) | rpl::start_with_next([=](int width, int convertWidth) {
const auto convertSkip = convertWidth
? (st::normalFont->spacew + convertWidth)
: 0;
label->resizeToNaturalWidth(width - convertSkip);
label->moveToLeft(0, 0, width);
if (convert) {
convert->moveToLeft(
label->width() + st::normalFont->spacew,
(st::giveawayGiftCodeValue.style.font->ascent
- st::starGiftSmallButton.style.font->ascent),
width);
}
}, label->lifetime());
label->heightValue() | rpl::start_with_next([=](int height) {
raw->resize(
raw->width(),
height + st::giveawayGiftCodeValueMargin.bottom());
}, raw->lifetime());
label->setAttribute(Qt::WA_TransparentForMouseEvents);
return result;
}
not_null<Ui::FlatLabel*> AddTableRow(
not_null<Ui::TableLayout*> table,
rpl::producer<QString> label,
rpl::producer<TextWithEntities> value) {
rpl::producer<TextWithEntities> value,
const Fn<std::any(Fn<void()>)> &makeContext = nullptr) {
auto widget = object_ptr<Ui::FlatLabel>(
table,
std::move(value),
st::giveawayGiftCodeValue);
st::giveawayGiftCodeValue,
st::defaultPopupMenu,
std::move(makeContext));
const auto result = widget.data();
AddTableRow(
table,
@@ -939,26 +1034,56 @@ void ResolveGiveawayInfo(
void AddStarGiftTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry) {
const Data::CreditsHistoryEntry &entry,
Fn<void()> convertToStars) {
auto table = container->add(
object_ptr<Ui::TableLayout>(
container,
st::giveawayGiftCodeTable),
st::giveawayGiftCodeTableMargin);
const auto peerId = PeerId(entry.barePeerId);
const auto session = &controller->session();
if (peerId) {
const auto withSendButton = entry.in;
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
controller,
peerId);
} else {
MakePeerTableValue(table, controller, peerId, withSendButton),
st::giveawayGiftCodePeerMargin);
} else if (!entry.soldOutInfo) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_peer_in(),
MakeHiddenPeerTableValue(table, controller),
st::giveawayGiftCodePeerMargin);
}
if (!entry.firstSaleDate.isNull()) {
AddTableRow(
table,
tr::lng_gift_link_label_first_sale(),
rpl::single(Ui::Text::WithEntities(
langDateTime(entry.firstSaleDate))));
}
if (!entry.lastSaleDate.isNull()) {
AddTableRow(
table,
tr::lng_gift_link_label_last_sale(),
rpl::single(Ui::Text::WithEntities(
langDateTime(entry.lastSaleDate))));
}
{
const auto margin = st::giveawayGiftCodeValueMargin
- QMargins(0, 0, 0, st::giveawayGiftCodeValueMargin.bottom());
AddTableRow(
table,
tr::lng_gift_link_label_value(),
MakeStarGiftStarsValue(
table,
controller,
entry,
std::move(convertToStars)),
margin);
}
if (!entry.date.isNull()) {
AddTableRow(
table,
@@ -967,14 +1092,14 @@ void AddStarGiftTable(
}
if (entry.limitedCount > 0) {
auto amount = rpl::single(TextWithEntities{
QString::number(entry.limitedCount)
Lang::FormatCountDecimal(entry.limitedCount)
});
AddTableRow(
table,
tr::lng_gift_availability(),
((entry.limitedLeft > 0)
? tr::lng_gift_availability_left(
lt_count,
lt_count_decimal,
rpl::single(entry.limitedLeft * 1.),
lt_amount,
std::move(amount),
@@ -985,7 +1110,6 @@ void AddStarGiftTable(
Ui::Text::WithEntities)));
}
if (!entry.description.empty()) {
const auto session = &controller->session();
const auto makeContext = [=](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
@@ -1138,6 +1262,14 @@ void AddCreditsHistoryEntryTable(
std::move(label),
st::giveawayGiftCodeValueMargin);
}
if (entry.floodSkip) {
AddTableRow(
table,
tr::lng_credits_box_history_entry_floodskip_row(),
rpl::single(
Ui::Text::WithEntities(
Lang::FormatCountDecimal(entry.floodSkip))));
}
if (!entry.date.isNull()) {
AddTableRow(
table,

View File

@@ -57,7 +57,8 @@ void ResolveGiveawayInfo(
void AddStarGiftTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,
const Data::CreditsHistoryEntry &entry);
const Data::CreditsHistoryEntry &entry,
Fn<void()> convertToStars);
void AddCreditsHistoryEntryTable(
not_null<Window::SessionNavigation*> controller,
not_null<Ui::VerticalLayout*> container,

View File

@@ -1944,6 +1944,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
}
}
if (_controller->overrideKeyboardNavigation(
direction,
_selected.index.value,
newSelectedIndex)) {
return { _selected.index.value, _selected.index.value };
}
_selected.index.value = newSelectedIndex;
_selected.element = 0;
if (newSelectedIndex >= 0) {

View File

@@ -357,6 +357,8 @@ public:
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
virtual void peerListSelectSkip(int direction) = 0;
virtual void peerListPressLeftToContextMenu(bool shown) = 0;
virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;
@@ -573,6 +575,13 @@ public:
Unexpected("PeerListController::customRowRippleMaskGenerator.");
}
virtual bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) {
return false;
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
@@ -1016,6 +1025,10 @@ public:
bool highlightRow,
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
void peerListSelectSkip(int direction) override {
_content->selectSkip(direction);
}
void peerListPressLeftToContextMenu(bool shown) override {
_content->pressLeftToContextMenu(shown);
}

View File

@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_credits.h"
#include "api/api_peer_photo.h"
#include "api/api_statistics.h"
#include "api/api_user_names.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
@@ -46,6 +47,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/admin_log/history_admin_log_section.h"
#include "info/bot/earn/info_bot_earn_widget.h"
#include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "info/channel_statistics/earn/earn_icons.h"
#include "info/channel_statistics/earn/info_channel_earn_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
@@ -352,7 +356,8 @@ private:
void fillPendingRequestsButton();
void fillBotUsernamesButton();
void fillBotBalanceButton();
void fillBotCurrencyButton();
void fillBotCreditsButton();
void fillBotEditIntroButton();
void fillBotEditCommandsButton();
void fillBotEditSettingsButton();
@@ -1174,7 +1179,8 @@ void Controller::fillManageSection() {
::AddSkip(container, 0);
fillBotUsernamesButton();
fillBotBalanceButton();
fillBotCurrencyButton();
fillBotCreditsButton();
fillBotEditIntroButton();
fillBotEditCommandsButton();
fillBotEditSettingsButton();
@@ -1583,7 +1589,72 @@ void Controller::fillBotUsernamesButton() {
{ &st::menuIconLinks });
}
void Controller::fillBotBalanceButton() {
void Controller::fillBotCurrencyButton() {
Expects(_isBot);
struct State final {
rpl::variable<QString> balance;
};
auto &lifetime = _controls.buttonsLayout->lifetime();
const auto state = lifetime.make_state<State>();
const auto format = [=](uint64 balance) {
return Info::ChannelEarn::MajorPart(balance)
+ Info::ChannelEarn::MinorPart(balance);
};
const auto was = _peer->session().credits().balanceCurrency(
_peer->id);
if (was) {
state->balance = format(was);
}
const auto wrap = _controls.buttonsLayout->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
_controls.buttonsLayout,
EditPeerInfoBox::CreateButton(
_controls.buttonsLayout,
tr::lng_manage_peer_bot_balance_currency(),
state->balance.value(),
[controller = _navigation->parentController(), peer = _peer] {
controller->showSection(Info::ChannelEarn::Make(peer));
},
st::manageGroupButton,
{})));
wrap->toggle(!state->balance.current().isEmpty(), anim::type::instant);
const auto button = wrap->entity();
{
const auto currencyLoad
= button->lifetime().make_state<Api::EarnStatistics>(_peer);
currencyLoad->request(
) | rpl::start_with_error_done([=](const QString &error) {
}, [=] {
const auto balance = currencyLoad->data().currentBalance;
if (balance) {
wrap->toggle(true, anim::type::normal);
}
state->balance = format(balance);
}, button->lifetime());
}
{
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
icon->resize(st::menuIconLinks.size());
const auto image = Ui::Earn::MenuIconCurrency(icon->size());
icon->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(icon);
p.drawImage(0, 0, image);
}, icon->lifetime());
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
icon->moveToLeft(
button->st().iconLeft,
(size.height() - icon->height()) / 2);
}, icon->lifetime());
}
}
void Controller::fillBotCreditsButton() {
Expects(_isBot);
struct State final {
@@ -1593,7 +1664,7 @@ void Controller::fillBotBalanceButton() {
auto &lifetime = _controls.buttonsLayout->lifetime();
const auto state = lifetime.make_state<State>();
if (const auto balance = _peer->session().credits().balance(_peer->id)) {
state->balance = QString::number(balance);
state->balance = Lang::FormatCountDecimal(balance);
}
const auto wrap = _controls.buttonsLayout->add(
@@ -1601,7 +1672,7 @@ void Controller::fillBotBalanceButton() {
_controls.buttonsLayout,
EditPeerInfoBox::CreateButton(
_controls.buttonsLayout,
tr::lng_manage_peer_bot_balance(),
tr::lng_manage_peer_bot_balance_credits(),
state->balance.value(),
[controller = _navigation->parentController(), peer = _peer] {
controller->showSection(Info::BotEarn::Make(peer));
@@ -1618,46 +1689,22 @@ void Controller::fillBotBalanceButton() {
if (data.balance) {
wrap->toggle(true, anim::type::normal);
}
state->balance = QString::number(data.balance);
state->balance = Lang::FormatCountDecimal(data.balance);
});
}
{
constexpr auto kSizeShift = 3;
constexpr auto kStrokeWidth = 5;
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
icon->resize(Size(st::menuIconLinks.width() - kSizeShift));
auto colorized = [&] {
auto f = QFile(Ui::Premium::Svg());
if (!f.open(QIODevice::ReadOnly)) {
return QString();
}
return QString::fromUtf8(
f.readAll()).replace(u"#fff"_q, u"#ffffff00"_q);
}();
colorized.replace(
u"stroke=\"none\""_q,
u"stroke=\"%1\""_q.arg(st::menuIconColor->c.name()));
colorized.replace(
u"stroke-width=\"1\""_q,
u"stroke-width=\"%1\""_q.arg(kStrokeWidth));
const auto svg = icon->lifetime().make_state<QSvgRenderer>(
colorized.toUtf8());
svg->setViewBox(svg->viewBox() + Margins(kStrokeWidth));
const auto starSize = Size(icon->height());
icon->paintRequest(
) | rpl::start_with_next([=] {
const auto image = Ui::Earn::MenuIconCredits();
icon->resize(image.size() / style::DevicePixelRatio());
icon->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(icon);
svg->render(&p, Rect(starSize));
p.drawImage(0, 0, image);
}, icon->lifetime());
button->sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
icon->moveToLeft(
button->st().iconLeft + kSizeShift / 2.,
button->st().iconLeft,
(size.height() - icon->height()) / 2);
}, icon->lifetime());
}

View File

@@ -956,12 +956,11 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
Ui::AddSkip(content);
Ui::AddSkip(content);
const auto &stUser = st::boostReplaceUserpic;
const auto photoSize = st::boostReplaceUserpic.photoSize;
const auto session = &row->peer()->session();
content->add(object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::UserpicButton>(content, channel, stUser))
)->setAttribute(Qt::WA_TransparentForMouseEvents);
Settings::SubscriptionUserpic(content, channel, photoSize)));
Ui::AddSkip(content);
Ui::AddSkip(content);

View File

@@ -534,15 +534,16 @@ void PeerShortInfoCover::handleStreamingUpdate(
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [&](const PreloadedVideo &update) {
}, [&](const UpdateVideo &update) {
}, [](PreloadedVideo) {
}, [&](UpdateVideo update) {
_videoPosition = update.position;
_widget->update();
}, [&](const PreloadedAudio &update) {
}, [&](const UpdateAudio &update) {
}, [&](const WaitingForData &update) {
}, [&](MutedByOther) {
}, [&](Finished) {
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/event_filter.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "api/api_premium.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/send_credits_box.h"
@@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/data_credits.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
@@ -40,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_credits.h"
#include "settings/settings_credits_graphics.h"
#include "settings/settings_premium.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
@@ -213,7 +216,7 @@ auto GenerateGiftMedia(
return tr::lng_action_gift_got_stars_text(
tr::now,
lt_count,
gift.convertStars,
gift.info.convertStars,
Ui::Text::RichLangValue);
});
auto description = data.text.empty()
@@ -280,7 +283,7 @@ void ShowSentToast(
return tr::lng_gift_sent_about(
tr::now,
lt_count,
gift.stars,
gift.info.stars,
Ui::Text::RichLangValue);
});
const auto strong = window->showToast({
@@ -338,7 +341,10 @@ void PreviewWrap::prepare(rpl::producer<GiftDetails> details) {
const auto cost = v::match(descriptor, [&](GiftTypePremium data) {
return FillAmountAndCurrency(data.cost, data.currency, true);
}, [&](GiftTypeStars data) {
return tr::lng_gift_stars_title(tr::now, lt_count, data.stars);
return tr::lng_gift_stars_title(
tr::now,
lt_count,
data.info.stars);
});
const auto text = tr::lng_action_gift_received(
tr::now,
@@ -508,14 +514,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
const auto &gifts = api->starGifts();
list.reserve(gifts.size());
for (auto &gift : gifts) {
list.push_back({
.id = gift.id,
.stars = gift.stars,
.convertStars = gift.convertStars,
.document = gift.document,
.limitedCount = gift.limitedCount,
.limitedLeft = gift.limitedLeft,
});
list.push_back({ .info = gift });
}
auto &map = Map[session];
if (map.last != list) {
@@ -587,7 +586,8 @@ struct GiftPriceTabs {
auto sameKey = 0;
for (const auto &gift : gifts) {
if (same) {
const auto key = gift.stars * (gift.limitedCount ? -1 : 1);
const auto key = gift.info.stars
* (gift.info.limitedCount ? -1 : 1);
if (!sameKey) {
sameKey = key;
} else if (sameKey != key) {
@@ -595,12 +595,12 @@ struct GiftPriceTabs {
}
}
if (gift.limitedCount
if (gift.info.limitedCount
&& (result.size() < 2 || result[1] != kPriceTabLimited)) {
result.insert(begin(result) + 1, kPriceTabLimited);
}
if (!ranges::contains(result, gift.stars)) {
result.push_back(gift.stars);
if (!ranges::contains(result, gift.info.stars)) {
result.push_back(gift.info.stars);
}
}
if (same) {
@@ -838,16 +838,38 @@ void SendGift(
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.id,
.giftId = gift.info.id,
.randomId = details.randomId,
.message = details.text,
.user = peer->asUser(),
.limitedCount = gift.limitedCount,
.limitedCount = gift.info.limitedCount,
.anonymous = details.anonymous,
}, done, processNonPanelPaymentFormFactory);
});
}
void SoldOutBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
const GiftTypeStars &gift) {
Settings::ReceiptCreditsBox(
box,
window,
Data::CreditsHistoryEntry{
.firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate),
.lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate),
.credits = uint64(gift.info.stars),
.bareGiftStickerId = gift.info.document->id,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
.limitedCount = gift.info.limitedCount,
.limitedLeft = gift.info.limitedLeft,
.soldOutInfo = true,
.gift = true,
},
Data::SubscriptionEntry());
}
void SendGiftBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> window,
@@ -873,7 +895,7 @@ void SendGiftBox(
};
}, [&](const GiftTypeStars &data) {
return Ui::CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(std::abs(data.stars)));
Lang::FormatCountDecimal(std::abs(data.info.stars)));
});
}());
@@ -1076,15 +1098,10 @@ void SendGiftBox(
button->setClickedCallback([=] {
const auto star = std::get_if<GiftTypeStars>(&descriptor);
if (star && star->limitedCount && !star->limitedLeft) {
window->showToast({
.title = tr::lng_gift_sold_out_title(tr::now),
.text = tr::lng_gift_sold_out_text(
tr::now,
lt_count_decimal,
star->limitedCount,
Ui::Text::RichLangValue),
});
if (star
&& star->info.limitedCount
&& !star->info.limitedLeft) {
window->show(Box(SoldOutBox, window, *star));
} else {
window->show(
Box(SendGiftBox, window, peer, api, descriptor));
@@ -1187,8 +1204,8 @@ void AddBlock(
) | rpl::map([=](std::vector<GiftTypeStars> &&gifts, int price) {
gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) {
return (price == kPriceTabLimited)
? (!gift.limitedCount)
: (price && gift.stars != price);
? (!gift.info.limitedCount)
: (price && gift.info.stars != price);
}), end(gifts));
return GiftsDescriptor{
gifts | ranges::to<std::vector<GiftDescriptor>>(),

View File

@@ -409,9 +409,7 @@ callRatingStar: IconButton {
icon: icon {{ "calls/call_rating", windowSubTextFg }};
iconPosition: point(-1px, -1px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 36px;
}
@@ -1410,9 +1408,7 @@ groupCallRtmpShowButton: IconButton(defaultIconButton) {
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 32px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
groupCallSettingsRtmpShowButton: IconButton(groupCallRtmpShowButton) {
ripple: groupCallRipple;

View File

@@ -73,8 +73,8 @@ private:
SourceButton _widget;
FlatLabel _label;
RoundRect _selectedRect;
RoundRect _activeRect;
Ui::RoundRect _selectedRect;
Ui::RoundRect _activeRect;
tgcalls::DesktopCaptureSource _source;
std::unique_ptr<Preview> _preview;
rpl::event_stream<> _activations;

View File

@@ -150,6 +150,8 @@ SendButton {
inner: IconButton;
record: icon;
recordOver: icon;
round: icon;
roundOver: icon;
sendDisabledFg: color;
}
@@ -331,9 +333,7 @@ stickersRemove: IconButton(defaultIconButton) {
rippleAreaSize: 40px;
rippleAreaPosition: point(0px, 0px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
stickersUndoRemove: RoundButton(defaultLightButton) {
width: -16px;
@@ -494,9 +494,7 @@ hashtagClose: IconButton {
rippleAreaPosition: point(5px, 5px);
rippleAreaSize: 20px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
stickerPanWidthMin: 64px;
@@ -898,9 +896,7 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
iconPosition: point(-1px, -1px);
rippleAreaSize: 40px;
rippleAreaPosition: point(4px, 9px);
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
height: 58px;
width: 48px;
}
@@ -927,9 +923,7 @@ historyReplyCancel: IconButton {
rippleAreaPosition: point(4px, 4px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
historyPinnedShowAll: IconButton(historyReplyCancel) {
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
@@ -1058,9 +1052,7 @@ historyAttach: IconButton(defaultIconButton) {
rippleAreaPosition: point(2px, 3px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
historyMessagesTTL: IconButtonWithText {
@@ -1082,6 +1074,13 @@ historyReplaceMedia: IconButton(historyAttach) {
color: lightButtonBgOver;
}
}
historyAddMedia: IconButton(historyAttach) {
icon: icon {{ "chat/input_attach", windowBgActive }};
iconOver: icon {{ "chat/input_attach", windowBgActive }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgOver;
}
}
historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }};
historyEmojiCircle: size(20px, 20px);
@@ -1169,6 +1168,10 @@ historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }};
historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }};
historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }};
historyRecordRoundIconPosition: point(0px, 0px);
historyRecordSendIconPosition: point(2px, 0px);
historyRecordVoiceRippleBgActive: lightButtonBgOver;
historyRecordSignalRadius: 5px;
@@ -1214,6 +1217,7 @@ historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}
historyRecordLockMargin: margins(4px, 4px, 4px, 4px);
historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }};
historyRecordLockInput: icon {{ "voice_lock/input_mic_s", historyToDownFg }};
historyRecordLockRound: icon {{ "voice_lock/input_round_s", historyToDownFg }};
historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);
historyRecordDelete: IconButton(historyAttach) {
@@ -1274,6 +1278,8 @@ historySend: SendButton {
}
record: historyRecordVoice;
recordOver: historyRecordVoiceOver;
round: historyRecordRound;
roundOver: historyRecordRoundOver;
sendDisabledFg: historyComposeIconFg;
}
@@ -1287,9 +1293,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) {
rippleAreaPosition: point(1px, 6px);
rippleAreaSize: 42px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
defaultComposeFilesField: InputField(defaultInputField) {
textMargins: margins(1px, 26px, 31px, 4px);
@@ -1349,9 +1353,7 @@ moreChatsBarClose: IconButton(defaultIconButton) {
rippleAreaPosition: point(0px, 4px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
reportReasonTopSkip: 8px;
@@ -1517,3 +1519,22 @@ pickLocationChooseOnMap: RoundButton(defaultActiveButton) {
sendGifBox: Box(defaultBox) {
shadowIgnoreBottomSkip: true;
}
processingVideoTipMaxWidth: 364px;
processingVideoTipShift: 8px;
processingVideoToast: Toast(defaultToast) {
minWidth: 32px;
maxWidth: 380px;
padding: margins(19px, 17px, 19px, 17px);
}
processingVideoPreviewSkip: 8px;
processingVideoView: RoundButton(defaultActiveButton) {
width: -24px;
height: 52px;
textTop: 17px;
textFg: mediaviewTextLinkFg;
textFgOver: mediaviewTextLinkFg;
textBg: transparent;
textBgOver: transparent;
ripple: emptyRippleAnimation;
}

View File

@@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Core {
namespace {
constexpr auto kInitialVideoQuality = 480; // Start with SD.
[[nodiscard]] WindowPosition Deserialize(const QByteArray &data) {
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_5_1);
@@ -88,6 +90,21 @@ void LogPosition(const WindowPosition &position, const QString &name) {
return RecentEmojiDocument{ id, (test == '1') };
}
[[nodiscard]] quint32 SerializeVideoQuality(Media::VideoQuality quality) {
static_assert(sizeof(Media::VideoQuality) == sizeof(uint32));
auto result = uint32();
const auto data = static_cast<const void*>(&quality);
memcpy(&result, data, sizeof(quality));
return result;
}
[[nodiscard]] Media::VideoQuality DeserializeVideoQuality(quint32 value) {
auto result = Media::VideoQuality();
const auto data = static_cast<void*>(&result);
memcpy(data, &value, sizeof(result));
return (result.height <= 4320) ? result : Media::VideoQuality();
}
} // namespace
[[nodiscard]] WindowPosition AdjustToScale(
@@ -124,7 +141,8 @@ Settings::Settings()
, _floatPlayerColumn(Window::Column::Second)
, _floatPlayerCorner(RectPart::TopRight)
, _dialogsWithChatWidthRatio(DefaultDialogsWidthRatio())
, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio()) {
, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio())
, _videoQuality({ .height = kInitialVideoQuality }) {
}
Settings::~Settings() = default;
@@ -222,7 +240,7 @@ QByteArray Settings::serialize() const {
+ Serialize::stringSize(_customFontFamily)
+ sizeof(qint32) * 3
+ Serialize::bytearraySize(_tonsiteStorageToken)
+ sizeof(qint32);
+ sizeof(qint32) * 5;
auto result = QByteArray();
result.reserve(size);
@@ -377,7 +395,11 @@ QByteArray Settings::serialize() const {
<< qint32(_systemUnlockEnabled ? 1 : 0)
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
<< _tonsiteStorageToken
<< qint32(_includeMutedCounterFolders ? 1 : 0);
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(_ivZoom.current())
<< qint32(_skipToastsInFocus ? 1 : 0)
<< qint32(_recordVideoMessages ? 1 : 0)
<< SerializeVideoQuality(_videoQuality);
}
Ensures(result.size() == size);
@@ -501,6 +523,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
QByteArray tonsiteStorageToken = _tonsiteStorageToken;
qint32 ivZoom = _ivZoom.current();
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
quint32 videoQuality = SerializeVideoQuality(_videoQuality);
stream >> themesAccentColors;
if (!stream.atEnd()) {
@@ -810,6 +836,18 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> includeMutedCounterFolders;
}
if (!stream.atEnd()) {
stream >> ivZoom;
}
if (!stream.atEnd()) {
stream >> skipToastsInFocus;
}
if (!stream.atEnd()) {
stream >> recordVideoMessages;
}
if (!stream.atEnd()) {
stream >> videoQuality;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@@ -1021,6 +1059,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
? std::optional<bool>()
: (weatherInCelsius == 1);
_tonsiteStorageToken = tonsiteStorageToken;
_ivZoom = ivZoom;
_skipToastsInFocus = (skipToastsInFocus == 1);
_recordVideoMessages = (recordVideoMessages == 1);
_videoQuality = DeserializeVideoQuality(videoQuality);
}
QString Settings::getSoundPath(const QString &key) const {
@@ -1347,6 +1389,7 @@ void Settings::resetOnLastLogout() {
_flashBounceNotify = true;
_notifyView = NotifyView::ShowPreview;
//_nativeNotifications = std::nullopt;
//_skipToastsInFocus = false;
//_notificationsCount = 3;
//_notificationsCorner = ScreenCorner::BottomRight;
_includeMutedCounter = true;
@@ -1408,6 +1451,9 @@ void Settings::resetOnLastLogout() {
_hiddenGroupCallTooltips = 0;
_storiesClickTooltipHidden = false;
_ttlVoiceClickTooltipHidden = false;
_ivZoom = 100;
_recordVideoMessages = false;
_videoQuality = {};
_recentEmojiPreload.clear();
_recentEmoji.clear();
@@ -1465,6 +1511,14 @@ void Settings::setNativeNotifications(bool value) {
: std::make_optional(value);
}
bool Settings::skipToastsInFocus() const {
return _skipToastsInFocus;
}
void Settings::setSkipToastsInFocus(bool value) {
_skipToastsInFocus = value;
}
void Settings::setTranslateButtonEnabled(bool value) {
_translateButtonEnabled = value;
}
@@ -1543,8 +1597,36 @@ auto Settings::skipTranslationLanguagesValue() const
void Settings::setRememberedDeleteMessageOnlyForYou(bool value) {
_rememberedDeleteMessageOnlyForYou = value;
}
bool Settings::rememberedDeleteMessageOnlyForYou() const {
return _rememberedDeleteMessageOnlyForYou;
}
int Settings::ivZoom() const {
return _ivZoom.current();
}
rpl::producer<int> Settings::ivZoomValue() const {
return _ivZoom.value();
}
void Settings::setIvZoom(int value) {
#ifdef Q_OS_WIN
constexpr auto kMin = 25;
constexpr auto kMax = 500;
#else
constexpr auto kMin = 30;
constexpr auto kMax = 200;
#endif
_ivZoom = std::clamp(value, kMin, kMax);
}
Media::VideoQuality Settings::videoQuality() const {
return _videoQuality;
}
void Settings::setVideoQuality(Media::VideoQuality value) {
_videoQuality = value;
}
} // namespace Core

View File

@@ -220,6 +220,9 @@ public:
[[nodiscard]] bool nativeNotifications() const;
void setNativeNotifications(bool value);
[[nodiscard]] bool skipToastsInFocus() const;
void setSkipToastsInFocus(bool value);
[[nodiscard]] int notificationsCount() const {
return _notificationsCount;
}
@@ -625,6 +628,13 @@ public:
return _floatPlayerCorner;
}
[[nodiscard]] bool recordVideoMessages() const {
return _recordVideoMessages;
}
void setRecordVideoMessages(bool value) {
_recordVideoMessages = value;
}
void updateDialogsWidthRatio(float64 ratio, bool nochat);
[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;
@@ -915,6 +925,13 @@ public:
_tonsiteStorageToken = value;
}
[[nodiscard]] int ivZoom() const;
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
void setIvZoom(int value);
[[nodiscard]] Media::VideoQuality videoQuality() const;
void setVideoQuality(Media::VideoQuality quality);
[[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
@@ -954,6 +971,7 @@ private:
bool _flashBounceNotify = true;
NotifyView _notifyView = NotifyView::ShowPreview;
std::optional<bool> _nativeNotifications;
bool _skipToastsInFocus = false;
int _notificationsCount = 3;
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
bool _includeMutedCounter = true;
@@ -1050,6 +1068,8 @@ private:
bool _systemUnlockEnabled = false;
std::optional<bool> _weatherInCelsius;
QByteArray _tonsiteStorageToken;
rpl::variable<int> _ivZoom = 100;
Media::VideoQuality _videoQuality;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
@@ -1060,6 +1080,8 @@ private:
bool _rememberedFlashBounceNotifyFromTray = false;
bool _dialogsWidthSetToZeroWithoutChat = false;
bool _recordVideoMessages = false;
QByteArray _photoEditorBrush;
};

View File

@@ -965,6 +965,17 @@ bool ShowStarsExamples(
return true;
}
bool ShowPopularAppsAbout(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
controller->show(Dialogs::PopularAppsAboutBox(controller));
return true;
}
void ExportTestChatTheme(
not_null<Window::SessionController*> controller,
not_null<const Data::CloudTheme*> theme) {
@@ -1431,6 +1442,10 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^stars_examples$"_q,
ShowStarsExamples,
},
{
u"^about_popular_apps$"_q,
ShowPopularAppsAbout,
},
};
return Result;
}

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "core/phone_click_handler.h"
#include "boxes/add_contact_box.h"
#include "core/click_handler_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
@@ -48,6 +49,9 @@ public:
void handleKeyPress(not_null<QKeyEvent*> e) override;
[[nodiscard]] QString firstName() const;
[[nodiscard]] QString lastName() const;
protected:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
@@ -130,6 +134,18 @@ ResolvePhoneAction::ResolvePhoneAction(
prepare();
}
QString ResolvePhoneAction::firstName() const {
const auto peer = _peer.current();
const auto user = peer ? peer->asUser() : nullptr;
return user ? user->firstName : QString();
}
QString ResolvePhoneAction::lastName() const {
const auto peer = _peer.current();
const auto user = peer ? peer->asUser() : nullptr;
return user ? user->lastName : QString();
}
void ResolvePhoneAction::paint(Painter &p) {
const auto selected = isSelected() && _peer.current();
const auto height = contentHeight();
@@ -314,14 +330,29 @@ void PhoneClickHandler::onClick(ClickContext context) const {
TextForMimeData::Simple(phone.trimmed()));
}, &st::menuIconCopy);
auto resolvePhoneAction = base::make_unique_q<ResolvePhoneAction>(
menu,
menu->st().menu,
phone,
controller);
if (Trim(phone) != Trim(controller->session().user()->phone())) {
menu->addAction(
tr::lng_info_add_as_contact(tr::now),
[=, raw = resolvePhoneAction.get()] {
controller->show(
Box<AddContactBox>(
_session,
raw->firstName(),
raw->lastName(),
Trim(phone)));
},
&st::menuIconInvite);
}
menu->addSeparator(&st::popupMenuExpandedSeparator.menu.separator);
menu->addAction(
base::make_unique_q<ResolvePhoneAction>(
menu,
menu->st().menu,
phone,
controller));
menu->addAction(std::move(resolvePhoneAction));
menu->popup(pos);
}

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 5006002;
constexpr auto AppVersionStr = "5.6.2";
constexpr auto AppVersion = 5007000;
constexpr auto AppVersionStr = "5.7";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -74,6 +74,11 @@ uint64 Credits::balance(PeerId peerId) const {
return (it != _cachedPeerBalances.end()) ? it->second : 0;
}
uint64 Credits::balanceCurrency(PeerId peerId) const {
const auto it = _cachedPeerCurrencyBalances.find(peerId);
return (it != _cachedPeerCurrencyBalances.end()) ? it->second : 0;
}
rpl::producer<uint64> Credits::balanceValue() const {
return _nonLockedBalance.value();
}
@@ -128,4 +133,8 @@ void Credits::apply(PeerId peerId, uint64 balance) {
_cachedPeerBalances[peerId] = balance;
}
void Credits::applyCurrency(PeerId peerId, uint64 balance) {
_cachedPeerCurrencyBalances[peerId] = balance;
}
} // namespace Data

View File

@@ -35,6 +35,9 @@ public:
[[nodiscard]] rpl::producer<float64> rateValue(
not_null<PeerData*> ownedBotOrChannel);
void applyCurrency(PeerId peerId, uint64 balance);
[[nodiscard]] uint64 balanceCurrency(PeerId peerId) const;
void lock(int count);
void unlock(int count);
void withdrawLocked(int count);
@@ -50,6 +53,7 @@ private:
std::unique_ptr<Api::CreditsStatus> _loader;
base::flat_map<PeerId, uint64> _cachedPeerBalances;
base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances;
uint64 _balance = 0;
uint64 _locked = 0;

View File

@@ -112,6 +112,7 @@ void RecentPeers::applyLocal(QByteArray serialized) {
).arg(streamAppVersion));
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
const auto streamPosition = stream.underlying().device()->pos();
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
@@ -123,6 +124,8 @@ void RecentPeers::applyLocal(QByteArray serialized) {
DEBUG_LOG(("Suggestions: Failed RecentPeers reading %1 / %2."
).arg(i + 1
).arg(count));
DEBUG_LOG(("Failed bytes: %1.").arg(
QString::fromUtf8(serialized.mid(streamPosition).toHex())));
_list.clear();
return;
}

View File

@@ -343,10 +343,20 @@ void ScheduledMessages::apply(
if (i == end(_data)) {
return;
}
for (const auto &id : update.vmessages().v) {
const auto sent = update.vsent_messages();
const auto &ids = update.vmessages().v;
for (auto k = 0, count = int(ids.size()); k != count; ++k) {
const auto id = ids[k].v;
const auto &list = i->second;
const auto j = list.itemById.find(id.v);
const auto j = list.itemById.find(id);
if (j != end(list.itemById)) {
if (sent && k < sent->v.size()) {
const auto &sentId = sent->v[k];
_session->data().sentFromScheduled({
.item = j->second,
.sentId = sentId.v,
});
}
j->second->destroy();
i = _data.find(history);
if (i == end(_data)) {

View File

@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/sponsored_message_bar.h"
#include "ui/text/text_utilities.h" // Ui::Text::RichLangValue.
namespace Data {
@@ -194,7 +195,21 @@ void SponsoredMessages::inject(
}
bool SponsoredMessages::canHaveFor(not_null<History*> history) const {
return history->peer->isChannel();
if (history->peer->isChannel()) {
return true;
} else if (const auto user = history->peer->asUser()) {
return user->isBot();
}
return false;
}
bool SponsoredMessages::isTopBarFor(not_null<History*> history) const {
if (peerIsUser(history->peer->id)) {
if (const auto user = history->peer->asUser()) {
return user->isBot();
}
}
return false;
}
void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
@@ -218,10 +233,8 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) {
}
}
}
const auto channel = history->peer->asChannel();
Assert(channel != nullptr);
request.requestId = _session->api().request(
MTPchannels_GetSponsoredMessages(channel->inputChannel)
MTPmessages_GetSponsoredMessages(history->peer->input)
).done([=](const MTPmessages_sponsoredMessages &result) {
parse(history, result);
if (done) {
@@ -257,12 +270,62 @@ void SponsoredMessages::parse(
list.postsBetween = postsBetween->v;
list.state = State::InjectToMiddle;
} else {
list.state = State::AppendToEnd;
list.state = history->peer->isChannel()
? State::AppendToEnd
: State::AppendToTopBar;
}
}, [](const MTPDmessages_sponsoredMessagesEmpty &) {
});
}
FullMsgId SponsoredMessages::fillTopBar(
not_null<History*> history,
not_null<Ui::RpWidget*> widget) {
const auto it = _data.find(history);
if (it != end(_data)) {
auto &list = it->second;
if (!list.entries.empty()) {
const auto &entry = list.entries.front();
const auto fullId = entry.itemFullId;
Ui::FillSponsoredMessageBar(
widget,
_session,
fullId,
entry.sponsored.from,
entry.sponsored.textWithEntities);
return fullId;
}
}
return {};
}
rpl::producer<> SponsoredMessages::itemRemoved(const FullMsgId &fullId) {
if (IsServerMsgId(fullId.msg) || !fullId) {
return rpl::never<>();
}
const auto history = _session->data().history(fullId.peer);
const auto it = _data.find(history);
if (it == end(_data)) {
return rpl::never<>();
}
auto &list = it->second;
const auto entryIt = ranges::find_if(list.entries, [&](const Entry &e) {
return e.itemFullId == fullId;
});
if (entryIt == end(list.entries)) {
return rpl::never<>();
}
if (!entryIt->optionalDestructionNotifier) {
entryIt->optionalDestructionNotifier
= std::make_unique<rpl::lifetime>();
entryIt->optionalDestructionNotifier->add([this, fullId] {
_itemRemoved.fire_copy(fullId);
});
}
return _itemRemoved.events(
) | rpl::filter(rpl::mappers::_1 == fullId) | rpl::to_empty;
}
void SponsoredMessages::append(
not_null<History*> history,
List &list,
@@ -283,7 +346,9 @@ void SponsoredMessages::append(
}, [&](const MTPDmessageMediaDocument &media) {
if (const auto tlDocument = media.vdocument()) {
tlDocument->match([&](const MTPDdocument &data) {
const auto d = history->owner().processDocument(data);
const auto d = history->owner().processDocument(
data,
media.valt_documents());
if (d->isVideoFile()
|| d->isSilentVideo()
|| d->isAnimation()
@@ -406,7 +471,7 @@ void SponsoredMessages::clearItems(not_null<History*> history) {
const SponsoredMessages::Entry *SponsoredMessages::find(
const FullMsgId &fullId) const {
if (!peerIsChannel(fullId.peer)) {
if (!peerIsChannel(fullId.peer) && !peerIsUser(fullId.peer)) {
return nullptr;
}
const auto history = _session->data().history(fullId.peer);
@@ -434,11 +499,11 @@ void SponsoredMessages::view(const FullMsgId &fullId) {
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
const auto channel = entryPtr->item->history()->peer->asChannel();
Assert(channel != nullptr);
request.requestId = _session->api().request(
MTPchannels_ViewSponsoredMessage(
channel->inputChannel,
MTPmessages_ViewSponsoredMessage(
entryPtr->item
? entryPtr->item->history()->peer->input
: _session->data().peer(fullId.peer)->input,
MTP_bytes(randomId))
).done([=] {
auto &request = _viewRequests[randomId];
@@ -489,14 +554,14 @@ void SponsoredMessages::clicked(
return;
}
const auto randomId = entryPtr->sponsored.randomId;
const auto channel = entryPtr->item->history()->peer->asChannel();
Assert(channel != nullptr);
using Flag = MTPchannels_ClickSponsoredMessage::Flag;
_session->api().request(MTPchannels_ClickSponsoredMessage(
using Flag = MTPmessages_ClickSponsoredMessage::Flag;
_session->api().request(MTPmessages_ClickSponsoredMessage(
MTP_flags(Flag(0)
| (isMedia ? Flag::f_media : Flag(0))
| (isFullscreen ? Flag::f_fullscreen : Flag(0))),
channel->inputChannel,
entryPtr->item
? entryPtr->item->history()->peer->input
: _session->data().peer(fullId.peer)->input,
MTP_bytes(randomId)
)).send();
}
@@ -525,11 +590,7 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
return;
}
const auto history = entry->item->history();
const auto channel = history->peer->asChannel();
if (!channel) {
return;
}
const auto history = _session->data().history(fullId.peer);
const auto erase = [=] {
const auto it = _data.find(history);
@@ -548,8 +609,8 @@ auto SponsoredMessages::createReportCallback(const FullMsgId &fullId)
}
state->requestId = _session->api().request(
MTPchannels_ReportSponsoredMessage(
channel->inputChannel,
MTPmessages_ReportSponsoredMessage(
history->peer->input,
MTP_bytes(entry->sponsored.randomId),
MTP_bytes(optionId))
).done([=](

View File

@@ -18,6 +18,10 @@ namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Data {
class MediaPreload;
@@ -76,6 +80,7 @@ public:
None,
AppendToEnd,
InjectToMiddle,
AppendToTopBar,
};
struct Details {
std::vector<TextWithEntities> info;
@@ -94,10 +99,15 @@ public:
~SponsoredMessages();
[[nodiscard]] bool canHaveFor(not_null<History*> history) const;
[[nodiscard]] bool isTopBarFor(not_null<History*> history) const;
void request(not_null<History*> history, Fn<void()> done);
void clearItems(not_null<History*> history);
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen);
[[nodiscard]] FullMsgId fillTopBar(
not_null<History*> history,
not_null<Ui::RpWidget*> widget);
[[nodiscard]] rpl::producer<> itemRemoved(const FullMsgId &);
[[nodiscard]] AppendResult append(not_null<History*> history);
void inject(
@@ -122,6 +132,7 @@ private:
FullMsgId itemFullId;
SponsoredMessage sponsored;
std::unique_ptr<MediaPreload> preload;
std::unique_ptr<rpl::lifetime> optionalDestructionNotifier;
};
struct List {
std::vector<Entry> entries;
@@ -156,6 +167,8 @@ private:
base::flat_map<not_null<History*>, Request> _requests;
base::flat_map<RandomId, Request> _viewRequests;
rpl::event_stream<FullMsgId> _itemRemoved;
rpl::lifetime _lifetime;
};

View File

@@ -300,6 +300,7 @@ void TopPeers::applyLocal(QByteArray serialized) {
_list.reserve(count);
for (auto i = 0; i != int(count); ++i) {
auto rating = quint64();
const auto streamPosition = stream.underlying().device()->pos();
const auto peer = Serialize::readPeer(
_session,
streamAppVersion,
@@ -313,6 +314,8 @@ void TopPeers::applyLocal(QByteArray serialized) {
} else {
DEBUG_LOG(("Suggestions: "
"Failed TopPeers reading %1 / %2.").arg(i + 1).arg(count));
DEBUG_LOG(("Failed bytes: %1.").arg(
QString::fromUtf8(serialized.mid(streamPosition).toHex())));
_list.clear();
return;
}

View File

@@ -46,12 +46,15 @@ struct CreditsHistoryEntry final {
Unsupported,
PremiumBot,
Ads,
API,
};
QString id;
QString title;
TextWithEntities description;
QDateTime date;
QDateTime firstSaleDate;
QDateTime lastSaleDate;
PhotoId photoId = 0;
std::vector<CreditsHistoryMedia> extended;
uint64 credits = 0;
@@ -66,10 +69,12 @@ struct CreditsHistoryEntry final {
int limitedCount = 0;
int limitedLeft = 0;
int convertStars = 0;
int floodSkip = 0;
bool converted = false;
bool anonymous = false;
bool savedToProfile = false;
bool fromGiftsList = false;
bool soldOutInfo = false;
bool reaction = false;
bool refunded = false;
bool pending = false;

View File

@@ -332,6 +332,8 @@ void DocumentData::setattributes(
validateLottieSticker();
auto wasVideoData = isVideoFile() ? std::move(_additional) : nullptr;
_videoPreloadPrefix = 0;
for (const auto &attribute : attributes) {
attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
@@ -388,11 +390,21 @@ void DocumentData::setattributes(
: VideoDocument;
if (data.is_round_message()) {
_additional = std::make_unique<RoundData>();
} else if (const auto size = data.vpreload_prefix_size()) {
if (size->v > 0 && size->v < kMaxAllowedPreloadPrefix) {
_videoPreloadPrefix = size->v;
} else {
if (const auto size = data.vpreload_prefix_size()) {
if (size->v > 0
&& size->v < kMaxAllowedPreloadPrefix) {
_videoPreloadPrefix = size->v;
}
}
_additional = wasVideoData
? std::move(wasVideoData)
: std::make_unique<VideoData>();
video()->codec = qs(
data.vvideo_codec().value_or_empty());
}
} else if (type == VideoDocument && wasVideoData) {
_additional = std::move(wasVideoData);
} else if (const auto info = sticker()) {
info->type = StickerType::Webm;
}
@@ -511,6 +523,108 @@ void DocumentData::setattributes(
}
}
void DocumentData::setVideoQualities(const QVector<MTPDocument> &list) {
auto qualities = std::vector<not_null<DocumentData*>>();
qualities.reserve(list.size());
for (const auto &document : list) {
qualities.push_back(owner().processDocument(document));
}
setVideoQualities(std::move(qualities));
}
void DocumentData::setVideoQualities(
std::vector<not_null<DocumentData*>> qualities) {
const auto data = video();
if (!data) {
return;
}
auto count = int(qualities.size());
if (qualities.empty()) {
return;
}
const auto good = [&](not_null<DocumentData*> document) {
return document->isVideoFile()
&& !document->dimensions.isEmpty()
&& !document->inappPlaybackFailed()
&& document->useStreamingLoader()
&& document->canBeStreamed(nullptr);
};
ranges::sort(
qualities,
ranges::greater(),
&DocumentData::resolveVideoQuality);
for (auto i = 0; i != count - 1;) {
const auto my = qualities[i];
const auto next = qualities[i + 1];
const auto myQuality = my->resolveVideoQuality();
const auto nextQuality = next->resolveVideoQuality();
const auto myGood = good(my);
const auto nextGood = good(next);
if (!myGood || !nextGood || myQuality == nextQuality) {
const auto removeMe = !myGood
|| (nextGood && (my->size > next->size));
const auto from = i + (removeMe ? 1 : 2);
for (auto j = from; j != count; ++j) {
qualities[j - 1] = qualities[j];
}
--count;
} else {
++i;
}
}
if (!qualities[count - 1]->resolveVideoQuality()) {
--count;
}
qualities.erase(qualities.begin() + count, qualities.end());
if (!qualities.empty()) {
if (const auto mine = resolveVideoQuality()) {
if (mine > qualities.front()->resolveVideoQuality()) {
qualities.insert(begin(qualities), this);
}
}
}
data->qualities = std::move(qualities);
}
int DocumentData::resolveVideoQuality() const {
const auto size = isVideoFile() ? dimensions : QSize();
return size.isEmpty() ? 0 : std::min(size.width(), size.height());
}
auto DocumentData::resolveQualities(HistoryItem *context) const
-> const std::vector<not_null<DocumentData*>> & {
static const auto empty = std::vector<not_null<DocumentData*>>();
const auto info = video();
const auto media = context ? context->media() : nullptr;
if (!info || !media || media->document() != this) {
return empty;
}
return media->hasQualitiesList() ? info->qualities : empty;
}
not_null<DocumentData*> DocumentData::chooseQuality(
HistoryItem *context,
Media::VideoQuality request) {
const auto &list = resolveQualities(context);
if (list.empty() || !request.height) {
return this;
}
const auto height = int(request.height);
auto closest = this;
auto closestAbs = std::abs(height - resolveVideoQuality());
auto closestSize = size;
for (const auto &quality : list) {
const auto abs = std::abs(height - quality->resolveVideoQuality());
if (abs < closestAbs
|| (abs == closestAbs && quality->size < closestSize)) {
closest = quality;
closestAbs = abs;
closestSize = quality->size;
}
}
return closest;
}
void DocumentData::validateLottieSticker() {
if (type == FileDocument
&& hasMimeType(u"application/x-tgsticker"_q)) {
@@ -1384,6 +1498,16 @@ const RoundData *DocumentData::round() const {
return const_cast<DocumentData*>(this)->round();
}
VideoData *DocumentData::video() {
return isVideoFile()
? static_cast<VideoData*>(_additional.get())
: nullptr;
}
const VideoData *DocumentData::video() const {
return const_cast<DocumentData*>(this)->video();
}
bool DocumentData::hasRemoteLocation() const {
return (_dc != 0 && _access != 0);
}

View File

@@ -31,11 +31,13 @@ struct Key;
} // namespace Storage
namespace Media {
namespace Streaming {
class Loader;
} // namespace Streaming
struct VideoQuality;
} // namespace Media
namespace Media::Streaming {
class Loader;
} // namespace Media::Streaming
namespace Data {
class Session;
class DocumentMedia;
@@ -92,6 +94,11 @@ struct VoiceData : public DocumentAdditionalData {
char wavemax = 0;
};
struct VideoData : public DocumentAdditionalData {
QString codec;
std::vector<not_null<DocumentData*>> qualities;
};
using RoundData = VoiceData;
namespace Serialize {
@@ -108,8 +115,16 @@ public:
void setattributes(
const QVector<MTPDocumentAttribute> &attributes);
void setVideoQualities(const QVector<MTPDocument> &list);
void automaticLoadSettingsChanged();
void setVideoQualities(std::vector<not_null<DocumentData*>> qualities);
[[nodiscard]] int resolveVideoQuality() const;
[[nodiscard]] auto resolveQualities(HistoryItem *context) const
-> const std::vector<not_null<DocumentData*>> &;
[[nodiscard]] not_null<DocumentData*> chooseQuality(
HistoryItem *context,
Media::VideoQuality request);
[[nodiscard]] bool loading() const;
[[nodiscard]] QString loadingFilePath() const;
@@ -161,6 +176,8 @@ public:
[[nodiscard]] const VoiceData *voice() const;
[[nodiscard]] RoundData *round();
[[nodiscard]] const RoundData *round() const;
[[nodiscard]] VideoData *video();
[[nodiscard]] const VideoData *video() const;
void forceIsStreamedAnimation();
[[nodiscard]] bool isVoiceMessage() const;

View File

@@ -87,6 +87,7 @@ struct FileReferenceAccumulator {
push(data.vphoto());
}, [&](const MTPDmessageMediaDocument &data) {
push(data.vdocument());
push(data.valt_documents());
}, [&](const MTPDmessageMediaWebPage &data) {
push(data.vwebpage());
}, [&](const MTPDmessageMediaGame &data) {

View File

@@ -551,6 +551,10 @@ DocumentData *Media::document() const {
return nullptr;
}
bool Media::hasQualitiesList() const {
return false;
}
PhotoData *Media::photo() const {
return nullptr;
}
@@ -964,12 +968,14 @@ MediaFile::MediaFile(
not_null<HistoryItem*> parent,
not_null<DocumentData*> document,
bool skipPremiumEffect,
bool hasQualitiesList,
bool spoiler,
crl::time ttlSeconds)
: Media(parent)
, _document(document)
, _emoji(document->sticker() ? document->sticker()->alt : QString())
, _skipPremiumEffect(skipPremiumEffect)
, _hasQualitiesList(hasQualitiesList)
, _spoiler(spoiler)
, _ttlSeconds(ttlSeconds) {
parent->history()->owner().registerDocumentItem(_document, parent);
@@ -999,6 +1005,7 @@ std::unique_ptr<Media> MediaFile::clone(not_null<HistoryItem*> parent) {
parent,
_document,
!_document->session().premium(),
_hasQualitiesList,
_spoiler,
_ttlSeconds);
}
@@ -1007,6 +1014,10 @@ DocumentData *MediaFile::document() const {
return _document;
}
bool MediaFile::hasQualitiesList() const {
return _hasQualitiesList;
}
bool MediaFile::uploading() const {
return _document->uploading();
}

View File

@@ -165,6 +165,7 @@ public:
virtual std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) = 0;
virtual DocumentData *document() const;
virtual bool hasQualitiesList() const;
virtual PhotoData *photo() const;
virtual WebPageData *webpage() const;
virtual MediaWebPageFlags webpageFlags() const;
@@ -287,6 +288,7 @@ public:
not_null<HistoryItem*> parent,
not_null<DocumentData*> document,
bool skipPremiumEffect,
bool hasQualitiesList,
bool spoiler,
crl::time ttlSeconds);
~MediaFile();
@@ -294,6 +296,7 @@ public:
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
DocumentData *document() const override;
bool hasQualitiesList() const override;
bool uploading() const override;
Storage::SharedMediaTypesMask sharedMediaTypes() const override;
@@ -324,6 +327,7 @@ private:
not_null<DocumentData*> _document;
QString _emoji;
bool _skipPremiumEffect = false;
bool _hasQualitiesList = false;
bool _spoiler = false;
// Video (unsupported) / Voice / Round.

View File

@@ -40,6 +40,25 @@ std::vector<ReactionId> SearchTagsFromQuery(
return result;
}
HashtagWithUsername HashtagWithUsernameFromQuery(QStringView query) {
const auto match = TextUtilities::RegExpHashtag(true).match(query);
if (match.hasMatch()) {
const auto username = match.capturedView(2).mid(1).toString();
const auto offset = int(match.capturedLength(1));
const auto full = int(query.size());
const auto length = full
- int(username.size())
- 1
- offset
- int(match.capturedLength(3));
if (!username.isEmpty() && length > 0 && offset + length <= full) {
const auto hashtag = query.mid(offset, length).toString();
return { hashtag, username };
}
}
return {};
}
QString ReactionEntityData(const ReactionId &id) {
if (id.empty()) {
return {};

View File

@@ -65,6 +65,13 @@ struct MessageReaction {
[[nodiscard]] std::vector<ReactionId> SearchTagsFromQuery(
const QString &query);
struct HashtagWithUsername {
QString hashtag;
QString username;
};
[[nodiscard]] HashtagWithUsername HashtagWithUsernameFromQuery(
QStringView query);
[[nodiscard]] QString ReactionEntityData(const ReactionId &id);
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);

View File

@@ -156,6 +156,18 @@ struct FullMsgId {
MsgId msg = 0;
};
#ifdef _DEBUG
inline QDebug operator<<(QDebug debug, const FullMsgId &fullMsgId) {
debug.nospace()
<< "FullMsgId(peer: "
<< fullMsgId.peer.value
<< ", msg: "
<< fullMsgId.msg.bare
<< ")";
return debug;
}
#endif // _DEBUG
Q_DECLARE_METATYPE(FullMsgId);
struct FullReplyTo {

View File

@@ -3139,17 +3139,24 @@ not_null<DocumentData*> Session::document(DocumentId id) {
return i->second.get();
}
not_null<DocumentData*> Session::processDocument(const MTPDocument &data) {
not_null<DocumentData*> Session::processDocument(
const MTPDocument &data,
const MTPVector<MTPDocument> *qualities) {
return data.match([&](const MTPDdocument &data) {
return processDocument(data);
return processDocument(data, qualities);
}, [&](const MTPDdocumentEmpty &data) {
return document(data.vid().v);
});
}
not_null<DocumentData*> Session::processDocument(const MTPDdocument &data) {
not_null<DocumentData*> Session::processDocument(
const MTPDdocument &data,
const MTPVector<MTPDocument> *qualities) {
const auto result = document(data.vid().v);
documentApplyFields(result, data);
if (qualities) {
result->setVideoQualities(qualities->v);
}
return result;
}
@@ -4806,6 +4813,22 @@ void Session::viewTagsChanged(
}
}
void Session::sentToScheduled(SentToScheduled value) {
_sentToScheduled.fire(std::move(value));
}
rpl::producer<SentToScheduled> Session::sentToScheduled() const {
return _sentToScheduled.events();
}
void Session::sentFromScheduled(SentFromScheduled value) {
_sentFromScheduled.fire(std::move(value));
}
rpl::producer<SentFromScheduled> Session::sentFromScheduled() const {
return _sentFromScheduled.events();
}
void Session::clearLocalStorage() {
_cache->close();
_cache->clear();

View File

@@ -89,6 +89,15 @@ struct GiftUpdate {
Action action = {};
};
struct SentToScheduled {
not_null<History*> history;
MsgId scheduledId = 0;
};
struct SentFromScheduled {
not_null<HistoryItem*> item;
MsgId sentId = 0;
};
class Session final {
public:
using ViewElement = HistoryView::Element;
@@ -558,8 +567,12 @@ public:
const ImageLocation &thumbnailLocation);
[[nodiscard]] not_null<DocumentData*> document(DocumentId id);
not_null<DocumentData*> processDocument(const MTPDocument &data);
not_null<DocumentData*> processDocument(const MTPDdocument &data);
not_null<DocumentData*> processDocument(
const MTPDocument &data,
const MTPVector<MTPDocument> *qualities = nullptr);
not_null<DocumentData*> processDocument(
const MTPDdocument &data,
const MTPVector<MTPDocument> *qualities = nullptr);
not_null<DocumentData*> processDocument(
const MTPdocument &data,
const ImageWithLocation &thumbnail);
@@ -787,6 +800,11 @@ public:
std::vector<ReactionId> &&was,
std::vector<ReactionId> &&now);
void sentToScheduled(SentToScheduled value);
[[nodiscard]] rpl::producer<SentToScheduled> sentToScheduled() const;
void sentFromScheduled(SentFromScheduled value);
[[nodiscard]] rpl::producer<SentFromScheduled> sentFromScheduled() const;
void clearLocalStorage();
private:
@@ -959,6 +977,8 @@ private:
rpl::event_stream<ChatListEntryRefresh> _chatListEntryRefreshes;
rpl::event_stream<> _unreadBadgeChanges;
rpl::event_stream<RepliesReadTillUpdate> _repliesReadTillUpdates;
rpl::event_stream<SentToScheduled> _sentToScheduled;
rpl::event_stream<SentFromScheduled> _sentFromScheduled;
Dialogs::MainList _chatsList;
Dialogs::IndexedList _contactsList;

View File

@@ -62,7 +62,9 @@ using UpdateFlag = StoryUpdate::Flag;
}, [&](const MTPDmessageMediaDocument &data)
-> std::optional<StoryMedia> {
if (const auto document = data.vdocument()) {
const auto result = owner->processDocument(*document);
const auto result = owner->processDocument(
*document,
data.valt_documents());
if (!result->isNull()
&& (result->isGifv() || result->isVideoFile())) {
result->setStoryMedia(true);

View File

@@ -41,6 +41,43 @@ bool PruneDestroyedAndSet(
return result;
}
[[nodiscard]] auto LookupOtherQualities(
DocumentData *original,
not_null<DocumentData*> quality,
HistoryItem *context)
-> std::vector<Media::Streaming::QualityDescriptor> {
if (!original || !context) {
return {};
}
auto qualities = original->resolveQualities(context);
if (qualities.empty()) {
return {};
}
auto result = std::vector<Media::Streaming::QualityDescriptor>();
result.reserve(qualities.size());
for (const auto &video : qualities) {
if (video != quality) {
if (const auto height = video->resolveVideoQuality()) {
result.push_back({
.sizeInBytes = uint32(video->size),
.height = uint32(height),
});
}
}
}
return result;
}
[[nodiscard]] auto LookupOtherQualities(
DocumentData *original,
not_null<PhotoData*> quality,
HistoryItem *context)
-> std::vector<Media::Streaming::QualityDescriptor> {
Expects(!original);
return {};
}
} // namespace
Streaming::Streaming(not_null<Session*> owner)
@@ -50,7 +87,6 @@ Streaming::Streaming(not_null<Session*> owner)
Streaming::~Streaming() = default;
template <typename Data>
[[nodiscard]] std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
base::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,
@@ -84,10 +120,16 @@ template <typename Data>
base::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,
base::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,
not_null<Data*> data,
DocumentData *original,
HistoryItem *context,
FileOrigin origin) {
auto otherQualities = LookupOtherQualities(original, data, context);
const auto i = documents.find(data);
if (i != end(documents)) {
if (auto result = i->second.lock()) {
if (!otherQualities.empty()) {
result->setOtherQualities(std::move(otherQualities));
}
return result;
}
}
@@ -95,7 +137,10 @@ template <typename Data>
if (!reader) {
return nullptr;
}
auto result = std::make_shared<Document>(data, std::move(reader));
auto result = std::make_shared<Document>(
data,
std::move(reader),
std::move(otherQualities));
if (!PruneDestroyedAndSet(documents, data, result)) {
documents.emplace_or_assign(data, result);
}
@@ -136,7 +181,27 @@ std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<DocumentData*> document,
FileOrigin origin) {
return sharedDocument(_fileDocuments, _fileReaders, document, origin);
return sharedDocument(
_fileDocuments,
_fileReaders,
document,
nullptr,
nullptr,
origin);
}
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<DocumentData*> quality,
not_null<DocumentData*> original,
HistoryItem *context,
FileOrigin origin) {
return sharedDocument(
_fileDocuments,
_fileReaders,
quality,
original,
context,
origin);
}
std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
@@ -149,7 +214,13 @@ std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<PhotoData*> photo,
FileOrigin origin) {
return sharedDocument(_photoDocuments, _photoReaders, photo, origin);
return sharedDocument(
_photoDocuments,
_photoReaders,
photo,
nullptr,
nullptr,
origin);
}
void Streaming::keepAlive(not_null<DocumentData*> document) {

View File

@@ -12,12 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class PhotoData;
class DocumentData;
namespace Media {
namespace Streaming {
namespace Media::Streaming {
class Reader;
class Document;
} // namespace Streaming
} // namespace Media
} // namespace Media::Streaming
namespace Data {
@@ -41,6 +39,11 @@ public:
[[nodiscard]] std::shared_ptr<Document> sharedDocument(
not_null<DocumentData*> document,
FileOrigin origin);
[[nodiscard]] std::shared_ptr<Document> sharedDocument(
not_null<DocumentData*> quality,
not_null<DocumentData*> original,
HistoryItem *context,
FileOrigin origin);
[[nodiscard]] std::shared_ptr<Reader> sharedReader(
not_null<PhotoData*> photo,
@@ -68,6 +71,8 @@ private:
base::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,
base::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,
not_null<Data*> data,
DocumentData *original,
HistoryItem *context,
FileOrigin origin);
template <typename Data>

View File

@@ -327,6 +327,8 @@ enum class MessageFlag : uint64 {
SensitiveContent = (1ULL << 47),
HasRestrictions = (1ULL << 48),
EstimatedDate = (1ULL << 49),
};
inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>;

View File

@@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_user.h"
#include "api/api_credits.h"
#include "api/api_sensitive_content.h"
#include "api/api_statistics.h"
#include "storage/localstorage.h"
#include "storage/storage_user_photos.h"
#include "main/main_session.h"
#include "data/business/data_business_common.h"
#include "data/business/data_business_info.h"
#include "data/components/credits.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_peer_bot_command.h"
@@ -635,6 +638,35 @@ void ApplyUserUpdate(not_null<UserData*> user, const MTPDuserFull &update) {
user,
Data::PeerUpdate::Flag::Rights);
}
if (info->canEditInformation) {
const auto id = user->id;
const auto weak = base::make_weak(&user->session());
const auto creditsLoadLifetime
= std::make_shared<rpl::lifetime>();
const auto creditsLoad
= creditsLoadLifetime->make_state<Api::CreditsStatus>(user);
creditsLoad->request({}, [=](Data::CreditsStatusSlice slice) {
if (const auto strong = weak.get()) {
strong->credits().apply(id, slice.balance);
creditsLoadLifetime->destroy();
}
});
const auto currencyLoadLifetime
= std::make_shared<rpl::lifetime>();
const auto currencyLoad
= currencyLoadLifetime->make_state<Api::EarnStatistics>(user);
currencyLoad->request(
) | rpl::start_with_error_done([=](const QString &error) {
currencyLoadLifetime->destroy();
}, [=] {
if (const auto strong = weak.get()) {
strong->credits().applyCurrency(
id,
currencyLoad->data().currentBalance);
currencyLoadLifetime->destroy();
}
}, *currencyLoadLifetime);
}
}
if (const auto paper = update.vwallpaper()) {

View File

@@ -222,9 +222,7 @@ dialogsMenuToggle: IconButton {
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
ripple: defaultRippleAnimationBgOver;
}
dialogsMenuToggleUnread: icon {
{ "dialogs/dialogs_menu_unread", dialogsMenuIconFg },
@@ -745,3 +743,7 @@ dialogsSearchTagPromoLeft: 6px;
dialogsSearchTagPromoRight: 1px;
dialogsSearchTagPromoSkip: 6px;
dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);
dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {
minWidth: 128px;
}

View File

@@ -88,6 +88,7 @@ namespace {
constexpr auto kHashtagResultsLimit = 5;
constexpr auto kStartReorderThreshold = 30;
constexpr auto kQueryPreviewLimit = 32;
constexpr auto kPreviewPostsLimit = 3;
[[nodiscard]] int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {
auto result = 0;
@@ -555,7 +556,7 @@ int InnerWidget::searchInChatSkip() const {
return _searchIn ? _searchIn->height() : 0;
}
int InnerWidget::searchedOffset() const {
int InnerWidget::previewOffset() const {
auto result = peerSearchOffset();
if (!_peerSearchResults.empty()) {
result += (_peerSearchResults.size() * st::dialogsRowHeight)
@@ -564,6 +565,15 @@ int InnerWidget::searchedOffset() const {
return result;
}
int InnerWidget::searchedOffset() const {
auto result = previewOffset();
if (!_previewResults.empty()) {
result += (_previewResults.size() * st::dialogsRowHeight)
+ st::searchedBarHeight;
}
return result;
}
void InnerWidget::changeOpenedFolder(Data::Folder *folder) {
Expects(!folder || !_savedSublists);
@@ -810,7 +820,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
}
if (_searchIn) {
p.translate(0, searchInChatSkip());
if (_searchResults.empty()) {
if (_previewResults.empty() && _searchResults.empty()) {
p.fillRect(0, 0, fullWidth, st::lineWidth, st::shadowFg);
}
}
@@ -924,7 +934,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
}
const auto showUnreadInSearchResults = uniqueSearchResults();
if (_searchResults.empty()) {
if (_previewResults.empty() && _searchResults.empty()) {
if (_loadingAnimation) {
const auto text = tr::lng_contacts_loading(tr::now);
p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);
@@ -933,7 +943,68 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text);
p.translate(0, st::searchedBarHeight);
}
} else {
return;
}
if (!_previewResults.empty()) {
const auto text = tr::lng_search_tab_public_posts(tr::now);
p.fillRect(0, 0, fullWidth, st::searchedBarHeight, st::searchedBarBg);
p.setFont(st::searchedBarFont);
p.setPen(st::searchedBarFg);
p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), text);
const auto moreFont = (_selectedMorePosts || _pressedMorePosts)
? st::searchedBarFont->underline()
: st::searchedBarFont;
{
const auto text = tr::lng_channels_your_more(tr::now);
if (!_morePostsWidth) {
_morePostsWidth = moreFont->width(text);
}
p.setFont(moreFont);
p.drawTextLeft(
width() - st::searchedBarPosition.x() - _morePostsWidth,
st::searchedBarPosition.y(),
width(),
text);
p.translate(0, st::searchedBarHeight);
}
auto skip = previewOffset();
auto from = floorclamp(r.y() - skip, _st->height, 0, _previewResults.size());
auto to = ceilclamp(r.y() + r.height() - skip, _st->height, 0, _previewResults.size());
p.translate(0, from * _st->height);
if (from < _previewResults.size()) {
for (; from < to; ++from) {
const auto &result = _previewResults[from];
const auto active = isSearchResultActive(result.get(), activeEntry);
const auto selected = _menuRow.key
? isSearchResultActive(result.get(), _menuRow)
: _chatPreviewRow.key
? isSearchResultActive(result.get(), _chatPreviewRow)
: (from == (isPressed()
? _previewPressed
: _previewSelected));
Ui::RowPainter::Paint(p, result.get(), {
.st = _st,
.folder = _openedFolder,
.forum = _openedForum,
.currentBg = currentBg(),
.filter = _filterId,
.now = ms,
.width = fullWidth,
.active = active,
.selected = selected,
.paused = videoPaused,
.search = true,
.narrow = (fullWidth < st::columnMinimalWidthLeft / 2),
.displayUnreadInfo = showUnreadInSearchResults,
});
p.translate(0, _st->height);
}
}
if (to < _previewResults.size()) {
p.translate(0, (_previewResults.size() - to) * _st->height);
}
}
if (!_searchResults.empty()) {
const auto text = showUnreadInSearchResults
? u"Search results"_q
: tr::lng_search_found_results(
@@ -1330,6 +1401,8 @@ void InnerWidget::clearIrrelevantState() {
setFilteredPressed(-1, false);
_peerSearchSelected = -1;
setPeerSearchPressed(-1);
_previewSelected = -1;
setPreviewPressed(-1);
_searchedSelected = -1;
setSearchedPressed(-1);
} else if (_state == WidgetState::Filtered) {
@@ -1446,6 +1519,32 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
updateSelectedRow();
}
}
if (!_previewResults.empty()) {
auto skip = previewOffset();
auto previewSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1;
if (previewSelected < 0 || previewSelected >= _previewResults.size()) {
previewSelected = -1;
}
if (_previewSelected != previewSelected) {
updateSelectedRow();
_previewSelected = previewSelected;
updateSelectedRow();
}
auto selectedMorePosts = false;
const auto from = skip - st::searchedBarHeight;
if (mouseY <= skip && mouseY >= from) {
const auto left = width()
- _morePostsWidth
- 2 * st::searchedBarPosition.x();
if (_morePostsWidth > 0 && local.x() >= left) {
selectedMorePosts = true;
}
}
if (_selectedMorePosts != selectedMorePosts) {
update(0, from, width(), st::searchedBarHeight);
_selectedMorePosts = selectedMorePosts;
}
}
if (!_searchResults.empty()) {
auto skip = searchedOffset();
auto searchedSelected = (mouseY >= skip) ? ((mouseY - skip) / _st->height) : -1;
@@ -1495,7 +1594,9 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
_hashtagDeletePressed = _hashtagDeleteSelected;
setFilteredPressed(_filteredSelected, _selectedTopicJump);
setPeerSearchPressed(_peerSearchSelected);
setPreviewPressed(_previewSelected);
setSearchedPressed(_searchedSelected);
_pressedMorePosts = _selectedMorePosts;
const auto alt = (e->modifiers() & Qt::AltModifier);
if (alt && showChatPreview()) {
@@ -1862,8 +1963,12 @@ void InnerWidget::mousePressReleased(
setFilteredPressed(-1, false);
auto peerSearchPressed = _peerSearchPressed;
setPeerSearchPressed(-1);
auto previewPressed = _previewPressed;
setPreviewPressed(-1);
auto searchedPressed = _searchedPressed;
setSearchedPressed(-1);
const auto pressedMorePosts = _pressedMorePosts;
_pressedMorePosts = false;
if (wasDragging) {
selectByMouse(globalPosition);
}
@@ -1879,8 +1984,12 @@ void InnerWidget::mousePressReleased(
|| (filteredPressed >= 0 && filteredPressed == _filteredSelected)
|| (peerSearchPressed >= 0
&& peerSearchPressed == _peerSearchSelected)
|| (previewPressed >= 0
&& previewPressed == _previewSelected)
|| (searchedPressed >= 0
&& searchedPressed == _searchedSelected)) {
&& searchedPressed == _searchedSelected)
|| (pressedMorePosts
&& pressedMorePosts == _selectedMorePosts)) {
chooseRow(modifiers, pressedTopicRootId);
}
}
@@ -1955,6 +2064,13 @@ void InnerWidget::setPeerSearchPressed(int pressed) {
_peerSearchPressed = pressed;
}
void InnerWidget::setPreviewPressed(int pressed) {
if (base::in_range(_previewPressed, 0, _previewResults.size())) {
_previewResults[_previewPressed]->stopLastRipple();
}
_previewPressed = pressed;
}
void InnerWidget::setSearchedPressed(int pressed) {
if (base::in_range(_searchedPressed, 0, _searchResults.size())) {
_searchResults[_searchedPressed]->stopLastRipple();
@@ -2313,6 +2429,8 @@ void InnerWidget::updateSelectedRow(Key key) {
}
} else if (_peerSearchSelected >= 0) {
update(0, peerSearchOffset() + _peerSearchSelected * st::dialogsRowHeight, width(), st::dialogsRowHeight);
} else if (_previewSelected >= 0) {
update(0, previewOffset() + _previewSelected * _st->height, width(), _st->height);
} else if (_searchedSelected >= 0) {
update(0, searchedOffset() + _searchedSelected * _st->height, width(), _st->height);
}
@@ -2354,9 +2472,11 @@ void InnerWidget::clearSelection() {
if (isSelected()) {
updateSelectedRow();
_collapsedSelected = -1;
_selectedMorePosts = false;
_selected = nullptr;
_filteredSelected
= _searchedSelected
= _previewSelected
= _peerSearchSelected
= _hashtagSelected
= -1;
@@ -2449,6 +2569,11 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
} else if (_state == WidgetState::Filtered) {
if (base::in_range(_filteredSelected, 0, _filterResults.size())) {
return { _filterResults[_filteredSelected].key(), FullMsgId() };
} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {
return {
_previewResults[_previewSelected]->item()->history(),
_previewResults[_previewSelected]->item()->fullId()
};
} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {
return {
_searchResults[_searchedSelected]->item()->history(),
@@ -2597,6 +2722,7 @@ void InnerWidget::searchRequested(bool loading) {
_searchLoading = loading;
if (loading) {
clearSearchResults(true);
clearPreviewResults();
}
refresh(true);
}
@@ -2660,6 +2786,7 @@ void InnerWidget::applySearchState(SearchState state) {
}
_searchState = std::move(state);
_searchHashOrCashtag = IsHashOrCashtagSearchQuery(_searchState.query);
_searchWithPostsPreview = computeSearchWithPostsPreview();
updateSearchIn();
moveSearchIn();
@@ -2767,7 +2894,7 @@ void InnerWidget::appendToFiltered(Key key) {
const auto height = filteredHeight();
_filterResults.emplace_back(i->second.get());
_filterResults.back().top = height;
trackSearchResultsHistory(key.owningHistory());
trackResultsHistory(key.owningHistory());
}
InnerWidget::~InnerWidget() {
@@ -2780,13 +2907,16 @@ void InnerWidget::clearSearchResults(bool clearPeerSearchResults) {
_peerSearchResults.clear();
}
_searchResults.clear();
_searchResultsLifetime.destroy();
_searchResultsHistories.clear();
_searchedCount = _searchedMigratedCount = 0;
}
void InnerWidget::trackSearchResultsHistory(not_null<History*> history) {
if (!_searchResultsHistories.emplace(history).second) {
void InnerWidget::clearPreviewResults() {
_previewResults.clear();
_previewCount = 0;
}
void InnerWidget::trackResultsHistory(not_null<History*> history) {
if (!_trackedHistories.emplace(history).second) {
return;
}
const auto channel = history->peer->asChannel();
@@ -2827,7 +2957,7 @@ void InnerWidget::trackSearchResultsHistory(not_null<History*> history) {
clearMouseSelection(true);
}
update();
}, _searchResultsLifetime);
}, _trackedLifetime);
if (const auto forum = channel->forum()) {
forum->topicDestroyed(
@@ -2857,7 +2987,7 @@ void InnerWidget::trackSearchResultsHistory(not_null<History*> history) {
if (_chatPreviewRow.key.topic() == topic) {
_chatPreviewRow = {};
}
}, _searchResultsLifetime);
}, _trackedLifetime);
}
}
@@ -2875,6 +3005,13 @@ Data::Thread *InnerWidget::updateFromParentDrag(QPoint globalPosition) {
} else if (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())) {
return session().data().history(
_peerSearchResults[_peerSearchSelected]->peer);
} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {
if (const auto item = _previewResults[_previewSelected]->item()) {
if (const auto topic = item->topic()) {
return topic;
}
return item->history();
}
} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {
if (const auto item = _searchResults[_searchedSelected]->item()) {
if (const auto topic = item->topic()) {
@@ -3028,12 +3165,14 @@ void InnerWidget::searchReceived(
_searchLoading = false;
const auto uniquePeers = uniqueSearchResults();
if (type == SearchRequestType::FromStart
|| type == SearchRequestType::PeerFromStart) {
const auto withPreview = _searchWithPostsPreview;
const auto toPreview = withPreview && type.posts;
if (type.start && !type.migrated && (!withPreview || !type.posts)) {
clearSearchResults(false);
}
const auto isMigratedSearch = (type == SearchRequestType::MigratedFromStart)
|| (type == SearchRequestType::MigratedFromOffset);
if (!withPreview || toPreview) {
clearPreviewResults();
}
const auto key = (!_openedForum || _searchState.inChat.topic())
? _searchState.inChat
@@ -3042,34 +3181,40 @@ void InnerWidget::searchReceived(
&& (!_searchState.inChat
|| inject->history() == _searchState.inChat.history())) {
Assert(_searchResults.empty());
Assert(!toPreview);
const auto index = int(_searchResults.size());
_searchResults.push_back(
std::make_unique<FakeRow>(
key,
inject,
[=] { repaintSearchResult(index); }));
trackSearchResultsHistory(inject->history());
trackResultsHistory(inject->history());
++fullCount;
}
auto &results = toPreview ? _previewResults : _searchResults;
for (const auto &item : messages) {
const auto history = item->history();
if (!uniquePeers || !hasHistoryInResults(history)) {
const auto index = int(_searchResults.size());
_searchResults.push_back(
std::make_unique<FakeRow>(
key,
item,
[=] { repaintSearchResult(index); }));
trackSearchResultsHistory(history);
if (uniquePeers && !history->unreadCountKnown()) {
if (toPreview || !uniquePeers || !hasHistoryInResults(history)) {
const auto index = int(results.size());
const auto repaint = toPreview
? Fn<void()>([=] { repaintSearchResult(index); })
: [=] { repaintPreviewResult(index); };
results.push_back(
std::make_unique<FakeRow>(key, item, repaint));
trackResultsHistory(history);
if (!toPreview && uniquePeers && !history->unreadCountKnown()) {
history->owner().histories().requestDialogEntry(history);
} else if (toPreview && results.size() >= kPreviewPostsLimit) {
break;
}
}
}
if (isMigratedSearch) {
if (type.migrated) {
_searchedMigratedCount = fullCount;
} else {
} else if (!withPreview || !toPreview) {
_searchedCount = fullCount;
} else {
_previewCount = fullCount;
}
refresh();
@@ -3313,6 +3458,7 @@ void InnerWidget::clearMouseSelection(bool clearSelection) {
} else if (_state == WidgetState::Filtered) {
_filteredSelected
= _peerSearchSelected
= _previewSelected
= _searchedSelected
= _hashtagSelected = -1;
}
@@ -3416,6 +3562,19 @@ void InnerWidget::repaintSearchResult(int index) {
_st->height);
}
void InnerWidget::repaintPreviewResult(int index) {
rtlupdate(
0,
previewOffset() + index * _st->height,
width(),
_st->height);
}
bool InnerWidget::computeSearchWithPostsPreview() const {
return (_searchHashOrCashtag != HashOrCashtag::None)
&& (_searchState.tab == ChatSearchTab::MyMessages);
}
void InnerWidget::clearFilter() {
if (_state == WidgetState::Filtered || _searchState.inChat) {
if (_searchState.inChat) {
@@ -3428,6 +3587,9 @@ void InnerWidget::clearFilter() {
_filterResultsGlobal.clear();
_peerSearchResults.clear();
_searchResults.clear();
_previewResults.clear();
_trackedHistories.clear();
_trackedLifetime.destroy();
_filter = QString();
refresh(true);
}
@@ -3474,15 +3636,22 @@ void InnerWidget::selectSkip(int32 direction) {
}
scrollToDefaultSelected();
} else if (_state == WidgetState::Filtered) {
if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty() && _searchResults.empty()) {
if (_hashtagResults.empty()
&& _filterResults.empty()
&& _peerSearchResults.empty()
&& _previewResults.empty()
&& _searchResults.empty()) {
return;
}
if ((_hashtagSelected < 0 || _hashtagSelected >= _hashtagResults.size()) &&
(_filteredSelected < 0 || _filteredSelected >= _filterResults.size()) &&
(_peerSearchSelected < 0 || _peerSearchSelected >= _peerSearchResults.size()) &&
(_searchedSelected < 0 || _searchedSelected >= _searchResults.size())) {
if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty()) {
if ((_hashtagSelected < 0 || _hashtagSelected >= _hashtagResults.size())
&& (_filteredSelected < 0 || _filteredSelected >= _filterResults.size())
&& (_peerSearchSelected < 0 || _peerSearchSelected >= _peerSearchResults.size())
&& (_previewSelected < 0 || _previewSelected >= _previewResults.size())
&& (_searchedSelected < 0 || _searchedSelected >= _searchResults.size())) {
if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty() && _previewResults.empty()) {
_searchedSelected = 0;
} else if (_hashtagResults.empty() && _filterResults.empty() && _peerSearchResults.empty()) {
_previewSelected = 0;
} else if (_hashtagResults.empty() && _filterResults.empty()) {
_peerSearchSelected = 0;
} else if (_hashtagResults.empty()) {
@@ -3493,30 +3662,36 @@ void InnerWidget::selectSkip(int32 direction) {
} else {
int32 cur = base::in_range(_hashtagSelected, 0, _hashtagResults.size())
? _hashtagSelected
: (base::in_range(_filteredSelected, 0, _filterResults.size())
? (_hashtagResults.size() + _filteredSelected)
: (base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())
? (_peerSearchSelected + _filterResults.size() + _hashtagResults.size())
: (_searchedSelected + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size())));
: base::in_range(_filteredSelected, 0, _filterResults.size())
? (_hashtagResults.size() + _filteredSelected)
: base::in_range(_peerSearchSelected, 0, _peerSearchResults.size())
? (_peerSearchSelected + _filterResults.size() + _hashtagResults.size())
: base::in_range(_previewSelected, 0, _previewResults.size())
? (_previewSelected + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size())
: (_searchedSelected + _previewResults.size() + _peerSearchResults.size() + _filterResults.size() + _hashtagResults.size());
cur = std::clamp(
cur + direction,
0,
static_cast<int>(_hashtagResults.size()
+ _filterResults.size()
+ _peerSearchResults.size()
+ _previewResults.size()
+ _searchResults.size()) - 1);
if (cur < _hashtagResults.size()) {
_hashtagSelected = cur;
_filteredSelected = _peerSearchSelected = _searchedSelected = -1;
_filteredSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1;
} else if (cur < _hashtagResults.size() + _filterResults.size()) {
_filteredSelected = cur - _hashtagResults.size();
_hashtagSelected = _peerSearchSelected = _searchedSelected = -1;
_hashtagSelected = _peerSearchSelected = _previewSelected = _searchedSelected = -1;
} else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size()) {
_peerSearchSelected = cur - _hashtagResults.size() - _filterResults.size();
_hashtagSelected = _filteredSelected = _searchedSelected = -1;
_hashtagSelected = _filteredSelected = _previewSelected = _searchedSelected = -1;
} else if (cur < _hashtagResults.size() + _filterResults.size() + _peerSearchResults.size() + _previewResults.size()) {
_previewSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size();
_hashtagSelected = _filteredSelected = _peerSearchSelected = _searchedSelected = -1;
} else {
_hashtagSelected = _filteredSelected = _peerSearchSelected = -1;
_searchedSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size();
_searchedSelected = cur - _hashtagResults.size() - _filterResults.size() - _peerSearchResults.size() - _previewResults.size();
_hashtagSelected = _filteredSelected = _peerSearchSelected = _previewSelected = -1;
}
}
if (base::in_range(_hashtagSelected, 0, _hashtagResults.size())) {
@@ -3533,6 +3708,13 @@ void InnerWidget::selectSkip(int32 direction) {
const auto height = st::dialogsRowHeight
+ (_peerSearchSelected ? 0 : st::searchedBarHeight);
scrollToItem(from, height);
} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {
const auto from = previewOffset()
+ _previewSelected * _st->height
+ (_previewSelected ? 0 : -st::searchedBarHeight);
const auto height = _st->height
+ (_previewSelected ? 0 : st::searchedBarHeight);
scrollToItem(from, height);
} else {
const auto from = searchedOffset()
+ _searchedSelected * _st->height
@@ -3551,7 +3733,14 @@ void InnerWidget::scrollToEntry(const RowDescriptor &entry) {
scrollToItem(dialogsOffset() + row->top(), row->height());
}
} else if (_state == WidgetState::Filtered) {
for (int32 i = 0, c = _searchResults.size(); i < c; ++i) {
for (auto i = 0, c = int(_previewResults.size()); i != c; ++i) {
if (isSearchResultActive(_previewResults[i].get(), entry)) {
const auto from = previewOffset() + i * _st->height;
scrollToItem(from, _st->height);
return;
}
}
for (auto i = 0, c = int(_searchResults.size()); i != c; ++i) {
if (isSearchResultActive(_searchResults[i].get(), entry)) {
const auto from = searchedOffset() + i * _st->height;
scrollToItem(from, _st->height);
@@ -3646,33 +3835,45 @@ void InnerWidget::preloadRowsData() {
}
yTo -= otherStart;
} else if (_state == WidgetState::Filtered) {
int32 from = (yFrom - filteredOffset()) / _st->height;
auto from = (yFrom - filteredOffset()) / _st->height;
if (from < 0) from = 0;
if (from < _filterResults.size()) {
int32 to = (yTo / _st->height) + 1;
if (to > _filterResults.size()) to = _filterResults.size();
const auto to = std::min(
((yTo - filteredOffset()) / _st->height) + 1,
int(_filterResults.size()));
for (; from < to; ++from) {
_filterResults[from].key().entry()->chatListPreloadData();
}
}
from = (yFrom > filteredOffset() + st::searchedBarHeight ? ((yFrom - filteredOffset() - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size();
from = (yFrom - peerSearchOffset()) / st::dialogsRowHeight;
if (from < 0) from = 0;
if (from < _peerSearchResults.size()) {
int32 to = (yTo > filteredOffset() + st::searchedBarHeight ? ((yTo - filteredOffset() - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() + 1;
if (to > _peerSearchResults.size()) to = _peerSearchResults.size();
const auto to = std::min(
((yTo - peerSearchOffset()) / st::dialogsRowHeight) + 1,
int(_peerSearchResults.size()));
for (; from < to; ++from) {
_peerSearchResults[from]->peer->loadUserpic();
}
}
from = (yFrom > filteredOffset() + ((_peerSearchResults.empty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight) ? ((yFrom - filteredOffset() - (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() - _peerSearchResults.size();
from = (yFrom - previewOffset()) / _st->height;
if (from < 0) from = 0;
if (from < _previewResults.size()) {
const auto to = std::min(
((yTo - previewOffset()) / _st->height) + 1,
int(_previewResults.size()));
for (; from < to; ++from) {
_previewResults[from]->item()->history()->peer->loadUserpic();
}
}
from = (yFrom - searchedOffset()) / _st->height;
if (from < 0) from = 0;
if (from < _searchResults.size()) {
int32 to = (yTo > filteredOffset() + (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) + st::searchedBarHeight ? ((yTo - filteredOffset() - (_peerSearchResults.empty() ? 0 : st::searchedBarHeight) - st::searchedBarHeight) / st::dialogsRowHeight) : 0) - _filterResults.size() - _peerSearchResults.size() + 1;
if (to > _searchResults.size()) to = _searchResults.size();
const auto to = std::min(
((yTo - searchedOffset()) / _st->height) + 1,
int(_searchResults.size()));
for (; from < to; ++from) {
_searchResults[from]->item()->history()->peer->loadUserpic();
}
@@ -3803,6 +4004,14 @@ ChosenRow InnerWidget::computeChosenRow() const {
.key = session().data().history(peer),
.message = Data::UnreadMessagePosition
};
} else if (base::in_range(_previewSelected, 0, _previewResults.size())) {
const auto result = _previewResults[_previewSelected].get();
const auto topic = result->topic();
const auto item = result->item();
return {
.key = (topic ? (Entry*)topic : (Entry*)item->history()),
.message = item->position()
};
} else if (base::in_range(_searchedSelected, 0, _searchResults.size())) {
const auto result = _searchResults[_searchedSelected].get();
const auto topic = result->topic();
@@ -3832,6 +4041,11 @@ bool InnerWidget::chooseRow(
MsgId pressedTopicRootId) {
if (chooseHashtag()) {
return true;
} else if (_selectedMorePosts) {
if (_searchHashOrCashtag != HashOrCashtag::None) {
_changeSearchTabRequests.fire(ChatSearchTab::PublicPosts);
}
return true;
}
const auto modifyChosenRow = [&](
ChosenRow row,

View File

@@ -72,13 +72,18 @@ struct ChosenRow {
bool newWindow : 1 = false;
};
enum class SearchRequestType : uchar {
FromStart,
FromOffset,
PeerFromStart,
PeerFromOffset,
MigratedFromStart,
MigratedFromOffset,
struct SearchRequestType {
bool migrated : 1 = false;
bool posts : 1 = false;
bool start : 1 = false;
bool peer : 1 = false;
friend inline constexpr auto operator<=>(
SearchRequestType a,
SearchRequestType b) = default;
friend inline constexpr bool operator==(
SearchRequestType a,
SearchRequestType b) = default;
};
enum class SearchRequestDelay : uchar {
@@ -283,6 +288,7 @@ private:
void setHashtagPressed(int pressed);
void setFilteredPressed(int pressed, bool pressedTopicJump);
void setPeerSearchPressed(int pressed);
void setPreviewPressed(int pressed);
void setSearchedPressed(int pressed);
bool isPressed() const {
return (_collapsedPressed >= 0)
@@ -290,7 +296,9 @@ private:
|| (_hashtagPressed >= 0)
|| (_filteredPressed >= 0)
|| (_peerSearchPressed >= 0)
|| (_searchedPressed >= 0);
|| (_previewPressed >= 0)
|| (_searchedPressed >= 0)
|| _pressedMorePosts;
}
bool isSelected() const {
return (_collapsedSelected >= 0)
@@ -298,7 +306,9 @@ private:
|| (_hashtagSelected >= 0)
|| (_filteredSelected >= 0)
|| (_peerSearchSelected >= 0)
|| (_searchedSelected >= 0);
|| (_previewSelected >= 0)
|| (_searchedSelected >= 0)
|| _selectedMorePosts;
}
bool uniqueSearchResults() const;
bool hasHistoryInResults(not_null<History*> history) const;
@@ -352,6 +362,7 @@ private:
[[nodiscard]] int filteredHeight(int till = -1) const;
[[nodiscard]] int peerSearchOffset() const;
[[nodiscard]] int searchInChatOffset() const;
[[nodiscard]] int previewOffset() const;
[[nodiscard]] int searchedOffset() const;
[[nodiscard]] int searchInChatSkip() const;
[[nodiscard]] int hashtagsOffset() const;
@@ -403,14 +414,18 @@ private:
// const Ui::Text::String &text) const;
void updateSearchIn();
void repaintSearchResult(int index);
void repaintPreviewResult(int index);
[[nodiscard]] bool computeSearchWithPostsPreview() const;
Ui::VideoUserpic *validateVideoUserpic(not_null<Row*> row);
Ui::VideoUserpic *validateVideoUserpic(not_null<History*> history);
Row *shownRowByKey(Key key);
void clearSearchResults(bool clearPeerSearchResults = true);
void clearPreviewResults();
void updateSelectedRow(Key key = Key());
void trackSearchResultsHistory(not_null<History*> history);
void trackResultsHistory(not_null<History*> history);
[[nodiscard]] QBrush currentBg() const;
[[nodiscard]] RowDescriptor computeChatPreviewRow() const;
@@ -449,6 +464,8 @@ private:
std::vector<std::unique_ptr<CollapsedRow>> _collapsedRows;
not_null<const style::DialogRow*> _st;
mutable std::unique_ptr<Ui::TopicJumpCache> _topicJumpCache;
bool _selectedMorePosts = false;
bool _pressedMorePosts = false;
int _collapsedSelected = -1;
int _collapsedPressed = -1;
bool _skipTopDialog = false;
@@ -487,14 +504,21 @@ private:
EmptyState _emptyState = EmptyState::None;
base::flat_set<not_null<History*>> _trackedHistories;
rpl::lifetime _trackedLifetime;
QString _peerSearchQuery;
std::vector<std::unique_ptr<PeerSearchResult>> _peerSearchResults;
int _peerSearchSelected = -1;
int _peerSearchPressed = -1;
std::vector<std::unique_ptr<FakeRow>> _previewResults;
int _previewCount = 0;
int _previewSelected = -1;
int _previewPressed = -1;
int _morePostsWidth = 0;
std::vector<std::unique_ptr<FakeRow>> _searchResults;
base::flat_set<not_null<History*>> _searchResultsHistories;
rpl::lifetime _searchResultsLifetime;
int _searchedCount = 0;
int _searchedMigratedCount = 0;
int _searchedSelected = -1;
@@ -516,6 +540,7 @@ private:
SearchState _searchState;
HashOrCashtag _searchHashOrCashtag = {};
bool _searchWithPostsPreview = false;
History *_searchInMigrated = nullptr;
PeerData *_searchFromShown = nullptr;
Ui::Text::String _searchFromUserText;

View File

@@ -234,7 +234,9 @@ void Widget::BottomButton::radialAnimationCallback() {
}
}
void Widget::BottomButton::onStateChanged(State was, StateChangeSource source) {
void Widget::BottomButton::onStateChanged(
State was,
StateChangeSource source) {
RippleButton::onStateChanged(was, source);
if ((was & StateFlag::Disabled) != (state() & StateFlag::Disabled)) {
_loading = isDisabled()
@@ -532,11 +534,11 @@ Widget::Widget(
}, lifetime());
}
_cancelSearch->setClickedCallback([this] {
_cancelSearch->setClickedCallback([=] {
cancelSearch({ .jumpBackToSearchedChat = true });
});
_jumpToDate->entity()->setClickedCallback([this] { showCalendar(); });
_chooseFromUser->entity()->setClickedCallback([this] { showSearchFrom(); });
_jumpToDate->entity()->setClickedCallback([=] { showCalendar(); });
_chooseFromUser->entity()->setClickedCallback([=] { showSearchFrom(); });
rpl::single(rpl::empty) | rpl::then(
session().domain().local().localPasscodeChanged()
) | rpl::start_with_next([=] {
@@ -576,11 +578,10 @@ Widget::Widget(
});
_inner->setLoadMoreCallback([=] {
const auto state = _inner->state();
const auto process = currentSearchProcess();
if (state == WidgetState::Filtered
&& (!_searchFull
|| (_searchInMigrated
&& _searchFull
&& !_searchFullMigrated))) {
&& (!process->full
|| (_searchInMigrated && !_migratedProcess.full))) {
searchMore();
} else if (_openedForum && state == WidgetState::Default) {
_openedForum->requestTopics();
@@ -931,7 +932,7 @@ void Widget::updateScrollUpVisibility() {
}
startScrollUpButtonAnimation(
(_scroll->scrollTop() > st::historyToDownShownAfter)
(_scroll->scrollTop() > (st::historyToDownShownAfter / 2))
&& (_scroll->scrollTop() < _scroll->scrollTopMax()));
}
@@ -1199,11 +1200,12 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) {
return !_searchQuery.isEmpty();
}) | rpl::start_with_next([=] {
_searchTimer.cancel();
_searchCache.clear();
_singleMessageSearch.clear();
for (const auto &[requestId, query] : base::take(_searchQueries)) {
_searchProcess.cache.clear();
const auto queries = base::take(_searchProcess.queries);
for (const auto &[requestId, query] : queries) {
session().api().request(requestId).cancel();
}
_singleMessageSearch.clear();
_searchQuery = QString();
_scroll->scrollToY(0);
cancelSearchRequest();
@@ -1529,6 +1531,7 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) {
_searchState.tab = forum
? ChatSearchTab::ThisPeer
: ChatSearchTab::MyMessages;
_searchWithPostsPreview = computeSearchWithPostsPreview();
_api.request(base::take(_topicSearchRequest)).cancel();
_inner->changeOpenedForum(forum);
storiesToggleExplicitExpand(false);
@@ -1673,11 +1676,7 @@ QPixmap Widget::grabForFolderSlideAnimation() {
_scrollToTop->hide();
}
const auto rect = QRect(
0,
0,
width(),
_scroll->y() + _scroll->height());
const auto rect = QRect(0, 0, width(), rect::bottom(_scroll));
auto result = Ui::GrabWidget(this, rect);
if (!hidden) {
@@ -2110,12 +2109,18 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
const auto fromPeer = searchFromPeer();
const auto &inTags = searchInTags();
const auto tab = _searchState.tab;
const auto fromStartType = inPeer
? SearchRequestType::PeerFromStart
: SearchRequestType::FromStart;
const auto fromStartType = SearchRequestType{
.start = true,
.peer = (inPeer != nullptr),
};
if (trimmed.isEmpty() && !fromPeer && inTags.empty()) {
cancelSearchRequest();
searchApplyEmpty(fromStartType, 0);
searchApplyEmpty(fromStartType, currentSearchProcess());
if (_searchWithPostsPreview) {
searchApplyEmpty(
{ .posts = true, .start = true },
&_postsProcess);
}
_api.request(base::take(_peerSearchRequest)).cancel();
_peerSearchQuery = QString();
peerSearchApplyEmpty(0);
@@ -2128,28 +2133,32 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
if (!success) {
return false;
}
const auto i = _searchCache.find(query);
if (i != _searchCache.end()) {
const auto process = currentSearchProcess();
const auto i = process->cache.find(query);
if (i != process->cache.end()) {
_searchQuery = query;
_searchQueryFrom = fromPeer;
_searchQueryTags = inTags;
_searchQueryTab = tab;
_searchNextRate = 0;
_searchFull = _searchFullMigrated = false;
process->nextRate = 0;
process->full = false;
_migratedProcess.full = false;
cancelSearchRequest();
searchReceived(fromStartType, i->second, 0);
searchReceived(fromStartType, i->second, process, true);
result = true;
}
} else if (_searchQuery != query
|| _searchQueryFrom != fromPeer
|| _searchQueryTags != inTags
|| _searchQueryTab != tab) {
const auto process = currentSearchProcess();
_searchQuery = query;
_searchQueryFrom = fromPeer;
_searchQueryTags = inTags;
_searchQueryTab = tab;
_searchNextRate = 0;
_searchFull = _searchFullMigrated = false;
process->nextRate = 0;
process->full = false;
_migratedProcess.full = false;
cancelSearchRequest();
if (inPeer) {
const auto topic = searchInTopic();
@@ -2163,83 +2172,55 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
const auto savedPeer = sublist
? sublist->peer().get()
: nullptr;
_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
const auto type = SearchRequestType::PeerFromStart;
_historiesRequest = histories.sendRequest(history, type, [=](
Fn<void()> finish) {
const auto type = SearchRequestType{
.start = true,
.peer = true,
};
using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
inPeer->input,
MTP_string(_searchQuery),
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(0), // offset_id
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0) // hash
)).done([=](const MTPmessages_Messages &result) {
_searchInHistoryRequest = 0;
searchReceived(type, result, _searchRequest);
process->requestId = session().api().request(
MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
inPeer->input,
MTP_string(_searchQuery),
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(0), // offset_id
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0)) // hash
).done([=](const MTPmessages_Messages &result) {
_historiesRequest = 0;
searchReceived(type, result, process);
finish();
}).fail([=](const MTP::Error &error) {
_searchInHistoryRequest = 0;
searchFailed(type, error, _searchRequest);
_historiesRequest = 0;
searchFailed(type, error, process);
finish();
}).send();
_searchQueries.emplace(_searchRequest, _searchQuery);
return _searchRequest;
process->queries.emplace(process->requestId, _searchQuery);
return process->requestId;
});
} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
const auto type = SearchRequestType::FromStart;
_searchRequest = session().api().request(MTPchannels_SearchPosts(
MTP_string(_searchState.query.trimmed().mid(1)),
MTP_int(0), // offset_rate
MTP_inputPeerEmpty(), // offset_peer
MTP_int(0), // offset_id
MTP_int(kSearchPerPage)
)).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, _searchRequest);
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, _searchRequest);
}).send();
_searchQueries.emplace(_searchRequest, _searchQuery);
requestPublicPosts(true);
} else {
const auto type = SearchRequestType::FromStart;
const auto flags = session().settings().skipArchiveInSearch()
? MTPmessages_SearchGlobal::Flag::f_folder_id
: MTPmessages_SearchGlobal::Flag(0);
const auto folderId = 0;
_searchRequest = session().api().request(MTPmessages_SearchGlobal(
MTP_flags(flags),
MTP_int(folderId),
MTP_string(_searchQuery),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(0), // offset_rate
MTP_inputPeerEmpty(), // offset_peer
MTP_int(0), // offset_id
MTP_int(kSearchPerPage)
)).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, _searchRequest);
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, _searchRequest);
}).send();
_searchQueries.emplace(_searchRequest, _searchQuery);
requestMessages(true);
}
_inner->searchRequested(true);
} else {
@@ -2261,7 +2242,9 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) {
_peerSearchRequest = _api.request(MTPcontacts_Search(
MTP_string(_peerSearchQuery),
MTP_int(SearchPeopleLimit)
)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {
)).done([=](
const MTPcontacts_Found &result,
mtpRequestId requestId) {
peerSearchReceived(result, requestId);
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
peerSearchFailed(error, requestId);
@@ -2368,11 +2351,12 @@ void Widget::searchTopics() {
}
void Widget::searchMore() {
if (_searchRequest
|| _searchInHistoryRequest
const auto process = currentSearchProcess();
if (process->requestId
|| _historiesRequest
|| _searchTimer.isActive()) {
return;
} else if (!_searchFull) {
} else if (!process->full) {
if (const auto peer = searchInPeer()) {
auto &histories = session().data().histories();
const auto topic = searchInTopic();
@@ -2385,154 +2369,213 @@ void Widget::searchMore() {
const auto savedPeer = sublist
? sublist->peer().get()
: nullptr;
_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
const auto type = _lastSearchId
? SearchRequestType::PeerFromOffset
: SearchRequestType::PeerFromStart;
_historiesRequest = histories.sendRequest(history, type, [=](
Fn<void()> finish) {
const auto type = SearchRequestType{
.start = !process->lastId,
.peer = true,
};
using Flag = MTPmessages_Search::Flag;
_searchRequest = session().api().request(MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
peer->input,
process->requestId = session().api().request(
MTPmessages_Search(
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_searchQueryTags.empty()
? Flag()
: Flag::f_saved_reaction)),
peer->input,
MTP_string(_searchQuery),
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer
? savedPeer->input
: MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(process->lastId),
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0)) // hash
).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, process);
_historiesRequest = 0;
finish();
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, process);
_historiesRequest = 0;
finish();
}).send();
if (!process->lastId) {
process->queries.emplace(
process->requestId,
_searchQuery);
}
return process->requestId;
});
} else if (_searchState.tab == ChatSearchTab::PublicPosts) {
requestPublicPosts(false);
} else {
requestMessages(false);
}
} else if (_searchInMigrated && !_migratedProcess.full) {
auto &histories = session().data().histories();
const auto type = Data::Histories::RequestType::History;
const auto history = _searchInMigrated;
_historiesRequest = histories.sendRequest(history, type, [=](
Fn<void()> finish) {
const auto type = SearchRequestType{
.migrated = true,
.start = !_migratedProcess.lastId,
};
const auto flags = _searchQueryFrom
? MTP_flags(MTPmessages_Search::Flag::f_from_id)
: MTP_flags(0);
_migratedProcess.requestId = session().api().request(
MTPmessages_Search(
flags,
_searchInMigrated->peer->input,
MTP_string(_searchQuery),
(fromPeer ? fromPeer->input : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input : MTP_inputPeerEmpty()),
MTP_vector_from_range(
_searchQueryTags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(topic ? topic->rootId() : 0),
(_searchQueryFrom
? _searchQueryFrom->input
: MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_lastSearchId),
MTP_int(_migratedProcess.lastId),
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0) // hash
)).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, _searchRequest);
_searchInHistoryRequest = 0;
finish();
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, _searchRequest);
_searchInHistoryRequest = 0;
finish();
}).send();
if (!_lastSearchId) {
_searchQueries.emplace(_searchRequest, _searchQuery);
}
return _searchRequest;
});
} else {
const auto type = _lastSearchId
? SearchRequestType::FromOffset
: SearchRequestType::FromStart;
const auto flags = session().settings().skipArchiveInSearch()
? MTPmessages_SearchGlobal::Flag::f_folder_id
: MTPmessages_SearchGlobal::Flag(0);
const auto folderId = 0;
_searchRequest = session().api().request(MTPmessages_SearchGlobal(
MTP_flags(flags),
MTP_int(folderId),
MTP_string(_searchQuery),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_searchNextRate),
(_lastSearchPeer
? _lastSearchPeer->input
: MTP_inputPeerEmpty()),
MTP_int(_lastSearchId),
MTP_int(kSearchPerPage)
)).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, _searchRequest);
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, _searchRequest);
}).send();
if (!_lastSearchId) {
_searchQueries.emplace(_searchRequest, _searchQuery);
}
}
} else if (_searchInMigrated && !_searchFullMigrated) {
auto &histories = session().data().histories();
const auto type = Data::Histories::RequestType::History;
const auto history = _searchInMigrated;
_searchInHistoryRequest = histories.sendRequest(history, type, [=](Fn<void()> finish) {
const auto type = _lastSearchMigratedId
? SearchRequestType::MigratedFromOffset
: SearchRequestType::MigratedFromStart;
const auto flags = _searchQueryFrom
? MTP_flags(MTPmessages_Search::Flag::f_from_id)
: MTP_flags(0);
_searchRequest = session().api().request(MTPmessages_Search(
flags,
_searchInMigrated->peer->input,
MTP_string(_searchQuery),
(_searchQueryFrom
? _searchQueryFrom->input
: MTP_inputPeerEmpty()),
MTPInputPeer(), // saved_peer_id
MTPVector<MTPReaction>(), // saved_reaction
MTPint(), // top_msg_id
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_lastSearchMigratedId),
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0) // hash
)).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, _searchRequest);
_searchInHistoryRequest = 0;
MTP_long(0)) // hash
).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, &_migratedProcess);
_historiesRequest = 0;
finish();
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, _searchRequest);
_searchInHistoryRequest = 0;
searchFailed(type, error, &_migratedProcess);
_historiesRequest = 0;
finish();
}).send();
return _searchRequest;
return _migratedProcess.requestId;
});
}
}
void Widget::requestPublicPosts(bool fromStart) {
if (!_postsProcess.lastId || !_postsProcess.lastPeer) {
fromStart = true;
}
const auto type = SearchRequestType{
.posts = true,
.start = fromStart,
};
_postsProcess.requestId = session().api().request(
MTPchannels_SearchPosts(
MTP_string(_searchState.query.trimmed().mid(1)),
MTP_int(fromStart ? 0 : _postsProcess.nextRate),
(fromStart
? MTP_inputPeerEmpty()
: _postsProcess.lastPeer->input),
MTP_int(fromStart ? 0 : _postsProcess.lastId),
MTP_int(kSearchPerPage))
).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, &_postsProcess);
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, &_postsProcess);
}).send();
if (fromStart) {
_postsProcess.queries.emplace(_postsProcess.requestId, _searchQuery);
}
}
void Widget::requestMessages(bool fromStart) {
if (!_searchProcess.lastId || !_searchProcess.lastPeer) {
fromStart = true;
}
const auto type = SearchRequestType{
.start = fromStart,
};
const auto flags = session().settings().skipArchiveInSearch()
? MTPmessages_SearchGlobal::Flag::f_folder_id
: MTPmessages_SearchGlobal::Flag(0);
const auto folderId = 0;
_searchProcess.requestId = session().api().request(
MTPmessages_SearchGlobal(
MTP_flags(flags),
MTP_int(folderId),
MTP_string(_searchQuery),
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(fromStart ? 0 : _searchProcess.nextRate),
(fromStart
? MTP_inputPeerEmpty()
: _searchProcess.lastPeer->input),
MTP_int(fromStart ? 0 : _searchProcess.lastId),
MTP_int(kSearchPerPage))
).done([=](const MTPmessages_Messages &result) {
searchReceived(type, result, &_searchProcess);
}).fail([=](const MTP::Error &error) {
searchFailed(type, error, &_searchProcess);
}).send();
if (!_searchProcess.lastId) {
_searchProcess.queries.emplace(
_searchProcess.requestId,
_searchQuery);
}
if (fromStart && _searchWithPostsPreview) {
requestPublicPosts(true);
}
}
auto Widget::currentSearchProcess() -> not_null<SearchProcessState*> {
return (_searchState.tab == ChatSearchTab::PublicPosts)
? &_postsProcess
: &_searchProcess;
}
bool Widget::computeSearchWithPostsPreview() const {
return (_searchHashOrCashtag != HashOrCashtag::None)
&& (_searchState.tab == ChatSearchTab::MyMessages);
}
void Widget::searchReceived(
SearchRequestType type,
const MTPmessages_Messages &result,
mtpRequestId requestId) {
not_null<SearchProcessState*> process,
bool cacheResults) {
const auto state = _inner->state();
if (state == WidgetState::Filtered) {
if (type == SearchRequestType::FromStart || type == SearchRequestType::PeerFromStart) {
auto i = _searchQueries.find(requestId);
if (i != _searchQueries.end()) {
_searchCache[i->second] = result;
_searchQueries.erase(i);
}
if (!cacheResults
&& (state == WidgetState::Filtered)
&& type.start) {
const auto i = process->queries.find(process->requestId);
if (i != process->queries.end()) {
process->cache[i->second] = result;
process->queries.erase(i);
}
}
const auto inject = (type == SearchRequestType::FromStart
|| type == SearchRequestType::PeerFromStart)
const auto inject = (type.start && !type.posts)
? *_singleMessageSearch.lookup(_searchQuery)
: nullptr;
if (_searchRequest != requestId) {
if (cacheResults && process->requestId) {
return;
}
if (type == SearchRequestType::FromStart
|| type == SearchRequestType::PeerFromStart) {
_lastSearchPeer = nullptr;
_lastSearchId = _lastSearchMigratedId = 0;
if (type.start) {
process->lastPeer = nullptr;
process->lastId = 0;
}
const auto isMigratedSearch = (type == SearchRequestType::MigratedFromStart)
|| (type == SearchRequestType::MigratedFromOffset);
const auto process = [&](const MTPVector<MTPMessage> &messages) {
const auto processList = [&](const MTPVector<MTPMessage> &messages) {
auto result = std::vector<not_null<HistoryItem*>>();
for (const auto &message : messages.v) {
const auto msgId = IdFromMessage(message);
@@ -2546,55 +2589,44 @@ void Widget::searchReceived(
NewMessageType::Existing);
result.push_back(item);
}
_lastSearchPeer = peer;
process->lastPeer = peer;
} else {
LOG(("API Error: a search results with not loaded peer %1"
).arg(peerId.value));
}
if (isMigratedSearch) {
_lastSearchMigratedId = msgId;
} else {
_lastSearchId = msgId;
}
process->lastId = msgId;
}
return result;
};
auto fullCount = 0;
auto messages = result.match([&](const MTPDmessages_messages &data) {
if (_searchRequest != 0) {
if (!cacheResults) {
// Don't apply cached data!
session().data().processUsers(data.vusers());
session().data().processChats(data.vchats());
}
if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
_searchFullMigrated = true;
} else {
_searchFull = true;
}
auto list = process(data.vmessages());
process->full = true;
auto list = processList(data.vmessages());
fullCount = list.size();
return list;
}, [&](const MTPDmessages_messagesSlice &data) {
if (_searchRequest != 0) {
if (!cacheResults) {
// Don't apply cached data!
session().data().processUsers(data.vusers());
session().data().processChats(data.vchats());
}
auto list = process(data.vmessages());
auto list = processList(data.vmessages());
const auto nextRate = data.vnext_rate();
const auto rateUpdated = nextRate && (nextRate->v != _searchNextRate);
const auto finished = (type == SearchRequestType::FromStart || type == SearchRequestType::FromOffset)
? !rateUpdated
: list.empty();
const auto rateUpdated = nextRate
&& (nextRate->v != process->nextRate);
const auto finished = (type.peer || type.migrated || type.posts)
? list.empty()
: !rateUpdated;
if (rateUpdated) {
_searchNextRate = nextRate->v;
process->nextRate = nextRate->v;
}
if (finished) {
if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
_searchFullMigrated = true;
} else {
_searchFull = true;
}
process->full = true;
}
fullCount = data.vcount().v;
return list;
@@ -2613,33 +2645,26 @@ void Widget::searchReceived(
"received messages.channelMessages when no channel "
"was passed! (Widget::searchReceived)"));
}
if (_searchRequest != 0) {
if (!cacheResults) {
// Don't apply cached data!
session().data().processUsers(data.vusers());
session().data().processChats(data.vchats());
}
auto list = process(data.vmessages());
auto list = processList(data.vmessages());
if (list.empty()) {
if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
_searchFullMigrated = true;
} else {
_searchFull = true;
}
process->full = true;
}
fullCount = data.vcount().v;
return list;
}, [&](const MTPDmessages_messagesNotModified &) {
LOG(("API Error: received messages.messagesNotModified! (Widget::searchReceived)"));
if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
_searchFullMigrated = true;
} else {
_searchFull = true;
}
LOG(("API Error: received messages.messagesNotModified! "
"(Widget::searchReceived)"));
process->full = true;
return std::vector<not_null<HistoryItem*>>();
});
_inner->searchReceived(messages, inject, type, fullCount);
_searchRequest = 0;
process->requestId = 0;
listScrollUpdated();
update();
}
@@ -2671,15 +2696,17 @@ void Widget::peerSearchReceived(
}
}
void Widget::searchApplyEmpty(SearchRequestType type, mtpRequestId id) {
_searchFull = _searchFullMigrated = true;
void Widget::searchApplyEmpty(
SearchRequestType type,
not_null<SearchProcessState*> process) {
process->full = true;
searchReceived(
type,
MTP_messages_messages(
MTP_vector<MTPMessage>(),
MTP_vector<MTPChat>(),
MTP_vector<MTPUser>()),
id);
process);
}
void Widget::peerSearchApplyEmpty(mtpRequestId id) {
@@ -2696,16 +2723,12 @@ void Widget::peerSearchApplyEmpty(mtpRequestId id) {
void Widget::searchFailed(
SearchRequestType type,
const MTP::Error &error,
mtpRequestId requestId) {
not_null<SearchProcessState*> process) {
if (error.type() == u"SEARCH_QUERY_EMPTY"_q) {
searchApplyEmpty(type, requestId);
} else if (_searchRequest == requestId) {
_searchRequest = 0;
if (type == SearchRequestType::MigratedFromStart || type == SearchRequestType::MigratedFromOffset) {
_searchFullMigrated = true;
} else {
_searchFull = true;
}
searchApplyEmpty(type, process);
} else {
process->requestId = 0;
process->full = true;
}
}
@@ -2838,6 +2861,7 @@ QString Widget::validateSearchQuery() {
} else {
_searchHashOrCashtag = IsHashOrCashtagSearchQuery(query);
}
_searchWithPostsPreview = computeSearchWithPostsPreview();
return query;
}
@@ -3096,6 +3120,7 @@ bool Widget::applySearchState(SearchState state) {
? peer->owner().history(migrateFrom).get()
: nullptr;
_searchState = state;
_searchWithPostsPreview = computeSearchWithPostsPreview();
if (queryChanged) {
updateLockUnlockVisibility(anim::type::normal);
updateLoadMoreChatsVisibility();
@@ -3111,16 +3136,20 @@ bool Widget::applySearchState(SearchState state) {
updateSearchFromVisibility();
updateLockUnlockPosition();
if ((state.query.isEmpty() && !state.fromPeer && state.tags.empty())
const auto searchCleared = state.query.isEmpty()
&& !state.fromPeer
&& state.tags.empty();
if (searchCleared
|| inChatChanged
|| fromPeerChanged
|| tagsChanged
|| tabChanged) {
clearSearchCache();
clearSearchCache(searchCleared);
}
if (state.query.isEmpty()) {
_peerSearchCache.clear();
for (const auto &[requestId, query] : base::take(_peerSearchQueries)) {
const auto queries = base::take(_peerSearchQueries);
for (const auto &[requestId, query] : queries) {
_api.request(requestId).cancel();
}
_peerSearchQuery = QString();
@@ -3158,15 +3187,23 @@ bool Widget::applySearchState(SearchState state) {
return true;
}
void Widget::clearSearchCache() {
_searchCache.clear();
void Widget::clearSearchCache(bool clearPosts) {
_searchProcess.cache.clear();
_singleMessageSearch.clear();
for (const auto &[requestId, query] : base::take(_searchQueries)) {
const auto queries = base::take(_searchProcess.queries);
for (const auto &[requestId, query] : queries) {
session().api().request(requestId).cancel();
}
_searchQuery = QString();
_searchQueryFrom = nullptr;
_searchQueryTags.clear();
if (clearPosts) {
_postsProcess.cache.clear();
const auto queries = base::take(_postsProcess.queries);
for (const auto &[requestId, query] : queries) {
session().api().request(requestId).cancel();
}
}
_topicSearchQuery = QString();
_topicSearchOffsetDate = 0;
_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
@@ -3241,10 +3278,17 @@ void Widget::completeHashtag(QString tag) {
if (cur == start + 1
|| base::StringViewMid(t, start + 1, cur - start - 1)
== base::StringViewMid(tag, 0, cur - start - 1)) {
for (; cur < t.size() && cur - start - 1 < tag.size(); ++cur) {
if (t.at(cur) != tag.at(cur - start - 1)) break;
while (cur < t.size() && cur - start - 1 < tag.size()) {
if (t.at(cur) != tag.at(cur - start - 1)) {
break;
}
++cur;
}
if (cur - start - 1 == tag.size()
&& cur < t.size()
&& t.at(cur) == ' ') {
++cur;
}
if (cur - start - 1 == tag.size() && cur < t.size() && t.at(cur) == ' ') ++cur;
hashtag = t.mid(0, start + 1) + tag + ' ' + t.mid(cur);
setSearchQuery(hashtag, start + 1 + tag.size() + 1);
applySearchUpdate();
@@ -3356,7 +3400,8 @@ void Widget::updateControlsGeometry() {
? st::dialogsFilterSkip
: (st::dialogsFilterPadding.x() + _mainMenu.toggle->width()))
+ st::dialogsFilterPadding.x();
const auto filterRight = st::dialogsFilterSkip + st::dialogsFilterPadding.x();
const auto filterRight = st::dialogsFilterSkip
+ st::dialogsFilterPadding.x();
const auto filterWidth = qMax(ratiow, smallw) - filterLeft - filterRight;
const auto filterAreaHeight = st::topBarHeight;
_searchControls->setGeometry(0, filterAreaTop, ratiow, filterAreaHeight);
@@ -3369,7 +3414,11 @@ void Widget::updateControlsGeometry() {
auto filterTop = (filterAreaHeight - _search->height()) / 2;
filterLeft = anim::interpolate(filterLeft, _narrowWidth, narrowRatio);
_search->setGeometryToLeft(filterLeft, filterTop, filterWidth, _search->height());
_search->setGeometryToLeft(
filterLeft,
filterTop,
filterWidth,
_search->height());
auto mainMenuLeft = anim::interpolate(
st::dialogsFilterPadding.x(),
@@ -3387,12 +3436,16 @@ void Widget::updateControlsGeometry() {
-_searchForNarrowLayout->width(),
(_narrowWidth - _searchForNarrowLayout->width()) / 2,
narrowRatio);
_searchForNarrowLayout->moveToLeft(searchLeft, st::dialogsFilterPadding.y());
_searchForNarrowLayout->moveToLeft(
searchLeft,
st::dialogsFilterPadding.y());
auto right = filterLeft + filterWidth;
_cancelSearch->moveToLeft(right - _cancelSearch->width(), _search->y());
right -= _jumpToDate->width(); _jumpToDate->moveToLeft(right, _search->y());
right -= _chooseFromUser->width(); _chooseFromUser->moveToLeft(right, _search->y());
right -= _jumpToDate->width();
_jumpToDate->moveToLeft(right, _search->y());
right -= _chooseFromUser->width();
_chooseFromUser->moveToLeft(right, _search->y());
const auto barw = width();
const auto expandedStoriesTop = filterAreaTop + filterAreaHeight;
@@ -3691,9 +3744,11 @@ void Widget::scrollToEntry(const RowDescriptor &entry) {
}
void Widget::cancelSearchRequest() {
session().api().request(base::take(_searchRequest)).cancel();
session().api().request(base::take(_searchProcess.requestId)).cancel();
session().api().request(base::take(_migratedProcess.requestId)).cancel();
session().api().request(base::take(_postsProcess.requestId)).cancel();
session().data().histories().cancelRequest(
base::take(_searchInHistoryRequest));
base::take(_historiesRequest));
}
PeerData *Widget::searchInPeer() const {
@@ -3810,8 +3865,12 @@ bool Widget::cancelSearch(CancelSearchOptions options) {
// Don't create suggestions in unfocus case.
setInnerFocus(true);
}
_lastSearchPeer = nullptr;
_lastSearchId = _lastSearchMigratedId = 0;
_searchProcess.lastPeer = nullptr;
_searchProcess.lastId = 0;
_migratedProcess.lastPeer = nullptr;
_migratedProcess.lastId = 0;
_postsProcess.lastPeer = nullptr;
_postsProcess.lastId = 0;
_inner->clearFilter();
applySearchState(std::move(updatedState));
if (_suggestions && clearSearchFocus) {

View File

@@ -75,7 +75,7 @@ class FakeRow;
class Key;
struct ChosenRow;
class InnerWidget;
enum class SearchRequestType : uchar;
struct SearchRequestType;
enum class SearchRequestDelay : uchar;
class Suggestions;
class ChatSearchIn;
@@ -151,10 +151,26 @@ protected:
void paintEvent(QPaintEvent *e) override;
private:
struct SearchProcessState {
base::flat_map<QString, MTPmessages_Messages> cache;
base::flat_map<mtpRequestId, QString> queries;
PeerData *lastPeer = nullptr;
MsgId lastId = 0;
int32 nextRate = 0;
mtpRequestId requestId = 0;
bool full = false;
};
void chosenRow(const ChosenRow &row);
void listScrollUpdated();
void searchCursorMoved();
void completeHashtag(QString tag);
void requestPublicPosts(bool fromStart);
void requestMessages(bool fromStart);
[[nodiscard]] not_null<SearchProcessState*> currentSearchProcess();
[[nodiscard]] bool computeSearchWithPostsPreview() const;
[[nodiscard]] QString currentSearchQuery() const;
[[nodiscard]] int currentSearchQueryCursorPosition() const;
@@ -168,7 +184,8 @@ private:
void searchReceived(
SearchRequestType type,
const MTPmessages_Messages &result,
mtpRequestId requestId);
not_null<SearchProcessState*> process,
bool cacheResults = false);
void peerSearchReceived(
const MTPcontacts_Found &result,
mtpRequestId requestId);
@@ -201,7 +218,7 @@ private:
void showCalendar();
void showSearchFrom();
void showMainMenu();
void clearSearchCache();
void clearSearchCache(bool clearPosts);
void setSearchQuery(const QString &query, int cursorPosition = -1);
void updateControlsVisibility(bool fast = false);
void updateLockUnlockVisibility(
@@ -244,9 +261,11 @@ private:
void searchFailed(
SearchRequestType type,
const MTP::Error &error,
mtpRequestId requestId);
not_null<SearchProcessState*> process);
void peerSearchFailed(const MTP::Error &error, mtpRequestId requestId);
void searchApplyEmpty(SearchRequestType type, mtpRequestId id);
void searchApplyEmpty(
SearchRequestType type,
not_null<SearchProcessState*> process);
void peerSearchApplyEmpty(mtpRequestId id);
void updateForceDisplayWide();
@@ -318,6 +337,7 @@ private:
bool _scrollToTopIsShown = false;
bool _forumSearchRequested = false;
HashOrCashtag _searchHashOrCashtag = {};
bool _searchWithPostsPreview = false;
Data::Folder *_openedFolder = nullptr;
Data::Forum *_openedForum = nullptr;
@@ -358,19 +378,13 @@ private:
PeerData *_searchQueryFrom = nullptr;
std::vector<Data::ReactionId> _searchQueryTags;
ChatSearchTab _searchQueryTab = {};
int32 _searchNextRate = 0;
bool _searchFull = false;
bool _searchFullMigrated = false;
int _searchInHistoryRequest = 0; // Not real mtpRequestId.
mtpRequestId _searchRequest = 0;
PeerData *_lastSearchPeer = nullptr;
MsgId _lastSearchId = 0;
MsgId _lastSearchMigratedId = 0;
SearchProcessState _searchProcess;
SearchProcessState _migratedProcess;
SearchProcessState _postsProcess;
int _historiesRequest = 0; // Not real mtpRequestId.
base::flat_map<QString, MTPmessages_Messages> _searchCache;
Api::SingleMessageSearch _singleMessageSearch;
base::flat_map<mtpRequestId, QString> _searchQueries;
base::flat_map<QString, MTPcontacts_Found> _peerSearchCache;
base::flat_map<mtpRequestId, QString> _peerSearchQueries;

View File

@@ -57,7 +57,6 @@ namespace {
constexpr auto kCollapsedChannelsCount = 5;
constexpr auto kProbablyMaxChannels = 1000;
constexpr auto kProbablyMaxRecommendations = 100;
constexpr auto kCollapsedAppsCount = 5;
constexpr auto kProbablyMaxApps = 100;
@@ -1163,8 +1162,22 @@ void PopularAppsController::fill() {
appendRow(bot);
}
}
const auto count = delegate()->peerListFullRowsCount();
setCount(count);
if (count > 0) {
delegate()->peerListSetBelowWidget(object_ptr<Ui::DividerLabel>(
(QWidget*)nullptr,
object_ptr<Ui::FlatLabel>(
(QWidget*)nullptr,
tr::lng_bot_apps_which(
lt_link,
tr::lng_bot_apps_which_link(
) | Ui::Text::ToLink(u"internal:about_popular_apps"_q),
Ui::Text::WithEntities),
st::dialogsPopularAppsAbout),
st::dialogsPopularAppsPadding));
}
delegate()->peerListRefreshRows();
setCount(delegate()->peerListFullRowsCount());
}
void PopularAppsController::appendRow(not_null<UserData*> bot) {
@@ -2326,4 +2339,21 @@ object_ptr<Ui::BoxContent> StarsExamplesBox(
return Box<PeerListBox>(std::move(controller), std::move(initBox));
}
object_ptr<Ui::BoxContent> PopularAppsAboutBox(
not_null<Window::SessionController*> window) {
return Ui::MakeInformBox({
.text = tr::lng_popular_apps_info_text(
lt_bot,
rpl::single(Ui::Text::Link(
u"@botfather"_q,
u"https://t.me/botfather"_q)),
lt_link,
tr::lng_popular_apps_info_here(
) | Ui::Text::ToLink(tr::lng_popular_apps_info_url(tr::now)),
Ui::Text::RichLangValue),
.confirmText = tr::lng_popular_apps_info_confirm(),
.title = tr::lng_popular_apps_info_title(),
});
}
} // namespace Dialogs

View File

@@ -219,4 +219,7 @@ private:
[[nodiscard]] object_ptr<Ui::BoxContent> StarsExamplesBox(
not_null<Window::SessionController*> window);
[[nodiscard]] object_ptr<Ui::BoxContent> PopularAppsAboutBox(
not_null<Window::SessionController*> window);
} // namespace Dialogs

View File

@@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer(
return {};
}
result->pb = io.get();
result->flags |= AVFMT_FLAG_CUSTOM_IO;
auto options = (AVDictionary*)nullptr;
const auto guard = gsl::finally([&] { av_dict_free(&options); });
av_dict_set(&options, "usetoc", "1", 0);
const auto error = AvErrorWrap(avformat_open_input(
&result,
nullptr,
@@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer(
return FormatPointer(result);
}
FormatPointer MakeWriteFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence),
const QByteArray &format) {
const AVOutputFormat *found = nullptr;
void *i = nullptr;
while ((found = av_muxer_iterate(&i))) {
if (found->name == format) {
break;
}
}
if (!found) {
LogError(
"av_muxer_iterate",
u"Format %1 not found"_q.arg(QString::fromUtf8(format)));
return {};
}
auto io = MakeIOPointer(opaque, read, write, seek);
if (!io) {
return {};
}
io->seekable = (seek != nullptr);
auto result = (AVFormatContext*)nullptr;
auto error = AvErrorWrap(avformat_alloc_output_context2(
&result,
(AVOutputFormat*)found,
nullptr,
nullptr));
if (!result || error) {
LogError("avformat_alloc_output_context2", error);
return {};
}
result->pb = io.get();
result->flags |= AVFMT_FLAG_CUSTOM_IO;
// Now FormatPointer will own and free the IO context.
io.release();
return FormatPointer(result);
}
void FormatDeleter::operator()(AVFormatContext *value) {
if (value) {
const auto deleter = IOPointer(value->pb);
@@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer(
existing);
}
void SwresampleDeleter::operator()(SwrContext *value) {
if (value) {
swr_free(&value);
}
}
SwresamplePointer MakeSwresamplePointer(
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVChannelLayout *srcLayout,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
uint64_t srcLayout,
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVSampleFormat srcFormat,
int srcRate,
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVChannelLayout *dstLayout,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
uint64_t dstLayout,
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVSampleFormat dstFormat,
int dstRate,
SwresamplePointer *existing) {
// We have to use custom caching for SwsContext, because
// sws_getCachedContext checks passed flags with existing context flags,
// and re-creates context if they're different, but in the process of
// context creation the passed flags are modified before being written
// to the resulting context, so the caching doesn't work.
if (existing && (*existing) != nullptr) {
const auto &deleter = existing->get_deleter();
if (true
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
&& srcLayout->nb_channels == deleter.srcChannels
&& dstLayout->nb_channels == deleter.dstChannels
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
&& (av_get_channel_layout_nb_channels(srcLayout)
== deleter.srcChannels)
&& (av_get_channel_layout_nb_channels(dstLayout)
== deleter.dstChannels)
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
&& srcFormat == deleter.srcFormat
&& dstFormat == deleter.dstFormat
&& srcRate == deleter.srcRate
&& dstRate == deleter.dstRate) {
return std::move(*existing);
}
}
// Initialize audio resampler
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
auto result = (SwrContext*)nullptr;
auto error = AvErrorWrap(swr_alloc_set_opts2(
&result,
dstLayout,
dstFormat,
dstRate,
srcLayout,
srcFormat,
srcRate,
0,
nullptr));
if (error || !result) {
LogError(u"swr_alloc_set_opts2"_q, error);
return SwresamplePointer();
}
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
auto result = swr_alloc_set_opts(
existing ? existing.get() : nullptr,
dstLayout,
dstFormat,
dstRate,
srcLayout,
srcFormat,
srcRate,
0,
nullptr);
if (!result) {
LogError(u"swr_alloc_set_opts"_q);
}
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
error = AvErrorWrap(swr_init(result));
if (error) {
LogError(u"swr_init"_q, error);
swr_free(&result);
return SwresamplePointer();
}
return SwresamplePointer(
result,
{
srcFormat,
srcRate,
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
srcLayout->nb_channels,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
av_get_channel_layout_nb_channels(srcLayout),
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
dstFormat,
dstRate,
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
dstLayout->nb_channels,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
av_get_channel_layout_nb_channels(dstLayout),
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
});
}
void SwscaleDeleter::operator()(SwsContext *value) {
if (value) {
sws_freeContext(value);
}
}
void LogError(const QString &method) {
LOG(("Streaming Error: Error in %1.").arg(method));
void LogError(const QString &method, const QString &details) {
LOG(("Streaming Error: Error in %1%2."
).arg(method
).arg(details.isEmpty() ? QString() : " - " + details));
}
void LogError(const QString &method, AvErrorWrap error) {
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)."
void LogError(
const QString &method,
AvErrorWrap error,
const QString &details) {
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4."
).arg(method
).arg(error.code()
).arg(error.text()));
).arg(error.text()
).arg(details.isEmpty() ? QString() : " - " + details));
}
crl::time PtsToTime(int64_t pts, AVRational timeBase) {

View File

@@ -19,6 +19,8 @@ extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavutil/opt.h>
#include <libavutil/version.h>
} // extern "C"
@@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence));
[[nodiscard]] FormatPointer MakeWriteFormatPointer(
void *opaque,
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
#if DA_FFMPEG_CONST_WRITE_CALLBACK
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
#else
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
#endif
int64_t(*seek)(void *opaque, int64_t offset, int whence),
const QByteArray &format);
struct CodecDeleter {
void operator()(AVCodecContext *value);
@@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
QSize resize,
SwscalePointer *existing = nullptr);
void LogError(const QString &method);
void LogError(const QString &method, FFmpeg::AvErrorWrap error);
struct SwresampleDeleter {
AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;
int srcRate = 0;
int srcChannels = 0;
AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;
int dstRate = 0;
int dstChannels = 0;
void operator()(SwrContext *value);
};
using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;
[[nodiscard]] SwresamplePointer MakeSwresamplePointer(
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVChannelLayout *srcLayout,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
uint64_t srcLayout,
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVSampleFormat srcFormat,
int srcRate,
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVChannelLayout *dstLayout,
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
uint64_t dstLayout,
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
AVSampleFormat dstFormat,
int dstRate,
SwresamplePointer *existing = nullptr);
void LogError(const QString &method, const QString &details = {});
void LogError(
const QString &method,
FFmpeg::AvErrorWrap error,
const QString &details = {});
[[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);
[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);

View File

@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/click_handler_types.h"
#include "history/history_item_helpers.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "api/api_report.h"
#include "history/view/controls/history_view_draft_options.h"
#include "boxes/moderate_messages_box.h"
#include "history/view/media/history_view_sticker.h"
@@ -36,12 +35,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/message_sending_animation_controller.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/text/text_options.h"
#include "ui/text/text_isolated_emoji.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/edit_factcheck_box.h"
#include "ui/boxes/report_box_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/controls/delete_message_context_action.h"
#include "ui/inactive_press.h"
#include "ui/painter.h"
@@ -57,7 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/delete_messages_box.h"
#include "boxes/report_messages_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/translate_box.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/emoji_interactions.h"
@@ -72,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session_settings.h"
#include "mainwidget.h"
#include "menu/menu_item_download_files.h"
#include "menu/menu_sponsored.h"
#include "core/application.h"
#include "apiwrap.h"
#include "api/api_attached_stickers.h"
@@ -135,60 +131,6 @@ int BinarySearchBlocksOrItems(const T &list, int edge) {
&& (!peer->isChannel() || peer->asChannel()->amIn()));
}
void FillSponsoredMessagesMenu(
not_null<Window::SessionController*> controller,
FullMsgId itemId,
not_null<Ui::PopupMenu*> menu) {
const auto &data = controller->session().sponsoredMessages();
const auto info = data.lookupDetails(itemId).info;
const auto show = controller->uiShow();
if (!info.empty()) {
auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
const auto allText = ranges::accumulate(
info,
TextWithEntities(),
[](TextWithEntities a, TextWithEntities b) {
return a.text.isEmpty() ? b : a.append('\n').append(b);
}).text;
const auto callback = [=] {
QGuiApplication::clipboard()->setText(allText);
show->showToast(tr::lng_text_copied(tr::now));
};
for (const auto &i : info) {
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
menu,
st::defaultMenu,
st::historySponsorInfoItem,
st::historyHasCustomEmojiPosition,
base::duplicate(i));
item->clicks(
) | rpl::start_with_next(callback, menu->lifetime());
menu->addAction(std::move(item));
if (i != info.back()) {
menu->addSeparator();
}
}
};
using namespace Ui::Menu;
CreateAddActionCallback(menu)(MenuCallback::Args{
.text = tr::lng_sponsored_info_menu(tr::now),
.handler = nullptr,
.icon = &st::menuIconChannel,
.fillSubmenu = std::move(fillSubmenu),
});
menu->addSeparator(&st::expandedMenuSeparator);
}
menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] {
if (controller->session().premium()) {
using Result = Data::SponsoredReportResult;
controller->session().sponsoredMessages().createReportCallback(
itemId)(Result::Id("-1"), [](const auto &) {});
} else {
ShowPremiumPreviewBox(controller, PremiumFeature::NoAds);
}
}, &st::menuIconCancel);
}
} // namespace
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
@@ -2252,22 +2194,22 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
return item;
};
const auto whoReactedItem = groupLeaderOrSelf(_dragStateItem);
const auto hasWhoReactedItem = whoReactedItem
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::All);
const auto leaderOrSelf = groupLeaderOrSelf(_dragStateItem);
const auto hasWhoReactedItem = leaderOrSelf
&& Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::All);
const auto clickedReaction = link
? link->property(
kReactionsCountEmojiProperty).value<Data::ReactionId>()
: Data::ReactionId();
_whoReactedMenuLifetime.destroy();
if (!clickedReaction.empty()
&& whoReactedItem
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::One)) {
&& leaderOrSelf
&& Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::One)) {
HistoryView::ShowWhoReactedMenu(
&_menu,
e->globalPos(),
this,
whoReactedItem,
leaderOrSelf,
clickedReaction,
_controller,
_whoReactedMenuLifetime);
@@ -2751,8 +2693,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
? link->copyToClipboardContextItemText()
: QString();
if (item && item->isSponsored()) {
FillSponsoredMessagesMenu(controller, item->fullId(), _menu);
const auto sponsored = (item && item->isSponsored())
? item
: (Element::Moused() && Element::Moused()->data()->isSponsored())
? Element::Moused()->data().get()
: nullptr;
if (sponsored) {
Menu::FillSponsored(
this,
Ui::Menu::CreateAddActionCallback(_menu),
controller->uiShow(),
sponsored->fullId(),
false);
}
if (isUponSelected > 0) {
addReplyAction(item);
@@ -2848,7 +2800,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
}
}
if (!actionText.isEmpty() && !hasCopyRestriction(item)) {
if (!actionText.isEmpty()) {
_menu->addAction(
actionText,
[text = link->copyToClipboardText()] {
@@ -2860,22 +2812,30 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
HistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History);
}, &st::menuIconLink);
}
if (item && item->isSponsored()) {
if (!_menu->empty()) {
_menu->addSeparator(&st::expandedMenuSeparator);
if (sponsored) {
const auto hasAbout = ranges::any_of(
_menu->actions(),
[about = tr::lng_sponsored_menu_revenued_about(tr::now)](
const QAction *action) {
return action->text() == about;
});
if (!hasAbout) {
if (!_menu->empty()) {
_menu->addSeparator(&st::expandedMenuSeparator);
}
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
_menu,
st::menuWithIcons,
st::historyHasCustomEmoji,
st::historySponsoredAboutMenuLabelPosition,
TextWithEntities{ tr::lng_sponsored_title(tr::now) },
&st::menuIconInfo);
item->clicks(
) | rpl::start_with_next([=] {
controller->show(Box(Ui::AboutSponsoredBox));
}, item->lifetime());
_menu->addAction(std::move(item));
}
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
_menu,
st::menuWithIcons,
st::historyHasCustomEmoji,
st::historySponsoredAboutMenuLabelPosition,
TextWithEntities{ tr::lng_sponsored_title(tr::now) },
&st::menuIconInfo);
item->clicks(
) | rpl::start_with_next([=] {
controller->show(Box(Ui::AboutSponsoredBox));
}, item->lifetime());
_menu->addAction(std::move(item));
}
if (isUponSelected > 1) {
if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
@@ -2935,18 +2895,29 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
if (_dragStateItem) {
const auto view = viewByItem(_dragStateItem);
const auto textItem = view ? view->textItem() : _dragStateItem;
const auto wasAmount = _menu->actions().size();
HistoryView::AddEmojiPacksAction(
_menu,
textItem ? textItem : _dragStateItem,
HistoryView::EmojiPacksSource::Message,
_controller);
const auto added = (_menu->actions().size() > wasAmount);
if (!added && !_menu->empty()) {
_menu->addSeparator();
}
HistoryView::AddSelectRestrictionAction(
_menu,
textItem ? textItem : _dragStateItem,
!added);
}
if (hasWhoReactedItem) {
HistoryView::AddWhoReactedAction(
_menu,
this,
whoReactedItem,
leaderOrSelf,
_controller);
} else if (leaderOrSelf) {
HistoryView::MaybeAddWhenEditedAction(_menu, leaderOrSelf);
}
if (_menu->empty()) {

View File

@@ -297,10 +297,12 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
return nullptr;
}
return document->match([&](const MTPDdocument &document) -> Result {
const auto list = media.valt_documents();
return std::make_unique<Data::MediaFile>(
item,
item->history()->owner().processDocument(document),
item->history()->owner().processDocument(document, list),
media.is_nopremium(),
list && !list->v.isEmpty(),
media.is_spoiler(),
media.vttl_seconds().value_or_empty());
}, [](const MTPDdocumentEmpty &) -> Result {
@@ -626,11 +628,13 @@ HistoryItem::HistoryItem(
createComponentsHelper(std::move(fields));
const auto skipPremiumEffect = !history->session().premium();
const auto video = document->video();
const auto spoiler = false;
_media = std::make_unique<Data::MediaFile>(
this,
document,
skipPremiumEffect,
video && !video->qualities.empty(),
spoiler,
/*ttlSeconds = */0);
setText(caption);
@@ -759,6 +763,10 @@ TimeId HistoryItem::date() const {
return _date;
}
bool HistoryItem::awaitingVideoProcessing() const {
return (_flags & MessageFlag::EstimatedDate);
}
HistoryServiceDependentData *HistoryItem::GetServiceDependentData() {
if (const auto pinned = Get<HistoryServicePinned>()) {
return pinned;
@@ -1470,12 +1478,10 @@ void HistoryItem::returnSavedMedia() {
}
void HistoryItem::savePreviousMedia() {
Expects(_media != nullptr);
AddComponents(HistoryMessageSavedMediaData::Bit());
const auto data = Get<HistoryMessageSavedMediaData>();
data->text = originalText();
data->media = _media->clone(this);
data->media = _media ? _media->clone(this) : nullptr;
}
bool HistoryItem::isEditingMedia() const {
@@ -1785,6 +1791,7 @@ void HistoryItem::setStoryFields(not_null<Data::Story*> story) {
this,
document,
/*skipPremiumEffect=*/false,
/*hasQualitiesList=*/false,
spoiler,
/*ttlSeconds = */0);
}
@@ -2208,6 +2215,10 @@ bool HistoryItem::allowsSendNow() const {
&& !isEditingMedia();
}
bool HistoryItem::allowsReschedule() const {
return allowsSendNow() && !awaitingVideoProcessing();
}
bool HistoryItem::allowsForward() const {
return !isService()
&& isRegular()
@@ -2231,6 +2242,11 @@ bool HistoryItem::allowsEdit(TimeId now) const {
&& !isEditingMedia();
}
bool HistoryItem::allowsEditMedia() const {
return !awaitingVideoProcessing()
&& (!_media || _media->allowsEditMedia());
}
bool HistoryItem::canBeEdited() const {
if ((!isRegular() && !isScheduled() && !isBusinessShortcut())
|| Has<HistoryMessageVia>()

View File

@@ -429,8 +429,10 @@ public:
[[nodiscard]] bool forbidsForward() const;
[[nodiscard]] bool forbidsSaving() const;
[[nodiscard]] bool allowsSendNow() const;
[[nodiscard]] bool allowsReschedule() const;
[[nodiscard]] bool allowsForward() const;
[[nodiscard]] bool allowsEdit(TimeId now) const;
[[nodiscard]] bool allowsEditMedia() const;
[[nodiscard]] bool canDelete() const;
[[nodiscard]] bool canDeleteForEveryone(TimeId now) const;
[[nodiscard]] bool suggestReport() const;
@@ -481,6 +483,7 @@ public:
[[nodiscard]] GlobalMsgId globalId() const;
[[nodiscard]] Data::MessagePosition position() const;
[[nodiscard]] TimeId date() const;
[[nodiscard]] bool awaitingVideoProcessing() const;
[[nodiscard]] Data::Media *media() const {
return _media.get();

View File

@@ -409,11 +409,21 @@ ClickHandlerPtr ReportSponsoredClickHandler(not_null<HistoryItem*> item) {
Menu::ShowSponsored(
controller->widget(),
controller->uiShow(),
item);
item->fullId());
}
});
}
ClickHandlerPtr AboutSponsoredClickHandler() {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
Menu::ShowSponsoredAbout(controller->uiShow(), my.itemId);
}
});
}
MessageFlags FlagsFromMTP(
MsgId id,
MTPDmessage::Flags flags,
@@ -441,7 +451,10 @@ MessageFlags FlagsFromMTP(
: Flag())
| ((flags & MTP::f_views) ? Flag::HasViews : Flag())
| ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag())
| ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag());
| ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag())
| ((flags & MTP::f_video_processing_pending)
? Flag::EstimatedDate
: Flag());
}
MessageFlags FlagsFromMTP(

View File

@@ -148,6 +148,7 @@ ClickHandlerPtr JumpToStoryClickHandler(
[[nodiscard]] ClickHandlerPtr HideSponsoredClickHandler();
[[nodiscard]] ClickHandlerPtr ReportSponsoredClickHandler(
not_null<HistoryItem*> item);
[[nodiscard]] ClickHandlerPtr AboutSponsoredClickHandler();
[[nodiscard]] not_null<HistoryItem*> GenerateJoinedMessage(
not_null<History*> history,

View File

@@ -150,6 +150,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "main/session/send_as_peers.h"
#include "webrtc/webrtc_environment.h"
#include "window/notifications_manager.h"
#include "window/window_adaptive.h"
#include "window/window_controller.h"
@@ -558,6 +559,14 @@ HistoryWidget::HistoryWidget(
Window::ActivateWindow(controller);
});
Core::App().mediaDevices().recordAvailabilityValue(
) | rpl::start_with_next([=](Webrtc::RecordAvailability value) {
_recordAvailability = value;
if (_list) {
updateSendButtonType();
}
}, lifetime());
session().data().newItemAdded(
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
newItemAdded(item);
@@ -714,6 +723,35 @@ HistoryWidget::HistoryWidget(
maybeMarkReactionsRead(update.item);
}, lifetime());
session().data().sentToScheduled(
) | rpl::start_with_next([=](const Data::SentToScheduled &value) {
const auto history = value.history;
if (history == _history) {
const auto id = value.scheduledId;
crl::on_main(this, [=] {
if (history == _history) {
controller->showSection(
std::make_shared<HistoryView::ScheduledMemento>(
history,
id));
}
});
return;
}
}, lifetime());
session().data().sentFromScheduled(
) | rpl::start_with_next([=](const Data::SentFromScheduled &value) {
if (value.item->awaitingVideoProcessing()
&& !_sentFromScheduledTip
&& HistoryView::ShowScheduledVideoPublished(
controller,
value,
crl::guard(this, [=] { _sentFromScheduledTip = false; }))) {
_sentFromScheduledTip = true;
}
}, lifetime());
using MediaSwitch = Media::Player::Instance::Switch;
Media::Player::instance()->switchToNextEvents(
) | rpl::filter([=](const MediaSwitch &pair) {
@@ -1042,6 +1080,7 @@ void HistoryWidget::initVoiceRecordBar() {
data.bytes,
data.waveform,
data.duration,
data.video,
action);
_voiceRecordBar->clearListenState();
}, lifetime());
@@ -1055,6 +1094,24 @@ void HistoryWidget::initVoiceRecordBar() {
_cornerButtons.updateUnreadThingsVisibility();
}, lifetime());
_voiceRecordBar->errors(
) | rpl::start_with_next([=](::Media::Capture::Error error) {
using Error = ::Media::Capture::Error;
switch (error) {
case Error::AudioInit:
case Error::AudioTimeout:
controller()->showToast(tr::lng_record_audio_problem(tr::now));
break;
case Error::VideoInit:
case Error::VideoTimeout:
controller()->showToast(tr::lng_record_video_problem(tr::now));
break;
default:
controller()->showToast(u"Unknown error."_q);
break;
}
}, lifetime());
_voiceRecordBar->updateSendButtonTypeRequests(
) | rpl::start_with_next([=] {
updateSendButtonType();
@@ -1067,7 +1124,20 @@ void HistoryWidget::initVoiceRecordBar() {
_voiceRecordBar->recordingTipRequests(
) | rpl::start_with_next([=] {
controller()->showToast(tr::lng_record_hold_tip(tr::now));
Core::App().settings().setRecordVideoMessages(
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record: {
const auto can = Webrtc::RecordAvailability::VideoAndAudio;
controller()->showToast((_recordAvailability == can)
? tr::lng_record_voice_tip(tr::now)
: tr::lng_record_hold_tip(tr::now));
} break;
case Ui::SendButton::Type::Round:
controller()->showToast(tr::lng_record_video_tip(tr::now));
break;
}
}, lifetime());
_voiceRecordBar->recordingStateChanges(
@@ -1581,6 +1651,9 @@ void HistoryWidget::orderWidgets() {
if (_translateBar) {
_translateBar->raise();
}
if (_sponsoredMessageBar) {
_sponsoredMessageBar->raise();
}
if (_pinnedBar) {
_pinnedBar->raise();
}
@@ -2104,6 +2177,7 @@ void HistoryWidget::showHistory(
MsgId showAtMsgId,
const TextWithEntities &highlightPart,
int highlightPartOffsetHint) {
_pinnedClickedId = FullMsgId();
_minPinnedId = std::nullopt;
_showAtMsgHighlightPart = {};
@@ -2253,6 +2327,7 @@ void HistoryWidget::showHistory(
_history->showAtMsgId = _showAtMsgId;
destroyUnreadBarOnClose();
_sponsoredMessageBar = nullptr;
_pinnedBar = nullptr;
_translateBar = nullptr;
_pinnedTracker = nullptr;
@@ -2281,7 +2356,7 @@ void HistoryWidget::showHistory(
_processingReplyItem = _replyEditMsg = nullptr;
_processingReplyTo = _replyTo = FullReplyTo();
_editMsgId = MsgId();
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
updateReplaceMediaButton();
_fieldBarCancel->hide();
@@ -2298,6 +2373,8 @@ void HistoryWidget::showHistory(
_contactStatus = nullptr;
_businessBotStatus = nullptr;
Core::App().mediaDevices().refreshRecordAvailability();
if (peerId) {
using namespace HistoryView;
_peer = session().data().peer(peerId);
@@ -2472,12 +2549,9 @@ void HistoryWidget::showHistory(
unreadCountUpdated(); // set _historyDown badge.
showAboutTopPromotion();
{
if (!session().sponsoredMessages().isTopBarFor(_history)) {
_scroll->setTrackingContent(false);
const auto checkState = crl::guard(this, [=, history = _history] {
if (history != _history) {
return;
}
const auto checkState = [=] {
using State = Data::SponsoredMessages::State;
const auto state = session().sponsoredMessages().state(
_history);
@@ -2487,9 +2561,17 @@ void HistoryWidget::showHistory(
session().sponsoredMessages().canHaveFor(_history));
} else if (state == State::InjectToMiddle) {
injectSponsoredMessages();
} else if (state == State::AppendToTopBar) {
}
});
session().sponsoredMessages().request(_history, checkState);
};
const auto history = _history;
session().sponsoredMessages().request(
_history,
crl::guard(this, [=, this] {
if (history == _history) {
checkState();
}
}));
checkState();
}
} else {
@@ -2667,7 +2749,7 @@ void HistoryWidget::setEditMsgId(MsgId msgId) {
_editMsgId = msgId;
if (!msgId) {
_mediaEditManager.cancel();
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
if (_preview) {
_preview->setDisabled(false);
}
@@ -2723,14 +2805,16 @@ void HistoryWidget::clearAllLoadRequests() {
}
bool HistoryWidget::updateReplaceMediaButton() {
if (!_canReplaceMedia) {
if (!_canReplaceMedia && !_canAddMedia) {
const auto result = (_replaceMedia != nullptr);
_replaceMedia.destroy();
return result;
} else if (_replaceMedia) {
return false;
}
_replaceMedia.create(this, st::historyReplaceMedia);
_replaceMedia.create(
this,
_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] {
base::call_delayed(hideDuration, this, [=] {
@@ -2896,6 +2980,9 @@ void HistoryWidget::updateControlsVisibility() {
if (_pinnedBar) {
_pinnedBar->show();
}
if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) {
_sponsoredMessageBar->toggle(true, anim::type::normal);
}
if (_translateBar) {
_translateBar->show();
}
@@ -4093,6 +4180,9 @@ void HistoryWidget::hideChildWidgets() {
if (_pinnedBar) {
_pinnedBar->hide();
}
if (_sponsoredMessageBar) {
_sponsoredMessageBar->toggle(false, anim::type::instant);
}
if (_translateBar) {
_translateBar->hide();
}
@@ -4253,7 +4343,11 @@ auto HistoryWidget::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return Type::Record;
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
const auto video = Core::App().settings().recordVideoMessages();
return (video && _recordAvailability == both)
? Type::Round
: Type::Record;
}
return Type::Send;
}
@@ -4426,6 +4520,7 @@ void HistoryWidget::showFinished() {
_showAnimation = nullptr;
doneShow();
synteticScrollToY(_scroll->scrollTop());
requestSponsoredMessageBar();
}
void HistoryWidget::doneShow() {
@@ -4446,6 +4541,10 @@ void HistoryWidget::doneShow() {
if (_pinnedBar) {
_pinnedBar->finishAnimating();
}
checkSponsoredMessageBar();
if (_sponsoredMessageBar) {
_sponsoredMessageBar->finishAnimating();
}
if (_translateBar) {
_translateBar->finishAnimating();
}
@@ -4587,7 +4686,8 @@ void HistoryWidget::sendButtonClicked() {
const auto type = _send->type();
if (type == Ui::SendButton::Type::Cancel) {
cancelInlineBot();
} else if (type != Ui::SendButton::Type::Record) {
} else if (type != Ui::SendButton::Type::Record
&& type != Ui::SendButton::Type::Round) {
send({});
}
}
@@ -4877,7 +4977,7 @@ bool HistoryWidget::isSearching() const {
}
bool HistoryWidget::showRecordButton() const {
return Media::Capture::instance()->available()
return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@@ -4908,7 +5008,9 @@ void HistoryWidget::updateSendButtonType() {
}();
_send->setSlowmodeDelay(delay);
_send->setDisabled(disabledBySlowmode
&& (type == Type::Send || type == Type::Record));
&& (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
if (delay != 0) {
base::call_delayed(
@@ -5664,7 +5766,7 @@ bool HistoryWidget::confirmSendingFiles(
Ui::PreparedList &&list,
const QString &insertTextOnCancel) {
if (_editMsgId) {
if (_canReplaceMedia) {
if (_canReplaceMedia || _canAddMedia) {
EditCaptionBox::StartMediaReplace(
controller(),
{ _history->peer->id, _editMsgId },
@@ -5949,8 +6051,14 @@ void HistoryWidget::updateControlsGeometry() {
_pinnedBar->move(0, pinnedBarTop);
_pinnedBar->resizeToWidth(width());
}
const auto translateTop = pinnedBarTop
const auto sponsoredMessageBarTop = pinnedBarTop
+ (_pinnedBar ? _pinnedBar->height() : 0);
if (_sponsoredMessageBar) {
_sponsoredMessageBar->move(0, sponsoredMessageBarTop);
_sponsoredMessageBar->resizeToWidth(width());
}
const auto translateTop = sponsoredMessageBarTop
+ (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0);
if (_translateBar) {
_translateBar->move(0, translateTop);
_translateBar->resizeToWidth(width());
@@ -6195,6 +6303,9 @@ void HistoryWidget::updateHistoryGeometry(
if (_translateBar) {
newScrollHeight -= _translateBar->height();
}
if (_sponsoredMessageBar) {
newScrollHeight -= _sponsoredMessageBar->height();
}
if (_pinnedBar) {
newScrollHeight -= _pinnedBar->height();
}
@@ -6612,6 +6723,7 @@ int HistoryWidget::computeMaxFieldHeight() const {
- _topBar->height()
- (_contactStatus ? _contactStatus->bar().height() : 0)
- (_businessBotStatus ? _businessBotStatus->bar().height() : 0)
- (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0)
- (_pinnedBar ? _pinnedBar->height() : 0)
- (_groupCallBar ? _groupCallBar->height() : 0)
- (_requestsBar ? _requestsBar->height() : 0)
@@ -7500,6 +7612,116 @@ void HistoryWidget::requestMessageData(MsgId msgId) {
session().api().requestMessageData(_peer, msgId, callback);
}
bool HistoryWidget::checkSponsoredMessageBarVisibility() const {
const auto h = _list->height()
- (_kbScroll->isHidden() ? 0 : _kbScroll->height());
return (h > _scroll->height());
}
void HistoryWidget::requestSponsoredMessageBar() {
if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {
return;
}
const auto checkState = [=, this] {
using State = Data::SponsoredMessages::State;
const auto state = session().sponsoredMessages().state(
_history);
_sponsoredMessagesStateKnown = (state != State::None);
if (state == State::AppendToTopBar) {
createSponsoredMessageBar();
if (checkSponsoredMessageBarVisibility()) {
_sponsoredMessageBar->toggle(true, anim::type::normal);
} else {
auto &lifetime = _sponsoredMessageBar->lifetime();
const auto heightLifetime
= lifetime.make_state<rpl::lifetime>();
_list->heightValue(
) | rpl::start_with_next([=, this] {
if (_sponsoredMessageBar->toggled()) {
heightLifetime->destroy();
} else if (checkSponsoredMessageBarVisibility()) {
_sponsoredMessageBar->toggle(
true,
anim::type::normal);
heightLifetime->destroy();
}
}, *heightLifetime);
}
}
};
const auto history = _history;
session().sponsoredMessages().request(
_history,
crl::guard(this, [=, this] {
if (history == _history) {
checkState();
}
}));
}
void HistoryWidget::checkSponsoredMessageBar() {
if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {
return;
}
const auto state = session().sponsoredMessages().state(_history);
if (state == Data::SponsoredMessages::State::AppendToTopBar) {
if (checkSponsoredMessageBarVisibility()) {
if (!_sponsoredMessageBar) {
createSponsoredMessageBar();
}
_sponsoredMessageBar->toggle(true, anim::type::instant);
}
}
}
void HistoryWidget::createSponsoredMessageBar() {
_sponsoredMessageBar = base::make_unique_q<Ui::SlideWrap<>>(
this,
object_ptr<Ui::RpWidget>(this));
_sponsoredMessageBar->entity()->resizeToWidth(_scroll->width());
const auto maybeFullId = session().sponsoredMessages().fillTopBar(
_history,
_sponsoredMessageBar->entity());
session().sponsoredMessages().itemRemoved(
maybeFullId
) | rpl::start_with_next([this] {
_sponsoredMessageBar->shownValue() | rpl::filter(
!rpl::mappers::_1
) | rpl::start_with_next([this] {
_sponsoredMessageBar = nullptr;
}, _sponsoredMessageBar->lifetime());
_sponsoredMessageBar->toggle(false, anim::type::normal);
}, _sponsoredMessageBar->lifetime());
if (maybeFullId) {
const auto viewLifetime
= _sponsoredMessageBar->lifetime().make_state<rpl::lifetime>();
rpl::combine(
_sponsoredMessageBar->entity()->heightValue(),
_sponsoredMessageBar->heightValue()
) | rpl::filter(
rpl::mappers::_1 == rpl::mappers::_2
) | rpl::start_with_next([=] {
session().sponsoredMessages().view(maybeFullId);
viewLifetime->destroy();
}, *viewLifetime);
}
_sponsoredMessageBarHeight = 0;
_sponsoredMessageBar->heightValue(
) | rpl::start_with_next([=](int height) {
_topDelta = _preserveScrollTop
? 0
: (height - _sponsoredMessageBarHeight);
_sponsoredMessageBarHeight = height;
updateHistoryGeometry();
updateControlsGeometry();
_topDelta = 0;
}, _sponsoredMessageBar->lifetime());
_sponsoredMessageBar->toggle(false, anim::type::instant);
}
bool HistoryWidget::sendExistingDocument(
not_null<DocumentData*> document,
Api::MessageToSend messageToSend,
@@ -7954,7 +8176,7 @@ void HistoryWidget::cancelEdit() {
return;
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
updateReplaceMediaButton();
_replyEditMsg = nullptr;
@@ -8309,7 +8531,16 @@ void HistoryWidget::updateReplyEditTexts(bool force) {
if (_editMsgId && _replyEditMsg) {
_mediaEditManager.start(_replyEditMsg);
}
_canReplaceMedia = editMedia && editMedia->allowsEditMedia();
_canReplaceMedia = _editMsgId && _replyEditMsg->allowsEditMedia();
if (editMedia) {
_canAddMedia = false;
} else {
_canAddMedia = base::take(_canReplaceMedia);
}
if (_canReplaceMedia || _canAddMedia) {
// Invalidate the button, maybe icon has changed.
_replaceMedia.destroy();
}
_photoEditMedia = (_canReplaceMedia
&& editMedia->photo()
&& !editMedia->photo()->isNull())

View File

@@ -73,12 +73,18 @@ class SpoilerAnimation;
class ChooseThemeController;
class ContinuousScroll;
struct ChatPaintHighlight;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Ui::Emoji {
class SuggestionsController;
} // namespace Ui::Emoji
namespace Webrtc {
enum class RecordAvailability : uchar;
} // namespace Webrtc
namespace Window {
class SessionController;
} // namespace Window
@@ -528,6 +534,11 @@ private:
void setupGroupCallBar();
void setupRequestsBar();
void checkSponsoredMessageBar();
[[nodiscard]] bool checkSponsoredMessageBarVisibility() const;
void requestSponsoredMessageBar();
void createSponsoredMessageBar();
void sendInlineResult(InlineBots::ResultSelected result);
void drawField(Painter &p, const QRect &rect);
@@ -661,6 +672,7 @@ private:
MsgId _editMsgId = 0;
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
bool _canReplaceMedia = false;
bool _canAddMedia = false;
HistoryView::MediaEditManager _mediaEditManager;
HistoryItem *_replyEditMsg = nullptr;
@@ -685,8 +697,12 @@ private:
std::unique_ptr<Ui::RequestsBar> _requestsBar;
int _requestsBarHeight = 0;
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _sponsoredMessageBar;
int _sponsoredMessageBarHeight = 0;
bool _preserveScrollTop = false;
bool _repaintFieldScheduled = false;
bool _sentFromScheduledTip = false;
mtpRequestId _saveEditMsgRequestId = 0;
@@ -746,6 +762,8 @@ private:
mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false;
Webrtc::RecordAvailability _recordAvailability = {};
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;

View File

@@ -28,6 +28,7 @@ struct VoiceToSend {
VoiceWaveform waveform;
crl::time duration = 0;
Api::SendOptions options;
bool video = false;
};
struct SendActionUpdate {
Api::SendProgressType type = Api::SendProgressType();

View File

@@ -81,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/silent_toggle.h"
#include "ui/chat/choose_send_as.h"
#include "ui/effects/spoiler_mess.h"
#include "webrtc/webrtc_environment.h"
#include "window/window_adaptive.h"
#include "window/window_session_controller.h"
#include "mainwindow.h"
@@ -1151,7 +1152,7 @@ void ComposeControls::setMimeDataHook(MimeDataHook hook) {
bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) {
if (!isEditingMessage() || !_regularWindow) {
return false;
} else if (_canReplaceMedia) {
} else if (_canReplaceMedia || _canAddMedia) {
const auto queryToEdit = _header->queryToEdit();
EditCaptionBox::StartMediaReplace(
_regularWindow,
@@ -1517,7 +1518,7 @@ void ComposeControls::orderControls() {
}
bool ComposeControls::showRecordButton() const {
return ::Media::Capture::instance()->available()
return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@@ -1942,7 +1943,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
_preview->apply({ .removed = true });
_preview->setDisabled(false);
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
return;
}
@@ -1962,7 +1963,16 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
const auto resolve = [=] {
if (const auto item = _history->owner().message(editingId)) {
const auto media = item->media();
_canReplaceMedia = media && media->allowsEditMedia();
_canReplaceMedia = item->allowsEditMedia();
if (media) {
_canAddMedia = false;
} else {
_canAddMedia = base::take(_canReplaceMedia);
}
if (_canReplaceMedia || _canAddMedia) {
// Invalidate the button, maybe icon has changed.
_replaceMedia = nullptr;
}
_photoEditMedia = (_canReplaceMedia
&& _regularWindow
&& media->photo()
@@ -1983,7 +1993,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
return true;
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
_header->editMessage(editingId, false);
return false;
@@ -2004,7 +2014,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
_header->replyToMessage({});
} else {
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
_header->replyToMessage(draft->reply);
if (_header->replyingToMessage()) {
@@ -2137,12 +2147,17 @@ void ComposeControls::initSendButton() {
}
};
SendMenu::SetupMenuAndShortcuts(
_send.get(),
_show,
[=] { return sendButtonMenuDetails(); },
sendAction);
Core::App().mediaDevices().recordAvailabilityValue(
) | rpl::start_with_next([=](Webrtc::RecordAvailability value) {
_recordAvailability = value;
updateSendButtonType();
}, _send->lifetime());
}
void ComposeControls::initSendAsButton(not_null<PeerData*> peer) {
@@ -2413,6 +2428,42 @@ void ComposeControls::initVoiceRecordBar() {
return false;
});
_voiceRecordBar->recordingTipRequests(
) | rpl::start_with_next([=] {
Core::App().settings().setRecordVideoMessages(
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record: {
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
_show->showToast((_recordAvailability == both)
? tr::lng_record_voice_tip(tr::now)
: tr::lng_record_hold_tip(tr::now));
} break;
case Ui::SendButton::Type::Round:
_show->showToast(tr::lng_record_video_tip(tr::now));
break;
}
}, _wrap->lifetime());
_voiceRecordBar->errors(
) | rpl::start_with_next([=](::Media::Capture::Error error) {
using Error = ::Media::Capture::Error;
switch (error) {
case Error::AudioInit:
case Error::AudioTimeout:
_show->showToast(tr::lng_record_audio_problem(tr::now));
break;
case Error::VideoInit:
case Error::VideoTimeout:
_show->showToast(tr::lng_record_video_problem(tr::now));
break;
default:
_show->showToast(u"Unknown error."_q);
break;
}
}, _wrap->lifetime());
_voiceRecordBar->updateSendButtonTypeRequests(
) | rpl::start_with_next([=] {
updateSendButtonType();
@@ -2454,7 +2505,11 @@ auto ComposeControls::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return Type::Record;
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
const auto video = Core::App().settings().recordVideoMessages();
return (video && _recordAvailability == both)
? Type::Round
: Type::Record;
}
return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
}
@@ -2487,7 +2542,9 @@ void ComposeControls::updateSendButtonType() {
}();
_send->setSlowmodeDelay(delay);
_send->setDisabled(_sendDisabledBySlowmode.current()
&& (type == Type::Send || type == Type::Record));
&& (type == Type::Send
|| type == Type::Record
|| type == Type::Round));
}
void ComposeControls::finishAnimating() {
@@ -2882,7 +2939,7 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) {
}
bool ComposeControls::updateReplaceMediaButton() {
if (!_canReplaceMedia || !_regularWindow) {
if ((!_canReplaceMedia && !_canAddMedia) || !_regularWindow) {
const auto result = (_replaceMedia != nullptr);
_replaceMedia = nullptr;
return result;
@@ -2891,7 +2948,7 @@ bool ComposeControls::updateReplaceMediaButton() {
}
_replaceMedia = std::make_unique<Ui::IconButton>(
_wrap.get(),
st::historyReplaceMedia);
_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
_replaceMedia->setClickedCallback([=] {
base::call_delayed(hideDuration, _wrap.get(), [=] {
@@ -3149,8 +3206,9 @@ bool ComposeControls::isRecording() const {
bool ComposeControls::isRecordingPressed() const {
return !_voiceRecordBar->isRecordingLocked()
&& (!_voiceRecordBar->isHidden()
|| (_send->type() == Ui::SendButton::Type::Record
&& _send->isDown()));
|| (_send->isDown()
&& (_send->type() == Ui::SendButton::Type::Record
|| _send->type() == Ui::SendButton::Type::Round)));
}
rpl::producer<bool> ComposeControls::recordingActiveValue() const {

View File

@@ -76,6 +76,10 @@ namespace Main {
class Session;
} // namespace Main
namespace Webrtc {
enum class RecordAvailability : uchar;
} // namespace Webrtc
namespace Window {
struct SectionShow;
class SessionController;
@@ -436,10 +440,12 @@ private:
bool _isInlineBot = false;
bool _botCommandShown = false;
bool _likeShown = false;
Webrtc::RecordAvailability _recordAvailability = {};
FullMsgId _editingId;
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
bool _canReplaceMedia = false;
bool _canAddMedia = false;
std::unique_ptr<Controls::WebpageProcessor> _preview;

View File

@@ -29,6 +29,7 @@ void MediaEditManager::start(
std::optional<bool> invertCaption) {
const auto media = item->media();
if (!media) {
cancel();
return;
}
_item = item;

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "chat_helpers/compose/compose_show.h"
@@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
@@ -911,6 +913,117 @@ void DraftOptionsBox(
}, box->lifetime());
}
}
struct AuthorSelector {
object_ptr<Ui::RpWidget> content = { nullptr };
Fn<bool(int, int, int)> overrideKey;
};
[[nodiscard]] AuthorSelector AuthorRowSelector(
not_null<Main::Session*> session,
FullReplyTo reply,
Fn<void(not_null<Data::Thread*>)> chosen) {
const auto item = session->data().message(reply.messageId);
if (!item) {
return {};
}
const auto displayFrom = item->displayFrom();
const auto from = displayFrom ? displayFrom : item->from().get();
if (!from->isUser() || from == item->history()->peer || from->isSelf()) {
return {};
}
class AuthorController final : public PeerListController {
public:
AuthorController(not_null<PeerData*> peer, Fn<void()> click)
: _peer(peer)
, _click(std::move(click)) {
}
void prepare() override {
delegate()->peerListAppendRow(
std::make_unique<ChatsListBoxController::Row>(
_peer->owner().history(_peer),
&computeListSt().item));
delegate()->peerListRefreshRows();
TrackPremiumRequiredChanges(this, _lifetime);
}
void loadMoreRows() override {
}
void rowClicked(not_null<PeerListRow*> row) override {
if (RecipientRow::ShowLockedError(this, row, WritePremiumRequiredError)) {
return;
} else if (const auto onstack = _click) {
onstack();
}
}
Main::Session &session() const override {
return _peer->session();
}
private:
const not_null<PeerData*> _peer;
Fn<void()> _click;
rpl::lifetime _lifetime;
};
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
container->add(CreatePeerListSectionSubtitle(
container,
tr::lng_reply_in_author()));
Ui::AddSkip(container);
const auto delegate = container->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = container->lifetime().make_state<
AuthorController
>(from, [=] { chosen(from->owner().history(from)); });
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
Ui::AddSkip(container);
container->add(CreatePeerListSectionSubtitle(
container,
tr::lng_reply_in_chats_list()));
const auto overrideKey = [=](int direction, int from, int to) {
if (!content->isVisible()) {
return false;
} else if (direction > 0 && from < 0 && to >= 0) {
if (content->hasSelection()) {
const auto was = content->selectedIndex();
const auto now = content->selectSkip(1).reallyMovedTo;
if (was != now) {
return true;
}
content->clearSelection();
} else {
content->selectSkip(1);
return true;
}
} else if (direction < 0 && to < 0) {
if (!content->hasSelection()) {
content->selectLast();
} else if (from >= 0 || content->hasSelection()) {
content->selectSkip(-1);
}
}
return false;
};
return {
.content = std::move(result),
.overrideKey = overrideKey,
};
}
} // namespace
void ShowReplyToChatBox(
@@ -921,7 +1034,7 @@ void ShowReplyToChatBox(
public:
using Chosen = not_null<Data::Thread*>;
Controller(not_null<Main::Session*> session)
Controller(not_null<Main::Session*> session, FullReplyTo reply)
: ChooseRecipientBoxController({
.session = session,
.callback = [=](Chosen thread) {
@@ -929,6 +1042,13 @@ void ShowReplyToChatBox(
},
.premiumRequiredError = WritePremiumRequiredError,
}) {
_authorRow = AuthorRowSelector(
session,
reply,
[=](Chosen thread) { _singleChosen.fire_copy(thread); });
if (_authorRow.content) {
setStyleOverrides(&st::peerListSmallSkips);
}
}
[[nodiscard]] rpl::producer<Chosen> singleChosen() const {
@@ -939,13 +1059,26 @@ void ShowReplyToChatBox(
return tr::lng_saved_quote_here(tr::now);
}
bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) override {
return _authorRow.overrideKey
&& _authorRow.overrideKey(direction, fromIndex, toIndex);
}
private:
void prepareViewHook() override {
if (_authorRow.content) {
delegate()->peerListSetAboveWidget(
std::move(_authorRow.content));
}
ChooseRecipientBoxController::prepareViewHook();
delegate()->peerListSetTitle(tr::lng_reply_in_another_title());
}
rpl::event_stream<Chosen> _singleChosen;
AuthorSelector _authorRow;
};
@@ -956,7 +1089,7 @@ void ShowReplyToChatBox(
};
const auto session = &show->session();
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
auto controller = std::make_unique<Controller>(session, reply);
const auto controllerRaw = controller.get();
auto box = Box<PeerListBox>(std::move(controller), [=](
not_null<PeerListBox*> box) {

View File

@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/random.h"
#include "base/unixtime.h"
#include "ui/boxes/confirm_box.h"
#include "calls/calls_instance.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/application.h"
#include "data/data_document.h"
@@ -27,27 +28,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio_capture.h"
#include "media/player/media_player_button.h"
#include "media/player/media_player_instance.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_round_preview.h"
#include "storage/storage_account.h"
#include "ui/controls/round_video_recorder.h"
#include "ui/controls/send_button.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/animation_value_f.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "ui/widgets/tooltip.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "webrtc/webrtc_video_track.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_media_player.h"
#include <tgcalls/VideoCaptureInterface.h>
namespace HistoryView::Controls {
namespace {
using SendActionUpdate = VoiceRecordBar::SendActionUpdate;
using VoiceToSend = VoiceRecordBar::VoiceToSend;
constexpr auto kAudioVoiceUpdateView = crl::time(200);
constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes
constexpr auto kMaxSamples
@@ -69,6 +75,61 @@ enum class FilterType {
Cancel,
};
class SoundedPreview final : public Ui::DynamicImage {
public:
SoundedPreview(
not_null<DocumentData*> document,
rpl::producer<> repaints);
std::shared_ptr<DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
const not_null<DocumentData*> _document;
QImage _roundingMask;
Fn<void()> _repaint;
rpl::lifetime _lifetime;
};
SoundedPreview::SoundedPreview(
not_null<DocumentData*> document,
rpl::producer<> repaints)
: _document(document) {
std::move(repaints) | rpl::start_with_next([=] {
if (const auto onstack = _repaint) {
onstack();
}
}, _lifetime);
}
std::shared_ptr<Ui::DynamicImage> SoundedPreview::clone() {
Unexpected("ListenWrap::videoPreview::clone.");
}
QImage SoundedPreview::image(int size) {
const auto player = ::Media::Player::instance();
const auto streamed = player->roundVideoPreview(_document);
if (!streamed) {
return {};
}
const auto full = QSize(size, size) * style::DevicePixelRatio();
if (_roundingMask.size() != full) {
_roundingMask = Images::EllipseMask(full);
}
const auto frame = streamed->frameWithInfo({
.resize = full,
.outer = full,
.mask = _roundingMask,
});
return frame.image;
}
void SoundedPreview::subscribeToUpdates(Fn<void()> callback) {
_repaint = std::move(callback);
}
[[nodiscard]] auto InactiveColor(const QColor &c) {
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
}
@@ -77,10 +138,6 @@ enum class FilterType {
return std::clamp(float64(low) / high, 0., 1.);
}
[[nodiscard]] crl::time Duration(int samples) {
return samples * crl::time(1000) / ::Media::Player::kDefaultFrequency;
}
[[nodiscard]] auto FormatVoiceDuration(int samples) {
const int duration = kPrecision
* (float64(samples) / ::Media::Player::kDefaultFrequency);
@@ -201,6 +258,44 @@ void PaintWaveform(
}
}
void FillWithMinithumbs(
QPainter &p,
not_null<const Ui::RoundVideoResult*> data,
QRect rect,
float64 progress) {
if (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) {
return;
}
const auto size = rect.height();
const auto single = data->minithumbSize;
const auto perrow = data->minithumbs.width() / single;
const auto thumbs = (rect.width() + size - 1) / size;
if (!thumbs || !perrow) {
return;
}
for (auto i = 0; i != thumbs - 1; ++i) {
const auto index = (i * data->minithumbsCount) / thumbs;
p.drawImage(
QRect(rect.x() + i * size, rect.y(), size, size),
data->minithumbs,
QRect(
(index % perrow) * single,
(index / perrow) * single,
single,
single));
}
const auto last = rect.width() - (thumbs - 1) * size;
const auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs;
p.drawImage(
QRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size),
data->minithumbs,
QRect(
(index % perrow) * single,
(index / perrow) * single,
(last * single) / size,
single));
}
[[nodiscard]] QRect DrawLockCircle(
QPainter &p,
const QRect &widgetRect,
@@ -269,7 +364,8 @@ class TTLButton final : public Ui::RippleButton {
public:
TTLButton(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st);
const style::RecordBar &st,
bool recordingVideo);
void clearState() override;
@@ -288,7 +384,8 @@ private:
TTLButton::TTLButton(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st)
const style::RecordBar &st,
bool recordingVideo)
: RippleButton(parent, st.lock.ripple)
, _st(st)
, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))
@@ -315,8 +412,10 @@ TTLButton::TTLButton(
}
auto text = rpl::conditional(
Core::App().settings().ttlVoiceClickTooltipHiddenValue(),
tr::lng_record_once_active_tooltip(
Ui::Text::RichLangValue),
(recordingVideo
? tr::lng_record_once_active_video
: tr::lng_record_once_active_tooltip)(
Ui::Text::RichLangValue),
tr::lng_record_once_first_tooltip(
Ui::Text::RichLangValue));
_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(
@@ -426,35 +525,37 @@ public:
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st,
not_null<Main::Session*> session,
::Media::Capture::Result *data,
not_null<Ui::RoundVideoResult*> data,
const style::font &font);
void requestPaintProgress(float64 progress);
rpl::producer<> stopRequests() const;
[[nodiscard]] rpl::producer<> stopRequests() const;
void playPause();
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();
rpl::lifetime &lifetime();
[[nodiscard]] rpl::lifetime &lifetime();
private:
void init();
void initPlayButton();
void initPlayProgress();
bool isInPlayer(const ::Media::Player::TrackState &state) const;
bool isInPlayer() const;
[[nodiscard]] bool isInPlayer(
const ::Media::Player::TrackState &state) const;
[[nodiscard]] bool isInPlayer() const;
int computeTopMargin(int height) const;
QRect computeWaveformRect(const QRect &centerRect) const;
[[nodiscard]] int computeTopMargin(int height) const;
[[nodiscard]] QRect computeWaveformRect(const QRect &centerRect) const;
not_null<Ui::RpWidget*> _parent;
const not_null<Ui::RpWidget*> _parent;
const style::RecordBar &_st;
const not_null<Main::Session*> _session;
const not_null<DocumentData*> _document;
const std::unique_ptr<VoiceData> _voiceData;
const std::shared_ptr<Data::DocumentMedia> _mediaView;
const not_null<::Media::Capture::Result*> _data;
const not_null<Ui::RoundVideoResult*> _data;
const base::unique_qptr<Ui::IconButton> _delete;
const style::font &_durationFont;
const QString _duration;
@@ -475,6 +576,7 @@ private:
anim::value _playProgress;
rpl::variable<float64> _showProgress = 0.;
rpl::event_stream<> _videoRepaints;
rpl::lifetime _lifetime;
@@ -484,7 +586,7 @@ ListenWrap::ListenWrap(
not_null<Ui::RpWidget*> parent,
const style::RecordBar &st,
not_null<Main::Session*> session,
::Media::Capture::Result *data,
not_null<Ui::RoundVideoResult*> data,
const style::font &font)
: _parent(parent)
, _st(st)
@@ -495,8 +597,7 @@ ListenWrap::ListenWrap(
, _data(data)
, _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove))
, _durationFont(font)
, _duration(Ui::FormatDurationText(
float64(_data->samples) / ::Media::Player::kDefaultFrequency))
, _duration(Ui::FormatDurationText(_data->duration / 1000))
, _durationWidth(_durationFont->width(_duration))
, _playPauseSt(st::mediaPlayerButton)
, _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))
@@ -603,20 +704,27 @@ void ListenWrap::init() {
}
// Waveform paint.
{
const auto rect = (progress == 1.)
? _waveformFgRect
: computeWaveformRect(bgCenterRect);
if (rect.width() > 0) {
p.translate(rect.topLeft());
const auto waveformRect = (progress == 1.)
? _waveformFgRect
: computeWaveformRect(bgCenterRect);
if (!waveformRect.isEmpty()) {
const auto playProgress = _playProgress.current();
if (_data->minithumbs.isNull()) {
p.translate(waveformRect.topLeft());
PaintWaveform(
p,
_voiceData.get(),
rect.width(),
waveformRect.width(),
_activeWaveformBar,
_inactiveWaveformBar,
_playProgress.current());
playProgress);
p.resetTransform();
} else {
FillWithMinithumbs(
p,
_data,
waveformRect,
playProgress);
}
}
}
@@ -630,9 +738,11 @@ void ListenWrap::initPlayButton() {
using namespace ::Media::Player;
using State = TrackState;
_mediaView->setBytes(_data->bytes);
_document->size = _data->bytes.size();
_document->type = VoiceDocument;
_mediaView->setBytes(_data->content);
_document->size = _data->content.size();
_document->type = _data->minithumbs.isNull()
? VoiceDocument
: RoundVideoDocument;
const auto &play = _playPauseSt.playOuter;
const auto &width = _waveformBgFinalCenterRect.height();
@@ -668,6 +778,9 @@ void ListenWrap::initPlayButton() {
) | rpl::start_with_next([=](const State &state) {
if (isInPlayer(state)) {
*showPause = ShowPauseIcon(state.state);
if (!_data->minithumbs.isNull()) {
_videoRepaints.fire({});
}
} else if (showPause->current()) {
*showPause = false;
}
@@ -678,6 +791,13 @@ void ListenWrap::initPlayButton() {
) | rpl::start_with_next([=] {
*showPause = false;
}, _lifetime);
_lifetime.add([=] {
const auto current = instance()->current(AudioMsgId::Type::Voice);
if (current.audio() == _document) {
instance()->stop(AudioMsgId::Type::Voice, true);
}
});
}
void ListenWrap::initPlayProgress() {
@@ -817,6 +937,12 @@ rpl::producer<> ListenWrap::stopRequests() const {
return _delete->clicks() | rpl::to_empty;
}
std::shared_ptr<Ui::DynamicImage> ListenWrap::videoPreview() {
return std::make_shared<SoundedPreview>(
_document,
_videoRepaints.events());
}
rpl::lifetime &ListenWrap::lifetime() {
return _lifetime;
}
@@ -831,6 +957,7 @@ public:
void requestPaintLockToStopProgress(float64 progress);
void requestPaintPauseToInputProgress(float64 progress);
void setVisibleTopPart(int part);
void setRecordingVideo(bool value);
[[nodiscard]] rpl::producer<> locks() const;
[[nodiscard]] bool isLocked() const;
@@ -859,6 +986,7 @@ private:
float64 _pauseToInputProgress = 0.;
rpl::variable<float64> _progress = 0.;
int _visibleTopPart = -1;
bool _recordingVideo = false;
};
@@ -882,6 +1010,10 @@ void RecordLock::setVisibleTopPart(int part) {
_visibleTopPart = part;
}
void RecordLock::setRecordingVideo(bool value) {
_recordingVideo = value;
}
void RecordLock::init() {
shownValue(
) | rpl::start_with_next([=](bool shown) {
@@ -972,9 +1104,10 @@ void RecordLock::drawProgress(QPainter &p) {
p.setBrush(_st.fg);
if (_pauseToInputProgress > 0.) {
p.setOpacity(_pauseToInputProgress);
st::historyRecordLockInput.paintInCenter(
p,
blockRect.toRect());
const auto &icon = _recordingVideo
? st::historyRecordLockRound
: st::historyRecordLockInput;
icon.paintInCenter(p, blockRect.toRect());
p.setOpacity(1. - _pauseToInputProgress);
}
p.drawRoundedRect(
@@ -1244,7 +1377,7 @@ VoiceRecordBar::VoiceRecordBar(
}
VoiceRecordBar::~VoiceRecordBar() {
if (isRecording()) {
if (isActive()) {
stopRecording(StopType::Cancel);
}
}
@@ -1298,7 +1431,12 @@ void VoiceRecordBar::updateTTLGeometry(
const auto parent = parentWidget();
const auto me = Ui::MapFrom(_outerContainer, parent, geometry());
const auto anyTop = me.y() - st::historyRecordLockPosition.y();
const auto ttlFrom = anyTop - _ttlButton->height() * 2;
const auto lockHiddenProgress = (_lockShowing.current() || !_fullRecord)
? 0.
: (1. - _showLockAnimation.value(0.));
const auto ttlFrom = anyTop
- _ttlButton->height()
- (_ttlButton->height() * (1. - lockHiddenProgress));
if (type == TTLAnimationType::RightLeft) {
const auto finalRight = _outerContainer->width()
- rect::right(me)
@@ -1418,6 +1556,9 @@ void VoiceRecordBar::init() {
} else if (value == 1. && show) {
computeAndSetLockProgress(QCursor::pos());
}
if (_fullRecord && !show) {
updateTTLGeometry(TTLAnimationType::RightLeft, 1.);
}
};
_showLockAnimation.start(std::move(callback), from, to, duration);
}, lifetime());
@@ -1473,7 +1614,6 @@ void VoiceRecordBar::init() {
if (!paused) {
return;
}
// _lockShowing = false;
const auto to = 1.;
auto callback = [=](float64 value) {
@@ -1496,7 +1636,8 @@ void VoiceRecordBar::init() {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
_st,
_recordingVideo);
}
_ttlButton->show();
}
@@ -1529,12 +1670,14 @@ void VoiceRecordBar::init() {
if (_startRecordingFilter && _startRecordingFilter()) {
return;
}
_recordingTipRequired = true;
_recordingTipRequire = crl::now();
_recordingVideo = (_send->type() == Ui::SendButton::Type::Round);
_fullRecord = false;
_ttlButton = nullptr;
_lock->setRecordingVideo(_recordingVideo);
_startTimer.callOnce(st::universalDuration);
} else if (e->type() == QEvent::MouseButtonRelease) {
if (base::take(_recordingTipRequired)) {
_recordingTipRequests.fire({});
}
checkTipRequired();
_startTimer.cancel();
}
}, lifetime());
@@ -1579,6 +1722,11 @@ void VoiceRecordBar::activeAnimate(bool active) {
}
void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
if (_send->type() == Ui::SendButton::Type::Round) {
_level->setType(VoiceRecordButton::Type::Round);
} else {
_level->setType(VoiceRecordButton::Type::Record);
}
const auto to = show ? 1. : 0.;
const auto from = show ? 0. : 1.;
auto animationCallback = [=, callback = std::move(callback)](auto value) {
@@ -1652,6 +1800,10 @@ void VoiceRecordBar::startRecording() {
}
using namespace ::Media::Capture;
if (_recordingVideo && !createVideoRecorder()) {
stop(false);
return;
}
if (!instance()->available()) {
stop(false);
return;
@@ -1664,16 +1816,36 @@ void VoiceRecordBar::startRecording() {
if (_paused.current()) {
_paused = false;
instance()->pause(false, nullptr);
if (_videoRecorder) {
_videoRecorder->resume({
.video = std::move(_data),
});
}
} else {
instance()->start();
instance()->start(_videoRecorder
? _videoRecorder->audioChunkProcessor()
: nullptr);
}
instance()->updated(
) | rpl::start_with_next_error([=](const Update &update) {
_recordingTipRequired = (update.samples < kMinSamples);
recordUpdated(update.level, update.samples);
}, [=] {
stop(false);
}, _recordingLifetime);
if (_videoRecorder) {
_videoRecorder->updated(
) | rpl::start_with_next_error([=](const Update &update) {
recordUpdated(update.level, update.samples);
if (update.finished) {
_fullRecord = true;
stopRecording(StopType::Listen);
_lockShowing = false;
}
}, [=](Error error) {
stop(false);
_errors.fire_copy(error);
}, _recordingLifetime);
}
_recordingLifetime.add([=] {
_recording = false;
});
@@ -1705,14 +1877,22 @@ void VoiceRecordBar::startRecording() {
}
computeAndSetLockProgress(mouse->globalPos());
} else if (type == QEvent::MouseButtonRelease) {
if (base::take(_recordingTipRequired)) {
_recordingTipRequests.fire({});
}
checkTipRequired();
stop(_inField.current());
}
}, _recordingLifetime);
}
void VoiceRecordBar::checkTipRequired() {
const auto require = base::take(_recordingTipRequire);
const auto duration = st::universalDuration
+ (kMinSamples * crl::time(1000)
/ ::Media::Player::kDefaultFrequency);
if (require && (require + duration > crl::now())) {
_recordingTipRequests.fire({});
}
}
void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
_level->requestPaintLevel(level);
_recordingSamples = samples;
@@ -1721,7 +1901,10 @@ void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
}
Core::App().updateNonIdle();
update(_durationRect);
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice });
const auto type = _recordingVideo
? Api::SendProgressType::RecordRound
: Api::SendProgressType::RecordVoice;
_sendActionUpdates.fire({ type });
}
void VoiceRecordBar::stop(bool send) {
@@ -1735,7 +1918,6 @@ void VoiceRecordBar::stop(bool send) {
const auto type = send ? StopType::Send : StopType::Cancel;
stopRecording(type, ttlBeforeHide);
};
// _lockShowing = false;
visibilityAnimate(false, std::move(disappearanceCallback));
}
@@ -1754,7 +1936,10 @@ void VoiceRecordBar::finish() {
[[maybe_unused]] const auto s = takeTTLState();
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
const auto type = _recordingVideo
? Api::SendProgressType::RecordRound
: Api::SendProgressType::RecordVoice;
_sendActionUpdates.fire({ type, -1 });
_data = {};
}
@@ -1769,39 +1954,99 @@ void VoiceRecordBar::hideFast() {
void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
using namespace ::Media::Capture;
if (type == StopType::Cancel) {
if (_videoRecorder) {
_videoRecorder->hide();
}
instance()->stop(crl::guard(this, [=](Result &&data) {
_cancelRequests.fire({});
}));
} else if (type == StopType::Listen) {
instance()->pause(true, crl::guard(this, [=](Result &&data) {
if (data.bytes.isEmpty()) {
// Close everything.
stop(false);
return;
}
_paused = true;
_data = std::move(data);
if (const auto recorder = _videoRecorder.get()) {
const auto weak = base::make_weak(recorder);
recorder->pause([=](Ui::RoundVideoResult data) {
crl::on_main(weak, [=, data = std::move(data)]() mutable {
window()->raise();
window()->activateWindow();
window()->raise();
window()->activateWindow();
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
_paused = true;
_data = std::move(data);
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
// _lockShowing = false;
}));
using SilentPreview = ::Media::Streaming::RoundPreview;
recorder->showPreview(
std::make_shared<SilentPreview>(
_data.content,
recorder->previewSize()),
_listen->videoPreview());
});
});
instance()->pause(true);
} else {
instance()->pause(true, crl::guard(this, [=](Result &&data) {
if (data.bytes.isEmpty()) {
// Close everything.
stop(false);
return;
}
_paused = true;
_data = Ui::RoundVideoResult{
.content = std::move(data.bytes),
.waveform = std::move(data.waveform),
.duration = data.duration,
};
window()->raise();
window()->activateWindow();
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
}));
}
} else if (type == StopType::Send) {
if (_videoRecorder) {
const auto weak = Ui::MakeWeak(this);
_videoRecorder->hide([=](Ui::RoundVideoResult data) {
crl::on_main([=, data = std::move(data)]() mutable {
if (weak) {
window()->raise();
window()->activateWindow();
const auto options = Api::SendOptions{
.ttlSeconds = (ttlBeforeHide
? std::numeric_limits<int>::max()
: 0),
};
_sendVoiceRequests.fire({
.bytes = data.content,
//.waveform = {},
.duration = data.duration,
.options = options,
.video = true,
});
}
});
});
}
instance()->stop(crl::guard(this, [=](Result &&data) {
if (data.bytes.isEmpty()) {
// Close everything.
stop(false);
return;
}
_data = std::move(data);
_data = Ui::RoundVideoResult{
.content = std::move(data.bytes),
.waveform = std::move(data.waveform),
.duration = data.duration,
};
window()->raise();
window()->activateWindow();
@@ -1811,10 +2056,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
: 0),
};
_sendVoiceRequests.fire({
_data.bytes,
_data.waveform,
Duration(_data.samples),
options,
.bytes = _data.content,
.waveform = _data.waveform,
.duration = _data.duration,
.options = options,
});
}));
}
@@ -1878,10 +2123,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
options.ttlSeconds = std::numeric_limits<int>::max();
}
_sendVoiceRequests.fire({
_data.bytes,
_data.waveform,
Duration(_data.samples),
options,
.bytes = _data.content,
.waveform = _data.waveform,
.duration = _data.duration,
.options = options,
.video = !_data.minithumbs.isNull(),
});
}
}
@@ -1954,6 +2200,10 @@ rpl::producer<> VoiceRecordBar::recordingTipRequests() const {
return _recordingTipRequests.events();
}
auto VoiceRecordBar::errors() const -> rpl::producer<Error> {
return _errors.events();
}
bool VoiceRecordBar::isLockPresent() const {
return _lockShowing.current();
}
@@ -1963,7 +2213,8 @@ bool VoiceRecordBar::isListenState() const {
}
bool VoiceRecordBar::isTypeRecord() const {
return (_send->type() == Ui::SendButton::Type::Record);
return (_send->type() == Ui::SendButton::Type::Record)
|| (_send->type() == Ui::SendButton::Type::Round);
}
bool VoiceRecordBar::isRecordingByAnotherBar() const {
@@ -2085,8 +2336,12 @@ void VoiceRecordBar::showDiscardBox(
};
_show->showBox(Ui::MakeConfirmBox({
.text = (isListenState()
? tr::lng_record_listen_cancel_sure
: tr::lng_record_lock_cancel_sure)(),
? (_recordingVideo
? tr::lng_record_listen_cancel_sure_round
: tr::lng_record_listen_cancel_sure)
: (_recordingVideo
? tr::lng_record_lock_cancel_sure_round
: tr::lng_record_lock_cancel_sure))(),
.confirmed = std::move(sure),
.confirmText = tr::lng_record_lock_discard(),
.confirmStyle = &st::attentionBoxButton,
@@ -2094,4 +2349,52 @@ void VoiceRecordBar::showDiscardBox(
_warningShown = true;
}
bool VoiceRecordBar::createVideoRecorder() {
if (_videoRecorder) {
return true;
}
const auto hiding = [=](not_null<Ui::RoundVideoRecorder*> which) {
if (_videoRecorder.get() == which) {
_videoHiding.push_back(base::take(_videoRecorder));
}
};
const auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) {
if (_videoRecorder.get() == which) {
_videoRecorder = nullptr;
}
_videoHiding.erase(
ranges::remove(
_videoHiding,
which.get(),
&std::unique_ptr<Ui::RoundVideoRecorder>::get),
end(_videoHiding));
};
auto capturer = Core::App().calls().getVideoCapture();
auto track = std::make_shared<Webrtc::VideoTrack>(
Webrtc::VideoState::Active);
capturer->setOutput(track->sink());
capturer->setPreferredAspectRatio(1.);
_videoCapturerLifetime = track->stateValue(
) | rpl::start_with_next([=](Webrtc::VideoState state) {
capturer->setState((state == Webrtc::VideoState::Active)
? tgcalls::VideoState::Active
: tgcalls::VideoState::Inactive);
});
_videoRecorder = std::make_unique<Ui::RoundVideoRecorder>(
Ui::RoundVideoRecorderDescriptor{
.container = _outerContainer,
.hiding = hiding,
.hidden = hidden,
.capturer = std::move(capturer),
.track = std::move(track),
.placeholder = _show->session().local().readRoundPlaceholder(),
});
_videoRecorder->placeholderUpdates(
) | rpl::start_with_next([=](QImage &&placeholder) {
_show->session().local().writeRoundPlaceholder(placeholder);
}, _videoCapturerLifetime);
return true;
}
} // namespace HistoryView::Controls

View File

@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer.h"
#include "history/view/controls/compose_controls_common.h"
#include "media/audio/media_audio_capture_common.h"
#include "ui/controls/round_video_recorder.h"
#include "ui/effects/animations.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
@@ -21,9 +22,14 @@ namespace style {
struct RecordBar;
} // namespace style
namespace Media::Capture {
enum class Error : uchar;
} // namespace Media::Capture
namespace Ui {
class AbstractButton;
class SendButton;
class RoundVideoRecorder;
} // namespace Ui
namespace Window {
@@ -56,6 +62,7 @@ public:
using SendActionUpdate = Controls::SendActionUpdate;
using VoiceToSend = Controls::VoiceToSend;
using FilterCallback = Fn<bool()>;
using Error = ::Media::Capture::Error;
VoiceRecordBar(
not_null<Ui::RpWidget*> parent,
@@ -87,6 +94,7 @@ public:
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;
[[nodiscard]] rpl::producer<> recordingTipRequests() const;
[[nodiscard]] rpl::producer<Error> errors() const;
void requestToSendWithOptions(Api::SendOptions options);
@@ -123,14 +131,12 @@ private:
void updateTTLGeometry(TTLAnimationType type, float64 progress);
void recordUpdated(quint16 level, int samples);
[[nodiscard]] bool recordingAnimationCallback(crl::time now);
void checkTipRequired();
void stop(bool send);
void stopRecording(StopType type, bool ttlBeforeHide = false);
void visibilityAnimate(bool show, Fn<void()> &&callback);
[[nodiscard]] bool showRecordButton() const;
void drawDuration(QPainter &p);
void drawRedCircle(QPainter &p);
void drawMessage(QPainter &p, float64 recordActive);
@@ -153,6 +159,8 @@ private:
[[nodiscard]] bool peekTTLState() const;
[[nodiscard]] bool takeTTLState() const;
[[nodiscard]] bool createVideoRecorder();
const style::RecordBar &_st;
const not_null<Ui::RpWidget*> _outerContainer;
const std::shared_ptr<ChatHelpers::Show> _show;
@@ -163,7 +171,7 @@ private:
std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen;
::Media::Capture::Result _data;
Ui::RoundVideoResult _data;
rpl::variable<bool> _paused;
base::Timer _startTimer;
@@ -172,6 +180,7 @@ private:
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
rpl::event_stream<> _cancelRequests;
rpl::event_stream<> _listenChanges;
rpl::event_stream<Error> _errors;
int _centerY = 0;
QRect _redCircleRect;
@@ -192,9 +201,15 @@ private:
float64 _redCircleProgress = 0.;
rpl::event_stream<> _recordingTipRequests;
bool _recordingTipRequired = false;
crl::time _recordingTipRequire = 0;
bool _lockFromBottom = false;
std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;
std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
rpl::lifetime _videoCapturerLifetime;
bool _recordingVideo = false;
bool _fullRecord = false;
const style::font &_cancelFont;
rpl::lifetime _recordingLifetime;

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