Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c3c8f888d | ||
|
|
a75d7f0381 | ||
|
|
7684466acf | ||
|
|
0067245739 | ||
|
|
4a5d8aa217 | ||
|
|
2d786aa02c | ||
|
|
b0933b96ef | ||
|
|
20fb73b626 | ||
|
|
ae7bd7112b | ||
|
|
2365363dcc | ||
|
|
e0e4a7bec6 | ||
|
|
ebd0c3696a | ||
|
|
e0a0d9c039 | ||
|
|
dae9f2ab2b | ||
|
|
7168a00ee4 | ||
|
|
1e2d0ced20 | ||
|
|
d6ac883efa | ||
|
|
a386d70ae4 | ||
|
|
bc8bf672b4 | ||
|
|
e38998214f | ||
|
|
472a2fe802 | ||
|
|
2d6d89b1cf | ||
|
|
66be2ac6ca | ||
|
|
3137c9f3f7 | ||
|
|
b9ebb02e72 | ||
|
|
1e02c475d6 | ||
|
|
4d2cda0692 | ||
|
|
cbd2b8f428 | ||
|
|
93605db690 | ||
|
|
2567096de0 | ||
|
|
6ed25d012f | ||
|
|
c2afef2bde | ||
|
|
0991e7d8a4 | ||
|
|
cf52f2a743 | ||
|
|
37dddda1a0 | ||
|
|
3f2f3ebd51 | ||
|
|
7ba78540ac | ||
|
|
19afb49fce | ||
|
|
46ab553fa5 | ||
|
|
68cc42047e | ||
|
|
e25cf27ba5 | ||
|
|
3895e6d958 | ||
|
|
24cf3984c8 | ||
|
|
6237675744 | ||
|
|
30dae049ff | ||
|
|
1dc30caee9 | ||
|
|
b2d340cbfb | ||
|
|
7d52787e54 | ||
|
|
ae3f16ccbd | ||
|
|
057222757b | ||
|
|
119f109904 | ||
|
|
23a77b1ba4 | ||
|
|
c076daa91f | ||
|
|
b7ef5325ac | ||
|
|
8b535c58fa | ||
|
|
6bc8daaeda | ||
|
|
7a2562e5bb | ||
|
|
9e0c731b32 | ||
|
|
2e0e4006a1 | ||
|
|
187139473d | ||
|
|
dbb0a5ad28 | ||
|
|
24aaed44b9 | ||
|
|
32b8d83c04 | ||
|
|
bf55c325ce | ||
|
|
3af288c74e | ||
|
|
e306d9ba35 | ||
|
|
b23a877d7e | ||
|
|
08fda055fc | ||
|
|
84055ed74e | ||
|
|
2db30690ce | ||
|
|
304bcfd343 | ||
|
|
8a1cf2bb3a | ||
|
|
c857c24a64 | ||
|
|
bbdcb047d0 | ||
|
|
78f2e70956 | ||
|
|
75a75626ce | ||
|
|
cec9688d58 | ||
|
|
81492b7d3a | ||
|
|
9166acbbb9 | ||
|
|
36de2b6ca6 | ||
|
|
21f909dd4b | ||
|
|
f2a92c9122 | ||
|
|
7ee2e3d8bc | ||
|
|
f89aeb6ad4 | ||
|
|
0397006894 | ||
|
|
d6863074b2 | ||
|
|
9c185a30e0 | ||
|
|
a8f492a027 | ||
|
|
0a92b1dc68 | ||
|
|
e6d661f8ee | ||
|
|
f48dfb5d81 | ||
|
|
cd041e8366 | ||
|
|
6787ea883e | ||
|
|
78937d716f | ||
|
|
9713abc002 | ||
|
|
b44b45cca0 | ||
|
|
9e2cf0ed73 | ||
|
|
b01d7ea5b9 | ||
|
|
ae89b65a98 | ||
|
|
9b9c3d788d | ||
|
|
ccc6c6daa5 | ||
|
|
9ce6636c6a | ||
|
|
6287d306c2 | ||
|
|
6cfa053328 | ||
|
|
9514b6eecd | ||
|
|
c8d4818d22 | ||
|
|
4142ada729 | ||
|
|
d7ffdbd78d | ||
|
|
e8d87d37bb | ||
|
|
343ffc23eb | ||
|
|
95e0086eed | ||
|
|
c010ecfe38 | ||
|
|
302e9371c8 | ||
|
|
7060c0e6d7 | ||
|
|
20a4c7f9f4 | ||
|
|
e59e4afd3e | ||
|
|
f74dd3ca1e | ||
|
|
511cfc524f | ||
|
|
4cf6173d25 | ||
|
|
17996757fd | ||
|
|
6bc1049858 | ||
|
|
ff44f626ba | ||
|
|
552343fa37 | ||
|
|
4dc7fd8cd1 | ||
|
|
285c96fd2e | ||
|
|
e6af33367e | ||
|
|
7092fe2242 | ||
|
|
a32ff46579 | ||
|
|
7f20cf59d1 | ||
|
|
a8a1b08127 | ||
|
|
1a1e777b87 | ||
|
|
9e76e64064 | ||
|
|
975ae17ef9 | ||
|
|
ed9dcef66f | ||
|
|
b1e537e54e | ||
|
|
e5886862c3 | ||
|
|
d85b668d4f | ||
|
|
b363d8bfb5 | ||
|
|
754d467440 | ||
|
|
598f08d6c7 | ||
|
|
224fdc1864 | ||
|
|
e646b4dc9a | ||
|
|
9077db2e97 | ||
|
|
7e14277ead | ||
|
|
d351a7d697 | ||
|
|
70ed43b811 | ||
|
|
913083ebc6 | ||
|
|
588a95a7ae | ||
|
|
c0a0ad4ec5 | ||
|
|
5eafe96525 |
@@ -995,6 +995,7 @@ PRIVATE
|
||||
info/statistics/info_statistics_list_controllers.h
|
||||
info/statistics/info_statistics_recent_message.cpp
|
||||
info/statistics/info_statistics_recent_message.h
|
||||
info/statistics/info_statistics_tag.h
|
||||
info/statistics/info_statistics_widget.cpp
|
||||
info/statistics/info_statistics_widget.h
|
||||
info/stories/info_stories_inner_widget.cpp
|
||||
@@ -1162,6 +1163,8 @@ PRIVATE
|
||||
media/streaming/media_streaming_player.h
|
||||
media/streaming/media_streaming_reader.cpp
|
||||
media/streaming/media_streaming_reader.h
|
||||
media/streaming/media_streaming_round_preview.cpp
|
||||
media/streaming/media_streaming_round_preview.h
|
||||
media/streaming/media_streaming_utility.cpp
|
||||
media/streaming/media_streaming_utility.h
|
||||
media/streaming/media_streaming_video_track.cpp
|
||||
@@ -1501,6 +1504,8 @@ PRIVATE
|
||||
ui/chat/choose_send_as.h
|
||||
ui/chat/choose_theme_controller.cpp
|
||||
ui/chat/choose_theme_controller.h
|
||||
ui/chat/sponsored_message_bar.cpp
|
||||
ui/chat/sponsored_message_bar.h
|
||||
ui/controls/emoji_button_factory.cpp
|
||||
ui/controls/emoji_button_factory.h
|
||||
ui/controls/location_picker.cpp
|
||||
@@ -1849,7 +1854,7 @@ endif()
|
||||
|
||||
set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
|
||||
|
||||
if (WIN32 AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
if (MSVC)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
delayimp
|
||||
@@ -1946,7 +1951,7 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
|
||||
base/platform/win/base_windows_safe_library.h
|
||||
)
|
||||
target_include_directories(Updater PRIVATE ${lib_base_loc})
|
||||
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
if (MSVC)
|
||||
target_link_libraries(Updater
|
||||
PRIVATE
|
||||
delayimp
|
||||
|
||||
BIN
Telegram/Resources/art/round_placeholder.jpg
Normal file
|
After Width: | Height: | Size: 787 B |
BIN
Telegram/Resources/icons/chat/input_video.png
Normal file
|
After Width: | Height: | Size: 621 B |
BIN
Telegram/Resources/icons/chat/input_video@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Telegram/Resources/icons/chat/input_video@3x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Telegram/Resources/icons/menu/edited_status.png
Normal file
|
After Width: | Height: | Size: 586 B |
BIN
Telegram/Resources/icons/menu/edited_status@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Telegram/Resources/icons/menu/edited_status@3x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Telegram/Resources/icons/player/player_settings.png
Normal file
|
After Width: | Height: | Size: 997 B |
BIN
Telegram/Resources/icons/player/player_settings@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/player/player_settings@3x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Telegram/Resources/icons/voice_lock/input_round_s.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
Telegram/Resources/icons/voice_lock/input_round_s@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/voice_lock/input_round_s@3x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -12,6 +12,7 @@ body {
|
||||
margin: 0;
|
||||
background-color: var(--td-window-bg);
|
||||
color: var(--td-window-fg);
|
||||
zoom: var(--td-zoom-percentage);
|
||||
}
|
||||
|
||||
html.custom_scroll ::-webkit-scrollbar {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -499,8 +499,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_notify_global" = "Global settings";
|
||||
"lng_settings_notify_title" = "Notifications for chats";
|
||||
"lng_settings_desktop_notify" = "Desktop notifications";
|
||||
"lng_settings_native_title" = "Native notifications";
|
||||
"lng_settings_native_title" = "System integration";
|
||||
"lng_settings_use_windows" = "Use Windows notifications";
|
||||
"lng_settings_skip_in_focus" = "Respect system Focus mode";
|
||||
"lng_settings_use_native_notifications" = "Use native notifications";
|
||||
"lng_settings_notifications_position" = "Location on the screen";
|
||||
"lng_settings_notifications_count" = "Notifications count";
|
||||
@@ -1593,6 +1594,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_manage_peer_bot_public_link" = "Public Link";
|
||||
"lng_manage_peer_bot_public_links" = "Public Links";
|
||||
"lng_manage_peer_bot_balance" = "Balance";
|
||||
"lng_manage_peer_bot_balance_currency" = "Toncoin";
|
||||
"lng_manage_peer_bot_balance_credits" = "Stars";
|
||||
"lng_manage_peer_bot_edit_intro" = "Edit Intro";
|
||||
"lng_manage_peer_bot_edit_commands" = "Edit Commands";
|
||||
"lng_manage_peer_bot_edit_settings" = "Change Bot Settings";
|
||||
@@ -1870,6 +1873,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_gift_got_subtitle" = "Gift from {user}";
|
||||
"lng_action_gift_got_stars_text#one" = "Display this gift on your page or convert it to **{count}** Star.";
|
||||
"lng_action_gift_got_stars_text#other" = "Display this gift on your page or convert it to **{count}** Stars.";
|
||||
"lng_action_gift_got_gift_text" = "You can keep this gift on your page.";
|
||||
"lng_action_gift_sent_subtitle" = "Gift for {user}";
|
||||
"lng_action_gift_sent_text#one" = "{user} can display this gift on their page or convert it to {count} Star.";
|
||||
"lng_action_gift_sent_text#other" = "{user} can display this gift on their page or convert it to {count} Stars.";
|
||||
@@ -2115,8 +2119,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_recommended_message_title" = "Recommended";
|
||||
"lng_edited" = "edited";
|
||||
"lng_commented" = "commented";
|
||||
"lng_approximate" = "appx.";
|
||||
"lng_edited_date" = "Edited: {date}";
|
||||
"lng_sent_date" = "Sent: {date}";
|
||||
"lng_approximate_about" = "Estimated date of video publishing.";
|
||||
"lng_views_tooltip#one" = "Views: {count}";
|
||||
"lng_views_tooltip#other" = "Views: {count}";
|
||||
"lng_forwards_tooltip#one" = "Shares: {count}";
|
||||
@@ -2151,6 +2157,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_media_cancel" = "Cancel";
|
||||
"lng_media_video" = "Video";
|
||||
"lng_media_audio" = "Voice message";
|
||||
"lng_media_round" = "Video message";
|
||||
|
||||
"lng_media_auto_settings" = "Automatic media download";
|
||||
"lng_media_auto_in_private" = "In private chats";
|
||||
@@ -2442,12 +2449,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_credits_box_history_entry_giveaway_name" = "Received Prize";
|
||||
"lng_credits_box_history_entry_gift_sent" = "Sent Gift";
|
||||
"lng_credits_box_history_entry_gift_converted" = "Converted Gift";
|
||||
"lng_credits_box_history_entry_gift_unavailable" = "Unavailable";
|
||||
"lng_credits_box_history_entry_gift_sold_out" = "This gift has sold out";
|
||||
"lng_credits_box_history_entry_gift_out_about" = "With Stars, **{user}** will be able to unlock content and services on Telegram.\n{link}";
|
||||
"lng_credits_box_history_entry_gift_in_about" = "Use Stars to unlock content and services on Telegram. {link}";
|
||||
"lng_credits_box_history_entry_gift_about_link" = "See Examples {emoji}";
|
||||
"lng_credits_box_history_entry_gift_examples" = "Examples";
|
||||
"lng_credits_box_history_entry_ads" = "Ads Platform";
|
||||
"lng_credits_box_history_entry_premium_bot" = "Stars Top-Up";
|
||||
"lng_credits_box_history_entry_api" = "Paid Broadcast";
|
||||
"lng_credits_box_history_entry_floodskip_about#one" = "{count} Message";
|
||||
"lng_credits_box_history_entry_floodskip_about#other" = "{count} Messages";
|
||||
"lng_credits_box_history_entry_floodskip_row" = "Messages";
|
||||
"lng_credits_box_history_entry_via_premium_bot" = "Premium Bot";
|
||||
"lng_credits_box_history_entry_id" = "Transaction ID";
|
||||
"lng_credits_box_history_entry_id_copied" = "Transaction ID copied to clipboard.";
|
||||
@@ -2988,6 +3001,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_link_reason_unclaimed" = "Incomplete Giveaway";
|
||||
"lng_gift_link_reason_chosen" = "You were selected by the channel";
|
||||
"lng_gift_link_label_date" = "Date";
|
||||
"lng_gift_link_label_first_sale" = "First Sale";
|
||||
"lng_gift_link_label_last_sale" = "Last Sale";
|
||||
"lng_gift_link_label_value" = "Value";
|
||||
"lng_gift_link_also_send" = "You can also {link} to a friend as a gift.";
|
||||
"lng_gift_link_also_send_link" = "send this link";
|
||||
"lng_gift_link_use" = "Use Link";
|
||||
@@ -3044,8 +3060,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_convert_to_stars#one" = "Convert to {count} Star";
|
||||
"lng_gift_convert_to_stars#other" = "Convert to {count} Stars";
|
||||
"lng_gift_convert_sure_title" = "Convert Gift to Stars";
|
||||
"lng_gift_convert_sure_text#one" = "Do you want to convert this gift from {user} to **{count} Star**?\n\nThis action cannot be undone.";
|
||||
"lng_gift_convert_sure_text#other" = "Do you want to convert this gift from {user} to **{count} Stars**?\n\nThis action cannot be undone.";
|
||||
"lng_gift_convert_sure_confirm#one" = "Do you want to convert this gift from {user} to **{count} Star**?";
|
||||
"lng_gift_convert_sure_confirm#other" = "Do you want to convert this gift from {user} to **{count} Stars**?";
|
||||
"lng_gift_convert_sure_limit#one" = "Conversion is available for the next **{count} day**.";
|
||||
"lng_gift_convert_sure_limit#other" = "Conversion is available for the next **{count} days**.";
|
||||
"lng_gift_convert_sure_caution" = "This action cannot be undone. This will permanently destroy the gift.";
|
||||
"lng_gift_convert_sure" = "Convert";
|
||||
"lng_gift_display_done" = "The gift is now shown on your profile page.";
|
||||
"lng_gift_display_done_hide" = "The gift is now hidden from your profile page.";
|
||||
@@ -3054,6 +3073,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_gift_sold_out_title" = "Sold Out!";
|
||||
"lng_gift_sold_out_text#one" = "All {count} gift was already sold.";
|
||||
"lng_gift_sold_out_text#other" = "All {count} gifts were already sold.";
|
||||
"lng_gift_send_small" = "send a gift";
|
||||
"lng_gift_sell_small#one" = "sell for {count} Star";
|
||||
"lng_gift_sell_small#other" = "sell for {count} Stars";
|
||||
|
||||
"lng_accounts_limit_title" = "Limit Reached";
|
||||
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
|
||||
@@ -3243,11 +3265,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_record_cancel" = "Release outside this field to cancel";
|
||||
"lng_record_cancel_stories" = "Release outside to cancel";
|
||||
"lng_record_lock_cancel_sure" = "Do you want to stop recording and discard your voice message?";
|
||||
"lng_record_lock_cancel_sure_round" = "Do you want to stop recording and discard your video message?";
|
||||
"lng_record_listen_cancel_sure" = "Do you want to discard your recorded voice message?";
|
||||
"lng_record_listen_cancel_sure_round" = "Do you want to discard your recorded video message?";
|
||||
"lng_record_lock_discard" = "Discard";
|
||||
"lng_record_hold_tip" = "Please hold the mouse button pressed to record a voice message.";
|
||||
"lng_record_voice_tip" = "Hold to record audio. Click to switch to video.";
|
||||
"lng_record_video_tip" = "Hold to record video. Click to switch to audio.";
|
||||
"lng_record_audio_problem" = "Could not start audio recording. Please check your microphone.";
|
||||
"lng_record_video_problem" = "Could not start video recording. Please check your camera.";
|
||||
"lng_record_once_first_tooltip" = "Click to set this message to **Play Once**.";
|
||||
"lng_record_once_active_tooltip" = "The recipient will be able to listen only once.";
|
||||
"lng_record_once_active_video" = "The recipient will be able to watch only once.";
|
||||
"lng_will_be_notified" = "Subscribers will be notified when you post.";
|
||||
"lng_wont_be_notified" = "Subscribers will receive a silent notification.";
|
||||
"lng_willbe_history" = "Select a chat to start messaging";
|
||||
@@ -3276,6 +3305,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_scheduled_send_now" = "Send message now?";
|
||||
"lng_scheduled_send_now_many#one" = "Send {count} message now?";
|
||||
"lng_scheduled_send_now_many#other" = "Send {count} messages now?";
|
||||
"lng_scheduled_video_tip_title" = "Improving video...";
|
||||
"lng_scheduled_video_tip_text" = "The video will be published after it's optimized for the best viewing experience.";
|
||||
"lng_scheduled_video_tip" = "Processing video may take a few minutes.";
|
||||
"lng_scheduled_video_published" = "Video Published.";
|
||||
"lng_scheduled_video_view" = "View";
|
||||
|
||||
"lng_replies_view#one" = "View {count} Reply";
|
||||
"lng_replies_view#other" = "View {count} Replies";
|
||||
@@ -3359,6 +3393,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_bot_settings" = "Settings";
|
||||
"lng_bot_open" = "Open Bot";
|
||||
"lng_bot_terms" = "Terms of Use";
|
||||
"lng_bot_privacy" = "Privacy Policy";
|
||||
"lng_bot_reload_page" = "Reload Page";
|
||||
"lng_bot_add_to_menu" = "{bot} asks your permission to be added as an option to your attachment menu so you can access it from any chat.";
|
||||
"lng_bot_add_to_menu_done" = "Bot added to the menu.";
|
||||
@@ -3576,6 +3611,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
|
||||
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
|
||||
|
||||
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
|
||||
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
|
||||
"lng_context_noforwards_info_bot" = "Copying and forwarding is not allowed from this bot.";
|
||||
|
||||
"lng_context_spoiler_effect" = "Hide with Spoiler";
|
||||
"lng_context_disable_spoiler" = "Remove Spoiler";
|
||||
"lng_context_make_paid" = "Make This Content Paid";
|
||||
@@ -3678,6 +3717,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
"lng_reply_in_another_title" = "Reply in...";
|
||||
"lng_reply_in_another_chat" = "Reply in Another Chat";
|
||||
"lng_reply_in_author" = "Message author";
|
||||
"lng_reply_in_chats_list" = "Your chats";
|
||||
"lng_reply_show_in_chat" = "Show in Chat";
|
||||
"lng_reply_remove" = "Do Not Reply";
|
||||
"lng_reply_about_quote" = "You can select a specific part to quote.";
|
||||
@@ -3865,6 +3906,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_mediaview_downloads" = "Downloads";
|
||||
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
|
||||
"lng_mediaview_rotate_video" = "Rotate video";
|
||||
"lng_mediaview_quality_auto" = "Auto";
|
||||
|
||||
"lng_theme_preview_title" = "Theme Preview";
|
||||
"lng_theme_preview_generating" = "Generating color theme preview...";
|
||||
@@ -3946,7 +3988,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
"lng_payments_webview_no_use" = "Unfortunately, you can't use payments with current system configuration.";
|
||||
"lng_payments_webview_install_edge" = "Please install {link}.";
|
||||
"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkitgtk-6.0/webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.";
|
||||
"lng_payments_webview_install_webkit" = "Please install WebKitGTK (webkit2gtk-4.1/webkit2gtk-4.0) using your package manager.";
|
||||
"lng_payments_webview_enable_opengl" = "Please enable OpenGL in application settings.";
|
||||
"lng_payments_webview_switch_x11" = "Unsupported display server. Please switch to X11.";
|
||||
"lng_payments_webview_update_windows" = "Please update your system to Windows 8.1 or later.";
|
||||
"lng_payments_sure_close" = "Are you sure you want to close this payment form? The changes you made will be lost.";
|
||||
"lng_payments_receipt_label" = "Receipt";
|
||||
@@ -5168,13 +5212,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_sponsored_revenued_subtitle" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:";
|
||||
"lng_sponsored_revenued_info1_title" = "Respect Your Privacy";
|
||||
"lng_sponsored_revenued_info1_description" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them.";
|
||||
"lng_sponsored_revenued_info1_bot_description" = "Ads on Telegram do not use your personal information and are based on the mini app in which you see them.";
|
||||
"lng_sponsored_revenued_info2_title" = "Help the Channel Creator";
|
||||
"lng_sponsored_revenued_info2_description" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed.";
|
||||
"lng_sponsored_revenued_info2_bot_description" = "50% of the revenue from Telegram Ads goes to the developer of the mini app where they are displayed.";
|
||||
"lng_sponsored_revenued_info3_title" = "Can Be Removed";
|
||||
"lng_sponsored_revenued_info3_description#one" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
|
||||
"lng_sponsored_revenued_info3_description#other" = "You can turn off ads by subscribing to {link}, and Level {count} channels can remove them for their subscribers.";
|
||||
"lng_sponsored_revenued_info3_bot_description" = "You can turn off ads in mini apps by subscribing to {link}.";
|
||||
"lng_sponsored_revenued_footer_title" = "Can I Launch an Ad?";
|
||||
"lng_sponsored_revenued_footer_description" = "Anyone can create an ad to display in this channel — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
|
||||
"lng_sponsored_revenued_footer_bot_description" = "Anyone can create an ad to display in this bot — with minimal budgets. Check out the **Telegram Ad Platform** for details. {link}";
|
||||
"lng_sponsored_top_bar_hide" = "remove";
|
||||
|
||||
"lng_telegram_features_url" = "https://t.me/TelegramTips";
|
||||
|
||||
@@ -5478,6 +5527,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
"lng_channel_earn_title" = "Monetization";
|
||||
"lng_channel_earn_about" = "Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}";
|
||||
"lng_channel_earn_about_bot" = "Telegram shares 50% of the revenue from ads displayed in your bot. {link}";
|
||||
"lng_channel_earn_about_link" = "Learn more {emoji}";
|
||||
"lng_channel_earn_overview_title" = "Rewards overview";
|
||||
"lng_channel_earn_available" = "Rewards available for collection";
|
||||
@@ -5510,8 +5560,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_channel_earn_cpm#one" = "{emoji} {count} CPM";
|
||||
"lng_channel_earn_cpm#other" = "{emoji} {count} CPM";
|
||||
"lng_channel_earn_learn_title" = "Earn From Your Channel";
|
||||
"lng_channel_earn_bot_learn_title" = "Earn From Your Bot";
|
||||
"lng_channel_earn_learn_in_subtitle" = "Telegram Ads";
|
||||
"lng_channel_earn_learn_in_about" = "Telegram can display ads in your channel.";
|
||||
"lng_channel_earn_learn_bot_in_about" = "Telegram can display ads in your bot.";
|
||||
"lng_channel_earn_learn_split_subtitle" = "50:50 revenue split";
|
||||
"lng_channel_earn_learn_split_about" = "You can receive 50% of the ad revenue as rewards in TON.";
|
||||
"lng_channel_earn_learn_out_subtitle" = "Flexible withdrawals";
|
||||
@@ -5558,6 +5610,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_iv_window_title" = "Instant View";
|
||||
"lng_iv_wrong_layout" = "Wrong layout?";
|
||||
"lng_iv_not_supported" = "This link appears to be invalid.";
|
||||
"lng_iv_zoom_tooltip_ctrl" = "Hold Ctrl to zoom by 5%.\nHold Alt to zoom by 1%.";
|
||||
"lng_iv_zoom_tooltip_cmd" = "Hold Cmd to zoom by 5%.\nHold Alt to zoom by 1%.";
|
||||
|
||||
"lng_limit_download_title" = "Download speed limited";
|
||||
"lng_limit_download_subscribe" = "Subscribe to {link} to increase download speed {increase}.";
|
||||
@@ -5595,6 +5649,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_channels_recommended" = "Similar channels";
|
||||
"lng_bot_apps_your" = "Apps you use";
|
||||
"lng_bot_apps_popular" = "Grossing apps";
|
||||
"lng_bot_apps_which" = "Which apps are included here? {link}";
|
||||
"lng_bot_apps_which_link" = "Learn >";
|
||||
|
||||
"lng_popular_apps_info_title" = "Top Mini Apps";
|
||||
"lng_popular_apps_info_text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average.";
|
||||
"lng_popular_apps_info_bot" = "@botfather";
|
||||
"lng_popular_apps_info_here" = "here";
|
||||
"lng_popular_apps_info_url" = "https://core.telegram.org/bots/webapps#launching-the-main-mini-app";
|
||||
"lng_popular_apps_info_confirm" = "Understood";
|
||||
|
||||
"lng_font_box_title" = "Choose font family";
|
||||
"lng_font_default" = "Default";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.6.2.0" />
|
||||
Version="5.7.0.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#if defined(__MINGW64__) || defined(__MINGW32__)
|
||||
// MinGW-w64, MinGW
|
||||
#if defined(__has_include) && __has_include(<winres.h>)
|
||||
#include <winres.h>
|
||||
#else
|
||||
#include <afxres.h>
|
||||
#include <winresrc.h>
|
||||
#endif
|
||||
#if defined(__has_include) && __has_include(<winres.h>)
|
||||
#include <winres.h>
|
||||
#else
|
||||
// MSVC, Windows SDK
|
||||
#include <winres.h>
|
||||
#include <afxres.h>
|
||||
#include <winresrc.h>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
@@ -44,8 +38,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,6,2,0
|
||||
PRODUCTVERSION 5,6,2,0
|
||||
FILEVERSION 5,7,0,0
|
||||
PRODUCTVERSION 5,7,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +56,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "5.6.2.0"
|
||||
VALUE "FileVersion", "5.7.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.6.2.0"
|
||||
VALUE "ProductVersion", "5.7.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#if defined(__MINGW64__) || defined(__MINGW32__)
|
||||
// MinGW-w64, MinGW
|
||||
#if defined(__has_include) && __has_include(<winres.h>)
|
||||
#include <winres.h>
|
||||
#else
|
||||
#include <afxres.h>
|
||||
#include <winresrc.h>
|
||||
#endif
|
||||
#if defined(__has_include) && __has_include(<winres.h>)
|
||||
#include <winres.h>
|
||||
#else
|
||||
// MSVC, Windows SDK
|
||||
#include <winres.h>
|
||||
#include <afxres.h>
|
||||
#include <winresrc.h>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
@@ -35,8 +29,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,6,2,0
|
||||
PRODUCTVERSION 5,6,2,0
|
||||
FILEVERSION 5,7,0,0
|
||||
PRODUCTVERSION 5,7,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +47,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "5.6.2.0"
|
||||
VALUE "FileVersion", "5.7.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.6.2.0"
|
||||
VALUE "ProductVersion", "5.7.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "settings/settings_credits_graphics.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/effects/premium_stars_colored.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
@@ -129,6 +130,7 @@ void ConfirmSubscriptionBox(
|
||||
struct State final {
|
||||
std::shared_ptr<Data::PhotoMedia> photoMedia;
|
||||
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
|
||||
QImage frame;
|
||||
|
||||
std::optional<MTP::Sender> api;
|
||||
Ui::RpWidget* saveButton = nullptr;
|
||||
@@ -146,25 +148,45 @@ void ConfirmSubscriptionBox(
|
||||
const auto userpic = userpicWrap->entity();
|
||||
const auto photoSize = st::confirmInvitePhotoSize;
|
||||
userpic->resize(Size(photoSize));
|
||||
const auto creditsIconSize = photoSize / 3;
|
||||
const auto creditsIconCallback =
|
||||
Ui::PaintOutlinedColoredCreditsIconCallback(
|
||||
creditsIconSize,
|
||||
1.5);
|
||||
state->frame = QImage(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
const auto options = Images::Option::RoundCircle;
|
||||
userpic->paintRequest(
|
||||
) | rpl::start_with_next([=, small = Data::PhotoSize::Small] {
|
||||
auto p = QPainter(userpic);
|
||||
if (state->photoMedia) {
|
||||
if (const auto image = state->photoMedia->image(small)) {
|
||||
p.drawPixmap(
|
||||
state->frame.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&state->frame);
|
||||
if (state->photoMedia) {
|
||||
if (const auto image = state->photoMedia->image(small)) {
|
||||
p.drawPixmap(
|
||||
0,
|
||||
0,
|
||||
image->pix(Size(photoSize), { .options = options }));
|
||||
}
|
||||
} else if (state->photoEmpty) {
|
||||
state->photoEmpty->paintCircle(
|
||||
p,
|
||||
0,
|
||||
0,
|
||||
image->pix(Size(photoSize), { .options = options }));
|
||||
userpic->width(),
|
||||
photoSize);
|
||||
}
|
||||
if (creditsIconCallback) {
|
||||
p.translate(
|
||||
photoSize - creditsIconSize,
|
||||
photoSize - creditsIconSize);
|
||||
creditsIconCallback(p);
|
||||
}
|
||||
} else if (state->photoEmpty) {
|
||||
state->photoEmpty->paintCircle(
|
||||
p,
|
||||
0,
|
||||
0,
|
||||
userpic->width(),
|
||||
photoSize);
|
||||
}
|
||||
auto p = QPainter(userpic);
|
||||
p.drawImage(0, 0, state->frame);
|
||||
}, userpicWrap->lifetime());
|
||||
userpicWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
if (photo) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -99,8 +99,8 @@ public:
|
||||
[[nodiscard]] Data::CreditsEarnStatistics data() const;
|
||||
|
||||
private:
|
||||
const bool _isUser = false;
|
||||
Data::CreditsEarnStatistics _data;
|
||||
bool _isUser = false;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -456,6 +456,7 @@ void SendConfirmedFile(
|
||||
not_null<Main::Session*> session,
|
||||
const std::shared_ptr<FilePrepareResult> &file) {
|
||||
const auto isEditing = (file->type != SendMediaType::Audio)
|
||||
&& (file->type != SendMediaType::Round)
|
||||
&& (file->to.replaceMediaOf != 0);
|
||||
const auto newId = FullMsgId(
|
||||
file->to.peer,
|
||||
@@ -525,7 +526,8 @@ void SendConfirmedFile(
|
||||
// Shortcut messages have no 'edited' badge.
|
||||
flags |= MessageFlag::HideEdited;
|
||||
}
|
||||
if (file->type == SendMediaType::Audio) {
|
||||
if (file->type == SendMediaType::Audio
|
||||
|| file->type == SendMediaType::Round) {
|
||||
if (!peer->isChannel() || peer->isMegagroup()) {
|
||||
flags |= MessageFlag::MediaIsUnread;
|
||||
}
|
||||
@@ -551,29 +553,25 @@ void SendConfirmedFile(
|
||||
MTPint());
|
||||
} else if (file->type == SendMediaType::Audio) {
|
||||
const auto ttlSeconds = file->to.options.ttlSeconds;
|
||||
const auto isVoice = [&] {
|
||||
return file->document.match([](const MTPDdocumentEmpty &d) {
|
||||
return false;
|
||||
}, [](const MTPDdocument &d) {
|
||||
return ranges::any_of(d.vattributes().v, [&](
|
||||
const MTPDocumentAttribute &attribute) {
|
||||
using Att = MTPDdocumentAttributeAudio;
|
||||
return attribute.match([](const Att &data) -> bool {
|
||||
return data.vflags().v & Att::Flag::f_voice;
|
||||
}, [](const auto &) {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}();
|
||||
using Flag = MTPDmessageMediaDocument::Flag;
|
||||
return MTP_messageMediaDocument(
|
||||
MTP_flags(Flag::f_document
|
||||
| (isVoice ? Flag::f_voice : Flag())
|
||||
| Flag::f_voice
|
||||
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())),
|
||||
file->document,
|
||||
MTPVector<MTPDocument>(), // alt_documents
|
||||
MTP_int(ttlSeconds));
|
||||
} else if (file->type == SendMediaType::Round) {
|
||||
using Flag = MTPDmessageMediaDocument::Flag;
|
||||
const auto ttlSeconds = file->to.options.ttlSeconds;
|
||||
return MTP_messageMediaDocument(
|
||||
MTP_flags(Flag::f_document
|
||||
| Flag::f_round
|
||||
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
|
||||
| (file->spoiler ? Flag::f_spoiler : Flag())),
|
||||
file->document,
|
||||
MTPVector<MTPDocument>(), // alt_documents
|
||||
MTP_int(ttlSeconds));
|
||||
} else {
|
||||
Unexpected("Type in sendFilesConfirmed.");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3329,6 +3329,7 @@ void ApiWrap::forwardMessages(
|
||||
}
|
||||
const auto requestType = Data::Histories::RequestType::Send;
|
||||
const auto idsCopy = localIds;
|
||||
const auto scheduled = action.options.scheduled;
|
||||
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
|
||||
history->sendRequestId = request(MTPmessages_ForwardMessages(
|
||||
MTP_flags(sendFlags),
|
||||
@@ -3341,6 +3342,9 @@ void ApiWrap::forwardMessages(
|
||||
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
|
||||
Data::ShortcutIdToMTP(_session, action.options.shortcutId)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
if (!scheduled) {
|
||||
this->updates().checkForSentToScheduled(result);
|
||||
}
|
||||
applyUpdates(result);
|
||||
if (shared && !--shared->requestsLeft) {
|
||||
shared->callback();
|
||||
@@ -3502,6 +3506,7 @@ void ApiWrap::sendVoiceMessage(
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
bool video,
|
||||
const SendAction &action) {
|
||||
const auto caption = TextWithTags();
|
||||
const auto to = FileLoadTaskOptions(action);
|
||||
@@ -3510,6 +3515,7 @@ void ApiWrap::sendVoiceMessage(
|
||||
result,
|
||||
duration,
|
||||
waveform,
|
||||
video,
|
||||
to,
|
||||
caption));
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ public:
|
||||
QByteArray result,
|
||||
VoiceWaveform waveform,
|
||||
crl::time duration,
|
||||
bool video,
|
||||
const SendAction &action);
|
||||
void sendFiles(
|
||||
Ui::PreparedList &&list,
|
||||
|
||||
@@ -154,9 +154,7 @@ contactsSortButton: IconButton(defaultIconButton) {
|
||||
iconPosition: point(10px, -1px);
|
||||
rippleAreaPosition: point(1px, 6px);
|
||||
rippleAreaSize: 42px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
contactsSortOnlineIcon: icon{{ "contacts_online", boxTitleCloseFg }};
|
||||
contactsSortOnlineIconOver: icon{{ "contacts_online", boxTitleCloseFgOver }};
|
||||
@@ -416,9 +414,7 @@ calendarPrevious: IconButton {
|
||||
|
||||
rippleAreaPosition: point(2px, 2px);
|
||||
rippleAreaSize: 44px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
calendarPreviousDisabled: icon {{ "calendar_down-flip_vertical", menuIconFg }};
|
||||
calendarNext: IconButton(calendarPrevious) {
|
||||
@@ -616,9 +612,7 @@ proxyTryIPv6Padding: margins(22px, 8px, 22px, 5px);
|
||||
proxyRowPadding: margins(22px, 8px, 8px, 8px);
|
||||
proxyRowIconSkip: 32px;
|
||||
proxyRowSkip: 2px;
|
||||
proxyRowRipple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
proxyRowRipple: defaultRippleAnimationBgOver;
|
||||
proxyRowTitleFg: windowFg;
|
||||
proxyRowTitlePalette: TextPalette(defaultTextPalette) {
|
||||
linkFg: windowSubTextFg;
|
||||
@@ -683,9 +677,7 @@ themesMenuToggle: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 36px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
themesMenuPosition: point(-2px, 25px);
|
||||
|
||||
@@ -738,9 +730,7 @@ createPollOptionRemove: CrossButton {
|
||||
|
||||
duration: 150;
|
||||
loadingPeriod: 1000;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
createPollOptionRemovePosition: point(11px, 9px);
|
||||
createPollOptionEmojiPositionSkip: 4px;
|
||||
@@ -888,6 +878,13 @@ peerListWithInviteViaLink: PeerList(peerListBox) {
|
||||
peerListSingleRow: PeerList(peerListBox) {
|
||||
padding: margins(0px, 0px, 0px, 0px);
|
||||
}
|
||||
peerListSmallSkips: PeerList(peerListBox) {
|
||||
padding: margins(
|
||||
0px,
|
||||
defaultVerticalListSkip,
|
||||
0px,
|
||||
defaultVerticalListSkip);
|
||||
}
|
||||
|
||||
scheduleHeight: 95px;
|
||||
scheduleDateTop: 38px;
|
||||
@@ -951,9 +948,7 @@ sponsoredUrlButton: RoundButton(defaultActiveButton) {
|
||||
textTop: 7px;
|
||||
style: defaultTextStyle;
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
requestPeerRestriction: FlatLabel(defaultFlatLabel) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1944,6 +1944,13 @@ PeerListContent::SkipResult PeerListContent::selectSkip(int direction) {
|
||||
}
|
||||
}
|
||||
|
||||
if (_controller->overrideKeyboardNavigation(
|
||||
direction,
|
||||
_selected.index.value,
|
||||
newSelectedIndex)) {
|
||||
return { _selected.index.value, _selected.index.value };
|
||||
}
|
||||
|
||||
_selected.index.value = newSelectedIndex;
|
||||
_selected.element = 0;
|
||||
if (newSelectedIndex >= 0) {
|
||||
|
||||
@@ -357,6 +357,8 @@ public:
|
||||
virtual int peerListPartitionRows(Fn<bool(const PeerListRow &a)> border) = 0;
|
||||
virtual std::shared_ptr<Main::SessionShow> peerListUiShow() = 0;
|
||||
|
||||
virtual void peerListSelectSkip(int direction) = 0;
|
||||
|
||||
virtual void peerListPressLeftToContextMenu(bool shown) = 0;
|
||||
virtual bool peerListTrackRowPressFromGlobal(QPoint globalPosition) = 0;
|
||||
|
||||
@@ -573,6 +575,13 @@ public:
|
||||
Unexpected("PeerListController::customRowRippleMaskGenerator.");
|
||||
}
|
||||
|
||||
virtual bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -1016,6 +1025,10 @@ public:
|
||||
bool highlightRow,
|
||||
Fn<void(not_null<Ui::PopupMenu*>)> destroyed = nullptr) override;
|
||||
|
||||
void peerListSelectSkip(int direction) override {
|
||||
_content->selectSkip(direction);
|
||||
}
|
||||
|
||||
void peerListPressLeftToContextMenu(bool shown) override {
|
||||
_content->pressLeftToContextMenu(shown);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -956,12 +956,11 @@ void Controller::rowClicked(not_null<PeerListRow*> row) {
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
const auto &stUser = st::boostReplaceUserpic;
|
||||
const auto photoSize = st::boostReplaceUserpic.photoSize;
|
||||
const auto session = &row->peer()->session();
|
||||
content->add(object_ptr<Ui::CenterWrap<>>(
|
||||
content,
|
||||
object_ptr<Ui::UserpicButton>(content, channel, stUser))
|
||||
)->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
Settings::SubscriptionUserpic(content, channel, photoSize)));
|
||||
|
||||
Ui::AddSkip(content);
|
||||
Ui::AddSkip(content);
|
||||
|
||||
@@ -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) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>(),
|
||||
|
||||
@@ -409,9 +409,7 @@ callRatingStar: IconButton {
|
||||
icon: icon {{ "calls/call_rating", windowSubTextFg }};
|
||||
iconPosition: point(-1px, -1px);
|
||||
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 36px;
|
||||
}
|
||||
@@ -1410,9 +1408,7 @@ groupCallRtmpShowButton: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 32px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
groupCallSettingsRtmpShowButton: IconButton(groupCallRtmpShowButton) {
|
||||
ripple: groupCallRipple;
|
||||
|
||||
@@ -73,8 +73,8 @@ private:
|
||||
|
||||
SourceButton _widget;
|
||||
FlatLabel _label;
|
||||
RoundRect _selectedRect;
|
||||
RoundRect _activeRect;
|
||||
Ui::RoundRect _selectedRect;
|
||||
Ui::RoundRect _activeRect;
|
||||
tgcalls::DesktopCaptureSource _source;
|
||||
std::unique_ptr<Preview> _preview;
|
||||
rpl::event_stream<> _activations;
|
||||
|
||||
@@ -150,6 +150,8 @@ SendButton {
|
||||
inner: IconButton;
|
||||
record: icon;
|
||||
recordOver: icon;
|
||||
round: icon;
|
||||
roundOver: icon;
|
||||
sendDisabledFg: color;
|
||||
}
|
||||
|
||||
@@ -331,9 +333,7 @@ stickersRemove: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaSize: 40px;
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
stickersUndoRemove: RoundButton(defaultLightButton) {
|
||||
width: -16px;
|
||||
@@ -494,9 +494,7 @@ hashtagClose: IconButton {
|
||||
|
||||
rippleAreaPosition: point(5px, 5px);
|
||||
rippleAreaSize: 20px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
stickerPanWidthMin: 64px;
|
||||
@@ -898,9 +896,7 @@ historyBusinessBotSettings: IconButton(defaultIconButton) {
|
||||
iconPosition: point(-1px, -1px);
|
||||
rippleAreaSize: 40px;
|
||||
rippleAreaPosition: point(4px, 9px);
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
height: 58px;
|
||||
width: 48px;
|
||||
}
|
||||
@@ -927,9 +923,7 @@ historyReplyCancel: IconButton {
|
||||
|
||||
rippleAreaPosition: point(4px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
historyPinnedShowAll: IconButton(historyReplyCancel) {
|
||||
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
|
||||
@@ -1058,9 +1052,7 @@ historyAttach: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(2px, 3px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
historyMessagesTTL: IconButtonWithText {
|
||||
@@ -1082,6 +1074,13 @@ historyReplaceMedia: IconButton(historyAttach) {
|
||||
color: lightButtonBgOver;
|
||||
}
|
||||
}
|
||||
historyAddMedia: IconButton(historyAttach) {
|
||||
icon: icon {{ "chat/input_attach", windowBgActive }};
|
||||
iconOver: icon {{ "chat/input_attach", windowBgActive }};
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: lightButtonBgOver;
|
||||
}
|
||||
}
|
||||
|
||||
historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }};
|
||||
historyEmojiCircle: size(20px, 20px);
|
||||
@@ -1169,6 +1168,10 @@ historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive
|
||||
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
|
||||
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
|
||||
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
|
||||
historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }};
|
||||
historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }};
|
||||
historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }};
|
||||
historyRecordRoundIconPosition: point(0px, 0px);
|
||||
historyRecordSendIconPosition: point(2px, 0px);
|
||||
historyRecordVoiceRippleBgActive: lightButtonBgOver;
|
||||
historyRecordSignalRadius: 5px;
|
||||
@@ -1214,6 +1217,7 @@ historyRecordLockBody: icon {{ "voice_lock/record_lock_body", historyToDownBg }}
|
||||
historyRecordLockMargin: margins(4px, 4px, 4px, 4px);
|
||||
historyRecordLockArrow: icon {{ "voice_lock/voice_arrow", historyToDownFg }};
|
||||
historyRecordLockInput: icon {{ "voice_lock/input_mic_s", historyToDownFg }};
|
||||
historyRecordLockRound: icon {{ "voice_lock/input_round_s", historyToDownFg }};
|
||||
historyRecordLockRippleMargin: margins(6px, 6px, 6px, 6px);
|
||||
|
||||
historyRecordDelete: IconButton(historyAttach) {
|
||||
@@ -1274,6 +1278,8 @@ historySend: SendButton {
|
||||
}
|
||||
record: historyRecordVoice;
|
||||
recordOver: historyRecordVoiceOver;
|
||||
round: historyRecordRound;
|
||||
roundOver: historyRecordRoundOver;
|
||||
sendDisabledFg: historyComposeIconFg;
|
||||
}
|
||||
|
||||
@@ -1287,9 +1293,7 @@ defaultComposeFilesMenu: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(1px, 6px);
|
||||
rippleAreaSize: 42px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
defaultComposeFilesField: InputField(defaultInputField) {
|
||||
textMargins: margins(1px, 26px, 31px, 4px);
|
||||
@@ -1349,9 +1353,7 @@ moreChatsBarClose: IconButton(defaultIconButton) {
|
||||
|
||||
rippleAreaPosition: point(0px, 4px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
|
||||
reportReasonTopSkip: 8px;
|
||||
@@ -1517,3 +1519,22 @@ pickLocationChooseOnMap: RoundButton(defaultActiveButton) {
|
||||
sendGifBox: Box(defaultBox) {
|
||||
shadowIgnoreBottomSkip: true;
|
||||
}
|
||||
|
||||
processingVideoTipMaxWidth: 364px;
|
||||
processingVideoTipShift: 8px;
|
||||
processingVideoToast: Toast(defaultToast) {
|
||||
minWidth: 32px;
|
||||
maxWidth: 380px;
|
||||
padding: margins(19px, 17px, 19px, 17px);
|
||||
}
|
||||
processingVideoPreviewSkip: 8px;
|
||||
processingVideoView: RoundButton(defaultActiveButton) {
|
||||
width: -24px;
|
||||
height: 52px;
|
||||
textTop: 17px;
|
||||
textFg: mediaviewTextLinkFg;
|
||||
textFgOver: mediaviewTextLinkFg;
|
||||
textBg: transparent;
|
||||
textBgOver: transparent;
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Core {
|
||||
namespace {
|
||||
|
||||
constexpr auto kInitialVideoQuality = 480; // Start with SD.
|
||||
|
||||
[[nodiscard]] WindowPosition Deserialize(const QByteArray &data) {
|
||||
QDataStream stream(data);
|
||||
stream.setVersion(QDataStream::Qt_5_1);
|
||||
@@ -88,6 +90,21 @@ void LogPosition(const WindowPosition &position, const QString &name) {
|
||||
return RecentEmojiDocument{ id, (test == '1') };
|
||||
}
|
||||
|
||||
[[nodiscard]] quint32 SerializeVideoQuality(Media::VideoQuality quality) {
|
||||
static_assert(sizeof(Media::VideoQuality) == sizeof(uint32));
|
||||
auto result = uint32();
|
||||
const auto data = static_cast<const void*>(&quality);
|
||||
memcpy(&result, data, sizeof(quality));
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Media::VideoQuality DeserializeVideoQuality(quint32 value) {
|
||||
auto result = Media::VideoQuality();
|
||||
const auto data = static_cast<void*>(&result);
|
||||
memcpy(data, &value, sizeof(result));
|
||||
return (result.height <= 4320) ? result : Media::VideoQuality();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] WindowPosition AdjustToScale(
|
||||
@@ -124,7 +141,8 @@ Settings::Settings()
|
||||
, _floatPlayerColumn(Window::Column::Second)
|
||||
, _floatPlayerCorner(RectPart::TopRight)
|
||||
, _dialogsWithChatWidthRatio(DefaultDialogsWidthRatio())
|
||||
, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio()) {
|
||||
, _dialogsNoChatWidthRatio(DefaultDialogsWidthRatio())
|
||||
, _videoQuality({ .height = kInitialVideoQuality }) {
|
||||
}
|
||||
|
||||
Settings::~Settings() = default;
|
||||
@@ -222,7 +240,7 @@ QByteArray Settings::serialize() const {
|
||||
+ Serialize::stringSize(_customFontFamily)
|
||||
+ sizeof(qint32) * 3
|
||||
+ Serialize::bytearraySize(_tonsiteStorageToken)
|
||||
+ sizeof(qint32);
|
||||
+ sizeof(qint32) * 5;
|
||||
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
@@ -377,7 +395,11 @@ QByteArray Settings::serialize() const {
|
||||
<< qint32(_systemUnlockEnabled ? 1 : 0)
|
||||
<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
|
||||
<< _tonsiteStorageToken
|
||||
<< qint32(_includeMutedCounterFolders ? 1 : 0);
|
||||
<< qint32(_includeMutedCounterFolders ? 1 : 0)
|
||||
<< qint32(_ivZoom.current())
|
||||
<< qint32(_skipToastsInFocus ? 1 : 0)
|
||||
<< qint32(_recordVideoMessages ? 1 : 0)
|
||||
<< SerializeVideoQuality(_videoQuality);
|
||||
}
|
||||
|
||||
Ensures(result.size() == size);
|
||||
@@ -501,6 +523,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
|
||||
qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
|
||||
QByteArray tonsiteStorageToken = _tonsiteStorageToken;
|
||||
qint32 ivZoom = _ivZoom.current();
|
||||
qint32 skipToastsInFocus = _skipToastsInFocus ? 1 : 0;
|
||||
qint32 recordVideoMessages = _recordVideoMessages ? 1 : 0;
|
||||
quint32 videoQuality = SerializeVideoQuality(_videoQuality);
|
||||
|
||||
stream >> themesAccentColors;
|
||||
if (!stream.atEnd()) {
|
||||
@@ -810,6 +836,18 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
if (!stream.atEnd()) {
|
||||
stream >> includeMutedCounterFolders;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> ivZoom;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> skipToastsInFocus;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> recordVideoMessages;
|
||||
}
|
||||
if (!stream.atEnd()) {
|
||||
stream >> videoQuality;
|
||||
}
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
LOG(("App Error: "
|
||||
"Bad data for Core::Settings::constructFromSerialized()"));
|
||||
@@ -1021,6 +1059,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
|
||||
? std::optional<bool>()
|
||||
: (weatherInCelsius == 1);
|
||||
_tonsiteStorageToken = tonsiteStorageToken;
|
||||
_ivZoom = ivZoom;
|
||||
_skipToastsInFocus = (skipToastsInFocus == 1);
|
||||
_recordVideoMessages = (recordVideoMessages == 1);
|
||||
_videoQuality = DeserializeVideoQuality(videoQuality);
|
||||
}
|
||||
|
||||
QString Settings::getSoundPath(const QString &key) const {
|
||||
@@ -1347,6 +1389,7 @@ void Settings::resetOnLastLogout() {
|
||||
_flashBounceNotify = true;
|
||||
_notifyView = NotifyView::ShowPreview;
|
||||
//_nativeNotifications = std::nullopt;
|
||||
//_skipToastsInFocus = false;
|
||||
//_notificationsCount = 3;
|
||||
//_notificationsCorner = ScreenCorner::BottomRight;
|
||||
_includeMutedCounter = true;
|
||||
@@ -1408,6 +1451,9 @@ void Settings::resetOnLastLogout() {
|
||||
_hiddenGroupCallTooltips = 0;
|
||||
_storiesClickTooltipHidden = false;
|
||||
_ttlVoiceClickTooltipHidden = false;
|
||||
_ivZoom = 100;
|
||||
_recordVideoMessages = false;
|
||||
_videoQuality = {};
|
||||
|
||||
_recentEmojiPreload.clear();
|
||||
_recentEmoji.clear();
|
||||
@@ -1465,6 +1511,14 @@ void Settings::setNativeNotifications(bool value) {
|
||||
: std::make_optional(value);
|
||||
}
|
||||
|
||||
bool Settings::skipToastsInFocus() const {
|
||||
return _skipToastsInFocus;
|
||||
}
|
||||
|
||||
void Settings::setSkipToastsInFocus(bool value) {
|
||||
_skipToastsInFocus = value;
|
||||
}
|
||||
|
||||
void Settings::setTranslateButtonEnabled(bool value) {
|
||||
_translateButtonEnabled = value;
|
||||
}
|
||||
@@ -1543,8 +1597,36 @@ auto Settings::skipTranslationLanguagesValue() const
|
||||
void Settings::setRememberedDeleteMessageOnlyForYou(bool value) {
|
||||
_rememberedDeleteMessageOnlyForYou = value;
|
||||
}
|
||||
|
||||
bool Settings::rememberedDeleteMessageOnlyForYou() const {
|
||||
return _rememberedDeleteMessageOnlyForYou;
|
||||
}
|
||||
|
||||
int Settings::ivZoom() const {
|
||||
return _ivZoom.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> Settings::ivZoomValue() const {
|
||||
return _ivZoom.value();
|
||||
}
|
||||
|
||||
void Settings::setIvZoom(int value) {
|
||||
#ifdef Q_OS_WIN
|
||||
constexpr auto kMin = 25;
|
||||
constexpr auto kMax = 500;
|
||||
#else
|
||||
constexpr auto kMin = 30;
|
||||
constexpr auto kMax = 200;
|
||||
#endif
|
||||
_ivZoom = std::clamp(value, kMin, kMax);
|
||||
}
|
||||
|
||||
Media::VideoQuality Settings::videoQuality() const {
|
||||
return _videoQuality;
|
||||
}
|
||||
|
||||
void Settings::setVideoQuality(Media::VideoQuality value) {
|
||||
_videoQuality = value;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -220,6 +220,9 @@ public:
|
||||
[[nodiscard]] bool nativeNotifications() const;
|
||||
void setNativeNotifications(bool value);
|
||||
|
||||
[[nodiscard]] bool skipToastsInFocus() const;
|
||||
void setSkipToastsInFocus(bool value);
|
||||
|
||||
[[nodiscard]] int notificationsCount() const {
|
||||
return _notificationsCount;
|
||||
}
|
||||
@@ -625,6 +628,13 @@ public:
|
||||
return _floatPlayerCorner;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool recordVideoMessages() const {
|
||||
return _recordVideoMessages;
|
||||
}
|
||||
void setRecordVideoMessages(bool value) {
|
||||
_recordVideoMessages = value;
|
||||
}
|
||||
|
||||
void updateDialogsWidthRatio(float64 ratio, bool nochat);
|
||||
[[nodiscard]] float64 dialogsWidthRatio(bool nochat) const;
|
||||
|
||||
@@ -915,6 +925,13 @@ public:
|
||||
_tonsiteStorageToken = value;
|
||||
}
|
||||
|
||||
[[nodiscard]] int ivZoom() const;
|
||||
[[nodiscard]] rpl::producer<int> ivZoomValue() const;
|
||||
void setIvZoom(int value);
|
||||
|
||||
[[nodiscard]] Media::VideoQuality videoQuality() const;
|
||||
void setVideoQuality(Media::VideoQuality quality);
|
||||
|
||||
[[nodiscard]] static bool ThirdColumnByDefault();
|
||||
[[nodiscard]] static float64 DefaultDialogsWidthRatio();
|
||||
|
||||
@@ -954,6 +971,7 @@ private:
|
||||
bool _flashBounceNotify = true;
|
||||
NotifyView _notifyView = NotifyView::ShowPreview;
|
||||
std::optional<bool> _nativeNotifications;
|
||||
bool _skipToastsInFocus = false;
|
||||
int _notificationsCount = 3;
|
||||
ScreenCorner _notificationsCorner = ScreenCorner::BottomRight;
|
||||
bool _includeMutedCounter = true;
|
||||
@@ -1050,6 +1068,8 @@ private:
|
||||
bool _systemUnlockEnabled = false;
|
||||
std::optional<bool> _weatherInCelsius;
|
||||
QByteArray _tonsiteStorageToken;
|
||||
rpl::variable<int> _ivZoom = 100;
|
||||
Media::VideoQuality _videoQuality;
|
||||
|
||||
bool _tabbedReplacedWithInfo = false; // per-window
|
||||
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
|
||||
@@ -1060,6 +1080,8 @@ private:
|
||||
bool _rememberedFlashBounceNotifyFromTray = false;
|
||||
bool _dialogsWidthSetToZeroWithoutChat = false;
|
||||
|
||||
bool _recordVideoMessages = false;
|
||||
|
||||
QByteArray _photoEditorBrush;
|
||||
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 5006002;
|
||||
constexpr auto AppVersionStr = "5.6.2";
|
||||
constexpr auto AppVersion = 5007000;
|
||||
constexpr auto AppVersionStr = "5.7";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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([=](
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -222,9 +222,7 @@ dialogsMenuToggle: IconButton {
|
||||
|
||||
rippleAreaPosition: point(0px, 0px);
|
||||
rippleAreaSize: 40px;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: windowBgOver;
|
||||
}
|
||||
ripple: defaultRippleAnimationBgOver;
|
||||
}
|
||||
dialogsMenuToggleUnread: icon {
|
||||
{ "dialogs/dialogs_menu_unread", dialogsMenuIconFg },
|
||||
@@ -745,3 +743,7 @@ dialogsSearchTagPromoLeft: 6px;
|
||||
dialogsSearchTagPromoRight: 1px;
|
||||
dialogsSearchTagPromoSkip: 6px;
|
||||
|
||||
dialogsPopularAppsPadding: margins(10px, 8px, 10px, 12px);
|
||||
dialogsPopularAppsAbout: FlatLabel(boxDividerLabel) {
|
||||
minWidth: 128px;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ namespace {
|
||||
|
||||
constexpr auto kCollapsedChannelsCount = 5;
|
||||
constexpr auto kProbablyMaxChannels = 1000;
|
||||
constexpr auto kProbablyMaxRecommendations = 100;
|
||||
constexpr auto kCollapsedAppsCount = 5;
|
||||
constexpr auto kProbablyMaxApps = 100;
|
||||
|
||||
@@ -1163,8 +1162,22 @@ void PopularAppsController::fill() {
|
||||
appendRow(bot);
|
||||
}
|
||||
}
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
setCount(count);
|
||||
if (count > 0) {
|
||||
delegate()->peerListSetBelowWidget(object_ptr<Ui::DividerLabel>(
|
||||
(QWidget*)nullptr,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
(QWidget*)nullptr,
|
||||
tr::lng_bot_apps_which(
|
||||
lt_link,
|
||||
tr::lng_bot_apps_which_link(
|
||||
) | Ui::Text::ToLink(u"internal:about_popular_apps"_q),
|
||||
Ui::Text::WithEntities),
|
||||
st::dialogsPopularAppsAbout),
|
||||
st::dialogsPopularAppsPadding));
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
setCount(delegate()->peerListFullRowsCount());
|
||||
}
|
||||
|
||||
void PopularAppsController::appendRow(not_null<UserData*> bot) {
|
||||
@@ -2326,4 +2339,21 @@ object_ptr<Ui::BoxContent> StarsExamplesBox(
|
||||
return Box<PeerListBox>(std::move(controller), std::move(initBox));
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> PopularAppsAboutBox(
|
||||
not_null<Window::SessionController*> window) {
|
||||
return Ui::MakeInformBox({
|
||||
.text = tr::lng_popular_apps_info_text(
|
||||
lt_bot,
|
||||
rpl::single(Ui::Text::Link(
|
||||
u"@botfather"_q,
|
||||
u"https://t.me/botfather"_q)),
|
||||
lt_link,
|
||||
tr::lng_popular_apps_info_here(
|
||||
) | Ui::Text::ToLink(tr::lng_popular_apps_info_url(tr::now)),
|
||||
Ui::Text::RichLangValue),
|
||||
.confirmText = tr::lng_popular_apps_info_confirm(),
|
||||
.title = tr::lng_popular_apps_info_title(),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -284,10 +284,12 @@ FormatPointer MakeFormatPointer(
|
||||
return {};
|
||||
}
|
||||
result->pb = io.get();
|
||||
result->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
|
||||
auto options = (AVDictionary*)nullptr;
|
||||
const auto guard = gsl::finally([&] { av_dict_free(&options); });
|
||||
av_dict_set(&options, "usetoc", "1", 0);
|
||||
|
||||
const auto error = AvErrorWrap(avformat_open_input(
|
||||
&result,
|
||||
nullptr,
|
||||
@@ -307,6 +309,54 @@ FormatPointer MakeFormatPointer(
|
||||
return FormatPointer(result);
|
||||
}
|
||||
|
||||
FormatPointer MakeWriteFormatPointer(
|
||||
void *opaque,
|
||||
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#if DA_FFMPEG_CONST_WRITE_CALLBACK
|
||||
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
|
||||
#else
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence),
|
||||
const QByteArray &format) {
|
||||
const AVOutputFormat *found = nullptr;
|
||||
void *i = nullptr;
|
||||
while ((found = av_muxer_iterate(&i))) {
|
||||
if (found->name == format) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LogError(
|
||||
"av_muxer_iterate",
|
||||
u"Format %1 not found"_q.arg(QString::fromUtf8(format)));
|
||||
return {};
|
||||
}
|
||||
|
||||
auto io = MakeIOPointer(opaque, read, write, seek);
|
||||
if (!io) {
|
||||
return {};
|
||||
}
|
||||
io->seekable = (seek != nullptr);
|
||||
|
||||
auto result = (AVFormatContext*)nullptr;
|
||||
auto error = AvErrorWrap(avformat_alloc_output_context2(
|
||||
&result,
|
||||
(AVOutputFormat*)found,
|
||||
nullptr,
|
||||
nullptr));
|
||||
if (!result || error) {
|
||||
LogError("avformat_alloc_output_context2", error);
|
||||
return {};
|
||||
}
|
||||
result->pb = io.get();
|
||||
result->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
|
||||
// Now FormatPointer will own and free the IO context.
|
||||
io.release();
|
||||
return FormatPointer(result);
|
||||
}
|
||||
|
||||
void FormatDeleter::operator()(AVFormatContext *value) {
|
||||
if (value) {
|
||||
const auto deleter = IOPointer(value->pb);
|
||||
@@ -448,21 +498,134 @@ SwscalePointer MakeSwscalePointer(
|
||||
existing);
|
||||
}
|
||||
|
||||
void SwresampleDeleter::operator()(SwrContext *value) {
|
||||
if (value) {
|
||||
swr_free(&value);
|
||||
}
|
||||
}
|
||||
|
||||
SwresamplePointer MakeSwresamplePointer(
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *srcLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t srcLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat srcFormat,
|
||||
int srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *dstLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t dstLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat dstFormat,
|
||||
int dstRate,
|
||||
SwresamplePointer *existing) {
|
||||
// We have to use custom caching for SwsContext, because
|
||||
// sws_getCachedContext checks passed flags with existing context flags,
|
||||
// and re-creates context if they're different, but in the process of
|
||||
// context creation the passed flags are modified before being written
|
||||
// to the resulting context, so the caching doesn't work.
|
||||
if (existing && (*existing) != nullptr) {
|
||||
const auto &deleter = existing->get_deleter();
|
||||
if (true
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& srcLayout->nb_channels == deleter.srcChannels
|
||||
&& dstLayout->nb_channels == deleter.dstChannels
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& (av_get_channel_layout_nb_channels(srcLayout)
|
||||
== deleter.srcChannels)
|
||||
&& (av_get_channel_layout_nb_channels(dstLayout)
|
||||
== deleter.dstChannels)
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
&& srcFormat == deleter.srcFormat
|
||||
&& dstFormat == deleter.dstFormat
|
||||
&& srcRate == deleter.srcRate
|
||||
&& dstRate == deleter.dstRate) {
|
||||
return std::move(*existing);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize audio resampler
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
auto result = (SwrContext*)nullptr;
|
||||
auto error = AvErrorWrap(swr_alloc_set_opts2(
|
||||
&result,
|
||||
dstLayout,
|
||||
dstFormat,
|
||||
dstRate,
|
||||
srcLayout,
|
||||
srcFormat,
|
||||
srcRate,
|
||||
0,
|
||||
nullptr));
|
||||
if (error || !result) {
|
||||
LogError(u"swr_alloc_set_opts2"_q, error);
|
||||
return SwresamplePointer();
|
||||
}
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
auto result = swr_alloc_set_opts(
|
||||
existing ? existing.get() : nullptr,
|
||||
dstLayout,
|
||||
dstFormat,
|
||||
dstRate,
|
||||
srcLayout,
|
||||
srcFormat,
|
||||
srcRate,
|
||||
0,
|
||||
nullptr);
|
||||
if (!result) {
|
||||
LogError(u"swr_alloc_set_opts"_q);
|
||||
}
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
|
||||
error = AvErrorWrap(swr_init(result));
|
||||
if (error) {
|
||||
LogError(u"swr_init"_q, error);
|
||||
swr_free(&result);
|
||||
return SwresamplePointer();
|
||||
}
|
||||
|
||||
return SwresamplePointer(
|
||||
result,
|
||||
{
|
||||
srcFormat,
|
||||
srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
srcLayout->nb_channels,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
av_get_channel_layout_nb_channels(srcLayout),
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
dstFormat,
|
||||
dstRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
dstLayout->nb_channels,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
av_get_channel_layout_nb_channels(dstLayout),
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
});
|
||||
}
|
||||
|
||||
void SwscaleDeleter::operator()(SwsContext *value) {
|
||||
if (value) {
|
||||
sws_freeContext(value);
|
||||
}
|
||||
}
|
||||
|
||||
void LogError(const QString &method) {
|
||||
LOG(("Streaming Error: Error in %1.").arg(method));
|
||||
void LogError(const QString &method, const QString &details) {
|
||||
LOG(("Streaming Error: Error in %1%2."
|
||||
).arg(method
|
||||
).arg(details.isEmpty() ? QString() : " - " + details));
|
||||
}
|
||||
|
||||
void LogError(const QString &method, AvErrorWrap error) {
|
||||
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)."
|
||||
void LogError(
|
||||
const QString &method,
|
||||
AvErrorWrap error,
|
||||
const QString &details) {
|
||||
LOG(("Streaming Error: Error in %1 (code: %2, text: %3)%4."
|
||||
).arg(method
|
||||
).arg(error.code()
|
||||
).arg(error.text()));
|
||||
).arg(error.text()
|
||||
).arg(details.isEmpty() ? QString() : " - " + details));
|
||||
}
|
||||
|
||||
crl::time PtsToTime(int64_t pts, AVRational timeBase) {
|
||||
|
||||
@@ -19,6 +19,8 @@ extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/version.h>
|
||||
} // extern "C"
|
||||
|
||||
@@ -138,6 +140,16 @@ using FormatPointer = std::unique_ptr<AVFormatContext, FormatDeleter>;
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence));
|
||||
[[nodiscard]] FormatPointer MakeWriteFormatPointer(
|
||||
void *opaque,
|
||||
int(*read)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#if DA_FFMPEG_CONST_WRITE_CALLBACK
|
||||
int(*write)(void *opaque, const uint8_t *buffer, int bufferSize),
|
||||
#else
|
||||
int(*write)(void *opaque, uint8_t *buffer, int bufferSize),
|
||||
#endif
|
||||
int64_t(*seek)(void *opaque, int64_t offset, int whence),
|
||||
const QByteArray &format);
|
||||
|
||||
struct CodecDeleter {
|
||||
void operator()(AVCodecContext *value);
|
||||
@@ -179,8 +191,39 @@ using SwscalePointer = std::unique_ptr<SwsContext, SwscaleDeleter>;
|
||||
QSize resize,
|
||||
SwscalePointer *existing = nullptr);
|
||||
|
||||
void LogError(const QString &method);
|
||||
void LogError(const QString &method, FFmpeg::AvErrorWrap error);
|
||||
struct SwresampleDeleter {
|
||||
AVSampleFormat srcFormat = AV_SAMPLE_FMT_NONE;
|
||||
int srcRate = 0;
|
||||
int srcChannels = 0;
|
||||
AVSampleFormat dstFormat = AV_SAMPLE_FMT_NONE;
|
||||
int dstRate = 0;
|
||||
int dstChannels = 0;
|
||||
|
||||
void operator()(SwrContext *value);
|
||||
};
|
||||
using SwresamplePointer = std::unique_ptr<SwrContext, SwresampleDeleter>;
|
||||
[[nodiscard]] SwresamplePointer MakeSwresamplePointer(
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *srcLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t srcLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat srcFormat,
|
||||
int srcRate,
|
||||
#if DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVChannelLayout *dstLayout,
|
||||
#else // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
uint64_t dstLayout,
|
||||
#endif // DA_FFMPEG_NEW_CHANNEL_LAYOUT
|
||||
AVSampleFormat dstFormat,
|
||||
int dstRate,
|
||||
SwresamplePointer *existing = nullptr);
|
||||
|
||||
void LogError(const QString &method, const QString &details = {});
|
||||
void LogError(
|
||||
const QString &method,
|
||||
FFmpeg::AvErrorWrap error,
|
||||
const QString &details = {});
|
||||
|
||||
[[nodiscard]] const AVCodec *FindDecoder(not_null<AVCodecContext*> context);
|
||||
[[nodiscard]] crl::time PtsToTime(int64_t pts, AVRational timeBase);
|
||||
|
||||
@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "core/click_handler_types.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "history/view/controls/history_view_forward_panel.h"
|
||||
#include "api/api_report.h"
|
||||
#include "history/view/controls/history_view_draft_options.h"
|
||||
#include "boxes/moderate_messages_box.h"
|
||||
#include "history/view/media/history_view_sticker.h"
|
||||
@@ -36,12 +35,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/path_shift_gradient.h"
|
||||
#include "ui/effects/message_sending_animation_controller.h"
|
||||
#include "ui/effects/reaction_fly_animation.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_isolated_emoji.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/boxes/edit_factcheck_box.h"
|
||||
#include "ui/boxes/report_box_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/controls/delete_message_context_action.h"
|
||||
#include "ui/inactive_press.h"
|
||||
#include "ui/painter.h"
|
||||
@@ -57,7 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/delete_messages_box.h"
|
||||
#include "boxes/report_messages_box.h"
|
||||
#include "boxes/sticker_set_box.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "boxes/translate_box.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/emoji_interactions.h"
|
||||
@@ -72,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session_settings.h"
|
||||
#include "mainwidget.h"
|
||||
#include "menu/menu_item_download_files.h"
|
||||
#include "menu/menu_sponsored.h"
|
||||
#include "core/application.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_attached_stickers.h"
|
||||
@@ -135,60 +131,6 @@ int BinarySearchBlocksOrItems(const T &list, int edge) {
|
||||
&& (!peer->isChannel() || peer->asChannel()->amIn()));
|
||||
}
|
||||
|
||||
void FillSponsoredMessagesMenu(
|
||||
not_null<Window::SessionController*> controller,
|
||||
FullMsgId itemId,
|
||||
not_null<Ui::PopupMenu*> menu) {
|
||||
const auto &data = controller->session().sponsoredMessages();
|
||||
const auto info = data.lookupDetails(itemId).info;
|
||||
const auto show = controller->uiShow();
|
||||
if (!info.empty()) {
|
||||
auto fillSubmenu = [&](not_null<Ui::PopupMenu*> menu) {
|
||||
const auto allText = ranges::accumulate(
|
||||
info,
|
||||
TextWithEntities(),
|
||||
[](TextWithEntities a, TextWithEntities b) {
|
||||
return a.text.isEmpty() ? b : a.append('\n').append(b);
|
||||
}).text;
|
||||
const auto callback = [=] {
|
||||
QGuiApplication::clipboard()->setText(allText);
|
||||
show->showToast(tr::lng_text_copied(tr::now));
|
||||
};
|
||||
for (const auto &i : info) {
|
||||
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
|
||||
menu,
|
||||
st::defaultMenu,
|
||||
st::historySponsorInfoItem,
|
||||
st::historyHasCustomEmojiPosition,
|
||||
base::duplicate(i));
|
||||
item->clicks(
|
||||
) | rpl::start_with_next(callback, menu->lifetime());
|
||||
menu->addAction(std::move(item));
|
||||
if (i != info.back()) {
|
||||
menu->addSeparator();
|
||||
}
|
||||
}
|
||||
};
|
||||
using namespace Ui::Menu;
|
||||
CreateAddActionCallback(menu)(MenuCallback::Args{
|
||||
.text = tr::lng_sponsored_info_menu(tr::now),
|
||||
.handler = nullptr,
|
||||
.icon = &st::menuIconChannel,
|
||||
.fillSubmenu = std::move(fillSubmenu),
|
||||
});
|
||||
menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
menu->addAction(tr::lng_sponsored_hide_ads(tr::now), [=] {
|
||||
if (controller->session().premium()) {
|
||||
using Result = Data::SponsoredReportResult;
|
||||
controller->session().sponsoredMessages().createReportCallback(
|
||||
itemId)(Result::Id("-1"), [](const auto &) {});
|
||||
} else {
|
||||
ShowPremiumPreviewBox(controller, PremiumFeature::NoAds);
|
||||
}
|
||||
}, &st::menuIconCancel);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
|
||||
@@ -2252,22 +2194,22 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}
|
||||
return item;
|
||||
};
|
||||
const auto whoReactedItem = groupLeaderOrSelf(_dragStateItem);
|
||||
const auto hasWhoReactedItem = whoReactedItem
|
||||
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::All);
|
||||
const auto leaderOrSelf = groupLeaderOrSelf(_dragStateItem);
|
||||
const auto hasWhoReactedItem = leaderOrSelf
|
||||
&& Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::All);
|
||||
const auto clickedReaction = link
|
||||
? link->property(
|
||||
kReactionsCountEmojiProperty).value<Data::ReactionId>()
|
||||
: Data::ReactionId();
|
||||
_whoReactedMenuLifetime.destroy();
|
||||
if (!clickedReaction.empty()
|
||||
&& whoReactedItem
|
||||
&& Api::WhoReactedExists(whoReactedItem, Api::WhoReactedList::One)) {
|
||||
&& leaderOrSelf
|
||||
&& Api::WhoReactedExists(leaderOrSelf, Api::WhoReactedList::One)) {
|
||||
HistoryView::ShowWhoReactedMenu(
|
||||
&_menu,
|
||||
e->globalPos(),
|
||||
this,
|
||||
whoReactedItem,
|
||||
leaderOrSelf,
|
||||
clickedReaction,
|
||||
_controller,
|
||||
_whoReactedMenuLifetime);
|
||||
@@ -2751,8 +2693,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
? link->copyToClipboardContextItemText()
|
||||
: QString();
|
||||
|
||||
if (item && item->isSponsored()) {
|
||||
FillSponsoredMessagesMenu(controller, item->fullId(), _menu);
|
||||
const auto sponsored = (item && item->isSponsored())
|
||||
? item
|
||||
: (Element::Moused() && Element::Moused()->data()->isSponsored())
|
||||
? Element::Moused()->data().get()
|
||||
: nullptr;
|
||||
if (sponsored) {
|
||||
Menu::FillSponsored(
|
||||
this,
|
||||
Ui::Menu::CreateAddActionCallback(_menu),
|
||||
controller->uiShow(),
|
||||
sponsored->fullId(),
|
||||
false);
|
||||
}
|
||||
if (isUponSelected > 0) {
|
||||
addReplyAction(item);
|
||||
@@ -2848,7 +2800,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!actionText.isEmpty() && !hasCopyRestriction(item)) {
|
||||
if (!actionText.isEmpty()) {
|
||||
_menu->addAction(
|
||||
actionText,
|
||||
[text = link->copyToClipboardText()] {
|
||||
@@ -2860,22 +2812,30 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
HistoryView::CopyPostLink(controller, itemId, HistoryView::Context::History);
|
||||
}, &st::menuIconLink);
|
||||
}
|
||||
if (item && item->isSponsored()) {
|
||||
if (!_menu->empty()) {
|
||||
_menu->addSeparator(&st::expandedMenuSeparator);
|
||||
if (sponsored) {
|
||||
const auto hasAbout = ranges::any_of(
|
||||
_menu->actions(),
|
||||
[about = tr::lng_sponsored_menu_revenued_about(tr::now)](
|
||||
const QAction *action) {
|
||||
return action->text() == about;
|
||||
});
|
||||
if (!hasAbout) {
|
||||
if (!_menu->empty()) {
|
||||
_menu->addSeparator(&st::expandedMenuSeparator);
|
||||
}
|
||||
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
|
||||
_menu,
|
||||
st::menuWithIcons,
|
||||
st::historyHasCustomEmoji,
|
||||
st::historySponsoredAboutMenuLabelPosition,
|
||||
TextWithEntities{ tr::lng_sponsored_title(tr::now) },
|
||||
&st::menuIconInfo);
|
||||
item->clicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
controller->show(Box(Ui::AboutSponsoredBox));
|
||||
}, item->lifetime());
|
||||
_menu->addAction(std::move(item));
|
||||
}
|
||||
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
|
||||
_menu,
|
||||
st::menuWithIcons,
|
||||
st::historyHasCustomEmoji,
|
||||
st::historySponsoredAboutMenuLabelPosition,
|
||||
TextWithEntities{ tr::lng_sponsored_title(tr::now) },
|
||||
&st::menuIconInfo);
|
||||
item->clicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
controller->show(Box(Ui::AboutSponsoredBox));
|
||||
}, item->lifetime());
|
||||
_menu->addAction(std::move(item));
|
||||
}
|
||||
if (isUponSelected > 1) {
|
||||
if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
|
||||
@@ -2935,18 +2895,29 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
if (_dragStateItem) {
|
||||
const auto view = viewByItem(_dragStateItem);
|
||||
const auto textItem = view ? view->textItem() : _dragStateItem;
|
||||
const auto wasAmount = _menu->actions().size();
|
||||
HistoryView::AddEmojiPacksAction(
|
||||
_menu,
|
||||
textItem ? textItem : _dragStateItem,
|
||||
HistoryView::EmojiPacksSource::Message,
|
||||
_controller);
|
||||
const auto added = (_menu->actions().size() > wasAmount);
|
||||
if (!added && !_menu->empty()) {
|
||||
_menu->addSeparator();
|
||||
}
|
||||
HistoryView::AddSelectRestrictionAction(
|
||||
_menu,
|
||||
textItem ? textItem : _dragStateItem,
|
||||
!added);
|
||||
}
|
||||
if (hasWhoReactedItem) {
|
||||
HistoryView::AddWhoReactedAction(
|
||||
_menu,
|
||||
this,
|
||||
whoReactedItem,
|
||||
leaderOrSelf,
|
||||
_controller);
|
||||
} else if (leaderOrSelf) {
|
||||
HistoryView::MaybeAddWhenEditedAction(_menu, leaderOrSelf);
|
||||
}
|
||||
|
||||
if (_menu->empty()) {
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -150,6 +150,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "main/session/send_as_peers.h"
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_controller.h"
|
||||
@@ -558,6 +559,14 @@ HistoryWidget::HistoryWidget(
|
||||
Window::ActivateWindow(controller);
|
||||
});
|
||||
|
||||
Core::App().mediaDevices().recordAvailabilityValue(
|
||||
) | rpl::start_with_next([=](Webrtc::RecordAvailability value) {
|
||||
_recordAvailability = value;
|
||||
if (_list) {
|
||||
updateSendButtonType();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
session().data().newItemAdded(
|
||||
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
||||
newItemAdded(item);
|
||||
@@ -714,6 +723,35 @@ HistoryWidget::HistoryWidget(
|
||||
maybeMarkReactionsRead(update.item);
|
||||
}, lifetime());
|
||||
|
||||
session().data().sentToScheduled(
|
||||
) | rpl::start_with_next([=](const Data::SentToScheduled &value) {
|
||||
const auto history = value.history;
|
||||
if (history == _history) {
|
||||
const auto id = value.scheduledId;
|
||||
crl::on_main(this, [=] {
|
||||
if (history == _history) {
|
||||
controller->showSection(
|
||||
std::make_shared<HistoryView::ScheduledMemento>(
|
||||
history,
|
||||
id));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
session().data().sentFromScheduled(
|
||||
) | rpl::start_with_next([=](const Data::SentFromScheduled &value) {
|
||||
if (value.item->awaitingVideoProcessing()
|
||||
&& !_sentFromScheduledTip
|
||||
&& HistoryView::ShowScheduledVideoPublished(
|
||||
controller,
|
||||
value,
|
||||
crl::guard(this, [=] { _sentFromScheduledTip = false; }))) {
|
||||
_sentFromScheduledTip = true;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
using MediaSwitch = Media::Player::Instance::Switch;
|
||||
Media::Player::instance()->switchToNextEvents(
|
||||
) | rpl::filter([=](const MediaSwitch &pair) {
|
||||
@@ -1042,6 +1080,7 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
data.video,
|
||||
action);
|
||||
_voiceRecordBar->clearListenState();
|
||||
}, lifetime());
|
||||
@@ -1055,6 +1094,24 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
_cornerButtons.updateUnreadThingsVisibility();
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->errors(
|
||||
) | rpl::start_with_next([=](::Media::Capture::Error error) {
|
||||
using Error = ::Media::Capture::Error;
|
||||
switch (error) {
|
||||
case Error::AudioInit:
|
||||
case Error::AudioTimeout:
|
||||
controller()->showToast(tr::lng_record_audio_problem(tr::now));
|
||||
break;
|
||||
case Error::VideoInit:
|
||||
case Error::VideoTimeout:
|
||||
controller()->showToast(tr::lng_record_video_problem(tr::now));
|
||||
break;
|
||||
default:
|
||||
controller()->showToast(u"Unknown error."_q);
|
||||
break;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->updateSendButtonTypeRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateSendButtonType();
|
||||
@@ -1067,7 +1124,20 @@ void HistoryWidget::initVoiceRecordBar() {
|
||||
|
||||
_voiceRecordBar->recordingTipRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
controller()->showToast(tr::lng_record_hold_tip(tr::now));
|
||||
Core::App().settings().setRecordVideoMessages(
|
||||
!Core::App().settings().recordVideoMessages());
|
||||
updateSendButtonType();
|
||||
switch (_send->type()) {
|
||||
case Ui::SendButton::Type::Record: {
|
||||
const auto can = Webrtc::RecordAvailability::VideoAndAudio;
|
||||
controller()->showToast((_recordAvailability == can)
|
||||
? tr::lng_record_voice_tip(tr::now)
|
||||
: tr::lng_record_hold_tip(tr::now));
|
||||
} break;
|
||||
case Ui::SendButton::Type::Round:
|
||||
controller()->showToast(tr::lng_record_video_tip(tr::now));
|
||||
break;
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_voiceRecordBar->recordingStateChanges(
|
||||
@@ -1581,6 +1651,9 @@ void HistoryWidget::orderWidgets() {
|
||||
if (_translateBar) {
|
||||
_translateBar->raise();
|
||||
}
|
||||
if (_sponsoredMessageBar) {
|
||||
_sponsoredMessageBar->raise();
|
||||
}
|
||||
if (_pinnedBar) {
|
||||
_pinnedBar->raise();
|
||||
}
|
||||
@@ -2104,6 +2177,7 @@ void HistoryWidget::showHistory(
|
||||
MsgId showAtMsgId,
|
||||
const TextWithEntities &highlightPart,
|
||||
int highlightPartOffsetHint) {
|
||||
|
||||
_pinnedClickedId = FullMsgId();
|
||||
_minPinnedId = std::nullopt;
|
||||
_showAtMsgHighlightPart = {};
|
||||
@@ -2253,6 +2327,7 @@ void HistoryWidget::showHistory(
|
||||
_history->showAtMsgId = _showAtMsgId;
|
||||
|
||||
destroyUnreadBarOnClose();
|
||||
_sponsoredMessageBar = nullptr;
|
||||
_pinnedBar = nullptr;
|
||||
_translateBar = nullptr;
|
||||
_pinnedTracker = nullptr;
|
||||
@@ -2281,7 +2356,7 @@ void HistoryWidget::showHistory(
|
||||
_processingReplyItem = _replyEditMsg = nullptr;
|
||||
_processingReplyTo = _replyTo = FullReplyTo();
|
||||
_editMsgId = MsgId();
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
updateReplaceMediaButton();
|
||||
_fieldBarCancel->hide();
|
||||
@@ -2298,6 +2373,8 @@ void HistoryWidget::showHistory(
|
||||
_contactStatus = nullptr;
|
||||
_businessBotStatus = nullptr;
|
||||
|
||||
Core::App().mediaDevices().refreshRecordAvailability();
|
||||
|
||||
if (peerId) {
|
||||
using namespace HistoryView;
|
||||
_peer = session().data().peer(peerId);
|
||||
@@ -2472,12 +2549,9 @@ void HistoryWidget::showHistory(
|
||||
unreadCountUpdated(); // set _historyDown badge.
|
||||
showAboutTopPromotion();
|
||||
|
||||
{
|
||||
if (!session().sponsoredMessages().isTopBarFor(_history)) {
|
||||
_scroll->setTrackingContent(false);
|
||||
const auto checkState = crl::guard(this, [=, history = _history] {
|
||||
if (history != _history) {
|
||||
return;
|
||||
}
|
||||
const auto checkState = [=] {
|
||||
using State = Data::SponsoredMessages::State;
|
||||
const auto state = session().sponsoredMessages().state(
|
||||
_history);
|
||||
@@ -2487,9 +2561,17 @@ void HistoryWidget::showHistory(
|
||||
session().sponsoredMessages().canHaveFor(_history));
|
||||
} else if (state == State::InjectToMiddle) {
|
||||
injectSponsoredMessages();
|
||||
} else if (state == State::AppendToTopBar) {
|
||||
}
|
||||
});
|
||||
session().sponsoredMessages().request(_history, checkState);
|
||||
};
|
||||
const auto history = _history;
|
||||
session().sponsoredMessages().request(
|
||||
_history,
|
||||
crl::guard(this, [=, this] {
|
||||
if (history == _history) {
|
||||
checkState();
|
||||
}
|
||||
}));
|
||||
checkState();
|
||||
}
|
||||
} else {
|
||||
@@ -2667,7 +2749,7 @@ void HistoryWidget::setEditMsgId(MsgId msgId) {
|
||||
_editMsgId = msgId;
|
||||
if (!msgId) {
|
||||
_mediaEditManager.cancel();
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
if (_preview) {
|
||||
_preview->setDisabled(false);
|
||||
}
|
||||
@@ -2723,14 +2805,16 @@ void HistoryWidget::clearAllLoadRequests() {
|
||||
}
|
||||
|
||||
bool HistoryWidget::updateReplaceMediaButton() {
|
||||
if (!_canReplaceMedia) {
|
||||
if (!_canReplaceMedia && !_canAddMedia) {
|
||||
const auto result = (_replaceMedia != nullptr);
|
||||
_replaceMedia.destroy();
|
||||
return result;
|
||||
} else if (_replaceMedia) {
|
||||
return false;
|
||||
}
|
||||
_replaceMedia.create(this, st::historyReplaceMedia);
|
||||
_replaceMedia.create(
|
||||
this,
|
||||
_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);
|
||||
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
|
||||
_replaceMedia->setClickedCallback([=] {
|
||||
base::call_delayed(hideDuration, this, [=] {
|
||||
@@ -2896,6 +2980,9 @@ void HistoryWidget::updateControlsVisibility() {
|
||||
if (_pinnedBar) {
|
||||
_pinnedBar->show();
|
||||
}
|
||||
if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) {
|
||||
_sponsoredMessageBar->toggle(true, anim::type::normal);
|
||||
}
|
||||
if (_translateBar) {
|
||||
_translateBar->show();
|
||||
}
|
||||
@@ -4093,6 +4180,9 @@ void HistoryWidget::hideChildWidgets() {
|
||||
if (_pinnedBar) {
|
||||
_pinnedBar->hide();
|
||||
}
|
||||
if (_sponsoredMessageBar) {
|
||||
_sponsoredMessageBar->toggle(false, anim::type::instant);
|
||||
}
|
||||
if (_translateBar) {
|
||||
_translateBar->hide();
|
||||
}
|
||||
@@ -4253,7 +4343,11 @@ auto HistoryWidget::computeSendButtonType() const {
|
||||
} else if (_isInlineBot) {
|
||||
return Type::Cancel;
|
||||
} else if (showRecordButton()) {
|
||||
return Type::Record;
|
||||
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
|
||||
const auto video = Core::App().settings().recordVideoMessages();
|
||||
return (video && _recordAvailability == both)
|
||||
? Type::Round
|
||||
: Type::Record;
|
||||
}
|
||||
return Type::Send;
|
||||
}
|
||||
@@ -4426,6 +4520,7 @@ void HistoryWidget::showFinished() {
|
||||
_showAnimation = nullptr;
|
||||
doneShow();
|
||||
synteticScrollToY(_scroll->scrollTop());
|
||||
requestSponsoredMessageBar();
|
||||
}
|
||||
|
||||
void HistoryWidget::doneShow() {
|
||||
@@ -4446,6 +4541,10 @@ void HistoryWidget::doneShow() {
|
||||
if (_pinnedBar) {
|
||||
_pinnedBar->finishAnimating();
|
||||
}
|
||||
checkSponsoredMessageBar();
|
||||
if (_sponsoredMessageBar) {
|
||||
_sponsoredMessageBar->finishAnimating();
|
||||
}
|
||||
if (_translateBar) {
|
||||
_translateBar->finishAnimating();
|
||||
}
|
||||
@@ -4587,7 +4686,8 @@ void HistoryWidget::sendButtonClicked() {
|
||||
const auto type = _send->type();
|
||||
if (type == Ui::SendButton::Type::Cancel) {
|
||||
cancelInlineBot();
|
||||
} else if (type != Ui::SendButton::Type::Record) {
|
||||
} else if (type != Ui::SendButton::Type::Record
|
||||
&& type != Ui::SendButton::Type::Round) {
|
||||
send({});
|
||||
}
|
||||
}
|
||||
@@ -4877,7 +4977,7 @@ bool HistoryWidget::isSearching() const {
|
||||
}
|
||||
|
||||
bool HistoryWidget::showRecordButton() const {
|
||||
return Media::Capture::instance()->available()
|
||||
return (_recordAvailability != Webrtc::RecordAvailability::None)
|
||||
&& !_voiceRecordBar->isListenState()
|
||||
&& !_voiceRecordBar->isRecordingByAnotherBar()
|
||||
&& !HasSendText(_field)
|
||||
@@ -4908,7 +5008,9 @@ void HistoryWidget::updateSendButtonType() {
|
||||
}();
|
||||
_send->setSlowmodeDelay(delay);
|
||||
_send->setDisabled(disabledBySlowmode
|
||||
&& (type == Type::Send || type == Type::Record));
|
||||
&& (type == Type::Send
|
||||
|| type == Type::Record
|
||||
|| type == Type::Round));
|
||||
|
||||
if (delay != 0) {
|
||||
base::call_delayed(
|
||||
@@ -5664,7 +5766,7 @@ bool HistoryWidget::confirmSendingFiles(
|
||||
Ui::PreparedList &&list,
|
||||
const QString &insertTextOnCancel) {
|
||||
if (_editMsgId) {
|
||||
if (_canReplaceMedia) {
|
||||
if (_canReplaceMedia || _canAddMedia) {
|
||||
EditCaptionBox::StartMediaReplace(
|
||||
controller(),
|
||||
{ _history->peer->id, _editMsgId },
|
||||
@@ -5949,8 +6051,14 @@ void HistoryWidget::updateControlsGeometry() {
|
||||
_pinnedBar->move(0, pinnedBarTop);
|
||||
_pinnedBar->resizeToWidth(width());
|
||||
}
|
||||
const auto translateTop = pinnedBarTop
|
||||
const auto sponsoredMessageBarTop = pinnedBarTop
|
||||
+ (_pinnedBar ? _pinnedBar->height() : 0);
|
||||
if (_sponsoredMessageBar) {
|
||||
_sponsoredMessageBar->move(0, sponsoredMessageBarTop);
|
||||
_sponsoredMessageBar->resizeToWidth(width());
|
||||
}
|
||||
const auto translateTop = sponsoredMessageBarTop
|
||||
+ (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0);
|
||||
if (_translateBar) {
|
||||
_translateBar->move(0, translateTop);
|
||||
_translateBar->resizeToWidth(width());
|
||||
@@ -6195,6 +6303,9 @@ void HistoryWidget::updateHistoryGeometry(
|
||||
if (_translateBar) {
|
||||
newScrollHeight -= _translateBar->height();
|
||||
}
|
||||
if (_sponsoredMessageBar) {
|
||||
newScrollHeight -= _sponsoredMessageBar->height();
|
||||
}
|
||||
if (_pinnedBar) {
|
||||
newScrollHeight -= _pinnedBar->height();
|
||||
}
|
||||
@@ -6612,6 +6723,7 @@ int HistoryWidget::computeMaxFieldHeight() const {
|
||||
- _topBar->height()
|
||||
- (_contactStatus ? _contactStatus->bar().height() : 0)
|
||||
- (_businessBotStatus ? _businessBotStatus->bar().height() : 0)
|
||||
- (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0)
|
||||
- (_pinnedBar ? _pinnedBar->height() : 0)
|
||||
- (_groupCallBar ? _groupCallBar->height() : 0)
|
||||
- (_requestsBar ? _requestsBar->height() : 0)
|
||||
@@ -7500,6 +7612,116 @@ void HistoryWidget::requestMessageData(MsgId msgId) {
|
||||
session().api().requestMessageData(_peer, msgId, callback);
|
||||
}
|
||||
|
||||
bool HistoryWidget::checkSponsoredMessageBarVisibility() const {
|
||||
const auto h = _list->height()
|
||||
- (_kbScroll->isHidden() ? 0 : _kbScroll->height());
|
||||
return (h > _scroll->height());
|
||||
}
|
||||
|
||||
void HistoryWidget::requestSponsoredMessageBar() {
|
||||
if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {
|
||||
return;
|
||||
}
|
||||
const auto checkState = [=, this] {
|
||||
using State = Data::SponsoredMessages::State;
|
||||
const auto state = session().sponsoredMessages().state(
|
||||
_history);
|
||||
_sponsoredMessagesStateKnown = (state != State::None);
|
||||
if (state == State::AppendToTopBar) {
|
||||
createSponsoredMessageBar();
|
||||
if (checkSponsoredMessageBarVisibility()) {
|
||||
_sponsoredMessageBar->toggle(true, anim::type::normal);
|
||||
} else {
|
||||
auto &lifetime = _sponsoredMessageBar->lifetime();
|
||||
const auto heightLifetime
|
||||
= lifetime.make_state<rpl::lifetime>();
|
||||
_list->heightValue(
|
||||
) | rpl::start_with_next([=, this] {
|
||||
if (_sponsoredMessageBar->toggled()) {
|
||||
heightLifetime->destroy();
|
||||
} else if (checkSponsoredMessageBarVisibility()) {
|
||||
_sponsoredMessageBar->toggle(
|
||||
true,
|
||||
anim::type::normal);
|
||||
heightLifetime->destroy();
|
||||
}
|
||||
}, *heightLifetime);
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto history = _history;
|
||||
session().sponsoredMessages().request(
|
||||
_history,
|
||||
crl::guard(this, [=, this] {
|
||||
if (history == _history) {
|
||||
checkState();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void HistoryWidget::checkSponsoredMessageBar() {
|
||||
if (!_history || !session().sponsoredMessages().isTopBarFor(_history)) {
|
||||
return;
|
||||
}
|
||||
const auto state = session().sponsoredMessages().state(_history);
|
||||
if (state == Data::SponsoredMessages::State::AppendToTopBar) {
|
||||
if (checkSponsoredMessageBarVisibility()) {
|
||||
if (!_sponsoredMessageBar) {
|
||||
createSponsoredMessageBar();
|
||||
}
|
||||
_sponsoredMessageBar->toggle(true, anim::type::instant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::createSponsoredMessageBar() {
|
||||
_sponsoredMessageBar = base::make_unique_q<Ui::SlideWrap<>>(
|
||||
this,
|
||||
object_ptr<Ui::RpWidget>(this));
|
||||
|
||||
_sponsoredMessageBar->entity()->resizeToWidth(_scroll->width());
|
||||
const auto maybeFullId = session().sponsoredMessages().fillTopBar(
|
||||
_history,
|
||||
_sponsoredMessageBar->entity());
|
||||
session().sponsoredMessages().itemRemoved(
|
||||
maybeFullId
|
||||
) | rpl::start_with_next([this] {
|
||||
_sponsoredMessageBar->shownValue() | rpl::filter(
|
||||
!rpl::mappers::_1
|
||||
) | rpl::start_with_next([this] {
|
||||
_sponsoredMessageBar = nullptr;
|
||||
}, _sponsoredMessageBar->lifetime());
|
||||
_sponsoredMessageBar->toggle(false, anim::type::normal);
|
||||
}, _sponsoredMessageBar->lifetime());
|
||||
|
||||
if (maybeFullId) {
|
||||
const auto viewLifetime
|
||||
= _sponsoredMessageBar->lifetime().make_state<rpl::lifetime>();
|
||||
rpl::combine(
|
||||
_sponsoredMessageBar->entity()->heightValue(),
|
||||
_sponsoredMessageBar->heightValue()
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_1 == rpl::mappers::_2
|
||||
) | rpl::start_with_next([=] {
|
||||
session().sponsoredMessages().view(maybeFullId);
|
||||
viewLifetime->destroy();
|
||||
}, *viewLifetime);
|
||||
}
|
||||
|
||||
_sponsoredMessageBarHeight = 0;
|
||||
_sponsoredMessageBar->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
_topDelta = _preserveScrollTop
|
||||
? 0
|
||||
: (height - _sponsoredMessageBarHeight);
|
||||
_sponsoredMessageBarHeight = height;
|
||||
updateHistoryGeometry();
|
||||
updateControlsGeometry();
|
||||
_topDelta = 0;
|
||||
}, _sponsoredMessageBar->lifetime());
|
||||
_sponsoredMessageBar->toggle(false, anim::type::instant);
|
||||
}
|
||||
|
||||
bool HistoryWidget::sendExistingDocument(
|
||||
not_null<DocumentData*> document,
|
||||
Api::MessageToSend messageToSend,
|
||||
@@ -7954,7 +8176,7 @@ void HistoryWidget::cancelEdit() {
|
||||
return;
|
||||
}
|
||||
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
updateReplaceMediaButton();
|
||||
_replyEditMsg = nullptr;
|
||||
@@ -8309,7 +8531,16 @@ void HistoryWidget::updateReplyEditTexts(bool force) {
|
||||
if (_editMsgId && _replyEditMsg) {
|
||||
_mediaEditManager.start(_replyEditMsg);
|
||||
}
|
||||
_canReplaceMedia = editMedia && editMedia->allowsEditMedia();
|
||||
_canReplaceMedia = _editMsgId && _replyEditMsg->allowsEditMedia();
|
||||
if (editMedia) {
|
||||
_canAddMedia = false;
|
||||
} else {
|
||||
_canAddMedia = base::take(_canReplaceMedia);
|
||||
}
|
||||
if (_canReplaceMedia || _canAddMedia) {
|
||||
// Invalidate the button, maybe icon has changed.
|
||||
_replaceMedia.destroy();
|
||||
}
|
||||
_photoEditMedia = (_canReplaceMedia
|
||||
&& editMedia->photo()
|
||||
&& !editMedia->photo()->isNull())
|
||||
|
||||
@@ -73,12 +73,18 @@ class SpoilerAnimation;
|
||||
class ChooseThemeController;
|
||||
class ContinuousScroll;
|
||||
struct ChatPaintHighlight;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Emoji {
|
||||
class SuggestionsController;
|
||||
} // namespace Ui::Emoji
|
||||
|
||||
namespace Webrtc {
|
||||
enum class RecordAvailability : uchar;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
@@ -528,6 +534,11 @@ private:
|
||||
void setupGroupCallBar();
|
||||
void setupRequestsBar();
|
||||
|
||||
void checkSponsoredMessageBar();
|
||||
[[nodiscard]] bool checkSponsoredMessageBarVisibility() const;
|
||||
void requestSponsoredMessageBar();
|
||||
void createSponsoredMessageBar();
|
||||
|
||||
void sendInlineResult(InlineBots::ResultSelected result);
|
||||
|
||||
void drawField(Painter &p, const QRect &rect);
|
||||
@@ -661,6 +672,7 @@ private:
|
||||
MsgId _editMsgId = 0;
|
||||
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
|
||||
bool _canReplaceMedia = false;
|
||||
bool _canAddMedia = false;
|
||||
HistoryView::MediaEditManager _mediaEditManager;
|
||||
|
||||
HistoryItem *_replyEditMsg = nullptr;
|
||||
@@ -685,8 +697,12 @@ private:
|
||||
std::unique_ptr<Ui::RequestsBar> _requestsBar;
|
||||
int _requestsBarHeight = 0;
|
||||
|
||||
base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _sponsoredMessageBar;
|
||||
int _sponsoredMessageBarHeight = 0;
|
||||
|
||||
bool _preserveScrollTop = false;
|
||||
bool _repaintFieldScheduled = false;
|
||||
bool _sentFromScheduledTip = false;
|
||||
|
||||
mtpRequestId _saveEditMsgRequestId = 0;
|
||||
|
||||
@@ -746,6 +762,8 @@ private:
|
||||
mtpRequestId _inlineBotResolveRequestId = 0;
|
||||
bool _isInlineBot = false;
|
||||
|
||||
Webrtc::RecordAvailability _recordAvailability = {};
|
||||
|
||||
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
|
||||
std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ struct VoiceToSend {
|
||||
VoiceWaveform waveform;
|
||||
crl::time duration = 0;
|
||||
Api::SendOptions options;
|
||||
bool video = false;
|
||||
};
|
||||
struct SendActionUpdate {
|
||||
Api::SendProgressType type = Api::SendProgressType();
|
||||
|
||||
@@ -81,6 +81,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/controls/silent_toggle.h"
|
||||
#include "ui/chat/choose_send_as.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "webrtc/webrtc_environment.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "mainwindow.h"
|
||||
@@ -1151,7 +1152,7 @@ void ComposeControls::setMimeDataHook(MimeDataHook hook) {
|
||||
bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) {
|
||||
if (!isEditingMessage() || !_regularWindow) {
|
||||
return false;
|
||||
} else if (_canReplaceMedia) {
|
||||
} else if (_canReplaceMedia || _canAddMedia) {
|
||||
const auto queryToEdit = _header->queryToEdit();
|
||||
EditCaptionBox::StartMediaReplace(
|
||||
_regularWindow,
|
||||
@@ -1517,7 +1518,7 @@ void ComposeControls::orderControls() {
|
||||
}
|
||||
|
||||
bool ComposeControls::showRecordButton() const {
|
||||
return ::Media::Capture::instance()->available()
|
||||
return (_recordAvailability != Webrtc::RecordAvailability::None)
|
||||
&& !_voiceRecordBar->isListenState()
|
||||
&& !_voiceRecordBar->isRecordingByAnotherBar()
|
||||
&& !HasSendText(_field)
|
||||
@@ -1942,7 +1943,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
_preview->apply({ .removed = true });
|
||||
_preview->setDisabled(false);
|
||||
}
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
return;
|
||||
}
|
||||
@@ -1962,7 +1963,16 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
const auto resolve = [=] {
|
||||
if (const auto item = _history->owner().message(editingId)) {
|
||||
const auto media = item->media();
|
||||
_canReplaceMedia = media && media->allowsEditMedia();
|
||||
_canReplaceMedia = item->allowsEditMedia();
|
||||
if (media) {
|
||||
_canAddMedia = false;
|
||||
} else {
|
||||
_canAddMedia = base::take(_canReplaceMedia);
|
||||
}
|
||||
if (_canReplaceMedia || _canAddMedia) {
|
||||
// Invalidate the button, maybe icon has changed.
|
||||
_replaceMedia = nullptr;
|
||||
}
|
||||
_photoEditMedia = (_canReplaceMedia
|
||||
&& _regularWindow
|
||||
&& media->photo()
|
||||
@@ -1983,7 +1993,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
_header->editMessage(editingId, false);
|
||||
return false;
|
||||
@@ -2004,7 +2014,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
|
||||
}
|
||||
_header->replyToMessage({});
|
||||
} else {
|
||||
_canReplaceMedia = false;
|
||||
_canReplaceMedia = _canAddMedia = false;
|
||||
_photoEditMedia = nullptr;
|
||||
_header->replyToMessage(draft->reply);
|
||||
if (_header->replyingToMessage()) {
|
||||
@@ -2137,12 +2147,17 @@ void ComposeControls::initSendButton() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
SendMenu::SetupMenuAndShortcuts(
|
||||
_send.get(),
|
||||
_show,
|
||||
[=] { return sendButtonMenuDetails(); },
|
||||
sendAction);
|
||||
|
||||
Core::App().mediaDevices().recordAvailabilityValue(
|
||||
) | rpl::start_with_next([=](Webrtc::RecordAvailability value) {
|
||||
_recordAvailability = value;
|
||||
updateSendButtonType();
|
||||
}, _send->lifetime());
|
||||
}
|
||||
|
||||
void ComposeControls::initSendAsButton(not_null<PeerData*> peer) {
|
||||
@@ -2413,6 +2428,42 @@ void ComposeControls::initVoiceRecordBar() {
|
||||
return false;
|
||||
});
|
||||
|
||||
_voiceRecordBar->recordingTipRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
Core::App().settings().setRecordVideoMessages(
|
||||
!Core::App().settings().recordVideoMessages());
|
||||
updateSendButtonType();
|
||||
switch (_send->type()) {
|
||||
case Ui::SendButton::Type::Record: {
|
||||
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
|
||||
_show->showToast((_recordAvailability == both)
|
||||
? tr::lng_record_voice_tip(tr::now)
|
||||
: tr::lng_record_hold_tip(tr::now));
|
||||
} break;
|
||||
case Ui::SendButton::Type::Round:
|
||||
_show->showToast(tr::lng_record_video_tip(tr::now));
|
||||
break;
|
||||
}
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_voiceRecordBar->errors(
|
||||
) | rpl::start_with_next([=](::Media::Capture::Error error) {
|
||||
using Error = ::Media::Capture::Error;
|
||||
switch (error) {
|
||||
case Error::AudioInit:
|
||||
case Error::AudioTimeout:
|
||||
_show->showToast(tr::lng_record_audio_problem(tr::now));
|
||||
break;
|
||||
case Error::VideoInit:
|
||||
case Error::VideoTimeout:
|
||||
_show->showToast(tr::lng_record_video_problem(tr::now));
|
||||
break;
|
||||
default:
|
||||
_show->showToast(u"Unknown error."_q);
|
||||
break;
|
||||
}
|
||||
}, _wrap->lifetime());
|
||||
|
||||
_voiceRecordBar->updateSendButtonTypeRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateSendButtonType();
|
||||
@@ -2454,7 +2505,11 @@ auto ComposeControls::computeSendButtonType() const {
|
||||
} else if (_isInlineBot) {
|
||||
return Type::Cancel;
|
||||
} else if (showRecordButton()) {
|
||||
return Type::Record;
|
||||
const auto both = Webrtc::RecordAvailability::VideoAndAudio;
|
||||
const auto video = Core::App().settings().recordVideoMessages();
|
||||
return (video && _recordAvailability == both)
|
||||
? Type::Round
|
||||
: Type::Record;
|
||||
}
|
||||
return (_mode == Mode::Normal) ? Type::Send : Type::Schedule;
|
||||
}
|
||||
@@ -2487,7 +2542,9 @@ void ComposeControls::updateSendButtonType() {
|
||||
}();
|
||||
_send->setSlowmodeDelay(delay);
|
||||
_send->setDisabled(_sendDisabledBySlowmode.current()
|
||||
&& (type == Type::Send || type == Type::Record));
|
||||
&& (type == Type::Send
|
||||
|| type == Type::Record
|
||||
|| type == Type::Round));
|
||||
}
|
||||
|
||||
void ComposeControls::finishAnimating() {
|
||||
@@ -2882,7 +2939,7 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) {
|
||||
}
|
||||
|
||||
bool ComposeControls::updateReplaceMediaButton() {
|
||||
if (!_canReplaceMedia || !_regularWindow) {
|
||||
if ((!_canReplaceMedia && !_canAddMedia) || !_regularWindow) {
|
||||
const auto result = (_replaceMedia != nullptr);
|
||||
_replaceMedia = nullptr;
|
||||
return result;
|
||||
@@ -2891,7 +2948,7 @@ bool ComposeControls::updateReplaceMediaButton() {
|
||||
}
|
||||
_replaceMedia = std::make_unique<Ui::IconButton>(
|
||||
_wrap.get(),
|
||||
st::historyReplaceMedia);
|
||||
_canReplaceMedia ? st::historyReplaceMedia : st::historyAddMedia);
|
||||
const auto hideDuration = st::historyReplaceMedia.ripple.hideDuration;
|
||||
_replaceMedia->setClickedCallback([=] {
|
||||
base::call_delayed(hideDuration, _wrap.get(), [=] {
|
||||
@@ -3149,8 +3206,9 @@ bool ComposeControls::isRecording() const {
|
||||
bool ComposeControls::isRecordingPressed() const {
|
||||
return !_voiceRecordBar->isRecordingLocked()
|
||||
&& (!_voiceRecordBar->isHidden()
|
||||
|| (_send->type() == Ui::SendButton::Type::Record
|
||||
&& _send->isDown()));
|
||||
|| (_send->isDown()
|
||||
&& (_send->type() == Ui::SendButton::Type::Record
|
||||
|| _send->type() == Ui::SendButton::Type::Round)));
|
||||
}
|
||||
|
||||
rpl::producer<bool> ComposeControls::recordingActiveValue() const {
|
||||
|
||||
@@ -76,6 +76,10 @@ namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Webrtc {
|
||||
enum class RecordAvailability : uchar;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Window {
|
||||
struct SectionShow;
|
||||
class SessionController;
|
||||
@@ -436,10 +440,12 @@ private:
|
||||
bool _isInlineBot = false;
|
||||
bool _botCommandShown = false;
|
||||
bool _likeShown = false;
|
||||
Webrtc::RecordAvailability _recordAvailability = {};
|
||||
|
||||
FullMsgId _editingId;
|
||||
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
|
||||
bool _canReplaceMedia = false;
|
||||
bool _canAddMedia = false;
|
||||
|
||||
std::unique_ptr<Controls::WebpageProcessor> _preview;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ void MediaEditManager::start(
|
||||
std::optional<bool> invertCaption) {
|
||||
const auto media = item->media();
|
||||
if (!media) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
_item = item;
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "boxes/filters/edit_filter_chats_list.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
@@ -41,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
@@ -911,6 +913,117 @@ void DraftOptionsBox(
|
||||
}, box->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthorSelector {
|
||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||
Fn<bool(int, int, int)> overrideKey;
|
||||
};
|
||||
[[nodiscard]] AuthorSelector AuthorRowSelector(
|
||||
not_null<Main::Session*> session,
|
||||
FullReplyTo reply,
|
||||
Fn<void(not_null<Data::Thread*>)> chosen) {
|
||||
const auto item = session->data().message(reply.messageId);
|
||||
if (!item) {
|
||||
return {};
|
||||
}
|
||||
const auto displayFrom = item->displayFrom();
|
||||
const auto from = displayFrom ? displayFrom : item->from().get();
|
||||
if (!from->isUser() || from == item->history()->peer || from->isSelf()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
class AuthorController final : public PeerListController {
|
||||
public:
|
||||
AuthorController(not_null<PeerData*> peer, Fn<void()> click)
|
||||
: _peer(peer)
|
||||
, _click(std::move(click)) {
|
||||
}
|
||||
|
||||
void prepare() override {
|
||||
delegate()->peerListAppendRow(
|
||||
std::make_unique<ChatsListBoxController::Row>(
|
||||
_peer->owner().history(_peer),
|
||||
&computeListSt().item));
|
||||
delegate()->peerListRefreshRows();
|
||||
TrackPremiumRequiredChanges(this, _lifetime);
|
||||
}
|
||||
void loadMoreRows() override {
|
||||
}
|
||||
void rowClicked(not_null<PeerListRow*> row) override {
|
||||
if (RecipientRow::ShowLockedError(this, row, WritePremiumRequiredError)) {
|
||||
return;
|
||||
} else if (const auto onstack = _click) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
Main::Session &session() const override {
|
||||
return _peer->session();
|
||||
}
|
||||
|
||||
private:
|
||||
const not_null<PeerData*> _peer;
|
||||
Fn<void()> _click;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
const auto container = result.data();
|
||||
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
tr::lng_reply_in_author()));
|
||||
Ui::AddSkip(container);
|
||||
|
||||
const auto delegate = container->lifetime().make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = container->lifetime().make_state<
|
||||
AuthorController
|
||||
>(from, [=] { chosen(from->owner().history(from)); });
|
||||
controller->setStyleOverrides(&st::peerListSingleRow);
|
||||
const auto content = container->add(object_ptr<PeerListContent>(
|
||||
container,
|
||||
controller));
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
|
||||
Ui::AddSkip(container);
|
||||
container->add(CreatePeerListSectionSubtitle(
|
||||
container,
|
||||
tr::lng_reply_in_chats_list()));
|
||||
|
||||
const auto overrideKey = [=](int direction, int from, int to) {
|
||||
if (!content->isVisible()) {
|
||||
return false;
|
||||
} else if (direction > 0 && from < 0 && to >= 0) {
|
||||
if (content->hasSelection()) {
|
||||
const auto was = content->selectedIndex();
|
||||
const auto now = content->selectSkip(1).reallyMovedTo;
|
||||
if (was != now) {
|
||||
return true;
|
||||
}
|
||||
content->clearSelection();
|
||||
} else {
|
||||
content->selectSkip(1);
|
||||
return true;
|
||||
}
|
||||
} else if (direction < 0 && to < 0) {
|
||||
if (!content->hasSelection()) {
|
||||
content->selectLast();
|
||||
} else if (from >= 0 || content->hasSelection()) {
|
||||
content->selectSkip(-1);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
.content = std::move(result),
|
||||
.overrideKey = overrideKey,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowReplyToChatBox(
|
||||
@@ -921,7 +1034,7 @@ void ShowReplyToChatBox(
|
||||
public:
|
||||
using Chosen = not_null<Data::Thread*>;
|
||||
|
||||
Controller(not_null<Main::Session*> session)
|
||||
Controller(not_null<Main::Session*> session, FullReplyTo reply)
|
||||
: ChooseRecipientBoxController({
|
||||
.session = session,
|
||||
.callback = [=](Chosen thread) {
|
||||
@@ -929,6 +1042,13 @@ void ShowReplyToChatBox(
|
||||
},
|
||||
.premiumRequiredError = WritePremiumRequiredError,
|
||||
}) {
|
||||
_authorRow = AuthorRowSelector(
|
||||
session,
|
||||
reply,
|
||||
[=](Chosen thread) { _singleChosen.fire_copy(thread); });
|
||||
if (_authorRow.content) {
|
||||
setStyleOverrides(&st::peerListSmallSkips);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Chosen> singleChosen() const {
|
||||
@@ -939,13 +1059,26 @@ void ShowReplyToChatBox(
|
||||
return tr::lng_saved_quote_here(tr::now);
|
||||
}
|
||||
|
||||
bool overrideKeyboardNavigation(
|
||||
int direction,
|
||||
int fromIndex,
|
||||
int toIndex) override {
|
||||
return _authorRow.overrideKey
|
||||
&& _authorRow.overrideKey(direction, fromIndex, toIndex);
|
||||
}
|
||||
|
||||
private:
|
||||
void prepareViewHook() override {
|
||||
if (_authorRow.content) {
|
||||
delegate()->peerListSetAboveWidget(
|
||||
std::move(_authorRow.content));
|
||||
}
|
||||
ChooseRecipientBoxController::prepareViewHook();
|
||||
delegate()->peerListSetTitle(tr::lng_reply_in_another_title());
|
||||
}
|
||||
|
||||
rpl::event_stream<Chosen> _singleChosen;
|
||||
AuthorSelector _authorRow;
|
||||
|
||||
};
|
||||
|
||||
@@ -956,7 +1089,7 @@ void ShowReplyToChatBox(
|
||||
};
|
||||
const auto session = &show->session();
|
||||
const auto state = [&] {
|
||||
auto controller = std::make_unique<Controller>(session);
|
||||
auto controller = std::make_unique<Controller>(session, reply);
|
||||
const auto controllerRaw = controller.get();
|
||||
auto box = Box<PeerListBox>(std::move(controller), [=](
|
||||
not_null<PeerListBox*> box) {
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "chat_helpers/compose/compose_show.h"
|
||||
#include "core/application.h"
|
||||
#include "data/data_document.h"
|
||||
@@ -27,27 +28,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
#include "media/player/media_player_button.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "media/streaming/media_streaming_instance.h"
|
||||
#include "media/streaming/media_streaming_round_preview.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "ui/controls/round_video_recorder.h"
|
||||
#include "ui/controls/send_button.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "webrtc/webrtc_video_track.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_player.h"
|
||||
|
||||
#include <tgcalls/VideoCaptureInterface.h>
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
namespace {
|
||||
|
||||
using SendActionUpdate = VoiceRecordBar::SendActionUpdate;
|
||||
using VoiceToSend = VoiceRecordBar::VoiceToSend;
|
||||
|
||||
constexpr auto kAudioVoiceUpdateView = crl::time(200);
|
||||
constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes
|
||||
constexpr auto kMaxSamples
|
||||
@@ -69,6 +75,61 @@ enum class FilterType {
|
||||
Cancel,
|
||||
};
|
||||
|
||||
class SoundedPreview final : public Ui::DynamicImage {
|
||||
public:
|
||||
SoundedPreview(
|
||||
not_null<DocumentData*> document,
|
||||
rpl::producer<> repaints);
|
||||
std::shared_ptr<DynamicImage> clone() override;
|
||||
QImage image(int size) override;
|
||||
void subscribeToUpdates(Fn<void()> callback) override;
|
||||
|
||||
private:
|
||||
const not_null<DocumentData*> _document;
|
||||
QImage _roundingMask;
|
||||
Fn<void()> _repaint;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
SoundedPreview::SoundedPreview(
|
||||
not_null<DocumentData*> document,
|
||||
rpl::producer<> repaints)
|
||||
: _document(document) {
|
||||
std::move(repaints) | rpl::start_with_next([=] {
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> SoundedPreview::clone() {
|
||||
Unexpected("ListenWrap::videoPreview::clone.");
|
||||
}
|
||||
|
||||
QImage SoundedPreview::image(int size) {
|
||||
const auto player = ::Media::Player::instance();
|
||||
const auto streamed = player->roundVideoPreview(_document);
|
||||
if (!streamed) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto full = QSize(size, size) * style::DevicePixelRatio();
|
||||
if (_roundingMask.size() != full) {
|
||||
_roundingMask = Images::EllipseMask(full);
|
||||
}
|
||||
const auto frame = streamed->frameWithInfo({
|
||||
.resize = full,
|
||||
.outer = full,
|
||||
.mask = _roundingMask,
|
||||
});
|
||||
return frame.image;
|
||||
}
|
||||
|
||||
void SoundedPreview::subscribeToUpdates(Fn<void()> callback) {
|
||||
_repaint = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto InactiveColor(const QColor &c) {
|
||||
return QColor(c.red(), c.green(), c.blue(), kInactiveWaveformBarAlpha);
|
||||
}
|
||||
@@ -77,10 +138,6 @@ enum class FilterType {
|
||||
return std::clamp(float64(low) / high, 0., 1.);
|
||||
}
|
||||
|
||||
[[nodiscard]] crl::time Duration(int samples) {
|
||||
return samples * crl::time(1000) / ::Media::Player::kDefaultFrequency;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto FormatVoiceDuration(int samples) {
|
||||
const int duration = kPrecision
|
||||
* (float64(samples) / ::Media::Player::kDefaultFrequency);
|
||||
@@ -201,6 +258,44 @@ void PaintWaveform(
|
||||
}
|
||||
}
|
||||
|
||||
void FillWithMinithumbs(
|
||||
QPainter &p,
|
||||
not_null<const Ui::RoundVideoResult*> data,
|
||||
QRect rect,
|
||||
float64 progress) {
|
||||
if (!data->minithumbsCount || !data->minithumbSize || rect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto size = rect.height();
|
||||
const auto single = data->minithumbSize;
|
||||
const auto perrow = data->minithumbs.width() / single;
|
||||
const auto thumbs = (rect.width() + size - 1) / size;
|
||||
if (!thumbs || !perrow) {
|
||||
return;
|
||||
}
|
||||
for (auto i = 0; i != thumbs - 1; ++i) {
|
||||
const auto index = (i * data->minithumbsCount) / thumbs;
|
||||
p.drawImage(
|
||||
QRect(rect.x() + i * size, rect.y(), size, size),
|
||||
data->minithumbs,
|
||||
QRect(
|
||||
(index % perrow) * single,
|
||||
(index / perrow) * single,
|
||||
single,
|
||||
single));
|
||||
}
|
||||
const auto last = rect.width() - (thumbs - 1) * size;
|
||||
const auto index = ((thumbs - 1) * data->minithumbsCount) / thumbs;
|
||||
p.drawImage(
|
||||
QRect(rect.x() + (thumbs - 1) * size, rect.y(), last, size),
|
||||
data->minithumbs,
|
||||
QRect(
|
||||
(index % perrow) * single,
|
||||
(index / perrow) * single,
|
||||
(last * single) / size,
|
||||
single));
|
||||
}
|
||||
|
||||
[[nodiscard]] QRect DrawLockCircle(
|
||||
QPainter &p,
|
||||
const QRect &widgetRect,
|
||||
@@ -269,7 +364,8 @@ class TTLButton final : public Ui::RippleButton {
|
||||
public:
|
||||
TTLButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st);
|
||||
const style::RecordBar &st,
|
||||
bool recordingVideo);
|
||||
|
||||
void clearState() override;
|
||||
|
||||
@@ -288,7 +384,8 @@ private:
|
||||
|
||||
TTLButton::TTLButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st)
|
||||
const style::RecordBar &st,
|
||||
bool recordingVideo)
|
||||
: RippleButton(parent, st.lock.ripple)
|
||||
, _st(st)
|
||||
, _rippleRect(Rect(Size(st::historyRecordLockTopShadow.width()))
|
||||
@@ -315,8 +412,10 @@ TTLButton::TTLButton(
|
||||
}
|
||||
auto text = rpl::conditional(
|
||||
Core::App().settings().ttlVoiceClickTooltipHiddenValue(),
|
||||
tr::lng_record_once_active_tooltip(
|
||||
Ui::Text::RichLangValue),
|
||||
(recordingVideo
|
||||
? tr::lng_record_once_active_video
|
||||
: tr::lng_record_once_active_tooltip)(
|
||||
Ui::Text::RichLangValue),
|
||||
tr::lng_record_once_first_tooltip(
|
||||
Ui::Text::RichLangValue));
|
||||
_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(
|
||||
@@ -426,35 +525,37 @@ public:
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st,
|
||||
not_null<Main::Session*> session,
|
||||
::Media::Capture::Result *data,
|
||||
not_null<Ui::RoundVideoResult*> data,
|
||||
const style::font &font);
|
||||
|
||||
void requestPaintProgress(float64 progress);
|
||||
rpl::producer<> stopRequests() const;
|
||||
[[nodiscard]] rpl::producer<> stopRequests() const;
|
||||
|
||||
void playPause();
|
||||
[[nodiscard]] std::shared_ptr<Ui::DynamicImage> videoPreview();
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
void init();
|
||||
void initPlayButton();
|
||||
void initPlayProgress();
|
||||
|
||||
bool isInPlayer(const ::Media::Player::TrackState &state) const;
|
||||
bool isInPlayer() const;
|
||||
[[nodiscard]] bool isInPlayer(
|
||||
const ::Media::Player::TrackState &state) const;
|
||||
[[nodiscard]] bool isInPlayer() const;
|
||||
|
||||
int computeTopMargin(int height) const;
|
||||
QRect computeWaveformRect(const QRect ¢erRect) const;
|
||||
[[nodiscard]] int computeTopMargin(int height) const;
|
||||
[[nodiscard]] QRect computeWaveformRect(const QRect ¢erRect) const;
|
||||
|
||||
not_null<Ui::RpWidget*> _parent;
|
||||
const not_null<Ui::RpWidget*> _parent;
|
||||
|
||||
const style::RecordBar &_st;
|
||||
const not_null<Main::Session*> _session;
|
||||
const not_null<DocumentData*> _document;
|
||||
const std::unique_ptr<VoiceData> _voiceData;
|
||||
const std::shared_ptr<Data::DocumentMedia> _mediaView;
|
||||
const not_null<::Media::Capture::Result*> _data;
|
||||
const not_null<Ui::RoundVideoResult*> _data;
|
||||
const base::unique_qptr<Ui::IconButton> _delete;
|
||||
const style::font &_durationFont;
|
||||
const QString _duration;
|
||||
@@ -475,6 +576,7 @@ private:
|
||||
anim::value _playProgress;
|
||||
|
||||
rpl::variable<float64> _showProgress = 0.;
|
||||
rpl::event_stream<> _videoRepaints;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
@@ -484,7 +586,7 @@ ListenWrap::ListenWrap(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::RecordBar &st,
|
||||
not_null<Main::Session*> session,
|
||||
::Media::Capture::Result *data,
|
||||
not_null<Ui::RoundVideoResult*> data,
|
||||
const style::font &font)
|
||||
: _parent(parent)
|
||||
, _st(st)
|
||||
@@ -495,8 +597,7 @@ ListenWrap::ListenWrap(
|
||||
, _data(data)
|
||||
, _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove))
|
||||
, _durationFont(font)
|
||||
, _duration(Ui::FormatDurationText(
|
||||
float64(_data->samples) / ::Media::Player::kDefaultFrequency))
|
||||
, _duration(Ui::FormatDurationText(_data->duration / 1000))
|
||||
, _durationWidth(_durationFont->width(_duration))
|
||||
, _playPauseSt(st::mediaPlayerButton)
|
||||
, _playPauseButton(base::make_unique_q<Ui::AbstractButton>(parent))
|
||||
@@ -603,20 +704,27 @@ void ListenWrap::init() {
|
||||
}
|
||||
|
||||
// Waveform paint.
|
||||
{
|
||||
const auto rect = (progress == 1.)
|
||||
? _waveformFgRect
|
||||
: computeWaveformRect(bgCenterRect);
|
||||
if (rect.width() > 0) {
|
||||
p.translate(rect.topLeft());
|
||||
const auto waveformRect = (progress == 1.)
|
||||
? _waveformFgRect
|
||||
: computeWaveformRect(bgCenterRect);
|
||||
if (!waveformRect.isEmpty()) {
|
||||
const auto playProgress = _playProgress.current();
|
||||
if (_data->minithumbs.isNull()) {
|
||||
p.translate(waveformRect.topLeft());
|
||||
PaintWaveform(
|
||||
p,
|
||||
_voiceData.get(),
|
||||
rect.width(),
|
||||
waveformRect.width(),
|
||||
_activeWaveformBar,
|
||||
_inactiveWaveformBar,
|
||||
_playProgress.current());
|
||||
playProgress);
|
||||
p.resetTransform();
|
||||
} else {
|
||||
FillWithMinithumbs(
|
||||
p,
|
||||
_data,
|
||||
waveformRect,
|
||||
playProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,9 +738,11 @@ void ListenWrap::initPlayButton() {
|
||||
using namespace ::Media::Player;
|
||||
using State = TrackState;
|
||||
|
||||
_mediaView->setBytes(_data->bytes);
|
||||
_document->size = _data->bytes.size();
|
||||
_document->type = VoiceDocument;
|
||||
_mediaView->setBytes(_data->content);
|
||||
_document->size = _data->content.size();
|
||||
_document->type = _data->minithumbs.isNull()
|
||||
? VoiceDocument
|
||||
: RoundVideoDocument;
|
||||
|
||||
const auto &play = _playPauseSt.playOuter;
|
||||
const auto &width = _waveformBgFinalCenterRect.height();
|
||||
@@ -668,6 +778,9 @@ void ListenWrap::initPlayButton() {
|
||||
) | rpl::start_with_next([=](const State &state) {
|
||||
if (isInPlayer(state)) {
|
||||
*showPause = ShowPauseIcon(state.state);
|
||||
if (!_data->minithumbs.isNull()) {
|
||||
_videoRepaints.fire({});
|
||||
}
|
||||
} else if (showPause->current()) {
|
||||
*showPause = false;
|
||||
}
|
||||
@@ -678,6 +791,13 @@ void ListenWrap::initPlayButton() {
|
||||
) | rpl::start_with_next([=] {
|
||||
*showPause = false;
|
||||
}, _lifetime);
|
||||
|
||||
_lifetime.add([=] {
|
||||
const auto current = instance()->current(AudioMsgId::Type::Voice);
|
||||
if (current.audio() == _document) {
|
||||
instance()->stop(AudioMsgId::Type::Voice, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ListenWrap::initPlayProgress() {
|
||||
@@ -817,6 +937,12 @@ rpl::producer<> ListenWrap::stopRequests() const {
|
||||
return _delete->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> ListenWrap::videoPreview() {
|
||||
return std::make_shared<SoundedPreview>(
|
||||
_document,
|
||||
_videoRepaints.events());
|
||||
}
|
||||
|
||||
rpl::lifetime &ListenWrap::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
@@ -831,6 +957,7 @@ public:
|
||||
void requestPaintLockToStopProgress(float64 progress);
|
||||
void requestPaintPauseToInputProgress(float64 progress);
|
||||
void setVisibleTopPart(int part);
|
||||
void setRecordingVideo(bool value);
|
||||
|
||||
[[nodiscard]] rpl::producer<> locks() const;
|
||||
[[nodiscard]] bool isLocked() const;
|
||||
@@ -859,6 +986,7 @@ private:
|
||||
float64 _pauseToInputProgress = 0.;
|
||||
rpl::variable<float64> _progress = 0.;
|
||||
int _visibleTopPart = -1;
|
||||
bool _recordingVideo = false;
|
||||
|
||||
};
|
||||
|
||||
@@ -882,6 +1010,10 @@ void RecordLock::setVisibleTopPart(int part) {
|
||||
_visibleTopPart = part;
|
||||
}
|
||||
|
||||
void RecordLock::setRecordingVideo(bool value) {
|
||||
_recordingVideo = value;
|
||||
}
|
||||
|
||||
void RecordLock::init() {
|
||||
shownValue(
|
||||
) | rpl::start_with_next([=](bool shown) {
|
||||
@@ -972,9 +1104,10 @@ void RecordLock::drawProgress(QPainter &p) {
|
||||
p.setBrush(_st.fg);
|
||||
if (_pauseToInputProgress > 0.) {
|
||||
p.setOpacity(_pauseToInputProgress);
|
||||
st::historyRecordLockInput.paintInCenter(
|
||||
p,
|
||||
blockRect.toRect());
|
||||
const auto &icon = _recordingVideo
|
||||
? st::historyRecordLockRound
|
||||
: st::historyRecordLockInput;
|
||||
icon.paintInCenter(p, blockRect.toRect());
|
||||
p.setOpacity(1. - _pauseToInputProgress);
|
||||
}
|
||||
p.drawRoundedRect(
|
||||
@@ -1244,7 +1377,7 @@ VoiceRecordBar::VoiceRecordBar(
|
||||
}
|
||||
|
||||
VoiceRecordBar::~VoiceRecordBar() {
|
||||
if (isRecording()) {
|
||||
if (isActive()) {
|
||||
stopRecording(StopType::Cancel);
|
||||
}
|
||||
}
|
||||
@@ -1298,7 +1431,12 @@ void VoiceRecordBar::updateTTLGeometry(
|
||||
const auto parent = parentWidget();
|
||||
const auto me = Ui::MapFrom(_outerContainer, parent, geometry());
|
||||
const auto anyTop = me.y() - st::historyRecordLockPosition.y();
|
||||
const auto ttlFrom = anyTop - _ttlButton->height() * 2;
|
||||
const auto lockHiddenProgress = (_lockShowing.current() || !_fullRecord)
|
||||
? 0.
|
||||
: (1. - _showLockAnimation.value(0.));
|
||||
const auto ttlFrom = anyTop
|
||||
- _ttlButton->height()
|
||||
- (_ttlButton->height() * (1. - lockHiddenProgress));
|
||||
if (type == TTLAnimationType::RightLeft) {
|
||||
const auto finalRight = _outerContainer->width()
|
||||
- rect::right(me)
|
||||
@@ -1418,6 +1556,9 @@ void VoiceRecordBar::init() {
|
||||
} else if (value == 1. && show) {
|
||||
computeAndSetLockProgress(QCursor::pos());
|
||||
}
|
||||
if (_fullRecord && !show) {
|
||||
updateTTLGeometry(TTLAnimationType::RightLeft, 1.);
|
||||
}
|
||||
};
|
||||
_showLockAnimation.start(std::move(callback), from, to, duration);
|
||||
}, lifetime());
|
||||
@@ -1473,7 +1614,6 @@ void VoiceRecordBar::init() {
|
||||
if (!paused) {
|
||||
return;
|
||||
}
|
||||
// _lockShowing = false;
|
||||
|
||||
const auto to = 1.;
|
||||
auto callback = [=](float64 value) {
|
||||
@@ -1496,7 +1636,8 @@ void VoiceRecordBar::init() {
|
||||
if (!_ttlButton) {
|
||||
_ttlButton = std::make_unique<TTLButton>(
|
||||
_outerContainer,
|
||||
_st);
|
||||
_st,
|
||||
_recordingVideo);
|
||||
}
|
||||
_ttlButton->show();
|
||||
}
|
||||
@@ -1529,12 +1670,14 @@ void VoiceRecordBar::init() {
|
||||
if (_startRecordingFilter && _startRecordingFilter()) {
|
||||
return;
|
||||
}
|
||||
_recordingTipRequired = true;
|
||||
_recordingTipRequire = crl::now();
|
||||
_recordingVideo = (_send->type() == Ui::SendButton::Type::Round);
|
||||
_fullRecord = false;
|
||||
_ttlButton = nullptr;
|
||||
_lock->setRecordingVideo(_recordingVideo);
|
||||
_startTimer.callOnce(st::universalDuration);
|
||||
} else if (e->type() == QEvent::MouseButtonRelease) {
|
||||
if (base::take(_recordingTipRequired)) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
checkTipRequired();
|
||||
_startTimer.cancel();
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -1579,6 +1722,11 @@ void VoiceRecordBar::activeAnimate(bool active) {
|
||||
}
|
||||
|
||||
void VoiceRecordBar::visibilityAnimate(bool show, Fn<void()> &&callback) {
|
||||
if (_send->type() == Ui::SendButton::Type::Round) {
|
||||
_level->setType(VoiceRecordButton::Type::Round);
|
||||
} else {
|
||||
_level->setType(VoiceRecordButton::Type::Record);
|
||||
}
|
||||
const auto to = show ? 1. : 0.;
|
||||
const auto from = show ? 0. : 1.;
|
||||
auto animationCallback = [=, callback = std::move(callback)](auto value) {
|
||||
@@ -1652,6 +1800,10 @@ void VoiceRecordBar::startRecording() {
|
||||
}
|
||||
|
||||
using namespace ::Media::Capture;
|
||||
if (_recordingVideo && !createVideoRecorder()) {
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
if (!instance()->available()) {
|
||||
stop(false);
|
||||
return;
|
||||
@@ -1664,16 +1816,36 @@ void VoiceRecordBar::startRecording() {
|
||||
if (_paused.current()) {
|
||||
_paused = false;
|
||||
instance()->pause(false, nullptr);
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->resume({
|
||||
.video = std::move(_data),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
instance()->start();
|
||||
instance()->start(_videoRecorder
|
||||
? _videoRecorder->audioChunkProcessor()
|
||||
: nullptr);
|
||||
}
|
||||
instance()->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
_recordingTipRequired = (update.samples < kMinSamples);
|
||||
recordUpdated(update.level, update.samples);
|
||||
}, [=] {
|
||||
stop(false);
|
||||
}, _recordingLifetime);
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->updated(
|
||||
) | rpl::start_with_next_error([=](const Update &update) {
|
||||
recordUpdated(update.level, update.samples);
|
||||
if (update.finished) {
|
||||
_fullRecord = true;
|
||||
stopRecording(StopType::Listen);
|
||||
_lockShowing = false;
|
||||
}
|
||||
}, [=](Error error) {
|
||||
stop(false);
|
||||
_errors.fire_copy(error);
|
||||
}, _recordingLifetime);
|
||||
}
|
||||
_recordingLifetime.add([=] {
|
||||
_recording = false;
|
||||
});
|
||||
@@ -1705,14 +1877,22 @@ void VoiceRecordBar::startRecording() {
|
||||
}
|
||||
computeAndSetLockProgress(mouse->globalPos());
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
if (base::take(_recordingTipRequired)) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
checkTipRequired();
|
||||
stop(_inField.current());
|
||||
}
|
||||
}, _recordingLifetime);
|
||||
}
|
||||
|
||||
void VoiceRecordBar::checkTipRequired() {
|
||||
const auto require = base::take(_recordingTipRequire);
|
||||
const auto duration = st::universalDuration
|
||||
+ (kMinSamples * crl::time(1000)
|
||||
/ ::Media::Player::kDefaultFrequency);
|
||||
if (require && (require + duration > crl::now())) {
|
||||
_recordingTipRequests.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
|
||||
_level->requestPaintLevel(level);
|
||||
_recordingSamples = samples;
|
||||
@@ -1721,7 +1901,10 @@ void VoiceRecordBar::recordUpdated(quint16 level, int samples) {
|
||||
}
|
||||
Core::App().updateNonIdle();
|
||||
update(_durationRect);
|
||||
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice });
|
||||
const auto type = _recordingVideo
|
||||
? Api::SendProgressType::RecordRound
|
||||
: Api::SendProgressType::RecordVoice;
|
||||
_sendActionUpdates.fire({ type });
|
||||
}
|
||||
|
||||
void VoiceRecordBar::stop(bool send) {
|
||||
@@ -1735,7 +1918,6 @@ void VoiceRecordBar::stop(bool send) {
|
||||
const auto type = send ? StopType::Send : StopType::Cancel;
|
||||
stopRecording(type, ttlBeforeHide);
|
||||
};
|
||||
// _lockShowing = false;
|
||||
visibilityAnimate(false, std::move(disappearanceCallback));
|
||||
}
|
||||
|
||||
@@ -1754,7 +1936,10 @@ void VoiceRecordBar::finish() {
|
||||
|
||||
[[maybe_unused]] const auto s = takeTTLState();
|
||||
|
||||
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
|
||||
const auto type = _recordingVideo
|
||||
? Api::SendProgressType::RecordRound
|
||||
: Api::SendProgressType::RecordVoice;
|
||||
_sendActionUpdates.fire({ type, -1 });
|
||||
|
||||
_data = {};
|
||||
}
|
||||
@@ -1769,39 +1954,99 @@ void VoiceRecordBar::hideFast() {
|
||||
void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
|
||||
using namespace ::Media::Capture;
|
||||
if (type == StopType::Cancel) {
|
||||
if (_videoRecorder) {
|
||||
_videoRecorder->hide();
|
||||
}
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
_cancelRequests.fire({});
|
||||
}));
|
||||
} else if (type == StopType::Listen) {
|
||||
instance()->pause(true, crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_paused = true;
|
||||
_data = std::move(data);
|
||||
if (const auto recorder = _videoRecorder.get()) {
|
||||
const auto weak = base::make_weak(recorder);
|
||||
recorder->pause([=](Ui::RoundVideoResult data) {
|
||||
crl::on_main(weak, [=, data = std::move(data)]() mutable {
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
_paused = true;
|
||||
_data = std::move(data);
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
|
||||
// _lockShowing = false;
|
||||
}));
|
||||
using SilentPreview = ::Media::Streaming::RoundPreview;
|
||||
recorder->showPreview(
|
||||
std::make_shared<SilentPreview>(
|
||||
_data.content,
|
||||
recorder->previewSize()),
|
||||
_listen->videoPreview());
|
||||
});
|
||||
});
|
||||
instance()->pause(true);
|
||||
} else {
|
||||
instance()->pause(true, crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_paused = true;
|
||||
_data = Ui::RoundVideoResult{
|
||||
.content = std::move(data.bytes),
|
||||
.waveform = std::move(data.waveform),
|
||||
.duration = data.duration,
|
||||
};
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
_listen = std::make_unique<ListenWrap>(
|
||||
this,
|
||||
_st,
|
||||
&_show->session(),
|
||||
&_data,
|
||||
_cancelFont);
|
||||
_listenChanges.fire({});
|
||||
}));
|
||||
}
|
||||
} else if (type == StopType::Send) {
|
||||
if (_videoRecorder) {
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
_videoRecorder->hide([=](Ui::RoundVideoResult data) {
|
||||
crl::on_main([=, data = std::move(data)]() mutable {
|
||||
if (weak) {
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
const auto options = Api::SendOptions{
|
||||
.ttlSeconds = (ttlBeforeHide
|
||||
? std::numeric_limits<int>::max()
|
||||
: 0),
|
||||
};
|
||||
_sendVoiceRequests.fire({
|
||||
.bytes = data.content,
|
||||
//.waveform = {},
|
||||
.duration = data.duration,
|
||||
.options = options,
|
||||
.video = true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
instance()->stop(crl::guard(this, [=](Result &&data) {
|
||||
if (data.bytes.isEmpty()) {
|
||||
// Close everything.
|
||||
stop(false);
|
||||
return;
|
||||
}
|
||||
_data = std::move(data);
|
||||
_data = Ui::RoundVideoResult{
|
||||
.content = std::move(data.bytes),
|
||||
.waveform = std::move(data.waveform),
|
||||
.duration = data.duration,
|
||||
};
|
||||
|
||||
window()->raise();
|
||||
window()->activateWindow();
|
||||
@@ -1811,10 +2056,10 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
|
||||
: 0),
|
||||
};
|
||||
_sendVoiceRequests.fire({
|
||||
_data.bytes,
|
||||
_data.waveform,
|
||||
Duration(_data.samples),
|
||||
options,
|
||||
.bytes = _data.content,
|
||||
.waveform = _data.waveform,
|
||||
.duration = _data.duration,
|
||||
.options = options,
|
||||
});
|
||||
}));
|
||||
}
|
||||
@@ -1878,10 +2123,11 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
|
||||
options.ttlSeconds = std::numeric_limits<int>::max();
|
||||
}
|
||||
_sendVoiceRequests.fire({
|
||||
_data.bytes,
|
||||
_data.waveform,
|
||||
Duration(_data.samples),
|
||||
options,
|
||||
.bytes = _data.content,
|
||||
.waveform = _data.waveform,
|
||||
.duration = _data.duration,
|
||||
.options = options,
|
||||
.video = !_data.minithumbs.isNull(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1954,6 +2200,10 @@ rpl::producer<> VoiceRecordBar::recordingTipRequests() const {
|
||||
return _recordingTipRequests.events();
|
||||
}
|
||||
|
||||
auto VoiceRecordBar::errors() const -> rpl::producer<Error> {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isLockPresent() const {
|
||||
return _lockShowing.current();
|
||||
}
|
||||
@@ -1963,7 +2213,8 @@ bool VoiceRecordBar::isListenState() const {
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isTypeRecord() const {
|
||||
return (_send->type() == Ui::SendButton::Type::Record);
|
||||
return (_send->type() == Ui::SendButton::Type::Record)
|
||||
|| (_send->type() == Ui::SendButton::Type::Round);
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::isRecordingByAnotherBar() const {
|
||||
@@ -2085,8 +2336,12 @@ void VoiceRecordBar::showDiscardBox(
|
||||
};
|
||||
_show->showBox(Ui::MakeConfirmBox({
|
||||
.text = (isListenState()
|
||||
? tr::lng_record_listen_cancel_sure
|
||||
: tr::lng_record_lock_cancel_sure)(),
|
||||
? (_recordingVideo
|
||||
? tr::lng_record_listen_cancel_sure_round
|
||||
: tr::lng_record_listen_cancel_sure)
|
||||
: (_recordingVideo
|
||||
? tr::lng_record_lock_cancel_sure_round
|
||||
: tr::lng_record_lock_cancel_sure))(),
|
||||
.confirmed = std::move(sure),
|
||||
.confirmText = tr::lng_record_lock_discard(),
|
||||
.confirmStyle = &st::attentionBoxButton,
|
||||
@@ -2094,4 +2349,52 @@ void VoiceRecordBar::showDiscardBox(
|
||||
_warningShown = true;
|
||||
}
|
||||
|
||||
bool VoiceRecordBar::createVideoRecorder() {
|
||||
if (_videoRecorder) {
|
||||
return true;
|
||||
}
|
||||
const auto hiding = [=](not_null<Ui::RoundVideoRecorder*> which) {
|
||||
if (_videoRecorder.get() == which) {
|
||||
_videoHiding.push_back(base::take(_videoRecorder));
|
||||
}
|
||||
};
|
||||
const auto hidden = [=](not_null<Ui::RoundVideoRecorder*> which) {
|
||||
if (_videoRecorder.get() == which) {
|
||||
_videoRecorder = nullptr;
|
||||
}
|
||||
_videoHiding.erase(
|
||||
ranges::remove(
|
||||
_videoHiding,
|
||||
which.get(),
|
||||
&std::unique_ptr<Ui::RoundVideoRecorder>::get),
|
||||
end(_videoHiding));
|
||||
};
|
||||
auto capturer = Core::App().calls().getVideoCapture();
|
||||
auto track = std::make_shared<Webrtc::VideoTrack>(
|
||||
Webrtc::VideoState::Active);
|
||||
capturer->setOutput(track->sink());
|
||||
capturer->setPreferredAspectRatio(1.);
|
||||
_videoCapturerLifetime = track->stateValue(
|
||||
) | rpl::start_with_next([=](Webrtc::VideoState state) {
|
||||
capturer->setState((state == Webrtc::VideoState::Active)
|
||||
? tgcalls::VideoState::Active
|
||||
: tgcalls::VideoState::Inactive);
|
||||
});
|
||||
_videoRecorder = std::make_unique<Ui::RoundVideoRecorder>(
|
||||
Ui::RoundVideoRecorderDescriptor{
|
||||
.container = _outerContainer,
|
||||
.hiding = hiding,
|
||||
.hidden = hidden,
|
||||
.capturer = std::move(capturer),
|
||||
.track = std::move(track),
|
||||
.placeholder = _show->session().local().readRoundPlaceholder(),
|
||||
});
|
||||
_videoRecorder->placeholderUpdates(
|
||||
) | rpl::start_with_next([=](QImage &&placeholder) {
|
||||
_show->session().local().writeRoundPlaceholder(placeholder);
|
||||
}, _videoCapturerLifetime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/timer.h"
|
||||
#include "history/view/controls/compose_controls_common.h"
|
||||
#include "media/audio/media_audio_capture_common.h"
|
||||
#include "ui/controls/round_video_recorder.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
@@ -21,9 +22,14 @@ namespace style {
|
||||
struct RecordBar;
|
||||
} // namespace style
|
||||
|
||||
namespace Media::Capture {
|
||||
enum class Error : uchar;
|
||||
} // namespace Media::Capture
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class SendButton;
|
||||
class RoundVideoRecorder;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
@@ -56,6 +62,7 @@ public:
|
||||
using SendActionUpdate = Controls::SendActionUpdate;
|
||||
using VoiceToSend = Controls::VoiceToSend;
|
||||
using FilterCallback = Fn<bool()>;
|
||||
using Error = ::Media::Capture::Error;
|
||||
|
||||
VoiceRecordBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
@@ -87,6 +94,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<not_null<QEvent*>> lockViewportEvents() const;
|
||||
[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> recordingTipRequests() const;
|
||||
[[nodiscard]] rpl::producer<Error> errors() const;
|
||||
|
||||
void requestToSendWithOptions(Api::SendOptions options);
|
||||
|
||||
@@ -123,14 +131,12 @@ private:
|
||||
void updateTTLGeometry(TTLAnimationType type, float64 progress);
|
||||
|
||||
void recordUpdated(quint16 level, int samples);
|
||||
|
||||
[[nodiscard]] bool recordingAnimationCallback(crl::time now);
|
||||
void checkTipRequired();
|
||||
|
||||
void stop(bool send);
|
||||
void stopRecording(StopType type, bool ttlBeforeHide = false);
|
||||
void visibilityAnimate(bool show, Fn<void()> &&callback);
|
||||
|
||||
[[nodiscard]] bool showRecordButton() const;
|
||||
void drawDuration(QPainter &p);
|
||||
void drawRedCircle(QPainter &p);
|
||||
void drawMessage(QPainter &p, float64 recordActive);
|
||||
@@ -153,6 +159,8 @@ private:
|
||||
[[nodiscard]] bool peekTTLState() const;
|
||||
[[nodiscard]] bool takeTTLState() const;
|
||||
|
||||
[[nodiscard]] bool createVideoRecorder();
|
||||
|
||||
const style::RecordBar &_st;
|
||||
const not_null<Ui::RpWidget*> _outerContainer;
|
||||
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||
@@ -163,7 +171,7 @@ private:
|
||||
std::unique_ptr<Ui::AbstractButton> _ttlButton;
|
||||
std::unique_ptr<ListenWrap> _listen;
|
||||
|
||||
::Media::Capture::Result _data;
|
||||
Ui::RoundVideoResult _data;
|
||||
rpl::variable<bool> _paused;
|
||||
|
||||
base::Timer _startTimer;
|
||||
@@ -172,6 +180,7 @@ private:
|
||||
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
|
||||
rpl::event_stream<> _cancelRequests;
|
||||
rpl::event_stream<> _listenChanges;
|
||||
rpl::event_stream<Error> _errors;
|
||||
|
||||
int _centerY = 0;
|
||||
QRect _redCircleRect;
|
||||
@@ -192,9 +201,15 @@ private:
|
||||
float64 _redCircleProgress = 0.;
|
||||
|
||||
rpl::event_stream<> _recordingTipRequests;
|
||||
bool _recordingTipRequired = false;
|
||||
crl::time _recordingTipRequire = 0;
|
||||
bool _lockFromBottom = false;
|
||||
|
||||
std::unique_ptr<Ui::RoundVideoRecorder> _videoRecorder;
|
||||
std::vector<std::unique_ptr<Ui::RoundVideoRecorder>> _videoHiding;
|
||||
rpl::lifetime _videoCapturerLifetime;
|
||||
bool _recordingVideo = false;
|
||||
bool _fullRecord = false;
|
||||
|
||||
const style::font &_cancelFont;
|
||||
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||