Compare commits

...

93 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
189 changed files with 5251 additions and 1686 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
@@ -1503,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
@@ -1851,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
@@ -1948,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: 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

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

@@ -1594,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";
@@ -1871,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.";
@@ -2116,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}";
@@ -2444,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.";
@@ -2990,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";
@@ -3046,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.";
@@ -3056,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.";
@@ -3249,12 +3269,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"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";
@@ -3283,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";
@@ -3879,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...";
@@ -3960,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";
@@ -5182,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";
@@ -5492,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";
@@ -5524,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";
@@ -5611,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.4.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,4,0
PRODUCTVERSION 5,6,4,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.4.0"
VALUE "FileVersion", "5.7.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.6.4.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,4,0
PRODUCTVERSION 5,6,4,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.4.0"
VALUE "FileVersion", "5.7.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "5.6.4.0"
VALUE "ProductVersion", "5.7.0.0"
END
END
BLOCK "VarFileInfo"

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

@@ -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();

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

@@ -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

@@ -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

@@ -1074,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);
@@ -1512,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) * 4;
+ sizeof(qint32) * 5;
auto result = QByteArray();
result.reserve(size);
@@ -380,7 +398,8 @@ QByteArray Settings::serialize() const {
<< qint32(_includeMutedCounterFolders ? 1 : 0)
<< qint32(_ivZoom.current())
<< qint32(_skipToastsInFocus ? 1 : 0)
<< qint32(_recordVideoMessages ? 1 : 0);
<< qint32(_recordVideoMessages ? 1 : 0)
<< SerializeVideoQuality(_videoQuality);
}
Ensures(result.size() == size);
@@ -507,6 +526,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 ivZoom = _ivZoom.current();
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
quint32 videoQuality = SerializeVideoQuality(_videoQuality);
stream >> themesAccentColors;
if (!stream.atEnd()) {
@@ -825,6 +845,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> recordVideoMessages;
}
if (!stream.atEnd()) {
stream >> videoQuality;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@@ -1039,6 +1062,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
_ivZoom = ivZoom;
_skipToastsInFocus = (skipToastsInFocus == 1);
_recordVideoMessages = (recordVideoMessages == 1);
_videoQuality = DeserializeVideoQuality(videoQuality);
}
QString Settings::getSoundPath(const QString &key) const {
@@ -1429,6 +1453,7 @@ void Settings::resetOnLastLogout() {
_ttlVoiceClickTooltipHidden = false;
_ivZoom = 100;
_recordVideoMessages = false;
_videoQuality = {};
_recentEmojiPreload.clear();
_recentEmoji.clear();
@@ -1572,6 +1597,7 @@ auto Settings::skipTranslationLanguagesValue() const
void Settings::setRememberedDeleteMessageOnlyForYou(bool value) {
_rememberedDeleteMessageOnlyForYou = value;
}
bool Settings::rememberedDeleteMessageOnlyForYou() const {
return _rememberedDeleteMessageOnlyForYou;
}
@@ -1579,13 +1605,28 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const {
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

@@ -929,6 +929,9 @@ public:
[[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();
@@ -1066,6 +1069,7 @@ private:
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

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 = 5006004;
constexpr auto AppVersionStr = "5.6.4";
constexpr auto AppBetaVersion = true;
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

@@ -743,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

@@ -1162,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) {
@@ -2325,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

@@ -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);
@@ -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) {
@@ -2942,7 +2902,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
HistoryView::EmojiPacksSource::Message,
_controller);
const auto added = (_menu->actions().size() > wasAmount);
if (!added) {
if (!added && !_menu->empty()) {
_menu->addSeparator();
}
HistoryView::AddSelectRestrictionAction(
@@ -2954,8 +2914,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
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

@@ -559,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);
@@ -715,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) {
@@ -1091,9 +1128,12 @@ void HistoryWidget::initVoiceRecordBar() {
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record:
controller()->showToast(tr::lng_record_voice_tip(tr::now));
break;
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;
@@ -1611,6 +1651,9 @@ void HistoryWidget::orderWidgets() {
if (_translateBar) {
_translateBar->raise();
}
if (_sponsoredMessageBar) {
_sponsoredMessageBar->raise();
}
if (_pinnedBar) {
_pinnedBar->raise();
}
@@ -2284,6 +2327,7 @@ void HistoryWidget::showHistory(
_history->showAtMsgId = _showAtMsgId;
destroyUnreadBarOnClose();
_sponsoredMessageBar = nullptr;
_pinnedBar = nullptr;
_translateBar = nullptr;
_pinnedTracker = nullptr;
@@ -2312,7 +2356,7 @@ void HistoryWidget::showHistory(
_processingReplyItem = _replyEditMsg = nullptr;
_processingReplyTo = _replyTo = FullReplyTo();
_editMsgId = MsgId();
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
updateReplaceMediaButton();
_fieldBarCancel->hide();
@@ -2329,7 +2373,7 @@ void HistoryWidget::showHistory(
_contactStatus = nullptr;
_businessBotStatus = nullptr;
updateRecordMediaState();
Core::App().mediaDevices().refreshRecordAvailability();
if (peerId) {
using namespace HistoryView;
@@ -2505,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);
@@ -2520,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 {
@@ -2700,7 +2749,7 @@ void HistoryWidget::setEditMsgId(MsgId msgId) {
_editMsgId = msgId;
if (!msgId) {
_mediaEditManager.cancel();
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
if (_preview) {
_preview->setDisabled(false);
}
@@ -2756,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, [=] {
@@ -2929,6 +2980,9 @@ void HistoryWidget::updateControlsVisibility() {
if (_pinnedBar) {
_pinnedBar->show();
}
if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) {
_sponsoredMessageBar->toggle(true, anim::type::normal);
}
if (_translateBar) {
_translateBar->show();
}
@@ -4126,6 +4180,9 @@ void HistoryWidget::hideChildWidgets() {
if (_pinnedBar) {
_pinnedBar->hide();
}
if (_sponsoredMessageBar) {
_sponsoredMessageBar->toggle(false, anim::type::instant);
}
if (_translateBar) {
_translateBar->hide();
}
@@ -4286,8 +4343,9 @@ auto HistoryWidget::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
const auto video = Core::App().settings().recordVideoMessages();
return (video && _recordAvailability == both)
? Type::Round
: Type::Record;
}
@@ -4462,6 +4520,7 @@ void HistoryWidget::showFinished() {
_showAnimation = nullptr;
doneShow();
synteticScrollToY(_scroll->scrollTop());
requestSponsoredMessageBar();
}
void HistoryWidget::doneShow() {
@@ -4482,6 +4541,10 @@ void HistoryWidget::doneShow() {
if (_pinnedBar) {
_pinnedBar->finishAnimating();
}
checkSponsoredMessageBar();
if (_sponsoredMessageBar) {
_sponsoredMessageBar->finishAnimating();
}
if (_translateBar) {
_translateBar->finishAnimating();
}
@@ -4914,7 +4977,7 @@ bool HistoryWidget::isSearching() const {
}
bool HistoryWidget::showRecordButton() const {
return _canRecordAudioMessage
return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@@ -5517,15 +5580,6 @@ void HistoryWidget::inlineBotChanged() {
}
}
void HistoryWidget::updateRecordMediaState() {
Media::Capture::instance()->check();
_canRecordAudioMessage = Media::Capture::instance()->available();
const auto environment = &Core::App().mediaDevices();
const auto type = Webrtc::DeviceType::Camera;
_canRecordVideoMessage = !environment->devices(type).empty();
}
void HistoryWidget::fieldResized() {
moveFieldControls();
updateHistoryGeometry();
@@ -5712,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 },
@@ -5997,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());
@@ -6243,6 +6303,9 @@ void HistoryWidget::updateHistoryGeometry(
if (_translateBar) {
newScrollHeight -= _translateBar->height();
}
if (_sponsoredMessageBar) {
newScrollHeight -= _sponsoredMessageBar->height();
}
if (_pinnedBar) {
newScrollHeight -= _pinnedBar->height();
}
@@ -6660,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)
@@ -7548,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,
@@ -8002,7 +8176,7 @@ void HistoryWidget::cancelEdit() {
return;
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
updateReplaceMediaButton();
_replyEditMsg = nullptr;
@@ -8357,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
@@ -497,7 +503,6 @@ private:
bool replyToPreviousMessage();
bool replyToNextMessage();
[[nodiscard]] bool showSlowmodeError();
void updateRecordMediaState();
void hideChildWidgets();
void hideSelectorControlsAnimated();
@@ -529,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);
@@ -662,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;
@@ -686,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;
@@ -747,8 +762,7 @@ private:
mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false;
bool _canRecordVideoMessage = false;
bool _canRecordAudioMessage = false;
Webrtc::RecordAvailability _recordAvailability = {};
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;

View File

@@ -964,7 +964,6 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) {
initWebpageProcess();
initWriteRestriction();
initForwardProcess();
updateRecordMediaState();
updateBotCommandShown();
updateLikeShown();
updateMessagesTTLShown();
@@ -1153,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,
@@ -1519,7 +1518,7 @@ void ComposeControls::orderControls() {
}
bool ComposeControls::showRecordButton() const {
return _canRecordAudioMessage
return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
@@ -1944,7 +1943,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
_preview->apply({ .removed = true });
_preview->setDisabled(false);
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
return;
}
@@ -1964,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()
@@ -1985,7 +1993,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
return true;
}
_canReplaceMedia = false;
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
_header->editMessage(editingId, false);
return false;
@@ -2006,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()) {
@@ -2139,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) {
@@ -2421,9 +2434,12 @@ void ComposeControls::initVoiceRecordBar() {
!Core::App().settings().recordVideoMessages());
updateSendButtonType();
switch (_send->type()) {
case Ui::SendButton::Type::Record:
_show->showToast(tr::lng_record_voice_tip(tr::now));
break;
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;
@@ -2454,15 +2470,6 @@ void ComposeControls::initVoiceRecordBar() {
}, _wrap->lifetime());
}
void ComposeControls::updateRecordMediaState() {
::Media::Capture::instance()->check();
_canRecordAudioMessage = ::Media::Capture::instance()->available();
const auto environment = &Core::App().mediaDevices();
const auto type = Webrtc::DeviceType::Camera;
_canRecordVideoMessage = !environment->devices(type).empty();
}
void ComposeControls::updateWrappingVisibility() {
const auto hidden = _hidden.current();
const auto &restriction = _writeRestriction.current();
@@ -2498,8 +2505,9 @@ auto ComposeControls::computeSendButtonType() const {
} else if (_isInlineBot) {
return Type::Cancel;
} else if (showRecordButton()) {
return (Core::App().settings().recordVideoMessages()
&& _canRecordVideoMessage)
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
const auto video = Core::App().settings().recordVideoMessages();
return (video && _recordAvailability == both)
? Type::Round
: Type::Record;
}
@@ -2931,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;
@@ -2940,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(), [=] {

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;
@@ -278,7 +282,6 @@ private:
bool updateSendAsButton();
void updateAttachBotsMenu();
void updateHeight();
void updateRecordMediaState();
void updateWrappingVisibility();
void updateControlsVisibility();
void updateControlsGeometry(QSize size);
@@ -437,13 +440,12 @@ private:
bool _isInlineBot = false;
bool _botCommandShown = false;
bool _likeShown = false;
bool _canRecordVideoMessage = false;
bool _canRecordAudioMessage = 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

@@ -364,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;
@@ -383,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()))
@@ -410,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>(
@@ -1427,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)
@@ -1547,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());
@@ -1624,7 +1636,8 @@ void VoiceRecordBar::init() {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
_st,
_recordingVideo);
}
_ttlButton->show();
}
@@ -1659,6 +1672,8 @@ void VoiceRecordBar::init() {
}
_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) {
@@ -1822,6 +1837,7 @@ void VoiceRecordBar::startRecording() {
) | rpl::start_with_next_error([=](const Update &update) {
recordUpdated(update.level, update.samples);
if (update.finished) {
_fullRecord = true;
stopRecording(StopType::Listen);
_lockShowing = false;
}
@@ -1885,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) {
@@ -1917,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 = {};
}

View File

@@ -208,6 +208,7 @@ private:
std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
rpl::lifetime _videoCapturerLifetime;
bool _recordingVideo = false;
bool _fullRecord = false;
const style::font &_cancelFont;

View File

@@ -407,6 +407,8 @@ void BottomInfo::layout() {
void BottomInfo::layoutDateText() {
const auto edited = (_data.flags & Data::Flag::Edited)
? (tr::lng_edited(tr::now) + ' ')
: (_data.flags & Data::Flag::EstimateDate)
? (tr::lng_approximate(tr::now) + ' ')
: QString();
const auto author = _data.author;
const auto prefix = !author.isEmpty() ? u", "_q : QString();
@@ -601,6 +603,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
if (forwarded && forwarded->imported) {
result.flags |= Flag::Imported;
}
if (item->awaitingVideoProcessing()) {
result.flags |= Flag::EstimateDate;
}
// We don't want to pass and update it in Data for now.
//if (item->unread()) {
// result.flags |= Flag::Unread;

View File

@@ -32,15 +32,16 @@ struct TextState;
class BottomInfo final : public Object {
public:
struct Data {
enum class Flag : uchar {
Edited = 0x01,
OutLayout = 0x02,
Sending = 0x04,
RepliesContext = 0x08,
Sponsored = 0x10,
Pinned = 0x20,
Imported = 0x40,
Shortcut = 0x80,
enum class Flag : uint16 {
Edited = 0x001,
OutLayout = 0x002,
Sending = 0x004,
RepliesContext = 0x008,
Sponsored = 0x010,
Pinned = 0x020,
Imported = 0x040,
Shortcut = 0x080,
EstimateDate = 0x100,
//Unread, // We don't want to pass and update it in Date for now.
};
friend inline constexpr bool is_flag_type(Flag) { return true; };

View File

@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_text.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/media/history_view_media.h"
@@ -521,7 +522,7 @@ bool AddRescheduleAction(
const auto owner = &request.navigation->session().data();
const auto goodSingle = HasEditMessageAction(request, list)
&& request.item->isScheduled();
&& request.item->allowsReschedule();
const auto goodMany = [&] {
if (goodSingle) {
return false;
@@ -533,7 +534,7 @@ bool AddRescheduleAction(
if (items.size() > kRescheduleLimit) {
return false;
}
return ranges::all_of(items, &SelectedItem::canSendNow);
return ranges::all_of(items, &SelectedItem::canReschedule);
}();
if (!goodSingle && !goodMany) {
return false;
@@ -1286,6 +1287,8 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
}
if (hasWhoReactedItem) {
AddWhoReactedAction(result, list, item, list->controller());
} else if (item) {
MaybeAddWhenEditedAction(result, item);
}
return result;
@@ -1441,6 +1444,24 @@ void AddSaveSoundForNotifications(
}, &st::menuIconSoundAdd);
}
void AddWhenEditedActionHelper(
not_null<Ui::PopupMenu*> menu,
not_null<HistoryItem*> item,
bool insertSeparator) {
if (item->history()->peer->isUser()) {
if (const auto edited = item->Get<HistoryMessageEdited>()) {
if (!item->hideEditedBadge()) {
if (insertSeparator && !menu->empty()) {
menu->addSeparator(&st::expandedMenuSeparator);
}
menu->addAction(Ui::WhenReadContextAction(
menu.get(),
Api::WhenEdited(item->from(), edited->date)));
}
}
}
}
void AddWhoReactedAction(
not_null<Ui::PopupMenu*> menu,
not_null<QWidget*> context,
@@ -1486,6 +1507,7 @@ void AddWhoReactedAction(
if (!menu->empty()) {
menu->addSeparator(&st::expandedMenuSeparator);
}
AddWhenEditedActionHelper(menu, item, false);
if (item->history()->peer->isUser()) {
menu->addAction(Ui::WhenReadContextAction(
menu.get(),
@@ -1501,6 +1523,12 @@ void AddWhoReactedAction(
}
}
void MaybeAddWhenEditedAction(
not_null<Ui::PopupMenu*> menu,
not_null<HistoryItem*> item) {
AddWhenEditedActionHelper(menu, item, true);
}
void AddEditTagAction(
not_null<Ui::PopupMenu*> menu,
const Data::ReactionId &id,

View File

@@ -84,6 +84,9 @@ void AddWhoReactedAction(
not_null<QWidget*> context,
not_null<HistoryItem*> item,
not_null<Window::SessionController*> controller);
void MaybeAddWhenEditedAction(
not_null<Ui::PopupMenu*> menu,
not_null<HistoryItem*> item);
void ShowWhoReactedMenu(
not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
QPoint position,

View File

@@ -264,6 +264,9 @@ QString DateTooltipText(not_null<Element*> view) {
const auto format = QLocale::LongFormat;
const auto item = view->data();
auto dateText = locale.toString(view->dateTime(), format);
if (item->awaitingVideoProcessing()) {
dateText += '\n' + tr::lng_approximate_about(tr::now);
}
if (const auto editedDate = view->displayedEditDate()) {
dateText += '\n' + tr::lng_edited_date(
tr::now,

View File

@@ -1183,6 +1183,7 @@ auto ListWidget::collectSelectedItems() const -> SelectedItems {
result.canDelete = selection.canDelete;
result.canForward = selection.canForward;
result.canSendNow = selection.canSendNow;
result.canReschedule = selection.canReschedule;
return result;
};
auto items = SelectedItems();
@@ -1317,6 +1318,7 @@ bool ListWidget::addToSelection(
iterator->second.canDelete = item->canDelete();
iterator->second.canForward = item->allowsForward();
iterator->second.canSendNow = item->allowsSendNow();
iterator->second.canReschedule = item->allowsReschedule();
return true;
}

View File

@@ -80,6 +80,7 @@ struct SelectedItem {
bool canDelete = false;
bool canForward = false;
bool canSendNow = false;
bool canReschedule = false;
};
struct MessagesBar {
@@ -232,6 +233,7 @@ struct SelectionData {
bool canDelete = false;
bool canForward = false;
bool canSendNow = false;
bool canReschedule = false;
};
using SelectedMap = base::flat_map<

View File

@@ -16,10 +16,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_drag_area.h"
#include "history/history_item_helpers.h" // GetErrorTextForSending.
#include "menu/menu_send.h" // SendMenu::Type.
#include "ui/widgets/buttons.h"
#include "ui/widgets/tooltip.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/ui_utility.h"
#include "api/api_editing.h"
#include "api/api_sending.h"
@@ -34,7 +39,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/mime_type.h"
#include "chat_helpers/tabbed_selector.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "data/components/scheduled_messages.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
@@ -55,19 +63,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QMimeData>
namespace HistoryView {
namespace {
ScheduledMemento::ScheduledMemento(not_null<History*> history)
: _history(history)
, _forumTopic(nullptr) {
constexpr auto kVideoProcessingInfoDuration = 4 * crl::time(1000);
} // namespace
ScheduledMemento::ScheduledMemento(
not_null<History*> history,
MsgId sentToScheduledId)
: _history(history)
, _forumTopic(nullptr)
, _sentToScheduledId(sentToScheduledId) {
const auto list = _history->session().scheduledMessages().list(_history);
if (!list.ids.empty()) {
_list.setScrollTopState({ .item = {.fullId = list.ids.front() } });
if (sentToScheduledId) {
_list.setScrollTopState({
.item = { .fullId = { _history->peer->id, sentToScheduledId } },
});
} else if (!list.ids.empty()) {
_list.setScrollTopState({ .item = { .fullId = list.ids.front() } });
}
}
ScheduledMemento::ScheduledMemento(not_null<Data::ForumTopic*> forumTopic)
: _history(forumTopic->owningHistory())
, _forumTopic(forumTopic) {
: _history(forumTopic->owningHistory())
, _forumTopic(forumTopic) {
const auto list = _history->session().scheduledMessages().list(
_forumTopic);
if (!list.ids.empty()) {
@@ -97,33 +117,33 @@ ScheduledWidget::ScheduledWidget(
not_null<Window::SessionController*> controller,
not_null<History*> history,
const Data::ForumTopic *forumTopic)
: Window::SectionWidget(parent, controller, history->peer)
, WindowListDelegate(controller)
, _show(controller->uiShow())
, _history(history)
, _forumTopic(forumTopic)
, _scroll(
this,
controller->chatStyle()->value(lifetime(), st::historyScroll),
false)
, _topBar(this, controller)
, _topBarShadow(this)
, _composeControls(std::make_unique<ComposeControls>(
this,
ComposeControlsDescriptor{
.show = controller->uiShow(),
.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
listShowPremiumToast(emoji);
},
.mode = ComposeControls::Mode::Scheduled,
.sendMenuDetails = [] { return SendMenu::Details(); },
.regularWindow = controller,
.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
}))
, _cornerButtons(
_scroll.data(),
controller->chatStyle(),
static_cast<HistoryView::CornerButtonsDelegate*>(this)) {
: Window::SectionWidget(parent, controller, history->peer)
, WindowListDelegate(controller)
, _show(controller->uiShow())
, _history(history)
, _forumTopic(forumTopic)
, _scroll(
this,
controller->chatStyle()->value(lifetime(), st::historyScroll),
false)
, _topBar(this, controller)
, _topBarShadow(this)
, _composeControls(std::make_unique<ComposeControls>(
this,
ComposeControlsDescriptor{
.show = controller->uiShow(),
.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
listShowPremiumToast(emoji);
},
.mode = ComposeControls::Mode::Scheduled,
.sendMenuDetails = [] { return SendMenu::Details(); },
.regularWindow = controller,
.stickerOrEmojiChosen = controller->stickerOrEmojiChosen(),
}))
, _cornerButtons(
_scroll.data(),
controller->chatStyle(),
static_cast<HistoryView::CornerButtonsDelegate*>(this)) {
controller->chatStyle()->paletteChanged(
) | rpl::start_with_next([=] {
_scroll->updateBars();
@@ -1063,6 +1083,24 @@ void ScheduledWidget::saveState(not_null<ScheduledMemento*> memento) {
void ScheduledWidget::restoreState(not_null<ScheduledMemento*> memento) {
_inner->restoreState(memento->list());
if (const auto id = memento->sentToScheduledId()) {
const auto item = _history->owner().message(_history->peer, id);
if (item) {
controller()->showToast({
.title = tr::lng_scheduled_video_tip_title(tr::now),
.text = { tr::lng_scheduled_video_tip_text(tr::now) },
.attach = RectPart::Top,
.duration = kVideoProcessingInfoDuration,
});
clearProcessingVideoTracking(false);
_processingVideoPosition = item->position();
_processingVideoTipTimer.setCallback([=] {
_processingVideoCanShow = true;
updateInnerVisibleArea();
});
_processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration);
}
}
}
void ScheduledWidget::resizeEvent(QResizeEvent *e) {
@@ -1134,9 +1172,153 @@ void ScheduledWidget::updateInnerVisibleArea() {
checkReplyReturns();
}
const auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
const auto scrollBottom = scrollTop + _scroll->height();
_inner->setVisibleTopBottom(scrollTop, scrollBottom);
_cornerButtons.updateJumpDownVisibility();
_cornerButtons.updateUnreadThingsVisibility();
if (!_processingVideoLifetime) {
if (const auto &position = _processingVideoPosition) {
if (const auto view = _inner->viewByPosition(position)) {
initProcessingVideoView(view);
}
}
}
checkProcessingVideoTooltip(scrollTop, scrollBottom);
}
void ScheduledWidget::initProcessingVideoView(not_null<Element*> view) {
_processingVideoView = view;
controller()->session().data().sentFromScheduled(
) | rpl::start_with_next([=](const Data::SentFromScheduled &value) {
if (value.item->position() == _processingVideoPosition) {
controller()->showPeerHistory(
value.item->history(),
Window::SectionShow::Way::Backward,
value.sentId);
}
}, _processingVideoLifetime);
controller()->session().data().viewRemoved(
) | rpl::start_with_next([=](not_null<const Element*> view) {
if (view == _processingVideoView.get()) {
const auto position = _processingVideoPosition;
if (const auto now = _inner->viewByPosition(position)) {
_processingVideoView = now;
updateProcessingVideoTooltipPosition();
} else {
clearProcessingVideoTracking(true);
}
}
}, _processingVideoLifetime);
controller()->session().data().viewResizeRequest(
) | rpl::start_with_next([this](not_null<const Element*> view) {
if (view->delegate() == _inner.data()) {
if (!_processingVideoUpdateScheduled) {
if (const auto tooltip = _processingVideoTooltip.get()) {
_processingVideoUpdateScheduled = true;
crl::on_main(tooltip, [=] {
_processingVideoUpdateScheduled = false;
updateProcessingVideoTooltipPosition();
});
}
}
}
}, _processingVideoLifetime);
}
void ScheduledWidget::clearProcessingVideoTracking(bool fast) {
if (const auto tooltip = _processingVideoTooltip.release()) {
tooltip->toggleAnimated(false);
}
_processingVideoPosition = {};
if (const auto tooltip = _processingVideoTooltip.release()) {
if (fast) {
tooltip->toggleFast(false);
} else {
tooltip->toggleAnimated(false);
}
}
_processingVideoTooltipShown = false;
_processingVideoCanShow = false;
_processingVideoView = nullptr;
_processingVideoTipTimer.cancel();
_processingVideoLifetime.destroy();
}
void ScheduledWidget::checkProcessingVideoTooltip(
int visibleTop,
int visibleBottom) {
if (_processingVideoTooltip
|| _processingVideoTooltipShown
|| !_processingVideoCanShow) {
return;
}
const auto view = _processingVideoView.get();
if (!view) {
_processingVideoCanShow = false;
return;
}
const auto rect = view->effectIconGeometry();
if (rect.top() > visibleTop
&& rect.top() + rect.height() <= visibleBottom) {
showProcessingVideoTooltip();
}
}
void ScheduledWidget::updateProcessingVideoTooltipPosition() {
const auto tooltip = _processingVideoTooltip.get();
if (!tooltip) {
return;
}
const auto view = _processingVideoView.get();
if (!view) {
clearProcessingVideoTracking(true);
return;
}
const auto shift = view->skipBlockWidth() / 2;
const auto rect = view->effectIconGeometry().translated(shift, 0);
const auto countPosition = [=](QSize size) {
const auto origin = rect.bottomLeft();
return origin - QPoint(
size.width() / 2,
size.height() + st::processingVideoTipShift);
};
tooltip->pointAt(rect, RectPart::Top, countPosition);
}
void ScheduledWidget::showProcessingVideoTooltip() {
_processingVideoTooltipShown = true;
_processingVideoTooltip = std::make_unique<Ui::ImportantTooltip>(
_inner.data(),
Ui::MakeNiceTooltipLabel(
_inner.data(),
tr::lng_scheduled_video_tip(Ui::Text::WithEntities),
st::processingVideoTipMaxWidth,
st::defaultImportantTooltipLabel),
st::defaultImportantTooltip);
const auto tooltip = _processingVideoTooltip.get();
const auto weak = QPointer<QWidget>(tooltip);
const auto destroy = [=] {
delete weak.data();
};
tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
tooltip->setHiddenCallback([=] {
const auto tip = _processingVideoTooltip.get();
if (tooltip == tip) {
_processingVideoTooltip.release();
}
crl::on_main(tip, [=] {
delete tip;
});
});
updateProcessingVideoTooltipPosition();
tooltip->toggleAnimated(true);
_processingVideoTipTimer.setCallback(crl::guard(tooltip, [=] {
tooltip->toggleAnimated(false);
}));
_processingVideoTipTimer.callOnce(kVideoProcessingInfoDuration);
}
void ScheduledWidget::showAnimatedHook(
@@ -1488,4 +1670,114 @@ void ScheduledWidget::setupDragArea() {
areas.photo->setDroppedCallback(droppedCallback(true));
}
bool ShowScheduledVideoPublished(
not_null<Window::SessionController*> controller,
const Data::SentFromScheduled &info,
Fn<void()> hidden) {
if (!controller->widget()->isActive()) {
return false;
}
const auto media = info.item->media();
const auto document = media ? media->document() : nullptr;
if (!document->isVideoFile()) {
return false;
}
const auto history = info.item->history();
const auto itemId = info.sentId;
const auto text = tr::lng_scheduled_video_published(
tr::now,
Ui::Text::Bold);
const auto &st = st::processingVideoToast;
const auto skip = st::processingVideoPreviewSkip;
const auto size = st.style.font->height * 2;
const auto view = tr::lng_scheduled_video_view(tr::now);
const auto additional = QMargins(
skip + size,
0,
(st::processingVideoView.style.font->width(view)
- (st::processingVideoView.width / 2)),
0);
const auto parent = controller->uiShow()->toastParent();
const auto weak = Ui::Toast::Show(parent, Ui::Toast::Config{
.text = text,
.padding = rpl::single(additional),
.st = &st,
.attach = RectPart::Top,
.acceptinput = true,
.duration = kVideoProcessingInfoDuration,
});
const auto strong = weak.get();
if (!strong) {
return false;
}
const auto widget = strong->widget();
const auto hideToast = [weak] {
if (const auto strong = weak.get()) {
strong->hideAnimated();
}
};
const auto clickableBackground = Ui::CreateChild<Ui::AbstractButton>(
widget.get());
clickableBackground->setPointerCursor(false);
clickableBackground->setAcceptBoth();
clickableBackground->show();
clickableBackground->addClickHandler([=](Qt::MouseButton button) {
if (button == Qt::RightButton) {
hideToast();
}
});
const auto button = Ui::CreateChild<Ui::RoundButton>(
widget.get(),
rpl::single(view),
st::processingVideoView);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->show();
rpl::combine(
widget->sizeValue(),
button->sizeValue()
) | rpl::start_with_next([=](QSize outer, QSize inner) {
button->moveToRight(
0,
(outer.height() - inner.height()) / 2,
outer.width());
clickableBackground->resize(outer);
}, widget->lifetime());
const auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());
preview->moveToLeft(skip, skip);
preview->resize(size, size);
preview->show();
const auto thumbnail = Ui::MakeDocumentThumbnail(document, FullMsgId(
history->peer->id,
itemId));
thumbnail->subscribeToUpdates([=] {
preview->update();
});
preview->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(preview);
const auto image = Images::Round(
thumbnail->image(size),
ImageRoundRadius::Small);
p.drawImage(QRect(0, 0, size, size), image);
}, preview->lifetime());
button->setClickedCallback([=] {
controller->showPeerHistory(
history,
Window::SectionShow::Way::Forward,
itemId);
hideToast();
});
if (hidden) {
widget->lifetime().add(std::move(hidden));
}
return true;
}
} // namespace HistoryView

View File

@@ -21,6 +21,10 @@ namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct SentFromScheduled;
} // namespace Data
namespace SendMenu {
struct Details;
} // namespace SendMenu
@@ -37,6 +41,7 @@ class PlainShadow;
class FlatButton;
struct PreparedList;
class SendFilesWay;
class ImportantTooltip;
} // namespace Ui
namespace Profile {
@@ -51,6 +56,10 @@ namespace HistoryView::Controls {
struct VoiceToSend;
} // namespace HistoryView::Controls
namespace Window {
class SessionController;
} // namespace Window
namespace HistoryView {
class Element;
@@ -199,6 +208,12 @@ private:
Data::MessagePosition position,
FullMsgId originId = {});
void initProcessingVideoView(not_null<Element*> view);
void checkProcessingVideoTooltip(int visibleTop, int visibleBottom);
void showProcessingVideoTooltip();
void updateProcessingVideoTooltipPosition();
void clearProcessingVideoTracking(bool fast);
void setupComposeControls();
void setupDragArea();
@@ -277,7 +292,16 @@ private:
std::unique_ptr<ComposeControls> _composeControls;
bool _skipScrollEvent = false;
Data::MessagePosition _processingVideoPosition;
base::weak_ptr<Element> _processingVideoView;
rpl::lifetime _processingVideoLifetime;
std::unique_ptr<HistoryView::StickerToast> _stickerToast;
std::unique_ptr<Ui::ImportantTooltip> _processingVideoTooltip;
base::Timer _processingVideoTipTimer;
bool _processingVideoUpdateScheduled = false;
bool _processingVideoTooltipShown = false;
bool _processingVideoCanShow = false;
CornerButtons _cornerButtons;
@@ -288,7 +312,9 @@ private:
class ScheduledMemento final : public Window::SectionMemento {
public:
ScheduledMemento(not_null<History*> history);
ScheduledMemento(
not_null<History*> history,
MsgId sentToScheduledId = 0);
ScheduledMemento(not_null<Data::ForumTopic*> forumTopic);
object_ptr<Window::SectionWidget> createWidget(
@@ -297,19 +323,29 @@ public:
Window::Column column,
const QRect &geometry) override;
not_null<History*> getHistory() const {
[[nodiscard]] not_null<History*> getHistory() const {
return _history;
}
not_null<ListMemento*> list() {
[[nodiscard]] not_null<ListMemento*> list() {
return &_list;
}
[[nodiscard]] MsgId sentToScheduledId() const {
return _sentToScheduledId;
}
private:
const not_null<History*> _history;
const Data::ForumTopic *_forumTopic;
ListMemento _list;
MsgId _sentToScheduledId = 0;
};
bool ShowScheduledVideoPublished(
not_null<Window::SessionController*> controller,
const Data::SentFromScheduled &info,
Fn<void()> hidden = nullptr);
} // namespace HistoryView

View File

@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_media_spoiler.h"
#include "window/window_session_controller.h"
#include "core/application.h" // Application::showDocument.
#include "core/core_settings.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/chat/chat_style.h"
#include "ui/image/image.h"
@@ -109,8 +110,10 @@ constexpr auto kMaxInlineArea = 1920 * 1080;
struct Gif::Streamed {
Streamed(
not_null<DocumentData*> chosen,
std::shared_ptr<::Media::Streaming::Document> shared,
Fn<void()> waitingCallback);
const not_null<DocumentData*> chosen;
::Media::Streaming::Instance instance;
::Media::Streaming::FrameRequest frozenRequest;
QImage frozenFrame;
@@ -118,9 +121,11 @@ struct Gif::Streamed {
};
Gif::Streamed::Streamed(
not_null<DocumentData*> chosen,
std::shared_ptr<::Media::Streaming::Document> shared,
Fn<void()> waitingCallback)
: instance(std::move(shared), std::move(waitingCallback)) {
: chosen(chosen)
, instance(std::move(shared), std::move(waitingCallback)) {
}
[[nodiscard]] bool IsHiddenRoundMessage(not_null<Element*> parent) {
@@ -1848,13 +1853,21 @@ void Gif::playAnimation(bool autoplay) {
}
void Gif::createStreamedPlayer() {
const auto quality = Core::App().settings().videoQuality();
const auto chosen = _data->chooseQuality(_realParent, quality);
if (_streamed && _streamed->chosen == chosen) {
return;
}
auto shared = _data->owner().streaming().sharedDocument(
chosen,
_data,
_realParent,
_realParent->fullId());
if (!shared) {
return;
}
setStreamed(std::make_unique<Streamed>(
chosen,
std::move(shared),
[=] { repaintStreamedContent(); }));
@@ -1865,6 +1878,20 @@ void Gif::createStreamedPlayer() {
handleStreamingError(std::move(error));
}, _streamed->instance.lifetime());
_streamed->instance.switchQualityRequests(
) | rpl::start_with_next([=](int quality) {
auto now = Core::App().settings().videoQuality();
if (now.manual || now.height == quality) {
return;
}
Core::App().settings().setVideoQuality({
.manual = 0,
.height = uint32(quality),
});
Core::App().saveSettingsDelayed();
createStreamedPlayer();
}, _streamed->instance.lifetime());
if (_streamed->instance.ready()) {
streamingReady(base::duplicate(_streamed->instance.info()));
}
@@ -1912,14 +1939,15 @@ void Gif::handleStreamingUpdate(::Media::Streaming::Update &&update) {
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [&](const PreloadedVideo &update) {
}, [&](const UpdateVideo &update) {
}, [](PreloadedVideo) {
}, [&](UpdateVideo) {
repaintStreamedContent();
}, [&](const PreloadedAudio &update) {
}, [&](const UpdateAudio &update) {
}, [&](const WaitingForData &update) {
}, [&](MutedByOther) {
}, [&](Finished) {
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}
@@ -1982,10 +2010,12 @@ bool Gif::dataLoaded() const {
}
bool Gif::needInfoDisplay() const {
if (_parent->data()->isFakeAboutView()) {
const auto item = _parent->data();
if (item->isFakeAboutView()) {
return false;
}
return _parent->data()->isSending()
return item->isSending()
|| item->awaitingVideoProcessing()
|| _data->uploading()
|| _parent->isUnderCursor()
|| (_parent->delegate()->elementContext() == Context::ChatPreview)

View File

@@ -893,9 +893,11 @@ bool GroupedMedia::computeNeedBubble() const {
}
bool GroupedMedia::needInfoDisplay() const {
const auto item = _parent->data();
return (_mode != Mode::Column)
&& (_parent->data()->isSending()
|| _parent->data()->hasFailed()
&& (item->isSending()
|| item->awaitingVideoProcessing()
|| item->hasFailed()
|| _parent->isUnderCursor()
|| (_parent->delegate()->elementContext() == Context::ChatPreview)
|| _parent->isLastAndSelfMessage());

View File

@@ -979,14 +979,15 @@ void Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) {
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [&](const PreloadedVideo &update) {
}, [&](const UpdateVideo &update) {
}, [](PreloadedVideo) {
}, [&](UpdateVideo) {
repaintStreamedContent();
}, [&](const PreloadedAudio &update) {
}, [&](const UpdateAudio &update) {
}, [&](const WaitingForData &update) {
}, [&](MutedByOther) {
}, [&](Finished) {
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}

View File

@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_sponsored_click_handler.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_sponsored.h"
@@ -73,11 +74,13 @@ constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000);
const auto spoiler = false;
for (const auto &item : data.items) {
if (const auto document = std::get_if<DocumentData*>(&item)) {
const auto hasQualitiesList = false;
const auto skipPremiumEffect = false;
result.push_back(std::make_unique<Data::MediaFile>(
parent,
*document,
skipPremiumEffect,
hasQualitiesList,
spoiler,
/*ttlSeconds = */0));
} else if (const auto photo = std::get_if<PhotoData*>(&item)) {
@@ -144,15 +147,6 @@ constexpr auto kFactcheckAboutDuration = 5 * crl::time(1000);
});
}
[[nodiscard]] 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());
}
});
}
[[nodiscard]] QString LookupFactcheckCountryIso2(
not_null<HistoryItem*> item) {
const auto info = item->Get<HistoryMessageFactcheck>();

View File

@@ -60,13 +60,9 @@ void AddHeader(
} // namespace
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer)
InnerWidget::InnerWidget(QWidget *parent, not_null<Controller*> controller)
: VerticalLayout(parent)
, _controller(controller)
, _peer(peer)
, _show(controller->uiShow()) {
}
@@ -75,7 +71,7 @@ void InnerWidget::load() {
const auto request = [=](Fn<void(Data::CreditsEarnStatistics)> done) {
const auto api = apiLifetime->make_state<Api::CreditsEarnStatistics>(
_peer->asUser());
peer()->asUser());
api->request(
) | rpl::start_with_error_done([show = _show](const QString &error) {
show->showToast(error);
@@ -92,18 +88,18 @@ void InnerWidget::load() {
_showFinished.events());
_showFinished.events(
) | rpl::take(1) | rpl::start_with_next([=] {
) | rpl::take(1) | rpl::start_with_next([=, this, peer = peer()] {
request([=](Data::CreditsEarnStatistics state) {
_state = state;
_loaded.fire(true);
fill();
_peer->session().account().mtpUpdates(
peer->session().account().mtpUpdates(
) | rpl::start_with_next([=](const MTPUpdates &updates) {
using TL = MTPDupdateStarsRevenueStatus;
Api::PerformForUpdate<TL>(updates, [&](const TL &d) {
const auto peerId = peerFromMTP(d.vpeer());
if (peerId == _peer->id) {
if (peerId == peer->id) {
request([=](Data::CreditsEarnStatistics state) {
_state = state;
_stateUpdated.fire({});
@@ -120,6 +116,7 @@ void InnerWidget::fill() {
const auto container = this;
const auto &data = _state;
const auto multiplier = data.usdRate * Data::kEarnMultiplier;
constexpr auto kMinorLength = 3;
auto availableBalanceValue = rpl::single(
data.availableBalance
@@ -128,7 +125,7 @@ void InnerWidget::fill() {
return _state.availableBalance;
})
);
auto valueToString = [](uint64 v) { return QString::number(v); };
auto valueToString = [](uint64 v) { return Lang::FormatCountDecimal(v); };
if (data.revenueGraph.chart) {
Ui::AddSkip(container);
@@ -170,7 +167,7 @@ void InnerWidget::fill() {
std::move(
value
) | rpl::map([=](uint64 v) {
return v ? ToUsd(v, multiplier) : QString();
return v ? ToUsd(v, multiplier, kMinorLength) : QString();
}),
st::channelEarnOverviewSubMinorLabel);
rpl::combine(
@@ -233,7 +230,7 @@ void InnerWidget::fill() {
::Settings::AddWithdrawalWidget(
container,
_controller->parentController(),
_peer,
peer(),
rpl::single(
data.buyAdsUrl
) | rpl::then(
@@ -247,7 +244,7 @@ void InnerWidget::fill() {
return !dt.isNull() || (!_state.isWithdrawalEnabled);
}),
rpl::duplicate(availableBalanceValue) | rpl::map([=](uint64 v) {
return v ? ToUsd(v, multiplier) : QString();
return v ? ToUsd(v, multiplier, kMinorLength) : QString();
}));
}
@@ -262,7 +259,7 @@ void InnerWidget::fillHistory() {
const auto sectionIndex = history->lifetime().make_state<int>(0);
const auto fill = [=, peer = _peer](
const auto fill = [=, peer = peer()](
not_null<PeerData*> premiumBot,
const Data::CreditsStatusSlice &fullSlice,
const Data::CreditsStatusSlice &inSlice,
@@ -395,7 +392,7 @@ void InnerWidget::fillHistory() {
const auto apiLifetime = history->lifetime().make_state<rpl::lifetime>();
rpl::single(rpl::empty) | rpl::then(
_stateUpdated.events()
) | rpl::start_with_next([=, peer = _peer] {
) | rpl::start_with_next([=, peer = peer()] {
using Api = Api::CreditsHistory;
const auto apiFull = apiLifetime->make_state<Api>(peer, true, true);
const auto apiIn = apiLifetime->make_state<Api>(peer, true, false);
@@ -450,7 +447,7 @@ void InnerWidget::setInnerFocus() {
}
not_null<PeerData*> InnerWidget::peer() const {
return _peer;
return _controller->statisticsTag().peer;
}
} // namespace Info::BotEarn

View File

@@ -32,10 +32,7 @@ public:
struct ShowRequest final {
};
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer);
InnerWidget(QWidget *parent, not_null<Controller*> controller);
[[nodiscard]] not_null<PeerData*> peer() const;
@@ -54,7 +51,6 @@ private:
void fillHistory();
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
std::shared_ptr<Ui::Show> _show;
Data::CreditsEarnStatistics _state;

View File

@@ -16,11 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Info::BotEarn {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(Info::Statistics::Tag{
controller->statisticsPeer(),
{},
{},
}) {
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
@@ -54,11 +50,7 @@ Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->statisticsPeer()))) {
, _inner(setInnerWidget(object_ptr<InnerWidget>(this, controller))) {
_inner->showRequests(
) | rpl::start_with_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
@@ -73,7 +65,7 @@ not_null<PeerData*> Widget::peer() const {
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsPeer() == peer());
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {

View File

@@ -208,10 +208,24 @@ startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimati
thickness: 2px;
}
starGiftBox: Box(giveawayGiftCodeBox) {
buttonPadding: margins(22px, 11px, 22px, 52px);
}
starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) {
color: windowActiveTextFg;
thickness: 2px;
}
starGiftSmallButton: RoundButton(defaultActiveButton) {
textFg: windowActiveTextFg;
textFgOver: windowActiveTextFg;
textBg: lightButtonBgOver;
textBgOver: lightButtonBgOver;
width: -12px;
height: 18px;
radius: 9px;
textTop: 0px;
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgRipple;
}
}

View File

@@ -16,11 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Info::Boosts {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(Info::Statistics::Tag{
controller->statisticsPeer(),
{},
{},
}) {
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
@@ -58,7 +54,7 @@ Widget::Widget(
object_ptr<InnerWidget>(
this,
controller,
controller->statisticsPeer()))) {
controller->statisticsTag().peer))) {
_inner->showRequests(
) | rpl::start_with_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
@@ -73,7 +69,7 @@ not_null<PeerData*> Widget::peer() const {
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsPeer() == peer());
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {

View File

@@ -46,7 +46,10 @@ QString MinorPart(EarnInt value) {
return result.chopped(zeroCount);
}
QString ToUsd(EarnInt value, float64 rate) {
QString ToUsd(
Data::EarnInt value,
float64 rate,
int afterFloat) {
constexpr auto kApproximately = QChar(0x2248);
const auto result = value
@@ -56,7 +59,9 @@ QString ToUsd(EarnInt value, float64 rate) {
return QString(kApproximately)
+ QChar('$')
+ MajorPart(result)
+ MinorPart(result);
+ ((afterFloat > 0)
? MinorPart(result).left(afterFloat)
: MinorPart(result));
}
} // namespace Info::ChannelEarn

View File

@@ -13,6 +13,9 @@ namespace Info::ChannelEarn {
[[nodiscard]] QString MajorPart(Data::EarnInt value);
[[nodiscard]] QString MinorPart(Data::EarnInt value);
[[nodiscard]] QString ToUsd(Data::EarnInt value, float64 rate);
[[nodiscard]] QString ToUsd(
Data::EarnInt value,
float64 rate,
int afterFloat);
} // namespace Info::ChannelEarn

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